diff --git a/.gitignore b/.gitignore index 882155b..2582c5c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,7 @@ __pycache__/ .pytest_cache/ .coverage htmlcov/ + +.turbo +dist +.wrangler \ No newline at end of file diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..2329c93 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,329 @@ +# Development Guide + +This monorepo uses [Turborepo](https://turbo.build/) to manage builds, development, and testing across multiple packages and applications. + +## Repository Structure + +``` +xtablo-source/ +├── apps/ +│ ├── main/ # Main application (@xtablo/main) +│ └── external/ # External application (@xtablo/external) +├── packages/ +│ ├── shared/ # Shared utilities and logic (@xtablo/shared) +│ └── ui/ # UI component library (@xtablo/ui) +``` + +## Prerequisites + +- Node.js >= 18 +- pnpm >= 10.19.0 + +## Getting Started + +```bash +# Install dependencies +pnpm install + +# Start development (packages are source-only, no build needed) +pnpm dev +``` + +## Available Commands + +### Building + +```bash +# Build all apps (packages are source-only, consumed directly by bundlers) +pnpm build + +# Build only apps (main, external) +pnpm build:apps + +# Build main app for specific environments +pnpm build:staging # Build for staging +pnpm build:prod # Build for production +``` + +**Note:** The `@xtablo/shared` and `@xtablo/ui` packages are source-only packages. They export TypeScript source files directly and are consumed by app bundlers (Vite) without a separate build step. This is faster and simpler for development. + +**Environment Builds:** The main app supports environment-specific builds (`staging`, `production`) that are properly cached by Turborepo based on environment-specific inputs (`.env.staging`, `.env.production`, etc.). + +### Development + +```bash +# Run all apps in development mode +pnpm dev + +# Run specific app +pnpm dev:main # Main app only +pnpm dev:external # External app only +``` + +**Note:** Since packages are source-only, there's no need to run them in watch mode. Changes to package files are instantly picked up by the app's bundler (Vite) through hot module replacement. + +### Testing + +```bash +# Run all tests +pnpm test + +# Run tests in watch mode +pnpm test:watch + +# Run tests for specific package +cd apps/main && pnpm test +cd apps/main && pnpm test:watch +cd apps/main && pnpm test:coverage +``` + +### Linting & Formatting + +```bash +# Check all packages +pnpm lint + +# Fix linting issues +pnpm lint:fix + +# Format code +pnpm format + +# Type checking +pnpm typecheck +``` + +### Cleaning + +```bash +# Clean all build artifacts +pnpm clean + +# Clean specific app +cd apps/main && pnpm clean +``` + +## Turborepo Features + +### Smart Caching + +Turborepo caches build outputs and skips unnecessary work: + +- Build outputs are cached based on input files +- If nothing changed, builds are instant +- Cache is shared across the team (when configured) + +### Parallel Execution + +Tasks run in parallel when possible: + +```bash +# Runs lint on all packages simultaneously +pnpm lint +``` + +### Task Dependencies + +Turborepo automatically handles task dependencies: + +- `build` depends on `^build` (builds dependencies first, though packages are source-only) +- Tasks run in topological order based on package dependencies + +### Filtering + +Run commands for specific packages: + +```bash +# Build only main app and its dependencies +turbo build --filter=@xtablo/main + +# Build only packages +turbo build --filter='./packages/*' + +# Build everything except external +turbo build --filter='!@xtablo/external' +``` + +### Package-Level Configuration + +Packages can have their own `turbo.json` file to define custom tasks or override root configuration: + +**Example:** `apps/main/turbo.json` defines environment-specific builds: + +- `build:staging` - Builds for staging with `.env.staging` +- `build:prod` - Builds for production with `.env.production` + +Each task: + +- Extends the root config with `"extends": ["//"]` +- Defines specific inputs (including environment files) +- Configures caching with appropriate outputs +- Can pass environment variables to the build process + +## Package Development Workflow + +### 1. Working on Packages + +Since packages are source-only, just run your app and edit package files directly: + +```bash +# Just run your app - changes to packages are instantly reflected +pnpm dev:main + +# Or run all apps +pnpm dev +``` + +The app's bundler (Vite) will automatically detect changes in `packages/shared` and `packages/ui` and hot-reload them. + +### 2. Adding a New Package + +1. Create the package in `packages/` +2. For source-only packages (like shared/ui), add these scripts to `package.json`: + ```json + { + "scripts": { + "lint": "biome check .", + "lint:fix": "biome check --write .", + "format": "biome format --write .", + "typecheck": "tsc --noEmit" + } + } + ``` +3. Set up exports in `package.json`: + ```json + { + "main": "./src/index.ts", + "types": "./src/index.ts", + "exports": { + ".": "./src/index.ts" + } + } + ``` +4. Add to workspace in `pnpm-workspace.yaml` (if not already included by glob) +5. Run `pnpm install` to link the package + +### 3. Testing Changes Across Packages + +When making changes that affect multiple packages: + +```bash +# Type check everything +pnpm typecheck + +# Run all tests +pnpm test + +# Lint everything +pnpm lint +``` + +## Performance Tips + +1. **Source-Only Packages**: Packages consume TypeScript directly for instant feedback +2. **Selective Builds**: Use filters to build only the apps you need +3. **Clean When Stuck**: Run `pnpm clean` if you encounter weird caching issues +4. **Hot Module Replacement**: Vite provides instant updates when editing package files + +## Troubleshooting + +### Build Errors + +If you encounter build errors: + +```bash +# Clean everything and rebuild +pnpm clean +pnpm install +pnpm build +``` + +### Type Errors in Apps + +If apps show type errors for packages: + +```bash +# Check TypeScript configuration in packages +cd packages/shared && pnpm typecheck +cd packages/ui && pnpm typecheck + +# Restart your IDE's TypeScript server +# In VS Code: Cmd+Shift+P > "TypeScript: Restart TS Server" +``` + +### Cache Issues + +If builds seem stale: + +```bash +# Clear Turbo cache +rm -rf node_modules/.cache/turbo + +# Clean and rebuild +pnpm clean +pnpm build +``` + +## CI/CD Considerations + +### Basic Pipeline + +For CI/CD pipelines: + +```bash +# Install dependencies +pnpm install --frozen-lockfile + +# Build everything +pnpm build + +# Run all checks +pnpm lint +pnpm typecheck +pnpm test +``` + +### Environment-Specific Deployments + +**Staging Pipeline:** + +```bash +# Install dependencies +pnpm install --frozen-lockfile + +# Build for staging +pnpm build:staging + +# Deploy (example with wrangler) +cd apps/main && pnpm deploy:staging +``` + +**Production Pipeline:** + +```bash +# Install dependencies +pnpm install --frozen-lockfile + +# Run all checks +pnpm lint +pnpm typecheck +pnpm test + +# Build for production +pnpm build:prod + +# Deploy (example with wrangler) +cd apps/main && pnpm deploy:prod +``` + +**Benefits:** + +- Turborepo caches builds per environment +- Environment-specific `.env` files are tracked as inputs +- Builds are only re-run when relevant files change + +## Additional Resources + +- [Turborepo Documentation](https://turbo.build/repo/docs) +- [pnpm Workspaces](https://pnpm.io/workspaces) +- [TypeScript Project References](https://www.typescriptlang.org/docs/handbook/project-references.html) diff --git a/README.md b/README.md new file mode 100644 index 0000000..3605441 --- /dev/null +++ b/README.md @@ -0,0 +1,156 @@ +# Xtablo Monorepo + +This is a Turborepo-based monorepo for the Xtablo project, containing multiple apps and shared packages. + +## Project Structure + +``` +xtablo-source/ +├── apps/ +│ ├── main/ # Main UI application +│ └── external/ # External booking widget microfrontend +├── packages/ +│ ├── ui/ # Shared UI components (buttons, inputs, etc.) +│ └── shared/ # Shared utilities, hooks, contexts, and types +├── api/ # TypeScript/Node.js API +├── backend/ # Python backend +├── go_backend/ # Go backend +└── xtablo-expo/ # React Native Expo app +``` + +## Getting Started + +### Prerequisites + +- Node.js 18+ and pnpm 10.19.0+ +- For other services: Python 3.11+, Go 1.21+ + +### Quick Start + +```bash +# Install dependencies +pnpm install + +# Start development (packages are source-only, no build needed) +pnpm dev +``` + +For detailed development workflows, see [DEVELOPMENT.md](./DEVELOPMENT.md). + +### Common Commands + +```bash +# Development +pnpm dev # Run all apps +pnpm dev:main # Run main app only +pnpm dev:external # Run external app only + +# Building +pnpm build # Build all apps +pnpm build:apps # Build apps only +pnpm build:staging # Build main app for staging +pnpm build:prod # Build main app for production + +# Testing & Quality +pnpm test # Run all tests +pnpm lint # Check all packages +pnpm lint:fix # Fix linting issues +pnpm typecheck # Type check everything + +# Cleaning +pnpm clean # Clean all build artifacts +``` + +## Packages + +### @xtablo/ui + +Shared UI components library used across the main and external apps. Contains all base UI components like buttons, inputs, dialogs, etc. + +**Usage:** + +```typescript +import { Button } from "@xtablo/ui/components/button"; +import { Input } from "@xtablo/ui/components/input"; +``` + +### @xtablo/shared + +Shared utilities, hooks, contexts, and types used across apps. + +**Usage:** + +```typescript +import { useSession } from "@xtablo/shared/contexts/SessionContext"; +import { api } from "@xtablo/shared/lib/api"; +import { Tables } from "@xtablo/shared/types/database.types"; +``` + +## Apps + +### Main (@xtablo/main) + +The main Xtablo application with full dashboard, planning, chat, and administrative features. + +**Local URL:** http://localhost:5173 + +### External (@xtablo/external) + +Embeddable booking widget that can be integrated into external websites. Supports both embedded and floating widget modes. + +**Local URL:** http://localhost:5174 + +**Usage:** + +- Embedded mode: `?mode=embed&eventTypeId=...` +- Floating widget: `?mode=widget&eventTypeId=...` + +## Turborepo Features + +This monorepo uses Turborepo for: + +- **Fast builds**: Parallel task execution and intelligent caching +- **Dependency management**: Automatic build ordering based on package dependencies +- **Code sharing**: Easy sharing of components and utilities between apps + +## Development Workflow + +For comprehensive development documentation including: + +- Package development workflow +- Testing strategies +- Troubleshooting guide +- CI/CD setup + +See [DEVELOPMENT.md](./DEVELOPMENT.md) + +## Adding a New Package + +See the "Adding a New Package" section in [DEVELOPMENT.md](./DEVELOPMENT.md) for detailed instructions. + +## Migration Notes + +This project was migrated from a single UI app to a Turborepo monorepo with the following changes: + +- **Before**: Single `ui/` directory with all code +- **After**: + - `apps/main/` - Main application + - `apps/external/` - Separate microfrontend for booking widgets + - `packages/ui/` - Shared UI components + - `packages/shared/` - Shared utilities and logic + +All import paths have been updated to use workspace packages (`@xtablo/ui`, `@xtablo/shared`). + +## Contributing + +When adding new shared code: + +1. Add to the appropriate package (`ui` for UI components, `shared` for logic/utils) +2. Export from the package's appropriate entry point +3. Use the workspace import in your apps + +For more details, see [DEVELOPMENT.md](./DEVELOPMENT.md) + +## License + +[Your License Here] diff --git a/api/.env.development b/api/.env.development index 8e2eb2b..20ef2b3 100644 --- a/api/.env.development +++ b/api/.env.development @@ -6,7 +6,8 @@ STREAM_CHAT_API_SECRET=zrr32sqenw3atpv9rnz2nhhyyncf7bunr7fmfqy9r7e69fcw978dhzevm XTABLO_URL="https://app-staging.xtablo.com" -CORS_ORIGIN="http://localhost:5173" + +CORS_ORIGIN="http://localhost:5173,http://localhost:5174" R2_ACCOUNT_ID="9715fa14c5e5d1612301572cf1c6bbee" R2_ACCESS_KEY_ID="caeb987bbcd601708a93c6aa562064ef" diff --git a/api/src/config.ts b/api/src/config.ts index b2cf36f..f2d688e 100644 --- a/api/src/config.ts +++ b/api/src/config.ts @@ -16,7 +16,7 @@ export interface AppConfig { R2_ACCOUNT_ID: string; R2_ACCESS_KEY_ID: string; R2_SECRET_ACCESS_KEY: string; - CORS_ORIGIN: string[]; + CORS_ORIGIN: string; LOG_LEVEL: "debug" | "info" | "warn" | "error"; SYNC_CALS_SECRET: string; } @@ -47,20 +47,38 @@ 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), - CORS_ORIGIN: [process.env.CORS_ORIGIN || "https://app.xtablo.com"], + 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/index.ts b/api/src/index.ts index 3b46319..3c23040 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -17,7 +17,7 @@ app.use(logger()); app.use("*", async (c, next) => { const corsMiddleware = cors({ - origin: config.CORS_ORIGIN, + origin: config.CORS_ORIGIN.split(","), allowHeaders: [ "Authorization", "Content-Type", diff --git a/apps/external/biome.json b/apps/external/biome.json new file mode 100644 index 0000000..7d53391 --- /dev/null +++ b/apps/external/biome.json @@ -0,0 +1,299 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false }, + "files": { + "ignoreUnknown": true, + "includes": ["src/**/*", "*.{ts,tsx,js,jsx,json}"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "attributePosition": "auto" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noAdjacentSpacesInRegex": "error", + "noBannedTypes": "error", + "noExtraBooleanCast": "error", + "noUselessCatch": "error", + "noUselessEscapeInRegex": "error", + "noUselessTypeConstraint": "error" + }, + "correctness": { + "noChildrenProp": "error", + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInvalidBuiltinInstantiation": "error", + "noInvalidConstructorSuper": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedPrivateClassMembers": "error", + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useIsNan": "error", + "useJsxKeyInIterable": "error", + "useValidForDirection": "error", + "useValidTypeof": "error", + "useYield": "error" + }, + "nursery": {}, + "security": { "noDangerouslySetInnerHtmlWithChildren": "error" }, + "style": { + "noCommonJs": "error", + "noNamespace": "error", + "useArrayLiterals": "error", + "useAsConstAssertion": "error", + "useConst": "error", + "useTemplate": "error" + }, + "suspicious": { + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCommentText": "error", + "noCompareNegZero": "error", + "noConstantBinaryExpressions": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateElseIf": "error", + "noDuplicateJsxProps": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "error", + "noExplicitAny": "error", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noIrregularWhitespace": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noSparseArray": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "noUselessRegexBackrefs": "error", + "noWith": "error", + "useGetterReturn": "error", + "useNamespaceKeyword": "error" + } + } + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "double", + "attributePosition": "auto", + "bracketSpacing": true + }, + "globals": [ + "onanimationend", + "ongamepadconnected", + "onlostpointercapture", + "onanimationiteration", + "onkeyup", + "onmousedown", + "onanimationstart", + "onslotchange", + "onprogress", + "ontransitionstart", + "onpause", + "onended", + "onpointerover", + "onscrollend", + "onformdata", + "ontransitionrun", + "onanimationcancel", + "ondrag", + "onchange", + "onbeforeinstallprompt", + "onbeforexrselect", + "onmessage", + "ontransitioncancel", + "onpointerdown", + "onabort", + "onpointerout", + "oncuechange", + "ongotpointercapture", + "onscrollsnapchanging", + "onsearch", + "onsubmit", + "onstalled", + "onsuspend", + "onreset", + "onerror", + "onresize", + "onmouseenter", + "ongamepaddisconnected", + "ondragover", + "onbeforetoggle", + "onmouseover", + "onpagehide", + "onmousemove", + "onratechange", + "onmessageerror", + "onwheel", + "ondevicemotion", + "onauxclick", + "ontransitionend", + "onpaste", + "onpageswap", + "ononline", + "ondeviceorientationabsolute", + "onkeydown", + "onclose", + "onselect", + "onpageshow", + "onpointercancel", + "onbeforematch", + "onpointerrawupdate", + "ondragleave", + "onscrollsnapchange", + "onseeked", + "onwaiting", + "onbeforeunload", + "onplaying", + "onvolumechange", + "ondragend", + "onstorage", + "onloadeddata", + "onfocus", + "onoffline", + "onplay", + "onafterprint", + "onclick", + "oncut", + "onmouseout", + "ondblclick", + "oncanplay", + "onloadstart", + "onappinstalled", + "onpointermove", + "ontoggle", + "oncontextmenu", + "onblur", + "oncancel", + "onbeforeprint", + "oncontextrestored", + "onloadedmetadata", + "onpointerup", + "onlanguagechange", + "oncopy", + "onselectstart", + "onscroll", + "onload", + "ondragstart", + "onbeforeinput", + "oncanplaythrough", + "oninput", + "oninvalid", + "ontimeupdate", + "ondurationchange", + "onselectionchange", + "onmouseup", + "location", + "onkeypress", + "onpointerleave", + "oncontextlost", + "ondrop", + "onsecuritypolicyviolation", + "oncontentvisibilityautostatechange", + "ondeviceorientation", + "onseeking", + "onrejectionhandled", + "onunload", + "onmouseleave", + "onhashchange", + "onpointerenter", + "onmousewheel", + "onunhandledrejection", + "ondragenter", + "onpopstate", + "onpagereveal", + "onemptied" + ] + }, + "json": { + "parser": { "allowComments": true, "allowTrailingCommas": false }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "trailingCommas": "none" + } + }, + "overrides": [ + { "linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } } }, + { "linter": { "rules": { "style": { "useNodejsImportProtocol": "off" } } } }, + { + "linter": { + "rules": { + "style": { "useNodejsImportProtocol": "off" }, + "suspicious": { "noExplicitAny": "off" } + } + } + }, + { + "includes": ["src/**/*.{ts,tsx}", "*.{ts,tsx}"], + "linter": { + "rules": { + "complexity": { "noArguments": "error" }, + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidBuiltinInstantiation": "off", + "noInvalidConstructorSuper": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { "useConst": "error" }, + "suspicious": { + "noClassAssign": "off", + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "noVar": "error", + "useGetterReturn": "off" + } + } + } + } + ] +} diff --git a/apps/external/index.html b/apps/external/index.html new file mode 100644 index 0000000..f49f514 --- /dev/null +++ b/apps/external/index.html @@ -0,0 +1,12 @@ + + + + + + Xtablo External + + +
+ + + diff --git a/apps/external/package.json b/apps/external/package.json new file mode 100644 index 0000000..8513522 --- /dev/null +++ b/apps/external/package.json @@ -0,0 +1,42 @@ +{ + "name": "@xtablo/external", + "private": true, + "version": "0.0.0", + "type": "module", + "scripts": { + "dev": "vite dev", + "build": "tsc -b && vite build", + "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" + }, + "devDependencies": { + "@biomejs/biome": "2.2.5", + "@tailwindcss/vite": "^4.0.14", + "@types/react": "19.0.10", + "@types/react-dom": "19.0.4", + "@vitejs/plugin-react": "^4.3.4", + "tailwindcss": "^4.0.14", + "tw-animate-css": "^1.4.0", + "typescript": "^5.7.0", + "vite": "^6.2.2", + "vite-tsconfig-paths": "^5.1.4" + }, + "dependencies": { + "@xtablo/ui": "workspace:*", + "@xtablo/shared": "workspace:*", + "@tanstack/react-query": "^5.69.0", + "clsx": "^2.1.1", + "lucide-react": "^0.460.0", + "react": "19.0.0", + "react-dom": "19.0.0", + "react-router-dom": "^7.9.4", + "tailwind-merge": "^3.0.2", + "ts-pattern": "^5.6.2", + "zustand": "^5.0.5" + } +} diff --git a/ui/public/icon.jpg b/apps/external/public/icon.jpg similarity index 100% rename from ui/public/icon.jpg rename to apps/external/public/icon.jpg diff --git a/ui/public/logo_dark.png b/apps/external/public/logo_dark.png similarity index 100% rename from ui/public/logo_dark.png rename to apps/external/public/logo_dark.png diff --git a/ui/public/logo_white.png b/apps/external/public/logo_white.png similarity index 100% rename from ui/public/logo_white.png rename to apps/external/public/logo_white.png diff --git a/ui/public/staging_icon.jpg b/apps/external/public/staging_icon.jpg similarity index 100% rename from ui/public/staging_icon.jpg rename to apps/external/public/staging_icon.jpg diff --git a/ui/public/vite.svg b/apps/external/public/vite.svg similarity index 100% rename from ui/public/vite.svg rename to apps/external/public/vite.svg diff --git a/ui/src/components/CustomModal.tsx b/apps/external/src/CustomModal.tsx similarity index 94% rename from ui/src/components/CustomModal.tsx rename to apps/external/src/CustomModal.tsx index c52c0ea..e19f466 100644 --- a/ui/src/components/CustomModal.tsx +++ b/apps/external/src/CustomModal.tsx @@ -1,5 +1,5 @@ -import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@ui/components/ui/dialog"; -import { cn } from "@ui/lib/utils"; +import { cn } from "@xtablo/shared"; +import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog"; // Custom Modal Component - now using shadcn/ui Dialog interface CustomModalProps { diff --git a/ui/src/pages/EmbeddedBookingPage.tsx b/apps/external/src/EmbeddedBookingPage.tsx similarity index 94% rename from ui/src/pages/EmbeddedBookingPage.tsx rename to apps/external/src/EmbeddedBookingPage.tsx index e0b0b4f..89ccaf9 100644 --- a/ui/src/pages/EmbeddedBookingPage.tsx +++ b/apps/external/src/EmbeddedBookingPage.tsx @@ -1,16 +1,18 @@ -import { CustomModal } from "@ui/components/CustomModal"; -import { LoadingSpinner } from "@ui/components/LoadingSpinner"; -import { Button } from "@ui/components/ui/button"; -import { FieldError } from "@ui/components/ui/field"; -import { Input } from "@ui/components/ui/input"; -import { Label } from "@ui/components/ui/label"; -import { Text, TypographyH3, TypographyH4, TypographyMuted } from "@ui/components/ui/typography"; -import { useSession } from "@ui/contexts/SessionContext"; -import { useSignUpWithoutPassword } from "@ui/hooks/auth"; -import { TimeSlot, usePublicSlots } from "@ui/hooks/public"; -import { useCreateTabloWithOwner } from "@ui/hooks/tablos"; -import { useMaybeUser } from "@ui/providers/UserStoreProvider"; -import { EventInsertInTablo } from "@ui/types/events.types"; +import { useCreateTabloWithOwner } from "@xtablo/shared"; +import { useSession } from "@xtablo/shared/contexts/SessionContext"; +import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth"; +import { TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public"; +import { EventInsertInTablo } from "@xtablo/shared/types/events.types"; +import { Button } from "@xtablo/ui/components/button"; +import { FieldError } from "@xtablo/ui/components/field"; +import { Input } from "@xtablo/ui/components/input"; +import { Label } from "@xtablo/ui/components/label"; +import { + Text, + TypographyH3, + TypographyH4, + TypographyMuted, +} from "@xtablo/ui/components/typography"; import { CalendarIcon, ChevronLeftIcon, @@ -22,6 +24,11 @@ import { 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"; type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red"; @@ -187,16 +194,14 @@ const getMutedTextColorFromBackground = (variant: ColorVariant): string => { }; export function EmbeddedBookingPage() { - const { user_info, event_type_standard_name } = useParams<{ - user_info: string; - event_type_standard_name: string; - }>(); + const params = useParams(); const [searchParams] = useSearchParams(); - const { mutateAsync: signUpWithoutPassword } = useSignUpWithoutPassword(); + const { mutateAsync: signUpWithoutPassword } = useSignUpWithoutPassword(supabase, api); const { session } = useSession(); const user = useMaybeUser(); - const shortUserId = user_info?.substring(user_info.lastIndexOf("-") + 1); + const userInfo = params.userInfo as string; + const eventTypeStandardName = params.eventTypeStandardName as string; // Get variants from URL params or props, with fallback to purple const backgroundVariant = (searchParams.get("backgroundVariant") as ColorVariant) || "black"; const buttonVariant = (searchParams.get("buttonVariant") as ColorVariant) || "purple"; @@ -207,12 +212,18 @@ export function EmbeddedBookingPage() { const txtColor = getTextColorFromBackground(backgroundVariant); const mutedTxtColor = getMutedTextColorFromBackground(backgroundVariant); + const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1); + + console.log({ shortUserId, eventTypeStandardName }); const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots( + api, shortUserId || "", - event_type_standard_name || "" + eventTypeStandardName || "" ); - const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(); + const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, () => { + handleCloseModal(); + }); const userProfile = publicSlots?.user; const eventType = publicSlots?.eventType; @@ -460,7 +471,7 @@ export function EmbeddedBookingPage() { {/* Left Side - Event Details */}
@@ -512,7 +523,7 @@ export function EmbeddedBookingPage() {
{ + handleCloseModal(); + setIsWidgetOpen(false); + }); + + const userProfile = publicSlots?.user; + const eventType = publicSlots?.eventType; + const slotsData = publicSlots?.slots || {}; + + // Widget state + const [isWidgetOpen, setIsWidgetOpen] = useState(false); + + // Calendar state + const [currentDate, setCurrentDate] = useState(new Date()); + const [selectedDate, setSelectedDate] = useState(null); + + // Modal state (for booking confirmation) + const [isModalOpen, setIsModalOpen] = useState(false); + const [selectedSlot, setSelectedSlot] = useState<{ + date: Date; + slot: TimeSlot; + } | null>(null); + const [formData, setFormData] = useState({ + email: "", + name: "", + }); + const [formErrors, setFormErrors] = useState({ + email: "", + name: "", + }); + + // Helper function to convert date to CET timezone string (YYYY-MM-DD) + const formatDateToCET = (date: Date): string => { + return date.toLocaleDateString("sv-SE", { timeZone: "Europe/Paris" }); + }; + + // Helper function to get current date in CET timezone + const getCurrentDateInCET = (): Date => { + const now = new Date(); + const cetTime = new Date(now.toLocaleString("en-US", { timeZone: "Europe/Paris" })); + return cetTime; + }; + + // Get available time slots for a specific date + const getAvailableSlots = (date: Date): TimeSlot[] => { + const dateStr = formatDateToCET(date); + return slotsData[dateStr]?.filter((slot) => slot.available) || []; + }; + + // Check if a date has any available slots + const hasAvailableSlots = (date: Date): boolean => { + const dateStr = formatDateToCET(date); + return slotsData[dateStr]?.some((slot) => slot.available) || false; + }; + + // Calendar helper functions + const getDaysInMonth = (date: Date) => { + const year = date.getFullYear(); + const month = date.getMonth(); + + // Create first day of month and get its day of week in CET + const firstDayStr = `${year}-${String(month + 1).padStart(2, "0")}-01`; + const firstDay = new Date(`${firstDayStr}T12:00:00`); + const firstDayOfWeekInCET = new Date( + firstDay.toLocaleString("en-US", { timeZone: "Europe/Paris" }) + ).getDay(); + + // Adjust for Monday as first day of week + const mondayStartingDay = firstDayOfWeekInCET === 0 ? 6 : firstDayOfWeekInCET - 1; + + // Get number of days in month + const lastDay = new Date(year, month + 1, 0); + const daysInMonth = lastDay.getDate(); + + const days = []; + + // Add empty cells for days before the first day of the month + for (let i = 0; i < mondayStartingDay; i++) { + days.push(null); + } + + // Add all days of the month + for (let day = 1; day <= daysInMonth; day++) { + const dayStr = `${year}-${String(month + 1).padStart(2, "0")}-${String(day).padStart( + 2, + "0" + )}`; + days.push(new Date(`${dayStr}T12:00:00`)); + } + + return days; + }; + + const navigateMonth = (direction: "prev" | "next") => { + setCurrentDate((prev) => { + const newDate = new Date(prev); + if (direction === "prev") { + newDate.setMonth(prev.getMonth() - 1); + } else { + newDate.setMonth(prev.getMonth() + 1); + } + return newDate; + }); + }; + + const isToday = (date: Date) => { + const todayInCET = getCurrentDateInCET(); + const todayStr = formatDateToCET(todayInCET); + const dateStr = formatDateToCET(date); + return dateStr === todayStr; + }; + + const isPastDate = (date: Date) => { + const todayInCET = getCurrentDateInCET(); + const todayStr = formatDateToCET(todayInCET); + const dateStr = formatDateToCET(date); + return dateStr < todayStr; + }; + + const formatMonthYear = (date: Date) => { + return date.toLocaleDateString("fr-FR", { month: "long", year: "numeric" }); + }; + + const formatDuration = (minutes: number) => { + if (minutes < 60) { + return `${minutes} min`; + } + const hours = Math.floor(minutes / 60); + const remainingMinutes = minutes % 60; + if (remainingMinutes === 0) { + return `${hours}h`; + } + return `${hours}h ${remainingMinutes}min`; + }; + + // Modal and form handlers + const handleSlotClick = (date: Date, slot: TimeSlot) => { + setSelectedSlot({ date, slot }); + setIsModalOpen(true); + setFormData({ email: "", name: "" }); + setFormErrors({ email: "", name: "" }); + }; + + const handleCloseModal = () => { + setIsModalOpen(false); + setSelectedSlot(null); + setFormData({ email: "", name: "" }); + setFormErrors({ email: "", name: "" }); + }; + + const validateForm = () => { + const errors = { email: "", name: "" }; + let isValid = true; + + if (!formData.email.trim()) { + errors.email = "L'adresse email est requise"; + isValid = false; + } else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(formData.email)) { + errors.email = "Veuillez entrer une adresse email valide"; + isValid = false; + } + + if (!formData.name.trim()) { + errors.name = "Le nom est requis"; + isValid = false; + } + + setFormErrors(errors); + return isValid; + }; + + // Calculate end time based on start time and duration + const calculateEndTime = (startTime: string, durationMinutes: number): string => { + if (!startTime) return ""; + + const [hours, minutes] = startTime.split(":").map(Number); + const startDate = new Date(); + startDate.setHours(hours, minutes, 0, 0); + + const endDate = new Date(startDate.getTime() + durationMinutes * 60000); + + return endDate.toTimeString().slice(0, 5); // Format as HH:MM + }; + + const handleSubmitIfNotLoggedIn = async () => { + if (validateForm()) { + const { session: sessionFromSignUp } = await signUpWithoutPassword({ + email: formData.email, + name: formData.name, + }); + + const startTime = selectedSlot?.slot.time || ""; + const duration = eventType?.duration || 60; // duration in minutes + const endTime = calculateEndTime(startTime, duration); + + await createTabloWithOwner({ + name: eventType?.name || "", + status: "todo", + owner_short_id: shortUserId || "", + event: { + description: eventType?.description || "", + end_time: endTime || "", + start_date: selectedSlot?.slot.date || "", + start_time: selectedSlot?.slot.time || "", + title: eventType?.name || "", + } as EventInsertInTablo, + access_token: sessionFromSignUp?.access_token || "", + }); + + handleCloseModal(); + setIsWidgetOpen(false); + } + }; + + const handleSubmitIfLoggedIn = async () => { + if (user) { + const startTime = selectedSlot?.slot.time || ""; + const duration = eventType?.duration || 60; // duration in minutes + const endTime = calculateEndTime(startTime, duration); + + await createTabloWithOwner({ + name: eventType?.name || "", + status: "todo", + owner_short_id: shortUserId || "", + event: { + description: eventType?.description || "", + end_time: endTime || "", + start_date: selectedSlot?.slot.date || "", + start_time: selectedSlot?.slot.time || "", + title: eventType?.name || "", + } as EventInsertInTablo, + access_token: session?.access_token || "", + }); + + handleCloseModal(); + setIsWidgetOpen(false); + } + }; + + if (isLoadingSlots) { + return ( +
+
+ +
+
+ ); + } + + return ( +
+ {/* Floating Button */} +
+ +
+ + {/* Floating Widget Popup */} + {isWidgetOpen && ( +
+ {/* Header */} +
+
+ {(userProfile as { name: string; avatar_url?: string })?.avatar_url ? ( + {userProfile?.name + ) : ( +
+ +
+ )} +
+

+ {eventType?.name || "Type d'événement"} +

+

+ {userProfile?.name || "Professionnel"} +

+
+
+ +
+ + {/* Event Info */} + {(eventType?.duration || eventType?.location) && ( +
+
+ {eventType?.duration && ( +
+ + {formatDuration(eventType.duration)} +
+ )} + {eventType?.location && ( +
+ + {eventType.location} +
+ )} +
+
+ )} + + {/* Calendar and Slots */} +
+ {/* Calendar */} +
+
+ + {formatMonthYear(currentDate)} + +
+ + +
+
+ + {/* Calendar Grid */} +
+ {["L", "M", "M", "J", "V", "S", "D"].map((day, i) => ( +
+ {day} +
+ ))} +
+ +
+ {getDaysInMonth(currentDate).map((date, index) => ( +
+ {date ? ( + + ) : ( +
+ )} +
+ ))} +
+
+ + {/* Time Slots */} + {selectedDate && ( +
+ + Créneaux disponibles + + + {selectedDate.toLocaleDateString("fr-FR", { + weekday: "long", + day: "numeric", + month: "long", + })} + + +
+ {getAvailableSlots(selectedDate).map((slot, index) => ( + + ))} + + {getAvailableSlots(selectedDate).length === 0 && ( +
+ + Aucun créneau disponible + +
+ )} +
+
+ )} + + {!selectedDate && ( +
+ + + Sélectionnez une date pour voir les créneaux disponibles + +
+ )} +
+ + {/* Footer */} +
+ + Powered by{" "} + + XTablo + + +
+
+ )} + + {/* Booking Modal */} + + {selectedSlot && ( +
+
+ + + {selectedSlot.date.toLocaleDateString("fr-FR", { + weekday: "long", + day: "numeric", + month: "long", + })} + +
+
+ + {selectedSlot.slot.time} +
+
+ )} + +
+
+ + setFormData((prev) => ({ ...prev, name: e.target.value }))} + disabled={!!user} + /> + {formErrors.name && } +
+ +
+ + setFormData((prev) => ({ ...prev, email: e.target.value }))} + disabled={!!user} + /> + {formErrors.email && } +
+ + {!user && ( +
+ + Un compte sera créé avec ces informations pour gérer votre réservation. + +
+ )} +
+ +
+ + +
+
+
+ ); +} diff --git a/ui/src/components/LoadingSpinner.tsx b/apps/external/src/LoadingSpinner.tsx similarity index 100% rename from ui/src/components/LoadingSpinner.tsx rename to apps/external/src/LoadingSpinner.tsx diff --git a/ui/src/providers/UserStoreProvider.tsx b/apps/external/src/UserStoreProvider.tsx similarity index 91% rename from ui/src/providers/UserStoreProvider.tsx rename to apps/external/src/UserStoreProvider.tsx index 2d0d573..876625d 100644 --- a/ui/src/providers/UserStoreProvider.tsx +++ b/apps/external/src/UserStoreProvider.tsx @@ -1,10 +1,10 @@ import { useQuery } from "@tanstack/react-query"; -import { LoadingSpinner } from "@ui/components/LoadingSpinner"; -import { useSession } from "@ui/contexts/SessionContext"; -import { api } from "@ui/lib/api"; -import { Tables } from "@ui/types/database.types"; +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"> & { streamToken: string | null; diff --git a/apps/external/src/lib/api.ts b/apps/external/src/lib/api.ts new file mode 100644 index 0000000..e00027f --- /dev/null +++ b/apps/external/src/lib/api.ts @@ -0,0 +1,3 @@ +import { buildApi } from "@xtablo/shared"; + +export const api = buildApi(import.meta.env.VITE_API_URL); diff --git a/apps/external/src/lib/supabase.ts b/apps/external/src/lib/supabase.ts new file mode 100644 index 0000000..99c2e17 --- /dev/null +++ b/apps/external/src/lib/supabase.ts @@ -0,0 +1,10 @@ +import { createSupabaseClient } from "@xtablo/shared"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error("Missing Supabase environment variables"); +} + +export const supabase = createSupabaseClient(supabaseUrl, supabaseAnonKey); diff --git a/ui/src/main.css b/apps/external/src/main.css similarity index 99% rename from ui/src/main.css rename to apps/external/src/main.css index 125ddee..a896ff7 100644 --- a/ui/src/main.css +++ b/apps/external/src/main.css @@ -301,8 +301,7 @@ transform: translate(-50%, -50%) rotate(0deg) translateX(150px) rotate(0deg); } 100% { - transform: translate(-50%, -50%) rotate(360deg) translateX(150px) - rotate(-360deg); + transform: translate(-50%, -50%) rotate(360deg) translateX(150px) rotate(-360deg); } } @@ -311,8 +310,7 @@ transform: translate(-50%, -50%) rotate(0deg) translateX(200px) rotate(0deg); } 100% { - transform: translate(-50%, -50%) rotate(-360deg) translateX(200px) - rotate(360deg); + transform: translate(-50%, -50%) rotate(-360deg) translateX(200px) rotate(360deg); } } @@ -321,8 +319,7 @@ transform: translate(-50%, -50%) rotate(0deg) translateX(100px) rotate(0deg); } 100% { - transform: translate(-50%, -50%) rotate(360deg) translateX(100px) - rotate(-360deg); + transform: translate(-50%, -50%) rotate(360deg) translateX(100px) rotate(-360deg); } } @@ -500,8 +497,7 @@ transform: translate(-50%, -50%) rotate(0deg) translateX(250px) rotate(0deg); } 100% { - transform: translate(-50%, -50%) rotate(360deg) translateX(250px) - rotate(-360deg); + transform: translate(-50%, -50%) rotate(360deg) translateX(250px) rotate(-360deg); } } @@ -510,8 +506,7 @@ transform: translate(-50%, -50%) rotate(0deg) translateX(120px) rotate(0deg); } 100% { - transform: translate(-50%, -50%) rotate(-360deg) translateX(120px) - rotate(360deg); + transform: translate(-50%, -50%) rotate(-360deg) translateX(120px) rotate(360deg); } } diff --git a/apps/external/src/main.tsx b/apps/external/src/main.tsx new file mode 100644 index 0000000..c79e82d --- /dev/null +++ b/apps/external/src/main.tsx @@ -0,0 +1,26 @@ +import { QueryClientProvider } from "@tanstack/react-query"; +import { queryClient } from "@xtablo/shared"; +import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext"; +import { Toaster } from "@xtablo/ui/components/sonner"; +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import { BrowserRouter as Router } from "react-router-dom"; +import AppRoutes from "./routes"; + +import "@xtablo/ui/styles/globals.css"; +import "./main.css"; + +createRoot(document.getElementById("external-root")!).render( + + + + + +
+ +
+
+
+
+
+); diff --git a/apps/external/src/routes.tsx b/apps/external/src/routes.tsx new file mode 100644 index 0000000..311d609 --- /dev/null +++ b/apps/external/src/routes.tsx @@ -0,0 +1,12 @@ +import { Route, Routes } from "react-router-dom"; +import { EmbeddedBookingPage } from "./EmbeddedBookingPage"; +import { FloatingBookingWidget } from "./FloatingBookingWidget"; + +export default function AppRoutes() { + return ( + + } /> + } /> + + ); +} diff --git a/ui/src/vite-env.d.ts b/apps/external/src/vite-env.d.ts similarity index 100% rename from ui/src/vite-env.d.ts rename to apps/external/src/vite-env.d.ts diff --git a/apps/external/tsconfig.json b/apps/external/tsconfig.json new file mode 100644 index 0000000..d6100fa --- /dev/null +++ b/apps/external/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react-jsx", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "baseUrl": ".", + "paths": { + "@xtablo/ui": ["../../packages/ui/src"], + "@xtablo/ui/*": ["../../packages/ui/src/*"], + "@xtablo/shared": ["../../packages/shared/src"], + "@xtablo/shared/*": ["../../packages/shared/src/*"] + } + }, + "include": ["src"], + "references": [] +} diff --git a/apps/external/tsconfig.tsbuildinfo b/apps/external/tsconfig.tsbuildinfo new file mode 100644 index 0000000..bf0b063 --- /dev/null +++ b/apps/external/tsconfig.tsbuildinfo @@ -0,0 +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 diff --git a/apps/external/vite.config.ts b/apps/external/vite.config.ts new file mode 100644 index 0000000..7c8420c --- /dev/null +++ b/apps/external/vite.config.ts @@ -0,0 +1,26 @@ +/// + +import tailwindcss from "@tailwindcss/vite"; +import react from "@vitejs/plugin-react"; +import { dirname, resolve } from "path"; +import { fileURLToPath } from "url"; +import { defineConfig } from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; + +const __dirname = dirname(fileURLToPath(import.meta.url)); + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react(), tailwindcss(), tsconfigPaths()], + server: { + cors: false, + port: 5174, + }, + build: { + rollupOptions: { + input: { + main: resolve(__dirname, "index.html"), + }, + }, + }, +}); diff --git a/apps/main/.biomeignore b/apps/main/.biomeignore new file mode 100644 index 0000000..5669c8d --- /dev/null +++ b/apps/main/.biomeignore @@ -0,0 +1,4 @@ +# Generated Wrangler files +worker-configuration.d.ts +worker/ + diff --git a/ui/.env.production b/apps/main/.env.production similarity index 100% rename from ui/.env.production rename to apps/main/.env.production diff --git a/ui/.env.staging b/apps/main/.env.staging similarity index 100% rename from ui/.env.staging rename to apps/main/.env.staging diff --git a/apps/main/biome.json b/apps/main/biome.json new file mode 100644 index 0000000..a8ec73b --- /dev/null +++ b/apps/main/biome.json @@ -0,0 +1,300 @@ +{ + "$schema": "https://biomejs.dev/schemas/2.2.5/schema.json", + "vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false }, + "files": { + "ignoreUnknown": true, + "maxSize": 10485760, + "includes": ["src/**/*"] + }, + "formatter": { + "enabled": true, + "formatWithErrors": false, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "attributePosition": "auto" + }, + "linter": { + "enabled": true, + "rules": { + "recommended": false, + "complexity": { + "noAdjacentSpacesInRegex": "error", + "noBannedTypes": "error", + "noExtraBooleanCast": "error", + "noUselessCatch": "error", + "noUselessEscapeInRegex": "error", + "noUselessTypeConstraint": "error" + }, + "correctness": { + "noChildrenProp": "error", + "noConstAssign": "error", + "noConstantCondition": "error", + "noEmptyCharacterClassInRegex": "error", + "noEmptyPattern": "error", + "noGlobalObjectCalls": "error", + "noInvalidBuiltinInstantiation": "error", + "noInvalidConstructorSuper": "error", + "noNonoctalDecimalEscape": "error", + "noPrecisionLoss": "error", + "noSelfAssign": "error", + "noSetterReturn": "error", + "noSwitchDeclarations": "error", + "noUndeclaredVariables": "error", + "noUnreachable": "error", + "noUnreachableSuper": "error", + "noUnsafeFinally": "error", + "noUnsafeOptionalChaining": "error", + "noUnusedLabels": "error", + "noUnusedPrivateClassMembers": "error", + "noUnusedVariables": "error", + "noUnusedImports": "error", + "useIsNan": "error", + "useJsxKeyInIterable": "error", + "useValidForDirection": "error", + "useValidTypeof": "error", + "useYield": "error" + }, + "nursery": {}, + "security": { "noDangerouslySetInnerHtmlWithChildren": "error" }, + "style": { + "noCommonJs": "error", + "noNamespace": "error", + "useArrayLiterals": "error", + "useAsConstAssertion": "error", + "useConst": "error", + "useTemplate": "error" + }, + "suspicious": { + "noAsyncPromiseExecutor": "error", + "noCatchAssign": "error", + "noClassAssign": "error", + "noCommentText": "error", + "noCompareNegZero": "error", + "noConstantBinaryExpressions": "error", + "noControlCharactersInRegex": "error", + "noDebugger": "error", + "noDuplicateCase": "error", + "noDuplicateClassMembers": "error", + "noDuplicateElseIf": "error", + "noDuplicateJsxProps": "error", + "noDuplicateObjectKeys": "error", + "noDuplicateParameters": "error", + "noEmptyBlockStatements": "error", + "noExplicitAny": "error", + "noExtraNonNullAssertion": "error", + "noFallthroughSwitchClause": "error", + "noFunctionAssign": "error", + "noGlobalAssign": "error", + "noImportAssign": "error", + "noIrregularWhitespace": "error", + "noMisleadingCharacterClass": "error", + "noMisleadingInstantiator": "error", + "noPrototypeBuiltins": "error", + "noRedeclare": "error", + "noShadowRestrictedNames": "error", + "noSparseArray": "error", + "noUnsafeDeclarationMerging": "error", + "noUnsafeNegation": "error", + "noUselessRegexBackrefs": "error", + "noWith": "error", + "useGetterReturn": "error", + "useNamespaceKeyword": "error" + } + } + }, + "javascript": { + "formatter": { + "jsxQuoteStyle": "double", + "quoteProperties": "asNeeded", + "trailingCommas": "es5", + "semicolons": "always", + "arrowParentheses": "always", + "bracketSameLine": false, + "quoteStyle": "double", + "attributePosition": "auto", + "bracketSpacing": true + }, + "globals": [ + "onanimationend", + "ongamepadconnected", + "onlostpointercapture", + "onanimationiteration", + "onkeyup", + "onmousedown", + "onanimationstart", + "onslotchange", + "onprogress", + "ontransitionstart", + "onpause", + "onended", + "onpointerover", + "onscrollend", + "onformdata", + "ontransitionrun", + "onanimationcancel", + "ondrag", + "onchange", + "onbeforeinstallprompt", + "onbeforexrselect", + "onmessage", + "ontransitioncancel", + "onpointerdown", + "onabort", + "onpointerout", + "oncuechange", + "ongotpointercapture", + "onscrollsnapchanging", + "onsearch", + "onsubmit", + "onstalled", + "onsuspend", + "onreset", + "onerror", + "onresize", + "onmouseenter", + "ongamepaddisconnected", + "ondragover", + "onbeforetoggle", + "onmouseover", + "onpagehide", + "onmousemove", + "onratechange", + "onmessageerror", + "onwheel", + "ondevicemotion", + "onauxclick", + "ontransitionend", + "onpaste", + "onpageswap", + "ononline", + "ondeviceorientationabsolute", + "onkeydown", + "onclose", + "onselect", + "onpageshow", + "onpointercancel", + "onbeforematch", + "onpointerrawupdate", + "ondragleave", + "onscrollsnapchange", + "onseeked", + "onwaiting", + "onbeforeunload", + "onplaying", + "onvolumechange", + "ondragend", + "onstorage", + "onloadeddata", + "onfocus", + "onoffline", + "onplay", + "onafterprint", + "onclick", + "oncut", + "onmouseout", + "ondblclick", + "oncanplay", + "onloadstart", + "onappinstalled", + "onpointermove", + "ontoggle", + "oncontextmenu", + "onblur", + "oncancel", + "onbeforeprint", + "oncontextrestored", + "onloadedmetadata", + "onpointerup", + "onlanguagechange", + "oncopy", + "onselectstart", + "onscroll", + "onload", + "ondragstart", + "onbeforeinput", + "oncanplaythrough", + "oninput", + "oninvalid", + "ontimeupdate", + "ondurationchange", + "onselectionchange", + "onmouseup", + "location", + "onkeypress", + "onpointerleave", + "oncontextlost", + "ondrop", + "onsecuritypolicyviolation", + "oncontentvisibilityautostatechange", + "ondeviceorientation", + "onseeking", + "onrejectionhandled", + "onunload", + "onmouseleave", + "onhashchange", + "onpointerenter", + "onmousewheel", + "onunhandledrejection", + "ondragenter", + "onpopstate", + "onpagereveal", + "onemptied" + ] + }, + "json": { + "parser": { "allowComments": true, "allowTrailingCommas": false }, + "formatter": { + "enabled": true, + "indentStyle": "space", + "indentWidth": 2, + "lineEnding": "lf", + "lineWidth": 100, + "trailingCommas": "none" + } + }, + "overrides": [ + { "linter": { "rules": { "suspicious": { "noExplicitAny": "off" } } } }, + { "linter": { "rules": { "style": { "useNodejsImportProtocol": "off" } } } }, + { + "linter": { + "rules": { + "style": { "useNodejsImportProtocol": "off" }, + "suspicious": { "noExplicitAny": "off" } + } + } + }, + { + "includes": ["src/**/*.{ts,tsx}", "worker/**/*.{ts,tsx}", "*.{ts,tsx}"], + "linter": { + "rules": { + "complexity": { "noArguments": "error" }, + "correctness": { + "noConstAssign": "off", + "noGlobalObjectCalls": "off", + "noInvalidBuiltinInstantiation": "off", + "noInvalidConstructorSuper": "off", + "noSetterReturn": "off", + "noUndeclaredVariables": "off", + "noUnreachable": "off", + "noUnreachableSuper": "off" + }, + "style": { "useConst": "error" }, + "suspicious": { + "noClassAssign": "off", + "noDuplicateClassMembers": "off", + "noDuplicateObjectKeys": "off", + "noDuplicateParameters": "off", + "noFunctionAssign": "off", + "noImportAssign": "off", + "noRedeclare": "off", + "noUnsafeNegation": "off", + "noVar": "error", + "useGetterReturn": "off" + } + } + } + } + ] +} diff --git a/ui/index.html b/apps/main/index.html similarity index 97% rename from ui/index.html rename to apps/main/index.html index e0981a4..598566b 100644 --- a/ui/index.html +++ b/apps/main/index.html @@ -10,4 +10,4 @@
- + \ No newline at end of file diff --git a/ui/package.json b/apps/main/package.json similarity index 89% rename from ui/package.json rename to apps/main/package.json index 8dd85b5..25414fe 100644 --- a/ui/package.json +++ b/apps/main/package.json @@ -1,23 +1,24 @@ { - "name": "ui", + "name": "@xtablo/main", "private": true, - "version": "0.0.0", + "version": "1.0.0", "type": "module", "scripts": { "dev": "vite dev", - "typecheck": "npx tsgo --build .", + "build": "tsc -b && vite build", + "typecheck": "tsc --noEmit", "lint": "biome check .", "lint:fix": "biome check --write .", "format": "biome format --write .", "preview": "vite preview", "build:staging": "tsc -b && vite build --mode staging", "build:prod": "tsc -b && vite build --mode production", - "deploy:staging": "pnpm run build:staging && wrangler deploy", - "deploy:prod": "pnpm run build:prod && wrangler deploy", + "deploy": "wrangler deploy --env=\"\"", "cf-typegen": "wrangler types", "test": "vitest run --mode dev --passWithNoTests", "test:watch": "vitest watch --passWithNoTests", - "test:coverage": "vitest run --coverage --passWithNoTests" + "test:coverage": "vitest run --coverage --passWithNoTests", + "clean": "rm -rf dist .vite tsconfig.tsbuildinfo node_modules/.vite" }, "devDependencies": { "@biomejs/biome": "2.2.5", @@ -55,7 +56,7 @@ "react-dom": "19.0.0", "rollup-plugin-visualizer": "^5.14.0", "tailwind-merge": "^3.0.2", - "tailwindcss": "^4.0.14", + "tailwindcss": "^4.1.15", "tailwindcss-animate": "^1.0.7", "tw-animate-css": "^1.4.0", "typescript": "^5.7.0", @@ -66,6 +67,8 @@ "wrangler": "^4.24.3" }, "dependencies": { + "@xtablo/ui": "workspace:*", + "@xtablo/shared": "workspace:*", "@datadog/browser-rum": "^6.13.0", "@datadog/browser-rum-react": "^6.13.0", "@hookform/resolvers": "^5.2.2", @@ -85,7 +88,7 @@ "@radix-ui/react-tooltip": "^1.2.8", "@react-stately/calendar": "^3.7.1", "@supabase/supabase-js": "^2.49.3", - "@tailwindcss/vite": "^4.0.14", + "@tailwindcss/vite": "^4.1.5", "@tanstack/react-query": "^5.69.0", "@types/react-router-dom": "^5.3.3", "@typescript/native-preview": "7.0.0-dev.20251010.1", @@ -109,11 +112,5 @@ "uuid": "^11.1.0", "zod": "^4.1.12", "zustand": "^5.0.5" - }, - "pnpm": { - "overrides": { - "form-data": "^4.0.4", - "linkifyjs": "^4.3.2" - } } } diff --git a/ui/src/assets/icon.jpg b/apps/main/public/icon.jpg similarity index 100% rename from ui/src/assets/icon.jpg rename to apps/main/public/icon.jpg diff --git a/ui/src/assets/icon.png b/apps/main/public/logo_dark.png similarity index 100% rename from ui/src/assets/icon.png rename to apps/main/public/logo_dark.png diff --git a/apps/main/public/logo_white.png b/apps/main/public/logo_white.png new file mode 100644 index 0000000..b34373b Binary files /dev/null and b/apps/main/public/logo_white.png differ diff --git a/ui/src/assets/staging_icon.jpg b/apps/main/public/staging_icon.jpg similarity index 100% rename from ui/src/assets/staging_icon.jpg rename to apps/main/public/staging_icon.jpg diff --git a/apps/main/public/vite.svg b/apps/main/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/apps/main/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/App.tsx b/apps/main/src/App.tsx similarity index 63% rename from ui/src/App.tsx rename to apps/main/src/App.tsx index a193b57..3033d57 100644 --- a/ui/src/App.tsx +++ b/apps/main/src/App.tsx @@ -1,14 +1,11 @@ -import { Toaster } from "@ui/components/ui/sonner"; -import { SessionProvider } from "@ui/contexts/SessionContext"; -import { ThemeProvider } from "@ui/contexts/ThemeContext"; -import { routes } from "@ui/lib/routes"; -import { DatadogRumProvider } from "@ui/providers/DatadogRumProvider"; -import { UserStoreProvider } from "@ui/providers/UserStoreProvider"; -import { AllCommunityModule, ModuleRegistry } from "ag-grid-community"; +import { SessionProvider } from "@xtablo/shared/contexts/SessionContext"; +import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext"; +import { Toaster } from "@xtablo/ui/components/sonner"; import { BrowserRouter as Router, useRoutes } from "react-router-dom"; - -// Register all Community features -ModuleRegistry.registerModules([AllCommunityModule]); +import { routes } from "./lib/routes"; +import { supabase } from "./lib/supabase"; +import { DatadogRumProvider } from "./providers/DatadogRumProvider"; +import { UserStoreProvider } from "./providers/UserStoreProvider"; const AppRoutes = () => { const element = useRoutes(routes); @@ -18,7 +15,7 @@ const AppRoutes = () => { export const App = () => { return ( - + diff --git a/ui/src/assets/google.png b/apps/main/src/assets/google.png similarity index 100% rename from ui/src/assets/google.png rename to apps/main/src/assets/google.png diff --git a/apps/main/src/assets/icon.jpg b/apps/main/src/assets/icon.jpg new file mode 100644 index 0000000..026425e Binary files /dev/null and b/apps/main/src/assets/icon.jpg differ diff --git a/apps/main/src/assets/icon.png b/apps/main/src/assets/icon.png new file mode 100644 index 0000000..4cee136 Binary files /dev/null and b/apps/main/src/assets/icon.png differ diff --git a/ui/src/assets/react.svg b/apps/main/src/assets/react.svg similarity index 100% rename from ui/src/assets/react.svg rename to apps/main/src/assets/react.svg diff --git a/apps/main/src/assets/staging_icon.jpg b/apps/main/src/assets/staging_icon.jpg new file mode 100644 index 0000000..9f3a6e6 Binary files /dev/null and b/apps/main/src/assets/staging_icon.jpg differ diff --git a/ui/src/components/AnimatedBackground.tsx b/apps/main/src/components/AnimatedBackground.tsx similarity index 100% rename from ui/src/components/AnimatedBackground.tsx rename to apps/main/src/components/AnimatedBackground.tsx diff --git a/ui/src/components/AuthenticationGateway.tsx b/apps/main/src/components/AuthenticationGateway.tsx similarity index 94% rename from ui/src/components/AuthenticationGateway.tsx rename to apps/main/src/components/AuthenticationGateway.tsx index 2bd73a8..4c8e124 100644 --- a/ui/src/components/AuthenticationGateway.tsx +++ b/apps/main/src/components/AuthenticationGateway.tsx @@ -1,8 +1,8 @@ import { useEffect, useState } from "react"; import { Navigate, Outlet, useSearchParams } from "react-router-dom"; import { match } from "ts-pattern"; +import { useMaybeUser } from "../providers/UserStoreProvider"; import { LoadingSpinner } from "./LoadingSpinner"; -import { useMaybeUser } from "@ui/providers/UserStoreProvider"; export const AuthenticationGateway = () => { const user = useMaybeUser(); diff --git a/ui/src/components/AuthenticationGateway.unit.tsx b/apps/main/src/components/AuthenticationGateway.unit.tsx similarity index 76% rename from ui/src/components/AuthenticationGateway.unit.tsx rename to apps/main/src/components/AuthenticationGateway.unit.tsx index 9d3b590..ee07143 100644 --- a/ui/src/components/AuthenticationGateway.unit.tsx +++ b/apps/main/src/components/AuthenticationGateway.unit.tsx @@ -1,13 +1,28 @@ import { screen, waitFor } from "@testing-library/react"; import { AuthenticationGateway } from "@ui/components/AuthenticationGateway"; -import { SessionTestProvider } from "@ui/contexts/SessionContext"; -import { renderWithRouter } from "@ui/utils/testHelpers"; +import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext"; import { Route, Routes } from "react-router-dom"; +import { renderWithRouter } from "../utils/testHelpers"; describe("PublicRoute", () => { it("shows loading state initially", () => { renderWithRouter( - + }> Login Page
} /> @@ -57,7 +72,7 @@ describe("PublicRoute", () => { it("renders public content when user is not authenticated", async () => { renderWithRouter( - + }> Login Page
} /> diff --git a/ui/src/components/AvailabilityCard.tsx b/apps/main/src/components/AvailabilityCard.tsx similarity index 97% rename from ui/src/components/AvailabilityCard.tsx rename to apps/main/src/components/AvailabilityCard.tsx index 675788c..199d9c9 100644 --- a/ui/src/components/AvailabilityCard.tsx +++ b/apps/main/src/components/AvailabilityCard.tsx @@ -1,7 +1,7 @@ -import { Button } from "@ui/components/ui/button"; -import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@ui/components/ui/card"; -import { Switch } from "@ui/components/ui/switch"; -import { TimeInput } from "@ui/components/ui/time-input"; +import { Button } from "@xtablo/ui/components/button"; +import { Card, CardAction, CardContent, CardHeader, CardTitle } from "@xtablo/ui/components/card"; +import { Switch } from "@xtablo/ui/components/switch"; +import { TimeInput } from "@xtablo/ui/components/time-input"; import { Copy as CopyIcon, Minus as MinusIcon, Plus as PlusIcon } from "lucide-react"; interface TimeRange { diff --git a/ui/src/components/AvailabilityVisualization.tsx b/apps/main/src/components/AvailabilityVisualization.tsx similarity index 80% rename from ui/src/components/AvailabilityVisualization.tsx rename to apps/main/src/components/AvailabilityVisualization.tsx index d9532d6..96f74bd 100644 --- a/ui/src/components/AvailabilityVisualization.tsx +++ b/apps/main/src/components/AvailabilityVisualization.tsx @@ -1,5 +1,5 @@ -import { Text } from "@ui/components/ui/typography"; -import { WeeklyAvailability } from "@ui/hooks/availabilities"; +import { Text } from "@xtablo/ui/components/typography"; +import { WeeklyAvailability } from "../hooks/availabilities"; // Check if a time slot is available for a given day const isTimeSlotAvailable = ( @@ -65,13 +65,13 @@ export const AvailabilityVisualization = ({
{/* Weekly Calendar Header */}
-
+
Heure
{DAYS_OF_WEEK.map((day) => (
{DAYS_OF_WEEK_DISPLAY[day]} @@ -93,7 +93,7 @@ export const AvailabilityVisualization = ({ key={timeSlot} className="grid grid-cols-8 border-b border-gray-100 dark:border-gray-700 hover:bg-slate-50/50 dark:hover:bg-slate-800/50 transition-colors duration-150" > -
+
{timeSlot} @@ -101,10 +101,10 @@ export const AvailabilityVisualization = ({ {DAYS_OF_WEEK.map((day) => (
{isTimeSlotAvailable(day, timeSlot, draftAvailabilities) ? ( -
+
) : ( diff --git a/ui/src/components/BrandButtons/LoginWithGoogle.test.tsx b/apps/main/src/components/BrandButtons/LoginWithGoogle.test.tsx similarity index 95% rename from ui/src/components/BrandButtons/LoginWithGoogle.test.tsx rename to apps/main/src/components/BrandButtons/LoginWithGoogle.test.tsx index 728a0cc..299ecba 100644 --- a/ui/src/components/BrandButtons/LoginWithGoogle.test.tsx +++ b/apps/main/src/components/BrandButtons/LoginWithGoogle.test.tsx @@ -1,7 +1,7 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle"; -import { useLoginGoogle } from "@ui/hooks/auth"; import { vi } from "vitest"; +import { useLoginGoogle } from "../../hooks/auth"; vi.mock("../../hooks/auth", () => ({ useLoginGoogle: vi.fn(), diff --git a/ui/src/components/BrandButtons/LoginWithGoogle.tsx b/apps/main/src/components/BrandButtons/LoginWithGoogle.tsx similarity index 100% rename from ui/src/components/BrandButtons/LoginWithGoogle.tsx rename to apps/main/src/components/BrandButtons/LoginWithGoogle.tsx diff --git a/ui/src/components/BrandButtons/login-with-google.css b/apps/main/src/components/BrandButtons/login-with-google.css similarity index 100% rename from ui/src/components/BrandButtons/login-with-google.css rename to apps/main/src/components/BrandButtons/login-with-google.css diff --git a/ui/src/components/ChannelBadge.tsx b/apps/main/src/components/ChannelBadge.tsx similarity index 92% rename from ui/src/components/ChannelBadge.tsx rename to apps/main/src/components/ChannelBadge.tsx index ea78b54..9dbdc51 100644 --- a/ui/src/components/ChannelBadge.tsx +++ b/apps/main/src/components/ChannelBadge.tsx @@ -1,4 +1,4 @@ -import { UserTablo } from "@ui/types/tablos.types"; +import { UserTablo } from "@xtablo/shared/types/tablos.types"; import { twMerge } from "tailwind-merge"; export const ChannelBadge = ({ diff --git a/ui/src/components/ChannelPreview.tsx b/apps/main/src/components/ChannelPreview.tsx similarity index 95% rename from ui/src/components/ChannelPreview.tsx rename to apps/main/src/components/ChannelPreview.tsx index 2cca8ae..b464145 100644 --- a/ui/src/components/ChannelPreview.tsx +++ b/apps/main/src/components/ChannelPreview.tsx @@ -1,6 +1,6 @@ import { ChannelBadge } from "@ui/components/ChannelBadge"; -import { Badge } from "@ui/components/ui/badge"; -import { UserTablo } from "@ui/types/tablos.types"; +import { UserTablo } from "@xtablo/shared/types/tablos.types"; +import { Badge } from "@xtablo/ui/components/badge"; import { ReactNode } from "react"; import { Channel } from "stream-chat"; import { twMerge } from "tailwind-merge"; @@ -104,7 +104,7 @@ export function ChannelPreview({ {displayTitle} {timestamp && ( - + {formatTimestamp(timestamp)} )} @@ -117,7 +117,7 @@ export function ChannelPreview({ {/* Unread count badge */} {unreadCount > 0 && ( -
+
void; + title: string; + children: React.ReactNode; + width?: "sm" | "md" | "lg" | "xl" | "2xl" | "full" | "auto"; +} + +export function CustomModal({ isOpen, onClose, title, children, width = "md" }: CustomModalProps) { + const getWidthClasses = () => { + switch (width) { + case "sm": + return "max-w-sm"; + case "md": + return "max-w-md"; + case "lg": + return "max-w-lg"; + case "xl": + return "max-w-xl"; + case "2xl": + return "max-w-2xl"; + case "full": + return "max-w-full mx-4"; + case "auto": + return "w-auto min-w-80 max-w-[90vw]"; + default: + return "max-w-md"; + } + }; + + return ( + + + + {title} + +
{children}
+
+
+ ); +} diff --git a/ui/src/components/DeleteTabloModal.tsx b/apps/main/src/components/DeleteTabloModal.tsx similarity index 98% rename from ui/src/components/DeleteTabloModal.tsx rename to apps/main/src/components/DeleteTabloModal.tsx index af8d4c5..5af4253 100644 --- a/ui/src/components/DeleteTabloModal.tsx +++ b/apps/main/src/components/DeleteTabloModal.tsx @@ -1,4 +1,4 @@ -import { UserTablo } from "@ui/types/tablos.types"; +import { UserTablo } from "@xtablo/shared/types/tablos.types"; import { ClickOutside } from "./ClickOutside"; interface DeleteTabloModalProps { diff --git a/ui/src/components/EmbedConfigModal.tsx b/apps/main/src/components/EmbedConfigModal.tsx similarity index 57% rename from ui/src/components/EmbedConfigModal.tsx rename to apps/main/src/components/EmbedConfigModal.tsx index 71d25e3..3f2b025 100644 --- a/ui/src/components/EmbedConfigModal.tsx +++ b/apps/main/src/components/EmbedConfigModal.tsx @@ -1,26 +1,29 @@ -import { Button } from "@ui/components/ui/button"; +import { Button } from "@xtablo/ui/components/button"; +import { CopyButton } from "@xtablo/ui/components/clipboard"; import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, -} from "@ui/components/ui/dialog"; -import { Label } from "@ui/components/ui/label"; +} from "@xtablo/ui/components/dialog"; +import { Label } from "@xtablo/ui/components/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, -} from "@ui/components/ui/select"; -import { CopyButton } from "@ui/components/ui/clipboard"; +} from "@xtablo/ui/components/select"; +import { TypographyMuted, TypographyP } from "@xtablo/ui/components/typography"; import { useState } from "react"; -import { TypographyMuted } from "@ui/components/ui/typography"; type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red"; +export type EmbedType = "embed" | "floating" | "normal"; + interface EmbedConfig { + embedType: EmbedType; backgroundVariant: ColorVariant; buttonVariant: ColorVariant; } @@ -28,26 +31,51 @@ interface EmbedConfig { interface EmbedConfigModalProps { isOpen: boolean; onClose: () => void; - baseEmbedUrl: string; + buildPublicLink: (type: "embed" | "floating" | "normal") => string; } -export function EmbedConfigModal({ isOpen, onClose, baseEmbedUrl }: EmbedConfigModalProps) { +export function EmbedConfigModal({ isOpen, onClose, buildPublicLink }: EmbedConfigModalProps) { const [embedConfig, setEmbedConfig] = useState({ + embedType: "embed", backgroundVariant: "purple", buttonVariant: "purple", }); const getEmbedUrl = () => { + const baseUrl = buildPublicLink(embedConfig.embedType); const params = new URLSearchParams({ - backgroundVariant: embedConfig.backgroundVariant, + mode: "embed", buttonVariant: embedConfig.buttonVariant, }); - return `${baseEmbedUrl}?${params.toString()}`; + + // Only add backgroundVariant for full embed + if (embedConfig.embedType === "embed") { + params.set("backgroundVariant", embedConfig.backgroundVariant); + } + + return `${baseUrl}?${params.toString()}`; }; const generateEmbedCode = () => { const embedUrl = getEmbedUrl(); + if (embedConfig.embedType === "floating") { + return ` +
+`; + } + return `