profit-planet-frontend/src/app/admin/dashboard-management/page.tsx
2026-03-15 18:34:19 +01:00

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>
)
}