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.
+
+
+
+ ) : (
+ <>
+ {spaces.length === 0 && showCreateRow ? (
+
+ Name your first space, then save to open the whiteboard.
+
+ ) : null}
+
+ {showCreateRow ? (
+
+ ) : 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",