├── .env.placeholder ├── .gitignore ├── README.md ├── api ├── .gitignore ├── Dockerfile ├── Starter.postman_collection.json ├── drizzle.config.ts ├── env.d.ts ├── package-lock.json ├── package.json ├── src │ ├── app.ts │ ├── controllers │ │ ├── authController.ts │ │ └── userController.ts │ ├── migrations │ │ ├── 0000_rainy_rocket_raccoon.sql │ │ └── meta │ │ │ ├── 0000_snapshot.json │ │ │ └── _journal.json │ ├── models │ │ └── user.ts │ ├── routes │ │ ├── authRoutes.ts │ │ ├── testRoutes.ts │ │ └── userRoutes.ts │ └── utils │ │ ├── db.ts │ │ ├── jwt.ts │ │ ├── logger.ts │ │ └── valkey.ts └── tsconfig.json ├── app ├── .gitignore ├── .vscode │ └── settings.json ├── Dockerfile ├── components.json ├── messages │ └── en.json ├── next-env.d.ts ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public │ ├── favicon.ico │ └── vercel.svg ├── src │ ├── app │ │ ├── fonts │ │ │ ├── GeistMonoVF.woff │ │ │ └── GeistVF.woff │ │ ├── layout.tsx │ │ ├── page.tsx │ │ └── providers.tsx │ ├── components │ │ ├── Button.tsx │ │ └── counter │ │ │ ├── Counter.tsx │ │ │ └── counter.module.css │ ├── i18n │ │ └── request.ts │ ├── redux │ │ ├── createAppAsyncThunk.ts │ │ ├── index.ts │ │ ├── rootReducer.ts │ │ ├── slices │ │ │ ├── counterSlice │ │ │ │ ├── counterSlice.ts │ │ │ │ ├── fetchIdentityCount.ts │ │ │ │ ├── index.ts │ │ │ │ ├── selectors.ts │ │ │ │ └── thunks.ts │ │ │ └── index.ts │ │ └── store.ts │ ├── styles │ │ ├── Home.module.css │ │ ├── globals.css │ │ └── tailwindcss-animate.css │ └── utils │ │ └── utils.ts └── tsconfig.json ├── backoffice └── README.md ├── biome.json ├── compose.override.yaml ├── compose.yaml ├── compose_override ├── development.yaml └── production.yaml ├── populate.sh └── start.sh /.env.placeholder: -------------------------------------------------------------------------------- 1 | ENV=development 2 | 3 | DB_HOST=postgres_db 4 | DB_USER=your_db_user 5 | DB_PASS=your_db_password 6 | DB_NAME=your_db_name 7 | DB_PORT=5432 8 | 9 | WORDPRESS_DB_HOST=mysql_db 10 | WORDPRESS_DB_USER=exampleuser 11 | WORDPRESS_DB_PASSWORD=examplepass 12 | WORDPRESS_DB_NAME=exampledb 13 | 14 | MYSQL_DATABASE=exampledb 15 | MYSQL_USER=exampleuser 16 | MYSQL_PASSWORD=examplepass 17 | MYSQL_RANDOM_ROOT_PASSWORD='1' 18 | 19 | VALKEY_HOST=valkey 20 | VALKEY_PASS=your_valkey_password 21 | VALKEY_PORT=6379 22 | 23 | ELASTICSEARCH_HOST=elasticsearch 24 | ELASTICSEARCH_PORT=9200 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | private 2 | .env 3 | docker-compose.override.yml 4 | lastpass 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # **Basic Full Stack Architecture Starter** 2 | 3 | This is a small toy project that I prepared to give some exercises to new colleagues who are approaching web development for the first time, so I can give them a starting point that allows me over time to be able to explain everything to them. 4 | 5 | This project integrates the following technologies: 6 | 7 | - **Frontend**: [Next.js (App Router)](https://nextjs.org/docs/app), [TailwindCSS](https://tailwindcss.com/docs/v4-beta), [Shadcn](https://ui.shadcn.com/docs/components/accordion), [TypeScript](https://www.typescriptlang.org/), [Redux Toolkit](https://redux-toolkit.js.org/), [i18n](https://next-intl-docs.vercel.app/docs/getting-started/app-router/without-i18n-routing). 8 | - **Backend**: [Fastify](https://www.fastify.io/), [Drizzle ORM](https://orm.drizzle.team/), [Zod](https://zod.dev/), [Pino](https://getpino.io/#/), [OpenAPI](https://swagger.io/), [Scalar](https://scalar.com/), [WordPress](https://wordpress.org/). 9 | - **Infrastructure**: [Docker](https://www.docker.com/), [Traefik](https://doc.traefik.io/traefik/), [Valkey](https://valkey.dev/). 10 | - **Database**: [PostgreSQL](https://www.postgresql.org/), [MySQL(for WordPress)](https://www.mysql.com/). 11 | - **Monitoring**: [ELK](https://www.elastic.co/what-is/elk-stack). 12 | - **Linting code**: [BiomeJS](https://biomejs.dev/). 13 | 14 | This setup provides a ready-to-use playground for exploring web development concepts and workflows, from frontend UI design to backend services and database interactions. 15 | 16 | --- 17 | 18 | ## **Getting Started** 19 | 20 | 1. **Create Environment File**: 21 | - Create a `.env` file in the root directory by renaming `.env.placeholder` to `.env`. 22 | - Fill in the necessary environment variables in the `.env` file. 23 | 24 | 2. **Start the Project**: 25 | - To launch the project using Docker, run the following script: 26 | 27 | ```bash 28 | ./start.sh 29 | ``` 30 | 31 | This script will handle all necessary setup and start the services in a containerized environment. 32 | 33 | 3. **Populate the Database**: 34 | - After starting the project, populate the database by running: 35 | 36 | ```bash 37 | ./populate.sh 38 | ``` 39 | 40 | --- 41 | 42 | ## **Available URLs** 43 | 44 | After starting the project, the following URLs will be available: 45 | 46 | - **Frontend (Next.js)**: [http://localhost](http://localhost:80) 47 | - **Backend API (Fastify)**: [http://localhost/api](http://localhost/api) 48 | - **Scalar**: [http://localhost/reference](http://localhost/reference) 49 | - **Traefik Dashboard**: [http://localhost:8080](http://localhost:8080) 50 | - **Elasticsearch**: [http://localhost:9200](http://localhost:9200) 51 | - **Kibana**: [http://localhost:5601](http://localhost:5601) 52 | - **WordPress**: [http://localhost:7000](http://localhost:7000) 53 | 54 | --- 55 | 56 | ## **What's Missing?** 57 | 58 | The following features and examples are not yet implemented in the current version but can serve as exercises or extensions for developers: 59 | 60 | 1. **Transaction Example** 61 | - Implement an example of database transactions using [Drizzle ORM transactions](https://orm.drizzle.team/docs/transactions#transactions). 62 | 63 | 2. **Backoffice Dashboard** 64 | - Create a functional backoffice dashboard with features like user management, analytics, or content control. 65 | 66 | --- 67 | 68 | ## **Future Goals** 69 | 70 | This starter project is intended to evolve with new tools and features. Some potential future enhancements include: 71 | 72 | - Integrating CI/CD workflows. 73 | - Adding unit and integration testing. 74 | - Expanding backend functionalities (e.g., file uploads, advanced authentication). 75 | - Enhancing Docker configuration for production-ready deployment. 76 | 77 | --- 78 | 79 | ## **Contributing** 80 | 81 | Contributions are welcome! If you'd like to add features, fix bugs, or enhance the documentation, feel free to fork the repository and submit a pull request. 82 | 83 | --- 84 | 85 | ## **License** 86 | 87 | This project is licensed under the MIT License. Feel free to use, modify, and distribute as needed. 88 | 89 | --- -------------------------------------------------------------------------------- /api/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | 4 | # Optional npm cache directory 5 | .npm 6 | 7 | # Optional eslint cache 8 | .eslintcache 9 | -------------------------------------------------------------------------------- /api/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-slim 2 | 3 | WORKDIR /home/node 4 | COPY package.json ./ 5 | RUN npm install --no-optional && npm cache clean --force 6 | ENV PATH=/home/node/node_modules/.bin:$PATH 7 | WORKDIR /home/node/api 8 | COPY ./ ./ 9 | 10 | CMD [ "npm", "run", "dev"] -------------------------------------------------------------------------------- /api/Starter.postman_collection.json: -------------------------------------------------------------------------------- 1 | { 2 | "info": { 3 | "_postman_id": "894dbd5b-95ff-472d-9cb5-8f05d4d8c734", 4 | "name": "Starter", 5 | "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json", 6 | "_exporter_id": "6610193" 7 | }, 8 | "item": [ 9 | { 10 | "name": "http://localhost/api/login", 11 | "request": { 12 | "method": "POST", 13 | "header": [ 14 | { 15 | "key": "Content-Type", 16 | "value": "application/json" 17 | } 18 | ], 19 | "body": { 20 | "mode": "raw", 21 | "raw": "{\"email\":\"test@example.com\",\"password\":\"testpass\"}" 22 | }, 23 | "url": { 24 | "raw": "http://localhost/api/login", 25 | "protocol": "http", 26 | "host": ["localhost"], 27 | "path": ["api", "login"] 28 | } 29 | }, 30 | "response": [] 31 | }, 32 | { 33 | "name": "http://localhost/api/healthcheck/ping", 34 | "request": { 35 | "method": "GET", 36 | "header": [], 37 | "url": { 38 | "raw": "http://localhost/api/healthcheck/ping", 39 | "protocol": "http", 40 | "host": ["localhost"], 41 | "path": ["api", "healthcheck", "ping"] 42 | } 43 | }, 44 | "response": [] 45 | }, 46 | { 47 | "name": "http://localhost/api/identity-count", 48 | "request": { 49 | "method": "POST", 50 | "header": [ 51 | { 52 | "key": "Content-Type", 53 | "value": "application/json" 54 | } 55 | ], 56 | "body": { 57 | "mode": "raw", 58 | "raw": "{\"amount\": 5}" 59 | }, 60 | "url": { 61 | "raw": "http://localhost/api/identity-count", 62 | "protocol": "http", 63 | "host": ["localhost"], 64 | "path": ["api", "identity-count"] 65 | } 66 | }, 67 | "response": [] 68 | }, 69 | { 70 | "name": "http://localhost/api/users", 71 | "request": { 72 | "method": "POST", 73 | "header": [ 74 | { 75 | "key": "Content-Type", 76 | "value": "application/json" 77 | } 78 | ], 79 | "body": { 80 | "mode": "raw", 81 | "raw": "{\"name\":\"Test User\",\"email\":\"test@example.com\",\"password\":\"testpass\"}" 82 | }, 83 | "url": { 84 | "raw": "http://localhost/api/users", 85 | "protocol": "http", 86 | "host": ["localhost"], 87 | "path": ["api", "users"] 88 | } 89 | }, 90 | "response": [] 91 | }, 92 | { 93 | "name": "http://localhost/api/users/123bec0a-d50e-44e7-a470-bb0e72b861c6", 94 | "request": { 95 | "method": "GET", 96 | "header": [], 97 | "url": { 98 | "raw": "http://localhost/api/users/123bec0a-d50e-44e7-a470-bb0e72b861c6", 99 | "protocol": "http", 100 | "host": ["localhost"], 101 | "path": ["api", "users", "123bec0a-d50e-44e7-a470-bb0e72b861c6"] 102 | } 103 | }, 104 | "response": [] 105 | }, 106 | { 107 | "name": "http://localhost/api/login", 108 | "request": { 109 | "method": "POST", 110 | "header": [ 111 | { 112 | "key": "Content-Type", 113 | "value": "application/json" 114 | } 115 | ], 116 | "body": { 117 | "mode": "raw", 118 | "raw": "{\"email\":\"test@example.com\",\"password\":\"testpass\"}", 119 | "options": { 120 | "raw": { 121 | "language": "json" 122 | } 123 | } 124 | }, 125 | "url": { 126 | "raw": "http://localhost/api/login", 127 | "protocol": "http", 128 | "host": ["localhost"], 129 | "path": ["api", "login"] 130 | }, 131 | "description": "Generated from cURL: curl -X POST http://localhost/api/login \\\n -H \"Content-Type: application/json\" \\\n -d '{\"email\":\"test@example.com\",\"password\":\"testpass\"}'" 132 | }, 133 | "response": [] 134 | }, 135 | { 136 | "name": "http://localhost/api/refresh-token", 137 | "request": { 138 | "method": "POST", 139 | "header": [ 140 | { 141 | "key": "Content-Type", 142 | "value": "application/json" 143 | } 144 | ], 145 | "body": { 146 | "mode": "raw", 147 | "raw": "{\"refreshToken\":\"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6IjEyM2JlYzBhLWQ1MGUtNDRlNy1hNDcwLWJiMGU3MmI4NjFjNiIsImVtYWlsIjoidGVzdEBleGFtcGxlLmNvbSIsImlhdCI6MTc0NjQ0Mjg2MiwiZXhwIjoxNzQ3MDQ3NjYyfQ.m6PRTpQV_MFz-YMs_pONDzGoHe7r4MtN1sXM7yS8Z08\"}", 148 | "options": { 149 | "raw": { 150 | "language": "json" 151 | } 152 | } 153 | }, 154 | "url": { 155 | "raw": "http://localhost/api/refresh-token", 156 | "protocol": "http", 157 | "host": ["localhost"], 158 | "path": ["api", "refresh-token"] 159 | }, 160 | "description": "Generated from cURL: curl -X POST http://localhost/api/refresh-token \\\n -H \"Content-Type: application/json\" \\\n -d '{\"refreshToken\":\"\"}'" 161 | }, 162 | "response": [] 163 | }, 164 | { 165 | "name": "http://localhost/api/profile", 166 | "request": { 167 | "method": "GET", 168 | "header": [ 169 | { 170 | "key": "Authorization", 171 | "value": "Bearer " 172 | } 173 | ], 174 | "url": { 175 | "raw": "http://localhost/api/profile", 176 | "protocol": "http", 177 | "host": ["localhost"], 178 | "path": ["api", "profile"] 179 | }, 180 | "description": "Generated from cURL: curl -X GET http://localhost/api/profile \\\n -H \"Authorization: Bearer \"" 181 | }, 182 | "response": [] 183 | } 184 | ] 185 | } 186 | -------------------------------------------------------------------------------- /api/drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'drizzle-kit' 2 | 3 | export default defineConfig({ 4 | schema: './src/models', 5 | out: './src/migrations', 6 | dialect: 'postgresql', 7 | dbCredentials: { 8 | url: `postgres://${process.env.DB_USER}:${process.env.DB_PASS}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` 9 | } 10 | }) 11 | -------------------------------------------------------------------------------- /api/env.d.ts: -------------------------------------------------------------------------------- 1 | declare global { 2 | namespace NodeJS { 3 | interface ProcessEnv { 4 | ENV: string 5 | DB_HOST: string 6 | DB_USER: string 7 | DB_PASS: string 8 | DB_NAME: string 9 | DB_PORT: number 10 | VALKEY_HOST: string 11 | VALKEY_PASS: string 12 | VALKEY_PORT: number 13 | ELASTICSEARCH_HOST: string 14 | ELASTICSEARCH_PORT: number 15 | } 16 | } 17 | } 18 | 19 | export {} 20 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "api", 3 | "version": "1.0.0", 4 | "main": "app.js", 5 | "license": "ISC", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "tsx watch src/app.ts", 9 | "generate": "npx drizzle-kit generate", 10 | "migrate": "npx drizzle-kit migrate", 11 | "check": "npx @biomejs/biome check . --write", 12 | "format": "npx @biomejs/biome format . --write", 13 | "lint": "npx @biomejs/biome lint ." 14 | }, 15 | "dependencies": { 16 | "@elastic/ecs-pino-format": "^1.5.0", 17 | "@fastify/jwt": "^9.1.0", 18 | "@fastify/swagger": "^9.5.1", 19 | "@scalar/fastify-api-reference": "^1.31.8", 20 | "bcrypt": "^6.0.0", 21 | "dayjs": "^1.11.13", 22 | "drizzle-orm": "^0.44.2", 23 | "fastify": "^5.3.3", 24 | "iovalkey": "^0.3.2", 25 | "pg": "^8.16.0", 26 | "pino": "^9.7.0", 27 | "pino-elasticsearch": "^8.1.0", 28 | "zod": "^4.0.0-beta.20250505T195954" 29 | }, 30 | "devDependencies": { 31 | "@biomejs/biome": "2.0.0-beta.6", 32 | "@types/bcrypt": "^5.0.2", 33 | "@types/node": "^22.15.30", 34 | "@types/pg": "^8.15.4", 35 | "drizzle-kit": "^0.31.1", 36 | "tsx": "^4.19.4", 37 | "typescript": "^5.8.3" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /api/src/app.ts: -------------------------------------------------------------------------------- 1 | import swagger from '@fastify/swagger' 2 | import scalar from '@scalar/fastify-api-reference' 3 | import Fastify from 'fastify' 4 | import authRoutes from './routes/authRoutes' 5 | import testRoutes from './routes/testRoutes' 6 | import userRoutes from './routes/userRoutes' 7 | import jwtPlugin from './utils/jwt' 8 | 9 | const app = Fastify({ logger: true }) 10 | 11 | app.register(jwtPlugin) 12 | 13 | app.register(swagger, { 14 | swagger: { 15 | info: { 16 | title: 'Fastify API', 17 | description: 'API documentation for Fastify project', 18 | version: '1.0.0' 19 | }, 20 | host: 'localhost', // Update as needed for your environment 21 | schemes: ['http'], 22 | consumes: ['application/json'], 23 | produces: ['application/json'], 24 | securityDefinitions: { 25 | bearerAuth: { 26 | type: 'apiKey', 27 | name: 'Authorization', 28 | in: 'header', 29 | description: 'Enter JWT Bearer token **_only_**' 30 | } 31 | } 32 | } 33 | }) 34 | 35 | app.register(scalar, { 36 | routePrefix: '/reference', 37 | configuration: { 38 | theme: 'fastify' 39 | } 40 | }) 41 | 42 | app.register(testRoutes) 43 | app.register(authRoutes) 44 | app.register(userRoutes) 45 | 46 | const startServer = async () => { 47 | try { 48 | await app.listen({ port: 5000, host: '0.0.0.0' }) 49 | console.log('Server is running') 50 | } catch (err) { 51 | app.log.error(err) 52 | process.exit(1) 53 | } 54 | } 55 | 56 | startServer() 57 | 58 | export default app 59 | -------------------------------------------------------------------------------- /api/src/controllers/authController.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import { eq } from 'drizzle-orm' 3 | import { users } from '../models/user' 4 | import db from '../utils/db' 5 | 6 | export const login = async (email: string, password: string, generateTokens: Function) => { 7 | try { 8 | const [user] = await db.select().from(users).where(eq(users.email, email)).limit(1) 9 | if (!user) { 10 | return Error('User not found') 11 | } 12 | 13 | // Verify password 14 | const match = await bcrypt.compare(password, user.password) 15 | if (!match) { 16 | return Error('Invalid credentials') 17 | } 18 | 19 | // Generate tokens 20 | const token = await generateTokens({ id: user.id, email: user.email }) 21 | return token 22 | } catch (error) { 23 | console.log(error) 24 | return Error('Database error') 25 | } 26 | } 27 | 28 | export const refreshToken = async ( 29 | refreshToken: string, 30 | verifyRefreshToken: Function, 31 | revokeRefreshToken: Function, 32 | generateTokens: Function 33 | ) => { 34 | try { 35 | const decoded = await verifyRefreshToken(refreshToken) 36 | await revokeRefreshToken(decoded.id) // Revoke old refresh token 37 | const token = await generateTokens({ id: decoded.id, email: decoded.email }) 38 | return token 39 | } catch (error) { 40 | console.log(error) 41 | return Error('Invalid refresh token') 42 | } 43 | } 44 | 45 | export const profile = async (userId: string) => { 46 | try { 47 | const [userProfile] = await db.select().from(users).where(eq(users.id, userId)).limit(1) 48 | return userProfile 49 | } catch (error) { 50 | console.log(error) 51 | return Error('Database error') 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/src/controllers/userController.ts: -------------------------------------------------------------------------------- 1 | import { createUser, getUser } from '../models/user' 2 | import db from '../utils/db' 3 | 4 | export const getUserHandler = async (userId: string) => { 5 | const user = await getUser(db, userId) 6 | return user 7 | } 8 | 9 | export const createUserHandler = async (name: string, email: string, password: string) => { 10 | const user = await createUser(db, name, email, password) 11 | return user 12 | } 13 | -------------------------------------------------------------------------------- /api/src/migrations/0000_rainy_rocket_raccoon.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "users" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "name" text, 4 | "email" varchar(255) NOT NULL, 5 | "password" varchar(100) NOT NULL, 6 | "created_at" timestamp DEFAULT now() NOT NULL 7 | ); 8 | -------------------------------------------------------------------------------- /api/src/migrations/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "f678761f-90c7-4682-93b2-22bd7a7db135", 3 | "prevId": "00000000-0000-0000-0000-000000000000", 4 | "version": "7", 5 | "dialect": "postgresql", 6 | "tables": { 7 | "public.users": { 8 | "name": "users", 9 | "schema": "", 10 | "columns": { 11 | "id": { 12 | "name": "id", 13 | "type": "uuid", 14 | "primaryKey": true, 15 | "notNull": true, 16 | "default": "gen_random_uuid()" 17 | }, 18 | "name": { 19 | "name": "name", 20 | "type": "text", 21 | "primaryKey": false, 22 | "notNull": false 23 | }, 24 | "email": { 25 | "name": "email", 26 | "type": "varchar(255)", 27 | "primaryKey": false, 28 | "notNull": true 29 | }, 30 | "password": { 31 | "name": "password", 32 | "type": "varchar(100)", 33 | "primaryKey": false, 34 | "notNull": true 35 | }, 36 | "created_at": { 37 | "name": "created_at", 38 | "type": "timestamp", 39 | "primaryKey": false, 40 | "notNull": true, 41 | "default": "now()" 42 | } 43 | }, 44 | "indexes": {}, 45 | "foreignKeys": {}, 46 | "compositePrimaryKeys": {}, 47 | "uniqueConstraints": {}, 48 | "policies": {}, 49 | "checkConstraints": {}, 50 | "isRLSEnabled": false 51 | } 52 | }, 53 | "enums": {}, 54 | "schemas": {}, 55 | "sequences": {}, 56 | "roles": {}, 57 | "policies": {}, 58 | "views": {}, 59 | "_meta": { 60 | "columns": {}, 61 | "schemas": {}, 62 | "tables": {} 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /api/src/migrations/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1734740631861, 9 | "tag": "0000_rainy_rocket_raccoon", 10 | "breakpoints": true 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /api/src/models/user.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcrypt' 2 | import type { InferSelectModel } from 'drizzle-orm' 3 | import { eq } from 'drizzle-orm' 4 | import type { NodePgDatabase } from 'drizzle-orm/node-postgres' 5 | import { pgTable, text, timestamp, uuid, varchar } from 'drizzle-orm/pg-core' 6 | 7 | export const users = pgTable('users', { 8 | id: uuid().defaultRandom().primaryKey(), 9 | name: text('name'), 10 | email: varchar('email', { length: 255 }).notNull(), 11 | password: varchar('password', { length: 100 }).notNull(), 12 | createdAt: timestamp('created_at').notNull().defaultNow() 13 | }) 14 | 15 | export type User = InferSelectModel 16 | 17 | export async function createUser(db: NodePgDatabase, name: string, email: string, password: string) { 18 | try { 19 | // Check if the user already exists 20 | const existingUser = await db.select().from(users).where(eq(users.email, email)).limit(1) 21 | if (existingUser.length > 0) { 22 | return Error('User already exist') 23 | } 24 | 25 | // Hash the password 26 | const saltRounds = 10 27 | const hashedPassword = await bcrypt.hash(password, saltRounds) 28 | 29 | // Insert the new user 30 | const [newUser] = await db 31 | .insert(users) 32 | .values({ 33 | name, 34 | email, 35 | password: hashedPassword 36 | }) 37 | .returning() 38 | 39 | return newUser 40 | } catch (error) { 41 | console.log(error) 42 | return Error('Generic error') 43 | } 44 | } 45 | 46 | export async function getUser(db: NodePgDatabase, id: string) { 47 | const [user] = await db.select().from(users).where(eq(users.id, id)).limit(1) 48 | return user 49 | } 50 | -------------------------------------------------------------------------------- /api/src/routes/authRoutes.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' 2 | import { login, profile, refreshToken } from 'src/controllers/authController' 3 | import { z } from 'zod' 4 | 5 | export const loginBodySchema = z.object({ 6 | email: z.email('Invalid email'), 7 | password: z.string().min(1, 'Password required') 8 | }) 9 | 10 | export const refreshTokenBodySchema = z.object({ 11 | refreshToken: z.string() 12 | }) 13 | 14 | export const loginResponseSchema = z.object({ 15 | success: z.boolean(), 16 | accessToken: z.string().optional(), 17 | refreshToken: z.string().optional(), 18 | message: z.string().optional() 19 | }) 20 | 21 | export const refreshTokenResponseSchema = loginResponseSchema 22 | 23 | export const profileResponseSchema = z.object({ 24 | success: z.boolean(), 25 | user: z 26 | .object({ 27 | id: z.string(), 28 | name: z.string().nullable(), 29 | email: z.email() 30 | }) 31 | .optional(), 32 | message: z.string().optional() 33 | }) 34 | 35 | async function authRoutes(fastify: FastifyInstance) { 36 | fastify.post('/api/login', { 37 | schema: { 38 | body: z.toJSONSchema(loginBodySchema), 39 | response: { 40 | 200: z.toJSONSchema(loginResponseSchema) 41 | }, 42 | tags: ['Auth'], 43 | description: 'Get a JWT Token' 44 | }, 45 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 46 | const parsedBody = loginBodySchema.safeParse(request.body) 47 | if (!parsedBody.success) { 48 | return reply.status(400).send({ success: false, message: parsedBody.error }) 49 | } 50 | const { email, password } = parsedBody.data 51 | const token = await login(email, password, fastify.generateTokens) 52 | if (token instanceof Error) { 53 | return reply.status(401).send({ success: false, message: token.message }) 54 | } 55 | const result = { success: true, ...token } 56 | const parsedResponse = loginResponseSchema.safeParse(result) 57 | if (!parsedResponse.success) { 58 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 59 | } 60 | reply.send(parsedResponse.data) 61 | } 62 | }) 63 | 64 | fastify.post('/api/refresh-token', { 65 | schema: { 66 | body: z.toJSONSchema(refreshTokenBodySchema), 67 | response: { 68 | 200: z.toJSONSchema(refreshTokenResponseSchema) 69 | }, 70 | tags: ['Auth'], 71 | description: 'Refresh JWT Token' 72 | }, 73 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 74 | const parsedBody = refreshTokenBodySchema.safeParse(request.body) 75 | if (!parsedBody.success) { 76 | return reply.status(400).send({ success: false, message: parsedBody.error }) 77 | } 78 | const { refreshToken: refreshTokenValue } = parsedBody.data 79 | const token = await refreshToken( 80 | refreshTokenValue, 81 | request.server.verifyRefreshToken, 82 | request.server.revokeRefreshToken, 83 | request.server.generateTokens 84 | ) 85 | if (token instanceof Error) { 86 | return reply.status(401).send({ success: false, message: token.message }) 87 | } 88 | const response = { success: true, ...token } 89 | const parsedResponse = refreshTokenResponseSchema.safeParse(response) 90 | if (!parsedResponse.success) { 91 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 92 | } 93 | reply.send(parsedResponse.data) 94 | } 95 | }) 96 | 97 | fastify.get('/api/profile', { 98 | schema: { 99 | response: { 100 | 200: z.toJSONSchema(profileResponseSchema) 101 | }, 102 | tags: ['Auth'], 103 | description: 'Get a user by JWT', 104 | security: [{ bearerAuth: [] }] 105 | }, 106 | preHandler: [fastify.authenticate], 107 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 108 | console.log('Profile route') 109 | const { user } = request as any // Type assertion to do 110 | if (!user || !user.id) { 111 | return reply.status(401).send({ success: false, message: 'Unauthorized' }) 112 | } 113 | const userProfile = await profile(user.id) 114 | if (userProfile instanceof Error) { 115 | return reply.status(500).send({ success: false, message: userProfile.message }) 116 | } 117 | const response = { success: true, user: userProfile } 118 | const parsedResponse = profileResponseSchema.safeParse(response) 119 | if (!parsedResponse.success) { 120 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 121 | } 122 | reply.send(parsedResponse.data) 123 | } 124 | }) 125 | } 126 | 127 | export default authRoutes 128 | -------------------------------------------------------------------------------- /api/src/routes/testRoutes.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' 2 | import logger from 'src/utils/logger' 3 | import valkey from 'src/utils/valkey' 4 | import { z } from 'zod' 5 | 6 | export const identityCountBodySchema = z.object({ 7 | amount: z.number({ error: 'Amount is required' }) 8 | }) 9 | 10 | export const identityCountResponseSchema = z.object({ 11 | success: z.boolean(), 12 | amount: z.number().optional(), 13 | message: z.string().optional() 14 | }) 15 | 16 | export const healthcheckResponseSchema = z.object({ 17 | success: z.boolean(), 18 | message: z.string() 19 | }) 20 | 21 | // Define the sleep function 22 | const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)) 23 | 24 | async function testRoutes(fastify: FastifyInstance) { 25 | fastify.get('/api/healthcheck/ping', { 26 | schema: { 27 | description: 'Test Fastify', 28 | tags: ['Test'], 29 | response: { 30 | 200: z.toJSONSchema(healthcheckResponseSchema) 31 | } 32 | }, 33 | handler: async (_request: FastifyRequest, reply: FastifyReply) => { 34 | const data = await valkey.get('test') 35 | if (!data) { 36 | await valkey.set('test', 'ping') 37 | logger.info('ping') 38 | } 39 | const response = { success: true, message: 'pong' } 40 | const parsedResponse = healthcheckResponseSchema.safeParse(response) 41 | if (!parsedResponse.success) { 42 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 43 | } 44 | reply.send(parsedResponse.data) 45 | } 46 | }) 47 | 48 | fastify.post('/api/identity-count', { 49 | schema: { 50 | description: 'Test Redux', 51 | tags: ['Test'], 52 | body: z.toJSONSchema(identityCountBodySchema), 53 | response: { 54 | 200: z.toJSONSchema(identityCountResponseSchema) 55 | } 56 | }, 57 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 58 | const parsedBody = identityCountBodySchema.safeParse(request.body) 59 | if (!parsedBody.success) { 60 | return reply.status(400).send({ success: false, message: parsedBody.error }) 61 | } 62 | const { amount } = parsedBody.data 63 | await sleep(700) 64 | const response = { success: true, amount } 65 | const parsedResponse = identityCountResponseSchema.safeParse(response) 66 | if (!parsedResponse.success) { 67 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 68 | } 69 | reply.send(parsedResponse.data) 70 | } 71 | }) 72 | } 73 | 74 | export default testRoutes 75 | -------------------------------------------------------------------------------- /api/src/routes/userRoutes.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' 2 | import { z } from 'zod' 3 | import { createUserHandler, getUserHandler } from '../controllers/userController' 4 | 5 | export const userParamsSchema = z.object({ 6 | userId: z.string() 7 | }) 8 | 9 | export const createUserBodySchema = z.object({ 10 | name: z.string(), 11 | email: z.email(), 12 | password: z.string() 13 | }) 14 | 15 | export const userResponseSchema = z.object({ 16 | success: z.boolean(), 17 | user: z 18 | .object({ 19 | id: z.string(), 20 | name: z.string().nullable(), 21 | email: z.string() 22 | }) 23 | .optional(), 24 | message: z.string().optional() 25 | }) 26 | 27 | async function userRoutes(fastify: FastifyInstance) { 28 | fastify.get('/api/users/:userId', { 29 | schema: { 30 | description: 'Get a user by ID', 31 | tags: ['User'], 32 | params: z.toJSONSchema(userParamsSchema), 33 | response: { 34 | 200: z.toJSONSchema(userResponseSchema) 35 | } 36 | }, 37 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 38 | const parsedParams = userParamsSchema.safeParse(request.params) 39 | if (!parsedParams.success) { 40 | return reply.status(400).send({ success: false, message: parsedParams.error }) 41 | } 42 | const { userId } = parsedParams.data 43 | const user = await getUserHandler(userId) 44 | const response = { success: true, user } 45 | const parsedResponse = userResponseSchema.safeParse(response) 46 | if (!parsedResponse.success) { 47 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 48 | } 49 | reply.send(parsedResponse.data) 50 | } 51 | }) 52 | 53 | fastify.post('/api/users', { 54 | schema: { 55 | description: 'Create a User', 56 | tags: ['User'], 57 | body: z.toJSONSchema(createUserBodySchema), 58 | response: { 59 | 200: z.toJSONSchema(userResponseSchema) 60 | } 61 | }, 62 | handler: async (request: FastifyRequest, reply: FastifyReply) => { 63 | const parsedBody = createUserBodySchema.safeParse(request.body) 64 | if (!parsedBody.success) { 65 | return reply.status(400).send({ success: false, message: parsedBody.error }) 66 | } 67 | const { name, email, password } = parsedBody.data 68 | const user = await createUserHandler(name, email, password) 69 | const response = { success: true, user } 70 | const parsedResponse = userResponseSchema.safeParse(response) 71 | if (!parsedResponse.success) { 72 | return reply.status(500).send({ success: false, message: parsedResponse.error }) 73 | } 74 | reply.send(parsedResponse.data) 75 | } 76 | }) 77 | } 78 | 79 | export default userRoutes 80 | -------------------------------------------------------------------------------- /api/src/utils/db.ts: -------------------------------------------------------------------------------- 1 | import { drizzle } from 'drizzle-orm/node-postgres' 2 | 3 | const url = `postgres://${process.env.DB_USER}:${process.env.DB_PASS}@${process.env.DB_HOST}:${process.env.DB_PORT}/${process.env.DB_NAME}` 4 | export default drizzle(url) 5 | -------------------------------------------------------------------------------- /api/src/utils/jwt.ts: -------------------------------------------------------------------------------- 1 | import jwt from '@fastify/jwt' 2 | import type { FastifyInstance, FastifyReply, FastifyRequest } from 'fastify' 3 | import fp from 'fastify-plugin' 4 | import valkey from './valkey' 5 | 6 | export default fp(async (fastify: FastifyInstance) => { 7 | fastify.register(jwt, { 8 | secret: 'superdupersecret' 9 | }) 10 | 11 | fastify.decorate('authenticate', async (request: FastifyRequest, reply: FastifyReply) => { 12 | try { 13 | await request.jwtVerify() 14 | } catch (err) { 15 | reply.send({ success: false, message: err }) 16 | } 17 | }) 18 | 19 | fastify.decorate('generateTokens', async (user: { id: string; email: string }) => { 20 | const accessToken = fastify.jwt.sign(user, { expiresIn: '15m' }) 21 | const refreshToken = fastify.jwt.sign(user, { expiresIn: '7d' }) 22 | await valkey.set(`refresh_${user.id}`, refreshToken) 23 | return { accessToken, refreshToken } 24 | }) 25 | 26 | fastify.decorate('verifyRefreshToken', async (refreshToken: string): Promise<{ id: string; email: string }> => { 27 | try { 28 | const decoded = fastify.jwt.verify<{ id: string; email: string }>(refreshToken) 29 | const storedToken = await valkey.get(`refresh_${decoded.id}`) 30 | if (storedToken !== refreshToken) { 31 | throw new Error('Invalid refresh token') 32 | } 33 | return decoded 34 | } catch (err) { 35 | throw new Error(`Invalid refresh token ${err}`) 36 | } 37 | }) 38 | 39 | fastify.decorate('revokeRefreshToken', async (userId: string) => { 40 | await valkey.del(`refresh_${userId}`) 41 | }) 42 | }) 43 | 44 | declare module 'fastify' { 45 | interface FastifyInstance { 46 | authenticate: (request: FastifyRequest, reply: FastifyReply) => unknown 47 | generateTokens: (user: { id: string; email: string }) => Promise<{ accessToken: string; refreshToken: string }> 48 | verifyRefreshToken: (refreshToken: string) => Promise<{ id: string; email: string }> 49 | revokeRefreshToken: (userId: string) => Promise 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /api/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import { ecsFormat } from '@elastic/ecs-pino-format' 2 | import pino from 'pino' 3 | import pinoElastic from 'pino-elasticsearch' 4 | 5 | const streamToElastic = pinoElastic({ 6 | index: 'info', 7 | node: `http://${process.env.ELASTICSEARCH_HOST}:${process.env.ELASTICSEARCH_PORT}`, 8 | opType: 'index', 9 | esVersion: 8, 10 | flushBytes: 1000 11 | }) 12 | 13 | const level = 'trace' 14 | const logger = pino( 15 | { level, ...ecsFormat }, 16 | pino.multistream([ 17 | { level, stream: streamToElastic }, 18 | { level, stream: process.stdout } 19 | ]) 20 | ) 21 | 22 | // Capture errors like unable to connect Elasticsearch instance. 23 | streamToElastic.on('error', (error) => { 24 | console.error('Elasticsearch client error:', error) 25 | }) 26 | // Capture errors returned from Elasticsearch, "it will be called every time a document can't be indexed". 27 | streamToElastic.on('insertError', (error) => { 28 | console.error('Elasticsearch server error:', error) 29 | }) 30 | 31 | export default logger 32 | -------------------------------------------------------------------------------- /api/src/utils/valkey.ts: -------------------------------------------------------------------------------- 1 | import Valkey from 'iovalkey' 2 | 3 | const valkey = new Valkey({ 4 | port: Number.parseInt(process.env.VALKEY_PORT), 5 | host: process.env.VALKEY_HOST, 6 | password: process.env.VALKEY_PASS, 7 | db: 0 // Defaults to 0 8 | }) 9 | 10 | export default valkey 11 | -------------------------------------------------------------------------------- /api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "strict": true, 7 | "esModuleInterop": true, 8 | "skipLibCheck": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "outDir": "./dist", 11 | "baseUrl": ".", 12 | "paths": { 13 | "@models/*": ["src/models/*"] 14 | } 15 | }, 16 | "include": ["src/**/*.ts"], 17 | "exclude": ["node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | 37 | /config.ts 38 | -------------------------------------------------------------------------------- /app/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true 4 | } 5 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:24-slim AS base 2 | WORKDIR /home/node 3 | COPY package*.json ./ 4 | ENV PATH=/home/node/node_modules/.bin:$PATH 5 | RUN npm install --force --no-optional --no-audit --loglevel info && npm cache clean --force 6 | RUN npx next telemetry disable 7 | RUN npx next telemetry status 8 | WORKDIR /home/node/app 9 | COPY ./ ./ 10 | 11 | FROM base AS base-remote 12 | ARG NEXT_PUBLIC_ENVIRONMENT 13 | RUN echo 'NEXT_PUBLIC_ENVIRONMENT=${NEXT_PUBLIC_ENVIRONMENT}' > /home/node/app/.env 14 | RUN npm run build 15 | 16 | FROM base AS image-development 17 | CMD [ "npm", "run", "dev" ] 18 | 19 | FROM base-remote AS image-production 20 | CMD [ "npm", "run", "start" ] 21 | -------------------------------------------------------------------------------- /app/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "styles/globals.css", 9 | "baseColor": "zinc", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/utils/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /app/messages/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "HomePage": { 3 | "title": "Welcome to" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | /// 4 | 5 | // NOTE: This file should not be edited 6 | // see https://nextjs.org/docs/app/building-your-application/configuring/typescript for more information. 7 | -------------------------------------------------------------------------------- /app/next.config.js: -------------------------------------------------------------------------------- 1 | import createNextIntlPlugin from 'next-intl/plugin' 2 | 3 | const withNextIntl = createNextIntlPlugin() 4 | 5 | /** @type {import('next').NextConfig} */ 6 | const nextConfig = { 7 | env: { 8 | NEXT_PUBLIC_ENVIRONMENT: process.env.NEXT_PUBLIC_ENVIRONMENT 9 | } 10 | } 11 | 12 | export default withNextIntl(nextConfig) 13 | -------------------------------------------------------------------------------- /app/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "app", 9 | "version": "1.0.0", 10 | "dependencies": { 11 | "@radix-ui/react-slot": "^1.2.3", 12 | "@reduxjs/toolkit": "^2.8.2", 13 | "class-variance-authority": "^0.7.1", 14 | "clsx": "^2.1.1", 15 | "lucide-react": "^0.513.0", 16 | "next": "^15.3.3", 17 | "next-intl": "^4.1.0", 18 | "react": "^19.1.0", 19 | "react-dom": "^19.1.0", 20 | "react-redux": "^9.2.0", 21 | "tailwind-merge": "^3.3.0" 22 | }, 23 | "devDependencies": { 24 | "@biomejs/biome": "2.0.0-beta.6", 25 | "@tailwindcss/postcss": "^4.1.8", 26 | "@types/node": "^22.15.30", 27 | "@types/react": "^19.1.6", 28 | "@types/react-dom": "^19.1.6", 29 | "tailwindcss": "^4.1.8", 30 | "typescript": "^5.8.3" 31 | } 32 | }, 33 | "node_modules/@alloc/quick-lru": { 34 | "version": "5.2.0", 35 | "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", 36 | "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", 37 | "dev": true, 38 | "license": "MIT", 39 | "engines": { 40 | "node": ">=10" 41 | }, 42 | "funding": { 43 | "url": "https://github.com/sponsors/sindresorhus" 44 | } 45 | }, 46 | "node_modules/@ampproject/remapping": { 47 | "version": "2.3.0", 48 | "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", 49 | "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", 50 | "dev": true, 51 | "license": "Apache-2.0", 52 | "dependencies": { 53 | "@jridgewell/gen-mapping": "^0.3.5", 54 | "@jridgewell/trace-mapping": "^0.3.24" 55 | }, 56 | "engines": { 57 | "node": ">=6.0.0" 58 | } 59 | }, 60 | "node_modules/@biomejs/biome": { 61 | "version": "2.0.0-beta.6", 62 | "resolved": "https://registry.npmjs.org/@biomejs/biome/-/biome-2.0.0-beta.6.tgz", 63 | "integrity": "sha512-14vw9b5QJxrcP7WLkCeRiB/fft9wNZwx6yEiikBDxFbN7IAp39Xtvt/gJPq4ifhZ5IS25CnQEAkLLwfBIDMjsA==", 64 | "dev": true, 65 | "license": "MIT OR Apache-2.0", 66 | "bin": { 67 | "biome": "bin/biome" 68 | }, 69 | "engines": { 70 | "node": ">=14.21.3" 71 | }, 72 | "funding": { 73 | "type": "opencollective", 74 | "url": "https://opencollective.com/biome" 75 | }, 76 | "optionalDependencies": { 77 | "@biomejs/cli-darwin-arm64": "2.0.0-beta.6", 78 | "@biomejs/cli-darwin-x64": "2.0.0-beta.6", 79 | "@biomejs/cli-linux-arm64": "2.0.0-beta.6", 80 | "@biomejs/cli-linux-arm64-musl": "2.0.0-beta.6", 81 | "@biomejs/cli-linux-x64": "2.0.0-beta.6", 82 | "@biomejs/cli-linux-x64-musl": "2.0.0-beta.6", 83 | "@biomejs/cli-win32-arm64": "2.0.0-beta.6", 84 | "@biomejs/cli-win32-x64": "2.0.0-beta.6" 85 | } 86 | }, 87 | "node_modules/@biomejs/cli-darwin-arm64": { 88 | "version": "2.0.0-beta.6", 89 | "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-arm64/-/cli-darwin-arm64-2.0.0-beta.6.tgz", 90 | "integrity": "sha512-L7PBLJlGTz5anougOMJQvEbzgG9sT1wKIXvgjFhu0dIsDZ/px2caWFCnv7Q9L2K0+yF08EYRTTZVvoVO5D//sQ==", 91 | "cpu": [ 92 | "arm64" 93 | ], 94 | "dev": true, 95 | "license": "MIT OR Apache-2.0", 96 | "optional": true, 97 | "os": [ 98 | "darwin" 99 | ], 100 | "engines": { 101 | "node": ">=14.21.3" 102 | } 103 | }, 104 | "node_modules/@biomejs/cli-darwin-x64": { 105 | "version": "2.0.0-beta.6", 106 | "resolved": "https://registry.npmjs.org/@biomejs/cli-darwin-x64/-/cli-darwin-x64-2.0.0-beta.6.tgz", 107 | "integrity": "sha512-ekhOOyhcVJ1ZRqHjq+eUOv8/3XMRKQ9Qf0URuO/PvHgopejv+PEoix0RIyxholYELKc049M4J3IJgsX4q2pZzw==", 108 | "cpu": [ 109 | "x64" 110 | ], 111 | "dev": true, 112 | "license": "MIT OR Apache-2.0", 113 | "optional": true, 114 | "os": [ 115 | "darwin" 116 | ], 117 | "engines": { 118 | "node": ">=14.21.3" 119 | } 120 | }, 121 | "node_modules/@biomejs/cli-linux-arm64": { 122 | "version": "2.0.0-beta.6", 123 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64/-/cli-linux-arm64-2.0.0-beta.6.tgz", 124 | "integrity": "sha512-pu+rCLI36ziPtwnJY53HRr154711uVeCt1i2KNXehvwNZZMK141wwg4yPkXkBdBvw7H7sez0HE/rCQR2fByJnQ==", 125 | "cpu": [ 126 | "arm64" 127 | ], 128 | "dev": true, 129 | "license": "MIT OR Apache-2.0", 130 | "optional": true, 131 | "os": [ 132 | "linux" 133 | ], 134 | "engines": { 135 | "node": ">=14.21.3" 136 | } 137 | }, 138 | "node_modules/@biomejs/cli-linux-arm64-musl": { 139 | "version": "2.0.0-beta.6", 140 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-arm64-musl/-/cli-linux-arm64-musl-2.0.0-beta.6.tgz", 141 | "integrity": "sha512-70WOWJI1/vZ97OUAt6r9HpiP5+vlL7yAdIoVQzVLjQy1TArfltN38KKqp9fnhgX173liUh0gry//MrWkKHYrIQ==", 142 | "cpu": [ 143 | "arm64" 144 | ], 145 | "dev": true, 146 | "license": "MIT OR Apache-2.0", 147 | "optional": true, 148 | "os": [ 149 | "linux" 150 | ], 151 | "engines": { 152 | "node": ">=14.21.3" 153 | } 154 | }, 155 | "node_modules/@biomejs/cli-linux-x64": { 156 | "version": "2.0.0-beta.6", 157 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64/-/cli-linux-x64-2.0.0-beta.6.tgz", 158 | "integrity": "sha512-emqZAuAyRw4Ug4B+CTgozIxVg1QLol28oZyIWuIjWEDr7eOo6Ek9zSZGeusmbwIEPu6r6qon8JAV6OdukxEwIg==", 159 | "cpu": [ 160 | "x64" 161 | ], 162 | "dev": true, 163 | "license": "MIT OR Apache-2.0", 164 | "optional": true, 165 | "os": [ 166 | "linux" 167 | ], 168 | "engines": { 169 | "node": ">=14.21.3" 170 | } 171 | }, 172 | "node_modules/@biomejs/cli-linux-x64-musl": { 173 | "version": "2.0.0-beta.6", 174 | "resolved": "https://registry.npmjs.org/@biomejs/cli-linux-x64-musl/-/cli-linux-x64-musl-2.0.0-beta.6.tgz", 175 | "integrity": "sha512-G9ZIoaNs6q9+mOoMURoXvNRfCOs28jrS4R8+3/y0h9ttOXpd4VALPOAfjzBGPpMd/4RoEMHXw/1Ts4dKvrv9zw==", 176 | "cpu": [ 177 | "x64" 178 | ], 179 | "dev": true, 180 | "license": "MIT OR Apache-2.0", 181 | "optional": true, 182 | "os": [ 183 | "linux" 184 | ], 185 | "engines": { 186 | "node": ">=14.21.3" 187 | } 188 | }, 189 | "node_modules/@biomejs/cli-win32-arm64": { 190 | "version": "2.0.0-beta.6", 191 | "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-arm64/-/cli-win32-arm64-2.0.0-beta.6.tgz", 192 | "integrity": "sha512-JijYVZC6R5qq94yLaElowLLzbZ4xR2qDiOVPQV8H1+ru3IqVOjQu5f/lIt4uuea1iRFbxS+mOaxOZM9tUl1pTQ==", 193 | "cpu": [ 194 | "arm64" 195 | ], 196 | "dev": true, 197 | "license": "MIT OR Apache-2.0", 198 | "optional": true, 199 | "os": [ 200 | "win32" 201 | ], 202 | "engines": { 203 | "node": ">=14.21.3" 204 | } 205 | }, 206 | "node_modules/@biomejs/cli-win32-x64": { 207 | "version": "2.0.0-beta.6", 208 | "resolved": "https://registry.npmjs.org/@biomejs/cli-win32-x64/-/cli-win32-x64-2.0.0-beta.6.tgz", 209 | "integrity": "sha512-zs29t/nxon11dKV+ckQB1yUOmhYx17e2+cHGK8PCVamqVGSMbjrd5evjtlfbnVJXP0ar7nNKhcg4ZWYGJ6aR1w==", 210 | "cpu": [ 211 | "x64" 212 | ], 213 | "dev": true, 214 | "license": "MIT OR Apache-2.0", 215 | "optional": true, 216 | "os": [ 217 | "win32" 218 | ], 219 | "engines": { 220 | "node": ">=14.21.3" 221 | } 222 | }, 223 | "node_modules/@emnapi/runtime": { 224 | "version": "1.4.1", 225 | "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.1.tgz", 226 | "integrity": "sha512-LMshMVP0ZhACNjQNYXiU1iZJ6QCcv0lUdPDPugqGvCGXt5xtRVBPdtA0qU12pEXZzpWAhWlZYptfdAFq10DOVQ==", 227 | "license": "MIT", 228 | "optional": true, 229 | "dependencies": { 230 | "tslib": "^2.4.0" 231 | } 232 | }, 233 | "node_modules/@formatjs/ecma402-abstract": { 234 | "version": "2.3.4", 235 | "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.4.tgz", 236 | "integrity": "sha512-qrycXDeaORzIqNhBOx0btnhpD1c+/qFIHAN9znofuMJX6QBwtbrmlpWfD4oiUUD2vJUOIYFA/gYtg2KAMGG7sA==", 237 | "license": "MIT", 238 | "dependencies": { 239 | "@formatjs/fast-memoize": "2.2.7", 240 | "@formatjs/intl-localematcher": "0.6.1", 241 | "decimal.js": "^10.4.3", 242 | "tslib": "^2.8.0" 243 | } 244 | }, 245 | "node_modules/@formatjs/ecma402-abstract/node_modules/@formatjs/intl-localematcher": { 246 | "version": "0.6.1", 247 | "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.1.tgz", 248 | "integrity": "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg==", 249 | "license": "MIT", 250 | "dependencies": { 251 | "tslib": "^2.8.0" 252 | } 253 | }, 254 | "node_modules/@formatjs/fast-memoize": { 255 | "version": "2.2.7", 256 | "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz", 257 | "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==", 258 | "license": "MIT", 259 | "dependencies": { 260 | "tslib": "^2.8.0" 261 | } 262 | }, 263 | "node_modules/@formatjs/icu-messageformat-parser": { 264 | "version": "2.11.2", 265 | "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.2.tgz", 266 | "integrity": "sha512-AfiMi5NOSo2TQImsYAg8UYddsNJ/vUEv/HaNqiFjnI3ZFfWihUtD5QtuX6kHl8+H+d3qvnE/3HZrfzgdWpsLNA==", 267 | "license": "MIT", 268 | "dependencies": { 269 | "@formatjs/ecma402-abstract": "2.3.4", 270 | "@formatjs/icu-skeleton-parser": "1.8.14", 271 | "tslib": "^2.8.0" 272 | } 273 | }, 274 | "node_modules/@formatjs/icu-skeleton-parser": { 275 | "version": "1.8.14", 276 | "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.14.tgz", 277 | "integrity": "sha512-i4q4V4qslThK4Ig8SxyD76cp3+QJ3sAqr7f6q9VVfeGtxG9OhiAk3y9XF6Q41OymsKzsGQ6OQQoJNY4/lI8TcQ==", 278 | "license": "MIT", 279 | "dependencies": { 280 | "@formatjs/ecma402-abstract": "2.3.4", 281 | "tslib": "^2.8.0" 282 | } 283 | }, 284 | "node_modules/@formatjs/intl-localematcher": { 285 | "version": "0.5.10", 286 | "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", 287 | "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", 288 | "license": "MIT", 289 | "dependencies": { 290 | "tslib": "2" 291 | } 292 | }, 293 | "node_modules/@img/sharp-darwin-arm64": { 294 | "version": "0.34.1", 295 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", 296 | "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", 297 | "cpu": [ 298 | "arm64" 299 | ], 300 | "license": "Apache-2.0", 301 | "optional": true, 302 | "os": [ 303 | "darwin" 304 | ], 305 | "engines": { 306 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 307 | }, 308 | "funding": { 309 | "url": "https://opencollective.com/libvips" 310 | }, 311 | "optionalDependencies": { 312 | "@img/sharp-libvips-darwin-arm64": "1.1.0" 313 | } 314 | }, 315 | "node_modules/@img/sharp-darwin-x64": { 316 | "version": "0.34.1", 317 | "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", 318 | "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", 319 | "cpu": [ 320 | "x64" 321 | ], 322 | "license": "Apache-2.0", 323 | "optional": true, 324 | "os": [ 325 | "darwin" 326 | ], 327 | "engines": { 328 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 329 | }, 330 | "funding": { 331 | "url": "https://opencollective.com/libvips" 332 | }, 333 | "optionalDependencies": { 334 | "@img/sharp-libvips-darwin-x64": "1.1.0" 335 | } 336 | }, 337 | "node_modules/@img/sharp-libvips-darwin-arm64": { 338 | "version": "1.1.0", 339 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", 340 | "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", 341 | "cpu": [ 342 | "arm64" 343 | ], 344 | "license": "LGPL-3.0-or-later", 345 | "optional": true, 346 | "os": [ 347 | "darwin" 348 | ], 349 | "funding": { 350 | "url": "https://opencollective.com/libvips" 351 | } 352 | }, 353 | "node_modules/@img/sharp-libvips-darwin-x64": { 354 | "version": "1.1.0", 355 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", 356 | "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", 357 | "cpu": [ 358 | "x64" 359 | ], 360 | "license": "LGPL-3.0-or-later", 361 | "optional": true, 362 | "os": [ 363 | "darwin" 364 | ], 365 | "funding": { 366 | "url": "https://opencollective.com/libvips" 367 | } 368 | }, 369 | "node_modules/@img/sharp-libvips-linux-arm": { 370 | "version": "1.1.0", 371 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", 372 | "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", 373 | "cpu": [ 374 | "arm" 375 | ], 376 | "license": "LGPL-3.0-or-later", 377 | "optional": true, 378 | "os": [ 379 | "linux" 380 | ], 381 | "funding": { 382 | "url": "https://opencollective.com/libvips" 383 | } 384 | }, 385 | "node_modules/@img/sharp-libvips-linux-arm64": { 386 | "version": "1.1.0", 387 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", 388 | "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", 389 | "cpu": [ 390 | "arm64" 391 | ], 392 | "license": "LGPL-3.0-or-later", 393 | "optional": true, 394 | "os": [ 395 | "linux" 396 | ], 397 | "funding": { 398 | "url": "https://opencollective.com/libvips" 399 | } 400 | }, 401 | "node_modules/@img/sharp-libvips-linux-ppc64": { 402 | "version": "1.1.0", 403 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", 404 | "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", 405 | "cpu": [ 406 | "ppc64" 407 | ], 408 | "license": "LGPL-3.0-or-later", 409 | "optional": true, 410 | "os": [ 411 | "linux" 412 | ], 413 | "funding": { 414 | "url": "https://opencollective.com/libvips" 415 | } 416 | }, 417 | "node_modules/@img/sharp-libvips-linux-s390x": { 418 | "version": "1.1.0", 419 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", 420 | "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", 421 | "cpu": [ 422 | "s390x" 423 | ], 424 | "license": "LGPL-3.0-or-later", 425 | "optional": true, 426 | "os": [ 427 | "linux" 428 | ], 429 | "funding": { 430 | "url": "https://opencollective.com/libvips" 431 | } 432 | }, 433 | "node_modules/@img/sharp-libvips-linux-x64": { 434 | "version": "1.1.0", 435 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", 436 | "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", 437 | "cpu": [ 438 | "x64" 439 | ], 440 | "license": "LGPL-3.0-or-later", 441 | "optional": true, 442 | "os": [ 443 | "linux" 444 | ], 445 | "funding": { 446 | "url": "https://opencollective.com/libvips" 447 | } 448 | }, 449 | "node_modules/@img/sharp-libvips-linuxmusl-arm64": { 450 | "version": "1.1.0", 451 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", 452 | "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", 453 | "cpu": [ 454 | "arm64" 455 | ], 456 | "license": "LGPL-3.0-or-later", 457 | "optional": true, 458 | "os": [ 459 | "linux" 460 | ], 461 | "funding": { 462 | "url": "https://opencollective.com/libvips" 463 | } 464 | }, 465 | "node_modules/@img/sharp-libvips-linuxmusl-x64": { 466 | "version": "1.1.0", 467 | "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", 468 | "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", 469 | "cpu": [ 470 | "x64" 471 | ], 472 | "license": "LGPL-3.0-or-later", 473 | "optional": true, 474 | "os": [ 475 | "linux" 476 | ], 477 | "funding": { 478 | "url": "https://opencollective.com/libvips" 479 | } 480 | }, 481 | "node_modules/@img/sharp-linux-arm": { 482 | "version": "0.34.1", 483 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", 484 | "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", 485 | "cpu": [ 486 | "arm" 487 | ], 488 | "license": "Apache-2.0", 489 | "optional": true, 490 | "os": [ 491 | "linux" 492 | ], 493 | "engines": { 494 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 495 | }, 496 | "funding": { 497 | "url": "https://opencollective.com/libvips" 498 | }, 499 | "optionalDependencies": { 500 | "@img/sharp-libvips-linux-arm": "1.1.0" 501 | } 502 | }, 503 | "node_modules/@img/sharp-linux-arm64": { 504 | "version": "0.34.1", 505 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", 506 | "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", 507 | "cpu": [ 508 | "arm64" 509 | ], 510 | "license": "Apache-2.0", 511 | "optional": true, 512 | "os": [ 513 | "linux" 514 | ], 515 | "engines": { 516 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 517 | }, 518 | "funding": { 519 | "url": "https://opencollective.com/libvips" 520 | }, 521 | "optionalDependencies": { 522 | "@img/sharp-libvips-linux-arm64": "1.1.0" 523 | } 524 | }, 525 | "node_modules/@img/sharp-linux-s390x": { 526 | "version": "0.34.1", 527 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", 528 | "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", 529 | "cpu": [ 530 | "s390x" 531 | ], 532 | "license": "Apache-2.0", 533 | "optional": true, 534 | "os": [ 535 | "linux" 536 | ], 537 | "engines": { 538 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 539 | }, 540 | "funding": { 541 | "url": "https://opencollective.com/libvips" 542 | }, 543 | "optionalDependencies": { 544 | "@img/sharp-libvips-linux-s390x": "1.1.0" 545 | } 546 | }, 547 | "node_modules/@img/sharp-linux-x64": { 548 | "version": "0.34.1", 549 | "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", 550 | "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", 551 | "cpu": [ 552 | "x64" 553 | ], 554 | "license": "Apache-2.0", 555 | "optional": true, 556 | "os": [ 557 | "linux" 558 | ], 559 | "engines": { 560 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 561 | }, 562 | "funding": { 563 | "url": "https://opencollective.com/libvips" 564 | }, 565 | "optionalDependencies": { 566 | "@img/sharp-libvips-linux-x64": "1.1.0" 567 | } 568 | }, 569 | "node_modules/@img/sharp-linuxmusl-arm64": { 570 | "version": "0.34.1", 571 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", 572 | "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", 573 | "cpu": [ 574 | "arm64" 575 | ], 576 | "license": "Apache-2.0", 577 | "optional": true, 578 | "os": [ 579 | "linux" 580 | ], 581 | "engines": { 582 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 583 | }, 584 | "funding": { 585 | "url": "https://opencollective.com/libvips" 586 | }, 587 | "optionalDependencies": { 588 | "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" 589 | } 590 | }, 591 | "node_modules/@img/sharp-linuxmusl-x64": { 592 | "version": "0.34.1", 593 | "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", 594 | "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", 595 | "cpu": [ 596 | "x64" 597 | ], 598 | "license": "Apache-2.0", 599 | "optional": true, 600 | "os": [ 601 | "linux" 602 | ], 603 | "engines": { 604 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 605 | }, 606 | "funding": { 607 | "url": "https://opencollective.com/libvips" 608 | }, 609 | "optionalDependencies": { 610 | "@img/sharp-libvips-linuxmusl-x64": "1.1.0" 611 | } 612 | }, 613 | "node_modules/@img/sharp-wasm32": { 614 | "version": "0.34.1", 615 | "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", 616 | "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", 617 | "cpu": [ 618 | "wasm32" 619 | ], 620 | "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", 621 | "optional": true, 622 | "dependencies": { 623 | "@emnapi/runtime": "^1.4.0" 624 | }, 625 | "engines": { 626 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 627 | }, 628 | "funding": { 629 | "url": "https://opencollective.com/libvips" 630 | } 631 | }, 632 | "node_modules/@img/sharp-win32-ia32": { 633 | "version": "0.34.1", 634 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", 635 | "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", 636 | "cpu": [ 637 | "ia32" 638 | ], 639 | "license": "Apache-2.0 AND LGPL-3.0-or-later", 640 | "optional": true, 641 | "os": [ 642 | "win32" 643 | ], 644 | "engines": { 645 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 646 | }, 647 | "funding": { 648 | "url": "https://opencollective.com/libvips" 649 | } 650 | }, 651 | "node_modules/@img/sharp-win32-x64": { 652 | "version": "0.34.1", 653 | "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", 654 | "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", 655 | "cpu": [ 656 | "x64" 657 | ], 658 | "license": "Apache-2.0 AND LGPL-3.0-or-later", 659 | "optional": true, 660 | "os": [ 661 | "win32" 662 | ], 663 | "engines": { 664 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 665 | }, 666 | "funding": { 667 | "url": "https://opencollective.com/libvips" 668 | } 669 | }, 670 | "node_modules/@isaacs/fs-minipass": { 671 | "version": "4.0.1", 672 | "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", 673 | "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", 674 | "dev": true, 675 | "license": "ISC", 676 | "dependencies": { 677 | "minipass": "^7.0.4" 678 | }, 679 | "engines": { 680 | "node": ">=18.0.0" 681 | } 682 | }, 683 | "node_modules/@jridgewell/gen-mapping": { 684 | "version": "0.3.8", 685 | "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", 686 | "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", 687 | "dev": true, 688 | "license": "MIT", 689 | "dependencies": { 690 | "@jridgewell/set-array": "^1.2.1", 691 | "@jridgewell/sourcemap-codec": "^1.4.10", 692 | "@jridgewell/trace-mapping": "^0.3.24" 693 | }, 694 | "engines": { 695 | "node": ">=6.0.0" 696 | } 697 | }, 698 | "node_modules/@jridgewell/resolve-uri": { 699 | "version": "3.1.2", 700 | "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", 701 | "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", 702 | "dev": true, 703 | "license": "MIT", 704 | "engines": { 705 | "node": ">=6.0.0" 706 | } 707 | }, 708 | "node_modules/@jridgewell/set-array": { 709 | "version": "1.2.1", 710 | "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", 711 | "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", 712 | "dev": true, 713 | "license": "MIT", 714 | "engines": { 715 | "node": ">=6.0.0" 716 | } 717 | }, 718 | "node_modules/@jridgewell/sourcemap-codec": { 719 | "version": "1.5.0", 720 | "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", 721 | "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", 722 | "dev": true, 723 | "license": "MIT" 724 | }, 725 | "node_modules/@jridgewell/trace-mapping": { 726 | "version": "0.3.25", 727 | "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", 728 | "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", 729 | "dev": true, 730 | "license": "MIT", 731 | "dependencies": { 732 | "@jridgewell/resolve-uri": "^3.1.0", 733 | "@jridgewell/sourcemap-codec": "^1.4.14" 734 | } 735 | }, 736 | "node_modules/@next/env": { 737 | "version": "15.3.3", 738 | "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", 739 | "integrity": "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==", 740 | "license": "MIT" 741 | }, 742 | "node_modules/@next/swc-darwin-arm64": { 743 | "version": "15.3.3", 744 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz", 745 | "integrity": "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==", 746 | "cpu": [ 747 | "arm64" 748 | ], 749 | "license": "MIT", 750 | "optional": true, 751 | "os": [ 752 | "darwin" 753 | ], 754 | "engines": { 755 | "node": ">= 10" 756 | } 757 | }, 758 | "node_modules/@next/swc-darwin-x64": { 759 | "version": "15.3.3", 760 | "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", 761 | "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", 762 | "cpu": [ 763 | "x64" 764 | ], 765 | "license": "MIT", 766 | "optional": true, 767 | "os": [ 768 | "darwin" 769 | ], 770 | "engines": { 771 | "node": ">= 10" 772 | } 773 | }, 774 | "node_modules/@next/swc-linux-arm64-gnu": { 775 | "version": "15.3.3", 776 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", 777 | "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", 778 | "cpu": [ 779 | "arm64" 780 | ], 781 | "license": "MIT", 782 | "optional": true, 783 | "os": [ 784 | "linux" 785 | ], 786 | "engines": { 787 | "node": ">= 10" 788 | } 789 | }, 790 | "node_modules/@next/swc-linux-arm64-musl": { 791 | "version": "15.3.3", 792 | "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", 793 | "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", 794 | "cpu": [ 795 | "arm64" 796 | ], 797 | "license": "MIT", 798 | "optional": true, 799 | "os": [ 800 | "linux" 801 | ], 802 | "engines": { 803 | "node": ">= 10" 804 | } 805 | }, 806 | "node_modules/@next/swc-linux-x64-gnu": { 807 | "version": "15.3.3", 808 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", 809 | "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", 810 | "cpu": [ 811 | "x64" 812 | ], 813 | "license": "MIT", 814 | "optional": true, 815 | "os": [ 816 | "linux" 817 | ], 818 | "engines": { 819 | "node": ">= 10" 820 | } 821 | }, 822 | "node_modules/@next/swc-linux-x64-musl": { 823 | "version": "15.3.3", 824 | "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", 825 | "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", 826 | "cpu": [ 827 | "x64" 828 | ], 829 | "license": "MIT", 830 | "optional": true, 831 | "os": [ 832 | "linux" 833 | ], 834 | "engines": { 835 | "node": ">= 10" 836 | } 837 | }, 838 | "node_modules/@next/swc-win32-arm64-msvc": { 839 | "version": "15.3.3", 840 | "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", 841 | "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", 842 | "cpu": [ 843 | "arm64" 844 | ], 845 | "license": "MIT", 846 | "optional": true, 847 | "os": [ 848 | "win32" 849 | ], 850 | "engines": { 851 | "node": ">= 10" 852 | } 853 | }, 854 | "node_modules/@next/swc-win32-x64-msvc": { 855 | "version": "15.3.3", 856 | "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", 857 | "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", 858 | "cpu": [ 859 | "x64" 860 | ], 861 | "license": "MIT", 862 | "optional": true, 863 | "os": [ 864 | "win32" 865 | ], 866 | "engines": { 867 | "node": ">= 10" 868 | } 869 | }, 870 | "node_modules/@radix-ui/react-compose-refs": { 871 | "version": "1.1.2", 872 | "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz", 873 | "integrity": "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==", 874 | "license": "MIT", 875 | "peerDependencies": { 876 | "@types/react": "*", 877 | "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 878 | }, 879 | "peerDependenciesMeta": { 880 | "@types/react": { 881 | "optional": true 882 | } 883 | } 884 | }, 885 | "node_modules/@radix-ui/react-slot": { 886 | "version": "1.2.3", 887 | "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz", 888 | "integrity": "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==", 889 | "license": "MIT", 890 | "dependencies": { 891 | "@radix-ui/react-compose-refs": "1.1.2" 892 | }, 893 | "peerDependencies": { 894 | "@types/react": "*", 895 | "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" 896 | }, 897 | "peerDependenciesMeta": { 898 | "@types/react": { 899 | "optional": true 900 | } 901 | } 902 | }, 903 | "node_modules/@reduxjs/toolkit": { 904 | "version": "2.8.2", 905 | "resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.8.2.tgz", 906 | "integrity": "sha512-MYlOhQ0sLdw4ud48FoC5w0dH9VfWQjtCjreKwYTT3l+r427qYC5Y8PihNutepr8XrNaBUDQo9khWUwQxZaqt5A==", 907 | "license": "MIT", 908 | "dependencies": { 909 | "@standard-schema/spec": "^1.0.0", 910 | "@standard-schema/utils": "^0.3.0", 911 | "immer": "^10.0.3", 912 | "redux": "^5.0.1", 913 | "redux-thunk": "^3.1.0", 914 | "reselect": "^5.1.0" 915 | }, 916 | "peerDependencies": { 917 | "react": "^16.9.0 || ^17.0.0 || ^18 || ^19", 918 | "react-redux": "^7.2.1 || ^8.1.3 || ^9.0.0" 919 | }, 920 | "peerDependenciesMeta": { 921 | "react": { 922 | "optional": true 923 | }, 924 | "react-redux": { 925 | "optional": true 926 | } 927 | } 928 | }, 929 | "node_modules/@schummar/icu-type-parser": { 930 | "version": "1.21.5", 931 | "resolved": "https://registry.npmjs.org/@schummar/icu-type-parser/-/icu-type-parser-1.21.5.tgz", 932 | "integrity": "sha512-bXHSaW5jRTmke9Vd0h5P7BtWZG9Znqb8gSDxZnxaGSJnGwPLDPfS+3g0BKzeWqzgZPsIVZkM7m2tbo18cm5HBw==", 933 | "license": "MIT" 934 | }, 935 | "node_modules/@standard-schema/spec": { 936 | "version": "1.0.0", 937 | "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", 938 | "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", 939 | "license": "MIT" 940 | }, 941 | "node_modules/@standard-schema/utils": { 942 | "version": "0.3.0", 943 | "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", 944 | "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==", 945 | "license": "MIT" 946 | }, 947 | "node_modules/@swc/counter": { 948 | "version": "0.1.3", 949 | "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", 950 | "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", 951 | "license": "Apache-2.0" 952 | }, 953 | "node_modules/@swc/helpers": { 954 | "version": "0.5.15", 955 | "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", 956 | "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", 957 | "license": "Apache-2.0", 958 | "dependencies": { 959 | "tslib": "^2.8.0" 960 | } 961 | }, 962 | "node_modules/@tailwindcss/node": { 963 | "version": "4.1.8", 964 | "resolved": "https://registry.npmjs.org/@tailwindcss/node/-/node-4.1.8.tgz", 965 | "integrity": "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q==", 966 | "dev": true, 967 | "license": "MIT", 968 | "dependencies": { 969 | "@ampproject/remapping": "^2.3.0", 970 | "enhanced-resolve": "^5.18.1", 971 | "jiti": "^2.4.2", 972 | "lightningcss": "1.30.1", 973 | "magic-string": "^0.30.17", 974 | "source-map-js": "^1.2.1", 975 | "tailwindcss": "4.1.8" 976 | } 977 | }, 978 | "node_modules/@tailwindcss/oxide": { 979 | "version": "4.1.8", 980 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide/-/oxide-4.1.8.tgz", 981 | "integrity": "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A==", 982 | "dev": true, 983 | "hasInstallScript": true, 984 | "license": "MIT", 985 | "dependencies": { 986 | "detect-libc": "^2.0.4", 987 | "tar": "^7.4.3" 988 | }, 989 | "engines": { 990 | "node": ">= 10" 991 | }, 992 | "optionalDependencies": { 993 | "@tailwindcss/oxide-android-arm64": "4.1.8", 994 | "@tailwindcss/oxide-darwin-arm64": "4.1.8", 995 | "@tailwindcss/oxide-darwin-x64": "4.1.8", 996 | "@tailwindcss/oxide-freebsd-x64": "4.1.8", 997 | "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", 998 | "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", 999 | "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", 1000 | "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", 1001 | "@tailwindcss/oxide-linux-x64-musl": "4.1.8", 1002 | "@tailwindcss/oxide-wasm32-wasi": "4.1.8", 1003 | "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", 1004 | "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" 1005 | } 1006 | }, 1007 | "node_modules/@tailwindcss/oxide-android-arm64": { 1008 | "version": "4.1.8", 1009 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-android-arm64/-/oxide-android-arm64-4.1.8.tgz", 1010 | "integrity": "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg==", 1011 | "cpu": [ 1012 | "arm64" 1013 | ], 1014 | "dev": true, 1015 | "license": "MIT", 1016 | "optional": true, 1017 | "os": [ 1018 | "android" 1019 | ], 1020 | "engines": { 1021 | "node": ">= 10" 1022 | } 1023 | }, 1024 | "node_modules/@tailwindcss/oxide-darwin-arm64": { 1025 | "version": "4.1.8", 1026 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-arm64/-/oxide-darwin-arm64-4.1.8.tgz", 1027 | "integrity": "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A==", 1028 | "cpu": [ 1029 | "arm64" 1030 | ], 1031 | "dev": true, 1032 | "license": "MIT", 1033 | "optional": true, 1034 | "os": [ 1035 | "darwin" 1036 | ], 1037 | "engines": { 1038 | "node": ">= 10" 1039 | } 1040 | }, 1041 | "node_modules/@tailwindcss/oxide-darwin-x64": { 1042 | "version": "4.1.8", 1043 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-darwin-x64/-/oxide-darwin-x64-4.1.8.tgz", 1044 | "integrity": "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw==", 1045 | "cpu": [ 1046 | "x64" 1047 | ], 1048 | "dev": true, 1049 | "license": "MIT", 1050 | "optional": true, 1051 | "os": [ 1052 | "darwin" 1053 | ], 1054 | "engines": { 1055 | "node": ">= 10" 1056 | } 1057 | }, 1058 | "node_modules/@tailwindcss/oxide-freebsd-x64": { 1059 | "version": "4.1.8", 1060 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-freebsd-x64/-/oxide-freebsd-x64-4.1.8.tgz", 1061 | "integrity": "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg==", 1062 | "cpu": [ 1063 | "x64" 1064 | ], 1065 | "dev": true, 1066 | "license": "MIT", 1067 | "optional": true, 1068 | "os": [ 1069 | "freebsd" 1070 | ], 1071 | "engines": { 1072 | "node": ">= 10" 1073 | } 1074 | }, 1075 | "node_modules/@tailwindcss/oxide-linux-arm-gnueabihf": { 1076 | "version": "4.1.8", 1077 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm-gnueabihf/-/oxide-linux-arm-gnueabihf-4.1.8.tgz", 1078 | "integrity": "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ==", 1079 | "cpu": [ 1080 | "arm" 1081 | ], 1082 | "dev": true, 1083 | "license": "MIT", 1084 | "optional": true, 1085 | "os": [ 1086 | "linux" 1087 | ], 1088 | "engines": { 1089 | "node": ">= 10" 1090 | } 1091 | }, 1092 | "node_modules/@tailwindcss/oxide-linux-arm64-gnu": { 1093 | "version": "4.1.8", 1094 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-gnu/-/oxide-linux-arm64-gnu-4.1.8.tgz", 1095 | "integrity": "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q==", 1096 | "cpu": [ 1097 | "arm64" 1098 | ], 1099 | "dev": true, 1100 | "license": "MIT", 1101 | "optional": true, 1102 | "os": [ 1103 | "linux" 1104 | ], 1105 | "engines": { 1106 | "node": ">= 10" 1107 | } 1108 | }, 1109 | "node_modules/@tailwindcss/oxide-linux-arm64-musl": { 1110 | "version": "4.1.8", 1111 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-arm64-musl/-/oxide-linux-arm64-musl-4.1.8.tgz", 1112 | "integrity": "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ==", 1113 | "cpu": [ 1114 | "arm64" 1115 | ], 1116 | "dev": true, 1117 | "license": "MIT", 1118 | "optional": true, 1119 | "os": [ 1120 | "linux" 1121 | ], 1122 | "engines": { 1123 | "node": ">= 10" 1124 | } 1125 | }, 1126 | "node_modules/@tailwindcss/oxide-linux-x64-gnu": { 1127 | "version": "4.1.8", 1128 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-gnu/-/oxide-linux-x64-gnu-4.1.8.tgz", 1129 | "integrity": "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g==", 1130 | "cpu": [ 1131 | "x64" 1132 | ], 1133 | "dev": true, 1134 | "license": "MIT", 1135 | "optional": true, 1136 | "os": [ 1137 | "linux" 1138 | ], 1139 | "engines": { 1140 | "node": ">= 10" 1141 | } 1142 | }, 1143 | "node_modules/@tailwindcss/oxide-linux-x64-musl": { 1144 | "version": "4.1.8", 1145 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-linux-x64-musl/-/oxide-linux-x64-musl-4.1.8.tgz", 1146 | "integrity": "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg==", 1147 | "cpu": [ 1148 | "x64" 1149 | ], 1150 | "dev": true, 1151 | "license": "MIT", 1152 | "optional": true, 1153 | "os": [ 1154 | "linux" 1155 | ], 1156 | "engines": { 1157 | "node": ">= 10" 1158 | } 1159 | }, 1160 | "node_modules/@tailwindcss/oxide-wasm32-wasi": { 1161 | "version": "4.1.8", 1162 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-wasm32-wasi/-/oxide-wasm32-wasi-4.1.8.tgz", 1163 | "integrity": "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg==", 1164 | "bundleDependencies": [ 1165 | "@napi-rs/wasm-runtime", 1166 | "@emnapi/core", 1167 | "@emnapi/runtime", 1168 | "@tybys/wasm-util", 1169 | "@emnapi/wasi-threads", 1170 | "tslib" 1171 | ], 1172 | "cpu": [ 1173 | "wasm32" 1174 | ], 1175 | "dev": true, 1176 | "license": "MIT", 1177 | "optional": true, 1178 | "dependencies": { 1179 | "@emnapi/core": "^1.4.3", 1180 | "@emnapi/runtime": "^1.4.3", 1181 | "@emnapi/wasi-threads": "^1.0.2", 1182 | "@napi-rs/wasm-runtime": "^0.2.10", 1183 | "@tybys/wasm-util": "^0.9.0", 1184 | "tslib": "^2.8.0" 1185 | }, 1186 | "engines": { 1187 | "node": ">=14.0.0" 1188 | } 1189 | }, 1190 | "node_modules/@tailwindcss/oxide-win32-arm64-msvc": { 1191 | "version": "4.1.8", 1192 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-arm64-msvc/-/oxide-win32-arm64-msvc-4.1.8.tgz", 1193 | "integrity": "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA==", 1194 | "cpu": [ 1195 | "arm64" 1196 | ], 1197 | "dev": true, 1198 | "license": "MIT", 1199 | "optional": true, 1200 | "os": [ 1201 | "win32" 1202 | ], 1203 | "engines": { 1204 | "node": ">= 10" 1205 | } 1206 | }, 1207 | "node_modules/@tailwindcss/oxide-win32-x64-msvc": { 1208 | "version": "4.1.8", 1209 | "resolved": "https://registry.npmjs.org/@tailwindcss/oxide-win32-x64-msvc/-/oxide-win32-x64-msvc-4.1.8.tgz", 1210 | "integrity": "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ==", 1211 | "cpu": [ 1212 | "x64" 1213 | ], 1214 | "dev": true, 1215 | "license": "MIT", 1216 | "optional": true, 1217 | "os": [ 1218 | "win32" 1219 | ], 1220 | "engines": { 1221 | "node": ">= 10" 1222 | } 1223 | }, 1224 | "node_modules/@tailwindcss/postcss": { 1225 | "version": "4.1.8", 1226 | "resolved": "https://registry.npmjs.org/@tailwindcss/postcss/-/postcss-4.1.8.tgz", 1227 | "integrity": "sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw==", 1228 | "dev": true, 1229 | "license": "MIT", 1230 | "dependencies": { 1231 | "@alloc/quick-lru": "^5.2.0", 1232 | "@tailwindcss/node": "4.1.8", 1233 | "@tailwindcss/oxide": "4.1.8", 1234 | "postcss": "^8.4.41", 1235 | "tailwindcss": "4.1.8" 1236 | } 1237 | }, 1238 | "node_modules/@types/node": { 1239 | "version": "22.15.30", 1240 | "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.30.tgz", 1241 | "integrity": "sha512-6Q7lr06bEHdlfplU6YRbgG1SFBdlsfNC4/lX+SkhiTs0cpJkOElmWls8PxDFv4yY/xKb8Y6SO0OmSX4wgqTZbA==", 1242 | "dev": true, 1243 | "license": "MIT", 1244 | "dependencies": { 1245 | "undici-types": "~6.21.0" 1246 | } 1247 | }, 1248 | "node_modules/@types/react": { 1249 | "version": "19.1.6", 1250 | "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz", 1251 | "integrity": "sha512-JeG0rEWak0N6Itr6QUx+X60uQmN+5t3j9r/OVDtWzFXKaj6kD1BwJzOksD0FF6iWxZlbE1kB0q9vtnU2ekqa1Q==", 1252 | "devOptional": true, 1253 | "license": "MIT", 1254 | "dependencies": { 1255 | "csstype": "^3.0.2" 1256 | } 1257 | }, 1258 | "node_modules/@types/react-dom": { 1259 | "version": "19.1.6", 1260 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.6.tgz", 1261 | "integrity": "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw==", 1262 | "dev": true, 1263 | "license": "MIT", 1264 | "peerDependencies": { 1265 | "@types/react": "^19.0.0" 1266 | } 1267 | }, 1268 | "node_modules/@types/use-sync-external-store": { 1269 | "version": "0.0.6", 1270 | "resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz", 1271 | "integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==", 1272 | "license": "MIT" 1273 | }, 1274 | "node_modules/busboy": { 1275 | "version": "1.6.0", 1276 | "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", 1277 | "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", 1278 | "dependencies": { 1279 | "streamsearch": "^1.1.0" 1280 | }, 1281 | "engines": { 1282 | "node": ">=10.16.0" 1283 | } 1284 | }, 1285 | "node_modules/caniuse-lite": { 1286 | "version": "1.0.30001679", 1287 | "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", 1288 | "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", 1289 | "funding": [ 1290 | { 1291 | "type": "opencollective", 1292 | "url": "https://opencollective.com/browserslist" 1293 | }, 1294 | { 1295 | "type": "tidelift", 1296 | "url": "https://tidelift.com/funding/github/npm/caniuse-lite" 1297 | }, 1298 | { 1299 | "type": "github", 1300 | "url": "https://github.com/sponsors/ai" 1301 | } 1302 | ], 1303 | "license": "CC-BY-4.0" 1304 | }, 1305 | "node_modules/chownr": { 1306 | "version": "3.0.0", 1307 | "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", 1308 | "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", 1309 | "dev": true, 1310 | "license": "BlueOak-1.0.0", 1311 | "engines": { 1312 | "node": ">=18" 1313 | } 1314 | }, 1315 | "node_modules/class-variance-authority": { 1316 | "version": "0.7.1", 1317 | "resolved": "https://registry.npmjs.org/class-variance-authority/-/class-variance-authority-0.7.1.tgz", 1318 | "integrity": "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==", 1319 | "license": "Apache-2.0", 1320 | "dependencies": { 1321 | "clsx": "^2.1.1" 1322 | }, 1323 | "funding": { 1324 | "url": "https://polar.sh/cva" 1325 | } 1326 | }, 1327 | "node_modules/client-only": { 1328 | "version": "0.0.1", 1329 | "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", 1330 | "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", 1331 | "license": "MIT" 1332 | }, 1333 | "node_modules/clsx": { 1334 | "version": "2.1.1", 1335 | "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", 1336 | "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", 1337 | "license": "MIT", 1338 | "engines": { 1339 | "node": ">=6" 1340 | } 1341 | }, 1342 | "node_modules/color": { 1343 | "version": "4.2.3", 1344 | "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", 1345 | "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", 1346 | "license": "MIT", 1347 | "optional": true, 1348 | "dependencies": { 1349 | "color-convert": "^2.0.1", 1350 | "color-string": "^1.9.0" 1351 | }, 1352 | "engines": { 1353 | "node": ">=12.5.0" 1354 | } 1355 | }, 1356 | "node_modules/color-convert": { 1357 | "version": "2.0.1", 1358 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 1359 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 1360 | "license": "MIT", 1361 | "optional": true, 1362 | "dependencies": { 1363 | "color-name": "~1.1.4" 1364 | }, 1365 | "engines": { 1366 | "node": ">=7.0.0" 1367 | } 1368 | }, 1369 | "node_modules/color-name": { 1370 | "version": "1.1.4", 1371 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 1372 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 1373 | "license": "MIT", 1374 | "optional": true 1375 | }, 1376 | "node_modules/color-string": { 1377 | "version": "1.9.1", 1378 | "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", 1379 | "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", 1380 | "license": "MIT", 1381 | "optional": true, 1382 | "dependencies": { 1383 | "color-name": "^1.0.0", 1384 | "simple-swizzle": "^0.2.2" 1385 | } 1386 | }, 1387 | "node_modules/csstype": { 1388 | "version": "3.1.2", 1389 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", 1390 | "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==", 1391 | "devOptional": true 1392 | }, 1393 | "node_modules/decimal.js": { 1394 | "version": "10.5.0", 1395 | "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", 1396 | "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", 1397 | "license": "MIT" 1398 | }, 1399 | "node_modules/detect-libc": { 1400 | "version": "2.0.4", 1401 | "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", 1402 | "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", 1403 | "devOptional": true, 1404 | "license": "Apache-2.0", 1405 | "engines": { 1406 | "node": ">=8" 1407 | } 1408 | }, 1409 | "node_modules/enhanced-resolve": { 1410 | "version": "5.18.1", 1411 | "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", 1412 | "integrity": "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg==", 1413 | "dev": true, 1414 | "license": "MIT", 1415 | "dependencies": { 1416 | "graceful-fs": "^4.2.4", 1417 | "tapable": "^2.2.0" 1418 | }, 1419 | "engines": { 1420 | "node": ">=10.13.0" 1421 | } 1422 | }, 1423 | "node_modules/graceful-fs": { 1424 | "version": "4.2.11", 1425 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 1426 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", 1427 | "dev": true, 1428 | "license": "ISC" 1429 | }, 1430 | "node_modules/immer": { 1431 | "version": "10.0.3", 1432 | "resolved": "https://registry.npmjs.org/immer/-/immer-10.0.3.tgz", 1433 | "integrity": "sha512-pwupu3eWfouuaowscykeckFmVTpqbzW+rXFCX8rQLkZzM9ftBmU/++Ra+o+L27mz03zJTlyV4UUr+fdKNffo4A==", 1434 | "funding": { 1435 | "type": "opencollective", 1436 | "url": "https://opencollective.com/immer" 1437 | } 1438 | }, 1439 | "node_modules/intl-messageformat": { 1440 | "version": "10.7.16", 1441 | "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.16.tgz", 1442 | "integrity": "sha512-UmdmHUmp5CIKKjSoE10la5yfU+AYJAaiYLsodbjL4lji83JNvgOQUjGaGhGrpFCb0Uh7sl7qfP1IyILa8Z40ug==", 1443 | "license": "BSD-3-Clause", 1444 | "dependencies": { 1445 | "@formatjs/ecma402-abstract": "2.3.4", 1446 | "@formatjs/fast-memoize": "2.2.7", 1447 | "@formatjs/icu-messageformat-parser": "2.11.2", 1448 | "tslib": "^2.8.0" 1449 | } 1450 | }, 1451 | "node_modules/is-arrayish": { 1452 | "version": "0.3.2", 1453 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", 1454 | "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", 1455 | "license": "MIT", 1456 | "optional": true 1457 | }, 1458 | "node_modules/jiti": { 1459 | "version": "2.4.2", 1460 | "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.4.2.tgz", 1461 | "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", 1462 | "dev": true, 1463 | "license": "MIT", 1464 | "bin": { 1465 | "jiti": "lib/jiti-cli.mjs" 1466 | } 1467 | }, 1468 | "node_modules/lightningcss": { 1469 | "version": "1.30.1", 1470 | "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.30.1.tgz", 1471 | "integrity": "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg==", 1472 | "dev": true, 1473 | "license": "MPL-2.0", 1474 | "dependencies": { 1475 | "detect-libc": "^2.0.3" 1476 | }, 1477 | "engines": { 1478 | "node": ">= 12.0.0" 1479 | }, 1480 | "funding": { 1481 | "type": "opencollective", 1482 | "url": "https://opencollective.com/parcel" 1483 | }, 1484 | "optionalDependencies": { 1485 | "lightningcss-darwin-arm64": "1.30.1", 1486 | "lightningcss-darwin-x64": "1.30.1", 1487 | "lightningcss-freebsd-x64": "1.30.1", 1488 | "lightningcss-linux-arm-gnueabihf": "1.30.1", 1489 | "lightningcss-linux-arm64-gnu": "1.30.1", 1490 | "lightningcss-linux-arm64-musl": "1.30.1", 1491 | "lightningcss-linux-x64-gnu": "1.30.1", 1492 | "lightningcss-linux-x64-musl": "1.30.1", 1493 | "lightningcss-win32-arm64-msvc": "1.30.1", 1494 | "lightningcss-win32-x64-msvc": "1.30.1" 1495 | } 1496 | }, 1497 | "node_modules/lightningcss-darwin-arm64": { 1498 | "version": "1.30.1", 1499 | "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.30.1.tgz", 1500 | "integrity": "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ==", 1501 | "cpu": [ 1502 | "arm64" 1503 | ], 1504 | "dev": true, 1505 | "license": "MPL-2.0", 1506 | "optional": true, 1507 | "os": [ 1508 | "darwin" 1509 | ], 1510 | "engines": { 1511 | "node": ">= 12.0.0" 1512 | }, 1513 | "funding": { 1514 | "type": "opencollective", 1515 | "url": "https://opencollective.com/parcel" 1516 | } 1517 | }, 1518 | "node_modules/lightningcss-darwin-x64": { 1519 | "version": "1.30.1", 1520 | "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.30.1.tgz", 1521 | "integrity": "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA==", 1522 | "cpu": [ 1523 | "x64" 1524 | ], 1525 | "dev": true, 1526 | "license": "MPL-2.0", 1527 | "optional": true, 1528 | "os": [ 1529 | "darwin" 1530 | ], 1531 | "engines": { 1532 | "node": ">= 12.0.0" 1533 | }, 1534 | "funding": { 1535 | "type": "opencollective", 1536 | "url": "https://opencollective.com/parcel" 1537 | } 1538 | }, 1539 | "node_modules/lightningcss-freebsd-x64": { 1540 | "version": "1.30.1", 1541 | "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.30.1.tgz", 1542 | "integrity": "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig==", 1543 | "cpu": [ 1544 | "x64" 1545 | ], 1546 | "dev": true, 1547 | "license": "MPL-2.0", 1548 | "optional": true, 1549 | "os": [ 1550 | "freebsd" 1551 | ], 1552 | "engines": { 1553 | "node": ">= 12.0.0" 1554 | }, 1555 | "funding": { 1556 | "type": "opencollective", 1557 | "url": "https://opencollective.com/parcel" 1558 | } 1559 | }, 1560 | "node_modules/lightningcss-linux-arm-gnueabihf": { 1561 | "version": "1.30.1", 1562 | "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.30.1.tgz", 1563 | "integrity": "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q==", 1564 | "cpu": [ 1565 | "arm" 1566 | ], 1567 | "dev": true, 1568 | "license": "MPL-2.0", 1569 | "optional": true, 1570 | "os": [ 1571 | "linux" 1572 | ], 1573 | "engines": { 1574 | "node": ">= 12.0.0" 1575 | }, 1576 | "funding": { 1577 | "type": "opencollective", 1578 | "url": "https://opencollective.com/parcel" 1579 | } 1580 | }, 1581 | "node_modules/lightningcss-linux-arm64-gnu": { 1582 | "version": "1.30.1", 1583 | "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.30.1.tgz", 1584 | "integrity": "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw==", 1585 | "cpu": [ 1586 | "arm64" 1587 | ], 1588 | "dev": true, 1589 | "license": "MPL-2.0", 1590 | "optional": true, 1591 | "os": [ 1592 | "linux" 1593 | ], 1594 | "engines": { 1595 | "node": ">= 12.0.0" 1596 | }, 1597 | "funding": { 1598 | "type": "opencollective", 1599 | "url": "https://opencollective.com/parcel" 1600 | } 1601 | }, 1602 | "node_modules/lightningcss-linux-arm64-musl": { 1603 | "version": "1.30.1", 1604 | "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.30.1.tgz", 1605 | "integrity": "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ==", 1606 | "cpu": [ 1607 | "arm64" 1608 | ], 1609 | "dev": true, 1610 | "license": "MPL-2.0", 1611 | "optional": true, 1612 | "os": [ 1613 | "linux" 1614 | ], 1615 | "engines": { 1616 | "node": ">= 12.0.0" 1617 | }, 1618 | "funding": { 1619 | "type": "opencollective", 1620 | "url": "https://opencollective.com/parcel" 1621 | } 1622 | }, 1623 | "node_modules/lightningcss-linux-x64-gnu": { 1624 | "version": "1.30.1", 1625 | "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.30.1.tgz", 1626 | "integrity": "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw==", 1627 | "cpu": [ 1628 | "x64" 1629 | ], 1630 | "dev": true, 1631 | "license": "MPL-2.0", 1632 | "optional": true, 1633 | "os": [ 1634 | "linux" 1635 | ], 1636 | "engines": { 1637 | "node": ">= 12.0.0" 1638 | }, 1639 | "funding": { 1640 | "type": "opencollective", 1641 | "url": "https://opencollective.com/parcel" 1642 | } 1643 | }, 1644 | "node_modules/lightningcss-linux-x64-musl": { 1645 | "version": "1.30.1", 1646 | "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.30.1.tgz", 1647 | "integrity": "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ==", 1648 | "cpu": [ 1649 | "x64" 1650 | ], 1651 | "dev": true, 1652 | "license": "MPL-2.0", 1653 | "optional": true, 1654 | "os": [ 1655 | "linux" 1656 | ], 1657 | "engines": { 1658 | "node": ">= 12.0.0" 1659 | }, 1660 | "funding": { 1661 | "type": "opencollective", 1662 | "url": "https://opencollective.com/parcel" 1663 | } 1664 | }, 1665 | "node_modules/lightningcss-win32-arm64-msvc": { 1666 | "version": "1.30.1", 1667 | "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.30.1.tgz", 1668 | "integrity": "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA==", 1669 | "cpu": [ 1670 | "arm64" 1671 | ], 1672 | "dev": true, 1673 | "license": "MPL-2.0", 1674 | "optional": true, 1675 | "os": [ 1676 | "win32" 1677 | ], 1678 | "engines": { 1679 | "node": ">= 12.0.0" 1680 | }, 1681 | "funding": { 1682 | "type": "opencollective", 1683 | "url": "https://opencollective.com/parcel" 1684 | } 1685 | }, 1686 | "node_modules/lightningcss-win32-x64-msvc": { 1687 | "version": "1.30.1", 1688 | "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.30.1.tgz", 1689 | "integrity": "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg==", 1690 | "cpu": [ 1691 | "x64" 1692 | ], 1693 | "dev": true, 1694 | "license": "MPL-2.0", 1695 | "optional": true, 1696 | "os": [ 1697 | "win32" 1698 | ], 1699 | "engines": { 1700 | "node": ">= 12.0.0" 1701 | }, 1702 | "funding": { 1703 | "type": "opencollective", 1704 | "url": "https://opencollective.com/parcel" 1705 | } 1706 | }, 1707 | "node_modules/lucide-react": { 1708 | "version": "0.513.0", 1709 | "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.513.0.tgz", 1710 | "integrity": "sha512-CJZKq2g8Y8yN4Aq002GahSXbG2JpFv9kXwyiOAMvUBv7pxeOFHUWKB0mO7MiY4ZVFCV4aNjv2BJFq/z3DgKPQg==", 1711 | "license": "ISC", 1712 | "peerDependencies": { 1713 | "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" 1714 | } 1715 | }, 1716 | "node_modules/magic-string": { 1717 | "version": "0.30.17", 1718 | "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", 1719 | "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", 1720 | "dev": true, 1721 | "license": "MIT", 1722 | "dependencies": { 1723 | "@jridgewell/sourcemap-codec": "^1.5.0" 1724 | } 1725 | }, 1726 | "node_modules/minipass": { 1727 | "version": "7.1.2", 1728 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 1729 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 1730 | "dev": true, 1731 | "license": "ISC", 1732 | "engines": { 1733 | "node": ">=16 || 14 >=14.17" 1734 | } 1735 | }, 1736 | "node_modules/minizlib": { 1737 | "version": "3.0.2", 1738 | "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", 1739 | "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", 1740 | "dev": true, 1741 | "license": "MIT", 1742 | "dependencies": { 1743 | "minipass": "^7.1.2" 1744 | }, 1745 | "engines": { 1746 | "node": ">= 18" 1747 | } 1748 | }, 1749 | "node_modules/mkdirp": { 1750 | "version": "3.0.1", 1751 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", 1752 | "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", 1753 | "dev": true, 1754 | "license": "MIT", 1755 | "bin": { 1756 | "mkdirp": "dist/cjs/src/bin.js" 1757 | }, 1758 | "engines": { 1759 | "node": ">=10" 1760 | }, 1761 | "funding": { 1762 | "url": "https://github.com/sponsors/isaacs" 1763 | } 1764 | }, 1765 | "node_modules/nanoid": { 1766 | "version": "3.3.8", 1767 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", 1768 | "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", 1769 | "funding": [ 1770 | { 1771 | "type": "github", 1772 | "url": "https://github.com/sponsors/ai" 1773 | } 1774 | ], 1775 | "license": "MIT", 1776 | "bin": { 1777 | "nanoid": "bin/nanoid.cjs" 1778 | }, 1779 | "engines": { 1780 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" 1781 | } 1782 | }, 1783 | "node_modules/negotiator": { 1784 | "version": "1.0.0", 1785 | "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", 1786 | "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", 1787 | "license": "MIT", 1788 | "engines": { 1789 | "node": ">= 0.6" 1790 | } 1791 | }, 1792 | "node_modules/next": { 1793 | "version": "15.3.3", 1794 | "resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz", 1795 | "integrity": "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==", 1796 | "license": "MIT", 1797 | "dependencies": { 1798 | "@next/env": "15.3.3", 1799 | "@swc/counter": "0.1.3", 1800 | "@swc/helpers": "0.5.15", 1801 | "busboy": "1.6.0", 1802 | "caniuse-lite": "^1.0.30001579", 1803 | "postcss": "8.4.31", 1804 | "styled-jsx": "5.1.6" 1805 | }, 1806 | "bin": { 1807 | "next": "dist/bin/next" 1808 | }, 1809 | "engines": { 1810 | "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" 1811 | }, 1812 | "optionalDependencies": { 1813 | "@next/swc-darwin-arm64": "15.3.3", 1814 | "@next/swc-darwin-x64": "15.3.3", 1815 | "@next/swc-linux-arm64-gnu": "15.3.3", 1816 | "@next/swc-linux-arm64-musl": "15.3.3", 1817 | "@next/swc-linux-x64-gnu": "15.3.3", 1818 | "@next/swc-linux-x64-musl": "15.3.3", 1819 | "@next/swc-win32-arm64-msvc": "15.3.3", 1820 | "@next/swc-win32-x64-msvc": "15.3.3", 1821 | "sharp": "^0.34.1" 1822 | }, 1823 | "peerDependencies": { 1824 | "@opentelemetry/api": "^1.1.0", 1825 | "@playwright/test": "^1.41.2", 1826 | "babel-plugin-react-compiler": "*", 1827 | "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", 1828 | "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", 1829 | "sass": "^1.3.0" 1830 | }, 1831 | "peerDependenciesMeta": { 1832 | "@opentelemetry/api": { 1833 | "optional": true 1834 | }, 1835 | "@playwright/test": { 1836 | "optional": true 1837 | }, 1838 | "babel-plugin-react-compiler": { 1839 | "optional": true 1840 | }, 1841 | "sass": { 1842 | "optional": true 1843 | } 1844 | } 1845 | }, 1846 | "node_modules/next-intl": { 1847 | "version": "4.1.0", 1848 | "resolved": "https://registry.npmjs.org/next-intl/-/next-intl-4.1.0.tgz", 1849 | "integrity": "sha512-JNJRjc7sdnfUxhZmGcvzDszZ60tQKrygV/VLsgzXhnJDxQPn1cN2rVpc53adA1SvBJwPK2O6Sc6b4gYSILjCzw==", 1850 | "funding": [ 1851 | { 1852 | "type": "individual", 1853 | "url": "https://github.com/sponsors/amannn" 1854 | } 1855 | ], 1856 | "license": "MIT", 1857 | "dependencies": { 1858 | "@formatjs/intl-localematcher": "^0.5.4", 1859 | "negotiator": "^1.0.0", 1860 | "use-intl": "^4.1.0" 1861 | }, 1862 | "peerDependencies": { 1863 | "next": "^12.0.0 || ^13.0.0 || ^14.0.0 || ^15.0.0", 1864 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0", 1865 | "typescript": "^5.0.0" 1866 | }, 1867 | "peerDependenciesMeta": { 1868 | "typescript": { 1869 | "optional": true 1870 | } 1871 | } 1872 | }, 1873 | "node_modules/next/node_modules/postcss": { 1874 | "version": "8.4.31", 1875 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", 1876 | "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", 1877 | "funding": [ 1878 | { 1879 | "type": "opencollective", 1880 | "url": "https://opencollective.com/postcss/" 1881 | }, 1882 | { 1883 | "type": "tidelift", 1884 | "url": "https://tidelift.com/funding/github/npm/postcss" 1885 | }, 1886 | { 1887 | "type": "github", 1888 | "url": "https://github.com/sponsors/ai" 1889 | } 1890 | ], 1891 | "license": "MIT", 1892 | "dependencies": { 1893 | "nanoid": "^3.3.6", 1894 | "picocolors": "^1.0.0", 1895 | "source-map-js": "^1.0.2" 1896 | }, 1897 | "engines": { 1898 | "node": "^10 || ^12 || >=14" 1899 | } 1900 | }, 1901 | "node_modules/picocolors": { 1902 | "version": "1.1.1", 1903 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 1904 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 1905 | "license": "ISC" 1906 | }, 1907 | "node_modules/postcss": { 1908 | "version": "8.4.49", 1909 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", 1910 | "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", 1911 | "dev": true, 1912 | "funding": [ 1913 | { 1914 | "type": "opencollective", 1915 | "url": "https://opencollective.com/postcss/" 1916 | }, 1917 | { 1918 | "type": "tidelift", 1919 | "url": "https://tidelift.com/funding/github/npm/postcss" 1920 | }, 1921 | { 1922 | "type": "github", 1923 | "url": "https://github.com/sponsors/ai" 1924 | } 1925 | ], 1926 | "license": "MIT", 1927 | "dependencies": { 1928 | "nanoid": "^3.3.7", 1929 | "picocolors": "^1.1.1", 1930 | "source-map-js": "^1.2.1" 1931 | }, 1932 | "engines": { 1933 | "node": "^10 || ^12 || >=14" 1934 | } 1935 | }, 1936 | "node_modules/react": { 1937 | "version": "19.1.0", 1938 | "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", 1939 | "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", 1940 | "license": "MIT", 1941 | "engines": { 1942 | "node": ">=0.10.0" 1943 | } 1944 | }, 1945 | "node_modules/react-dom": { 1946 | "version": "19.1.0", 1947 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", 1948 | "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", 1949 | "license": "MIT", 1950 | "dependencies": { 1951 | "scheduler": "^0.26.0" 1952 | }, 1953 | "peerDependencies": { 1954 | "react": "^19.1.0" 1955 | } 1956 | }, 1957 | "node_modules/react-redux": { 1958 | "version": "9.2.0", 1959 | "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz", 1960 | "integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==", 1961 | "license": "MIT", 1962 | "dependencies": { 1963 | "@types/use-sync-external-store": "^0.0.6", 1964 | "use-sync-external-store": "^1.4.0" 1965 | }, 1966 | "peerDependencies": { 1967 | "@types/react": "^18.2.25 || ^19", 1968 | "react": "^18.0 || ^19", 1969 | "redux": "^5.0.0" 1970 | }, 1971 | "peerDependenciesMeta": { 1972 | "@types/react": { 1973 | "optional": true 1974 | }, 1975 | "redux": { 1976 | "optional": true 1977 | } 1978 | } 1979 | }, 1980 | "node_modules/redux": { 1981 | "version": "5.0.1", 1982 | "resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz", 1983 | "integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==" 1984 | }, 1985 | "node_modules/redux-thunk": { 1986 | "version": "3.1.0", 1987 | "resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-3.1.0.tgz", 1988 | "integrity": "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==", 1989 | "peerDependencies": { 1990 | "redux": "^5.0.0" 1991 | } 1992 | }, 1993 | "node_modules/reselect": { 1994 | "version": "5.1.1", 1995 | "resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz", 1996 | "integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==", 1997 | "license": "MIT" 1998 | }, 1999 | "node_modules/scheduler": { 2000 | "version": "0.26.0", 2001 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", 2002 | "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", 2003 | "license": "MIT" 2004 | }, 2005 | "node_modules/semver": { 2006 | "version": "7.7.1", 2007 | "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", 2008 | "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", 2009 | "license": "ISC", 2010 | "optional": true, 2011 | "bin": { 2012 | "semver": "bin/semver.js" 2013 | }, 2014 | "engines": { 2015 | "node": ">=10" 2016 | } 2017 | }, 2018 | "node_modules/sharp": { 2019 | "version": "0.34.1", 2020 | "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", 2021 | "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", 2022 | "hasInstallScript": true, 2023 | "license": "Apache-2.0", 2024 | "optional": true, 2025 | "dependencies": { 2026 | "color": "^4.2.3", 2027 | "detect-libc": "^2.0.3", 2028 | "semver": "^7.7.1" 2029 | }, 2030 | "engines": { 2031 | "node": "^18.17.0 || ^20.3.0 || >=21.0.0" 2032 | }, 2033 | "funding": { 2034 | "url": "https://opencollective.com/libvips" 2035 | }, 2036 | "optionalDependencies": { 2037 | "@img/sharp-darwin-arm64": "0.34.1", 2038 | "@img/sharp-darwin-x64": "0.34.1", 2039 | "@img/sharp-libvips-darwin-arm64": "1.1.0", 2040 | "@img/sharp-libvips-darwin-x64": "1.1.0", 2041 | "@img/sharp-libvips-linux-arm": "1.1.0", 2042 | "@img/sharp-libvips-linux-arm64": "1.1.0", 2043 | "@img/sharp-libvips-linux-ppc64": "1.1.0", 2044 | "@img/sharp-libvips-linux-s390x": "1.1.0", 2045 | "@img/sharp-libvips-linux-x64": "1.1.0", 2046 | "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", 2047 | "@img/sharp-libvips-linuxmusl-x64": "1.1.0", 2048 | "@img/sharp-linux-arm": "0.34.1", 2049 | "@img/sharp-linux-arm64": "0.34.1", 2050 | "@img/sharp-linux-s390x": "0.34.1", 2051 | "@img/sharp-linux-x64": "0.34.1", 2052 | "@img/sharp-linuxmusl-arm64": "0.34.1", 2053 | "@img/sharp-linuxmusl-x64": "0.34.1", 2054 | "@img/sharp-wasm32": "0.34.1", 2055 | "@img/sharp-win32-ia32": "0.34.1", 2056 | "@img/sharp-win32-x64": "0.34.1" 2057 | } 2058 | }, 2059 | "node_modules/simple-swizzle": { 2060 | "version": "0.2.2", 2061 | "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", 2062 | "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", 2063 | "license": "MIT", 2064 | "optional": true, 2065 | "dependencies": { 2066 | "is-arrayish": "^0.3.1" 2067 | } 2068 | }, 2069 | "node_modules/source-map-js": { 2070 | "version": "1.2.1", 2071 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", 2072 | "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", 2073 | "license": "BSD-3-Clause", 2074 | "engines": { 2075 | "node": ">=0.10.0" 2076 | } 2077 | }, 2078 | "node_modules/streamsearch": { 2079 | "version": "1.1.0", 2080 | "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", 2081 | "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", 2082 | "engines": { 2083 | "node": ">=10.0.0" 2084 | } 2085 | }, 2086 | "node_modules/styled-jsx": { 2087 | "version": "5.1.6", 2088 | "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", 2089 | "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", 2090 | "license": "MIT", 2091 | "dependencies": { 2092 | "client-only": "0.0.1" 2093 | }, 2094 | "engines": { 2095 | "node": ">= 12.0.0" 2096 | }, 2097 | "peerDependencies": { 2098 | "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" 2099 | }, 2100 | "peerDependenciesMeta": { 2101 | "@babel/core": { 2102 | "optional": true 2103 | }, 2104 | "babel-plugin-macros": { 2105 | "optional": true 2106 | } 2107 | } 2108 | }, 2109 | "node_modules/tailwind-merge": { 2110 | "version": "3.3.0", 2111 | "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.0.tgz", 2112 | "integrity": "sha512-fyW/pEfcQSiigd5SNn0nApUOxx0zB/dm6UDU/rEwc2c3sX2smWUNbapHv+QRqLGVp9GWX3THIa7MUGPo+YkDzQ==", 2113 | "license": "MIT", 2114 | "funding": { 2115 | "type": "github", 2116 | "url": "https://github.com/sponsors/dcastil" 2117 | } 2118 | }, 2119 | "node_modules/tailwindcss": { 2120 | "version": "4.1.8", 2121 | "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.8.tgz", 2122 | "integrity": "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og==", 2123 | "dev": true, 2124 | "license": "MIT" 2125 | }, 2126 | "node_modules/tapable": { 2127 | "version": "2.2.2", 2128 | "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.2.tgz", 2129 | "integrity": "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg==", 2130 | "dev": true, 2131 | "license": "MIT", 2132 | "engines": { 2133 | "node": ">=6" 2134 | } 2135 | }, 2136 | "node_modules/tar": { 2137 | "version": "7.4.3", 2138 | "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", 2139 | "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", 2140 | "dev": true, 2141 | "license": "ISC", 2142 | "dependencies": { 2143 | "@isaacs/fs-minipass": "^4.0.0", 2144 | "chownr": "^3.0.0", 2145 | "minipass": "^7.1.2", 2146 | "minizlib": "^3.0.1", 2147 | "mkdirp": "^3.0.1", 2148 | "yallist": "^5.0.0" 2149 | }, 2150 | "engines": { 2151 | "node": ">=18" 2152 | } 2153 | }, 2154 | "node_modules/tslib": { 2155 | "version": "2.8.1", 2156 | "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", 2157 | "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", 2158 | "license": "0BSD" 2159 | }, 2160 | "node_modules/typescript": { 2161 | "version": "5.8.3", 2162 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 2163 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 2164 | "devOptional": true, 2165 | "license": "Apache-2.0", 2166 | "bin": { 2167 | "tsc": "bin/tsc", 2168 | "tsserver": "bin/tsserver" 2169 | }, 2170 | "engines": { 2171 | "node": ">=14.17" 2172 | } 2173 | }, 2174 | "node_modules/undici-types": { 2175 | "version": "6.21.0", 2176 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", 2177 | "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", 2178 | "dev": true, 2179 | "license": "MIT" 2180 | }, 2181 | "node_modules/use-intl": { 2182 | "version": "4.1.0", 2183 | "resolved": "https://registry.npmjs.org/use-intl/-/use-intl-4.1.0.tgz", 2184 | "integrity": "sha512-mQvDYFvoGn+bm/PWvlQOtluKCknsQ5a9F1Cj0hMfBjMBVTwnOqLPd6srhjvVdEQEQFVyHM1PfyifKqKYb11M9Q==", 2185 | "license": "MIT", 2186 | "dependencies": { 2187 | "@formatjs/fast-memoize": "^2.2.0", 2188 | "@schummar/icu-type-parser": "1.21.5", 2189 | "intl-messageformat": "^10.5.14" 2190 | }, 2191 | "peerDependencies": { 2192 | "react": "^17.0.0 || ^18.0.0 || >=19.0.0-rc <19.0.0 || ^19.0.0" 2193 | } 2194 | }, 2195 | "node_modules/use-sync-external-store": { 2196 | "version": "1.4.0", 2197 | "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.4.0.tgz", 2198 | "integrity": "sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==", 2199 | "license": "MIT", 2200 | "peerDependencies": { 2201 | "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" 2202 | } 2203 | }, 2204 | "node_modules/yallist": { 2205 | "version": "5.0.0", 2206 | "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", 2207 | "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", 2208 | "dev": true, 2209 | "license": "BlueOak-1.0.0", 2210 | "engines": { 2211 | "node": ">=18" 2212 | } 2213 | } 2214 | } 2215 | } 2216 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "dev": "next dev --turbopack", 8 | "build": "next build", 9 | "start": "next start", 10 | "check": "npx @biomejs/biome check --write .", 11 | "format": "npx @biomejs/biome format --write .", 12 | "lint": "npx @biomejs/biome lint ." 13 | }, 14 | "dependencies": { 15 | "@radix-ui/react-slot": "^1.2.3", 16 | "@reduxjs/toolkit": "^2.8.2", 17 | "class-variance-authority": "^0.7.1", 18 | "clsx": "^2.1.1", 19 | "lucide-react": "^0.513.0", 20 | "next": "^15.3.3", 21 | "next-intl": "^4.1.0", 22 | "react": "^19.1.0", 23 | "react-dom": "^19.1.0", 24 | "react-redux": "^9.2.0", 25 | "tailwind-merge": "^3.3.0" 26 | }, 27 | "devDependencies": { 28 | "@biomejs/biome": "2.0.0-beta.6", 29 | "@tailwindcss/postcss": "^4.1.8", 30 | "@types/node": "^22.15.30", 31 | "@types/react": "^19.1.6", 32 | "@types/react-dom": "^19.1.6", 33 | "tailwindcss": "^4.1.8", 34 | "typescript": "^5.8.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | '@tailwindcss/postcss': {} 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreaGiulianini/fullstack_basic_starter/01b7749d54577b77f0d18fcb992790f74f6ef159/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /app/src/app/fonts/GeistMonoVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreaGiulianini/fullstack_basic_starter/01b7749d54577b77f0d18fcb992790f74f6ef159/app/src/app/fonts/GeistMonoVF.woff -------------------------------------------------------------------------------- /app/src/app/fonts/GeistVF.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AndreaGiulianini/fullstack_basic_starter/01b7749d54577b77f0d18fcb992790f74f6ef159/app/src/app/fonts/GeistVF.woff -------------------------------------------------------------------------------- /app/src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from 'next' 2 | import localFont from 'next/font/local' 3 | import { NextIntlClientProvider } from 'next-intl' 4 | import { getLocale, getMessages } from 'next-intl/server' 5 | import type React from 'react' 6 | import Providers from './providers' 7 | import '../styles/globals.css' 8 | 9 | const geistSans = localFont({ 10 | src: './fonts/GeistVF.woff', 11 | variable: '--font-geist-sans', 12 | weight: '100 900' 13 | }) 14 | 15 | const geistMono = localFont({ 16 | src: './fonts/GeistMonoVF.woff', 17 | variable: '--font-geist-mono', 18 | weight: '100 900' 19 | }) 20 | 21 | export const metadata: Metadata = { 22 | title: 'Create Next App', 23 | description: 'Generated by create next app' 24 | } 25 | 26 | export default async function RootLayout({ children }: { children: React.ReactNode }) { 27 | const locale = await getLocale() 28 | 29 | // Providing all messages to the client 30 | // side is the easiest way to get started 31 | const messages = await getMessages() 32 | 33 | return ( 34 | 35 | 36 | 37 | {children} 38 | 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /app/src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from 'next/image' 2 | import { getTranslations } from 'next-intl/server' 3 | import Counter from '../components/counter/Counter' 4 | import styles from '../styles/Home.module.css' 5 | 6 | async function fetchServerData() { 7 | const res = await fetch('http://traefik/api/healthcheck/ping') // Use traefik for server call, localhost if you find on client side 8 | const data = await res.json() 9 | return data 10 | } 11 | 12 | export default async function HomePage() { 13 | const t = await getTranslations('HomePage') 14 | const data = await fetchServerData() 15 | console.log('server', data) 16 | 17 | return ( 18 |
19 |
20 |

