refactor: inline single-use components, remove dead code
- Inline TopBar into root layout (+layout.svelte) — only used there - Inline Center into home page (+page.svelte) — only used there - Remove ComboBox.svelte — dead code, no imports - Remove TipTap.svelte — dead code, no imports - Remove empty container/ui/admin/tiptap directories - Keep Window.svelte in components/containers/ — used by 4 pages
This commit is contained in:
@@ -1,160 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
type Option = { label: string; value: string | number };
|
|
||||||
|
|
||||||
type Props = {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
options: Option[];
|
|
||||||
placeholder?: string;
|
|
||||||
value?: string | number;
|
|
||||||
};
|
|
||||||
|
|
||||||
let { id, name, options, placeholder = 'Search...', value }: Props = $props();
|
|
||||||
|
|
||||||
let search = $state('');
|
|
||||||
let open = $state(false);
|
|
||||||
let highlightIndex = $state(-1);
|
|
||||||
let inputEl = $state<HTMLInputElement | null>(null);
|
|
||||||
let listEl = $state<HTMLUListElement | null>(null);
|
|
||||||
|
|
||||||
let selectedValue = $state<string | number | undefined>(undefined);
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
if (value != null && value !== '') {
|
|
||||||
selectedValue = value;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let selectedLabel = $derived(() => {
|
|
||||||
if (selectedValue == null || selectedValue === '') return '';
|
|
||||||
const match = options.find((o) => String(o.value) === String(selectedValue));
|
|
||||||
return match?.label ?? '';
|
|
||||||
});
|
|
||||||
|
|
||||||
let filtered = $derived(() => {
|
|
||||||
if (!search) return options;
|
|
||||||
const q = search.toLowerCase();
|
|
||||||
return options.filter((o) => o.label.toLowerCase().includes(q));
|
|
||||||
});
|
|
||||||
|
|
||||||
function select(opt: Option) {
|
|
||||||
selectedValue = opt.value;
|
|
||||||
search = '';
|
|
||||||
open = false;
|
|
||||||
highlightIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function clear() {
|
|
||||||
selectedValue = undefined;
|
|
||||||
search = '';
|
|
||||||
open = true;
|
|
||||||
inputEl?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInputFocus() {
|
|
||||||
open = true;
|
|
||||||
highlightIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleInputBlur(e: FocusEvent) {
|
|
||||||
const related = e.relatedTarget as HTMLElement | null;
|
|
||||||
if (related?.closest(`[data-combobox="${id}"]`)) return;
|
|
||||||
open = false;
|
|
||||||
highlightIndex = -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleKeydown(e: KeyboardEvent) {
|
|
||||||
const items = filtered();
|
|
||||||
if (e.key === 'ArrowDown') {
|
|
||||||
e.preventDefault();
|
|
||||||
open = true;
|
|
||||||
highlightIndex = Math.min(highlightIndex + 1, items.length - 1);
|
|
||||||
scrollToHighlighted();
|
|
||||||
} else if (e.key === 'ArrowUp') {
|
|
||||||
e.preventDefault();
|
|
||||||
highlightIndex = Math.max(highlightIndex - 1, 0);
|
|
||||||
scrollToHighlighted();
|
|
||||||
} else if (e.key === 'Enter') {
|
|
||||||
e.preventDefault();
|
|
||||||
if (open && highlightIndex >= 0 && highlightIndex < items.length) {
|
|
||||||
select(items[highlightIndex]);
|
|
||||||
}
|
|
||||||
} else if (e.key === 'Escape') {
|
|
||||||
open = false;
|
|
||||||
search = '';
|
|
||||||
highlightIndex = -1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function scrollToHighlighted() {
|
|
||||||
requestAnimationFrame(() => {
|
|
||||||
const el = listEl?.children[highlightIndex] as HTMLElement | undefined;
|
|
||||||
el?.scrollIntoView({ block: 'nearest' });
|
|
||||||
});
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="relative" data-combobox={id}>
|
|
||||||
<input type="hidden" {name} value={selectedValue ?? ''} />
|
|
||||||
<input type="hidden" name={name + 'Label'} value={selectedValue != null && selectedValue !== '' ? selectedLabel() : search} />
|
|
||||||
|
|
||||||
{#if selectedValue != null && selectedValue !== ''}
|
|
||||||
<div
|
|
||||||
class="flex w-full items-center justify-between rounded-lg border border-gray-300 bg-white px-3 py-2 transition-shadow outline-none focus-within:border-blue-500 focus-within:ring-2 focus-within:ring-blue-500"
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="flex-1 cursor-pointer text-left text-gray-900"
|
|
||||||
onclick={() => clear()}
|
|
||||||
>
|
|
||||||
{selectedLabel()}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="ml-2 shrink-0 text-gray-400 hover:text-gray-600"
|
|
||||||
onclick={() => clear()}
|
|
||||||
tabindex={-1}
|
|
||||||
>
|
|
||||||
✕
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
{id}
|
|
||||||
bind:this={inputEl}
|
|
||||||
bind:value={search}
|
|
||||||
{placeholder}
|
|
||||||
autocomplete="off"
|
|
||||||
class="w-full rounded-lg border border-gray-300 bg-white px-3 py-2 transition-shadow outline-none focus:border-blue-500 focus:ring-2 focus:ring-blue-500"
|
|
||||||
onfocus={handleInputFocus}
|
|
||||||
onblur={handleInputBlur}
|
|
||||||
onkeydown={handleKeydown}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if open}
|
|
||||||
<ul
|
|
||||||
bind:this={listEl}
|
|
||||||
class="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-lg border border-gray-200 bg-white py-1 shadow-lg"
|
|
||||||
role="listbox"
|
|
||||||
>
|
|
||||||
{#each filtered() as opt, i}
|
|
||||||
<li role="option" aria-selected={highlightIndex === i}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="w-full px-3 py-2 text-left text-sm {highlightIndex === i
|
|
||||||
? 'bg-blue-50 text-blue-700'
|
|
||||||
: 'text-gray-700 hover:bg-gray-50'}"
|
|
||||||
onmousedown={(e: MouseEvent) => { e.preventDefault(); select(opt); }}
|
|
||||||
onmouseenter={() => (highlightIndex = i)}
|
|
||||||
>
|
|
||||||
{opt.label}
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
{:else}
|
|
||||||
<li class="px-3 py-2 text-sm text-gray-400">No matches</li>
|
|
||||||
{/each}
|
|
||||||
</ul>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
let { children } = $props();
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="flex flex-1 flex-col items-center justify-center">
|
|
||||||
{@render children()}
|
|
||||||
</div>
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
import { Editor } from '@tiptap/core';
|
|
||||||
import StarterKit from '@tiptap/starter-kit';
|
|
||||||
|
|
||||||
let { initialContent = '', name = 'content' } = $props<{ initialContent: string; name?: string }>();
|
|
||||||
|
|
||||||
let element: HTMLElement;
|
|
||||||
let editor: Editor;
|
|
||||||
let content = $state('');
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
content = initialContent;
|
|
||||||
|
|
||||||
editor = new Editor({
|
|
||||||
element,
|
|
||||||
extensions: [StarterKit],
|
|
||||||
content: initialContent,
|
|
||||||
editorProps: {
|
|
||||||
attributes: {
|
|
||||||
class: 'outline-none h-full'
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onUpdate: ({ editor }) => {
|
|
||||||
content = editor.getHTML();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return () => editor.destroy();
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div class="border border-gray-300 rounded-lg overflow-hidden">
|
|
||||||
<div class="flex gap-1 p-2 bg-gray-100 border-b border-gray-300">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => editor.chain().focus().toggleBold().run()}
|
|
||||||
class="px-3 py-1 rounded font-bold hover:bg-gray-200 transition-colors"
|
|
||||||
>B</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => editor.chain().focus().toggleItalic().run()}
|
|
||||||
class="px-3 py-1 rounded italic hover:bg-gray-200 transition-colors"
|
|
||||||
>I</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => editor.chain().focus().toggleStrike().run()}
|
|
||||||
class="px-3 py-1 rounded line-through hover:bg-gray-200 transition-colors"
|
|
||||||
>S</button>
|
|
||||||
<div class="w-px bg-gray-300 mx-1"></div>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => editor.chain().focus().toggleBulletList().run()}
|
|
||||||
class="px-3 py-1 rounded hover:bg-gray-200 transition-colors"
|
|
||||||
>• List</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onclick={() => editor.chain().focus().toggleOrderedList().run()}
|
|
||||||
class="px-3 py-1 rounded hover:bg-gray-200 transition-colors"
|
|
||||||
>1. List</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
bind:this={element}
|
|
||||||
role="textbox"
|
|
||||||
tabindex="-1"
|
|
||||||
class="p-3 min-h-40 prose prose-sm max-w-none cursor-text [&_.tiptap]:outline-none [&_.tiptap]:h-full [&_.tiptap_p]:my-1 [&_.tiptap_p:first-child]:mt-0 [&_.tiptap_p:last-child]:mb-0"
|
|
||||||
onmousedown={() => editor?.commands.focus()}
|
|
||||||
></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<input type="hidden" {name} value={content} />
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
<script lang="ts">
|
|
||||||
import { authClient } from '$lib/auth-client';
|
|
||||||
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
|
||||||
import { onMount } from 'svelte';
|
|
||||||
|
|
||||||
let { userRoleId = null }: { userRoleId: number | null } = $props();
|
|
||||||
const session = authClient.useSession();
|
|
||||||
let isAdmin = $derived(canAccessAdmin(userRoleId));
|
|
||||||
|
|
||||||
let dark = $state(false);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
dark = document.documentElement.classList.contains('dark');
|
|
||||||
});
|
|
||||||
|
|
||||||
function toggleTheme() {
|
|
||||||
dark = !dark;
|
|
||||||
if (dark) {
|
|
||||||
document.documentElement.classList.add('dark');
|
|
||||||
localStorage.setItem('theme', 'dark');
|
|
||||||
} else {
|
|
||||||
document.documentElement.classList.remove('dark');
|
|
||||||
localStorage.setItem('theme', 'light');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<nav class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100">
|
|
||||||
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
|
|
||||||
<div class="flex items-center gap-4">
|
|
||||||
<button onclick={toggleTheme} class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300">
|
|
||||||
{dark ? 'Light' : 'Dark'}
|
|
||||||
</button>
|
|
||||||
{#if $session.data}
|
|
||||||
{#if isAdmin}
|
|
||||||
<a href="/admin" class="dark:text-gray-300 dark:hover:text-gray-100">Admin Panel</a>
|
|
||||||
{/if}
|
|
||||||
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
|
|
||||||
<button
|
|
||||||
class="dark:text-gray-300 dark:hover:text-gray-100"
|
|
||||||
onclick={async () => {
|
|
||||||
await authClient.signOut();
|
|
||||||
window.location.href = '/';
|
|
||||||
}}>Log Out</button
|
|
||||||
>
|
|
||||||
{:else}
|
|
||||||
<span class="dark:text-gray-300"><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> | <a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
</nav>
|
|
||||||
@@ -1,9 +1,31 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { authClient } from '$lib/auth-client';
|
||||||
|
import { canAccessAdmin } from '$lib/svelte-admin/roles';
|
||||||
import './layout.css';
|
import './layout.css';
|
||||||
import favicon from '$lib/assets/favicon.svg';
|
import favicon from '$lib/assets/favicon.svg';
|
||||||
import TopBar from '$lib/components/ui/topBar/TopBar.svelte';
|
|
||||||
|
|
||||||
let { data, children } = $props();
|
let { data, children } = $props();
|
||||||
|
|
||||||
|
const session = authClient.useSession();
|
||||||
|
let isAdmin = $derived(canAccessAdmin(data.userRoleId));
|
||||||
|
|
||||||
|
let dark = $state(false);
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
dark = document.documentElement.classList.contains('dark');
|
||||||
|
});
|
||||||
|
|
||||||
|
function toggleTheme() {
|
||||||
|
dark = !dark;
|
||||||
|
if (dark) {
|
||||||
|
document.documentElement.classList.add('dark');
|
||||||
|
localStorage.setItem('theme', 'dark');
|
||||||
|
} else {
|
||||||
|
document.documentElement.classList.remove('dark');
|
||||||
|
localStorage.setItem('theme', 'light');
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:head>
|
<svelte:head>
|
||||||
@@ -18,7 +40,29 @@
|
|||||||
<meta name="twitter:card" content="summary" />
|
<meta name="twitter:card" content="summary" />
|
||||||
</svelte:head>
|
</svelte:head>
|
||||||
|
|
||||||
<TopBar userRoleId={data.userRoleId} />
|
<nav class="flex w-full items-center justify-between border-b border-gray-200 px-8 py-6 dark:border-gray-700 dark:bg-gray-950 dark:text-gray-100">
|
||||||
|
<span><a href="/" class="dark:text-gray-100">Rhema Wave Technologies</a></span>
|
||||||
|
<div class="flex items-center gap-4">
|
||||||
|
<button onclick={toggleTheme} class="border border-gray-300 px-3 py-1 text-sm dark:border-gray-600 dark:text-gray-300">
|
||||||
|
{dark ? 'Light' : 'Dark'}
|
||||||
|
</button>
|
||||||
|
{#if $session.data}
|
||||||
|
{#if isAdmin}
|
||||||
|
<a href="/admin" class="dark:text-gray-300 dark:hover:text-gray-100">Admin Panel</a>
|
||||||
|
{/if}
|
||||||
|
<a href="/dashboard" class="dark:text-gray-300 dark:hover:text-gray-100">Dashboard</a>
|
||||||
|
<button
|
||||||
|
class="dark:text-gray-300 dark:hover:text-gray-100"
|
||||||
|
onclick={async () => {
|
||||||
|
await authClient.signOut();
|
||||||
|
window.location.href = '/';
|
||||||
|
}}>Log Out</button
|
||||||
|
>
|
||||||
|
{:else}
|
||||||
|
<span class="dark:text-gray-300"><a href="/login" class="dark:text-gray-300 dark:hover:text-gray-100">Log In</a> | <a href="/register" class="dark:text-gray-300 dark:hover:text-gray-100">Register</a></span>
|
||||||
|
{/if}
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
|
<div class="flex min-h-0 flex-1 px-8 dark:bg-gray-950">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
<script lang="ts">
|
<div class="flex flex-1 flex-col items-center justify-center">
|
||||||
import Center from '$lib/components/containers/Center.svelte';
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<Center>
|
|
||||||
<h1 class="dark:text-gray-100">Hello</h1>
|
<h1 class="dark:text-gray-100">Hello</h1>
|
||||||
</Center>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user