Compare commits

..

15 Commits

Author SHA1 Message Date
BeauTroll
d552d31f4a set healthcheck
Some checks failed
CI / test (push) Has been cancelled
CI / build (push) Has been cancelled
CI / deploy-staging (push) Has been cancelled
CI / deploy-production (push) Has been cancelled
CI / deploy-manual (push) Has been cancelled
2026-01-19 19:27:49 +01:00
BeauTroll
a22fed0f9d feat: set homepage 2026-01-19 19:03:16 +01:00
BeauTroll
f6621200cd feat: add readme 2026-01-19 19:03:05 +01:00
BeauTroll
54abb81a07 add pnpm caching 2026-01-19 19:00:08 +01:00
BeauTroll
1045aea277 feat: add Gitea Actions CI workflow
Some checks failed
CI / test (push) Failing after 18m16s
CI / build (push) Has been cancelled
CI / deploy-staging (push) Has been cancelled
CI / deploy-production (push) Has been cancelled
CI / deploy-manual (push) Has been cancelled
2026-01-19 18:53:05 +01:00
BeauTroll
d470eb2cd5 a gitea ci 2026-01-19 18:36:27 +01:00
BeauTroll
cbd0cdced4 biome lint applied 2026-01-19 18:09:23 +01:00
BeauTroll
e8269bb2fe add javascript linting config 2026-01-19 18:04:52 +01:00
BeauTroll
ba8a46fcd3 add biome 2026-01-19 18:01:31 +01:00
BeauTroll
0bea7bc3f4 added biome 2026-01-19 13:35:20 +01:00
BeauTroll
10859a4e64 feat: add healthcheck 2026-01-19 12:23:01 +01:00
BeauTroll
2b78d4e332 feat: add prisma 2026-01-19 12:22:08 +01:00
BeauTroll
448721f0a0 feat: add docker-compose 2026-01-19 09:52:44 +01:00
BeauTroll
eb7c7a7221 feat: add Dockerfile 2026-01-19 09:42:01 +01:00
BeauTroll
4c8a6e9fd3 init tanstack 2026-01-19 09:32:47 +01:00
65 changed files with 10340 additions and 1346 deletions

16
.cta.json Normal file
View File

@@ -0,0 +1,16 @@
{
"projectName": "dofus-manager2",
"mode": "file-router",
"typescript": true,
"tailwind": false,
"packageManager": "pnpm",
"git": true,
"install": true,
"addOnOptions": {},
"version": 1,
"framework": "react-cra",
"chosenAddOns": [
"start",
"nitro"
]
}

9
.env.example Normal file
View File

@@ -0,0 +1,9 @@
# Database
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/dofus_manager?schema=public"
# App
APP_URL="http://localhost:3000"
NODE_ENV="development"
# Session
SESSION_SECRET="change-me-to-a-random-string-min-32-chars"

146
.gitea/workflows/ci.yml Normal file
View File

@@ -0,0 +1,146 @@
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy'
required: true
default: 'staging'
type: choice
options:
- staging
- production
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: dofus_manager
ports:
- 5432:5432
options: >-
--health-cmd "pg_isready -U postgres"
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'pnpm'
- name: Get pnpm store directory
shell: bash
run: echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV
- name: Cache pnpm store
uses: actions/cache@v4
with:
path: ${{ env.STORE_PATH }}
key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }}
restore-keys: |
${{ runner.os }}-pnpm-store-
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Generate Prisma client
run: pnpm prisma generate
- name: Run migrations
run: pnpm prisma migrate deploy
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/dofus_manager
- name: Lint
run: pnpm lint
- name: Type check
run: pnpm typecheck
- name: Test
run: pnpm test
env:
DATABASE_URL: postgresql://postgres:postgres@localhost:5432/dofus_manager
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Registry
uses: docker/login-action@v3
with:
registry: ${{ vars.REGISTRY_URL }}
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Build and push
uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: |
${{ vars.REGISTRY_URL }}/theo/dofus-manager:${{ github.sha }}
${{ vars.REGISTRY_URL }}/theo/dofus-manager:latest
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/develop'
environment: staging
steps:
- name: Deploy to staging
run: |
echo "Deploying to staging..."
curl -X POST "${{ secrets.STAGING_WEBHOOK_URL }}" || true
deploy-production:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to production
run: |
echo "Deploying to production..."
curl -X POST "${{ secrets.PRODUCTION_WEBHOOK_URL }}" || true
deploy-manual:
runs-on: ubuntu-latest
if: github.event_name == 'workflow_dispatch'
environment: ${{ github.event.inputs.environment }}
steps:
- name: Deploy to ${{ github.event.inputs.environment }}
run: |
echo "Manual deploy to ${{ github.event.inputs.environment }}"
# Ajoutez votre logique de déploiement ici

15
.gitignore vendored Normal file
View File

@@ -0,0 +1,15 @@
node_modules
.DS_Store
dist
dist-ssr
*.local
count.txt
.env
.nitro
.tanstack
.wrangler
.output
.vinxi
todos.json
/generated/prisma

11
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,11 @@
{
"files.watcherExclude": {
"**/routeTree.gen.ts": true
},
"search.exclude": {
"**/routeTree.gen.ts": true
},
"files.readonlyInclude": {
"**/routeTree.gen.ts": true
}
}

93
README.md Normal file
View File

@@ -0,0 +1,93 @@
# Dofus Manager
Application de gestion de personnages, comptes et équipes pour Dofus.
## Prérequis
- Node.js 20+
- pnpm 9+
- Docker & Docker Compose
## Installation
```bash
# Cloner le repo
git clone <repo-url>
cd dofus-manager2
# Installer les dépendances
pnpm install
# Copier les variables d'environnement
cp .env.example .env
```
## Développement local
```bash
# Lancer PostgreSQL
docker compose -f docker/docker-compose.dev.yml up -d
# Appliquer les migrations
pnpm prisma migrate dev
# Lancer le serveur de développement
pnpm dev
```
L'application est accessible sur `http://localhost:3000`
## Scripts disponibles
| Script | Description |
|--------|-------------|
| `pnpm dev` | Lancer le serveur de développement |
| `pnpm build` | Build de production |
| `pnpm start` | Lancer le build de production |
| `pnpm lint` | Vérifier le code avec Biome |
| `pnpm lint:fix` | Corriger les erreurs de lint |
| `pnpm format` | Formater le code |
| `pnpm typecheck` | Vérifier les types TypeScript |
| `pnpm test` | Lancer les tests |
| `pnpm prisma studio` | Ouvrir Prisma Studio |
| `pnpm prisma migrate dev` | Créer/appliquer les migrations |
## Variables d'environnement
| Variable | Description | Exemple |
|----------|-------------|---------|
| `DATABASE_URL` | URL de connexion PostgreSQL | `postgresql://postgres:postgres@localhost:5432/dofus_manager` |
| `APP_URL` | URL de l'application | `http://localhost:3000` |
| `NODE_ENV` | Environnement | `development` / `production` |
| `SESSION_SECRET` | Clé secrète pour les sessions (min 32 chars) | `change-me-to-a-random-string` |
## Structure du projet
```
├── .gitea/workflows/ # CI/CD Gitea Actions
├── docker/ # Dockerfile et docker-compose
├── prisma/ # Schema et migrations Prisma
├── src/
│ ├── components/ # Composants React
│ ├── lib/ # Utilitaires et services
│ ├── routes/ # Pages et API (TanStack Router)
│ └── styles/ # CSS global
└── tests/ # Tests unitaires et E2E
```
## Docker
```bash
# Build l'image
docker build -f docker/Dockerfile -t dofus-manager .
# Lancer en production
docker compose -f docker/docker-compose.yml up -d
```
## Tech Stack
- **Frontend:** React 19, TanStack Router, TanStack Query, Tailwind CSS, shadcn/ui
- **Backend:** TanStack Start, Prisma
- **Database:** PostgreSQL 16
- **DevOps:** Docker, Gitea Actions

37
biome.json Normal file
View File

@@ -0,0 +1,37 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.11/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["src/**/*.ts", "src/**/*.tsx"],
"ignoreUnknown": false
},
"formatter": {
"enabled": true,
"indentStyle": "tab"
},
"linter": {
"enabled": true,
"rules": {
"recommended": true
}
},
"javascript": {
"formatter": {
"quoteStyle": "single",
"semicolons": "always",
"trailingCommas": "all"
}
},
"assist": {
"enabled": true,
"actions": {
"source": {
"organizeImports": "on"
}
}
}
}

22
components.json Normal file
View File

@@ -0,0 +1,22 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "src/app/globals.css",
"baseColor": "stone",
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "lucide",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {}
}

45
docker/Dockerfile Normal file
View File

@@ -0,0 +1,45 @@
# docker/Dockerfile
# ============ Build stage ============
FROM node:20-alpine AS builder
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Copy package files
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
# Copy source and build
COPY . .
RUN pnpm prisma generate
RUN pnpm build
# ============ Production stage ============
FROM node:20-alpine AS runner
WORKDIR /app
# Install pnpm
RUN corepack enable && corepack prepare pnpm@latest --activate
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 app
# Copy built application
COPY --from=builder --chown=app:nodejs /app/.output ./.output
COPY --from=builder --chown=app:nodejs /app/node_modules ./node_modules
COPY --from=builder --chown=app:nodejs /app/package.json ./package.json
COPY --from=builder --chown=app:nodejs /app/prisma ./prisma
USER app
EXPOSE 3000
ENV NODE_ENV=production
ENV PORT=3000
CMD ["node", ".output/server/index.mjs"]

View File

@@ -0,0 +1,20 @@
services:
postgres:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=postgres
- POSTGRES_DB=dofus_manager
ports:
- "5432:5432"
volumes:
- postgres_dev_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d dofus_manager"]
interval: 5s
timeout: 5s
retries: 5
volumes:
postgres_dev_data:

47
docker/docker-compose.yml Normal file
View File

