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
|
```bash
|
||||||
|
# Cloner le repo
|
||||||
|
git clone <repo-url>
|
||||||
|
cd dofus-manager2
|
||||||
|
|
||||||
|
# Installer les dépendances
|
||||||
pnpm install
|
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
|
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
|
```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:
|
- **Frontend:** React 19, TanStack Router, TanStack Query, Tailwind CSS, shadcn/ui
|
||||||
|
- **Backend:** TanStack Start, Prisma
|
||||||
```bash
|
- **Database:** PostgreSQL 16
|
||||||
pnpm test
|
- **DevOps:** Docker, Gitea Actions
|
||||||
```
|
|
||||||
|
|
||||||
## 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).
|
|
||||||
|
|||||||
@@ -61,39 +61,39 @@ Draft
|
|||||||
- [x] Add lint and format scripts to `package.json`
|
- [x] Add lint and format scripts to `package.json`
|
||||||
- [x] Verify linting works on project files
|
- [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] Create `.gitea/workflows/ci.yml`
|
||||||
- [x] Configure test job: lint, typecheck, test
|
- [x] Configure test job: lint, typecheck, test
|
||||||
- [x] Configure build job: Docker image build and push
|
- [x] Configure build job: Docker image build and push
|
||||||
- [x] Configure deploy jobs (staging/production) with manual triggers
|
- [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)
|
- [x] Task 7: Create README documentation (AC: 8)
|
||||||
- [ ] Document project overview
|
- [x] Document project overview
|
||||||
- [ ] Document prerequisites (Node 20, pnpm, Docker)
|
- [x] Document prerequisites (Node 20, pnpm, Docker)
|
||||||
- [ ] Document local development setup steps
|
- [x] Document local development setup steps
|
||||||
- [ ] Document available npm scripts
|
- [x] Document available npm scripts
|
||||||
- [ ] Document environment variables
|
- [x] Document environment variables
|
||||||
|
|
||||||
- [ ] Task 8: Create home page (AC: 9)
|
- [x] Task 8: Create home page (AC: 9)
|
||||||
- [ ] Create `src/routes/index.tsx` as home page
|
- [x] Create `src/routes/index.tsx` as home page
|
||||||
- [ ] Display "Dofus Manager" title
|
- [x] Display "Dofus Manager" title
|
||||||
- [ ] Add basic layout structure
|
- [x] Add basic layout structure
|
||||||
- [ ] Create `src/styles/globals.css` with Tailwind imports
|
- [x] Create `src/styles/globals.css` with Tailwind imports
|
||||||
- [ ] Verify application starts and renders correctly
|
- [x] Verify application starts and renders correctly
|
||||||
|
|
||||||
- [ ] Task 9: Create health check endpoint (AC: 10)
|
- [x] Task 9: Create health check endpoint (AC: 10)
|
||||||
- [ ] Create `src/routes/api/health.ts` server function
|
- [x] Create `src/routes/api/health.ts` server function
|
||||||
- [ ] Return JSON `{ status: "ok", timestamp: Date }`
|
- [x] Return JSON `{ status: "ok", timestamp: Date }`
|
||||||
- [ ] Optionally check database connectivity
|
- [x] Optionally check database connectivity
|
||||||
- [ ] Verify endpoint responds at `GET /api/health`
|
- [x] Verify endpoint responds at `GET /api/health`
|
||||||
|
|
||||||
- [ ] Task 10: Final verification
|
- [x] Task 10: Final verification
|
||||||
- [ ] Run `pnpm dev` and verify app starts
|
- [x] Run `pnpm dev` and verify app starts
|
||||||
- [ ] Run `pnpm lint` and verify no errors
|
- [x] Run `pnpm lint` and verify no errors
|
||||||
- [ ] Run `pnpm typecheck` and verify no errors
|
- [x] Run `pnpm typecheck` and verify no errors
|
||||||
- [ ] Test Docker build locally
|
- [x] Test Docker build locally
|
||||||
- [ ] Verify PostgreSQL connection via Prisma
|
- [x] Verify PostgreSQL connection via Prisma
|
||||||
|
|
||||||
## Dev Notes
|
## Dev Notes
|
||||||
|
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -13,8 +13,18 @@
|
|||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@prisma/adapter-pg": "^7.2.0",
|
||||||
"@prisma/client": "^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-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-tabs": "^1.1.13",
|
||||||
|
"@radix-ui/react-tooltip": "^1.2.8",
|
||||||
"@tailwindcss/vite": "^4.1.18",
|
"@tailwindcss/vite": "^4.1.18",
|
||||||
"@tanstack/react-devtools": "^0.7.0",
|
"@tanstack/react-devtools": "^0.7.0",
|
||||||
"@tanstack/react-router": "^1.132.0",
|
"@tanstack/react-router": "^1.132.0",
|
||||||
@@ -26,6 +36,7 @@
|
|||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^0.561.0",
|
"lucide-react": "^0.561.0",
|
||||||
"nitro": "npm:nitro-nightly@latest",
|
"nitro": "npm:nitro-nightly@latest",
|
||||||
|
"pg": "^8.17.1",
|
||||||
"react": "^19.2.0",
|
"react": "^19.2.0",
|
||||||
"react-dom": "^19.2.0",
|
"react-dom": "^19.2.0",
|
||||||
"tailwind-merge": "^3.4.0",
|
"tailwind-merge": "^3.4.0",
|
||||||
@@ -38,6 +49,7 @@
|
|||||||
"@testing-library/dom": "^10.4.0",
|
"@testing-library/dom": "^10.4.0",
|
||||||
"@testing-library/react": "^16.2.0",
|
"@testing-library/react": "^16.2.0",
|
||||||
"@types/node": "^22.10.2",
|
"@types/node": "^22.10.2",
|
||||||
|
"@types/pg": "^8.16.0",
|
||||||
"@types/react": "^19.2.0",
|
"@types/react": "^19.2.0",
|
||||||
"@types/react-dom": "^19.2.0",
|
"@types/react-dom": "^19.2.0",
|
||||||
"@vitejs/plugin-react": "^5.0.4",
|
"@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 { PrismaClient } from '@prisma/client';
|
||||||
|
import { Pool } from 'pg';
|
||||||
|
|
||||||
const globalForPrisma = globalThis as unknown as {
|
const globalForPrisma = globalThis as unknown as {
|
||||||
prisma: PrismaClient | undefined;
|
prisma: PrismaClient | undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prisma =
|
function createPrismaClient() {
|
||||||
globalForPrisma.prisma ??
|
const pool = new Pool({
|
||||||
new PrismaClient({
|
connectionString: process.env.DATABASE_URL,
|
||||||
|
});
|
||||||
|
const adapter = new PrismaPg(pool);
|
||||||
|
|
||||||
|
return new PrismaClient({
|
||||||
|
adapter,
|
||||||
log:
|
log:
|
||||||
process.env.NODE_ENV === 'development'
|
process.env.NODE_ENV === 'development'
|
||||||
? ['query', 'error', 'warn']
|
? ['query', 'error', 'warn']
|
||||||
: ['error'],
|
: ['error'],
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production') {
|
if (process.env.NODE_ENV !== 'production') {
|
||||||
globalForPrisma.prisma = prisma;
|
globalForPrisma.prisma = prisma;
|
||||||
|
|||||||
@@ -8,99 +8,99 @@
|
|||||||
// You should NOT make any changes in this file as it will be overwritten.
|
// 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.
|
// 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 rootRouteImport } from './routes/__root';
|
||||||
import { Route as IndexRouteImport } from './routes/index'
|
import { Route as ApiHealthRouteImport } from './routes/api/health';
|
||||||
import { Route as ApiHealthRouteImport } from './routes/api/health'
|
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names';
|
||||||
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs'
|
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request';
|
||||||
import { Route as DemoStartApiRequestRouteImport } from './routes/demo/start.api-request'
|
import { Route as DemoStartServerFuncsRouteImport } from './routes/demo/start.server-funcs';
|
||||||
import { Route as DemoApiNamesRouteImport } from './routes/demo/api.names'
|
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only';
|
||||||
import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index'
|
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr';
|
||||||
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode'
|
import { Route as DemoStartSsrIndexRouteImport } from './routes/demo/start.ssr.index';
|
||||||
import { Route as DemoStartSsrFullSsrRouteImport } from './routes/demo/start.ssr.full-ssr'
|
import { Route as DemoStartSsrSpaModeRouteImport } from './routes/demo/start.ssr.spa-mode';
|
||||||
import { Route as DemoStartSsrDataOnlyRouteImport } from './routes/demo/start.ssr.data-only'
|
import { Route as IndexRouteImport } from './routes/index';
|
||||||
|
|
||||||
const IndexRoute = IndexRouteImport.update({
|
const IndexRoute = IndexRouteImport.update({
|
||||||
id: '/',
|
id: '/',
|
||||||
path: '/',
|
path: '/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const ApiHealthRoute = ApiHealthRouteImport.update({
|
const ApiHealthRoute = ApiHealthRouteImport.update({
|
||||||
id: '/api/health',
|
id: '/api/health',
|
||||||
path: '/api/health',
|
path: '/api/health',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
|
const DemoStartServerFuncsRoute = DemoStartServerFuncsRouteImport.update({
|
||||||
id: '/demo/start/server-funcs',
|
id: '/demo/start/server-funcs',
|
||||||
path: '/demo/start/server-funcs',
|
path: '/demo/start/server-funcs',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
|
const DemoStartApiRequestRoute = DemoStartApiRequestRouteImport.update({
|
||||||
id: '/demo/start/api-request',
|
id: '/demo/start/api-request',
|
||||||
path: '/demo/start/api-request',
|
path: '/demo/start/api-request',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoApiNamesRoute = DemoApiNamesRouteImport.update({
|
const DemoApiNamesRoute = DemoApiNamesRouteImport.update({
|
||||||
id: '/demo/api/names',
|
id: '/demo/api/names',
|
||||||
path: '/demo/api/names',
|
path: '/demo/api/names',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({
|
const DemoStartSsrIndexRoute = DemoStartSsrIndexRouteImport.update({
|
||||||
id: '/demo/start/ssr/',
|
id: '/demo/start/ssr/',
|
||||||
path: '/demo/start/ssr/',
|
path: '/demo/start/ssr/',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({
|
const DemoStartSsrSpaModeRoute = DemoStartSsrSpaModeRouteImport.update({
|
||||||
id: '/demo/start/ssr/spa-mode',
|
id: '/demo/start/ssr/spa-mode',
|
||||||
path: '/demo/start/ssr/spa-mode',
|
path: '/demo/start/ssr/spa-mode',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({
|
const DemoStartSsrFullSsrRoute = DemoStartSsrFullSsrRouteImport.update({
|
||||||
id: '/demo/start/ssr/full-ssr',
|
id: '/demo/start/ssr/full-ssr',
|
||||||
path: '/demo/start/ssr/full-ssr',
|
path: '/demo/start/ssr/full-ssr',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
|
const DemoStartSsrDataOnlyRoute = DemoStartSsrDataOnlyRouteImport.update({
|
||||||
id: '/demo/start/ssr/data-only',
|
id: '/demo/start/ssr/data-only',
|
||||||
path: '/demo/start/ssr/data-only',
|
path: '/demo/start/ssr/data-only',
|
||||||
getParentRoute: () => rootRouteImport,
|
getParentRoute: () => rootRouteImport,
|
||||||
} as any)
|
} as any);
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute;
|
||||||
'/api/health': typeof ApiHealthRoute
|
'/api/health': typeof ApiHealthRoute;
|
||||||
'/demo/api/names': typeof DemoApiNamesRoute
|
'/demo/api/names': typeof DemoApiNamesRoute;
|
||||||
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
'/demo/start/api-request': typeof DemoStartApiRequestRoute;
|
||||||
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
|
||||||
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
|
||||||
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
|
||||||
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
|
||||||
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute
|
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute;
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute;
|
||||||
'/api/health': typeof ApiHealthRoute
|
'/api/health': typeof ApiHealthRoute;
|
||||||
'/demo/api/names': typeof DemoApiNamesRoute
|
'/demo/api/names': typeof DemoApiNamesRoute;
|
||||||
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
'/demo/start/api-request': typeof DemoStartApiRequestRoute;
|
||||||
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
|
||||||
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
|
||||||
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
|
||||||
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
|
||||||
'/demo/start/ssr': typeof DemoStartSsrIndexRoute
|
'/demo/start/ssr': typeof DemoStartSsrIndexRoute;
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport;
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute;
|
||||||
'/api/health': typeof ApiHealthRoute
|
'/api/health': typeof ApiHealthRoute;
|
||||||
'/demo/api/names': typeof DemoApiNamesRoute
|
'/demo/api/names': typeof DemoApiNamesRoute;
|
||||||
'/demo/start/api-request': typeof DemoStartApiRequestRoute
|
'/demo/start/api-request': typeof DemoStartApiRequestRoute;
|
||||||
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute
|
'/demo/start/server-funcs': typeof DemoStartServerFuncsRoute;
|
||||||
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute
|
'/demo/start/ssr/data-only': typeof DemoStartSsrDataOnlyRoute;
|
||||||
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute
|
'/demo/start/ssr/full-ssr': typeof DemoStartSsrFullSsrRoute;
|
||||||
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute
|
'/demo/start/ssr/spa-mode': typeof DemoStartSsrSpaModeRoute;
|
||||||
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute
|
'/demo/start/ssr/': typeof DemoStartSsrIndexRoute;
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath;
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/api/health'
|
| '/api/health'
|
||||||
@@ -110,8 +110,8 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/start/ssr/data-only'
|
| '/demo/start/ssr/data-only'
|
||||||
| '/demo/start/ssr/full-ssr'
|
| '/demo/start/ssr/full-ssr'
|
||||||
| '/demo/start/ssr/spa-mode'
|
| '/demo/start/ssr/spa-mode'
|
||||||
| '/demo/start/ssr/'
|
| '/demo/start/ssr/';
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo;
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/api/health'
|
| '/api/health'
|
||||||
@@ -121,7 +121,7 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/start/ssr/data-only'
|
| '/demo/start/ssr/data-only'
|
||||||
| '/demo/start/ssr/full-ssr'
|
| '/demo/start/ssr/full-ssr'
|
||||||
| '/demo/start/ssr/spa-mode'
|
| '/demo/start/ssr/spa-mode'
|
||||||
| '/demo/start/ssr'
|
| '/demo/start/ssr';
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
@@ -132,86 +132,86 @@ export interface FileRouteTypes {
|
|||||||
| '/demo/start/ssr/data-only'
|
| '/demo/start/ssr/data-only'
|
||||||
| '/demo/start/ssr/full-ssr'
|
| '/demo/start/ssr/full-ssr'
|
||||||
| '/demo/start/ssr/spa-mode'
|
| '/demo/start/ssr/spa-mode'
|
||||||
| '/demo/start/ssr/'
|
| '/demo/start/ssr/';
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById;
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
IndexRoute: typeof IndexRoute
|
IndexRoute: typeof IndexRoute;
|
||||||
ApiHealthRoute: typeof ApiHealthRoute
|
ApiHealthRoute: typeof ApiHealthRoute;
|
||||||
DemoApiNamesRoute: typeof DemoApiNamesRoute
|
DemoApiNamesRoute: typeof DemoApiNamesRoute;
|
||||||
DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute
|
DemoStartApiRequestRoute: typeof DemoStartApiRequestRoute;
|
||||||
DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute
|
DemoStartServerFuncsRoute: typeof DemoStartServerFuncsRoute;
|
||||||
DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute
|
DemoStartSsrDataOnlyRoute: typeof DemoStartSsrDataOnlyRoute;
|
||||||
DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute
|
DemoStartSsrFullSsrRoute: typeof DemoStartSsrFullSsrRoute;
|
||||||
DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute
|
DemoStartSsrSpaModeRoute: typeof DemoStartSsrSpaModeRoute;
|
||||||
DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute
|
DemoStartSsrIndexRoute: typeof DemoStartSsrIndexRoute;
|
||||||
}
|
}
|
||||||
|
|
||||||
declare module '@tanstack/react-router' {
|
declare module '@tanstack/react-router' {
|
||||||
interface FileRoutesByPath {
|
interface FileRoutesByPath {
|
||||||
'/': {
|
'/': {
|
||||||
id: '/'
|
id: '/';
|
||||||
path: '/'
|
path: '/';
|
||||||
fullPath: '/'
|
fullPath: '/';
|
||||||
preLoaderRoute: typeof IndexRouteImport
|
preLoaderRoute: typeof IndexRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/api/health': {
|
'/api/health': {
|
||||||
id: '/api/health'
|
id: '/api/health';
|
||||||
path: '/api/health'
|
path: '/api/health';
|
||||||
fullPath: '/api/health'
|
fullPath: '/api/health';
|
||||||
preLoaderRoute: typeof ApiHealthRouteImport
|
preLoaderRoute: typeof ApiHealthRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/start/server-funcs': {
|
'/demo/start/server-funcs': {
|
||||||
id: '/demo/start/server-funcs'
|
id: '/demo/start/server-funcs';
|
||||||
path: '/demo/start/server-funcs'
|
path: '/demo/start/server-funcs';
|
||||||
fullPath: '/demo/start/server-funcs'
|
fullPath: '/demo/start/server-funcs';
|
||||||
preLoaderRoute: typeof DemoStartServerFuncsRouteImport
|
preLoaderRoute: typeof DemoStartServerFuncsRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/start/api-request': {
|
'/demo/start/api-request': {
|
||||||
id: '/demo/start/api-request'
|
id: '/demo/start/api-request';
|
||||||
path: '/demo/start/api-request'
|
path: '/demo/start/api-request';
|
||||||
fullPath: '/demo/start/api-request'
|
fullPath: '/demo/start/api-request';
|
||||||
preLoaderRoute: typeof DemoStartApiRequestRouteImport
|
preLoaderRoute: typeof DemoStartApiRequestRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/api/names': {
|
'/demo/api/names': {
|
||||||
id: '/demo/api/names'
|
id: '/demo/api/names';
|
||||||
path: '/demo/api/names'
|
path: '/demo/api/names';
|
||||||
fullPath: '/demo/api/names'
|
fullPath: '/demo/api/names';
|
||||||
preLoaderRoute: typeof DemoApiNamesRouteImport
|
preLoaderRoute: typeof DemoApiNamesRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/start/ssr/': {
|
'/demo/start/ssr/': {
|
||||||
id: '/demo/start/ssr/'
|
id: '/demo/start/ssr/';
|
||||||
path: '/demo/start/ssr'
|
path: '/demo/start/ssr';
|
||||||
fullPath: '/demo/start/ssr/'
|
fullPath: '/demo/start/ssr/';
|
||||||
preLoaderRoute: typeof DemoStartSsrIndexRouteImport
|
preLoaderRoute: typeof DemoStartSsrIndexRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/start/ssr/spa-mode': {
|
'/demo/start/ssr/spa-mode': {
|
||||||
id: '/demo/start/ssr/spa-mode'
|
id: '/demo/start/ssr/spa-mode';
|
||||||
path: '/demo/start/ssr/spa-mode'
|
path: '/demo/start/ssr/spa-mode';
|
||||||
fullPath: '/demo/start/ssr/spa-mode'
|
fullPath: '/demo/start/ssr/spa-mode';
|
||||||
preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport
|
preLoaderRoute: typeof DemoStartSsrSpaModeRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/start/ssr/full-ssr': {
|
'/demo/start/ssr/full-ssr': {
|
||||||
id: '/demo/start/ssr/full-ssr'
|
id: '/demo/start/ssr/full-ssr';
|
||||||
path: '/demo/start/ssr/full-ssr'
|
path: '/demo/start/ssr/full-ssr';
|
||||||
fullPath: '/demo/start/ssr/full-ssr'
|
fullPath: '/demo/start/ssr/full-ssr';
|
||||||
preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport
|
preLoaderRoute: typeof DemoStartSsrFullSsrRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
'/demo/start/ssr/data-only': {
|
'/demo/start/ssr/data-only': {
|
||||||
id: '/demo/start/ssr/data-only'
|
id: '/demo/start/ssr/data-only';
|
||||||
path: '/demo/start/ssr/data-only'
|
path: '/demo/start/ssr/data-only';
|
||||||
fullPath: '/demo/start/ssr/data-only'
|
fullPath: '/demo/start/ssr/data-only';
|
||||||
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport
|
preLoaderRoute: typeof DemoStartSsrDataOnlyRouteImport;
|
||||||
parentRoute: typeof rootRouteImport
|
parentRoute: typeof rootRouteImport;
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,16 +225,17 @@ const rootRouteChildren: RootRouteChildren = {
|
|||||||
DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute,
|
DemoStartSsrFullSsrRoute: DemoStartSsrFullSsrRoute,
|
||||||
DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute,
|
DemoStartSsrSpaModeRoute: DemoStartSsrSpaModeRoute,
|
||||||
DemoStartSsrIndexRoute: DemoStartSsrIndexRoute,
|
DemoStartSsrIndexRoute: DemoStartSsrIndexRoute,
|
||||||
}
|
};
|
||||||
export const routeTree = rootRouteImport
|
export const routeTree = rootRouteImport
|
||||||
._addFileChildren(rootRouteChildren)
|
._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' {
|
declare module '@tanstack/react-start' {
|
||||||
interface Register {
|
interface Register {
|
||||||
ssr: true
|
ssr: true;
|
||||||
router: Awaited<ReturnType<typeof getRouter>>
|
router: Awaited<ReturnType<typeof getRouter>>;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,7 @@ import { TanStackDevtools } from '@tanstack/react-devtools';
|
|||||||
import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
|
import { createRootRoute, HeadContent, Scripts } from '@tanstack/react-router';
|
||||||
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
|
import { TanStackRouterDevtoolsPanel } from '@tanstack/react-router-devtools';
|
||||||
|
|
||||||
import Header from '../components/Header';
|
import appCss from '../app/globals.css?url';
|
||||||
|
|
||||||
import appCss from '../styles.css?url';
|
|
||||||
|
|
||||||
export const Route = createRootRoute({
|
export const Route = createRootRoute({
|
||||||
head: () => ({
|
head: () => ({
|
||||||
@@ -17,7 +15,7 @@ export const Route = createRootRoute({
|
|||||||
content: 'width=device-width, initial-scale=1',
|
content: 'width=device-width, initial-scale=1',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: 'TanStack Start Starter',
|
title: 'Dofus Manager',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
links: [
|
links: [
|
||||||
@@ -38,7 +36,6 @@ function RootDocument({ children }: { children: React.ReactNode }) {
|
|||||||
<HeadContent />
|
<HeadContent />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<Header />
|
|
||||||
{children}
|
{children}
|
||||||
<TanStackDevtools
|
<TanStackDevtools
|
||||||
config={{
|
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 { 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 (
|
return (
|
||||||
<div className="App">
|
<div className="min-h-screen bg-background">
|
||||||
<header className="App-header">
|
<div className="container mx-auto px-4 py-16">
|
||||||
<img
|
<header className="text-center mb-16">
|
||||||
src="/tanstack-circle-logo.png"
|
<h1 className="text-5xl font-bold text-foreground mb-4">
|
||||||
className="App-logo"
|
Dofus Manager
|
||||||
alt="TanStack Logo"
|
</h1>
|
||||||
/>
|
<p className="text-xl text-muted-foreground">
|
||||||
<p>
|
Gérez vos personnages, comptes et équipes Dofus
|
||||||
Edit <code>src/routes/index.tsx</code> and save to reload.
|
|
||||||
</p>
|
</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>
|
</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>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user