Compare commits
5 Commits
d470eb2cd5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d552d31f4a | ||
|
|
a22fed0f9d | ||
|
|
f6621200cd | ||
|
|
54abb81a07 | ||
|
|
1045aea277 |
@@ -1,135 +0,0 @@
|
||||
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: 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 }}/dofus-manager:${{ github.sha }}
|
||||
${{ vars.REGISTRY_URL }}/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..."
|
||||
# SSH ou webhook vers votre serveur 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 }}"
|
||||
# Votre logique de déploiement ici
|
||||
146
.gitea/workflows/ci.yml
Normal file
146
.gitea/workflows/ci.yml
Normal 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
|
||||
355
README.md
355
README.md
@@ -1,290 +1,93 @@
|
||||
Welcome to your new TanStack app!
|
||||
# Dofus Manager
|
||||
|
||||
# Getting Started
|
||||
Application de gestion de personnages, comptes et équipes pour Dofus.
|
||||
|
||||
To run this application:
|
||||
## 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
|
||||
```
|
||||
|
||||
# Building For Production
|
||||
L'application est accessible sur `http://localhost:3000`
|
||||
|
||||
To build this application for production:
|
||||
## 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
|
||||
pnpm build
|
||||
# Build l'image
|
||||
docker build -f docker/Dockerfile -t dofus-manager .
|
||||
|
||||
# Lancer en production
|
||||
docker compose -f docker/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
## Testing
|
||||
## Tech Stack
|
||||
|
||||
This project uses [Vitest](https://vitest.dev/) for testing. You can run the tests with:
|
||||
|
||||
```bash
|
||||
pnpm test
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
This project uses CSS for styling.
|
||||
|
||||
|
||||
|
||||
|
||||
## Routing
|
||||
This project uses [TanStack Router](https://tanstack.com/router). The initial setup is a file based router. Which means that the routes are managed as files in `src/routes`.
|
||||
|
||||
### Adding A Route
|
||||
|
||||
To add a new route to your application just add another a new file in the `./src/routes` directory.
|
||||
|
||||
TanStack will automatically generate the content of the route file for you.
|
||||
|
||||
Now that you have two routes you can use a `Link` component to navigate between them.
|
||||
|
||||
### Adding Links
|
||||
|
||||
To use SPA (Single Page Application) navigation you will need to import the `Link` component from `@tanstack/react-router`.
|
||||
|
||||
```tsx
|
||||
import { Link } from "@tanstack/react-router";
|
||||
```
|
||||
|
||||
Then anywhere in your JSX you can use it like so:
|
||||
|
||||
```tsx
|
||||
<Link to="/about">About</Link>
|
||||
```
|
||||
|
||||
This will create a link that will navigate to the `/about` route.
|
||||
|
||||
More information on the `Link` component can be found in the [Link documentation](https://tanstack.com/router/v1/docs/framework/react/api/router/linkComponent).
|
||||
|
||||
### Using A Layout
|
||||
|
||||
In the File Based Routing setup the layout is located in `src/routes/__root.tsx`. Anything you add to the root route will appear in all the routes. The route content will appear in the JSX where you use the `<Outlet />` component.
|
||||
|
||||
Here is an example layout that includes a header:
|
||||
|
||||
```tsx
|
||||
import { Outlet, createRootRoute } from '@tanstack/react-router'
|
||||
import { TanStackRouterDevtools } from '@tanstack/react-router-devtools'
|
||||
|
||||
import { Link } from "@tanstack/react-router";
|
||||
|
||||
export const Route = createRootRoute({
|
||||
component: () => (
|
||||
<>
|
||||
<header>
|
||||
<nav>
|
||||
<Link to="/">Home</Link>
|
||||
<Link to="/about">About</Link>
|
||||
</nav>
|
||||
</header>
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
),
|
||||
})
|
||||
```
|
||||
|
||||
The `<TanStackRouterDevtools />` component is not required so you can remove it if you don't want it in your layout.
|
||||
|
||||
More information on layouts can be found in the [Layouts documentation](https://tanstack.com/router/latest/docs/framework/react/guide/routing-concepts#layouts).
|
||||
|
||||
|
||||
## Data Fetching
|
||||
|
||||
There are multiple ways to fetch data in your application. You can use TanStack Query to fetch data from a server. But you can also use the `loader` functionality built into TanStack Router to load the data for a route before it's rendered.
|
||||
|
||||
For example:
|
||||
|
||||
```tsx
|
||||
const peopleRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: "/people",
|
||||
loader: async () => {
|
||||
const response = await fetch("https://swapi.dev/api/people");
|
||||
return response.json() as Promise<{
|
||||
results: {
|
||||
name: string;
|
||||
}[];
|
||||
}>;
|
||||
},
|
||||
component: () => {
|
||||
const data = peopleRoute.useLoaderData();
|
||||
return (
|
||||
<ul>
|
||||
{data.results.map((person) => (
|
||||
<li key={person.name}>{person.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
);
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
Loaders simplify your data fetching logic dramatically. Check out more information in the [Loader documentation](https://tanstack.com/router/latest/docs/framework/react/guide/data-loading#loader-parameters).
|
||||
|
||||
### React-Query
|
||||
|
||||
React-Query is an excellent addition or alternative to route loading and integrating it into you application is a breeze.
|
||||
|
||||
First add your dependencies:
|
||||
|
||||
```bash
|
||||
pnpm add @tanstack/react-query @tanstack/react-query-devtools
|
||||
```
|
||||
|
||||
Next we'll need to create a query client and provider. We recommend putting those in `main.tsx`.
|
||||
|
||||
```tsx
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
|
||||
// ...
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
// ...
|
||||
|
||||
if (!rootElement.innerHTML) {
|
||||
const root = ReactDOM.createRoot(rootElement);
|
||||
|
||||
root.render(
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<RouterProvider router={router} />
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
You can also add TanStack Query Devtools to the root route (optional).
|
||||
|
||||
```tsx
|
||||
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: () => (
|
||||
<>
|
||||
<Outlet />
|
||||
<ReactQueryDevtools buttonPosition="top-right" />
|
||||
<TanStackRouterDevtools />
|
||||
</>
|
||||
),
|
||||
});
|
||||
```
|
||||
|
||||
Now you can use `useQuery` to fetch your data.
|
||||
|
||||
```tsx
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
|
||||
import "./App.css";
|
||||
|
||||
function App() {
|
||||
const { data } = useQuery({
|
||||
queryKey: ["people"],
|
||||
queryFn: () =>
|
||||
fetch("https://swapi.dev/api/people")
|
||||
.then((res) => res.json())
|
||||
.then((data) => data.results as { name: string }[]),
|
||||
initialData: [],
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ul>
|
||||
{data.map((person) => (
|
||||
<li key={person.name}>{person.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
You can find out everything you need to know on how to use React-Query in the [React-Query documentation](https://tanstack.com/query/latest/docs/framework/react/overview).
|
||||
|
||||
## State Management
|
||||
|
||||
Another common requirement for React applications is state management. There are many options for state management in React. TanStack Store provides a great starting point for your project.
|
||||
|
||||
First you need to add TanStack Store as a dependency:
|
||||
|
||||
```bash
|
||||
pnpm add @tanstack/store
|
||||
```
|
||||
|
||||
Now let's create a simple counter in the `src/App.tsx` file as a demonstration.
|
||||
|
||||
```tsx
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Store } from "@tanstack/store";
|
||||
import "./App.css";
|
||||
|
||||
const countStore = new Store(0);
|
||||
|
||||
function App() {
|
||||
const count = useStore(countStore);
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => countStore.setState((n) => n + 1)}>
|
||||
Increment - {count}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
One of the many nice features of TanStack Store is the ability to derive state from other state. That derived state will update when the base state updates.
|
||||
|
||||
Let's check this out by doubling the count using derived state.
|
||||
|
||||
```tsx
|
||||
import { useStore } from "@tanstack/react-store";
|
||||
import { Store, Derived } from "@tanstack/store";
|
||||
import "./App.css";
|
||||
|
||||
const countStore = new Store(0);
|
||||
|
||||
const doubledStore = new Derived({
|
||||
fn: () => countStore.state * 2,
|
||||
deps: [countStore],
|
||||
});
|
||||
doubledStore.mount();
|
||||
|
||||
function App() {
|
||||
const count = useStore(countStore);
|
||||
const doubledCount = useStore(doubledStore);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button onClick={() => countStore.setState((n) => n + 1)}>
|
||||
Increment - {count}
|
||||
</button>
|
||||
<div>Doubled - {doubledCount}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
We use the `Derived` class to create a new store that is derived from another store. The `Derived` class has a `mount` method that will start the derived store updating.
|
||||
|
||||
Once we've created the derived store we can use it in the `App` component just like we would any other store using the `useStore` hook.
|
||||
|
||||
You can find out everything you need to know on how to use TanStack Store in the [TanStack Store documentation](https://tanstack.com/store/latest).
|
||||
|
||||
# Demo files
|
||||
|
||||
Files prefixed with `demo` can be safely deleted. They are there to provide a starting point for you to play around with the features you've installed.
|
||||
|
||||
# Learn More
|
||||
|
||||
You can learn more about all of the offerings from TanStack in the [TanStack documentation](https://tanstack.com).
|
||||
- **Frontend:** React 19, TanStack Router, TanStack Query, Tailwind CSS, shadcn/ui
|
||||
- **Backend:** TanStack Start, Prisma
|
||||
- **Database:** PostgreSQL 16
|
||||
- **DevOps:** Docker, Gitea Actions
|
||||
|
||||
@@ -61,39 +61,39 @@ Draft
|
||||
- [x] Add lint and format scripts to `package.json`
|
||||
- [x] Verify linting works on project files
|
||||
|
||||
- [ ] Task 6: Setup Gitea Actions workflow (AC: 6)
|
||||
- [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
|
||||
- [ ] Add caching for pnpm store
|
||||
- [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: Create health check endpoint (AC: 10)
|
||||
- [ ] Create `src/routes/api/health.ts` server function
|
||||
- [ ] Return JSON `{ status: "ok", timestamp: Date }`
|
||||
- [ ] Optionally check database connectivity
|
||||
- [ ] Verify endpoint responds at `GET /api/health`
|
||||
- [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`
|
||||
|
||||
- [ ] Task 10: 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 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
|
||||
|
||||
|
||||
12
package.json
12
package.json
@@ -13,8 +13,18 @@
|
||||
"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",
|
||||
@@ -26,6 +36,7 @@
|
||||
"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",
|
||||
@@ -38,6 +49,7 @@
|
||||
"@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",
|
||||
|
||||
1140
pnpm-lock.yaml
generated
1140
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
46
src/components/ui/badge.tsx
Normal file
46
src/components/ui/badge.tsx
Normal 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 }
|
||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal 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 }
|
||||
33
src/components/ui/collapsible.tsx
Normal file
33
src/components/ui/collapsible.tsx
Normal 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 }
|
||||
143
src/components/ui/dialog.tsx
Normal file
143
src/components/ui/dialog.tsx
Normal 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,
|
||||
}
|
||||
255
src/components/ui/dropdown-menu.tsx
Normal file
255
src/components/ui/dropdown-menu.tsx
Normal 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,
|
||||
}
|
||||
31
src/components/ui/progress.tsx
Normal file
31
src/components/ui/progress.tsx
Normal 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 }
|
||||
45
src/components/ui/radio-group.tsx
Normal file
45
src/components/ui/radio-group.tsx
Normal 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 }
|
||||
188
src/components/ui/select.tsx
Normal file
188
src/components/ui/select.tsx
Normal 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,
|
||||
}
|
||||
64
src/components/ui/tabs.tsx
Normal file
64
src/components/ui/tabs.tsx
Normal 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 }
|
||||
59
src/components/ui/tooltip.tsx
Normal file
59
src/components/ui/tooltip.tsx
Normal 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 }
|
||||
@@ -1,13 +0,0 @@
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
|
||||
export const getPunkSongs = createServerFn({
|
||||
method: 'GET',
|
||||
}).handler(async () => [
|
||||
{ id: 1, name: 'Teenage Dirtbag', artist: 'Wheatus' },
|
||||
{ id: 2, name: 'Smells Like Teen Spirit', artist: 'Nirvana' },
|
||||
{ id: 3, name: 'The Middle', artist: 'Jimmy Eat World' },
|
||||
{ id: 4, name: 'My Own Worst Enemy', artist: 'Lit' },
|
||||
{ id: 5, name: 'Fat Lip', artist: 'Sum 41' },
|
||||
{ id: 6, name: 'All the Small Things', artist: 'blink-182' },
|
||||
{ id: 7, name: 'Beverly Hills', artist: 'Weezer' },
|
||||
]);
|
||||
@@ -1,17 +1,27 @@
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
import { Pool } from 'pg';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma =
|
||||
globalForPrisma.prisma ??
|
||||
new PrismaClient({
|
||||
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;
|
||||
|
||||
@@ -8,99 +8,99 @@
|
||||
// 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 IndexRouteImport } from './routes/index'
|
||||
import { Route as ApiHealthRouteImport } from './routes/api/health'
|
||||
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs'
|
||||
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request'
|
||||
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names'
|
||||
import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index'
|
||||
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode'
|
||||
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr'
|
||||
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only'
|
||||
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)
|
||||
} as any);
|
||||
const ApiHealthRoute = ApiHealthRouteImport.update({
|
||||
id: '/api/health',
|
||||
path: '/api/health',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
|
||||
id: '/demo/start/server-funcs',
|
||||
path: '/demo/start/server-funcs',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
|
||||
id: '/demo/start/api-request',
|
||||
path: '/demo/start/api-request',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoApiNamesRoute = DemoApiNamesRouteImport.update({
|
||||
id: '/demo/api/names',
|
||||
path: '/demo/api/names',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({
|
||||
id: '/demo/start/ssr/',
|
||||
path: '/demo/start/ssr/',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({
|
||||
id: '/demo/start/ssr/spa-mode',
|
||||
path: '/demo/start/ssr/spa-mode',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({
|
||||
id: '/demo/start/ssr/full-ssr',
|
||||
path: '/demo/start/ssr/full-ssr',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} as any);
|
||||
const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
|
||||
id: '/demo/start/ssr/data-only',
|
||||
path: '/demo/start/ssr/data-only',
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any)
|
||||
} 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
|
||||
'/': 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
|
||||
'/': 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
|
||||
__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
|
||||
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||
fullPaths:
|
||||
| '/'
|
||||
| '/api/health'
|
||||
@@ -110,8 +110,8 @@ export interface FileRouteTypes {
|
||||
| '/demo/start/ssr/data-only'
|
||||
| '/demo/start/ssr/full-ssr'
|
||||
| '/demo/start/ssr/spa-mode'
|
||||
| '/demo/start/ssr/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
| '/demo/start/ssr/';
|
||||
fileRoutesByTo: FileRoutesByTo;
|
||||
to:
|
||||
| '/'
|
||||
| '/api/health'
|
||||
@@ -121,7 +121,7 @@ export interface FileRouteTypes {
|
||||
| '/demo/start/ssr/data-only'
|
||||
| '/demo/start/ssr/full-ssr'
|
||||
| '/demo/start/ssr/spa-mode'
|
||||
| '/demo/start/ssr'
|
||||
| '/demo/start/ssr';
|
||||
id:
|
||||
| '__root__'
|
||||
| '/'
|
||||
@@ -132,86 +132,86 @@ export interface FileRouteTypes {
|
||||
| '/demo/start/ssr/data-only'
|
||||
| '/demo/start/ssr/full-ssr'
|
||||
| '/demo/start/ssr/spa-mode'
|
||||
| '/demo/start/ssr/'
|
||||
fileRoutesById: FileRoutesById
|
||||
| '/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
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
id: '/demo/start/ssr/data-only';
|
||||
path: '/demo/start/ssr/data-only';
|
||||
fullPath: '/demo/start/ssr/data-only';
|
||||
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -225,16 +225,17 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute,
|
||||
DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute,
|
||||
DemoStartSsrIndexRoute: DemoStartSsrIndexRoute,
|
||||
}
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
._addFileTypes<FileRouteTypes>()
|
||||
._addFileTypes<FileRouteTypes>();
|
||||
|
||||
import type { createStart } from '@tanstack/react-start';
|
||||
import type { getRouter } from './router.tsx';
|
||||
|
||||
import type { getRouter } from './router.tsx'
|
||||
import type { createStart } from '@tanstack/react-start'
|
||||
declare module '@tanstack/react-start' {
|
||||
interface Register {
|
||||
ssr: true
|
||||
router: Awaited<ReturnType<typeof getRouter>>
|
||||
ssr: true;
|
||||
router: Awaited<ReturnType<typeof getRouter>>;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ import { TanStackDevtools } from '@tanstack/react-devtools';
|
||||
import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
|
||||
|
||||
import Header from '../components/Header';
|
||||
|
||||
import appCss from '../styles.css?url';
|
||||
import appCss from '../app/globals.css?url';
|
||||
|
||||
export const Route = createRootRoute({
|
||||
head: () => ({
|
||||
@@ -17,7 +15,7 @@ export const Route = createRootRoute({
|
||||
content: 'width=device-width, initial-scale=1',
|
||||
},
|
||||
{
|
||||
title: 'TanStack Start Starter',
|
||||
title: 'Dofus Manager',
|
||||
},
|
||||
],
|
||||
links: [
|
||||
@@ -38,7 +36,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
||||
<HeadContent />
|
||||
</head>
|
||||
<body>
|
||||
<Header />
|
||||
{children}
|
||||
<TanStackDevtools
|
||||
config={{
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
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,
|
||||
})
|
||||
},
|
||||
});
|
||||
30
src/routes/api/health.tsx
Normal file
30
src/routes/api/health.tsx
Normal 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>;
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { json } from '@tanstack/react-start';
|
||||
|
||||
export const Route = createFileRoute('/demo/api/names')({
|
||||
server: {
|
||||
handlers: {
|
||||
GET: () => json(['Alice', 'Bob', 'Charlie']),
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -1,34 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import './start.css';
|
||||
|
||||
function getNames() {
|
||||
return fetch('/demo/api/names').then(
|
||||
(res) => res.json() as Promise<string[]>,
|
||||
);
|
||||
}
|
||||
|
||||
export const Route = createFileRoute('/demo/start/api-request')({
|
||||
component: Home,
|
||||
});
|
||||
|
||||
function Home() {
|
||||
const [names, setNames] = useState<Array<string>>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getNames().then(setNames);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="api-page">
|
||||
<div className="content">
|
||||
<h1>Start API Request Demo - Names List</h1>
|
||||
<ul>
|
||||
{names.map((name) => (
|
||||
<li key={name}>{name}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
.api-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 100vh;
|
||||
padding: 1rem;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.api-page .content {
|
||||
width: 100%;
|
||||
max-width: 2xl;
|
||||
padding: 8rem;
|
||||
border-radius: 1rem;
|
||||
backdrop-filter: blur(1rem);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
box-shadow: 0 0 1rem 0 rgba(0, 0, 0, 0.1);
|
||||
border: 0.5rem solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.api-page .content h1 {
|
||||
font-size: 2rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.api-page .content ul {
|
||||
margin-bottom: 1rem;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.api-page .content li {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
padding: 0.5rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||
backdrop-filter: blur(0.5rem);
|
||||
box-shadow: 0 0 0.5rem 0 rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.api-page .content li span {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import fs from 'node:fs';
|
||||
import { createFileRoute, useRouter } from '@tanstack/react-router';
|
||||
import { createServerFn } from '@tanstack/react-start';
|
||||
import { useCallback, useState } from 'react';
|
||||
import './start.css';
|
||||
|
||||
/*
|
||||
const loggingMiddleware = createMiddleware().server(
|
||||
async ({ next, request }) => {
|
||||
console.log("Request:", request.url);
|
||||
return next();
|
||||
}
|
||||
);
|
||||
const loggedServerFunction = createServerFn({ method: "GET" }).middleware([
|
||||
loggingMiddleware,
|
||||
]);
|
||||
*/
|
||||
|
||||
const TODOS_FILE = 'todos.json';
|
||||
|
||||
async function readTodos() {
|
||||
return JSON.parse(
|
||||
await fs.promises.readFile(TODOS_FILE, 'utf-8').catch(() =>
|
||||
JSON.stringify(
|
||||
[
|
||||
{ id: 1, name: 'Get groceries' },
|
||||
{ id: 2, name: 'Buy a new phone' },
|
||||
],
|
||||
null,
|
||||
2,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
const getTodos = createServerFn({
|
||||
method: 'GET',
|
||||
}).handler(async () => await readTodos());
|
||||
|
||||
const addTodo = createServerFn({ method: 'POST' })
|
||||
.inputValidator((d: string) => d)
|
||||
.handler(async ({ data }) => {
|
||||
const todos = await readTodos();
|
||||
todos.push({ id: todos.length + 1, name: data });
|
||||
await fs.promises.writeFile(TODOS_FILE, JSON.stringify(todos, null, 2));
|
||||
return todos;
|
||||
});
|
||||
|
||||
export const Route = createFileRoute('/demo/start/server-funcs')({
|
||||
component: Home,
|
||||
loader: async () => await getTodos(),
|
||||
});
|
||||
|
||||
function Home() {
|
||||
const router = useRouter();
|
||||
let todos = Route.useLoaderData();
|
||||
|
||||
const [todo, setTodo] = useState('');
|
||||
|
||||
const submitTodo = useCallback(async () => {
|
||||
todos = await addTodo({ data: todo });
|
||||
setTodo('');
|
||||
router.invalidate();
|
||||
}, [addTodo, todo]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Start Server Functions - Todo Example</h1>
|
||||
<ul>
|
||||
{todos?.map((t) => (
|
||||
<li key={t.id}>{t.name}</li>
|
||||
))}
|
||||
</ul>
|
||||
<div className="flex flex-col gap-2">
|
||||
<input
|
||||
type="text"
|
||||
value={todo}
|
||||
onChange={(e) => setTodo(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') {
|
||||
submitTodo();
|
||||
}
|
||||
}}
|
||||
placeholder="Enter a new todo..."
|
||||
/>
|
||||
<button disabled={todo.trim().length === 0} onClick={submitTodo}>
|
||||
Add todo
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { getPunkSongs } from '@/data/demo.punk-songs';
|
||||
|
||||
export const Route = createFileRoute('/demo/start/ssr/data-only')({
|
||||
ssr: 'data-only',
|
||||
component: RouteComponent,
|
||||
loader: async () => await getPunkSongs(),
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const punkSongs = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Data Only SSR - Punk Songs</h1>
|
||||
<ul>
|
||||
{punkSongs.map((song) => (
|
||||
<li key={song.id}>
|
||||
{song.name} - {song.artist}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { getPunkSongs } from '@/data/demo.punk-songs';
|
||||
|
||||
export const Route = createFileRoute('/demo/start/ssr/full-ssr')({
|
||||
component: RouteComponent,
|
||||
loader: async () => await getPunkSongs(),
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const punkSongs = Route.useLoaderData();
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>Full SSR - Punk Songs</h1>
|
||||
<ul>
|
||||
{punkSongs.map((song) => (
|
||||
<li key={song.id}>
|
||||
{song.name} - {song.artist}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
import { createFileRoute, Link } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/demo/start/ssr/')({
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div>
|
||||
<h1>SSR Demos</h1>
|
||||
<ul>
|
||||
<li>
|
||||
<Link to="/demo/start/ssr/spa-mode">SPA Mode</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/demo/start/ssr/full-ssr">Full SSR</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link to="/demo/start/ssr/data-only">Data Only</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getPunkSongs } from '@/data/demo.punk-songs';
|
||||
|
||||
export const Route = createFileRoute('/demo/start/ssr/spa-mode')({
|
||||
ssr: false,
|
||||
component: RouteComponent,
|
||||
});
|
||||
|
||||
function RouteComponent() {
|
||||
const [punkSongs, setPunkSongs] = useState<
|
||||
Awaited<ReturnType<typeof getPunkSongs>>
|
||||
>([]);
|
||||
|
||||
useEffect(() => {
|
||||
getPunkSongs().then(setPunkSongs);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h1>SPA Mode - Punk Songs</h1>
|
||||
<ul>
|
||||
{punkSongs.map((song) => (
|
||||
<li key={song.id}>
|
||||
{song.name} - {song.artist}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,37 +1,87 @@
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import '../App.css';
|
||||
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: App });
|
||||
export const Route = createFileRoute('/')({ component: HomePage });
|
||||
|
||||
function App() {
|
||||
function HomePage() {
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img
|
||||
src="/tanstack-circle-logo.png"
|
||||
className="App-logo"
|
||||
alt="TanStack Logo"
|
||||
/>
|
||||
<p>
|
||||
Edit <code>src/routes/index.tsx</code> and save to reload.
|
||||
<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>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://tanstack.com"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn TanStack
|
||||
</a>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user