diff --git a/.env b/.env deleted file mode 100644 index dad709f..0000000 --- a/.env +++ /dev/null @@ -1,5 +0,0 @@ -# Do not hardcode secret values here in production. -# In Kubernetes, DATABASE_URL and JWT_SECRET are injected from secrets. -# For local development, use an untracked .env.local. - -NEXT_PUBLIC_SITE_NAME=Hotel La Huasca \ No newline at end of file diff --git a/.gitignore b/.gitignore index eb2238a..7dad8d6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +.env .env.local tmp/ *.log diff --git a/deploy-db.sh b/deploy-db.sh index 09c81bb..b263b19 100755 --- a/deploy-db.sh +++ b/deploy-db.sh @@ -3,7 +3,7 @@ set -euo pipefail # Deploy Postgres for la-huasca in its own namespace -NAMESPACE="lahuasca" +NAMESPACE="${NAMESPACE:-lahuasca}" SECRET_NAME="la-huasca-db-secrets" PVC_NAME="postgres-data" diff --git a/deploy.sh b/deploy.sh index ce8e53e..d61706f 100755 --- a/deploy.sh +++ b/deploy.sh @@ -12,7 +12,7 @@ set -euo pipefail # - docker or buildah/push capability (we build locally and push) # - registry secret exists or REGISTRY_USER/REGISTRY_PASS are exported -NAMESPACE="lahuasca" +NAMESPACE="${NAMESPACE:-lahuasca}" IMAGE="gitea.ajavasoft.com/muken/la-huasca-ts" TAG="${TAG:-$(git rev-parse --short HEAD)}" HOSTS=("lahuasca.com" "www.lahuasca.com") diff --git a/init-db-local.sh b/init-db-local.sh new file mode 100755 index 0000000..b52efc4 --- /dev/null +++ b/init-db-local.sh @@ -0,0 +1,221 @@ +#!/bin/bash + +# Database initialization script for La Huasca (local version) +# This script creates the necessary tables and seeds initial data + +set -e # Exit on any error + +echo "Initializing La Huasca database (local version)..." + +# Execute the schema directly +echo "Executing database schema..." +sudo -u postgres psql -d lahuasca << 'EOF' +-- Create all tables +CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')), + comments_disabled BOOLEAN DEFAULT FALSE, + first_name VARCHAR(100), + last_name VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS images ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) NOT NULL, + data TEXT NOT NULL, + category VARCHAR(100) DEFAULT 'other', + room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL, + location_target VARCHAR(100) DEFAULT 'gallery', + uploaded_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS reservations ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + date DATE NOT NULL, + time TIME NOT NULL, + guests INTEGER NOT NULL, + table_name VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS coupons ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + code VARCHAR(50) NOT NULL, + discount VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS offers ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS benefits ( + id SERIAL PRIMARY KEY, + text VARCHAR(500) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS config ( + key VARCHAR(100) PRIMARY KEY, + value TEXT NOT NULL +); + +CREATE TABLE IF NOT EXISTS rooms ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + price NUMERIC(10,2) NOT NULL, + photos TEXT[] DEFAULT '{}', + featured_photo VARCHAR(500), + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS menu_items ( + id SERIAL PRIMARY KEY, + category VARCHAR(100) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + price NUMERIC(10,2) NOT NULL, + is_daily_special BOOLEAN DEFAULT FALSE, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS places ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + photos TEXT[] DEFAULT '{}', + featured_photo VARCHAR(500), + distance_km NUMERIC(8,2), + visit_time VARCHAR(100), + suggestion TEXT, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS reviews ( + id SERIAL PRIMARY KEY, + author_name VARCHAR(255) NOT NULL, + rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5), + text TEXT NOT NULL, + approved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS slides ( + id SERIAL PRIMARY KEY, + title VARCHAR(255), + subtitle TEXT, + image_id INTEGER REFERENCES images(id) ON DELETE SET NULL, + image_url VARCHAR(500), + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS room_comments ( + id SERIAL PRIMARY KEY, + room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE, + photo_url VARCHAR(500), + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + first_name VARCHAR(100) NOT NULL, + last_initial VARCHAR(10), + text TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + deleted_by_admin BOOLEAN DEFAULT FALSE +); + +CREATE TABLE IF NOT EXISTS social_events ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + price VARCHAR(50), + date DATE, + photos TEXT[] DEFAULT '{}', + featured_photo VARCHAR(500), + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS social_event_reservations ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE, + guests INTEGER NOT NULL, + notes TEXT, + status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')), + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, social_event_id) +); + +CREATE TABLE IF NOT EXISTS calendar_events ( + id SERIAL PRIMARY KEY, + date DATE NOT NULL UNIQUE, + title VARCHAR(255) NOT NULL, + type VARCHAR(100) NOT NULL, + description TEXT, + color VARCHAR(20) NOT NULL +); + +CREATE TABLE IF NOT EXISTS user_sessions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + token TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Add missing columns if they don't exist +ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500); +ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE; +ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100); +ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100); + +-- Insert initial data +-- Default admin user (password: admin) +INSERT INTO users (username, password_hash, role) VALUES + ('admin', '$2a$10$8K1p/a0dhrxiowP.dnkgNORTWgdEDHn5L2/xjpEWuC.QQv4rKO9jO', 'admin') + ON CONFLICT (username) DO NOTHING; + +-- Default config values +INSERT INTO config (key, value) VALUES + ('wifi_password', 'LaHuascaGuest2026'), + ('tagline', 'Hotel · Restaurant · Andean Highlands') + ON CONFLICT (key) DO NOTHING; + +-- Sample offers +INSERT INTO offers (title, description, active, sort_order) VALUES + ('Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.', true, 1), + ('Sunset Dinner', 'Three-course menu on the terrace every Friday.', true, 2) + ON CONFLICT DO NOTHING; + +-- Sample benefits +INSERT INTO benefits (text) VALUES + ('Priority booking for peak weekends'), + ('Late checkout on availability'), + ('Free welcome cocktail'), + ('Member-only events and tastings') + ON CONFLICT DO NOTHING; +EOF + +# Verify that the tables were created +echo "Verifying database tables..." +sudo -u postgres psql -d lahuasca -c "\dt" + +echo "Database initialization completed successfully!" \ No newline at end of file diff --git a/init-db.sh b/init-db.sh index 714269f..a85c50d 100755 --- a/init-db.sh +++ b/init-db.sh @@ -8,7 +8,7 @@ set -e # Exit on any error echo "Initializing La Huasca database..." # Get the postgres pod name -POSTGRES_POD=$(kubectl get pods -n lahuasca | grep postgres | awk '{print $1}') +POSTGRES_POD=$(kubectl get pods -n "${NAMESPACE:-lahuasca}" | grep postgres | awk '{print $1}') if [ -z "$POSTGRES_POD" ]; then echo "Error: Could not find postgres pod" @@ -199,12 +199,12 @@ ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100); -- Insert initial data -- Default admin user (password: admin) INSERT INTO users (username, password_hash, role) VALUES - ('admin', '$2a$10$8K1p/a0dhrxiowP.dnkgNORTWgdEDHn5L2/xjpEWuC.QQv4rKO9jO', 'admin') + ('admin', '${BOOTSTRAP_ADMIN_PASSWORD_HASH}', 'admin') ON CONFLICT (username) DO NOTHING; -- Default config values INSERT INTO config (key, value) VALUES - ('wifi_password', 'LaHuascaGuest2026'), + ('wifi_password', '${WIFI_PASSWORD}'), ('tagline', 'Hotel · Restaurant · Andean Highlands') ON CONFLICT (key) DO NOTHING; @@ -225,14 +225,14 @@ EOF # Copy the schema file to the postgres pod echo "Copying schema to postgres pod..." -kubectl cp /tmp/schema.sql lahuasca/$POSTGRES_POD:/tmp/schema.sql +kubectl cp /tmp/schema.sql "${NAMESPACE:-lahuasca}/$POSTGRES_POD:/tmp/schema.sql" # Execute the schema echo "Executing database schema..." -kubectl exec -n lahuasca $POSTGRES_POD -- psql -U lahuasca -d lahuasca -f /tmp/schema.sql +kubectl exec -n "${NAMESPACE:-lahuasca}" "$POSTGRES_POD" -- psql -U "${POSTGRES_USER:-lahuasca}" -d "${POSTGRES_DB:-lahuasca}" -f /tmp/schema.sql # Verify that the tables were created echo "Verifying database tables..." -kubectl exec -n lahuasca deploy/postgres -- psql -U lahuasca -d lahuasca -c "\dt" +kubectl exec -n "${NAMESPACE:-lahuasca}" deploy/postgres -- psql -U "${POSTGRES_USER:-lahuasca}" -d "${POSTGRES_DB:-lahuasca}" -c "\dt" echo "Database initialization completed successfully!" \ No newline at end of file diff --git a/k8s/app.yaml b/k8s/app.yaml index 2a5193c..9136206 100644 --- a/k8s/app.yaml +++ b/k8s/app.yaml @@ -29,6 +29,42 @@ spec: secretKeyRef: name: la-huasca-db-secrets key: DATABASE_URL + - name: BOOTSTRAP_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_ADMIN_PASSWORD + - name: BOOTSTRAP_USER_PASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_USER_PASSWORD + - name: BOOTSTRAP_WIFI_PASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_WIFI_PASSWORD + - name: BOOTSTRAP_TAGLINE + valueFrom: + configMapKeyRef: + name: la-huasca-bootstrap-config + key: BOOTSTRAP_TAGLINE + readinessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + timeoutSeconds: 2 + failureThreshold: 3 + livenessProbe: + httpGet: + path: /api/health + port: http + initialDelaySeconds: 15 + periodSeconds: 20 + timeoutSeconds: 2 + failureThreshold: 3 resources: requests: cpu: 200m @@ -56,8 +92,6 @@ kind: Ingress metadata: name: la-huasca-ts namespace: lahuasca - annotations: - traefik.ingress.kubernetes.io/router.entrypoints: web,websecure spec: ingressClassName: traefik tls: diff --git a/k8s/bootstrap-configmap.yaml b/k8s/bootstrap-configmap.yaml new file mode 100644 index 0000000..4bc9256 --- /dev/null +++ b/k8s/bootstrap-configmap.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: la-huasca-bootstrap-config + namespace: lahuasca +data: + BOOTSTRAP_TAGLINE: "Hotel · Restaurant · Andean Highlands" diff --git a/k8s/bootstrap-secrets.yaml b/k8s/bootstrap-secrets.yaml new file mode 100644 index 0000000..6e1b3e2 --- /dev/null +++ b/k8s/bootstrap-secrets.yaml @@ -0,0 +1,10 @@ +apiVersion: v1 +kind: Secret +metadata: + name: la-huasca-bootstrap-secrets + namespace: lahuasca +type: Opaque +stringData: + BOOTSTRAP_ADMIN_PASSWORD: "CHANGE_ME_ADMIN_PASSWORD" + BOOTSTRAP_USER_PASSWORD: "CHANGE_ME_USER_PASSWORD" + BOOTSTRAP_WIFI_PASSWORD: "CHANGE_ME_WIFI_PASSWORD" diff --git a/k8s/deployment.json b/k8s/deployment.json index 90be4ef..cc6ac1a 100644 --- a/k8s/deployment.json +++ b/k8s/deployment.json @@ -3,7 +3,7 @@ "kind": "Deployment", "metadata": { "name": "la-huasca-ts", - "namespace": "la-huasca" + "namespace": "lahuasca" }, "spec": { "replicas": 1, diff --git a/k8s/ingress.json b/k8s/ingress.json index 6f16e52..a3e9fb2 100644 --- a/k8s/ingress.json +++ b/k8s/ingress.json @@ -3,7 +3,7 @@ "kind": "Ingress", "metadata": { "name": "la-huasca-ts", - "namespace": "la-huasca", + "namespace": "lahuasca", "annotations": { "traefik.ingress.kubernetes.io/router.entrypoints": "web,websecure" } diff --git a/k8s/init-db-bootstrap-configmap.yaml b/k8s/init-db-bootstrap-configmap.yaml new file mode 100644 index 0000000..016c84c --- /dev/null +++ b/k8s/init-db-bootstrap-configmap.yaml @@ -0,0 +1,220 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + name: la-huasca-bootstrap-config + namespace: lahuasca +data: + BOOTSTRAP_TAGLINE: Hotel · Restaurant · Andean Highlands + schema.sql: | + CREATE TABLE IF NOT EXISTS users ( + id SERIAL PRIMARY KEY, + username VARCHAR(50) UNIQUE NOT NULL, + password_hash VARCHAR(255) NOT NULL, + role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')), + comments_disabled BOOLEAN DEFAULT FALSE, + first_name VARCHAR(100), + last_name VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS images ( + id SERIAL PRIMARY KEY, + filename VARCHAR(255) NOT NULL, + data TEXT NOT NULL, + category VARCHAR(100) DEFAULT 'other', + room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL, + location_target VARCHAR(100) DEFAULT 'gallery', + uploaded_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS reservations ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + date DATE NOT NULL, + time TIME NOT NULL, + guests INTEGER NOT NULL, + table_name VARCHAR(100) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS coupons ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + code VARCHAR(50) NOT NULL, + discount VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS offers ( + id SERIAL PRIMARY KEY, + title VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS benefits ( + id SERIAL PRIMARY KEY, + text VARCHAR(500) NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS config ( + key VARCHAR(100) PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS rooms ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + price NUMERIC(10,2) NOT NULL, + photos TEXT[] DEFAULT '{}', + featured_photo VARCHAR(500), + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS menu_items ( + id SERIAL PRIMARY KEY, + category VARCHAR(100) NOT NULL, + name VARCHAR(255) NOT NULL, + description TEXT, + price NUMERIC(10,2) NOT NULL, + is_daily_special BOOLEAN DEFAULT FALSE, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS places ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT NOT NULL, + photos TEXT[] DEFAULT '{}', + featured_photo VARCHAR(500), + distance_km NUMERIC(8,2), + visit_time VARCHAR(100), + suggestion TEXT, + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS reviews ( + id SERIAL PRIMARY KEY, + author_name VARCHAR(255) NOT NULL, + rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5), + text TEXT NOT NULL, + approved BOOLEAN DEFAULT FALSE, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS slides ( + id SERIAL PRIMARY KEY, + title VARCHAR(255), + subtitle TEXT, + image_id INTEGER REFERENCES images(id) ON DELETE SET NULL, + image_url VARCHAR(500), + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS room_comments ( + id SERIAL PRIMARY KEY, + room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE, + photo_url VARCHAR(500), + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + first_name VARCHAR(100) NOT NULL, + last_initial VARCHAR(10), + text TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW(), + deleted_by_admin BOOLEAN DEFAULT FALSE + ); + + CREATE TABLE IF NOT EXISTS social_events ( + id SERIAL PRIMARY KEY, + name VARCHAR(255) NOT NULL, + description TEXT, + price VARCHAR(50), + date DATE, + photos TEXT[] DEFAULT '{}', + featured_photo VARCHAR(500), + active BOOLEAN DEFAULT TRUE, + sort_order INTEGER DEFAULT 0, + created_at TIMESTAMP DEFAULT NOW() + ); + + CREATE TABLE IF NOT EXISTS social_event_reservations ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE, + guests INTEGER NOT NULL, + notes TEXT, + status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')), + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(user_id, social_event_id) + ); + + CREATE TABLE IF NOT EXISTS calendar_events ( + id SERIAL PRIMARY KEY, + date DATE NOT NULL UNIQUE, + title VARCHAR(255) NOT NULL, + type VARCHAR(100) NOT NULL, + description TEXT, + color VARCHAR(20) NOT NULL + ); + + CREATE TABLE IF NOT EXISTS user_sessions ( + id SERIAL PRIMARY KEY, + user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, + token TEXT NOT NULL, + created_at TIMESTAMP DEFAULT NOW() + ); + + ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500); + ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE; + ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100); + ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100); + seed.sh: | + #!/bin/sh + set -eu + + : "${BOOTSTRAP_ADMIN_USERNAME:?missing BOOTSTRAP_ADMIN_USERNAME}" + : "${BOOTSTRAP_ADMIN_PASSWORD:?missing BOOTSTRAP_ADMIN_PASSWORD}" + : "${BOOTSTRAP_USER_USERNAME:?missing BOOTSTRAP_USER_USERNAME}" + : "${BOOTSTRAP_USER_PASSWORD:?missing BOOTSTRAP_USER_PASSWORD}" + : "${BOOTSTRAP_WIFI_PASSWORD:?missing BOOTSTRAP_WIFI_PASSWORD}" + : "${BOOTSTRAP_TAGLINE:?missing BOOTSTRAP_TAGLINE}" + + psql -h "${DB_HOST}" -p "${DB_PORT}" -U "$POSTGRES_USER" -d "$POSTGRES_DB" <<'SQL' + INSERT INTO users (username, password_hash, role) + VALUES + ('${BOOTSTRAP_ADMIN_USERNAME}', '${BOOTSTRAP_ADMIN_PASSWORD}', 'admin'), + ('${BOOTSTRAP_USER_USERNAME}', '${BOOTSTRAP_USER_PASSWORD}', 'user') + ON CONFLICT (username) DO NOTHING; + + INSERT INTO config (key, value) + VALUES + ('wifi_password', '${BOOTSTRAP_WIFI_PASSWORD}'), + ('tagline', '${BOOTSTRAP_TAGLINE}') + ON CONFLICT (key) DO NOTHING; + + INSERT INTO offers (title, description, active, sort_order) + VALUES + ('Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.', true, 1), + ('Sunset Dinner', 'Three-course menu on the terrace every Friday.', true, 2) + ON CONFLICT DO NOTHING; + + INSERT INTO benefits (text) + VALUES + ('Priority booking for peak weekends'), + ('Late checkout on availability'), + ('Free welcome cocktail'), + ('Member-only events and tastings') + ON CONFLICT DO NOTHING; + SQL diff --git a/k8s/init-db-job.yaml b/k8s/init-db-job.yaml index a48c125..4f4c65e 100644 --- a/k8s/init-db-job.yaml +++ b/k8s/init-db-job.yaml @@ -4,239 +4,93 @@ metadata: name: init-database namespace: lahuasca spec: + backoffLimit: 4 template: spec: - containers: - - name: init-db - image: postgres:15 - command: - - sh - - -c - - | - apk add --no-cache curl - # Wait for postgres to be ready - until pg_isready -h postgres.lahuasca.svc.cluster.local -U lahuasca; do - echo "Waiting for postgres..." - sleep 2 - done - - # Create tables and seed data - psql -h postgres.lahuasca.svc.cluster.local -U lahuasca -d lahuasca << 'EOF' - -- Create all tables - CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - username VARCHAR(50) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - role VARCHAR(20) NOT NULL CHECK (role IN ('admin', 'user')), - comments_disabled BOOLEAN DEFAULT FALSE, - first_name VARCHAR(100), - last_name VARCHAR(100), - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS images ( - id SERIAL PRIMARY KEY, - filename VARCHAR(255) NOT NULL, - data TEXT NOT NULL, - category VARCHAR(100) DEFAULT 'other', - room_id INTEGER REFERENCES rooms(id) ON DELETE SET NULL, - location_target VARCHAR(100) DEFAULT 'gallery', - uploaded_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS reservations ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - date DATE NOT NULL, - time TIME NOT NULL, - guests INTEGER NOT NULL, - table_name VARCHAR(100) NOT NULL, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS coupons ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - code VARCHAR(50) NOT NULL, - discount VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS offers ( - id SERIAL PRIMARY KEY, - title VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - active BOOLEAN DEFAULT TRUE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS benefits ( - id SERIAL PRIMARY KEY, - text VARCHAR(500) NOT NULL, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS config ( - key VARCHAR(100) PRIMARY KEY, - value TEXT NOT NULL - ); - - CREATE TABLE IF NOT EXISTS rooms ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - price NUMERIC(10,2) NOT NULL, - photos TEXT[] DEFAULT '{}', - featured_photo VARCHAR(500), - active BOOLEAN DEFAULT TRUE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW(), - updated_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS menu_items ( - id SERIAL PRIMARY KEY, - category VARCHAR(100) NOT NULL, - name VARCHAR(255) NOT NULL, - description TEXT, - price NUMERIC(10,2) NOT NULL, - is_daily_special BOOLEAN DEFAULT FALSE, - active BOOLEAN DEFAULT TRUE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS places ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT NOT NULL, - photos TEXT[] DEFAULT '{}', - featured_photo VARCHAR(500), - distance_km NUMERIC(8,2), - visit_time VARCHAR(100), - suggestion TEXT, - active BOOLEAN DEFAULT TRUE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS reviews ( - id SERIAL PRIMARY KEY, - author_name VARCHAR(255) NOT NULL, - rating INTEGER NOT NULL CHECK (rating BETWEEN 1 AND 5), - text TEXT NOT NULL, - approved BOOLEAN DEFAULT FALSE, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS slides ( - id SERIAL PRIMARY KEY, - title VARCHAR(255), - subtitle TEXT, - image_id INTEGER REFERENCES images(id) ON DELETE SET NULL, - image_url VARCHAR(500), - active BOOLEAN DEFAULT TRUE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS room_comments ( - id SERIAL PRIMARY KEY, - room_id INTEGER REFERENCES rooms(id) ON DELETE CASCADE, - photo_url VARCHAR(500), - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - first_name VARCHAR(100) NOT NULL, - last_initial VARCHAR(10), - text TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW(), - deleted_by_admin BOOLEAN DEFAULT FALSE - ); - - CREATE TABLE IF NOT EXISTS social_events ( - id SERIAL PRIMARY KEY, - name VARCHAR(255) NOT NULL, - description TEXT, - price VARCHAR(50), - date DATE, - photos TEXT[] DEFAULT '{}', - featured_photo VARCHAR(500), - active BOOLEAN DEFAULT TRUE, - sort_order INTEGER DEFAULT 0, - created_at TIMESTAMP DEFAULT NOW() - ); - - CREATE TABLE IF NOT EXISTS social_event_reservations ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - social_event_id INTEGER REFERENCES social_events(id) ON DELETE CASCADE, - guests INTEGER NOT NULL, - notes TEXT, - status VARCHAR(20) DEFAULT 'pending' CHECK (status IN ('pending', 'confirmed', 'cancelled')), - created_at TIMESTAMP DEFAULT NOW(), - UNIQUE(user_id, social_event_id) - ); - - CREATE TABLE IF NOT EXISTS calendar_events ( - id SERIAL PRIMARY KEY, - date DATE NOT NULL UNIQUE, - title VARCHAR(255) NOT NULL, - type VARCHAR(100) NOT NULL, - description TEXT, - color VARCHAR(20) NOT NULL - ); - - CREATE TABLE IF NOT EXISTS user_sessions ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id) ON DELETE CASCADE, - token TEXT NOT NULL, - created_at TIMESTAMP DEFAULT NOW() - ); - - -- Add missing columns if they don't exist - ALTER TABLE rooms ADD COLUMN IF NOT EXISTS featured_photo VARCHAR(500); - ALTER TABLE users ADD COLUMN IF NOT EXISTS comments_disabled BOOLEAN DEFAULT FALSE; - ALTER TABLE users ADD COLUMN IF NOT EXISTS first_name VARCHAR(100); - ALTER TABLE users ADD COLUMN IF NOT EXISTS last_name VARCHAR(100); - - -- Insert initial data - -- Default admin user (password: admin) - INSERT INTO users (username, password_hash, role) VALUES - ('admin', '$2a$10$8K1p/a0dhrxiowP.dnkgNORTWgdEDHn5L2/xjpEWuC.QQv4rKO9jO', 'admin') - ON CONFLICT (username) DO NOTHING; - - -- Default config values - INSERT INTO config (key, value) VALUES - ('wifi_password', 'LaHuascaGuest2026'), - ('tagline', 'Hotel · Restaurant · Andean Highlands') - ON CONFLICT (key) DO NOTHING; - - -- Sample offers - INSERT INTO offers (title, description, active, sort_order) VALUES - ('Summer Wine Weekend', 'Complimentary wine tasting with every two-night stay.', true, 1), - ('Sunset Dinner', 'Three-course menu on the terrace every Friday.', true, 2) - ON CONFLICT DO NOTHING; - - -- Sample benefits - INSERT INTO benefits (text) VALUES - ('Priority booking for peak weekends'), - ('Late checkout on availability'), - ('Free welcome cocktail'), - ('Member-only events and tastings') - ON CONFLICT DO NOTHING; - EOF - - echo "Database initialization completed!" - env: - - name: PGPASSWORD - valueFrom: - secretKeyRef: - name: la-huasca-db-secrets - key: POSTGRES_PASSWORD - - name: POSTGRES_USER - valueFrom: - secretKeyRef: - name: la-huasca-db-secrets - key: POSTGRES_USER restartPolicy: Never - backoffLimit: 4 \ No newline at end of file + containers: + - name: init-db + image: postgres:15 + command: + - sh + - -ec + - | + until pg_isready -h "${DB_HOST}" -p "${DB_PORT}" -U "$POSTGRES_USER"; do + echo "Waiting for postgres..." + sleep 2 + done + + psql \ + -h "${DB_HOST}" \ + -p "${DB_PORT}" \ + -U "$POSTGRES_USER" \ + -d "$POSTGRES_DB" \ + -f /bootstrap/schema.sql + + sh /bootstrap/seed.sh + + echo "Database initialization completed!" + env: + - name: DB_HOST + value: postgres + - name: DB_PORT + value: "5432" + - name: PGPASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-db-secrets + key: POSTGRES_PASSWORD + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: la-huasca-db-secrets + key: POSTGRES_USER + - name: POSTGRES_DB + valueFrom: + secretKeyRef: + name: la-huasca-db-secrets + key: POSTGRES_DB + - name: BOOTSTRAP_ADMIN_USERNAME + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_ADMIN_USERNAME + - name: BOOTSTRAP_ADMIN_PASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_ADMIN_PASSWORD + - name: BOOTSTRAP_USER_USERNAME + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_USER_USERNAME + - name: BOOTSTRAP_USER_PASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_USER_PASSWORD + - name: BOOTSTRAP_WIFI_PASSWORD + valueFrom: + secretKeyRef: + name: la-huasca-bootstrap-secrets + key: BOOTSTRAP_WIFI_PASSWORD + - name: BOOTSTRAP_TAGLINE + valueFrom: + configMapKeyRef: + name: la-huasca-bootstrap-config + key: BOOTSTRAP_TAGLINE + volumeMounts: + - name: bootstrap + mountPath: /bootstrap + readOnly: true + volumes: + - name: bootstrap + configMap: + name: la-huasca-bootstrap-config + defaultMode: 0755 + items: + - key: schema.sql + path: schema.sql + - key: seed.sh + path: seed.sh diff --git a/k8s/registry-secret.yaml b/k8s/registry-secret.yaml index 389909e..1831b79 100644 --- a/k8s/registry-secret.yaml +++ b/k8s/registry-secret.yaml @@ -2,7 +2,7 @@ apiVersion: v1 kind: Secret metadata: name: la-huasca-registry - namespace: ${NAMESPACE} + namespace: lahuasca type: kubernetes.io/dockerconfigjson stringData: .dockerconfigjson: | diff --git a/k8s/service.json b/k8s/service.json index 77a3cd6..7a43834 100644 --- a/k8s/service.json +++ b/k8s/service.json @@ -3,7 +3,7 @@ "kind": "Service", "metadata": { "name": "la-huasca-ts", - "namespace": "la-huasca" + "namespace": "lahuasca" }, "spec": { "ports": [ diff --git a/package-lock.json b/package-lock.json index e1eaf13..5b1bbb6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -17,12 +17,125 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^2.4.6", "@types/node": "^20.17.30", "@types/pg": "^8.11.6", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.6", - "typescript": "^5.4.5" + "@vitejs/plugin-react": "^6.0.3", + "jsdom": "^29.1.1", + "typescript": "^5.4.5", + "vitest": "^4.1.9" + } + }, + "node_modules/@adobe/css-tools": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.5.0.tgz", + "integrity": "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@asamuzakjp/css-color": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/dom-selector": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", + "bidi-js": "^1.0.3", + "css-tree": "^3.2.1", + "is-potential-custom-element-name": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bramus/specificity": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", + "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.0" + }, + "bin": { + "specificity": "bin/cli.js" } }, "node_modules/@chenglou/pretext": { @@ -31,6 +144,224 @@ "integrity": "sha512-yqm2GMxnPI7VHcHwe84P8ZF0JK/2d2DMKPqMN+s95jQhwDMYYXKVFVJUMEaVWckQStdsjdLav/0Vu+d9YbtGxA==", "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", + "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@csstools/css-calc": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.9.tgz", + "integrity": "sha512-paQcIaOO53Rk5+YrBaBjm/SgrV4INImjo2BT1DtQRYr+XeTRbeAYlS+jxXp9drqvKmtFnWRJKIalDLhZZDu42A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^6.1.0", + "@csstools/css-calc": "^3.2.1" + }, + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^4.0.0", + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", + "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^4.0.0" + } + }, + "node_modules/@csstools/css-syntax-patches-for-csstree": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.6.tgz", + "integrity": "sha512-TcJCWFbXLPpJYq6z7bfOyjWYJDiDg2/I4gyUC9pqPNqHFRIey0EB0q0L5cSnQDfWJg8Jd6VadakxdIez/3zkqQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "peerDependencies": { + "css-tree": "^3.2.1" + }, + "peerDependenciesMeta": { + "css-tree": { + "optional": true + } + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", + "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@exodus/bytes": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.1.tgz", + "integrity": "sha512-S6mL0yNB/Abt9Ei4tq8gDhcczc4S3+vQ4ra7vxnAf+YHC02srtqxKKZghx2Dq6p0e66THKwR6r8N6P95wEty7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + }, + "peerDependencies": { + "@noble/hashes": "^1.8.0 || ^2.0.0" + }, + "peerDependenciesMeta": { + "@noble/hashes": { + "optional": true + } + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, "node_modules/@next/env": { "version": "14.2.28", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.28.tgz", @@ -193,6 +524,305 @@ "node": ">= 10" } }, + "node_modules/@oxc-project/types": { + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", @@ -209,6 +839,101 @@ "tslib": "^2.4.0" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.2", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.2.tgz", + "integrity": "sha512-XU5/SytQM+ykqMnAnvB2umaJNIOsLF3PVv//1Ew4CTcpz0/BRyy/af40qqrt7SjKpDdT1saBMc42CUok5gaw+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", + "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/bcryptjs": { "version": "2.4.6", "resolved": "https://registry.npmjs.org/@types/bcryptjs/-/bcryptjs-2.4.6.tgz", @@ -216,6 +941,31 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/node": { "version": "20.19.43", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", @@ -266,12 +1016,206 @@ "@types/react": "^18.0.0" } }, + "node_modules/@vitejs/plugin-react": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.3.tgz", + "integrity": "sha512-vmFvco5/QuC2f9Oj+wTk0+9XeDFkHxSamwZKYc7MxYwKICfvUvlMhqKI0VuICPltGqh1neqBKDvO4kes1ya8vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rolldown/pluginutils": "^1.0.1" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", + "babel-plugin-react-compiler": "^1.0.0", + "vite": "^8.0.0" + }, + "peerDependenciesMeta": { + "@rolldown/plugin-babel": { + "optional": true + }, + "babel-plugin-react-compiler": { + "optional": true + } + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.9", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.9", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.9", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/bcryptjs": { "version": "2.4.3", "resolved": "https://registry.npmjs.org/bcryptjs/-/bcryptjs-2.4.3.tgz", "integrity": "sha512-V/Hy/X9Vt7f3BbPJEi8BdVFMByHi+jNXrYkW3huaybV/kQ0KJg0Y6PkEMbn+zeT+i+SiKZ/HMqJGIIt4LZDqNQ==", "license": "MIT" }, + "node_modules/bidi-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", + "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "require-from-string": "^2.0.2" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -303,12 +1247,50 @@ ], "license": "CC-BY-4.0" }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/csstype": { "version": "3.2.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", @@ -316,12 +1298,164 @@ "dev": true, "license": "MIT" }, + "node_modules/data-urls": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", + "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/entities": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", + "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", "license": "ISC" }, + "node_modules/html-encoding-sniffer": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", + "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.6.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, "node_modules/jose": { "version": "5.10.0", "resolved": "https://registry.npmjs.org/jose/-/jose-5.10.0.tgz", @@ -337,6 +1471,320 @@ "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "license": "MIT" }, + "node_modules/jsdom": { + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", + "@bramus/specificity": "^2.4.2", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", + "css-tree": "^3.2.1", + "data-urls": "^7.0.0", + "decimal.js": "^10.6.0", + "html-encoding-sniffer": "^6.0.0", + "is-potential-custom-element-name": "^1.0.1", + "lru-cache": "^11.3.5", + "parse5": "^8.0.1", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^8.0.1", + "whatwg-mimetype": "^5.0.0", + "whatwg-url": "^16.0.1", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -349,6 +1797,54 @@ "loose-envify": "cli.js" } }, + "node_modules/lru-cache": { + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/nanoid": { "version": "3.3.15", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", @@ -418,6 +1914,40 @@ } } }, + "node_modules/obug": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.3.tgz", + "integrity": "sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/parse5": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", + "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^8.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, "node_modules/pg": { "version": "8.22.0", "resolved": "https://registry.npmjs.org/pg/-/pg-8.22.0.tgz", @@ -513,6 +2043,19 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "license": "ISC" }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -580,6 +2123,32 @@ "node": ">=0.10.0" } }, + "node_modules/pretty-format": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz", + "integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ansi-regex": "^5.0.1", + "ansi-styles": "^5.0.0", + "react-is": "^17.0.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/react": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", @@ -605,6 +2174,85 @@ "react": "^18.3.1" } }, + "node_modules/react-is": { + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rolldown": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.137.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" + } + }, + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } + }, "node_modules/scheduler": { "version": "0.23.2", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", @@ -614,6 +2262,13 @@ "loose-envify": "^1.1.0" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -632,6 +2287,20 @@ "node": ">= 10.x" } }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -640,6 +2309,19 @@ "node": ">=10.0.0" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/styled-jsx": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", @@ -663,6 +2345,103 @@ } } }, + "node_modules/symbol-tree": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", + "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tldts": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.5.tgz", + "integrity": "sha512-RfEzKWcq5fHUOFq7J3rl3Oz6ylKGtcHqUznzj4EcXsxLSIjJcvpbXAQtWGeJQ0xKnimR5e0Cn+cn9TssfMzm+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^7.4.5" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "7.4.5", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.5.tgz", + "integrity": "sha512-pGrwzZDvPwKe+7NNUqAunb6rqTfynr0VOUhCMdqbu5xlvNiszsAJygRzwvpVycdzejlbpY+SWJOn+s75Og7FEA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tough-cookie": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^7.0.5" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/tr46": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", + "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -683,6 +2462,16 @@ "node": ">=14.17" } }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -690,6 +2479,285 @@ "dev": true, "license": "MIT" }, + "node_modules/vite": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.3.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.16", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz", + "integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/webidl-conversions": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", + "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-mimetype": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", + "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/whatwg-url": { + "version": "16.0.1", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", + "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@exodus/bytes": "^1.11.0", + "tr46": "^6.0.0", + "webidl-conversions": "^8.0.1" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/xmlchars": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", + "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", + "dev": true, + "license": "MIT" + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 598e549..b6e71c1 100644 --- a/package.json +++ b/package.json @@ -5,8 +5,9 @@ "scripts": { "dev": "next dev", "build": "next build", - "start": "next start -H 0.0.0.0 -p 3000", - "lint": "next lint" + "start": "node .next/standalone/server.js", + "lint": "next lint", + "test": "vitest run" }, "dependencies": { "@chenglou/pretext": "^0.0.8", @@ -18,11 +19,16 @@ "react-dom": "^18.3.1" }, "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", "@types/bcryptjs": "^2.4.6", "@types/node": "^20.17.30", "@types/pg": "^8.11.6", "@types/react": "^18.3.20", "@types/react-dom": "^18.3.6", - "typescript": "^5.4.5" + "@vitejs/plugin-react": "^6.0.3", + "jsdom": "^29.1.1", + "typescript": "^5.4.5", + "vitest": "^4.1.9" } } diff --git a/src/app/api/admin/menu/route.ts b/src/app/api/admin/menu/route.ts index 2b6802b..3e89adb 100644 --- a/src/app/api/admin/menu/route.ts +++ b/src/app/api/admin/menu/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireRole } from '@/lib/auth'; import { db } from '@/lib/db'; +export const dynamic = 'force-dynamic'; // Prevents static generation + // GET /api/admin/menu — returns ALL menu items (including inactive) export async function GET() { try { diff --git a/src/app/api/admin/reviews/route.ts b/src/app/api/admin/reviews/route.ts index 899a897..6b74ae2 100644 --- a/src/app/api/admin/reviews/route.ts +++ b/src/app/api/admin/reviews/route.ts @@ -2,6 +2,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { requireRole } from '@/lib/auth'; import { db } from '@/lib/db'; +export const dynamic = 'force-dynamic'; // Prevents static generation + export async function GET() { try { await requireRole('admin'); diff --git a/src/app/api/bootstrap-user/route.ts b/src/app/api/bootstrap-user/route.ts index 3347e37..fbc09c7 100644 --- a/src/app/api/bootstrap-user/route.ts +++ b/src/app/api/bootstrap-user/route.ts @@ -1,11 +1,18 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; import { createUser } from '@/lib/auth'; export async function POST() { try { - const user = await createUser('mmendoza', 'W3canbeheroes*-*', 'admin'); + const username = process.env.BOOTSTRAP_ADMIN_USERNAME || 'mmendoza'; + const password = process.env.BOOTSTRAP_ADMIN_PASSWORD; + + if (!password) { + return NextResponse.json({ error: 'Bootstrap password is not configured' }, { status: 500 }); + } + + const user = await createUser(username, password, 'admin'); return NextResponse.json({ ok: true, user: { id: user.id, username: user.username, role: user.role } }); } catch (err: any) { - return NextResponse.json({ error: err.message || 'Failed to create user' }, { status: 500 }); + return NextResponse.json({ error: err?.message || 'Failed to create bootstrap user' }, { status: 500 }); } } diff --git a/src/app/api/health/route.ts b/src/app/api/health/route.ts new file mode 100644 index 0000000..1010846 --- /dev/null +++ b/src/app/api/health/route.ts @@ -0,0 +1,19 @@ +import { NextResponse } from 'next/server'; + +export const dynamic = 'force-dynamic'; + +export async function GET() { + return NextResponse.json( + { + ok: true, + status: 'healthy', + timestamp: new Date().toISOString(), + }, + { + status: 200, + headers: { + 'cache-control': 'no-store, max-age=0', + }, + } + ); +} diff --git a/src/app/api/social_events/public/route.ts b/src/app/api/social_events/public/route.ts index b779294..ae1542d 100644 --- a/src/app/api/social_events/public/route.ts +++ b/src/app/api/social_events/public/route.ts @@ -1,6 +1,8 @@ import { db } from '@/lib/db'; import { NextResponse } from 'next/server'; +export const dynamic = 'force-dynamic'; // Prevents static generation + export async function GET() { try { const { rows } = await db.query(` diff --git a/src/app/api/user/portal/route.ts b/src/app/api/user/portal/route.ts index d7a03f8..b001840 100644 --- a/src/app/api/user/portal/route.ts +++ b/src/app/api/user/portal/route.ts @@ -2,6 +2,8 @@ import { NextResponse } from 'next/server'; import { requireRole } from '@/lib/auth'; import { db } from '@/lib/db'; +export const dynamic = 'force-dynamic'; // Prevents static generation + export async function GET() { try { const session = await requireRole('user'); diff --git a/src/app/page.tsx b/src/app/page.tsx index 5373892..31509f2 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -3,26 +3,50 @@ import { useEffect, useState } from 'react'; import Slideshow from '@/components/Slideshow'; import CalendarStrip from '@/components/CalendarStrip'; -import MeasuredText from '@/components/MeasuredText'; import WeatherWidget from '@/components/WeatherWidget'; import LanguageSwitcher from '@/components/LanguageSwitcher'; -import { t, getLanguage } from '@/lib/i18n'; +import { t, useLanguage } from '@/lib/i18n'; const gold = '#E8A849'; const navy = '#010D1E'; const cream = '#f5f0e8'; +const homeCopy = { + en: { + title: 'La Huasca', + subtitle: 'A mountain retreat with warm hospitality and local flavor.', + highlights: ['common.hotel', 'common.restaurant', 'common.andean_highlands'], + }, + es: { + title: 'La Huasca', + subtitle: 'Un refugio de montaña con hospitalidad cálida y sabor local.', + highlights: ['common.hotel', 'common.restaurant', 'common.andean_highlands'], + }, +} as const; + +const fallbackTaglineKey = 'common.home_tagline'; + export default function HomePage() { - const [tagline, setTagline] = useState('Hotel · Restaurant · Andean Highlands'); + const language = useLanguage(); + const [tagline, setTagline] = useState(t(fallbackTaglineKey, language)); useEffect(() => { + let cancelled = false; + const fallbackTagline = t(fallbackTaglineKey, language); + setTagline(fallbackTagline); fetch('/api/site-assets?key=tagline') .then((r) => r.json()) .then((j) => { - if (j.data) setTagline(j.data); + if (!cancelled && j.data) setTagline(j.data); }) .catch(() => {}); - }, []); + + return () => { + cancelled = true; + }; + }, [language]); + + const copy = homeCopy[language]; return (
@@ -64,10 +88,18 @@ export default function HomePage() { color: cream, }} > - La Huasca + {copy.title}

