- Updated package.json to include new dependencies: `@radix-ui/react-dialog` and `cmdk`. - Enhanced global styles in globals.css for improved visual depth in dark mode. - Refactored dashboard page components to improve layout and accessibility, including new icons and responsive design adjustments. - Implemented suspense handling in commands page for better loading states. - Improved sidebar navigation with new icons and mobile responsiveness. - Added FieldAnchor components across various forms for better accessibility and structure. - Enhanced commands manager with search parameter handling for improved user experience. - Updated multiple module forms to include FieldAnchor for consistent layout and accessibility.
177 lines
5.6 KiB
TypeScript
177 lines
5.6 KiB
TypeScript
'use client';
|
|
|
|
import { Search, Terminal, Settings2, LayoutGrid } from 'lucide-react';
|
|
import { useRouter } from 'next/navigation';
|
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
|
import { useTranslations } from '@/components/locale-provider';
|
|
import {
|
|
CommandDialog,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList
|
|
} from '@/components/ui/command';
|
|
import {
|
|
OWNER_NAV_ENTRIES,
|
|
buildGuildSearchHref,
|
|
buildGuildSearchIndex,
|
|
type SearchIndexEntry,
|
|
type SearchItemKind
|
|
} from '@/lib/dashboard-search-index';
|
|
|
|
function kindIcon(kind: SearchItemKind) {
|
|
switch (kind) {
|
|
case 'command':
|
|
return Terminal;
|
|
case 'setting':
|
|
return Settings2;
|
|
default:
|
|
return LayoutGrid;
|
|
}
|
|
}
|
|
|
|
interface DashboardCommandPaletteProps {
|
|
guildId?: string;
|
|
mode?: 'guild' | 'owner';
|
|
}
|
|
|
|
export function DashboardCommandPalette({ guildId, mode = 'guild' }: DashboardCommandPaletteProps) {
|
|
const t = useTranslations();
|
|
const router = useRouter();
|
|
const [open, setOpen] = useState(false);
|
|
|
|
const entries = useMemo(() => {
|
|
if (mode === 'owner') {
|
|
return OWNER_NAV_ENTRIES;
|
|
}
|
|
return buildGuildSearchIndex();
|
|
}, [mode]);
|
|
|
|
useEffect(() => {
|
|
function onKeyDown(event: KeyboardEvent) {
|
|
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') {
|
|
event.preventDefault();
|
|
setOpen((prev) => !prev);
|
|
}
|
|
}
|
|
window.addEventListener('keydown', onKeyDown);
|
|
return () => window.removeEventListener('keydown', onKeyDown);
|
|
}, []);
|
|
|
|
const resolveLabel = useCallback(
|
|
(entry: SearchIndexEntry) => {
|
|
if (entry.labelOverride) {
|
|
return entry.labelOverride;
|
|
}
|
|
if (entry.labelKey) {
|
|
return t(entry.labelKey);
|
|
}
|
|
return entry.id;
|
|
},
|
|
[t]
|
|
);
|
|
|
|
function navigateTo(entry: SearchIndexEntry) {
|
|
setOpen(false);
|
|
if (mode === 'owner') {
|
|
router.push(entry.href);
|
|
return;
|
|
}
|
|
if (!guildId) {
|
|
return;
|
|
}
|
|
const href = buildGuildSearchHref(guildId, entry);
|
|
router.push(href);
|
|
if (entry.hash) {
|
|
window.setTimeout(() => {
|
|
document.getElementById(entry.hash!)?.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}, 250);
|
|
}
|
|
}
|
|
|
|
const pages = entries.filter((e) => e.kind === 'page');
|
|
const commands = entries.filter((e) => e.kind === 'command');
|
|
const settings = entries.filter((e) => e.kind === 'setting');
|
|
|
|
const isMac =
|
|
typeof navigator !== 'undefined' && /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
|
|
|
|
return (
|
|
<>
|
|
<button
|
|
type="button"
|
|
onClick={() => setOpen(true)}
|
|
className="inline-flex h-9 w-full max-w-xs items-center gap-2 rounded-md border border-input bg-background/60 px-3 text-sm text-muted-foreground shadow-sm transition-colors hover:bg-accent/50 hover:text-foreground"
|
|
>
|
|
<Search className="size-4 shrink-0" />
|
|
<span className="flex-1 truncate text-left">{t('search.placeholder')}</span>
|
|
<kbd className="pointer-events-none hidden h-5 select-none items-center gap-1 rounded border border-border bg-muted px-1.5 font-mono text-[10px] font-medium text-muted-foreground sm:inline-flex">
|
|
{isMac ? '⌘' : 'Ctrl'}K
|
|
</kbd>
|
|
</button>
|
|
|
|
<CommandDialog open={open} onOpenChange={setOpen} title={t('search.title')}>
|
|
<CommandInput placeholder={t('search.placeholder')} />
|
|
<CommandList>
|
|
<CommandEmpty>{t('search.empty')}</CommandEmpty>
|
|
{pages.length > 0 ? (
|
|
<CommandGroup heading={t('search.groups.pages')}>
|
|
{pages.map((entry) => {
|
|
const Icon = kindIcon(entry.kind);
|
|
const label = resolveLabel(entry);
|
|
return (
|
|
<CommandItem
|
|
key={entry.id}
|
|
value={`${label} ${entry.keywords?.join(' ') ?? ''} ${entry.labelKey ?? ''}`}
|
|
onSelect={() => navigateTo(entry)}
|
|
>
|
|
<Icon className="size-4 text-muted-foreground" />
|
|
<span>{label}</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
) : null}
|
|
{commands.length > 0 ? (
|
|
<CommandGroup heading={t('search.groups.commands')}>
|
|
{commands.map((entry) => {
|
|
const Icon = kindIcon(entry.kind);
|
|
const label = resolveLabel(entry);
|
|
return (
|
|
<CommandItem
|
|
key={entry.id}
|
|
value={`${label} ${entry.keywords?.join(' ') ?? ''}`}
|
|
onSelect={() => navigateTo(entry)}
|
|
>
|
|
<Icon className="size-4 text-muted-foreground" />
|
|
<span>{label}</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
) : null}
|
|
{settings.length > 0 ? (
|
|
<CommandGroup heading={t('search.groups.settings')}>
|
|
{settings.map((entry) => {
|
|
const Icon = kindIcon(entry.kind);
|
|
const label = resolveLabel(entry);
|
|
return (
|
|
<CommandItem
|
|
key={entry.id}
|
|
value={`${label} ${entry.keywords?.join(' ') ?? ''} ${entry.labelKey ?? ''}`}
|
|
onSelect={() => navigateTo(entry)}
|
|
>
|
|
<Icon className="size-4 text-muted-foreground" />
|
|
<span>{label}</span>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
) : null}
|
|
</CommandList>
|
|
</CommandDialog>
|
|
</>
|
|
);
|
|
}
|