21 | {t('title')} Next.js! 22 |

23 | 24 |
25 | 26 | 38 |
39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /app/src/app/providers.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import type { ReactNode } from 'react' 4 | import { Provider } from 'react-redux' 5 | import { reduxStore } from '../redux/store' 6 | 7 | export default function Providers({ children }: { children: ReactNode }) { 8 | return {children} 9 | } 10 | -------------------------------------------------------------------------------- /app/src/components/Button.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from '@radix-ui/react-slot' 2 | import { cva, type VariantProps } from 'class-variance-authority' 3 | import type * as React from 'react' 4 | 5 | import { cn } from '@/utils/utils' 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: 'bg-primary text-primary-foreground shadow-xs hover:bg-primary/90', 13 | destructive: 14 | 'bg-destructive text-white shadow-xs hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60', 15 | outline: 16 | 'border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50', 17 | secondary: 'bg-secondary text-secondary-foreground shadow-xs hover:bg-secondary/80', 18 | ghost: 'hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50', 19 | link: 'text-primary underline-offset-4 hover:underline' 20 | }, 21 | size: { 22 | default: 'h-9 px-4 py-2 has-[>svg]:px-3', 23 | sm: 'h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5', 24 | lg: 'h-10 rounded-md px-6 has-[>svg]:px-4', 25 | icon: 'size-9' 26 | } 27 | }, 28 | defaultVariants: { 29 | variant: 'default', 30 | size: 'default' 31 | } 32 | } 33 | ) 34 | 35 | function Button({ 36 | className, 37 | variant, 38 | size, 39 | asChild = false, 40 | ...props 41 | }: React.ComponentProps<'button'> & 42 | VariantProps & { 43 | asChild?: boolean 44 | }) { 45 | const Comp = asChild ? Slot : 'button' 46 | 47 | return 48 | } 49 | 50 | export { Button, buttonVariants } 51 | -------------------------------------------------------------------------------- /app/src/components/counter/Counter.tsx: -------------------------------------------------------------------------------- 1 | 'use client' 2 | 3 | import { useState } from 'react' 4 | import { useDispatch, useSelector } from 'react-redux' 5 | import { counterSlice, incrementAsync, incrementIfOddAsync, selectCount } from '../../redux/slices/index' 6 | import { Button } from '../Button' 7 | import styles from './counter.module.css' 8 | 9 | function Counter() { 10 | const dispatch = useDispatch() 11 | const count = useSelector(selectCount) 12 | const [incrementAmount, setIncrementAmount] = useState(2) 13 | 14 | return ( 15 |
16 |
17 | 20 | 21 | {count} 22 | 23 | 26 |
27 |
28 | setIncrementAmount(Number(e.target.value ?? 0))} 33 | /> 34 | 35 | 36 | 37 |
38 |
39 | ) 40 | } 41 | 42 | export default Counter 43 | -------------------------------------------------------------------------------- /app/src/components/counter/counter.module.css: -------------------------------------------------------------------------------- 1 | .row { 2 | display: flex; 3 | align-items: center; 4 | justify-content: center; 5 | } 6 | 7 | .row > button { 8 | margin-left: 4px; 9 | margin-right: 8px; 10 | } 11 | 12 | .row:not(:last-child) { 13 | margin-bottom: 16px; 14 | } 15 | 16 | .value { 17 | font-size: 78px; 18 | padding-left: 16px; 19 | padding-right: 16px; 20 | margin-top: 2px; 21 | font-family: "Courier New", Courier, monospace; 22 | } 23 | 24 | .textbox { 25 | font-size: 32px; 26 | padding: 2px; 27 | width: 64px; 28 | text-align: center; 29 | margin-right: 4px; 30 | } 31 | -------------------------------------------------------------------------------- /app/src/i18n/request.ts: -------------------------------------------------------------------------------- 1 | import { getRequestConfig } from 'next-intl/server' 2 | 3 | export default getRequestConfig(async () => { 4 | // Provide a static locale, fetch a user setting, 5 | // read from `cookies()`, `headers()`, etc. 6 | const locale = 'en' 7 | 8 | return { 9 | locale, 10 | messages: (await import(`../../messages/${locale}.json`)).default 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /app/src/redux/createAppAsyncThunk.ts: -------------------------------------------------------------------------------- 1 | /* Core */ 2 | import { createAsyncThunk } from '@reduxjs/toolkit' 3 | 4 | /* Instruments */ 5 | import type { ReduxDispatch, ReduxState } from './store' 6 | 7 | /** 8 | * ? A utility function to create a typed Async Thnuk Actions. 9 | */ 10 | export const createAppAsyncThunk = createAsyncThunk.withTypes<{ 11 | state: ReduxState 12 | dispatch: ReduxDispatch 13 | rejectValue: string 14 | }>() 15 | -------------------------------------------------------------------------------- /app/src/redux/index.ts: -------------------------------------------------------------------------------- 1 | export * from './slices' 2 | export * from './store' 3 | -------------------------------------------------------------------------------- /app/src/redux/rootReducer.ts: -------------------------------------------------------------------------------- 1 | /* Instruments */ 2 | import { counterSlice } from './slices' 3 | 4 | export const reducer = { 5 | counter: counterSlice.reducer 6 | } 7 | -------------------------------------------------------------------------------- /app/src/redux/slices/counterSlice/counterSlice.ts: -------------------------------------------------------------------------------- 1 | /* Core */ 2 | import { createSlice, type PayloadAction } from '@reduxjs/toolkit' 3 | 4 | /* Instruments */ 5 | import { incrementAsync } from './thunks' 6 | 7 | const initialState: CounterSliceState = { 8 | value: 0, 9 | status: 'idle' 10 | } 11 | 12 | export const counterSlice = createSlice({ 13 | name: 'counter', 14 | initialState, 15 | // The `reducers` field lets us define reducers and generate associated actions 16 | reducers: { 17 | increment: (state) => { 18 | // Redux Toolkit allows us to write "mutating" logic in reducers. It 19 | // doesn't actually mutate the state because it uses the Immer library, 20 | // which detects changes to a "draft state" and produces a brand new 21 | // immutable state based off those changes 22 | state.value += 1 23 | }, 24 | decrement: (state) => { 25 | state.value -= 1 26 | }, 27 | // Use the PayloadAction type to declare the contents of `action.payload` 28 | incrementByAmount: (state, action: PayloadAction) => { 29 | state.value += action.payload 30 | } 31 | }, 32 | // The `extraReducers` field lets the slice handle actions defined elsewhere, 33 | // including actions generated by createAsyncThunk or in other slices. 34 | extraReducers: (builder) => { 35 | builder 36 | .addCase(incrementAsync.pending, (state) => { 37 | state.status = 'loading' 38 | }) 39 | .addCase(incrementAsync.fulfilled, (state, action) => { 40 | state.status = 'idle' 41 | state.value += action.payload 42 | }) 43 | } 44 | }) 45 | 46 | /* Types */ 47 | export interface CounterSliceState { 48 | value: number 49 | status: 'idle' | 'loading' | 'failed' 50 | } 51 | -------------------------------------------------------------------------------- /app/src/redux/slices/counterSlice/fetchIdentityCount.ts: -------------------------------------------------------------------------------- 1 | export const fetchIdentityCount = async (amount = 1): Promise<{ amount: number }> => { 2 | const response = await fetch('/api/identity-count', { 3 | method: 'POST', 4 | headers: { 'Content-Type': 'application/json' }, 5 | body: JSON.stringify({ amount }) 6 | }) 7 | const result = await response.json() 8 | 9 | return result 10 | } 11 | -------------------------------------------------------------------------------- /app/src/redux/slices/counterSlice/index.ts: -------------------------------------------------------------------------------- 1 | export * from './counterSlice' 2 | export * from './selectors' 3 | export * from './thunks' 4 | -------------------------------------------------------------------------------- /app/src/redux/slices/counterSlice/selectors.ts: -------------------------------------------------------------------------------- 1 | /* Instruments */ 2 | import type { ReduxState } from '../../../redux' 3 | 4 | // The function below is called a selector and allows us to select a value from 5 | // the state. Selectors can also be defined inline where they're used instead of 6 | // in the slice file. For example: `useSelector((state: RootState) => state.counter.value)` 7 | export const selectCount = (state: ReduxState) => state.counter.value 8 | -------------------------------------------------------------------------------- /app/src/redux/slices/counterSlice/thunks.ts: -------------------------------------------------------------------------------- 1 | /* Instruments */ 2 | import type { ReduxThunkAction } from '../../../redux' 3 | import { createAppAsyncThunk } from '../../../redux/createAppAsyncThunk' 4 | import { counterSlice } from './counterSlice' 5 | import { fetchIdentityCount } from './fetchIdentityCount' 6 | import { selectCount } from './selectors' 7 | 8 | // The function below is called a thunk and allows us to perform async logic. It 9 | // can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This 10 | // will call the thunk with the `dispatch` function as the first argument. Async 11 | // code can then be executed and other actions can be dispatched. Thunks are 12 | // typically used to make async requests. 13 | export const incrementAsync = createAppAsyncThunk('counter/fetchIdentityCount', async (amount: number) => { 14 | const response = await fetchIdentityCount(amount) 15 | // The value we return becomes the `fulfilled` action payload 16 | return response.amount 17 | }) 18 | 19 | // We can also write thunks by hand, which may contain both sync and async logic. 20 | // Here's an example of conditionally dispatching actions based on current state. 21 | export const incrementIfOddAsync = 22 | (amount: number): ReduxThunkAction => 23 | (dispatch, getState) => { 24 | const currentValue = selectCount(getState()) 25 | 26 | if (currentValue % 2 === 1) { 27 | dispatch(counterSlice.actions.incrementByAmount(amount)) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /app/src/redux/slices/index.ts: -------------------------------------------------------------------------------- 1 | export * from './counterSlice' 2 | -------------------------------------------------------------------------------- /app/src/redux/store.ts: -------------------------------------------------------------------------------- 1 | /* Core */ 2 | import { type Action, type ConfigureStoreOptions, configureStore, type ThunkAction } from '@reduxjs/toolkit' 3 | import { 4 | type TypedUseSelectorHook, 5 | useDispatch as useReduxDispatch, 6 | useSelector as useReduxSelector 7 | } from 'react-redux' 8 | 9 | /* Instruments */ 10 | import { reducer } from './rootReducer' 11 | 12 | const configreStoreDefaultOptions: ConfigureStoreOptions = { reducer } 13 | 14 | export const makeReduxStore = (options: ConfigureStoreOptions = configreStoreDefaultOptions) => { 15 | const store = configureStore(options) 16 | 17 | return store 18 | } 19 | 20 | export const reduxStore = configureStore({ 21 | reducer 22 | }) 23 | export const useDispatch = () => useReduxDispatch() 24 | export const useSelector: TypedUseSelectorHook = useReduxSelector 25 | 26 | /* Types */ 27 | export type ReduxStore = typeof reduxStore 28 | export type ReduxState = ReturnType 29 | export type ReduxDispatch = typeof reduxStore.dispatch 30 | export type ReduxThunkAction = ThunkAction 31 | -------------------------------------------------------------------------------- /app/src/styles/Home.module.css: -------------------------------------------------------------------------------- 1 | .container { 2 | padding: 0 2rem; 3 | } 4 | 5 | .main { 6 | min-height: 100vh; 7 | padding: 4rem 0; 8 | flex: 1; 9 | display: flex; 10 | flex-direction: column; 11 | justify-content: center; 12 | align-items: center; 13 | } 14 | 15 | .footer { 16 | display: flex; 17 | flex: 1; 18 | padding: 2rem 0; 19 | border-top: 1px solid #eaeaea; 20 | justify-content: center; 21 | align-items: center; 22 | } 23 | 24 | .footer a { 25 | display: flex; 26 | justify-content: center; 27 | align-items: center; 28 | flex-grow: 1; 29 | } 30 | 31 | .title a { 32 | color: #0070f3; 33 | text-decoration: none; 34 | } 35 | 36 | .title a:hover, 37 | .title a:focus, 38 | .title a:active { 39 | text-decoration: underline; 40 | } 41 | 42 | .title { 43 | margin: 0; 44 | line-height: 1.15; 45 | font-size: 4rem; 46 | } 47 | 48 | .title, 49 | .description { 50 | text-align: center; 51 | } 52 | 53 | .description { 54 | margin: 4rem 0; 55 | line-height: 1.5; 56 | font-size: 1.5rem; 57 | } 58 | 59 | .code { 60 | background: #fafafa; 61 | border-radius: 5px; 62 | padding: 0.75rem; 63 | font-size: 1.1rem; 64 | } 65 | 66 | .grid { 67 | display: flex; 68 | align-items: center; 69 | justify-content: center; 70 | flex-wrap: wrap; 71 | max-width: 800px; 72 | } 73 | 74 | .card { 75 | margin: 1rem; 76 | padding: 1.5rem; 77 | text-align: left; 78 | color: inherit; 79 | text-decoration: none; 80 | border: 1px solid #eaeaea; 81 | border-radius: 10px; 82 | transition: 83 | color 0.15s ease, 84 | border-color 0.15s ease; 85 | max-width: 300px; 86 | } 87 | 88 | .card:hover, 89 | .card:focus, 90 | .card:active { 91 | color: #0070f3; 92 | border-color: #0070f3; 93 | } 94 | 95 | .card h2 { 96 | margin: 0 0 1rem 0; 97 | font-size: 1.5rem; 98 | } 99 | 100 | .card p { 101 | margin: 0; 102 | font-size: 1.25rem; 103 | line-height: 1.5; 104 | } 105 | 106 | .logo { 107 | height: 1em; 108 | margin-left: 0.5rem; 109 | } 110 | 111 | @media (max-width: 600px) { 112 | .grid { 113 | width: 100%; 114 | flex-direction: column; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /app/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | @import "./tailwindcss-animate.css"; 3 | 4 | @variant dark (&:is(.dark *)); 5 | 6 | @theme { 7 | --radius-lg: var(--radius); 8 | --radius-md: calc(var(--radius) - 2px); 9 | --radius-sm: calc(var(--radius) - 4px); 10 | 11 | --color-background: hsl(var(--background)); 12 | --color-foreground: hsl(var(--foreground)); 13 | 14 | --color-card: hsl(var(--card)); 15 | --color-card-foreground: hsl(var(--card-foreground)); 16 | 17 | --color-popover: hsl(var(--popover)); 18 | --color-popover-foreground: hsl(var(--popover-foreground)); 19 | 20 | --color-primary: hsl(var(--primary)); 21 | --color-primary-foreground: hsl(var(--primary-foreground)); 22 | 23 | --color-secondary: hsl(var(--secondary)); 24 | --color-secondary-foreground: hsl(var(--secondary-foreground)); 25 | 26 | --color-muted: hsl(var(--muted)); 27 | --color-muted-foreground: hsl(var(--muted-foreground)); 28 | 29 | --color-accent: hsl(var(--accent)); 30 | --color-accent-foreground: hsl(var(--accent-foreground)); 31 | 32 | --color-destructive: hsl(var(--destructive)); 33 | --color-destructive-foreground: hsl(var(--destructive-foreground)); 34 | 35 | --color-border: hsl(var(--border)); 36 | --color-input: hsl(var(--input)); 37 | --color-ring: hsl(var(--ring)); 38 | 39 | --color-chart-1: hsl(var(--chart-1)); 40 | --color-chart-2: hsl(var(--chart-2)); 41 | --color-chart-3: hsl(var(--chart-3)); 42 | --color-chart-4: hsl(var(--chart-4)); 43 | --color-chart-5: hsl(var(--chart-5)); 44 | } 45 | 46 | /* 47 | The default border color has changed to `currentColor` in Tailwind CSS v4, 48 | so we've added these compatibility styles to make sure everything still 49 | looks the same as it did with Tailwind CSS v3. 50 | 51 | If we ever want to remove these styles, we need to add an explicit border 52 | color utility to any element that depends on these defaults. 53 | */ 54 | @layer base { 55 | *, 56 | ::after, 57 | ::before, 58 | ::backdrop, 59 | ::file-selector-button { 60 | border-color: var(--color-gray-200, currentColor); 61 | } 62 | } 63 | 64 | @layer utilities { 65 | html, 66 | body { 67 | padding: 0; 68 | margin: 0; 69 | } 70 | 71 | a { 72 | color: inherit; 73 | text-decoration: none; 74 | } 75 | 76 | * { 77 | box-sizing: border-box; 78 | } 79 | } 80 | 81 | @layer base { 82 | :root { 83 | --background: 0 0% 100%; 84 | --foreground: 240 10% 3.9%; 85 | --card: 0 0% 100%; 86 | --card-foreground: 240 10% 3.9%; 87 | --popover: 0 0% 100%; 88 | --popover-foreground: 240 10% 3.9%; 89 | --primary: 240 5.9% 10%; 90 | --primary-foreground: 0 0% 98%; 91 | --secondary: 240 4.8% 95.9%; 92 | --secondary-foreground: 240 5.9% 10%; 93 | --muted: 240 4.8% 95.9%; 94 | --muted-foreground: 240 3.8% 46.1%; 95 | --accent: 240 4.8% 95.9%; 96 | --accent-foreground: 240 5.9% 10%; 97 | --destructive: 0 84.2% 60.2%; 98 | --destructive-foreground: 0 0% 98%; 99 | --border: 240 5.9% 90%; 100 | --input: 240 5.9% 90%; 101 | --ring: 240 10% 3.9%; 102 | --chart-1: 12 76% 61%; 103 | --chart-2: 173 58% 39%; 104 | --chart-3: 197 37% 24%; 105 | --chart-4: 43 74% 66%; 106 | --chart-5: 27 87% 67%; 107 | --radius: 0.5rem; 108 | } 109 | .dark { 110 | --background: 240 10% 3.9%; 111 | --foreground: 0 0% 98%; 112 | --card: 240 10% 3.9%; 113 | --card-foreground: 0 0% 98%; 114 | --popover: 240 10% 3.9%; 115 | --popover-foreground: 0 0% 98%; 116 | --primary: 0 0% 98%; 117 | --primary-foreground: 240 5.9% 10%; 118 | --secondary: 240 3.7% 15.9%; 119 | --secondary-foreground: 0 0% 98%; 120 | --muted: 240 3.7% 15.9%; 121 | --muted-foreground: 240 5% 64.9%; 122 | --accent: 240 3.7% 15.9%; 123 | --accent-foreground: 0 0% 98%; 124 | --destructive: 0 62.8% 30.6%; 125 | --destructive-foreground: 0 0% 98%; 126 | --border: 240 3.7% 15.9%; 127 | --input: 240 3.7% 15.9%; 128 | --ring: 240 4.9% 83.9%; 129 | --chart-1: 220 70% 50%; 130 | --chart-2: 160 60% 45%; 131 | --chart-3: 30 80% 55%; 132 | --chart-4: 280 65% 60%; 133 | --chart-5: 340 75% 55%; 134 | } 135 | } 136 | 137 | @layer base { 138 | * { 139 | @apply border-border; 140 | } 141 | body { 142 | @apply bg-background text-foreground; 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /app/src/styles/tailwindcss-animate.css: -------------------------------------------------------------------------------- 1 | @theme inline { 2 | --animation-delay-0: 0s; 3 | --animation-delay-75: 75ms; 4 | --animation-delay-100: 0.1s; 5 | --animation-delay-150: 0.15s; 6 | --animation-delay-200: 0.2s; 7 | --animation-delay-300: 0.3s; 8 | --animation-delay-500: 0.5s; 9 | --animation-delay-700: 0.7s; 10 | --animation-delay-1000: 1s; 11 | 12 | --animation-repeat-0: 0; 13 | --animation-repeat-1: 1; 14 | --animation-repeat-infinite: infinite; 15 | 16 | --animation-direction-normal: normal; 17 | --animation-direction-reverse: reverse; 18 | --animation-direction-alternate: alternate; 19 | --animation-direction-alternate-reverse: alternate-reverse; 20 | 21 | --animation-fill-mode-none: none; 22 | --animation-fill-mode-forwards: forwards; 23 | --animation-fill-mode-backwards: backwards; 24 | --animation-fill-mode-both: both; 25 | 26 | --animate-in: var(--tw-duration, 150ms) var(--tw-ease, ease) enter; 27 | --animate-out: var(--tw-duration, 150ms) var(--tw-ease, ease) exit; 28 | 29 | --percentage-0: 0; 30 | --percentage-5: 0.05; 31 | --percentage-10: 0.1; 32 | --percentage-15: 0.15; 33 | --percentage-20: 0.2; 34 | --percentage-25: 0.25; 35 | --percentage-30: 0.3; 36 | --percentage-35: 0.35; 37 | --percentage-40: 0.4; 38 | --percentage-45: 0.45; 39 | --percentage-50: 0.5; 40 | --percentage-55: 0.55; 41 | --percentage-60: 0.6; 42 | --percentage-65: 0.65; 43 | --percentage-70: 0.7; 44 | --percentage-75: 0.75; 45 | --percentage-80: 0.8; 46 | --percentage-85: 0.85; 47 | --percentage-90: 0.9; 48 | --percentage-95: 0.95; 49 | --percentage-100: 1; 50 | 51 | @keyframes enter { 52 | from { 53 | opacity: var(--tw-enter-opacity, 1); 54 | transform: translate3d( 55 | var(--tw-enter-translate-x, 0), 56 | var(--tw-enter-translate-y, 0), 57 | 0 58 | ) 59 | scale3d( 60 | var(--tw-enter-scale, 1), 61 | var(--tw-enter-scale, 1), 62 | var(--tw-enter-scale, 1) 63 | ) 64 | rotate(var(--tw-enter-rotate, 0)); 65 | } 66 | } 67 | 68 | @keyframes exit { 69 | to { 70 | opacity: var(--tw-exit-opacity, 1); 71 | transform: translate3d( 72 | var(--tw-exit-translate-x, 0), 73 | var(--tw-exit-translate-y, 0), 74 | 0 75 | ) 76 | scale3d( 77 | var(--tw-exit-scale, 1), 78 | var(--tw-exit-scale, 1), 79 | var(--tw-exit-scale, 1) 80 | ) 81 | rotate(var(--tw-exit-rotate, 0)); 82 | } 83 | } 84 | } 85 | 86 | /* 87 | * Tailwind's default `duration` utility sets the `--tw-duration` variable, so 88 | * can set `animation-duration` directly in the animation definition in the 89 | * `@theme` section above. Same goes for the `animation-timing-function`, set 90 | * with `--tw-ease`. 91 | */ 92 | 93 | @utility delay-* { 94 | animation-delay: --value([duration]); 95 | animation-delay: calc(--value(integer) * 1ms); 96 | animation-delay: --value(--animation-delay- *); 97 | } 98 | 99 | @utility repeat-* { 100 | animation-iteration-count: --value(--animation-repeat- *, integer); 101 | } 102 | 103 | @utility direction-* { 104 | animation-direction: --value(--animation-direction- *); 105 | } 106 | 107 | @utility fill-mode-* { 108 | animation-fill-mode: --value(--animation-fill-mode- *); 109 | } 110 | 111 | @utility running { 112 | animation-play-state: running; 113 | } 114 | @utility paused { 115 | animation-play-state: paused; 116 | } 117 | 118 | @utility fade-in-* { 119 | --tw-enter-opacity: --value(--percentage- *); 120 | } 121 | @utility fade-out-* { 122 | --tw-exit-opacity: --value(--percentage- *); 123 | } 124 | 125 | @utility zoom-in-* { 126 | --tw-enter-scale: --value(--percentage- *); 127 | } 128 | @utility zoom-out-* { 129 | --tw-exit-scale: --value(--percentage- *); 130 | } 131 | 132 | @utility spin-in-* { 133 | --tw-enter-rotate: calc(--value(integer) * 1deg); 134 | --tw-enter-rotate: --value(--rotate- *, [angle]); 135 | } 136 | @utility spin-out-* { 137 | --tw-exit-rotate: calc(--value(integer) * 1deg); 138 | --tw-exit-rotate: --value(--rotate- *, [angle]); 139 | } 140 | 141 | @utility slide-in-from-top-* { 142 | --tw-enter-translate-y: calc(--value([percentage], [length]) * -1); 143 | } 144 | @utility slide-in-from-bottom-* { 145 | --tw-enter-translate-y: --value([percentage], [length]); 146 | } 147 | @utility slide-in-from-left-* { 148 | --tw-enter-translate-x: calc(--value([percentage], [length]) * -1); 149 | } 150 | @utility slide-in-from-right-* { 151 | --tw-enter-translate-x: --value([percentage], [length]); 152 | } 153 | 154 | @utility slide-out-to-top-* { 155 | --tw-exit-translate-y: calc(--value([percentage], [length]) * -1); 156 | } 157 | @utility slide-out-to-bottom-* { 158 | --tw-exit-translate-y: --value([percentage], [length]); 159 | } 160 | @utility slide-out-to-left-* { 161 | --tw-exit-translate-x: calc(--value([percentage], [length]) * -1); 162 | } 163 | @utility slide-out-to-right-* { 164 | --tw-exit-translate-x: --value([percentage], [length]); 165 | } 166 | -------------------------------------------------------------------------------- /app/src/utils/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from 'clsx' 2 | import { twMerge } from 'tailwind-merge' 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | -------------------------------------------------------------------------------- /backoffice/README.md: -------------------------------------------------------------------------------- 1 | In thid directory you can put your code for BO -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.0.0-beta.6/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "includes": [ 10 | "**", 11 | "!**/.next/**", 12 | "!**/node_modules/**", 13 | "!**/public/img/brand/g_files/**", 14 | "!**/tailwindcss-animate.css" 15 | ] 16 | }, 17 | "assist": { 18 | "actions": { 19 | "source": { 20 | "organizeImports": "on" 21 | } 22 | } 23 | }, 24 | "linter": { 25 | "enabled": true, 26 | "rules": { 27 | "style": { 28 | "useLiteralEnumMembers": "error", 29 | "useNodejsImportProtocol": "error", 30 | "useAsConstAssertion": "error", 31 | "useEnumInitializers": "error", 32 | "useSelfClosingElements": "error", 33 | "useConst": "error", 34 | "useSingleVarDeclarator": "error", 35 | "noUnusedTemplateLiteral": "error", 36 | "useNumberNamespace": "error", 37 | "noInferrableTypes": "error", 38 | "useExponentiationOperator": "error", 39 | "useTemplate": "error", 40 | "noParameterAssign": "error", 41 | "noNonNullAssertion": "error", 42 | "useDefaultParameterLast": "error", 43 | "useImportType": "error", 44 | "useExportType": "error", 45 | "noUselessElse": "error", 46 | "useShorthandFunctionType": "error" 47 | }, 48 | "a11y": { 49 | "useKeyWithClickEvents": "off" 50 | }, 51 | "correctness": { 52 | "noUndeclaredVariables": "warn", 53 | "noUnusedImports": "warn", 54 | "noUnreachable": "warn", 55 | "noUnusedVariables": "warn" 56 | }, 57 | "nursery": { 58 | "useSortedClasses": "warn" 59 | }, 60 | "complexity": { 61 | "noExcessiveCognitiveComplexity": "warn" 62 | } 63 | } 64 | }, 65 | "formatter": { 66 | "enabled": true, 67 | "indentStyle": "space", 68 | "indentWidth": 2, 69 | "lineEnding": "lf", 70 | "lineWidth": 120 71 | }, 72 | "javascript": { 73 | "formatter": { 74 | "indentStyle": "space", 75 | "indentWidth": 2, 76 | "lineWidth": 120, 77 | "semicolons": "asNeeded", 78 | "quoteStyle": "single", 79 | "jsxQuoteStyle": "single", 80 | "trailingCommas": "none" 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /compose.override.yaml: -------------------------------------------------------------------------------- 1 | volumes: 2 | pgdata: null 3 | valkeydata: null 4 | esdata: null 5 | wordpress: null 6 | mysqldata: null 7 | 8 | services: 9 | 10 | app: 11 | develop: 12 | watch: 13 | - action: sync 14 | path: ./app 15 | target: /home/node/app 16 | ignore: 17 | - ./app/node_modules/ 18 | - action: rebuild 19 | path: ./app/package.json 20 | volumes: 21 | - /home/node/app/node_modules 22 | - /home/node/app/.next 23 | restart: unless-stopped 24 | depends_on: 25 | - postgres_db 26 | 27 | api: 28 | develop: 29 | watch: 30 | - action: sync 31 | path: ./api 32 | target: /home/node/api 33 | ignore: 34 | - ./api/node_modules/ 35 | - action: rebuild 36 | path: ./api/package.json 37 | volumes: 38 | - /home/node/api/node_modules 39 | restart: always 40 | depends_on: 41 | - postgres_db 42 | 43 | postgres_db: 44 | image: 'postgres:17' 45 | container_name: postgres_db 46 | environment: 47 | POSTGRES_USER: '${DB_USER}' 48 | POSTGRES_PASSWORD: '${DB_PASS}' 49 | POSTGRES_DB: '${DB_NAME}' 50 | ports: 51 | - '5432:5432' 52 | volumes: 53 | - 'pgdata:/var/lib/postgresql/data' 54 | 55 | valkey: 56 | image: 'valkey/valkey:8' 57 | container_name: valkey 58 | command: valkey-server --port 6379 --cluster-enabled no --requirepass ${VALKEY_PASS} 59 | restart: unless-stopped 60 | ports: 61 | - '6379:6379' 62 | volumes: 63 | - 'valkeydata:/data' 64 | 65 | elasticsearch: 66 | image: 'docker.elastic.co/elasticsearch/elasticsearch:9.0.0' 67 | container_name: elasticsearch 68 | environment: 69 | - discovery.type=single-node 70 | - xpack.security.enabled=false 71 | - ES_JAVA_OPTS=-Xms512m -Xmx512m 72 | ports: 73 | - '9200:9200' 74 | volumes: 75 | - 'esdata:/usr/share/elasticsearch/data' 76 | 77 | kibana: 78 | image: 'docker.elastic.co/kibana/kibana:9.0.0' 79 | container_name: kibana 80 | environment: 81 | - 'ELASTICSEARCH_HOSTS=http://${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}' 82 | - xpack.security.enabled=false 83 | ports: 84 | - '5601:5601' 85 | depends_on: 86 | - elasticsearch 87 | 88 | wordpress: 89 | image: wordpress 90 | container_name: wordpress 91 | restart: always 92 | labels: 93 | - traefik.enable=true 94 | - traefik.http.routers.wordpress.rule=Host(`wordpress.example.com`) 95 | - traefik.http.services.wordpress.loadbalancer.server.port=80 96 | - traefik.http.routers.wordpress.entrypoints=web 97 | ports: 98 | - '7000:80' 99 | environment: 100 | WORDPRESS_DB_HOST: '${WORDPRESS_DB_HOST}' 101 | WORDPRESS_DB_USER: '${WORDPRESS_DB_USER}' 102 | WORDPRESS_DB_PASSWORD: '${WORDPRESS_DB_PASSWORD}' 103 | WORDPRESS_DB_NAME: '${WORDPRESS_DB_NAME}' 104 | volumes: 105 | - 'wordpress:/var/www/html' 106 | depends_on: 107 | - mysql_db 108 | 109 | mysql_db: 110 | image: 'mysql:9' 111 | container_name: mysql_db 112 | restart: always 113 | ports: 114 | - '3306:3306' 115 | environment: 116 | MYSQL_DATABASE: '${MYSQL_DATABASE}' 117 | MYSQL_USER: '${MYSQL_USER}' 118 | MYSQL_PASSWORD: '${MYSQL_PASSWORD}' 119 | MYSQL_RANDOM_ROOT_PASSWORD: '${MYSQL_RANDOM_ROOT_PASSWORD}' 120 | volumes: 121 | - 'mysqldata:/var/lib/mysql' 122 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | traefik: 3 | image: 'traefik:3.3.5' 4 | container_name: traefik 5 | command: 6 | - '--api.insecure=true' 7 | - '--providers.docker=true' 8 | - '--entrypoints.web.address=:80' 9 | - '--entrypoints.websecure.address=:443' 10 | ports: 11 | - '80:80' 12 | - '443:443' 13 | - '8080:8080' 14 | volumes: 15 | - '/var/run/docker.sock:/var/run/docker.sock' 16 | - './letsencrypt:/letsencrypt' 17 | depends_on: 18 | - app 19 | - api 20 | 21 | app: 22 | build: 23 | context: ./app 24 | target: 'image-${ENV}' 25 | args: 26 | NEXT_PUBLIC_ENVIRONMENT: '${ENV}' 27 | image: app 28 | container_name: app 29 | labels: 30 | - traefik.enable=true 31 | - traefik.http.routers.app.rule=PathPrefix(`/`) 32 | - traefik.http.services.app.loadbalancer.server.port=3000 33 | environment: 34 | TZ: Europe/Rome 35 | NEXT_PUBLIC_ENVIRONMENT: '${ENV}' 36 | restart: always 37 | 38 | api: 39 | build: 40 | context: ./api 41 | image: api 42 | container_name: api 43 | labels: 44 | - traefik.enable=true 45 | - traefik.http.routers.api.rule=PathPrefix(`/api`) 46 | - traefik.http.routers.reference.rule=PathPrefix(`/reference`) 47 | - traefik.http.services.api.loadbalancer.server.port=5000 48 | environment: 49 | ENV: '${ENV}' 50 | DB_HOST: '${DB_HOST}' 51 | DB_USER: '${DB_USER}' 52 | DB_PASS: '${DB_PASS}' 53 | DB_NAME: '${DB_NAME}' 54 | DB_PORT: '${DB_PORT}' 55 | VALKEY_HOST: '${VALKEY_HOST}' 56 | VALKEY_PASS: '${VALKEY_PASS}' 57 | VALKEY_PORT: '${VALKEY_PORT}' 58 | ELASTICSEARCH_HOST: '${ELASTICSEARCH_HOST}' 59 | ELASTICSEARCH_PORT: '${ELASTICSEARCH_PORT}' 60 | restart: always 61 | -------------------------------------------------------------------------------- /compose_override/development.yaml: -------------------------------------------------------------------------------- 1 | volumes: 2 | pgdata: null 3 | valkeydata: null 4 | esdata: null 5 | wordpress: null 6 | mysqldata: null 7 | 8 | services: 9 | 10 | app: 11 | develop: 12 | watch: 13 | - action: sync 14 | path: ./app 15 | target: /home/node/app 16 | ignore: 17 | - ./app/node_modules/ 18 | - action: rebuild 19 | path: ./app/package.json 20 | volumes: 21 | - /home/node/app/node_modules 22 | - /home/node/app/.next 23 | restart: unless-stopped 24 | depends_on: 25 | - postgres_db 26 | 27 | api: 28 | develop: 29 | watch: 30 | - action: sync 31 | path: ./api 32 | target: /home/node/api 33 | ignore: 34 | - ./api/node_modules/ 35 | - action: rebuild 36 | path: ./api/package.json 37 | volumes: 38 | - /home/node/api/node_modules 39 | restart: always 40 | depends_on: 41 | - postgres_db 42 | 43 | postgres_db: 44 | image: 'postgres:17' 45 | container_name: postgres_db 46 | environment: 47 | POSTGRES_USER: '${DB_USER}' 48 | POSTGRES_PASSWORD: '${DB_PASS}' 49 | POSTGRES_DB: '${DB_NAME}' 50 | ports: 51 | - '5432:5432' 52 | volumes: 53 | - 'pgdata:/var/lib/postgresql/data' 54 | 55 | valkey: 56 | image: 'valkey/valkey:8' 57 | container_name: valkey 58 | command: valkey-server --port 6379 --cluster-enabled no --requirepass ${VALKEY_PASS} 59 | restart: unless-stopped 60 | ports: 61 | - '6379:6379' 62 | volumes: 63 | - 'valkeydata:/data' 64 | 65 | elasticsearch: 66 | image: 'docker.elastic.co/elasticsearch/elasticsearch:9.0.0' 67 | container_name: elasticsearch 68 | environment: 69 | - discovery.type=single-node 70 | - xpack.security.enabled=false 71 | - ES_JAVA_OPTS=-Xms512m -Xmx512m 72 | ports: 73 | - '9200:9200' 74 | volumes: 75 | - 'esdata:/usr/share/elasticsearch/data' 76 | 77 | kibana: 78 | image: 'docker.elastic.co/kibana/kibana:9.0.0' 79 | container_name: kibana 80 | environment: 81 | - 'ELASTICSEARCH_HOSTS=http://${ELASTICSEARCH_HOST}:${ELASTICSEARCH_PORT}' 82 | - xpack.security.enabled=false 83 | ports: 84 | - '5601:5601' 85 | depends_on: 86 | - elasticsearch 87 | 88 | wordpress: 89 | image: wordpress 90 | container_name: wordpress 91 | restart: always 92 | labels: 93 | - traefik.enable=true 94 | - traefik.http.routers.wordpress.rule=Host(`wordpress.example.com`) 95 | - traefik.http.services.wordpress.loadbalancer.server.port=80 96 | - traefik.http.routers.wordpress.entrypoints=web 97 | ports: 98 | - '7000:80' 99 | environment: 100 | WORDPRESS_DB_HOST: '${WORDPRESS_DB_HOST}' 101 | WORDPRESS_DB_USER: '${WORDPRESS_DB_USER}' 102 | WORDPRESS_DB_PASSWORD: '${WORDPRESS_DB_PASSWORD}' 103 | WORDPRESS_DB_NAME: '${WORDPRESS_DB_NAME}' 104 | volumes: 105 | - 'wordpress:/var/www/html' 106 | depends_on: 107 | - mysql_db 108 | 109 | mysql_db: 110 | image: 'mysql:9' 111 | container_name: mysql_db 112 | restart: always 113 | ports: 114 | - '3306:3306' 115 | environment: 116 | MYSQL_DATABASE: '${MYSQL_DATABASE}' 117 | MYSQL_USER: '${MYSQL_USER}' 118 | MYSQL_PASSWORD: '${MYSQL_PASSWORD}' 119 | MYSQL_RANDOM_ROOT_PASSWORD: '${MYSQL_RANDOM_ROOT_PASSWORD}' 120 | volumes: 121 | - 'mysqldata:/var/lib/mysql' 122 | -------------------------------------------------------------------------------- /compose_override/production.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | api: 3 | restart: always 4 | -------------------------------------------------------------------------------- /populate.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | docker exec api npm run generate 6 | docker exec api npm run migrate 7 | -------------------------------------------------------------------------------- /start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -eu 4 | 5 | cp compose_override/development.yml compose.override.yml 6 | docker compose down && docker compose up --build #PROD 7 | # docker compose down && docker compose watch #DEV 8 | 9 | --------------------------------------------------------------------------------