- {t('common.hotel', getLanguage())} · {t('common.restaurant', getLanguage())} · {t('common.andean_highlands', getLanguage())} + {copy.highlights.map((key, index) => ( + + {t(key, language)} + {index < copy.highlights.length - 1 ? ' · ' : ''} + + ))} +

+

+ {copy.subtitle}

diff --git a/src/components/LanguageSwitcher.tsx b/src/components/LanguageSwitcher.tsx index 70ff58d..1ea20c8 100644 --- a/src/components/LanguageSwitcher.tsx +++ b/src/components/LanguageSwitcher.tsx @@ -1,21 +1,35 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { getLanguage, setLanguage } from '@/lib/i18n'; +import { Language, setLanguage, useLanguage } from '@/lib/i18n'; + +function Flag({ language }: { language: Language }) { + if (language === 'en') { + return ( + + + + + + ); + } + + return ( + + + + + + + + ); +} export default function LanguageSwitcher() { - const [currentLang, setCurrentLang] = useState<'en' | 'es'>('en'); - - useEffect(() => { - setCurrentLang(getLanguage()); - }, []); + const currentLang = useLanguage(); + const nextLanguage = currentLang === 'en' ? 'es' : 'en'; const toggleLanguage = () => { - const newLang = currentLang === 'en' ? 'es' : 'en'; - setLanguage(newLang); - setCurrentLang(newLang); - // Reload the page to apply the language change - window.location.reload(); + setLanguage(nextLanguage); }; return ( @@ -29,13 +43,15 @@ export default function LanguageSwitcher() { background: 'rgba(255, 255, 255, 0.9)', border: '1px solid #ddd', borderRadius: '4px', - padding: '0.5rem 1rem', + padding: '0.5rem', cursor: 'pointer', - fontSize: '0.9rem', - fontWeight: 'bold', + display: 'flex', + alignItems: 'center', + justifyContent: 'center', }} + aria-label={`Switch to ${currentLang === 'en' ? 'Spanish' : 'English'}`} > - {currentLang === 'en' ? 'ES' : 'EN'} + ); -} \ No newline at end of file +} diff --git a/src/components/admin/AdminSidebar.tsx b/src/components/admin/AdminSidebar.tsx new file mode 100644 index 0000000..0ad09ce --- /dev/null +++ b/src/components/admin/AdminSidebar.tsx @@ -0,0 +1,74 @@ +'use client'; + +export type AdminNavItem = { + id: string; + label: string; + icon: string; +}; + +export type AdminSidebarProps = { + items: AdminNavItem[]; + activeSection: string; + onSelect: (id: string) => void; +}; + +const C = { + gold: '#D4A853', + goldLight: 'rgba(212,168,83,0.12)', + navy: '#010D1E', +}; + +export default function AdminSidebar({ items, activeSection, onSelect }: AdminSidebarProps) { + return ( + + ); +} diff --git a/src/components/admin/Dashboard.tsx b/src/components/admin/Dashboard.tsx new file mode 100644 index 0000000..52a9e64 --- /dev/null +++ b/src/components/admin/Dashboard.tsx @@ -0,0 +1,55 @@ +'use client'; + +const C = { + gold: '#D4A853', + navy: '#010D1E', + card: '#FFFFFF', + shadow: '0 1px 4px rgba(1,13,30,0.06)', + textLight: '#888888', +}; + +function StatCard({ icon, value, label }: { icon: string; value: number; label: string }) { + return ( +
+ {icon} +
+
{value}
+
{label}
+
+
+ ); +} + +export type DashboardProps = { + rooms: Array; + slides: Array; + events: Array; + reviews: Array<{ approved?: boolean }>; + places: Array; +}; + +export default function Dashboard({ rooms, slides, events, reviews, places }: DashboardProps) { + return ( +
+

Dashboard

+
+ + + + + r.approved)?.length || 0} label="Approved Reviews" /> +
+
+ ); +} diff --git a/src/components/admin/WeatherSection.tsx b/src/components/admin/WeatherSection.tsx index 9396762..2b94fae 100644 --- a/src/components/admin/WeatherSection.tsx +++ b/src/components/admin/WeatherSection.tsx @@ -1,154 +1,171 @@ 'use client'; -import { useState, useEffect } from 'react'; -import { t } from '@/lib/i18n'; +import { useEffect, useState } from 'react'; + +const C = { + gold: '#D4A853', + navy: '#010D1E', + bg: '#F7F4EF', + card: '#FFFFFF', + border: 'rgba(1,13,30,0.08)', + text: '#444444', + textLight: '#888888', + danger: '#D32F2F', +}; + +function I({ label, value, onChange, type = 'text', style }: any) { + return ( +
+ {label && } + onChange(e.target.value)} + style={{ + padding: '8px 12px', + borderRadius: 8, + border: `1px solid ${C.border}`, + fontSize: '14px', + outline: 'none', + background: '#FAFAFA', + ...style, + }} + /> +
+ ); +} + +function SecHead({ title }: { title: string }) { + return ( +
+

{title}

+
+ ); +} export default function WeatherSection({ onToast }: { onToast: (msg: string, type: string) => void }) { const [enabled, setEnabled] = useState(true); - const [cities, setCities] = useState>([ - { name: 'La Huasca', lat: -31.525, lon: -65.125 } - ]); + const [cities, setCities] = useState<{ name: string; lat: string; lon: string }[]>([]); + const [newCity, setNewCity] = useState({ name: '', lat: '', lon: '' }); + const [loading, setLoading] = useState(true); - // Load weather config from API useEffect(() => { - fetch('/api/site-assets?key=weather_config') - .then(r => r.json()) - .then(data => { - if (data.data) { - try { + const loadConfig = async () => { + try { + const res = await fetch('/api/site-assets?key=weather_config'); + if (res.ok) { + const data = await res.json(); + if (data.data) { const config = JSON.parse(data.data); setEnabled(config.enabled ?? true); - setCities(config.cities ?? [{ name: 'La Huasca', lat: -31.525, lon: -65.125 }]); - } catch (e) { - console.error('Failed to parse weather config:', e); + setCities(config.cities ?? []); } } - }) - .catch(err => { + } catch (err) { console.error('Failed to load weather config:', err); - }); + } finally { + setLoading(false); + } + }; + loadConfig(); }, []); - const addCity = () => { - setCities([...cities, { name: '', lat: 0, lon: 0 }]); + const saveConfig = async () => { + try { + const config = { enabled, cities }; + const res = await fetch('/api/site-assets', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ key: 'weather_config', value: JSON.stringify(config) }), + }); + + if (res.ok) { + onToast('Weather settings saved', 'success'); + } else { + throw new Error('Failed to save'); + } + } catch (err) { + onToast('Error saving weather settings', 'error'); + } }; - const updateCity = (index: number, field: string, value: string | number) => { - const updated = [...cities]; - if (field === 'name') { - updated[index].name = value as string; - } else if (field === 'lat') { - updated[index].lat = value as number; - } else if (field === 'lon') { - updated[index].lon = value as number; + const addCity = () => { + if (newCity.name && newCity.lat && newCity.lon) { + setCities([...cities, newCity]); + setNewCity({ name: '', lat: '', lon: '' }); } - setCities(updated); }; const removeCity = (index: number) => { setCities(cities.filter((_, i) => i !== index)); }; - const saveConfig = async () => { - try { - const config = { enabled, cities }; - const response = await fetch('/api/site-assets', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - key: 'weather_config', - data: JSON.stringify(config) - }) - }); - - if (response.ok) { - onToast(t('admin.weather.save_success', 'en'), 'success'); - } else { - onToast(t('admin.weather.save_error', 'en'), 'error'); - } - } catch (err) { - console.error('Failed to save weather config:', err); - onToast(t('admin.weather.save_error', 'en'), 'error'); - } - }; + if (loading) { + return ( +
+ +
Loading...
+
+ ); + } return ( -
-

{t('admin.weather.title', 'en')}

-

{t('admin.weather.description', 'en')}

+
+ +
+

Configure the weather widget displayed on the homepage

-
- -
+
+ +
-
-

{t('admin.weather.cities', 'en')}

- {cities.map((city, index) => ( -
-
- - updateCity(index, 'name', e.target.value)} - style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px' }} - /> +

Cities

+
+ {cities.map((city, index) => ( +
+ {city.name} ({city.lat}, {city.lon}) +
-
- - updateCity(index, 'lat', parseFloat(e.target.value) || 0)} - style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }} - /> -
-
- - updateCity(index, 'lon', parseFloat(e.target.value) || 0)} - style={{ padding: '0.5rem', border: '1px solid #ddd', borderRadius: '4px', width: '100px' }} - /> -
- + ))} +
+ +
+