@@ -0,0 +1,47 @@
services:
app:
build:
context: ..
dockerfile: docker/Dockerfile
restart: unless-stopped
environment:
- DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/dofus_manager
- SESSION_SECRET=${SESSION_SECRET}
- NODE_ENV=production
ports:
- "3000:3000"
depends_on:
postgres:
condition: service_healthy
healthcheck:
test:
["CMD", "wget", "-q", "--spider", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
networks:
- internal
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${DB_PASSWORD}
- POSTGRES_DB=dofus_manager
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d dofus_manager"]
interval: 5s
timeout: 5s
retries: 5
networks:
- internal
volumes:
postgres_data:
networks:
internal:

View File

@@ -22,10 +22,10 @@ src/server/
```typescript
// src/server/functions/characters.ts
import { createServerFn } from '@tanstack/react-start/server';
import { z } from 'zod';
import { db } from '@/lib/server/db';
import { requireAuth } from '@/server/middleware/auth';
import { createServerFn } from "@tanstack/react-start/server";
import { z } from "zod";
import { db } from "@/lib/server/db";
import { requireAuth } from "@/server/middleware/auth";
// Schemas
const createCharacterSchema = z.object({
@@ -48,7 +48,7 @@ const characterFiltersSchema = z.object({
});
// Functions
export const getCharacters = createServerFn({ method: 'GET' })
export const getCharacters = createServerFn({ method: "GET" })
.validator((data: unknown) => characterFiltersSchema.parse(data))
.handler(async ({ data }) => {
const session = await requireAuth();
@@ -57,7 +57,7 @@ export const getCharacters = createServerFn({ method: 'GET' })
const where = {
account: { userId: session.userId },
...(search && {
name: { contains: search, mode: 'insensitive' as const },
name: { contains: search, mode: "insensitive" as const },
}),
...(classIds?.length && { classId: { in: classIds } }),
...(serverIds?.length && { serverId: { in: serverIds } }),
@@ -70,7 +70,7 @@ export const getCharacters = createServerFn({ method: 'GET' })
include: { account: { select: { id: true, name: true } } },
skip: (page - 1) * limit,
take: limit,
orderBy: { name: 'asc' },
orderBy: { name: "asc" },
}),
db.character.count({ where }),
]);
@@ -86,7 +86,7 @@ export const getCharacters = createServerFn({ method: 'GET' })
};
});
export const createCharacter = createServerFn({ method: 'POST' })
export const createCharacter = createServerFn({ method: "POST" })
.validator((data: unknown) => createCharacterSchema.parse(data))
.handler(async ({ data }) => {
const session = await requireAuth();
@@ -97,14 +97,16 @@ export const createCharacter = createServerFn({ method: 'POST' })
});
if (!account) {
throw new Error('Account not found');
throw new Error("Account not found");
}
return db.character.create({ data });
});
export const bulkDeleteCharacters = createServerFn({ method: 'POST' })
.validator((data: unknown) => z.object({ ids: z.array(z.string().cuid()) }).parse(data))
export const bulkDeleteCharacters = createServerFn({ method: "POST" })
.validator((data: unknown) =>
z.object({ ids: z.array(z.string().cuid()) }).parse(data),
)
.handler(async ({ data }) => {
const session = await requireAuth();
@@ -123,8 +125,8 @@ export const bulkDeleteCharacters = createServerFn({ method: 'POST' })
```typescript
// src/server/middleware/auth.ts
import { getWebRequest } from '@tanstack/react-start/server';
import { db } from '@/lib/server/db';
import { getWebRequest } from "@tanstack/react-start/server";
import { db } from "@/lib/server/db";
interface Session {
userId: string;
@@ -133,13 +135,14 @@ interface Session {
export async function requireAuth(): Promise<Session> {
const request = getWebRequest();
const sessionId = request.headers.get('cookie')
?.split(';')
.find(c => c.trim().startsWith('session='))
?.split('=')[1];
const sessionId = request.headers
.get("cookie")
?.split(";")
.find((c) => c.trim().startsWith("session="))
?.split("=")[1];
if (!sessionId) {
throw new Error('Unauthorized');
throw new Error("Unauthorized");
}
const session = await db.session.findUnique({
@@ -148,7 +151,7 @@ export async function requireAuth(): Promise<Session> {
});
if (!session || session.expiresAt < new Date()) {
throw new Error('Session expired');
throw new Error("Session expired");
}
return {
@@ -170,7 +173,7 @@ export async function getOptionalAuth(): Promise<Session | null> {
```typescript
// src/lib/server/cache.ts
import NodeCache from 'node-cache';
import NodeCache from "node-cache";
// Different TTLs for different data types
const caches = {
@@ -196,7 +199,9 @@ export const userCache = {
// Helper to invalidate all cache for a user
invalidateUser: (userId: string): void => {
const keys = caches.user.keys().filter(k => k.startsWith(`user:${userId}:`));
const keys = caches.user
.keys()
.filter((k) => k.startsWith(`user:${userId}:`));
caches.user.del(keys);
},
};

View File

@@ -16,77 +16,84 @@ Draft
2. Docker Compose configuration with app service and PostgreSQL 16
3. Prisma configured and connected to PostgreSQL
4. shadcn/ui installed with base components (Button, Input, Card, Table)
5. ESLint + Prettier configured with recommended rules
6. GitLab CI pipeline: build, lint, test stages
5. Biome configured for linting and formatting
6. Gitea Actions workflow: build, lint, test stages
7. Dockerfile multi-stage pour production build
8. README avec instructions de setup local
9. Application démarre et affiche une page d'accueil "Dofus Manager"
10. Health check endpoint `/api/health` pour Docker healthcheck
## Tasks / Subtasks
- [ ] Task 1: Initialize TanStack Start project (AC: 1)
- [ ] Create new TanStack Start project with `pnpm create @tanstack/start`
- [ ] Configure `tsconfig.json` with strict mode enabled
- [ ] Configure path aliases (`@/` pointing to `src/`)
- [ ] Verify TypeScript strict compilation works
- [x] Task 1: Initialize TanStack Start project (AC: 1)
- [x] Create new TanStack Start project with `pnpm create @tanstack/start`
- [x] Configure `tsconfig.json` with strict mode enabled
- [x] Configure path aliases (`@/` pointing to `src/`)
- [x] Verify TypeScript strict compilation works
- [ ] Task 2: Setup Docker environment (AC: 2, 7)
- [ ] Create `docker/` directory
- [ ] Create `docker/Dockerfile` with multi-stage build (builder + runner)
- [ ] Create `docker/docker-compose.yml` with app and postgres services
- [ ] Create `docker/docker-compose.dev.yml` for local development (postgres only)
- [ ] Configure PostgreSQL 16-alpine with healthcheck
- [ ] Test database connectivity
- [x] Task 2: Setup Docker environment (AC: 2, 7)
- [x] Create `docker/` directory
- [x] Create `docker/Dockerfile` with multi-stage build (builder + runner)
- [x] Create `docker/docker-compose.yml` with app and postgres services
- [x] Create `docker/docker-compose.dev.yml` for local development (postgres only)
- [x] Configure PostgreSQL 16-alpine with healthcheck
- [x] Test database connectivity
- [ ] Task 3: Configure Prisma ORM (AC: 3)
- [ ] Install Prisma dependencies (`prisma`, `@prisma/client`)
- [ ] Initialize Prisma with `pnpm prisma init`
- [ ] Configure `prisma/schema.prisma` with PostgreSQL provider
- [ ] Create `src/lib/server/db.ts` for Prisma client singleton
- [ ] Create `.env.example` with DATABASE_URL template
- [ ] Verify Prisma connects to database
- [x] Task 3: Configure Prisma ORM (AC: 3)
- [x] Install Prisma dependencies (`prisma`, `@prisma/client`)
- [x] Initialize Prisma with `pnpm prisma init`
- [x] Configure `prisma/schema.prisma` with PostgreSQL provider
- [x] Create `src/lib/server/db.ts` for Prisma client singleton
- [x] Create `.env.example` with DATABASE_URL template
- [x] Verify Prisma connects to database
- [ ] Task 4: Install and configure shadcn/ui (AC: 4)
- [ ] Install Tailwind CSS 4.x
- [ ] Initialize shadcn/ui with `pnpm dlx shadcn@latest init`
- [ ] Configure `components.json` for path aliases
- [ ] Install base components: Button, Input, Card, Table
- [ ] Create `src/lib/utils.ts` with `cn` utility function
- [ ] Install Lucide React for icons
- [x] Task 4: Install and configure shadcn/ui (AC: 4)
- [x] Install Tailwind CSS 4.x
- [x] Initialize shadcn/ui with `pnpm dlx shadcn@latest init`
- [x] Configure `components.json` for path aliases
- [x] Install base components: Button, Input, Card, Table
- [x] Create `src/lib/utils.ts` with `cn` utility function
- [x] Install Lucide React for icons
- [ ] Task 5: Configure linting and formatting (AC: 5)
- [ ] Install Biome (as specified in tech stack, not ESLint+Prettier)
- [ ] Create `biome.json` with recommended rules
- [ ] Add lint and format scripts to `package.json`
- [ ] Verify linting works on project files
- [x] Task 5: Configure linting and formatting (AC: 5)
- [x] Install Biome (as specified in tech stack, not ESLint+Prettier)
- [x] Create `biome.json` with recommended rules
- [x] Add lint and format scripts to `package.json`
- [x] Verify linting works on project files
- [ ] Task 6: Setup GitLab CI/CD pipeline (AC: 6)
- [ ] Create `.gitlab-ci.yml` with stages: test, build, deploy
- [ ] Configure test stage: lint, typecheck, test
- [ ] Configure build stage: Docker image build and push
- [ ] Configure deploy stages (staging/production) with manual triggers
- [ ] Add caching for node_modules
- [x] Task 6: Setup Gitea Actions workflow (AC: 6)
- [x] Create `.gitea/workflows/ci.yml`
- [x] Configure test job: lint, typecheck, test
- [x] Configure build job: Docker image build and push
- [x] Configure deploy jobs (staging/production) with manual triggers
- [x] Add caching for pnpm store
- [ ] Task 7: Create README documentation (AC: 8)
- [ ] Document project overview
- [ ] Document prerequisites (Node 20, pnpm, Docker)
- [ ] Document local development setup steps
- [ ] Document available npm scripts
- [ ] Document environment variables
- [x] Task 7: Create README documentation (AC: 8)
- [x] Document project overview
- [x] Document prerequisites (Node 20, pnpm, Docker)
- [x] Document local development setup steps
- [x] Document available npm scripts
- [x] Document environment variables
- [ ] Task 8: Create home page (AC: 9)
- [ ] Create `src/routes/index.tsx` as home page
- [ ] Display "Dofus Manager" title
- [ ] Add basic layout structure
- [ ] Create `src/styles/globals.css` with Tailwind imports
- [ ] Verify application starts and renders correctly
- [x] Task 8: Create home page (AC: 9)
- [x] Create `src/routes/index.tsx` as home page
- [x] Display "Dofus Manager" title
- [x] Add basic layout structure
- [x] Create `src/styles/globals.css` with Tailwind imports
- [x] Verify application starts and renders correctly
- [ ] Task 9: Final verification
- [ ] Run `pnpm dev` and verify app starts
- [ ] Run `pnpm lint` and verify no errors
- [ ] Run `pnpm typecheck` and verify no errors
- [ ] Test Docker build locally
- [ ] Verify PostgreSQL connection via Prisma
- [x] Task 9: Create health check endpoint (AC: 10)
- [x] Create `src/routes/api/health.ts` server function
- [x] Return JSON `{ status: "ok", timestamp: Date }`
- [x] Optionally check database connectivity
- [x] Verify endpoint responds at `GET /api/health`
- [x] Task 10: Final verification
- [x] Run `pnpm dev` and verify app starts
- [x] Run `pnpm lint` and verify no errors
- [x] Run `pnpm typecheck` and verify no errors
- [x] Test Docker build locally
- [x] Verify PostgreSQL connection via Prisma
## Dev Notes
@@ -118,7 +125,7 @@ Draft
- Docker
- Docker Compose
- GitLab CI
- Gitea Actions
**Dev Tools:**
@@ -131,9 +138,13 @@ Draft
```
dofus-manager/
├── .gitea/
│ └── workflows/
│ └── ci.yml
├── docker/
│ ├── Dockerfile
── docker-compose.yml
── docker-compose.yml
│ └── docker-compose.dev.yml
├── prisma/
│ ├── schema.prisma
│ └── migrations/
@@ -156,7 +167,9 @@ dofus-manager/
│ │ └── logger.ts
│ ├── routes/
│ │ ├── __root.tsx
│ │ ── index.tsx
│ │ ── index.tsx
│ │ └── api/
│ │ └── health.ts
│ ├── styles/
│ │ └── globals.css
│ └── app.tsx
@@ -187,22 +200,66 @@ dofus-manager/
- PostgreSQL 16-alpine with healthcheck
- Traefik labels for reverse proxy (production)
### GitLab CI/CD [Source: architecture/14-deployment-architecture.md#gitlab-cicd-pipeline]
### Gitea Actions [Adapted from architecture]
**Stages:** test, build, deploy
**Workflow file:** `.gitea/workflows/ci.yml`
**Test stage:**
**Jobs:** test, build, deploy
- image: node:20-alpine
- Commands: pnpm lint, pnpm typecheck, pnpm test
- Cache node_modules
**Test job:**
**Build stage:**
- runs-on: ubuntu-latest
- Steps: checkout, setup pnpm, setup node, install, lint, typecheck, test
- Cache pnpm store
- image: docker:24
- Build and push Docker image to registry
**Build job:**
- needs: test
- Build and push Docker image
- Only on main/develop branches
**Exemple de workflow:**
```yaml
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v2
with:
version: 9
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: pnpm lint
- run: pnpm typecheck
- run: pnpm test
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/develop'
steps:
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- uses: docker/build-push-action@v5
with:
context: .
file: docker/Dockerfile
push: true
tags: registry.example.com/dofus-manager:${{ github.sha }}
```
### Environment Variables [Source: architecture/13-development-workflow.md#environment-variables]
```bash
@@ -234,9 +291,45 @@ SESSION_SECRET="your-secret-key-min-32-chars"
- Types/Interfaces: PascalCase
- Constants: SCREAMING_SNAKE_CASE
### Important Discrepancy Note
### Health Check Endpoint [Source: architecture/19-monitoring-observability.md]
AC #5 specifies "ESLint + Prettier" but the architecture documents (3-technology-stack.md) specify **Biome** for linting and formatting. Recommend following the architecture document and using Biome instead, as it's the project standard.
**Endpoint:** `GET /api/health`
**Response:**
```json
{
"status": "ok",
"timestamp": "2026-01-19T10:00:00.000Z",
"database": "connected"
}
```
**Implementation avec TanStack Start:**
```typescript
// src/routes/api/health.ts
import { createAPIFileRoute } from "@tanstack/start/api";
import { prisma } from "@/lib/server/db";
export const Route = createAPIFileRoute("/api/health")({
GET: async () => {
let dbStatus = "disconnected";
try {
await prisma.$queryRaw`SELECT 1`;
dbStatus = "connected";
} catch {
dbStatus = "error";
}
return Response.json({
status: "ok",
timestamp: new Date().toISOString(),
database: dbStatus,
});
},
});
```
## Testing
@@ -265,8 +358,9 @@ AC #5 specifies "ESLint + Prettier" but the architecture documents (3-technology
## Change Log
| Date | Version | Description | Author |
| ---------- | ------- | ---------------------- | -------- |
| ---------- | ------- | --------------------------------------------- | -------- |
| 2026-01-19 | 1.0 | Initial story creation | SM Agent |
| 2026-01-19 | 1.1 | Gitea Actions, Biome, Health endpoint ajoutés | SM Agent |
---

65
package.json Normal file
View File

@@ -0,0 +1,65 @@
{
"name": "dofus-manager2",
"private": true,
"type": "module",
"scripts": {
"dev": "vite dev --port 3000",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"format": "biome format --write .",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@prisma/adapter-pg": "^7.2.0",
"@prisma/client": "^7.2.0",
"@radix-ui/react-checkbox": "^1.3.3",
"@radix-ui/react-collapsible": "^1.1.12",
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-progress": "^1.1.8",
"@radix-ui/react-radio-group": "^1.3.8",
"@radix-ui/react-select": "^2.2.6",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tabs": "^1.1.13",
"@radix-ui/react-tooltip": "^1.2.8",
"@tailwindcss/vite": "^4.1.18",
"@tanstack/react-devtools": "^0.7.0",
"@tanstack/react-router": "^1.132.0",
"@tanstack/react-router-devtools": "^1.132.0",
"@tanstack/react-router-ssr-query": "^1.131.7",
"@tanstack/react-start": "^1.132.0",
"@tanstack/router-plugin": "^1.132.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-react": "^0.561.0",
"nitro": "npm:nitro-nightly@latest",
"pg": "^8.17.1",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"tailwind-merge": "^3.4.0",
"tailwindcss": "^4.1.18",
"vite-tsconfig-paths": "^6.0.2"
},
"devDependencies": {
"@biomejs/biome": "2.3.11",
"@tanstack/devtools-vite": "^0.3.11",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/node": "^22.10.2",
"@types/pg": "^8.16.0",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
"@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3",
"jsdom": "^27.0.0",
"prisma": "^7.2.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.7.2",
"vite": "^7.1.7",
"vitest": "^3.0.5",
"web-vitals": "^5.1.0"
}
}

6175
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

4
pnpm-workspace.yaml Normal file
View File

@@ -0,0 +1,4 @@
onlyBuiltDependencies:
- '@prisma/engines'
- esbuild
- prisma

14
prisma.config.ts Normal file
View File

@@ -0,0 +1,14 @@
// This file was generated by Prisma, and assumes you have installed the following:
// npm install --save-dev prisma dotenv
import "dotenv/config";
import { defineConfig } from "prisma/config";
export default defineConfig({
schema: "prisma/schema.prisma",
migrations: {
path: "prisma/migrations",
},
datasource: {
url: process.env["DATABASE_URL"],
},
});

View File

@@ -0,0 +1,187 @@
-- CreateEnum
CREATE TYPE "TeamType" AS ENUM ('MAIN', 'SECONDARY', 'CUSTOM');
-- CreateEnum
CREATE TYPE "ProgressionType" AS ENUM ('QUEST', 'DUNGEON', 'ACHIEVEMENT', 'DOFUS');
-- CreateTable
CREATE TABLE "users" (
"id" TEXT NOT NULL,
"email" TEXT NOT NULL,
"password_hash" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "users_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sessions" (
"id" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"expires_at" TIMESTAMP(3) NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "sessions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "accounts" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "accounts_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "characters" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"level" INTEGER NOT NULL DEFAULT 1,
"class_id" INTEGER NOT NULL,
"class_name" TEXT NOT NULL,
"server_id" INTEGER NOT NULL,
"server_name" TEXT NOT NULL,
"account_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "characters_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "teams" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "TeamType" NOT NULL DEFAULT 'CUSTOM',
"user_id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "teams_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "team_members" (
"id" TEXT NOT NULL,
"team_id" TEXT NOT NULL,
"character_id" TEXT NOT NULL,
"joined_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "team_members_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "progressions" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"type" "ProgressionType" NOT NULL,
"category" TEXT NOT NULL,
"dofusdb_id" INTEGER,
CONSTRAINT "progressions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "character_progressions" (
"id" TEXT NOT NULL,
"character_id" TEXT NOT NULL,
"progression_id" TEXT NOT NULL,
"completed" BOOLEAN NOT NULL DEFAULT false,
"completed_at" TIMESTAMP(3),
CONSTRAINT "character_progressions_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-- CreateIndex
CREATE INDEX "sessions_user_id_idx" ON "sessions"("user_id");
-- CreateIndex
CREATE INDEX "sessions_expires_at_idx" ON "sessions"("expires_at");
-- CreateIndex
CREATE INDEX "accounts_user_id_idx" ON "accounts"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "accounts_user_id_name_key" ON "accounts"("user_id", "name");
-- CreateIndex
CREATE INDEX "characters_account_id_idx" ON "characters"("account_id");
-- CreateIndex
CREATE INDEX "characters_class_id_idx" ON "characters"("class_id");
-- CreateIndex
CREATE INDEX "characters_server_id_idx" ON "characters"("server_id");
-- CreateIndex
CREATE INDEX "characters_level_idx" ON "characters"("level");
-- CreateIndex
CREATE UNIQUE INDEX "characters_account_id_name_key" ON "characters"("account_id", "name");
-- CreateIndex
CREATE INDEX "teams_user_id_idx" ON "teams"("user_id");
-- CreateIndex
CREATE UNIQUE INDEX "teams_user_id_name_key" ON "teams"("user_id", "name");
-- CreateIndex
CREATE INDEX "team_members_team_id_idx" ON "team_members"("team_id");
-- CreateIndex
CREATE INDEX "team_members_character_id_idx" ON "team_members"("character_id");
-- CreateIndex
CREATE UNIQUE INDEX "team_members_team_id_character_id_key" ON "team_members"("team_id", "character_id");
-- CreateIndex
CREATE UNIQUE INDEX "progressions_dofusdb_id_key" ON "progressions"("dofusdb_id");
-- CreateIndex
CREATE INDEX "progressions_type_idx" ON "progressions"("type");
-- CreateIndex
CREATE INDEX "progressions_category_idx" ON "progressions"("category");
-- CreateIndex
CREATE INDEX "character_progressions_character_id_idx" ON "character_progressions"("character_id");
-- CreateIndex
CREATE INDEX "character_progressions_progression_id_idx" ON "character_progressions"("progression_id");
-- CreateIndex
CREATE INDEX "character_progressions_completed_idx" ON "character_progressions"("completed");
-- CreateIndex
CREATE UNIQUE INDEX "character_progressions_character_id_progression_id_key" ON "character_progressions"("character_id", "progression_id");
-- AddForeignKey
ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "characters" ADD CONSTRAINT "characters_account_id_fkey" FOREIGN KEY ("account_id") REFERENCES "accounts"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "teams" ADD CONSTRAINT "teams_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "teams"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_character_id_fkey" FOREIGN KEY ("character_id") REFERENCES "characters"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "character_progressions" ADD CONSTRAINT "character_progressions_character_id_fkey" FOREIGN KEY ("character_id") REFERENCES "characters"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "character_progressions" ADD CONSTRAINT "character_progressions_progression_id_fkey" FOREIGN KEY ("progression_id") REFERENCES "progressions"("id") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@@ -0,0 +1,3 @@
# Please do not edit this file manually
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

140
prisma/schema.prisma Normal file
View File

@@ -0,0 +1,140 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
}
model User {
id String @id @default(cuid())
email String @unique
passwordHash String @map("password_hash")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
accounts Account[]
sessions Session[]
teams Team[]
@@map("users")
}
model Session {
id String @id @default(cuid())
userId String @map("user_id")
expiresAt DateTime @map("expires_at")
createdAt DateTime @default(now()) @map("created_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@index([userId])
@@index([expiresAt])
@@map("sessions")
}
model Account {
id String @id @default(cuid())
name String
userId String @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
characters Character[]
@@unique([userId, name])
@@index([userId])
@@map("accounts")
}
model Character {
id String @id @default(cuid())
name String
level Int @default(1)
classId Int @map("class_id")
className String @map("class_name")
serverId Int @map("server_id")
serverName String @map("server_name")
accountId String @map("account_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
progressions CharacterProgression[]
account Account @relation(fields: [accountId], references: [id], onDelete: Cascade)
teamMembers TeamMember[]
@@unique([accountId, name])
@@index([accountId])
@@index([classId])
@@index([serverId])
@@index([level])
@@map("characters")
}
model Team {
id String @id @default(cuid())
name String
type TeamType @default(CUSTOM)
userId String @map("user_id")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
members TeamMember[]
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
@@unique([userId, name])
@@index([userId])
@@map("teams")
}
model TeamMember {
id String @id @default(cuid())
teamId String @map("team_id")
characterId String @map("character_id")
joinedAt DateTime @default(now()) @map("joined_at")
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
team Team @relation(fields: [teamId], references: [id], onDelete: Cascade)
@@unique([teamId, characterId])
@@index([teamId])
@@index([characterId])
@@map("team_members")
}
model Progression {
id String @id @default(cuid())
name String
type ProgressionType
category String
dofusDbId Int? @unique @map("dofusdb_id")
characterProgressions CharacterProgression[]
@@index([type])
@@index([category])
@@map("progressions")
}
model CharacterProgression {
id String @id @default(cuid())
characterId String @map("character_id")
progressionId String @map("progression_id")
completed Boolean @default(false)
completedAt DateTime? @map("completed_at")
character Character @relation(fields: [characterId], references: [id], onDelete: Cascade)
progression Progression @relation(fields: [progressionId], references: [id], onDelete: Cascade)
@@unique([characterId, progressionId])
@@index([characterId])
@@index([progressionId])
@@index([completed])
@@map("character_progressions")
}
enum TeamType {
MAIN
SECONDARY
CUSTOM
}
enum ProgressionType {
QUEST
DUNGEON
ACHIEVEMENT
DOFUS
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View File

@@ -0,0 +1,25 @@
{
"short_name": "TanStack App",
"name": "Create TanStack App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View File

@@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

Binary file not shown.

After

Width:  |  Height:  |  Size: 259 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 15 KiB

38
src/App.css Normal file
View File

@@ -0,0 +1,38 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -5,75 +5,75 @@
/* Updated to Dofus Manager dark gaming theme */
:root {
--background: #0f172a;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--popover: #1e293b;
--popover-foreground: #f8fafc;
--primary: #60a5fa;
--primary-foreground: #0f172a;
--secondary: #334155;
--secondary-foreground: #f8fafc;
--muted: #334155;
--muted-foreground: #94a3b8;
--accent: #334155;
--accent-foreground: #f8fafc;
--destructive: #ef4444;
--background: oklch(1 0 0);
--foreground: oklch(0.147 0.004 49.25);
--card: oklch(1 0 0);
--card-foreground: oklch(0.147 0.004 49.25);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.147 0.004 49.25);
--primary: oklch(0.216 0.006 56.043);
--primary-foreground: oklch(0.985 0.001 106.423);
--secondary: oklch(0.97 0.001 106.424);
--secondary-foreground: oklch(0.216 0.006 56.043);
--muted: oklch(0.97 0.001 106.424);
--muted-foreground: oklch(0.553 0.013 58.071);
--accent: oklch(0.97 0.001 106.424);
--accent-foreground: oklch(0.216 0.006 56.043);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: #f8fafc;
--border: #334155;
--input: #334155;
--ring: #60a5fa;
--chart-1: #60a5fa;
--chart-2: #4ade80;
--chart-3: #f87171;
--chart-4: #fbbf24;
--chart-5: #a78bfa;
--radius: 0.5rem;
--sidebar: #1e293b;
--sidebar-foreground: #f8fafc;
--sidebar-primary: #60a5fa;
--sidebar-primary-foreground: #0f172a;
--sidebar-accent: #334155;
--sidebar-accent-foreground: #f8fafc;
--sidebar-border: #334155;
--sidebar-ring: #60a5fa;
--border: oklch(0.923 0.003 48.717);
--input: oklch(0.923 0.003 48.717);
--ring: oklch(0.709 0.01 56.259);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0.001 106.423);
--sidebar-foreground: oklch(0.147 0.004 49.25);
--sidebar-primary: oklch(0.216 0.006 56.043);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.97 0.001 106.424);
--sidebar-accent-foreground: oklch(0.216 0.006 56.043);
--sidebar-border: oklch(0.923 0.003 48.717);
--sidebar-ring: oklch(0.709 0.01 56.259);
}
/* Keep dark class same as root for dark-first approach */
.dark {
--background: #0f172a;
--foreground: #f8fafc;
--card: #1e293b;
--card-foreground: #f8fafc;
--popover: #1e293b;
--popover-foreground: #f8fafc;
--primary: #60a5fa;
--primary-foreground: #0f172a;
--secondary: #334155;
--secondary-foreground: #f8fafc;
--muted: #334155;
--muted-foreground: #94a3b8;
--accent: #334155;
--accent-foreground: #f8fafc;
--destructive: #ef4444;
--background: oklch(0.147 0.004 49.25);
--foreground: oklch(0.985 0.001 106.423);
--card: oklch(0.216 0.006 56.043);
--card-foreground: oklch(0.985 0.001 106.423);
--popover: oklch(0.216 0.006 56.043);
--popover-foreground: oklch(0.985 0.001 106.423);
--primary: oklch(0.923 0.003 48.717);
--primary-foreground: oklch(0.216 0.006 56.043);
--secondary: oklch(0.268 0.007 34.298);
--secondary-foreground: oklch(0.985 0.001 106.423);
--muted: oklch(0.268 0.007 34.298);
--muted-foreground: oklch(0.709 0.01 56.259);
--accent: oklch(0.268 0.007 34.298);
--accent-foreground: oklch(0.985 0.001 106.423);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: #f8fafc;
--border: #334155;
--input: #334155;
--ring: #60a5fa;
--chart-1: #60a5fa;
--chart-2: #4ade80;
--chart-3: #f87171;
--chart-4: #fbbf24;
--chart-5: #a78bfa;
--sidebar: #1e293b;
--sidebar-foreground: #f8fafc;
--sidebar-primary: #60a5fa;
--sidebar-primary-foreground: #0f172a;
--sidebar-accent: #334155;
--sidebar-accent-foreground: #f8fafc;
--sidebar-border: #334155;
--sidebar-ring: #60a5fa;
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.553 0.013 58.071);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.216 0.006 56.043);
--sidebar-foreground: oklch(0.985 0.001 106.423);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0.001 106.423);
--sidebar-accent: oklch(0.268 0.007 34.298);
--sidebar-accent-foreground: oklch(0.985 0.001 106.423);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.553 0.013 58.071);
}
@theme inline {
@@ -115,6 +115,9 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--radius-2xl: calc(var(--radius) + 8px);
--radius-3xl: calc(var(--radius) + 12px);
--radius-4xl: calc(var(--radius) + 16px);
}
@layer base {

18
src/components/Header.css Normal file
View File

@@ -0,0 +1,18 @@
.header {
padding: 0.5rem;
display: flex;
gap: 0.5rem;
background-color: #fff;
color: #000;
justify-content: space-between;
}
.nav {
display: flex;
flex-direction: row;
}
.nav-item {
padding: 0 0.5rem;
font-weight: bold;
}

27
src/components/Header.tsx Normal file
View File

@@ -0,0 +1,27 @@
import { Link } from '@tanstack/react-router';
import './Header.css';
export default function Header() {
return (
<header className="header">
<nav className="nav">
<div className="nav-item">
<Link to="/">Home</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/start/server-funcs">Start - Server Functions</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/start/api-request">Start - API Request</Link>
</div>
<div className="px-2 font-bold">
<Link to="/demo/start/ssr">Start - SSR Demos</Link>
</div>
</nav>
</header>
);
}

View File

@@ -1,14 +1,33 @@
import * as React from "react";
import {
Search,
Plus,
ArrowUpDown,
ChevronDown,
ChevronUp,
ArrowUpDown,
} from "lucide-react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
Plus,
Search,
} from 'lucide-react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Checkbox } from '@/components/ui/checkbox';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from '@/components/ui/collapsible';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import { Input } from '@/components/ui/input';
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
@@ -16,106 +35,87 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { cn } from "@/lib/utils";
} from '@/components/ui/table';
import { cn } from '@/lib/utils';
// Sample data
const characters = [
{
id: 1,
nom: "Krosmaster",
classe: "Cra",
nom: 'Krosmaster',
classe: 'Cra',
niveau: 200,
serveur: "Imagiro",
compte: "Compte1",
serveur: 'Imagiro',
compte: 'Compte1',
},
{
id: 2,
nom: "TankMaster",
classe: "Iop",
nom: 'TankMaster',
classe: 'Iop',
niveau: 200,
serveur: "Imagiro",
compte: "Compte1",
serveur: 'Imagiro',
compte: 'Compte1',
},
{
id: 3,
nom: "MoneyMaker",
classe: "Enu",
nom: 'MoneyMaker',
classe: 'Enu',
niveau: 200,
serveur: "Imagiro",
compte: "Compte2",
serveur: 'Imagiro',
compte: 'Compte2',
},
{
id: 4,
nom: "HealBot",
classe: "Eni",
nom: 'HealBot',
classe: 'Eni',
niveau: 195,
serveur: "Tylezia",
compte: "Compte2",
serveur: 'Tylezia',
compte: 'Compte2',
},
{
id: 5,
nom: "ShadowKill",
classe: "Sram",
nom: 'ShadowKill',
classe: 'Sram',
niveau: 200,
serveur: "Draconiros",
compte: "Compte3",
serveur: 'Draconiros',
compte: 'Compte3',
},
{
id: 6,
nom: "TimeWarp",
classe: "Elio",
nom: 'TimeWarp',
classe: 'Elio',
niveau: 180,
serveur: "Imagiro",
compte: "Compte1",
serveur: 'Imagiro',
compte: 'Compte1',
},
{
id: 7,
nom: "ArrowStorm",
classe: "Cra",
nom: 'ArrowStorm',
classe: 'Cra',
niveau: 200,
serveur: "Tylezia",
compte: "Compte4",
serveur: 'Tylezia',
compte: 'Compte4',
},
{
id: 8,
nom: "BerserkerX",
classe: "Iop",
nom: 'BerserkerX',
classe: 'Iop',
niveau: 175,
serveur: "Draconiros",
compte: "Compte3",
serveur: 'Draconiros',
compte: 'Compte3',
},
];
const classes = [
{ name: "Cra", count: 12 },
{ name: "Iop", count: 8 },
{ name: "Enu", count: 6 },
{ name: "Eni", count: 5 },
{ name: "Elio", count: 4 },
{ name: "Sram", count: 6 },
{ name: 'Cra', count: 12 },
{ name: 'Iop', count: 8 },
{ name: 'Enu', count: 6 },
{ name: 'Eni', count: 5 },
{ name: 'Elio', count: 4 },
{ name: 'Sram', count: 6 },
];
const serveurs = ["Imagiro", "Tylezia", "Draconiros"];
const serveurs = ['Imagiro', 'Tylezia', 'Draconiros'];
export function CharacterList() {
const [selectedIds, setSelectedIds] = React.useState<number[]>([]);
@@ -286,7 +286,7 @@ export function CharacterList() {
<>
<span className="text-sm text-[#94A3B8]">
{selectedIds.length} sélectionné
{selectedIds.length > 1 ? "s" : ""}
{selectedIds.length > 1 ? 's' : ''}
</span>
<DropdownMenu>
<DropdownMenuTrigger asChild>
@@ -355,10 +355,10 @@ export function CharacterList() {
<TableRow
key={character.id}
className={cn(
"border-[#334155] h-12 transition-colors",
'border-[#334155] h-12 transition-colors',
isSelected
? "bg-[#1E293B] border-l-2 border-l-[#60A5FA]"
: "hover:bg-[#1E293B]/50",
? 'bg-[#1E293B] border-l-2 border-l-[#60A5FA]'
: 'hover:bg-[#1E293B]/50',
)}
>
<TableCell className="w-[40px]">
@@ -399,7 +399,7 @@ export function CharacterList() {
size="sm"
className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
>
{"<"}
{'<'}
</Button>
<Button
variant="outline"
@@ -427,7 +427,7 @@ export function CharacterList() {
size="sm"
className="border-[#475569] text-[#94A3B8] hover:text-[#F8FAFC] hover:bg-[#334155] rounded-[6px] bg-transparent"
>
{">"}
{'>'}
</Button>
</div>
</div>

View File

@@ -1,3 +1,4 @@
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
@@ -5,8 +6,7 @@ import {
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
} from '@/components/ui/dialog';
interface ConfirmationModalProps {
open: boolean;
@@ -32,7 +32,7 @@ export function ConfirmationModal({
<DialogHeader>
<DialogTitle>Confirmer la mise à jour</DialogTitle>
<DialogDescription>
Marquer {progressionName} comme fait pour {incompleteCount}{" "}
Marquer {progressionName} comme fait pour {incompleteCount}{' '}
personnages ?
</DialogDescription>
</DialogHeader>

View File

@@ -1,17 +1,17 @@
import { useLocation } from "@tanstack/react-router";
import { Button } from "@/components/ui/button";
import { Moon, Sun, ChevronRight } from "lucide-react";
import { useLocation } from '@tanstack/react-router';
import { ChevronRight, Moon, Sun } from 'lucide-react';
import { Button } from '@/components/ui/button';
const routeLabels: Record<string, string> = {
"/": "Dashboard",
"/characters": "Personnages",
"/accounts": "Comptes",
"/teams": "Teams",
"/settings": "Paramètres",
'/': 'Dashboard',
'/characters': 'Personnages',
'/accounts': 'Comptes',
'/teams': 'Teams',
'/settings': 'Paramètres',
};
interface AppHeaderProps {
theme: "dark" | "light";
theme: 'dark' | 'light';
onToggleTheme: () => void;
}
@@ -19,12 +19,12 @@ export function AppHeader({ theme, onToggleTheme }: AppHeaderProps) {
const { pathname } = useLocation();
// Generate breadcrumb from pathname
const segments = pathname.split("/").filter(Boolean);
const segments = pathname.split('/').filter(Boolean);
const breadcrumbs =
segments.length === 0
? [{ label: "Dashboard", href: "/" }]
? [{ label: 'Dashboard', href: '/' }]
: segments.map((segment, index) => {
const href = "/" + segments.slice(0, index + 1).join("/");
const href = '/' + segments.slice(0, index + 1).join('/');
const label =
routeLabels[href] ||
segment.charAt(0).toUpperCase() + segment.slice(1);
@@ -43,8 +43,8 @@ export function AppHeader({ theme, onToggleTheme }: AppHeaderProps) {
<span
className={
index === breadcrumbs.length - 1
? "font-medium text-foreground"
: "text-muted-foreground"
? 'font-medium text-foreground'
: 'text-muted-foreground'
}
>
{crumb.label}
@@ -60,7 +60,7 @@ export function AppHeader({ theme, onToggleTheme }: AppHeaderProps) {
onClick={onToggleTheme}
className="h-9 w-9"
>
{theme === "dark" ? (
{theme === 'dark' ? (
<Sun className="h-5 w-5" />
) : (
<Moon className="h-5 w-5" />

View File

@@ -1,9 +1,9 @@
import type React from "react";
import type React from 'react';
import { useState, useEffect } from "react";
import { cn } from "@/lib/utils";
import { AppSidebar } from "@/components/layout/app-sidebar";
import { AppHeader } from "@/components/layout/app-header";
import { useEffect, useState } from 'react';
import { AppHeader } from '@/components/layout/app-header';
import { AppSidebar } from '@/components/layout/app-sidebar';
import { cn } from '@/lib/utils';
interface AppShellProps {
children: React.ReactNode;
@@ -11,20 +11,20 @@ interface AppShellProps {
export function AppShell({ children }: AppShellProps) {
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [theme, setTheme] = useState<"dark" | "light">("dark");
const [theme, setTheme] = useState<'dark' | 'light'>('dark');
// Apply theme class to html element
useEffect(() => {
const root = document.documentElement;
if (theme === "light") {
root.classList.add("light");
if (theme === 'light') {
root.classList.add('light');
} else {
root.classList.remove("light");
root.classList.remove('light');
}
}, [theme]);
const toggleTheme = () => {
setTheme((prev) => (prev === "dark" ? "light" : "dark"));
setTheme((prev) => (prev === 'dark' ? 'light' : 'dark'));
};
return (
@@ -37,8 +37,8 @@ export function AppShell({ children }: AppShellProps) {
{/* Main content area */}
<div
className={cn(
"flex min-h-screen flex-col transition-all duration-200",
sidebarCollapsed ? "pl-16" : "pl-60",
'flex min-h-screen flex-col transition-all duration-200',
sidebarCollapsed ? 'pl-16' : 'pl-60',
)}
>
<AppHeader theme={theme} onToggleTheme={toggleTheme} />

View File

@@ -1,28 +1,28 @@
import { Link, useLocation } from "@tanstack/react-router";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import { Link, useLocation } from '@tanstack/react-router';
import {
ChevronLeft,
ChevronRight,
Folder,
Home,
Settings,
Swords,
Users,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import {
Home,
Users,
Folder,
Swords,
Settings,
ChevronLeft,
ChevronRight,
} from "lucide-react";
} from '@/components/ui/tooltip';
import { cn } from '@/lib/utils';
const navItems = [
{ icon: Home, label: "Dashboard", href: "/" },
{ icon: Users, label: "Personnages", href: "/characters" },
{ icon: Folder, label: "Comptes", href: "/accounts" },
{ icon: Swords, label: "Teams", href: "/teams" },
{ icon: Settings, label: "Paramètres", href: "/settings" },
{ icon: Home, label: 'Dashboard', href: '/' },
{ icon: Users, label: 'Personnages', href: '/characters' },
{ icon: Folder, label: 'Comptes', href: '/accounts' },
{ icon: Swords, label: 'Teams', href: '/teams' },
{ icon: Settings, label: 'Paramètres', href: '/settings' },
];
interface AppSidebarProps {
@@ -37,8 +37,8 @@ export function AppSidebar({ collapsed, onToggle }: AppSidebarProps) {
<TooltipProvider delayDuration={0}>
<aside
className={cn(
"fixed left-0 top-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200",
collapsed ? "w-16" : "w-60",
'fixed left-0 top-0 z-40 flex h-screen flex-col border-r border-sidebar-border bg-sidebar transition-all duration-200',
collapsed ? 'w-16' : 'w-60',
)}
>
{/* Header with logo and toggle */}
@@ -53,8 +53,8 @@ export function AppSidebar({ collapsed, onToggle }: AppSidebarProps) {
size="icon"
onClick={onToggle}
className={cn(
"h-8 w-8 text-sidebar-foreground hover:bg-sidebar-accent",
collapsed && "mx-auto",
'h-8 w-8 text-sidebar-foreground hover:bg-sidebar-accent',
collapsed && 'mx-auto',
)}
>
{collapsed ? (
@@ -76,10 +76,10 @@ export function AppSidebar({ collapsed, onToggle }: AppSidebarProps) {
<Link
to={item.href}
className={cn(
"flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors",
'flex items-center gap-3 rounded-md px-3 py-2 text-sm font-medium transition-colors',
isActive
? "bg-sidebar-primary text-sidebar-primary-foreground"
: "text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground",
? 'bg-sidebar-primary text-sidebar-primary-foreground'
: 'text-sidebar-foreground hover:bg-sidebar-accent hover:text-sidebar-accent-foreground',
)}
>
<Icon className="h-5 w-5 shrink-0" />

View File

@@ -1,29 +1,29 @@
import type React from "react";
import {
FolderOpen,
Users,
Swords,
Coins,
BarChart3,
Plus,
ChevronDown,
ArrowRight,
BarChart3,
ChevronDown,
Coins,
FolderOpen,
Plus,
RefreshCw,
} from "lucide-react";
Swords,
Users,
} from 'lucide-react';
import type React from 'react';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Button } from "@/components/ui/button";
} from '@/components/ui/card';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
} from '@/components/ui/dropdown-menu';
// Custom progress bar with color support
function ColoredProgress({
@@ -31,12 +31,12 @@ function ColoredProgress({
color,
}: {
value: number;
color: "success" | "warning" | "info";
color: 'success' | 'warning' | 'info';
}) {
const colorClasses = {
success: "bg-[#4ADE80]",
warning: "bg-[#FBBF24]",
info: "bg-[#60A5FA]",
success: 'bg-[#4ADE80]',
warning: 'bg-[#FBBF24]',
info: 'bg-[#60A5FA]',
};
return (
@@ -57,7 +57,7 @@ function StatCard({
secondary,
linkText,
children,
className = "",
className = '',
}: {
icon: React.ElementType;
title: string;
@@ -101,16 +101,16 @@ function StatCard({
export function Dashboard() {
// Mock data
const currencies = [
{ name: "Doplons", amount: "12,450" },
{ name: "Orichor", amount: "3,200" },
{ name: "Kamas glace", amount: "8,100" },
{ name: "Nuggets", amount: "2,340" },
{ name: 'Doplons', amount: '12,450' },
{ name: 'Orichor', amount: '3,200' },
{ name: 'Kamas glace', amount: '8,100' },
{ name: 'Nuggets', amount: '2,340' },
];
const progressions = [
{ label: "Dofus", value: 72, color: "success" as const },
{ label: "Donjons", value: 45, color: "warning" as const },
{ label: "Recherchés", value: 61, color: "info" as const },
{ label: 'Dofus', value: 72, color: 'success' as const },
{ label: 'Donjons', value: 45, color: 'warning' as const },
{ label: 'Recherchés', value: 61, color: 'info' as const },
];
return (

View File

@@ -1,12 +1,12 @@
import { useState } from "react";
import { ChevronDown, ChevronRight, Check } from "lucide-react";
import { Checkbox } from "@/components/ui/checkbox";
import { Check, ChevronDown, ChevronRight } from 'lucide-react';
import { useState } from 'react';
import { Checkbox } from '@/components/ui/checkbox';
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
} from '@/components/ui/collapsible';
import { cn } from '@/lib/utils';
interface ProgressionItem {
id: string;
@@ -18,7 +18,7 @@ interface ProgressionItem {
interface ProgressionSectionProps {
title: string;
items: ProgressionItem[];
filter: "all" | "done" | "not-done";
filter: 'all' | 'done' | 'not-done';
}
export function ProgressionSection({
@@ -30,8 +30,8 @@ export function ProgressionSection({
const [localItems, setLocalItems] = useState(items);
const filteredItems = localItems.filter((item) => {
if (filter === "done") return item.completed;
if (filter === "not-done") return !item.completed;
if (filter === 'done') return item.completed;
if (filter === 'not-done') return !item.completed;
return true;
});
@@ -46,7 +46,7 @@ export function ProgressionSection({
...item,
completed: !item.completed,
completedDate: !item.completed
? new Date().toISOString().split("T")[0]
? new Date().toISOString().split('T')[0]
: undefined,
}
: item,
@@ -84,16 +84,16 @@ export function ProgressionSection({
<label
htmlFor={item.id}
className={cn(
"flex-1 text-sm cursor-pointer",
item.completed ? "text-foreground" : "text-muted-foreground",
'flex-1 text-sm cursor-pointer',
item.completed ? 'text-foreground' : 'text-muted-foreground',
)}
>
{item.name}
</label>
{item.completed ? (
<div className="flex items-center gap-1.5 text-sm">
<Check className="h-3.5 w-3.5" style={{ color: "#4ADE80" }} />
<span style={{ color: "#4ADE80" }}>{item.completedDate}</span>
<Check className="h-3.5 w-3.5" style={{ color: '#4ADE80' }} />
<span style={{ color: '#4ADE80' }}>{item.completedDate}</span>
</div>
) : (
<span className="text-muted-foreground text-sm"></span>

View File

@@ -1,22 +1,23 @@
import { useState } from "react";
import {
CheckCircle,
ChevronRight,
Pencil,
Trash2,
CheckCircle,
XCircle,
} from "lucide-react";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
} from 'lucide-react';
import { useState } from 'react';
import { ConfirmationModal } from '@/components/confirmation-modal';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Progress } from '@/components/ui/progress';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Progress } from "@/components/ui/progress";
import { Button } from "@/components/ui/button";
} from '@/components/ui/select';
import {
Table,
TableBody,
@@ -24,32 +25,31 @@ import {
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { ConfirmationModal } from "@/components/confirmation-modal";
} from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
// Mock data
const teamMembers = [
{ id: 1, name: "Krosmaster", completed: true, date: "2026-01-10" },
{ id: 2, name: "TankMaster", completed: true, date: "2026-01-10" },
{ id: 3, name: "HealBot", completed: false, date: null },
{ id: 4, name: "SramKiller", completed: false, date: null },
{ id: 5, name: "Eniripsa", completed: true, date: "2026-01-09" },
{ id: 6, name: "Sacrieur", completed: true, date: "2026-01-08" },
{ id: 7, name: "Pandawa", completed: true, date: "2026-01-10" },
{ id: 8, name: "Eliotrope", completed: true, date: "2026-01-07" },
{ id: 1, name: 'Krosmaster', completed: true, date: '2026-01-10' },
{ id: 2, name: 'TankMaster', completed: true, date: '2026-01-10' },
{ id: 3, name: 'HealBot', completed: false, date: null },
{ id: 4, name: 'SramKiller', completed: false, date: null },
{ id: 5, name: 'Eniripsa', completed: true, date: '2026-01-09' },
{ id: 6, name: 'Sacrieur', completed: true, date: '2026-01-08' },
{ id: 7, name: 'Pandawa', completed: true, date: '2026-01-10' },
{ id: 8, name: 'Eliotrope', completed: true, date: '2026-01-07' },
];
const progressions = [
{ id: "dofus-turquoise", name: "Dofus Turquoise" },
{ id: "donjon-bethel", name: "Donjon Bethel" },
{ id: "quete-ebene", name: "Quête Ébène" },
{ id: "succes-dimension", name: "Succès Dimension" },
{ id: 'dofus-turquoise', name: 'Dofus Turquoise' },
{ id: 'donjon-bethel', name: 'Donjon Bethel' },
{ id: 'quete-ebene', name: 'Quête Ébène' },
{ id: 'succes-dimension', name: 'Succès Dimension' },
];
export default function TeamDetailPage() {
const [selectedProgression, setSelectedProgression] =
useState("dofus-turquoise");
useState('dofus-turquoise');
const [isModalOpen, setIsModalOpen] = useState(false);
const completedCount = teamMembers.filter((m) => m.completed).length;
@@ -58,7 +58,7 @@ export default function TeamDetailPage() {
const progressPercentage = Math.round((completedCount / totalCount) * 100);
const selectedProgressionName =
progressions.find((p) => p.id === selectedProgression)?.name || "";
progressions.find((p) => p.id === selectedProgression)?.name || '';
return (
<div className="min-h-screen bg-background p-6">
@@ -201,7 +201,7 @@ export default function TeamDetailPage() {
)}
</TableCell>
<TableCell className="py-2 text-muted-foreground">
{member.date || "—"}
{member.date || '—'}
</TableCell>
</TableRow>
))}

View File

@@ -0,0 +1,46 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-full border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90",
secondary:
"border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90",
destructive:
"border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:
"text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
function Badge({
className,
variant,
asChild = false,
...props
}: React.ComponentProps<"span"> &
VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
const Comp = asChild ? Slot : "span"
return (
<Comp
data-slot="badge"
className={cn(badgeVariants({ variant }), className)}
{...props}
/>
)
}
export { Badge, badgeVariants }

View File

@@ -0,0 +1,62 @@
import { Slot } from '@radix-ui/react-slot';
import { cva, type VariantProps } from 'class-variance-authority';
import type * as React from 'react';
import { cn } from '@/lib/utils';
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
{
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/90',
destructive:
'bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60',
outline:
'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50',
secondary:
'bg-secondary text-secondary-foreground hover:bg-secondary/80',
ghost:
'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-9 px-4 py-2 has-[>svg]:px-3',
sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5',
lg: 'h-10 rounded-md px-6 has-[>svg]:px-4',
icon: 'size-9',
'icon-sm': 'size-8',
'icon-lg': 'size-10',
},
},
defaultVariants: {
variant: 'default',
size: 'default',
},
},
);
function Button({
className,
variant = 'default',
size = 'default',
asChild = false,
...props
}: React.ComponentProps<'button'> &
VariantProps<typeof buttonVariants> & {
asChild?: boolean;
}) {
const Comp = asChild ? Slot : 'button';
return (
<Comp
data-slot="button"
data-variant={variant}
data-size={size}
className={cn(buttonVariants({ variant, size, className }))}
{...props}
/>
);
}
export { Button, buttonVariants };

View File

@@ -0,0 +1,92 @@
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Card({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card"
className={cn(
'bg-card text-card-foreground flex flex-col gap-6 rounded-xl border py-6 shadow-sm',
className,
)}
{...props}
/>
);
}
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-header"
className={cn(
'@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6',
className,
)}
{...props}
/>
);
}
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-title"
className={cn('leading-none font-semibold', className)}
{...props}
/>
);
}
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-description"
className={cn('text-muted-foreground text-sm', className)}
{...props}
/>
);
}
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-action"
className={cn(
'col-start-2 row-span-2 row-start-1 self-start justify-self-end',
className,
)}
{...props}
/>
);
}
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-content"
className={cn('px-6', className)}
{...props}
/>
);
}
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
return (
<div
data-slot="card-footer"
className={cn('flex items-center px-6 [.border-t]:pt-6', className)}
{...props}
/>
);
}
export {
Card,
CardHeader,
CardFooter,
CardTitle,
CardAction,
CardDescription,
CardContent,
};

View File

@@ -0,0 +1,30 @@
import * as React from "react"
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
import { CheckIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Checkbox({
className,
...props
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (
<CheckboxPrimitive.Root
data-slot="checkbox"
className={cn(
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<CheckboxPrimitive.Indicator
data-slot="checkbox-indicator"
className="grid place-content-center text-current transition-none"
>
<CheckIcon className="size-3.5" />
</CheckboxPrimitive.Indicator>
</CheckboxPrimitive.Root>
)
}
export { Checkbox }

View File

@@ -0,0 +1,33 @@
"use client"
import * as CollapsiblePrimitive from "@radix-ui/react-collapsible"
function Collapsible({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.Root>) {
return <CollapsiblePrimitive.Root data-slot="collapsible" {...props} />
}
function CollapsibleTrigger({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleTrigger>) {
return (
<CollapsiblePrimitive.CollapsibleTrigger
data-slot="collapsible-trigger"
{...props}
/>
)
}
function CollapsibleContent({
...props
}: React.ComponentProps<typeof CollapsiblePrimitive.CollapsibleContent>) {
return (
<CollapsiblePrimitive.CollapsibleContent
data-slot="collapsible-content"
{...props}
/>
)
}
export { Collapsible, CollapsibleTrigger, CollapsibleContent }

View File

@@ -0,0 +1,143 @@
"use client"
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { XIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Dialog({
...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
return <DialogPrimitive.Root data-slot="dialog" {...props} />
}
function DialogTrigger({
...props
}: React.ComponentProps<typeof DialogPrimitive.Trigger>) {
return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />
}
function DialogPortal({
...props
}: React.ComponentProps<typeof DialogPrimitive.Portal>) {
return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />
}
function DialogClose({
...props
}: React.ComponentProps<typeof DialogPrimitive.Close>) {
return <DialogPrimitive.Close data-slot="dialog-close" {...props} />
}
function DialogOverlay({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Overlay>) {
return (
<DialogPrimitive.Overlay
data-slot="dialog-overlay"
className={cn(
"data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/50",
className
)}
{...props}
/>
)
}
function DialogContent({
className,
children,
showCloseButton = true,
...props
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
showCloseButton?: boolean
}) {
return (
<DialogPortal data-slot="dialog-portal">
<DialogOverlay />
<DialogPrimitive.Content
data-slot="dialog-content"
className={cn(
"bg-background data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border p-6 shadow-lg duration-200 outline-none sm:max-w-lg",
className
)}
{...props}
>
{children}
{showCloseButton && (
<DialogPrimitive.Close
data-slot="dialog-close"
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
>
<XIcon />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
)}
</DialogPrimitive.Content>
</DialogPortal>
)
}
function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-header"
className={cn("flex flex-col gap-2 text-center sm:text-left", className)}
{...props}
/>
)
}
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="dialog-footer"
className={cn(
"flex flex-col-reverse gap-2 sm:flex-row sm:justify-end",
className
)}
{...props}
/>
)
}
function DialogTitle({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Title>) {
return (
<DialogPrimitive.Title
data-slot="dialog-title"
className={cn("text-lg leading-none font-semibold", className)}
{...props}
/>
)
}
function DialogDescription({
className,
...props
}: React.ComponentProps<typeof DialogPrimitive.Description>) {
return (
<DialogPrimitive.Description
data-slot="dialog-description"
className={cn("text-muted-foreground text-sm", className)}
{...props}
/>
)
}
export {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogOverlay,
DialogPortal,
DialogTitle,
DialogTrigger,
}

View File

@@ -0,0 +1,255 @@
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { CheckIcon, ChevronRightIcon, CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function DropdownMenu({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
}
function DropdownMenuPortal({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Portal>) {
return (
<DropdownMenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />
)
}
function DropdownMenuTrigger({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
return (
<DropdownMenuPrimitive.Trigger
data-slot="dropdown-menu-trigger"
{...props}
/>
)
}
function DropdownMenuContent({
className,
sideOffset = 4,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border p-1 shadow-md",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
)
}
function DropdownMenuGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Group>) {
return (
<DropdownMenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />
)
}
function DropdownMenuItem({
className,
inset,
variant = "default",
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
variant?: "default" | "destructive"
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-inset={inset}
data-variant={variant}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:!text-destructive [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
checked={checked}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
)
}
function DropdownMenuRadioGroup({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioGroup>) {
return (
<DropdownMenuPrimitive.RadioGroup
data-slot="dropdown-menu-radio-group"
{...props}
/>
)
}
function DropdownMenuRadioItem({
className,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<CircleIcon className="size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
)
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
data-inset={inset}
className={cn(
"px-2 py-1.5 text-sm font-medium data-[inset]:pl-8",
className
)}
{...props}
/>
)
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn("bg-border -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function DropdownMenuShortcut({
className,
...props
}: React.ComponentProps<"span">) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn(
"text-muted-foreground ml-auto text-xs tracking-widest",
className
)}
{...props}
/>
)
}
function DropdownMenuSub({
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />
}
function DropdownMenuSubTrigger({
className,
inset,
children,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
data-inset={inset}
className={cn(
"focus:bg-accent focus:text-accent-foreground data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[inset]:pl-8 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<ChevronRightIcon className="ml-auto size-4" />
</DropdownMenuPrimitive.SubTrigger>
)
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border p-1 shadow-lg",
className
)}
{...props}
/>
)
}
export {
DropdownMenu,
DropdownMenuPortal,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuLabel,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubTrigger,
DropdownMenuSubContent,
}

View File

@@ -0,0 +1,21 @@
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
return (
<input
type={type}
data-slot="input"
className={cn(
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
className,
)}
{...props}
/>
);
}
export { Input };

View File

@@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as ProgressPrimitive from "@radix-ui/react-progress"
import { cn } from "@/lib/utils"
function Progress({
className,
value,
...props
}: React.ComponentProps<typeof ProgressPrimitive.Root>) {
return (
<ProgressPrimitive.Root
data-slot="progress"
className={cn(
"bg-primary/20 relative h-2 w-full overflow-hidden rounded-full",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
data-slot="progress-indicator"
className="bg-primary h-full w-full flex-1 transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
)
}
export { Progress }

View File

@@ -0,0 +1,45 @@
"use client"
import * as React from "react"
import * as RadioGroupPrimitive from "@radix-ui/react-radio-group"
import { CircleIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function RadioGroup({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Root>) {
return (
<RadioGroupPrimitive.Root
data-slot="radio-group"
className={cn("grid gap-3", className)}
{...props}
/>
)
}
function RadioGroupItem({
className,
...props
}: React.ComponentProps<typeof RadioGroupPrimitive.Item>) {
return (
<RadioGroupPrimitive.Item
data-slot="radio-group-item"
className={cn(
"border-input text-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 aspect-square size-4 shrink-0 rounded-full border shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
>
<RadioGroupPrimitive.Indicator
data-slot="radio-group-indicator"
className="relative flex items-center justify-center"
>
<CircleIcon className="fill-primary absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2" />
</RadioGroupPrimitive.Indicator>
</RadioGroupPrimitive.Item>
)
}
export { RadioGroup, RadioGroupItem }

View File

@@ -0,0 +1,188 @@
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function Select({
...props
}: React.ComponentProps<typeof SelectPrimitive.Root>) {
return <SelectPrimitive.Root data-slot="select" {...props} />
}
function SelectGroup({
...props
}: React.ComponentProps<typeof SelectPrimitive.Group>) {
return <SelectPrimitive.Group data-slot="select-group" {...props} />
}
function SelectValue({
...props
}: React.ComponentProps<typeof SelectPrimitive.Value>) {
return <SelectPrimitive.Value data-slot="select-value" {...props} />
}
function SelectTrigger({
className,
size = "default",
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Trigger> & {
size?: "sm" | "default"
}) {
return (
<SelectPrimitive.Trigger
data-slot="select-trigger"
data-size={size}
className={cn(
"border-input data-[placeholder]:text-muted-foreground [&_svg:not([class*='text-'])]:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 dark:hover:bg-input/50 flex w-fit items-center justify-between gap-2 rounded-md border bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDownIcon className="size-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
)
}
function SelectContent({
className,
children,
position = "item-aligned",
align = "center",
...props
}: React.ComponentProps<typeof SelectPrimitive.Content>) {
return (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
data-slot="select-content"
className={cn(
"bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border shadow-md",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
align={align}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)] scroll-my-1"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
)
}
function SelectLabel({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Label>) {
return (
<SelectPrimitive.Label
data-slot="select-label"
className={cn("text-muted-foreground px-2 py-1.5 text-xs", className)}
{...props}
/>
)
}
function SelectItem({
className,
children,
...props
}: React.ComponentProps<typeof SelectPrimitive.Item>) {
return (
<SelectPrimitive.Item
data-slot="select-item"
className={cn(
"focus:bg-accent focus:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 text-sm outline-hidden select-none data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
className
)}
{...props}
>
<span
data-slot="select-item-indicator"
className="absolute right-2 flex size-3.5 items-center justify-center"
>
<SelectPrimitive.ItemIndicator>
<CheckIcon className="size-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
)
}
function SelectSeparator({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.Separator>) {
return (
<SelectPrimitive.Separator
data-slot="select-separator"
className={cn("bg-border pointer-events-none -mx-1 my-1 h-px", className)}
{...props}
/>
)
}
function SelectScrollUpButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollUpButton>) {
return (
<SelectPrimitive.ScrollUpButton
data-slot="select-scroll-up-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUpIcon className="size-4" />
</SelectPrimitive.ScrollUpButton>
)
}
function SelectScrollDownButton({
className,
...props
}: React.ComponentProps<typeof SelectPrimitive.ScrollDownButton>) {
return (
<SelectPrimitive.ScrollDownButton
data-slot="select-scroll-down-button"
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDownIcon className="size-4" />
</SelectPrimitive.ScrollDownButton>
)
}
export {
Select,
SelectContent,
SelectGroup,
SelectItem,
SelectLabel,
SelectScrollDownButton,
SelectScrollUpButton,
SelectSeparator,
SelectTrigger,
SelectValue,
}

114
src/components/ui/table.tsx Normal file
View File

@@ -0,0 +1,114 @@
import type * as React from 'react';
import { cn } from '@/lib/utils';
function Table({ className, ...props }: React.ComponentProps<'table'>) {
return (
<div
data-slot="table-container"
className="relative w-full overflow-x-auto"
>
<table
data-slot="table"
className={cn('w-full caption-bottom text-sm', className)}
{...props}
/>
</div>
);
}
function TableHeader({ className, ...props }: React.ComponentProps<'thead'>) {
return (
<thead
data-slot="table-header"
className={cn('[&_tr]:border-b', className)}
{...props}
/>
);
}
function TableBody({ className, ...props }: React.ComponentProps<'tbody'>) {
return (
<tbody
data-slot="table-body"
className={cn('[&_tr:last-child]:border-0', className)}
{...props}
/>
);
}
function TableFooter({ className, ...props }: React.ComponentProps<'tfoot'>) {
return (
<tfoot
data-slot="table-footer"
className={cn(
'bg-muted/50 border-t font-medium [&>tr]:last:border-b-0',
className,
)}
{...props}
/>
);
}
function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
return (
<tr
data-slot="table-row"
className={cn(
'hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors',
className,
)}
{...props}
/>
);
}
function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
return (
<th
data-slot="table-head"
className={cn(
'text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
);
}
function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
return (
<td
data-slot="table-cell"
className={cn(
'p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
className,
)}
{...props}
/>
);
}
function TableCaption({
className,
...props
}: React.ComponentProps<'caption'>) {
return (
<caption
data-slot="table-caption"
className={cn('text-muted-foreground mt-4 text-sm', className)}
{...props}
/>
);
}
export {
Table,
TableHeader,
TableBody,
TableFooter,
TableHead,
TableRow,
TableCell,
TableCaption,
};

View File

@@ -0,0 +1,64 @@
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
function Tabs({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
return (
<TabsPrimitive.Root
data-slot="tabs"
className={cn("flex flex-col gap-2", className)}
{...props}
/>
)
}
function TabsList({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.List>) {
return (
<TabsPrimitive.List
data-slot="tabs-list"
className={cn(
"bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]",
className
)}
{...props}
/>
)
}
function TabsTrigger({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
return (
<TabsPrimitive.Trigger
data-slot="tabs-trigger"
className={cn(
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function TabsContent({
className,
...props
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
return (
<TabsPrimitive.Content
data-slot="tabs-content"
className={cn("flex-1 outline-none", className)}
{...props}
/>
)
}
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@@ -0,0 +1,59 @@
import * as React from "react"
import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
function TooltipProvider({
delayDuration = 0,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return (
<TooltipPrimitive.Provider
data-slot="tooltip-provider"
delayDuration={delayDuration}
{...props}
/>
)
}
function Tooltip({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
function TooltipTrigger({
...props
}: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-foreground text-background animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-xs text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow className="bg-foreground fill-foreground z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px]" />
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

28
src/lib/server/db.ts Normal file
View File

@@ -0,0 +1,28 @@
import { PrismaPg } from '@prisma/adapter-pg';
import { PrismaClient } from '@prisma/client';
import { Pool } from 'pg';
const globalForPrisma = globalThis as unknown as {
prisma: PrismaClient | undefined;
};
function createPrismaClient() {
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
const adapter = new PrismaPg(pool);
return new PrismaClient({
adapter,
log:
process.env.NODE_ENV === 'development'
? ['query', 'error', 'warn']
: ['error'],
});
}
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
if (process.env.NODE_ENV !== 'production') {
globalForPrisma.prisma = prisma;
}

6
src/lib/utils.ts Normal file
View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from 'clsx';
import { twMerge } from 'tailwind-merge';
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

12
src/logo.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 19 KiB

241
src/routeTree.gen.ts Normal file
View File

@@ -0,0 +1,241 @@
/* eslint-disable */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// This file was automatically generated by TanStack Router.
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root';
import { Route as ApiHealthRouteImport } from './routes/api/health';
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names';
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request';
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs';
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only';
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr';
import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index';
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode';
import { Route as IndexRouteImport } from './routes/index';
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
getParentRoute: () => rootRouteImport,
} as any);
const ApiHealthRoute = ApiHealthRouteImport.update({
id: '/api/health',
path: '/api/health',
getParentRoute: () => rootRouteImport,
} as any);
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
id: '/demo/start/server-funcs',
path: '/demo/start/server-funcs',
getParentRoute: () => rootRouteImport,
} as any);
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
id: '/demo/start/api-request',
path: '/demo/start/api-request',
getParentRoute: () => rootRouteImport,
} as any);
const DemoApiNamesRoute = DemoApiNamesRouteImport.update({
id: '/demo/api/names',
path: '/demo/api/names',
getParentRoute: () => rootRouteImport,
} as any);
const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({
id: '/demo/start/ssr/',
path: '/demo/start/ssr/',
getParentRoute: () => rootRouteImport,
} as any);
const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({
id: '/demo/start/ssr/spa-mode',
path: '/demo/start/ssr/spa-mode',
getParentRoute: () => rootRouteImport,
} as any);
const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({
id: '/demo/start/ssr/full-ssr',
path: '/demo/start/ssr/full-ssr',
getParentRoute: () => rootRouteImport,
} as any);
const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
id: '/demo/start/ssr/data-only',
path: '/demo/start/ssr/data-only',
getParentRoute: () => rootRouteImport,
} as any);
export interface FileRoutesByFullPath {
'/': typeof IndexRoute;
'/api/health': typeof ApiHealthRoute;
'/demo/api/names': typeof DemoApiNamesRoute;
'/demo/start/api-request': typeof DemoStartApiRequestRoute;
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute;
}
export interface FileRoutesByTo {
'/': typeof IndexRoute;
'/api/health': typeof ApiHealthRoute;
'/demo/api/names': typeof DemoApiNamesRoute;
'/demo/start/api-request': typeof DemoStartApiRequestRoute;
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
'/demo/start/ssr': typeof DemoStartSsrIndexRoute;
}
export interface FileRoutesById {
__root__: typeof rootRouteImport;
'/': typeof IndexRoute;
'/api/health': typeof ApiHealthRoute;
'/demo/api/names': typeof DemoApiNamesRoute;
'/demo/start/api-request': typeof DemoStartApiRequestRoute;
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
| '/'
| '/api/health'
| '/demo/api/names'
| '/demo/start/api-request'
| '/demo/start/server-funcs'
| '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode'
| '/demo/start/ssr/';
fileRoutesByTo: FileRoutesByTo;
to:
| '/'
| '/api/health'
| '/demo/api/names'
| '/demo/start/api-request'
| '/demo/start/server-funcs'
| '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode'
| '/demo/start/ssr';
id:
| '__root__'
| '/'
| '/api/health'
| '/demo/api/names'
| '/demo/start/api-request'
| '/demo/start/server-funcs'
| '/demo/start/ssr/data-only'
| '/demo/start/ssr/full-ssr'
| '/demo/start/ssr/spa-mode'
| '/demo/start/ssr/';
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute;
ApiHealthRoute: typeof ApiHealthRoute;
DemoApiNamesRoute: typeof DemoApiNamesRoute;
DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute;
DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute;
DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute;
DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute;
DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute;
DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute;
}
declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/': {
id: '/';
path: '/';
fullPath: '/';
preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport;
};
'/api/health': {
id: '/api/health';
path: '/api/health';
fullPath: '/api/health';
preLoaderRoute: typeof ApiHealthRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/start/server-funcs': {
id: '/demo/start/server-funcs';
path: '/demo/start/server-funcs';
fullPath: '/demo/start/server-funcs';
preLoaderRoute: typeof DemoStartServerFuncsRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/start/api-request': {
id: '/demo/start/api-request';
path: '/demo/start/api-request';
fullPath: '/demo/start/api-request';
preLoaderRoute: typeof DemoStartApiRequestRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/api/names': {
id: '/demo/api/names';
path: '/demo/api/names';
fullPath: '/demo/api/names';
preLoaderRoute: typeof DemoApiNamesRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/start/ssr/': {
id: '/demo/start/ssr/';
path: '/demo/start/ssr';
fullPath: '/demo/start/ssr/';
preLoaderRoute: typeof DemoStartSsrIndexRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/start/ssr/spa-mode': {
id: '/demo/start/ssr/spa-mode';
path: '/demo/start/ssr/spa-mode';
fullPath: '/demo/start/ssr/spa-mode';
preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/start/ssr/full-ssr': {
id: '/demo/start/ssr/full-ssr';
path: '/demo/start/ssr/full-ssr';
fullPath: '/demo/start/ssr/full-ssr';
preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport;
parentRoute: typeof rootRouteImport;
};
'/demo/start/ssr/data-only': {
id: '/demo/start/ssr/data-only';
path: '/demo/start/ssr/data-only';
fullPath: '/demo/start/ssr/data-only';
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport;
parentRoute: typeof rootRouteImport;
};
}
}
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
ApiHealthRoute: ApiHealthRoute,
DemoApiNamesRoute: DemoApiNamesRoute,
DemoStartApiRequestRoute: DemoStartApiRequestRoute,
DemoStartServerFuncsRoute: DemoStartServerFuncsRoute,
DemoStartSsrDataOnlyRoute: DemoStartSsrDataOnlyRoute,
DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute,
DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute,
DemoStartSsrIndexRoute: DemoStartSsrIndexRoute,
};
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>();
import type { createStart } from '@tanstack/react-start';
import type { getRouter } from './router.tsx';
declare module '@tanstack/react-start' {
interface Register {
ssr: true;
router: Awaited<ReturnType<typeof getRouter>>;
}
}

17
src/router.tsx Normal file
View File

@@ -0,0 +1,17 @@
import { createRouter } from '@tanstack/react-router';
// Import the generated route tree
import { routeTree } from './routeTree.gen';
// Create a new router instance
export const getRouter = () => {
const router = createRouter({
routeTree,
context: {},
scrollRestoration: true,
defaultPreloadStaleTime: 0,
});
return router;
};

View File

@@ -1,5 +1,55 @@
import { AppShell } from "@/components/layout/app-shell";
import { TanStackDevtools } from '@tanstack/react-devtools';
import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
export default function RootLayout({ children }) {
return <AppShell>{children}</AppShell>;
import appCss from '../app/globals.css?url';
export const Route = createRootRoute({
head: () => ({
meta: [
{
charSet: 'utf-8',
},
{
name: 'viewport',
content: 'width=device-width, initial-scale=1',
},
{
title: 'Dofus Manager',
},
],
links: [
{
rel: 'stylesheet',
href: appCss,
},
],
}),
shellComponent: RootDocument,
});
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html lang="en">
<head>
<HeadContent />
</head>
<body>
{children}
<TanStackDevtools
config={{
position: 'bottom-right',
}}
plugins={[
{
name: 'Tanstack Router',
render: <TanStackRouterDevtoolsPanel />,
},
]}
/>
<Scripts />
</body>
</html>
);
}

30
src/routes/api/health.tsx Normal file
View File

@@ -0,0 +1,30 @@
import { createFileRoute } from '@tanstack/react-router';
import { createServerFn } from '@tanstack/react-start';
import { prisma } from '@/lib/server/db';
const checkHealth = createServerFn({ method: 'GET' }).handler(async () => {
let dbStatus = 'disconnected';
try {
await prisma.$queryRaw`SELECT 1`;
dbStatus = 'connected';
} catch {
dbStatus = 'error';
}
return {
status: 'ok',
timestamp: new Date().toISOString(),
database: dbStatus,
};
});
export const Route = createFileRoute('/api/health')({
loader: () => checkHealth(),
component: HealthPage,
});
function HealthPage() {
const data = Route.useLoaderData();
return <pre>{JSON.stringify(data, null, 2)}</pre>;
}

87
src/routes/index.tsx Normal file
View File

@@ -0,0 +1,87 @@
import { createFileRoute } from '@tanstack/react-router';
import { Server, Swords, User, Users } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from '@/components/ui/card';
export const Route = createFileRoute('/')({ component: HomePage });
function HomePage() {
return (
<div className="min-h-screen bg-background">
<div className="container mx-auto px-4 py-16">
<header className="text-center mb-16">
<h1 className="text-5xl font-bold text-foreground mb-4">
Dofus Manager
</h1>
<p className="text-xl text-muted-foreground">
Gérez vos personnages, comptes et équipes Dofus
</p>
</header>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-16">
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
<User className="w-10 h-10 text-primary mb-2" />
<CardTitle>Personnages</CardTitle>
<CardDescription>Gérez tous vos personnages</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" className="w-full">
Voir les personnages
</Button>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
<Users className="w-10 h-10 text-primary mb-2" />
<CardTitle>Comptes</CardTitle>
<CardDescription>Organisez vos comptes Dofus</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" className="w-full">
Voir les comptes
</Button>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
<Swords className="w-10 h-10 text-primary mb-2" />
<CardTitle>Équipes</CardTitle>
<CardDescription>Composez vos équipes</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" className="w-full">
Voir les équipes
</Button>
</CardContent>
</Card>
<Card className="hover:shadow-lg transition-shadow">
<CardHeader>
<Server className="w-10 h-10 text-primary mb-2" />
<CardTitle>Serveurs</CardTitle>
<CardDescription>Vos serveurs de jeu</CardDescription>
</CardHeader>
<CardContent>
<Button variant="outline" className="w-full">
Voir les serveurs
</Button>
</CardContent>
</Card>
</div>
<footer className="text-center text-muted-foreground">
<p>Dofus Manager - Gérez votre aventure</p>
</footer>
</div>
</div>
);
}

14
src/styles.css Normal file
View File

@@ -0,0 +1,14 @@
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen",
"Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue",
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New",
monospace;
}

View File

@@ -0,0 +1,7 @@
import { describe, expect, it } from 'vitest';
describe('Example', () => {
it('should pass', () => {
expect(1 + 1).toBe(2);
});
});

28
tsconfig.json Normal file
View File

@@ -0,0 +1,28 @@
{
"include": ["**/*.ts", "**/*.tsx"],
"compilerOptions": {
"target": "ES2022",
"jsx": "react-jsx",
"module": "ESNext",
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": false,
"noEmit": true,
/* Linting */
"skipLibCheck": true,
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
}
}

28
vite.config.ts Normal file
View File

@@ -0,0 +1,28 @@
import { defineConfig } from 'vite'
import { devtools } from '@tanstack/devtools-vite'
import { tanstackStart } from '@tanstack/react-start/plugin/vite'
import viteReact from '@vitejs/plugin-react'
import viteTsConfigPaths from 'vite-tsconfig-paths'
import { fileURLToPath, URL } from 'url'
import { nitro } from 'nitro/vite'
const config = defineConfig({
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),
},
},
plugins: [
devtools(),
nitro(),
// this is the plugin that enables path aliases
viteTsConfigPaths({
projects: ['./tsconfig.json'],
}),
tanstackStart(),
viteReact(),
],
})
export default config