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 (
-
-

-
- );
-};
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) {