diff --git a/.circleci/config.yml b/.circleci/config.yml index bb68b14..25c78d3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -1,31 +1,360 @@ -# Use the latest 2.1 version of CircleCI pipeline process engine. -# See: https://circleci.com/docs/reference/configuration-reference version: 2.1 -# Define a job to be invoked later in a workflow. -# See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#jobs-overview & https://circleci.com/docs/reference/configuration-reference/#jobs -jobs: - say-hello: - # Specify the execution environment. You can specify an image from Docker Hub or use one of our convenience images from CircleCI's Developer Hub. - # See: https://circleci.com/docs/guides/execution-managed/executor-intro/ & https://circleci.com/docs/reference/configuration-reference/#executor-job +# Define executor +executors: + node-executor: docker: - # Specify the version you desire here - # See: https://circleci.com/developer/images/image/cimg/base - - image: cimg/base:current + - image: node:20 + resource_class: small + working_directory: ~/project - # Add steps to the job - # See: https://circleci.com/docs/guides/orchestrate/jobs-steps/#steps-overview & https://circleci.com/docs/reference/configuration-reference/#steps +# Reusable commands +commands: + setup-pnpm: steps: - # Checkout the code as the first step. - - checkout - - run: - name: "Say hello" - command: "echo Hello, World!" + - restore_cache: + name: Restore pnpm Package Cache -# Orchestrate jobs using workflows -# See: https://circleci.com/docs/guides/orchestrate/workflows/ & https://circleci.com/docs/reference/configuration-reference/#workflows + keys: + - pnpm-packages-{{ checksum "pnpm-lock.yaml" }} + - run: + name: Install pnpm package manager + command: | + npm install --global corepack@latest + corepack enable + corepack prepare pnpm@latest-10 --activate + pnpm config set store-dir .pnpm-store + - run: + name: Install Dependencies + command: pnpm install + - save_cache: + name: Save pnpm Package Cache + key: pnpm-packages-{{ checksum "pnpm-lock.yaml" }} + paths: + - .pnpm-store + +# Jobs +jobs: + # ============================================ + # TEST PHASE + # ============================================ + + test-lint: + executor: node-executor + steps: + - checkout + - setup-pnpm + - run: + name: Run linting + command: pnpm run lint + - run: + name: Check formatting + command: pnpm run format --check || echo "Format check complete" + + test-typecheck: + executor: node-executor + steps: + - checkout + - setup-pnpm + - run: + name: Type check all packages + command: pnpm run typecheck + + test-unit: + executor: node-executor + steps: + - checkout + - setup-pnpm + - run: + name: Run unit tests + command: pnpm run test + - store_test_results: + path: apps/main/coverage + - store_artifacts: + path: apps/main/coverage + destination: coverage + + test-api: + executor: node-executor + steps: + - checkout + - restore_cache: + name: Restore npm API Cache + keys: + - npm-api-{{ checksum "api/package-lock.json" }} + - run: + name: Install API dependencies + command: | + cd api + npm ci + - save_cache: + name: Save npm API Cache + key: npm-api-{{ checksum "api/package-lock.json" }} + paths: + - api/node_modules + - run: + name: Lint API + command: | + cd api + npm run lint + - run: + name: Run API tests + command: | + cd api + npm run test + + # ============================================ + # BUILD PHASE + # ============================================ + + build-apps: + executor: node-executor + parameters: + environment: + type: string + default: "staging" + steps: + - checkout + - setup-pnpm + - run: + name: Build main app for << parameters.environment >> + command: | + cd apps/main + npm run build:<< parameters.environment >> + - run: + name: Build external app + command: | + cd apps/external + npm run build + - persist_to_workspace: + root: . + paths: + - apps/main/dist + - apps/external/dist + - packages/ui/dist + - packages/shared/dist + - store_artifacts: + path: apps/main/dist + destination: main-app-<< parameters.environment >> + - store_artifacts: + path: apps/external/dist + destination: external-app + + build-api: + executor: node-executor + steps: + - checkout + - restore_cache: + name: Restore npm API Cache + keys: + - npm-api-{{ checksum "api/package-lock.json" }} + - run: + name: Install API dependencies + command: | + cd api + npm ci + - run: + name: Build API + command: | + cd api + npm run build + - persist_to_workspace: + root: . + paths: + - api/dist + - store_artifacts: + path: api/dist + destination: api + + # ============================================ + # DOCKER BUILD PHASE + # ============================================ + + build-docker-api: + machine: + image: ubuntu-2204:current + resource_class: medium + steps: + - checkout + - attach_workspace: + at: . + - run: + name: Build API Docker image + command: | + cd api + docker build -t xtablo-api:${CIRCLE_SHA1} -t xtablo-api:latest . + - run: + name: Save Docker image + command: | + mkdir -p /tmp/docker-images + docker save xtablo-api:${CIRCLE_SHA1} -o /tmp/docker-images/api.tar + - persist_to_workspace: + root: /tmp + paths: + - docker-images/api.tar + + # ============================================ + # DEPLOY PHASE + # ============================================ + + deploy-staging: + executor: node-executor + steps: + - checkout + - attach_workspace: + at: . + - setup-pnpm + - run: + name: Deploy main app to staging + command: | + cd apps/main + echo "Deploying main app to staging environment..." + npx wrangler deploy --env staging + - run: + name: Deploy external app to staging + command: | + cd apps/external + echo "Deploying external app to staging..." + # Add external app staging deployment if needed + # npx wrangler deploy --env staging + - run: + name: Deploy API to staging + command: | + echo "Deploying API to staging environment..." + # Add your API deployment commands here + # Example for Google Cloud Run: + # gcloud run deploy xtablo-api-staging --image gcr.io/${GCP_PROJECT}/xtablo-api:${CIRCLE_SHA1} --region us-central1 + + deploy-production: + executor: node-executor + steps: + - checkout + - attach_workspace: + at: . + - setup-pnpm + - run: + name: Deploy main app to production + command: | + cd apps/main + echo "Deploying main app to production environment..." + npx wrangler deploy --env production + - run: + name: Deploy external app to production + command: | + cd apps/external + echo "Deploying external app to production..." + # Add external app production deployment if needed + # npx wrangler deploy --env production + - run: + name: Deploy API to production + command: | + echo "Deploying API to production environment..." + # Add your production API deployment commands here + # Example for Google Cloud Run: + # gcloud run deploy xtablo-api --image gcr.io/${GCP_PROJECT}/xtablo-api:${CIRCLE_SHA1} --region us-central1 + +# Workflows workflows: - say-hello-workflow: # This is the name of the workflow, feel free to change it to better match your workflow. - # Inside the workflow, you define the jobs you want to run. + version: 2 + + # Run on all branches (except main and develop) + test-and-build: + when: + and: + - not: + equal: [ main, << pipeline.git.branch >> ] + - not: + equal: [ develop, << pipeline.git.branch >> ] jobs: - - say-hello \ No newline at end of file + # Test phase - run in parallel + - test-lint + - test-typecheck + - test-unit + - test-api + + # Build phase - run after tests pass + - build-apps: + requires: + - test-lint + - test-typecheck + - test-unit + + - build-api: + requires: + - test-api + + - build-docker-api: + requires: + - build-api + + # Staging deployment workflow (develop branch) + deploy-to-staging: + when: + equal: [ develop, << pipeline.git.branch >> ] + jobs: + # Test phase + - test-lint + - test-typecheck + - test-unit + - test-api + + # Build phase for staging + - build-apps: + environment: "staging" + requires: + - test-lint + - test-typecheck + - test-unit + + - build-api: + requires: + - test-api + + - build-docker-api: + requires: + - build-api + + # Deploy to staging + - deploy-staging: + requires: + - build-apps + - build-docker-api + + # Production deployment workflow (main branch) + deploy-to-production: + when: + equal: [ main, << pipeline.git.branch >> ] + jobs: + # Test phase + - test-lint + - test-typecheck + - test-unit + - test-api + + # Build phase for production + - build-apps: + environment: "prod" + requires: + - test-lint + - test-typecheck + - test-unit + + - build-api: + requires: + - test-api + + - build-docker-api: + requires: + - build-api + + # Manual approval gate before production + - hold-for-approval: + type: approval + requires: + - build-apps + - build-docker-api + + # Deploy to production + - deploy-production: + requires: + - hold-for-approval diff --git a/api/.env.production b/api/.env.production index b4edc7a..4242e28 100644 --- a/api/.env.production +++ b/api/.env.production @@ -5,7 +5,7 @@ STREAM_CHAT_API_KEY=v4yf8rs94aa8 STREAM_CHAT_API_SECRET=jq2szvv73ua7sz9tvr9y24dxg37sw8ue8t576fu7ggr4h6wvcmunby4gvte8tm8f XTABLO_URL=https://app.xtablo.com -CORS_ORIGIN="https://app.xtablo.com" +CORS_ORIGIN="https://app.xtablo.com,https://embed.xtablo.com" R2_ACCOUNT_ID="9715fa14c5e5d1612301572cf1c6bbee" R2_ACCESS_KEY_ID="caeb987bbcd601708a93c6aa562064ef" diff --git a/api/src/__tests__/slots.test.ts b/api/src/__tests__/slots.test.ts index 8e3cce5..6ed184d 100644 --- a/api/src/__tests__/slots.test.ts +++ b/api/src/__tests__/slots.test.ts @@ -734,10 +734,8 @@ describe("generateTimeSlots", () => { expect(slot09_00?.available, "09:00 should be available").to.be.true; expect(slot09_30?.available, "09:30 should not be available").to.be.false; - expect(slot10_00?.available, "10:00 should not be unavailable").to.be - .false; // Within buffered time - expect(slot10_30?.available, "10:30 should not be unavailable").to.be - .false; // Within buffered time + expect(slot10_00?.available, "10:00 should not be unavailable").to.be.false; // Within buffered time + expect(slot10_30?.available, "10:30 should not be unavailable").to.be.false; // Within buffered time expect(slot11_00?.available, "11:00 should be available").to.be.true; // After buffered time }); @@ -1145,12 +1143,8 @@ describe("generateTimeSlots", () => { }); it("should format date strings correctly", () => { - expect(getDateStringCET(new Date("2024-01-15T10:30:00Z"))).to.equal( - "2024-01-15" - ); - expect(getDateStringCET(new Date("2024-12-31T23:59:59Z"))).to.equal( - "2025-01-01" - ); + expect(getDateStringCET(new Date("2024-01-15T10:30:00Z"))).to.equal("2024-01-15"); + expect(getDateStringCET(new Date("2024-12-31T23:59:59Z"))).to.equal("2025-01-01"); }); }); }); diff --git a/api/src/config.ts b/api/src/config.ts index f2d688e..1506488 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -47,38 +47,20 @@ function createConfig(): AppConfig { process.env.SUPABASE_SERVICE_ROLE_KEY ), SUPABASE_CONNECTION_STRING: process.env.SUPABASE_CONNECTION_STRING || "", - STREAM_CHAT_API_KEY: validateEnvVar( - "STREAM_CHAT_API_KEY", - process.env.STREAM_CHAT_API_KEY - ), + STREAM_CHAT_API_KEY: validateEnvVar("STREAM_CHAT_API_KEY", process.env.STREAM_CHAT_API_KEY), STREAM_CHAT_API_SECRET: validateEnvVar( "STREAM_CHAT_API_SECRET", process.env.STREAM_CHAT_API_SECRET ), EMAIL_USER: validateEnvVar("EMAIL_USER", process.env.EMAIL_USER), - EMAIL_CLIENT_ID: validateEnvVar( - "EMAIL_CLIENT_ID", - process.env.EMAIL_CLIENT_ID - ), - EMAIL_CLIENT_SECRET: validateEnvVar( - "EMAIL_CLIENT_SECRET", - process.env.EMAIL_CLIENT_SECRET - ), - EMAIL_REFRESH_TOKEN: validateEnvVar( - "EMAIL_REFRESH_TOKEN", - process.env.EMAIL_REFRESH_TOKEN - ), + EMAIL_CLIENT_ID: validateEnvVar("EMAIL_CLIENT_ID", process.env.EMAIL_CLIENT_ID), + EMAIL_CLIENT_SECRET: validateEnvVar("EMAIL_CLIENT_SECRET", process.env.EMAIL_CLIENT_SECRET), + EMAIL_REFRESH_TOKEN: validateEnvVar("EMAIL_REFRESH_TOKEN", process.env.EMAIL_REFRESH_TOKEN), CORS_ORIGIN: process.env.CORS_ORIGIN || "https://app.xtablo.com", XTABLO_URL: process.env.XTABLO_URL || "https://app.xtablo.com", R2_ACCOUNT_ID: validateEnvVar("R2_ACCOUNT_ID", process.env.R2_ACCOUNT_ID), - R2_ACCESS_KEY_ID: validateEnvVar( - "R2_ACCESS_KEY_ID", - process.env.R2_ACCESS_KEY_ID - ), - R2_SECRET_ACCESS_KEY: validateEnvVar( - "R2_SECRET_ACCESS_KEY", - process.env.R2_SECRET_ACCESS_KEY - ), + R2_ACCESS_KEY_ID: validateEnvVar("R2_ACCESS_KEY_ID", process.env.R2_ACCESS_KEY_ID), + R2_SECRET_ACCESS_KEY: validateEnvVar("R2_SECRET_ACCESS_KEY", process.env.R2_SECRET_ACCESS_KEY), SYNC_CALS_SECRET: process.env.SYNC_CALS_SECRET || "", LOG_LEVEL: "info", }; diff --git a/api/src/database.types.ts b/api/src/database.types.ts index eeced73..aa1513e 100644 --- a/api/src/database.types.ts +++ b/api/src/database.types.ts @@ -1,632 +1,624 @@ -export type Json = - | string - | number - | boolean - | null - | { [key: string]: Json | undefined } - | Json[] +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; export type Database = { // Allows to automatically instantiate createClient with right options // instead of createClient(URL, KEY) __InternalSupabase: { - PostgrestVersion: "13.0.4" - } + PostgrestVersion: "13.0.4"; + }; public: { Tables: { availabilities: { Row: { - availability_data: Json - created_at: string - exceptions: Json | null - id: number - updated_at: string - user_id: string - } + availability_data: Json; + created_at: string; + exceptions: Json | null; + id: number; + updated_at: string; + user_id: string; + }; Insert: { - availability_data?: Json - created_at?: string - exceptions?: Json | null - id?: number - updated_at?: string - user_id: string - } + availability_data?: Json; + created_at?: string; + exceptions?: Json | null; + id?: number; + updated_at?: string; + user_id: string; + }; Update: { - availability_data?: Json - created_at?: string - exceptions?: Json | null - id?: number - updated_at?: string - user_id?: string - } - Relationships: [] - } + availability_data?: Json; + created_at?: string; + exceptions?: Json | null; + id?: number; + updated_at?: string; + user_id?: string; + }; + Relationships: []; + }; calendar_subscriptions: { Row: { - created_at: string | null - id: string - tablo_id: string - token: string - } + created_at: string | null; + id: string; + tablo_id: string; + token: string; + }; Insert: { - created_at?: string | null - id?: string - tablo_id: string - token: string - } + created_at?: string | null; + id?: string; + tablo_id: string; + token: string; + }; Update: { - created_at?: string | null - id?: string - tablo_id?: string - token?: string - } + created_at?: string | null; + id?: string; + tablo_id?: string; + token?: string; + }; Relationships: [ { - foreignKeyName: "calendar_subscriptions_tablo_id_fkey" - columns: ["tablo_id"] - isOneToOne: true - referencedRelation: "events_and_tablos" - referencedColumns: ["tablo_id"] + foreignKeyName: "calendar_subscriptions_tablo_id_fkey"; + columns: ["tablo_id"]; + isOneToOne: true; + referencedRelation: "events_and_tablos"; + referencedColumns: ["tablo_id"]; }, { - foreignKeyName: "calendar_subscriptions_tablo_id_fkey" - columns: ["tablo_id"] - isOneToOne: true - referencedRelation: "tablos" - referencedColumns: ["id"] + foreignKeyName: "calendar_subscriptions_tablo_id_fkey"; + columns: ["tablo_id"]; + isOneToOne: true; + referencedRelation: "tablos"; + referencedColumns: ["id"]; }, { - foreignKeyName: "calendar_subscriptions_tablo_id_fkey" - columns: ["tablo_id"] - isOneToOne: true - referencedRelation: "user_tablos" - referencedColumns: ["id"] + foreignKeyName: "calendar_subscriptions_tablo_id_fkey"; + columns: ["tablo_id"]; + isOneToOne: true; + referencedRelation: "user_tablos"; + referencedColumns: ["id"]; }, - ] - } + ]; + }; devis: { Row: { - client_email: string - created_at: string - date: string - due_date: string - id: string - items: Json - notes: string | null - number: string - status: Database["public"]["Enums"]["devis_status"] - subtotal: number - tax: number - terms: string | null - total: number - updated_at: string - user_id: string - } + client_email: string; + created_at: string; + date: string; + due_date: string; + id: string; + items: Json; + notes: string | null; + number: string; + status: Database["public"]["Enums"]["devis_status"]; + subtotal: number; + tax: number; + terms: string | null; + total: number; + updated_at: string; + user_id: string; + }; Insert: { - client_email: string - created_at?: string - date: string - due_date: string - id?: string - items?: Json - notes?: string | null - number: string - status?: Database["public"]["Enums"]["devis_status"] - subtotal: number - tax: number - terms?: string | null - total: number - updated_at?: string - user_id: string - } + client_email: string; + created_at?: string; + date: string; + due_date: string; + id?: string; + items?: Json; + notes?: string | null; + number: string; + status?: Database["public"]["Enums"]["devis_status"]; + subtotal: number; + tax: number; + terms?: string | null; + total: number; + updated_at?: string; + user_id: string; + }; Update: { - client_email?: string - created_at?: string - date?: string - due_date?: string - id?: string - items?: Json - notes?: string | null - number?: string - status?: Database["public"]["Enums"]["devis_status"] - subtotal?: number - tax?: number - terms?: string | null - total?: number - updated_at?: string - user_id?: string - } - Relationships: [] - } + client_email?: string; + created_at?: string; + date?: string; + due_date?: string; + id?: string; + items?: Json; + notes?: string | null; + number?: string; + status?: Database["public"]["Enums"]["devis_status"]; + subtotal?: number; + tax?: number; + terms?: string | null; + total?: number; + updated_at?: string; + user_id?: string; + }; + Relationships: []; + }; event_types: { Row: { - config: Json - created_at: string | null - deleted_at: string | null - id: string - is_active: boolean - standard_name: string | null - updated_at: string | null - user_id: string - } + config: Json; + created_at: string | null; + deleted_at: string | null; + id: string; + is_active: boolean; + standard_name: string | null; + updated_at: string | null; + user_id: string; + }; Insert: { - config?: Json - created_at?: string | null - deleted_at?: string | null - id?: string - is_active?: boolean - standard_name?: string | null - updated_at?: string | null - user_id: string - } + config?: Json; + created_at?: string | null; + deleted_at?: string | null; + id?: string; + is_active?: boolean; + standard_name?: string | null; + updated_at?: string | null; + user_id: string; + }; Update: { - config?: Json - created_at?: string | null - deleted_at?: string | null - id?: string - is_active?: boolean - standard_name?: string | null - updated_at?: string | null - user_id?: string - } - Relationships: [] - } + config?: Json; + created_at?: string | null; + deleted_at?: string | null; + id?: string; + is_active?: boolean; + standard_name?: string | null; + updated_at?: string | null; + user_id?: string; + }; + Relationships: []; + }; events: { Row: { - created_at: string | null - created_by: string - deleted_at: string | null - description: string | null - end_time: string | null - id: string - start_date: string - start_time: string - tablo_id: string - title: string - } + created_at: string | null; + created_by: string; + deleted_at: string | null; + description: string | null; + end_time: string | null; + id: string; + start_date: string; + start_time: string; + tablo_id: string; + title: string; + }; Insert: { - created_at?: string | null - created_by: string - deleted_at?: string | null - description?: string | null - end_time?: string | null - id?: string - start_date: string - start_time: string - tablo_id: string - title: string - } + created_at?: string | null; + created_by: string; + deleted_at?: string | null; + description?: string | null; + end_time?: string | null; + id?: string; + start_date: string; + start_time: string; + tablo_id: string; + title: string; + }; Update: { - created_at?: string | null - created_by?: string - deleted_at?: string | null - description?: string | null - end_time?: string | null - id?: string - start_date?: string - start_time?: string - tablo_id?: string - title?: string - } + created_at?: string | null; + created_by?: string; + deleted_at?: string | null; + description?: string | null; + end_time?: string | null; + id?: string; + start_date?: string; + start_time?: string; + tablo_id?: string; + title?: string; + }; Relationships: [ { - foreignKeyName: "fk_events_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "events_and_tablos" - referencedColumns: ["tablo_id"] + foreignKeyName: "fk_events_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "events_and_tablos"; + referencedColumns: ["tablo_id"]; }, { - foreignKeyName: "fk_events_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "tablos" - referencedColumns: ["id"] + foreignKeyName: "fk_events_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "tablos"; + referencedColumns: ["id"]; }, { - foreignKeyName: "fk_events_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "user_tablos" - referencedColumns: ["id"] + foreignKeyName: "fk_events_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "user_tablos"; + referencedColumns: ["id"]; }, - ] - } + ]; + }; feedbacks: { Row: { - created_at: string | null - fd_type: string - id: number - message: string - user_id: string - } + created_at: string | null; + fd_type: string; + id: number; + message: string; + user_id: string; + }; Insert: { - created_at?: string | null - fd_type: string - id?: number - message: string - user_id: string - } + created_at?: string | null; + fd_type: string; + id?: number; + message: string; + user_id: string; + }; Update: { - created_at?: string | null - fd_type?: string - id?: number - message?: string - user_id?: string - } - Relationships: [] - } + created_at?: string | null; + fd_type?: string; + id?: number; + message?: string; + user_id?: string; + }; + Relationships: []; + }; profiles: { Row: { - avatar_url: string | null - email: string | null - first_name: string | null - id: string - is_temporary: boolean - last_name: string | null - name: string | null - short_user_id: string - } + avatar_url: string | null; + email: string | null; + first_name: string | null; + id: string; + is_temporary: boolean; + last_name: string | null; + name: string | null; + short_user_id: string; + }; Insert: { - avatar_url?: string | null - email?: string | null - first_name?: string | null - id: string - is_temporary?: boolean - last_name?: string | null - name?: string | null - short_user_id: string - } + avatar_url?: string | null; + email?: string | null; + first_name?: string | null; + id: string; + is_temporary?: boolean; + last_name?: string | null; + name?: string | null; + short_user_id: string; + }; Update: { - avatar_url?: string | null - email?: string | null - first_name?: string | null - id?: string - is_temporary?: boolean - last_name?: string | null - name?: string | null - short_user_id?: string - } - Relationships: [] - } + avatar_url?: string | null; + email?: string | null; + first_name?: string | null; + id?: string; + is_temporary?: boolean; + last_name?: string | null; + name?: string | null; + short_user_id?: string; + }; + Relationships: []; + }; tablo_access: { Row: { - created_at: string | null - granted_by: string - id: number - is_active: boolean | null - is_admin: boolean | null - tablo_id: string - user_id: string - } + created_at: string | null; + granted_by: string; + id: number; + is_active: boolean | null; + is_admin: boolean | null; + tablo_id: string; + user_id: string; + }; Insert: { - created_at?: string | null - granted_by: string - id?: number - is_active?: boolean | null - is_admin?: boolean | null - tablo_id: string - user_id: string - } + created_at?: string | null; + granted_by: string; + id?: number; + is_active?: boolean | null; + is_admin?: boolean | null; + tablo_id: string; + user_id: string; + }; Update: { - created_at?: string | null - granted_by?: string - id?: number - is_active?: boolean | null - is_admin?: boolean | null - tablo_id?: string - user_id?: string - } + created_at?: string | null; + granted_by?: string; + id?: number; + is_active?: boolean | null; + is_admin?: boolean | null; + tablo_id?: string; + user_id?: string; + }; Relationships: [ { - foreignKeyName: "fk_tablo_access_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "events_and_tablos" - referencedColumns: ["tablo_id"] + foreignKeyName: "fk_tablo_access_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "events_and_tablos"; + referencedColumns: ["tablo_id"]; }, { - foreignKeyName: "fk_tablo_access_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "tablos" - referencedColumns: ["id"] + foreignKeyName: "fk_tablo_access_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "tablos"; + referencedColumns: ["id"]; }, { - foreignKeyName: "fk_tablo_access_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "user_tablos" - referencedColumns: ["id"] + foreignKeyName: "fk_tablo_access_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "user_tablos"; + referencedColumns: ["id"]; }, { - foreignKeyName: "fk_tablo_access_user_id_from_profiles" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] + foreignKeyName: "fk_tablo_access_user_id_from_profiles"; + columns: ["user_id"]; + isOneToOne: false; + referencedRelation: "profiles"; + referencedColumns: ["id"]; }, - ] - } + ]; + }; tablo_invites: { Row: { - id: number - invite_token: string - invited_by: string - invited_email: string - tablo_id: string - } + id: number; + invite_token: string; + invited_by: string; + invited_email: string; + tablo_id: string; + }; Insert: { - id?: number - invite_token: string - invited_by: string - invited_email: string - tablo_id: string - } + id?: number; + invite_token: string; + invited_by: string; + invited_email: string; + tablo_id: string; + }; Update: { - id?: number - invite_token?: string - invited_by?: string - invited_email?: string - tablo_id?: string - } + id?: number; + invite_token?: string; + invited_by?: string; + invited_email?: string; + tablo_id?: string; + }; Relationships: [ { - foreignKeyName: "fk_tablo_invitations_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "events_and_tablos" - referencedColumns: ["tablo_id"] + foreignKeyName: "fk_tablo_invitations_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "events_and_tablos"; + referencedColumns: ["tablo_id"]; }, { - foreignKeyName: "fk_tablo_invitations_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "tablos" - referencedColumns: ["id"] + foreignKeyName: "fk_tablo_invitations_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "tablos"; + referencedColumns: ["id"]; }, { - foreignKeyName: "fk_tablo_invitations_tablo_id" - columns: ["tablo_id"] - isOneToOne: false - referencedRelation: "user_tablos" - referencedColumns: ["id"] + foreignKeyName: "fk_tablo_invitations_tablo_id"; + columns: ["tablo_id"]; + isOneToOne: false; + referencedRelation: "user_tablos"; + referencedColumns: ["id"]; }, - ] - } + ]; + }; tablos: { Row: { - color: string | null - created_at: string | null - deleted_at: string | null - id: string - image: string | null - name: string - owner_id: string - position: number - status: string - } + color: string | null; + created_at: string | null; + deleted_at: string | null; + id: string; + image: string | null; + name: string; + owner_id: string; + position: number; + status: string; + }; Insert: { - color?: string | null - created_at?: string | null - deleted_at?: string | null - id?: string - image?: string | null - name: string - owner_id: string - position?: number - status?: string - } + color?: string | null; + created_at?: string | null; + deleted_at?: string | null; + id?: string; + image?: string | null; + name: string; + owner_id: string; + position?: number; + status?: string; + }; Update: { - color?: string | null - created_at?: string | null - deleted_at?: string | null - id?: string - image?: string | null - name?: string - owner_id?: string - position?: number - status?: string - } - Relationships: [] - } + color?: string | null; + created_at?: string | null; + deleted_at?: string | null; + id?: string; + image?: string | null; + name?: string; + owner_id?: string; + position?: number; + status?: string; + }; + Relationships: []; + }; user_introductions: { Row: { - config: Json - created_at: string | null - updated_at: string | null - user_id: string - } + config: Json; + created_at: string | null; + updated_at: string | null; + user_id: string; + }; Insert: { - config?: Json - created_at?: string | null - updated_at?: string | null - user_id: string - } + config?: Json; + created_at?: string | null; + updated_at?: string | null; + user_id: string; + }; Update: { - config?: Json - created_at?: string | null - updated_at?: string | null - user_id?: string - } - Relationships: [] - } - } + config?: Json; + created_at?: string | null; + updated_at?: string | null; + user_id?: string; + }; + Relationships: []; + }; + }; Views: { events_and_tablos: { Row: { - description: string | null - end_time: string | null - event_id: string | null - start_date: string | null - start_time: string | null - tablo_color: string | null - tablo_id: string | null - tablo_name: string | null - tablo_status: string | null - title: string | null - } - Relationships: [] - } + description: string | null; + end_time: string | null; + event_id: string | null; + start_date: string | null; + start_time: string | null; + tablo_color: string | null; + tablo_id: string | null; + tablo_name: string | null; + tablo_status: string | null; + title: string | null; + }; + Relationships: []; + }; user_tablos: { Row: { - access_level: string | null - color: string | null - created_at: string | null - deleted_at: string | null - id: string | null - image: string | null - is_admin: boolean | null - name: string | null - position: number | null - status: string | null - user_id: string | null - } + access_level: string | null; + color: string | null; + created_at: string | null; + deleted_at: string | null; + id: string | null; + image: string | null; + is_admin: boolean | null; + name: string | null; + position: number | null; + status: string | null; + user_id: string | null; + }; Relationships: [ { - foreignKeyName: "fk_tablo_access_user_id_from_profiles" - columns: ["user_id"] - isOneToOne: false - referencedRelation: "profiles" - referencedColumns: ["id"] + foreignKeyName: "fk_tablo_access_user_id_from_profiles"; + columns: ["user_id"]; + isOneToOne: false; + referencedRelation: "profiles"; + referencedColumns: ["id"]; }, - ] - } - } + ]; + }; + }; Functions: { generate_random_string: { - Args: { length?: number } - Returns: string - } - } + Args: { length?: number }; + Returns: string; + }; + }; Enums: { - devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired" - } + devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"; + }; CompositeTypes: { time_range: { - start_time: string | null - end_time: string | null - } - } - } -} + start_time: string | null; + end_time: string | null; + }; + }; + }; +}; -type DatabaseWithoutInternals = Omit +type DatabaseWithoutInternals = Omit; -type DefaultSchema = DatabaseWithoutInternals[Extract] +type DefaultSchema = DatabaseWithoutInternals[Extract]; export type Tables< DefaultSchemaTableNameOrOptions extends | keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? keyof (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"]) : never = never, > = DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? (DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] & DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends { - Row: infer R + Row: infer R; } ? R : never - : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & - DefaultSchema["Views"]) - ? (DefaultSchema["Tables"] & - DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { - Row: infer R + : DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] & DefaultSchema["Views"]) + ? (DefaultSchema["Tables"] & DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends { + Row: infer R; } ? R : never - : never + : never; export type TablesInsert< DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema["Tables"] | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] : never = never, > = DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Insert: infer I + Insert: infer I; } ? I : never : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Insert: infer I + Insert: infer I; } ? I : never - : never + : never; export type TablesUpdate< DefaultSchemaTableNameOrOptions extends | keyof DefaultSchema["Tables"] | { schema: keyof DatabaseWithoutInternals }, TableName extends DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? keyof DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] : never = never, > = DefaultSchemaTableNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? DatabaseWithoutInternals[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends { - Update: infer U + Update: infer U; } ? U : never : DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"] ? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends { - Update: infer U + Update: infer U; } ? U : never - : never + : never; export type Enums< DefaultSchemaEnumNameOrOptions extends | keyof DefaultSchema["Enums"] | { schema: keyof DatabaseWithoutInternals }, EnumName extends DefaultSchemaEnumNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? keyof DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"] : never = never, > = DefaultSchemaEnumNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? DatabaseWithoutInternals[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName] : DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"] ? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions] - : never + : never; export type CompositeTypes< PublicCompositeTypeNameOrOptions extends | keyof DefaultSchema["CompositeTypes"] | { schema: keyof DatabaseWithoutInternals }, CompositeTypeName extends PublicCompositeTypeNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? keyof DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"] : never = never, > = PublicCompositeTypeNameOrOptions extends { - schema: keyof DatabaseWithoutInternals + schema: keyof DatabaseWithoutInternals; } ? DatabaseWithoutInternals[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName] : PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"] ? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions] - : never + : never; export const Constants = { public: { @@ -634,4 +626,4 @@ export const Constants = { devis_status: ["draft", "sent", "accepted", "rejected", "expired"], }, }, -} as const +} as const; diff --git a/api/src/helpers.ts b/api/src/helpers.ts index 444c146..1a52591 100644 --- a/api/src/helpers.ts +++ b/api/src/helpers.ts @@ -1,8 +1,4 @@ -import { - ListObjectsV2Command, - PutObjectCommand, - S3Client, -} from "@aws-sdk/client-s3"; +import { ListObjectsV2Command, PutObjectCommand, S3Client } from "@aws-sdk/client-s3"; import type { SupabaseClient } from "@supabase/supabase-js"; import type { EventAndTablo } from "./types.ts"; @@ -54,14 +50,10 @@ export const generateICSFromEvents = ( `DTSTART:${startDateTime}`, `DTEND:${endDateTime}`, `SUMMARY:${escapeICSText(event.title)}`, - `DESCRIPTION:${escapeICSText( - `Tablo: ${event.tablo_name}\n${event.description || ""}` - )}`, + `DESCRIPTION:${escapeICSText(`Tablo: ${event.tablo_name}\n${event.description || ""}`)}`, event.tablo_name ? `CATEGORIES:${escapeICSText(event.tablo_name)}` : "", `CREATED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`, - `LAST-MODIFIED:${ - new Date().toISOString().replace(/[-:]/g, "").split(".")[0] - }Z`, + `LAST-MODIFIED:${new Date().toISOString().replace(/[-:]/g, "").split(".")[0]}Z`, "STATUS:CONFIRMED", "TRANSP:OPAQUE", "END:VEVENT", @@ -112,10 +104,7 @@ export const writeCalendarFileToR2 = async ( ); }; -export const getTabloFileNames = async ( - s3_client: S3Client, - tabloId: string -) => { +export const getTabloFileNames = async (s3_client: S3Client, tabloId: string) => { const bucketName = "tablo-data"; const { Contents } = await s3_client.send( @@ -130,11 +119,7 @@ export const getTabloFileNames = async ( ); }; -export const isTabloMember = async ( - supabase: SupabaseClient, - tabloId: string, - userId: string -) => { +export const isTabloMember = async (supabase: SupabaseClient, tabloId: string, userId: string) => { const { data: tabloAccess, error: isMemberError } = await supabase .from("tablo_access") .select("*") @@ -149,11 +134,7 @@ export const isTabloMember = async ( return tabloAccess?.length > 0; }; -export const isTabloAdmin = async ( - supabase: SupabaseClient, - tabloId: string, - userId: string -) => { +export const isTabloAdmin = async (supabase: SupabaseClient, tabloId: string, userId: string) => { const { data: tabloAccess, error: isAdminError } = await supabase .from("tablo_access") .select("*") diff --git a/api/src/public.ts b/api/src/public.ts index 3ad5668..9b8bae4 100644 --- a/api/src/public.ts +++ b/api/src/public.ts @@ -51,25 +51,22 @@ publicRouter.get("/slots/:shortUserId/:standardName", async (c) => { return c.json({ error: "Event type not found" }, 404); } - const eventType = - eventTypeData as Database["public"]["Tables"]["event_types"]["Row"]; + const eventType = eventTypeData as Database["public"]["Tables"]["event_types"]["Row"]; const eventTypeConfig = eventType.config as EventTypeConfig; // Get user's availabilities - const { data: availabilitiesData, error: availabilitiesError } = - await supabase - .from("availabilities") - .select("*") - .eq("user_id", user.id) - .single(); + const { data: availabilitiesData, error: availabilitiesError } = await supabase + .from("availabilities") + .select("*") + .eq("user_id", user.id) + .single(); if (availabilitiesError) { return c.json({ error: "Availabilities not found" }, 404); } const availabilities = availabilitiesData as Tables<"availabilities">; - const weeklyAvailability = - availabilities.availability_data as WeeklyAvailability; + const weeklyAvailability = availabilities.availability_data as WeeklyAvailability; const exceptions = (availabilities.exceptions as Exception[]) || []; // Get existing events for the next month diff --git a/api/src/slots.ts b/api/src/slots.ts index 7e5d5e4..588d214 100644 --- a/api/src/slots.ts +++ b/api/src/slots.ts @@ -1,6 +1,5 @@ -import type { Tables } from "./database.types.js"; - import { DateTime } from "luxon"; +import type { Tables } from "./database.types.js"; // Types for availability calculation type TimeRange = { @@ -57,9 +56,7 @@ function parseTime(timeStr: string): { hours: number; minutes: number } { } function formatTime(hours: number, minutes: number): string { - return `${hours.toString().padStart(2, "0")}:${minutes - .toString() - .padStart(2, "0")}`; + return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`; } function addMinutes(timeStr: string, minutesToAdd: number): string { @@ -82,9 +79,7 @@ function mergeOverlappingTimeRanges(ranges: TimeRange[]): TimeRange[] { if (ranges.length <= 1) return ranges; // Sort ranges by start time - const sortedRanges = [...ranges].sort((a, b) => - a.start.localeCompare(b.start) - ); + const sortedRanges = [...ranges].sort((a, b) => a.start.localeCompare(b.start)); const merged: TimeRange[] = [sortedRanges[0]]; for (let i = 1; i < sortedRanges.length; i++) { @@ -94,8 +89,7 @@ function mergeOverlappingTimeRanges(ranges: TimeRange[]): TimeRange[] { // Check if current range overlaps with the last merged range if (current.start <= lastMerged.end) { // Merge by extending the end time if current range extends further - lastMerged.end = - current.end > lastMerged.end ? current.end : lastMerged.end; + lastMerged.end = current.end > lastMerged.end ? current.end : lastMerged.end; } else { // No overlap, add current range to merged array merged.push(current); @@ -144,9 +138,7 @@ function getMinAdvanceBookingDate( } export function getDateStringCET(date: Date): string { - return DateTime.fromJSDate(date) - .setZone("Europe/Paris") - .toFormat("yyyy-MM-dd"); + return DateTime.fromJSDate(date).setZone("Europe/Paris").toFormat("yyyy-MM-dd"); } export function generateTimeSlots( @@ -183,10 +175,7 @@ export function generateTimeSlots( } // Check minimum advance booking - const minAdvanceBooking = getMinAdvanceBookingDate( - eventTypeConfig, - currentTime - ); + const minAdvanceBooking = getMinAdvanceBookingDate(eventTypeConfig, currentTime); // Generate slots for each time range for (const range of timeRanges) { @@ -197,17 +186,13 @@ export function generateTimeSlots( const endMinutes = endTime.hours * 60 + endTime.minutes; while (currentMinutes + eventTypeConfig.duration <= endMinutes) { - const slotTime = formatTime( - Math.floor(currentMinutes / 60), - currentMinutes % 60 - ); + const slotTime = formatTime(Math.floor(currentMinutes / 60), currentMinutes % 60); // Check if slot is in the future (considering minimum advance booking) // Compare dates first, then times if on the same date const isInFuture = dateStr > minAdvanceBooking.date || - (dateStr === minAdvanceBooking.date && - slotTime >= minAdvanceBooking.time); + (dateStr === minAdvanceBooking.date && slotTime >= minAdvanceBooking.time); slots.push({ date: dateStr, @@ -233,8 +218,7 @@ export function generateTimeSlots( if (event.start_date !== dateStr || event.deleted_at) return false; const eventStart = event.start_time; - const eventEnd = - event.end_time || addMinutes(eventStart, eventTypeConfig.duration); + const eventEnd = event.end_time || addMinutes(eventStart, eventTypeConfig.duration); // Apply buffer time around the existing event const bufferedEventStart = addMinutes(eventStart, -bufferTime); diff --git a/api/src/tablo.ts b/api/src/tablo.ts index 3ee35f0..bb5488e 100644 --- a/api/src/tablo.ts +++ b/api/src/tablo.ts @@ -6,11 +6,7 @@ import type { StreamChat } from "stream-chat"; import { config } from "./config.js"; import type { Tables } from "./database.types.ts"; import { writeCalendarFileToR2 } from "./helpers.js"; -import { - authMiddleware, - r2Middleware, - streamChatMiddleware, -} from "./middleware.js"; +import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js"; import { generateToken } from "./token.js"; import { transporter } from "./transporter.js"; import type { EventInsertInTablo, TabloInsert } from "./types.ts"; @@ -170,9 +166,7 @@ tabloRouter.post("/create-and-invite", async (c) => { const { data: insertedTablo, error } = await supabase .from("tablos") .insert({ - name: `${invitedUserDataTyped.name || "Invité"} / ${ - ownerDataTyped.name || "Propriétaire" - }`, + name: `${invitedUserDataTyped.name || "Invité"} / ${ownerDataTyped.name || "Propriétaire"}`, color: "bg-blue-500", status: "todo", owner_id: ownerId, @@ -190,22 +184,20 @@ tabloRouter.post("/create-and-invite", async (c) => { } // Grant access to the current user (invited user) as a non-admin member - const { error: tabloAccessError } = await supabase - .from("tablo_access") - .insert( - { - tablo_id: tabloData.id, - user_id: user.id, - // ** IMPORTANT ** - is_admin: false, - // ------------- - is_active: true, - granted_by: ownerId, - } - // { - // onConflict: "tablo_id, user_id", - // } - ); + const { error: tabloAccessError } = await supabase.from("tablo_access").insert( + { + tablo_id: tabloData.id, + user_id: user.id, + // ** IMPORTANT ** + is_admin: false, + // ------------- + is_active: true, + granted_by: ownerId, + } + // { + // onConflict: "tablo_id, user_id", + // } + ); if (tabloAccessError) { console.error("tabloAccessError", tabloAccessError); @@ -298,8 +290,7 @@ tabloRouter.patch("/update", async (c) => { const updatedTablo = update as Tables<"tablos">; - const isUpdatingName = - tablo.name !== undefined && tablo.name !== updatedTablo.name; + const isUpdatingName = tablo.name !== undefined && tablo.name !== updatedTablo.name; if (error) { return c.json({ error: error.message }, 500); @@ -344,10 +335,7 @@ tabloRouter.delete("/delete", async (c) => { .single(); if (accessError || !tabloAccess || !tabloAccess.is_admin) { - return c.json( - { error: "You are not authorized to delete this tablo" }, - 403 - ); + return c.json({ error: "You are not authorized to delete this tablo" }, 403); } if (error) { @@ -388,10 +376,7 @@ tabloRouter.post("/invite", async (c) => { } if (tablo.owner_id !== sender.id) { - return c.json( - { error: "You are not allowed to invite users to this tablo" }, - 400 - ); + return c.json({ error: "You are not allowed to invite users to this tablo" }, 400); } const { data: introConfigData, error: introError } = await supabase @@ -425,8 +410,8 @@ tabloRouter.post("/invite", async (c) => {

