Update forum

This commit is contained in:
DariusIII
2024-04-26 22:11:49 +02:00
parent 83122f9dd7
commit a1e313cd2a
109 changed files with 4048 additions and 50 deletions
+18 -18
View File
@@ -4,10 +4,10 @@ return [
/*
|--------------------------------------------------------------------------
| Enable/disable
| Enable/disable feature
|--------------------------------------------------------------------------
|
| Set to false if you want to effectively disable the API.
| Whether or not to enable the API feature.
|
*/
@@ -24,21 +24,6 @@ return [
'enable_search' => true,
/*
|--------------------------------------------------------------------------
| Resources
|--------------------------------------------------------------------------
|
| Override to return your own resources for API responses
|
*/
'resources' => [
'category' => TeamTeaTime\Forum\Http\Resources\CategoryResource::class,
'thread' => TeamTeaTime\Forum\Http\Resources\ThreadResource::class,
'post' => TeamTeaTime\Forum\Http\Resources\PostResource::class,
],
/*
|--------------------------------------------------------------------------
| Router
@@ -51,9 +36,24 @@ return [
'router' => [
'prefix' => '/forum/api',
'as' => 'forum.api.',
'namespace' => '\TeamTeaTime\Forum\Http\Controllers\Api',
'namespace' => '\\TeamTeaTime\\Forum\\Http\\Controllers\\Api',
'middleware' => ['api', 'auth:api'],
'auth_middleware' => ['auth:api'],
],
/*
|--------------------------------------------------------------------------
| Resources
|--------------------------------------------------------------------------
|
| Override to return your own resources for API responses.
|
*/
'resources' => [
'category' => TeamTeaTime\Forum\Http\Resources\CategoryResource::class,
'thread' => TeamTeaTime\Forum\Http\Resources\ThreadResource::class,
'post' => TeamTeaTime\Forum\Http\Resources\PostResource::class,
],
];
@@ -4,28 +4,39 @@ return [
/*
|--------------------------------------------------------------------------
| Enable/disable
| Enable/disable feature
|--------------------------------------------------------------------------
|
| Set to false if you want to effectively disable the frontend.
| Whether or not to enable the frontend feature.
|
*/
'enable' => true,
/*
|--------------------------------------------------------------------------
| Preset
|--------------------------------------------------------------------------
|
| The frontend preset to use. Must be installed with the
| forum:preset-install command.
|
*/
'preset' => 'livewire-tailwind',
/*
|--------------------------------------------------------------------------
| Router
|--------------------------------------------------------------------------
|
| Web router config.
| Router config for the frontend routes.
|
*/
'router' => [
'prefix' => '/forum',
'as' => 'forum.',
'namespace' => '\TeamTeaTime\Forum\Http\Controllers\Web',
'middleware' => ['web'],
'auth_middleware' => ['auth'],
],
@@ -35,7 +46,7 @@ return [
| Route Prefixes
|--------------------------------------------------------------------------
|
| Prefixes to use for each model.
| Prefixes to use for each model in frontend routes.
|
*/
@@ -61,11 +72,11 @@ return [
| Utility Class
|--------------------------------------------------------------------------
|
| Here we specify the class to use for various web utility methods.
| Here we specify the class to use for various frontend utility methods.
| This is automatically aliased to 'Forum' for ease of use in views.
|
*/
'utility_class' => TeamTeaTime\Forum\Support\Web\Forum::class,
'utility_class' => TeamTeaTime\Forum\Support\Frontend\Forum::class,
];
@@ -16,7 +16,7 @@ return [
|
*/
'old_thread_threshold' => '30 days',
'old_thread_threshold' => '7 days',
/*
|--------------------------------------------------------------------------
-24
View File
@@ -1,24 +0,0 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| MyceliumGear authorization configuration
|--------------------------------------------------------------------------
*/
'gateway_id' => env('MYCELIUM_GATEWAY_ID', ''),
'gateway_secret' => env('MYCELIUM_GATEWAY_SECRET', ''),
/*
|--------------------------------------------------------------------------
| Default configuration
|--------------------------------------------------------------------------
|
| Define default callback data that will be used before any custom data are set
*/
'defaults' => [
'callback_data' => [
//'foo' => 'bar'
],
],
];
View File
@@ -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]');
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,115 @@
@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"
@start="drag=true"
@end="drag=false"
:group="{ name: 'categories' }"
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>
<draggable-category-list :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,5 @@
<div class="mb-3">
<label for="color">{{ trans('forum::general.color') }}</label>
<div class="pickr"></div>
<input type="hidden" value="{{ isset($category->color_light_mode) ? $category->color_light_mode : (old('color') ?? config('forum.frontend.default_category_color')) }}" name="color">
</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,94 @@
<!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 id="navbar" class="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="{{ 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
@@ -0,0 +1,83 @@
@import url('https://fonts.googleapis.com/css2?family=Source+Sans+3:ital,wght@0,400;0,500;0,700;1,400&display=swap');
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
.forum {
font-family: "Source Sans 3", sans-serif;
font-optical-sizing: auto;
font-size: 18px;
@apply text-slate-900;
}
.breadcrumbs ol li {
display: inline-block;
}
.breadcrumbs ol li + li {
margin-left: .5rem;
}
.breadcrumbs ol li + li::before {
float: left;
padding-right: .5rem;
color: #6c757d;
content: "/";
}
.forum .clr-field button {
@apply w-6 h-6 mr-2 rounded-full;
}
.forum .nested-sort .ns-targeted {
@apply rounded-md border-green-500 outline outline-green-500;
}
.bg-category {
background-color: var(--category-light);
}
.text-category {
color: var(--category-light);
}
div, p {
font-size: 1rem;
}
h1 {
@apply text-4xl my-6;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-xl;
}
a:not(.link-button):not(.group-button):not(.text-category) {
@apply text-slate-600 hover:text-slate-900;
}
@media (prefers-color-scheme: dark) {
.forum {
@apply text-slate-400;
}
.bg-category {
background-color: var(--category-dark);
}
.text-category {
color: var(--category-dark);
}
a:not(.link-button):not(.group-button):not(.text-category) {
@apply text-slate-300 hover:text-white;
}
}
}
@@ -0,0 +1,9 @@
import { formatDistance } from 'date-fns';
import '@melloware/coloris/dist/coloris.css';
import Coloris from '@melloware/coloris';
import NestedSort from 'nested-sort';
window.dateFormatDistance = formatDistance;
window.Coloris = Coloris;
window.Coloris.init();
window.NestedSort = NestedSort;
@@ -0,0 +1,64 @@
<div x-data="alerts" @alert.window="onReceiveAlert" class="grid place-content-center fixed bottom-4 left-0 right-0 z-50">
<template x-for="alert in list" :key="alert.id">
<div
x-show="alert.show"
x-transition:enter="transition-all ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-2"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition-all ease-in duration-300"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-2"
class="min-w-80 max-w-96 px-6 py-4 mb-4 text-lg text-center rounded-md"
:class="alert.classes">
<span x-text="alert.message"></span>
</div>
</template>
</div>
@script
<script>
const SHOW_DURATION = 4000;
const FADE_DURATION = 300;
const TICK_INTERVAL = 250;
Alpine.data('alerts', () => {
return {
list: [],
baseClasses: '',
styleMap: {
'success': 'bg-green-400 text-green-900',
'warning': 'bg-orange-400 text-orange-900',
},
init() {
setInterval(() => {
for (let i = this.list.length - 1; i >= 0; --i) {
if (this.list[i].showFor === SHOW_DURATION) {
this.list[i].show = true;
}
this.list[i].showFor -= TICK_INTERVAL;
if (this.list[i].showFor <= FADE_DURATION) {
this.list[i].show = false;
}
if (this.list[i].showFor <= 0) {
this.list.splice(i, 1);
}
}
}, TICK_INTERVAL);
},
onReceiveAlert(event) {
this.list.unshift({
id: Math.random().toString(36).slice(2, 7),
show: false,
type: event.detail.type,
message: event.detail.message,
showFor: SHOW_DURATION,
classes: this.styleMap[event.detail.type]
});
}
}
});
</script>
@endscript
@@ -0,0 +1,4 @@
@if ($category->parent !== null)
@include ('forum::components.breadcrumb-categories', ['category' => $category->parent])
@endif
<li class="breadcrumb-item"><a href="{{ Forum::route('category.show', $category) }}">{{ $category->title }}</a></li>
@@ -0,0 +1,23 @@
<nav class="breadcrumbs bg-slate-300 rounded-lg p-2 my-2 dark:bg-slate-700" aria-label="breadcrumb">
<ol class="flex flex-wrap">
<li>
<a href="{{ url(config('forum.frontend.router.prefix')) }}" class="flex items-center">
<span class="inline-block mr-1">
@include ('forum::components.icons.home-mini')
</span>
{{ trans('forum::general.index') }}
</a>
</li>
@if (isset($category) && $category || isset($thread->category) && $thread->category)
@include ('forum::components.breadcrumb-categories', ['category' => $category ?? $thread->category])
@endif
@if (isset($thread) && $thread)
<li>{{ trans_choice('forum::threads.thread', 1) }}</li>
@endif
@if (isset($breadcrumbs_append))
@foreach ($breadcrumbs_append as $breadcrumb)
<li>{{ $breadcrumb }}</li>
@endforeach
@endif
</ol>
</nav>
@@ -0,0 +1,21 @@
@php
$colorClasses = match ($intent) {
'primary', '', null => 'text-white bg-blue-600 hover:text-white hover:bg-blue-500',
'secondary' => 'text-zinc-800 bg-zinc-400/50 hover:bg-zinc-400/35 dark:text-slate-800 dark:bg-slate-400/50 dark:hover:bg-slate-400/65',
'danger' => 'text-white bg-red-500 hover:bg-red-400'
};
$sizeClasses = match ($size) {
'regular', '', null => 'min-w-36 px-4 py-2',
'small' => 'px-4 py-1',
};
@endphp
<button
class="inline-block rounded-full font-medium text-lg text-center disabled:text-slate-500 disabled:bg-slate-300 dark:disabled:text-slate-300 dark:disabled:bg-slate-500 {{ $colorClasses }} {{ $sizeClasses }}"
{{ $attributes }}>
@if (isset($icon) && !empty($icon))
@include ("forum::components.icons.{$icon}", ['size' => '5'])
@endif
{{ $label }}
</button>
@@ -0,0 +1,103 @@
<div class="my-4" style="{{ $category->styleVariables }}">
<div class="bg-white shadow-md rounded-lg p-4 flex flex-col sm:flex-row sm:items-center sm:content-center dark:bg-slate-700">
<div class="hidden sm:block self-stretch">
<div class="w-3 h-full rounded-full mr-4 bg-category"></div>
</div>
<div class="grow text-center sm:text-left">
<h2>
<a href="{{ $category->route }}" class="text-category">{{ $category->title }}</a>
</h2>
<h3 class="text-slate-600 dark:text-slate-400">{{ $category->description }}</h3>
</div>
<div class="text-center text-base mt-2 sm:mt-0">
@if ($category->accepts_threads)
<livewire:forum::components.pill
icon="chat-bubbles-mini"
:text="trans_choice('forum::threads.thread', 2) . ': ' . $category->thread_count" />
<livewire:forum::components.pill
icon="chat-bubble-text-mini"
:text="trans_choice('forum::posts.post', 2) . ': ' . $category->post_count" />
@endif
</div>
<div class="min-w-30 sm:min-w-48 lg:min-w-96 xl:w-full xl:max-w-lg text-center sm:text-right mt-2 sm:mt-0">
@if ($category->accepts_threads)
@if ($category->newestThread)
<div>
@include ("forum::components.icons.chat-bubbles-mini")
<a href="{{ $category->newestThread->route }}" class="inline-block max-w-36 md:max-w-48 lg:max-w-64 truncate align-middle">
{{ $category->newestThread->title }}
</a>
<span class="inline-block align-middle">
<livewire:forum::components.timestamp :carbon="$category->newestThread->created_at" />
</span>
</div>
@endif
@if ($category->latestActiveThread && $category->latestActiveThread->reply_count > 1)
<div>
@include ("forum::components.icons.chat-bubble-text-mini")
<a href="{{ $category->latestActiveThread->lastPost->route }}" class="inline-block max-w-36 md:max-w-48 lg:max-w-64 truncate align-middle">
Re: {{ $category->latestActiveThread->title }}
</a>
<span class="inline-block align-middle">
<livewire:forum::components.timestamp :carbon="$category->latestActiveThread->lastPost->created_at" />
</span>
</div>
@endif
@endif
</div>
</div>
@if (count($category->children) > 0)
@foreach ($category->children as $subcategory)
<div class="flex mt-4" style="{{ $subcategory->styleVariables }}">
<div class="min-w-12 sm:min-w-24 self-center text-center text-slate-300 dark:text-slate-700">
@include ('forum::components.icons.subcategory', ['size' => '12'])
</div>
<div class="grow flex flex-col sm:flex-row sm:items-center sm:content-center items-center justify-items-center bg-white shadow-md rounded-lg p-4 dark:bg-slate-700">
<div class="grow text-center sm:text-left">
<h3>
<a href="{{ $subcategory->route }}" class="text-category">{{ $subcategory->title }}</a>
</h3>
<h3 class="text-slate-600 text-base dark:text-slate-400">{{ $subcategory->description }}</h3>
</div>
<div class="text-center text-base mt-2 sm:mt-0">
@if ($subcategory->accepts_threads)
<livewire:forum::components.pill
icon="chat-bubbles-mini"
:text="trans_choice('forum::threads.thread', 2) . ': ' . $subcategory->thread_count" />
<livewire:forum::components.pill
icon="chat-bubble-text-mini"
:text="trans_choice('forum::posts.post', 2) . ': ' . $subcategory->post_count" />
@endif
</div>
<div class="min-w-30 sm:min-w-48 lg:min-w-96 xl:w-full xl:max-w-lg text-center sm:text-right mt-2 sm:mt-0">
@if ($subcategory->accepts_threads)
@if ($subcategory->newestThread)
<div>
@include ("forum::components.icons.chat-bubbles-mini")
<a href="{{ $subcategory->newestThread->route }}" class="inline-block max-w-36 md:max-w-48 lg:max-w-64 truncate align-middle">
{{ $subcategory->newestThread->title }}
</a>
<span class="inline-block align-middle">
<livewire:forum::components.timestamp :carbon="$subcategory->newestThread->created_at" />
</span>
</div>
@endif
@if ($subcategory->latestActiveThread && $subcategory->latestActiveThread->reply_count > 1)
<div>
@include ("forum::components.icons.chat-bubble-text-mini")
<a href="{{ $subcategory->latestActiveThread->lastPost->route }}" class="inline-block max-w-36 md:max-w-48 lg:max-w-64 truncate align-middle">
Re: {{ $subcategory->latestActiveThread->title }}
</a>
<span class="inline-block align-middle">
<livewire:forum::components.timestamp :carbon="$subcategory->latestActiveThread->lastPost->created_at" />
</span>
</div>
@endif
@endif
</div>
</div>
</div>
@endforeach
@endif
</div>
@@ -0,0 +1,18 @@
@foreach ($categories as $category)
<li class="font-medium border border-slate-300 dark:border-slate-600 rounded-md text-lg p-4 my-2 cursor-move" data-id="{{ $category->id }}">
<span class="flex">
<span class="text-slate-400 mr-1" style="margin-left: -8px">
@include ('forum::components.icons.drag-area-vertical')
</span>
<span class="grow select-none" data-title>{{ $category->title }}</span>
<a href="{{ Forum::route('category.edit', $category) }}">
{{ trans('forum::general.edit') }}
</a>
</span>
@if (count($category->children) > 0)
<ol data-id="{{ $category->id }}">
@include ('forum::components.category.draggable-items', ['categories' => $category->children])
</ol>
@endif
</li>
@endforeach
@@ -0,0 +1,10 @@
@foreach ($categories as $category)
<option value="{{ $category->id }}" {{ isset($disable) && $category->id == $disable || !$category->accepts_threads ? 'disabled' : '' }}>
@for ($i = 0; $i < $category->depth; ++$i)- @endfor
{{ $category->title }}
</option>
@if ($category->children)
@include ('forum::components.category.options', ['categories' => $category->children])
@endif
@endforeach
@@ -0,0 +1,3 @@
<div class="mt-2 text-pink-800 font-medium">
@error($id) {{ $message }} @enderror
</div>
@@ -0,0 +1,11 @@
<div {!! isset($xShow) && !empty($xShow) ? "x-show=\"{$xShow}\"" : "" !!} class="flex {{ isset($reverse) && $reverse ? 'flex-row-reverse' : '' }} items-center mb-2">
<input
id="{{ $id }}"
type="checkbox"
value="{{ $value }}"
class="w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 rounded focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-800 dark:border-gray-600"
{{ $attributes }} />
@if (isset($label))
<label for="{{ $id }}" class="ms-2 font-medium text-gray-900 select-none dark:text-slate-400">{{ $label }}</label>
@endif
</div>
@@ -0,0 +1,11 @@
<div {!! isset($xShow) && !empty($xShow) ? "x-show=\"{$xShow}\"" : "" !!} class="mb-4">
<label for="{{ $id }}" class="block mb-2 font-medium text-gray-900 dark:text-slate-400">{{ $label }}</label>
<select
id="{{ $id }}"
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
{{ $attributes }}>
{{ $slot }}
</select>
@include ('forum::components.form.error')
</div>
@@ -0,0 +1,11 @@
<div {!! isset($xShow) && !empty($xShow) ? "x-show=\"{$xShow}\"" : "" !!} class="mb-4">
<label for="{{ $id }}" class="block mb-2 font-medium text-gray-900 dark:text-slate-400">{{ $label }}</label>
<input
type="{{ $type ?? 'text' }}"
id="{{ $id }}"
value="{{ $value }}"
class="bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
{{ $attributes }} />
@include ('forum::components.form.error')
</div>
@@ -0,0 +1,11 @@
<div {!! isset($xShow) && !empty($xShow) ? "x-show=\"{$xShow}\"" : "" !!} class="mb-4">
@if (isset($label))
<label for="{{ $id }}" class="block mb-2 font-medium text-gray-900 dark:text-slate-400">{{ $label }}</label>
@endif
<textarea
id="{{ $id }}"
class="block p-2.5 w-full min-h-36 text-gray-900 bg-gray-50 rounded-lg border border-gray-300 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-800 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
{{ $attributes }}></textarea>
@include ('forum::components.form.error')
</div>
@@ -0,0 +1,14 @@
@php
$colorClasses = match ($intent) {
'primary', '', null => 'text-white bg-blue-600 hover:text-white hover:bg-blue-500',
'secondary' => 'text-zinc-800 bg-zinc-400/50 hover:bg-zinc-400/35 dark:text-slate-800 dark:bg-slate-400/50 dark:hover:bg-slate-400/65',
'danger' => 'text-white bg-red-500 hover:bg-red-400'
};
@endphp
<a href="{{ $href }}" class="group-button py-2 px-5 text-base font-medium inline-flex items-center gap-x-2 -ms-px first:rounded-s-xl first:ms-0 last:rounded-e-xl focus:z-10 disabled:opacity-50 disabled:pointer-events-none {{ $colorClasses }}" {{ $attributes }}>
@if (isset($icon) && !empty($icon))
@include ("forum::components.icons.{$icon}", ['size' => '5'])
@endif
{{ $label }}
</a>
@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M10 3a.75.75 0 0 1 .75.75v10.638l3.96-4.158a.75.75 0 1 1 1.08 1.04l-5.25 5.5a.75.75 0 0 1-1.08 0l-5.25-5.5a.75.75 0 1 1 1.08-1.04l3.96 4.158V3.75A.75.75 0 0 1 10 3Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 389 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M15.312 11.424a5.5 5.5 0 0 1-9.201 2.466l-.312-.311h2.433a.75.75 0 0 0 0-1.5H3.989a.75.75 0 0 0-.75.75v4.242a.75.75 0 0 0 1.5 0v-2.43l.31.31a7 7 0 0 0 11.712-3.138.75.75 0 0 0-1.449-.39Zm1.23-3.723a.75.75 0 0 0 .219-.53V2.929a.75.75 0 0 0-1.5 0V5.36l-.31-.31A7 7 0 0 0 3.239 8.188a.75.75 0 1 0 1.448.389A5.5 5.5 0 0 1 13.89 6.11l.311.31h-2.432a.75.75 0 0 0 0 1.5h4.243a.75.75 0 0 0 .53-.219Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 616 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M3 10a.75.75 0 0 1 .75-.75h10.638L10.23 5.29a.75.75 0 1 1 1.04-1.08l5.5 5.25a.75.75 0 0 1 0 1.08l-5.5 5.25a.75.75 0 1 1-1.04-1.08l4.158-3.96H3.75A.75.75 0 0 1 3 10Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 389 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M10 18a8 8 0 1 0 0-16 8 8 0 0 0 0 16Zm-.75-4.75a.75.75 0 0 0 1.5 0V8.66l1.95 2.1a.75.75 0 1 0 1.1-1.02l-3.25-3.5a.75.75 0 0 0-1.1 0L6.2 9.74a.75.75 0 1 0 1.1 1.02l1.95-2.1v4.59Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 404 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M10 17a.75.75 0 0 1-.75-.75V5.612L5.29 9.77a.75.75 0 0 1-1.08-1.04l5.25-5.5a.75.75 0 0 1 1.08 0l5.25 5.5a.75.75 0 1 1-1.08 1.04l-3.96-4.158V16.25A.75.75 0 0 1 10 17Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 390 B

@@ -0,0 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="inline-block w-{{ $size ?? '6' }} h-{{ $size ?? '6' }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6.042A8.967 8.967 0 0 0 6 3.75c-1.052 0-2.062.18-3 .512v14.25A8.987 8.987 0 0 1 6 18c2.305 0 4.408.867 6 2.292m0-14.25a8.966 8.966 0 0 1 6-2.292c1.052 0 2.062.18 3 .512v14.25A8.987 8.987 0 0 0 18 18a8.967 8.967 0 0 0-6 2.292m0-14.25v14.25" />
</svg>

After

Width:  |  Height:  |  Size: 515 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M10 2c-2.236 0-4.43.18-6.57.524C1.993 2.755 1 4.014 1 5.426v5.148c0 1.413.993 2.67 2.43 2.902 1.168.188 2.352.327 3.55.414.28.02.521.18.642.413l1.713 3.293a.75.75 0 0 0 1.33 0l1.713-3.293a.783.783 0 0 1 .642-.413 41.102 41.102 0 0 0 3.55-.414c1.437-.231 2.43-1.49 2.43-2.902V5.426c0-1.413-.993-2.67-2.43-2.902A41.289 41.289 0 0 0 10 2ZM6.75 6a.75.75 0 0 0 0 1.5h6.5a.75.75 0 0 0 0-1.5h-6.5Zm0 2.5a.75.75 0 0 0 0 1.5h3.5a.75.75 0 0 0 0-1.5h-3.5Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 667 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M7.5 8.25h9m-9 3H12m-9.75 1.51c0 1.6 1.123 2.994 2.707 3.227 1.129.166 2.27.293 3.423.379.35.026.67.21.865.501L12 21l2.755-4.133a1.14 1.14 0 0 1 .865-.501 48.172 48.172 0 0 0 3.423-.379c1.584-.233 2.707-1.626 2.707-3.228V6.741c0-1.602-1.123-2.995-2.707-3.228A48.394 48.394 0 0 0 12 3c-2.392 0-4.744.175-7.043.513C3.373 3.746 2.25 5.14 2.25 6.741v6.018Z" />
</svg>

After

Width:  |  Height:  |  Size: 582 B

@@ -0,0 +1,8 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path d="M3.505 2.365A41.369 41.369 0 0 1 9 2c1.863 0 3.697.124 5.495.365 1.247.167 2.18 1.108 2.435 2.268a4.45 4.45 0 0 0-.577-.069 43.141 43.141 0 0 0-4.706 0C9.229 4.696 7.5 6.727 7.5 8.998v2.24c0 1.413.67 2.735 1.76 3.562l-2.98 2.98A.75.75 0 0 1 5 17.25v-3.443c-.501-.048-1-.106-1.495-.172C2.033 13.438 1 12.162 1 10.72V5.28c0-1.441 1.033-2.717 2.505-2.914Z" />
<path d="M14 6c-.762 0-1.52.02-2.271.062C10.157 6.148 9 7.472 9 8.998v2.24c0 1.519 1.147 2.839 2.71 2.935.214.013.428.024.642.034.2.009.385.09.518.224l2.35 2.35a.75.75 0 0 0 1.28-.531v-2.07c1.453-.195 2.5-1.463 2.5-2.915V8.998c0-1.526-1.157-2.85-2.729-2.936A41.645 41.645 0 0 0 14 6Z" />
</svg>

After

Width:  |  Height:  |  Size: 825 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '5' }} h-{{ $size ?? '5' }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 8.511c.884.284 1.5 1.128 1.5 2.097v4.286c0 1.136-.847 2.1-1.98 2.193-.34.027-.68.052-1.02.072v3.091l-3-3c-1.354 0-2.694-.055-4.02-.163a2.115 2.115 0 0 1-.825-.242m9.345-8.334a2.126 2.126 0 0 0-.476-.095 48.64 48.64 0 0 0-8.048 0c-1.131.094-1.976 1.057-1.976 2.192v4.286c0 .837.46 1.58 1.155 1.951m9.345-8.334V6.637c0-1.621-1.152-3.026-2.76-3.235A48.455 48.455 0 0 0 11.25 3c-2.115 0-4.198.137-6.24.402-1.608.209-2.76 1.614-2.76 3.235v6.226c0 1.621 1.152 3.026 2.76 3.235.577.075 1.157.14 1.74.194V21l4.155-4.155" />
</svg>

After

Width:  |  Height:  |  Size: 748 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 15 15"
fill="currentColor"
class="inline-block w-{{ $size ?? '8' }} h-{{ $size ?? '8' }}">
<path clip-rule="evenodd" d="M4.49999 2.5C4.49999 1.94772 4.9477 1.5 5.49999 1.5C6.05227 1.5 6.49999 1.94772 6.49999 2.5C6.49999 3.05228 6.05227 3.5 5.49999 3.5C4.9477 3.5 4.49999 3.05228 4.49999 2.5ZM8.49999 2.5C8.49999 1.94772 8.9477 1.5 9.49999 1.5C10.0523 1.5 10.5 1.94772 10.5 2.5C10.5 3.05228 10.0523 3.5 9.49999 3.5C8.9477 3.5 8.49999 3.05229 8.49999 2.5ZM4.49998 7.5C4.49998 6.94772 4.9477 6.5 5.49998 6.5C6.05227 6.5 6.49998 6.94772 6.49998 7.5C6.49998 8.05228 6.05227 8.5 5.49998 8.5C4.9477 8.5 4.49998 8.05228 4.49998 7.5ZM8.49998 7.5C8.49998 6.94771 8.9477 6.5 9.49999 6.5C10.0523 6.5 10.5 6.94772 10.5 7.5C10.5 8.05228 10.0523 8.5 9.49998 8.5C8.9477 8.5 8.49998 8.05228 8.49998 7.5ZM4.49998 12.5C4.49998 11.9477 4.9477 11.5 5.49998 11.5C6.05227 11.5 6.49998 11.9477 6.49998 12.5C6.49998 13.0523 6.05227 13.5 5.49998 13.5C4.9477 13.5 4.49998 13.0523 4.49998 12.5ZM8.49998 12.5C8.49998 11.9477 8.9477 11.5 9.49998 11.5C10.0523 11.5 10.5 11.9477 10.5 12.5C10.5 13.0523 10.0523 13.5 9.49998 13.5C8.9477 13.5 8.49998 13.0523 8.49998 12.5Z" />
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M9.293 2.293a1 1 0 0 1 1.414 0l7 7A1 1 0 0 1 17 11h-1v6a1 1 0 0 1-1 1h-2a1 1 0 0 1-1-1v-3a1 1 0 0 0-1-1H9a1 1 0 0 0-1 1v3a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1v-6H3a1 1 0 0 1-.707-1.707l7-7Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 405 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M10 1a4.5 4.5 0 0 0-4.5 4.5V9H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-.5V5.5A4.5 4.5 0 0 0 10 1Zm3 8V5.5a3 3 0 1 0-6 0V9h6Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 375 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M14.5 1A4.5 4.5 0 0 0 10 5.5V9H3a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2h-1.5V5.5a3 3 0 1 1 6 0v2.75a.75.75 0 0 0 1.5 0V5.5A4.5 4.5 0 0 0 14.5 1Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 394 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path d="m2.695 14.762-1.262 3.155a.5.5 0 0 0 .65.65l3.155-1.262a4 4 0 0 0 1.343-.886L17.5 5.501a2.121 2.121 0 0 0-3-3L3.58 13.419a4 4 0 0 0-.885 1.343Z" />
</svg>

After

Width:  |  Height:  |  Size: 328 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="inline-block w-{{ $size ?? '6' }} h-{{ $size ?? '6' }}">
<path stroke-linecap="round" stroke-linejoin="round" d="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L6.832 19.82a4.5 4.5 0 0 1-1.897 1.13l-2.685.8.8-2.685a4.5 4.5 0 0 1 1.13-1.897L16.863 4.487Zm0 0L19.5 7.125" />
</svg>

After

Width:  |  Height:  |  Size: 395 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path d="M15.98 1.804a1 1 0 0 0-1.96 0l-.24 1.192a1 1 0 0 1-.784.785l-1.192.238a1 1 0 0 0 0 1.962l1.192.238a1 1 0 0 1 .785.785l.238 1.192a1 1 0 0 0 1.962 0l.238-1.192a1 1 0 0 1 .785-.785l1.192-.238a1 1 0 0 0 0-1.962l-1.192-.238a1 1 0 0 1-.785-.785l-.238-1.192ZM6.949 5.684a1 1 0 0 0-1.898 0l-.683 2.051a1 1 0 0 1-.633.633l-2.051.683a1 1 0 0 0 0 1.898l2.051.684a1 1 0 0 1 .633.632l.683 2.051a1 1 0 0 0 1.898 0l.683-2.051a1 1 0 0 1 .633-.633l2.051-.683a1 1 0 0 0 0-1.898l-2.051-.683a1 1 0 0 1-.633-.633L6.95 5.684ZM13.949 13.684a1 1 0 0 0-1.898 0l-.184.551a1 1 0 0 1-.632.633l-.551.183a1 1 0 0 0 0 1.898l.551.183a1 1 0 0 1 .633.633l.183.551a1 1 0 0 0 1.898 0l.184-.551a1 1 0 0 1 .632-.633l.551-.183a1 1 0 0 0 0-1.898l-.551-.184a1 1 0 0 1-.633-.632l-.183-.551Z" />
</svg>

After

Width:  |  Height:  |  Size: 933 B

@@ -0,0 +1,9 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="inline-block w-{{ $size ?? '6' }} h-{{ $size ?? '6' }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.5 16.875h3.375m0 0h3.375m-3.375 0V13.5m0 3.375v3.375M6 10.5h2.25a2.25 2.25 0 0 0 2.25-2.25V6a2.25 2.25 0 0 0-2.25-2.25H6A2.25 2.25 0 0 0 3.75 6v2.25A2.25 2.25 0 0 0 6 10.5Zm0 9.75h2.25A2.25 2.25 0 0 0 10.5 18v-2.25a2.25 2.25 0 0 0-2.25-2.25H6a2.25 2.25 0 0 0-2.25 2.25V18A2.25 2.25 0 0 0 6 20.25Zm9.75-9.75H18a2.25 2.25 0 0 0 2.25-2.25V6A2.25 2.25 0 0 0 18 3.75h-2.25A2.25 2.25 0 0 0 13.5 6v2.25a2.25 2.25 0 0 0 2.25 2.25Z" />
</svg>

After

Width:  |  Height:  |  Size: 699 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path d="M18.29 15.71l-4.58 4.58c-.39.39-1.03.39-1.42 0-.39-.39-.39-1.03 0-1.42L15.17 16H5c-.55 0-1-.45-1-1V5c0-.55.45-1 1-1s1 .45 1 1v9h9.17l-2.88-2.87c-.39-.39-.39-1.03 0-1.42.39-.39 1.03-.39 1.42 0l4.58 4.58c.39.39.39 1.03 0 1.42z"></path>
</svg>

After

Width:  |  Height:  |  Size: 414 B

@@ -0,0 +1,7 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
class="inline-block w-{{ $size ?? '4' }} h-{{ $size ?? '4' }}">
<path fill-rule="evenodd" d="M8.75 1A2.75 2.75 0 0 0 6 3.75v.443c-.795.077-1.584.176-2.365.298a.75.75 0 1 0 .23 1.482l.149-.022.841 10.518A2.75 2.75 0 0 0 7.596 19h4.807a2.75 2.75 0 0 0 2.742-2.53l.841-10.52.149.023a.75.75 0 0 0 .23-1.482A41.03 41.03 0 0 0 14 4.193V3.75A2.75 2.75 0 0 0 11.25 1h-2.5ZM10 4c.84 0 1.673.025 2.5.075V3.75c0-.69-.56-1.25-1.25-1.25h-2.5c-.69 0-1.25.56-1.25 1.25v.325C8.327 4.025 9.16 4 10 4ZM8.58 7.72a.75.75 0 0 0-1.5.06l.3 7.5a.75.75 0 1 0 1.5-.06l-.3-7.5Zm4.34.06a.75.75 0 1 0-1.5-.06l-.3 7.5a.75.75 0 1 0 1.5.06l.3-7.5Z" clip-rule="evenodd" />
</svg>

After

Width:  |  Height:  |  Size: 747 B

@@ -0,0 +1,8 @@
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor" class="inline-block w-{{ $size ?? '6' }} h-{{ $size ?? '6' }}">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>

After

Width:  |  Height:  |  Size: 289 B

@@ -0,0 +1,19 @@
@php
$colorClasses = match ($intent) {
'primary', '', null => 'text-white bg-blue-600 hover:text-white hover:bg-blue-500',
'secondary' => 'text-zinc-800 bg-zinc-400/50 hover:bg-zinc-400/35 dark:text-slate-800 dark:bg-slate-400/50 dark:hover:bg-slate-400/65',
'danger' => 'text-white bg-red-500 hover:bg-red-400'
};
$sizeClasses = match ($size) {
'regular', '', null => 'min-w-36 px-4 py-2',
'small' => 'px-4 py-1',
};
@endphp
<a href="{{ $href }}" class="link-button inline-block rounded-full font-medium text-lg text-center {{ $colorClasses }} {{ $sizeClasses }}" {{ $attributes }}>
@if (isset($icon) && !empty($icon))
@include ("forum::components.icons.{$icon}", ['size' => '5'])
@endif
{{ $label }}
</a>
@@ -0,0 +1,15 @@
<div wire:loading class="fixed top-0 right-0 bottom-0 left-0 bg-white/40 z-50">
<div class="flex h-screen place-content-center">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 200 200" class="w-16 w-16">
<circle fill="#9A9A9A" stroke="#9A9A9A" stroke-width="15" r="15" cx="40" cy="100">
<animate attributeName="opacity" calcMode="spline" dur="2" values="1;0;1;" keySplines=".5 0 .5 1;.5 0 .5 1" repeatCount="indefinite" begin="-.4"></animate>
</circle>
<circle fill="#9A9A9A" stroke="#9A9A9A" stroke-width="15" r="15" cx="100" cy="100">
<animate attributeName="opacity" calcMode="spline" dur="2" values="1;0;1;" keySplines=".5 0 .5 1;.5 0 .5 1" repeatCount="indefinite" begin="-.2"></animate>
</circle>
<circle fill="#9A9A9A" stroke="#9A9A9A" stroke-width="15" r="15" cx="160" cy="100">
<animate attributeName="opacity" calcMode="spline" dur="2" values="1;0;1;" keySplines=".5 0 .5 1;.5 0 .5 1" repeatCount="indefinite" begin="0"></animate>
</circle>
</svg>
</div>
</div>
@@ -0,0 +1,21 @@
<div class="fixed top-0 right-0 bottom-0 left-0 bg-white/40 z-40 dark:bg-slate-900/40" {{ $attributes }}>
<div class="grid h-screen justify-items-center items-center" {!! !empty($onClose) ? "@click.self=\"{$onClose}\"" : '' !!}>
<div class="sm:min-w-96 bg-white shadow-lg rounded-md dark:bg-slate-700">
<div class="flex border-b border-slate-300 p-6 pb-4 dark:border-slate-500">
<div class="grow">
<h2>{{ $heading }}</h2>
</div>
@if (!empty($onClose))
<div class="pl-4 pt-1">
<a href="#" class="" @click="{{ $onClose }}">
@include ('forum::components.icons.x-mark')
</a>
</div>
@endif
</div>
<div class="p-6 pt-4 mt-2">
{{ $slot }}
</div>
</div>
</div>
</div>
@@ -0,0 +1,106 @@
@if ($paginator->hasPages())
<nav role="navigation" aria-label="{{ __('Pagination Navigation') }}" class="flex flex-col items-center justify-between">
<div class="flex justify-between flex-1 sm:hidden">
@if ($paginator->onFirstPage())
<span class="relative inline-flex items-center px-4 py-2 font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
{!! __('pagination.previous') !!}
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" class="relative inline-flex items-center px-4 py-2 font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.previous') !!}
</a>
@endif
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" class="relative inline-flex items-center px-4 py-2 ml-3 font-medium text-gray-700 bg-white border border-gray-300 leading-5 rounded-md hover:text-gray-500 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-300 dark:focus:border-blue-700 dark:active:bg-gray-700 dark:active:text-gray-300">
{!! __('pagination.next') !!}
</a>
@else
<span class="relative inline-flex items-center px-4 py-2 ml-3 font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 rounded-md dark:text-gray-600 dark:bg-gray-800 dark:border-gray-600">
{!! __('pagination.next') !!}
</span>
@endif
</div>
<div class="hidden sm:flex-1 sm:flex sm:flex-col sm:items-center sm:justify-between">
<div class="mb-4">
<p class="text-gray-700 leading-5 dark:text-gray-400">
{!! __('Showing') !!}
@if ($paginator->firstItem())
<span class="font-medium">{{ $paginator->firstItem() }}</span>
{!! __('to') !!}
<span class="font-medium">{{ $paginator->lastItem() }}</span>
@else
{{ $paginator->count() }}
@endif
{!! __('of') !!}
<span class="font-medium">{{ $paginator->total() }}</span>
{!! __('results') !!}
</p>
</div>
<div class="text-center">
<span class="relative z-0 inline-flex rtl:flex-row-reverse shadow-sm rounded-md">
{{-- Previous Page Link --}}
@if ($paginator->onFirstPage())
<span aria-disabled="true" aria-label="{{ __('pagination.previous') }}">
<span class="relative inline-flex items-center px-2 py-2 font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-l-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</span>
</span>
@else
<a href="{{ $paginator->previousPageUrl() }}" rel="prev" class="relative inline-flex items-center px-2 py-2 font-medium text-gray-500 bg-white border border-gray-300 rounded-l-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('pagination.previous') }}">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
</svg>
</a>
@endif
{{-- Pagination Elements --}}
@foreach ($elements as $element)
{{-- "Three Dots" Separator --}}
@if (is_string($element))
<span aria-disabled="true">
<span class="relative inline-flex items-center px-4 py-2 -ml-px font-medium text-gray-700 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600">{{ $element }}</span>
</span>
@endif
{{-- Array Of Links --}}
@if (is_array($element))
@foreach ($element as $page => $url)
@if ($page == $paginator->currentPage())
<span aria-current="page">
<span class="relative inline-flex items-center px-4 py-2 -ml-px font-medium text-gray-500 bg-white border border-gray-300 cursor-default leading-5 dark:bg-gray-800 dark:border-gray-600">{{ $page }}</span>
</span>
@else
<a href="{{ $url }}" class="relative inline-flex items-center px-4 py-2 -ml-px font-medium text-gray-700 bg-white border border-gray-300 leading-5 hover:text-gray-500 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-700 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-400 dark:hover:text-gray-300 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('Go to page :page', ['page' => $page]) }}">
{{ $page }}
</a>
@endif
@endforeach
@endif
@endforeach
{{-- Next Page Link --}}
@if ($paginator->hasMorePages())
<a href="{{ $paginator->nextPageUrl() }}" rel="next" class="relative inline-flex items-center px-2 py-2 -ml-px font-medium text-gray-500 bg-white border border-gray-300 rounded-r-md leading-5 hover:text-gray-400 focus:z-10 focus:outline-none focus:ring ring-gray-300 focus:border-blue-300 active:bg-gray-100 active:text-gray-500 transition ease-in-out duration-150 dark:bg-gray-800 dark:border-gray-600 dark:active:bg-gray-700 dark:focus:border-blue-800" aria-label="{{ __('pagination.next') }}">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</a>
@else
<span aria-disabled="true" aria-label="{{ __('pagination.next') }}">
<span class="relative inline-flex items-center px-2 py-2 -ml-px font-medium text-gray-500 bg-white border border-gray-300 cursor-default rounded-r-md leading-5 dark:bg-gray-800 dark:border-gray-600" aria-hidden="true">
<svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
<path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
</svg>
</span>
</span>
@endif
</span>
</div>
</div>
</nav>
@endif
@@ -0,0 +1,14 @@
<span class="inline-block
rounded-full
text-base
text-nowrap
align-middle
{{ $bgColor ?? 'bg-zinc-300 dark:bg-slate-500' }}
{{ $textColor ?? 'text-zinc-600 dark:text-slate-200' }}
{{ $padding ?? 'px-2' }}
{{ $margin ?? 'mx-2' }}">
@if (isset($icon))
@include ("forum::components.icons.{$icon}")
@endif
{{ $text }}
</span>
@@ -0,0 +1,101 @@
<div id="post-{{ $post->sequence }}" class="post-card my-4" x-data="postCard" data-post="{{ $post->id }}" {{ $selectable ? 'x-on:change=onPostChanged' : '' }}>
<div class="bg-white shadow-md rounded-lg flex flex-col sm:flex-row items-stretch dark:bg-slate-700 {{ $post->trashed() ? 'opacity-65' : '' }}" :class="classes">
@if ($showAuthorPane)
<div class="flex flex-row sm:flex-col w-full sm:w-1/5 px-6 py-4 sm:py-6 border-b sm:border-b-0 sm:border-r border-slate-200 dark:border-slate-600">
<div class="grow text-lg font-medium truncate">
{{ $post->authorName }}
</div>
<div>
@if (! isset($single) || ! $single)
<a href="{{ Forum::route('thread.show', $post) }}">#{{ $post->sequence }}</a>
@endif
</div>
</div>
@endif
<div class="grow p-6 w-full sm:w-4/5">
@if (isset($post->parent))
<livewire:forum::components.post.quote :post="$post->parent" />
@endif
<div class="dark:text-slate-100">
@if ($post->trashed())
@can ('viewTrashedPosts')
<div class="mb-4">
{!! Forum::render($post->content) !!}
</div>
@endcan
<div>
<livewire:forum::components.pill
bg-color="bg-zinc-400"
text-color="text-zinc-950"
margin="mr-2"
icon="trash-mini"
:text="trans('forum::general.deleted')" />
</div>
@else
{!! Forum::render($post->content) !!}
@endif
</div>
<div class="flex flex-col sm:flex-row mt-4">
<div class="grow text-slate-500">
<livewire:forum::components.timestamp :carbon="$post->created_at" />
@if ($post->hasBeenUpdated())
<span class="mx-1 text-slate-500"></span>
{{ trans('forum::general.last_updated') }} <livewire:forum::components.timestamp :carbon="$post->updated_at" />
@endif
</div>
@if (!isset($single) || !$single)
<div class="text-right sm:text-left mt-2 sm:mt-0">
@if (!$post->trashed())
<a href="{{ Forum::route('post.show', $post) }}" class="font-medium">
{{ trans('forum::general.permalink') }}
</a>
@can ('edit', $post)
<a href="{{ Forum::route('post.edit', $post) }}" class="font-medium ml-2">
{{ trans('forum::general.edit') }}
</a>
@endcan
@can ('reply', $post->thread)
<a href="{{ Forum::route('thread.reply', $post->thread) }}?parent_id={{ $post->id }}" class="font-medium ml-2">
{{ trans('forum::general.reply') }}
</a>
@endcan
@endif
@if ($selectable)
<div class="inline-block ml-4">
<x-forum::form.input-checkbox
id=""
:value="$post->id"
@change="onChanged" />
</div>
@endif
</div>
@endif
</div>
</div>
</div>
</div>
@script
<script>
Alpine.data('postCard', () => {
return {
classes: 'outline-none',
onChanged(event) {
event.stopPropagation();
if (event.target.checked) {
this.classes = 'outline outline-blue-500';
} else {
this.classes = 'outline-none';
}
$dispatch('change', { isSelected: event.target.checked, id: event.target.value });
}
}
});
</script>
@endscript
@@ -0,0 +1,16 @@
<div class="border border-slate-200 rounded-md p-6 mb-4 dark:border-slate-600">
{!! Forum::render($post->content) !!}
<div class="flex mt-4">
<div class="grow">
<span class="font-medium">
{{ $post->authorName }}
</span>
<span class="text-slate-500">
<livewire:forum::components.timestamp :carbon="$post->updated_at" />
</span>
</div>
<div>
<a href="{{ Forum::route('thread.show', $post) }}">#{{ $post->sequence }}</a>
</div>
</div>
</div>
@@ -0,0 +1,95 @@
<div class="thread-card my-4" x-data="threadCard" data-thread="{{ $thread->id }}" {{ $selectable ? 'x-on:change=onThreadChanged' : '' }} style="{{ $thread->category->styleVariables }}">
<div class="bg-white transition ease-in-out shadow-md rounded-lg p-4 flex flex-col sm:flex-row items-center justify-items-center dark:bg-slate-700 {{ $thread->trashed() ? 'opacity-75' : '' }}" :class="classes">
<div class="grow text-center sm:text-left">
<a href="{{ $thread->route }}" class="block text-xl mb-2">
@if ($thread->pinned)
<livewire:forum::components.pill
bg-color="bg-amber-400"
text-color="text-amber-950"
margin="mr-2"
icon="arrow-up-circle-mini"
:text="trans('forum::threads.pinned')" />
@endif
@if ($thread->locked)
<livewire:forum::components.pill
bg-color="bg-rose-400"
text-color="text-rose-950"
margin="mr-2"
icon="lock-closed-mini"
:text="trans('forum::threads.locked')" />
@endif
@if ($thread->userReadStatus !== null && ! $thread->trashed())
<livewire:forum::components.pill
bg-color="bg-teal-400"
text-color="text-teal-950"
margin="mr-2"
icon="sparkles-mini"
:text="trans($thread->userReadStatus)" />
@endif
@if ($thread->trashed())
<livewire:forum::components.pill
bg-color="bg-zinc-400"
text-color="text-zinc-950"
margin="mr-2"
icon="trash-mini"
:text="trans('forum::general.deleted')" />
@endif
<span class="inline-block align-middle {{ !$showCategory ? 'text-category' : '' }}">
{{ $thread->title }}
</span>
</a>
@if ($showCategory)
<a href="{{ $thread->category->route }}" class="font-medium text-category">
{{ $thread->category->title }}
</a>
<span class="mx-1 text-slate-500"></span>
@endif
{{ $thread->author->name }}
<span class="text-slate-500">
<livewire:forum::components.timestamp :carbon="$thread->created_at" />
</span>
</div>
<div class="text-center text-base mt-2 sm:mt-0">
<livewire:forum::components.pill
icon="chat-bubble-text-mini"
:text="trans('forum::general.replies') . ': ' . $thread->reply_count" />
</div>
<div class="min-w-30 sm:min-w-48 lg:min-w-96 xl:w-full xl:max-w-lg text-center sm:text-right mt-2 sm:mt-0">
<a href="{{ Forum::route('thread.show', $thread->lastPost) }}" class="text-lg font-medium">{{ trans('forum::posts.view') }} @include ("forum::components.icons.arrow-right-mini")</a>
<br>
{{ $thread->lastPost->authorName }}
<span class="text-slate-500">
<livewire:forum::components.timestamp :carbon="$thread->lastPost->created_at" />
</span>
</div>
@if ($selectable)
<div class="pl-4">
<x-forum::form.input-checkbox
id=""
:value="$thread->id"
@change="onChanged" />
</div>
@endif
</div>
</div>
@script
<script>
Alpine.data('threadCard', () => {
return {
classes: 'outline-none',
onChanged(event) {
event.stopPropagation();
if (event.target.checked) {
this.classes = 'outline outline-blue-500';
} else {
this.classes = 'outline-none';
}
$dispatch('change', { isSelected: event.target.checked, id: event.target.value });
}
}
});
</script>
@endscript
@@ -0,0 +1,20 @@
<time x-data="timestamp"
x-effect="setNaturalDiff(new Date('{{ $carbon->toIso8601String() }}'), $store.time.now)"
class="timestamp"
datetime="{{ $carbon }}"
title="{{ $carbon->toDayDateTimeString() }}">
<span x-text="naturalDiff"></span>
</time>
@script
<script type="module">
Alpine.data('timestamp', () => {
return {
naturalDiff: '',
setNaturalDiff(then, now) {
this.naturalDiff = dateFormatDistance(then, $store.time.now, { addSuffix: true });
}
}
});
</script>
@endscript
@@ -0,0 +1,137 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>
@if (isset($thread))
{{ $thread->title }}
@endif
@if (isset($category))
{{ $category->title }}
@endif
{{ trans('forum::general.home_title') }}
</title>
@vite([
'resources/forum/livewire-tailwind/css/forum.css',
'resources/forum/livewire-tailwind/js/forum.js'
])
</head>
<body class="forum bg-slate-200 dark:bg-slate-800">
<div x-data="navbar">
<div class="bg-white shadow-md border-b border-slate-100 dark:bg-slate-800 dark:border-slate-700 dark:shadow-none" @click.outside="closeMenu">
<div class="container mx-auto p-4">
<div class="flex flex-col md:flex-row flex-wrap items-center justify-between">
<div class="flex w-full md:w-auto items-center justify-between">
<div class="grow">
<a href="/" class="text-lg font-medium">
{{ config('app.name') }}
</a>
</div>
<button type="button" class="md:hidden inline-flex items-center p-2 w-10 h-10 justify-center text-sm text-slate-400 rounded-lg hover:bg-slate-100 dark:hover:bg-slate-600 focus:outline-none focus:ring-2 focus:ring-slate-200 dark:focus:ring-slate-600" aria-controls="navbar" aria-expanded="false" @click="toggleMenu">
<svg class="w-5 h-5" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 17 14">
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M1 1h15M1 7h15M1 13h15"/>
</svg>
</button>
</div>
<div class="w-full md:w-auto md:block font-medium flex flex-col items-center justify-between mt-4 md:mt-0 md:flex-row rtl:space-x-reverse text-center" :class="{ hidden: isMenuCollapsed }">
<span class="w-full md:w-auto">
<a href="{{ route('forum.category.index') }}" class="block hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent md:inline dark:md:inline">{{ trans('forum::general.home_title') }}</a>
</span>
<span class="w-full md:w-auto">
<a href="{{ route('forum.recent') }}" class="block hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent md:inline">{{ trans('forum::threads.recent') }}</a>
</span>
@auth
<span class="w-full md:w-auto">
<a href="{{ route('forum.unread') }}" class="block hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent md:inline">{{ trans('forum::threads.unread_updated') }}</a>
</span>
@endauth
@can ('moveCategories')
<span class="w-full md:w-auto">
<a href="{{ route('forum.category.order') }}" class="block hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent md:inline">{{ trans('forum::general.manage') }}</a>
</span>
@endcan
@if (Auth::check())
<span class="relative w-full md:w-auto" @click.outside="closeUserDropdown">
<a class="block flex flex-row items-center place-content-center gap-1 hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent w-full md:w-auto md:inline" href="#" id="navbarDropdownMenuLink" @click="toggleUserDropdown">
{{ $username }}
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="inline 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="absolute right-0 left-0 md:left-auto w-auto md:w-44 divide-y border border-slate-200 dark:border-slate-600 bg-white dark:bg-slate-700 rounded-md" :class="{ hidden: isUserDropdownCollapsed }" aria-labelledby="navbarDropdownMenuLink">
<a class="block px-4 py-2" href="#" @click.prevent="logout">
{{ __('Log out') }}
</a>
</div>
</span>
@else
<span class="w-full md:w-auto">
<a href="{{ url('/login') }}" class="block hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent md:inline">
{{ __('Log in') }}
</a>
</span>
<span class="w-full md:w-auto">
<a href="{{ url('/register') }}" class="block hover:bg-slate-100 dark:hover:bg-slate-600 rounded-md px-4 py-2 md:hover:bg-transparent md:dark:hover:bg-transparent md:inline">
{{ __('Register') }}
</a>
</span>
@endif
</div>
</div>
</div>
</div>
</div>
<div class="container mx-auto p-4">
{{ $slot }}
</div>
<livewire:forum::components.alerts />
<script type="module">
document.addEventListener('alpine:init', () => {
Alpine.store('time', {
now: new Date(),
init() {
setInterval(() => {
this.now = new Date();
}, 1000);
}
})
});
Alpine.data('navbar', () => {
return {
isMenuCollapsed: true,
isUserDropdownCollapsed: true,
toggleMenu() {
this.isMenuCollapsed = !this.isMenuCollapsed;
},
closeMenu() {
this.isMenuCollapsed = true;
},
toggleUserDropdown() {
this.isUserDropdownCollapsed = !this.isUserDropdownCollapsed;
},
closeUserDropdown() {
this.isUserDropdownCollapsed = true;
},
logout() {
const csrfToken = document.head.querySelector("[name=csrf-token]").content;
fetch('/logout', {
method: 'POST',
headers: {
'X-CSRF-TOKEN': csrfToken
}
}).then(() => { window.location.reload(); });
}
}
});
</script>
</body>
</html>
@@ -0,0 +1,60 @@
<div>
@include ('forum::components.loading-overlay')
@include ('forum::components.breadcrumbs')
<div class="flex justify-center items-center">
<div class="grow max-w-screen-md">
<h1>{{ trans('forum::categories.create') }}</h1>
<div class="bg-white rounded-md shadow-md my-2 p-6 dark:bg-slate-700">
<form wire:submit="create">
<x-forum::form.input-text
id="title"
:label="trans('forum::general.title')"
wire:model="title" />
<x-forum::form.input-text
id="description"
:label="trans('forum::general.description')"
wire:model="description" />
<x-forum::form.input-text
id="color"
:label="trans('forum::general.color_light_mode')"
wire:model="color_light_mode"
data-coloris />
<x-forum::form.input-text
id="color"
:label="trans('forum::general.color_dark_mode')"
wire:model="color_dark_mode"
data-coloris />
@if ($categories->count() > 0)
<x-forum::form.input-select
id="parent-category"
:label="trans('forum::categories.parent')"
wire:model="parent_category">
<option value="0">{{ trans('forum::general.none') }}</option>
@include ('forum::components.category.options', ['categories' => $categories])
</x-forum::form.input-select>
@endif
<x-forum::form.input-checkbox
id="accepts-threads"
:label="trans('forum::categories.enable_threads')"
wire:model="accepts_threads" />
<x-forum::form.input-checkbox
id="make-private"
:label="trans('forum::categories.make_private')"
wire:model="is_private" />
<div class="text-center mt-4">
<x-forum::button :label="trans('forum::general.create')" type="submit" />
</div>
</form>
</div>
</div>
</div>
</div>
@@ -0,0 +1,107 @@
<div x-data="editCategory">
@include ('forum::components.loading-overlay')
@include ('forum::components.breadcrumbs')
<div class="flex justify-center items-center">
<div class="grow max-w-screen-md">
<h1 class="mb-2">{{ trans('forum::categories.edit') }}</h1>
<h2 class="mb-4 text-slate-500">{{ $category->title }}</h2>
<div class="bg-white rounded-md shadow-md my-2 p-6 dark:bg-slate-700">
<form wire:submit="save">
<x-forum::form.input-text
id="title"
:label="trans('forum::general.title')"
wire:model="title" />
<x-forum::form.input-text
id="description"
:label="trans('forum::general.description')"
wire:model="description" />
<x-forum::form.input-text
id="color"
:label="trans('forum::general.color_light_mode')"
wire:model="color_light_mode"
data-coloris />
<x-forum::form.input-text
id="color"
:label="trans('forum::general.color_dark_mode')"
wire:model="color_dark_mode"
data-coloris />
@if ($categories->count() > 0)
<x-forum::form.input-select
id="parent-category"
:label="trans('forum::categories.parent')"
wire:model="parent_category">
<option value="0">{{ trans('forum::general.none') }}</option>
@include ('forum::components.category.options', ['categories' => $categories])
</x-forum::form.input-select>
@endif
<x-forum::form.input-checkbox
id="accepts-threads"
:label="trans('forum::categories.enable_threads')"
wire:model="accepts_threads" />
<x-forum::form.input-checkbox
id="make-private"
:label="trans('forum::categories.make_private')"
wire:model="is_private" />
<div class="flex mt-4">
<div class="grow">
<x-forum::button
intent="danger"
:label="trans('forum::general.delete')"
@click.prevent="requestDelete" />
</div>
<div>
<x-forum::button :label="trans('forum::general.save')" type="submit" />
</div>
</div>
</form>
</div>
</div>
</div>
<x-forum::modal
:heading="trans('forum::general.generic_confirm')"
x-show="showDeleteModal"
onClose="showDeleteModal = false">
{{ trans('forum::categories.confirm_nonempty_delete') }}
<div class="flex flex-wrap mt-6">
<div class="grow">
<x-forum::button
intent="secondary"
:label="trans('forum::general.cancel')"
@click="showDeleteModal = false" />
</div>
<div>
<x-forum::button
intent="primary"
:label="trans('forum::general.proceed')"
@click="confirmDelete" />
</div>
</div>
</x-forum::modal>
</div>
@script
<script>
Alpine.data('editCategory', () => {
return {
showDeleteModal: false,
requestDelete(event) {
this.showDeleteModal = true;
},
confirmDelete() {
$wire.delete();
}
}
});
</script>
@endscript
@@ -0,0 +1,23 @@
<div>
@include ('forum::components.loading-overlay')
@include ('forum::components.breadcrumbs')
<h1>{{ trans('forum::general.index') }}</h1>
<div class="flex">
<div class="grow">
</div>
<div>
@can ('createCategories')
<x-forum::link-button
:label="trans('forum::categories.create')"
icon="squares-plus-outline"
:href="Forum::route('category.create')" />
@endcan
</div>
</div>
@foreach ($categories as $category)
<livewire:forum::components.category.card :$category :key="$category->id" />
@endforeach
</div>
@@ -0,0 +1,83 @@
<div x-data="manage">
@include ('forum::components.loading-overlay')
@include ('forum::components.breadcrumbs')
<div class="flex justify-center items-center">
<div class="grow max-w-screen-lg">
<h1>{{ trans('forum::general.manage') }}</h1>
@can ('createCategories')
<div class="mb-6 text-right">
<x-forum::link-button
:label="trans('forum::categories.create')"
icon="squares-plus-outline"
:href="Forum::route('category.create')" />
</div>
@endcan
<div class="bg-white dark:bg-slate-700 rounded-md shadow-md my-2 p-6">
<ol id="category-tree">
@include ('forum::components.category.draggable-items', ['categories' => $categories])
</ol>
<div class="mt-4 text-right">
<x-forum::button
id="save"
:label="trans('forum::general.save')"
x-ref="button"
@click="save"
disabled />
</div>
</div>
</div>
</div>
</div>
@script
<script>
Alpine.data('manage', () => {
return {
nestedSort: null,
init () {
this.initialiseNestedSort();
},
initialiseNestedSort() {
this.nestedSort = new NestedSort({
propertyMap: {
id: 'id',
parent: 'parent_id',
text: 'title',
},
actions: {
onDrop: () => $refs.button.disabled = false
},
el: '#category-tree',
listItemClassNames: 'border border-slate-300 rounded-md text-lg p-4 my-2'
});
},
getItems (ol) {
let tree = [];
for (let i = 0; i < ol.children.length; ++i) {
let item = { id: ol.children[i].dataset.id, children: [] };
for (let j = 0; j < ol.children[i].children.length; ++j) {
if (ol.children[i].children[j].tagName == 'OL') {
item.children = this.getItems(ol.children[i].children[j]);
}
}
tree.push(item);
}
return tree;
},
async save () {
let tree = this.getItems(document.getElementById('category-tree'));
$wire.tree = tree;
const result = await $wire.save();
$dispatch('alert', result);
this.initialiseNestedSort();
}
}
});
</script>
@endscript
@@ -0,0 +1,197 @@
<div x-data="category" x-on:page-changed="onPageChanged" style="{{ $category->styleVariables }}">
@include ('forum::components.loading-overlay')
@include ('forum::components.breadcrumbs')
<h1 class="mb-0 text-category">{{ $category->title }}</h1>
<h2 class="mt-0 text-slate-500">{{ $category->description }}</h2>
<div class="flex mt-6 mb-8">
<div class="grow">
@can ('edit', $category)
<x-forum::link-button
intent="secondary"
:href="Forum::route('category.edit', $category)"
:label="trans('forum::categories.edit')" />
@endcan
</div>
@can ('createCategories')
<div>
<x-forum::link-button
:label="trans('forum::categories.create')"
icon="squares-plus-outline"
:href="Forum::route('category.create') . '?parent_id=' . $category->id" />
</div>
@endcan
</div>
@foreach ($category->descendants as $child)
<livewire:forum::components.category.card :category="$child" :key="$child->id" />
@endforeach
<div class="flex mt-6 mb-8">
<div class="grow">
</div>
@if ($category->accepts_threads)
@can ('createThreads', $category)
<div>
<x-forum::link-button
:href="Forum::route('thread.create', $category)"
icon="pencil-outline"
:label="trans('forum::threads.new_thread')" />
</div>
@endcan
@endif
</div>
@if (count($selectableThreadIds) > 0)
<div class="flex justify-end">
<x-forum::form.input-checkbox
id="toggle-all"
value=""
:label="trans('forum::threads.select_all')"
x-model="toggledAllThreads"
@click="toggleAllThreads" />
</div>
@endif
<div class="my-4">
@foreach ($threads as $thread)
<livewire:forum::components.thread.card
:$thread
:key="$thread->id . $updateKey"
:selectable="in_array($thread->id, $selectableThreadIds)" />
@endforeach
@if ($category->accepts_threads && $threads->count() == 0)
<div class="p-6 border border-slate-300 dark:border-slate-700 rounded-md text-center text-slate-500 text-lg font-medium">
{{ trans('forum::threads.none_found') }}
</div>
@endif
</div>
<div x-show="selectedThreads.length > 0" class="fixed bottom-0 right-0 z-40 min-w-96 bg-white shadow-md rounded-md m-4 p-6 dark:bg-slate-700">
<h3>{{ trans('forum::general.with_selection') }}</h3>
<x-forum::form.input-select
id="selected-action"
x-model="selectedAction">
<option value="none" disabled>{{ trans_choice('forum::general.actions', 1) }}...</option>
@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::form.input-select>
@if (config('forum.general.soft_deletes'))
<x-forum::form.input-checkbox
id="permadelete"
value=""
:label="trans('forum::general.perma_delete')"
x-show="selectedAction == 'delete'"
x-model="permadelete" />
@endif
<x-forum::form.input-select
id="destination-category"
:label="trans_choice('forum::categories.category', 1)"
x-show="selectedAction == 'move'"
x-model="destinationCategory">
<option value="0" disabled>...</option>
@include ('forum::components.category.options', ['categories' => $threadDestinationCategories, 'disable' => $category->id])
</x-forum::form.input-select>
<x-forum::button
:label="trans('forum::general.proceed')"
@click="applySelectedAction"
x-bind:disabled="selectedAction == 'none' || selectedAction == 'move' && destinationCategory == 0" />
</div>
{{ $threads->links('forum::components.pagination') }}
</div>
@script
<script>
Alpine.data('category', () => {
return {
toggledAllThreads: false,
selectedThreads: [],
selectedAction: 'none',
permadelete: false,
destinationCategory: 0,
confirmMessage: "{{ trans('forum::general.generic_confirm') }}",
reset() {
this.toggledAllThreads = false;
this.selectedThreads = [];
this.permadelete = false;
this.destinationCategory = 0;
},
onThreadChanged(event) {
if (event.detail.isSelected) {
this.selectedThreads.push(event.detail.id);
} else {
this.selectedThreads.splice(this.selectedThreads.indexOf(event.detail.id), 1);
}
},
onPageChanged(event) {
this.reset();
},
async applySelectedAction() {
if (this.selectedAction == null || this.selectedThreads.length == 0) {
return;
}
let result;
switch (this.selectedAction) {
case 'delete':
if (!confirm(this.confirmMessage)) return;
result = await $wire.deleteThreads(this.selectedThreads, this.permadelete);
break;
case 'restore':
result = await $wire.restoreThreads(this.selectedThreads);
break;
case 'move':
result = await $wire.moveThreads(this.selectedThreads, this.destinationCategory);
break;
case 'lock':
result = await $wire.lockThreads(this.selectedThreads);
break;
case 'unlock':
result = await $wire.unlockThreads(this.selectedThreads);
break;
case 'pin':
result = await $wire.pinThreads(this.selectedThreads);
break;
case 'unpin':
result = await $wire.unpinThreads(this.selectedThreads);
break;
}
if (result.type == 'success') this.reset();
$dispatch('alert', result);
},
toggleAllThreads(event) {
this.toggledAllThreads = !this.toggledAllThreads;
if (!this.toggledAllThreads) this.selectedThreads = [];
const checkboxes = document.querySelectorAll('[data-thread] input[type=checkbox]');
checkboxes.forEach(checkbox => {
checkbox.checked = this.toggledAllThreads;
checkbox.dispatchEvent(new Event('change'));
});
}
}
});
</script>
@endscript

Some files were not shown because too many files have changed in this diff Show More