From 789d2bf0a65d2be77ae206ad17fae3f822f647ea Mon Sep 17 00:00:00 2001 From: Dianaka123 Date: Wed, 25 Feb 2026 22:46:50 +0300 Subject: [PATCH] Full app rebuild: FastAPI backend + React Native mobile with auth, championships, admin Backend (FastAPI + SQLAlchemy + SQLite): - JWT auth with access/refresh tokens, bcrypt password hashing - User model with member/organizer/admin roles, auto-approve members - Championship, Registration, ParticipantList, Notification models - Alembic async migrations, seed data with test users - Registration endpoint returns tokens for members, pending for organizers - /registrations/my returns championship title/date/location via eager loading - Admin endpoints: list users, approve/reject organizers Mobile (React Native + Expo + TypeScript): - Zustand auth store, Axios client with token refresh interceptor - Role-based registration (Member vs Organizer) with contextual form labels - Tab navigation with Ionicons, safe area headers, admin tab for admin role - Championships list with status badges, detail screen with registration progress - My Registrations with championship title, progress bar, and tap-to-navigate - Admin panel with pending/all filter, approve/reject with confirmation - Profile screen with role badge, Ionicons info rows, sign out - Password visibility toggle (Ionicons), keyboard flow hints (returnKeyType) Co-Authored-By: Claude Opus 4.6 --- .env.example | 19 + .gitignore | 24 + MANUAL_TESTS.md | 159 + backend/Dockerfile | 10 + backend/alembic.ini | 38 + backend/alembic/env.py | 59 + backend/alembic/script.py.mako | 26 + ...947192af5_add_organizer_fields_to_users.py | 32 + .../versions/657f22c8aa55_initial_schema.py | 125 + backend/app/__init__.py | 0 backend/app/config.py | 28 + backend/app/crud/__init__.py | 3 + backend/app/crud/crud_championship.py | 63 + backend/app/crud/crud_participant.py | 34 + backend/app/crud/crud_registration.py | 86 + backend/app/crud/crud_user.py | 57 + backend/app/database.py | 17 + backend/app/dependencies.py | 42 + backend/app/main.py | 41 + backend/app/models/__init__.py | 14 + backend/app/models/championship.py | 42 + backend/app/models/notification.py | 27 + backend/app/models/participant.py | 25 + backend/app/models/registration.py | 36 + backend/app/models/user.py | 44 + backend/app/routers/__init__.py | 3 + backend/app/routers/auth.py | 79 + backend/app/routers/championships.py | 69 + backend/app/routers/participant_lists.py | 55 + backend/app/routers/registrations.py | 108 + backend/app/routers/users.py | 46 + backend/app/schemas/__init__.py | 0 backend/app/schemas/auth.py | 36 + backend/app/schemas/championship.py | 73 + backend/app/schemas/participant.py | 19 + backend/app/schemas/registration.py | 57 + backend/app/schemas/user.py | 52 + backend/app/services/__init__.py | 0 backend/app/services/auth_service.py | 81 + backend/requirements.txt | 18 + backend/seed.py | 134 + backend/tests/__init__.py | 0 dance-champ-app.md | 310 - dancechamp-claude-code/CLAUDE.md | 176 + dancechamp-claude-code/docs/DATABASE.md | 357 + dancechamp-claude-code/docs/DESIGN-SYSTEM.md | 258 + dancechamp-claude-code/docs/PLAN.md | 316 + dancechamp-claude-code/docs/SCREENS.md | 371 + dancechamp-claude-code/docs/SPEC.md | 267 + .../prototypes/admin-panel.jsx | 517 + .../prototypes/member-app.jsx | 643 ++ dancechamp-claude-code/prototypes/org-app.jsx | 683 ++ docker-compose.yml | 36 + mobile/.gitignore | 41 + mobile/App.tsx | 19 + mobile/app.json | 28 + mobile/assets/adaptive-icon.png | Bin 0 -> 17547 bytes mobile/assets/favicon.png | Bin 0 -> 1466 bytes mobile/assets/icon.png | Bin 0 -> 22380 bytes mobile/assets/splash-icon.png | Bin 0 -> 17547 bytes mobile/index.ts | 8 + mobile/package-lock.json | 8705 +++++++++++++++++ mobile/package.json | 34 + mobile/src/api/auth.ts | 33 + mobile/src/api/championships.ts | 25 + mobile/src/api/client.ts | 73 + mobile/src/api/users.ts | 12 + mobile/src/navigation/index.tsx | 114 + mobile/src/screens/admin/AdminScreen.tsx | 290 + mobile/src/screens/auth/LoginScreen.tsx | 128 + .../screens/auth/PendingApprovalScreen.tsx | 36 + mobile/src/screens/auth/RegisterScreen.tsx | 297 + .../ChampionshipDetailScreen.tsx | 246 + .../championships/ChampionshipsScreen.tsx | 135 + .../championships/MyRegistrationsScreen.tsx | 189 + mobile/src/screens/profile/ProfileScreen.tsx | 149 + mobile/src/store/auth.store.ts | 94 + mobile/src/types/index.ts | 63 + mobile/src/utils/tokenStorage.ts | 49 + mobile/tsconfig.json | 6 + start-backend.bat | 4 + 81 files changed, 16283 insertions(+), 310 deletions(-) create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 MANUAL_TESTS.md create mode 100644 backend/Dockerfile create mode 100644 backend/alembic.ini create mode 100644 backend/alembic/env.py create mode 100644 backend/alembic/script.py.mako create mode 100644 backend/alembic/versions/43d947192af5_add_organizer_fields_to_users.py create mode 100644 backend/alembic/versions/657f22c8aa55_initial_schema.py create mode 100644 backend/app/__init__.py create mode 100644 backend/app/config.py create mode 100644 backend/app/crud/__init__.py create mode 100644 backend/app/crud/crud_championship.py create mode 100644 backend/app/crud/crud_participant.py create mode 100644 backend/app/crud/crud_registration.py create mode 100644 backend/app/crud/crud_user.py create mode 100644 backend/app/database.py create mode 100644 backend/app/dependencies.py create mode 100644 backend/app/main.py create mode 100644 backend/app/models/__init__.py create mode 100644 backend/app/models/championship.py create mode 100644 backend/app/models/notification.py create mode 100644 backend/app/models/participant.py create mode 100644 backend/app/models/registration.py create mode 100644 backend/app/models/user.py create mode 100644 backend/app/routers/__init__.py create mode 100644 backend/app/routers/auth.py create mode 100644 backend/app/routers/championships.py create mode 100644 backend/app/routers/participant_lists.py create mode 100644 backend/app/routers/registrations.py create mode 100644 backend/app/routers/users.py create mode 100644 backend/app/schemas/__init__.py create mode 100644 backend/app/schemas/auth.py create mode 100644 backend/app/schemas/championship.py create mode 100644 backend/app/schemas/participant.py create mode 100644 backend/app/schemas/registration.py create mode 100644 backend/app/schemas/user.py create mode 100644 backend/app/services/__init__.py create mode 100644 backend/app/services/auth_service.py create mode 100644 backend/requirements.txt create mode 100644 backend/seed.py create mode 100644 backend/tests/__init__.py delete mode 100644 dance-champ-app.md create mode 100644 dancechamp-claude-code/CLAUDE.md create mode 100644 dancechamp-claude-code/docs/DATABASE.md create mode 100644 dancechamp-claude-code/docs/DESIGN-SYSTEM.md create mode 100644 dancechamp-claude-code/docs/PLAN.md create mode 100644 dancechamp-claude-code/docs/SCREENS.md create mode 100644 dancechamp-claude-code/docs/SPEC.md create mode 100644 dancechamp-claude-code/prototypes/admin-panel.jsx create mode 100644 dancechamp-claude-code/prototypes/member-app.jsx create mode 100644 dancechamp-claude-code/prototypes/org-app.jsx create mode 100644 docker-compose.yml create mode 100644 mobile/.gitignore create mode 100644 mobile/App.tsx create mode 100644 mobile/app.json create mode 100644 mobile/assets/adaptive-icon.png create mode 100644 mobile/assets/favicon.png create mode 100644 mobile/assets/icon.png create mode 100644 mobile/assets/splash-icon.png create mode 100644 mobile/index.ts create mode 100644 mobile/package-lock.json create mode 100644 mobile/package.json create mode 100644 mobile/src/api/auth.ts create mode 100644 mobile/src/api/championships.ts create mode 100644 mobile/src/api/client.ts create mode 100644 mobile/src/api/users.ts create mode 100644 mobile/src/navigation/index.tsx create mode 100644 mobile/src/screens/admin/AdminScreen.tsx create mode 100644 mobile/src/screens/auth/LoginScreen.tsx create mode 100644 mobile/src/screens/auth/PendingApprovalScreen.tsx create mode 100644 mobile/src/screens/auth/RegisterScreen.tsx create mode 100644 mobile/src/screens/championships/ChampionshipDetailScreen.tsx create mode 100644 mobile/src/screens/championships/ChampionshipsScreen.tsx create mode 100644 mobile/src/screens/championships/MyRegistrationsScreen.tsx create mode 100644 mobile/src/screens/profile/ProfileScreen.tsx create mode 100644 mobile/src/store/auth.store.ts create mode 100644 mobile/src/types/index.ts create mode 100644 mobile/src/utils/tokenStorage.ts create mode 100644 mobile/tsconfig.json create mode 100644 start-backend.bat diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a1b7c27 --- /dev/null +++ b/.env.example @@ -0,0 +1,19 @@ +# Database +DATABASE_URL=postgresql+asyncpg://pole:pole@localhost:5432/poledance + +# JWT +SECRET_KEY=change-me-to-a-random-32-char-string +ALGORITHM=HS256 +ACCESS_TOKEN_EXPIRE_MINUTES=15 +REFRESH_TOKEN_EXPIRE_DAYS=7 + +# Instagram (optional — needed for Phase 6) +INSTAGRAM_USER_ID= +INSTAGRAM_ACCESS_TOKEN= +INSTAGRAM_POLL_INTERVAL=1800 + +# Expo Push +EXPO_ACCESS_TOKEN= + +# CORS +CORS_ORIGINS=http://localhost:8081,exp:// diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bf27b4b --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python +__pycache__/ +*.pyc +*.pyo +backend/.venv/ +*.db + +# Env +.env +backend/.env + +# Logs +server.log + +# IDE +.vscode/ +.idea/ + +# OS +.DS_Store +Thumbs.db + +# Claude +.claude/ diff --git a/MANUAL_TESTS.md b/MANUAL_TESTS.md new file mode 100644 index 0000000..827af2d --- /dev/null +++ b/MANUAL_TESTS.md @@ -0,0 +1,159 @@ +# Manual Test Tracker — Pole Championships App + +Mark each item with ✅ PASS / ❌ FAIL / ⏭ SKIP + +--- + +## Setup + +| # | Action | Expected | Result | +|---|--------|----------|--------| +| S1 | Backend running at `http://localhost:8000` | `{"status":"ok"}` at `/internal/health` | | +| S2 | Expo app loaded on phone | Login screen visible | | +| S3 | Seed data present | Run `backend/.venv/Scripts/python seed.py` — no errors | | + +--- + +## 1. Registration (new account) + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 1.1 | Tap "Don't have an account? Register" | Register screen opens | | | +| 1.2 | Submit form with all fields empty | Error: "fill in all required fields" | | | +| 1.3 | Fill: Name=`Test User`, Email=`test@test.com`, Password=`Test1234` → Submit | "Application Submitted" screen shown | | | +| 1.4 | Tap "Go to Sign In" | Login screen shown | | | +| 1.5 | Try to register same email again | Error: "Email already registered" | | | + +--- + +## 2. Login — Pending account (cannot access app) + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 2.1 | Login with `pending@pole.dev` / `Pending1234` | Login succeeds but stays on auth screen (status=pending) | | | +| 2.2 | Login with wrong password `pending@pole.dev` / `wrongpass` | Error: "Invalid credentials" | | | +| 2.3 | Login with non-existent email | Error: "Invalid credentials" | | | + +--- + +## 3. Login — Approved member + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 3.1 | Login with `member@pole.dev` / `Member1234` | App opens, Championships tab visible | | | +| 3.2 | Championships list loads | 2 championships shown (Spring Open 2026, Summer Championship 2026) | | | +| 3.3 | Spring Open 2026 shows green OPEN badge | Green badge visible | | | +| 3.4 | Summer Championship 2026 shows grey DRAFT badge | Grey badge visible | | | +| 3.5 | Spring Open shows entry fee 50 BYN | "💰 Entry fee: 50 BYN" visible | | | +| 3.6 | Spring Open shows location | "📍 Cultural Center, Minsk" visible | | | +| 3.7 | Spring Open shows image | Banner image loads | | | + +--- + +## 4. Championship Detail + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 4.1 | Tap "Spring Open 2026" | Detail screen opens | | | +| 4.2 | Detail shows title, location, date | All fields visible | | | +| 4.3 | Categories shown: Novice, Amateur, Professional | 3 purple tags visible | | | +| 4.4 | Judges section shows 2 judges | Oksana Ivanova + Marta Sokolova | | | +| 4.5 | "Open Registration Form ↗" button visible | Tapping opens link in browser | | | +| 4.6 | "Register for Championship" button visible | Button at bottom | | | +| 4.7 | Tap "Summer Championship 2026" | Detail opens, no Register button (status=draft) | | | + +--- + +## 5. Registration Flow + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 5.1 | On Spring Open detail, tap "Register for Championship" | Confirmation alert appears | | | +| 5.2 | Tap Cancel in alert | Alert dismisses, no registration created | | | +| 5.3 | Tap Register → confirm | Success alert shown | | | +| 5.4 | After registering: progress steps appear | "Application submitted" step shown as green | | | +| 5.5 | "Register for Championship" button disappears | No duplicate registration possible | | | +| 5.6 | Go back, tap Spring Open again | Progress steps still visible | | | +| 5.7 | Try registering for same championship again (via API or re-tap) | Error: "Already registered" | | | + +--- + +## 6. My Registrations Tab + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 6.1 | Tap "My Registrations" tab | Screen opens | | | +| 6.2 | Registration for Spring Open visible | Shows reg ID + SUBMITTED badge (yellow) | | | +| 6.3 | Pull to refresh | List refreshes | | | +| 6.4 | Login as `pending@pole.dev` | My Registrations shows empty | | | + +--- + +## 7. Profile Tab + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 7.1 | Tap Profile tab | Profile screen opens | | | +| 7.2 | Shows full name, email, role=member, status=approved | All correct | | | +| 7.3 | Avatar shows first letter of name | "A" for Anna Petrova | | | +| 7.4 | Tap "Sign Out" | Confirmation alert appears | | | +| 7.5 | Confirm sign out | Redirected to Login screen | | | +| 7.6 | After logout, reopen app | Login screen shown (not auto-logged in) | | | + +--- + +## 8. Token Persistence (session restore) + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 8.1 | Login as member, close Expo Go completely | — | | | +| 8.2 | Reopen Expo Go, scan QR | App opens directly on Championships (no re-login) | | | +| 8.3 | Restart backend server while logged in | App still works after backend restarts | | | + +--- + +## 9. Admin user (via API — no admin UI yet) + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 9.1 | Login as `admin@pole.dev` / `Admin1234` | App opens (role=admin, status=approved) | | | +| 9.2 | Championships visible | Same list as member | | | +| 9.3 | Approve pending user via API:
`curl -X PATCH http://localhost:8000/api/v1/users//approve -H "Authorization: Bearer "` | Returns `status: approved` | | | + +--- + +## 10. Edge Cases & Error Handling + +| # | Action | Expected | Result | Notes | +|---|--------|----------|--------|-------| +| 10.1 | Stop backend server, open app | Championships shows "Failed to load championships" | | | +| 10.2 | Stop backend, try login | Login shows error message (not crash) | | | +| 10.3 | Pull to refresh on Championships | Spinner appears, list reloads | | | +| 10.4 | Very slow network (throttle in phone settings) | Loading spinners show while waiting | | | + +--- + +## Summary + +| Section | Total | Pass | Fail | Skip | +|---------|-------|------|------|------| +| 1. Registration | 5 | | | | +| 2. Login — Pending | 3 | | | | +| 3. Login — Member | 7 | | | | +| 4. Championship Detail | 7 | | | | +| 5. Registration Flow | 7 | | | | +| 6. My Registrations | 4 | | | | +| 7. Profile | 6 | | | | +| 8. Session Restore | 3 | | | | +| 9. Admin | 3 | | | | +| 10. Edge Cases | 4 | | | | +| **Total** | **49** | | | | + +--- + +## Known Limitations (not bugs) + +- No admin UI — user approval done via API only +- Organizer role has no extra UI in the mobile app yet +- Push notifications not implemented +- Instagram sync not implemented (requires credentials) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..534cbc6 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,10 @@ +FROM python:3.12-slim + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY . . + +CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000"] diff --git a/backend/alembic.ini b/backend/alembic.ini new file mode 100644 index 0000000..b20fe7a --- /dev/null +++ b/backend/alembic.ini @@ -0,0 +1,38 @@ +[alembic] +script_location = alembic +prepend_sys_path = . +sqlalchemy.url = postgresql+asyncpg://pole:pole@localhost:5432/poledance + +[loggers] +keys = root,sqlalchemy,alembic + +[handlers] +keys = console + +[formatters] +keys = generic + +[logger_root] +level = WARN +handlers = console +qualname = + +[logger_sqlalchemy] +level = WARN +handlers = +qualname = sqlalchemy.engine + +[logger_alembic] +level = INFO +handlers = +qualname = alembic + +[handler_console] +class = StreamHandler +args = (sys.stderr,) +level = NOTSET +formatter = generic + +[formatter_generic] +format = %(levelname)-5.5s [%(name)s] %(message)s +datefmt = %H:%M:%S diff --git a/backend/alembic/env.py b/backend/alembic/env.py new file mode 100644 index 0000000..d04d28d --- /dev/null +++ b/backend/alembic/env.py @@ -0,0 +1,59 @@ +import asyncio +from logging.config import fileConfig + +from alembic import context +from sqlalchemy import pool +from sqlalchemy.ext.asyncio import async_engine_from_config + +from app.config import settings +from app.database import Base + +# Import all models so they are registered on Base.metadata +import app.models # noqa: F401 + +config = context.config +config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + +if config.config_file_name is not None: + fileConfig(config.config_file_name) + +target_metadata = Base.metadata + + +def run_migrations_offline() -> None: + url = config.get_main_option("sqlalchemy.url") + context.configure( + url=url, + target_metadata=target_metadata, + literal_binds=True, + dialect_opts={"paramstyle": "named"}, + ) + with context.begin_transaction(): + context.run_migrations() + + +def do_run_migrations(connection): + context.configure(connection=connection, target_metadata=target_metadata) + with context.begin_transaction(): + context.run_migrations() + + +async def run_async_migrations() -> None: + connectable = async_engine_from_config( + config.get_section(config.config_ini_section, {}), + prefix="sqlalchemy.", + poolclass=pool.NullPool, + ) + async with connectable.connect() as connection: + await connection.run_sync(do_run_migrations) + await connectable.dispose() + + +def run_migrations_online() -> None: + asyncio.run(run_async_migrations()) + + +if context.is_offline_mode(): + run_migrations_offline() +else: + run_migrations_online() diff --git a/backend/alembic/script.py.mako b/backend/alembic/script.py.mako new file mode 100644 index 0000000..fbc4b07 --- /dev/null +++ b/backend/alembic/script.py.mako @@ -0,0 +1,26 @@ +"""${message} + +Revision ID: ${up_revision} +Revises: ${down_revision | comma,n} +Create Date: ${create_date} + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa +${imports if imports else ""} + +# revision identifiers, used by Alembic. +revision: str = ${repr(up_revision)} +down_revision: Union[str, None] = ${repr(down_revision)} +branch_labels: Union[str, Sequence[str], None] = ${repr(branch_labels)} +depends_on: Union[str, Sequence[str], None] = ${repr(depends_on)} + + +def upgrade() -> None: + ${upgrades if upgrades else "pass"} + + +def downgrade() -> None: + ${downgrades if downgrades else "pass"} diff --git a/backend/alembic/versions/43d947192af5_add_organizer_fields_to_users.py b/backend/alembic/versions/43d947192af5_add_organizer_fields_to_users.py new file mode 100644 index 0000000..85be1fb --- /dev/null +++ b/backend/alembic/versions/43d947192af5_add_organizer_fields_to_users.py @@ -0,0 +1,32 @@ +"""add organizer fields to users + +Revision ID: 43d947192af5 +Revises: 657f22c8aa55 +Create Date: 2026-02-25 21:18:04.707870 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '43d947192af5' +down_revision: Union[str, None] = '657f22c8aa55' +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('users', sa.Column('organization_name', sa.String(length=255), nullable=True)) + op.add_column('users', sa.Column('instagram_handle', sa.String(length=100), nullable=True)) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_column('users', 'instagram_handle') + op.drop_column('users', 'organization_name') + # ### end Alembic commands ### diff --git a/backend/alembic/versions/657f22c8aa55_initial_schema.py b/backend/alembic/versions/657f22c8aa55_initial_schema.py new file mode 100644 index 0000000..f241ea0 --- /dev/null +++ b/backend/alembic/versions/657f22c8aa55_initial_schema.py @@ -0,0 +1,125 @@ +"""initial schema + +Revision ID: 657f22c8aa55 +Revises: +Create Date: 2026-02-25 00:23:12.480733 + +""" +from typing import Sequence, Union + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision: str = '657f22c8aa55' +down_revision: Union[str, None] = None +branch_labels: Union[str, Sequence[str], None] = None +depends_on: Union[str, Sequence[str], None] = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.create_table('championships', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('location', sa.String(length=500), nullable=True), + sa.Column('event_date', sa.DateTime(timezone=True), nullable=True), + sa.Column('registration_open_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('registration_close_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('form_url', sa.String(length=2048), nullable=True), + sa.Column('entry_fee', sa.Float(), nullable=True), + sa.Column('video_max_duration', sa.Integer(), nullable=True), + sa.Column('judges', sa.Text(), nullable=True), + sa.Column('categories', sa.Text(), nullable=True), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('source', sa.String(length=20), nullable=False), + sa.Column('instagram_media_id', sa.String(length=255), nullable=True), + sa.Column('image_url', sa.String(length=2048), nullable=True), + sa.Column('raw_caption_text', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('instagram_media_id') + ) + op.create_table('users', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('email', sa.String(length=255), nullable=False), + sa.Column('hashed_password', sa.String(length=255), nullable=False), + sa.Column('full_name', sa.String(length=255), nullable=False), + sa.Column('phone', sa.String(length=50), nullable=True), + sa.Column('role', sa.String(length=20), nullable=False), + sa.Column('status', sa.String(length=20), nullable=False), + sa.Column('expo_push_token', sa.String(length=255), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('updated_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_users_email'), 'users', ['email'], unique=True) + op.create_table('participant_lists', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('championship_id', sa.Uuid(), nullable=False), + sa.Column('published_by', sa.Uuid(), nullable=True), + sa.Column('is_published', sa.Boolean(), nullable=False), + sa.Column('published_at', sa.DateTime(timezone=True), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('created_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.ForeignKeyConstraint(['championship_id'], ['championships.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['published_by'], ['users.id'], ondelete='SET NULL'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('championship_id') + ) + op.create_table('refresh_tokens', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('token_hash', sa.String(length=64), nullable=False), + sa.Column('expires_at', sa.DateTime(timezone=True), nullable=False), + sa.Column('revoked', sa.Boolean(), nullable=False), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + op.create_index(op.f('ix_refresh_tokens_token_hash'), 'refresh_tokens', ['token_hash'], unique=False) + op.create_table('registrations', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('championship_id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('category', sa.String(length=255), nullable=True), + sa.Column('level', sa.String(length=100), nullable=True), + sa.Column('notes', sa.Text(), nullable=True), + sa.Column('status', sa.String(length=30), nullable=False), + sa.Column('video_url', sa.String(length=2048), nullable=True), + sa.Column('submitted_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('decided_at', sa.DateTime(timezone=True), nullable=True), + sa.ForeignKeyConstraint(['championship_id'], ['championships.id'], ondelete='CASCADE'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('championship_id', 'user_id', name='uq_registration_champ_user') + ) + op.create_table('notification_log', + sa.Column('id', sa.Uuid(), nullable=False), + sa.Column('user_id', sa.Uuid(), nullable=False), + sa.Column('registration_id', sa.Uuid(), nullable=True), + sa.Column('type', sa.String(length=50), nullable=False), + sa.Column('title', sa.String(length=255), nullable=False), + sa.Column('body', sa.Text(), nullable=False), + sa.Column('sent_at', sa.DateTime(timezone=True), server_default=sa.text('(CURRENT_TIMESTAMP)'), nullable=False), + sa.Column('delivery_status', sa.String(length=20), nullable=False), + sa.ForeignKeyConstraint(['registration_id'], ['registrations.id'], ondelete='SET NULL'), + sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'), + sa.PrimaryKeyConstraint('id') + ) + # ### end Alembic commands ### + + +def downgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table('notification_log') + op.drop_table('registrations') + op.drop_index(op.f('ix_refresh_tokens_token_hash'), table_name='refresh_tokens') + op.drop_table('refresh_tokens') + op.drop_table('participant_lists') + op.drop_index(op.f('ix_users_email'), table_name='users') + op.drop_table('users') + op.drop_table('championships') + # ### end Alembic commands ### diff --git a/backend/app/__init__.py b/backend/app/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/config.py b/backend/app/config.py new file mode 100644 index 0000000..95719b4 --- /dev/null +++ b/backend/app/config.py @@ -0,0 +1,28 @@ +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") + + # Default: SQLite for local dev. Set DATABASE_URL=postgresql+asyncpg://... for production. + DATABASE_URL: str = "sqlite+aiosqlite:///./poledance.db" + + SECRET_KEY: str = "dev-secret-change-in-production" + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 15 + REFRESH_TOKEN_EXPIRE_DAYS: int = 7 + + INSTAGRAM_USER_ID: str = "" + INSTAGRAM_ACCESS_TOKEN: str = "" + INSTAGRAM_POLL_INTERVAL: int = 1800 + + EXPO_ACCESS_TOKEN: str = "" + + CORS_ORIGINS: str = "http://localhost:8081,exp://" + + @property + def cors_origins_list(self) -> list[str]: + return [o.strip() for o in self.CORS_ORIGINS.split(",") if o.strip()] + + +settings = Settings() diff --git a/backend/app/crud/__init__.py b/backend/app/crud/__init__.py new file mode 100644 index 0000000..02cf482 --- /dev/null +++ b/backend/app/crud/__init__.py @@ -0,0 +1,3 @@ +from app.crud import crud_user, crud_championship, crud_registration, crud_participant + +__all__ = ["crud_user", "crud_championship", "crud_registration", "crud_participant"] diff --git a/backend/app/crud/crud_championship.py b/backend/app/crud/crud_championship.py new file mode 100644 index 0000000..b8770ab --- /dev/null +++ b/backend/app/crud/crud_championship.py @@ -0,0 +1,63 @@ +import json +import uuid + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.championship import Championship +from app.schemas.championship import ChampionshipCreate, ChampionshipUpdate + + +def _serialize(value) -> str | None: + if value is None: + return None + return json.dumps(value) + + +async def get(db: AsyncSession, champ_id: str | uuid.UUID) -> Championship | None: + cid = champ_id if isinstance(champ_id, uuid.UUID) else uuid.UUID(str(champ_id)) + result = await db.execute(select(Championship).where(Championship.id == cid)) + return result.scalar_one_or_none() + + +async def list_all( + db: AsyncSession, + status: str | None = None, + skip: int = 0, + limit: int = 50, +) -> list[Championship]: + q = select(Championship).order_by(Championship.event_date.asc()) + if status: + q = q.where(Championship.status == status) + q = q.offset(skip).limit(limit) + result = await db.execute(q) + return list(result.scalars().all()) + + +async def create(db: AsyncSession, data: ChampionshipCreate) -> Championship: + payload = data.model_dump(exclude={"judges", "categories"}) + payload["judges"] = _serialize(data.judges) + payload["categories"] = _serialize(data.categories) + champ = Championship(**payload) + db.add(champ) + await db.commit() + await db.refresh(champ) + return champ + + +async def update(db: AsyncSession, champ: Championship, data: ChampionshipUpdate) -> Championship: + raw = data.model_dump(exclude_none=True, exclude={"judges", "categories"}) + for field, value in raw.items(): + setattr(champ, field, value) + if data.judges is not None: + champ.judges = _serialize(data.judges) + if data.categories is not None: + champ.categories = _serialize(data.categories) + await db.commit() + await db.refresh(champ) + return champ + + +async def delete(db: AsyncSession, champ: Championship) -> None: + await db.delete(champ) + await db.commit() diff --git a/backend/app/crud/crud_participant.py b/backend/app/crud/crud_participant.py new file mode 100644 index 0000000..c0666f7 --- /dev/null +++ b/backend/app/crud/crud_participant.py @@ -0,0 +1,34 @@ +import uuid +from datetime import UTC, datetime + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.participant import ParticipantList + + +async def get_for_championship(db: AsyncSession, championship_id: str | uuid.UUID) -> ParticipantList | None: + cid = championship_id if isinstance(championship_id, uuid.UUID) else uuid.UUID(str(championship_id)) + result = await db.execute(select(ParticipantList).where(ParticipantList.championship_id == cid)) + return result.scalar_one_or_none() + + +async def create_or_get(db: AsyncSession, championship_id: uuid.UUID, published_by: uuid.UUID) -> ParticipantList: + existing = await get_for_championship(db, championship_id) + if existing: + return existing + pl = ParticipantList(championship_id=championship_id, published_by=published_by) + db.add(pl) + await db.commit() + await db.refresh(pl) + return pl + + +async def publish(db: AsyncSession, pl: ParticipantList, notes: str | None = None) -> ParticipantList: + pl.is_published = True + pl.published_at = datetime.now(UTC) + if notes is not None: + pl.notes = notes + await db.commit() + await db.refresh(pl) + return pl diff --git a/backend/app/crud/crud_registration.py b/backend/app/crud/crud_registration.py new file mode 100644 index 0000000..88ef4a7 --- /dev/null +++ b/backend/app/crud/crud_registration.py @@ -0,0 +1,86 @@ +import uuid +from datetime import UTC, datetime + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import selectinload + +from app.models.registration import Registration +from app.schemas.registration import RegistrationCreate, RegistrationUpdate + + +async def get(db: AsyncSession, reg_id: str | uuid.UUID) -> Registration | None: + rid = reg_id if isinstance(reg_id, uuid.UUID) else uuid.UUID(str(reg_id)) + result = await db.execute( + select(Registration).where(Registration.id == rid).options(selectinload(Registration.user)) + ) + return result.scalar_one_or_none() + + +async def get_by_user_and_championship( + db: AsyncSession, user_id: uuid.UUID, championship_id: uuid.UUID +) -> Registration | None: + result = await db.execute( + select(Registration).where( + Registration.user_id == user_id, + Registration.championship_id == championship_id, + ) + ) + return result.scalar_one_or_none() + + +async def list_for_championship( + db: AsyncSession, championship_id: str | uuid.UUID, skip: int = 0, limit: int = 100 +) -> list[Registration]: + cid = championship_id if isinstance(championship_id, uuid.UUID) else uuid.UUID(str(championship_id)) + result = await db.execute( + select(Registration) + .where(Registration.championship_id == cid) + .options(selectinload(Registration.user)) + .offset(skip) + .limit(limit) + ) + return list(result.scalars().all()) + + +async def list_for_user(db: AsyncSession, user_id: uuid.UUID, skip: int = 0, limit: int = 50) -> list[Registration]: + result = await db.execute( + select(Registration) + .where(Registration.user_id == user_id) + .options(selectinload(Registration.championship)) + .order_by(Registration.submitted_at.desc()) + .offset(skip) + .limit(limit) + ) + return list(result.scalars().all()) + + +async def create(db: AsyncSession, user_id: uuid.UUID, data: RegistrationCreate) -> Registration: + reg = Registration( + championship_id=data.championship_id, + user_id=user_id, + category=data.category, + level=data.level, + notes=data.notes, + status="submitted", + ) + db.add(reg) + await db.commit() + await db.refresh(reg) + return reg + + +async def update(db: AsyncSession, reg: Registration, data: RegistrationUpdate) -> Registration: + raw = data.model_dump(exclude_none=True) + for field, value in raw.items(): + setattr(reg, field, value) + if "status" in raw and raw["status"] in ("accepted", "rejected", "waitlisted"): + reg.decided_at = datetime.now(UTC) + await db.commit() + await db.refresh(reg) + return reg + + +async def delete(db: AsyncSession, reg: Registration) -> None: + await db.delete(reg) + await db.commit() diff --git a/backend/app/crud/crud_user.py b/backend/app/crud/crud_user.py new file mode 100644 index 0000000..4ee8ede --- /dev/null +++ b/backend/app/crud/crud_user.py @@ -0,0 +1,57 @@ +import uuid + +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + +from app.models.user import User +from app.schemas.user import UserRegister, UserUpdate +from app.services.auth_service import hash_password + + +async def get_by_id(db: AsyncSession, user_id: str | uuid.UUID) -> User | None: + uid = user_id if isinstance(user_id, uuid.UUID) else uuid.UUID(str(user_id)) + result = await db.execute(select(User).where(User.id == uid)) + return result.scalar_one_or_none() + + +async def get_by_email(db: AsyncSession, email: str) -> User | None: + result = await db.execute(select(User).where(User.email == email.lower())) + return result.scalar_one_or_none() + + +async def create(db: AsyncSession, data: UserRegister) -> User: + user = User( + email=data.email.lower(), + hashed_password=hash_password(data.password), + full_name=data.full_name, + phone=data.phone, + role=data.requested_role, + organization_name=data.organization_name, + instagram_handle=data.instagram_handle, + # Members are auto-approved; organizers require admin review + status="approved" if data.requested_role == "member" else "pending", + ) + db.add(user) + await db.commit() + await db.refresh(user) + return user + + +async def update(db: AsyncSession, user: User, data: UserUpdate) -> User: + for field, value in data.model_dump(exclude_none=True).items(): + setattr(user, field, value) + await db.commit() + await db.refresh(user) + return user + + +async def set_status(db: AsyncSession, user: User, status: str) -> User: + user.status = status + await db.commit() + await db.refresh(user) + return user + + +async def list_all(db: AsyncSession, skip: int = 0, limit: int = 100) -> list[User]: + result = await db.execute(select(User).offset(skip).limit(limit)) + return list(result.scalars().all()) diff --git a/backend/app/database.py b/backend/app/database.py new file mode 100644 index 0000000..b121be9 --- /dev/null +++ b/backend/app/database.py @@ -0,0 +1,17 @@ +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from sqlalchemy.orm import DeclarativeBase + +from app.config import settings + +_connect_args = {"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {} +engine = create_async_engine(settings.DATABASE_URL, echo=False, connect_args=_connect_args) +AsyncSessionLocal = async_sessionmaker(engine, class_=AsyncSession, expire_on_commit=False) + + +class Base(DeclarativeBase): + pass + + +async def get_db() -> AsyncSession: + async with AsyncSessionLocal() as session: + yield session diff --git a/backend/app/dependencies.py b/backend/app/dependencies.py new file mode 100644 index 0000000..0dbfc80 --- /dev/null +++ b/backend/app/dependencies.py @@ -0,0 +1,42 @@ +from fastapi import Depends, HTTPException, status +from fastapi.security import OAuth2PasswordBearer +from sqlalchemy.ext.asyncio import AsyncSession + +from app.database import get_db +from app.models.user import User +from app.services.auth_service import decode_access_token +from app.crud import crud_user + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/api/v1/auth/login") + + +async def get_current_user( + token: str = Depends(oauth2_scheme), + db: AsyncSession = Depends(get_db), +) -> User: + payload = decode_access_token(token) + if payload is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid or expired token") + + user = await crud_user.get_by_id(db, payload["sub"]) + if user is None: + raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found") + return user + + +async def get_approved_user(current_user: User = Depends(get_current_user)) -> User: + if current_user.status != "approved": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Account not yet approved") + return current_user + + +async def get_organizer(current_user: User = Depends(get_approved_user)) -> User: + if current_user.role not in ("organizer", "admin"): + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Organizer access required") + return current_user + + +async def get_admin(current_user: User = Depends(get_approved_user)) -> User: + if current_user.role != "admin": + raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required") + return current_user diff --git a/backend/app/main.py b/backend/app/main.py new file mode 100644 index 0000000..807f1fd --- /dev/null +++ b/backend/app/main.py @@ -0,0 +1,41 @@ +from contextlib import asynccontextmanager + +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware + +from app.config import settings +from app.routers import auth, championships, registrations, participant_lists, users + + +@asynccontextmanager +async def lifespan(app: FastAPI): + # Start Instagram sync scheduler if configured + if settings.INSTAGRAM_USER_ID and settings.INSTAGRAM_ACCESS_TOKEN: + from app.services.instagram_service import start_scheduler + scheduler = start_scheduler() + yield + scheduler.shutdown() + else: + yield + + +app = FastAPI(title="Pole Dance Championships API", version="1.0.0", lifespan=lifespan) + +app.add_middleware( + CORSMiddleware, + allow_origins=settings.cors_origins_list, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +app.include_router(auth.router, prefix="/api/v1/auth", tags=["auth"]) +app.include_router(users.router, prefix="/api/v1/users", tags=["users"]) +app.include_router(championships.router, prefix="/api/v1/championships", tags=["championships"]) +app.include_router(registrations.router, prefix="/api/v1/registrations", tags=["registrations"]) +app.include_router(participant_lists.router, prefix="/api/v1", tags=["participant-lists"]) + + +@app.get("/internal/health", tags=["health"]) +async def health(): + return {"status": "ok"} diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py new file mode 100644 index 0000000..0fa623a --- /dev/null +++ b/backend/app/models/__init__.py @@ -0,0 +1,14 @@ +from app.models.user import User, RefreshToken +from app.models.championship import Championship +from app.models.registration import Registration +from app.models.participant import ParticipantList +from app.models.notification import NotificationLog + +__all__ = [ + "User", + "RefreshToken", + "Championship", + "Registration", + "ParticipantList", + "NotificationLog", +] diff --git a/backend/app/models/championship.py b/backend/app/models/championship.py new file mode 100644 index 0000000..c61d7ad --- /dev/null +++ b/backend/app/models/championship.py @@ -0,0 +1,42 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, Float, Integer, String, Text, Uuid, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class Championship(Base): + __tablename__ = "championships" + + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) + title: Mapped[str] = mapped_column(String(255), nullable=False) + description: Mapped[str | None] = mapped_column(Text) + location: Mapped[str | None] = mapped_column(String(500)) + event_date: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + registration_open_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + registration_close_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + + # Extended fields + form_url: Mapped[str | None] = mapped_column(String(2048)) + entry_fee: Mapped[float | None] = mapped_column(Float) + video_max_duration: Mapped[int | None] = mapped_column(Integer) # seconds + judges: Mapped[str | None] = mapped_column(Text) # JSON string: [{name, bio, instagram}] + categories: Mapped[str | None] = mapped_column(Text) # JSON string: [str] + + # Status: 'draft' | 'open' | 'closed' | 'completed' + status: Mapped[str] = mapped_column(String(20), nullable=False, default="draft") + # Source: 'manual' | 'instagram' + source: Mapped[str] = mapped_column(String(20), nullable=False, default="manual") + instagram_media_id: Mapped[str | None] = mapped_column(String(255), unique=True) + image_url: Mapped[str | None] = mapped_column(String(2048)) + raw_caption_text: Mapped[str | None] = mapped_column(Text) + + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + registrations: Mapped[list["Registration"]] = relationship(back_populates="championship", cascade="all, delete-orphan") # type: ignore[name-defined] + participant_list: Mapped["ParticipantList | None"] = relationship(back_populates="championship", uselist=False, cascade="all, delete-orphan") # type: ignore[name-defined] diff --git a/backend/app/models/notification.py b/backend/app/models/notification.py new file mode 100644 index 0000000..d189b22 --- /dev/null +++ b/backend/app/models/notification.py @@ -0,0 +1,27 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, String, Text, Uuid, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class NotificationLog(Base): + __tablename__ = "notification_log" + + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + registration_id: Mapped[uuid.UUID | None] = mapped_column( + Uuid(as_uuid=True), ForeignKey("registrations.id", ondelete="SET NULL") + ) + type: Mapped[str] = mapped_column(String(50), nullable=False) + title: Mapped[str] = mapped_column(String(255), nullable=False) + body: Mapped[str] = mapped_column(Text, nullable=False) + sent_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + delivery_status: Mapped[str] = mapped_column(String(20), default="pending") + + user: Mapped["User"] = relationship(back_populates="notification_logs") # type: ignore[name-defined] + registration: Mapped["Registration | None"] = relationship(back_populates="notification_logs") # type: ignore[name-defined] diff --git a/backend/app/models/participant.py b/backend/app/models/participant.py new file mode 100644 index 0000000..f264fbd --- /dev/null +++ b/backend/app/models/participant.py @@ -0,0 +1,25 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, Text, Uuid, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class ParticipantList(Base): + __tablename__ = "participant_lists" + + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) + championship_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), ForeignKey("championships.id", ondelete="CASCADE"), nullable=False, unique=True + ) + published_by: Mapped[uuid.UUID | None] = mapped_column( + Uuid(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL") + ) + is_published: Mapped[bool] = mapped_column(Boolean, default=False) + published_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + notes: Mapped[str | None] = mapped_column(Text) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + + championship: Mapped["Championship"] = relationship(back_populates="participant_list") # type: ignore[name-defined] diff --git a/backend/app/models/registration.py b/backend/app/models/registration.py new file mode 100644 index 0000000..f89891c --- /dev/null +++ b/backend/app/models/registration.py @@ -0,0 +1,36 @@ +import uuid +from datetime import datetime + +from sqlalchemy import DateTime, ForeignKey, String, Text, UniqueConstraint, Uuid, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class Registration(Base): + __tablename__ = "registrations" + __table_args__ = (UniqueConstraint("championship_id", "user_id", name="uq_registration_champ_user"),) + + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) + championship_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), ForeignKey("championships.id", ondelete="CASCADE"), nullable=False + ) + user_id: Mapped[uuid.UUID] = mapped_column( + Uuid(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE"), nullable=False + ) + category: Mapped[str | None] = mapped_column(String(255)) + level: Mapped[str | None] = mapped_column(String(100)) + notes: Mapped[str | None] = mapped_column(Text) + + # Multi-stage status: + # 'submitted' → 'form_submitted' → 'payment_pending' → 'payment_confirmed' → + # 'video_submitted' → 'accepted' | 'rejected' | 'waitlisted' + status: Mapped[str] = mapped_column(String(30), nullable=False, default="submitted") + video_url: Mapped[str | None] = mapped_column(String(2048)) + + submitted_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + decided_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True)) + + championship: Mapped["Championship"] = relationship(back_populates="registrations") # type: ignore[name-defined] + user: Mapped["User"] = relationship(back_populates="registrations") # type: ignore[name-defined] + notification_logs: Mapped[list["NotificationLog"]] = relationship(back_populates="registration") # type: ignore[name-defined] diff --git a/backend/app/models/user.py b/backend/app/models/user.py new file mode 100644 index 0000000..15d80c1 --- /dev/null +++ b/backend/app/models/user.py @@ -0,0 +1,44 @@ +import uuid +from datetime import datetime + +from sqlalchemy import Boolean, DateTime, ForeignKey, String, Uuid, func +from sqlalchemy.orm import Mapped, mapped_column, relationship + +from app.database import Base + + +class User(Base): + __tablename__ = "users" + + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) + email: Mapped[str] = mapped_column(String(255), unique=True, nullable=False, index=True) + hashed_password: Mapped[str] = mapped_column(String(255), nullable=False) + full_name: Mapped[str] = mapped_column(String(255), nullable=False) + phone: Mapped[str | None] = mapped_column(String(50)) + # Organizer-specific fields + organization_name: Mapped[str | None] = mapped_column(String(255)) + instagram_handle: Mapped[str | None] = mapped_column(String(100)) + role: Mapped[str] = mapped_column(String(20), nullable=False, default="member") + # 'pending' | 'approved' | 'rejected' + status: Mapped[str] = mapped_column(String(20), nullable=False, default="pending") + expo_push_token: Mapped[str | None] = mapped_column(String(255)) + created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now()) + updated_at: Mapped[datetime] = mapped_column( + DateTime(timezone=True), server_default=func.now(), onupdate=func.now() + ) + + refresh_tokens: Mapped[list["RefreshToken"]] = relationship(back_populates="user", cascade="all, delete-orphan") + registrations: Mapped[list["Registration"]] = relationship(back_populates="user") # type: ignore[name-defined] + notification_logs: Mapped[list["NotificationLog"]] = relationship(back_populates="user") # type: ignore[name-defined] + + +class RefreshToken(Base): + __tablename__ = "refresh_tokens" + + id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), primary_key=True, default=uuid.uuid4) + user_id: Mapped[uuid.UUID] = mapped_column(Uuid(as_uuid=True), ForeignKey("users.id", ondelete="CASCADE")) + token_hash: Mapped[str] = mapped_column(String(64), nullable=False, index=True) + expires_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), nullable=False) + revoked: Mapped[bool] = mapped_column(Boolean, default=False) + + user: Mapped["User"] = relationship(back_populates="refresh_tokens") diff --git a/backend/app/routers/__init__.py b/backend/app/routers/__init__.py new file mode 100644 index 0000000..be7db71 --- /dev/null +++ b/backend/app/routers/__init__.py @@ -0,0 +1,3 @@ +from app.routers import auth, championships, registrations, participant_lists, users + +__all__ = ["auth", "championships", "registrations", "participant_lists", "users"] diff --git a/backend/app/routers/auth.py b/backend/app/routers/auth.py new file mode 100644 index 0000000..c039b54 --- /dev/null +++ b/backend/app/routers/auth.py @@ -0,0 +1,79 @@ +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.crud import crud_user +from app.database import get_db +from app.dependencies import get_current_user +from app.models.user import User +from app.schemas.auth import LogoutRequest, RefreshRequest, RegisterResponse, TokenPair, TokenRefreshed +from app.schemas.user import UserLogin, UserOut, UserRegister, UserUpdate +from app.services.auth_service import ( + create_access_token, + create_refresh_token, + revoke_refresh_token, + rotate_refresh_token, + verify_password, +) + +router = APIRouter() + + +@router.post("/register", response_model=RegisterResponse, status_code=status.HTTP_201_CREATED) +async def register(data: UserRegister, db: AsyncSession = Depends(get_db)): + if await crud_user.get_by_email(db, data.email): + raise HTTPException(status_code=400, detail="Email already registered") + user = await crud_user.create(db, data) + # Members are auto-approved — issue tokens immediately so they can log in right away + if user.role == "member": + access_token = create_access_token(user.id) + refresh_token = await create_refresh_token(db, user.id) + return RegisterResponse( + user=UserOut.model_validate(user), + access_token=access_token, + refresh_token=refresh_token, + ) + # Organizers must wait for admin approval + return RegisterResponse(user=UserOut.model_validate(user)) + + +@router.post("/login", response_model=TokenPair) +async def login(data: UserLogin, db: AsyncSession = Depends(get_db)): + user = await crud_user.get_by_email(db, data.email) + if not user or not verify_password(data.password, user.hashed_password): + raise HTTPException(status_code=401, detail="Invalid credentials") + access_token = create_access_token(user.id) + refresh_token = await create_refresh_token(db, user.id) + return TokenPair( + access_token=access_token, + refresh_token=refresh_token, + user=UserOut.model_validate(user), + ) + + +@router.post("/refresh", response_model=TokenRefreshed) +async def refresh(data: RefreshRequest, db: AsyncSession = Depends(get_db)): + result = await rotate_refresh_token(db, data.refresh_token) + if result is None: + raise HTTPException(status_code=401, detail="Invalid or expired refresh token") + new_refresh, user_id = result + access_token = create_access_token(user_id) + return TokenRefreshed(access_token=access_token, refresh_token=new_refresh) + + +@router.post("/logout", status_code=status.HTTP_204_NO_CONTENT) +async def logout(data: LogoutRequest, db: AsyncSession = Depends(get_db)): + await revoke_refresh_token(db, data.refresh_token) + + +@router.get("/me", response_model=UserOut) +async def me(current_user: User = Depends(get_current_user)): + return current_user + + +@router.patch("/me", response_model=UserOut) +async def update_me( + data: UserUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + return await crud_user.update(db, current_user, data) diff --git a/backend/app/routers/championships.py b/backend/app/routers/championships.py new file mode 100644 index 0000000..8b05797 --- /dev/null +++ b/backend/app/routers/championships.py @@ -0,0 +1,69 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, Query, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.crud import crud_championship +from app.database import get_db +from app.dependencies import get_approved_user, get_organizer +from app.models.user import User +from app.schemas.championship import ChampionshipCreate, ChampionshipOut, ChampionshipUpdate + +router = APIRouter() + + +@router.get("", response_model=list[ChampionshipOut]) +async def list_championships( + status: str | None = Query(None), + skip: int = 0, + limit: int = 50, + _user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + return await crud_championship.list_all(db, status=status, skip=skip, limit=limit) + + +@router.get("/{champ_id}", response_model=ChampionshipOut) +async def get_championship( + champ_id: uuid.UUID, + _user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + champ = await crud_championship.get(db, champ_id) + if not champ: + raise HTTPException(status_code=404, detail="Championship not found") + return champ + + +@router.post("", response_model=ChampionshipOut, status_code=status.HTTP_201_CREATED) +async def create_championship( + data: ChampionshipCreate, + _user: User = Depends(get_organizer), + db: AsyncSession = Depends(get_db), +): + return await crud_championship.create(db, data) + + +@router.patch("/{champ_id}", response_model=ChampionshipOut) +async def update_championship( + champ_id: uuid.UUID, + data: ChampionshipUpdate, + _user: User = Depends(get_organizer), + db: AsyncSession = Depends(get_db), +): + champ = await crud_championship.get(db, champ_id) + if not champ: + raise HTTPException(status_code=404, detail="Championship not found") + return await crud_championship.update(db, champ, data) + + +@router.delete("/{champ_id}", status_code=status.HTTP_204_NO_CONTENT) +async def delete_championship( + champ_id: uuid.UUID, + _user: User = Depends(get_organizer), + db: AsyncSession = Depends(get_db), +): + champ = await crud_championship.get(db, champ_id) + if not champ: + raise HTTPException(status_code=404, detail="Championship not found") + await crud_championship.delete(db, champ) diff --git a/backend/app/routers/participant_lists.py b/backend/app/routers/participant_lists.py new file mode 100644 index 0000000..13984ec --- /dev/null +++ b/backend/app/routers/participant_lists.py @@ -0,0 +1,55 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.crud import crud_participant, crud_registration +from app.database import get_db +from app.dependencies import get_approved_user, get_organizer +from app.models.user import User +from app.schemas.participant import ParticipantListOut, ParticipantListPublish +from app.schemas.registration import RegistrationWithUser + +router = APIRouter() + + +@router.get("/championships/{champ_id}/participant-list", response_model=ParticipantListOut | None) +async def get_participant_list( + champ_id: uuid.UUID, + _user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + return await crud_participant.get_for_championship(db, champ_id) + + +@router.post( + "/championships/{champ_id}/participant-list/publish", + response_model=ParticipantListOut, + status_code=status.HTTP_201_CREATED, +) +async def publish_participant_list( + champ_id: uuid.UUID, + data: ParticipantListPublish, + current_user: User = Depends(get_organizer), + db: AsyncSession = Depends(get_db), +): + pl = await crud_participant.create_or_get(db, champ_id, current_user.id) + if pl.is_published: + raise HTTPException(status_code=409, detail="Participant list already published") + pl = await crud_participant.publish(db, pl, data.notes) + + # TODO: send push notifications to accepted participants + return pl + + +@router.get( + "/championships/{champ_id}/participant-list/registrations", + response_model=list[RegistrationWithUser], +) +async def list_accepted_registrations( + champ_id: uuid.UUID, + _user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + regs = await crud_registration.list_for_championship(db, champ_id) + return [r for r in regs if r.status == "accepted"] diff --git a/backend/app/routers/registrations.py b/backend/app/routers/registrations.py new file mode 100644 index 0000000..1aa310b --- /dev/null +++ b/backend/app/routers/registrations.py @@ -0,0 +1,108 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException, status +from sqlalchemy.ext.asyncio import AsyncSession + +from app.crud import crud_championship, crud_registration +from app.database import get_db +from app.dependencies import get_approved_user, get_organizer +from app.models.user import User +from app.schemas.registration import ( + RegistrationCreate, + RegistrationListItem, + RegistrationOut, + RegistrationUpdate, + RegistrationWithUser, +) + +router = APIRouter() + + +@router.post("", response_model=RegistrationOut, status_code=status.HTTP_201_CREATED) +async def register_for_championship( + data: RegistrationCreate, + current_user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + champ = await crud_championship.get(db, data.championship_id) + if not champ: + raise HTTPException(status_code=404, detail="Championship not found") + if champ.status != "open": + raise HTTPException(status_code=400, detail="Registration is not open for this championship") + + existing = await crud_registration.get_by_user_and_championship(db, current_user.id, data.championship_id) + if existing: + raise HTTPException(status_code=409, detail="Already registered for this championship") + + return await crud_registration.create(db, current_user.id, data) + + +@router.get("/my", response_model=list[RegistrationListItem]) +async def my_registrations( + current_user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + return await crud_registration.list_for_user(db, current_user.id) + + +@router.get("/{reg_id}", response_model=RegistrationOut) +async def get_registration( + reg_id: uuid.UUID, + current_user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + reg = await crud_registration.get(db, reg_id) + if not reg: + raise HTTPException(status_code=404, detail="Registration not found") + if reg.user_id != current_user.id and current_user.role not in ("organizer", "admin"): + raise HTTPException(status_code=403, detail="Access denied") + return reg + + +@router.patch("/{reg_id}", response_model=RegistrationOut) +async def update_registration( + reg_id: uuid.UUID, + data: RegistrationUpdate, + current_user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + reg = await crud_registration.get(db, reg_id) + if not reg: + raise HTTPException(status_code=404, detail="Registration not found") + + # Members can only update their own registration (video_url, notes) + if current_user.role == "member": + if reg.user_id != current_user.id: + raise HTTPException(status_code=403, detail="Access denied") + allowed_fields = {"video_url", "notes"} + update_data = data.model_dump(exclude_none=True) + if not set(update_data.keys()).issubset(allowed_fields): + raise HTTPException(status_code=403, detail="Members can only update video_url and notes") + + return await crud_registration.update(db, reg, data) + + +@router.delete("/{reg_id}", status_code=status.HTTP_204_NO_CONTENT) +async def cancel_registration( + reg_id: uuid.UUID, + current_user: User = Depends(get_approved_user), + db: AsyncSession = Depends(get_db), +): + reg = await crud_registration.get(db, reg_id) + if not reg: + raise HTTPException(status_code=404, detail="Registration not found") + if reg.user_id != current_user.id and current_user.role not in ("organizer", "admin"): + raise HTTPException(status_code=403, detail="Access denied") + await crud_registration.delete(db, reg) + + +# Organizer: list all registrations for a championship +@router.get("/championship/{champ_id}", response_model=list[RegistrationWithUser]) +async def list_registrations_for_championship( + champ_id: uuid.UUID, + _user: User = Depends(get_organizer), + db: AsyncSession = Depends(get_db), + skip: int = 0, + limit: int = 100, +): + return await crud_registration.list_for_championship(db, champ_id, skip=skip, limit=limit) diff --git a/backend/app/routers/users.py b/backend/app/routers/users.py new file mode 100644 index 0000000..18b1fe0 --- /dev/null +++ b/backend/app/routers/users.py @@ -0,0 +1,46 @@ +import uuid + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession + +from app.crud import crud_user +from app.database import get_db +from app.dependencies import get_admin +from app.models.user import User +from app.schemas.user import UserOut + +router = APIRouter() + + +@router.get("", response_model=list[UserOut]) +async def list_users( + _admin: User = Depends(get_admin), + db: AsyncSession = Depends(get_db), + skip: int = 0, + limit: int = 100, +): + return await crud_user.list_all(db, skip=skip, limit=limit) + + +@router.patch("/{user_id}/approve", response_model=UserOut) +async def approve_user( + user_id: uuid.UUID, + _admin: User = Depends(get_admin), + db: AsyncSession = Depends(get_db), +): + user = await crud_user.get_by_id(db, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return await crud_user.set_status(db, user, "approved") + + +@router.patch("/{user_id}/reject", response_model=UserOut) +async def reject_user( + user_id: uuid.UUID, + _admin: User = Depends(get_admin), + db: AsyncSession = Depends(get_db), +): + user = await crud_user.get_by_id(db, user_id) + if not user: + raise HTTPException(status_code=404, detail="User not found") + return await crud_user.set_status(db, user, "rejected") diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/schemas/auth.py b/backend/app/schemas/auth.py new file mode 100644 index 0000000..bd94cf8 --- /dev/null +++ b/backend/app/schemas/auth.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel + +from app.schemas.user import UserOut + + +class TokenPair(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" + user: UserOut + + +class RefreshRequest(BaseModel): + refresh_token: str + + +class TokenRefreshed(BaseModel): + access_token: str + refresh_token: str + token_type: str = "bearer" + + +class LogoutRequest(BaseModel): + refresh_token: str + + +class RegisterResponse(BaseModel): + """ + Returned after registration. + Members get tokens immediately (auto-approved). + Organizers only get the user object (pending approval). + """ + user: UserOut + access_token: str | None = None + refresh_token: str | None = None + token_type: str = "bearer" diff --git a/backend/app/schemas/championship.py b/backend/app/schemas/championship.py new file mode 100644 index 0000000..1e1d933 --- /dev/null +++ b/backend/app/schemas/championship.py @@ -0,0 +1,73 @@ +import json +import uuid +from datetime import datetime + +from pydantic import BaseModel, model_validator + + +class ChampionshipCreate(BaseModel): + title: str + description: str | None = None + location: str | None = None + event_date: datetime | None = None + registration_open_at: datetime | None = None + registration_close_at: datetime | None = None + form_url: str | None = None + entry_fee: float | None = None + video_max_duration: int | None = None + judges: list[dict] | None = None # [{name, bio, instagram}] + categories: list[str] | None = None + status: str = "draft" + image_url: str | None = None + + +class ChampionshipUpdate(BaseModel): + title: str | None = None + description: str | None = None + location: str | None = None + event_date: datetime | None = None + registration_open_at: datetime | None = None + registration_close_at: datetime | None = None + form_url: str | None = None + entry_fee: float | None = None + video_max_duration: int | None = None + judges: list[dict] | None = None + categories: list[str] | None = None + status: str | None = None + image_url: str | None = None + + +class ChampionshipOut(BaseModel): + model_config = {"from_attributes": True} + + id: uuid.UUID + title: str + description: str | None + location: str | None + event_date: datetime | None + registration_open_at: datetime | None + registration_close_at: datetime | None + form_url: str | None + entry_fee: float | None + video_max_duration: int | None + judges: list[dict] | None + categories: list[str] | None + status: str + source: str + instagram_media_id: str | None + image_url: str | None + created_at: datetime + updated_at: datetime + + @model_validator(mode="before") + @classmethod + def parse_json_fields(cls, v): + # judges and categories are stored as JSON strings in the DB + if hasattr(v, "__dict__"): + raw_j = getattr(v, "judges", None) + raw_c = getattr(v, "categories", None) + if isinstance(raw_j, str): + v.__dict__["judges"] = json.loads(raw_j) + if isinstance(raw_c, str): + v.__dict__["categories"] = json.loads(raw_c) + return v diff --git a/backend/app/schemas/participant.py b/backend/app/schemas/participant.py new file mode 100644 index 0000000..8b3c553 --- /dev/null +++ b/backend/app/schemas/participant.py @@ -0,0 +1,19 @@ +import uuid +from datetime import datetime + +from pydantic import BaseModel + + +class ParticipantListOut(BaseModel): + model_config = {"from_attributes": True} + + id: uuid.UUID + championship_id: uuid.UUID + is_published: bool + published_at: datetime | None + notes: str | None + created_at: datetime + + +class ParticipantListPublish(BaseModel): + notes: str | None = None diff --git a/backend/app/schemas/registration.py b/backend/app/schemas/registration.py new file mode 100644 index 0000000..67f0153 --- /dev/null +++ b/backend/app/schemas/registration.py @@ -0,0 +1,57 @@ +import uuid +from datetime import datetime +from typing import Any + +from pydantic import BaseModel, model_validator + +from app.schemas.user import UserOut + + +class RegistrationCreate(BaseModel): + championship_id: uuid.UUID + category: str | None = None + level: str | None = None + notes: str | None = None + + +class RegistrationUpdate(BaseModel): + status: str | None = None + video_url: str | None = None + category: str | None = None + level: str | None = None + notes: str | None = None + + +class RegistrationOut(BaseModel): + model_config = {"from_attributes": True} + + id: uuid.UUID + championship_id: uuid.UUID + user_id: uuid.UUID + category: str | None + level: str | None + notes: str | None + status: str + video_url: str | None + submitted_at: datetime + decided_at: datetime | None + + +class RegistrationListItem(RegistrationOut): + championship_title: str | None = None + championship_event_date: datetime | None = None + championship_location: str | None = None + + @model_validator(mode="before") + @classmethod + def extract_championship(cls, data: Any) -> Any: + if hasattr(data, "championship") and data.championship: + champ = data.championship + data.__dict__["championship_title"] = champ.title + data.__dict__["championship_event_date"] = champ.event_date + data.__dict__["championship_location"] = champ.location + return data + + +class RegistrationWithUser(RegistrationOut): + user: UserOut diff --git a/backend/app/schemas/user.py b/backend/app/schemas/user.py new file mode 100644 index 0000000..3c5d896 --- /dev/null +++ b/backend/app/schemas/user.py @@ -0,0 +1,52 @@ +import uuid +from datetime import datetime +from typing import Literal + +from pydantic import BaseModel, EmailStr, field_validator + + +class UserRegister(BaseModel): + email: EmailStr + password: str + full_name: str + phone: str | None = None + # Role requested at registration: 'member' or 'organizer' + requested_role: Literal["member", "organizer"] = "member" + # Organizer-only fields + organization_name: str | None = None + instagram_handle: str | None = None + + @field_validator("organization_name") + @classmethod + def org_name_required_for_organizer(cls, v, info): + if info.data.get("requested_role") == "organizer" and not v: + raise ValueError("Organization name is required for organizer registration") + return v + + +class UserLogin(BaseModel): + email: EmailStr + password: str + + +class UserOut(BaseModel): + model_config = {"from_attributes": True} + + id: uuid.UUID + email: str + full_name: str + phone: str | None + role: str + status: str + organization_name: str | None + instagram_handle: str | None + expo_push_token: str | None + created_at: datetime + + +class UserUpdate(BaseModel): + full_name: str | None = None + phone: str | None = None + organization_name: str | None = None + instagram_handle: str | None = None + expo_push_token: str | None = None diff --git a/backend/app/services/__init__.py b/backend/app/services/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/backend/app/services/auth_service.py b/backend/app/services/auth_service.py new file mode 100644 index 0000000..29e029f --- /dev/null +++ b/backend/app/services/auth_service.py @@ -0,0 +1,81 @@ +import hashlib +import uuid +from datetime import UTC, datetime, timedelta + +import bcrypt +from jose import JWTError, jwt +from sqlalchemy.ext.asyncio import AsyncSession + +from app.config import settings +from app.models.user import RefreshToken + + +def hash_password(password: str) -> str: + return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode() + + +def verify_password(plain: str, hashed: str) -> bool: + return bcrypt.checkpw(plain.encode(), hashed.encode()) + + +def create_access_token(user_id: uuid.UUID) -> str: + expire = datetime.now(UTC) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) + return jwt.encode({"sub": str(user_id), "exp": expire}, settings.SECRET_KEY, algorithm=settings.ALGORITHM) + + +def decode_access_token(token: str) -> dict | None: + try: + return jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) + except JWTError: + return None + + +def _hash_token(token: str) -> str: + return hashlib.sha256(token.encode()).hexdigest() + + +async def create_refresh_token(db: AsyncSession, user_id: uuid.UUID) -> str: + raw = str(uuid.uuid4()) + expires_at = datetime.now(UTC) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + record = RefreshToken(user_id=user_id, token_hash=_hash_token(raw), expires_at=expires_at) + db.add(record) + await db.commit() + return raw + + +async def rotate_refresh_token(db: AsyncSession, raw_token: str) -> tuple[str, uuid.UUID] | None: + """Validate old token, revoke it, issue a new one. Returns (new_raw, user_id) or None.""" + from sqlalchemy import select + + token_hash = _hash_token(raw_token) + result = await db.execute( + select(RefreshToken).where( + RefreshToken.token_hash == token_hash, + RefreshToken.revoked.is_(False), + RefreshToken.expires_at > datetime.now(UTC), + ) + ) + record = result.scalar_one_or_none() + if record is None: + return None + + record.revoked = True + await db.flush() + + new_raw = str(uuid.uuid4()) + expires_at = datetime.now(UTC) + timedelta(days=settings.REFRESH_TOKEN_EXPIRE_DAYS) + new_record = RefreshToken(user_id=record.user_id, token_hash=_hash_token(new_raw), expires_at=expires_at) + db.add(new_record) + await db.commit() + return new_raw, record.user_id + + +async def revoke_refresh_token(db: AsyncSession, raw_token: str) -> None: + from sqlalchemy import select + + token_hash = _hash_token(raw_token) + result = await db.execute(select(RefreshToken).where(RefreshToken.token_hash == token_hash)) + record = result.scalar_one_or_none() + if record: + record.revoked = True + await db.commit() diff --git a/backend/requirements.txt b/backend/requirements.txt new file mode 100644 index 0000000..bbd4e1c --- /dev/null +++ b/backend/requirements.txt @@ -0,0 +1,18 @@ +fastapi==0.115.6 +uvicorn[standard]==0.32.1 +sqlalchemy[asyncio]==2.0.36 +alembic==1.14.0 +aiosqlite==0.20.0 +# asyncpg==0.30.0 # uncomment for PostgreSQL production use +pydantic==2.10.3 +pydantic-settings==2.7.0 +python-jose[cryptography]==3.3.0 +bcrypt==4.2.1 +pydantic[email] +python-multipart==0.0.20 +httpx==0.28.1 +apscheduler==3.10.4 +slowapi==0.1.9 +pytest==8.3.4 +pytest-asyncio==0.24.0 +pytest-httpx==0.35.0 diff --git a/backend/seed.py b/backend/seed.py new file mode 100644 index 0000000..95b24c5 --- /dev/null +++ b/backend/seed.py @@ -0,0 +1,134 @@ +"""Seed script — creates test users and one championship. +Run from backend/: .venv/Scripts/python seed.py +""" +import asyncio +import json +from datetime import UTC, datetime, timedelta + +from app.database import AsyncSessionLocal +from app.models.championship import Championship +from app.models.user import User +from app.services.auth_service import hash_password +from sqlalchemy import select + + +async def seed(): + async with AsyncSessionLocal() as db: + # ── Users ────────────────────────────────────────────────────────────── + users_data = [ + { + "email": "admin@pole.dev", + "full_name": "Diana Admin", + "password": "Admin1234", + "role": "admin", + "status": "approved", + }, + { + "email": "organizer@pole.dev", + "full_name": "Ekaterina Organizer", + "password": "Org1234", + "role": "organizer", + "status": "approved", + }, + { + "email": "member@pole.dev", + "full_name": "Anna Petrova", + "password": "Member1234", + "role": "member", + "status": "approved", + }, + { + "email": "pending@pole.dev", + "full_name": "New Applicant", + "password": "Pending1234", + "role": "member", + "status": "pending", + }, + ] + + created_users = {} + for ud in users_data: + result = await db.execute(select(User).where(User.email == ud["email"])) + user = result.scalar_one_or_none() + if user is None: + user = User( + email=ud["email"], + hashed_password=hash_password(ud["password"]), + full_name=ud["full_name"], + role=ud["role"], + status=ud["status"], + ) + db.add(user) + print(f" Created user: {ud['email']}") + else: + # Update role/status if needed + user.role = ud["role"] + user.status = ud["status"] + user.hashed_password = hash_password(ud["password"]) + print(f" Updated user: {ud['email']}") + created_users[ud["email"]] = user + + await db.flush() + + # ── Championships ────────────────────────────────────────────────────── + championships_data = [ + { + "title": "Spring Open 2026", + "description": "Annual spring pole dance championship. All levels welcome.", + "location": "Cultural Center, Minsk", + "event_date": datetime(2026, 4, 15, 10, 0, tzinfo=UTC), + "registration_open_at": datetime(2026, 3, 1, 0, 0, tzinfo=UTC), + "registration_close_at": datetime(2026, 4, 1, 0, 0, tzinfo=UTC), + "form_url": "https://forms.example.com/spring2026", + "entry_fee": 50.0, + "video_max_duration": 180, + "judges": json.dumps([ + {"name": "Oksana Ivanova", "bio": "Champion 2023", "instagram": "@oksana_pole"}, + {"name": "Marta Sokolova", "bio": "Certified judge", "instagram": "@marta_pole"}, + ]), + "categories": json.dumps(["Novice", "Amateur", "Professional"]), + "status": "open", + "source": "manual", + "image_url": "https://images.unsplash.com/photo-1524594152303-9fd13543fe6e?w=800", + }, + { + "title": "Summer Championship 2026", + "description": "The biggest pole dance event of the summer.", + "location": "Sports Palace, Minsk", + "event_date": datetime(2026, 7, 20, 9, 0, tzinfo=UTC), + "registration_open_at": datetime(2026, 6, 1, 0, 0, tzinfo=UTC), + "registration_close_at": datetime(2026, 7, 5, 0, 0, tzinfo=UTC), + "entry_fee": 75.0, + "video_max_duration": 240, + "judges": json.dumps([ + {"name": "Elena Kozlova", "bio": "World finalist", "instagram": "@elena_wpc"}, + ]), + "categories": json.dumps(["Junior", "Senior", "Masters"]), + "status": "draft", + "source": "manual", + }, + ] + + for cd in championships_data: + result = await db.execute( + select(Championship).where(Championship.title == cd["title"]) + ) + champ = result.scalar_one_or_none() + if champ is None: + champ = Championship(**cd) + db.add(champ) + print(f" Created championship: {cd['title']}") + else: + print(f" Championship already exists: {cd['title']}") + + await db.commit() + print("\nSeed complete!") + print("\n=== TEST CREDENTIALS ===") + print("Admin: admin@pole.dev / Admin1234") + print("Organizer: organizer@pole.dev / Org1234") + print("Member: member@pole.dev / Member1234") + print("Pending: pending@pole.dev / Pending1234") + + +if __name__ == "__main__": + asyncio.run(seed()) diff --git a/backend/tests/__init__.py b/backend/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dance-champ-app.md b/dance-champ-app.md deleted file mode 100644 index e3e0a30..0000000 --- a/dance-champ-app.md +++ /dev/null @@ -1,310 +0,0 @@ -# Pole Dance Championships App — Technical Document - -## 1. Project Overview - -### Problem - -Pole dance championships lack dedicated tools for organizers and participants. Organizers rely on Instagram posts to announce events, and registrations are managed via Google Forms — leading to: - -- **No visibility** — Participants can't track their registration status in one place. -- **Scattered info** — Dates, rules, and results are buried across Instagram posts. -- **No access control** — Anyone can register; organizers need a way to accept or reject participants. - -### Solution - -A **members-only mobile app** for pole dance championship participants and organizers. - -- New users register and wait for admin approval before accessing the app. -- Championship announcements are auto-imported from Instagram (Graph API). -- Members browse championships, submit registrations, and track their status. -- Organizers accept, reject, or waitlist participants and publish the final participant list. -- Push notifications alert members when results are published. - ---- - -## 2. User Roles - -| Role | Access | -|---|---| -| **Member** | Browse championships, register, track own registration status | -| **Organizer** | All of the above + manage registrations, publish participant lists | -| **Admin** | All of the above + approve/reject new member accounts | - -New accounts start as `pending` and must be approved by an admin before they can use the app. - ---- - -## 3. Tech Stack - -| Layer | Technology | -|---|---| -| **Mobile** | React Native + TypeScript (Expo managed workflow) | -| **Navigation** | React Navigation v6 | -| **Server state** | TanStack React Query v5 | -| **Client state** | Zustand | -| **Backend** | FastAPI (Python, async) | -| **ORM** | SQLAlchemy 2.0 async | -| **Database** | SQLite (local dev) / PostgreSQL (production) | -| **Migrations** | Alembic | -| **Auth** | JWT — access tokens (15 min) + refresh tokens (7 days, rotation-based) | -| **Instagram sync** | Instagram Graph API, polled every 30 min via APScheduler | -| **Push notifications** | Expo Push API (routes to FCM + APNs) | -| **Token storage** | expo-secure-store (with in-memory cache for reliability) | - ---- - -## 4. Architecture - -``` -┌─────────────────────┐ ┌──────────────────────────────┐ -│ React Native App │◄──────►│ FastAPI Backend │ -│ (Expo Go / APK) │ HTTPS │ /api/v1/... │ -└─────────────────────┘ │ │ - │ ┌──────────────────────┐ │ - │ │ APScheduler │ │ - │ │ - Instagram poll/30m│ │ - │ │ - Token refresh/7d │ │ - │ └──────────────────────┘ │ - │ │ - │ ┌──────────────────────┐ │ - │ │ SQLite / PostgreSQL │ │ - │ └──────────────────────┘ │ - └──────────────────────────────┘ -``` - ---- - -## 5. Data Model - -### User -``` -id UUID (PK) -email String (unique) -full_name String -phone String (nullable) -hashed_password String -role Enum: member | organizer | admin -status Enum: pending | approved | rejected -expo_push_token String (nullable) -created_at DateTime -updated_at DateTime -``` - -### Championship -``` -id UUID (PK) -title String -description String (nullable) -location String (nullable) -event_date DateTime (nullable) -registration_open_at DateTime (nullable) -registration_close_at DateTime (nullable) -status Enum: draft | open | closed | completed -source Enum: manual | instagram -instagram_media_id String (nullable, unique) -image_url String (nullable) -raw_caption_text Text (nullable) -created_at DateTime -updated_at DateTime -``` - -### Registration -``` -id UUID (PK) -championship_id UUID (FK → Championship) -user_id UUID (FK → User) -category String (nullable) -level String (nullable) -notes Text (nullable) -status Enum: submitted | accepted | rejected | waitlisted -submitted_at DateTime -decided_at DateTime (nullable) -``` - -### ParticipantList -``` -id UUID (PK) -championship_id UUID (FK → Championship, unique) -published_by UUID (FK → User) -is_published Boolean -published_at DateTime (nullable) -notes Text (nullable) -``` - -### RefreshToken -``` -id UUID (PK) -user_id UUID (FK → User) -token_hash String (SHA-256, unique) -expires_at DateTime -revoked Boolean -created_at DateTime -``` - -### NotificationLog -``` -id UUID (PK) -user_id UUID (FK → User) -championship_id UUID (FK → Championship, nullable) -title String -body String -sent_at DateTime -status String -``` - ---- - -## 6. API Endpoints - -### Auth -| Method | Path | Description | -|---|---|---| -| POST | `/api/v1/auth/register` | Register new account (status=pending) | -| POST | `/api/v1/auth/login` | Login, get access + refresh tokens | -| POST | `/api/v1/auth/refresh` | Refresh access token | -| POST | `/api/v1/auth/logout` | Revoke refresh token | -| GET | `/api/v1/auth/me` | Get current user | - -### Championships -| Method | Path | Access | -|---|---|---| -| GET | `/api/v1/championships` | Approved members | -| GET | `/api/v1/championships/{id}` | Approved members | -| POST | `/api/v1/championships` | Organizer+ | -| PATCH | `/api/v1/championships/{id}` | Organizer+ | -| DELETE | `/api/v1/championships/{id}` | Admin | - -### Registrations -| Method | Path | Access | -|---|---|---| -| POST | `/api/v1/championships/{id}/register` | Approved member | -| GET | `/api/v1/championships/{id}/registrations` | Organizer+ | -| PATCH | `/api/v1/registrations/{id}/status` | Organizer+ | -| GET | `/api/v1/registrations/mine` | Member (own only) | -| DELETE | `/api/v1/registrations/{id}` | Member (own, before decision) | - -### Participant Lists -| Method | Path | Access | -|---|---|---| -| GET | `/api/v1/championships/{id}/participant-list` | Approved member | -| PUT | `/api/v1/championships/{id}/participant-list` | Organizer+ | -| POST | `/api/v1/championships/{id}/participant-list/publish` | Organizer+ | -| POST | `/api/v1/championships/{id}/participant-list/unpublish` | Organizer+ | - -### Users (Admin) -| Method | Path | Description | -|---|---|---| -| GET | `/api/v1/users` | List all users | -| PATCH | `/api/v1/users/{id}/status` | Approve/reject member | -| PATCH | `/api/v1/users/me/push-token` | Register push token | - ---- - -## 7. Mobile Screens - -### Auth Flow -- **LoginScreen** — Email + password login -- **RegisterScreen** — Full name, phone, email, password -- **PendingApprovalScreen** — Shown after register until admin approves - -### App (approved members) -- **ChampionshipListScreen** — All championships with status badges -- **ChampionshipDetailScreen** — Full info + registration button -- **RegistrationFormScreen** — Category, level, notes -- **ProfileScreen** — User info, logout - -### Navigation -``` -No user ──► AuthStack (Login / Register) -Pending ──► PendingApprovalScreen -Approved ──► AppStack (Championships / Profile) -``` - ---- - -## 8. Instagram Integration - -### How It Works -1. Admin configures `INSTAGRAM_USER_ID` and `INSTAGRAM_ACCESS_TOKEN` in `.env`. -2. APScheduler polls the Instagram Graph API every 30 minutes. -3. New media posts are parsed: title (first line of caption), location (`Место:` / `Location:` prefix), dates (regex for RU + EN date formats). -4. New championships are created with `status=draft` for admin review. -5. Long-lived tokens are refreshed weekly automatically. - -### Parsing Logic -- **Title** — First non-empty line of the caption. -- **Location** — Line starting with `Место:` or `Location:`. -- **Date** — Regex matches formats like `15 апреля 2026`, `April 15, 2026`, `15.04.2026`. -- **Deduplication** — Championships are matched by `instagram_media_id`. - ---- - -## 9. Local Development Setup - -### Prerequisites -- Python 3.11+ -- Node.js 18+ -- Expo Go app on phone - -### Backend -```bat -cd backend -python -m venv .venv -.venv\Scripts\pip install -r requirements.txt -alembic upgrade head -python scripts/create_admin.py # creates admin@poledance.app / Admin1234 -uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload -``` - -Or use: `start-backend.bat` - -### Mobile -```bat -cd mobile -npm install -npx expo start --lan --clear -``` - -Or use: `start-mobile.bat` - -### Environment (backend/.env) -``` -DATABASE_URL=sqlite+aiosqlite:///./poledance.db -SECRET_KEY=dev-secret-key-change-before-production-32x -INSTAGRAM_USER_ID= -INSTAGRAM_ACCESS_TOKEN= -INSTAGRAM_POLL_INTERVAL=1800 -``` - -### Environment (mobile/.env) -``` -EXPO_PUBLIC_API_URL=http://:8000/api/v1 -``` - -### Test Accounts -| Email | Password | Role | Status | -|---|---|---|---| -| admin@poledance.app | Admin1234 | admin | approved | -| anna@example.com | Member1234 | member | approved | -| maria@example.com | Member1234 | member | approved | -| elena@example.com | Member1234 | member | pending | - ---- - -## 10. Known Limitations (Local Dev) - -- **SQLite** is used instead of PostgreSQL — change `DATABASE_URL` for production. -- **Push notifications** don't work in Expo Go SDK 53 — requires a development build. -- **Instagram polling** requires a valid Business/Creator account token. -- Windows Firewall must allow inbound TCP on port 8000 for phone to reach the backend. - ---- - -## 11. Future Features - -- **Web admin panel** — Browser-based dashboard for organizers to manage registrations and championships. -- **In-app notifications feed** — History of received push notifications. -- **Calendar sync** — Export championship dates to phone calendar. -- **Production deployment** — Docker + PostgreSQL + nginx + SSL. -- **OCR / LLM parsing** — Better extraction of championship details from Instagram images and captions. -- **Multi-organizer** — Support multiple Instagram accounts for different championships. diff --git a/dancechamp-claude-code/CLAUDE.md b/dancechamp-claude-code/CLAUDE.md new file mode 100644 index 0000000..0ab08bc --- /dev/null +++ b/dancechamp-claude-code/CLAUDE.md @@ -0,0 +1,176 @@ +# CLAUDE.md — DanceChamp + +## What is this project? + +DanceChamp is a mobile platform for **pole dance championships**. Three apps, one database: + +- **Member App** (React Native / Expo) — Dancers discover championships, register, track their 10-step progress +- **Org App** (React Native / Expo) — Championship organizers create events, manage members, review videos, confirm payments +- **Admin Panel** (React + Vite, web) — Platform admin approves orgs, reviews championships from unverified orgs, manages users + +## Project Structure + +``` +/ +├── CLAUDE.md ← You are here +├── apps/ +│ ├── mobile/ ← Expo app (Member + Org views, switched by role) +│ │ ├── src/ +│ │ │ ├── screens/ +│ │ │ │ ├── member/ ← Home, MyChamps, Search, Profile, ChampDetail, Progress +│ │ │ │ ├── org/ ← Dashboard, ChampDetail (tabbed), MemberDetail, Settings +│ │ │ │ └── auth/ ← SignIn, SignUp, Onboarding +│ │ │ ├── components/ ← Shared UI components +│ │ │ ├── navigation/ ← Tab + Stack navigators +│ │ │ ├── store/ ← Zustand stores +│ │ │ ├── lib/ ← Supabase client, helpers +│ │ │ └── theme/ ← Colors, fonts, spacing +│ │ └── app.json +│ └── admin/ ← Vite React app +│ └── src/ +│ ├── pages/ ← Dashboard, Orgs, Champs, Users +│ ├── components/ +│ └── lib/ +├── packages/ +│ └── shared/ ← Shared types, constants, validation +│ ├── types.ts ← TypeScript interfaces (User, Championship, etc.) +│ └── constants.ts ← Status enums, role enums +├── supabase/ +│ ├── migrations/ ← SQL migration files +│ └── seed.sql ← Demo data +└── docs/ + ├── SPEC.md ← Full technical specification + ├── PLAN.md ← Phase-by-phase dev plan with checkboxes + ├── DATABASE.md ← Complete database schema + RLS policies + ├── DESIGN-SYSTEM.md ← Colors, fonts, components, patterns + └── SCREENS.md ← Screen-by-screen reference for all 3 apps +``` + +## Tech Stack + +| Layer | Choice | Notes | +|---|---|---| +| Mobile | React Native (Expo) | `npx create-expo-app` with TypeScript | +| Admin | React + Vite | Separate web app | +| Language | TypeScript | Everywhere | +| Navigation | React Navigation | Bottom tabs + stack | +| State | Zustand | Lightweight stores | +| Backend | Supabase | Auth, Postgres DB, Storage, Realtime, Edge Functions | +| Push | Expo Notifications | Via Supabase Edge Function triggers | + +## Key Architecture Decisions + +### 1. One mobile app, two views +Member and Org use the **same Expo app**. After login, the app checks `user.role` and shows the appropriate navigation: +- `role === "member"` → Member tabs (Home, My Champs, Search, Profile) +- `role === "organization"` → Org tabs (Dashboard, Settings) + +### 2. Everything is scoped per-championship +Members, results, categories, rules, fees, judges — all belong to a specific championship. There is no "global members list" for an org. Each championship is self-contained. + +### 3. Configurable tabs (Org) +Orgs don't fill a giant wizard. They quick-create a championship (name + date + location), then configure each section (Categories, Fees, Rules, Judges) at their own pace. Each section has a "✓ Mark as Done" button. Championship can only go live when all sections are done. + +### 4. Approval flow +- **Verified orgs** → "Go Live" sets status to `live` immediately (auto-approved) +- **Unverified orgs** → "Go Live" sets status to `pending_approval` → admin must approve + +### 5. Registration dates (not deadline) +Championships have: `event_date`, `reg_start`, `reg_end`. Registration close date must be before event date. No single "deadline" field. + +### 6. Judges = People, Scoring = Rules +The "Judges" tab shows judge profiles (name, instagram, bio). Scoring criteria and penalties live in the "Rules" tab. + +## Conventions + +### Code Style +- Functional components only, no class components +- Use hooks: `useState`, `useEffect`, custom hooks for data fetching +- Zustand for global state (auth, current user, championships cache) +- Local state for UI-only state (modals, form inputs, filters) +- TypeScript strict mode + +### Naming +- Files: `kebab-case.ts` / `kebab-case.tsx` +- Components: `PascalCase` +- Hooks: `useCamelCase` +- Zustand stores: `use[Name]Store` +- DB tables: `snake_case` +- DB columns: `snake_case` + +### Supabase Patterns +```typescript +// Client init +import { createClient } from '@supabase/supabase-js' +const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY) + +// Fetching +const { data, error } = await supabase + .from('championships') + .select('*, disciplines(*), fees(*)') + .eq('status', 'live') + +// Realtime subscription +supabase.channel('registrations') + .on('postgres_changes', { event: '*', schema: 'public', table: 'registrations' }, handler) + .subscribe() +``` + +### Navigation Pattern +```typescript +// Member +const MemberTabs = () => ( + + + + + + +) + +// Org +const OrgTabs = () => ( + + + + +) +``` + +## Important Docs + +Before coding any feature, read the relevant doc: + +| Doc | When to read | +|---|---| +| `docs/SPEC.md` | Full feature spec — read first for any new feature | +| `docs/PLAN.md` | Dev plan with phases — check what's next | +| `docs/DATABASE.md` | Schema — read before any DB work | +| `docs/DESIGN-SYSTEM.md` | UI — read before any screen work | +| `docs/SCREENS.md` | Screen details — read before building specific screens | + +## Quick Commands + +```bash +# Start mobile app +cd apps/mobile && npx expo start + +# Start admin panel +cd apps/admin && npm run dev + +# Supabase local dev +npx supabase start +npx supabase db reset # Reset + re-seed + +# Generate types from Supabase +npx supabase gen types typescript --local > packages/shared/database.types.ts +``` + +## Current Status + +Prototypes completed (JSX files in `/prototypes`): +- `dance-champ-mvp.jsx` — Member app prototype +- `dance-champ-org.jsx` — Org app prototype +- `dance-champ-admin.jsx` — Admin panel prototype + +These are reference implementations showing the exact UI, data structure, and flows. Use them as visual guides — don't copy the code directly (they're single-file React prototypes, not production React Native). diff --git a/dancechamp-claude-code/docs/DATABASE.md b/dancechamp-claude-code/docs/DATABASE.md new file mode 100644 index 0000000..1c8e83f --- /dev/null +++ b/dancechamp-claude-code/docs/DATABASE.md @@ -0,0 +1,357 @@ +# DanceChamp — Database Schema + +## Overview + +Backend: **Supabase** (PostgreSQL + Auth + Storage + Realtime) + +All tables use `uuid` primary keys generated by `gen_random_uuid()`. +All tables have `created_at` and `updated_at` timestamps. + +--- + +## Tables + +### users +Extended from Supabase Auth. This is a `public.users` table that mirrors `auth.users` via trigger. + +```sql +create table public.users ( + id uuid primary key references auth.users(id) on delete cascade, + email text not null, + name text not null, + role text not null check (role in ('admin', 'organization', 'member')), + city text, + instagram_handle text, + experience_years integer, + disciplines text[] default '{}', -- ['Pole Exotic', 'Pole Art'] + auth_provider text default 'email', -- 'email' | 'google' | 'instagram' + avatar_url text, + status text not null default 'active' check (status in ('active', 'warned', 'blocked')), + warn_reason text, + block_reason text, + created_at timestamptz default now(), + updated_at timestamptz default now() +); +``` + +### organizations +One-to-one with a user (role = 'organization'). + +```sql +create table public.organizations ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references public.users(id) on delete cascade, + name text not null, + instagram_handle text, + email text, + city text, + logo_url text, + verified boolean not null default false, + status text not null default 'pending' check (status in ('active', 'pending', 'blocked')), + block_reason text, + created_at timestamptz default now(), + updated_at timestamptz default now() +); +``` + +### championships +Belongs to an organization. Core entity. + +```sql +create table public.championships ( + id uuid primary key default gen_random_uuid(), + org_id uuid not null references public.organizations(id) on delete cascade, + name text not null, + subtitle text, + event_date text, -- "May 30, 2026" or ISO date + reg_start text, -- registration opens + reg_end text, -- registration closes (must be before event_date) + location text, -- "Minsk, Belarus" + venue text, -- "Prime Hall" + accent_color text default '#D4145A', + image_emoji text default '💃', + status text not null default 'draft' check (status in ('draft', 'pending_approval', 'live', 'completed', 'blocked')), + -- configurable sections progress + config_info boolean not null default false, + config_categories boolean not null default false, + config_fees boolean not null default false, + config_rules boolean not null default false, + config_judges boolean not null default false, + -- links + form_url text, -- Google Forms URL + rules_doc_url text, -- Rules document URL + created_at timestamptz default now(), + updated_at timestamptz default now() +); +``` + +### disciplines +Championship has many disciplines. Each discipline has levels. + +```sql +create table public.disciplines ( + id uuid primary key default gen_random_uuid(), + championship_id uuid not null references public.championships(id) on delete cascade, + name text not null, -- "Exotic Pole Dance" + levels text[] default '{}', -- ['Beginners', 'Amateur', 'Semi-Pro', 'Profi', 'Elite'] + sort_order integer default 0, + created_at timestamptz default now() +); +``` + +### styles +Championship-level styles (not per-discipline). + +```sql +create table public.styles ( + id uuid primary key default gen_random_uuid(), + championship_id uuid not null references public.championships(id) on delete cascade, + name text not null, -- "Classic", "Flow", "Theater" + sort_order integer default 0, + created_at timestamptz default now() +); +``` + +### fees +One-to-one with championship. + +```sql +create table public.fees ( + id uuid primary key default gen_random_uuid(), + championship_id uuid not null unique references public.championships(id) on delete cascade, + video_selection text, -- "50 BYN / 1,500 RUB" + solo text, -- "280 BYN / 7,500 RUB" + duet text, -- "210 BYN / 5,800 RUB pp" + "group" text, -- "190 BYN / 4,500 RUB pp" + refund_note text, + created_at timestamptz default now(), + updated_at timestamptz default now() +); +``` + +### rules +Championship has many rules across sections. + +```sql +create table public.rules ( + id uuid primary key default gen_random_uuid(), + championship_id uuid not null references public.championships(id) on delete cascade, + section text not null check (section in ('general', 'costume', 'scoring', 'penalty')), + name text not null, -- rule text or criterion name + value text, -- for scoring: "10" (max), for penalty: "-2" or "DQ" + sort_order integer default 0, + created_at timestamptz default now() +); +``` + +### judges +Championship has many judges. + +```sql +create table public.judges ( + id uuid primary key default gen_random_uuid(), + championship_id uuid not null references public.championships(id) on delete cascade, + name text not null, + instagram text, + bio text, + photo_url text, + sort_order integer default 0, + created_at timestamptz default now() +); +``` + +### registrations +Links a member to a championship. Tracks the 10-step progress. + +```sql +create table public.registrations ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references public.users(id) on delete cascade, + championship_id uuid not null references public.championships(id) on delete cascade, + discipline_id uuid references public.disciplines(id), + level text, -- "Semi-Pro" + style text, -- "Classic" + participation_type text default 'solo' check (participation_type in ('solo', 'duet', 'group')), + -- Progress steps (step 1–10) + step_rules_reviewed boolean default false, + step_category_selected boolean default false, + step_video_recorded boolean default false, + step_form_submitted boolean default false, + step_video_fee_paid boolean default false, -- confirmed by org + step_video_fee_receipt_url text, -- uploaded receipt + step_results text check (step_results in ('pending', 'passed', 'failed')), + step_champ_fee_paid boolean default false, + step_champ_fee_receipt_url text, + step_about_me_submitted boolean default false, + step_insurance_confirmed boolean default false, + step_insurance_doc_url text, + -- Video + video_url text, + -- Meta + current_step integer default 1, + created_at timestamptz default now(), + updated_at timestamptz default now(), + unique(user_id, championship_id) +); +``` + +### notifications +Push to member's in-app feed. + +```sql +create table public.notifications ( + id uuid primary key default gen_random_uuid(), + user_id uuid not null references public.users(id) on delete cascade, + championship_id uuid references public.championships(id) on delete set null, + type text not null check (type in ( + 'category_changed', 'payment_confirmed', 'results', + 'deadline_reminder', 'registration_confirmed', 'announcement', + 'champ_approved', 'champ_rejected', 'org_approved', 'org_rejected' + )), + title text not null, + message text not null, + read boolean not null default false, + created_at timestamptz default now() +); +``` + +### activity_logs +Admin audit trail. + +```sql +create table public.activity_logs ( + id uuid primary key default gen_random_uuid(), + actor_id uuid references public.users(id) on delete set null, + action text not null, -- "org_approved", "user_blocked", "champ_auto_approved" + target_type text not null, -- "organization", "championship", "user" + target_id uuid, + target_name text, -- denormalized for display + details jsonb, -- extra context + created_at timestamptz default now() +); +``` + +--- + +## Relationships Diagram + +``` +users (1) ──── (1) organizations + │ + │ has many + ▼ + championships + ┌────┼────┬────┬────┐ + │ │ │ │ │ + disciplines styles fees rules judges + │ + registrations ─┘ + (user + championship) + │ + notifications +``` + +--- + +## Row Level Security (RLS) + +Enable RLS on all tables. + +### users +```sql +-- Members can read/update their own row +create policy "Users can read own" on users for select using (auth.uid() = id); +create policy "Users can update own" on users for update using (auth.uid() = id); + +-- Org admins can read members registered to their championships +create policy "Orgs can read their members" on users for select using ( + id in ( + select r.user_id from registrations r + join championships c on r.championship_id = c.id + join organizations o on c.org_id = o.id + where o.user_id = auth.uid() + ) +); + +-- Admin can read/update all +create policy "Admin full access" on users for all using ( + exists (select 1 from users where id = auth.uid() and role = 'admin') +); +``` + +### championships +```sql +-- Anyone can read live championships +create policy "Public read live" on championships for select using (status = 'live'); + +-- Org can CRUD their own +create policy "Org manages own" on championships for all using ( + org_id in (select id from organizations where user_id = auth.uid()) +); + +-- Admin full access +create policy "Admin full access" on championships for all using ( + exists (select 1 from users where id = auth.uid() and role = 'admin') +); +``` + +### registrations +```sql +-- Members can read/create their own +create policy "Member own registrations" on registrations for select using (user_id = auth.uid()); +create policy "Member can register" on registrations for insert with check (user_id = auth.uid()); + +-- Org can read/update registrations for their championships +create policy "Org manages registrations" on registrations for all using ( + championship_id in ( + select c.id from championships c + join organizations o on c.org_id = o.id + where o.user_id = auth.uid() + ) +); + +-- Admin full access +create policy "Admin full access" on registrations for all using ( + exists (select 1 from users where id = auth.uid() and role = 'admin') +); +``` + +### notifications +```sql +-- Users can read their own notifications +create policy "Read own" on notifications for select using (user_id = auth.uid()); +-- Users can mark their own as read +create policy "Update own" on notifications for update using (user_id = auth.uid()); +``` + +--- + +## Storage Buckets + +``` +receipts/ -- Payment receipt screenshots + {user_id}/{registration_id}/receipt.jpg + +insurance/ -- Insurance documents + {user_id}/{registration_id}/insurance.pdf + +judge-photos/ -- Judge profile photos + {championship_id}/{judge_id}.jpg + +org-logos/ -- Organization logos + {org_id}/logo.jpg +``` + +--- + +## Seed Data + +For development, seed with: +- 1 admin user +- 2 organizations (1 verified, 1 unverified/pending) +- 2 championships for verified org (1 live, 1 draft) +- 1 championship for unverified org (pending_approval) +- 7 member users with registrations at various progress stages +- Sample notifications, activity logs + +This matches the prototype demo data. diff --git a/dancechamp-claude-code/docs/DESIGN-SYSTEM.md b/dancechamp-claude-code/docs/DESIGN-SYSTEM.md new file mode 100644 index 0000000..9725ccd --- /dev/null +++ b/dancechamp-claude-code/docs/DESIGN-SYSTEM.md @@ -0,0 +1,258 @@ +# DanceChamp — Design System + +## Theme: Dark Luxury + +The app has a premium dark aesthetic. Think high-end dance competition branding — elegant, minimal, confident. + +--- + +## Colors + +### Core Palette +``` +Background: #08070D (near-black with slight purple) +Card: #12111A (elevated surface) +Card Hover: #1A1926 (pressed/active state) +Border: #1F1E2E (subtle separator) +Text Primary: #F2F0FA (off-white) +Text Dim: #5E5C72 (labels, placeholders) +Text Mid: #8F8DA6 (secondary info) +``` + +### Accent Colors +``` +Pink (Primary): #D4145A ← Member app + Org app default +Purple: #7C3AED ← Secondary accent (styles, alt champ branding) +Indigo: #6366F1 ← Admin panel accent +``` + +### Semantic Colors +``` +Green: #10B981 (success, passed, active, confirmed) +Yellow: #F59E0B (warning, pending, draft) +Red: #EF4444 (error, failed, blocked, danger) +Blue: #60A5FA (info, links, video) +Orange: #F97316 (warned status, awaiting review) +``` + +### Transparent Variants +Each semantic color has a 10% opacity background: +``` +Green Soft: rgba(16,185,129,0.10) +Yellow Soft: rgba(245,158,11,0.10) +Red Soft: rgba(239,68,68,0.10) +Blue Soft: rgba(96,165,250,0.10) +Purple Soft: rgba(139,92,246,0.10) +``` + +For accent overlays use 15% opacity: `${color}15` +For accent borders use 30% opacity: `${color}30` + +### Per-Championship Branding +Each championship can have its own accent color: +- Zero Gravity: `#D4145A` (pink) +- Pole Star: `#7C3AED` (purple) + +This color is used for the championship's tab highlights, buttons, and member tags. + +--- + +## Typography + +### Font Stack +``` +Display: 'Playfair Display', Georgia, serif ← Headings, numbers, titles +Body: 'DM Sans', 'Segoe UI', sans-serif ← Body text, labels, buttons +Mono: 'JetBrains Mono', monospace ← Badges, timestamps, codes, small labels +``` + +### Sizes & Usage +``` +Screen title: Playfair Display, 20px, 700 weight +Section title: Playfair Display, 14px, 700 weight, Text Mid color +Card title: DM Sans, 14-16px, 600 weight +Body text: DM Sans, 12-13px, 400 weight +Small label: JetBrains Mono, 9-10px, 500 weight, uppercase, letter-spacing 0.3-0.5 +Badge: JetBrains Mono, 8px, 700 weight, uppercase, letter-spacing 0.8 +Stat number: Playfair Display, 16-20px, 700 weight +``` + +--- + +## Components + +### Card +``` +Background: #12111A +Border: 1px solid #1F1E2E +Border Radius: 14px +Padding: 16px +``` + +### Status Badge +Small pill with semantic color + soft background. +``` +Font: JetBrains Mono, 8px, 700 weight, uppercase +Padding: 3px 8px +Border Radius: 4px +``` + +Status mappings: +| Status | Label | Color | Background | +|---|---|---|---| +| active / live | ACTIVE / LIVE | Green | Green Soft | +| pending | PENDING | Yellow | Yellow Soft | +| pending_approval | AWAITING REVIEW | Orange | Orange Soft | +| draft | DRAFT | Dim | Dim 15% | +| blocked | BLOCKED | Red | Red Soft | +| warned | WARNED | Orange | Orange Soft | +| passed | PASSED | Green | Green Soft | +| failed | FAILED | Red | Red Soft | + +### Tab Bar (in-screen tabs, not bottom nav) +``` +Container: horizontal scroll, no scrollbar, gap 3px +Tab: JetBrains Mono, 9px, 600 weight +Active: accent color text, accent 15% bg, accent 30% border +Inactive: Dim color text, transparent bg +Border Radius: 16px (pill shape) +Padding: 5px 10px +``` + +Configurable tabs have a status dot (6px circle): +- Green dot = section configured ✓ +- Yellow dot = section pending + +### Input Field +``` +Background: #08070D (same as page bg) +Border: 1px solid #1F1E2E +Border Radius: 10px +Padding: 10px 12px +Font: DM Sans, 13px +Label: JetBrains Mono, 9px, uppercase, Dim color, 6px margin bottom +``` + +### Action Button +Two variants: +- **Filled**: solid background, white text (for primary actions) +- **Outline**: transparent bg, colored border 30%, colored text (for secondary/danger) + +``` +Padding: 8px 14px +Border Radius: 8px +Font: DM Sans, 11px, 700 weight +``` + +### Tag Editor +For lists of editable items (rules, levels, styles): +``` +Tag: DM Sans 11px, colored bg 10%, colored border 25%, 4px 10px padding, 16px radius +Remove (×): 10px, Dim color +Add input: same as Input Field but smaller (8px 12px, 12px font) +Add button: colored bg, white "+" text, 8px 14px +``` + +### Header +``` +Padding: 14px 20px 6px +Title: Playfair Display, 20px, 700 +Subtitle: DM Sans, 11px, Dim color +Back button: 32×32px, Card bg, Border, 8px radius, "←" centered +``` + +### Bottom Navigation +``` +Border top: 1px solid #1F1E2E +Padding: 10px 0 8px +Items: flex, space-around +Icon: 18px emoji +Label: JetBrains Mono, 8px, letter-spacing 0.3 +Active: opacity 1 +Inactive: opacity 0.35 +``` + +--- + +## Patterns + +### Progress/Setup Checklist +For configurable tabs on org side: +``` +Each row: + [Checkbox 22×22] [Label capitalize] [Configure › or ✓] + +Checkbox: 6px radius, 2px border + Done: Green border, Green Soft bg, "✓" inside + Pending: Yellow border, transparent bg + +Label done: Dim color, line-through +Label pending: Text Primary, clickable → navigates to tab +``` + +### Readiness Bar (dashboard cards) +``` +Track: 4px height, Border color bg, 2px radius +Fill: accent color, width = (done/total * 100)% +Below: list of section names with ✓ (green) or ○ (yellow) +``` + +### Member Card +``` +Container: Card style, 12px padding +Name: DM Sans 13px, 600 weight +Instagram: JetBrains Mono 10px, accent color +Tags: DM Sans 9px, Mid color, Mid 10% bg, 2px 7px padding, 10px radius +Status badge: top-right corner +``` + +### Stat Box +``` +Container: Card style, 10px 6px padding, centered +Number: Playfair Display, 16-20px, 700 weight, semantic color +Label: JetBrains Mono, 7px, uppercase, Dim color +``` + +--- + +## Phone Frame (for prototypes) + +``` +Width: 375px +Height: 740px +Border Radius: 36px +Border: 1.5px solid #1F1E2E +Shadow: 0 0 80px rgba(accent, 0.06), 0 20px 40px rgba(0,0,0,0.5) + +Status bar: 8px 24px padding + Time: JetBrains Mono 11px, Dim + Notch: 100×28px black, 14px radius + Indicators: "●●●" JetBrains Mono 11px, Dim +``` + +--- + +## React Native Adaptation + +The prototypes use inline styles. For React Native: + +| Prototype | React Native | +|---|---| +| `div` | `View` | +| `span`, `p`, `h1` | `Text` | +| `input` | `TextInput` | +| `onClick` | `onPress` (via `Pressable` or `TouchableOpacity`) | +| `overflow: auto` | `ScrollView` or `FlatList` | +| `cursor: pointer` | Not needed | +| `border: 1px solid` | `borderWidth: 1, borderColor:` | +| `fontFamily: 'DM Sans'` | Loaded via `expo-font` | +| `gap` | Use `marginBottom` on children (gap not fully supported) | +| `overflowX: auto` with scrollbar hidden | `ScrollView horizontal showsHorizontalScrollIndicator={false}` | + +### Fonts Loading (Expo) +```typescript +import { useFonts } from 'expo-font'; +import { PlayfairDisplay_700Bold } from '@expo-google-fonts/playfair-display'; +import { DMSans_400Regular, DMSans_500Medium, DMSans_600SemiBold } from '@expo-google-fonts/dm-sans'; +import { JetBrainsMono_400Regular, JetBrainsMono_500Medium, JetBrainsMono_700Bold } from '@expo-google-fonts/jetbrains-mono'; +``` diff --git a/dancechamp-claude-code/docs/PLAN.md b/dancechamp-claude-code/docs/PLAN.md new file mode 100644 index 0000000..5bd1d75 --- /dev/null +++ b/dancechamp-claude-code/docs/PLAN.md @@ -0,0 +1,316 @@ +# DanceChamp — Vibe Coding Plan + +## How to use this plan +- Work phase by phase, top to bottom +- Check off tasks as you go: `[ ]` → `[x]` +- Each phase has a **"Done when"** — don't move on until it's met +- 🔴 = blocker (must do), 🟡 = important, 🟢 = nice to have for MVP +- Estimated time is for vibe coding with AI (Claude Code / Cursor) + +--- + +## Phase 0: Project Setup +**Time: ~1 hour** + +- [ ] 🔴 Init Expo project (React Native): `npx create-expo-app DanceChamp --template blank-typescript` +- [ ] 🔴 Init Web admin panel: `npm create vite@latest admin-panel -- --template react-ts` +- [ ] 🔴 Setup Supabase project (or Firebase): create account, new project +- [ ] 🔴 Setup database tables (see Phase 1) +- [ ] 🔴 Install core deps: `react-navigation`, `zustand`, `supabase-js` +- [ ] 🟡 Setup Git repo + `.gitignore` +- [ ] 🟡 Create `/apps/mobile`, `/apps/admin`, `/packages/shared` monorepo structure +- [ ] 🟢 Add ESLint + Prettier + +**Done when:** Both apps run locally, Supabase dashboard is accessible + +--- + +## Phase 1: Database & Auth +**Time: ~2-3 hours** + +### 1.1 Database Tables +- [ ] 🔴 `users` — id, email, name, role (admin | organization | member), city, instagram_handle, experience_years, disciplines[], auth_provider, status, created_at +- [ ] 🔴 `organizations` — id, user_id (FK), name, instagram_handle, email, city, logo_url, verified (bool), status (active | pending | blocked), block_reason, created_at +- [ ] 🔴 `championships` — id, org_id (FK), name, subtitle, event_date, reg_start, reg_end, location, venue, status (draft | pending_approval | live | completed | blocked), accent_color, created_at +- [ ] 🔴 `disciplines` — id, championship_id (FK), name, levels[], styles[] +- [ ] 🔴 `fees` — id, championship_id (FK), video_selection, solo, duet, group, refund_note +- [ ] 🔴 `rules` — id, championship_id (FK), section (general | costume | scoring | penalty), text, value (for penalties) +- [ ] 🔴 `judges` — id, championship_id (FK), name, instagram, bio, photo_url +- [ ] 🔴 `registrations` — id, user_id (FK), championship_id (FK), discipline_id, level, style, type (solo | duet | group), current_step, video_url, fee_paid, receipt_uploaded, insurance_uploaded, passed (null | true | false), created_at +- [ ] 🔴 `notifications` — id, user_id (FK), championship_id, type, title, message, read (bool), created_at +- [ ] 🟡 `activity_logs` — id, actor_id, action, target_type, target_id, details, created_at + +### 1.2 Auth +- [ ] 🔴 Supabase Auth: enable Email + Google OAuth +- [ ] 🔴 Role-based access: Row Level Security (RLS) policies + - Members see only their own registrations + - Orgs see only their own championships & members + - Admin sees everything +- [ ] 🔴 Sign up / Sign in screens (mobile) +- [ ] 🔴 Admin login (web panel) +- [ ] 🟡 Instagram OAuth (for member profiles) +- [ ] 🟡 Onboarding flow: name → city → discipline → experience → done + +**Done when:** Can sign up as member, org, and admin. RLS blocks cross-access. + +--- + +## Phase 2: Member App — Core Screens +**Time: ~4-5 hours** + +### 2.1 Navigation +- [ ] 🔴 Bottom tab nav: Home, My Champs, Search, Profile +- [ ] 🔴 Stack navigation: screens → detail → sub-screens + +### 2.2 Home Screen +- [ ] 🔴 "Upcoming championships" feed — cards with name, date, location, status badge +- [ ] 🔴 "My active registrations" section with progress bars +- [ ] 🟡 Bell icon → notifications feed +- [ ] 🟡 Deadline urgency banners ("Registration closes in 3 days!") + +### 2.3 Championship Detail +- [ ] 🔴 Header: name, dates, location, venue, registration period +- [ ] 🔴 Tab: Overview (info + registration funnel) +- [ ] 🔴 Tab: Categories (disciplines, levels, styles + eligibility) +- [ ] 🔴 Tab: Rules (general, costume, scoring criteria, penalties) +- [ ] 🔴 Tab: Fees (video selection + championship fees) +- [ ] 🔴 Tab: Judges (judge profiles with photo, instagram, bio) +- [ ] 🔴 "Register" button → starts onboarding + +### 2.4 Search & Discover +- [ ] 🔴 Search by championship name +- [ ] 🔴 Filter by: discipline, location, status (open/upcoming/past) +- [ ] 🟡 Sort by: date, popularity + +### 2.5 Profile +- [ ] 🔴 View/edit: name, city, instagram, disciplines, experience +- [ ] 🔴 "My Championships" list (past + active) +- [ ] 🟢 Competition history + +**Done when:** Can browse championships, view full details across all tabs, search/filter, see profile. + +--- + +## Phase 3: Member App — Registration & Progress Tracker +**Time: ~4-5 hours** + +### 3.1 Registration Flow +- [ ] 🔴 Choose discipline → level → style → solo/duet/group +- [ ] 🔴 Create `registration` record in DB +- [ ] 🔴 Show 10-step progress checklist + +### 3.2 Progress Steps (per championship) +- [ ] 🔴 Step 1: Review rules — mark done when user opens Rules tab +- [ ] 🔴 Step 2: Select category — saved from registration +- [ ] 🔴 Step 3: Record video — manual toggle ("I've recorded my video") +- [ ] 🔴 Step 4: Submit video form — manual toggle or link to Google Form +- [ ] 🔴 Step 5: Pay video fee — upload receipt screenshot +- [ ] 🔴 Step 6: Wait for results — shows "pending" until org decides +- [ ] 🔴 Step 7: Results — auto-updates when org passes/fails +- [ ] 🔴 Step 8: Pay championship fee — upload receipt (only if passed) +- [ ] 🔴 Step 9: Submit "About Me" — manual toggle or link +- [ ] 🔴 Step 10: Confirm insurance — upload document + +### 3.3 Receipt & Document Upload +- [ ] 🔴 Camera / gallery picker for receipt photos +- [ ] 🔴 Upload to Supabase Storage +- [ ] 🔴 Show upload status (pending org confirmation) + +### 3.4 Notifications +- [ ] 🔴 In-app notification feed (bell icon + unread count) +- [ ] 🔴 Notification types: category changed, payment confirmed, results, deadline reminder, announcement +- [ ] 🟡 Push notifications via Expo Notifications +- [ ] 🟢 Notification preferences (toggle on/off) + +**Done when:** Can register for a championship, track all 10 steps, upload receipts, receive notifications. + +--- + +## Phase 4: Org App — Dashboard & Championship Management +**Time: ~5-6 hours** + +### 4.1 Org Dashboard +- [ ] 🔴 Championship cards: name, dates, status badge, member count, progress bar (if draft) +- [ ] 🔴 "+" button → Quick Create (name, date, location → creates draft) +- [ ] 🔴 Tap card → championship detail + +### 4.2 Championship Detail (tabbed, configurable) +- [ ] 🔴 Overview tab: setup progress checklist, event info (editable), stats (if live) +- [ ] 🔴 Categories tab: add/remove levels, add/remove styles → "Mark as Done" +- [ ] 🔴 Fees tab: video selection + solo/duet/group fees → "Mark as Done" +- [ ] 🔴 Rules tab: general rules + costume rules + scoring criteria + penalties → "Mark as Done" +- [ ] 🔴 Judges tab: add judge profiles (name, instagram, bio) → "Mark as Done" +- [ ] 🔴 "Go Live" button — appears when all sections are done + - [ ] 🔴 If org is verified → status = `live` (auto-approved) + - [ ] 🔴 If org is unverified → status = `pending_approval` (needs admin) + +### 4.3 Members Tab (only for live championships) +- [ ] 🔴 Member list with search + filters (All, Receipts, Videos, Passed) +- [ ] 🔴 Member card: name, instagram, level, style, status badge, progress +- [ ] 🔴 Tap member → member detail + +### 4.4 Member Detail +- [ ] 🔴 Profile info, registration details +- [ ] 🔴 Edit level (picker + "member will be notified" warning) +- [ ] 🔴 Edit style (picker + notification) +- [ ] 🔴 Video section: view link + Pass/Fail buttons +- [ ] 🔴 Payment section: view receipt + Confirm button +- [ ] 🔴 "Send Notification" button + +### 4.5 Results Tab +- [ ] 🔴 Pending review list with Pass/Fail buttons per member +- [ ] 🔴 Decided list (passed/failed) +- [ ] 🔴 "Publish Results" button → notifies all members + +### 4.6 Org Settings +- [ ] 🔴 Edit org profile (name, instagram) +- [ ] 🔴 Notification preferences (toggles) +- [ ] 🟡 Connected accounts (Instagram, Gmail, Telegram) + +**Done when:** Org can create championship, configure all tabs, go live, manage members, pass/fail videos, publish results. + +--- + +## Phase 5: Admin Panel (Web) +**Time: ~3-4 hours** + +### 5.1 Dashboard +- [ ] 🔴 Platform stats: orgs count, live champs, total users +- [ ] 🔴 "Needs Attention" section: pending orgs, pending champs (from unverified orgs) +- [ ] 🔴 Platform health: revenue, blocked users +- [ ] 🔴 Recent activity log + +### 5.2 Organizations Management +- [ ] 🔴 List with search + filters (Active, Pending, Blocked) +- [ ] 🔴 Org detail: profile, championships list, approval policy +- [ ] 🔴 Actions: Approve / Reject, Block / Unblock, Verify, Delete + +### 5.3 Championships Management +- [ ] 🔴 List with search + filters (Live, Awaiting Review, Draft, Blocked) +- [ ] 🔴 Champ detail: stats, approval policy indicator +- [ ] 🔴 Actions: Approve / Reject (for unverified orgs), Suspend, Reinstate, Delete + +### 5.4 Users Management +- [ ] 🔴 List with search + filters (Active, Warned, Blocked, Org Admins) +- [ ] 🔴 User detail: profile, role, championships joined +- [ ] 🔴 Actions: Warn, Block / Unblock, Delete + +**Done when:** Admin can approve/reject orgs, review championships from unverified orgs, manage users. + +--- + +## Phase 6: Connecting It All +**Time: ~3-4 hours** + +### 6.1 Real-Time Sync +- [ ] 🔴 Supabase Realtime: members see status updates instantly (pass/fail, payment confirmed) +- [ ] 🔴 Org dashboard updates when new member registers +- [ ] 🟡 Admin panel live counters + +### 6.2 Notification Triggers +- [ ] 🔴 Org passes/fails video → member gets notification +- [ ] 🔴 Org confirms receipt → member gets notification +- [ ] 🔴 Org changes member's level/style → member gets notification +- [ ] 🔴 Org publishes results → all members get notification +- [ ] 🟡 Auto deadline reminders (cron job: 7 days, 3 days, 1 day before) + +### 6.3 Approval Flow +- [ ] 🔴 Unverified org clicks "Go Live" → status = pending_approval +- [ ] 🔴 Admin sees it in "Needs Attention" +- [ ] 🔴 Admin approves → status = live, org gets notification +- [ ] 🔴 Admin rejects → status = blocked, org gets notification with reason + +### 6.4 File Uploads +- [ ] 🔴 Receipt photo upload → Supabase Storage → org sees thumbnail in member detail +- [ ] 🔴 Insurance doc upload → same flow +- [ ] 🟢 Judge profile photos + +**Done when:** All three apps talk to the same DB. Actions in one app reflect in others in real time. + +--- + +## Phase 7: Polish & UX +**Time: ~2-3 hours** + +- [ ] 🟡 Loading states (skeletons, spinners) +- [ ] 🟡 Empty states ("No championships yet", "No members match") +- [ ] 🟡 Error handling (network errors, failed uploads, toast messages) +- [ ] 🟡 Pull-to-refresh on lists +- [ ] 🟡 Haptic feedback on key actions (pass/fail, payment confirm) +- [ ] 🟡 Dark theme consistency across all screens +- [ ] 🟡 Animations: tab transitions, card press, progress bar fill +- [ ] 🟢 Onboarding walkthrough (first-time user tips) +- [ ] 🟢 Swipe gestures on member cards (swipe right = pass, left = fail) + +**Done when:** App feels smooth and professional. No raw errors shown to users. + +--- + +## Phase 8: Integrations (Post-MVP) +**Time: varies** + +### 8.1 Instagram Parsing +- [ ] 🟢 Apify Instagram scraper setup +- [ ] 🟢 Claude API: extract structured data from post captions +- [ ] 🟢 OCR (Google Vision): parse results from photo posts +- [ ] 🟢 "Import from Instagram" button in org's championship creation + +### 8.2 Gmail Integration +- [ ] 🟢 Google OAuth for members +- [ ] 🟢 Detect Google Forms confirmation emails → auto-mark steps + +### 8.3 Payments +- [ ] 🟢 In-app payment tracking +- [ ] 🟢 Replace receipt uploads with direct payment + +### 8.4 Multi-Language +- [ ] 🟢 i18n setup (Russian + English) + +--- + +## Phase 9: Testing & Launch +**Time: ~2-3 hours** + +- [ ] 🔴 Test full flow: member registers → org reviews → admin monitors +- [ ] 🔴 Test approval flow: unverified org → pending → admin approves → live +- [ ] 🔴 Test notifications: level change, payment confirm, results +- [ ] 🔴 Test on real device (iOS + Android via Expo Go) +- [ ] 🟡 Test edge cases: empty championships, blocked orgs, duplicate registrations +- [ ] 🟡 Performance check: list with 50+ members, 10+ championships +- [ ] 🟡 Expo build: `eas build` for iOS/Android +- [ ] 🟢 TestFlight / Google Play internal testing +- [ ] 🟢 Admin panel deploy (Vercel / Netlify) + +**Done when:** All three roles can complete their full flow without bugs. + +--- + +## Quick Reference: What Goes Where + +| Feature | Member App | Org App | Admin Panel | +|---|:---:|:---:|:---:| +| Browse championships | ✅ | — | ✅ (view all) | +| Register for championship | ✅ | — | — | +| Progress tracker | ✅ | — | — | +| Create/edit championship | — | ✅ | ✅ (edit/delete) | +| Review members | — | ✅ | ✅ (view) | +| Pass/Fail videos | — | ✅ | — | +| Confirm payments | — | ✅ | — | +| Approve orgs & champs | — | — | ✅ | +| Block/warn users | — | — | ✅ | +| Notifications | ✅ (receive) | ✅ (send) | — | + +--- + +## Priority Order (if short on time) + +If you need to ship fast, do these phases in order and stop when you have enough: + +1. **Phase 0 + 1** — Foundation (can't skip) +2. **Phase 2** — Member app core (users need to see something) +3. **Phase 4** — Org app (orgs need to create championships) +4. **Phase 3** — Registration flow (connects member ↔ org) +5. **Phase 6** — Wire it together +6. **Phase 5** — Admin panel (can manage via Supabase dashboard temporarily) +7. **Phase 7** — Polish +8. **Phase 8** — Integrations (post-launch) diff --git a/dancechamp-claude-code/docs/SCREENS.md b/dancechamp-claude-code/docs/SCREENS.md new file mode 100644 index 0000000..f5c221b --- /dev/null +++ b/dancechamp-claude-code/docs/SCREENS.md @@ -0,0 +1,371 @@ +# DanceChamp — Screen Reference + +## Member App Screens + +### Navigation: Bottom Tabs +`Home` | `My Champs` | `Search` | `Profile` + +--- + +### M1: Home +**Purpose:** Dashboard for the dancer + +**Elements:** +- Header: "DanceChamp" title + bell icon (🔔) with unread count badge +- "Your Active" section: cards for championships they're registered in, showing progress bar (e.g. "Step 5/10") +- "Upcoming Championships" section: browseable cards for live championships +- Each card: championship name, org name, dates, location, status badge, accent color bar + +**Data:** `championships` (status = 'live') + `registrations` (for current user) + +**Navigation:** +- Tap card → M5 (Championship Detail) +- Tap bell → M7 (Notifications) + +--- + +### M2: My Champs +**Purpose:** All championships user is registered for + +**Elements:** +- Tabs: Active | Past +- Championship cards with progress indicator +- Status per card: "Step 3/10 — Pay video fee" or "✅ Registered" or "❌ Failed" + +**Data:** `registrations` joined with `championships` + +**Navigation:** +- Tap card → M6 (Progress Tracker) + +--- + +### M3: Search +**Purpose:** Discover championships + +**Elements:** +- Search bar (text input) +- Filter chips: All, Pole Exotic, Pole Art, Registration Open, Upcoming +- Championship result cards + +**Data:** `championships` + `organizations` (for org name) + +**Navigation:** +- Tap card → M5 (Championship Detail) + +--- + +### M4: Profile +**Purpose:** User profile and settings + +**Elements:** +- Avatar, name, instagram handle, city +- Dance profile: disciplines, experience years +- "My Championships" summary (X active, Y completed) +- Settings list: Edit Profile, Notification Preferences, Connected Accounts, Help, Log Out + +**Data:** `users` (current user) + +--- + +### M5: Championship Detail +**Purpose:** Full championship info — 5 tabs + +**Header:** Championship name, org name, back button +**Tabs:** Overview | Categories | Rules | Fees | Judges + +#### Tab: Overview +- Event info: date, location, venue, registration period (start → end) +- Stats: members registered, spots left (if limited) +- "Register" button (if registration open and user not registered) +- If already registered: shows current progress step + +#### Tab: Categories +- Disciplines list, each with levels +- Styles list +- If registered: user's selected level/style highlighted + +#### Tab: Rules +- General rules (expandable list) +- Costume rules +- Scoring criteria: name + max score (0–10) +- Penalties: name + deduction / DQ + +#### Tab: Fees +- Video selection fee +- Championship fees: solo, duet, group +- Refund note + +#### Tab: Judges +- Judge profile cards: photo/emoji, name, instagram link, bio + +**Data:** Full championship with all related tables + +**Navigation:** +- "Register" → M6 (starts registration flow, then shows Progress Tracker) + +--- + +### M6: Progress Tracker +**Purpose:** 10-step registration checklist for a specific championship + +**Header:** Championship name, back button + +**Elements:** +- Vertical step list (1–10) +- Each step: number, icon, title, status (locked/available/done/failed) +- Current step expanded with action area: + - Step 3 "Record Video": toggle "I've recorded my video" + - Step 5 "Pay Video Fee": upload receipt button, status after upload + - Step 7 "Results": shows PASS ✅ / FAIL ❌ / ⏳ Pending + - Step 10 "Insurance": upload document button +- Progress bar at top: X/10 completed + +**Data:** `registrations` (single record for this user + championship) + +**Actions:** Update step fields in `registrations` table + +--- + +### M7: Notifications +**Purpose:** In-app notification feed + +**Header:** "Notifications" + "Read all" button + +**Elements:** +- Notification cards: icon, type badge, championship name, message, time ago +- Unread: colored left border + accent background tint + dot indicator +- Tap: marks as read + +**Types:** 🔄 Category Changed, ✅ Payment Confirmed, 🏆 Results, ⏰ Deadline, 📋 Registered, 📢 Announcement + +**Data:** `notifications` (for current user, ordered by created_at desc) + +--- + +## Org App Screens + +### Navigation: Bottom Tabs +`Dashboard` | `Settings` + +--- + +### O1: Dashboard +**Purpose:** Overview of all org's championships + +**Elements:** +- Header: "Dashboard" + org name + org logo +- "New Championship" card (accent gradient, "+" icon) +- Championship cards: name, dates, location, status badge (LIVE / SETUP 3/5 / AWAITING REVIEW) +- For drafts: readiness bar + section checklist (info ✓, categories ○, fees ○, etc.) +- For live: mini stats (Members, Passed, Pending) + +**Data:** `championships` (where org_id = current org) + `registrations` (counts) + +**Navigation:** +- Tap "New Championship" → O2 (Quick Create) +- Tap championship card → O3 (Championship Detail) + +--- + +### O2: Quick Create +**Purpose:** Minimal form to create a draft championship + +**Elements:** +- Header: "New Championship" + back button +- 3 inputs: Championship Name, Event Date, Location +- Info card: "Your championship will be created as a draft. Configure details later." +- "✨ Create Draft" button (disabled until name filled) + +**Action:** Creates championship with status = 'draft', navigates to O3 + +--- + +### O3: Championship Detail (Tabbed) +**Purpose:** Configure and manage a championship + +**Header:** Championship name, subtitle, back button, "🚀 Go Live" (if all config done) + +**Tabs (with config status dots):** +`Overview` | `Categories` (🟢/🟡) | `Fees` (🟢/🟡) | `Rules` (🟢/🟡) | `Judges` (🟢/🟡) + +For live championships, add: | `Members (N)` | `Results` + +#### Tab: Overview +- **If draft:** Setup Progress checklist (5 items with checkmarks, tap incomplete → jumps to tab) +- **If all done:** "🚀 Open Registration" button (or "Go Live") +- Event Info card: inline edit (✎ Edit / ✕ Close toggle) + - Editable fields: name, subtitle, event date, location, venue + - Registration period: Opens (date) + Closes (date), side by side + - Warning: "⚠️ Registration close date must be before event date" +- **If live:** Stats boxes (Members, Passed, Failed, Pending) + "⚡ Needs Action" (receipts to review, videos to review) + +#### Tab: Categories +- Levels: tag editor (add/remove levels) +- Styles: tag editor (add/remove styles) +- "✓ Mark Categories as Done" button (shown when at least 1 level + 1 style) + +#### Tab: Fees +- Video Selection Fee input +- Championship Fees: Solo, Duet (pp), Group (pp) inputs +- "✓ Mark Fees as Done" button (shown when video fee filled) + +#### Tab: Rules +- General Rules: tag editor +- Costume Rules: tag editor +- Scoring Criteria (0–10): list + tag editor to add new +- Penalties: list with colored values (-2 yellow, DQ red) + tag editor +- "✓ Mark Rules as Done" button (shown when at least 1 rule) + +#### Tab: Judges +- Judge profile cards: emoji avatar, name, instagram, bio, × to remove +- "Add Judge" form: name, instagram, bio inputs + "+ Add Judge" button +- "✓ Mark Judges as Done" button (shown when at least 1 judge) + +#### Tab: Members (live only) +- Search bar +- Filter chips: All (N), 📸 Receipts (N), 🎬 Videos (N), ✅ Passed (N) +- Member cards: name, instagram, level, style, city tags, status badge +- Tap member → O4 (Member Detail) + +#### Tab: Results (live only) +- Stat boxes: Pending, Passed, Failed +- Pending review cards: member name/level + "🎥 View" + Pass/Fail buttons +- Decided list: member name + badge +- "📢 Publish Results" button + +--- + +### O4: Member Detail +**Purpose:** View/edit a single member's registration + +**Header:** Member name, championship + instagram, back button + +**Elements:** +- Profile card: avatar, name, instagram, city, status badge +- Registration section: + - Discipline (read-only) + - Type: solo/duet/group (read-only) + - **Level:** value + "✎ Edit" button → expands picker with ⚠️ "Member will be notified" + - **Style:** value + "✎ Edit" button → expands picker with ⚠️ warning +- Video section: link display + Pass/Fail buttons (if pending) or status badge +- Payment section: fee amount, receipt status, "📸 Confirm" button (if receipt uploaded) +- "🔔 Send Notification" button + +**Actions:** Update `registrations` fields + create `notifications` record + +--- + +### O5: Org Settings +**Purpose:** Organization profile and preferences + +**Elements:** +- Org profile: logo, name, instagram (editable inline when "Edit Organization Profile" tapped) +- Menu items: + - ✎ Edit Organization Profile → inline form (name + instagram) replaces menu + - 🔔 Notification Preferences → sub-screen with toggle switches + - 🔗 Connected Accounts → sub-screen (Instagram ✅, Gmail ✅, Telegram ❌) + - ❓ Help & Support + - 🚪 Log Out (red text) + +--- + +## Admin Panel Screens (Web) + +### Navigation: Bottom Tabs (mobile-style for prototype, sidebar for production) +`Overview` | `Orgs` | `Champs` | `Users` + +--- + +### A1: Overview (Dashboard) +**Purpose:** Platform health at a glance + +**Elements:** +- Header: "Admin Panel" + platform name + version +- Stat boxes: Active Orgs, Live Champs, Total Users +- "⚡ Needs Attention" card (yellow tint): + - 🏢 Organizations awaiting approval (count) → tap goes to A2 + - 🏆 Champs awaiting approval from unverified orgs (count) → tap goes to A4 +- Platform Health table: total revenue, active/total orgs, blocked users, avg members/champ +- Recent Activity log: action + target (colored by type) + date + actor + +--- + +### A2: Organizations List +**Purpose:** All organizations on the platform + +**Elements:** +- Search bar +- Filter chips: All (N), ✅ Active (N), ⏳ Pending (N), 🚫 Blocked (N) +- Org cards: logo, name, instagram, status badge, city, champs count, members count +- Tap → A3 (Org Detail) + +--- + +### A3: Organization Detail +**Purpose:** Review and manage a single org + +**Elements:** +- Profile card: logo, name, instagram, email, city, status badge +- Details table: Joined, Championships, Total members, Verified (✅/❌) +- Approval Policy card: + - Verified: "🛡️ Verified — Auto-approve events" (green tint) + - Unverified: "⏳ Unverified — Events need manual approval" (yellow tint) +- Championships list (belonging to this org) +- Block reason card (if blocked, red tint) +- Actions: + - Pending: [Approve ✅] [Reject ❌] + - Active: [Block 🚫] + [Verify 🛡️] (if not verified) + - Blocked: [Unblock ✅] + - Always: [Delete 🗑️] + +--- + +### A4: Championships List +**Purpose:** All championships across all orgs + +**Elements:** +- Search bar +- Filter chips: All (N), 🟢 Live (N), ⏳ Awaiting (N), 📝 Draft (N), 🚫 Blocked (N) +- Champ cards: name, "by Org Name 🛡️" (shield if verified), status badge, dates, location, members count +- Tap → A5 (Champ Detail) + +--- + +### A5: Championship Detail +**Purpose:** Review and manage a single championship + +**Elements:** +- Profile card: trophy emoji, name, org name, dates, location, status badge +- Stats table: Members, Passed, Pending, Revenue +- Approval Policy card: explains why auto-approved or needs review +- Actions: + - Pending approval: [Approve ✅] [Reject ❌] + - Live: [Suspend ⏸️] + - Blocked: [Reinstate ✅] + - Always: [Delete 🗑️] + +--- + +### A6: Users List +**Purpose:** All platform users + +**Elements:** +- Search bar (name, @handle, email) +- Filter chips: All (N), ✅ Active (N), ⚠️ Warned (N), 🚫 Blocked (N), 🏢 Org Admins (N) +- User cards: avatar (👤 or 🏢 for org admins), name, instagram, city, status badge +- Tap → A7 (User Detail) + +--- + +### A7: User Detail +**Purpose:** Review and manage a single user + +**Elements:** +- Profile card: avatar, name, instagram, email, status badge +- Info table: City, Joined, Championships joined, Role (Member or Org Admin + org name) +- Warning/Block reason card (if applicable, orange or red tint) +- Actions: + - Active: [Warn ⚠️] [Block 🚫] + - Warned: [Remove Warning ✅] [Block 🚫] + - Blocked: [Unblock ✅] + - Always: [Delete 🗑️] diff --git a/dancechamp-claude-code/docs/SPEC.md b/dancechamp-claude-code/docs/SPEC.md new file mode 100644 index 0000000..99a08c4 --- /dev/null +++ b/dancechamp-claude-code/docs/SPEC.md @@ -0,0 +1,267 @@ +# DanceChamp — Technical Specification + +## 1. Overview + +A mobile platform for pole dance championships. Dancers discover events and track registration. Organizers create and manage championships. Platform admin oversees everything. + +### The Problem +Championship info is scattered across Instagram posts, Telegram chats, and Google Docs. Dancers miss deadlines, lose track of multi-step registration, and can't verify submissions went through. Organizers manually manage everything via spreadsheets and DMs. + +### The Solution +One app with three roles: +- **Members** browse championships, register, track 10-step progress +- **Organizations** create championships, configure rules/fees/categories, review videos, confirm payments +- **Admin** approves organizations, reviews championships from unverified orgs, manages users + +### Real-World Reference: "Zero Gravity" +International Pole Exotic Championship, Minsk, Belarus. Two disciplines (Exotic Pole Dance + Pole Art), 6+ levels per discipline, two-stage payment (video selection fee + championship fee), video selection with pass/fail, detailed judging criteria (6 categories, 0–10), strict costume/equipment rules, insurance required. + +--- + +## 2. Roles & Permissions + +### Member (Dancer) +**Access:** Mobile app (member view) +- Browse & search championships +- View full details (rules, fees, categories, judges) +- Register for championships +- Track 10-step progress per championship +- Upload receipts & documents +- Receive notifications (results, deadline reminders, announcements) +- View past participation history + +**Cannot:** Create/edit championships, see other members' data, access org/admin features + +### Organization +**Access:** Mobile app (org view) +- Create championships (quick create → configure tabs) +- Manage disciplines, levels, styles, fees, rules, scoring, judges +- View & manage registered members per championship +- Review videos (pass/fail) +- Confirm receipt payments +- Edit member's level/style (triggers notification) +- Publish results +- Send announcements + +**Cannot:** See other orgs' data, access admin features + +### Admin +**Access:** Web admin panel +- View all orgs, championships, users +- Approve/reject pending organizations +- Approve/reject championships from unverified orgs +- Block/unblock orgs and users +- Warn users +- Verify organizations (changes approval policy) +- Delete any entity +- View platform stats and activity logs + +--- + +## 3. Championship Lifecycle + +``` +[Org: Quick Create] + name + date + location → status: DRAFT + │ + ▼ +[Org: Configure Tabs] + Categories ✓ → Fees ✓ → Rules ✓ → Judges ✓ + (any order, mark each as done) + │ + ▼ +[Org: "Go Live"] + │ + ├── Org is VERIFIED (🛡️) + │ → status: LIVE (auto-approved) + │ → visible to members immediately + │ + └── Org is UNVERIFIED + → status: PENDING_APPROVAL + → admin sees in "Needs Attention" + → admin approves → LIVE + → admin rejects → BLOCKED + │ + ▼ +[LIVE — Registration Open] + reg_start ≤ today ≤ reg_end + Members can register + │ + ▼ +[Registration Closed] + today > reg_end + No new registrations + │ + ▼ +[Event Day] + event_date + │ + ▼ +[COMPLETED] +``` + +--- + +## 4. Championship Data Structure + +Each championship contains: + +### Event Info +- Name, subtitle +- Event date (when it happens) +- Registration period: start date → end date (must be before event date) +- Location (city, country) +- Venue name +- Accent color (for branding) + +### Categories (configurable tab) +- Disciplines: e.g. "Exotic Pole Dance", "Pole Art" +- Levels per discipline: e.g. "Beginners", "Amateur", "Semi-Pro", "Profi", "Elite", "Duets & Groups" +- Styles: e.g. "Classic", "Flow", "Theater" + +### Fees (configurable tab) +- Video selection fee (e.g. "50 BYN / 1,500 RUB") +- Championship fees by type: + - Solo (e.g. "280 BYN / 7,500 RUB") + - Duet per person (e.g. "210 BYN / 5,800 RUB pp") + - Group per person (e.g. "190 BYN / 4,500 RUB pp") +- Refund note (typically non-refundable) + +### Rules (configurable tab) +- General rules (list of text items) +- Costume rules (list of text items) +- Scoring criteria: name + max score (e.g. "Artistry: 0–10", "Technique: 0–10") +- Penalties: name + value (e.g. "Fall: -2", "Exposure: DQ") + +### Judges (configurable tab) +- Judge profiles: name, Instagram handle, bio/description +- These are people, not scoring criteria + +### Members (only when LIVE) +- Registered members scoped to this championship +- Each with: discipline, level, style, type (solo/duet/group), progress steps, video URL, payment status, pass/fail result + +--- + +## 5. Member Registration Flow (10 Steps) + +``` +Step 1: 📋 Review Rules → Auto (tracked when user opens Rules tab) +Step 2: 🎯 Select Category → Auto (saved from registration picker) +Step 3: 🎬 Record Video → Manual toggle ("I've recorded my video") +Step 4: 📝 Submit Video Form → Manual / link to Google Form +Step 5: 💳 Pay Video Fee → Upload receipt screenshot → Org confirms +Step 6: ⏳ Wait for Results → Pending until org decides +Step 7: 🏆 Results → Auto-updates on pass/fail + ├── FAIL → Flow ends + └── PASS → Continue ▼ +Step 8: 💰 Pay Championship Fee → Upload receipt → Org confirms +Step 9: 📄 Submit "About Me" → Manual / link to form +Step 10: 🛡️ Confirm Insurance → Upload document → Org confirms + └── ✅ REGISTERED! +``` + +### Detection Methods +| Step | Method | +|---|---| +| Review rules | Auto — tracked on tab open | +| Select category | Auto — saved from picker | +| Record video | Manual toggle | +| Submit video form | Manual or Gmail auto-detect (future) | +| Pay video fee | Receipt upload → org confirms | +| Results | Auto — org pass/fail updates member | +| Pay championship fee | Receipt upload → org confirms | +| About Me form | Manual or Gmail auto-detect (future) | +| Insurance | Document upload → org confirms | + +--- + +## 6. Notifications + +### Types +| Type | Trigger | Direction | +|---|---|---| +| 🔄 Category Changed | Org changes member's level/style | Org → Member | +| ✅ Payment Confirmed | Org confirms uploaded receipt | Org → Member | +| 🏆 Results | Org passes/fails video selection | Org → Member | +| ⏰ Deadline Reminder | Auto (7d, 3d, 1d before reg_end) | System → Member | +| 📋 Registration Confirmed | All 10 steps complete | System → Member | +| 📢 Announcement | Org sends broadcast | Org → All Members | + +### Delivery +- In-app: Bell icon with unread count, notification feed +- Push: Expo Notifications for critical updates +- Email: Future enhancement + +--- + +## 7. Org App — Configurable Tabs + +When org creates a championship, it starts as DRAFT with 5 configurable sections: + +| Section | Tab | What to configure | Mark as Done when | +|---|---|---|---| +| Info | Overview | Name, dates, location, venue, reg period | Has date + location | +| Categories | Categories | Levels + styles | At least 1 level + 1 style | +| Fees | Fees | Video fee + championship fees | Video fee filled | +| Rules | Rules | General rules, costume rules, scoring criteria, penalties | At least 1 rule | +| Judges | Judges | Judge profiles (name, instagram, bio) | At least 1 judge | + +Setup progress shown on Overview tab as checklist. Each section shows green dot (done) or yellow dot (pending) in tab bar. "Go Live" button appears when all 5 sections are ✓. + +--- + +## 8. Admin — Approval Flow + +### Organization Approval +- New org registers → status: `pending` +- Admin reviews → Approve (status: `active`) or Reject (status: `blocked`) +- Admin can also **verify** an active org (🛡️ badge) + +### Championship Approval +- Depends on org's verification status: + - **Verified org** → Go Live = instant `live` + - **Unverified org** → Go Live = `pending_approval` → admin reviews + +### Admin Actions +| Entity | Actions | +|---|---| +| Organization | Approve, Reject, Block, Unblock, Verify, Delete | +| Championship | Approve, Reject, Suspend, Reinstate, Delete | +| User | Warn, Block, Unblock, Delete | + +--- + +## 9. Org Settings + +- **Edit Organization Profile**: name, instagram (inline edit form) +- **Notification Preferences**: toggles for push, email, registration alerts, payment alerts, deadline reminders +- **Connected Accounts**: Instagram (connected/not), Gmail, Telegram +- Help & Support +- Log Out + +--- + +## 10. Search & Discovery (Member) + +Members can find championships by: +- Text search (name, org name) +- Filters: discipline, location, status (Registration Open / Upcoming / Past) +- Sort: by date, by popularity + +Championship cards show: name, org, dates, location, status badge, member count. + +--- + +## 11. Future Features (Out of MVP Scope) + +- Instagram parsing: auto-import championship data from org's posts +- Gmail integration: auto-detect Google Forms confirmations +- OCR results detection: parse results from Instagram photo posts +- In-app payments: replace receipt uploads +- In-app forms: replace Google Forms links +- Telegram monitoring: detect results from Telegram chats +- Category recommendation engine +- Calendar sync (export to phone calendar) +- Social features (see which friends are registered) +- Multi-language (Russian + English) diff --git a/dancechamp-claude-code/prototypes/admin-panel.jsx b/dancechamp-claude-code/prototypes/admin-panel.jsx new file mode 100644 index 0000000..5126c81 --- /dev/null +++ b/dancechamp-claude-code/prototypes/admin-panel.jsx @@ -0,0 +1,517 @@ +import { useState } from "react"; + +/* ── Platform Data ── */ +const PLATFORM = { name: "DanceChamp", version: "1.0 MVP", totalRevenue: "12,450 BYN" }; + +const ORGS_DATA = [ + { id: "o1", name: "Zero Gravity Team", instagram: "@zerogravity_pole", logo: "💃", status: "active", joined: "Jan 15, 2026", champsCount: 2, membersCount: 24, city: "Minsk", email: "team@zerogravity.by", verified: true }, + { id: "o2", name: "Pole Universe", instagram: "@pole_universe", logo: "🌌", status: "active", joined: "Feb 2, 2026", champsCount: 1, membersCount: 12, city: "Moscow", email: "info@poleuniverse.ru", verified: true }, + { id: "o3", name: "Sky Pole Studio", instagram: "@sky_pole", logo: "☁️", status: "pending", joined: "Feb 20, 2026", champsCount: 0, membersCount: 0, city: "St. Petersburg", email: "hello@skypole.ru", verified: false }, + { id: "o4", name: "Dance Flames", instagram: "@dance_flames", logo: "🔥", status: "blocked", joined: "Dec 10, 2025", champsCount: 1, membersCount: 5, city: "Kyiv", email: "admin@danceflames.ua", verified: false, blockReason: "Fake organization — no real events" }, +]; + +const CHAMPS_DATA = [ + { id: "c1", orgId: "o1", orgName: "Zero Gravity Team", name: "Zero Gravity", dates: "May 30, 2026", location: "Minsk", status: "live", members: 24, passed: 8, pending: 8, revenue: "4,200 BYN", orgVerified: true }, + { id: "c2", orgId: "o1", orgName: "Zero Gravity Team", name: "Pole Star", dates: "Jul 12, 2026", location: "Moscow", status: "draft", members: 1, passed: 0, pending: 0, revenue: "0", orgVerified: true }, + { id: "c3", orgId: "o2", orgName: "Pole Universe", name: "Galactic Pole", dates: "Sep 15, 2026", location: "Moscow", status: "live", members: 12, passed: 0, pending: 12, revenue: "1,800 BYN", orgVerified: true }, + { id: "c4", orgId: "o3", orgName: "Sky Pole Studio", name: "Sky Open", dates: "Oct 5, 2026", location: "St. Petersburg", status: "pending_approval", members: 0, passed: 0, pending: 0, revenue: "0", orgVerified: false }, + { id: "c5", orgId: "o4", orgName: "Dance Flames", name: "Fire Cup", dates: "Mar 1, 2026", location: "Kyiv", status: "blocked", members: 5, passed: 0, pending: 0, revenue: "250 BYN", orgVerified: false }, +]; + +const USERS_DATA = [ + { id: "u1", name: "Alex Petrova", instagram: "@alex_pole", email: "alex@mail.ru", city: "Moscow", joined: "Jan 20, 2026", champsJoined: 2, status: "active", role: "member" }, + { id: "u2", name: "Maria Ivanova", instagram: "@maria_exotic", email: "maria@gmail.com", city: "Minsk", joined: "Jan 22, 2026", champsJoined: 1, status: "active", role: "member" }, + { id: "u3", name: "Elena Kozlova", instagram: "@elena.pole", email: "elena@ya.ru", city: "St. Petersburg", joined: "Feb 1, 2026", champsJoined: 1, status: "active", role: "member" }, + { id: "u4", name: "Daria Sokolova", instagram: "@daria_art", email: "daria@mail.ru", city: "Kyiv", joined: "Feb 5, 2026", champsJoined: 1, status: "active", role: "member" }, + { id: "u5", name: "Anna Belova", instagram: "@anna.b_pole", email: "anna@gmail.com", city: "Minsk", joined: "Feb 10, 2026", champsJoined: 1, status: "active", role: "member" }, + { id: "u6", name: "Olga Morozova", instagram: "@olga_exotic", email: "olga@mail.ru", city: "Moscow", joined: "Feb 12, 2026", champsJoined: 3, status: "warned", role: "member", warnReason: "Disputed payment — under review" }, + { id: "u7", name: "Ivan Petrov", instagram: "@ivan_admin", email: "ivan@zerogravity.by", city: "Minsk", joined: "Jan 10, 2026", champsJoined: 0, status: "active", role: "org_admin", org: "Zero Gravity Team" }, + { id: "u8", name: "Spam Bot", instagram: "@totally_real", email: "spam@fake.com", city: "Unknown", joined: "Feb 22, 2026", champsJoined: 0, status: "blocked", role: "member", blockReason: "Spam account" }, +]; + +const LOGS_DATA = [ + { id: "l1", action: "Org approved & verified", target: "Pole Universe", by: "Admin", date: "Feb 2, 2026", type: "org" }, + { id: "l2", action: "User blocked", target: "Spam Bot", by: "Admin", date: "Feb 22, 2026", type: "user" }, + { id: "l3", action: "Org blocked", target: "Dance Flames", by: "Admin", date: "Feb 23, 2026", type: "org" }, + { id: "l4", action: "Champ auto-approved (verified org)", target: "Zero Gravity", by: "System", date: "Feb 1, 2026", type: "champ" }, + { id: "l5", action: "User warned", target: "Olga Morozova", by: "Admin", date: "Feb 20, 2026", type: "user" }, + { id: "l6", action: "New org registered (pending)", target: "Sky Pole Studio", by: "System", date: "Feb 20, 2026", type: "org" }, + { id: "l7", action: "Champ submitted for review", target: "Sky Open", by: "Sky Pole Studio", date: "Feb 21, 2026", type: "champ" }, +]; + +/* ── Theme (admin = darker, more neutral accent) ── */ +const c = { bg: "#07060C", card: "#111019", cardH: "#18172290", brd: "#1D1C2B", text: "#F2F0FA", dim: "#5E5C72", mid: "#8F8DA6", accent: "#6366F1", accentS: "rgba(99,102,241,0.10)", green: "#10B981", greenS: "rgba(16,185,129,0.10)", yellow: "#F59E0B", yellowS: "rgba(245,158,11,0.10)", purple: "#8B5CF6", purpleS: "rgba(139,92,246,0.10)", blue: "#60A5FA", blueS: "rgba(96,165,250,0.10)", red: "#EF4444", redS: "rgba(239,68,68,0.10)", orange: "#F97316", orangeS: "rgba(249,115,22,0.10)" }; +const f = { d: "'Playfair Display',Georgia,serif", b: "'DM Sans','Segoe UI',sans-serif", m: "'JetBrains Mono',monospace" }; + +/* ── Shared UI ── */ +const Cd = ({ children, style: s }) =>
{children}
; +const ST = ({ children, right }) =>

{children}

{right}
; +const Bg = ({ label, color, bg }) => {label}; + +const statusConfig = { + active: { l: "ACTIVE", c: c.green, b: c.greenS }, live: { l: "LIVE", c: c.green, b: c.greenS }, + pending: { l: "PENDING", c: c.yellow, b: c.yellowS }, pending_approval: { l: "AWAITING REVIEW", c: c.orange, b: c.orangeS }, + draft: { l: "DRAFT", c: c.dim, b: `${c.dim}15` }, + blocked: { l: "BLOCKED", c: c.red, b: c.redS }, warned: { l: "WARNED", c: c.orange, b: c.orangeS }, +}; + +function Hdr({ title, subtitle, onBack, right }) { + return
+ {onBack &&
} +

{title}

{subtitle &&

{subtitle}

}
+ {right} +
; +} + +function Nav({ active, onChange }) { + return
+ {[{ id: "dash", i: "📊", l: "Overview" }, { id: "orgs", i: "🏢", l: "Orgs" }, { id: "champs", i: "🏆", l: "Champs" }, { id: "users", i: "👥", l: "Users" }].map(x => +
onChange(x.id)} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, cursor: "pointer", opacity: active === x.id ? 1 : 0.35 }}>{x.i}{x.l}
+ )} +
; +} + +function SearchBar({ value, onChange, placeholder }) { + return
+ 🔍 + onChange(e.target.value)} style={{ background: "transparent", border: "none", outline: "none", color: c.text, fontFamily: f.b, fontSize: 13, width: "100%" }} /> +
; +} + +function FilterChips({ filters, active, onChange, accent }) { + return
+ {filters.map(fi =>
onChange(fi.id)} style={{ fontFamily: f.m, fontSize: 9, fontWeight: 600, whiteSpace: "nowrap", color: active === fi.id ? accent || c.accent : c.dim, background: active === fi.id ? `${accent || c.accent}15` : "transparent", border: `1px solid ${active === fi.id ? `${accent || c.accent}30` : "transparent"}`, padding: "5px 10px", borderRadius: 16, cursor: "pointer" }}>{fi.l}{fi.n !== undefined ? ` (${fi.n})` : ""}
)} +
; +} + +function ActionBtn({ label, color, onClick, icon, filled }) { + return
+ {icon && {icon}} + {label} +
; +} + +/* ── Dashboard ── */ +function Dashboard({ orgs, champs, users, onNav }) { + const pendingOrgs = orgs.filter(o => o.status === "pending").length; + const pendingChamps = champs.filter(c2 => c2.status === "pending_approval").length; + const blockedUsers = users.filter(u => u.status === "blocked").length; + + return
+
+ } /> +
+ + {/* Platform stats */} +
+ {[{ n: orgs.filter(o => o.status === "active").length, l: "Orgs", co: c.accent, go: "orgs" }, + { n: champs.filter(c2 => c2.status === "live").length, l: "Live Champs", co: c.green, go: "champs" }, + { n: users.length, l: "Users", co: c.blue, go: "users" }, + ].map(s =>
onNav(s.go)} style={{ flex: 1, background: c.card, border: `1px solid ${c.brd}`, borderRadius: 12, padding: "10px 6px", textAlign: "center", cursor: "pointer" }}> +

{s.n}

+

{s.l}

+
)} +
+ + {/* Needs attention */} + {(pendingOrgs > 0 || pendingChamps > 0) && + }>⚡ Needs Attention + {pendingOrgs > 0 &&
onNav("orgs")} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: `1px solid ${c.brd}`, cursor: "pointer" }}> + 🏢 + Organizations awaiting approval + {pendingOrgs} + +
} + {pendingChamps > 0 &&
onNav("champs")} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", cursor: "pointer" }}> + 🏆 + Champs awaiting approval (unverified orgs) + {pendingChamps} + +
} +
} + + {/* Quick stats */} + + Platform Health + {[{ l: "Total revenue", v: PLATFORM.totalRevenue, co: c.green }, + { l: "Active orgs", v: `${orgs.filter(o => o.status === "active").length}/${orgs.length}`, co: c.accent }, + { l: "Blocked users", v: `${blockedUsers}`, co: blockedUsers > 0 ? c.red : c.green }, + { l: "Avg members/champ", v: Math.round(users.filter(u => u.role === "member").length / Math.max(champs.filter(c2 => c2.status === "live").length, 1)), co: c.blue }, + ].map(s =>
+ {s.l} + {s.v} +
)} +
+ + {/* Recent activity */} + + {LOGS_DATA.length} entries}>Recent Activity + {LOGS_DATA.slice(0, 5).map(log => { + const tc = { org: c.accent, user: c.blue, champ: c.green }[log.type] || c.dim; + return
+
+
+

{log.action}: {log.target}

+

{log.date} · {log.by}

+
+
; + })} + +
+
; +} + +/* ── Organizations ── */ +function OrgsList({ orgs, onOrgTap }) { + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + + const filters = [ + { id: "all", l: "All", n: orgs.length }, + { id: "active", l: "✅ Active", n: orgs.filter(o => o.status === "active").length }, + { id: "pending", l: "⏳ Pending", n: orgs.filter(o => o.status === "pending").length }, + { id: "blocked", l: "🚫 Blocked", n: orgs.filter(o => o.status === "blocked").length }, + ]; + + const filtered = orgs.filter(o => { + const q = !search || o.name.toLowerCase().includes(search.toLowerCase()) || o.instagram.toLowerCase().includes(search.toLowerCase()); + if (!q) return false; + if (filter === "active") return o.status === "active"; + if (filter === "pending") return o.status === "pending"; + if (filter === "blocked") return o.status === "blocked"; + return true; + }); + + return
+ +
+ + + {filtered.map(o => { + const st = statusConfig[o.status] || statusConfig.active; + return
onOrgTap(o)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 14, cursor: "pointer" }}> +
+
{o.logo}
+
+
+

{o.name}

+ +
+

{o.instagram}

+
+
+
+ 📍 {o.city} + 🏆 {o.champsCount} champs + 👥 {o.membersCount} members +
+
; + })} +
+
; +} + +/* ── Org Detail ── */ +function OrgDetail({ org: initial, onBack, champs }) { + const [o, setO] = useState(initial); + const st = statusConfig[o.status] || statusConfig.active; + const orgChamps = champs.filter(c2 => c2.orgId === o.id); + + return
+ +
+ {/* Profile */} + +
{o.logo}
+
+

{o.name}

+

{o.instagram}

+

📍 {o.city} · 📧 {o.email}

+
+ +
+ + {/* Info */} + + Details + {[{ l: "Joined", v: o.joined }, { l: "Championships", v: o.champsCount }, { l: "Total members", v: o.membersCount }, { l: "Verified", v: o.verified ? "✅ Yes" : "❌ No" }].map(r => +
+ {r.l} + {r.v} +
+ )} +
+ + {/* Approval policy */} + +
+ {o.verified ? "🛡️" : "⏳"} +
+

{o.verified ? "Verified — Auto-approve events" : "Unverified — Events need manual approval"}

+

{o.verified ? "Championships go live instantly when org clicks 'Go Live'" : "Admin must review & approve each championship before it becomes visible"}

+
+
+
+ + {/* Championships */} + {orgChamps.length > 0 && + {orgChamps.length}}>Championships + {orgChamps.map(ch => { + const cs = statusConfig[ch.status] || statusConfig.draft; + return
+

{ch.name}

{ch.dates} · {ch.location}

+ +
; + })} +
} + + {/* Block reason */} + {o.blockReason && +

Block Reason

+

{o.blockReason}

+
} + + {/* Actions */} +
+ {o.status === "pending" &&
+ setO(p => ({ ...p, status: "active", verified: true }))} icon="✅" filled /> + setO(p => ({ ...p, status: "blocked", blockReason: "Rejected by admin" }))} icon="❌" filled /> +
} + {o.status === "active" && setO(p => ({ ...p, status: "blocked", blockReason: "Blocked by admin" }))} icon="🚫" />} + {o.status === "blocked" && setO(p => ({ ...p, status: "active", blockReason: null }))} icon="✅" />} + {!o.verified && o.status !== "blocked" && setO(p => ({ ...p, verified: true }))} icon="🛡️" />} + {}} icon="🗑️" /> +
+
+
; +} + +/* ── Championships ── */ +function ChampsList({ champs, onChampTap }) { + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + + const filters = [ + { id: "all", l: "All", n: champs.length }, + { id: "live", l: "🟢 Live", n: champs.filter(c2 => c2.status === "live").length }, + { id: "pending_approval", l: "⏳ Awaiting", n: champs.filter(c2 => c2.status === "pending_approval").length }, + { id: "draft", l: "📝 Draft", n: champs.filter(c2 => c2.status === "draft").length }, + { id: "blocked", l: "🚫 Blocked", n: champs.filter(c2 => c2.status === "blocked").length }, + ]; + + const filtered = champs.filter(ch => { + const q = !search || ch.name.toLowerCase().includes(search.toLowerCase()) || ch.orgName.toLowerCase().includes(search.toLowerCase()); + if (!q) return false; + if (filter !== "all") return ch.status === filter; + return true; + }); + + return
+ +
+ + + {filtered.map(ch => { + const st = statusConfig[ch.status] || statusConfig.draft; + return
onChampTap(ch)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 14, cursor: "pointer" }}> +
+

{ch.name}

by {ch.orgName} {ch.orgVerified ? "🛡️" : ""}

+ +
+
+ 📅 {ch.dates} + 📍 {ch.location} + 👥 {ch.members} +
+
; + })} +
+
; +} + +/* ── Championship Detail ── */ +function ChampDetail({ ch: initial, onBack }) { + const [ch, setCh] = useState(initial); + const st = statusConfig[ch.status] || statusConfig.draft; + + return
+ +
+ +
🏆
+
+

{ch.name}

+

{ch.orgName}

+

📅 {ch.dates} · 📍 {ch.location}

+
+ +
+ + + Stats + {[{ l: "Members", v: ch.members }, { l: "Passed", v: ch.passed }, { l: "Pending", v: ch.pending }, { l: "Revenue", v: ch.revenue }].map(r => +
+ {r.l} + {r.v} +
+ )} +
+ + {/* Approval info */} + {ch.orgVerified !== undefined && + Approval Policy +
+ {ch.orgVerified ? "🛡️" : "⏳"} +
+

{ch.orgVerified ? "Verified org — auto-approved" : "Unverified org — manual review required"}

+

{ch.orgVerified ? "This org can go live without admin approval" : "Admin must approve before members can see this event"}

+
+
+
} + +
+ {ch.status === "pending_approval" &&
+ setCh(p => ({ ...p, status: "live" }))} icon="✅" filled /> + setCh(p => ({ ...p, status: "blocked" }))} icon="❌" filled /> +
} + {ch.status === "live" && setCh(p => ({ ...p, status: "blocked" }))} icon="⏸️" />} + {ch.status === "blocked" && setCh(p => ({ ...p, status: "live" }))} icon="✅" />} + {}} icon="🗑️" /> +
+
+
; +} + +/* ── Users ── */ +function UsersList({ users, onUserTap }) { + const [search, setSearch] = useState(""); + const [filter, setFilter] = useState("all"); + + const filters = [ + { id: "all", l: "All", n: users.length }, + { id: "active", l: "✅ Active", n: users.filter(u => u.status === "active").length }, + { id: "warned", l: "⚠️ Warned", n: users.filter(u => u.status === "warned").length }, + { id: "blocked", l: "🚫 Blocked", n: users.filter(u => u.status === "blocked").length }, + { id: "org_admin", l: "🏢 Org Admins", n: users.filter(u => u.role === "org_admin").length }, + ]; + + const filtered = users.filter(u => { + const q = !search || u.name.toLowerCase().includes(search.toLowerCase()) || u.instagram.toLowerCase().includes(search.toLowerCase()) || u.email.toLowerCase().includes(search.toLowerCase()); + if (!q) return false; + if (filter === "active") return u.status === "active"; + if (filter === "warned") return u.status === "warned"; + if (filter === "blocked") return u.status === "blocked"; + if (filter === "org_admin") return u.role === "org_admin"; + return true; + }); + + return
+ +
+ + + {filtered.map(u => { + const st = statusConfig[u.status] || statusConfig.active; + return
onUserTap(u)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 12, cursor: "pointer" }}> +
+
{u.role === "org_admin" ? "🏢" : "👤"}
+
+
+

{u.name}

+ +
+
+ {u.instagram} + {u.city} +
+
+
+
; + })} +
+
; +} + +/* ── User Detail ── */ +function UserDetail({ user: initial, onBack }) { + const [u, setU] = useState(initial); + const st = statusConfig[u.status] || statusConfig.active; + + return
+ +
+ +
👤
+
+

{u.name}

+

{u.instagram}

+

📧 {u.email}

+
+ +
+ + + Info + {[{ l: "City", v: u.city }, { l: "Joined", v: u.joined }, { l: "Championships", v: u.champsJoined }, + { l: "Role", v: u.role === "org_admin" ? `Org Admin (${u.org})` : "Member" }, + ].map(r =>
+ {r.l} + {r.v} +
)} +
+ + {(u.blockReason || u.warnReason) && +

{u.status === "blocked" ? "Block" : "Warning"} Reason

+

{u.blockReason || u.warnReason}

+
} + +
+ {u.status === "active" && <> + setU(p => ({ ...p, status: "warned", warnReason: "Warning issued by admin" }))} icon="⚠️" /> + setU(p => ({ ...p, status: "blocked", blockReason: "Blocked by admin" }))} icon="🚫" /> + } + {u.status === "warned" && <> + setU(p => ({ ...p, status: "active", warnReason: null }))} icon="✅" /> + setU(p => ({ ...p, status: "blocked", blockReason: "Blocked after warning" }))} icon="🚫" /> + } + {u.status === "blocked" && setU(p => ({ ...p, status: "active", blockReason: null }))} icon="✅" />} + {}} icon="🗑️" /> +
+
+
; +} + +/* ── App Shell ── */ +export default function AdminApp() { + const [scr, setScr] = useState("dash"); + const [sel, setSel] = useState(null); + + const go = (screen, data) => { setScr(screen); setSel(data || null); }; + + const render = () => { + if (scr === "orgDetail" && sel) return go("orgs")} champs={CHAMPS_DATA} />; + if (scr === "champDetail" && sel) return go("champs")} />; + if (scr === "userDetail" && sel) return go("users")} />; + if (scr === "orgs") return go("orgDetail", o)} />; + if (scr === "champs") return go("champDetail", ch)} />; + if (scr === "users") return go("userDetail", u)} />; + return ; + }; + + const showNav = ["dash", "orgs", "champs", "users"].includes(scr); + + return
+ + +
+
+ 9:41 +
+ ●●● +
+
{render()}
+ {showNav &&
+
; +} diff --git a/dancechamp-claude-code/prototypes/member-app.jsx b/dancechamp-claude-code/prototypes/member-app.jsx new file mode 100644 index 0000000..658240f --- /dev/null +++ b/dancechamp-claude-code/prototypes/member-app.jsx @@ -0,0 +1,643 @@ +import { useState } from "react"; + +/* ── Data ── */ +const CHAMPS = [ + { + id: "1", name: "Zero Gravity", subtitle: "International Pole Exotic Championship", + org: "Zero Gravity Team", dates: "May 30, 2026", location: "Minsk, Belarus", + venue: "Prime Hall", address: "Pr. Pobeditelei, 65", + disciplines: [ + { name: "Exotic Pole Dance", performanceReq: "70% floor & mid-level, 30% upper level", categories: [ + { name: "Beginners", duration: "2:00–3:00", eligibility: "Up to 2 yrs, no instructor/pro background", type: "solo" }, + { name: "Amateur", duration: "2:30–3:00", eligibility: "2–4 yrs, no instructor/pro background", type: "solo" }, + { name: "Semi-Pro", duration: "2:50–3:20", eligibility: "3+ yrs, instructor OR pro OR prizes in Amateur", type: "solo" }, + { name: "Profi", duration: "3:00–3:30", eligibility: "4+ yrs, instructor OR pro OR prizes in Semi-Pro", type: "solo" }, + { name: "Elite", duration: "3:00–4:00", eligibility: "3+ prizes in Profi OR widely known", type: "solo" }, + { name: "Duets & Groups", duration: "3:00–4:20", eligibility: "Open to all levels", type: "group" }, + ]}, + { name: "Pole Art", performanceReq: "60% floor & mid-level, 40% upper level", categories: [ + { name: "Amateur", duration: "2:30–3:00", eligibility: "Up to 2 yrs, no instructor/pro background", type: "solo" }, + { name: "Semi-Pro", duration: "2:50–3:20", eligibility: "3+ yrs, instructor OR pro OR prizes in Amateur", type: "solo" }, + { name: "Profi", duration: "3:00–3:30", eligibility: "4+ yrs, instructor OR pro OR prizes in Semi-Pro", type: "solo" }, + ]}, + ], + fees: { videoSelection: "50 BYN / 1,500 RUB", championship: { solo: "280 BYN / 7,500 RUB", duet: "210 BYN / 5,800 RUB pp", group: "190 BYN / 4,500 RUB pp" }, refundNote: "Non-refundable. All fees are charitable contributions." }, + videoReqs: { minDuration: "1:30", editing: "No editing or splicing", maxAge: "Less than 1 year old", note: "Must reflect your level" }, + judging: [ + { name: "Image", max: 10, desc: "Costume, hair, makeup, originality" }, + { name: "Artistry", max: 10, desc: "Charisma, stage presence, emotion" }, + { name: "Choreography", max: 10, desc: "Body control, complexity, originality" }, + { name: "Musicality", max: 10, desc: "Timing, feeling, accent play" }, + { name: "Technique", max: 10, desc: "Clean execution, transitions, tricks" }, + { name: "Overall", max: 10, desc: "General impression" }, + { name: "Synchronicity", max: 10, desc: "Duets only" }, + ], + penalties: [ + { name: "Missed element", points: -2 }, { name: "Fall", points: -2 }, + { name: "Leaving stage", consequence: "DQ" }, { name: "Exposure", consequence: "DQ" }, + { name: "Substance influence", consequence: "DQ" }, { name: "No special shoes", consequence: "DQ" }, + ], + venueSpecs: { poles: "2 (Static & Spinning)", poleHeight: "3.5 m", poleDiameter: "42 mm", stageSize: "6m × 14m" }, + costumeRules: ["Neat and well-fitted", "No advertising", "No spikes/sharp objects", "No thongs/sheer/pasties", "Specialized shoes for Exotic", "Creativity is scored"], + generalRules: ["Must be 18+", "No medical contraindications", "Valid life & health insurance", "No lotions/bronzers 24h before", "Grip aids allowed (no wax/rosin)", "Judges' decision is final", "Organizers may change your category"], + prizes: ["1st–3rd in each category", "Nominations per block", "Medals, diplomas, sponsor prizes", "All get participation diplomas", "1st Elite → judge next champ"], + resultsChannels: ["Email", "Instagram", "Telegram"], + applicationDeadline: "August 22, 2026", + formUrl: "https://docs.google.com/forms/d/e/1FAIpQLSfLaNg5Sf2QMAI6anpMrnLu-2qYfT3tdwh0dsynQFn8xMhi2g/viewform", + status: "registration_open", accent: "#D4145A", image: "💃", + }, + { + id: "2", name: "Pole Star", subtitle: "National Pole Championship", + org: "Pole Star Events", dates: "Jul 12–13, 2026", location: "Moscow, Russia", venue: "Crystal Hall", + disciplines: [{ name: "Exotic Pole Dance", categories: [ + { name: "Amateur", duration: "2:30–3:00", eligibility: "2–4 years", type: "solo" }, + { name: "Profi", duration: "3:00–3:30", eligibility: "4+ years", type: "solo" }, + ]}], + fees: { videoSelection: "2,000 RUB", championship: { solo: "8,000 RUB" } }, + videoReqs: { minDuration: "1:00", editing: "No editing", maxAge: "6 months" }, + status: "upcoming", applicationDeadline: "Jun 1, 2026", accent: "#7C3AED", image: "⭐", + }, +]; + +const STEPS = [ + { id: "s1", label: "Review rules & eligibility", icon: "📋", detect: "auto", detectLabel: "Auto: tracked in app" }, + { id: "s2", label: "Select category", icon: "🏷️", detect: "auto", detectLabel: "Auto: saved in app" }, + { id: "s3", label: "Record video (min 1:30)", icon: "🎬", detect: "manual", detectLabel: "You confirm" }, + { id: "s4", label: "Submit video selection form", icon: "📤", detect: "email", detectLabel: "Auto: Gmail confirmation" }, + { id: "s5", label: "Pay video selection fee", icon: "💳", warn: true, detect: "receipt", detectLabel: "Upload receipt → Org confirms" }, + { id: "s6", label: "Results (auto-detected)", icon: "🤖", detect: "auto", detectLabel: "Auto: Instagram OCR" }, + { id: "s7", label: "Pay championship fee", icon: "💰", warn: true, detect: "receipt", detectLabel: "Upload receipt → Org confirms" }, + { id: "s8", label: 'Fill "About Me" form', icon: "👤", detect: "email", detectLabel: "Auto: Gmail confirmation" }, + { id: "s9", label: "Confirm insurance", icon: "🛡️", detect: "receipt", detectLabel: "Upload doc → Org confirms" }, + { id: "s10", label: "Submit music & performance", icon: "🎶", detect: "email", detectLabel: "Auto: Gmail confirmation" }, +]; + +const USER = { name: "Alex", city: "Moscow", disciplines: ["Pole Exotic", "Pole Art"], experienceYears: 3, isInstructor: false, instagram: "@alex_pole" }; + +const NOTIFICATIONS = [ + { id: "n1", type: "category_change", champ: "Zero Gravity", from: "Amateur", to: "Semi-Pro", field: "Level", date: "Feb 24, 2026", read: false, message: "Your level was changed from Amateur to Semi-Pro by the organizer." }, + { id: "n2", type: "payment_confirmed", champ: "Zero Gravity", date: "Feb 23, 2026", read: false, message: "Your video selection fee payment has been confirmed." }, + { id: "n3", type: "result", champ: "Zero Gravity", date: "Feb 22, 2026", read: true, message: "Video selection results are out! You passed! 🎉" }, + { id: "n4", type: "deadline", champ: "Zero Gravity", date: "Feb 20, 2026", read: true, message: "Reminder: registration deadline is Aug 22, 2026." }, +]; + +const MY_REGISTRATIONS = [ + { champId: "1", discipline: "Exotic Pole Dance", category: "Semi-Pro", status: "in_progress", currentStep: 4, stepsCompleted: 3, nextAction: "Submit video selection form", deadline: "Aug 22, 2026" }, + { champId: "2", discipline: "Exotic Pole Dance", category: "Profi", status: "planned", currentStep: 1, stepsCompleted: 0, nextAction: "Review rules & eligibility", deadline: "Jun 1, 2026" }, +]; + +/* ── Theme ── */ +const c = { bg: "#08070D", card: "#12111A", cardH: "#1A1926", brd: "#1F1E2E", text: "#F2F0FA", dim: "#5E5C72", mid: "#8F8DA6", accent: "#D4145A", accentS: "rgba(212,20,90,0.10)", green: "#10B981", greenS: "rgba(16,185,129,0.10)", yellow: "#F59E0B", yellowS: "rgba(245,158,11,0.10)", purple: "#8B5CF6" }; +const f = { d: "'Playfair Display',Georgia,serif", b: "'DM Sans','Segoe UI',sans-serif", m: "'JetBrains Mono',monospace" }; + +/* ── Shared ── */ +const Badge = ({ status }) => { const m = { registration_open: { l: "REG OPEN", c: c.green, b: c.greenS }, upcoming: { l: "UPCOMING", c: c.yellow, b: c.yellowS } }; const s = m[status] || m.upcoming; return {s.l}; }; +const Chip = ({ text, color = c.mid, bg = c.card, border = c.brd }) => {text}; +const Info = ({ icon, text }) => {icon} {text}; +const ST = ({ children, right }) =>

{children}

{right && {right}}
; +const Cd = ({ children, style: s }) =>
{children}
; + +function Tabs({ tabs, active, onChange, accent: ac }) { + return
+ {tabs.map(t =>
onChange(t)} style={{ fontFamily: f.m, fontSize: 10, fontWeight: 600, letterSpacing: 0.4, color: active === t ? ac || c.accent : c.dim, background: active === t ? `${ac || c.accent}15` : "transparent", border: `1px solid ${active === t ? `${ac || c.accent}30` : "transparent"}`, padding: "5px 12px", borderRadius: 16, cursor: "pointer", whiteSpace: "nowrap" }}>{t}
)} +
; +} + +function Nav({ active, onChange }) { + return
+ {[{ id: "home", i: "🏠", l: "Home" }, { id: "my", i: "🎯", l: "My Champs" }, { id: "search", i: "🔍", l: "Search" }, { id: "profile", i: "👤", l: "Profile" }].map(x => +
onChange(x.id)} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, cursor: "pointer", opacity: active === x.id ? 1 : 0.35 }}>{x.i}{x.l}
+ )} +
; +} + +function Hdr({ title, subtitle, onBack, right }) { + return
+ {onBack &&
} +

{title}

{subtitle &&

{subtitle}

}
+ {right} +
; +} + +/* ── Home ── */ +function Home({ onTap, onNotifications }) { + const unread = NOTIFICATIONS.filter(n => !n.read).length; + return
+ + 🔔 + {unread > 0 &&
+ {unread} +
} +
+ } /> +
+
+

🔔 Zero Gravity — Deadline: Aug 22!

+
+ Championships + {CHAMPS.map(ch => )} +
+
; +} + +function ChampCard({ ch, onTap }) { + const [h, setH] = useState(false); + return
onTap(ch)} onMouseEnter={() => setH(true)} onMouseLeave={() => setH(false)} style={{ background: h ? c.cardH : c.card, border: `1px solid ${c.brd}`, borderRadius: 16, padding: 16, cursor: "pointer", transition: "all 0.2s", transform: h ? "translateY(-2px)" : "none", boxShadow: h ? "0 8px 24px rgba(0,0,0,0.3)" : "none" }}> +
+
{ch.image}
+ +
+

{ch.name}

+

{ch.subtitle}

+
+ {ch.disciplines &&
{ch.disciplines.map(d => )}
} +
; +} + +/* ── Championship Detail ── */ +function Detail({ ch, onBack, onProgress }) { + const [tab, setTab] = useState("Info"); + const tabs = ["Info", "Categories", "Fees", "Judging", "Rules"]; + const completedCount = 3; // mock + + return
+ +
+ + {/* Hero */} +
+
+ {ch.image} +
+
+ + + {ch.applicationDeadline && } + {ch.resultsChannels && } +
+
+ + {/* Register + Progress buttons */} +
+
onProgress(ch)} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 6, padding: "11px 12px", borderRadius: 12, background: c.card, border: `1px solid ${c.brd}`, cursor: "pointer" }}> + 📋 + Progress + {completedCount}/{STEPS.length} +
+ {ch.formUrl && ch.status === "registration_open" && + ✍️ Register + } +
+ + + + {/* Info */} + {tab === "Info" &&
+ {ch.disciplines && Disciplines{ch.disciplines.map(d =>
+

{d.name}

+ {d.performanceReq &&

{d.performanceReq}

} +
{d.categories.map(cat => )}
+
)}
} + {ch.videoReqs && Video Requirements
+ + + + +
} + {ch.prizes && Prizes{ch.prizes.map((p, i) =>

🏆 {p}

)}
} +
} + + {/* Categories */} + {tab === "Categories" && ch.disciplines &&
+ {ch.disciplines.map(d => {d.name}{d.categories.map(cat => { + const match = (USER.experienceYears >= 2 && USER.experienceYears <= 4 && !USER.isInstructor && cat.name === "Amateur") || (USER.experienceYears >= 3 && cat.name === "Semi-Pro") || cat.name === "Duets & Groups"; + return
+
+ {cat.name} +
+ {cat.duration} + {match && MATCH} +
+
+

{cat.eligibility}

+
; + })}
)} +

⚠️ Organizers may change your category if level doesn't match

+
} + + {/* Fees */} + {tab === "Fees" && ch.fees &&
+ Stage 1: Video Selection +
Fee{ch.fees.videoSelection}
+

⚠️ Non-refundable even if you don't pass

+
+ Stage 2: Championship (after passing) + {Object.entries(ch.fees.championship).map(([t, a]) =>
{t}{a}
)} + {ch.fees.refundNote &&

⚠️ {ch.fees.refundNote}

} +
+
} + + {/* Judging */} + {tab === "Judging" && ch.judging &&
+ Scoring (0–10 each){ch.judging.map(j =>
+
{j.name}0–{j.max}
+

{j.desc}

+
)}
+ {ch.penalties && Penalties{ch.penalties.map((p, i) =>
+ {p.name} + {p.consequence || `${p.points}`} +
)}
} +
} + + {/* Rules + Venue */} + {tab === "Rules" &&
+ {ch.generalRules && General{ch.generalRules.map((r, i) =>

• {r}

)}
} + {ch.costumeRules && Costume & Shoes{ch.costumeRules.map((r, i) =>

• {r}

)}
} + {ch.venueSpecs && Stage & Equipment +
{Object.entries(ch.venueSpecs).map(([k, v]) =>

{k.replace(/([A-Z])/g, " $1")}

{v}

)}
+
} +
} + +
+
; +} + +/* ── Progress Screen (separate full view) ── */ +function Progress({ ch, onBack }) { + const [done, setDone] = useState({ s1: true, s2: true, s3: true }); + const [uploads, setUploads] = useState({}); + const [orgConfirmed, setOrgConfirmed] = useState({}); + const cnt = Object.values(done).filter(Boolean).length; + const pct = (cnt / STEPS.length) * 100; + + const detectColors = { auto: { c: c.green, bg: c.greenS, label: "AUTO" }, email: { c: "#60A5FA", bg: "rgba(96,165,250,0.10)", label: "GMAIL" }, receipt: { c: c.yellow, bg: c.yellowS, label: "UPLOAD" }, manual: { c: c.mid, bg: `${c.mid}15`, label: "MANUAL" } }; + + const handleUpload = (stepId) => { + setUploads(p => ({ ...p, [stepId]: true })); + }; + + return
+ +
+ + {/* Summary */} +
+
+ {Math.round(pct)}% + {cnt} of {STEPS.length} steps +
+
+
+
+
+ + {/* Legend */} +
+ {Object.entries(detectColors).map(([k, v]) => + {v.label} + )} + ORG ✓ +
+ + {/* Steps */} + + {STEPS.map((s, i) => { + const d = done[s.id]; + const isN = !d && cnt === i; + const uploaded = uploads[s.id]; + const confirmed = orgConfirmed[s.id]; + const dc = detectColors[s.detect]; + + return
+ {/* Main row */} +
{ if (s.detect === "manual" || s.detect === "auto") setDone(p => ({ ...p, [s.id]: !p[s.id] })); }} style={{ display: "flex", alignItems: "center", gap: 10, cursor: s.detect === "manual" || s.detect === "auto" ? "pointer" : "default", background: isN ? `${ch.accent}06` : "transparent", borderRadius: 8, padding: "2px 0" }}> +
{d ? "✓" : i + 1}
+ {s.icon} +
+ {s.label} +
+ {s.warn && !d && ⚠️} + {isN && NEXT} +
+ + {/* Detection method + action */} + {!d &&
+ {dc.label} + {s.detectLabel} +
} + + {/* Upload action for receipt steps */} + {!d && isN && s.detect === "receipt" &&
+ {!uploaded ? ( +
handleUpload(s.id)} style={{ display: "inline-flex", alignItems: "center", gap: 6, padding: "7px 14px", borderRadius: 8, background: `${c.yellow}15`, border: `1px solid ${c.yellow}30`, cursor: "pointer" }}> + 📸 + Upload receipt +
+ ) : !confirmed ? ( +
+ 📸 Uploaded + Waiting for org to confirm... + {/* Demo: simulate org confirm */} + { setOrgConfirmed(p => ({ ...p, [s.id]: true })); setDone(p => ({ ...p, [s.id]: true })); }} style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: c.purple, background: `${c.purple}15`, padding: "2px 8px", borderRadius: 4, cursor: "pointer" }}>Demo: Org ✓ +
+ ) : null} +
} + + {/* Email detection indicator */} + {!d && isN && s.detect === "email" &&
+ 📧 + Monitoring Gmail for confirmation... + {/* Demo: simulate detection */} + setDone(p => ({ ...p, [s.id]: true }))} style={{ fontFamily: f.m, fontSize: 8, fontWeight: 700, color: "#60A5FA", background: "rgba(96,165,250,0.10)", padding: "2px 8px", borderRadius: 4, cursor: "pointer" }}>Demo: Detected +
} + + {/* Auto completed indicator */} + {d && s.detect === "auto" &&
+ ✓ Auto-detected +
} + {d && s.detect === "email" &&
+ ✓ Gmail confirmation received +
} + {d && s.detect === "receipt" &&
+ ✓ Receipt uploaded · Org confirmed +
} +
; + })} +
+ + {/* Auto-detection monitoring */} + +
+ 🤖 +
+

Auto-Detection Active

+

Monitoring multiple channels

+
+
+ {[ + { ch: "Instagram", icon: "📸", desc: "Results photo OCR", status: "Monitoring" }, + { ch: "Gmail", icon: "📧", desc: "Form confirmations & results", status: "Connected" }, + { ch: "Telegram", icon: "💬", desc: "Championship chat", status: "Monitoring" }, + ].map(x =>
+ {x.icon} +
+

{x.ch}

+

{x.desc}

+
+ {x.status} +
)} +
+ + {/* Register */} + {ch.formUrl && ch.status === "registration_open" && + ✍️ Register Now + } +
+
; +} + +/* ── My Championships ── */ +function MyChamps({ onTap, onProgress }) { + const active = MY_REGISTRATIONS.filter(r => r.status === "in_progress"); + const planned = MY_REGISTRATIONS.filter(r => r.status === "planned"); + const completed = MY_REGISTRATIONS.filter(r => r.status === "completed"); + + const RegCard = ({ reg }) => { + const ch = CHAMPS.find(c2 => c2.id === reg.champId); + if (!ch) return null; + const pct = (reg.stepsCompleted / STEPS.length) * 100; + const statusMap = { in_progress: { label: "IN PROGRESS", color: c.green, bg: c.greenS }, planned: { label: "PLANNED", color: c.yellow, bg: c.yellowS }, completed: { label: "COMPLETED", color: c.purple, bg: `${c.purple}15` } }; + const st = statusMap[reg.status]; + + return + {/* Color accent bar */} +
+
+ {/* Header */} +
+
+
{ch.image}
+
+

{ch.name}

+

{ch.dates} · {ch.location}

+
+
+ {st.label} +
+ + {/* Category */} +
+ + +
+ + {/* Progress bar */} +
+
+ {reg.stepsCompleted}/{STEPS.length} steps + {Math.round(pct)}% +
+
+
+
+
+ + {/* Next action */} + {reg.nextAction &&
+

Next step

+

{STEPS[reg.currentStep - 1]?.icon} {reg.nextAction}

+
} + + {/* Deadline */} + {reg.deadline &&
+ + Deadline: {reg.deadline} +
} + + {/* Actions */} +
+
onTap(ch)} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "9px", borderRadius: 10, background: c.bg, border: `1px solid ${c.brd}`, cursor: "pointer" }}> + ℹ️ + Details +
+
onProgress(ch)} style={{ flex: 1, display: "flex", alignItems: "center", justifyContent: "center", gap: 5, padding: "9px", borderRadius: 10, background: ch.accent, cursor: "pointer" }}> + 📋 + Progress +
+
+
+ ; + }; + + return
+ +
+ + {active.length > 0 && <> + Active + {active.map(r => )} + } + + {planned.length > 0 && <> + Planned + {planned.map(r => )} + } + + {completed.length > 0 && <> + Completed + {completed.map(r => )} + } + + {MY_REGISTRATIONS.length === 0 &&
+ 🔍 +

No championships yet

+

Browse championships and start your journey!

+
} +
+
; +} + +/* ── Notifications ── */ +function Notifications({ onBack }) { + const [notifs, setNotifs] = useState(NOTIFICATIONS); + const markRead = (id) => setNotifs(p => p.map(n => n.id === id ? { ...n, read: true } : n)); + const markAllRead = () => setNotifs(p => p.map(n => ({ ...n, read: true }))); + const unread = notifs.filter(n => !n.read).length; + + const typeConfig = { + category_change: { icon: "🔄", color: c.yellow, label: "Category Changed" }, + payment_confirmed: { icon: "✅", color: c.green, label: "Payment Confirmed" }, + result: { icon: "🏆", color: c.accent, label: "Results" }, + deadline: { icon: "⏰", color: c.yellow, label: "Deadline Reminder" }, + style_change: { icon: "🔄", color: c.purple, label: "Style Changed" }, + registration_confirmed: { icon: "📋", color: c.green, label: "Registration" }, + announcement: { icon: "📢", color: c.blue, label: "Announcement" }, + }; + + return
+ 0 ? `${unread} unread` : "All caught up ✓"} onBack={onBack} right={ + unread > 0 ?
Read all
: null + } /> +
+ {notifs.length === 0 &&
+ 🔕 +

No notifications

+

You'll see updates from championships here

+
} + {notifs.map(n => { + const tc = typeConfig[n.type] || typeConfig.announcement; + return
markRead(n.id)} style={{ + display: "flex", gap: 12, padding: "12px 14px", borderRadius: 12, cursor: "pointer", + background: n.read ? c.card : `${tc.color}08`, + border: `1px solid ${n.read ? c.brd : `${tc.color}20`}`, + }}> +
{tc.icon}
+
+
+ {tc.label} +
+ {n.date} + {!n.read &&
} +
+
+

{n.message}

+

{n.champ}

+
+
; + })} +
+
; +} + +/* ── Search ── */ +function Search({ onTap }) { + const [q, setQ] = useState(""); + const [fl, setFl] = useState("all"); + const fs = [{ id: "all", l: "All" }, { id: "registration_open", l: "Open" }, { id: "upcoming", l: "Upcoming" }]; + const res = CHAMPS.filter(ch => (!q || ch.name.toLowerCase().includes(q.toLowerCase()) || ch.location.toLowerCase().includes(q.toLowerCase())) && (fl === "all" || ch.status === fl)); + return
+ +
+
+ 🔍 + setQ(e.target.value)} style={{ background: "transparent", border: "none", outline: "none", color: c.text, fontFamily: f.b, fontSize: 13, width: "100%" }} /> +
+
{fs.map(x =>
setFl(x.id)} style={{ fontFamily: f.m, fontSize: 10, fontWeight: 600, color: fl === x.id ? c.accent : c.dim, background: fl === x.id ? c.accentS : c.card, border: `1px solid ${fl === x.id ? `${c.accent}30` : c.brd}`, padding: "5px 12px", borderRadius: 16, cursor: "pointer" }}>{x.l}
)}
+
{res.length ? res.map(ch => ) :
🤷

No results

}
+
+
; +} + +/* ── Profile ── */ +function Profile() { + return
+ +
+
+
💃
+

{USER.name}

+

{USER.instagram}

+
+
+ {[{ i: "📍", l: "City", v: USER.city }, { i: "💃", l: "Disciplines", v: USER.disciplines.join(", ") }, { i: "📅", l: "Experience", v: `${USER.experienceYears} years` }, { i: "🎓", l: "Instructor", v: USER.isInstructor ? "Yes" : "No" }].map(r => + {r.i}

{r.l}

{r.v}

+ )} +
+ Eligible Categories + {["Amateur (Exotic)", "Semi-Pro (Exotic)", "Duets & Groups", "Amateur (Pole Art)", "Semi-Pro (Pole Art)"].map(cat => +
{cat}
+ )}
+ Stats +
{[{ n: "2", l: "Champs", co: c.accent }, { n: "1", l: "Passed", co: c.green }, { n: "1", l: "Pending", co: c.yellow }].map(s => +

{s.n}

{s.l}

+ )}
+
{["Edit Profile", "Competition History", "Notifications", "Settings", "Log Out"].map((x, i, a) => +
{x}
+ )}
+
+
; +} + +/* ── App Shell ── */ +export default function App() { + const [scr, setScr] = useState("home"); + const [sel, setSel] = useState(null); + const [prev, setPrev] = useState("home"); + + const go = (s, ch) => { setPrev(scr); setScr(s); if (ch) setSel(ch); }; + const goBack = () => { setScr(prev || "home"); setSel(null); }; + + const render = () => { + if (scr === "progress" && sel) return go("detail")} />; + if (scr === "detail" && sel) return go("progress", ch)} />; + if (scr === "notifications") return go("home")} />; + if (scr === "my") return go("detail", ch)} onProgress={ch => go("progress", ch)} />; + if (scr === "search") return go("detail", ch)} />; + if (scr === "profile") return ; + return go("detail", ch)} onNotifications={() => go("notifications")} />; + }; + + const showNav = scr === "home" || scr === "search" || scr === "profile" || scr === "my"; + + return
+ + +
+
+ 9:41 +
+ ●●● +
+
{render()}
+ {showNav &&
+
; +} diff --git a/dancechamp-claude-code/prototypes/org-app.jsx b/dancechamp-claude-code/prototypes/org-app.jsx new file mode 100644 index 0000000..5c50646 --- /dev/null +++ b/dancechamp-claude-code/prototypes/org-app.jsx @@ -0,0 +1,683 @@ +import { useState } from "react"; + +/* ── Data ── */ +const ORG = { name: "Zero Gravity Team", instagram: "@zerogravity_pole", logo: "💃" }; + +const makeCh = (overrides) => ({ + id: "", name: "", subtitle: "", eventDate: "", regStart: "", regEnd: "", location: "", venue: "", accent: "#D4145A", image: "💃", status: "draft", + disciplines: [], styles: [], fees: null, judging: [], penalties: [], judges: [], rules: [], costumeRules: [], members: [], + formUrl: "", rulesUrl: "", + configured: { info: false, categories: false, fees: false, rules: false, judging: false }, + ...overrides, +}); + +const INITIAL_CHAMPS = [ + makeCh({ + id: "ch1", name: "Zero Gravity", subtitle: "International Pole Exotic Championship", + eventDate: "May 30, 2026", regStart: "Feb 1, 2026", regEnd: "Apr 22, 2026", location: "Minsk, Belarus", venue: "Prime Hall", status: "registration_open", accent: "#D4145A", image: "💃", + disciplines: [ + { name: "Exotic Pole Dance", levels: ["Beginners", "Amateur", "Semi-Pro", "Profi", "Elite", "Duets & Groups"] }, + { name: "Pole Art", levels: ["Amateur", "Semi-Pro", "Profi"] }, + ], + styles: ["Classic", "Flow", "Theater"], + fees: { videoSelection: "50 BYN / 1,500 RUB", solo: "280 BYN / 7,500 RUB", duet: "210 BYN / 5,800 RUB pp", group: "190 BYN / 4,500 RUB pp" }, + judging: [{ name: "Image", max: 10 }, { name: "Artistry", max: 10 }, { name: "Choreography", max: 10 }, { name: "Musicality", max: 10 }, { name: "Technique", max: 10 }, { name: "Overall", max: 10 }], + penalties: [{ name: "Missed element", val: "-2" }, { name: "Fall", val: "-2" }, { name: "Leaving stage", val: "DQ" }, { name: "Exposure", val: "DQ" }], + judges: [ + { id: "j1", name: "Anastasia Skukhtorova", instagram: "@skukhtorova", bio: "World Pole Art Champion. International judge with 10+ years experience." }, + { id: "j2", name: "Marion Crampe", instagram: "@marioncrampe", bio: "Pole Art legend, multiple international championship winner and judge." }, + { id: "j3", name: "Dmitry Politov", instagram: "@dmitry_politov", bio: "World Pole Sports Champion. Certified IPSF judge." }, + ], + rules: ["Must be 18+", "Valid life & health insurance", "No lotions/bronzers 24h before", "Grip aids allowed (no wax/rosin)", "Judges' decision is final"], + costumeRules: ["Neat and well-fitted", "No advertising", "No spikes/sharp objects", "Specialized shoes for Exotic"], + configured: { info: true, categories: true, fees: true, rules: true, judging: true }, + members: [ + { id: "m1", name: "Alex Petrova", instagram: "@alex_pole", level: "Semi-Pro", style: "Classic", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 3, videoUrl: "https://youtube.com/...", feePaid: false, receiptUploaded: true, insuranceUploaded: false, passed: null, city: "Moscow" }, + { id: "m2", name: "Maria Ivanova", instagram: "@maria_exotic", level: "Amateur", style: "Flow", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: true, passed: true, city: "Minsk" }, + { id: "m3", name: "Elena Kozlova", instagram: "@elena.pole", level: "Profi", style: "Theater", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: false, passed: true, city: "St. Petersburg" }, + { id: "m4", name: "Daria Sokolova", instagram: "@daria_art", level: "Amateur", style: "Classic", discipline: "Pole Art", type: "solo", stepsCompleted: 4, videoUrl: "https://youtube.com/...", feePaid: false, receiptUploaded: true, insuranceUploaded: false, passed: null, city: "Kyiv" }, + { id: "m5", name: "Anna Belova", instagram: "@anna.b_pole", level: "Beginners", style: "Flow", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 2, videoUrl: null, feePaid: false, receiptUploaded: false, insuranceUploaded: false, passed: null, city: "Minsk" }, + { id: "m6", name: "Olga Morozova", instagram: "@olga_exotic", level: "Elite", style: "Classic", discipline: "Exotic Pole Dance", type: "solo", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: true, passed: false, city: "Moscow" }, + { id: "m7", name: "Katya & Nina", instagram: "@katya_nina", level: "Semi-Pro", style: "Theater", discipline: "Exotic Pole Dance", type: "duet", stepsCompleted: 5, videoUrl: "https://youtube.com/...", feePaid: true, receiptUploaded: true, insuranceUploaded: false, passed: null, city: "Kazan" }, + ], + }), + makeCh({ + id: "ch2", name: "Pole Star", subtitle: "National Pole Championship", + eventDate: "Jul 12–13, 2026", regStart: "", regEnd: "", location: "Moscow, Russia", venue: "Crystal Hall", status: "draft", accent: "#7C3AED", image: "⭐", + configured: { info: true, categories: false, fees: false, rules: false, judging: false }, + members: [], + }), +]; + +/* ── Theme ── */ +const c = { bg: "#08070D", card: "#12111A", cardH: "#1A1926", brd: "#1F1E2E", text: "#F2F0FA", dim: "#5E5C72", mid: "#8F8DA6", accent: "#D4145A", accentS: "rgba(212,20,90,0.10)", green: "#10B981", greenS: "rgba(16,185,129,0.10)", yellow: "#F59E0B", yellowS: "rgba(245,158,11,0.10)", purple: "#8B5CF6", purpleS: "rgba(139,92,246,0.10)", blue: "#60A5FA", blueS: "rgba(96,165,250,0.10)", red: "#EF4444", redS: "rgba(239,68,68,0.10)" }; +const f = { d: "'Playfair Display',Georgia,serif", b: "'DM Sans','Segoe UI',sans-serif", m: "'JetBrains Mono',monospace" }; + +/* ── Shared UI ── */ +const Cd = ({ children, style: s }) =>
{children}
; +const ST = ({ children, right }) =>

{children}

{right}
; +const Bg = ({ label, color, bg }) => {label}; + +function Hdr({ title, subtitle, onBack, right }) { + return
+ {onBack &&
} +

{title}

{subtitle &&

{subtitle}

}
+ {right} +
; +} + +function Tabs({ tabs, active, onChange, accent: ac }) { + return
+ {tabs.map(t =>
onChange(t.id)} style={{ fontFamily: f.m, fontSize: 9, fontWeight: 600, letterSpacing: 0.3, display: "flex", alignItems: "center", gap: 4, color: active === t.id ? ac || c.accent : c.dim, background: active === t.id ? `${ac || c.accent}15` : "transparent", border: `1px solid ${active === t.id ? `${ac || c.accent}30` : "transparent"}`, padding: "5px 10px", borderRadius: 16, cursor: "pointer", whiteSpace: "nowrap" }}> + {t.done !== undefined && } + {t.label} +
)} +
; +} + +function Nav({ active, onChange }) { + return
+ {[{ id: "dash", i: "📊", l: "Dashboard" }, { id: "orgSettings", i: "⚙️", l: "Settings" }].map(x => +
onChange(x.id)} style={{ display: "flex", flexDirection: "column", alignItems: "center", gap: 2, cursor: "pointer", opacity: active === x.id ? 1 : 0.35 }}>{x.i}{x.l}
+ )} +
; +} + +function Input({ label, value, onChange, placeholder }) { + return
+

{label}

+ onChange(e.target.value)} placeholder={placeholder} style={{ width: "100%", padding: "10px 12px", borderRadius: 10, background: c.bg, border: `1px solid ${c.brd}`, color: c.text, fontFamily: f.b, fontSize: 13, outline: "none", boxSizing: "border-box" }} /> +
; +} + +function TagEditor({ items, onAdd, onRemove, color, placeholder, addLabel }) { + const [val, setVal] = useState(""); + const submit = () => { if (val.trim()) { onAdd(val.trim()); setVal(""); } }; + return
+
+ {items.map((item, i) =>
+ {item} + onRemove(i)} style={{ fontSize: 10, color: c.dim, cursor: "pointer", lineHeight: 1 }}>× +
)} + {items.length === 0 && None added yet} +
+
+ setVal(e.target.value)} placeholder={placeholder} onKeyDown={e => e.key === "Enter" && submit()} style={{ flex: 1, padding: "8px 12px", borderRadius: 8, background: c.bg, border: `1px solid ${c.brd}`, color: c.text, fontFamily: f.b, fontSize: 12, outline: "none" }} /> +
+
+
+
; +} + +/* ── Dashboard ── */ +function Dashboard({ champs, org, onChampTap, onCreateChamp }) { + return
+ {org.logo}
+ } /> +
+
+
+
+

New Championship

Quick create — configure later

+ +
+ + {champs.length} events}>Your Championships + + {champs.map(ch => { + const cfg = ch.configured; + const done = Object.values(cfg).filter(Boolean).length; + const total = Object.keys(cfg).length; + const ready = done === total; + const stMap = { registration_open: { l: "LIVE", c: c.green, b: c.greenS }, draft: { l: `SETUP ${done}/${total}`, c: c.yellow, b: c.yellowS } }; + const st = stMap[ch.status] || stMap.draft; + return
onChampTap(ch)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, overflow: "hidden", cursor: "pointer" }}> +
+
+
+
{ch.image}
+
+
+

{ch.name}

+ +
+

{ch.eventDate} · {ch.location}

+
+
+ {/* Readiness bar */} + {!ready &&
+
+
+
+
+ {Object.entries(cfg).map(([k, v]) => {v ? "✓" : "○"} {k})} +
+
} + {/* Stats for live champs */} + {ch.status === "registration_open" &&
+ {[{ n: ch.members.length, l: "Members", co: c.mid }, { n: ch.members.filter(m => m.passed === true).length, l: "Passed", co: c.green }, { n: ch.members.filter(m => m.videoUrl && m.passed === null).length, l: "Pending", co: c.yellow }].map(s => +

{s.n}

{s.l}

+ )} +
} +
+
; + })} +
+
; +} + +/* ── Championship Detail (configurable tabs) ── */ +function ChampDetail({ ch: initial, onBack, onMemberTap, onUpdate }) { + const [ch, setCh] = useState(initial); + const [tab, setTab] = useState("Overview"); + const [members, setMembers] = useState(ch.members); + const [memFilter, setMemFilter] = useState("all"); + const [memSearch, setMemSearch] = useState(""); + const [editing, setEditing] = useState(null); + const [newJudge, setNewJudge] = useState({ name: "", instagram: "", bio: "" }); + + const upd = (key, val) => setCh(p => ({ ...p, [key]: val })); + const markDone = (section) => setCh(p => ({ ...p, configured: { ...p.configured, [section]: true } })); + const allDone = Object.values(ch.configured).every(Boolean); + + const stats = { + total: members.length, videoSent: members.filter(m => m.videoUrl).length, + passed: members.filter(m => m.passed === true).length, failed: members.filter(m => m.passed === false).length, + pending: members.filter(m => m.videoUrl && m.passed === null).length, feePaid: members.filter(m => m.feePaid).length, + receipts: members.filter(m => m.receiptUploaded && !m.feePaid).length, + }; + + const decide = (id, pass) => setMembers(p => p.map(m => m.id === id ? { ...m, passed: pass } : m)); + + const tabDefs = [ + { id: "Overview", label: "Overview" }, + { id: "Categories", label: "Categories", done: ch.configured.categories }, + { id: "Fees", label: "Fees", done: ch.configured.fees }, + { id: "Rules", label: "Rules", done: ch.configured.rules }, + { id: "Judges", label: "Judges", done: ch.configured.judging }, + ...(ch.status === "registration_open" ? [{ id: "Members", label: `Members (${members.length})` }, { id: "Results", label: "Results" }] : []), + ]; + + const memFilters = [ + { id: "all", l: "All", n: members.length }, { id: "receipts", l: "📸 Receipts", n: stats.receipts }, + { id: "videos", l: "🎬 Videos", n: stats.pending }, { id: "passed", l: "✅ Passed", n: stats.passed }, + ]; + const filteredMem = members.filter(m => { + const q = !memSearch || m.name.toLowerCase().includes(memSearch.toLowerCase()) || m.instagram.toLowerCase().includes(memSearch.toLowerCase()); + if (!q) return false; + if (memFilter === "receipts") return m.receiptUploaded && !m.feePaid; + if (memFilter === "videos") return m.videoUrl && m.passed === null; + if (memFilter === "passed") return m.passed === true; + return true; + }); + + return
+ setCh(p => ({ ...p, status: "registration_open" }))} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 700, color: "#fff", background: c.green, padding: "6px 12px", borderRadius: 8, cursor: "pointer" }}>🚀 Go Live
: null + } /> +
+ + + {/* ═══ OVERVIEW ═══ */} + {tab === "Overview" &&
+ {/* Setup progress */} + {ch.status === "draft" && + {Object.values(ch.configured).filter(Boolean).length}/{Object.keys(ch.configured).length}}>⚙️ Setup Progress + {Object.entries(ch.configured).map(([section, done]) => { + const tabMap = { info: "Overview", categories: "Categories", fees: "Fees", rules: "Rules", judging: "Judges" }; + return
!done && setTab(tabMap[section] || section)} style={{ display: "flex", alignItems: "center", gap: 10, padding: "8px 0", borderBottom: `1px solid ${c.brd}`, cursor: done ? "default" : "pointer" }}> +
{done ? "✓" : ""}
+ {section === "judging" ? "judges" : section} + {!done && Configure ›} +
; + })} + {allDone &&
setCh(p => ({ ...p, status: "registration_open" }))} style={{ marginTop: 10, padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}> + 🚀 Open Registration +
} +
} + + {/* Info (always editable) */} + +
+

Event Info

+
setEditing(editing === "info" ? null : "info")} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 600, color: editing === "info" ? c.dim : "#fff", background: editing === "info" ? "transparent" : ch.accent, border: `1px solid ${editing === "info" ? c.brd : ch.accent}`, padding: "3px 10px", borderRadius: 6, cursor: "pointer" }}>{editing === "info" ? "✕ Close" : "✎ Edit"}
+
+ {editing === "info" ? <> + upd("name", v)} placeholder="Championship name" /> + upd("subtitle", v)} placeholder="Subtitle" /> + upd("eventDate", v)} placeholder="e.g. May 30, 2026" /> + upd("location", v)} placeholder="City, Country" /> + upd("venue", v)} placeholder="Venue name" /> +
+

REGISTRATION PERIOD

+
+
upd("regStart", v)} placeholder="e.g. Feb 1, 2026" />
+
upd("regEnd", v)} placeholder="e.g. Apr 22, 2026" />
+
+

⚠️ Registration close date must be before event date

+
{ markDone("info"); setEditing(null); }} style={{ padding: "10px", borderRadius: 8, background: c.green, textAlign: "center", cursor: "pointer" }}>✓ Save
+ :
+
+ 📅 {ch.eventDate || "—"} + 📍 {ch.venue ? `${ch.venue}, ` : ""}{ch.location || "—"} +
+ {(ch.regStart || ch.regEnd) &&
+ 📋 Registration: + {ch.regStart || "?"} → {ch.regEnd || "?"} +
} +
} + + + {/* Stats (only for live) */} + {ch.status === "registration_open" && <> +
+ {[{ n: stats.total, l: "Members", co: c.mid }, { n: stats.passed, l: "Passed", co: c.green }, { n: stats.failed, l: "Failed", co: c.red }, { n: stats.pending, l: "Pending", co: c.yellow }].map(s => +
+

{s.n}

+

{s.l}

+
+ )} +
+ + tap to view}>⚡ Needs Action + {[ + { l: "Receipts to review", n: stats.receipts, icon: "📸", co: c.yellow, go: "Members" }, + { l: "Videos to review", n: stats.pending, icon: "🎬", co: c.blue, go: "Results" }, + ].map(a =>
{ if (a.go === "Members") setMemFilter("receipts"); setTab(a.go); }} style={{ display: "flex", alignItems: "center", gap: 10, padding: "10px 0", borderBottom: `1px solid ${c.brd}`, cursor: "pointer" }}> + {a.icon} + {a.l} + {a.n} + +
)} +
+ } +
} + + {/* ═══ CATEGORIES ═══ */} + {tab === "Categories" &&
+ + : null}>Levels + d.levels).filter((v, i, a) => a.indexOf(v) === i)} color={ch.accent} placeholder="Add level (e.g. Amateur)" + onAdd={v => { const d = ch.disciplines.length ? [...ch.disciplines] : [{ name: "Exotic Pole Dance", levels: [] }]; d[0] = { ...d[0], levels: [...d[0].levels, v] }; upd("disciplines", d); }} + onRemove={i => { const all = ch.disciplines.flatMap(d => d.levels).filter((v, idx, a) => a.indexOf(v) === idx); const rm = all[i]; const d = ch.disciplines.map(d2 => ({ ...d2, levels: d2.levels.filter(l => l !== rm) })); upd("disciplines", d); }} /> + + + Styles + upd("styles", [...ch.styles, v])} onRemove={i => upd("styles", ch.styles.filter((_, j) => j !== i))} /> + + {!ch.configured.categories && (ch.disciplines.some(d => d.levels.length > 0) && ch.styles.length > 0) &&
markDone("categories")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}> + ✓ Mark Categories as Done +
} +
} + + {/* ═══ FEES ═══ */} + {tab === "Fees" &&
+ + : null}>Video Selection Fee + upd("fees", { ...ch.fees, videoSelection: v })} placeholder="e.g. 50 BYN / 1,500 RUB" /> + + + Championship Fees + upd("fees", { ...ch.fees, solo: v })} placeholder="e.g. 280 BYN" /> + upd("fees", { ...ch.fees, duet: v })} placeholder="e.g. 210 BYN" /> + upd("fees", { ...ch.fees, group: v })} placeholder="e.g. 190 BYN" /> + + {!ch.configured.fees && ch.fees?.videoSelection &&
markDone("fees")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}> + ✓ Mark Fees as Done +
} +
} + + {/* ═══ RULES ═══ */} + {tab === "Rules" &&
+ + : null}>General Rules + upd("rules", [...ch.rules, v])} onRemove={i => upd("rules", ch.rules.filter((_, j) => j !== i))} /> + + + Costume Rules + upd("costumeRules", [...ch.costumeRules, v])} onRemove={i => upd("costumeRules", ch.costumeRules.filter((_, j) => j !== i))} /> + + + Scoring Criteria (0–10) + {ch.judging.map((j, i) =>
+ {j.name} + 0–{j.max} + upd("judging", ch.judging.filter((_, k) => k !== i))} style={{ fontSize: 10, color: c.dim, cursor: "pointer" }}>× +
)} + upd("judging", [...ch.judging, { name: v, max: 10 }])} onRemove={() => {}} /> +
+ + Penalties + {ch.penalties.map((p, i) =>
+ {p.name} + {p.val} + upd("penalties", ch.penalties.filter((_, k) => k !== i))} style={{ fontSize: 10, color: c.dim, cursor: "pointer" }}>× +
)} + { const [name, val] = v.includes(":") ? v.split(":").map(s => s.trim()) : [v, "-2"]; upd("penalties", [...ch.penalties, { name, val }]); }} onRemove={() => {}} /> +
+ {!ch.configured.rules && ch.rules.length > 0 &&
markDone("rules")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}> + ✓ Mark Rules as Done +
} +
} + + {/* ═══ JUDGES ═══ */} + {tab === "Judges" &&
+ : {ch.judges.length} judges}>Jury Panel + {ch.judges.map((j, i) => +
+
👩‍⚖️
+
+
+

{j.name}

+ upd("judges", ch.judges.filter((_, k) => k !== i))} style={{ fontSize: 10, color: c.dim, cursor: "pointer", padding: "4px" }}>× +
+

{j.instagram}

+

{j.bio}

+
+
+
)} + + {/* Add judge form */} + + Add Judge + setNewJudge(p => ({ ...p, name: v }))} placeholder="e.g. Anastasia Skukhtorova" /> + setNewJudge(p => ({ ...p, instagram: v }))} placeholder="e.g. @skukhtorova" /> + setNewJudge(p => ({ ...p, bio: v }))} placeholder="Experience, titles, achievements..." /> +
{ if (newJudge.name) { upd("judges", [...ch.judges, { ...newJudge, id: `j${Date.now()}` }]); setNewJudge({ name: "", instagram: "", bio: "" }); } }} style={{ padding: "10px", borderRadius: 8, background: newJudge.name ? c.purple : c.brd, textAlign: "center", cursor: newJudge.name ? "pointer" : "default", opacity: newJudge.name ? 1 : 0.5 }}> + + Add Judge +
+
+ + {!ch.configured.judging && ch.judges.length > 0 &&
markDone("judging")} style={{ padding: "12px", borderRadius: 10, background: c.green, textAlign: "center", cursor: "pointer" }}> + ✓ Mark Judges as Done +
} +
} + + {/* ═══ MEMBERS ═══ */} + {tab === "Members" &&
+
+ 🔍 + setMemSearch(e.target.value)} style={{ background: "transparent", border: "none", outline: "none", color: c.text, fontFamily: f.b, fontSize: 13, width: "100%" }} /> +
+
+ {memFilters.map(fi =>
setMemFilter(fi.id)} style={{ fontFamily: f.m, fontSize: 9, fontWeight: 600, whiteSpace: "nowrap", color: memFilter === fi.id ? ch.accent : c.dim, background: memFilter === fi.id ? `${ch.accent}15` : "transparent", border: `1px solid ${memFilter === fi.id ? `${ch.accent}30` : "transparent"}`, padding: "5px 10px", borderRadius: 16, cursor: "pointer" }}>{fi.l} ({fi.n})
)} +
+ {filteredMem.map(m =>
onMemberTap(m, ch)} style={{ background: c.card, border: `1px solid ${c.brd}`, borderRadius: 14, padding: 12, cursor: "pointer" }}> +
+

{m.name}

{m.instagram}

+ +
+
{[m.level, m.style, m.city].map(t => {t})}
+
)} + {filteredMem.length === 0 &&
🤷

No members match

} +
} + + {/* ═══ RESULTS ═══ */} + {tab === "Results" &&
+
+ {[{ n: stats.pending, l: "Pending", co: c.yellow }, { n: stats.passed, l: "Passed", co: c.green }, { n: stats.failed, l: "Failed", co: c.red }].map(s => +
+

{s.n}

+

{s.l}

+
+ )} +
+ {members.filter(m => m.videoUrl && m.passed === null).map(m => +
+

{m.name}

{m.level} · {m.style}

+ 🎥 View +
+
+
decide(m.id, true)} style={{ flex: 1, padding: "10px", borderRadius: 10, background: `${c.green}15`, border: `1px solid ${c.green}30`, cursor: "pointer", textAlign: "center" }}>✅ Pass
+
decide(m.id, false)} style={{ flex: 1, padding: "10px", borderRadius: 10, background: `${c.red}15`, border: `1px solid ${c.red}30`, cursor: "pointer", textAlign: "center" }}>❌ Fail
+
+
)} + {members.filter(m => m.passed !== null).length > 0 && <> + Decided + {members.filter(m => m.passed !== null).map(m =>
+

{m.name}

{m.level}

+ +
)} + } +
📢 Publish Results
+
} + +
+
; +} + +/* ── Member Detail ── */ +function MemberDetail({ member, champ, onBack }) { + const [m, setM] = useState(member); + const [showLvl, setShowLvl] = useState(false); + const [showSty, setShowSty] = useState(false); + const levels = champ.disciplines.flatMap(d => d.levels).filter((v, i, a) => a.indexOf(v) === i); + + return
+ +
+ +
👤
+

{m.name}

{m.instagram}

📍 {m.city}

+ +
+ + + Registration + {[{ l: "Discipline", v: m.discipline }, { l: "Type", v: m.type }].map(r =>
{r.l}{r.v}
)} + + {/* Level */} +
+
+ Level +
+ {m.level} +
{ setShowLvl(!showLvl); setShowSty(false); }} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 600, color: showLvl ? c.dim : "#fff", background: showLvl ? "transparent" : champ.accent, border: `1px solid ${showLvl ? c.brd : champ.accent}`, padding: "3px 10px", borderRadius: 6, cursor: "pointer" }}>{showLvl ? "✕" : "✎ Edit"}
+
+
+ {showLvl &&
+

⚠️ Member will be notified

+ {levels.map(l =>
{ setM(p => ({ ...p, level: l })); setShowLvl(false); }} style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", marginBottom: 3, background: l === m.level ? `${champ.accent}15` : "transparent", border: `1px solid ${l === m.level ? `${champ.accent}30` : c.brd}` }}> + {l} +
)} +
} +
+ + {/* Style */} +
+
+ Style +
+ {m.style} +
{ setShowSty(!showSty); setShowLvl(false); }} style={{ fontFamily: f.b, fontSize: 10, fontWeight: 600, color: showSty ? c.dim : "#fff", background: showSty ? "transparent" : c.purple, border: `1px solid ${showSty ? c.brd : c.purple}`, padding: "3px 10px", borderRadius: 6, cursor: "pointer" }}>{showSty ? "✕" : "✎ Edit"}
+
+
+ {showSty &&
+

⚠️ Member will be notified

+ {champ.styles.map(s =>
{ setM(p => ({ ...p, style: s })); setShowSty(false); }} style={{ padding: "8px 10px", borderRadius: 6, cursor: "pointer", marginBottom: 3, background: s === m.style ? `${c.purple}15` : "transparent", border: `1px solid ${s === m.style ? `${c.purple}30` : c.brd}` }}> + {s} +
)} +
} +
+
+ + {/* Video */} + + 🎬 Video + {m.videoUrl ? <> +
🎥

{m.videoUrl}

+ {m.passed === null ?
+
setM(p => ({ ...p, passed: true }))} style={{ flex: 1, padding: "10px", borderRadius: 10, background: c.green, cursor: "pointer", textAlign: "center" }}>✅ Pass
+
setM(p => ({ ...p, passed: false }))} style={{ flex: 1, padding: "10px", borderRadius: 10, background: c.red, cursor: "pointer", textAlign: "center" }}>❌ Fail
+
: } + :

No video yet

} +
+ + {/* Payment */} + + 💳 Payment +
+

Video fee

{champ.fees?.videoSelection || "—"}

+ {m.receiptUploaded && !m.feePaid ?
setM(p => ({ ...p, feePaid: true }))} style={{ padding: "6px 12px", borderRadius: 8, background: `${c.green}15`, border: `1px solid ${c.green}30`, cursor: "pointer" }}>📸 Confirm
+ : } +
+
+ +
🔔Send Notification
+
+
; +} + +/* ── Quick Create ── */ +function QuickCreate({ onBack, onDone }) { + const [name, setName] = useState(""); + const [eventDate, setEventDate] = useState(""); + const [location, setLocation] = useState(""); + + return
+ +
+ + + + + + + +

💡 What happens next?

+

Your championship will be created as a draft. Configure categories, fees, rules, and judging at your own pace. Once everything is set, hit "Go Live" to open registration.

+
+ +
name && onDone(makeCh({ id: `ch${Date.now()}`, name, eventDate, location, status: "draft", configured: { info: !!eventDate && !!location, categories: false, fees: false, rules: false, judging: false } }))} style={{ padding: "14px", borderRadius: 12, background: name ? c.accent : c.brd, textAlign: "center", cursor: name ? "pointer" : "default", opacity: name ? 1 : 0.5 }}> + ✨ Create Draft +
+
+
; +} + +/* ── Org Settings ── */ +function OrgSettings({ org, onUpdateOrg }) { + const [editing, setEditing] = useState(false); + const [name, setName] = useState(org.name); + const [instagram, setInstagram] = useState(org.instagram); + const [subScreen, setSubScreen] = useState(null); + + if (subScreen === "notifications") return
+ setSubScreen(null)} /> +
+ {[{ l: "Push notifications", d: "Get notified on new registrations", on: true }, + { l: "Email notifications", d: "Receive email for payments & uploads", on: true }, + { l: "Registration alerts", d: "When a new member registers", on: true }, + { l: "Payment alerts", d: "When a receipt is uploaded", on: true }, + { l: "Deadline reminders", d: "Auto-remind members before deadlines", on: false }, + ].map(n => )} +
+
; + + if (subScreen === "accounts") return
+ setSubScreen(null)} /> +
+ {[{ name: "Instagram", handle: org.instagram, icon: "📸", connected: true, color: c.purple }, + { name: "Gmail", handle: "zerogravity@gmail.com", icon: "📧", connected: true, color: c.red }, + { name: "Telegram", handle: "@zerogravity_bot", icon: "💬", connected: false, color: c.blue }, + ].map(a => +
{a.icon}
+
+

{a.name}

+

{a.connected ? a.handle : "Not connected"}

+
+ +
)} +
+
; + + return
+ +
+ {/* Profile header */} +
+
{org.logo}
+ {editing ?
+ + +
+
setEditing(false)} style={{ flex: 1, padding: "10px", borderRadius: 8, background: c.card, border: `1px solid ${c.brd}`, textAlign: "center", cursor: "pointer" }}>Cancel
+
{ onUpdateOrg({ name, instagram }); setEditing(false); }} style={{ flex: 1, padding: "10px", borderRadius: 8, background: c.green, textAlign: "center", cursor: "pointer" }}>✓ Save
+
+
: <> +

{org.name}

+

{org.instagram}

+ } +
+ + {/* Menu items — hide when editing */} + {!editing &&
+ {[ + { label: "Edit Organization Profile", action: () => setEditing(true), icon: "✎" }, + { label: "Notification Preferences", action: () => setSubScreen("notifications"), icon: "🔔" }, + { label: "Connected Accounts", action: () => setSubScreen("accounts"), icon: "🔗" }, + { label: "Help & Support", action: () => {}, icon: "❓" }, + { label: "Log Out", action: () => {}, icon: "🚪", danger: true }, + ].map((x, i, a) => +
+ {x.icon} + {x.label} + +
+ )} +
} +
+
; +} + +/* Toggle row helper */ +function ToggleRow({ label, desc, defaultOn }) { + const [on, setOn] = useState(defaultOn); + return +
+

{label}

+ {desc &&

{desc}

} +
+
setOn(!on)} style={{ width: 42, height: 24, borderRadius: 12, background: on ? c.green : c.brd, padding: 2, cursor: "pointer", transition: "background 0.2s" }}> +
+
+ ; +} + +/* ── App Shell ── */ +export default function OrgApp() { + const [champs, setChamps] = useState(INITIAL_CHAMPS); + const [org, setOrg] = useState(ORG); + const [scr, setScr] = useState("dash"); + const [selChamp, setSelChamp] = useState(null); + const [selMember, setSelMember] = useState(null); + + const addChamp = ch => { setChamps(p => [...p, ch]); setSelChamp(ch); setScr("champ"); }; + const updateOrg = updates => setOrg(p => ({ ...p, ...updates })); + + const render = () => { + if (scr === "create") return setScr("dash")} onDone={addChamp} />; + if (scr === "champ" && selChamp) return { setScr("dash"); setSelChamp(null); }} onMemberTap={(m, ch) => { setSelMember({ m, ch }); setScr("member"); }} />; + if (scr === "member" && selMember) return setScr("champ")} />; + if (scr === "orgSettings") return ; + return { setSelChamp(ch); setScr("champ"); }} onCreateChamp={() => setScr("create")} />; + }; + + const showNav = scr === "dash" || scr === "orgSettings"; + + return
+ + +
+
+ 9:41 +
+ ●●● +
+
{render()}
+ {showNav &&
+
; +} diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..d2c2d73 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,36 @@ +version: "3.9" + +services: + db: + image: postgres:16-alpine + environment: + POSTGRES_USER: pole + POSTGRES_PASSWORD: pole + POSTGRES_DB: poledance + ports: + - "5432:5432" + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U pole -d poledance"] + interval: 5s + timeout: 5s + retries: 10 + + api: + build: ./backend + ports: + - "8000:8000" + environment: + DATABASE_URL: postgresql+asyncpg://pole:pole@db:5432/poledance + env_file: + - .env + depends_on: + db: + condition: service_healthy + volumes: + - ./backend:/app + command: uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload + +volumes: + pgdata: diff --git a/mobile/.gitignore b/mobile/.gitignore new file mode 100644 index 0000000..d914c32 --- /dev/null +++ b/mobile/.gitignore @@ -0,0 +1,41 @@ +# Learn more https://docs.github.com/en/get-started/getting-started-with-git/ignoring-files + +# dependencies +node_modules/ + +# Expo +.expo/ +dist/ +web-build/ +expo-env.d.ts + +# Native +.kotlin/ +*.orig.* +*.jks +*.p8 +*.p12 +*.key +*.mobileprovision + +# Metro +.metro-health-check* + +# debug +npm-debug.* +yarn-debug.* +yarn-error.* + +# macOS +.DS_Store +*.pem + +# local env files +.env*.local + +# typescript +*.tsbuildinfo + +# generated native folders +/ios +/android diff --git a/mobile/App.tsx b/mobile/App.tsx new file mode 100644 index 0000000..3d10746 --- /dev/null +++ b/mobile/App.tsx @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import { StatusBar } from 'expo-status-bar'; +import RootNavigator from './src/navigation'; +import { useAuthStore } from './src/store/auth.store'; + +export default function App() { + const initialize = useAuthStore((s) => s.initialize); + + useEffect(() => { + initialize(); + }, []); + + return ( + <> + + + + ); +} diff --git a/mobile/app.json b/mobile/app.json new file mode 100644 index 0000000..076e6d2 --- /dev/null +++ b/mobile/app.json @@ -0,0 +1,28 @@ +{ + "expo": { + "name": "Pole Championships", + "slug": "pole-championships", + "version": "1.0.0", + "orientation": "portrait", + "icon": "./assets/icon.png", + "userInterfaceStyle": "light", + "newArchEnabled": false, + "splash": { + "image": "./assets/splash-icon.png", + "resizeMode": "contain", + "backgroundColor": "#ffffff" + }, + "ios": { + "supportsTablet": true + }, + "android": { + "adaptiveIcon": { + "foregroundImage": "./assets/adaptive-icon.png", + "backgroundColor": "#ffffff" + } + }, + "web": { + "favicon": "./assets/favicon.png" + } + } +} diff --git a/mobile/assets/adaptive-icon.png b/mobile/assets/adaptive-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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 literal 0 HcmV?d00001 diff --git a/mobile/assets/icon.png b/mobile/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..a0b1526fc7b78680fd8d733dbc6113e1af695487 GIT binary patch literal 22380 zcma&NXFwBA)Gs`ngeqM?rCU%8AShC#M(H35F#)9rii(013!tDx|bcg~9p;sv(x$FOVKfIsreLf|7>hGMHJu^FJH{SV>t+=RyC;&j*-p&dS z00#Ms0m5kH$L?*gw<9Ww*BeXm9UqYx~jJ+1t_4 zJ1{Wx<45o0sR{IH8 zpmC-EeHbTu>$QEi`V0Qoq}8`?({Rz68cT=&7S_Iul9ZEM5bRQwBQDxnr>(iToF)+n z|JO^V$Ny90|8HRG;s3_y|EE!}{=bF6^uYgbVbpK_-xw{eD%t$*;YA)DTk&JD*qleJ z3TBmRf4+a|j^2&HXyGR4BQKdWw|n?BtvJ!KqCQ={aAW0QO*2B496##!#j&gBie2#! zJqxyG2zbFyOA35iJ|1mKYsk?1s;L@_PFX7rKfhZiQdNiEao^8KiD5~5!EgHUD82iG z2XpL^%96Md=;9x?U3$~srSaj;7MG>wT)P_wCb&+1hO4~8uflnL7sq6JejFX4?J(MR z(VPq?4ewa9^aaSgWBhg7Ud4T;BZ7{82adX7MF%W0zZ_mYu+wLYAP^lOQLYY@cUjE4 zBeFNA4tH1neDX`Q|J)mZ`?;#~XzBag&Di1NCjfbREm)XTezLrDtUcF|>r`6d+9;Z2K=0gYw6{= zO`r(C`LX~v_q!oQTzP=V(dpBYRX_m=XTYed%&nR+E%|WO3PI)^4uPRJk7kq+L(WmAOy(ux(#<@^3fSK25b1mHZ&DAw`q0&a5 zXU$pWf=NbJ*j}V$*`Y zMAz4Zi@A4?iMs{U8hRx*ihsZYHPTpP)TpG}jw4o_5!ny)yKkJoo=Bir+@d$gzUtPf z76rl^DOsUwy9uARy%q+*hrZZzh_{hGBXepC05GjPV+X0aCfbk@fQWuf;3wQF@_yMe zt5AXhdB6CNa}=s;{GA3bi9jK8Kx#cdW9+*ie&)lhyA|*h09Nk?0_r>m95{nVXO$6+ z$R>+ZL^ryBs*)RkM6AqpNS?#{nnq$qo^Vt5G+ytRnl4dc&s0sMr1WG4?WRPcp+ zP;4wHTl?f)^!Gj@FV%`g0(eGv;HbO<_}J0}FndK2L|Kcxs9q1mJ&rMg$cKcFmX!S! z0vJ1OH3owS*d>`!`*;8rrX8t`(L`=H!AifKdlcO~&e#f~Gz*D+&)!2#ud^j$6ZANS!q}@cvw*7N5+0Q4R zvKIiqx03&fsKF9NtB8=DY2R$GBF zFO>1hO8{sMa4qRW4rz_ZeDmKOIy>H_iVr#{5#Sj@pJ!sj&rhsFLFP!^^K&|Dr6uLtPu&2WmLoOp+72f`> zM88yjBZc@DHb&cF31E_s3Lc>O?h=~(jh!O*kcTy{W=1>28}m0z!NXv!+39S{1Oo=094 zX=(h?=(7}XGb1D8Le$|=j;d-;;crtG&kl~$1R;+jNJ~%pbCYscUVDFEU78K}k--e# za(QZW#pp2ud*;SAz*bwBzqqTRikI2Y#5?gmB4!gw{q?IKxBJ$Ekk*C1u@L4^va%|d zg`199czf=a{W_rZV(o9cO3-ss^nlj#!JCtP7Us%{K*#UAfC_J8t8O95*4X1neL!uT z7q+4#870U_4@PTELQHYcP!d#&(5s=1xX@nu4~{P ziXP#%91t7KLLnvdo!MHcGH5gCyUtMXC>j$4q!W8-qKL+{QA?W|P_g@&o};Qr{V>;Uw00_+`9LV$n}g$1Wz-iO^%O9@tw3qx-3ufU%wo0W1X6 zd5hj=!1>$2#x-W=@#r)rb>i#BX;&5+G{ip^1}TzYa#zzvid~=DT3juEZzPd*Ptx5PlmOekc^%T@qfGKnX zVLtTc?`|*HLs@&g^HLc-XM;hT*okFVoGV>Rk7|YR#rP|>d%?%Ac6a6tD?jV(PEM2| z)!GQ%0<#4uaBClL!}ieEL#lNYchYI!%yOx-k)Hrt@v}`10WkK6dpyGbIn3J}K<9>6 z&Qr3w#HH4O-)FlVQbmE0IsYU?*2#U}c**@5bJg+B;Z3a{C!Wn z%}5?fNU7QX-m!{(5YE8DV9$RRbxu+^pZ&ZnAiN>7Ej;=f|mchq~oo_duHA zm}UoOBhc=BYSg6-FC`~!vzKFuZxq)d%0s_mkb=8gcX@+)g%YXM+P;snBBP?OLzICI z^nONGyOXmz_6V@ewl4VaqES4q;1}i2cE%ze0*luwQ@4j=-woV5=th~qD7<$}vxHqH zki`K3_K?tAp3?w8qw7CdG)(7lggoq>PPlkt@rNqVm`Ycg!CT9)9T8abyZIZA;Y;5m z%X*dax+I%)X7Yjc(a(`}0da228T?%A)(62CEkfr13$PzqKi>>_-(@aRUSr2JRNn||G!L%}1dKJ|E9+0HUy|x0-9#8- z__=}bb&@;)o<6PQ+SsWesX{>caBlo2%~rhkUU6n+Pfy5N$X8vK18kZm*^~XJsG(og zBO`Kur%3CE5}R|r$by?(@1|{;bLg+dG6WvJ5JO>#SNDdi)Mq0e&KQ?o%pyICN1`}n zIPG++itoD%6Zjho*jBp)LaVIDkPL41VQx_s+y{K#ZZMFUJN!!59D>C?pv3!jpgav( zrWmF`%6QG9&{*|Y2TOEg;yXX+f+FH}@zJ?z;cQ;60`OsF+Pun!-_^Oh_aQkQeRK|! z@R;}3_d5Uqj>@W;{SAaq0{e2oR($}c?m}x>mw3U&EK8p zbDNT;)(io|2H)fID;xYi(7M`Pl2^igo1pxecivhQoZrDJYYqKXg7)kPm6M}H&wk?1 z|CR)0PYBK27ml4L*mD4!ulgjD!q2H)&b>^b(Z}^4enh{P^oa<(*DW{p)=!K!Cf2yxArAy8esW_t$!wO}OC;g>-Y;p?(8K5Lqzo zVOhL8FZn_oA~?Q9?Wp}%Z1Q|bKd}2%!+#WJCx^^$C*0K6QZ2#Lm}2_VciwAguz0^a zyw?EN>H_b-HZ}3A`6@(yG~8IYa)emU9NjV=esnMsEpL5I0ZtmYfC8%y6>s_lxxw#E zG^q&>1%X%Rq$(&YCp2v6OnGR-mI-$;?ekV}$>8saMk6~@idK;{+s(Zq?`iUsro#Rn zzK=vUonDa1DE+ob8@-xJ^13dF>)CrThqq%v97t^q4e`&PYde{8V33VaZdX`=oBAPu4=@9clN{P5AM&b z`|?IsKKKQs>6f)XqgFHWEv{GF=(s$!WorDO7lh60_n?q_z;I`mZq z*dn<86V%zQ*m>k6jwwD*+Tvl&G&c*s)!Qmq5P(FqOG?8SR457Mh3XI}o* zNHJnfNc3rddr4S%F5TL`3ttEi2p&B*92mBV{y_fFcD~9Cc1oH&eyi!@W)XDmr!-Lc}2ziivlJ7K)m%-)5hd*#%qjqpv-I0wp)Ww;Zmhe}i%+uMaYSzlf15j7cS4Lcg zSw_~_f!|o?!98lFa72N~m5HV*@680?k@kjT&o_ld&VK=i#LoRgmXTJI{t}u-HdRZ?xP84*Y8~` zqFW_yBG2VbRtq|$md@m7E{$t7b^3%Cqa|@prg-_BqkTptrIu-ROancLO)(0 z`=1nJO?$p%(=%NhuS`x@r3G||Oy!YPtYHd3F8}Gpd5? zgBlTI*{@j)(&e2)r%evo5bP~_(UYOO{MQk^fQqpvQIEd=s`Y7!rEyHF6#dd&lqXBj z{|hLWB%YCqcVlq&AE8P_$lodI-p~4@dR;nHMQ2FmIOOL`<)D1t5VfCd_YzcanOlBt zsL8m#o5134a;vzx!oLHR`N~~sP@WwvT?bz)a<^pV!b6r$f9^=S!iu>(V~l$UF_QW@ z!jio9i1}8uto)xGyTH-HFBncUqGi4lrD{Q`&u+;dL z7?|h3?1oggBM*H{DI5sULUT1H*YkzV_qLG^sc%iIgZTIw;OSOeyh1tMAY zSE>_9do_gknQA?7{grd7)rmnvoMHyAhTAnruXGW5CH(TqWX~?>l+3`Z`IZ{MAO_}t z>z0mi4wXAv4ZRp4DOLP=OH9o7w>!9tx#eDG2oy4Ma3!FI|DH(Z`MZqlPjidSN?!+$ zxAP0oI8On(1j=wbLHW9&CxWKM7y*dfaz2%0e>3Bk9$HH+poGt8IM4O2Zp!L+{o>)TGM-lB`>PR8Dne1b=v{V}GsGFDR6 zL?jl3X>eP9=IXDRx^qg$yDfIGM{KhS@4j*WHp6TdG>Mie2RHg82( z!YwvpPJtaPNlyo|V5-ByJ~FNdS3jtrR5LFZZFjc~l%lkvldKPru(A4oET?;Mo0KeZZgt?p`a4@) z)CnT%?S_k4DegHCHilm~^F_lg&w*-=5wnY--|%|j;2c`kM4F~{#!A9F)TLy9i5Om! zGf^3|Fd`_!fUwfTJ2E~!Q?Nf4IKX|HVM;0LSu(H^|202t;=Pkd%$wl(mvzH4!mEbw zygM6z8hzkanzrS;p+34V;Ahu&2H1nB;i!W~D1yw={CxUbmC`pccY_aa!KB#G3x?Ji zjkKo#t+c@lLa%4C|1#`FT!RHCmzUmffD-n|KTh5?_aJ_j@Nf4G@ZKA5hRyL~KE=D;$L6#A z+anClym(vFCUa6`mh2H+eCQ}j7N2II_7beG;%^FrtEsL|yur#E`@#U~)2`~Y^efsA z&Upac9Y>`9d312?bE^)0sxhayO07&;g z#&4bUh`Z(-7Y*$M_{0jbRs9@D@;s;4AI~j|qj`T1G9)vhRn0lBf&; zDThp@IKRj>^IItes}_6lK!YanIoN&LGLU&fXeWbwO$Lw+3`D`~?+tZ)+C3D*F4VD! z!YA~jLKQc(iUKMbQ${@@%PvI=Cvet*TcTe`3Tm9?Jw8D`#1kU0%T!+yTD58D#$S?< z08SIHoPJ5$Fu7)8-82N`9ssG(k|}5@(`$kkOa^DI=sjZ>mJDIzT@2*l#~G!|Y;P30 zEuj{><|Y7e0`>g8mDh}S)d-(egD^KCCcoEcx=L42Y*7{IQPA_2Gj63jC*yH7VYxse z^WgiuLu--n2w?CMkhX~&mpdQ?WAV5g_oGDJALfosHq;QF2`+9#-&$?d77|K|-T`aV z+KtI?WJ6w|m{mH^#phJS02_?+l7+Op8`d)%&%CXKh)>}rVP{1RNQ;v^0vU&c_mg}) z=~Xr1v*?=v8`h%Z(4W5)bGiKujAq3i}g-nmv90otzcnAI&?}v10NoRzG$vHYtyd4DyePWNt^4l%sO^^H!E(f~f8VWd6 zaJO8ZJ&I;+fTqUsn|B1gu%75Zzq_eGBQ(ZuR)Zt@d4&PdgiG-=F~!N8!zgM0#=p=> z+GPqp`i^As;$u*G^A&%^ML+kf0E*Dj;~-lx&ovlnsXlm+u4shDPz!rV$sP&RKi|8G z|6ruV{hm;FVq8i|l0F6a1wYu8{yckALq*+Y>?Xe)`jeFxXP#11gM(6xUBeSk{Uk!krUo5_7H>e;Dv&W$_2jrFH?#*z2jY zI#JyAOQ@r-f0EX@5RWJ8!L|#5xZB3zS2t_qd=bafdoDfGk8lF3pL8KAZ!a4!!pgf83>i5Pu zYMyimE!m+Pmb_Cldje-6xU_|0Y~>W12^QzJUQ%KCfn-h(j9E~e3Rza5+0iCjw=GkR zllb*}Z;86cW~@;2#H$^c?SJjen|Sl%_P;(afLk#HkXSF6^#|7u~~%Oy-b&-M3mB zF)Nw4XIen0`tv16 zUQginofO=-m#!+HAyx5_)7k><*g@oL(=yTyqlA8~)>yHvh1y^rUuUl|# zX@i}tPv7iUsqQXZG$9MxrNW8?H{CBD{?0gIv|}eNLWrI3|6z_KZp)J8kIAx3`nI`v zt!LS*vFdaj6)Dg7@H4xJox2zl%!i(imn*s>~@mV%AwKd#8KUFwB& zsSP3wcW}%>|F!f^RigSket-v+*WKx%61S80a{Wkv_#Epof`lZKNR<`w^~r~xkgQ$3|sxDc|{U&nVydhl3 z5zEN}oJ`pV{udB9#Pgu;WrF(!CAP~yte|3PJ3KnMU4zxuhn{w+$U_6zeNK0}-V(8T zgBs86T&@CVG+5dDki6y_0YK$NCZ?s>68}OCmdv1jjBwgApk%Vl5O&WmNnmUbPR9p= z8=TL5VlG1b?Z8?9uY5Fb#-(Ca&__o^EzC02_O!n$pmUEcluV)@_mE8G_r7g{ z_dMXFp3`5VcBcz&2MP)FotYrnziA%ADhbT`;&Ak?>a(iE$j4wQ3*>1=%u=6@W^d-C z%A0mJAG1qSL9I{~*5uT(0rwc&$7OB58ZO&-S@Fq*eJO+;gL|V0+B|VwE|{mlwy&vl zgIqxW`{S9=(Z_^TBe@wDxibSgU!NH4kui-Vtf02zv`cDBj-yuqg+sEjCj|C`%bCEz zd=kBf@b^zG#QC+Y^taq&f>5r6Jz;_Y0JF+M#7-rxfdn~+_XuFj7@zDz7Y!k6LSo$4 z$wm>j>f*QauR^_q@}2~WpSig8*rvl1v^_a%eD5pXhgbDkB`mompqC=tJ=rz?(E=S*zcha14B;fw`=0=Vl# zgMX@BccXu%)OHr^5;@K=bbFX5Nwh7X0Gt`DcnnM4LDq?(HMn}+Yi>c!UV>MgD~62( zz*Zgf$8KU|VoDT#%^svR|3%G4!?Vu%0#YboHfZpIV5L%~V?g6=gDp91Zq2Vt2(x1M z77X|ci>WCA|J04*{}gkXhJ5ILR$)pUeJ3mhMt&Xtgx`FX(a=dzs9rdk8u90I*_@`_ zth12y2|+N)Lf?KMI)~=XJBIe%q~Mol^c#HbRX7E4PlS>4x)3$T;RmP;F(BMKK*SE5 z{)0t5YoK5m;t(td&e9&^*&9*FyHA05x1VDD!sk8c5ktSwKpC`#vG$jPAetb*=iBy$ z>&Mp?mGMJs`6l^9tOa09&^^SVUc7i}h&4SyPuUxD)YFkzn1md*nE@dxAxDv_bBOk# zXqA9%{Ai@0-zGeif6w7I41QxK3U;xSpq=7%(x1Iq)vdNoU}xemV0yJ zp7HDQfyym#9qDVe6<{;O0bJ|9IPfYkoIxYRY=XToDSunStmuT3fFT64FNWDKgmGvD z+f6=CH$a|_tey)ajUTUAI=(O7+LKn>f5AQEF3Bh7e8pbYAwz~5egE7&ptm+z-r ztWoekP40Rl7K4-YzWjX{be8rm34X7}$`P2iORL~tixDmlq;Z(fG2o+6@qWrhOStVH zbFcjxChq=9_whhS;w4xF7=1W?>Tc(uzAY@zJVX0>TUFAI4CAZ({12O=K;08G;HA}m zTle>T!oaprs}9KTCixt#IrR`=L^qo~CFr$2!*6|hf=&oCk!lpxnBpJVeO(9`3TWUz zZDza?g3o_-DtI#na}{pxV%bgz{6@2-t|V?A&nt_S1jF1s{BopN-!rP?!q3KJq+J4X zTV>T0fuo^!)nIXJJRwXu#an<$St-rAHVvxLg<$z_;7-Ff&?=hkh+PKb3LYhn3(357 zDnQd1arx>TLs}B3|G?tC_R!SP-r zw?k?T@6*IVnPNzb5UjxT#9LtWdM#V~D+v|Cun;5jN}Nb=>u(MG@@Zs%8>2HGlbMu= z`%Pbj7}DG~>bwy~&0C>?Y z=Ebap803V9nrSLWlB0m#wf^lDz8jeR{RNkf3n(pvhmRn~{$~@9B*CW6Lj1A~xEO;^ z=ahG9j{u)sV1->1D{F1bm&T)d}DZNCGRjEBpw}K1i|b z#T=G>O^6Zw1^7m}Pk2$Y>SfknQS)zt2RC1|i)j${u&nn!|=9;ZYe-{Wb@? zRyg;gyZDsCD0rCvVZ-dYSgc(1$yY?0eT+#-*^ln+xfo+$?4hj+6b{e`mEB*rvx2qX z9?~=^hk9F~>6E?ocXN-Dq-h~r8RbqKX;HY|qIb9lTy|SyZ-7#NpBFz*TM_5lQf9M) z);F*BGk}$qK~up`>nKwFp)PWhrXcOSCYx=j@i-CFkcVdP^uHo)A%YWvm0DE2@HETU zHjUOU(KtnAaHMlwCX7(*v>3IOVPEjZz+L0v-eQCA(6r8gK#Kn9L7Wid&nszI!9PyL ziTfR#&;G2Z3Zix}9E2Ea>R=iYV2mF=G#icUe)U+t1`aNHMD&N(-zKfu5JKNrNWA;; zD(VPWTDdrNo)%%s&&My{$^xWo@;@X(z~dLj8Os#?z~^thrTkOw1PN9%E_P5O4h!NO zBy@|K!p=CRg$#G8$@PhaK*yFm_P-3?xkYFr>*QZc%4{)AGZ8l~^-N}&7=a{dk3!~)!n3yks4(~nhE0wleQu)VTDwl*>Uk^-2Gj4kQ*l>vLAU^j$%7@IaFaE8@0 z3+dWFd@ab3WmUHBX`ruH0!@0wF-_tc5a;j6>m8^&Or>Ib!PR}jU`GZs@`(21VCOIA z1ghU0)IsLDEE=pCSw!gou?-)uI-XmTlYlMum7H#9be#y@S9Yzkk7BU1QZ-%oZLqu2 zECe!NhNpcOm#t+zq#vxuop!(byd(5p^ORt-5ZJlP1>6k*rca9CEfu}`N%b_KCXTuN z_29!yXf20wQyU?cgyCEp%v3?v;9+k1&6qSv(3%$MwtE7O0!w`&QQ*PpCwIn>7ZS7# zqrh~jK--svvT)WJUVaF=}_FZ?L%^AOmN)&-7wBK+d>6 z)}kj_AS$2c9{zGy7*e%GJ_O?{zo2PRrvuWC>0Ol<1q1TH*1chmD!BE<9YRz`@BHBS zC<7RUL#|q%;MW1K$EC-?^h5=Afdb$jVoc9$sw3x@;iCh7avo={xt8I<^m+8XJ3Rpc z|D)s#sNWp|b2q9miZm(EN)T9H-0LLVVLF)G?2qf2mgP5 zk-yAxE#$J{9`irn&WLLP7>oYxSiDE=r<*xqd{b<*Fac1#h^}mZLF8?uaH737@S)5? z>|mi?h-%CRaDIZJFNLvadCv0#^=JqF&qvu4;^Jl*1aV~Jo<(d+q__;9qV=NkHIeB?H;{gu+oLz=pX zF;2vEjY=KRwZD8^Xl(r~SzZKg;hQ$cIk@4V5FJ&&zppbTVfzX9W#IGh;0|*zK6*!T zpVtA%`BBB#-4E*KKz^cZ@Q>y?V0rq7`|W^xl7JRr_8JNy#b168_X^}&7`uVG7m!-X zdqs0_z<-QbrW>Sh4pgq;$FeqW%R@7GuT2Eyv{V>ix=B6Fo&UDQ?G)10{SqOk<@&ww zX6~c2M}^&27F2e${pMltA2fUS84aKHJ6b;o;l3fQfxDO}0!`y{;y|`@ zMTJNy5u`k)Jyip@30b2^MBYS?0Q!P}Bzzmo)_12HaLg}2QauF+2MAk;99YN{Y*83D zZahhIpNPMe5iAJ*A^%!QcNS!$eawnb>8GD$z475a`<4D(qVqsAhyq`Jm7GSi2e+gP zoZZev?JNDqcq!I818$!c$n3&bY-&{xy#T=$>z@r@MpxX}15`o8%Q|ypRnc)yFg`zb zWW9EwA~ib=3R(hopPP_E}og1_mqyHwHqH`>JPK(jK3U+6qr%&EDiuevSEe=wQ=GH}5$N zo5U^;$A2(Hjg;Ki>2wE64xb{|(=K}k8qidag5Dlwhd&hyXk}1ytqnh8&9D)IgPgLM zZHrDnH3OjQm6zS3?Zh0@@93aZ@)S0>Wig43rR{-;;{qcu8eeNA*Pr0F3cT5#IZnE+T~Z>)gy+e_Q$xsj*}TIUz5Bd`7LREo`%zq zT9a88Gs%pwD{P1JIx3n|(r#^f$4|RK_8Ja7pofd^UT5hx9?4Lcgqv^T1$bM=^(We+mGxRi6*8Ipg z;PPw#RQki84bK<0I4w3#gH}D9pW|>1Y>?KhgQ5}|dTv?B9?TlQ^z{75CZFW=<_Yvs zGzfXrCXku~zp?>6_-L`L7Z<{vOv|UCkkYAr0b!rE;4MoA*gG^lK92~tQjF1&*Oq}) z5O0s2K8c4+EkT9>vbF9wwN4eh)z|SKM6=1!$Q^MvGy4c_-0VYPY8~lndlVQk$)e#u z?PQF3bx!BCZ4XWU21kp&^m1HC91tf@k#0SOtg-t9I-lXi-_<;~kJgJixU?RcU;8{7 z@)M2QFejGga0u$h0H0T1rng*P(&Y3{_=a5$ObI8(ZBCE`vD|cn`e&;Jht7I*#T7|V zr$|2v6jZ_1FXA7C81?46k^SBW&w|+^m}^XK;1l1dnS;HitpLUEC5yk7|D#1rm?Z) zg&P;AwTWL*f&ga;qusIEptBAyKKyDj)tEeHpILiMNAGN~6M%P(ZqiPZ2TEH&*-F!f z6~&;}Uz=BW9o6<(jv3^1t+b8E#)LeuErSpReL2(q{cq`vD+;`nG0LaBK*5{QAOcH7 zUKNFR$i479)BYRD_P7*|@&*MrBmhP*pNl6+GX^A1J$kv%>K_n~mjpa$ofX^|jMZ-x zhR+JM$3>Lp3}V1pVdP;Va@ykoNZwLOZg<<7ySZ~ zVrYV0HZ*9ithjz<&v}cP%0$YlV{98R;>_9Cy*(vQ+gCL;J14v1to%<+flFbW0%vbr zo_5p^37EI{dMt4zhH^la(|_;q+!WozZ17sauRU;7a943PDIaP@9w4n&uzcHB$~xZKw$x)E5L>JU$XZtC-K6W9ZQDGil8&(C<^w!V^)6 zNC_}mvjVLH9Ej=bB?$Izl%q`^GT~`|;*Ev9ne1t|>bP;Q`32zS)~`B*DaAd}^>p=r zROYm=E;Q+1XXAUOsrQpBX5Bdcgt3vE5&ZF}asB)Am#G@)dB6Onv9Ob)O@Q-!^zy19 zXa&8d*mDufmCoK zQy(&#k4XGEc*e3Ap5veCHM{#fs}c={uAEz<>Xt!6JVNRrI_sm?-_};^HMAzv6he zzJ7i;H0!YLc4>+P0rtQQE>!bWxL0|w* zjxBAUBj&B>tGyH@JR$r^n(7VekMfOhLK|84th-9kf1JC`pRBJ&vco>0PeDG!zJz`u z4g++no(Q2fpf`%q&7jW%54KY{k>Dut(#ugdbN|U5xZRe70mzQorRg=HWk=iP6OC2qnOWDytmOau8PU9a$_gVr!b=s}mk=^LHAN zhF;wBXZf99rLWu{1tLWK$^{Ew0%_h$OlF}r5pW*?0=>w5=W92XjG73Bx}Be3oxeg} zRkV&?DhK1y_5}Js8x}cRmtea@uSF8NA;9!K&?+9b;T|F2CvT+4zo+z06rq8?KEZbQ zddUG7i`dQ5F_|wO(+GzARU`@HENgRmDL>A3f%H>CqT=hTS}Lzn-y1p4DH8?G_2|n! zpyv`|xDlg^BDgt-#MQfDS^3@q)5L{wFvaoEgIBJUkdiqAA;GdN?`xxt4~$)CyLcOB zi4}vO>Sy34#@Y*Sz6#40mRhLg%XSVt`cNQ>e2GI3hb6?=QN5+4K zpC%y`n~>&je;bM?WJtOA#1L5lFI&=Khe{AEABsK~@kXuHA=Lh1?k3tU=o&mvuTjm9 zmWMOfLn>OF(#pFlN*D2DRB z$7c_YE;}Qfn)l!J)Sp}{oohJ8q%C9~j|7^m-6v$I1rfU{#h2C-EY=eCpqSfEG=0h| z5%I1`VOP1+(tk(ACyD!%`X*7_&=2{&-%RPrK#rp=_TH4T5_1u{p?FcOYIX| zbam;>yyqKFzaTY@vvKH7%3fMd5>K7Hf1!``V7EA{ z1wfp4Pd!A;Kstvm^z=AAQ1*5zEXWGy2d^#@?rfFeY!((vGw` zDdT0qa^$BC;Gifg9Q@PvUrwx3;fP1DOkGH%a>_$x80qX}tQ$WJ zqe865Jb3J)%JpLfw}t%onQ4aI-(#IaXaw4%-Wj zXg>WbwKSV@FpBojDzRtfkBig2*_t*vo=bXyIR~e^$P103Eb$Pt+CW70YAj z2_gq57u5l3KlPY-`|l|}%PI9MSgD17lw4kCb?wW*&EhW0PM;6Dra9|#Q?C66l>%!g0MA-f46xZaAU@`@OSeBho_TBL&2DXRGdheZ~P(Z)}XJq2Q8k=q8N$` zL;S>jYc@wOBwOe}X9xwDqor4g`L{f4FEpuYgH?i0pUe6+hH{yNRtR=G1QX0kgH)dn z-gA@VWM%~2QX#znU+mL*T@=@v&B{d8La-YDWGrFV{t}w*l#8 z-8?eqS=B}mIRCXGtM~Uh!7C6jhqjwxd3qg;jmUmql_zVIzej$q|KOQuKS>LH_iO>! z0=pZ|T^wbx>dF+n`hh?MX4H4-%n6Zd9&9?WSBt>!g`QqQ> z+xI;;rbR0~ZERT1-|?FBAjj(P10exmQ)oM>6!UAl{(@=qiKoHbC&7ivr-yQmUkmmq z%*fv%Z@LqtC7oz^dYMobXqf)7$XW+1xInOVZtBl#^8-~= z&Y|KAqijRzdGE0*3-K*(A{E+KDC1$wAXVdylLr{zT1oub<7J-e1dW{R*oeDV#2M96 z&Iu%*@Z@Tm1%nTu&fH&(7Hl&(jI-qP51t$R}hJ{Z~{i+tbob)(Tr zZUAZs`y{LrcqY&RJoxQPTcft01g4pIz>Hn=OMxH&BKtqJsb<0&ZX&FPl<>jE7jDQ` zpwnujjafn{#H)fL!|FiApOcyY0DC+;zXOrekddL+Z~89FHeTykiP?athQ^tIZ3HoJ z2ULxy4orq4KEHK>-fM_YX*k~^%3nJbL2GECl6s7~5y(Q5ZK?wOnaIe^2~P*qtV6(V z1&;i}eS%2vHI@k<53C8*k%dEYdE^TZif;Jdy&Wb`4-~M5ix!&n4z6IDcJ zvt)%^3k3MK4AmT7z0dE|qTaldwnj6~l3bq-X|iAr?+Gu)^;NSbN0cIUg}S)0*AMg2 zYHjzT)5WyI1XJkYZR)zqDw8UAz4cu9Xg6dU*%CZ~>20c>Y~yD?^oI6%+u?H0VQKwA zy70#FuKY0~`-2uy2}&cD%wE4^Nj_-p zRhJ9BP%vMZUr*6p(T!7A}v3+URVm6+e?B9Q7i3|P)NaorWDmpz;PX(cJ> zs_kx9aqq|7+_0P{a^$`{LjE+~%>$i7SV^j45KN^Oxx&G&d5Tqp3mdp8MIUUmPa#(x59Rm$?~Jh*N`sHcsBBY~3YF4KF(k=0&)Ao=sG$!j6loq>WMrvGo4pt_ zV+)DWC?5$$VGxOIX;8w5!OZXR{eJ)bet&<>eeQXm<(@P5dA;s)&pB~b@8zq=k*{~c zo+b+Tevv7!NP6JD%7%AOs(V&|IPxsbt&!1pqdFp^TlK813HicpPm>MQ1F2%`LqB1r zzNi_M+VX?0=`=z^S*pU!&kUPN*naNY3BNQddunqPbsf1*bSt5Ur49S@8~<@K;caS! zHf8q++8mVo(EDf>o7!x-Y=sqzJiJt?>}v5#mla&JBMMYaHoB~asR6bYlOuN|h_R?? z&O~~^GZtRqs-nh?^O)Svt-~4TMhQ)eH04F?>z{1MB*r~YAlrxgsR139W;MNnuJAJ} zco#7P;jt*eaxQ)MQRs6ewODwL61f4@{Sh;Pg$_0)K>T@%p{wYHhgV&3IPNn>*Agog zd>k^bhS)T5mawZ}@B?Vuf=ntXvUs-&^Q8F2z7?DyEG9!rF5v(<8raq`BRp9wtK}

_m_Cz!aI|OA~=>rPyDZB}LviY`DTRyq;E+O1bb*mtHP+eDp`ie;@gD)I~c+6GFbPa%hM z`8Vex*~}cS+digqY0sJMuZM`)j&b;BN&8Bf8ycw7yWTmLRzF2`&mV!i;_!0GY1hGp zb*$&h%G&BIe^cNQG&UZZL;uTN8%^xvNkkx~^#*AkS2X%ziIv8gqo$-Nk*@_^rPWH^ z*L)RAHm5TNw>h1~z)`GS!g!lHyu<>rZ>9iOrAIRH!X2`(0Nu~%Lxif$TC5$#DE+cE z{ijLX5#>7=*o}4n?U~M}J*BAU9vkM+h)#@@4!X98>sImyC=SSCNgT*sNI%C2T>i<-!9=`VB~MoE;PLJfXms7b`3UkFsopktZsUu2`1dq zLkKAkxB;K`WB#D)vXr>P;vI^hlReihTzq^o^ujke-_P4>d&|7Z>G0neSdVpD=_A{p zzaXC1y}rJtmP2<8MZ2q_YZJL9G7Oh;K{yL5V|e}*m1NTIb3GA>WrghgOgWuW{3aYU zC!vPfD%{X@ANAJ&0p;vM@vCuDDUKM~vORWNZI%l6eB+aw;A5p(Le52ja>c7Dso?Z& zwJa(*Ju3oD?8P4uRoM4M$N_2sO2~Y$I{|HGih=XE!=%b(>#B&zHELo519p)LB}gf- zIcriktD7O1*bNvLRB?xUzAHNJL=zjS55!G$oTK{=ZsKKXWsUA>L407$9?hfeuNv~+ zV(7Nu1QQsdH@enfB8Y2~QO~5;=if?cz*gq9X|3Oj_Vr;ouRHdF_LpwG7$hWA?kw3I z7lNtHprmKTT;3k$nlzOWd^!OqefbPJs~VbLtR(+^r?&D;fs8LVlbz?b9l`FSq~E(Q z91@`=0oM3ougBzcJV0l?;+o3fAH7d^yD$I5@`-MzfvacD@$=fV=KQoICRXSms6$j*@>%B4$Zu&2iJZcpZYc6IalE1 zvefh96Nz{OLsVyVDL-r{ysURGx|WF#U5f9I>~y(I5`<}kCXXnY+n?H0FP$I_-U7NC zxGwSeTidqo))zxLP)@I5(L~*=60Ol$Z|zvxKIIeB@$eRugHua)KcSQG)z^+&6VTUW zGtS?*TVEaJklp@53!^@M0ri?zw*fJk58rQwXay8SlYr?8f8V)T5>yKz;CSB*aYb_tKPX(}k z<-Nmh>UaB*isssB>l(Sc?2X_1yb(&R{dv+c%5t+gBCN;0xu5V?nJWM1H61Xu#Q*ew zJ3g<6)$zcaK4}DZ6IW4tG;oOLZ6<<;6p{b;!^tC7(Ks^) z7)I|ml)Sf?8KO4675nLqP{t$9E@ObSbK$D%tRu=_g_8-a-qXAKb8gT2ENXawopM}4 z0`lHRiIa78$mX9-^xSbw7iByhx3cEk`BBmpZkY%zy)f+zaG@Bq(IQtnzo z%PE_dB+x4QTfAxUhdM?2aBnQt7!^jLP z6p1kMLr{zdHvBSSTdkwCAXC?&5(J9{m-Ddn%kR(4`PhTobU%IrLb8Xe#eG)?%W0Dz zCiC}6s*q#m0+iHJhxXXVNrcM6jX(nHy~;=~xk4PSZ&~V2j?k zG|`DtuOZxpw-AY`^ORuoHM0{}8K&Q|>4z}_GxXGN26MhH(*yL)Wh#Wq)~aU7Y+-t> z2Gi$X&&c{>T-F`5Id&^R_U(!2wJTKOCLLzNOV-BSUQ;j8Q_q&Bo)TCfrbifrN`A(C zsH8<9&qKAN7yoI|fj4+LZmmiVQ< zr)G;VNGNJ!3WxTKPt)_?T-;#uwgw5u2GX}-upj0;v5T$T^D>^-KKl#8xUn$h*i zDKNN+<#-{d5?`yhYH`5sJC$>we$z~cVgB&3Jlr7Xs@bI=O}lU<@hcjBqsqiK(ddWR zYH?T;6}Jl8x@9lZ+iv&Fx08o7jo19{-!6WPLCH=sPP5mqNwP(Pe7Qa@-c*=m-8&6YljhO=0g=sdnhY>(3u~b(HH7@hHN! zX_EN{NMW6@`eU4I(!C1BI za8t+(oEN(5)x_I2Q%qwX2%Ga>6go|O}1S`eIgR_1yGQ?Hs-gyHadT(a8-+F!f z*)M+!Jx-xzC>i(}?yZ@6l485#m1y7R-Cf2u5bj1IZk^rTLEjINCq>OKTR9g$^`6)* zr9)BhS$FoZ(+d&QTZ~+`h&Q(?vO6>Il=h8HlDRsrr0>_6OD&&gzv9_NO);lzCZ8Y; zlZw$=iRH{7R#O9Q@WEj$xOA^PfS3a>_!E8cF;wGL;mDCQ%|Kc%DHEo5d}1cD zd9eexRBf?fEF`B65$6Z>3Q1koOhDvF+{lM&T=_X1q^7>_Ff1P>l?AE0dR;LShNmC~ z_@Lr)p+XNXZDGu8g})2-Jq7hry0Tg?gDg&N^$nqJ7WBcLE6LH~-@}7>Bc25)q;?>m zMU(z~brJ_7V&6_d4=G+9NFt`doaw#pgaxaojM?Vx*@f62rL3DlsW{2CULK+K7og#3 z1tLqeluZc3rCJ1e?U}8P`xKTNeNolv3Z6F}{ zWeYeL>MG~?E&R4;0^cr$Wc|YG3@A#FrgaMsbmdV3bC}}Q$P@fl-zo{zxaBwS_AGkq zh5l*L+f{%=A@|J)p&zkGt#s9UIpjVFDi)!dk;Gv~FMr2WL}E7gO}COZB2n_I*t8Vj zl~Mg2vDV1*ulDL2MLtTP;{;dY(}*G>GCZIrt_Zmyhg|i$2r3A~uuAfsFH-hIvE{d} zc&&Z<1O~v)g+GgFvnx*d-7o$FX$$q;LtkiWyAcAxOL(F+0K0mr3qK5xu1vhe6A`Oh zD&31jfrychVu37ZscaUNdFcD86P-1XR;NfIWx=OV`q2?e8sy4sa ziLnwCyu#GvqAVK?w-V@l#EA~_=;_r!jb%*J<7SdkL`W(*(1!n*aYYNEX`-zxnAW;g zhsNcRs*9+1v@LRq1^c$V_{VPNgOIc8l@vbTdXU{|a9}xQ z1j!X9x2p_NmI=RgC}3bMC1@tid=-wnJef4(FMPWecsB5oaJ{RH9t&D)2u;^xYC4c! zOu*McDTa5XGpeG+iAFZEzz~t|lmcC1?pc^bM7XP#}O^uD@>2uHf zvY@iHgUC7+G!Du~M)<3e(0 zz6vYN92GBHwcKV=9C*E+{BCQE!>Re>8P6m`yiMT;GrqX;4=+9h6yc zcumctv&^SaUv@5ZWTN5r5yLX|cceP_gdt@WSE43Q*656Q>d?GpFTo^s~$(q0a!#*Y0^2DTl?R*d#Ly|?u@6<(g3mi!=$zFfeZ zv$uR~_T9qh?LQfRk0swkGBA@x#u}lsAu@vCyW-uelR1ZORH@y28R591A;ewXIxt!- z_FpjlQ$LCN$&0}W;@x1HmiZlhx=-}H6*1C2chKjlM95CX;y){Eyu&5Z>s*@AdtFn} zMCi$NlTn?0W0GAd;urGp;xO|Wuc2pVNKR;WDXOE<9|bSvf7CX(sp4EETTrb1oEpmc zOBM`^2Jlm_*`+>i5_+U#G2wpt&gMBQ%x5<8GlS+u`vrGAU*YlzaodXC-kWq0>q@_f zn5zMiqn8{>*#AD@W0DC>26`cvj{oli-hCX6>?l5MjfMU*;QyH$gE0WW`&~tyL1z_C z#zZrwk#?@a+?*z)mFq$h9WQcp93kMDOGtxP5rgsMKfnJI^lzee!T$^Tfk^zHAfD*o eYX2uFQ^E?}>e@W{JrCL6z=m|hvgm+s%>M!WQ(8m- literal 0 HcmV?d00001 diff --git a/mobile/assets/splash-icon.png b/mobile/assets/splash-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..03d6f6b6c6727954aec1d8206222769afd178d8d GIT binary patch literal 17547 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 App); +// It also ensures that whether you load the app in Expo Go or in a native build, +// the environment is set up appropriately +registerRootComponent(App); diff --git a/mobile/package-lock.json b/mobile/package-lock.json new file mode 100644 index 0000000..d40723b --- /dev/null +++ b/mobile/package-lock.json @@ -0,0 +1,8705 @@ +{ + "name": "mobile", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mobile", + "version": "1.0.0", + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@react-navigation/bottom-tabs": "^7.14.0", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.13.0", + "@tanstack/react-query": "^5.90.21", + "axios": "^1.13.5", + "expo": "~54.0.33", + "expo-secure-store": "^15.0.8", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-hook-form": "^7.71.2", + "react-native": "0.81.5", + "react-native-safe-area-context": "^5.7.0", + "react-native-screens": "4.16.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "typescript": "~5.9.2" + } + }, + "node_modules/@0no-co/graphql.web": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@0no-co/graphql.web/-/graphql.web-1.2.0.tgz", + "integrity": "sha512-/1iHy9TTr63gE1YcR5idjx8UREz1s0kFhydf3bBLCXyqjhkIc6igAzTOx3zPifCwFR87tsh/4Pa9cNts6d2otw==", + "peerDependencies": { + "graphql": "^14.0.0 || ^15.0.0 || ^16.0.0" + }, + "peerDependenciesMeta": { + "graphql": { + "optional": true + } + } + }, + "node_modules/@babel/code-frame": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.4.tgz", + "integrity": "sha512-vG6SvB6oYEhvgisZNFRmRCUkLz11c7rp+tbNTynGqc6mS1d5ATd/sGyV6W0KZZnXRKMTzZDRgQT3Ou9jhpAfUg==", + "dependencies": { + "@babel/highlight": "^7.10.4" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "dependencies": { + "@babel/types": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", + "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.28.6", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", + "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "regexpu-core": "^6.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", + "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "debug": "^4.4.3", + "lodash.debounce": "^4.0.8", + "resolve": "^1.22.11" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", + "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", + "dependencies": { + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "dependencies": { + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", + "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.28.5", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", + "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.25.9.tgz", + "integrity": "sha512-llL88JShoCsth8fF8R4SJnIn+WLvR6ccFxu1H3FlMhDontdcmZWf2HgIZ7AIqV3Xcck1idlohrN4EUBQz6klbw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-proposal-decorators": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.29.0.tgz", + "integrity": "sha512-CVBVv3VY/XRMxRYq5dwr2DS7/MvqPm23cOCjbwNnVrfOqcWlnefua1uUs0sjdKOGjvPUG633o07uWzJq4oI6dA==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-decorators": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-export-default-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-default-from/-/plugin-proposal-export-default-from-7.27.1.tgz", + "integrity": "sha512-hjlsMBl1aJc5lp8MoCDEZCiYzlgdRAShOjAfRw6X+GlpLpUPU7c3XNLsKFZbQk/1cRzBlJ7CXg3xJAJMrFa1Uw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-decorators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.28.6.tgz", + "integrity": "sha512-71EYI0ONURHJBL4rSFXnITXqXrrY8q4P0q006DPfN+Rk+ASM+++IBXem/ruokgBZR8YNEWZ8R6B+rCb8VcUTqA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-default-from": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-default-from/-/plugin-syntax-export-default-from-7.28.6.tgz", + "integrity": "sha512-Svlx1fjJFnNz0LZeUaybRukSxZI3KkpApUmIRzEdXC5k8ErTOz0OD0kNrICi5Vc3GlpP5ZCeRyRO+mfWTSz+iQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-flow": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-flow/-/plugin-syntax-flow-7.28.6.tgz", + "integrity": "sha512-D+OrJumc9McXNEBI/JmFnc/0uCM2/Y3PEBG3gfV3QIYkKv5pvnpzFrl1kYCrcHJP8nOeFB/SHi1IHz29pNGuew==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.29.0.tgz", + "integrity": "sha512-va0VdWro4zlBr2JsXC+ofCPB2iG12wPtVGTWFx2WLDOM3nYQZZIGP82qku2eW/JR83sD+k2k+CsNtyEbUqhU6w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.29.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.28.6.tgz", + "integrity": "sha512-ilTRcmbuXjsMmcZ3HASTe4caH5Tpo93PkTxF9oG2VZsSWsahydmcEHhix9Ik122RcTnZnUzPbmux4wh1swfv7g==", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-remap-async-to-generator": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", + "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", + "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", + "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", + "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-globals": "^7.28.0", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-replace-supers": "^7.28.6", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", + "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/template": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", + "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-flow-strip-types": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-flow-strip-types/-/plugin-transform-flow-strip-types-7.27.1.tgz", + "integrity": "sha512-G5eDKsu50udECw7DL2AcsysXiQyB7Nfg521t2OAJ4tbfTJ27doHLeF/vlI1NZGlLdbb/v+ibvtL1YBQqYOwJGg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-syntax-flow": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", + "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", + "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", + "dependencies": { + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", + "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.28.5", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", + "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", + "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", + "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", + "dependencies": { + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-transform-destructuring": "^7.28.5", + "@babel/plugin-transform-parameters": "^7.27.7", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", + "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", + "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.27.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", + "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", + "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", + "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", + "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.28.6.tgz", + "integrity": "sha512-61bxqhiRfAACulXSLd/GxqmAedUSrRZIu/cbaT18T1CetkTmtDN15it7i80ru4DVqRK1WMxQhXs+Lf9kajm5Ow==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/plugin-syntax-jsx": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", + "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", + "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", + "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.29.0.tgz", + "integrity": "sha512-jlaRT5dJtMaMCV6fAuLbsQMSwz/QkvaHOHOSXRitGGwSpR1blCY4KUKoyP2tYO8vJcqYe8cEj96cqSztv3uF9w==", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "babel-plugin-polyfill-corejs2": "^0.4.14", + "babel-plugin-polyfill-corejs3": "^0.13.0", + "babel-plugin-polyfill-regenerator": "^0.6.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", + "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", + "integrity": "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw==", + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.27.3", + "@babel/helper-create-class-features-plugin": "^7.28.6", + "@babel/helper-plugin-utils": "^7.28.6", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.28.5.tgz", + "integrity": "sha512-Z3J8vhRq7CeLjdC58jLv4lnZ5RKFUJWqH5emvxmv9Hv3BD1T9R/Im713R4MTKwvFaV74ejZ3sM01LyEKk4ugNQ==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-transform-react-display-name": "^7.28.0", + "@babel/plugin-transform-react-jsx": "^7.27.1", + "@babel/plugin-transform-react-jsx-development": "^7.27.1", + "@babel/plugin-transform-react-pure-annotations": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.28.5.tgz", + "integrity": "sha512-+bQy5WOI2V6LJZpPVxY+yp66XdZ2yifu0Mc1aP5CQKgjn4QM5IN2i5fAZ4xKop47pr8rpVhiAeu+nDQa12C8+g==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map": { + "name": "@babel/traverse", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse--for-generate-function-map/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@expo/code-signing-certificates": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/@expo/code-signing-certificates/-/code-signing-certificates-0.0.6.tgz", + "integrity": "sha512-iNe0puxwBNEcuua9gmTGzq+SuMDa0iATai1FlFTMHJ/vUmKvN/V//drXoLJkVb5i5H3iE/n/qIJxyoBnXouD0w==", + "dependencies": { + "node-forge": "^1.3.3" + } + }, + "node_modules/@expo/config": { + "version": "12.0.13", + "resolved": "https://registry.npmjs.org/@expo/config/-/config-12.0.13.tgz", + "integrity": "sha512-Cu52arBa4vSaupIWsF0h7F/Cg//N374nYb7HAxV0I4KceKA7x2UXpYaHOL7EEYYvp7tZdThBjvGpVmr8ScIvaQ==", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "@expo/config-plugins": "~54.0.4", + "@expo/config-types": "^54.0.10", + "@expo/json-file": "^10.0.8", + "deepmerge": "^4.3.1", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0", + "resolve-workspace-root": "^2.0.0", + "semver": "^7.6.0", + "slugify": "^1.3.4", + "sucrase": "~3.35.1" + } + }, + "node_modules/@expo/config-plugins": { + "version": "54.0.4", + "resolved": "https://registry.npmjs.org/@expo/config-plugins/-/config-plugins-54.0.4.tgz", + "integrity": "sha512-g2yXGICdoOw5i3LkQSDxl2Q5AlQCrG7oniu0pCPPO+UxGb7He4AFqSvPSy8HpRUj55io17hT62FTjYRD+d6j3Q==", + "dependencies": { + "@expo/config-types": "^54.0.10", + "@expo/json-file": "~10.0.8", + "@expo/plist": "^0.4.8", + "@expo/sdk-runtime-versions": "^1.0.0", + "chalk": "^4.1.2", + "debug": "^4.3.5", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.5.4", + "slash": "^3.0.0", + "slugify": "^1.6.6", + "xcode": "^3.0.1", + "xml2js": "0.6.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/config-plugins/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/config-plugins/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/config-plugins/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/config-plugins/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/config-plugins/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/config-types": { + "version": "54.0.10", + "resolved": "https://registry.npmjs.org/@expo/config-types/-/config-types-54.0.10.tgz", + "integrity": "sha512-/J16SC2an1LdtCZ67xhSkGXpALYUVUNyZws7v+PVsFZxClYehDSoKLqyRaGkpHlYrCc08bS0RF5E0JV6g50psA==" + }, + "node_modules/@expo/devcert": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@expo/devcert/-/devcert-1.2.1.tgz", + "integrity": "sha512-qC4eaxmKMTmJC2ahwyui6ud8f3W60Ss7pMkpBq40Hu3zyiAaugPXnZ24145U7K36qO9UHdZUVxsCvIpz2RYYCA==", + "dependencies": { + "@expo/sudo-prompt": "^9.3.1", + "debug": "^3.1.0" + } + }, + "node_modules/@expo/devcert/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/@expo/devtools": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@expo/devtools/-/devtools-0.1.8.tgz", + "integrity": "sha512-SVLxbuanDjJPgc0sy3EfXUMLb/tXzp6XIHkhtPVmTWJAp+FOr6+5SeiCfJrCzZFet0Ifyke2vX3sFcKwEvCXwQ==", + "dependencies": { + "chalk": "^4.1.2" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/@expo/devtools/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/devtools/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/devtools/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/devtools/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/devtools/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/devtools/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/env": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@expo/env/-/env-2.0.8.tgz", + "integrity": "sha512-5VQD6GT8HIMRaSaB5JFtOXuvfDVU80YtZIuUT/GDhUF782usIXY13Tn3IdDz1Tm/lqA9qnRZQ1BF4t7LlvdJPA==", + "dependencies": { + "chalk": "^4.0.0", + "debug": "^4.3.4", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0" + } + }, + "node_modules/@expo/env/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/env/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/env/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/env/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/env/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/env/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/fingerprint": { + "version": "0.15.4", + "resolved": "https://registry.npmjs.org/@expo/fingerprint/-/fingerprint-0.15.4.tgz", + "integrity": "sha512-eYlxcrGdR2/j2M6pEDXo9zU9KXXF1vhP+V+Tl+lyY+bU8lnzrN6c637mz6Ye3em2ANy8hhUR03Raf8VsT9Ogng==", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "arg": "^5.0.2", + "chalk": "^4.1.2", + "debug": "^4.3.4", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "ignore": "^5.3.1", + "minimatch": "^9.0.0", + "p-limit": "^3.1.0", + "resolve-from": "^5.0.0", + "semver": "^7.6.0" + }, + "bin": { + "fingerprint": "bin/cli.js" + } + }, + "node_modules/@expo/fingerprint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/fingerprint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/fingerprint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/fingerprint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/fingerprint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/fingerprint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/image-utils": { + "version": "0.8.8", + "resolved": "https://registry.npmjs.org/@expo/image-utils/-/image-utils-0.8.8.tgz", + "integrity": "sha512-HHHaG4J4nKjTtVa1GG9PCh763xlETScfEyNxxOvfTRr8IKPJckjTyqSLEtdJoFNJ1vqiABEjW7tqGhqGibZLeA==", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "getenv": "^2.0.0", + "jimp-compact": "0.16.1", + "parse-png": "^2.1.0", + "resolve-from": "^5.0.0", + "resolve-global": "^1.0.0", + "semver": "^7.6.0", + "temp-dir": "~2.0.0", + "unique-string": "~2.0.0" + } + }, + "node_modules/@expo/image-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/image-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/image-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/image-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/image-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/image-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/json-file": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@expo/json-file/-/json-file-10.0.8.tgz", + "integrity": "sha512-9LOTh1PgKizD1VXfGQ88LtDH0lRwq9lsTb4aichWTWSWqy3Ugfkhfm3BhzBIkJJfQQ5iJu3m/BoRlEIjoCGcnQ==", + "dependencies": { + "@babel/code-frame": "~7.10.4", + "json5": "^2.2.3" + } + }, + "node_modules/@expo/metro": { + "version": "54.2.0", + "resolved": "https://registry.npmjs.org/@expo/metro/-/metro-54.2.0.tgz", + "integrity": "sha512-h68TNZPGsk6swMmLm9nRSnE2UXm48rWwgcbtAHVMikXvbxdS41NDHHeqg1rcQ9AbznDRp6SQVC2MVpDnsRKU1w==", + "dependencies": { + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3" + } + }, + "node_modules/@expo/osascript": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/@expo/osascript/-/osascript-2.3.8.tgz", + "integrity": "sha512-/TuOZvSG7Nn0I8c+FcEaoHeBO07yu6vwDgk7rZVvAXoeAK5rkA09jRyjYsZo+0tMEFaToBeywA6pj50Mb3ny9w==", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "exec-async": "^2.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/package-manager": { + "version": "1.9.10", + "resolved": "https://registry.npmjs.org/@expo/package-manager/-/package-manager-1.9.10.tgz", + "integrity": "sha512-axJm+NOj3jVxep49va/+L3KkF3YW/dkV+RwzqUJedZrv4LeTqOG4rhrCaCPXHTvLqCTDKu6j0Xyd28N7mnxsGA==", + "dependencies": { + "@expo/json-file": "^10.0.8", + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.0.0", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "resolve-workspace-root": "^2.0.0" + } + }, + "node_modules/@expo/package-manager/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/package-manager/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/package-manager/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/package-manager/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/package-manager/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/package-manager/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/plist": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/@expo/plist/-/plist-0.4.8.tgz", + "integrity": "sha512-pfNtErGGzzRwHP+5+RqswzPDKkZrx+Cli0mzjQaus1ZWFsog5ibL+nVT3NcporW51o8ggnt7x813vtRbPiyOrQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.2.3", + "xmlbuilder": "^15.1.1" + } + }, + "node_modules/@expo/schema-utils": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/@expo/schema-utils/-/schema-utils-0.1.8.tgz", + "integrity": "sha512-9I6ZqvnAvKKDiO+ZF8BpQQFYWXOJvTAL5L/227RUbWG1OVZDInFifzCBiqAZ3b67NRfeAgpgvbA7rejsqhY62A==" + }, + "node_modules/@expo/sdk-runtime-versions": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@expo/sdk-runtime-versions/-/sdk-runtime-versions-1.0.0.tgz", + "integrity": "sha512-Doz2bfiPndXYFPMRwPyGa1k5QaKDVpY806UJj570epIiMzWaYyCtobasyfC++qfIXVb5Ocy7r3tP9d62hAQ7IQ==" + }, + "node_modules/@expo/spawn-async": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@expo/spawn-async/-/spawn-async-1.7.2.tgz", + "integrity": "sha512-QdWi16+CHB9JYP7gma19OVVg0BFkvU8zNj9GjWorYI8Iv8FUxjOCcYRuAmX4s/h91e4e7BPsskc8cSrZYho9Ew==", + "dependencies": { + "cross-spawn": "^7.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@expo/sudo-prompt": { + "version": "9.3.2", + "resolved": "https://registry.npmjs.org/@expo/sudo-prompt/-/sudo-prompt-9.3.2.tgz", + "integrity": "sha512-HHQigo3rQWKMDzYDLkubN5WQOYXJJE2eNqIQC2axC2iO3mHdwnIR7FgZVvHWtBwAdzBgAP0ECp8KqS8TiMKvgw==" + }, + "node_modules/@expo/ws-tunnel": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@expo/ws-tunnel/-/ws-tunnel-1.0.6.tgz", + "integrity": "sha512-nDRbLmSrJar7abvUjp3smDwH8HcbZcoOEa5jVPUv9/9CajgmWw20JNRwTuBRzWIWIkEJDkz20GoNA+tSwUqk0Q==" + }, + "node_modules/@expo/xcpretty": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@expo/xcpretty/-/xcpretty-4.4.1.tgz", + "integrity": "sha512-KZNxZvnGCtiM2aYYZ6Wz0Ix5r47dAvpNLApFtZWnSoERzAdOMzVBOPysBoM0JlF6FKWZ8GPqgn6qt3dV/8Zlpg==", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "chalk": "^4.1.0", + "js-yaml": "^4.1.0" + }, + "bin": { + "excpretty": "build/cli.js" + } + }, + "node_modules/@expo/xcpretty/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@expo/xcpretty/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@expo/xcpretty/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/@expo/xcpretty/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@expo/xcpretty/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@expo/xcpretty/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@expo/xcpretty/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@expo/xcpretty/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@expo/xcpretty/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@hookform/resolvers": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz", + "integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==", + "dependencies": { + "@standard-schema/utils": "^0.3.0" + }, + "peerDependencies": { + "react-hook-form": "^7.55.0" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@isaacs/ttlcache": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/@isaacs/ttlcache/-/ttlcache-1.4.1.tgz", + "integrity": "sha512-RQgQ4uQ+pLbqXfOmieB91ejmLwvSgv9nLx6sT6sD83s7umBypgg+OIBOBbEUiJXrfpnp9j0mRhYYdzp9uqq3lA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/create-cache-key-function": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/create-cache-key-function/-/create-cache-key-function-29.7.0.tgz", + "integrity": "sha512-4QqS3LY5PBmTRHj9sAg1HLoPzqAI0uOX6wI/TRqHIcOxlFidy6YEmCQJk6FSZjNLGCeubDMfmkWL+qaLKhSGQA==", + "dependencies": { + "@jest/types": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "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==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@react-native/assets-registry": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", + "integrity": "sha512-705B6x/5Kxm1RKRvSv0ADYWm5JOnoiQ1ufW7h8uu2E6G9Of/eE6hP/Ivw3U5jI16ERqZxiKQwk34VJbB0niX9w==", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-plugin-codegen": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-plugin-codegen/-/babel-plugin-codegen-0.81.5.tgz", + "integrity": "sha512-oF71cIH6je3fSLi6VPjjC3Sgyyn57JLHXs+mHWc9MoCiJJcM4nqsS5J38zv1XQ8d3zOW2JtHro+LF0tagj2bfQ==", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@react-native/codegen": "0.81.5" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/babel-preset": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/babel-preset/-/babel-preset-0.81.5.tgz", + "integrity": "sha512-UoI/x/5tCmi+pZ3c1+Ypr1DaRMDLI3y+Q70pVLLVgrnC3DHsHRIbHcCHIeG/IJvoeFqFM2sTdhSOLJrf8lOPrA==", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-transform-arrow-functions": "^7.24.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.4", + "@babel/plugin-transform-async-to-generator": "^7.24.7", + "@babel/plugin-transform-block-scoping": "^7.25.0", + "@babel/plugin-transform-class-properties": "^7.25.4", + "@babel/plugin-transform-classes": "^7.25.4", + "@babel/plugin-transform-computed-properties": "^7.24.7", + "@babel/plugin-transform-destructuring": "^7.24.8", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-for-of": "^7.24.7", + "@babel/plugin-transform-function-name": "^7.25.1", + "@babel/plugin-transform-literals": "^7.25.2", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", + "@babel/plugin-transform-numeric-separator": "^7.24.7", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-optional-catch-binding": "^7.24.7", + "@babel/plugin-transform-optional-chaining": "^7.24.8", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-react-display-name": "^7.24.7", + "@babel/plugin-transform-react-jsx": "^7.25.2", + "@babel/plugin-transform-react-jsx-self": "^7.24.7", + "@babel/plugin-transform-react-jsx-source": "^7.24.7", + "@babel/plugin-transform-regenerator": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/plugin-transform-shorthand-properties": "^7.24.7", + "@babel/plugin-transform-spread": "^7.24.7", + "@babel/plugin-transform-sticky-regex": "^7.24.7", + "@babel/plugin-transform-typescript": "^7.25.2", + "@babel/plugin-transform-unicode-regex": "^7.24.7", + "@babel/template": "^7.25.0", + "@react-native/babel-plugin-codegen": "0.81.5", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/codegen/-/codegen-0.81.5.tgz", + "integrity": "sha512-a2TDA03Up8lpSa9sh5VRGCQDXgCTOyDOFH+aqyinxp1HChG8uk89/G+nkJ9FPd0rqgi25eCTR16TWdS3b+fA6g==", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/parser": "^7.25.3", + "glob": "^7.1.1", + "hermes-parser": "0.29.1", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "yargs": "^17.6.2" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@babel/core": "*" + } + }, + "node_modules/@react-native/codegen/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/@react-native/codegen/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@react-native/codegen/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@react-native/codegen/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==" + }, + "node_modules/@react-native/codegen/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/@react-native/codegen/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@react-native/community-cli-plugin": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/community-cli-plugin/-/community-cli-plugin-0.81.5.tgz", + "integrity": "sha512-yWRlmEOtcyvSZ4+OvqPabt+NS36vg0K/WADTQLhrYrm9qdZSuXmq8PmdJWz/68wAqKQ+4KTILiq2kjRQwnyhQw==", + "dependencies": { + "@react-native/dev-middleware": "0.81.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "metro": "^0.83.1", + "metro-config": "^0.83.1", + "metro-core": "^0.83.1", + "semver": "^7.1.3" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@react-native-community/cli": "*", + "@react-native/metro-config": "*" + }, + "peerDependenciesMeta": { + "@react-native-community/cli": { + "optional": true + }, + "@react-native/metro-config": { + "optional": true + } + } + }, + "node_modules/@react-native/debugger-frontend": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/debugger-frontend/-/debugger-frontend-0.81.5.tgz", + "integrity": "sha512-bnd9FSdWKx2ncklOetCgrlwqSGhMHP2zOxObJbOWXoj7GHEmih4MKarBo5/a8gX8EfA1EwRATdfNBQ81DY+h+w==", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/dev-middleware/-/dev-middleware-0.81.5.tgz", + "integrity": "sha512-WfPfZzboYgo/TUtysuD5xyANzzfka8Ebni6RIb2wDxhb56ERi7qDrE4xGhtPsjCL4pQBXSVxyIlCy0d8I6EgGA==", + "dependencies": { + "@isaacs/ttlcache": "^1.4.1", + "@react-native/debugger-frontend": "0.81.5", + "chrome-launcher": "^0.15.2", + "chromium-edge-launcher": "^0.2.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "invariant": "^2.2.4", + "nullthrows": "^1.1.1", + "open": "^7.0.3", + "serve-static": "^1.16.2", + "ws": "^6.2.3" + }, + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/dev-middleware/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/@react-native/gradle-plugin": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/gradle-plugin/-/gradle-plugin-0.81.5.tgz", + "integrity": "sha512-hORRlNBj+ReNMLo9jme3yQ6JQf4GZpVEBLxmTXGGlIL78MAezDZr5/uq9dwElSbcGmLEgeiax6e174Fie6qPLg==", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/js-polyfills": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/js-polyfills/-/js-polyfills-0.81.5.tgz", + "integrity": "sha512-fB7M1CMOCIUudTRuj7kzxIBTVw2KXnsgbQ6+4cbqSxo8NmRRhA0Ul4ZUzZj3rFd3VznTL4Brmocv1oiN0bWZ8w==", + "engines": { + "node": ">= 20.19.4" + } + }, + "node_modules/@react-native/normalize-colors": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/normalize-colors/-/normalize-colors-0.81.5.tgz", + "integrity": "sha512-0HuJ8YtqlTVRXGZuGeBejLE04wSQsibpTI+RGOyVqxZvgtlLLC/Ssw0UmbHhT4lYMp2fhdtvKZSs5emWB1zR/g==" + }, + "node_modules/@react-navigation/bottom-tabs": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@react-navigation/bottom-tabs/-/bottom-tabs-7.14.0.tgz", + "integrity": "sha512-oG2VdoInuIyK0o9o90Yo47hTCS+sPyVE7k8eSB37vt3pq3uQxjh8V3xJpsQfOfNlRUXOPB/ejH93nSBlP7ZHmQ==", + "dependencies": { + "@react-navigation/elements": "^2.9.5", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.28", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/core": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@react-navigation/core/-/core-7.14.0.tgz", + "integrity": "sha512-tMpzskBzVp0E7CRNdNtJIdXjk54Kwe/TF9ViXAef+YFM1kSfGv4e/B2ozfXE+YyYgmh4WavTv8fkdJz1CNyu+g==", + "dependencies": { + "@react-navigation/routers": "^7.5.3", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "query-string": "^7.1.3", + "react-is": "^19.1.0", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "react": ">= 18.2.0" + } + }, + "node_modules/@react-navigation/core/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/core/node_modules/react-is": { + "version": "19.2.4", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.4.tgz", + "integrity": "sha512-W+EWGn2v0ApPKgKKCy/7s7WHXkboGcsrXE+2joLyVxkbyVQfO3MUEaUQDHoSmb8TFFrSKYa9mw64WZHNHSDzYA==" + }, + "node_modules/@react-navigation/elements": { + "version": "2.9.5", + "resolved": "https://registry.npmjs.org/@react-navigation/elements/-/elements-2.9.5.tgz", + "integrity": "sha512-iHZU8rRN1014Upz73AqNVXDvSMZDh5/ktQ1CMe21rdgnOY79RWtHHBp9qOS3VtqlUVYGkuX5GEw5mDt4tKdl0g==", + "dependencies": { + "color": "^4.2.3", + "use-latest-callback": "^0.2.4", + "use-sync-external-store": "^1.5.0" + }, + "peerDependencies": { + "@react-native-masked-view/masked-view": ">= 0.2.0", + "@react-navigation/native": "^7.1.28", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0" + }, + "peerDependenciesMeta": { + "@react-native-masked-view/masked-view": { + "optional": true + } + } + }, + "node_modules/@react-navigation/native": { + "version": "7.1.28", + "resolved": "https://registry.npmjs.org/@react-navigation/native/-/native-7.1.28.tgz", + "integrity": "sha512-d1QDn+KNHfHGt3UIwOZvupvdsDdiHYZBEj7+wL2yDVo3tMezamYy60H9s3EnNVE1Ae1ty0trc7F2OKqo/RmsdQ==", + "dependencies": { + "@react-navigation/core": "^7.14.0", + "escape-string-regexp": "^4.0.0", + "fast-deep-equal": "^3.1.3", + "nanoid": "^3.3.11", + "use-latest-callback": "^0.2.4" + }, + "peerDependencies": { + "react": ">= 18.2.0", + "react-native": "*" + } + }, + "node_modules/@react-navigation/native-stack": { + "version": "7.13.0", + "resolved": "https://registry.npmjs.org/@react-navigation/native-stack/-/native-stack-7.13.0.tgz", + "integrity": "sha512-5OOp1IKEd5woHl9hGBU0qCAfrQ4+7Tqej0HzDzGQeXzS8tg9gq84x1qUdRvFk5BXbhuAyvJliY9F1/I07d2X0A==", + "dependencies": { + "@react-navigation/elements": "^2.9.5", + "color": "^4.2.3", + "sf-symbols-typescript": "^2.1.0", + "warn-once": "^0.1.1" + }, + "peerDependencies": { + "@react-navigation/native": "^7.1.28", + "react": ">= 18.2.0", + "react-native": "*", + "react-native-safe-area-context": ">= 4.0.0", + "react-native-screens": ">= 4.0.0" + } + }, + "node_modules/@react-navigation/native/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@react-navigation/routers": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/@react-navigation/routers/-/routers-7.5.3.tgz", + "integrity": "sha512-1tJHg4KKRJuQ1/EvJxatrMef3NZXEPzwUIUZ3n1yJ2t7Q97siwRtbynRpQG9/69ebbtiZ8W3ScOZF/OmhvM4Rg==", + "dependencies": { + "nanoid": "^3.3.11" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@standard-schema/utils": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz", + "integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==" + }, + "node_modules/@tanstack/query-core": { + "version": "5.90.20", + "resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz", + "integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + } + }, + "node_modules/@tanstack/react-query": { + "version": "5.90.21", + "resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz", + "integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==", + "dependencies": { + "@tanstack/query-core": "5.90.20" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "react": "^18 || ^19" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", + "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/react": { + "version": "19.1.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.17.tgz", + "integrity": "sha512-Qec1E3mhALmaspIrhWt9jkQMNdw6bReVu64mjvhbhq2NFPftLPVr+l1SZgmw/66WwBNpDh7ao5AT6gF5v41PFA==", + "devOptional": true, + "dependencies": { + "csstype": "^3.0.2" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==" + }, + "node_modules/@urql/core": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@urql/core/-/core-5.2.0.tgz", + "integrity": "sha512-/n0ieD0mvvDnVAXEQgX/7qJiVcvYvNkOHeBvkwtylfjydar123caCXcl58PXFY11oU1oquJocVXHxLAbtv4x1A==", + "dependencies": { + "@0no-co/graphql.web": "^1.0.13", + "wonka": "^6.3.2" + } + }, + "node_modules/@urql/exchange-retry": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@urql/exchange-retry/-/exchange-retry-1.3.2.tgz", + "integrity": "sha512-TQMCz2pFJMfpNxmSfX1VSfTjwUIFx/mL+p1bnfM1xjjdla7Z+KnGMW/EhFbpckp3LyWAH4PgOsMwOMnIN+MBFg==", + "dependencies": { + "@urql/core": "^5.1.2", + "wonka": "^6.3.2" + }, + "peerDependencies": { + "@urql/core": "^5.0.0" + } + }, + "node_modules/@xmldom/xmldom": { + "version": "0.8.11", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.8.11.tgz", + "integrity": "sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/anser": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/anser/-/anser-1.4.10.tgz", + "integrity": "sha512-hCv9AqTQ8ycjpSd3upOJd7vFwW1JaoYQ7tpham03GJ1ca8/65rqn0RpaWpItOAd6ylW9wAw6luXYPJIyPFVOww==" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==" + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==" + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" + }, + "node_modules/async-limiter": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", + "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.13.5", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", + "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.15", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", + "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-define-polyfill-provider": "^0.6.6", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", + "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.5", + "core-js-compat": "^3.43.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", + "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.6" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-react-compiler": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-react-compiler/-/babel-plugin-react-compiler-1.0.0.tgz", + "integrity": "sha512-Ixm8tFfoKKIPYdCCKYTsqv+Fd4IJ0DQqMyEimo+pxUOMUR9cVPlwTrFt9Avu+3cb6Zp3mAzl+t1MrG2fxxKsxw==", + "dependencies": { + "@babel/types": "^7.26.0" + } + }, + "node_modules/babel-plugin-react-native-web": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/babel-plugin-react-native-web/-/babel-plugin-react-native-web-0.21.2.tgz", + "integrity": "sha512-SPD0J6qjJn8231i0HZhlAGH6NORe+QvRSQM2mwQEzJ2Fb3E4ruWTiiicPlHjmeWShDXLcvoorOCXjeR7k/lyWA==" + }, + "node_modules/babel-plugin-syntax-hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/babel-plugin-syntax-hermes-parser/-/babel-plugin-syntax-hermes-parser-0.29.1.tgz", + "integrity": "sha512-2WFYnoWGdmih1I1J5eIqxATOeycOqRwYxAQBu3cUu/rhwInwHUg7k60AFNbuGjSDL8tje5GDrAnxzRLcu2pYcA==", + "dependencies": { + "hermes-parser": "0.29.1" + } + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==" + }, + "node_modules/babel-plugin-syntax-hermes-parser/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/babel-plugin-transform-flow-enums": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-transform-flow-enums/-/babel-plugin-transform-flow-enums-0.0.2.tgz", + "integrity": "sha512-g4aaCrDDOsWjbm0PUUeVnkcVd6AKJsVc/MbnPhEotEpkeJQP6b8nzewohQi7+QS8UyPehOhGWn0nOwjvWpmMvQ==", + "dependencies": { + "@babel/plugin-syntax-flow": "^7.12.1" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/better-opn": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/better-opn/-/better-opn-3.0.2.tgz", + "integrity": "sha512-aVNobHnJqLiUelTaHat9DZ1qM2w0C0Eym4LPI/3JxOnSokGVdsl1T1kN7TFvsEAD8G47A6VKQ0TVHqbBnYMJlQ==", + "dependencies": { + "open": "^8.0.4" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/better-opn/node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/big-integer": { + "version": "1.6.52", + "resolved": "https://registry.npmjs.org/big-integer/-/big-integer-1.6.52.tgz", + "integrity": "sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/bplist-creator": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/bplist-creator/-/bplist-creator-0.1.0.tgz", + "integrity": "sha512-sXaHZicyEEmY86WyueLTQesbeoH/mquvarJaQNbjuOQO+7gbFcDEWqKmcWA4cOTLzFlfgvkiVxolk1k5bBIpmg==", + "dependencies": { + "stream-buffers": "2.2.x" + } + }, + "node_modules/bplist-parser": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/bplist-parser/-/bplist-parser-0.3.1.tgz", + "integrity": "sha512-PyJxiNtA5T2PlLIeBot4lbp7rj4OadzjnMZD/G5zuBNt8ei/yCU7+wW0h2bag9vr8c+/WuRWmSxbqAl9hL1rBA==", + "dependencies": { + "big-integer": "1.6.x" + }, + "engines": { + "node": ">= 5.10.0" + } + }, + "node_modules/brace-expansion": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.3.tgz", + "integrity": "sha512-fy6KJm2RawA5RcHkLa1z/ScpBeA762UF9KmZQxwIbDtRJrgLzM10depAiEQ+CXYcoiqW1/m96OAAoke2nE9EeA==", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001774", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001774.tgz", + "integrity": "sha512-DDdwPGz99nmIEv216hKSgLD+D4ikHQHjBC/seF98N9CPqRX4M5mSxT9eTV6oyisnJcuzxtZy4n17yKKQYmYQOA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "engines": { + "node": ">=18" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chrome-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chromium-edge-launcher": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/chromium-edge-launcher/-/chromium-edge-launcher-0.2.0.tgz", + "integrity": "sha512-JfJjUnq25y9yg4FABRRVPmBGWPZZi+AQXT4mxupb67766/0UlhG8PAZCz6xzEMXTbW3CsSoE8PcCWA49n35mKg==", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0", + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + } + }, + "node_modules/chromium-edge-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ci-info": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", + "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==" + }, + "node_modules/cli-cursor": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha512-8lgKz8LmCRYZZQDpRyT2m5rKJ08TnU4tR9FFFW2rxpxR1FzWi4PQ/NfyODchAatHaUgnSPVcx/R5w6NuTBzFiw==", + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==" + }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, + "node_modules/color/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "engines": { + "node": ">= 10" + } + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "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==" + }, + "node_modules/core-js-compat": { + "version": "3.48.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", + "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", + "dependencies": { + "browserslist": "^4.28.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "devOptional": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "engines": { + "node": ">=8" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "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==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.302", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.302.tgz", + "integrity": "sha512-sM6HAN2LyK82IyPBpznDRqlTQAtuSaO+ShzFiWTvoMJLHyZ+Y39r8VMfHzwbU8MVBzQ4Wdn85+wlZl2TLGIlwg==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/env-editor": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/env-editor/-/env-editor-0.4.2.tgz", + "integrity": "sha512-ObFo8v4rQJAE59M69QzwloxPZtd33TpYEIjtKD1rrFDcM1Gd7IkDxEBU+HriziN6HSHQnBJi8Dmy+JWkav5HKA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/error-stack-parser": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.1.4.tgz", + "integrity": "sha512-Sk5V6wVazPhq5MhpO+AUxJn5x7XSXGl1R93Vn7i+zS15KDVxQijejNCrz8340/2bgLBjR9GtEG8ZVKONDjcqGQ==", + "dependencies": { + "stackframe": "^1.3.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/exec-async": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/exec-async/-/exec-async-2.2.0.tgz", + "integrity": "sha512-87OpwcEiMia/DeiKFzaQNBNFeN3XkkpYIh9FyOqq5mS2oKv3CBE67PXoEKcr6nodWdXNogTiQ0jE2NGuoffXPw==" + }, + "node_modules/expo": { + "version": "54.0.33", + "resolved": "https://registry.npmjs.org/expo/-/expo-54.0.33.tgz", + "integrity": "sha512-3yOEfAKqo+gqHcV8vKcnq0uA5zxlohnhA3fu4G43likN8ct5ZZ3LjAh9wDdKteEkoad3tFPvwxmXW711S5OHUw==", + "dependencies": { + "@babel/runtime": "^7.20.0", + "@expo/cli": "54.0.23", + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/devtools": "0.1.8", + "@expo/fingerprint": "0.15.4", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "54.0.14", + "@expo/vector-icons": "^15.0.3", + "@ungap/structured-clone": "^1.3.0", + "babel-preset-expo": "~54.0.10", + "expo-asset": "~12.0.12", + "expo-constants": "~18.0.13", + "expo-file-system": "~19.0.21", + "expo-font": "~14.0.11", + "expo-keep-awake": "~15.0.8", + "expo-modules-autolinking": "3.0.24", + "expo-modules-core": "3.0.29", + "pretty-format": "^29.7.0", + "react-refresh": "^0.14.2", + "whatwg-url-without-unicode": "8.0.0-3" + }, + "bin": { + "expo": "bin/cli", + "expo-modules-autolinking": "bin/autolinking", + "fingerprint": "bin/fingerprint" + }, + "peerDependencies": { + "@expo/dom-webview": "*", + "@expo/metro-runtime": "*", + "react": "*", + "react-native": "*", + "react-native-webview": "*" + }, + "peerDependenciesMeta": { + "@expo/dom-webview": { + "optional": true + }, + "@expo/metro-runtime": { + "optional": true + }, + "react-native-webview": { + "optional": true + } + } + }, + "node_modules/expo-modules-autolinking": { + "version": "3.0.24", + "resolved": "https://registry.npmjs.org/expo-modules-autolinking/-/expo-modules-autolinking-3.0.24.tgz", + "integrity": "sha512-TP+6HTwhL7orDvsz2VzauyQlXJcAWyU3ANsZ7JGL4DQu8XaZv/A41ZchbtAYLfozNA2Ya1Hzmhx65hXryBMjaQ==", + "dependencies": { + "@expo/spawn-async": "^1.7.2", + "chalk": "^4.1.0", + "commander": "^7.2.0", + "require-from-string": "^2.0.2", + "resolve-from": "^5.0.0" + }, + "bin": { + "expo-modules-autolinking": "bin/expo-modules-autolinking.js" + } + }, + "node_modules/expo-modules-autolinking/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expo-modules-autolinking/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/expo-modules-autolinking/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/expo-modules-autolinking/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/expo-modules-autolinking/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/expo-modules-autolinking/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/expo-modules-core": { + "version": "3.0.29", + "resolved": "https://registry.npmjs.org/expo-modules-core/-/expo-modules-core-3.0.29.tgz", + "integrity": "sha512-LzipcjGqk8gvkrOUf7O2mejNWugPkf3lmd9GkqL9WuNyeN2fRwU0Dn77e3ZUKI3k6sI+DNwjkq4Nu9fNN9WS7Q==", + "dependencies": { + "invariant": "^2.2.4" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo-secure-store": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz", + "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==", + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo-server": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.5.tgz", + "integrity": "sha512-IGR++flYH70rhLyeXF0Phle56/k4cee87WeQ4mamS+MkVAVP+dDlOHf2nN06Z9Y2KhU0Gp1k+y61KkghF7HdhA==", + "engines": { + "node": ">=20.16.0" + } + }, + "node_modules/expo-status-bar": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz", + "integrity": "sha512-xyYyVg6V1/SSOZWh4Ni3U129XHCnFHBTcUo0dhWtFDrZbNp/duw5AGsQfb2sVeU0gxWHXSY1+5F0jnKYC7WuOw==", + "dependencies": { + "react-native-is-edge-to-edge": "^1.2.1" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/expo/node_modules/@expo/cli": { + "version": "54.0.23", + "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.23.tgz", + "integrity": "sha512-km0h72SFfQCmVycH/JtPFTVy69w6Lx1cHNDmfLfQqgKFYeeHTjx7LVDP4POHCtNxFP2UeRazrygJhlh4zz498g==", + "dependencies": { + "@0no-co/graphql.web": "^1.0.8", + "@expo/code-signing-certificates": "^0.0.6", + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/devcert": "^1.2.1", + "@expo/env": "~2.0.8", + "@expo/image-utils": "^0.8.8", + "@expo/json-file": "^10.0.8", + "@expo/metro": "~54.2.0", + "@expo/metro-config": "~54.0.14", + "@expo/osascript": "^2.3.8", + "@expo/package-manager": "^1.9.10", + "@expo/plist": "^0.4.8", + "@expo/prebuild-config": "^54.0.8", + "@expo/schema-utils": "^0.1.8", + "@expo/spawn-async": "^1.7.2", + "@expo/ws-tunnel": "^1.0.1", + "@expo/xcpretty": "^4.3.0", + "@react-native/dev-middleware": "0.81.5", + "@urql/core": "^5.0.6", + "@urql/exchange-retry": "^1.3.0", + "accepts": "^1.3.8", + "arg": "^5.0.2", + "better-opn": "~3.0.2", + "bplist-creator": "0.1.0", + "bplist-parser": "^0.3.1", + "chalk": "^4.0.0", + "ci-info": "^3.3.0", + "compression": "^1.7.4", + "connect": "^3.7.0", + "debug": "^4.3.4", + "env-editor": "^0.4.1", + "expo-server": "^1.0.5", + "freeport-async": "^2.0.0", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "lan-network": "^0.1.6", + "minimatch": "^9.0.0", + "node-forge": "^1.3.3", + "npm-package-arg": "^11.0.0", + "ora": "^3.4.0", + "picomatch": "^3.0.1", + "pretty-bytes": "^5.6.0", + "pretty-format": "^29.7.0", + "progress": "^2.0.3", + "prompts": "^2.3.2", + "qrcode-terminal": "0.11.0", + "require-from-string": "^2.0.2", + "requireg": "^0.2.2", + "resolve": "^1.22.2", + "resolve-from": "^5.0.0", + "resolve.exports": "^2.0.3", + "semver": "^7.6.0", + "send": "^0.19.0", + "slugify": "^1.3.4", + "source-map-support": "~0.5.21", + "stacktrace-parser": "^0.1.10", + "structured-headers": "^0.4.1", + "tar": "^7.5.2", + "terminal-link": "^2.1.1", + "undici": "^6.18.2", + "wrap-ansi": "^7.0.0", + "ws": "^8.12.1" + }, + "bin": { + "expo-internal": "build/bin/cli" + }, + "peerDependencies": { + "expo": "*", + "expo-router": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "expo-router": { + "optional": true + }, + "react-native": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@expo/cli/node_modules/@expo/prebuild-config": { + "version": "54.0.8", + "resolved": "https://registry.npmjs.org/@expo/prebuild-config/-/prebuild-config-54.0.8.tgz", + "integrity": "sha512-EA7N4dloty2t5Rde+HP0IEE+nkAQiu4A/+QGZGT9mFnZ5KKjPPkqSyYcRvP5bhQE10D+tvz6X0ngZpulbMdbsg==", + "dependencies": { + "@expo/config": "~12.0.13", + "@expo/config-plugins": "~54.0.4", + "@expo/config-types": "^54.0.10", + "@expo/image-utils": "^0.8.8", + "@expo/json-file": "^10.0.8", + "@react-native/normalize-colors": "0.81.5", + "debug": "^4.3.1", + "resolve-from": "^5.0.0", + "semver": "^7.6.0", + "xml2js": "0.6.0" + }, + "peerDependencies": { + "expo": "*" + } + }, + "node_modules/expo/node_modules/@expo/metro-config": { + "version": "54.0.14", + "resolved": "https://registry.npmjs.org/@expo/metro-config/-/metro-config-54.0.14.tgz", + "integrity": "sha512-hxpLyDfOR4L23tJ9W1IbJJsG7k4lv2sotohBm/kTYyiG+pe1SYCAWsRmgk+H42o/wWf/HQjE5k45S5TomGLxNA==", + "dependencies": { + "@babel/code-frame": "^7.20.0", + "@babel/core": "^7.20.0", + "@babel/generator": "^7.20.5", + "@expo/config": "~12.0.13", + "@expo/env": "~2.0.8", + "@expo/json-file": "~10.0.8", + "@expo/metro": "~54.2.0", + "@expo/spawn-async": "^1.7.2", + "browserslist": "^4.25.0", + "chalk": "^4.1.0", + "debug": "^4.3.2", + "dotenv": "~16.4.5", + "dotenv-expand": "~11.0.6", + "getenv": "^2.0.0", + "glob": "^13.0.0", + "hermes-parser": "^0.29.1", + "jsc-safe-url": "^0.2.4", + "lightningcss": "^1.30.1", + "minimatch": "^9.0.0", + "postcss": "~8.4.32", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "expo": "*" + }, + "peerDependenciesMeta": { + "expo": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/@expo/vector-icons": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@expo/vector-icons/-/vector-icons-15.1.1.tgz", + "integrity": "sha512-Iu2VkcoI5vygbtYngm7jb4ifxElNVXQYdDrYkT7UCEIiKLeWnQY0wf2ZhHZ+Wro6Sc5TaumpKUOqDRpLi5rkvw==", + "peerDependencies": { + "expo-font": ">=14.0.4", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/expo/node_modules/babel-preset-expo": { + "version": "54.0.10", + "resolved": "https://registry.npmjs.org/babel-preset-expo/-/babel-preset-expo-54.0.10.tgz", + "integrity": "sha512-wTt7POavLFypLcPW/uC5v8y+mtQKDJiyGLzYCjqr9tx0Qc3vCXcDKk1iCFIj/++Iy5CWhhTflEa7VvVPNWeCfw==", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/plugin-proposal-decorators": "^7.12.9", + "@babel/plugin-proposal-export-default-from": "^7.24.7", + "@babel/plugin-syntax-export-default-from": "^7.24.7", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-flow-strip-types": "^7.25.2", + "@babel/plugin-transform-modules-commonjs": "^7.24.8", + "@babel/plugin-transform-object-rest-spread": "^7.24.7", + "@babel/plugin-transform-parameters": "^7.24.7", + "@babel/plugin-transform-private-methods": "^7.24.7", + "@babel/plugin-transform-private-property-in-object": "^7.24.7", + "@babel/plugin-transform-runtime": "^7.24.7", + "@babel/preset-react": "^7.22.15", + "@babel/preset-typescript": "^7.23.0", + "@react-native/babel-preset": "0.81.5", + "babel-plugin-react-compiler": "^1.0.0", + "babel-plugin-react-native-web": "~0.21.0", + "babel-plugin-syntax-hermes-parser": "^0.29.1", + "babel-plugin-transform-flow-enums": "^0.0.2", + "debug": "^4.3.4", + "resolve-from": "^5.0.0" + }, + "peerDependencies": { + "@babel/runtime": "^7.20.0", + "expo": "*", + "react-refresh": ">=0.14.0 <1.0.0" + }, + "peerDependenciesMeta": { + "@babel/runtime": { + "optional": true + }, + "expo": { + "optional": true + } + } + }, + "node_modules/expo/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/expo/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/expo/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/expo/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/expo/node_modules/expo-asset": { + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/expo-asset/-/expo-asset-12.0.12.tgz", + "integrity": "sha512-CsXFCQbx2fElSMn0lyTdRIyKlSXOal6ilLJd+yeZ6xaC7I9AICQgscY5nj0QcwgA+KYYCCEQEBndMsmj7drOWQ==", + "dependencies": { + "@expo/image-utils": "^0.8.8", + "expo-constants": "~18.0.12" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-constants": { + "version": "18.0.13", + "resolved": "https://registry.npmjs.org/expo-constants/-/expo-constants-18.0.13.tgz", + "integrity": "sha512-FnZn12E1dRYKDHlAdIyNFhBurKTS3F9CrfrBDJI5m3D7U17KBHMQ6JEfYlSj7LG7t+Ulr+IKaj58L1k5gBwTcQ==", + "dependencies": { + "@expo/config": "~12.0.13", + "@expo/env": "~2.0.8" + }, + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-file-system": { + "version": "19.0.21", + "resolved": "https://registry.npmjs.org/expo-file-system/-/expo-file-system-19.0.21.tgz", + "integrity": "sha512-s3DlrDdiscBHtab/6W1osrjGL+C2bvoInPJD7sOwmxfJ5Woynv2oc+Fz1/xVXaE/V7HE/+xrHC/H45tu6lZzzg==", + "peerDependencies": { + "expo": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-font": { + "version": "14.0.11", + "resolved": "https://registry.npmjs.org/expo-font/-/expo-font-14.0.11.tgz", + "integrity": "sha512-ga0q61ny4s/kr4k8JX9hVH69exVSIfcIc19+qZ7gt71Mqtm7xy2c6kwsPTCyhBW2Ro5yXTT8EaZOpuRi35rHbg==", + "dependencies": { + "fontfaceobserver": "^2.1.0" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, + "node_modules/expo/node_modules/expo-keep-awake": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-keep-awake/-/expo-keep-awake-15.0.8.tgz", + "integrity": "sha512-YK9M1VrnoH1vLJiQzChZgzDvVimVoriibiDIFLbQMpjYBnvyfUeHJcin/Gx1a+XgupNXy92EQJLgI/9ZuXajYQ==", + "peerDependencies": { + "expo": "*", + "react": "*" + } + }, + "node_modules/expo/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/expo/node_modules/hermes-estree": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.29.1.tgz", + "integrity": "sha512-jl+x31n4/w+wEqm0I2r4CMimukLbLQEYpisys5oCre611CI5fc9TxhqkBBCJ1edDG4Kza0f7CgNz8xVMLZQOmQ==" + }, + "node_modules/expo/node_modules/hermes-parser": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.29.1.tgz", + "integrity": "sha512-xBHWmUtRC5e/UL0tI7Ivt2riA/YBq9+SiYFU7C1oBa/j2jYGlIF9043oak1F47ihuDIxQ5nbsKueYJDRY02UgA==", + "dependencies": { + "hermes-estree": "0.29.1" + } + }, + "node_modules/expo/node_modules/picomatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-3.0.1.tgz", + "integrity": "sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/expo/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/expo/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.3.tgz", + "integrity": "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/filter-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", + "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flow-enums-runtime": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/flow-enums-runtime/-/flow-enums-runtime-0.0.6.tgz", + "integrity": "sha512-3PYnM29RFXwvAN6Pc/scUfkI7RwhQ/xqyLUyPNlXUp9S40zI8nup9tUSrTLSVnWGBN38FNiGWbwZOB6uR4OGdw==" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fontfaceobserver": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/fontfaceobserver/-/fontfaceobserver-2.3.0.tgz", + "integrity": "sha512-6FPvD/IVyT4ZlNe7Wcn5Fb/4ChigpucKYSvD6a+0iMoLn2inpo711eyIcKjmDtE5XNcgAkSH9uN/nfAeZzHEfg==" + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/freeport-async": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/freeport-async/-/freeport-async-2.0.0.tgz", + "integrity": "sha512-K7od3Uw45AJg00XUmy15+Hae2hOcgKcmN3/EF6Y7i01O0gaqiRx8sUSpsb9+BRNL8RPBrhzPsVfy8q9ADlJuWQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/getenv": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/getenv/-/getenv-2.0.0.tgz", + "integrity": "sha512-VilgtJj/ALgGY77fiLam5iD336eSWi96Q15JSAG1zi8NRBysm3LXKdGnHb4m5cuyxvOLQQKWpBZAT6ni4FI2iQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", + "integrity": "sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/global-dirs": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-0.1.1.tgz", + "integrity": "sha512-NknMLn7F2J7aflwFOlGdNIuCDpN3VGoSoB+aap3KABFWbHVn1TCgFC+np23J8W2BiZbjfEw3BFBycSMv1AFblg==", + "dependencies": { + "ini": "^1.3.4" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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==" + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hermes-estree": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.32.0.tgz", + "integrity": "sha512-KWn3BqnlDOl97Xe1Yviur6NbgIZ+IP+UVSpshlZWkq+EtoHg6/cwiDj/osP9PCEgFE15KBm1O55JRwbMEm5ejQ==" + }, + "node_modules/hermes-parser": { + "version": "0.32.0", + "resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.32.0.tgz", + "integrity": "sha512-g4nBOWFpuiTqjR3LZdRxKUkij9iyveWeuks7INEsMX741f3r9xxrOe8TeQfUxtda0eXmiIFiMQzoeSQEno33Hw==", + "dependencies": { + "hermes-estree": "0.32.0" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==" + }, + "node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "engines": { + "node": ">= 4" + } + }, + "node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, + "node_modules/is-arrayish": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.4.tgz", + "integrity": "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jimp-compact": { + "version": "0.16.1", + "resolved": "https://registry.npmjs.org/jimp-compact/-/jimp-compact-0.16.1.tgz", + "integrity": "sha512-dZ6Ra7u1G8c4Letq/B5EzAxj4tLFHL+cGtdpR+PVm4yzPDj+lCk+AbivWt1eOM+ikzkowtyV7qSqX6qr3t71Ww==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsc-safe-url": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/jsc-safe-url/-/jsc-safe-url-0.2.4.tgz", + "integrity": "sha512-0wM3YBWtYePOjfyXQH5MWQ8H7sdk5EXSwZvmSLKk2RboVQ2Bu239jycHDz5J/8Blf3K0Qnoy2b6xD+z10MFB+Q==" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lan-network": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/lan-network/-/lan-network-0.1.7.tgz", + "integrity": "sha512-mnIlAEMu4OyEvUNdzco9xpuB9YVcPkQec+QsgycBCtPZvEqWPCDPfbAE4OJMdBBWpZWtpCn1xw9jJYlwjWI5zQ==", + "bin": { + "lan-network": "dist/lan-network-cli.js" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "engines": { + "node": ">=6" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/lightningcss": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.31.1.tgz", + "integrity": "sha512-l51N2r93WmGUye3WuFoN5k10zyvrVs0qfKBhyC5ogUQ6Ew6JUSswh78mbSO+IU3nTWsyOArqPCcShdQSadghBQ==", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.31.1", + "lightningcss-darwin-arm64": "1.31.1", + "lightningcss-darwin-x64": "1.31.1", + "lightningcss-freebsd-x64": "1.31.1", + "lightningcss-linux-arm-gnueabihf": "1.31.1", + "lightningcss-linux-arm64-gnu": "1.31.1", + "lightningcss-linux-arm64-musl": "1.31.1", + "lightningcss-linux-x64-gnu": "1.31.1", + "lightningcss-linux-x64-musl": "1.31.1", + "lightningcss-win32-arm64-msvc": "1.31.1", + "lightningcss-win32-x64-msvc": "1.31.1" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.31.1.tgz", + "integrity": "sha512-HXJF3x8w9nQ4jbXRiNppBCqeZPIAfUo8zE/kOEGbW5NZvGc/K7nMxbhIr+YlFlHW5mpbg/YFPdbnCh1wAXCKFg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.31.1.tgz", + "integrity": "sha512-02uTEqf3vIfNMq3h/z2cJfcOXnQ0GRwQrkmPafhueLb2h7mqEidiCzkE4gBMEH65abHRiQvhdcQ+aP0D0g67sg==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.31.1.tgz", + "integrity": "sha512-1ObhyoCY+tGxtsz1lSx5NXCj3nirk0Y0kB/g8B8DT+sSx4G9djitg9ejFnjb3gJNWo7qXH4DIy2SUHvpoFwfTA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.31.1.tgz", + "integrity": "sha512-1RINmQKAItO6ISxYgPwszQE1BrsVU5aB45ho6O42mu96UiZBxEXsuQ7cJW4zs4CEodPUioj/QrXW1r9pLUM74A==", + "cpu": [ + "x64" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.31.1.tgz", + "integrity": "sha512-OOCm2//MZJ87CdDK62rZIu+aw9gBv4azMJuA8/KB74wmfS3lnC4yoPHm0uXZ/dvNNHmnZnB8XLAZzObeG0nS1g==", + "cpu": [ + "arm" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.31.1.tgz", + "integrity": "sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==", + "cpu": [ + "arm64" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.31.1.tgz", + "integrity": "sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==", + "cpu": [ + "arm64" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.31.1.tgz", + "integrity": "sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==", + "cpu": [ + "x64" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.31.1.tgz", + "integrity": "sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==", + "cpu": [ + "x64" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.31.1.tgz", + "integrity": "sha512-aJReEbSEQzx1uBlQizAOBSjcmr9dCdL3XuC/6HLXAxmtErsj2ICo5yYggg1qOODQMtnjNQv2UHb9NpOuFtYe4w==", + "cpu": [ + "arm64" + ], + "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.31.1", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.31.1.tgz", + "integrity": "sha512-I9aiFrbd7oYHwlnQDqr1Roz+fTz61oDDJX7n9tYF9FJymH1cIN1DtKw3iYt6b8WZgEjoNwVSncwF4wx/ZedMhw==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" + }, + "node_modules/lodash.throttle": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.throttle/-/lodash.throttle-4.1.1.tgz", + "integrity": "sha512-wIkUCfVKpVsWo3JSZlc+8MB5it+2AN5W8J7YVMST30UrvcQNZ1Okbj+rbVniijTWE6FGYy4XJq/rHkas8qJMLQ==" + }, + "node_modules/log-symbols": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-2.2.0.tgz", + "integrity": "sha512-VeIAFslyIerEJLXHziedo2basKbMKtTw3vfn5IzG0XTjhAVEJyNHnL2p7vc+wBDSdQuUpNw3M2u6xb9QsAY5Eg==", + "dependencies": { + "chalk": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/memoize-one": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz", + "integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==" + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" + }, + "node_modules/metro": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro/-/metro-0.83.3.tgz", + "integrity": "sha512-+rP+/GieOzkt97hSJ0MrPOuAH/jpaS21ZDvL9DJ35QYRDlQcwzcvUlGUf79AnQxq/2NPiS/AULhhM4TKutIt8Q==", + "dependencies": { + "@babel/code-frame": "^7.24.7", + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "@babel/types": "^7.25.2", + "accepts": "^1.3.7", + "chalk": "^4.0.0", + "ci-info": "^2.0.0", + "connect": "^3.6.5", + "debug": "^4.4.0", + "error-stack-parser": "^2.0.6", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "hermes-parser": "0.32.0", + "image-size": "^1.0.2", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "jsc-safe-url": "^0.2.2", + "lodash.throttle": "^4.1.1", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-config": "0.83.3", + "metro-core": "0.83.3", + "metro-file-map": "0.83.3", + "metro-resolver": "0.83.3", + "metro-runtime": "0.83.3", + "metro-source-map": "0.83.3", + "metro-symbolicate": "0.83.3", + "metro-transform-plugins": "0.83.3", + "metro-transform-worker": "0.83.3", + "mime-types": "^2.1.27", + "nullthrows": "^1.1.1", + "serialize-error": "^2.1.0", + "source-map": "^0.5.6", + "throat": "^5.0.0", + "ws": "^7.5.10", + "yargs": "^17.6.2" + }, + "bin": { + "metro": "src/cli.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-babel-transformer": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-babel-transformer/-/metro-babel-transformer-0.83.3.tgz", + "integrity": "sha512-1vxlvj2yY24ES1O5RsSIvg4a4WeL7PFXgKOHvXTXiW0deLvQr28ExXj6LjwCCDZ4YZLhq6HddLpZnX4dEdSq5g==", + "dependencies": { + "@babel/core": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "hermes-parser": "0.32.0", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-cache": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-cache/-/metro-cache-0.83.3.tgz", + "integrity": "sha512-3jo65X515mQJvKqK3vWRblxDEcgY55Sk3w4xa6LlfEXgQ9g1WgMh9m4qVZVwgcHoLy0a2HENTPCCX4Pk6s8c8Q==", + "dependencies": { + "exponential-backoff": "^3.1.1", + "flow-enums-runtime": "^0.0.6", + "https-proxy-agent": "^7.0.5", + "metro-core": "0.83.3" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-cache-key": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-cache-key/-/metro-cache-key-0.83.3.tgz", + "integrity": "sha512-59ZO049jKzSmvBmG/B5bZ6/dztP0ilp0o988nc6dpaDsU05Cl1c/lRf+yx8m9WW/JVgbmfO5MziBU559XjI5Zw==", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-config": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-config/-/metro-config-0.83.3.tgz", + "integrity": "sha512-mTel7ipT0yNjKILIan04bkJkuCzUUkm2SeEaTads8VfEecCh+ltXchdq6DovXJqzQAXuR2P9cxZB47Lg4klriA==", + "dependencies": { + "connect": "^3.6.5", + "flow-enums-runtime": "^0.0.6", + "jest-validate": "^29.7.0", + "metro": "0.83.3", + "metro-cache": "0.83.3", + "metro-core": "0.83.3", + "metro-runtime": "0.83.3", + "yaml": "^2.6.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-core": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-core/-/metro-core-0.83.3.tgz", + "integrity": "sha512-M+X59lm7oBmJZamc96usuF1kusd5YimqG/q97g4Ac7slnJ3YiGglW5CsOlicTR5EWf8MQFxxjDoB6ytTqRe8Hw==", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "lodash.throttle": "^4.1.1", + "metro-resolver": "0.83.3" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-file-map": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-file-map/-/metro-file-map-0.83.3.tgz", + "integrity": "sha512-jg5AcyE0Q9Xbbu/4NAwwZkmQn7doJCKGW0SLeSJmzNB9Z24jBe0AL2PHNMy4eu0JiKtNWHz9IiONGZWq7hjVTA==", + "dependencies": { + "debug": "^4.4.0", + "fb-watchman": "^2.0.0", + "flow-enums-runtime": "^0.0.6", + "graceful-fs": "^4.2.4", + "invariant": "^2.2.4", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "nullthrows": "^1.1.1", + "walker": "^1.0.7" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-minify-terser": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-minify-terser/-/metro-minify-terser-0.83.3.tgz", + "integrity": "sha512-O2BmfWj6FSfzBLrNCXt/rr2VYZdX5i6444QJU0fFoc7Ljg+Q+iqebwE3K0eTvkI6TRjELsXk1cjU+fXwAR4OjQ==", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "terser": "^5.15.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-resolver": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-resolver/-/metro-resolver-0.83.3.tgz", + "integrity": "sha512-0js+zwI5flFxb1ktmR///bxHYg7OLpRpWZlBBruYG8OKYxeMP7SV0xQ/o/hUelrEMdK4LJzqVtHAhBm25LVfAQ==", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-runtime": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-runtime/-/metro-runtime-0.83.3.tgz", + "integrity": "sha512-JHCJb9ebr9rfJ+LcssFYA2x1qPYuSD/bbePupIGhpMrsla7RCwC/VL3yJ9cSU+nUhU4c9Ixxy8tBta+JbDeZWw==", + "dependencies": { + "@babel/runtime": "^7.25.0", + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-source-map": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-source-map/-/metro-source-map-0.83.3.tgz", + "integrity": "sha512-xkC3qwUBh2psVZgVavo8+r2C9Igkk3DibiOXSAht1aYRRcztEZNFtAMtfSB7sdO2iFMx2Mlyu++cBxz/fhdzQg==", + "dependencies": { + "@babel/traverse": "^7.25.3", + "@babel/traverse--for-generate-function-map": "npm:@babel/traverse@^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-symbolicate": "0.83.3", + "nullthrows": "^1.1.1", + "ob1": "0.83.3", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-symbolicate": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-symbolicate/-/metro-symbolicate-0.83.3.tgz", + "integrity": "sha512-F/YChgKd6KbFK3eUR5HdUsfBqVsanf5lNTwFd4Ca7uuxnHgBC3kR/Hba/RGkenR3pZaGNp5Bu9ZqqP52Wyhomw==", + "dependencies": { + "flow-enums-runtime": "^0.0.6", + "invariant": "^2.2.4", + "metro-source-map": "0.83.3", + "nullthrows": "^1.1.1", + "source-map": "^0.5.6", + "vlq": "^1.0.0" + }, + "bin": { + "metro-symbolicate": "src/index.js" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-plugins": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-transform-plugins/-/metro-transform-plugins-0.83.3.tgz", + "integrity": "sha512-eRGoKJU6jmqOakBMH5kUB7VitEWiNrDzBHpYbkBXW7C5fUGeOd2CyqrosEzbMK5VMiZYyOcNFEphvxk3OXey2A==", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/template": "^7.25.0", + "@babel/traverse": "^7.25.3", + "flow-enums-runtime": "^0.0.6", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro-transform-worker": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/metro-transform-worker/-/metro-transform-worker-0.83.3.tgz", + "integrity": "sha512-Ztekew9t/gOIMZX1tvJOgX7KlSLL5kWykl0Iwu2cL2vKMKVALRl1hysyhUw0vjpAvLFx+Kfq9VLjnHIkW32fPA==", + "dependencies": { + "@babel/core": "^7.25.2", + "@babel/generator": "^7.25.0", + "@babel/parser": "^7.25.3", + "@babel/types": "^7.25.2", + "flow-enums-runtime": "^0.0.6", + "metro": "0.83.3", + "metro-babel-transformer": "0.83.3", + "metro-cache": "0.83.3", + "metro-cache-key": "0.83.3", + "metro-minify-terser": "0.83.3", + "metro-source-map": "0.83.3", + "metro-transform-plugins": "0.83.3", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/metro/node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/metro/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/metro/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/metro/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/metro/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/metro/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/metro/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.6.tgz", + "integrity": "sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==", + "dependencies": { + "brace-expansion": "^5.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.1.0.tgz", + "integrity": "sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/nested-error-stacks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nested-error-stacks/-/nested-error-stacks-2.0.1.tgz", + "integrity": "sha512-SrQrok4CATudVzBS7coSz26QRSmlK9TzzoFbeKfcPBUFPjcQM9Rqvr/DlJkOrwI/0KcgvMub1n1g5Jt9EgRn4A==" + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-package-arg": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-11.0.3.tgz", + "integrity": "sha512-sHGJy8sOC1YraBywpzQlIKBE4pBbGbiF95U6Auspzyem956E0+FtDtsx1ZxlOJkQCZ1AFXAY/yuvtFYrOxF+Bw==", + "dependencies": { + "hosted-git-info": "^7.0.0", + "proc-log": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/nullthrows": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", + "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==" + }, + "node_modules/ob1": { + "version": "0.83.3", + "resolved": "https://registry.npmjs.org/ob1/-/ob1-0.83.3.tgz", + "integrity": "sha512-egUxXCDwoWG06NGCS5s5AdcpnumHKJlfd3HH06P3m9TEMwwScfcY35wpQxbm9oHof+dM/lVH9Rfyu1elTVelSA==", + "dependencies": { + "flow-enums-runtime": "^0.0.6" + }, + "engines": { + "node": ">=20.19.4" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", + "dependencies": { + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/open": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-7.4.2.tgz", + "integrity": "sha512-MVHddDVweXZF3awtlAS+6pgKLlm/JgxZ90+/NBurBoQctVOOB/zDdVjcyPzQ+0laDGbsWgrRkflI65sQeOgT9Q==", + "dependencies": { + "is-docker": "^2.0.0", + "is-wsl": "^2.1.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-3.4.0.tgz", + "integrity": "sha512-eNwHudNbO1folBP3JsZ19v9azXWtQZjICdr3Q0TDPIaeBQ3mXLrh54wM+er0+hSp+dWKf+Z8KM58CYzEyIYxYg==", + "dependencies": { + "chalk": "^2.4.2", + "cli-cursor": "^2.1.0", + "cli-spinners": "^2.0.0", + "log-symbols": "^2.2.0", + "strip-ansi": "^5.2.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/ansi-regex": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.1.tgz", + "integrity": "sha512-ILlv4k/3f6vfQ4OoP2AGvirOktlQ98ZEL1k9FaQjxa3L1abBgbuTDAdPOpvbGncC0BTVQrl+OM8xZGK6tWXt7g==", + "engines": { + "node": ">=6" + } + }, + "node_modules/ora/node_modules/strip-ansi": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", + "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", + "dependencies": { + "ansi-regex": "^4.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-png": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/parse-png/-/parse-png-2.1.0.tgz", + "integrity": "sha512-Nt/a5SfCLiTnQAjx3fHlqp8hRgTL3z7kTQZzvIMS9uCAepnCyjpdEc6M/sz69WqMBdaDBw9sF1F1UaHROYzGkQ==", + "dependencies": { + "pngjs": "^3.3.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.2.6", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.6.tgz", + "integrity": "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ==", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/plist": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/plist/-/plist-3.1.0.tgz", + "integrity": "sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==", + "dependencies": { + "@xmldom/xmldom": "^0.8.8", + "base64-js": "^1.5.1", + "xmlbuilder": "^15.1.1" + }, + "engines": { + "node": ">=10.4.0" + } + }, + "node_modules/pngjs": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", + "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "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" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/pretty-format/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==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/promise": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise/-/promise-8.3.0.tgz", + "integrity": "sha512-rZPNPKTOYVNEEKFaq1HqTgOwZD+4/YHS5ukLzQCypkj+OkYx7iv0mA91lJlpPPZ8vMau3IIGj5Qlwrx+8iiSmg==", + "dependencies": { + "asap": "~2.0.6" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qrcode-terminal": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.11.0.tgz", + "integrity": "sha512-Uu7ii+FQy4Qf82G4xu7ShHhjhGahEpCWc3x8UavY3CTcWV+ufmmCtwkr7ZKsX42jdL0kr1B5FKUeqJvAn51jzQ==", + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/query-string": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-7.1.3.tgz", + "integrity": "sha512-hh2WYhq4fi8+b+/2Kg9CEge4fDPvHS534aOOvOZeQ3+Vf2mCFsaFBYj0i+iXcAq6I9Vzp5fjMFBlONvayDC1qg==", + "dependencies": { + "decode-uri-component": "^0.2.2", + "filter-obj": "^1.1.0", + "split-on-first": "^1.0.0", + "strict-uri-encode": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/react": { + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-devtools-core": { + "version": "6.1.5", + "resolved": "https://registry.npmjs.org/react-devtools-core/-/react-devtools-core-6.1.5.tgz", + "integrity": "sha512-ePrwPfxAnB+7hgnEr8vpKxL9cmnp7F322t8oqcPshbIQQhDKgFDW4tjhF2wjVbdXF9O/nyuy3sQWd9JGpiLPvA==", + "dependencies": { + "shell-quote": "^1.6.1", + "ws": "^7" + } + }, + "node_modules/react-freeze": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/react-freeze/-/react-freeze-1.0.4.tgz", + "integrity": "sha512-r4F0Sec0BLxWicc7HEyo2x3/2icUTrRmDjaaRyzzn+7aDyFZliszMDOgLVwSnQnYENOlL1o569Ze2HZefk8clA==", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=17.0.0" + } + }, + "node_modules/react-hook-form": { + "version": "7.71.2", + "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", + "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", + "engines": { + "node": ">=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/react-hook-form" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/react-native": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/react-native/-/react-native-0.81.5.tgz", + "integrity": "sha512-1w+/oSjEXZjMqsIvmkCRsOc8UBYv163bTWKTI8+1mxztvQPhCRYGTvZ/PL1w16xXHneIj/SLGfxWg2GWN2uexw==", + "dependencies": { + "@jest/create-cache-key-function": "^29.7.0", + "@react-native/assets-registry": "0.81.5", + "@react-native/codegen": "0.81.5", + "@react-native/community-cli-plugin": "0.81.5", + "@react-native/gradle-plugin": "0.81.5", + "@react-native/js-polyfills": "0.81.5", + "@react-native/normalize-colors": "0.81.5", + "@react-native/virtualized-lists": "0.81.5", + "abort-controller": "^3.0.0", + "anser": "^1.4.9", + "ansi-regex": "^5.0.0", + "babel-jest": "^29.7.0", + "babel-plugin-syntax-hermes-parser": "0.29.1", + "base64-js": "^1.5.1", + "commander": "^12.0.0", + "flow-enums-runtime": "^0.0.6", + "glob": "^7.1.1", + "invariant": "^2.2.4", + "jest-environment-node": "^29.7.0", + "memoize-one": "^5.0.0", + "metro-runtime": "^0.83.1", + "metro-source-map": "^0.83.1", + "nullthrows": "^1.1.1", + "pretty-format": "^29.7.0", + "promise": "^8.3.0", + "react-devtools-core": "^6.1.5", + "react-refresh": "^0.14.0", + "regenerator-runtime": "^0.13.2", + "scheduler": "0.26.0", + "semver": "^7.1.3", + "stacktrace-parser": "^0.1.10", + "whatwg-fetch": "^3.0.0", + "ws": "^6.2.3", + "yargs": "^17.6.2" + }, + "bin": { + "react-native": "cli.js" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "^19.1.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native-is-edge-to-edge": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/react-native-is-edge-to-edge/-/react-native-is-edge-to-edge-1.2.1.tgz", + "integrity": "sha512-FLbPWl/MyYQWz+KwqOZsSyj2JmLKglHatd3xLZWskXOpRaio4LfEDEz8E/A6uD8QoTHW6Aobw1jbEwK7KMgR7Q==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-safe-area-context": { + "version": "5.7.0", + "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.7.0.tgz", + "integrity": "sha512-/9/MtQz8ODphjsLdZ+GZAIcC/RtoqW9EeShf7Uvnfgm/pzYrJ75y3PV/J1wuAV1T5Dye5ygq4EAW20RoBq0ABQ==", + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native-screens": { + "version": "4.16.0", + "resolved": "https://registry.npmjs.org/react-native-screens/-/react-native-screens-4.16.0.tgz", + "integrity": "sha512-yIAyh7F/9uWkOzCi1/2FqvNvK6Wb9Y1+Kzn16SuGfN9YFJDTbwlzGRvePCNTOX0recpLQF3kc2FmvMUhyTCH1Q==", + "dependencies": { + "react-freeze": "^1.0.0", + "react-native-is-edge-to-edge": "^1.2.1", + "warn-once": "^0.1.0" + }, + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, + "node_modules/react-native/node_modules/@react-native/virtualized-lists": { + "version": "0.81.5", + "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", + "integrity": "sha512-UVXgV/db25OPIvwZySeToXD/9sKKhOdkcWmmf4Jh8iBZuyfML+/5CasaZ1E7Lqg6g3uqVQq75NqIwkYmORJMPw==", + "dependencies": { + "invariant": "^2.2.4", + "nullthrows": "^1.1.1" + }, + "engines": { + "node": ">= 20.19.4" + }, + "peerDependencies": { + "@types/react": "^19.1.0", + "react": "*", + "react-native": "*" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-native/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/react-native/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/react-native/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "engines": { + "node": ">=18" + } + }, + "node_modules/react-native/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/react-native/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/react-native/node_modules/ws": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.3.tgz", + "integrity": "sha512-jmTjYU0j60B+vHey6TfR3Z7RD61z/hmxBS3VMSGIrroOWXQEneK1zNuotOUrGyBHQj0yrpsLHPWtigEFd13ndA==", + "dependencies": { + "async-limiter": "~1.0.0" + } + }, + "node_modules/react-refresh": { + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==" + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", + "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/regexpu-core": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", + "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.2", + "regjsgen": "^0.8.0", + "regjsparser": "^0.13.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.2.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==" + }, + "node_modules/regjsparser": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", + "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", + "dependencies": { + "jsesc": "~3.1.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "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==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requireg": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/requireg/-/requireg-0.2.2.tgz", + "integrity": "sha512-nYzyjnFcPNGR3lx9lwPPPnuQxv6JWEZd2Ci0u9opN7N5zUEPIhY/GbL3vMGOr2UXwEg9WwSyV9X9Y/kLFgPsOg==", + "dependencies": { + "nested-error-stacks": "~2.0.1", + "rc": "~1.2.7", + "resolve": "~1.7.1" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/requireg/node_modules/resolve": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.7.1.tgz", + "integrity": "sha512-c7rwLofp8g1U+h1KNyHL/jicrKg1Ek4q+Lr33AL65uZTinUZHe30D5HlyN5V9NW0JX1D5dXQ4jqW5l7Sy/kGfw==", + "dependencies": { + "path-parse": "^1.0.5" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-global": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-global/-/resolve-global-1.0.0.tgz", + "integrity": "sha512-zFa12V4OLtT5XUX/Q4VLvTfBf+Ok0SPc1FNGM/z9ctUdiU618qwKpWnd0CHs3+RqROfyEg/DhuHbMWYqcgljEw==", + "dependencies": { + "global-dirs": "^0.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-workspace-root": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/resolve-workspace-root/-/resolve-workspace-root-2.0.1.tgz", + "integrity": "sha512-nR23LHAvaI6aHtMg6RWoaHpdR4D881Nydkzi2CixINyg9T00KgaJdJI6Vwty+Ps8WLxZHuxsS0BseWjxSA4C+w==" + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "engines": { + "node": ">=10" + } + }, + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha512-6IzJLuGi4+R14vwagDHX+JrXmPVtPpn4mffDJ1UdR7/Edm87fl6yi8mMBIVvFtJaNTUvjughmW4hwLhRG7gC1Q==", + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/sax": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", + "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/scheduler": { + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", + "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.1", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "~2.4.1", + "range-parser": "~1.2.1", + "statuses": "~2.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-error": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/serialize-error/-/serialize-error-2.1.0.tgz", + "integrity": "sha512-ghgmKt5o4Tly5yEG/UJp8qTd0AN7Xalw4XBtDEKP655B699qMEtra1WlXeE6WIvdEG481JvRxULKsInq/iNysw==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/serve-static": { + "version": "1.16.3", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", + "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "~0.19.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/sf-symbols-typescript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/sf-symbols-typescript/-/sf-symbols-typescript-2.2.0.tgz", + "integrity": "sha512-TPbeg0b7ylrswdGCji8FRGFAKuqbpQlLbL8SOle3j1iHSs5Ob5mhvMAxWN2UItOjgALAB5Zp3fmMfj8mbWvXKw==", + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.3", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", + "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/simple-plist": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/simple-plist/-/simple-plist-1.3.1.tgz", + "integrity": "sha512-iMSw5i0XseMnrhtIzRb7XpQEXepa9xhWxGUojHBL43SIpQuDQkh3Wpy67ZbDzZVr6EKxvwVChnVpdl8hEVLDiw==", + "dependencies": { + "bplist-creator": "0.1.0", + "bplist-parser": "0.3.1", + "plist": "^3.0.5" + } + }, + "node_modules/simple-swizzle": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.4.tgz", + "integrity": "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==", + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/slugify": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.6.tgz", + "integrity": "sha512-h+z7HKHYXj6wJU+AnS/+IH8Uh9fdcX1Lrhg1/VMdf9PwoBQXFcXiAdsy2tSK0P6gKwJLXp02r90ahUCqHk9rrw==", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split-on-first": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", + "integrity": "sha512-43ZssAJaMusuKWL8sKUBQXHWOpq8d6CfN/u1p4gUzfJkM05C8rxTmYrkIPTXapZpORA6LkkzcUulJ8FqA7Uudw==", + "engines": { + "node": ">=6" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/stackframe": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.3.4.tgz", + "integrity": "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw==" + }, + "node_modules/stacktrace-parser": { + "version": "0.1.11", + "resolved": "https://registry.npmjs.org/stacktrace-parser/-/stacktrace-parser-0.1.11.tgz", + "integrity": "sha512-WjlahMgHmCJpqzU8bIBy4qtsZdU9lRlcZE3Lvyej6t4tuOuv1vk57OW3MBrj6hXBFx/nNoC9MPMTcr5YA7NQbg==", + "dependencies": { + "type-fest": "^0.7.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-buffers": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/stream-buffers/-/stream-buffers-2.2.0.tgz", + "integrity": "sha512-uyQK/mx5QjHun80FLJTfaWE7JtwfRMKBLkMne6udYOmvH0CawotVa7TfgYHzAnpphn4+TweIx1QKMnRIbipmUg==", + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/strict-uri-encode": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", + "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/structured-headers": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/structured-headers/-/structured-headers-0.4.1.tgz", + "integrity": "sha512-0MP/Cxx5SzeeZ10p/bZI0S6MpgD+yxAhi1BOQ34jgnMXsCq3j1t6tQnZu+KdlL7dvJTLT3g9xN8tl10TqgFMcg==" + }, + "node_modules/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/sucrase/node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "engines": { + "node": ">= 6" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tar": { + "version": "7.5.9", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.9.tgz", + "integrity": "sha512-BTLcK0xsDh2+PUe9F6c2TlRp4zOOBMTkoQHQIWSIzI0R7KG46uEwq4OPk2W7bZcprBMsuaeFsqwYr7pjh6CuHg==", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "engines": { + "node": ">=18" + } + }, + "node_modules/temp-dir": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-2.0.0.tgz", + "integrity": "sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.3.tgz", + "integrity": "sha512-M2GCs7Vk83NxkUyQV1bkABc4yxgz9kILhHImZiBPAZ9ybuvCb0/H7lEl5XvIg3g+9d4eNotkZA5IWwYl0tibaA==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/throat": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", + "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==" + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==" + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.7.1.tgz", + "integrity": "sha512-Ne2YiiGN8bmrmJJEuTWTLJR32nh/JdL1+PSicowtNb0WFpn59GK8/lfD61bVtzguz7b3PBt74nxpv/Pw5po5Rg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici": { + "version": "6.23.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.23.0.tgz", + "integrity": "sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", + "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", + "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", + "dependencies": { + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/use-latest-callback": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/use-latest-callback/-/use-latest-callback-0.2.6.tgz", + "integrity": "sha512-FvRG9i1HSo0wagmX63Vrm8SnlUU3LMM3WyZkQ76RnslpBrX694AdG4A0zQBx2B3ZifFA0yv/BaEHGBnEax5rZg==", + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/use-sync-external-store": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-7.0.3.tgz", + "integrity": "sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg==", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.1.tgz", + "integrity": "sha512-OljLrQ9SQdOUqTaQxqL5dEfZWrXExyyWsozYlAWFawPVNuD83igl7uJD2RTkNMbniIYgt8l81eCJGIdQF7avLQ==", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vlq": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/vlq/-/vlq-1.0.1.tgz", + "integrity": "sha512-gQpnTgkubC6hQgdIcRdYGDSDc+SaujOdyesZQMv6JlfQee/9Mp0Qhnys6WxDWvQnL5WZdT7o2Ul187aSt0Rq+w==" + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/warn-once": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/warn-once/-/warn-once-0.1.1.tgz", + "integrity": "sha512-VkQZJbO8zVImzYFteBXvBOZEl1qL175WH8VmZcxF2fZAoudNhNDvHi+doCaAEdU2l2vtcIwa2zn0QK5+I1HQ3Q==" + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webidl-conversions": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", + "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" + }, + "node_modules/whatwg-url-without-unicode": { + "version": "8.0.0-3", + "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", + "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", + "dependencies": { + "buffer": "^5.4.3", + "punycode": "^2.1.1", + "webidl-conversions": "^5.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wonka": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/wonka/-/wonka-6.3.5.tgz", + "integrity": "sha512-SSil+ecw6B4/Dm7Pf2sAshKQ5hWFvfyGlfPbEd6A14dOH6VDjrmbY86u6nZvy9omGwwIPFR8V41+of1EezgoUw==" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xcode": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/xcode/-/xcode-3.0.1.tgz", + "integrity": "sha512-kCz5k7J7XbJtjABOvkc5lJmkiDh8VhjVCGNiqdKCscmVpdVUpEAyXv1xmCLkQJ5dsHqx3IPO4XW+NTDhU/fatA==", + "dependencies": { + "simple-plist": "^1.1.0", + "uuid": "^7.0.3" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/xml2js": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.0.tgz", + "integrity": "sha512-eLTh0kA8uHceqesPqSE+VvO1CDDJWMwlQfB6LuN6T8w6MaDJ8Txm8P7s5cHD0miF0V+GGTZrDQfxPZQVsur33w==", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xml2js/node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/xmlbuilder": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", + "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", + "engines": { + "node": ">=8.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", + "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zustand": { + "version": "5.0.11", + "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.11.tgz", + "integrity": "sha512-fdZY+dk7zn/vbWNCYmzZULHRrss0jx5pPFiOuMZ/5HJN6Yv3u+1Wswy/4MpZEkEGhtNH+pwxZB8OKgUBPzYAGg==", + "engines": { + "node": ">=12.20.0" + }, + "peerDependencies": { + "@types/react": ">=18.0.0", + "immer": ">=9.0.6", + "react": ">=18.0.0", + "use-sync-external-store": ">=1.2.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "immer": { + "optional": true + }, + "react": { + "optional": true + }, + "use-sync-external-store": { + "optional": true + } + } + } + } +} diff --git a/mobile/package.json b/mobile/package.json new file mode 100644 index 0000000..ac578cc --- /dev/null +++ b/mobile/package.json @@ -0,0 +1,34 @@ +{ + "name": "mobile", + "version": "1.0.0", + "main": "index.ts", + "scripts": { + "start": "expo start", + "android": "expo start --android", + "ios": "expo start --ios", + "web": "expo start --web" + }, + "dependencies": { + "@hookform/resolvers": "^5.2.2", + "@react-navigation/bottom-tabs": "^7.14.0", + "@react-navigation/native": "^7.1.28", + "@react-navigation/native-stack": "^7.13.0", + "@tanstack/react-query": "^5.90.21", + "axios": "^1.13.5", + "expo": "~54.0.33", + "expo-secure-store": "^15.0.8", + "expo-status-bar": "~3.0.9", + "react": "19.1.0", + "react-hook-form": "^7.71.2", + "react-native": "0.81.5", + "react-native-safe-area-context": "^5.7.0", + "react-native-screens": "4.16.0", + "zod": "^4.3.6", + "zustand": "^5.0.11" + }, + "devDependencies": { + "@types/react": "~19.1.0", + "typescript": "~5.9.2" + }, + "private": true +} diff --git a/mobile/src/api/auth.ts b/mobile/src/api/auth.ts new file mode 100644 index 0000000..39ba1f7 --- /dev/null +++ b/mobile/src/api/auth.ts @@ -0,0 +1,33 @@ +import { apiClient } from './client'; +import type { TokenPair, User } from '../types'; + +export const authApi = { + register: (data: { + email: string; + password: string; + full_name: string; + phone?: string; + requested_role: 'member' | 'organizer'; + organization_name?: string; + instagram_handle?: string; + }) => + apiClient + .post<{ user: User; access_token?: string; refresh_token?: string }>('/auth/register', data) + .then((r) => r.data), + + login: (data: { email: string; password: string }) => + apiClient.post('/auth/login', data).then((r) => r.data), + + refresh: (refresh_token: string) => + apiClient + .post<{ access_token: string; refresh_token: string }>('/auth/refresh', { refresh_token }) + .then((r) => r.data), + + logout: (refresh_token: string) => + apiClient.post('/auth/logout', { refresh_token }), + + me: () => apiClient.get('/auth/me').then((r) => r.data), + + updateMe: (data: { full_name?: string; phone?: string; expo_push_token?: string }) => + apiClient.patch('/auth/me', data).then((r) => r.data), +}; diff --git a/mobile/src/api/championships.ts b/mobile/src/api/championships.ts new file mode 100644 index 0000000..d225e32 --- /dev/null +++ b/mobile/src/api/championships.ts @@ -0,0 +1,25 @@ +import { apiClient } from './client'; +import type { Championship, Registration } from '../types'; + +export const championshipsApi = { + list: (status?: string) => + apiClient.get('/championships', { params: status ? { status } : {} }).then((r) => r.data), + + get: (id: string) => + apiClient.get(`/championships/${id}`).then((r) => r.data), + + register: (data: { championship_id: string; category?: string; level?: string; notes?: string }) => + apiClient.post('/registrations', data).then((r) => r.data), + + myRegistrations: () => + apiClient.get('/registrations/my').then((r) => r.data), + + getRegistration: (id: string) => + apiClient.get(`/registrations/${id}`).then((r) => r.data), + + updateRegistration: (id: string, data: { video_url?: string; notes?: string }) => + apiClient.patch(`/registrations/${id}`, data).then((r) => r.data), + + cancelRegistration: (id: string) => + apiClient.delete(`/registrations/${id}`), +}; diff --git a/mobile/src/api/client.ts b/mobile/src/api/client.ts new file mode 100644 index 0000000..c534499 --- /dev/null +++ b/mobile/src/api/client.ts @@ -0,0 +1,73 @@ +import axios from 'axios'; +import { tokenStorage } from '../utils/tokenStorage'; + +// Replace with your machine's LAN IP when testing on a physical device +export const BASE_URL = 'http://192.168.2.56:8000/api/v1'; + +export const apiClient = axios.create({ + baseURL: BASE_URL, + timeout: 10000, +}); + +// Attach access token from in-memory cache (synchronous — no await needed) +apiClient.interceptors.request.use((config) => { + const token = tokenStorage.getAccessTokenSync(); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } + return config; +}); + +// Refresh token on 401 +let isRefreshing = false; +let queue: Array<{ resolve: (token: string) => void; reject: (err: unknown) => void }> = []; + +function processQueue(error: unknown, token: string | null = null) { + queue.forEach((p) => (error ? p.reject(error) : p.resolve(token!))); + queue = []; +} + +apiClient.interceptors.response.use( + (res) => res, + async (error) => { + const original = error.config; + if (error.response?.status === 401 && !original._retry) { + if (isRefreshing) { + return new Promise((resolve, reject) => { + queue.push({ + resolve: (token) => { + original.headers.Authorization = `Bearer ${token}`; + resolve(apiClient(original)); + }, + reject, + }); + }); + } + + original._retry = true; + isRefreshing = true; + + try { + const refreshToken = tokenStorage.getRefreshTokenSync(); + if (!refreshToken) throw new Error('No refresh token'); + + const { data } = await axios.post(`${BASE_URL}/auth/refresh`, { + refresh_token: refreshToken, + }); + + await tokenStorage.saveTokens(data.access_token, data.refresh_token); + apiClient.defaults.headers.common.Authorization = `Bearer ${data.access_token}`; + processQueue(null, data.access_token); + original.headers.Authorization = `Bearer ${data.access_token}`; + return apiClient(original); + } catch (err) { + processQueue(err, null); + await tokenStorage.clearTokens(); + return Promise.reject(err); + } finally { + isRefreshing = false; + } + } + return Promise.reject(error); + } +); diff --git a/mobile/src/api/users.ts b/mobile/src/api/users.ts new file mode 100644 index 0000000..c23c86e --- /dev/null +++ b/mobile/src/api/users.ts @@ -0,0 +1,12 @@ +import { apiClient } from './client'; +import type { User } from '../types'; + +export const usersApi = { + list: () => apiClient.get('/users').then((r) => r.data), + + approve: (id: string) => + apiClient.patch(`/users/${id}/approve`).then((r) => r.data), + + reject: (id: string) => + apiClient.patch(`/users/${id}/reject`).then((r) => r.data), +}; diff --git a/mobile/src/navigation/index.tsx b/mobile/src/navigation/index.tsx new file mode 100644 index 0000000..de71bdc --- /dev/null +++ b/mobile/src/navigation/index.tsx @@ -0,0 +1,114 @@ +import { NavigationContainer } from '@react-navigation/native'; +import { createNativeStackNavigator } from '@react-navigation/native-stack'; +import { createBottomTabNavigator } from '@react-navigation/bottom-tabs'; +import { ActivityIndicator, View } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; + +import { useAuthStore } from '../store/auth.store'; + +// Screens +import LoginScreen from '../screens/auth/LoginScreen'; +import RegisterScreen from '../screens/auth/RegisterScreen'; +import PendingApprovalScreen from '../screens/auth/PendingApprovalScreen'; +import ChampionshipsScreen from '../screens/championships/ChampionshipsScreen'; +import ChampionshipDetailScreen from '../screens/championships/ChampionshipDetailScreen'; +import MyRegistrationsScreen from '../screens/championships/MyRegistrationsScreen'; +import ProfileScreen from '../screens/profile/ProfileScreen'; +import AdminScreen from '../screens/admin/AdminScreen'; + +export type AuthStackParams = { + Login: undefined; + Register: undefined; + PendingApproval: undefined; +}; + +export type AppStackParams = { + Tabs: undefined; + ChampionshipDetail: { id: string }; +}; + +export type TabParams = { + Championships: undefined; + MyRegistrations: undefined; + Admin: undefined; + Profile: undefined; +}; + +const AuthStack = createNativeStackNavigator(); +const AppStack = createNativeStackNavigator(); +const Tab = createBottomTabNavigator(); + +function AppTabs({ isAdmin }: { isAdmin: boolean }) { + return ( + ({ + headerShown: true, + headerTitleStyle: { fontWeight: '700', fontSize: 18, color: '#1a1a2e' }, + headerShadowVisible: false, + headerStyle: { backgroundColor: '#fff' }, + tabBarActiveTintColor: '#7c3aed', + tabBarInactiveTintColor: '#9ca3af', + tabBarIcon: ({ focused, color, size }) => { + if (route.name === 'Championships') { + return ; + } + if (route.name === 'MyRegistrations') { + return ; + } + if (route.name === 'Admin') { + return ; + } + if (route.name === 'Profile') { + return ; + } + }, + })} + > + + + {isAdmin && } + + + ); +} + +function AppNavigator({ isAdmin }: { isAdmin: boolean }) { + return ( + + + {() => } + + + + ); +} + +function AuthNavigator() { + return ( + + + + + + ); +} + +export default function RootNavigator() { + const { user, isInitialized } = useAuthStore(); + + if (!isInitialized) { + return ( + + + + ); + } + + return ( + + {user?.status === 'approved' + ? + : } + + ); +} diff --git a/mobile/src/screens/admin/AdminScreen.tsx b/mobile/src/screens/admin/AdminScreen.tsx new file mode 100644 index 0000000..4ef56d7 --- /dev/null +++ b/mobile/src/screens/admin/AdminScreen.tsx @@ -0,0 +1,290 @@ +import { useEffect, useState, useCallback } from 'react'; +import { + View, + Text, + FlatList, + TouchableOpacity, + StyleSheet, + Alert, + ActivityIndicator, + RefreshControl, +} from 'react-native'; +import { usersApi } from '../../api/users'; +import type { User } from '../../types'; + +const STATUS_COLOR: Record = { + pending: '#f59e0b', + approved: '#16a34a', + rejected: '#dc2626', +}; + +const ROLE_LABEL: Record = { + member: 'Member', + organizer: 'Organizer', + admin: 'Admin', +}; + +function UserCard({ + user, + onApprove, + onReject, + acting, +}: { + user: User; + onApprove: () => void; + onReject: () => void; + acting: boolean; +}) { + return ( + + + + {user.full_name.charAt(0).toUpperCase()} + + + {user.full_name} + {user.email} + {user.organization_name && ( + {user.organization_name} + )} + {user.phone && {user.phone}} + {user.instagram_handle && ( + {user.instagram_handle} + )} + + + + + + Role: {ROLE_LABEL[user.role] ?? user.role} + + Registered: {new Date(user.created_at).toLocaleDateString()} + + + + {user.status === 'pending' && ( + + + {acting ? ( + + ) : ( + Approve + )} + + + Reject + + + )} + + {user.status !== 'pending' && ( + + + {user.status === 'approved' ? '✓ Approved' : '✗ Rejected'} + + + )} + + ); +} + +export default function AdminScreen() { + const [users, setUsers] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [actingId, setActingId] = useState(null); + const [filter, setFilter] = useState<'pending' | 'all'>('pending'); + + const load = useCallback(async (silent = false) => { + if (!silent) setLoading(true); + try { + const data = await usersApi.list(); + setUsers(data); + } catch { + Alert.alert('Error', 'Failed to load users'); + } finally { + setLoading(false); + setRefreshing(false); + } + }, []); + + useEffect(() => { load(); }, [load]); + + const handleApprove = (user: User) => { + Alert.alert('Approve', `Approve "${user.full_name}" (${user.organization_name ?? user.email})?`, [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Approve', + onPress: async () => { + setActingId(user.id); + try { + const updated = await usersApi.approve(user.id); + setUsers((prev) => prev.map((u) => (u.id === updated.id ? updated : u))); + } catch { + Alert.alert('Error', 'Failed to approve user'); + } finally { + setActingId(null); + } + }, + }, + ]); + }; + + const handleReject = (user: User) => { + Alert.alert('Reject', `Reject "${user.full_name}"? They will not be able to sign in.`, [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Reject', + style: 'destructive', + onPress: async () => { + setActingId(user.id); + try { + const updated = await usersApi.reject(user.id); + setUsers((prev) => prev.map((u) => (u.id === updated.id ? updated : u))); + } catch { + Alert.alert('Error', 'Failed to reject user'); + } finally { + setActingId(null); + } + }, + }, + ]); + }; + + const displayed = filter === 'pending' + ? users.filter((u) => u.status === 'pending') + : users; + + const pendingCount = users.filter((u) => u.status === 'pending').length; + + if (loading) { + return ( + + + + ); + } + + return ( + item.id} + contentContainerStyle={styles.list} + refreshControl={ + { setRefreshing(true); load(true); }} /> + } + ListHeaderComponent={ + + + setFilter('pending')} + > + + Pending {pendingCount > 0 ? `(${pendingCount})` : ''} + + + setFilter('all')} + > + + All Users + + + + + } + ListEmptyComponent={ + + + {filter === 'pending' ? 'No pending approvals' : 'No users found'} + + + } + renderItem={({ item }) => ( + handleApprove(item)} + onReject={() => handleReject(item)} + acting={actingId === item.id} + /> + )} + /> + ); +} + +const styles = StyleSheet.create({ + list: { padding: 16, flexGrow: 1 }, + heading: { fontSize: 24, fontWeight: '700', color: '#1a1a2e', marginBottom: 16 }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 60 }, + empty: { color: '#9ca3af', fontSize: 15 }, + + filterRow: { flexDirection: 'row', gap: 8, marginBottom: 16 }, + filterBtn: { + paddingHorizontal: 16, + paddingVertical: 8, + borderRadius: 20, + borderWidth: 1.5, + borderColor: '#e5e7eb', + }, + filterBtnActive: { borderColor: '#7c3aed', backgroundColor: '#f3f0ff' }, + filterText: { fontSize: 13, fontWeight: '600', color: '#9ca3af' }, + filterTextActive: { color: '#7c3aed' }, + + card: { + backgroundColor: '#fff', + borderRadius: 14, + marginBottom: 12, + padding: 16, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.07, + shadowRadius: 6, + elevation: 3, + }, + cardHeader: { flexDirection: 'row', alignItems: 'flex-start' }, + avatar: { + width: 44, + height: 44, + borderRadius: 22, + backgroundColor: '#7c3aed', + justifyContent: 'center', + alignItems: 'center', + marginRight: 12, + }, + avatarText: { color: '#fff', fontSize: 18, fontWeight: '700' }, + cardInfo: { flex: 1 }, + cardName: { fontSize: 15, fontWeight: '700', color: '#1a1a2e' }, + cardEmail: { fontSize: 13, color: '#6b7280', marginTop: 1 }, + cardOrg: { fontSize: 13, color: '#7c3aed', fontWeight: '600', marginTop: 3 }, + cardDetail: { fontSize: 12, color: '#9ca3af', marginTop: 1 }, + statusDot: { width: 10, height: 10, borderRadius: 5, marginTop: 4 }, + + meta: { flexDirection: 'row', justifyContent: 'space-between', marginTop: 12, marginBottom: 12 }, + metaText: { fontSize: 12, color: '#9ca3af' }, + + actions: { flexDirection: 'row', gap: 8 }, + btn: { + flex: 1, + paddingVertical: 10, + borderRadius: 8, + alignItems: 'center', + }, + btnDisabled: { opacity: 0.5 }, + approveBtn: { backgroundColor: '#16a34a' }, + rejectBtn: { backgroundColor: '#fff', borderWidth: 1.5, borderColor: '#ef4444' }, + btnText: { fontSize: 14, fontWeight: '600', color: '#fff' }, + rejectText: { color: '#ef4444' }, + + resolvedBanner: { alignItems: 'center', paddingTop: 4 }, + resolvedText: { fontSize: 13, fontWeight: '600' }, +}); diff --git a/mobile/src/screens/auth/LoginScreen.tsx b/mobile/src/screens/auth/LoginScreen.tsx new file mode 100644 index 0000000..f9ec10c --- /dev/null +++ b/mobile/src/screens/auth/LoginScreen.tsx @@ -0,0 +1,128 @@ +import { useRef, useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + KeyboardAvoidingView, + Platform, + Alert, + ActivityIndicator, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Ionicons } from '@expo/vector-icons'; +import { useAuthStore } from '../../store/auth.store'; +import type { AuthStackParams } from '../../navigation'; + +type Props = NativeStackScreenProps; + +export default function LoginScreen({ navigation }: Props) { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const passwordRef = useRef(null); + const { login, isLoading } = useAuthStore(); + + const handleLogin = async () => { + if (!email.trim() || !password.trim()) { + Alert.alert('Error', 'Please enter email and password'); + return; + } + try { + await login(email.trim().toLowerCase(), password); + } catch (err: any) { + const msg = err?.response?.data?.detail ?? 'Login failed. Check your credentials.'; + Alert.alert('Login failed', msg); + } + }; + + return ( + + + Pole Championships + Sign in to your account + + passwordRef.current?.focus()} + value={email} + onChangeText={setEmail} + /> + + + setShowPassword((v) => !v)}> + + + + + + {isLoading ? : Sign In} + + + navigation.navigate('Register')}> + Don't have an account? Register + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fff' }, + inner: { flex: 1, justifyContent: 'center', padding: 24 }, + title: { fontSize: 28, fontWeight: '700', textAlign: 'center', marginBottom: 8, color: '#1a1a2e' }, + subtitle: { fontSize: 15, textAlign: 'center', color: '#666', marginBottom: 32 }, + input: { + borderWidth: 1, + borderColor: '#ddd', + borderRadius: 10, + padding: 14, + marginBottom: 14, + fontSize: 16, + backgroundColor: '#fafafa', + }, + passwordRow: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderColor: '#ddd', + borderRadius: 10, + backgroundColor: '#fafafa', + marginBottom: 14, + }, + passwordInput: { + flex: 1, + padding: 14, + fontSize: 16, + }, + eyeBtn: { + paddingHorizontal: 14, + paddingVertical: 14, + }, + btn: { + backgroundColor: '#7c3aed', + padding: 16, + borderRadius: 10, + alignItems: 'center', + marginTop: 8, + marginBottom: 20, + }, + btnText: { color: '#fff', fontSize: 16, fontWeight: '600' }, + link: { textAlign: 'center', color: '#7c3aed', fontSize: 14 }, +}); diff --git a/mobile/src/screens/auth/PendingApprovalScreen.tsx b/mobile/src/screens/auth/PendingApprovalScreen.tsx new file mode 100644 index 0000000..3b2ee95 --- /dev/null +++ b/mobile/src/screens/auth/PendingApprovalScreen.tsx @@ -0,0 +1,36 @@ +import { View, Text, StyleSheet, TouchableOpacity } from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import type { AuthStackParams } from '../../navigation'; + +type Props = NativeStackScreenProps; + +export default function PendingApprovalScreen({ navigation }: Props) { + return ( + + + Application Submitted + + Your registration has been received. An administrator will review and approve your account shortly. + {'\n\n'} + Once approved, you can sign in with your email and password. + + navigation.navigate('Login')}> + Go to Sign In + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, justifyContent: 'center', alignItems: 'center', padding: 32, backgroundColor: '#fff' }, + icon: { fontSize: 64, marginBottom: 20 }, + title: { fontSize: 24, fontWeight: '700', color: '#1a1a2e', marginBottom: 16, textAlign: 'center' }, + body: { fontSize: 15, color: '#555', lineHeight: 24, textAlign: 'center', marginBottom: 36 }, + btn: { + backgroundColor: '#7c3aed', + paddingVertical: 14, + paddingHorizontal: 40, + borderRadius: 10, + }, + btnText: { color: '#fff', fontSize: 16, fontWeight: '600' }, +}); diff --git a/mobile/src/screens/auth/RegisterScreen.tsx b/mobile/src/screens/auth/RegisterScreen.tsx new file mode 100644 index 0000000..a5d1ff8 --- /dev/null +++ b/mobile/src/screens/auth/RegisterScreen.tsx @@ -0,0 +1,297 @@ +import { useState } from 'react'; +import { + View, + Text, + TextInput, + TouchableOpacity, + StyleSheet, + KeyboardAvoidingView, + Platform, + Alert, + ActivityIndicator, + ScrollView, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { Ionicons } from '@expo/vector-icons'; +import { useAuthStore } from '../../store/auth.store'; +import type { AuthStackParams } from '../../navigation'; + +type Props = NativeStackScreenProps; +type Role = 'member' | 'organizer'; + +export default function RegisterScreen({ navigation }: Props) { + const [role, setRole] = useState('member'); + const [fullName, setFullName] = useState(''); + const [email, setEmail] = useState(''); + const [phone, setPhone] = useState(''); + const [password, setPassword] = useState(''); + const [orgName, setOrgName] = useState(''); + const [instagram, setInstagram] = useState(''); + const [showPassword, setShowPassword] = useState(false); + const { register, isLoading } = useAuthStore(); + + const handleRegister = async () => { + if (!fullName.trim() || !email.trim() || !password.trim()) { + Alert.alert('Error', 'Please fill in all required fields'); + return; + } + if (role === 'organizer' && !orgName.trim()) { + Alert.alert('Error', 'Organization name is required for organizers'); + return; + } + try { + const autoLoggedIn = await register({ + email: email.trim().toLowerCase(), + password, + full_name: fullName.trim(), + phone: phone.trim() || undefined, + requested_role: role, + organization_name: role === 'organizer' ? orgName.trim() : undefined, + instagram_handle: role === 'organizer' && instagram.trim() ? instagram.trim() : undefined, + }); + if (!autoLoggedIn) { + // Organizer — navigate to pending screen + navigation.navigate('PendingApproval'); + } + // Member — autoLoggedIn=true means the store already has user set, + // RootNavigator will switch to AppStack automatically + } catch (err: any) { + const detail = err?.response?.data?.detail; + const msg = Array.isArray(detail) + ? detail.map((d: any) => d.msg).join('\n') + : detail ?? 'Registration failed'; + Alert.alert('Registration failed', msg); + } + }; + + return ( + + + Create Account + Who are you registering as? + + {/* Role selector — large cards */} + + setRole('member')} + activeOpacity={0.8} + > + 🏅 + Member + + Compete in championships + + {role === 'member' && } + + + setRole('organizer')} + activeOpacity={0.8} + > + 🏆 + Organizer + + Create & manage events + + {role === 'organizer' && } + + + + {/* Info banner — organizer only */} + {role === 'organizer' && ( + + + ⏳ Organizer accounts require admin approval before you can sign in. + + + )} + + {/* Common fields */} + {role === 'organizer' ? 'Contact Person *' : 'Full Name *'} + + + Email * + + + {role === 'organizer' ? 'Contact Phone' : 'Phone'} + + + Password * + + + setShowPassword((v) => !v)}> + + + + + {/* Organizer-only fields */} + {role === 'organizer' && ( + <> + + Organization Details + + + Organization Name * + + + Instagram Handle + setInstagram(v.startsWith('@') ? v : v ? `@${v}` : '')} + /> + + )} + + + {isLoading ? ( + + ) : ( + + {role === 'member' ? 'Register & Sign In' : 'Submit Application'} + + )} + + + navigation.goBack()}> + Already have an account? Sign In + + + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fff' }, + inner: { flexGrow: 1, padding: 24, paddingTop: 48 }, + title: { fontSize: 26, fontWeight: '700', color: '#1a1a2e', marginBottom: 4, textAlign: 'center' }, + subtitle: { fontSize: 14, color: '#6b7280', textAlign: 'center', marginBottom: 20 }, + + // Role cards + roleRow: { flexDirection: 'row', gap: 12, marginBottom: 16 }, + roleCard: { + flex: 1, + padding: 16, + borderRadius: 14, + borderWidth: 2, + borderColor: '#e5e7eb', + alignItems: 'center', + backgroundColor: '#f9fafb', + position: 'relative', + }, + roleCardActive: { borderColor: '#7c3aed', backgroundColor: '#f3f0ff' }, + roleEmoji: { fontSize: 28, marginBottom: 8 }, + roleTitle: { fontSize: 16, fontWeight: '700', color: '#9ca3af', marginBottom: 4 }, + roleTitleActive: { color: '#7c3aed' }, + roleDesc: { fontSize: 12, color: '#d1d5db', textAlign: 'center', lineHeight: 16 }, + roleDescActive: { color: '#a78bfa' }, + roleCheck: { + position: 'absolute', + top: 8, + right: 8, + width: 20, + height: 20, + borderRadius: 10, + backgroundColor: '#7c3aed', + justifyContent: 'center', + alignItems: 'center', + }, + roleCheckText: { color: '#fff', fontSize: 11, fontWeight: '700' }, + + // Info banner + infoBanner: { borderRadius: 10, padding: 12, marginBottom: 20 }, + infoBannerAmber: { backgroundColor: '#fef3c7' }, + infoText: { fontSize: 13, lineHeight: 19 }, + infoTextAmber: { color: '#92400e' }, + + // Form + label: { fontSize: 13, fontWeight: '600', color: '#374151', marginBottom: 5 }, + input: { + borderWidth: 1, + borderColor: '#e5e7eb', + borderRadius: 10, + padding: 13, + marginBottom: 14, + fontSize: 15, + backgroundColor: '#fafafa', + }, + passwordRow: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderColor: '#e5e7eb', + borderRadius: 10, + backgroundColor: '#fafafa', + marginBottom: 14, + }, + passwordInput: { + flex: 1, + padding: 13, + fontSize: 15, + }, + eyeBtn: { + paddingHorizontal: 13, + paddingVertical: 13, + }, + divider: { + flexDirection: 'row', + alignItems: 'center', + marginVertical: 16, + }, + dividerLabel: { + fontSize: 13, + fontWeight: '700', + color: '#7c3aed', + backgroundColor: '#fff', + paddingRight: 8, + }, + + btn: { + backgroundColor: '#7c3aed', + padding: 16, + borderRadius: 12, + alignItems: 'center', + marginBottom: 16, + marginTop: 4, + }, + btnText: { color: '#fff', fontSize: 16, fontWeight: '600' }, + link: { textAlign: 'center', color: '#7c3aed', fontSize: 14 }, +}); diff --git a/mobile/src/screens/championships/ChampionshipDetailScreen.tsx b/mobile/src/screens/championships/ChampionshipDetailScreen.tsx new file mode 100644 index 0000000..e2aaf37 --- /dev/null +++ b/mobile/src/screens/championships/ChampionshipDetailScreen.tsx @@ -0,0 +1,246 @@ +import { useEffect, useState } from 'react'; +import { + View, + Text, + ScrollView, + StyleSheet, + TouchableOpacity, + Alert, + ActivityIndicator, + Image, + Linking, +} from 'react-native'; +import { NativeStackScreenProps } from '@react-navigation/native-stack'; +import { championshipsApi } from '../../api/championships'; +import type { Championship, Registration } from '../../types'; +import type { AppStackParams } from '../../navigation'; + +type Props = NativeStackScreenProps; + +export default function ChampionshipDetailScreen({ route }: Props) { + const { id } = route.params; + const [champ, setChamp] = useState(null); + const [myReg, setMyReg] = useState(null); + const [loading, setLoading] = useState(true); + const [registering, setRegistering] = useState(false); + + useEffect(() => { + const load = async () => { + try { + const detail = await championshipsApi.get(id); + setChamp(detail); + try { + const regs = await championshipsApi.myRegistrations(); + setMyReg(regs.find((r) => r.championship_id === id) ?? null); + } catch { + // myRegistrations failing shouldn't hide the championship + } + } finally { + setLoading(false); + } + }; + load(); + }, [id]); + + const handleRegister = async () => { + if (!champ) return; + Alert.alert('Register', `Register for "${champ.title}"?`, [ + { text: 'Cancel', style: 'cancel' }, + { + text: 'Register', + onPress: async () => { + setRegistering(true); + try { + const reg = await championshipsApi.register({ championship_id: id }); + setMyReg(reg); + Alert.alert('Success', 'You are registered! Complete the next steps on the registration form.'); + } catch (err: any) { + Alert.alert('Error', err?.response?.data?.detail ?? 'Registration failed'); + } finally { + setRegistering(false); + } + }, + }, + ]); + }; + + if (loading) { + return ( + + + + ); + } + + if (!champ) { + return ( + + Championship not found + + ); + } + + const steps = [ + { key: 'submitted', label: 'Application submitted' }, + { key: 'form_submitted', label: 'Registration form submitted' }, + { key: 'payment_pending', label: 'Payment pending' }, + { key: 'payment_confirmed', label: 'Payment confirmed' }, + { key: 'video_submitted', label: 'Video submitted' }, + { key: 'accepted', label: 'Accepted' }, + ]; + + const currentStepIndex = myReg + ? steps.findIndex((s) => s.key === myReg.status) + : -1; + + return ( + + {champ.image_url && ( + + )} + + {champ.title} + + {champ.location && 📍 {champ.location}} + {champ.event_date && ( + + 📅 {new Date(champ.event_date).toLocaleDateString('en-GB', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' })} + + )} + {champ.entry_fee != null && 💰 Entry fee: {champ.entry_fee} BYN} + {champ.video_max_duration != null && 🎥 Max video duration: {champ.video_max_duration}s} + + {champ.description && {champ.description}} + + {/* Categories */} + {champ.categories && champ.categories.length > 0 && ( + + Categories + + {champ.categories.map((cat) => ( + + {cat} + + ))} + + + )} + + {/* Judges */} + {champ.judges && champ.judges.length > 0 && ( + + Judges + {champ.judges.map((j) => ( + + {j.name} + {j.bio && {j.bio}} + {j.instagram && {j.instagram}} + + ))} + + )} + + {/* Registration form link */} + {champ.form_url && ( + Linking.openURL(champ.form_url!)}> + Open Registration Form ↗ + + )} + + {/* My registration progress */} + {myReg && ( + + My Registration Progress + {steps.map((step, i) => { + const done = i <= currentStepIndex; + const isRejected = myReg.status === 'rejected' || myReg.status === 'waitlisted'; + return ( + + + + {step.label} + + + ); + })} + {(myReg.status === 'rejected' || myReg.status === 'waitlisted') && ( + + Status: {myReg.status === 'rejected' ? '❌ Rejected' : '⏳ Waitlisted'} + + )} + + )} + + {/* Register button / status */} + {!myReg && ( + champ.status === 'open' ? ( + + {registering ? ( + + ) : ( + Register for Championship + )} + + ) : ( + + + {champ.status === 'draft' && '⏳ Registration is not open yet'} + {champ.status === 'closed' && '🔒 Registration is closed'} + {champ.status === 'completed' && '✅ This championship has ended'} + + + ) + )} + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fff' }, + content: { paddingBottom: 40 }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + image: { width: '100%', height: 220 }, + title: { fontSize: 22, fontWeight: '700', color: '#1a1a2e', margin: 16, marginBottom: 8 }, + meta: { fontSize: 14, color: '#555', marginHorizontal: 16, marginBottom: 4 }, + description: { fontSize: 14, color: '#444', lineHeight: 22, margin: 16, marginTop: 12 }, + section: { marginHorizontal: 16, marginTop: 20 }, + sectionTitle: { fontSize: 17, fontWeight: '600', color: '#1a1a2e', marginBottom: 12 }, + tags: { flexDirection: 'row', flexWrap: 'wrap', gap: 8 }, + tag: { backgroundColor: '#f3f0ff', paddingHorizontal: 12, paddingVertical: 6, borderRadius: 8 }, + tagText: { color: '#7c3aed', fontSize: 13, fontWeight: '500' }, + judgeRow: { marginBottom: 12, padding: 12, backgroundColor: '#f9fafb', borderRadius: 10 }, + judgeName: { fontSize: 15, fontWeight: '600', color: '#1a1a2e' }, + judgeBio: { fontSize: 13, color: '#555', marginTop: 2 }, + judgeInsta: { fontSize: 13, color: '#7c3aed', marginTop: 2 }, + formBtn: { + margin: 16, + padding: 14, + borderWidth: 2, + borderColor: '#7c3aed', + borderRadius: 10, + alignItems: 'center', + }, + formBtnText: { color: '#7c3aed', fontSize: 15, fontWeight: '600' }, + step: { flexDirection: 'row', alignItems: 'center', marginBottom: 10 }, + stepDot: { width: 14, height: 14, borderRadius: 7, backgroundColor: '#ddd', marginRight: 10 }, + stepDotDone: { backgroundColor: '#16a34a' }, + stepLabel: { fontSize: 14, color: '#9ca3af' }, + stepLabelDone: { color: '#1a1a2e' }, + rejectedText: { fontSize: 14, color: '#dc2626', marginTop: 8, fontWeight: '600' }, + registerBtn: { + margin: 16, + backgroundColor: '#7c3aed', + padding: 16, + borderRadius: 10, + alignItems: 'center', + }, + registerBtnText: { color: '#fff', fontSize: 16, fontWeight: '600' }, + closedBanner: { + margin: 16, + padding: 14, + backgroundColor: '#f3f4f6', + borderRadius: 10, + alignItems: 'center', + }, + closedText: { color: '#6b7280', fontSize: 14, fontWeight: '500' }, +}); diff --git a/mobile/src/screens/championships/ChampionshipsScreen.tsx b/mobile/src/screens/championships/ChampionshipsScreen.tsx new file mode 100644 index 0000000..54c5ac3 --- /dev/null +++ b/mobile/src/screens/championships/ChampionshipsScreen.tsx @@ -0,0 +1,135 @@ +import { useEffect, useState } from 'react'; +import { + View, + Text, + FlatList, + TouchableOpacity, + StyleSheet, + RefreshControl, + ActivityIndicator, + Image, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { championshipsApi } from '../../api/championships'; +import type { Championship } from '../../types'; +import type { AppStackParams } from '../../navigation'; + +const STATUS_COLOR: Record = { + open: '#16a34a', + draft: '#9ca3af', + closed: '#dc2626', + completed: '#2563eb', +}; + +function StatusBadge({ status }: { status: string }) { + return ( + + {status.toUpperCase()} + + ); +} + +function ChampionshipCard({ item, onPress }: { item: Championship; onPress: () => void }) { + return ( + + {item.image_url && ( + + )} + + + {item.title} + + + {item.location && 📍 {item.location}} + {item.event_date && ( + + 📅 {new Date(item.event_date).toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' })} + + )} + {item.entry_fee != null && ( + 💰 Entry fee: {item.entry_fee} BYN + )} + + + ); +} + +export default function ChampionshipsScreen() { + const navigation = useNavigation>(); + const [championships, setChampionships] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + const [error, setError] = useState(null); + + const load = async (silent = false) => { + if (!silent) setLoading(true); + setError(null); + try { + const data = await championshipsApi.list(); + setChampionships(data); + } catch { + setError('Failed to load championships'); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { load(); }, []); + + if (loading) { + return ( + + + + ); + } + + return ( + item.id} + contentContainerStyle={styles.list} + ListEmptyComponent={ + + {error ?? 'No championships yet'} + + } + renderItem={({ item }) => ( + navigation.navigate('ChampionshipDetail', { id: item.id })} + /> + )} + refreshControl={ + { setRefreshing(true); load(true); }} /> + } + /> + ); +} + +const styles = StyleSheet.create({ + list: { padding: 16 }, + heading: { fontSize: 24, fontWeight: '700', color: '#1a1a2e', marginBottom: 16 }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 60 }, + empty: { color: '#9ca3af', fontSize: 15 }, + card: { + backgroundColor: '#fff', + borderRadius: 14, + marginBottom: 14, + overflow: 'hidden', + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.08, + shadowRadius: 6, + elevation: 3, + }, + cardImage: { width: '100%', height: 160 }, + cardBody: { padding: 14 }, + cardHeader: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 8 }, + cardTitle: { flex: 1, fontSize: 17, fontWeight: '600', color: '#1a1a2e', marginRight: 8 }, + badge: { paddingHorizontal: 8, paddingVertical: 3, borderRadius: 6 }, + badgeText: { color: '#fff', fontSize: 11, fontWeight: '700' }, + cardMeta: { fontSize: 13, color: '#555', marginTop: 4 }, +}); diff --git a/mobile/src/screens/championships/MyRegistrationsScreen.tsx b/mobile/src/screens/championships/MyRegistrationsScreen.tsx new file mode 100644 index 0000000..4fdc236 --- /dev/null +++ b/mobile/src/screens/championships/MyRegistrationsScreen.tsx @@ -0,0 +1,189 @@ +import { useEffect, useState } from 'react'; +import { + View, + Text, + FlatList, + TouchableOpacity, + StyleSheet, + RefreshControl, + ActivityIndicator, + Alert, +} from 'react-native'; +import { useNavigation } from '@react-navigation/native'; +import { NativeStackNavigationProp } from '@react-navigation/native-stack'; +import { Ionicons } from '@expo/vector-icons'; +import { championshipsApi } from '../../api/championships'; +import type { Registration } from '../../types'; +import type { AppStackParams } from '../../navigation'; + +const STATUS_CONFIG: Record = { + submitted: { color: '#f59e0b', icon: 'time-outline', label: 'Submitted' }, + form_submitted: { color: '#3b82f6', icon: 'document-text-outline', label: 'Form Done' }, + payment_pending: { color: '#f97316', icon: 'card-outline', label: 'Payment Pending' }, + payment_confirmed: { color: '#8b5cf6', icon: 'checkmark-circle-outline', label: 'Paid' }, + video_submitted: { color: '#06b6d4', icon: 'videocam-outline', label: 'Video Sent' }, + accepted: { color: '#16a34a', icon: 'trophy-outline', label: 'Accepted' }, + rejected: { color: '#dc2626', icon: 'close-circle-outline', label: 'Rejected' }, + waitlisted: { color: '#9ca3af', icon: 'hourglass-outline', label: 'Waitlisted' }, +}; + +const STEP_KEYS = ['submitted', 'form_submitted', 'payment_pending', 'payment_confirmed', 'video_submitted', 'accepted']; + +function RegistrationCard({ item, onPress }: { item: Registration; onPress: () => void }) { + const config = STATUS_CONFIG[item.status] ?? { color: '#9ca3af', icon: 'help-outline', label: item.status }; + const stepIndex = STEP_KEYS.indexOf(item.status); + const isFinal = item.status === 'rejected' || item.status === 'waitlisted'; + + return ( + + + + + {item.championship_title ?? 'Championship'} + + {item.championship_location && ( + + {item.championship_location} + + )} + {item.championship_event_date && ( + + {' '} + {new Date(item.championship_event_date).toLocaleDateString('en-GB', { + day: 'numeric', + month: 'long', + year: 'numeric', + })} + + )} + + + + {config.label} + + + + {/* Progress bar */} + + {STEP_KEYS.map((key, i) => { + const done = !isFinal && i <= stepIndex; + return ; + })} + + + + + Registered {new Date(item.submitted_at).toLocaleDateString()} + + + + + ); +} + +export default function MyRegistrationsScreen() { + const navigation = useNavigation>(); + const [registrations, setRegistrations] = useState([]); + const [loading, setLoading] = useState(true); + const [refreshing, setRefreshing] = useState(false); + + const load = async (silent = false) => { + if (!silent) setLoading(true); + try { + const data = await championshipsApi.myRegistrations(); + setRegistrations(data); + } catch { + Alert.alert('Error', 'Failed to load registrations'); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { load(); }, []); + + if (loading) { + return ( + + + + ); + } + + return ( + item.id} + contentContainerStyle={styles.list} + ListEmptyComponent={ + + + No registrations yet + Browse championships and register for events + + } + renderItem={({ item }) => ( + navigation.navigate('ChampionshipDetail', { id: item.championship_id })} + /> + )} + refreshControl={ + { setRefreshing(true); load(true); }} /> + } + /> + ); +} + +const styles = StyleSheet.create({ + list: { padding: 16, flexGrow: 1 }, + heading: { fontSize: 24, fontWeight: '700', color: '#1a1a2e', marginBottom: 16 }, + center: { flex: 1, justifyContent: 'center', alignItems: 'center' }, + emptyContainer: { flex: 1, justifyContent: 'center', alignItems: 'center', paddingTop: 80 }, + empty: { color: '#6b7280', fontSize: 16, fontWeight: '600', marginTop: 12, marginBottom: 4 }, + emptySub: { color: '#9ca3af', fontSize: 13 }, + + card: { + backgroundColor: '#fff', + borderRadius: 14, + padding: 16, + marginBottom: 12, + shadowColor: '#000', + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.07, + shadowRadius: 6, + elevation: 3, + }, + cardTop: { flexDirection: 'row', justifyContent: 'space-between', alignItems: 'flex-start' }, + cardTitleArea: { flex: 1, marginRight: 10 }, + cardTitle: { fontSize: 16, fontWeight: '700', color: '#1a1a2e', marginBottom: 4 }, + cardMeta: { fontSize: 12, color: '#6b7280', marginTop: 2 }, + + statusBadge: { + flexDirection: 'row', + alignItems: 'center', + paddingHorizontal: 8, + paddingVertical: 4, + borderRadius: 8, + gap: 4, + }, + statusText: { fontSize: 11, fontWeight: '700' }, + + progressRow: { flexDirection: 'row', gap: 4, marginTop: 14, marginBottom: 12 }, + progressDot: { + flex: 1, + height: 4, + borderRadius: 2, + backgroundColor: '#e5e7eb', + }, + + cardBottom: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + borderTopWidth: 1, + borderTopColor: '#f3f4f6', + paddingTop: 10, + }, + dateText: { fontSize: 12, color: '#9ca3af' }, +}); diff --git a/mobile/src/screens/profile/ProfileScreen.tsx b/mobile/src/screens/profile/ProfileScreen.tsx new file mode 100644 index 0000000..6c37138 --- /dev/null +++ b/mobile/src/screens/profile/ProfileScreen.tsx @@ -0,0 +1,149 @@ +import { View, Text, StyleSheet, TouchableOpacity, Alert, ScrollView } from 'react-native'; +import { Ionicons } from '@expo/vector-icons'; +import { useAuthStore } from '../../store/auth.store'; + +const ROLE_CONFIG: Record = { + member: { color: '#16a34a', bg: '#f0fdf4', label: 'Member' }, + organizer: { color: '#7c3aed', bg: '#f3f0ff', label: 'Organizer' }, + admin: { color: '#dc2626', bg: '#fef2f2', label: 'Admin' }, +}; + +export default function ProfileScreen() { + const { user, logout } = useAuthStore(); + + const handleLogout = () => { + Alert.alert('Sign Out', 'Are you sure you want to sign out?', [ + { text: 'Cancel', style: 'cancel' }, + { text: 'Sign Out', style: 'destructive', onPress: logout }, + ]); + }; + + if (!user) return null; + + const roleConfig = ROLE_CONFIG[user.role] ?? { color: '#6b7280', bg: '#f3f4f6', label: user.role }; + + return ( + + {/* Avatar + Name */} + + + {user.full_name.charAt(0).toUpperCase()} + + {user.full_name} + {user.email} + + {roleConfig.label} + + + + {/* Info Card */} + + {user.phone && ( + + )} + {user.organization_name && ( + + )} + {user.instagram_handle && ( + + )} + + + + {/* Sign Out */} + + + Sign Out + + + ); +} + +function Row({ + icon, + label, + value, + isLast, +}: { + icon: string; + label: string; + value: string; + isLast?: boolean; +}) { + return ( + + + + {label} + + {value} + + ); +} + +const styles = StyleSheet.create({ + container: { flex: 1, backgroundColor: '#fff' }, + content: { padding: 24, paddingBottom: 40 }, + + header: { alignItems: 'center', marginBottom: 28 }, + avatar: { + width: 80, + height: 80, + borderRadius: 40, + backgroundColor: '#7c3aed', + justifyContent: 'center', + alignItems: 'center', + marginBottom: 14, + }, + avatarText: { color: '#fff', fontSize: 32, fontWeight: '700' }, + name: { fontSize: 22, fontWeight: '700', color: '#1a1a2e', marginBottom: 4 }, + email: { fontSize: 14, color: '#6b7280', marginBottom: 10 }, + roleBadge: { + paddingHorizontal: 14, + paddingVertical: 5, + borderRadius: 20, + }, + roleText: { fontSize: 13, fontWeight: '700' }, + + card: { + backgroundColor: '#f9fafb', + borderRadius: 14, + marginBottom: 28, + overflow: 'hidden', + }, + row: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 14, + paddingHorizontal: 16, + borderBottomWidth: 1, + borderBottomColor: '#f3f4f6', + }, + rowLast: { borderBottomWidth: 0 }, + rowLeft: { flexDirection: 'row', alignItems: 'center', gap: 8 }, + rowLabel: { fontSize: 14, color: '#6b7280' }, + rowValue: { fontSize: 14, color: '#1a1a2e', fontWeight: '500' }, + + logoutBtn: { + flexDirection: 'row', + gap: 8, + justifyContent: 'center', + alignItems: 'center', + borderWidth: 1.5, + borderColor: '#fecaca', + backgroundColor: '#fef2f2', + borderRadius: 12, + padding: 14, + }, + logoutText: { color: '#ef4444', fontSize: 15, fontWeight: '600' }, +}); diff --git a/mobile/src/store/auth.store.ts b/mobile/src/store/auth.store.ts new file mode 100644 index 0000000..557995b --- /dev/null +++ b/mobile/src/store/auth.store.ts @@ -0,0 +1,94 @@ +import { create } from 'zustand'; +import { apiClient } from '../api/client'; +import { authApi } from '../api/auth'; +import { tokenStorage } from '../utils/tokenStorage'; +import type { User } from '../types'; + +interface AuthState { + user: User | null; + isLoading: boolean; + isInitialized: boolean; + login: (email: string, password: string) => Promise; + // Returns true if auto-logged in (member), false if pending approval (organizer) + register: (data: { + email: string; + password: string; + full_name: string; + phone?: string; + requested_role: 'member' | 'organizer'; + organization_name?: string; + instagram_handle?: string; + }) => Promise; + logout: () => Promise; + initialize: () => Promise; +} + +export const useAuthStore = create((set) => ({ + user: null, + isLoading: false, + isInitialized: false, + + initialize: async () => { + try { + await tokenStorage.loadFromStorage(); + const token = tokenStorage.getAccessTokenSync(); + if (token) { + apiClient.defaults.headers.common.Authorization = `Bearer ${token}`; + const user = await authApi.me(); + set({ user, isInitialized: true }); + } else { + set({ isInitialized: true }); + } + } catch { + await tokenStorage.clearTokens(); + set({ user: null, isInitialized: true }); + } + }, + + login: async (email, password) => { + set({ isLoading: true }); + try { + const data = await authApi.login({ email, password }); + await tokenStorage.saveTokens(data.access_token, data.refresh_token); + apiClient.defaults.headers.common.Authorization = `Bearer ${data.access_token}`; + set({ user: data.user, isLoading: false }); + } catch (err) { + set({ isLoading: false }); + throw err; + } + }, + + register: async (data) => { + set({ isLoading: true }); + try { + const res = await authApi.register(data); + if (res.access_token && res.refresh_token) { + // Member: auto-approved — save tokens and log in immediately + await tokenStorage.saveTokens(res.access_token, res.refresh_token); + apiClient.defaults.headers.common.Authorization = `Bearer ${res.access_token}`; + set({ user: res.user, isLoading: false }); + return true; + } + // Organizer: pending admin approval + set({ isLoading: false }); + return false; + } catch (err) { + set({ isLoading: false }); + throw err; + } + }, + + logout: async () => { + const refresh = tokenStorage.getRefreshTokenSync(); + if (refresh) { + try { + await authApi.logout(refresh); + } catch { + // ignore + } + } + await tokenStorage.clearTokens(); + delete apiClient.defaults.headers.common.Authorization; + set({ user: null }); + }, +})); diff --git a/mobile/src/types/index.ts b/mobile/src/types/index.ts new file mode 100644 index 0000000..e320aec --- /dev/null +++ b/mobile/src/types/index.ts @@ -0,0 +1,63 @@ +export interface User { + id: string; + email: string; + full_name: string; + phone: string | null; + role: 'member' | 'organizer' | 'admin'; + status: 'pending' | 'approved' | 'rejected'; + organization_name: string | null; + instagram_handle: string | null; + expo_push_token: string | null; + created_at: string; +} + +export interface TokenPair { + access_token: string; + refresh_token: string; + token_type: string; + user: User; +} + +export interface Championship { + id: string; + title: string; + description: string | null; + location: string | null; + event_date: string | null; + registration_open_at: string | null; + registration_close_at: string | null; + form_url: string | null; + entry_fee: number | null; + video_max_duration: number | null; + judges: { name: string; bio: string; instagram: string }[] | null; + categories: string[] | null; + status: 'draft' | 'open' | 'closed' | 'completed'; + source: string; + image_url: string | null; + created_at: string; + updated_at: string; +} + +export interface Registration { + id: string; + championship_id: string; + user_id: string; + category: string | null; + level: string | null; + notes: string | null; + status: + | 'submitted' + | 'form_submitted' + | 'payment_pending' + | 'payment_confirmed' + | 'video_submitted' + | 'accepted' + | 'rejected' + | 'waitlisted'; + video_url: string | null; + submitted_at: string; + decided_at: string | null; + championship_title: string | null; + championship_event_date: string | null; + championship_location: string | null; +} diff --git a/mobile/src/utils/tokenStorage.ts b/mobile/src/utils/tokenStorage.ts new file mode 100644 index 0000000..fe01209 --- /dev/null +++ b/mobile/src/utils/tokenStorage.ts @@ -0,0 +1,49 @@ +import * as SecureStore from 'expo-secure-store'; + +const ACCESS_KEY = 'access_token'; +const REFRESH_KEY = 'refresh_token'; + +// In-memory cache so synchronous reads work immediately after login +let _accessToken: string | null = null; +let _refreshToken: string | null = null; + +export const tokenStorage = { + async saveTokens(access: string, refresh: string): Promise { + _accessToken = access; + _refreshToken = refresh; + await SecureStore.setItemAsync(ACCESS_KEY, access); + await SecureStore.setItemAsync(REFRESH_KEY, refresh); + }, + + getAccessTokenSync(): string | null { + return _accessToken; + }, + + getRefreshTokenSync(): string | null { + return _refreshToken; + }, + + async getAccessToken(): Promise { + if (_accessToken) return _accessToken; + _accessToken = await SecureStore.getItemAsync(ACCESS_KEY); + return _accessToken; + }, + + async getRefreshToken(): Promise { + if (_refreshToken) return _refreshToken; + _refreshToken = await SecureStore.getItemAsync(REFRESH_KEY); + return _refreshToken; + }, + + async clearTokens(): Promise { + _accessToken = null; + _refreshToken = null; + await SecureStore.deleteItemAsync(ACCESS_KEY); + await SecureStore.deleteItemAsync(REFRESH_KEY); + }, + + async loadFromStorage(): Promise { + _accessToken = await SecureStore.getItemAsync(ACCESS_KEY); + _refreshToken = await SecureStore.getItemAsync(REFRESH_KEY); + }, +}; diff --git a/mobile/tsconfig.json b/mobile/tsconfig.json new file mode 100644 index 0000000..b9567f6 --- /dev/null +++ b/mobile/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "expo/tsconfig.base", + "compilerOptions": { + "strict": true + } +} diff --git a/start-backend.bat b/start-backend.bat new file mode 100644 index 0000000..e530b02 --- /dev/null +++ b/start-backend.bat @@ -0,0 +1,4 @@ +@echo off +echo Starting Pole Championships backend... +cd /d "%~dp0backend" +.venv\Scripts\python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --reload