From ab76f207ae09135d06a071e6da96d07794c38591 Mon Sep 17 00:00:00 2001 From: Sebastian Date: Sun, 29 Mar 2026 15:19:47 +0000 Subject: [PATCH] feat: initial SchwarmbLog monorepo setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .env.example | 4 + .gitignore | 7 + README.md | 47 ++++++ apps/api/.env.example | 3 + apps/api/Dockerfile | 24 +++ apps/api/package.json | 32 ++++ apps/api/prisma/schema.prisma | 260 ++++++++++++++++++++++++++++++++ apps/api/src/index.ts | 30 ++++ apps/api/src/lib/db.ts | 13 ++ apps/api/src/middleware/auth.ts | 26 ++++ apps/api/tsconfig.json | 9 ++ apps/web/Dockerfile | 21 +++ apps/web/index.html | 13 ++ apps/web/nginx.conf | 15 ++ apps/web/package.json | 33 ++++ apps/web/postcss.config.js | 6 + apps/web/src/index.css | 9 ++ apps/web/src/lib/api.ts | 24 +++ apps/web/src/main.tsx | 23 +++ apps/web/src/routes/__root.tsx | 18 +++ apps/web/src/routes/index.tsx | 15 ++ apps/web/tailwind.config.ts | 37 +++++ apps/web/tsconfig.json | 11 ++ apps/web/vite.config.ts | 19 +++ docker-compose.yml | 40 +++++ package.json | 16 ++ pnpm-workspace.yaml | 3 + tsconfig.json | 10 ++ 28 files changed, 768 insertions(+) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 README.md create mode 100644 apps/api/.env.example create mode 100644 apps/api/Dockerfile create mode 100644 apps/api/package.json create mode 100644 apps/api/prisma/schema.prisma create mode 100644 apps/api/src/index.ts create mode 100644 apps/api/src/lib/db.ts create mode 100644 apps/api/src/middleware/auth.ts create mode 100644 apps/api/tsconfig.json create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/index.html create mode 100644 apps/web/nginx.conf create mode 100644 apps/web/package.json create mode 100644 apps/web/postcss.config.js create mode 100644 apps/web/src/index.css create mode 100644 apps/web/src/lib/api.ts create mode 100644 apps/web/src/main.tsx create mode 100644 apps/web/src/routes/__root.tsx create mode 100644 apps/web/src/routes/index.tsx create mode 100644 apps/web/tailwind.config.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 package.json create mode 100644 pnpm-workspace.yaml create mode 100644 tsconfig.json diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..367a9a9 --- /dev/null +++ b/.env.example @@ -0,0 +1,4 @@ +MYSQL_ROOT_PASSWORD=supersecret +MYSQL_USER=schwarmblog +MYSQL_PASSWORD=schwarmblog_pw +JWT_SECRET=change-me-in-production diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97404a9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +.env +.env.local +.DS_Store +*.log +.turbo/ diff --git a/README.md b/README.md new file mode 100644 index 0000000..66b5dcb --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# 🐟 SchwarmbLog +**Gemeinsam die Welt entdecken** + +BNE-Reiseblog-Portal der SALM Bremerhaven. + +## Stack +- **Frontend:** React + Vite + TanStack Router + TanStack Query + Tailwind CSS +- **Backend:** Hono + Prisma + MySQL +- **Deployment:** Docker + Dokploy + +## Monorepo-Struktur +``` +schwarmblog/ +β”œβ”€β”€ apps/ +β”‚ β”œβ”€β”€ api/ # Hono Backend +β”‚ └── web/ # React Frontend +β”œβ”€β”€ packages/ # Shared code (zukΓΌnftig) +└── docker-compose.yml +``` + +## Lokale Entwicklung + +```bash +# AbhΓ€ngigkeiten installieren +pnpm install + +# .env anlegen +cp .env.example .env +cp apps/api/.env.example apps/api/.env +# .env Werte anpassen + +# Datenbank starten (Docker) +docker compose up db -d + +# Migrationen ausfΓΌhren +pnpm --filter @schwarmblog/api db:migrate + +# Dev-Server starten +pnpm dev +``` + +## Deployment (Dokploy) +Zwei Services in Dokploy anlegen: +- `schwarmblog-api` β†’ apps/api/Dockerfile +- `schwarmblog-web` β†’ apps/web/Dockerfile + +Subdomain z.B. `schwarmblog.science-teaching.de` auf web zeigen lassen. diff --git a/apps/api/.env.example b/apps/api/.env.example new file mode 100644 index 0000000..fd20e56 --- /dev/null +++ b/apps/api/.env.example @@ -0,0 +1,3 @@ +DATABASE_URL="mysql://user:password@localhost:3306/schwarmblog" +JWT_SECRET="change-me-in-production" +PORT=3001 diff --git a/apps/api/Dockerfile b/apps/api/Dockerfile new file mode 100644 index 0000000..c7bb18c --- /dev/null +++ b/apps/api/Dockerfile @@ -0,0 +1,24 @@ +FROM node:20-alpine AS base +RUN corepack enable pnpm + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-workspace.yaml ./ +COPY apps/api/package.json ./apps/api/ +RUN pnpm install --frozen-lockfile + +FROM base AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/api/node_modules ./apps/api/node_modules +COPY . . +WORKDIR /app/apps/api +RUN pnpm db:generate +RUN pnpm build + +FROM node:20-alpine AS runner +WORKDIR /app +COPY --from=build /app/apps/api/dist ./dist +COPY --from=build /app/apps/api/node_modules ./node_modules +EXPOSE 3001 +CMD ["node", "dist/index.js"] diff --git a/apps/api/package.json b/apps/api/package.json new file mode 100644 index 0000000..93bf94b --- /dev/null +++ b/apps/api/package.json @@ -0,0 +1,32 @@ +{ + "name": "@schwarmblog/api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx watch src/index.ts", + "build": "tsc", + "typecheck": "tsc --noEmit", + "db:generate": "prisma generate", + "db:migrate": "prisma migrate dev", + "db:studio": "prisma studio" + }, + "dependencies": { + "@prisma/client": "^5.14.0", + "hono": "^4.4.0", + "@hono/node-server": "^1.12.0", + "bcryptjs": "^2.4.3", + "jsonwebtoken": "^9.0.2", + "uuid": "^10.0.0", + "dotenv": "^16.4.5" + }, + "devDependencies": { + "@types/bcryptjs": "^2.4.6", + "@types/jsonwebtoken": "^9.0.6", + "@types/uuid": "^10.0.0", + "@types/node": "^20.14.0", + "prisma": "^5.14.0", + "tsx": "^4.15.0", + "typescript": "^5.4.5" + } +} diff --git a/apps/api/prisma/schema.prisma b/apps/api/prisma/schema.prisma new file mode 100644 index 0000000..5b6cd43 --- /dev/null +++ b/apps/api/prisma/schema.prisma @@ -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") +} diff --git a/apps/api/src/index.ts b/apps/api/src/index.ts new file mode 100644 index 0000000..a493602 --- /dev/null +++ b/apps/api/src/index.ts @@ -0,0 +1,30 @@ +import { serve } from "@hono/node-server"; +import { Hono } from "hono"; +import { cors } from "hono/cors"; +import { logger } from "hono/logger"; +import "dotenv/config"; + +const app = new Hono(); + +// Middleware +app.use("*", logger()); +app.use( + "*", + cors({ + origin: process.env.FRONTEND_URL ?? "http://localhost:5173", + credentials: true, + }) +); + +// Health check +app.get("/health", (c) => c.json({ status: "ok", app: "SchwarmbLog API" })); + +// Routes (werden schrittweise ergΓ€nzt) +// app.route("/auth", authRoutes); +// app.route("/stations", stationRoutes); +// app.route("/blogs", blogRoutes); + +const port = Number(process.env.PORT ?? 3001); +console.log(`🐟 SchwarmbLog API lΓ€uft auf Port ${port}`); + +serve({ fetch: app.fetch, port }); diff --git a/apps/api/src/lib/db.ts b/apps/api/src/lib/db.ts new file mode 100644 index 0000000..938167b --- /dev/null +++ b/apps/api/src/lib/db.ts @@ -0,0 +1,13 @@ +import { PrismaClient } from "@prisma/client"; + +const globalForPrisma = globalThis as unknown as { + prisma: PrismaClient | undefined; +}; + +export const db = + globalForPrisma.prisma ?? + new PrismaClient({ + log: process.env.NODE_ENV === "development" ? ["query", "error"] : ["error"], + }); + +if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = db; diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts new file mode 100644 index 0000000..2489329 --- /dev/null +++ b/apps/api/src/middleware/auth.ts @@ -0,0 +1,26 @@ +import { createMiddleware } from "hono/factory"; +import jwt from "jsonwebtoken"; + +export type AuthUser = { + id: string; + role: string; +}; + +declare module "hono" { + interface ContextVariableMap { + user: AuthUser; + } +} + +export const requireAuth = createMiddleware(async (c, next) => { + const token = c.req.header("Authorization")?.replace("Bearer ", ""); + if (!token) return c.json({ error: "Unauthorized" }, 401); + + try { + const payload = jwt.verify(token, process.env.JWT_SECRET ?? "") as AuthUser; + c.set("user", payload); + await next(); + } catch { + return c.json({ error: "Invalid token" }, 401); + } +}); diff --git a/apps/api/tsconfig.json b/apps/api/tsconfig.json new file mode 100644 index 0000000..6df00a3 --- /dev/null +++ b/apps/api/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "rootDir": "src", + "moduleResolution": "node" + }, + "include": ["src"] +} diff --git a/apps/web/Dockerfile b/apps/web/Dockerfile new file mode 100644 index 0000000..1e54a52 --- /dev/null +++ b/apps/web/Dockerfile @@ -0,0 +1,21 @@ +FROM node:20-alpine AS base +RUN corepack enable pnpm + +FROM base AS deps +WORKDIR /app +COPY package.json pnpm-workspace.yaml ./ +COPY apps/web/package.json ./apps/web/ +RUN pnpm install --frozen-lockfile + +FROM base AS build +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY --from=deps /app/apps/web/node_modules ./apps/web/node_modules +COPY . . +WORKDIR /app/apps/web +RUN pnpm build + +FROM nginx:alpine AS runner +COPY --from=build /app/apps/web/dist /usr/share/nginx/html +COPY apps/web/nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/apps/web/index.html b/apps/web/index.html new file mode 100644 index 0000000..7aa1d3a --- /dev/null +++ b/apps/web/index.html @@ -0,0 +1,13 @@ + + + + + + SchwarmbLog – Gemeinsam die Welt entdecken + + + +
+ + + diff --git a/apps/web/nginx.conf b/apps/web/nginx.conf new file mode 100644 index 0000000..10a9643 --- /dev/null +++ b/apps/web/nginx.conf @@ -0,0 +1,15 @@ +server { + listen 80; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } + + location /api/ { + proxy_pass http://api:3001/; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + } +} diff --git a/apps/web/package.json b/apps/web/package.json new file mode 100644 index 0000000..5cbe3df --- /dev/null +++ b/apps/web/package.json @@ -0,0 +1,33 @@ +{ + "name": "@schwarmblog/web", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc && vite build", + "preview": "vite preview", + "typecheck": "tsc --noEmit", + "lint": "eslint src" + }, + "dependencies": { + "@tanstack/react-query": "^5.45.0", + "@tanstack/react-router": "^1.42.0", + "@tanstack/router-devtools": "^1.42.0", + "@tanstack/react-query-devtools": "^5.45.0", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "lucide-react": "^0.395.0" + }, + "devDependencies": { + "@types/react": "^18.3.3", + "@types/react-dom": "^18.3.0", + "@vitejs/plugin-react": "^4.3.1", + "autoprefixer": "^10.4.19", + "postcss": "^8.4.38", + "tailwindcss": "^3.4.4", + "typescript": "^5.4.5", + "vite": "^5.3.1", + "@tanstack/router-plugin": "^1.42.0" + } +} diff --git a/apps/web/postcss.config.js b/apps/web/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/apps/web/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/apps/web/src/index.css b/apps/web/src/index.css new file mode 100644 index 0000000..22e4902 --- /dev/null +++ b/apps/web/src/index.css @@ -0,0 +1,9 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + body { + @apply bg-ocean-50 text-gray-900 font-sans; + } +} diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts new file mode 100644 index 0000000..551fe4b --- /dev/null +++ b/apps/web/src/lib/api.ts @@ -0,0 +1,24 @@ +const BASE = "/api"; + +async function request(path: string, init?: RequestInit): Promise { + const token = localStorage.getItem("token"); + const res = await fetch(`${BASE}${path}`, { + ...init, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + ...init?.headers, + }, + }); + if (!res.ok) throw new Error(await res.text()); + return res.json(); +} + +export const api = { + get: (path: string) => request(path), + post: (path: string, body: unknown) => + request(path, { method: "POST", body: JSON.stringify(body) }), + patch: (path: string, body: unknown) => + request(path, { method: "PATCH", body: JSON.stringify(body) }), + delete: (path: string) => request(path, { method: "DELETE" }), +}; diff --git a/apps/web/src/main.tsx b/apps/web/src/main.tsx new file mode 100644 index 0000000..39625f5 --- /dev/null +++ b/apps/web/src/main.tsx @@ -0,0 +1,23 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import { RouterProvider, createRouter } from "@tanstack/react-router"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { routeTree } from "./routeTree.gen"; +import "./index.css"; + +const queryClient = new QueryClient(); +const router = createRouter({ routeTree, context: { queryClient } }); + +declare module "@tanstack/react-router" { + interface Register { + router: typeof router; + } +} + +ReactDOM.createRoot(document.getElementById("root")!).render( + + + + + +); diff --git a/apps/web/src/routes/__root.tsx b/apps/web/src/routes/__root.tsx new file mode 100644 index 0000000..f067035 --- /dev/null +++ b/apps/web/src/routes/__root.tsx @@ -0,0 +1,18 @@ +import { createRootRouteWithContext, Outlet } from "@tanstack/react-router"; +import { TanStackRouterDevtools } from "@tanstack/router-devtools"; +import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; +import type { QueryClient } from "@tanstack/react-query"; + +interface RouterContext { + queryClient: QueryClient; +} + +export const Route = createRootRouteWithContext()({ + component: () => ( + <> + + + + + ), +}); diff --git a/apps/web/src/routes/index.tsx b/apps/web/src/routes/index.tsx new file mode 100644 index 0000000..c9238c9 --- /dev/null +++ b/apps/web/src/routes/index.tsx @@ -0,0 +1,15 @@ +import { createFileRoute } from "@tanstack/react-router"; + +export const Route = createFileRoute("/")({ + component: Index, +}); + +function Index() { + return ( +
+
🐟
+

SchwarmbLog

+

Gemeinsam die Welt entdecken

+
+ ); +} diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts new file mode 100644 index 0000000..0af7367 --- /dev/null +++ b/apps/web/tailwind.config.ts @@ -0,0 +1,37 @@ +import type { Config } from "tailwindcss"; + +export default { + content: ["./index.html", "./src/**/*.{ts,tsx}"], + theme: { + extend: { + colors: { + // SchwarmbLog Farbpalette – Ozean/Lachs + salm: { + 50: "#fff5f0", + 100: "#ffe0d0", + 200: "#ffb899", + 300: "#ff8c5a", + 400: "#ff6a2b", + 500: "#e84d0e", + 600: "#c43a07", + 700: "#9c2d05", + 800: "#7a2207", + 900: "#631d09", + }, + ocean: { + 50: "#f0f9ff", + 100: "#e0f2fe", + 200: "#b9e6fd", + 300: "#7dd3fc", + 400: "#38bdf8", + 500: "#0ea5e9", + 600: "#0284c7", + 700: "#0369a1", + 800: "#075985", + 900: "#0c4a6e", + }, + }, + }, + }, + plugins: [], +} satisfies Config; diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json new file mode 100644 index 0000000..30330f2 --- /dev/null +++ b/apps/web/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "jsx": "react-jsx", + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} diff --git a/apps/web/vite.config.ts b/apps/web/vite.config.ts new file mode 100644 index 0000000..62c3017 --- /dev/null +++ b/apps/web/vite.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; +import { TanStackRouterVite } from "@tanstack/router-plugin/vite"; + +export default defineConfig({ + plugins: [ + TanStackRouterVite(), + react(), + ], + server: { + port: 5173, + proxy: { + "/api": { + target: "http://localhost:3001", + rewrite: (path) => path.replace(/^\/api/, ""), + }, + }, + }, +}); diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..3e0429e --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,40 @@ +services: + db: + image: mysql:8.0 + restart: unless-stopped + environment: + MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} + MYSQL_DATABASE: schwarmblog + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + volumes: + - db_data:/var/lib/mysql + ports: + - "3306:3306" + + api: + build: + context: . + dockerfile: apps/api/Dockerfile + restart: unless-stopped + environment: + DATABASE_URL: mysql://${MYSQL_USER}:${MYSQL_PASSWORD}@db:3306/schwarmblog + JWT_SECRET: ${JWT_SECRET} + NODE_ENV: production + depends_on: + - db + ports: + - "3001:3001" + + web: + build: + context: . + dockerfile: apps/web/Dockerfile + restart: unless-stopped + depends_on: + - api + ports: + - "80:80" + +volumes: + db_data: diff --git a/package.json b/package.json new file mode 100644 index 0000000..ee39531 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "schwarmblog", + "version": "0.1.0", + "private": true, + "packageManager": "pnpm@9.0.0", + "scripts": { + "dev": "pnpm --parallel -r dev", + "build": "pnpm -r build", + "lint": "pnpm -r lint", + "typecheck": "pnpm -r typecheck" + }, + "workspaces": [ + "apps/*", + "packages/*" + ] +} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..e9b0dad --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'apps/*' + - 'packages/*' diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..89f3362 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,10 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "esModuleInterop": true + } +}