Add City

+ setNewCity({ ...newCity, name: v })} /> +
+ setNewCity({ ...newCity, lat: v })} /> + setNewCity({ ...newCity, lon: v })} />
- ))} - -
+ +
- +
+ +
+
); -} \ No newline at end of file +} diff --git a/src/lib/db.ts b/src/lib/db.ts index 20efcc3..55f40ec 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -2,9 +2,12 @@ import { Pool } from 'pg'; function createPool() { const connectionString = process.env.DATABASE_URL; + const isBuildPhase = typeof process.env.NEXT_PHASE === 'string' && process.env.NEXT_PHASE.includes('phase-production-build'); if (!connectionString) { - if (typeof process.env.NEXT_PHASE === 'string' && process.env.NEXT_PHASE.includes('phase-production-build')) { + if (isBuildPhase) { + // Build-time fallback only: keeps static rendering and type generation from crashing. + // Runtime imports should still fail fast when DATABASE_URL is missing. return { query: async () => { throw new Error('DATABASE_URL environment variable is not set'); @@ -17,6 +20,7 @@ function createPool() { removeListener: () => {}, } as unknown as Pool; } + throw new Error('DATABASE_URL environment variable is not set'); } diff --git a/src/lib/i18n.ts b/src/lib/i18n.ts index 6193ae7..180b9c6 100644 --- a/src/lib/i18n.ts +++ b/src/lib/i18n.ts @@ -1,12 +1,17 @@ -// Simple translation utility for La Huasca website +// Translation utility for La Huasca. +// Keep it simple, but structured enough to scale without scattering key lookups. + +import { useSyncExternalStore } from 'react'; -// Define languages export type Language = 'en' | 'es'; +export const DEFAULT_LANGUAGE: Language = 'en'; +export const supportedLanguages: Language[] = ['en', 'es']; -// Translation data -export const translations = { +const LANGUAGE_STORAGE_KEY = 'language'; +const LANGUAGE_CHANGE_EVENT = 'languagechange'; + +export const translations: Record> = { en: { - // Common 'common.hotel': 'Hotel', 'common.restaurant': 'Restaurant', 'common.andean_highlands': 'Andean Highlands', @@ -18,8 +23,9 @@ export const translations = { 'common.user_portal': 'User Portal', 'common.login': 'Login', 'common.logout': 'Logout', - - // Navigation + 'common.language': 'Language', + 'common.home_tagline': 'Hotel · Restaurant · Andean Highlands', + 'common.home_subtitle': 'A mountain retreat with warm hospitality and local flavor.', 'nav.brand': 'Brand', 'nav.gallery': 'Images', 'nav.slideshow': 'Slideshow', @@ -31,8 +37,6 @@ export const translations = { 'nav.reviews': 'Reviews', 'nav.users': 'Users', 'nav.weather': 'Weather', - - // Weather widget 'weather.currently': 'Currently', 'weather.today': 'Today', 'weather.max': 'Max', @@ -40,8 +44,6 @@ export const translations = { 'weather.wind': 'Wind', 'weather.kmh': 'km/h', 'weather_updated': 'Updated', - - // Admin panel 'admin.brand': 'Brand', 'admin.gallery': 'Images', 'admin.slideshow': 'Slideshow', @@ -53,8 +55,6 @@ export const translations = { 'admin.reviews': 'Reviews', 'admin.users': 'Users', 'admin.weather': 'Weather', - - // Weather admin 'admin.weather.title': 'Weather Widget', 'admin.weather.description': 'Configure the weather widget displayed on the homepage', 'admin.weather.enabled': 'Enable Weather Widget', @@ -66,7 +66,6 @@ export const translations = { 'admin.weather.save': 'Save Settings', }, es: { - // Common 'common.hotel': 'Hotel', 'common.restaurant': 'Restaurante', 'common.andean_highlands': 'Tierras Altas Andinas', @@ -78,8 +77,9 @@ export const translations = { 'common.user_portal': 'Portal de Usuario', 'common.login': 'Iniciar Sesión', 'common.logout': 'Cerrar Sesión', - - // Navigation + 'common.language': 'Idioma', + 'common.home_tagline': 'Hotel · Restaurante · Tierras Altas Andinas', + 'common.home_subtitle': 'Un refugio de montaña con hospitalidad cálida y sabor local.', 'nav.brand': 'Marca', 'nav.gallery': 'Imágenes', 'nav.slideshow': 'Presentación', @@ -91,8 +91,6 @@ export const translations = { 'nav.reviews': 'Reseñas', 'nav.users': 'Usuarios', 'nav.weather': 'Clima', - - // Weather widget 'weather.currently': 'Actualmente', 'weather.today': 'Hoy', 'weather.max': 'Máx', @@ -100,8 +98,6 @@ export const translations = { 'weather.wind': 'Viento', 'weather.kmh': 'km/h', 'weather_updated': 'Actualizado', - - // Admin panel 'admin.brand': 'Marca', 'admin.gallery': 'Imágenes', 'admin.slideshow': 'Presentación', @@ -113,8 +109,6 @@ export const translations = { 'admin.reviews': 'Reseñas', 'admin.users': 'Usuarios', 'admin.weather': 'Clima', - - // Weather admin 'admin.weather.title': 'Widget del Clima', 'admin.weather.description': 'Configurar el widget del clima mostrado en la página de inicio', 'admin.weather.enabled': 'Habilitar Widget del Clima', @@ -124,36 +118,65 @@ export const translations = { 'admin.weather.latitude': 'Latitud', 'admin.weather.longitude': 'Longitud', 'admin.weather.save': 'Guardar Configuración', - } + }, }; -// Simple translation function -export function t(key: string, language: Language = 'en'): string { - if (language === 'es' && key in translations.es) { - return translations.es[key as keyof typeof translations.es]; - } - - if (key in translations.en) { - return translations.en[key as keyof typeof translations.en]; - } - +function normalizeLanguage(language?: string | null): Language { + return language === 'es' ? 'es' : DEFAULT_LANGUAGE; +} + +export function t(key: string, language: Language = DEFAULT_LANGUAGE): string { + const current = translations[language]?.[key]; + if (current) return current; + + const fallback = translations[DEFAULT_LANGUAGE]?.[key]; + if (fallback) return fallback; + return key; } -// Get language from localStorage or default to 'en' export function getLanguage(): Language { - if (typeof window !== 'undefined') { - const savedLang = localStorage.getItem('language'); - if (savedLang && (savedLang === 'en' || savedLang === 'es')) { - return savedLang as Language; - } + if (typeof window === 'undefined') { + return DEFAULT_LANGUAGE; + } + + try { + return normalizeLanguage(localStorage.getItem(LANGUAGE_STORAGE_KEY)); + } catch { + return DEFAULT_LANGUAGE; } - return 'en'; } -// Set language in localStorage export function setLanguage(lang: Language) { - if (typeof window !== 'undefined') { - localStorage.setItem('language', lang); - } -} \ No newline at end of file + if (typeof window === 'undefined') return; + + localStorage.setItem(LANGUAGE_STORAGE_KEY, lang); + window.dispatchEvent(new Event(LANGUAGE_CHANGE_EVENT)); +} + +export function subscribeLanguage(callback: () => void) { + if (typeof window === 'undefined') return () => {}; + + const onLanguageChange = () => callback(); + const onStorage = (event: StorageEvent) => { + if (event.key === LANGUAGE_STORAGE_KEY) { + callback(); + } + }; + + window.addEventListener(LANGUAGE_CHANGE_EVENT, onLanguageChange); + window.addEventListener('storage', onStorage); + + return () => { + window.removeEventListener(LANGUAGE_CHANGE_EVENT, onLanguageChange); + window.removeEventListener('storage', onStorage); + }; +} + +export function useLanguage(): Language { + return useSyncExternalStore(subscribeLanguage, getLanguage, () => DEFAULT_LANGUAGE); +} + +export function getFallbackLanguage(language: Language): Language { + return language === 'es' ? 'en' : 'es'; +} diff --git a/src/lib/schema.ts b/src/lib/schema.ts index 2189417..feeb09b 100644 --- a/src/lib/schema.ts +++ b/src/lib/schema.ts @@ -257,14 +257,25 @@ export async function seed() { throw new Error('Seeding is disabled in production'); } + const adminPassword = process.env.BOOTSTRAP_ADMIN_PASSWORD; + const userPassword = process.env.BOOTSTRAP_USER_PASSWORD; + const wifiPassword = process.env.BOOTSTRAP_WIFI_PASSWORD; + const tagline = process.env.BOOTSTRAP_TAGLINE || 'Hotel · Restaurant · Andean Highlands'; + const { rows: adminRows } = await db.query(`SELECT id FROM users WHERE username = 'admin'`); - if (adminRows.length === 0) { - const adminHash = bcrypt.hashSync('admin', 10); - const userHash = bcrypt.hashSync('user', 10); + if (adminRows.length === 0 && adminPassword) { + const adminHash = bcrypt.hashSync(adminPassword, 10); + const seedRows: Array<[string, string, string]> = [['admin', adminHash, 'admin']]; + + if (userPassword) { + seedRows.push(['user', bcrypt.hashSync(userPassword, 10), 'user']); + } await db.query( - `INSERT INTO users (username, password_hash, role) VALUES ($1, $2, $3), ($4, $5, $6)`, - ['admin', adminHash, 'admin', 'user', userHash, 'user'] + `INSERT INTO users (username, password_hash, role) VALUES ${seedRows + .map((_, index) => `($${index * 3 + 1}, $${index * 3 + 2}, $${index * 3 + 3})`) + .join(', ')}`, + seedRows.flat() ); } @@ -295,13 +306,13 @@ export async function seed() { } const { rows: configWifiRows } = await db.query(`SELECT key FROM config WHERE key = 'wifi_password'`); - if (configWifiRows.length === 0) { - await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['wifi_password', 'LaHuascaGuest2026']); + if (configWifiRows.length === 0 && wifiPassword) { + await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['wifi_password', wifiPassword]); } const { rows: configTaglineRows } = await db.query(`SELECT key FROM config WHERE key = 'tagline'`); if (configTaglineRows.length === 0) { - await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['tagline', 'Hotel · Restaurant · Andean Highlands']); + await db.query(`INSERT INTO config (key, value) VALUES ($1, $2)`, ['tagline', tagline]); } } diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..f78019b --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,40 @@ +import { NextResponse } from 'next/server'; +import type { NextRequest } from 'next/server'; + +// Simple middleware for Next.js Edge Runtime +// This is a basic implementation that doesn't verify tokens +// In a production environment, you would want to verify the token +export function middleware(request: NextRequest) { + const { pathname } = request.nextUrl; + + // Protect admin routes + if (pathname.startsWith('/admin')) { + // Get session token from cookies + const token = request.cookies.get('session')?.value; + + // If no token, redirect to login + if (!token) { + return NextResponse.redirect(new URL('/login', request.url)); + } + + // Note: In a real implementation, you would verify the token here + // For now, we're just checking that a token exists + } + + // Redirect authenticated users from login page to home + if (pathname === '/login') { + const token = request.cookies.get('session')?.value; + if (token) { + // In a real implementation, you would verify the token and check role + // For now, we'll just redirect to home + return NextResponse.redirect(new URL('/', request.url)); + } + } + + return NextResponse.next(); +} + +// Configure which paths the middleware should run on +export const config = { + matcher: ['/admin/:path*', '/login'], +}; \ No newline at end of file diff --git a/tests/auth.test.ts b/tests/auth.test.ts new file mode 100644 index 0000000..76b01a3 --- /dev/null +++ b/tests/auth.test.ts @@ -0,0 +1,52 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { authenticateUser, createUser, hashPassword, verifyPassword } from '@/lib/auth'; + +const { query } = vi.hoisted(() => ({ query: vi.fn() })); + +vi.mock('@/lib/db', () => ({ + db: { + query, + }, +})); + +describe('auth helpers', () => { + beforeEach(() => { + query.mockReset(); + }); + + it('hashes passwords before storing a user', async () => { + query.mockResolvedValueOnce({ rows: [{ id: 1, username: 'alice', role: 'user' }] }); + + const user = await createUser('alice', 's3cret', 'user'); + + expect(user).toEqual({ id: 1, username: 'alice', role: 'user' }); + expect(query).toHaveBeenCalledTimes(1); + + const [, params] = query.mock.calls[0]; + expect(params[0]).toBe('alice'); + expect(params[1]).not.toBe('s3cret'); + expect(await verifyPassword('s3cret', params[1] as string)).toBe(true); + }); + + it('authenticates a user with the stored password hash', async () => { + const passwordHash = await hashPassword('secret-password'); + query.mockResolvedValueOnce({ + rows: [{ id: 2, username: 'bob', password_hash: passwordHash, role: 'admin' }], + }); + + await expect(authenticateUser('bob', 'secret-password')).resolves.toEqual({ + id: 2, + username: 'bob', + role: 'admin', + }); + }); + + it('rejects an invalid password', async () => { + const passwordHash = await hashPassword('secret-password'); + query.mockResolvedValueOnce({ + rows: [{ id: 2, username: 'bob', password_hash: passwordHash, role: 'admin' }], + }); + + await expect(authenticateUser('bob', 'wrong-password')).resolves.toBeNull(); + }); +}); diff --git a/tests/db-init-route.test.ts b/tests/db-init-route.test.ts new file mode 100644 index 0000000..69b590c --- /dev/null +++ b/tests/db-init-route.test.ts @@ -0,0 +1,55 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { POST } from '@/app/api/db/init/route'; + +const { initSchema, seed, resetDatabase, migrateFromLegacyPlaces } = vi.hoisted(() => ({ + initSchema: vi.fn(), + seed: vi.fn(), + resetDatabase: vi.fn(), + migrateFromLegacyPlaces: vi.fn(), +})); + +vi.mock('@/lib/schema', () => ({ + initSchema, + seed, + resetDatabase, + migrateFromLegacyPlaces, +})); + +describe('/api/db/init', () => { + beforeEach(() => { + initSchema.mockReset(); + seed.mockReset(); + resetDatabase.mockReset(); + migrateFromLegacyPlaces.mockReset(); + }); + + it('initializes schema and seed data', async () => { + initSchema.mockResolvedValueOnce(undefined); + migrateFromLegacyPlaces.mockResolvedValueOnce(undefined); + seed.mockResolvedValueOnce(undefined); + + const response = await POST(new Request('http://localhost/api/db/init')); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ ok: true }); + expect(initSchema).toHaveBeenCalledTimes(1); + expect(migrateFromLegacyPlaces).toHaveBeenCalledTimes(1); + expect(seed).toHaveBeenCalledTimes(1); + expect(resetDatabase).not.toHaveBeenCalled(); + }); + + it('uses the reset path when requested', async () => { + resetDatabase.mockResolvedValueOnce(undefined); + + const response = await POST(new Request('http://localhost/api/db/init?reset=true')); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ ok: true, reset: true }); + expect(resetDatabase).toHaveBeenCalledTimes(1); + expect(initSchema).not.toHaveBeenCalled(); + expect(seed).not.toHaveBeenCalled(); + expect(migrateFromLegacyPlaces).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/health-route.test.ts b/tests/health-route.test.ts new file mode 100644 index 0000000..ec32f0e --- /dev/null +++ b/tests/health-route.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from 'vitest'; +import { GET } from '@/app/api/health/route'; + +describe('/api/health', () => { + it('returns a fast healthy response', async () => { + const response = await GET(); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body.ok).toBe(true); + expect(body.status).toBe('healthy'); + expect(response.headers.get('cache-control')).toContain('no-store'); + }); +}); diff --git a/tests/language-switcher.test.tsx b/tests/language-switcher.test.tsx new file mode 100644 index 0000000..83fe682 --- /dev/null +++ b/tests/language-switcher.test.tsx @@ -0,0 +1,21 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { fireEvent, render, screen } from '@testing-library/react'; +import LanguageSwitcher from '@/components/LanguageSwitcher'; + +describe('LanguageSwitcher', () => { + beforeEach(() => { + localStorage.clear(); + }); + + it('switches language without reloading the page', () => { + localStorage.setItem('language', 'en'); + + render(); + + const button = screen.getByRole('button', { name: 'Switch to Spanish' }); + fireEvent.click(button); + + expect(localStorage.getItem('language')).toBe('es'); + expect(screen.getByRole('button', { name: 'Switch to English' })).toBeInTheDocument(); + }); +}); diff --git a/tests/rooms-route.test.ts b/tests/rooms-route.test.ts new file mode 100644 index 0000000..e869567 --- /dev/null +++ b/tests/rooms-route.test.ts @@ -0,0 +1,57 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { GET, POST } from '@/app/api/rooms/route'; + +const { query, requireRole } = vi.hoisted(() => ({ query: vi.fn(), requireRole: vi.fn() })); + +vi.mock('@/lib/db', () => ({ + db: { + query, + }, +})); + +vi.mock('@/lib/auth', () => ({ + requireRole, +})); + +describe('/api/rooms', () => { + beforeEach(() => { + query.mockReset(); + requireRole.mockReset(); + }); + + it('returns active rooms', async () => { + query.mockResolvedValueOnce({ rows: [{ id: 1, name: 'Suite', active: true }] }); + + const response = await GET(); + const body = await response.json(); + + expect(response.status).toBe(200); + expect(body).toEqual({ data: [{ id: 1, name: 'Suite', active: true }] }); + }); + + it('creates a room for admins', async () => { + requireRole.mockResolvedValueOnce({ id: 1, username: 'admin', role: 'admin' }); + query.mockResolvedValueOnce({ rows: [{ id: 7, name: 'Garden Room', price: '120.00' }] }); + + const response = await POST( + new Request('http://localhost/api/rooms', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + name: 'Garden Room', + description: 'Nice view', + price: '120.00', + photos: ['a.jpg'], + sort_order: 1, + featured_photo: 'a.jpg', + }), + }) as any + ); + + const body = await response.json(); + + expect(requireRole).toHaveBeenCalledWith('admin'); + expect(response.status).toBe(200); + expect(body).toEqual({ data: { id: 7, name: 'Garden Room', price: '120.00' } }); + }); +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..72d5777 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import { fileURLToPath } from 'node:url'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: ['./vitest.setup.ts'], + css: false, + }, + resolve: { + alias: { + '@': fileURLToPath(new URL('./src', import.meta.url)), + }, + }, +}); diff --git a/vitest.setup.ts b/vitest.setup.ts new file mode 100644 index 0000000..bb02c60 --- /dev/null +++ b/vitest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom/vitest';