From 8fd421512882ed4af4de388536709109f53aed3a Mon Sep 17 00:00:00 2001 From: kinga Date: Thu, 23 Apr 2026 02:37:28 +0200 Subject: [PATCH] refactor(creative-space): enhance Creative Space component with inline creation and improved UI elements, including date formatting and loading states --- .../student/creative-space-home-client.tsx | 336 ++++++++++++++++-- LearningPlatform/package.json | 2 +- 2 files changed, 300 insertions(+), 38 deletions(-) diff --git a/LearningPlatform/components/student/creative-space-home-client.tsx b/LearningPlatform/components/student/creative-space-home-client.tsx index 36f9e41..3181855 100644 --- a/LearningPlatform/components/student/creative-space-home-client.tsx +++ b/LearningPlatform/components/student/creative-space-home-client.tsx @@ -2,8 +2,12 @@ import Link from 'next/link' import { useCallback, useEffect, useState } from 'react' +import { Clock, Loader2, Trash2 } from 'lucide-react' import { Button } from '@/components/ui/button' -import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card' +import { Input } from '@/components/ui/input' +import { studentGlassCard } from '@/lib/student-glass-styles' +import { cn } from '@/lib/utils' type CreativeSpace = { id: string @@ -11,8 +15,102 @@ type CreativeSpace = { updatedAt: string } +function formatUpdatedAt(iso: string) { + const d = new Date(iso) + if (Number.isNaN(d.getTime())) return iso + return new Intl.DateTimeFormat(undefined, { + dateStyle: 'medium', + timeStyle: 'short', + }).format(d) +} + +const spaceRowSurface = + 'group flex flex-col gap-4 rounded-2xl border p-4 sm:flex-row sm:items-stretch sm:justify-between sm:gap-6 sm:p-5 ' + + 'border-slate-300/45 bg-white/[0.28] shadow-sm backdrop-blur-md transition-[box-shadow,transform] duration-200 ' + + 'hover:shadow-md dark:border-white/15 dark:bg-white/[0.1] dark:shadow-none dark:hover:brightness-[1.02]' + +const inlineNewRowSurface = + 'flex flex-col gap-4 rounded-2xl border p-4 sm:flex-row sm:items-stretch sm:justify-between sm:gap-6 sm:p-5 ' + + 'border-primary/35 bg-white/[0.34] shadow-sm ring-2 ring-primary/20 backdrop-blur-md dark:border-primary/30 dark:bg-white/[0.12] dark:ring-primary/25' + +type CreativeSpaceRowProps = { + space: CreativeSpace + deleting: boolean + deleteBusy: boolean + onDelete: () => void +} + +function CreativeSpaceRow({ space, deleting, deleteBusy, onDelete }: CreativeSpaceRowProps) { + const openDisabled = deleting + + return ( +
+
+

+ {space.title} +

+
+ +
+

+ Last updated +

+ +
+
+
+
+ {openDisabled ? ( + + ) : ( + + )} + +
+
+ ) +} + export function CreativeSpaceHomeClient() { const [spaces, setSpaces] = useState([]) + const [showCreateRow, setShowCreateRow] = useState(false) + const [createTitle, setCreateTitle] = useState('') + const [createSubmitting, setCreateSubmitting] = useState(false) + const [createError, setCreateError] = useState(null) + const [deletingId, setDeletingId] = useState(null) const loadSpaces = useCallback(async () => { const res = await fetch('/api/creative-spaces') @@ -28,48 +126,212 @@ export function CreativeSpaceHomeClient() { return () => window.clearTimeout(t) }, [loadSpaces]) - const createSpace = useCallback(async () => { - const name = window.prompt('Creative space name') - if (!name) return - const res = await fetch('/api/creative-spaces', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ title: name }), - }) - if (!res.ok) return - await loadSpaces() + const startInlineCreate = useCallback(() => { + setCreateTitle('') + setCreateError(null) + setShowCreateRow(true) + }, []) + + const cancelInlineCreate = useCallback(() => { + if (createSubmitting) return + setShowCreateRow(false) + setCreateTitle('') + setCreateError(null) + }, [createSubmitting]) + + useEffect(() => { + if (!showCreateRow) return + const t = window.setTimeout(() => { + document.getElementById('inline-create-space-name')?.focus() + }, 0) + return () => window.clearTimeout(t) + }, [showCreateRow]) + + useEffect(() => { + if (!showCreateRow) return + const onKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') cancelInlineCreate() + } + window.addEventListener('keydown', onKey) + return () => window.removeEventListener('keydown', onKey) + }, [showCreateRow, cancelInlineCreate]) + + const submitCreateSpace = useCallback(async () => { + const name = createTitle.trim() + if (!name) { + setCreateError('Enter a name for your space.') + document.getElementById('inline-create-space-name')?.focus() + return + } + setCreateSubmitting(true) + setCreateError(null) + try { + const res = await fetch('/api/creative-spaces', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title: name }), + }) + if (!res.ok) { + setCreateError('Could not create the space. Try again.') + return + } + setShowCreateRow(false) + setCreateTitle('') + await loadSpaces() + } finally { + setCreateSubmitting(false) + } + }, [createTitle, loadSpaces]) + + const deleteSpace = useCallback(async (id: string) => { + setDeletingId(id) + try { + const res = await fetch(`/api/creative-spaces/${encodeURIComponent(id)}`, { method: 'DELETE' }) + if (!res.ok) return + await loadSpaces() + } finally { + setDeletingId(null) + } }, [loadSpaces]) + const listHasContent = spaces.length > 0 || showCreateRow + return ( -
- - - Creative Spaces - - - - {spaces.length === 0 ? ( -

No creative spaces yet.

- ) : ( - spaces.map((space) => ( -
+
+
+
+
+

+ Creative Spaces +

+

+ Open a board or start a new canvas for notes, decks, and sketches. +

+
+
+ + + +
+

+ Your boards +

+ + All creative spaces + + + Each space is its own whiteboard. Create as many as you need. + +
+ +
+ + {!listHasContent ? ( +
+

+ No creative spaces yet. Create one to open the whiteboard.

+
- -
- )) - )} - - + ) : ( + <> + {spaces.length === 0 && showCreateRow ? ( +

+ Name your first space, then save to open the whiteboard. +

+ ) : null} + + {showCreateRow ? ( +
{ + e.preventDefault() + void submitCreateSpace() + }} + > +
+
+

+ New creative space +

+
+ { + setCreateTitle(e.target.value) + if (createError) setCreateError(null) + }} + disabled={createSubmitting} + aria-invalid={createError ? true : undefined} + aria-describedby={createError ? 'inline-create-error' : undefined} + maxLength={120} + className="h-10 bg-white/50 text-base dark:bg-black/20 sm:max-w-xl" + /> + {createError ? ( +

+ {createError} +

+ ) : null} +
+
+ + +
+
+ ) : null} + + {spaces.map((space) => ( + void deleteSpace(space.id)} + /> + ))} + + )} + + + +
) } diff --git a/LearningPlatform/package.json b/LearningPlatform/package.json index eeb0e5c..daa26ba 100644 --- a/LearningPlatform/package.json +++ b/LearningPlatform/package.json @@ -7,7 +7,7 @@ "npm": ">=10.0.0" }, "scripts": { - "dev": "next dev", + "dev": "next dev --webpack", "build": "next build --webpack", "start": "next start -H 0.0.0.0 -p ${PORT:-10000}", "postinstall": "prisma generate",