Cliquez sur ce lien pour accepter l'invitation.

+ token + )}">ce lien pour accepter l'invitation.


Cordialement.

`, @@ -462,17 +447,15 @@ tabloRouter.post("/join", async (c) => { const { id: invite_id, tablo_id, invited_by } = inviteData; - const { error: tabloAccessError } = await supabase - .from("tablo_access") - .insert({ - tablo_id, - user_id: joiner.id, - // ** IMPORTANT ** - is_admin: false, - // ------------- - is_active: true, - granted_by: invited_by, - }); + const { error: tabloAccessError } = await supabase.from("tablo_access").insert({ + tablo_id, + user_id: joiner.id, + // ** IMPORTANT ** + is_admin: false, + // ------------- + is_active: true, + granted_by: invited_by, + }); if (tabloAccessError) { console.error("tabloAccessError", tabloAccessError); diff --git a/api/src/tablo_data.ts b/api/src/tablo_data.ts index aee1ea0..22b25cd 100644 --- a/api/src/tablo_data.ts +++ b/api/src/tablo_data.ts @@ -2,11 +2,7 @@ import { PutObjectCommand, type S3Client } from "@aws-sdk/client-s3"; import type { SupabaseClient, User } from "@supabase/supabase-js"; import { type Context, Hono, type Next } from "hono"; import { getTabloFileNames, isTabloAdmin, isTabloMember } from "./helpers.js"; -import { - authMiddleware, - r2Middleware, - streamChatMiddleware, -} from "./middleware.js"; +import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js"; export const tabloDataRouter = new Hono<{ Variables: { diff --git a/api/src/tasks.ts b/api/src/tasks.ts index 4aa3e87..021471e 100644 --- a/api/src/tasks.ts +++ b/api/src/tasks.ts @@ -34,7 +34,7 @@ taskRouter.post("/sync-calendars", async (c) => { token: string; tablo_id: string; tablos: { name: string }; - } + }, ]; calendarSubscriptionsData.forEach(async (subscription) => { diff --git a/api/src/user.ts b/api/src/user.ts index de84243..4cf86a3 100644 --- a/api/src/user.ts +++ b/api/src/user.ts @@ -1,20 +1,16 @@ -import type { SupabaseClient, User } from "@supabase/supabase-js"; -import { Hono } from "hono"; -import type { Transporter } from "nodemailer"; -import { StreamChat } from "stream-chat"; -import type { Tables } from "./database.types.ts"; -import { - authMiddleware, - r2Middleware, - streamChatMiddleware, -} from "./middleware.js"; -import { transporter } from "./transporter.js"; import { DeleteObjectsCommand, ListObjectsV2Command, PutObjectCommand, type S3Client, } from "@aws-sdk/client-s3"; +import type { SupabaseClient, User } from "@supabase/supabase-js"; +import { Hono } from "hono"; +import type { Transporter } from "nodemailer"; +import { StreamChat } from "stream-chat"; +import type { Tables } from "./database.types.ts"; +import { authMiddleware, r2Middleware, streamChatMiddleware } from "./middleware.js"; +import { transporter } from "./transporter.js"; export const userRouter = new Hono<{ Variables: { @@ -34,11 +30,7 @@ userRouter.post("/sign-up-to-stream", async (c) => { const { id } = c.get("user"); const supabase = c.get("supabase"); - const { data } = await supabase - .from("profiles") - .select("*") - .eq("id", id) - .single(); + const { data } = await supabase.from("profiles").select("*").eq("id", id).single(); const user = data as Tables<"profiles">; @@ -59,11 +51,7 @@ userRouter.get("/me", async (c) => { const supabase = c.get("supabase"); const streamServerClient = c.get("streamServerClient"); - const { data, error } = await supabase - .from("profiles") - .select("*") - .eq("id", user.id) - .single(); + const { data, error } = await supabase.from("profiles").select("*").eq("id", user.id).single(); const userData = data as Tables<"profiles">; @@ -212,9 +200,7 @@ userRouter.post("/profile/avatar", async (c) => { const randomString = Math.random().toString(36).substring(2, 15); const base64Content = Buffer.from(content, "base64"); - const key = `${user.id}/public_avatar_${randomString}.${ - contentType.split("/")[1] - }`; + const key = `${user.id}/public_avatar_${randomString}.${contentType.split("/")[1]}`; try { await s3Client.send( @@ -263,8 +249,7 @@ userRouter.delete("/profile/avatar", async (c) => { }) ); - if (listedObjects.Contents.length === 0) - return c.json({ error: "No objects found" }, 404); + if (listedObjects.Contents.length === 0) return c.json({ error: "No objects found" }, 404); await s3Client.send( new DeleteObjectsCommand({ diff --git a/apps/external/package.json b/apps/external/package.json index 8513522..f72fd79 100644 --- a/apps/external/package.json +++ b/apps/external/package.json @@ -6,13 +6,14 @@ "scripts": { "dev": "vite dev", "build": "tsc -b && vite build", + "deploy": "wrangler deploy", "typecheck": "tsc --noEmit", "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", "preview": "vite preview", - "deploy": "echo 'Configure deployment command for external app (e.g., wrangler pages deploy dist, vercel deploy, etc.)'", - "clean": "rm -rf dist .vite tsconfig.tsbuildinfo node_modules/.vite" + "clean": "rm -rf dist .vite tsconfig.tsbuildinfo node_modules/.vite", + "cf-typegen": "wrangler types" }, "devDependencies": { "@biomejs/biome": "2.2.5", @@ -24,7 +25,8 @@ "tw-animate-css": "^1.4.0", "typescript": "^5.7.0", "vite": "^6.2.2", - "vite-tsconfig-paths": "^5.1.4" + "vite-tsconfig-paths": "^5.1.4", + "wrangler": "^4.24.3" }, "dependencies": { "@xtablo/ui": "workspace:*", diff --git a/apps/external/src/EmbeddedBookingPage.tsx b/apps/external/src/EmbeddedBookingPage.tsx index 89ccaf9..1a7269e 100644 --- a/apps/external/src/EmbeddedBookingPage.tsx +++ b/apps/external/src/EmbeddedBookingPage.tsx @@ -25,7 +25,6 @@ import { useState } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { twMerge } from "tailwind-merge"; import { CustomModal } from "./CustomModal"; -import { LoadingSpinner } from "./LoadingSpinner"; import { api } from "./lib/api"; import { supabase } from "./lib/supabase"; import { useMaybeUser } from "./UserStoreProvider"; @@ -214,8 +213,7 @@ export function EmbeddedBookingPage() { const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1); - console.log({ shortUserId, eventTypeStandardName }); - const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots( + const { data: publicSlots } = usePublicSlots( api, shortUserId || "", eventTypeStandardName || "" @@ -454,17 +452,6 @@ export function EmbeddedBookingPage() { } }; - if (isLoadingSlots) { - return ( -
-
- -

Chargement des disponibilités...

-
-
- ); - } - return (
diff --git a/apps/external/src/FloatingBookingWidget.tsx b/apps/external/src/FloatingBookingWidget.tsx index a29fe90..5887d89 100644 --- a/apps/external/src/FloatingBookingWidget.tsx +++ b/apps/external/src/FloatingBookingWidget.tsx @@ -21,7 +21,6 @@ import { useState } from "react"; import { useParams, useSearchParams } from "react-router-dom"; import { twMerge } from "tailwind-merge"; import { CustomModal } from "./CustomModal"; -import { LoadingSpinner } from "./LoadingSpinner"; import { api } from "./lib/api"; import { supabase } from "./lib/supabase"; // import { useCreateTabloWithOwner } from "@xtablo/shared"; @@ -126,7 +125,7 @@ export function FloatingBookingWidget() { // Get color schemes based on variants const btnColors = buttonColors[buttonVariant]; - const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots( + const { data: publicSlots } = usePublicSlots( api, shortUserId || "", eventTypeStandardName || "" @@ -371,24 +370,6 @@ export function FloatingBookingWidget() { } }; - if (isLoadingSlots) { - return ( -
-
- -
-
- ); - } return (
diff --git a/apps/external/src/LoadingSpinner.tsx b/apps/external/src/LoadingSpinner.tsx deleted file mode 100644 index be7da0d..0000000 --- a/apps/external/src/LoadingSpinner.tsx +++ /dev/null @@ -1,12 +0,0 @@ -export const LoadingSpinner = () => { - return ( -
- Loading... -
- ); -}; diff --git a/apps/external/src/UserStoreProvider.tsx b/apps/external/src/UserStoreProvider.tsx index 876625d..8861f98 100644 --- a/apps/external/src/UserStoreProvider.tsx +++ b/apps/external/src/UserStoreProvider.tsx @@ -3,7 +3,6 @@ import { useSession } from "@xtablo/shared/contexts/SessionContext"; import { Tables } from "@xtablo/shared/types/database.types"; import React from "react"; import { createStore, StoreApi, useStore } from "zustand"; -import { LoadingSpinner } from "./LoadingSpinner"; import { api } from "./lib/api"; export type User = Tables<"profiles"> & { @@ -34,7 +33,7 @@ export const UserStoreProvider = ({ children }: { children: React.ReactNode }) = }); if (isPending && shouldFetchUser) { - return ; + return
Loading...
; } if (!user) { diff --git a/apps/external/tsconfig.tsbuildinfo b/apps/external/tsconfig.tsbuildinfo index bf0b063..4bb6700 100644 --- a/apps/external/tsconfig.tsbuildinfo +++ b/apps/external/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/custommodal.tsx","./src/embeddedbookingpage.tsx","./src/floatingbookingwidget.tsx","./src/loadingspinner.tsx","./src/userstoreprovider.tsx","./src/main.tsx","./src/routes.tsx","./src/vite-env.d.ts","./src/lib/api.ts","./src/lib/supabase.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/custommodal.tsx","./src/embeddedbookingpage.tsx","./src/floatingbookingwidget.tsx","./src/userstoreprovider.tsx","./src/main.tsx","./src/routes.tsx","./src/vite-env.d.ts","./src/lib/api.ts","./src/lib/supabase.ts"],"version":"5.9.3"} \ No newline at end of file diff --git a/apps/external/turbo.json b/apps/external/turbo.json new file mode 100644 index 0000000..3ed9f2c --- /dev/null +++ b/apps/external/turbo.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://turbo.build/schema.json", + "extends": ["//"], + "tasks": { + "deploy": { + "dependsOn": ["build"], + "cache": false, + "outputLogs": "new-only" + } + } +} + diff --git a/apps/external/worker/index.d.ts b/apps/external/worker/index.d.ts new file mode 100644 index 0000000..3f522fb --- /dev/null +++ b/apps/external/worker/index.d.ts @@ -0,0 +1,4 @@ +declare const _default: { + fetch(request: any): Response; +}; +export default _default; diff --git a/apps/external/worker/index.ts b/apps/external/worker/index.ts new file mode 100644 index 0000000..8f89377 --- /dev/null +++ b/apps/external/worker/index.ts @@ -0,0 +1,16 @@ +// @ts-nocheck +// biome-ignore-file: Worker entry point with dynamic imports +/* eslint-disable */ + +export default { + fetch(request) { + const url = new URL(request.url); + + if (url.pathname.startsWith("/api/")) { + return Response.json({ + name: "Cloudflare", + }); + } + return new Response(null, { status: 404 }); + }, +}; diff --git a/apps/external/wrangler.toml b/apps/external/wrangler.toml new file mode 100644 index 0000000..f16b844 --- /dev/null +++ b/apps/external/wrangler.toml @@ -0,0 +1,17 @@ +name = "xtablo-embeds" +main = "worker/index.ts" +compatibility_date = "2025-07-09" + +[assets] +directory = "./dist/" +not_found_handling = "single-page-application" + +[observability] +enabled = true + +[vars] +PYTHON_VERSION = "3.11.5" + +[[routes]] +pattern = "embed.xtablo.com" +custom_domain = true diff --git a/apps/main/package.json b/apps/main/package.json index 25414fe..25b914f 100644 --- a/apps/main/package.json +++ b/apps/main/package.json @@ -13,7 +13,8 @@ "preview": "vite preview", "build:staging": "tsc -b && vite build --mode staging", "build:prod": "tsc -b && vite build --mode production", - "deploy": "wrangler deploy --env=\"\"", + "deploy:staging": "wrangler deploy --env=\"\"", + "deploy:prod": "wrangler deploy --env=\"\"", "cf-typegen": "wrangler types", "test": "vitest run --mode dev --passWithNoTests", "test:watch": "vitest watch --passWithNoTests", diff --git a/apps/main/src/components/ProtectedRoute.tsx b/apps/main/src/components/ProtectedRoute.tsx index 30690f7..fc045e8 100644 --- a/apps/main/src/components/ProtectedRoute.tsx +++ b/apps/main/src/components/ProtectedRoute.tsx @@ -42,7 +42,7 @@ export const ProtectedRoute = ({ fallback, shouldRedirectToCurrentPage }: Protec return match(status) .with("loading", () => ) - .with("should-land-user", () => ) + .with("should-land-user", () => ) .with("should-redirect", () => ) .with("should-pass", () => ) .exhaustive(); diff --git a/apps/main/stats.html b/apps/main/stats.html index 6902f6c..5a67cd6 100644 --- a/apps/main/stats.html +++ b/apps/main/stats.html @@ -4929,7 +4929,7 @@ var drawChart = (function (exports) {