stadtwerke/innungsapp/apps/admin/app/[slug]/dashboard/news/[id]/page.tsx

236 lines
8.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'use client'
import { use, useState, useEffect } from 'react'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc-client'
import { getTrpcErrorMessage } from '@/lib/trpc-error'
import Link from 'next/link'
import dynamic from 'next/dynamic'
const MDEditor = dynamic(() => import('@uiw/react-md-editor'), { ssr: false })
const KATEGORIEN = [
{ value: 'Wichtig', label: 'Wichtig' },
{ value: 'Pruefung', label: 'Prüfung' },
{ value: 'Foerderung', label: 'Förderung' },
{ value: 'Veranstaltung', label: 'Veranstaltung' },
{ value: 'Allgemein', label: 'Allgemein' },
]
export default function NewsEditPage({ params }: { params: Promise<{ id: string }> }) {
const { id } = use(params)
const router = useRouter()
const { data: news, isLoading } = trpc.news.byId.useQuery({ id })
const updateMutation = trpc.news.update.useMutation({
onSuccess: () => router.push('/dashboard/news'),
})
const deleteMutation = trpc.news.delete.useMutation({
onSuccess: () => router.push('/dashboard/news'),
})
const [title, setTitle] = useState('')
const [body, setBody] = useState('')
const [kategorie, setKategorie] = useState('Allgemein')
const [uploading, setUploading] = useState(false)
const [attachments, setAttachments] = useState<
Array<{ name: string; storagePath: string; sizeBytes: number; mimeType?: string | null }>
>([])
useEffect(() => {
if (news) {
setTitle(news.title)
setBody(news.body)
setKategorie(news.kategorie)
if (news.attachments) {
setAttachments(news.attachments.map((a: typeof news.attachments[number]) => ({ ...a, sizeBytes: a.sizeBytes ?? 0 })))
}
}
}, [news])
if (isLoading) return <div className="text-gray-500 text-sm">Wird geladen...</div>
if (!news) return <div className="text-gray-500 text-sm">Beitrag nicht gefunden.</div>
async function handleFileUpload(e: React.ChangeEvent<HTMLInputElement>) {
const file = e.target.files?.[0]
if (!file) return
setUploading(true)
const formData = new FormData()
formData.append('file', file)
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData })
const data = await res.json()
setAttachments((prev) => [...prev, data])
} catch {
alert('Upload fehlgeschlagen')
} finally {
setUploading(false)
}
}
function handleSave(publishNow: boolean) {
if (!title.trim() || !body.trim()) return
updateMutation.mutate({
id,
data: {
title,
body,
kategorie: kategorie as never,
publishedAt: publishNow ? new Date().toISOString() : undefined,
attachments: attachments.map((a) => ({
name: a.name,
storagePath: a.storagePath,
sizeBytes: a.sizeBytes,
mimeType: a.mimeType || 'application/pdf',
})),
},
})
}
function handleUnpublish() {
updateMutation.mutate({ id, data: { publishedAt: null } })
}
const isPublished = !!news.publishedAt
return (
<div className="max-w-4xl space-y-6">
<div className="flex items-center gap-3">
<Link href="/dashboard/news" className="text-xs text-gray-400 hover:text-gray-600 uppercase tracking-wide">
Zurück
</Link>
<span className="text-gray-200">/</span>
<h1 className="text-2xl font-bold text-gray-900">Beitrag bearbeiten</h1>
{isPublished && (
<span className="text-[11px] font-medium bg-green-100 text-green-700 px-2 py-0.5 rounded-full">
Publiziert
</span>
)}
{!isPublished && (
<span className="text-[11px] font-medium bg-gray-100 text-gray-500 px-2 py-0.5 rounded-full">
Entwurf
</span>
)}
</div>
<div className="bg-white rounded-lg border p-6 space-y-4">
<div className="grid grid-cols-3 gap-4">
<div className="col-span-2">
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Titel</label>
<input
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Titel..."
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
/>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Kategorie</label>
<select
value={kategorie}
onChange={(e) => setKategorie(e.target.value)}
className="w-full px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-brand-500 focus:border-transparent"
>
{KATEGORIEN.map((k) => (
<option key={k.value} value={k.value}>{k.label}</option>
))}
</select>
</div>
</div>
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Inhalt</label>
<div data-color-mode="light">
<MDEditor
value={body}
onChange={(v) => setBody(v ?? '')}
height={400}
preview="live"
/>
</div>
</div>
{/* Attachments */}
<div>
<label className="block text-xs font-medium text-gray-500 uppercase tracking-wide mb-1">Anhänge (PDF)</label>
<label className="cursor-pointer inline-flex items-center gap-2 px-4 py-2 border border-dashed border-gray-300 rounded-lg text-sm text-gray-500 hover:border-brand-500 hover:text-brand-500 transition-colors">
{uploading ? '⏳ Hochladen...' : '📎 Datei anhängen'}
<input
type="file"
accept=".pdf,image/*"
onChange={handleFileUpload}
disabled={uploading}
className="hidden"
/>
</label>
{attachments.length > 0 && (
<ul className="mt-2 space-y-1">
{attachments.map((a, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-600">
<span>📄</span>
<span>{a.name}</span>
{a.sizeBytes != null && (
<span className="text-gray-400">({Math.round(a.sizeBytes / 1024)} KB)</span>
)}
<button
onClick={() => setAttachments(prev => prev.filter((_, idx) => idx !== i))}
className="text-red-500 hover:text-red-700 ml-2"
title="Entfernen"
>
×
</button>
</li>
))}
</ul>
)}
</div>
{updateMutation.error && (
<p className="text-sm text-red-600 bg-red-50 px-4 py-2 rounded-lg">
{getTrpcErrorMessage(updateMutation.error)}
</p>
)}
<div className="flex items-center justify-between pt-2 border-t">
<div className="flex gap-3">
{!isPublished && (
<button
onClick={() => handleSave(true)}
disabled={updateMutation.isPending}
className="bg-brand-500 text-white px-5 py-2 rounded-lg text-sm font-medium hover:bg-brand-600 disabled:opacity-60 transition-colors"
>
Publizieren
</button>
)}
<button
onClick={() => handleSave(false)}
disabled={updateMutation.isPending}
className="px-5 py-2 rounded-lg text-sm font-medium text-gray-700 border border-gray-200 hover:bg-gray-50 transition-colors"
>
{updateMutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
</button>
{isPublished && (
<button
onClick={handleUnpublish}
disabled={updateMutation.isPending}
className="px-5 py-2 rounded-lg text-sm font-medium text-gray-500 hover:text-gray-700 hover:bg-gray-50 transition-colors"
>
Depublizieren
</button>
)}
</div>
<button
onClick={() => {
if (confirm('Beitrag wirklich löschen?')) deleteMutation.mutate({ id })
}}
disabled={deleteMutation.isPending}
className="text-sm text-red-500 hover:text-red-700 transition-colors"
>
Löschen
</button>
</div>
</div>
</div>
)
}