Update forum forntend

This commit is contained in:
DariusIII
2025-11-19 11:48:06 +01:00
parent 811fe993b1
commit fff4e79cdf
48 changed files with 1850 additions and 10 deletions
+2 -1
View File
@@ -23,7 +23,8 @@ return [
|
*/
'preset' => 'livewire-tailwind',
//'preset' => 'livewire-tailwind',
'preset' => 'blade-tailwind',
/*
|--------------------------------------------------------------------------
+10 -5
View File
@@ -11,20 +11,25 @@
"@tailwindcss/forms": "^0.5.8",
"@tailwindcss/vite": "^4.0.0",
"autoprefixer": "^10.4.20",
"axios": "^1.7.7",
"axios": "^1.13.2",
"lodash": "^4.17.21",
"npm-check-updates": "^17.1.1",
"postcss": "^8.4.44",
"prettier": "^3.3.3",
"tailwindcss": "^3.4.10",
"tailwindcss": "^3.4.18",
"vite": "^7.0",
"vue": "^3.4.38"
"vue": "^3.5.24"
},
"dependencies": {
"@fortawesome/fontawesome-free": "^6.6.0",
"@melloware/coloris": "^0.24.0",
"@melloware/coloris": "^0.24.2",
"@simonwep/pickr": "^1.9.1",
"alpinejs": "^3.15.2",
"date-fns": "^3.6.0",
"feather-icons": "^4.29.2",
"laravel-echo": "^2.2.6",
"laravel-vite-plugin": "^2.0",
"nested-sort": "^5.2.0"
"nested-sort": "^5.2.0",
"vuedraggable": "^4.1.0"
}
}
@@ -0,0 +1,35 @@
@import url('https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
.forum {
font-family: "Open Sans", sans-serif;
font-optical-sizing: auto;
font-weight: 400;
font-style: normal;
font-variation-settings: "wdth" 100;
}
h1 {
@apply text-4xl my-6;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-xl;
}
a {
@apply text-blue-500;
}
a:hover {
@apply text-blue-800;
}
}
+154
View File
@@ -0,0 +1,154 @@
import feather from 'feather-icons';
import { createApp, ref, reactive, watch } from 'vue/dist/vue.esm-bundler.js';
import axios from 'axios';
import Pickr from '@simonwep/pickr';
import draggable from 'vuedraggable/src/vuedraggable';
import '@simonwep/pickr/dist/themes/classic.min.css';
window.axios = axios;
window.Vue = { createApp, ref, reactive, watch };
window.VueDraggable = draggable;
document.addEventListener('DOMContentLoaded', function () {
createApp({
setup() {
const isCollapsed = ref(true);
const isUserDropdownCollapsed = ref(true);
window.addEventListener('click', event => {
const ignore = ['navbar-toggler', 'navbar-toggler-icon', 'dropdown-toggle'];
if (ignore.some(className => event.target.classList.contains(className))) return;
if (!isCollapsed.value) isCollapsed.value = true;
if (!isUserDropdownCollapsed.value) isUserDropdownCollapsed.value = true;
});
return {
isCollapsed,
isUserDropdownCollapsed,
};
}
}).mount('.v-navbar');
function findModal(key)
{
const modal = document.querySelector(`[data-modal=${key}]`);
if (!modal) throw `Attempted to open modal '${key}' but no such modal found.`;
return modal;
}
function openModal(modal)
{
setTimeout(function()
{
modal.classList.remove('hidden');
modal.classList.add('flex');
}, 200);
}
document.querySelectorAll('[data-open-modal]').forEach(item =>
{
item.addEventListener('click', event =>
{
event.preventDefault();
openModal(findModal(event.currentTarget.dataset.openModal));
});
});
document.querySelectorAll('[data-close-modal]').forEach(modalClose =>
{
modalClose.addEventListener('click', event =>
{
event.preventDefault
setTimeout(function()
{
modalClose.closest('[data-modal]').classList.remove('flex');
modalClose.closest('[data-modal]').classList.add('hidden');
}, 200);
});
});
document.querySelectorAll('[data-dismiss]').forEach(item =>
{
item.addEventListener('click', event => event.currentTarget.parentElement.style.display = 'none');
});
const hash = window.location.hash.substr(1);
if (hash.startsWith('modal='))
{
openModal(findModal(hash.replace('modal=','')));
}
feather.replace();
const input = document.querySelector('input[name=color_light_mode]');
if (!input) return;
const pickr = Pickr.create({
el: '.pickr',
theme: 'classic',
default: input.value || null,
swatches: [
window.defaultCategoryColor,
'#f44336',
'#e91e63',
'#9c27b0',
'#673ab7',
'#3f51b5',
'#2196f3',
'#03a9f4',
'#00bcd4',
'#009688',
'#4caf50',
'#8bc34a',
'#cddc39',
'#ffeb3b',
'#ffc107'
],
components: {
preview: true,
hue: true,
interaction: {
input: true,
save: true
}
},
strings: {
save: 'Apply'
}
});
pickr.on('save', instance => pickr.hide())
.on('clear', instance =>
{
input.value = '';
input.dispatchEvent(new Event('change'));
})
.on('cancel', instance =>
{
const selectedColor = instance
.getSelectedColor()
.toHEXA()
.toString();
input.value = selectedColor;
input.dispatchEvent(new Event('change'));
})
.on('change', (color, instance) =>
{
const selectedColor = color
.toHEXA()
.toString();
input.value = selectedColor;
input.dispatchEvent(new Event('change'));
});
});
@@ -0,0 +1,20 @@
{{-- $category is passed as NULL to the master layout view to prevent it from showing in the breadcrumbs --}}
@extends ('forum::layouts.main', ['category' => null])
@section ('content')
<div class="flex flex-row justify-between my-3">
<h2 class="grow text-3xl font-medium">{{ trans('forum::general.index') }}</h2>
@can ('createCategories')
<x-forum::button type="button" data-open-modal="create-category">
{{ trans('forum::categories.create') }}
</x-forum::button>
@include ('forum::category.modals.create')
@endcan
</div>
@foreach ($categories as $category)
@include ('forum::category.partials.list', ['titleClass' => 'lead'])
@endforeach
@stop
@@ -0,0 +1,120 @@
@extends ('forum::layouts.main', ['category' => null, 'thread' => null, 'breadcrumbs_append' => [trans('forum::general.manage')]])
@section ('content')
<div class="flex flex-row justify-between mb-2">
<h2 class="text-3xl font-medium my-3">{{ trans('forum::general.manage') }}</h2>
@can ('createCategories')
<x-forum::button type="button" data-open-modal="create-category" class="px-6">
{{ trans('forum::categories.create') }}
</x-forum::button>
@include ('forum::category.modals.create')
@endcan
</div>
<div id="manage-categories">
<draggable-category-list :categories="state.categories"></draggable-category-list>
<transition name="fade">
<div v-show="state.changesApplied" class="bg-green-100 mb-4 text-green-700 mt-3 px-4 py-3" role="alert">
{{ trans('forum::general.changes_applied') }}
</div>
</transition>
<div class="flex justify-end py-3">
<button type="button" class="bg-blue-500 text-white rounded py-2 px-8 hover:cursor-pointer disabled:opacity-50" :disabled="state.isSavingDisabled" @click="onSave">
{{ trans('forum::general.save') }}
</button>
</div>
</div>
<script type="text/x-template" id="draggable-category-list-template">
<draggable
:list="categories"
tag="ul"
class="cursor-move"
@start="drag=true"
@end="drag=false"
:group="{ name: 'categories' }"
:empty-insert-threshold="50"
item-key="id">
<template #item="{element}">
<li class="bg-white p-4 my-2 rounded-md border" :data-id="element.id">
<span class="flex">
<span class="grow">
<strong :style="{ color: element.color }">@{{ element.title }}</strong>
<div class="text-muted">@{{ element.description }}</div>
</span>
<span>
<a class="text-blue-500 hover:text-blue-800 px-3 py-2 text-sm ml-2" :href="element.route + '#modal=edit-category'">{{ trans('forum::general.edit') }}</a>
<a class="bg-red-500 hover:bg-red-400 text-white hover:text-white px-3 py-2 text-sm rounded ml-2" :href="element.route + '#modal=delete-category'">{{ trans('forum::general.delete') }}</a>
</span>
</span>
<span v-if="element.children.length == 0" class="block min-h-5 border border-dashed border-slate-400 rounded">
<draggable-category-list :categories="element.children" />
</span>
<draggable-category-list v-else :categories="element.children" />
</li>
</template>
</draggable>
</script>
<script type="module">
const app = Vue.createApp({
setup() {
const state = Vue.reactive({
categories: @json($categories),
isSavingDisabled: true,
changesApplied: false,
});
Vue.watch(
() => state.categories,
async (newValue, oldValue) => {
state.isSavingDisabled = false;
},
{ deep: true }
);
function onSave()
{
state.isSavingDisabled = true;
state.changesApplied = false;
var payload = { categories: state.categories };
axios.post('{{ route('forum.bulk.category.manage') }}', payload)
.then(response => {
state.changesApplied = true;
setTimeout(() => state.changesApplied = false, 3000);
})
.catch(error => {
state.isSavingDisabled = false;
console.log(error);
});
}
return {
state,
onSave
};
}
});
app.component(
'Draggable',
VueDraggable
);
app.component(
'DraggableCategoryList',
{
props: ['categories'],
template: '#draggable-category-list-template',
}
);
app.mount('#manage-categories');
</script>
@stop
@@ -0,0 +1,31 @@
@component('forum::modal-form')
@slot('key', 'create-category')
@slot('title', trans('forum::categories.create'))
@slot('route', Forum::route('category.store'))
<div class="mb-3">
<x-forum::label for="title">{{ trans('forum::general.title') }}</x-forum::label>
<x-forum::input type="text" name="title" value="{{ old('title') }}" class="w-full" />
</div>
<div class="mb-3">
<x-forum::label for="description">{{ trans('forum::general.description') }}</x-forum::label>
<x-forum::input type="text" name="description" value="{{ old('description') }}" class="w-full" />
</div>
<div class="mb-3">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="accepts_threads" id="accepts-threads" value="1" {{ old('accepts_threads') ? 'checked' : '' }}>
<label class="form-check-label" for="accepts-threads">{{ trans('forum::categories.enable_threads') }}</label>
</div>
</div>
<div class="mb-3">
<div>
<input type="checkbox" name="is_private" id="is-private" value="1" {{ old('is_private') ? 'checked' : '' }}>
<label for="is-private">{{ trans('forum::categories.make_private') }}</label>
</div>
</div>
@include ('forum::category.partials.inputs.color')
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.create') }}</x-forum::button>
@endslot
@endcomponent
@@ -0,0 +1,21 @@
@component('forum::modal-form')
@slot('key', 'delete-category')
@slot('title', trans('forum::general.delete'))
@slot('route', Forum::route('category.delete', $category))
@slot('method', 'DELETE')
{{ trans('forum::general.generic_confirm') }}
@if(!$category->isEmpty())
<div class="form-check mt-3">
<input class="form-check-input" type="checkbox" value="1" name="force" id="forceDelete">
<label class="form-check-label" for="forceDelete">
{{ trans('forum::categories.confirm_nonempty_delete') }}
</label>
</div>
@endif
@slot('actions')
<x-forum::button type="submit" class="bg-red-500 hover:bg-red-400">{{ trans('forum::general.delete') }}</x-forum::button>
@endslot
@endcomponent
@@ -0,0 +1,40 @@
@component('forum::modal-form')
@slot('key', 'edit-category')
@slot('title', trans('forum::general.edit'))
@slot('route', Forum::route('category.update', $category))
@slot('method', 'PATCH')
<div class="mb-3">
<x-forum::label for="title">{{ trans('forum::general.title') }}</x-forum::label>
<x-forum::input type="text" name="title" value="{{ old('title') ?? $category->title }}" class="w-full" />
</div>
<div class="mb-3">
<x-forum::label for="description">{{ trans('forum::general.description') }}</x-forum::label>
<x-forum::input type="text" name="description" value="{{ old('description') ?? $category->description }}" class="w-full" />
</div>
<div class="mb-3">
<div class="form-check">
<input type="hidden" name="accepts_threads" value="0" />
<input class="form-check-input" type="checkbox" name="accepts_threads" id="accepts-threads" value="1" {{ $category->accepts_threads ? 'checked' : '' }}>
<label class="form-check-label" for="accepts-threads">{{ trans('forum::categories.enable_threads') }}</label>
</div>
</div>
<div class="mb-3">
<div class="form-check">
<input type="hidden" name="is_private" value="0" />
<input class="form-check-input" type="checkbox" name="is_private" id="is-private" value="1" {{ $category->is_private ? 'checked' : '' }} {{ $privateAncestor != null ? 'disabled' : '' }}>
<label class="form-check-label" for="is-private">{{ trans('forum::categories.make_private') }}</label>
</div>
</div>
@if ($privateAncestor != null)
<div class="alert alert-primary" role="alert">
{!! trans('forum::categories.access_controlled_by_private_ancestor', ['category' => "<a href=\"{$privateAncestor->route}\">{$privateAncestor->title}</a>"]) !!}
</div>
@endif
@include ('forum::category.partials.inputs.color')
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.save') }}</x-forum::button>
@endslot
@endcomponent
@@ -0,0 +1,16 @@
@component('forum::modal-form')
@slot('key', 'mark-threads-as-read')
@slot('title', trans('forum::categories.mark_read'))
@slot('route', Forum::route('unread.mark-as-read'))
@slot('method', 'PATCH')
<input type="hidden" name="category_id" value="{{ $category->id }}" />
<p>{{ trans('forum::general.generic_confirm') }}</p>
@slot('actions')
<x-forum::button type="submit">
{{ trans('forum::general.mark_read') }}
</x-forum::button>
@endslot
@endcomponent
@@ -0,0 +1,7 @@
<div class="mb-3">
<label for="color_light_mode">{{ trans('forum::general.color') }}</label>
<div class="pickr"></div>
<input type="hidden"
value="{{ isset($category->color_light_mode) ? $category->color_light_mode : (old('color_light_mode') ?? config('forum.frontend.default_category_color')) }}"
name="color_light_mode" />
</div>
@@ -0,0 +1,75 @@
<div class="my-4">
<div class="bg-white shadow rounded-md relative">
<div class="flex flex-col md:items-start md:flex-row md:justify-between md:gap-4 p-6">
<div class="md:w-3/6 text-center md:text-left">
<h5 class="text-lg">
<a href="{{ Forum::route('category.show', $category) }}" style="color: {{ $category->color_light_mode }};">{{ $category->title }}</a>
</h5>
<p class="text-gray-500">{{ $category->description }}</p>
</div>
<div class="md:w-1/6 flex flex-col items-center gap-1 mt-2 md:mt-0">
@if ($category->accepts_threads)
<x-forum::badge style="background: {{ $category->color_light_mode }};">
{{ trans_choice('forum::threads.thread', 2) }}: {{ $category->thread_count }}
</x-forum::badge>
<x-forum::badge style="background: {{ $category->color_light_mode }};">
{{ trans_choice('forum::posts.post', 2) }}: {{ $category->post_count }}
</x-forum::badge>
@endif
</div>
<div class="md:w-2/6 text-gray-500 text-center md:text-right mt-2 md:mt-0">
@if ($category->accepts_threads)
@if ($category->newestThread)
<div>
<a href="{{ Forum::route('thread.show', $category->newestThread) }}" class="mr-1">{{ $category->newestThread->title }}</a>
@include ('forum::partials.timestamp', ['carbon' => $category->newestThread->created_at])
</div>
@endif
@if ($category->latestActiveThread && $category->latestActiveThread->post_count > 1)
<div>
<a href="{{ Forum::route('thread.show', $category->latestActiveThread->lastPost) }}" class="mr-1">Re: {{ $category->latestActiveThread->title }}</a>
@include ('forum::partials.timestamp', ['carbon' => $category->latestActiveThread->lastPost->created_at])
</div>
@endif
@endif
</div>
</div>
</div>
@if ($category->children->count() > 0)
<div class="subcategories">
@foreach ($category->children as $subcategory)
<div class="bg-white -mt-1 shadow rounded-b-md">
<div class="flex flex-col md:items-start md:flex-row md:justify-between md:gap-4 p-6">
<div class="md:w-3/6 text-center md:text-left">
<a href="{{ Forum::route('category.show', $subcategory) }}" style="color: {{ $subcategory->color_light_mode }};">{{ $subcategory->title }}</a>
<div class="text-muted">{{ $subcategory->description }}</div>
</div>
<div class="md:w-1/6 flex flex-col items-center gap-1 mt-2 md:mt-0">
<x-forum::badge style="background: {{ $subcategory->color_light_mode }};">
{{ trans_choice('forum::threads.thread', 2) }}: {{ $subcategory->thread_count }}
</x-forum::badge>
<x-forum::badge style="background: {{ $subcategory->color_light_mode }};">
{{ trans_choice('forum::posts.post', 2) }}: {{ $subcategory->post_count }}
</x-forum::badge>
</div>
<div class="md:w-2/6 text-gray-500 md:items-end text-center md:text-right mt-2 md:mt-0">
@if ($subcategory->newestThread)
<div>
<a href="{{ Forum::route('thread.show', $subcategory->newestThread) }}" class="mr-1">{{ $subcategory->newestThread->title }}</a>
@include ('forum::partials.timestamp', ['carbon' => $subcategory->newestThread->created_at])
</div>
@endif
@if ($subcategory->latestActiveThread && $subcategory->latestActiveThread->post_count > 1)
<div>
<a href="{{ Forum::route('thread.show', $subcategory->latestActiveThread->lastPost) }}" class="mr-1">Re: {{ $subcategory->latestActiveThread->title }}</a>
@include ('forum::partials.timestamp', ['carbon' => $subcategory->latestActiveThread->lastPost->created_at])
</div>
@endif
</div>
</div>
</div>
@endforeach
</div>
@endif
</div>
@@ -0,0 +1,12 @@
@foreach ($categories as $cat)
@if (!isset($hide) || (isset($hide) && $cat->id != $hide->id))
<option value="{{ $cat->id }}" {{ (isset($category) && $cat->id == $category->id) ? 'selected' : '' }}>
@for ($i = 0; $i < $cat->depth; $i++)- @endfor
{{ $cat->title }}
</option>
@endif
@if ($cat->children)
@include ('forum::category.partials.options', ['categories' => $cat->children])
@endif
@endforeach
@@ -0,0 +1,248 @@
{{-- $thread is passed as NULL to the master layout view to prevent it from showing in the breadcrumbs --}}
@extends('forum::layouts.main', ['thread' => null])
@section('content')
<div class="flex flex-row justify-between mb-2">
<h2 class="text-3xl" style="color: {{ $category->color_light_mode }};">
{{ $category->title }} &nbsp;
@if ($category->description)
<small>{{ $category->description }}</small>
@endif
</h2>
</div>
<div id="category">
<div class="flex justify-between flex-row-reverse">
@if ($category->accepts_threads)
@can ('createThreads', $category)
<x-forum::button-link href="{{ Forum::route('thread.create', $category) }}" class="float-end">{{ trans('forum::threads.new_thread') }}</x-forum::button-link>
@endcan
@endif
<x-forum::button-group>
@can ('editCategories')
@can ('edit', $category)
<x-forum::button-secondary type="button" data-open-modal="edit-category">
{{ trans('forum::general.edit') }}
</x-forum::button-secondary>
@endcan
@endcan
</x-forum::button-group>
</div>
@if (!$category->children->isEmpty())
@foreach ($category->children as $subcategory)
@include ('forum::category.partials.list', ['category' => $subcategory])
@endforeach
@endif
@if ($category->accepts_threads)
@if (!$threads->isEmpty())
<div class="mt-4">
{{ $threads->links('forum::pagination') }}
</div>
@if (count($selectableThreadIds) > 0)
@can ('manageThreads', $category)
<form :action="actions[state.selectedAction]" method="POST">
@csrf
<input type="hidden" name="_method" :value="actionMethods[state.selectedAction]" />
<div class="text-end mt-2">
<div class="form-check">
<label for="selectAllThreads">
{{ trans('forum::threads.select_all') }}
</label>
<input type="checkbox" value="" id="selectAllThreads" class="align-middle" @click="toggleAll" :checked="state.selectedThreads.length == selectableThreadIds.length">
</div>
</div>
@endcan
@endif
<div class="threads list-group my-3 shadow-sm">
@foreach ($threads as $thread)
@include ('forum::thread.partials.list')
@endforeach
</div>
@if (count($selectableThreadIds) > 0)
@can ('manageThreads', $category)
<div class="fixed bottom-0 right-0 m-2" style="z-index: 1000;" v-if="state.selectedThreads.length">
<div class="bg-white shadow-sm rounded-md min-w-96 max-w-full">
<div class="border-b text-center py-4 px-6">
{{ trans('forum::general.with_selection') }}
</div>
<div class="p-6">
<div class="mb-3">
<div>
<x-forum::label for="bulk-actions">{{ trans_choice('forum::general.actions', 1) }}</x-forum::label>
</div>
<x-forum::select id="bulk-actions" v-model="state.selectedAction">
@can ('deleteThreads', $category)
<option value="delete">{{ trans('forum::general.delete') }}</option>
@endcan
@can ('restoreThreads', $category)
<option value="restore">{{ trans('forum::general.restore') }}</option>
@endcan
@can ('moveThreadsFrom', $category)
<option value="move">{{ trans('forum::general.move') }}</option>
@endcan
@can ('lockThreads', $category)
<option value="lock">{{ trans('forum::threads.lock') }}</option>
<option value="unlock">{{ trans('forum::threads.unlock') }}</option>
@endcan
@can ('pinThreads', $category)
<option value="pin">{{ trans('forum::threads.pin') }}</option>
<option value="unpin">{{ trans('forum::threads.unpin') }}</option>
@endcan
</x-forum::select>
</div>
<div class="mb-3" v-if="state.selectedAction == 'move'">
<div>
<label for="category-id">{{ trans_choice('forum::categories.category', 1) }}</label>
</div>
<x-forum::select name="category_id" id="category-id">
@include ('forum::category.partials.options', [
'categories' => $threadDestinationCategories,
'hide' => $category
])
</x-forum::select>
</div>
@if (config('forum.general.soft_deletes'))
<div class="mb-3" v-if="state.selectedAction == 'delete'">
<input class="form-check-input" type="checkbox" name="permadelete" value="1" id="permadelete">
<label class="form-check-label" for="permadelete">
{{ trans('forum::general.perma_delete') }}
</label>
</div>
@endif
<div class="text-end">
<button type="submit" class="bg-blue-500 text-white rounded-md px-3 py-1" @click="submit" :disabled="state.selectedAction == null">{{ trans('forum::general.proceed') }}</button>
</div>
</div>
</div>
</div>
</form>
@endcan
@endif
@else
<div class="card my-3">
<div class="card-body">
{{ trans('forum::threads.none_found') }}
@can ('createThreads', $category)
<br>
<a href="{{ Forum::route('thread.create', $category) }}">{{ trans('forum::threads.post_the_first') }}</a>
@endcan
</div>
</div>
@endif
<div class="row">
<div class="col col-xs-8">
{{ $threads->links('forum::pagination') }}
</div>
<div class="col col-xs-4 text-end">
@if ($category->accepts_threads)
@can ('createThreads', $category)
<x-forum::button-link href="{{ Forum::route('thread.create', $category) }}">{{ trans('forum::threads.new_thread') }}</x-forum::button-link>
@endcan
@endif
</div>
</div>
@endif
</div>
@if (!$threads->isEmpty())
@can ('markThreadsAsRead')
<div class="text-center mt-3">
<x-forum::button class="inline-flex px-6 items-center gap-2" data-open-modal="mark-threads-as-read">
<i data-feather="book"></i> {{ trans('forum::general.mark_read') }}
</x-forum::button>
</div>
@include ('forum::category.modals.mark-threads-as-read')
@endcan
@endif
@can ('editCategories')
@can ('edit', $category)
@include ('forum::category.modals.edit')
@endcan
@endcan
@can ('deleteCategories')
@can ('delete', $category)
@include ('forum::category.modals.delete')
@endcan
@endcan
<script type="module">
Vue.createApp({
setup() {
const selectableThreadIds = @json($selectableThreadIds);
const actions = {
delete: "{{ Forum::route('bulk.thread.delete') }}",
restore: "{{ Forum::route('bulk.thread.restore') }}",
lock: "{{ Forum::route('bulk.thread.lock') }}",
unlock: "{{ Forum::route('bulk.thread.unlock') }}",
pin: "{{ Forum::route('bulk.thread.pin') }}",
unpin: "{{ Forum::route('bulk.thread.unpin') }}",
move: "{{ Forum::route('bulk.thread.move') }}"
};
const actionMethods = {
delete: 'DELETE',
restore: 'POST',
lock: 'POST',
unlock: 'POST',
pin: 'POST',
unpin: 'POST',
move: 'POST'
};
const state = Vue.reactive({
selectedAction: null,
selectedThreads: [],
isEditModalOpen: false,
isDeleteModalOpen: false
});
function toggleAll()
{
state.selectedThreads = (state.selectedThreads.length < selectableThreadIds.length) ? selectableThreadIds : [];
}
function submit(event)
{
if (actionMethods[state.selectedAction] === 'DELETE' && !confirm("{{ trans('forum::general.generic_confirm') }}"))
{
event.preventDefault();
}
}
function onClickModal(event)
{
if (event.target.classList.contains('modal'))
{
state.isEditModalOpen = false;
state.isDeleteModalOpen = false;
}
}
return {
selectableThreadIds,
actions,
actionMethods,
state,
toggleAll,
submit,
onClickModal,
};
}
}).mount('#category');
</script>
@stop
@@ -0,0 +1,27 @@
@props([
'type' => 'default'
])
@php
switch($type) {
case('info'):
$color = 'bg-blue-500';
break;
case('danger'):
$color = 'bg-red-500';
break;
case('warning'):
$color = 'bg-orange-500';
break;
default:
$color = 'bg-gray-400';
break;
}
@endphp
<span {{ $attributes->merge(['class' => "$color rounded-full px-2 py-1 text-white text-xs font-semibold"]) }}>
{{ $slot }}
</span>
@@ -0,0 +1,3 @@
<div class="flex flex-wrap items-center [&_a:first-child]:rounded-r-none [&_a:not(:first-child):not(:last-child)]:rounded-none [&_a:last-child]:rounded-l-none" role="group">
{{ $slot }}
</div>
@@ -0,0 +1,3 @@
<a {{ $attributes->merge(['class' => 'bg-blue-500 hover:bg-blue-400 text-white font-semibold hover:text-white px-3 py-2 rounded-md inline-block']) }}>
{{ $slot }}
</a>
@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['class' => 'bg-gray-300 hover:bg-gray-200 text-gray-600 font-semibold px-3 py-2 rounded-md inline-block']) }}>
{{ $slot }}
</button>
@@ -0,0 +1,3 @@
<button {{ $attributes->merge(['class' => 'bg-blue-500 hover:bg-blue-400 text-white font-semibold px-3 py-2 rounded-md inline-block']) }}>
{{ $slot }}
</button>
@@ -0,0 +1 @@
<input {{ $attributes->merge(['type' => 'text', 'class' => 'px-3 py-1 border-gray-300 focus:border-blue-500 focus:ring-blue-500 rounded-md border shadow-sm']) }}>
@@ -0,0 +1,3 @@
<label {{ $attributes->merge(['class' => 'block w-full mb-1']) }}>
{{ $slot }}
</label>
@@ -0,0 +1,5 @@
<div class="inline-block relative w-64">
<select {{ $attributes->merge(['class' => 'block appearance-none w-full bg-white border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline']) }}>
{{ $slot }}
</select>
</div>
@@ -0,0 +1 @@
<textarea {{ $attributes->merge(['type' => 'text', 'class' => 'px-3 py-1 border-gray-300 focus:border-blue-500 focus:ring-blue-500 rounded-md border shadow-sm']) }}>{{ $slot }}</textarea>
@@ -0,0 +1,97 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>
@if (isset($thread_title))
{{ $thread_title }}
@endif
@if (isset($category))
{{ $category->title }}
@endif
{{ trans('forum::general.home_title') }}
</title>
@vite(['resources/forum/blade-tailwind/css/forum.css', 'resources/forum/blade-tailwind/js/forum.js'])
</head>
<body class="forum bg-gray-100">
<nav class="v-navbar bg-white shadow py-4">
<div class="container mx-auto px-4 md:flex md:items-center md:gap-4">
<div class="flex justify-between items-center">
<a class="text-lg font-semibold" href="{{ url(config('forum.frontend.router.prefix')) }}">Laravel Forum</a>
<button class="navbar-toggler block md:hidden border rounded-md px-2 py-1" type="button" :class="{ collapsed: isCollapsed }" @click="isCollapsed = !isCollapsed">
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="navbar-toggler-icon w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
</button>
</div>
<div class="grow justify-between navbar-collapse" :class="{ 'flex flex-col': !isCollapsed, 'hidden md:flex': isCollapsed }">
<ul class="flex flex-col md:flex-row gap-3 mb-4 md:mb-0">
<li>
<a class="text-gray-500 hover:text-gray-800" href="{{ route('home') }}">Home</a>
</li>
<li>
<a class="text-gray-500 hover:text-gray-800" href="{{ url(config('forum.frontend.router.prefix')) }}">{{ trans('forum::general.index') }}</a>
</li>
<li>
<a class="text-gray-500 hover:text-gray-800" href="{{ route('forum.recent') }}">{{ trans('forum::threads.recent') }}</a>
</li>
@auth
<li>
<a class="text-gray-500 hover:text-gray-800" href="{{ route('forum.unread') }}">{{ trans('forum::threads.unread_updated') }}</a>
</li>
@endauth
@can ('moveCategories')
<li>
<a class="text-gray-500 hover:text-gray-800" href="{{ route('forum.category.manage') }}">{{ trans('forum::general.manage') }}</a>
</li>
@endcan
</ul>
<ul class="navbar-nav flex gap-4 flex-col md:flex-row">
@if (Auth::check())
<li class="nav-item dropdown relative">
<a class="dropdown-toggle text-gray-500 flex items-center gap-1" href="#" id="navbarDropdownMenuLink" @click="isUserDropdownCollapsed = !isUserDropdownCollapsed">
{{ $username }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-3 h-3">
<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 8.25l-7.5 7.5-7.5-7.5" />
</svg>
</a>
<div class="border absolute left-0 bg-white rounded-md w-44 divide-y" :class="{ hidden: isUserDropdownCollapsed }" aria-labelledby="navbarDropdownMenuLink">
<a class="block px-4 py-2" href="{{ url('/logout') }}" onclick="event.preventDefault(); document.getElementById('logout-form').submit();">
Log out
</a>
<form id="logout-form" action="{{ url('/logout') }}" method="POST" style="display: none;">
@csrf
</form>
</div>
</li>
@else
<li class="nav-item">
<a class="nav-link" href="{{ url('/login') }}">Log in</a>
</li>
<li class="nav-item">
<a class="nav-link" href="{{ url('/register') }}">Register</a>
</li>
@endif
</ul>
</div>
</div>
</nav>
<div id="main" class="container mx-auto p-4">
@include ('forum::partials.breadcrumbs')
@include ('forum::partials.alerts')
@yield('content')
</div>
@yield('footer')
<script>
window.defaultCategoryColor = '{{ config('forum.frontend.default_category_color') }}';
</script>
</body>
</html>
@@ -0,0 +1,30 @@
<div tabindex="-1" role="dialog" data-modal="{{ $key }}" class="fixed top-0 left-0 w-full h-full hidden z-10 items-center justify-center">
<div class="fixed top-0 left-0 w-full h-full bg-black/25" data-close-modal></div>
<div class="relative bg-white rounded-md max-w-screen-sm w-full m-2" role="document">
<div class="">
<div class="border-b px-6 py-4 flex justify-between">
<h5 class="text-xl font-medium flex items-center gap-1">{!!$title !!}</h5>
<button type="button" aria-label="Close" data-close-modal>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<form action="{{ $route }}" method="POST">
@csrf
@if (isset($method))
@method($method)
@endif
<div class="p-6">
{{ $slot }}
</div>
<div class="border-t px-6 py-4 flex justify-end gap-4">
<x-forum::button-secondary type="button" class="btn btn-secondary" data-close-modal>{{ trans('forum::general.cancel') }}</x-forum::button-secondary>
{{ $actions }}
</div>
</form>
</div>
</div>
</div>
@@ -0,0 +1,46 @@
@if ($paginator->hasPages())
<nav>
<ul class="pagination">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.previous')">
<span class="page-link" aria-hidden="true">&lsaquo;</span>
</li>
@else
<li class="page-item">
<a class="page-link" href="{{ $paginator->previousPageUrl() }}" rel="prev" aria-label="@lang('pagination.previous')">&lsaquo;</a>
</li>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<li class="page-item disabled" aria-disabled="true"><span class="page-link">{{ $element }}</span></li>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<li class="page-item active" aria-current="page"><span class="page-link">{{ $page }}</span></li>
@else
<li class="page-item"><a class="page-link" href="{{ $url }}">{{ $page }}</a></li>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<li class="page-item">
<a class="page-link" href="{{ $paginator->nextPageUrl() }}" rel="next" aria-label="@lang('pagination.next')">&rsaquo;</a>
</li>
@else
<li class="page-item disabled" aria-disabled="true" aria-label="@lang('pagination.next')">
<span class="page-link" aria-hidden="true">&rsaquo;</span>
</li>
@endif
</ul>
</nav>
@endif
@@ -0,0 +1,18 @@
@php
$colorClasses = match ($type) {
'primary', '', null => 'bg-blue-100 text-blue-700',
'success', '', null => 'bg-green-100 text-green-700',
'danger' => 'bg-orange-100 text-orange-700'
};
@endphp
<div class="alert alert-{{ $type }} alert-dismissable rounded-md my-4 p-4 flex justify-between gap-4 {{ $colorClasses }}">
<div class="message">
{!! $message !!}
</div>
<button type="button" data-dismiss="alert" aria-hidden="true">
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor" aria-hidden="true">
<path d="M6.28 5.22a.75.75 0 00-1.06 1.06L8.94 10l-3.72 3.72a.75.75 0 101.06 1.06L10 11.06l3.72 3.72a.75.75 0 101.06-1.06L11.06 10l3.72-3.72a.75.75 0 00-1.06-1.06L10 8.94 6.28 5.22z" />
</svg>
</button>
</div>
@@ -0,0 +1,11 @@
@if (Session::has('alerts'))
@foreach (Session::get('alerts') as $alert)
@include ('forum::partials.alert', $alert)
@endforeach
@endif
@if (isset($errors) && !$errors->isEmpty())
@foreach ($errors->all() as $error)
@include ('forum::partials.alert', ['type' => 'danger', 'message' => $error])
@endforeach
@endif
@@ -0,0 +1,4 @@
@if ($category->parent !== null)
@include ('forum::partials.breadcrumb-categories', ['category' => $category->parent])
@endif
<li class=""><a href="{{ Forum::route('category.show', $category) }}" class="text-blue-500">{{ $category->title }}</a></li>
@@ -0,0 +1,16 @@
<nav aria-label="breadcrumb" class="mb-4">
<ol class="flex flex-wrap [&_li]:after:content-['/'] [&_li]:after:px-2 [&_li]:after:text-gray-500 [&_li:last-child]:after:content-['']">
<li class=""><a href="{{ url(config('forum.frontend.router.prefix')) }}" class="text-blue-500">{{ trans('forum::general.index') }}</a></li>
@if (isset($category) && $category)
@include ('forum::partials.breadcrumb-categories', ['category' => $category])
@endif
@if (isset($thread) && $thread)
<li class=""><a href="{{ Forum::route('thread.show', $thread) }}" class="text-blue-500">{{ $thread->title }}</a></li>
@endif
@if (isset($breadcrumbs_append) && count($breadcrumbs_append) > 0)
@foreach ($breadcrumbs_append as $breadcrumb)
<li class="">{{ $breadcrumb }}</li>
@endforeach
@endif
</ol>
</nav>
@@ -0,0 +1 @@
<time class="timestamp" datetime="{{ $carbon }}" title="{{ $carbon->toDayDateTimeString() }}">{{ $carbon->diffForHumans() }}</time>
@@ -0,0 +1,37 @@
@extends ('forum::layouts.main', ['breadcrumbs_append' => [trans_choice('forum::posts.delete', 1)]])
@section ('content')
<div id="delete-post">
<h2 class="text-3xl">{{ trans_choice('forum::posts.delete', 1) }}</h2>
<hr>
@include ('forum::post.partials.list', ['post' => $post, 'single' => true])
<form method="POST" action="{{ Forum::route('post.delete', $post) }}">
@csrf
@method('DELETE')
<div class="bg-white border rounded-md mb-3">
<div class="p-4">
@if (config('forum.general.soft_deletes'))
<div class="form-check" v-if="state.selectedPostAction == 'delete'">
<input class="form-check-input" type="checkbox" name="permadelete" value="1" id="permadelete">
<label class="form-check-label" for="permadelete">
{{ trans('forum::general.perma_delete') }}
</label>
</div>
@else
{{ trans('forum::general.generic_confirm') }}
@endif
</div>
</div>
<div class="flex justify-end items-center gap-2">
<x-forum::button-link href="{{ URL::previous() }}">{{ trans('forum::general.cancel') }}</x-forum::button-link>
<x-forum::button type="submit" class="bg-red-500 px-5">{{ trans('forum::general.delete') }}</x-forum::button>
</div>
</form>
</div>
@stop
@@ -0,0 +1,29 @@
@extends ('forum::layouts.main', ['breadcrumbs_append' => [trans_choice('forum::posts.restore', 1)]])
@section ('content')
<div id="delete-post">
<h2 class="flex-grow-1">{{ trans_choice('forum::posts.restore', 1) }}</h2>
<hr>
@include ('forum::post.partials.list', ['post' => $post, 'single' => true])
<form method="POST" action="{{ Forum::route('post.restore', $post) }}">
@csrf
@method('POST')
<div class="card mb-3">
<div class="card-body">
{{ trans('forum::general.generic_confirm') }}
</div>
</div>
<div class="text-end">
<a href="{{ URL::previous() }}" class="btn btn-link">{{ trans('forum::general.cancel') }}</a>
<x-forum::button type="submit" class="px-5">
{{ trans('forum::general.restore') }}
</x-forum::button>
</div>
</form>
</div>
@stop
@@ -0,0 +1,32 @@
@extends ('forum::layouts.main', ['breadcrumbs_append' => [trans('forum::general.new_reply')]])
@section ('content')
<div id="create-post">
<h2 class="text-3xl font-medium my-3">{{ trans('forum::general.new_reply') }} ({{ $thread->title }})</h2>
@if ($post !== null && !$post->trashed())
<div class="mb-2">{{ trans('forum::general.replying_to', ['item' => $post->authorName]) }}:</div>
@include ('forum::post.partials.quote')
@endif
<hr class="my-4" />
<form method="POST" action="{{ Forum::route('post.store', $thread) }}">
{!! csrf_field() !!}
@if ($post !== null)
<input type="hidden" name="post" value="{{ $post->id }}" />
@endif
<div class="mb-3">
<x-forum::textarea name="content" class="w-full min-h-48">{{ old('content') }}</x-forum::textarea>
</div>
<div class="flex justify-end items-center gap-4">
<a href="{{ URL::previous() }}" class="text-blue-500 underline">{{ trans('forum::general.cancel') }}</a>
<x-forum::button type="submit" class="">{{ trans('forum::general.reply') }}</x-forum::button>
</div>
</form>
</div>
@stop
@@ -0,0 +1,29 @@
@extends ('forum::layouts.main', ['breadcrumbs_append' => [trans('forum::posts.edit')]])
@section ('content')
<div id="edit-post">
<h2 class="text-3xl font-medium my-3">{{ trans('forum::posts.edit') }} ({{ $thread->title }})</h2>
<hr class="mb-4">
@if ($post->parent)
<h3>{{ trans('forum::general.response_to', ['item' => $post->parent->authorName]) }}...</h3>
@include ('forum::post.partials.list', ['post' => $post->parent, 'single' => true])
@endif
<form method="POST" action="{{ Forum::route('post.update', $post) }}">
@csrf
@method('PATCH')
<div class="mb-3">
<x-forum::textarea name="content" class="w-full">{{ old('content') !== null ? old('content') : $post->content }}</x-forum::textarea>
</div>
<div class="flex items-center gap-4 justify-end">
<a href="{{ URL::previous() }}" class="underline text-blue-500">{{ trans('forum::general.cancel') }}</a>
<x-forum::button type="submit">{{ trans('forum::general.save') }}</x-forum::button>
</div>
</form>
</div>
@stop
@@ -0,0 +1,23 @@
<div class="panel panel-default" data-actions>
<div class="panel-heading">
<span class="glyphicon glyphicon-option-vertical"></span>
<a href="#" data-toggle="collapse" data-target=".collapse.post-actions">{{ trans('forum::posts.actions') }}</a>
</div>
<div class="collapse post-actions">
<div class="panel-body">
<div class="mb-3">
<label for="action">{{ trans_choice('forum::general.actions', 1) }}</label>
<select name="action" id="action" class="form-control">
@can ('deletePosts', $post->thread)
@can ('delete', $post)
<option value="delete" data-confirm="true" data-method="delete">{{ trans('forum::general.delete') }}</option>
@endcan
@endcan
</select>
</div>
</div>
<div class="panel-footer clearfix">
<button type="submit" class="btn btn-default pull-right">{{ trans('forum::general.proceed') }}</button>
</div>
</div>
</div>
@@ -0,0 +1,70 @@
<div @if (!$post->trashed())id="post-{{ $post->sequence }}"@endif
class="bg-white border mb-2 rounded-md {{ $post->trashed() || $thread->trashed() ? 'opacity-50' : '' }}"
:class="{ 'border-blue-500': state.selectedPosts.includes({{ $post->id }}) }">
<div class="bg-gray-100 border-b px-6 py-4 flex justify-between flex-row-reverse rounded-t-md">
@if (!isset($single) || !$single)
<span class="float-end">
<a href="{{ Forum::route('thread.show', $post) }}" class="text-blue-500">#{{ $post->sequence }}</a>
@if ($post->sequence != 1)
@can ('deletePosts', $post->thread)
@can ('delete', $post)
<input type="checkbox" name="posts[]" :value="{{ $post->id }}" v-model="state.selectedPosts" class="ml-2" />
@endcan
@endcan
@endif
</span>
@endif
<div>
{{ $post->authorName }}
<span class="text-gray-500">
@include ('forum::partials.timestamp', ['carbon' => $post->created_at])
@if ($post->hasBeenUpdated())
({{ trans('forum::general.last_updated') }} @include ('forum::partials.timestamp', ['carbon' => $post->updated_at]))
@endif
</span>
</div>
</div>
<div class="p-6">
@if ($post->parent !== null)
@include ('forum::post.partials.quote', ['post' => $post->parent])
@endif
@if ($post->trashed())
@can ('viewTrashedPosts')
{!! Forum::render($post->content) !!}
<br>
@endcan
<x-forum::badge type="danger">{{ trans('forum::general.deleted') }}</x-forum::badge>
@else
{!! Forum::render($post->content) !!}
@endif
@if (!isset($single) || !$single)
<div class="flex items-center gap-4 justify-end mt-4">
@if (!$post->trashed())
<a href="{{ Forum::route('post.show', $post) }}" class="text-gray-500 hover:text-gray-800">{{ trans('forum::general.permalink') }}</a>
@if ($post->sequence != 1)
@can ('deletePosts', $post->thread)
@can ('delete', $post)
<a href="{{ Forum::route('post.confirm-delete', $post) }}" class="text-red-500 hover:text-red-800">{{ trans('forum::general.delete') }}</a>
@endcan
@endcan
@endif
@can ('edit', $post)
<a href="{{ Forum::route('post.edit', $post) }}" class="text-blue-500">{{ trans('forum::general.edit') }}</a>
@endcan
@can ('reply', $post->thread)
<a href="{{ Forum::route('post.create', $post) }}" class="text-blue-500">{{ trans('forum::general.reply') }}</a>
@endcan
@else
@can ('restorePosts', $post->thread)
@can ('restore', $post)
<a href="{{ Forum::route('post.confirm-restore', $post) }}" class="card-link">{{ trans('forum::general.restore') }}</a>
@endcan
@endcan
@endif
</div>
@endif
</div>
</div>
@@ -0,0 +1,13 @@
<div class="bg-white rounded-md border mb-2">
<div class="p-6">
<div class="flex justify-between flex-row-reverse mb-2">
<span>
<a href="{{ Forum::route('thread.show', $post) }}" class="text-gray-500">#{{ $post->sequence }}</a>
</span>
<div>
<strong>{{ $post->authorName }}</strong> <span class="text-gray-500">{{ $post->posted }}</span>
</div>
</div>
{!! \Illuminate\Support\Str::limit(Forum::render($post->content)) !!}
</div>
</div>
@@ -0,0 +1,14 @@
@extends ('forum::layouts.main', ['breadcrumbs_append' => [trans('forum::posts.view')]])
@section ('content')
<div id="post">
<div class="flex flex-row justify-between mb-3">
<h2 class="grow">{{ trans('forum::posts.view') }} ({{ $thread->title }})</h2>
<x-forum::button-link href="{{ Forum::route('thread.show', $thread) }}">{{ trans('forum::threads.view') }}</x-forum::button-link>
</div>
<hr>
@include ('forum::post.partials.list', ['post' => $post, 'single' => true])
</div>
@stop
@@ -0,0 +1,25 @@
@extends ('forum::layouts.main', ['breadcrumbs_append' => [trans('forum::threads.new_thread')]])
@section ('content')
<div id="create-thread">
<h2 class="text-3xl">{{ trans('forum::threads.new_thread') }} ({{ $category->title }})</h2>
<form method="POST" action="{{ Forum::route('thread.store', $category) }}">
@csrf
<div class="mb-3">
<x-forum::label for="title">{{ trans('forum::general.title') }}</x-forum::label>
<x-forum::input type="text" name="title" value="{{ old('title') }}" class="w-full" />
</div>
<div class="mb-3">
<x-forum::textarea name="content" class="w-full">{{ old('content') }}</x-forum::textarea>
</div>
<div class="text-end">
<x-forum::button-link href="{{ URL::previous() }}" class="bg-gray-500">{{ trans('forum::general.cancel') }}</x-forum::button-link>
<x-forum::button type="submit">{{ trans('forum::general.create') }}</x-forum::button>
</div>
</form>
</div>
@stop
@@ -0,0 +1,14 @@
@component('forum::modal-form')
@slot('key', 'mark-as-read')
@slot('title', trans('forum::general.mark_read'))
@slot('route', Forum::route('unread.mark-as-read'))
@slot('method', 'PATCH')
<p>{{ trans('forum::general.generic_confirm') }}</p>
@slot('actions')
<x-forum::button type="submit">
{{ trans('forum::general.mark_read') }}
</x-forum::button>
@endslot
@endcomponent
@@ -0,0 +1,52 @@
<div class="bg-white" :class="{ 'ring-1 ring-blue-500': state.selectedThreads.includes({{ $thread->id }}) }">
<div class="flex flex-col md:items-start md:flex-row md:justify-between md:gap-4 p-6">
<div class="md:w-3/6 text-center md:text-left">
<span class="lead">
<a href="{{ Forum::route('thread.show', $thread) }}" @if (isset($category))style="color: {{ $category->color_light_mode }};"@endif class="text-lg">{{ $thread->title }}</a>
</span>
<br>
{{ $thread->authorName }}
<span class="text-gray-500"> @include ('forum::partials.timestamp', ['carbon' => $thread->created_at])</span>
@if (!isset($category))
<br>
<a href="{{ Forum::route('category.show', $thread->category) }}" style="color: {{ $thread->category->color_light_mode }};">{{ $thread->category->title }}</a>
@endif
</div>
<div class="md:w-2/6 flex flex-wrap justify-center items-center md:justify-end gap-1">
@if ($thread->pinned)
<x-forum::badge type="info">{{ trans('forum::threads.pinned') }}</x-forum::badge>
@endif
@if ($thread->locked)
<x-forum::badge type="warning">{{ trans('forum::threads.locked') }}</x-forum::badge>
@endif
@if ($thread->userReadStatus !== null && !$thread->trashed())
<x-forum::badge type="success">{{ trans($thread->userReadStatus) }}</x-forum::badge>
@endif
@if ($thread->trashed())
<x-forum::badge type="danger">{{ trans('forum::general.deleted') }}</x-forum::badge>
@endif
<x-forum::badge :style="(isset($category) && $category->color_light_mode) ? 'background: '.$category->color_light_mode .';' : null">
{{ trans('forum::general.replies') }}:
{{ $thread->reply_count }}
</x-forum::badge>
</div>
@if ($thread->lastPost)
<div class="md:w-1/6 flex justify-center md:flex-col md:items-end">
<a href="{{ Forum::route('thread.show', $thread->lastPost) }}" class="text-blue-500">{{ trans('forum::posts.view') }} &raquo;</a>
<div>
{{ $thread->lastPost->authorName }}
<span class="text-gray-500"> @include ('forum::partials.timestamp', ['carbon' => $thread->lastPost->created_at])</span>
</div>
</div>
@endif
@if (isset($category) && isset($selectableThreadIds) && in_array($thread->id, $selectableThreadIds))
<div class="" style="flex: 0;">
<input type="checkbox" name="threads[]" :value="{{ $thread->id }}" v-model="state.selectedThreads">
</div>
@endif
</div>
</div>
@@ -0,0 +1,21 @@
@extends ('forum::layouts.main', ['thread' => null, 'breadcrumbs_append' => [trans('forum::threads.recent')]])
@section ('content')
<div id="new-posts">
<h2 class="text-3xl font-medium my-3">{{ trans('forum::threads.recent') }}</h2>
@if (!$threads->isEmpty())
<div class="my-3">
@foreach ($threads as $thread)
@include ('forum::thread.partials.list')
@endforeach
</div>
@else
<div class="bg-white my-3">
<div class="card-body text-center text-muted">
{{ trans('forum::threads.none_found') }}
</div>
</div>
@endif
</div>
@stop
@@ -0,0 +1,391 @@
@extends ('forum::layouts.main', ['thread' => null, 'breadcrumbs_append' => [$thread->title], 'thread_title' => $thread->title])
@section ('content')
<div id="thread">
<div class="flex flex-col md:flex-row justify-between my-4">
<h2 class="grow text-3xl font-semibold">{{ $thread->title }}</h2>
<div class="flex flex-col md:flex-row items-center gap-2">
@if (Gate::allows('deleteThreads', $thread->category) && Gate::allows('delete', $thread))
@if ($thread->trashed())
<x-forum::button-link href="#" class="bg-red-500 hover:bg-red-400" data-open-modal="perma-delete-thread">
<i data-feather="trash"></i> {{ trans('forum::general.perma_delete') }}
</x-forum::button-link>
@else
<x-forum::button-link href="#" class="bg-red-500 hover:bg-red-400 inline-flex items-center gap-2" data-open-modal="delete-thread">
<i data-feather="trash" class="w-4"></i> {{ trans('forum::general.delete') }}
</x-forum::button-link>
@endif
@endif
@if ($thread->trashed() && Gate::allows('restoreThreads', $thread->category) && Gate::allows('restore', $thread))
<x-forum::button-link href="#" data-open-modal="restore-thread" class="inline-flex items-center gap-2">
<i data-feather="refresh-cw" class="w-4"></i> {{ trans('forum::general.restore') }}
</x-forum::button-link>
@endif
@if (Gate::allows('lockThreads', $category)
|| Gate::allows('pinThreads', $category)
|| Gate::allows('rename', $thread)
|| Gate::allows('moveThreadsFrom', $category))
<x-forum::button-group>
@if (!$thread->trashed())
@can ('lockThreads', $category)
@if ($thread->locked)
<x-forum::button-link href="#" data-open-modal="unlock-thread" class="inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-400">
<i data-feather="unlock" class="w-4"></i> {{ trans('forum::threads.unlock') }}
</x-forum::button-link>
@else
<x-forum::button-link href="#" data-open-modal="lock-thread" class="inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-400">
<i data-feather="lock" class="w-4"></i> {{ trans('forum::threads.lock') }}
</x-forum::button-link>
@endif
@endcan
@can ('pinThreads', $category)
@if ($thread->pinned)
<x-forum::button-link href="#" data-open-modal="unpin-thread" class="inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-400">
<i data-feather="arrow-down"></i> {{ trans('forum::threads.unpin') }}
</x-forum::button-link>
@else
<x-forum::button-link href="#" data-open-modal="pin-thread" class="inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-400">
<i data-feather="arrow-up" class="w-4"></i> {{ trans('forum::threads.pin') }}
</x-forum::button-link>
@endif
@endcan
@can ('rename', $thread)
<x-forum::button-link href="#" data-open-modal="rename-thread" class="inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-400">
<i data-feather="edit-2" class="w-4"></i> {{ trans('forum::general.rename') }}
</x-forum::button-link>
@endcan
@can ('moveThreadsFrom', $category)
<x-forum::button-link href="#" data-open-modal="move-thread" class="inline-flex items-center gap-2 bg-gray-500 hover:bg-gray-400">
<i data-feather="corner-up-right" class="w-4"></i> {{ trans('forum::general.move') }}
</x-forum::button-link>
@endcan
@endif
</x-forum::button-group>
@endcan
</div>
</div>
<div class="flex flex-wrap gap-x-1">
@if ($thread->trashed())
<x-forum::badge type="danger">{{ trans('forum::general.deleted') }}</x-forum::badge>
@endif
@if ($thread->pinned)
<x-forum::badge type="info">{{ trans('forum::threads.pinned') }}</x-forum::badge>
@endif
@if ($thread->locked)
<x-forum::badge type="warning">{{ trans('forum::threads.locked') }}</x-forum::badge>
@endif
</div>
@if ((count($posts) > 1 || $posts->currentPage() > 1) && (Gate::allows('deletePosts', $thread) || Gate::allows('restorePosts', $thread)) && count($selectablePosts) > 0)
<form :action="postActions[state.selectedPostAction]" method="POST">
@csrf
<input type="hidden" name="_method" :value="postActionMethods[state.selectedPostAction]" />
@endif
<div class="my-4">
<div>
{{ $posts->links('forum::pagination') }}
</div>
<div class="flex justify-end">
@if (!$thread->trashed())
@can ('reply', $thread)
<x-forum::button-group>
<x-forum::button-link href="{{ Forum::route('post.create', $thread) }}">
{{ trans('forum::general.new_reply') }}
</x-forum::button-link>
<x-forum::button-link href="#quick-reply">
{{ trans('forum::general.quick_reply') }}
</x-forum::button-link>
</x-forum::button-group>
@endcan
@endif
</div>
</div>
@if ((count($posts) > 1 || $posts->currentPage() > 1) && (Gate::allows('deletePosts', $thread) || Gate::allows('restorePosts', $thread)) && count($selectablePosts) > 0)
<div class="text-end mb-2">
<div class="form-check">
<label for="selectAllPosts">
{{ trans('forum::posts.select_all') }}
</label>
<input type="checkbox" value="" id="selectAllPosts" class="align-middle" @click="toggleAll" :checked="state.selectedPosts.length == posts.data.length">
</div>
</div>
@endif
@foreach ($posts as $post)
@include ('forum::post.partials.list', compact('post'))
@endforeach
@if ((count($posts) > 1 || $posts->currentPage() > 1) && (Gate::allows('deletePosts', $thread) || Gate::allows('restorePosts', $thread)) && count($selectablePosts) > 0)
<div class="fixed bottom-0 right-0 m-2" style="z-index: 1000;" v-if="state.selectedPosts.length">
<div class="bg-white shadow-sm rounded-md min-w-96 max-w-full">
<div class="border-b text-center py-4 px-6">
{{ trans('forum::general.with_selection') }}
</div>
<div class="p-6">
<div class="mb-3">
<div>
<x-forum::label for="bulk-actions">{{ trans_choice('forum::general.actions', 1) }}</x-forum::label>
</div>
<x-forum::select id="bulk-actions" v-model="state.selectedPostAction">
<option value="delete">{{ trans('forum::general.delete') }}</option>
<option value="restore">{{ trans('forum::general.restore') }}</option>
</x-forum::select>
</div>
@if (config('forum.general.soft_deletes'))
<div class="form-check mb-3" v-if="state.selectedPostAction == 'delete'">
<input class="form-check-input" type="checkbox" name="permadelete" value="1" id="permadelete">
<label class="form-check-label" for="permadelete">
{{ trans('forum::general.perma_delete') }}
</label>
</div>
@endif
<div class="text-end">
<x-forum::button type="submit" class="px-5" @click="submitPosts">{{ trans('forum::general.proceed') }}</x-forum::button>
</div>
</div>
</div>
</div>
</form>
@endif
{{ $posts->links('forum::pagination') }}
@if (!$thread->trashed())
@can ('reply', $thread)
<h2>{{ trans('forum::general.quick_reply') }}</h2>
<div id="quick-reply" class="mt-4">
<form method="POST" action="{{ Forum::route('post.store', $thread) }}">
@csrf
<div class="mb-3">
<x-forum::textarea name="content" class="w-full min-h-48">{{ old('content') }}</x-forum::textarea>
</div>
<div class="text-end">
<x-forum::button type="submit" class="px-5">{{ trans('forum::general.reply') }}</x-forum::button>
</div>
</form>
</div>
@endcan
@endif
</div>
@if ($thread->trashed() && Gate::allows('restoreThreads', $thread->category) && Gate::allows('restore', $thread))
@component('forum::modal-form')
@slot('key', 'restore-thread')
@slot('title', '<i data-feather="refresh-cw" class="text-gray-500"></i>' . trans('forum::general.restore'))
@slot('route', Forum::route('thread.restore', $thread))
@slot('method', 'POST')
{{ trans('forum::general.generic_confirm') }}
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@endif
@if (Gate::allows('deleteThreads', $thread->category) && Gate::allows('delete', $thread))
@component('forum::modal-form')
@slot('key', 'delete-thread')
@slot('title', '<i data-feather="trash" class="text-gray-500"></i>' . trans('forum::threads.delete'))
@slot('route', Forum::route('thread.delete', $thread))
@slot('method', 'DELETE')
@if (config('forum.general.soft_deletes'))
<div class="form-check">
<input class="form-check-input" type="checkbox" name="permadelete" value="1" id="permadelete">
<label class="form-check-label" for="permadelete">
{{ trans('forum::general.perma_delete') }}
</label>
</div>
@else
{{ trans('forum::general.generic_confirm') }}
@endif
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@if (config('forum.general.soft_deletes'))
@component('forum::modal-form')
@slot('key', 'perma-delete-thread')
@slot('title', '<i data-feather="trash" class="text-gray-500"></i>' . trans_choice('forum::threads.perma_delete', 1))
@slot('route', Forum::route('thread.delete', $thread))
@slot('method', 'DELETE')
<input type="hidden" name="permadelete" value="1" />
{{ trans('forum::general.generic_confirm') }}
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@endif
@endif
@if (!$thread->trashed())
@can ('lockThreads', $category)
@if ($thread->locked)
@component('forum::modal-form')
@slot('key', 'unlock-thread')
@slot('title', '<i data-feather="unlock" class="text-gray-500"></i> ' . trans('forum::threads.unlock'))
@slot('route', Forum::route('thread.unlock', $thread))
@slot('method', 'POST')
{{ trans('forum::general.generic_confirm') }}
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@else
@component('forum::modal-form')
@slot('key', 'lock-thread')
@slot('title', '<i data-feather="lock" class="text-gray-500"></i> ' . trans('forum::threads.lock'))
@slot('route', Forum::route('thread.lock', $thread))
@slot('method', 'POST')
{{ trans('forum::general.generic_confirm') }}
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@endif
@endcan
@can ('pinThreads', $category)
@if ($thread->pinned)
@component('forum::modal-form')
@slot('key', 'unpin-thread')
@slot('title', '<i data-feather="arrow-down" class="text-gray-500"></i> ' . trans('forum::threads.unpin'))
@slot('route', Forum::route('thread.unpin', $thread))
@slot('method', 'POST')
{{ trans('forum::general.generic_confirm') }}
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@else
@component('forum::modal-form')
@slot('key', 'pin-thread')
@slot('title', '<i data-feather="arrow-up" class="text-gray-500"></i> ' . trans('forum::threads.pin'))
@slot('route', Forum::route('thread.pin', $thread))
@slot('method', 'POST')
{{ trans('forum::general.generic_confirm') }}
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@endif
@endcan
@can ('rename', $thread)
@component('forum::modal-form')
@slot('key', 'rename-thread')
@slot('title', '<i data-feather="edit-2" class="text-gray-500"></i> ' . trans('forum::general.rename'))
@slot('route', Forum::route('thread.rename', $thread))
@slot('method', 'POST')
<div>
<x-forum::label for="new-title">{{ trans('forum::general.title') }}</x-forum::label>
<x-forum::input type="text" name="title" value="{{ $thread->title }}" class="w-full" />
</div>
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@endcan
@can ('moveThreadsFrom', $category)
@component('forum::modal-form')
@slot('key', 'move-thread')
@slot('title', '<i data-feather="corner-up-right" class="text-gray-500"></i> ' . trans('forum::general.move'))
@slot('route', Forum::route('thread.move', $thread))
@slot('method', 'POST')
<div class="input-group">
<div class="input-group-prepend">
<label class="input-group-text" for="category-id">{{ trans_choice('forum::categories.category', 1) }}</label>
</div>
<select name="category_id" id="category-id" class="form-select">
@include ('forum::category.partials.options', ['hide' => $thread->category])
</select>
</div>
@slot('actions')
<x-forum::button type="submit">{{ trans('forum::general.proceed') }}</x-forum::button>
@endslot
@endcomponent
@endcan
@endif
<script type="module">
Vue.createApp({
setup() {
let posts = @json($posts);
posts.data = posts.data.filter(post => post.sequence > 1);
const selectablePosts = @json($selectablePosts);
const postActions = {
delete: "{{ Forum::route('bulk.post.delete') }}",
restore: "{{ Forum::route('bulk.post.restore') }}"
};
const postActionMethods = {
delete: 'DELETE',
restore: 'POST',
};
const state = Vue.reactive({
selectedPostAction: 'delete',
selectedPosts: [],
selectedThreadAction: null,
});
function toggleAll() {
state.selectedPosts = (state.selectedPosts.length < selectablePosts.length) ? selectablePosts : [];
}
function submitThread(event) {
if (threadActionMethods[state.selectedThreadAction] === 'DELETE' && !confirm("{{ trans('forum::general.generic_confirm') }}"))
{
event.preventDefault();
}
}
function submitPosts(event) {
if (postActionMethods[state.selectedPostAction] === 'DELETE' && !confirm("{{ trans('forum::general.generic_confirm') }}")) {
event.preventDefault();
}
}
return {
posts,
selectablePosts,
postActions,
postActionMethods,
state,
toggleAll,
submitThread,
submitPosts,
};
}
}).mount('#thread');
</script>
@stop
@@ -0,0 +1,31 @@
@extends ('forum::layouts.main', ['thread' => null, 'breadcrumbs_append' => [trans('forum::threads.unread_updated')]])
@section ('content')
<div id="new-posts">
<h2 class="text-3xl text-medium my-4">{{ trans('forum::threads.unread_updated') }}</h2>
@if (!$threads->isEmpty())
<div class="">
@foreach ($threads as $thread)
@include ('forum::thread.partials.list')
@endforeach
</div>
@else
<div class="bg-white shadow rounded text-gray-500 text-center py-4">
{{ trans('forum::threads.none_found') }}
</div>
@endif
</div>
@if (!$threads->isEmpty())
@can ('markThreadsAsRead')
<div class="flex justify-center mt-4">
<x-forum::button class="px-5 flex items-center gap-2" data-open-modal="mark-as-read">
<i data-feather="book"></i> {{ trans('forum::general.mark_read') }}
</x-forum::button>
</div>
@include ('forum::thread.modals.mark-as-read')
@endcan
@endif
@stop
@@ -19,13 +19,13 @@
wire:model="description" />
<x-forum::form.input-text
id="color"
id="color-light-mode"
:label="trans('forum::general.color_light_mode')"
wire:model="color_light_mode"
data-coloris />
<x-forum::form.input-text
id="color"
id="color-dark-mode"
:label="trans('forum::general.color_dark_mode')"
wire:model="color_dark_mode"
data-coloris />
@@ -20,13 +20,13 @@
wire:model="description" />
<x-forum::form.input-text
id="color"
id="color-light-mode"
:label="trans('forum::general.color_light_mode')"
wire:model="color_light_mode"
data-coloris />
<x-forum::form.input-text
id="color"
id="color-dark-mode"
:label="trans('forum::general.color_dark_mode')"
wire:model="color_dark_mode"
data-coloris />
+2
View File
@@ -9,6 +9,8 @@ export default defineConfig({
'resources/js/app.js',
'resources/forum/livewire-tailwind/css/forum.css',
'resources/forum/livewire-tailwind/js/forum.js',
'resources/forum/blade-tailwind/css/forum.css',
'resources/forum/blade-tailwind/js/forum.js',
],
refresh: true,
}),