feat: initial SchwarmbLog monorepo setup

- React + Vite + TanStack Router/Query frontend
- Hono + Prisma + MySQL backend
- Prisma schema mit allen Entitäten
- Docker + docker-compose Setup
- Tailwind mit Salm/Ozean Farbpalette
This commit is contained in:
Sebastian
2026-03-29 15:19:47 +00:00
commit ab76f207ae
28 changed files with 768 additions and 0 deletions

View File

@@ -0,0 +1,260 @@
// SchwarmbLog Prisma Schema
// Stack: Hono + Prisma + MySQL
// Conventions: camelCase in Prisma, snake_case in DB, cuid() for IDs
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
}
// ─────────────────────────────────────────────
// USERS & AUTH
// ─────────────────────────────────────────────
model User {
id String @id @default(cuid())
name String
email String? @unique
role Role @default(STUDENT)
// Auth
passwordHash String?
qrToken String? @unique @map("qr_token")
qrTokenCreated DateTime? @map("qr_token_created")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
// Relations
classMemberships ClassMembership[]
swarmMemberships SwarmMembership[]
blog Blog?
assignedTodos Todo[] @relation("AssignedTodos")
reactions Reaction[]
comments Comment[]
@@map("users")
}
enum Role {
ADMIN
TEACHER
STUDENT
PARENT
}
// ─────────────────────────────────────────────
// CLASSES & SWARMS
// ─────────────────────────────────────────────
model Class {
id String @id @default(cuid())
name String // z.B. "7a"
schoolYear String @map("school_year") // z.B. "2025/26"
createdAt DateTime @default(now()) @map("created_at")
memberships ClassMembership[]
swarms Swarm[]
blogs Blog[]
@@map("classes")
}
model ClassMembership {
id String @id @default(cuid())
user User @relation(fields: [userId], references: [id])
userId String @map("user_id")
class Class @relation(fields: [classId], references: [id])
classId String @map("class_id")
role ClassRole @default(STUDENT)
@@unique([userId, classId])
@@map("class_memberships")
}
enum ClassRole {
TEACHER
STUDENT
}
// Schwarm = Sichtbarkeitsgruppe (Klasse oder ganzer Jahrgang)
model Swarm {
id String @id @default(cuid())
name String // z.B. "Schwarm 7a" oder "Schwarm Jahrgang 7"
scope SwarmScope
class Class? @relation(fields: [classId], references: [id])
classId String? @map("class_id")
createdAt DateTime @default(now()) @map("created_at")
memberships SwarmMembership[]
@@map("swarms")
}
enum SwarmScope {
CLASS // nur eine Klasse
YEAR // ganzer Jahrgang
}
model SwarmMembership {
id String @id @default(cuid())
swarm Swarm @relation(fields: [swarmId], references: [id])
swarmId String @map("swarm_id")
user User @relation(fields: [userId], references: [id])
userId String @map("user_id")
@@unique([swarmId, userId])
@@map("swarm_memberships")
}
// ─────────────────────────────────────────────
// STATIONEN & TEMPLATES
// ─────────────────────────────────────────────
model Station {
id String @id @default(cuid())
name String // z.B. "Lagos"
country String
latitude Float
longitude Float
bneTopic String @map("bne_topic") // z.B. "Elektroschrott"
description String? @db.Text
orderIndex Int @map("order_index") // optimale Reihenfolge
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
templates EntryTemplate[]
blogEntries BlogEntry[]
@@map("stations")
}
// Vorstrukturierter Eintrag (Lückentext, Bildblock, etc.)
// blocks: JSON-Array von Block-Objekten
// Beispiel: [{ type: "gap_text", content: "Ich bin in ___ angekommen..." }, { type: "image" }, { type: "free_text" }]
model EntryTemplate {
id String @id @default(cuid())
station Station @relation(fields: [stationId], references: [id])
stationId String @map("station_id")
title String
blocks Json // Block-Struktur des Templates
isOptional Boolean @default(false) @map("is_optional")
orderIndex Int @default(0) @map("order_index")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
todos Todo[]
blogEntries BlogEntry[]
@@map("entry_templates")
}
// ─────────────────────────────────────────────
// BLOG & EINTRÄGE
// ─────────────────────────────────────────────
model Blog {
id String @id @default(cuid())
student User @relation(fields: [studentId], references: [id])
studentId String @unique @map("student_id")
class Class @relation(fields: [classId], references: [id])
classId String @map("class_id")
title String @default("Mein Reiseblog")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
entries BlogEntry[]
todos Todo[]
@@map("blogs")
}
model BlogEntry {
id String @id @default(cuid())
blog Blog @relation(fields: [blogId], references: [id])
blogId String @map("blog_id")
station Station? @relation(fields: [stationId], references: [id])
stationId String? @map("station_id")
template EntryTemplate? @relation(fields: [templateId], references: [id])
templateId String? @map("template_id") // null = freier Eintrag
title String
blocks Json // Ausgefüllte Block-Inhalte (Text, Bildpfad, etc.)
status EntryStatus @default(DRAFT)
visibility Visibility @default(PRIVATE)
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
reactions Reaction[]
comments Comment[]
@@map("blog_entries")
}
enum EntryStatus {
DRAFT
PUBLISHED
}
enum Visibility {
PRIVATE // nur Schüler + Lehrkraft
CLASS // Klassen-Schwarm
YEAR // Jahrgangs-Schwarm
PARENTS // zusätzlich Eltern
}
// ─────────────────────────────────────────────
// TODOS
// ─────────────────────────────────────────────
// Lehrkraft weist Schüler einen vorstrukturierten Eintrag zu
model Todo {
id String @id @default(cuid())
blog Blog @relation(fields: [blogId], references: [id])
blogId String @map("blog_id")
template EntryTemplate @relation(fields: [templateId], references: [id])
templateId String @map("template_id")
assignedBy User @relation("AssignedTodos", fields: [assignedById], references: [id])
assignedById String @map("assigned_by")
dueDate DateTime? @map("due_date")
completedAt DateTime? @map("completed_at")
createdAt DateTime @default(now()) @map("created_at")
@@map("todos")
}
// ─────────────────────────────────────────────
// SOZIALES
// ─────────────────────────────────────────────
model Reaction {
id String @id @default(cuid())
entry BlogEntry @relation(fields: [entryId], references: [id])
entryId String @map("entry_id")
user User @relation(fields: [userId], references: [id])
userId String @map("user_id")
type ReactionType @default(LIKE)
@@unique([entryId, userId]) // pro Eintrag nur eine Reaktion pro User
@@map("reactions")
}
enum ReactionType {
LIKE
}
model Comment {
id String @id @default(cuid())
entry BlogEntry @relation(fields: [entryId], references: [id])
entryId String @map("entry_id")
user User @relation(fields: [userId], references: [id])
userId String @map("user_id")
text String @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@map("comments")
}