feat: initial project setup for Meme Studio

Next.js 14 App Router application for managing homophone pun game levels:

- Better Auth with Prisma adapter for authentication
- MySQL database with Prisma ORM
- Level CRUD operations with drag-and-drop reordering
- Tencent COS integration for image uploads
- shadcn/ui components with Tailwind CSS
- TanStack Query for server state management
This commit is contained in:
richarjiang
2026-03-15 15:01:47 +08:00
commit 4854f1cefc
43 changed files with 11543 additions and 0 deletions

View File

@@ -0,0 +1,134 @@
'use client'
import { useState, useCallback } from 'react'
import { Level } from '@/types'
import {
DndContext,
closestCenter,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors,
DragEndEvent,
} from '@dnd-kit/core'
import {
arrayMove,
SortableContext,
sortableKeyboardCoordinates,
verticalListSortingStrategy,
useSortable,
} from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { LevelCard } from './level-card'
interface SortableLevelCardProps {
level: Level
onEdit: (level: Level) => void
onDelete: (id: string) => void
}
function SortableLevelCard({ level, onEdit, onDelete }: SortableLevelCardProps) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: level.id })
const style = {
transform: CSS.Transform.toString(transform),
transition,
}
return (
<div ref={setNodeRef} style={style} {...attributes} {...listeners}>
<LevelCard
level={level}
onEdit={onEdit}
onDelete={onDelete}
isDragging={isDragging}
/>
</div>
)
}
interface LevelListProps {
levels: Level[]
onReorder: (orders: { id: string; sortOrder: number }[]) => void
onEdit: (level: Level) => void
onDelete: (id: string) => void
}
export function LevelList({ levels, onReorder, onEdit, onDelete }: LevelListProps) {
const [items, setItems] = useState<Level[]>(levels)
// Update items when levels prop changes
if (JSON.stringify(items.map(i => i.id)) !== JSON.stringify(levels.map(l => l.id))) {
setItems(levels)
}
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
distance: 8,
},
}),
useSensor(KeyboardSensor, {
coordinateGetter: sortableKeyboardCoordinates,
})
)
const handleDragEnd = useCallback(
(event: DragEndEvent) => {
const { active, over } = event
if (over && active.id !== over.id) {
const oldIndex = items.findIndex((item) => item.id === active.id)
const newIndex = items.findIndex((item) => item.id === over.id)
const newItems = arrayMove(items, oldIndex, newIndex)
setItems(newItems)
// Notify parent of new order
const orders = newItems.map((item, index) => ({
id: item.id,
sortOrder: index,
}))
onReorder(orders)
}
},
[items, onReorder]
)
if (items.length === 0) {
return (
<div className="text-center py-12 text-gray-500">
<p></p>
<p className="text-sm mt-2">&ldquo;&rdquo;</p>
</div>
)
}
return (
<DndContext
sensors={sensors}
collisionDetection={closestCenter}
onDragEnd={handleDragEnd}
>
<SortableContext items={items} strategy={verticalListSortingStrategy}>
<div className="space-y-3">
{items.map((level) => (
<SortableLevelCard
key={level.id}
level={level}
onEdit={onEdit}
onDelete={onDelete}
/>
))}
</div>
</SortableContext>
</DndContext>
)
}