260 lines
12 KiB
TypeScript
260 lines
12 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import PageLayout from '../../components/PageLayout'
|
|
import {
|
|
DASHBOARD_PLATFORMS_COLOR_OPTIONS,
|
|
type DashboardPlatform,
|
|
type DashboardPlatformColorClass
|
|
} from '../../utils/dashboardPlatforms'
|
|
import { PlusIcon, TrashIcon, CheckIcon, ChevronDownIcon, ChevronUpIcon } from '@heroicons/react/24/outline'
|
|
import { useAdminDashboardPlatforms, type PlatformRow } from './hooks/useAdminDashboardPlatforms'
|
|
|
|
export default function AdminDashboardManagementPage() {
|
|
const {
|
|
platforms,
|
|
loading,
|
|
saving,
|
|
error,
|
|
savedAt,
|
|
hasValidationErrors,
|
|
addPlatform,
|
|
updatePlatform,
|
|
removeNewPlatform,
|
|
setPlatformState,
|
|
save,
|
|
isValidHref,
|
|
} = useAdminDashboardPlatforms()
|
|
|
|
const [openById, setOpenById] = useState<Record<string, boolean>>({})
|
|
|
|
const toggleOpen = (id: string) => {
|
|
setOpenById(prev => ({ ...prev, [id]: !prev[id] }))
|
|
}
|
|
|
|
const addAndOpen = () => {
|
|
const id = addPlatform()
|
|
setOpenById(prev => ({ ...prev, [id]: true }))
|
|
}
|
|
|
|
const isOpen = (p: PlatformRow) => Boolean(openById[p.id] ?? p._isNew)
|
|
|
|
return (
|
|
<PageLayout>
|
|
<div className="bg-gradient-to-tr from-blue-50 via-white to-blue-100 min-h-screen">
|
|
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6 md:py-10">
|
|
<div className="rounded-3xl bg-white/70 backdrop-blur-md border border-white/60 shadow-lg p-6 sm:p-8">
|
|
<header className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-4 mb-6">
|
|
<div>
|
|
<h1 className="text-3xl sm:text-4xl font-extrabold text-blue-900 tracking-tight">Dashboard Management</h1>
|
|
<p className="text-sm sm:text-base text-blue-700 mt-2">
|
|
Manage the “Platforms” cards shown on the user dashboard.
|
|
</p>
|
|
</div>
|
|
|
|
<div className="flex flex-col sm:flex-row gap-2 sm:items-center">
|
|
<button
|
|
type="button"
|
|
onClick={addAndOpen}
|
|
disabled={loading || saving}
|
|
className="inline-flex items-center justify-center gap-2 rounded-lg bg-blue-900 text-white px-4 py-2 text-sm font-semibold hover:bg-blue-800"
|
|
>
|
|
<PlusIcon className="h-5 w-5" />
|
|
Add Platform
|
|
</button>
|
|
<button
|
|
type="button"
|
|
onClick={save}
|
|
disabled={hasValidationErrors || loading || saving}
|
|
className={
|
|
hasValidationErrors || loading || saving
|
|
? 'inline-flex items-center justify-center gap-2 rounded-lg bg-gray-300 text-gray-600 px-4 py-2 text-sm font-semibold cursor-not-allowed'
|
|
: 'inline-flex items-center justify-center gap-2 rounded-lg bg-emerald-600 text-white px-4 py-2 text-sm font-semibold hover:bg-emerald-500'
|
|
}
|
|
>
|
|
<CheckIcon className="h-5 w-5" />
|
|
{saving ? 'Saving…' : 'Save'}
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
{error && (
|
|
<div className="mb-6 rounded-xl border border-red-200 bg-red-50 px-4 py-3 text-sm text-red-800">
|
|
{error}
|
|
</div>
|
|
)}
|
|
|
|
{savedAt && (
|
|
<div className="mb-6 rounded-xl border border-emerald-200 bg-emerald-50 px-4 py-3 text-sm text-emerald-800">
|
|
Saved at {new Date(savedAt).toLocaleTimeString('de-DE')}
|
|
</div>
|
|
)}
|
|
|
|
{hasValidationErrors && (
|
|
<div className="mb-6 rounded-xl border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
|
Please ensure every platform has a title and a valid link (must start with “/” or “http(s)://”).
|
|
</div>
|
|
)}
|
|
|
|
<div className="grid grid-cols-1 gap-4">
|
|
{loading && (
|
|
<div className="rounded-2xl border border-gray-200 bg-white p-6 text-sm text-gray-600">
|
|
Loading…
|
|
</div>
|
|
)}
|
|
|
|
{!loading && platforms.map(platform => (
|
|
<div key={platform.id} className="rounded-2xl bg-white border border-gray-100 shadow p-4 sm:p-5">
|
|
<div className="flex flex-col gap-4">
|
|
<div className="flex items-start justify-between gap-3">
|
|
<div className="min-w-0">
|
|
<div className="text-base font-semibold text-gray-900 truncate">{platform.title}</div>
|
|
<div className="text-xs text-gray-500 truncate">{platform.href}</div>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<button
|
|
type="button"
|
|
onClick={() => toggleOpen(platform.id)}
|
|
disabled={saving}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-gray-200 bg-white text-gray-800 px-3 py-2 text-xs font-semibold hover:bg-gray-50"
|
|
>
|
|
{isOpen(platform) ? <ChevronUpIcon className="h-4 w-4" /> : <ChevronDownIcon className="h-4 w-4" />}
|
|
{isOpen(platform) ? 'Close' : 'Edit'}
|
|
</button>
|
|
|
|
<button
|
|
type="button"
|
|
onClick={async () => {
|
|
if (platform._isNew) {
|
|
removeNewPlatform(platform.id)
|
|
return
|
|
}
|
|
await setPlatformState(platform, false)
|
|
}}
|
|
disabled={saving}
|
|
className="inline-flex items-center gap-2 rounded-lg border border-red-200 bg-red-50 text-red-700 px-3 py-2 text-xs font-semibold hover:bg-red-100"
|
|
>
|
|
<TrashIcon className="h-4 w-4" />
|
|
{platform._isNew ? 'Remove' : 'Deactivate'}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
{isOpen(platform) && (
|
|
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
|
<label className="block">
|
|
<div className="text-xs font-semibold text-gray-700">Title</div>
|
|
<input
|
|
value={platform.title}
|
|
onChange={e => updatePlatform(platform.id, { title: e.target.value })}
|
|
disabled={saving}
|
|
className="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm"
|
|
/>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="text-xs font-semibold text-gray-700">Description</div>
|
|
<input
|
|
value={platform.description}
|
|
onChange={e => updatePlatform(platform.id, { description: e.target.value })}
|
|
disabled={saving}
|
|
className="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm"
|
|
/>
|
|
</label>
|
|
|
|
<label className="block md:col-span-2">
|
|
<div className="text-xs font-semibold text-gray-700">Link</div>
|
|
<input
|
|
value={platform.href}
|
|
onChange={e => updatePlatform(platform.id, { href: e.target.value })}
|
|
disabled={saving}
|
|
placeholder="Example: /shop or https://example.com"
|
|
className={
|
|
'mt-1 w-full rounded-lg border px-3 py-2 text-sm ' +
|
|
(isValidHref(platform.href) ? 'border-gray-200' : 'border-red-300')
|
|
}
|
|
/>
|
|
{!isValidHref(platform.href) && (
|
|
<div className="mt-1 text-xs text-red-600">Must start with “/” or “http(s)://”.</div>
|
|
)}
|
|
<div className="mt-1 text-xs text-gray-500">
|
|
Use a relative path (starts with “/”) for internal pages, or a full URL for external pages.
|
|
</div>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="text-xs font-semibold text-gray-700">Icon</div>
|
|
<input
|
|
value={'Link'}
|
|
disabled
|
|
className="mt-1 w-full rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-sm text-gray-700"
|
|
/>
|
|
</label>
|
|
|
|
<label className="block">
|
|
<div className="text-xs font-semibold text-gray-700">Color</div>
|
|
<select
|
|
value={platform.color}
|
|
onChange={e => updatePlatform(platform.id, { color: e.target.value as DashboardPlatformColorClass })}
|
|
disabled={saving}
|
|
className="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm"
|
|
>
|
|
{DASHBOARD_PLATFORMS_COLOR_OPTIONS.map(opt => (
|
|
<option key={opt.value} value={opt.value}>{opt.label}</option>
|
|
))}
|
|
</select>
|
|
</label>
|
|
|
|
<div className="flex flex-wrap gap-4 md:col-span-2">
|
|
<label className="inline-flex items-center gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={platform.isActive}
|
|
onChange={e => { void setPlatformState(platform, e.target.checked) }}
|
|
disabled={saving}
|
|
/>
|
|
Active (visible on dashboard)
|
|
</label>
|
|
|
|
<label className="inline-flex items-center gap-2 text-sm">
|
|
<input
|
|
type="checkbox"
|
|
checked={Boolean(platform.disabled)}
|
|
onChange={e => updatePlatform(platform.id, { disabled: e.target.checked })}
|
|
disabled={saving}
|
|
/>
|
|
Disabled
|
|
</label>
|
|
</div>
|
|
|
|
{platform.disabled && (
|
|
<label className="block md:col-span-2">
|
|
<div className="text-xs font-semibold text-gray-700">Disabled message</div>
|
|
<input
|
|
value={platform.disabledText || ''}
|
|
onChange={e => updatePlatform(platform.id, { disabledText: e.target.value })}
|
|
disabled={saving}
|
|
className="mt-1 w-full rounded-lg border border-gray-200 px-3 py-2 text-sm"
|
|
placeholder="Optional"
|
|
/>
|
|
</label>
|
|
)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
|
|
{!loading && platforms.length === 0 && (
|
|
<div className="rounded-2xl border border-gray-200 bg-white p-8 text-center text-sm text-gray-600">
|
|
No platforms configured.
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</PageLayout>
|
|
)
|
|
}
|