Huge bump with turborepo
4
.gitignore
vendored
|
|
@ -31,3 +31,7 @@ __pycache__/
|
|||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
.turbo
|
||||
dist
|
||||
.wrangler
|
||||
172
README.md
Normal file
|
|
@ -0,0 +1,172 @@
|
|||
# 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-components/ # 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
|
||||
- For other services: Python 3.11+, Go 1.21+
|
||||
|
||||
### Installation
|
||||
|
||||
Install all dependencies:
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
This will install dependencies for all apps and packages in the workspace.
|
||||
|
||||
### Development
|
||||
|
||||
Run all apps in development mode:
|
||||
|
||||
```bash
|
||||
turbo dev
|
||||
```
|
||||
|
||||
Run specific app:
|
||||
|
||||
```bash
|
||||
# Main UI app (http://localhost:5173)
|
||||
turbo dev --filter @xtablo/main
|
||||
|
||||
# External microfrontend (http://localhost:5174)
|
||||
turbo dev --filter @xtablo/external
|
||||
```
|
||||
|
||||
### Building
|
||||
|
||||
Build all apps:
|
||||
|
||||
```bash
|
||||
turbo build
|
||||
```
|
||||
|
||||
Build specific app:
|
||||
|
||||
```bash
|
||||
turbo build --filter @xtablo/main
|
||||
turbo build --filter @xtablo/external
|
||||
```
|
||||
|
||||
### Linting and Formatting
|
||||
|
||||
```bash
|
||||
# Lint all packages
|
||||
turbo lint
|
||||
|
||||
# Format all packages
|
||||
turbo format
|
||||
```
|
||||
|
||||
## Packages
|
||||
|
||||
### @xtablo/ui-components
|
||||
|
||||
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, Input, Dialog } from "@xtablo/ui-components";
|
||||
```
|
||||
|
||||
### @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
|
||||
|
||||
## Scripts
|
||||
|
||||
- `turbo dev` - Start all apps in development mode
|
||||
- `turbo build` - Build all apps
|
||||
- `turbo lint` - Lint all packages
|
||||
- `turbo format` - Format all packages
|
||||
- `turbo typecheck` - Type check all packages
|
||||
- `turbo test` - Run tests for all packages
|
||||
- `turbo clean` - Clean all build artifacts and node_modules
|
||||
|
||||
## Adding a New Package
|
||||
|
||||
1. Create a new directory under `packages/`
|
||||
2. Add a `package.json` with name `@xtablo/package-name`
|
||||
3. Update `pnpm-workspace.yaml` if needed (already configured for `packages/*`)
|
||||
4. Install in your app: `pnpm --filter @xtablo/your-app add @xtablo/package-name@workspace:*`
|
||||
|
||||
## 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-components/` - Shared UI components
|
||||
- `packages/shared/` - Shared utilities and logic
|
||||
|
||||
All import paths have been updated to use workspace packages (`@xtablo/ui-components`, `@xtablo/shared`).
|
||||
|
||||
## Contributing
|
||||
|
||||
When adding new shared code:
|
||||
|
||||
1. Add to the appropriate package (`ui-components` for UI, `shared` for logic/utils)
|
||||
2. Export from the package's `index.ts`
|
||||
3. Use the workspace import in your apps
|
||||
|
||||
## License
|
||||
|
||||
[Your License Here]
|
||||
342
apps/external/biome.json
vendored
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
|
||||
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
||||
"files": {
|
||||
"includes": [
|
||||
"ui/src/**/*",
|
||||
"ui/worker/**/*",
|
||||
"ui/*.{ts,tsx,js,jsx,json}",
|
||||
"api/src/**/*",
|
||||
"api/*.{ts,tsx,js,jsx,json}",
|
||||
"xtablo-expo/app/**/*",
|
||||
"xtablo-expo/components/**/*",
|
||||
"xtablo-expo/hooks/**/*",
|
||||
"xtablo-expo/lib/**/*",
|
||||
"xtablo-expo/providers/**/*",
|
||||
"xtablo-expo/stores/**/*",
|
||||
"xtablo-expo/types/**/*",
|
||||
"xtablo-expo/*.{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": [
|
||||
"ui/src/**/*.{ts,tsx}",
|
||||
"ui/worker/**/*.{ts,tsx}",
|
||||
"ui/*.{ts,tsx}",
|
||||
"api/src/**/*.{ts,tsx}",
|
||||
"api/*.{ts,tsx}",
|
||||
"xtablo-expo/**/*.{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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["xtablo-expo/**/*.{js,jsx,ts,tsx}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["api/src/**/*.{js,ts}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"noCommonJs": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
@ -3,9 +3,10 @@
|
|||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Xtablo External</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="external-root"></div>
|
||||
<script type="module" src="/src/external/main.tsx"></script>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
41
apps/external/package.json
vendored
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,5 +1,5 @@
|
|||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@ui/components/ui/dialog";
|
||||
import { cn } from "@ui/lib/utils";
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog";
|
||||
import { cn } from "@xtablo/shared";
|
||||
|
||||
// Custom Modal Component - now using shadcn/ui Dialog
|
||||
interface CustomModalProps {
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
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 { CustomModal } from "./CustomModal";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
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 { useSession } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth";
|
||||
import { TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public";
|
||||
import { useCreateTabloWithOwner } from "@xtablo/shared";
|
||||
import { useMaybeUser } from "./UserStoreProvider";
|
||||
import { EventInsertInTablo } from "@xtablo/shared/types/events.types";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronLeftIcon,
|
||||
|
|
@ -22,6 +22,8 @@ import {
|
|||
import { useState } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { supabase } from "./lib/supabase";
|
||||
import { api } from "./lib/api";
|
||||
|
||||
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red";
|
||||
|
||||
|
|
@ -187,16 +189,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 +207,21 @@ export function EmbeddedBookingPage() {
|
|||
const txtColor = getTextColorFromBackground(backgroundVariant);
|
||||
const mutedTxtColor = getMutedTextColorFromBackground(backgroundVariant);
|
||||
|
||||
const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots(
|
||||
shortUserId || "",
|
||||
event_type_standard_name || ""
|
||||
);
|
||||
const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1);
|
||||
|
||||
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner();
|
||||
console.log({shortUserId, eventTypeStandardName})
|
||||
const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots(
|
||||
api,
|
||||
shortUserId || "",
|
||||
eventTypeStandardName || ""
|
||||
);
|
||||
|
||||
|
||||
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api,
|
||||
() => {
|
||||
handleCloseModal();
|
||||
}
|
||||
);
|
||||
|
||||
const userProfile = publicSlots?.user;
|
||||
const eventType = publicSlots?.eventType;
|
||||
|
|
@ -460,7 +469,7 @@ export function EmbeddedBookingPage() {
|
|||
{/* Left Side - Event Details */}
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-[400px] bg-gradient-to-br p-8 flex flex-col relative overflow-hidden",
|
||||
"w-[400px] bg-linear-to-br p-8 flex flex-col relative overflow-hidden",
|
||||
bgColors.gradient,
|
||||
txtColor
|
||||
)}
|
||||
|
|
@ -468,7 +477,7 @@ export function EmbeddedBookingPage() {
|
|||
{/* Subtle accent overlay */}
|
||||
<div
|
||||
className={twMerge(
|
||||
"absolute inset-0 bg-gradient-to-br pointer-events-none",
|
||||
"absolute inset-0 bg-linear-to-br pointer-events-none",
|
||||
bgColors.overlay
|
||||
)}
|
||||
></div>
|
||||
|
|
@ -512,7 +521,7 @@ export function EmbeddedBookingPage() {
|
|||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center flex-shrink-0",
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center shrink-0",
|
||||
bgColors.iconBg,
|
||||
bgColors.iconBorder
|
||||
)}
|
||||
|
|
@ -530,7 +539,7 @@ export function EmbeddedBookingPage() {
|
|||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center flex-shrink-0",
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center shrink-0",
|
||||
bgColors.iconBg,
|
||||
bgColors.iconBorder
|
||||
)}
|
||||
|
|
@ -548,7 +557,7 @@ export function EmbeddedBookingPage() {
|
|||
<div className="flex items-center gap-3">
|
||||
<div
|
||||
className={twMerge(
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center flex-shrink-0",
|
||||
"w-10 h-10 rounded-lg border flex items-center justify-center shrink-0",
|
||||
bgColors.iconBg,
|
||||
bgColors.iconBorder
|
||||
)}
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
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, 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 { CustomModal } from "./CustomModal";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
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, TypographyH4, TypographyMuted } from "@xtablo/ui/components/typography";
|
||||
import { useSession } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth";
|
||||
import { TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public";
|
||||
// import { useCreateTabloWithOwner } from "@xtablo/shared";
|
||||
import { useMaybeUser } from "./UserStoreProvider";
|
||||
import { EventInsertInTablo } from "@xtablo/shared/types/events.types";
|
||||
import {
|
||||
CalendarIcon,
|
||||
ChevronLeftIcon,
|
||||
|
|
@ -23,6 +23,9 @@ import {
|
|||
import { useState } from "react";
|
||||
import { useParams, useSearchParams } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { supabase } from "./lib/supabase";
|
||||
import { api } from "./lib/api";
|
||||
import { useCreateTabloWithOwner } from "@xtablo/shared";
|
||||
|
||||
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red";
|
||||
|
||||
|
|
@ -108,15 +111,14 @@ const buttonColors = {
|
|||
};
|
||||
|
||||
export function FloatingBookingWidget() {
|
||||
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;
|
||||
const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1);
|
||||
|
||||
// Get variants from URL params with fallback to purple
|
||||
const buttonVariant = (searchParams.get("buttonVariant") as ColorVariant) || "purple";
|
||||
|
|
@ -125,11 +127,17 @@ export function FloatingBookingWidget() {
|
|||
const btnColors = buttonColors[buttonVariant];
|
||||
|
||||
const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots(
|
||||
api,
|
||||
shortUserId || "",
|
||||
event_type_standard_name || ""
|
||||
eventTypeStandardName || ""
|
||||
);
|
||||
|
||||
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner();
|
||||
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api,
|
||||
() => {
|
||||
handleCloseModal();
|
||||
setIsWidgetOpen(false);
|
||||
}
|
||||
);
|
||||
|
||||
const userProfile = publicSlots?.user;
|
||||
const eventType = publicSlots?.eventType;
|
||||
|
|
@ -411,10 +419,10 @@ export function FloatingBookingWidget() {
|
|||
<img
|
||||
src={(userProfile as { name: string; avatar_url?: string }).avatar_url}
|
||||
alt={userProfile?.name || "Profile"}
|
||||
className="w-12 h-12 rounded-full object-cover border-2 border-gray-200 dark:border-gray-600 flex-shrink-0"
|
||||
className="w-12 h-12 rounded-full object-cover border-2 border-gray-200 dark:border-gray-600 shrink-0"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-12 h-12 rounded-full bg-gray-200 dark:bg-gray-700 flex items-center justify-center shrink-0">
|
||||
<UserIcon className="w-6 h-6 text-gray-500 dark:text-gray-400" />
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -430,7 +438,7 @@ export function FloatingBookingWidget() {
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-8 w-8 flex-shrink-0"
|
||||
className="h-8 w-8 shrink-0"
|
||||
onClick={() => {
|
||||
setIsWidgetOpen(false);
|
||||
setSelectedDate(null);
|
||||
97
apps/external/src/UserStoreProvider.tsx
vendored
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
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 { api } from "./lib/api";
|
||||
|
||||
export type User = Tables<"profiles"> & {
|
||||
streamToken: string | null;
|
||||
};
|
||||
|
||||
const UserStoreContext = React.createContext<StoreApi<User> | null>(null);
|
||||
|
||||
|
||||
export const UserStoreProvider = ({ children }: { children: React.ReactNode }) => {
|
||||
const { session } = useSession();
|
||||
const shouldFetchUser = !!session?.access_token;
|
||||
const { data: user, isPending } = useQuery<User | null>({
|
||||
queryKey: ["user"],
|
||||
queryFn: async () => {
|
||||
try {
|
||||
const { data: user } = await api.get<User>("/api/v1/users/me", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.error("Failed to get user:", error);
|
||||
return null;
|
||||
}
|
||||
},
|
||||
enabled: shouldFetchUser,
|
||||
});
|
||||
|
||||
if (isPending && shouldFetchUser) {
|
||||
return <LoadingSpinner />;
|
||||
}
|
||||
|
||||
if (!user) {
|
||||
return children;
|
||||
}
|
||||
|
||||
const store = createStore<User>()(() => user);
|
||||
|
||||
return (
|
||||
<UserStoreContext.Provider value={store as StoreApi<User>}>
|
||||
{children}
|
||||
</UserStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
export const useUser = () => {
|
||||
const store = React.useContext(UserStoreContext);
|
||||
if (!store) {
|
||||
throw new Error("Missing UserStoreProvider");
|
||||
}
|
||||
return useStore(store);
|
||||
};
|
||||
|
||||
export const useMaybeUser = () => {
|
||||
const store = React.useContext(UserStoreContext);
|
||||
if (!store) {
|
||||
return null;
|
||||
}
|
||||
return useStore(store);
|
||||
};
|
||||
|
||||
// TestUserStoreProvider component
|
||||
export const TestUserStoreProvider = ({
|
||||
children,
|
||||
user,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
user: User | null;
|
||||
}) => {
|
||||
if (!user) {
|
||||
return children;
|
||||
}
|
||||
const store = createStore<User>()(() => user);
|
||||
|
||||
return (
|
||||
<UserStoreContext.Provider value={store as StoreApi<User>}>
|
||||
{children}
|
||||
</UserStoreContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
// // Test useUser hook
|
||||
// export const useTestUser = () => {
|
||||
// const store = React.useContext(TestUserStoreContext);
|
||||
// if (!store) {
|
||||
// throw new Error("Missing TestUserStoreProvider");
|
||||
// }
|
||||
// return useStore(store);
|
||||
// };
|
||||
3
apps/external/src/lib/api.ts
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { buildApi } from "@xtablo/shared";
|
||||
|
||||
export const api = buildApi(import.meta.env.VITE_API_URL);
|
||||
10
apps/external/src/lib/supabase.ts
vendored
Normal file
|
|
@ -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);
|
||||
0
ui/src/main.css → apps/external/src/main.css
vendored
|
|
@ -1,18 +1,15 @@
|
|||
|
||||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { queryClient } from "@ui/lib/api";
|
||||
import { externalRoutes } from "./routes";
|
||||
import { ThemeProvider } from "src/contexts/ThemeContext";
|
||||
import { Toaster } from "src/components/ui/sonner";
|
||||
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
|
||||
import { Toaster } from "@xtablo/ui/components/sonner";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import AppRoutes from "./routes";
|
||||
|
||||
import "../main.css";
|
||||
|
||||
const ExternalAppRoutes = () => {
|
||||
const element = useRoutes(externalRoutes);
|
||||
return element;
|
||||
};
|
||||
import "@xtablo/ui/styles/globals.css";
|
||||
import "./main.css";
|
||||
|
||||
createRoot(document.getElementById("external-root")!).render(
|
||||
<StrictMode>
|
||||
|
|
@ -21,7 +18,7 @@ createRoot(document.getElementById("external-root")!).render(
|
|||
<Toaster />
|
||||
<Router>
|
||||
<div className="min-h-screen bg-background">
|
||||
<ExternalAppRoutes />
|
||||
<AppRoutes />
|
||||
</div>
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
12
apps/external/src/routes.tsx
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
import { Routes, Route } from 'react-router-dom';
|
||||
import { EmbeddedBookingPage } from './EmbeddedBookingPage';
|
||||
import { FloatingBookingWidget } from './FloatingBookingWidget';
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/embed/:userInfo/:eventTypeStandardName" element={<EmbeddedBookingPage />} />
|
||||
<Route path="/widget/:userInfo/:eventTypeStandardName" element={<FloatingBookingWidget />} />
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,33 +1,32 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"strict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedSideEffectImports": true,
|
||||
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"*": ["./*"],
|
||||
"@ui/*": ["./src/*"],
|
||||
"@external/*": ["./src/external/*"]
|
||||
"@xtablo/ui": ["../../packages/ui/src"],
|
||||
"@xtablo/ui/*": ["../../packages/ui/src/*"],
|
||||
"@xtablo/shared": ["../../packages/shared/src"],
|
||||
"@xtablo/shared/*": ["../../packages/shared/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
"references": [
|
||||
{ "path": "../../packages/ui" },
|
||||
{ "path": "../../packages/shared" }
|
||||
]
|
||||
}
|
||||
|
||||
26
apps/external/vite.config.ts
vendored
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
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"),
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
342
apps/main/biome.json
Normal file
|
|
@ -0,0 +1,342 @@
|
|||
{
|
||||
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
|
||||
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
|
||||
"files": {
|
||||
"includes": [
|
||||
"ui/src/**/*",
|
||||
"ui/worker/**/*",
|
||||
"ui/*.{ts,tsx,js,jsx,json}",
|
||||
"api/src/**/*",
|
||||
"api/*.{ts,tsx,js,jsx,json}",
|
||||
"xtablo-expo/app/**/*",
|
||||
"xtablo-expo/components/**/*",
|
||||
"xtablo-expo/hooks/**/*",
|
||||
"xtablo-expo/lib/**/*",
|
||||
"xtablo-expo/providers/**/*",
|
||||
"xtablo-expo/stores/**/*",
|
||||
"xtablo-expo/types/**/*",
|
||||
"xtablo-expo/*.{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": [
|
||||
"ui/src/**/*.{ts,tsx}",
|
||||
"ui/worker/**/*.{ts,tsx}",
|
||||
"ui/*.{ts,tsx}",
|
||||
"api/src/**/*.{ts,tsx}",
|
||||
"api/*.{ts,tsx}",
|
||||
"xtablo-expo/**/*.{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"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["xtablo-expo/**/*.{js,jsx,ts,tsx}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"correctness": {
|
||||
"noUndeclaredVariables": "off"
|
||||
},
|
||||
"suspicious": {
|
||||
"noExplicitAny": "warn"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"includes": ["api/src/**/*.{js,ts}"],
|
||||
"linter": {
|
||||
"rules": {
|
||||
"style": {
|
||||
"noCommonJs": "off"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
13
apps/main/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/jpg+xml" href="/public/icon.jpg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>XTablo</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,19 +1,20 @@
|
|||
{
|
||||
"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:staging": "pnpm run build:staging && wrangler deploy --env staging",
|
||||
"deploy:prod": "pnpm run build:prod && wrangler deploy --env production",
|
||||
"cf-typegen": "wrangler types",
|
||||
"test": "vitest run --mode dev --passWithNoTests",
|
||||
"test:watch": "vitest watch --passWithNoTests",
|
||||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 84 KiB After Width: | Height: | Size: 84 KiB |
BIN
apps/main/public/logo_white.png
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
1
apps/main/public/vite.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
|
@ -1,10 +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 { Toaster } from "@xtablo/ui/components/sonner";
|
||||
import { SessionProvider } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
|
||||
import { UserStoreProvider } from "./providers/UserStoreProvider";
|
||||
import { routes } from "./lib/routes";
|
||||
import { DatadogRumProvider } from "./providers/DatadogRumProvider";
|
||||
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
|
||||
import { supabase } from "./lib/supabase";
|
||||
|
||||
const AppRoutes = () => {
|
||||
const element = useRoutes(routes);
|
||||
|
|
@ -14,7 +15,7 @@ const AppRoutes = () => {
|
|||
export const App = () => {
|
||||
return (
|
||||
<ThemeProvider>
|
||||
<SessionProvider>
|
||||
<SessionProvider supabase={supabase}>
|
||||
<UserStoreProvider>
|
||||
<Toaster />
|
||||
<Router>
|
||||
|
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.6 KiB |
BIN
apps/main/src/assets/icon.jpg
Normal file
|
After Width: | Height: | Size: 42 KiB |
BIN
apps/main/src/assets/icon.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 4 KiB After Width: | Height: | Size: 4 KiB |
BIN
apps/main/src/assets/staging_icon.jpg
Normal file
|
After Width: | Height: | Size: 53 KiB |
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||
import { Navigate, Outlet, useSearchParams } from "react-router-dom";
|
||||
import { match } from "ts-pattern";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
import { useMaybeUser } from "@ui/providers/UserStoreProvider";
|
||||
import { useMaybeUser } from "../providers/UserStoreProvider";
|
||||
|
||||
export const AuthenticationGateway = () => {
|
||||
const user = useMaybeUser();
|
||||
|
|
@ -1,13 +1,26 @@
|
|||
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 { renderWithRouter } from "../utils/testHelpers";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
|
||||
describe("PublicRoute", () => {
|
||||
it("shows loading state initially", () => {
|
||||
renderWithRouter(
|
||||
<SessionTestProvider>
|
||||
<SessionTestProvider testUser={{
|
||||
id: "123",
|
||||
app_metadata: {},
|
||||
user_metadata: {
|
||||
full_name: "Test User",
|
||||
email: "test@example.com",
|
||||
email_verified: true,
|
||||
first_name: "Test",
|
||||
last_name: "User",
|
||||
business_name: "Test Business",
|
||||
},
|
||||
aud: "authenticated",
|
||||
created_at: new Date().toISOString(),
|
||||
}}>
|
||||
<Routes>
|
||||
<Route element={<AuthenticationGateway />}>
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
|
|
@ -57,7 +70,7 @@ describe("PublicRoute", () => {
|
|||
|
||||
it("renders public content when user is not authenticated", async () => {
|
||||
renderWithRouter(
|
||||
<SessionTestProvider>
|
||||
<SessionTestProvider testUser={undefined}>
|
||||
<Routes>
|
||||
<Route element={<AuthenticationGateway />}>
|
||||
<Route path="/login" element={<div>Login Page</div>} />
|
||||
|
|
@ -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 {
|
||||
|
|
@ -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 = ({
|
|||
<div className="bg-white dark:bg-gray-700/40 rounded-xl shadow-sm dark:shadow-gray-900/20 border border-gray-200 dark:border-gray-600/50 overflow-hidden">
|
||||
{/* Weekly Calendar Header */}
|
||||
<div className="grid grid-cols-8 border-b-2 border-gray-200 dark:border-gray-600">
|
||||
<div className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700/60 dark:to-slate-800/60 border-r border-gray-200 dark:border-gray-600">
|
||||
<div className="p-4 bg-linear-to-br from-slate-50 to-slate-100 dark:from-slate-700/60 dark:to-slate-800/60 border-r border-gray-200 dark:border-gray-600">
|
||||
<Text className="font-bold text-sm text-slate-700 dark:text-slate-300">Heure</Text>
|
||||
</div>
|
||||
{DAYS_OF_WEEK.map((day) => (
|
||||
<div
|
||||
key={day}
|
||||
className="p-4 bg-gradient-to-br from-slate-50 to-slate-100 dark:from-slate-700/60 dark:to-slate-800/60 border-r border-gray-200 dark:border-gray-600 last:border-r-0 text-center"
|
||||
className="p-4 bg-linear-to-br from-slate-50 to-slate-100 dark:from-slate-700/60 dark:to-slate-800/60 border-r border-gray-200 dark:border-gray-600 last:border-r-0 text-center"
|
||||
>
|
||||
<Text className="font-bold text-sm text-slate-700 dark:text-slate-300">
|
||||
{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"
|
||||
>
|
||||
<div className="p-3 border-r border-gray-200 dark:border-gray-600 bg-gradient-to-r from-slate-50/80 to-slate-100/80 dark:from-slate-800/80 dark:to-slate-900/80">
|
||||
<div className="p-3 border-r border-gray-200 dark:border-gray-600 bg-linear-to-r from-slate-50/80 to-slate-100/80 dark:from-slate-800/80 dark:to-slate-900/80">
|
||||
<Text className="text-xs font-semibold text-slate-600 dark:text-slate-400">
|
||||
{timeSlot}
|
||||
</Text>
|
||||
|
|
@ -101,10 +101,10 @@ export const AvailabilityVisualization = ({
|
|||
{DAYS_OF_WEEK.map((day) => (
|
||||
<div
|
||||
key={`${day}-${timeSlot}`}
|
||||
className="p-3 border-r border-gray-200 dark:border-gray-600 last:border-r-0 min-h-[3rem] flex items-center justify-center bg-gradient-to-br from-white to-slate-50/30 dark:from-gray-700/40 dark:to-slate-800/40"
|
||||
className="p-3 border-r border-gray-200 dark:border-gray-600 last:border-r-0 min-h-[3rem] flex items-center justify-center bg-linear-to-br from-white to-slate-50/30 dark:from-gray-700/40 dark:to-slate-800/40"
|
||||
>
|
||||
{isTimeSlotAvailable(day, timeSlot, draftAvailabilities) ? (
|
||||
<div className="w-full h-8 bg-gradient-to-r from-emerald-400 via-emerald-500 to-emerald-600 dark:from-emerald-500 dark:via-emerald-600 dark:to-emerald-700 rounded-lg shadow-sm border border-emerald-300 dark:border-emerald-600 flex items-center justify-center group hover:shadow-md transition-all duration-200 hover:scale-105">
|
||||
<div className="w-full h-8 bg-linear-to-r from-emerald-400 via-emerald-500 to-emerald-600 dark:from-emerald-500 dark:via-emerald-600 dark:to-emerald-700 rounded-lg shadow-sm border border-emerald-300 dark:border-emerald-600 flex items-center justify-center group hover:shadow-md transition-all duration-200 hover:scale-105">
|
||||
<div className="w-3 h-3 bg-white/90 rounded-full shadow-sm group-hover:bg-white transition-colors duration-200"></div>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
|
||||
import { useLoginGoogle } from "@ui/hooks/auth";
|
||||
import { useLoginGoogle } from "../../hooks/auth";
|
||||
import { vi } from "vitest";
|
||||
|
||||
vi.mock("../../hooks/auth", () => ({
|
||||
|
|
@ -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 = ({
|
||||
|
|
@ -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 { Badge } from "@xtablo/ui/components/badge";
|
||||
import { UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { ReactNode } from "react";
|
||||
import { Channel } from "stream-chat";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
|
@ -104,7 +104,7 @@ export function ChannelPreview({
|
|||
{displayTitle}
|
||||
</h3>
|
||||
{timestamp && (
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2 flex-shrink-0">
|
||||
<span className="text-xs text-gray-500 dark:text-gray-400 ml-2 shrink-0">
|
||||
{formatTimestamp(timestamp)}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -117,7 +117,7 @@ export function ChannelPreview({
|
|||
|
||||
{/* Unread count badge */}
|
||||
{unreadCount > 0 && (
|
||||
<div className="ml-2 flex-shrink-0">
|
||||
<div className="ml-2 shrink-0">
|
||||
<Badge
|
||||
color="indigo"
|
||||
className="text-xs min-w-[20px] h-5 px-2 py-0 flex items-center justify-center"
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import React from "react";
|
||||
import { useClickOutside } from "../hooks/useClickOutside";
|
||||
import { useClickOutside } from "@xtablo/shared/hooks/useClickOutside";
|
||||
|
||||
interface ClickOutsideProps {
|
||||
children: React.ReactNode;
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Database } from "@ui/types/database.types";
|
||||
import { Database } from "@xtablo/shared/types/database.types";
|
||||
import { useState } from "react";
|
||||
import { ClickOutside } from "./ClickOutside";
|
||||
import { ImageColorPicker } from "./ImageColorPicker";
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { UserTablo } from "@ui/types/tablos.types";
|
||||
import { UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { ChannelHeader, useChannelStateContext } from "stream-chat-react";
|
||||
import { ChannelBadge } from "./ChannelBadge";
|
||||
|
||||
45
apps/main/src/components/CustomModal.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog";
|
||||
import { cn } from "@xtablo/shared";
|
||||
|
||||
// Custom Modal Component - now using shadcn/ui Dialog
|
||||
interface CustomModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => 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 (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className={cn("max-h-[90vh] flex flex-col", getWidthClasses())}>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{title}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="overflow-y-auto flex-1">{children}</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
@ -1,26 +1,26 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
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 { CopyButton } from "@xtablo/ui/components/clipboard";
|
||||
import { useState } from "react";
|
||||
import { TypographyMuted, TypographyP } from "@ui/components/ui/typography";
|
||||
import { TypographyMuted, TypographyP } from "@xtablo/ui/components/typography";
|
||||
|
||||
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red";
|
||||
|
||||
type EmbedType = "full" | "floating";
|
||||
export type EmbedType = "embed" | "floating" | "normal";
|
||||
|
||||
interface EmbedConfig {
|
||||
embedType: EmbedType;
|
||||
|
|
@ -31,30 +31,25 @@ interface EmbedConfig {
|
|||
interface EmbedConfigModalProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
baseEmbedUrl: string;
|
||||
baseFloatingUrl: string;
|
||||
buildPublicLink: (type: "embed" | "floating" | "normal") => string;
|
||||
}
|
||||
|
||||
export function EmbedConfigModal({
|
||||
isOpen,
|
||||
onClose,
|
||||
baseEmbedUrl,
|
||||
baseFloatingUrl,
|
||||
}: EmbedConfigModalProps) {
|
||||
export function EmbedConfigModal({ isOpen, onClose, buildPublicLink }: EmbedConfigModalProps) {
|
||||
const [embedConfig, setEmbedConfig] = useState<EmbedConfig>({
|
||||
embedType: "full",
|
||||
embedType: "embed",
|
||||
backgroundVariant: "purple",
|
||||
buttonVariant: "purple",
|
||||
});
|
||||
|
||||
const getEmbedUrl = () => {
|
||||
const baseUrl = embedConfig.embedType === "full" ? baseEmbedUrl : baseFloatingUrl;
|
||||
const baseUrl = buildPublicLink(embedConfig.embedType);
|
||||
const params = new URLSearchParams({
|
||||
mode: "embed",
|
||||
buttonVariant: embedConfig.buttonVariant,
|
||||
});
|
||||
|
||||
// Only add backgroundVariant for full embed
|
||||
if (embedConfig.embedType === "full") {
|
||||
if (embedConfig.embedType === "embed") {
|
||||
params.set("backgroundVariant", embedConfig.backgroundVariant);
|
||||
}
|
||||
|
||||
|
|
@ -122,7 +117,7 @@ export function EmbedConfigModal({
|
|||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="full">
|
||||
<SelectItem value="embed">
|
||||
<div className="flex flex-col items-start">
|
||||
<TypographyP className="font-medium">Page complète</TypographyP>
|
||||
</div>
|
||||
|
|
@ -136,7 +131,7 @@ export function EmbedConfigModal({
|
|||
</Select>
|
||||
</div>
|
||||
|
||||
{embedConfig.embedType === "full" && (
|
||||
{embedConfig.embedType === "embed" && (
|
||||
<div className="space-y-2">
|
||||
<Label>Couleur de fond</Label>
|
||||
<Select
|
||||
|
|
@ -199,7 +194,7 @@ export function EmbedConfigModal({
|
|||
/>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="flex-shrink-0"
|
||||
className="shrink-0"
|
||||
onClick={() => window.open(getEmbedUrl(), "_blank")}
|
||||
>
|
||||
Aperçu
|
||||
|
|
@ -217,7 +212,7 @@ export function EmbedConfigModal({
|
|||
</TypographyMuted>
|
||||
<div className="relative min-w-0">
|
||||
<div className="overflow-auto max-w-full">
|
||||
<pre className="p-4 pr-16 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md text-xs whitespace-pre-wrap break-words w-full">
|
||||
<pre className="p-4 pr-16 bg-gray-100 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-md text-xs whitespace-pre-wrap wrap-break-word w-full">
|
||||
<code className="break-all">{generateEmbedCode()}</code>
|
||||
</pre>
|
||||
</div>
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Strong, Text } from "@ui/components/ui/typography";
|
||||
import { EventAndTablo } from "@ui/types/events.types";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { Strong, Text } from "@xtablo/ui/components/typography";
|
||||
import { EventAndTablo } from "@xtablo/shared/types/events.types";
|
||||
import { CalendarIcon, User } from "lucide-react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { CustomModal } from "./CustomModal";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
|
||||
import { Button } from "@ui/components/ui/button";
|
||||
import { DatePicker } from "@ui/components/ui/date-picker";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { DatePicker } from "@xtablo/ui/components/date-picker";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -8,22 +8,22 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import { Input } from "@ui/components/ui/input";
|
||||
import { Label } from "@ui/components/ui/label";
|
||||
} from "@xtablo/ui/components/dialog";
|
||||
import { Input } from "@xtablo/ui/components/input";
|
||||
import { Label } from "@xtablo/ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { Textarea } from "@ui/components/ui/textarea";
|
||||
import { TimeInput } from "@ui/components/ui/time-input";
|
||||
import { useCreateEvents, useEvent, useUpdateEvent } from "@ui/hooks/events";
|
||||
import { useTablosList } from "@ui/hooks/tablos";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { Event, EventInsert } from "@ui/types/events.types";
|
||||
} from "@xtablo/ui/components/select";
|
||||
import { Textarea } from "@xtablo/ui/components/textarea";
|
||||
import { TimeInput } from "@xtablo/ui/components/time-input";
|
||||
import { useCreateEvents, useEvent, useUpdateEvent } from "../hooks/events";
|
||||
import { useTablosList } from "../hooks/tablos";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { Event, EventInsert } from "@xtablo/shared/types/events.types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import {
|
||||
Card,
|
||||
CardAction,
|
||||
|
|
@ -6,9 +6,9 @@ import {
|
|||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@ui/components/ui/card";
|
||||
import { EmbedConfigModal } from "@ui/components/EmbedConfigModal";
|
||||
import { EventType, EventTypeConfig, useEventTypes } from "@ui/hooks/event-types";
|
||||
} from "@xtablo/ui/components/card";
|
||||
import { EmbedConfigModal, EmbedType } from "@ui/components/EmbedConfigModal";
|
||||
import { EventType, EventTypeConfig, useEventTypes } from "../hooks/event-types";
|
||||
import {
|
||||
CheckIcon,
|
||||
EditIcon,
|
||||
|
|
@ -18,7 +18,8 @@ import {
|
|||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useUser } from "src/providers/UserStoreProvider";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { match } from "ts-pattern";
|
||||
|
||||
export function EventTypeCard({
|
||||
eventType,
|
||||
|
|
@ -31,10 +32,7 @@ export function EventTypeCard({
|
|||
const user = useUser();
|
||||
const [isEmbedModalOpen, setIsEmbedModalOpen] = useState(false);
|
||||
|
||||
const getPublicLink = (
|
||||
standardName: string | null,
|
||||
type: "normal" | "embed" | "floating" = "normal"
|
||||
) => {
|
||||
const getPublicLink = (standardName: string | null, type: EmbedType) => {
|
||||
// Sanitize user name for URL (replace spaces with hyphens, lowercase, remove special chars)
|
||||
const sanitizedUserName = user.name
|
||||
?.toLowerCase()
|
||||
|
|
@ -44,13 +42,17 @@ export function EventTypeCard({
|
|||
const shortUserId = user.id.substring(0, 6);
|
||||
// Construct the public booking URL
|
||||
const baseUrl = window.location.origin;
|
||||
if (type === "embed") {
|
||||
return `${baseUrl}/embed/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
}
|
||||
if (type === "floating") {
|
||||
return `${baseUrl}/widget/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
}
|
||||
return `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
return match(type)
|
||||
.with("embed", () => {
|
||||
return `${baseUrl}/external/?mode=embed&userInfo=${sanitizedUserName}-${shortUserId}&eventTypeStandardName=${standardName}`;
|
||||
})
|
||||
.with("floating", () => {
|
||||
return `${baseUrl}/external/?mode=widget&userInfo=${sanitizedUserName}-${shortUserId}&eventTypeStandardName=${standardName}`;
|
||||
})
|
||||
.with("normal", () => {
|
||||
return `${baseUrl}/book/${sanitizedUserName}-${shortUserId}/${standardName}`;
|
||||
})
|
||||
.exhaustive();
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
@ -73,7 +75,9 @@ export function EventTypeCard({
|
|||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => window.open(getPublicLink(eventType.standardName ?? null), "_blank")}
|
||||
onClick={() =>
|
||||
window.open(getPublicLink(eventType.standardName ?? null, "normal"), "_blank")
|
||||
}
|
||||
aria-label="Aperçu"
|
||||
>
|
||||
<ExternalLinkIcon className="w-4 h-4" />
|
||||
|
|
@ -154,8 +158,9 @@ export function EventTypeCard({
|
|||
<EmbedConfigModal
|
||||
isOpen={isEmbedModalOpen}
|
||||
onClose={() => setIsEmbedModalOpen(false)}
|
||||
baseEmbedUrl={getPublicLink(eventType.standardName ?? null, "embed")}
|
||||
baseFloatingUrl={getPublicLink(eventType.standardName ?? null, "floating")}
|
||||
buildPublicLink={(type: "embed" | "floating" | "normal") =>
|
||||
getPublicLink(eventType.standardName ?? null, type)
|
||||
}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
|
|
@ -1,23 +1,23 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import { FieldDescription } from "@ui/components/ui/field";
|
||||
import { Input } from "@ui/components/ui/input";
|
||||
import { Label } from "@ui/components/ui/label";
|
||||
} from "@xtablo/ui/components/dialog";
|
||||
import { FieldDescription } from "@xtablo/ui/components/field";
|
||||
import { Input } from "@xtablo/ui/components/input";
|
||||
import { Label } from "@xtablo/ui/components/label";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { Textarea } from "@ui/components/ui/textarea";
|
||||
import { EventTypeConfig } from "@ui/hooks/event-types";
|
||||
} from "@xtablo/ui/components/select";
|
||||
import { Textarea } from "@xtablo/ui/components/textarea";
|
||||
import { EventTypeConfig } from "../hooks/event-types";
|
||||
|
||||
export function EventTypeModal({
|
||||
isModalOpen,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { Button } from "@ui/components/ui/button";
|
||||
import { ButtonGroup } from "@ui/components/ui/button-group";
|
||||
import { DatePickerV1 } from "@ui/components/ui/date-picker";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { ButtonGroup } from "@xtablo/ui/components/button-group";
|
||||
import { DatePickerV1 } from "@xtablo/ui/components/date-picker";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
|
|
@ -9,11 +9,11 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import { Label } from "@ui/components/ui/label";
|
||||
import { TimeInput } from "@ui/components/ui/time-input";
|
||||
import { Exception } from "@ui/hooks/availabilities";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
} from "@xtablo/ui/components/dialog";
|
||||
import { Label } from "@xtablo/ui/components/label";
|
||||
import { TimeInput } from "@xtablo/ui/components/time-input";
|
||||
import { Exception } from "../hooks/availabilities";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { PlusIcon } from "lucide-react";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
|
@ -8,10 +8,10 @@ import {
|
|||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import { Button } from "@ui/components/ui/button";
|
||||
import { Slider } from "@ui/components/ui/slider";
|
||||
import { Label } from "@ui/components/ui/label";
|
||||
} from "@xtablo/ui/components/dialog";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { Slider } from "@xtablo/ui/components/slider";
|
||||
import { Label } from "@xtablo/ui/components/label";
|
||||
|
||||
interface ImageCropDialogProps {
|
||||
open: boolean;
|
||||
|
|
@ -4,14 +4,14 @@ import {
|
|||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { useCreateEvents } from "@ui/hooks/events";
|
||||
import { useCreateTablo, useTablosList } from "@ui/hooks/tablos";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { EventInsert } from "@ui/types/events.types";
|
||||
import { CreateTablo } from "@ui/types/tablos.types";
|
||||
import { ParsedICSEvent, parseICSFile } from "@ui/utils/helpers";
|
||||
} from "@xtablo/ui/components/select";
|
||||
import { useCreateEvents } from "../hooks/events";
|
||||
import { useCreateTablo, useTablosList } from "../hooks/tablos";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { EventInsert } from "@xtablo/shared/types/events.types";
|
||||
import { CreateTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { ParsedICSEvent, parseICSFile } from "@xtablo/shared";
|
||||
import { useRef, useState } from "react";
|
||||
|
||||
interface ImportICSModalProps {
|
||||
|
|
@ -175,7 +175,7 @@ export const ImportICSModal = ({ onClose }: ImportICSModalProps) => {
|
|||
<div className="fixed inset-0 bg-black/80 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white dark:bg-gray-900 rounded-xl shadow-2xl w-full max-w-lg mx-4 overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="bg-gradient-to-r from-green-500 to-green-600 p-6 text-white">
|
||||
<div className="bg-linear-to-r from-green-500 to-green-600 p-6 text-white">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-xl font-medium">Importer un fichier ICS</h2>
|
||||
<button
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { Layout } from "@ui/components/Layout";
|
||||
import { SessionProvider } from "@ui/contexts/SessionContext";
|
||||
import { renderWithProviders } from "@ui/utils/testHelpers";
|
||||
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { renderWithProviders } from "../utils/testHelpers";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
|
||||
describe("Layout", () => {
|
||||
|
|
@ -19,9 +19,9 @@ describe("Layout", () => {
|
|||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<SessionProvider>
|
||||
<SessionTestProvider testUser={undefined}>
|
||||
<Layout />
|
||||
</SessionProvider>
|
||||
</SessionTestProvider>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { MenuIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { Outlet } from "react-router-dom";
|
||||
12
apps/main/src/components/LoadingSpinner.tsx
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
export const LoadingSpinner = () => {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<img
|
||||
src="/icon.jpg"
|
||||
alt="Loading..."
|
||||
role="status"
|
||||
className="animate-spin rounded-full h-16 w-16 object-cover"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { fireEvent, screen } from "@testing-library/react";
|
||||
import { MainNavigation, SideNavigation, UserMenuPopover } from "@ui/components/NavigationBar";
|
||||
import { renderWithProviders } from "@ui/utils/testHelpers";
|
||||
import { renderWithProviders } from "../utils/testHelpers";
|
||||
|
||||
describe("NavigationBar", () => {
|
||||
describe("SideNavigation", () => {
|
||||
|
|
@ -1,16 +1,16 @@
|
|||
// shadcn components
|
||||
import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "@ui/components/ui/avatar";
|
||||
import { Button } from "@ui/components/ui/button";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@ui/components/ui/dropdown-menu";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { isProd, isStaging } from "@ui/utils/helpers";
|
||||
import { getXtabloIcon } from "@ui/utils/iconHelpers";
|
||||
Avatar,
|
||||
AvatarBadge,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from "@xtablo/ui/components/avatar";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger } from "@xtablo/ui/components/dropdown-menu";
|
||||
import { TypographyLarge, TypographyMuted } from "@xtablo/ui/components/typography";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { isProd, isStaging } from "../lib/env";
|
||||
import { getXtabloIcon } from "../utils/iconHelpers";
|
||||
import {
|
||||
CalendarCheckIcon,
|
||||
CalendarIcon,
|
||||
|
|
@ -32,10 +32,9 @@ import { Separator } from "react-aria-components";
|
|||
import { Link as RouterLink, useLocation } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { ThemeSwitcher } from "./ThemeSwitcher";
|
||||
import { TypographyLarge, TypographyMuted } from "./ui/typography";
|
||||
import { cva, type VariantProps } from "class-variance-authority";
|
||||
import { cn } from "src/lib/utils";
|
||||
import { useLogout } from "src/hooks/auth";
|
||||
import { cn } from "@xtablo/shared/lib/cn.ts";
|
||||
import { useLogout } from "../hooks/auth";
|
||||
|
||||
type NavLinkItem = {
|
||||
isActive?: boolean;
|
||||
|
|
@ -49,17 +48,17 @@ function NavLink({ isActive, children }: NavLinkProps) {
|
|||
<div
|
||||
className={twMerge(
|
||||
"group w-full gap-x-3 overflow-hidden px-2.5 py-1.5 text-nowrap hover:bg-navbar-darker hover:no-underline focus-visible:outline-offset-0 [&>[data-ui=icon]:not([class*=size-])]:size-4.5",
|
||||
"[&>[data-ui=notification-badge]]:bg-navbar-darker",
|
||||
"[&>[data-ui=notification-badge]]:rounded-md",
|
||||
"[&>[data-ui=notification-badge]]:top-1/2",
|
||||
"[&>[data-ui=notification-badge]]:right-1",
|
||||
"[&>[data-ui=notification-badge]]:-translate-y-1/2",
|
||||
"[&>[data-ui=notification-badge]]:bg-navbar-darker",
|
||||
"[&>[data-ui=notification-badge]]:p-3",
|
||||
"[&>[data-ui=nxotification-badge]]:text-xs/6",
|
||||
"[&>[data-ui=notification-badge]]:font-semibold",
|
||||
"*:data-[ui=notification-badge]:bg-navbar-darker",
|
||||
"*:data-[ui=notification-badge]:rounded-md",
|
||||
"*:data-[ui=notification-badge]:top-1/2",
|
||||
"*:data-[ui=notification-badge]:right-1",
|
||||
"*:data-[ui=notification-badge]:-translate-y-1/2",
|
||||
"*:data-[ui=notification-badge]:bg-navbar-darker",
|
||||
"*:data-[ui=notification-badge]:p-3",
|
||||
"*:data-[ui=notification-badge]:text-xs/6",
|
||||
"*:data-[ui=notification-badge]:font-semibold",
|
||||
isActive
|
||||
? "bg-navbar-darker font-semibold text-white [&>[data-ui=notification-badge]]:bg-transparent"
|
||||
? "bg-navbar-darker font-semibold text-white *:data-[ui=notification-badge]:bg-transparent"
|
||||
: ["font-medium", "text-gray-300/90 [&:not(:hover)>[data-ui=icon]]:bg-navbar-darker"]
|
||||
)}
|
||||
>
|
||||
|
|
@ -142,7 +141,7 @@ export function UserMenuPopover({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
sideOffset={-8}
|
||||
>
|
||||
<div className="flex gap-2 p-1">
|
||||
<Avatar>
|
||||
<Avatar className="size-8">
|
||||
<AvatarImage src={user.avatar_url ?? undefined} alt={user.name ?? "User avatar"} />
|
||||
<AvatarFallback className="bg-gray-700 text-white">
|
||||
{user.name?.charAt(0).toUpperCase()}
|
||||
|
|
@ -162,7 +161,7 @@ export function UserMenuPopover({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
</div>
|
||||
|
||||
<MenuSeparator />
|
||||
|
||||
|
||||
<MenuDropdownItem
|
||||
icon={<LogOutIcon className="w-5 h-5" aria-hidden="true" />}
|
||||
label="Se déconnecter"
|
||||
|
|
@ -394,10 +393,10 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
<NavLink isActive={location.pathname === "/feedback"}>
|
||||
<RouterLink
|
||||
to="/feedback"
|
||||
className={twMerge("w-full", isCollapsed ? "" : "pl-2")}
|
||||
className="w-full"
|
||||
aria-label={isCollapsed ? "Feedback" : undefined}
|
||||
>
|
||||
<div className="flex items-center gap-x-2">
|
||||
<div className={twMerge("flex items-center gap-x-2", isCollapsed ? "" : "pl-2")}>
|
||||
<SendIcon className="w-5 h-5" aria-hidden="true" />
|
||||
<TypographyLarge
|
||||
className={twMerge(
|
||||
|
|
@ -415,4 +414,4 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
|
|||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { screen, waitFor } from "@testing-library/react";
|
||||
import { ProtectedRoute } from "@ui/components/ProtectedRoute";
|
||||
import { SessionTestProvider } from "@ui/contexts/SessionContext";
|
||||
import { renderWithRouter } from "@ui/utils/testHelpers";
|
||||
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { renderWithRouter } from "../utils/testHelpers";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { TestUserStoreProvider } from "src/providers/UserStoreProvider";
|
||||
import { TestUserStoreProvider } from "../providers/UserStoreProvider";
|
||||
|
||||
describe("ProtectedRoute", () => {
|
||||
beforeEach(() => {
|
||||
|
|
@ -2,7 +2,7 @@ import { useEffect, useState } from "react";
|
|||
import { Navigate, Outlet } from "react-router-dom";
|
||||
import { match } from "ts-pattern";
|
||||
import { LoadingSpinner } from "./LoadingSpinner";
|
||||
import { useMaybeUser } from "src/providers/UserStoreProvider";
|
||||
import { useMaybeUser } from "../providers/UserStoreProvider";
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
fallback?: string;
|
||||
|
|
@ -42,7 +42,7 @@ export const ProtectedRoute = ({ fallback, shouldRedirectToCurrentPage }: Protec
|
|||
|
||||
return match(status)
|
||||
.with("loading", () => <LoadingSpinner />)
|
||||
.with("should-land-user", () => <Navigate to="/landing" replace />)
|
||||
.with("should-land-user", () => <Navigate to={redirectUrl} replace />)
|
||||
.with("should-redirect", () => <Navigate to={redirectUrl} replace />)
|
||||
.with("should-pass", () => <Outlet />)
|
||||
.exhaustive();
|
||||
|
|
@ -1,15 +1,15 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { useInviteUser } from "@ui/hooks/invite";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { useInviteUser } from "../hooks/invite";
|
||||
import {
|
||||
useCreateTabloFile,
|
||||
useDeleteTabloFile,
|
||||
useDownloadTabloFile,
|
||||
useTabloFileNames,
|
||||
} from "@ui/hooks/tablo_data";
|
||||
import { useTabloMembers } from "@ui/hooks/tablos";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { TabloUpdate, UserTablo } from "@ui/types/tablos.types";
|
||||
} from "../hooks/tablo_data";
|
||||
import { useTabloMembers } from "../hooks/tablos";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { FileTrigger } from "react-aria-components";
|
||||
import { DownloadIcon, Trash2Icon } from "lucide-react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
|
|
@ -226,10 +226,10 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
|
|||
<ClickOutside onClickOutside={onClose}>
|
||||
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-2xl w-full min-w-[32rem] max-w-2xl max-h-[95vh] flex flex-col overflow-hidden">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700 flex-shrink-0">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-b border-gray-200 dark:border-gray-700 shrink-0">
|
||||
<div className="flex items-center space-x-3 flex-1">
|
||||
{/* Tablo Color/Image Preview */}
|
||||
<div className="flex-shrink-0">
|
||||
<div className="shrink-0">
|
||||
{tablo.image ? (
|
||||
<img
|
||||
src={tablo.image}
|
||||
|
|
@ -314,7 +314,7 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
|
|||
{error && (
|
||||
<div className="mx-6 mt-4 p-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg flex items-center space-x-2">
|
||||
<svg
|
||||
className="w-5 h-5 text-red-500 flex-shrink-0"
|
||||
className="w-5 h-5 text-red-500 shrink-0"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
|
|
@ -866,7 +866,7 @@ export const TabloModal = ({ tablo, onClose, onEdit }: TabloModalProps) => {
|
|||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 flex-shrink-0 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="flex items-center justify-between px-6 py-4 border-t border-gray-200 dark:border-gray-700 shrink-0 bg-gray-50 dark:bg-gray-900/50">
|
||||
<div className="flex space-x-3 ml-auto">
|
||||
{!isAdmin ? (
|
||||
<button
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { ArrowLeft, ArrowRight, HelpCircle, X } from "lucide-react";
|
||||
import React, { useState } from "react";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { fireEvent, render, screen } from "@testing-library/react";
|
||||
import { ThemeSwitcher } from "@ui/components/ThemeSwitcher";
|
||||
import * as ThemeContext from "@ui/contexts/ThemeContext";
|
||||
import * as ThemeContext from "@xtablo/shared/contexts/ThemeContext";
|
||||
import { vi } from "vitest";
|
||||
|
||||
// Mock the ThemeProvider and useTheme hook
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { ButtonGroup } from "@ui/components/ui/button-group";
|
||||
import { useTheme } from "@ui/contexts/ThemeContext";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { ButtonGroup } from "@xtablo/ui/components/button-group";
|
||||
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
|
||||
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
|
||||
|
||||
const translation = {
|
||||
|
|
@ -4,21 +4,16 @@ import {
|
|||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from "@ui/components/ui/dialog";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@ui/components/ui/select";
|
||||
import { useTablosList } from "@ui/hooks/tablos";
|
||||
import { useGenerateWebcalToken } from "@ui/hooks/webcal";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
} from "@xtablo/ui/components/dialog";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@xtablo/ui/components/select";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { Input } from "@xtablo/ui/components/input";
|
||||
import { Label } from "@xtablo/ui/components/label";
|
||||
import { useTablosList } from "../hooks/tablos";
|
||||
import { useGenerateWebcalToken } from "../hooks/webcal";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useState } from "react";
|
||||
import { Button } from "./ui/button";
|
||||
import { Input } from "./ui/input";
|
||||
import { Label } from "./ui/label";
|
||||
|
||||
import { CopyIcon } from "lucide-react";
|
||||
interface WebcalModalProps {
|
||||
open: boolean;
|
||||
|
|
@ -43,7 +38,6 @@ export const WebcalModal = ({ open, onOpenChange }: WebcalModalProps) => {
|
|||
{ timeout: 2000 }
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Failed to copy:", error);
|
||||
toast.add(
|
||||
{
|
||||
title: "Erreur",
|
||||
|
|
@ -69,7 +63,7 @@ export const WebcalModal = ({ open, onOpenChange }: WebcalModalProps) => {
|
|||
}}
|
||||
>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader className="bg-gradient-to-r from-purple-500 to-purple-600 -m-6 mb-0 p-6 text-white rounded-t-lg">
|
||||
<DialogHeader className="bg-linear-to-r from-purple-500 to-purple-600 -m-6 mb-0 p-6 text-white rounded-t-lg">
|
||||
<DialogTitle className="text-xl font-medium text-white">
|
||||
Synchronisation de calendrier
|
||||
</DialogTitle>
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { Button } from "@ui/components/ui/button";
|
||||
import { getXtabloIcon } from "@ui/utils/iconHelpers";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { getXtabloIcon } from "../utils/iconHelpers";
|
||||
import { Link } from "react-router-dom";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
|
|
@ -1,14 +1,15 @@
|
|||
import {
|
||||
createClient,
|
||||
Session,
|
||||
User as SupabaseUser,
|
||||
} from "@supabase/supabase-js";
|
||||
import { Session, User as SupabaseUser } from "@supabase/supabase-js";
|
||||
import { useMutation } from "@tanstack/react-query";
|
||||
import { api, queryClient } from "@ui/lib/api";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { api } from "../lib/api";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { match } from "ts-pattern";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { AxiosInstance } from "axios";
|
||||
import { useSession } from "@xtablo/shared";
|
||||
import { useSignUpToStream } from "@xtablo/shared/hooks/auth";
|
||||
|
||||
export type User = SupabaseUser & {
|
||||
user_metadata: {
|
||||
|
|
@ -20,15 +21,6 @@ export type User = SupabaseUser & {
|
|||
};
|
||||
};
|
||||
|
||||
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 = createClient(supabaseUrl, supabaseAnonKey);
|
||||
|
||||
interface SignUpData {
|
||||
email: string;
|
||||
password: string;
|
||||
|
|
@ -51,7 +43,7 @@ interface AuthResponse {
|
|||
export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
|
||||
const navigate = useNavigate();
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const { signUpToStream } = useSignUpToStream();
|
||||
const { signUpToStream } = useSignUpToStream(api);
|
||||
const { mutate, isPending } = useMutation<
|
||||
AuthResponse,
|
||||
{ message: string; code: string },
|
||||
|
|
@ -110,100 +102,10 @@ export function useSignUp({ redirectUrl }: { redirectUrl: string | null }) {
|
|||
return { mutate, isPending, errors };
|
||||
}
|
||||
|
||||
export function useSignUpWithoutPassword() {
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const { signUpToStream } = useSignUpToStream();
|
||||
const { mutateAsync, isPending } = useMutation<
|
||||
AuthResponse,
|
||||
{ message: string; code: string },
|
||||
{ email: string; name: string }
|
||||
>({
|
||||
mutationFn: async (data: { email: string; name: string }) => {
|
||||
// Generate a temporary password for the user
|
||||
const tempPassword =
|
||||
Math.random().toString(36).slice(-8) +
|
||||
Math.random().toString(36).slice(-8);
|
||||
|
||||
const { data: response, error } = await supabase.auth.signUp({
|
||||
email: data.email.trim(),
|
||||
password: tempPassword,
|
||||
options: {
|
||||
data: {
|
||||
first_name: data.name.trim().split(" ")[0] || "",
|
||||
last_name: data.name.trim().split(" ").slice(1).join(" ") || "",
|
||||
business_name: "",
|
||||
},
|
||||
},
|
||||
});
|
||||
if (error) throw error;
|
||||
if (response.session?.access_token) {
|
||||
await signUpToStream(response.session.access_token);
|
||||
}
|
||||
|
||||
// Mark the user as temporary
|
||||
if (response.session?.access_token) {
|
||||
await api.post(
|
||||
"/api/v1/users/mark-temporary",
|
||||
{
|
||||
temporary_password: tempPassword,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${response.session.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
return response;
|
||||
},
|
||||
onError: (error) => {
|
||||
const errMap: Record<string, string> = {};
|
||||
|
||||
match(error.code)
|
||||
.with("user_already_exists", () => {
|
||||
errMap["email"] = "Cette adresse email est déjà utilisée";
|
||||
})
|
||||
.otherwise(() => {
|
||||
toast.add(
|
||||
{
|
||||
title: "Erreur",
|
||||
description: error.message,
|
||||
type: "error",
|
||||
position: "top-left",
|
||||
},
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
setErrors(errMap);
|
||||
},
|
||||
});
|
||||
return { mutateAsync, isPending, errors };
|
||||
}
|
||||
|
||||
export function useSignUpToStream() {
|
||||
const { mutate: signUpToStream } = useMutation({
|
||||
mutationFn: async (accessToken: string) => {
|
||||
const { data } = await api.post<{ streamToken: string }>(
|
||||
"/api/v1/users/sign-up-to-stream",
|
||||
{},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
return { signUpToStream };
|
||||
}
|
||||
export function useLoginEmail({ redirectUrl }: { redirectUrl: string | null }) {
|
||||
const navigate = useNavigate();
|
||||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||||
const { signUpToStream } = useSignUpToStream();
|
||||
const { signUpToStream } = useSignUpToStream(api);
|
||||
const { mutate, isPending } = useMutation<
|
||||
AuthResponse,
|
||||
{ message: string; code: string },
|
||||
|
|
@ -290,3 +192,12 @@ export function useLogout() {
|
|||
},
|
||||
});
|
||||
}
|
||||
|
||||
export const useAuthedApi = (): AxiosInstance => {
|
||||
const { session } = useSession();
|
||||
return api.create({
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { supabase } from "@ui/hooks/auth";
|
||||
import { queryClient } from "@ui/lib/api";
|
||||
import { Database } from "@ui/types/database.types";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { Database } from "@xtablo/shared/types/database.types";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "src/lib/toast";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
|
||||
export type TimeRange = {
|
||||
start: string;
|
||||
|
|
@ -53,8 +53,7 @@ export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
|
|||
);
|
||||
|
||||
export function useAvailabilities() {
|
||||
const { session } = useSession();
|
||||
|
||||
const user = useUser();
|
||||
const { data: availabilities, isLoading } = useQuery<
|
||||
Database["public"]["Tables"]["availabilities"]["Row"]
|
||||
>({
|
||||
|
|
@ -63,12 +62,12 @@ export function useAvailabilities() {
|
|||
const { data, error } = await supabase
|
||||
.from("availabilities")
|
||||
.select("*")
|
||||
.eq("user_id", session?.user.id)
|
||||
.eq("user_id", user.id)
|
||||
.limit(1);
|
||||
if (error) throw error;
|
||||
return data?.[0] as Database["public"]["Tables"]["availabilities"]["Row"];
|
||||
},
|
||||
enabled: !!session?.user.id,
|
||||
enabled: !!user.id,
|
||||
});
|
||||
|
||||
const { mutate: updateAvailabilities } = useMutation<
|
||||
|
|
@ -96,7 +95,7 @@ export function useAvailabilities() {
|
|||
{
|
||||
availability_data: newAvailabilities,
|
||||
exceptions: newExceptions,
|
||||
user_id: session?.user.id,
|
||||
user_id: user.id,
|
||||
},
|
||||
{
|
||||
onConflict: "user_id",
|
||||
|
|
@ -138,7 +137,7 @@ export function useAvailabilities() {
|
|||
availability_data:
|
||||
availabilities?.availability_data || DEFAULT_AVAILABILITIES,
|
||||
exceptions: updatedExceptions,
|
||||
user_id: session?.user.id,
|
||||
user_id: user.id,
|
||||
},
|
||||
{
|
||||
onConflict: "user_id",
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { Database } from "@ui/types/database.types";
|
||||
import { supabase } from "./auth";
|
||||
import { useSession } from "@xtablo/shared";
|
||||
import { Database } from "@xtablo/shared/types/database.types";
|
||||
import { supabase } from "../lib/supabase";
|
||||
|
||||
type Devis = Database["public"]["Tables"]["devis"];
|
||||
|
||||
|
|
@ -25,7 +25,10 @@ export const useDevis = (id: string) => {
|
|||
return useQuery({
|
||||
queryKey: ["devis", id],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.from("devis").select("*").eq("id", id);
|
||||
const { data, error } = await supabase
|
||||
.from("devis")
|
||||
.select("*")
|
||||
.eq("id", id);
|
||||
if (error) throw error;
|
||||
return data[0];
|
||||
},
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { queryClient } from "@ui/lib/api";
|
||||
import { Database } from "@ui/types/database.types";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { Database } from "@xtablo/shared/types/database.types";
|
||||
import { useMemo } from "react";
|
||||
import { supabase } from "./auth";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { useUser } from "src/providers/UserStoreProvider";
|
||||
|
||||
export type EventTypeData = {
|
||||
id: string;
|
||||
|
|
@ -49,7 +49,7 @@ export type EventTypePayload = {
|
|||
const QUERY_KEY = ["event-types"];
|
||||
|
||||
export function useEventTypes() {
|
||||
const { session } = useSession();
|
||||
const user = useUser();
|
||||
|
||||
const { data: eventTypesData, isLoading } = useQuery<
|
||||
Database["public"]["Tables"]["event_types"]["Row"][]
|
||||
|
|
@ -59,12 +59,12 @@ export function useEventTypes() {
|
|||
const { data, error } = await supabase
|
||||
.from("event_types")
|
||||
.select("*")
|
||||
.eq("user_id", session?.user.id)
|
||||
.eq("user_id", user.id)
|
||||
.is("deleted_at", null);
|
||||
if (error) throw error;
|
||||
return data;
|
||||
},
|
||||
enabled: !!session?.user.id,
|
||||
enabled: !!user.id,
|
||||
});
|
||||
|
||||
const { mutate: addEventType } = useMutation<
|
||||
|
|
@ -78,7 +78,7 @@ export function useEventTypes() {
|
|||
const { error } = await supabase.from("event_types").insert({
|
||||
config: eventType,
|
||||
is_active: true,
|
||||
user_id: session?.user.id,
|
||||
user_id: user.id,
|
||||
});
|
||||
if (error) throw error;
|
||||
},
|
||||
|
|
@ -95,7 +95,13 @@ export function useEventTypes() {
|
|||
eventType: EventTypePayload;
|
||||
}
|
||||
>({
|
||||
mutationFn: async ({ id, eventType }: { id: string; eventType: EventTypePayload }) => {
|
||||
mutationFn: async ({
|
||||
id,
|
||||
eventType,
|
||||
}: {
|
||||
id: string;
|
||||
eventType: EventTypePayload;
|
||||
}) => {
|
||||
const { error } = await supabase
|
||||
.from("event_types")
|
||||
.update({
|
||||
|
|
@ -1,8 +1,13 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { Event, EventAndTablo, EventInsert, EventUpdate } from "@ui/types/events.types";
|
||||
import { supabase } from "./auth";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import {
|
||||
Event,
|
||||
EventAndTablo,
|
||||
EventInsert,
|
||||
EventUpdate,
|
||||
} from "@xtablo/shared/types/events.types";
|
||||
import { supabase } from "../lib/supabase";
|
||||
|
||||
// Fetch events for a specific tablo
|
||||
export const useEventsByTablo = (tabloId: string | null) => {
|
||||
|
|
@ -34,7 +39,11 @@ export const useEventsByTablo = (tabloId: string | null) => {
|
|||
};
|
||||
|
||||
// Fetch events for a specific date range
|
||||
export const useEventsByDateRange = (tabloId: string, startDate: string, endDate: string) => {
|
||||
export const useEventsByDateRange = (
|
||||
tabloId: string,
|
||||
startDate: string,
|
||||
endDate: string
|
||||
) => {
|
||||
return useQuery({
|
||||
queryKey: ["events", tabloId, "dateRange", startDate, endDate],
|
||||
queryFn: async () => {
|
||||
|
|
@ -103,7 +112,9 @@ export const useCreateEvents = () => {
|
|||
queryClient.invalidateQueries({ queryKey: ["events"] });
|
||||
const eventsCount = Array.isArray(data) ? data.length : 1;
|
||||
const pluralizeText =
|
||||
eventsCount === 1 ? "L'événement a été créé" : "Les événements ont été créés";
|
||||
eventsCount === 1
|
||||
? "L'événement a été créé"
|
||||
: "Les événements ont été créés";
|
||||
toast.add(
|
||||
{
|
||||
title: `${eventsCount} événement(s) créé(s)`,
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { FeedbackData } from "@ui/pages/feedback";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { supabase } from "./auth";
|
||||
import { FeedbackData } from "../pages/feedback";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { supabase } from "../lib/supabase";
|
||||
|
||||
// Create new feedback
|
||||
export const useCreateFeedback = () => {
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { QueryKey, useMutation, useQuery } from "@tanstack/react-query";
|
||||
import { supabase } from "@ui/hooks/auth";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { queryClient } from "src/lib/api";
|
||||
import { Tables } from "@ui/types/database.types";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { Tables } from "@xtablo/shared/types/database.types";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
type IntroductionConfig = {
|
||||
|
|
@ -1,11 +1,10 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { api } from "@ui/lib/api";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useAuthedApi } from "./auth";
|
||||
|
||||
// Invite user by email
|
||||
export const useInviteUser = () => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async ({
|
||||
email,
|
||||
|
|
@ -14,18 +13,10 @@ export const useInviteUser = () => {
|
|||
email: string;
|
||||
tablo_id: string;
|
||||
}) => {
|
||||
const { data } = await api.post(
|
||||
"/api/v1/tablos/invite",
|
||||
{
|
||||
email,
|
||||
tablo_id,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const { data } = await api.post("/api/v1/tablos/invite", {
|
||||
email,
|
||||
tablo_id,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -45,18 +36,10 @@ export const useInviteUser = () => {
|
|||
};
|
||||
|
||||
export const useJoinTablo = () => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const { mutate } = useMutation({
|
||||
mutationFn: async ({ token }: { token: string }) => {
|
||||
const { data } = await api.post(
|
||||
"/api/v1/tablos/join",
|
||||
{ token },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const { data } = await api.post("/api/v1/tablos/join", { token });
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,9 +1,9 @@
|
|||
import { QueryKey, useMutation } from "@tanstack/react-query";
|
||||
import { supabase } from "@ui/hooks/auth";
|
||||
import { api, queryClient } from "@ui/lib/api";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { useAuthedApi } from "./auth";
|
||||
|
||||
/**
|
||||
* Hook to update user profile using Supabase client
|
||||
|
|
@ -66,7 +66,7 @@ type FileUploadRequest = {
|
|||
};
|
||||
|
||||
export const useUploadAvatar = () => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const { mutate, isPending } = useMutation({
|
||||
mutationFn: async ({ file }: { file: File | null }) => {
|
||||
if (!file) {
|
||||
|
|
@ -104,13 +104,7 @@ export const useUploadAvatar = () => {
|
|||
// Upload to backend
|
||||
const response = await api.post(
|
||||
"/api/v1/users/profile/avatar",
|
||||
uploadRequest,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
uploadRequest
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
|
|
@ -143,14 +137,10 @@ export const useUploadAvatar = () => {
|
|||
};
|
||||
|
||||
export const useRemoveAvatar = () => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const { mutateAsync, isPending } = useMutation({
|
||||
mutationFn: async () => {
|
||||
await api.delete("/api/v1/users/profile/avatar", {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
await api.delete("/api/v1/users/profile/avatar");
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.add({
|
||||
|
|
@ -1,7 +1,11 @@
|
|||
import { QueryClient, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { api } from "@ui/lib/api";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import {
|
||||
QueryClient,
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
} from "@tanstack/react-query";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useAuthedApi } from "./auth";
|
||||
|
||||
// Types for tablo data API responses
|
||||
export interface TabloFile {
|
||||
|
|
@ -34,15 +38,11 @@ export const toastOptions = {
|
|||
|
||||
// Hook to get all file names for a tablo
|
||||
export function useTabloFileNames(tabloId: string) {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const { data, isLoading, error } = useQuery<TabloFileList>({
|
||||
queryKey: ["tablo-files", tabloId],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/api/v1/tablo-data/${tabloId}/filenames`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const response = await api.get(`/api/v1/tablo-data/${tabloId}/filenames`);
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to fetch tablo files");
|
||||
}
|
||||
|
|
@ -55,15 +55,13 @@ export function useTabloFileNames(tabloId: string) {
|
|||
|
||||
// Hook to get a specific file from a tablo
|
||||
export function useTabloFile(tabloId: string, fileName: string) {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
return useQuery<TabloFile>({
|
||||
queryKey: ["tablo-file", tabloId, fileName],
|
||||
queryFn: async () => {
|
||||
const response = await api.get(`/api/v1/tablo-data/${tabloId}/${fileName}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const response = await api.get(
|
||||
`/api/v1/tablo-data/${tabloId}/${fileName}`
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to fetch file");
|
||||
}
|
||||
|
|
@ -75,16 +73,14 @@ export function useTabloFile(tabloId: string, fileName: string) {
|
|||
|
||||
// Hook to download a file from a tablo
|
||||
export function useDownloadTabloFile() {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
|
||||
return useMutation<void, Error, { tabloId: string; fileName: string }>({
|
||||
mutationFn: async ({ tabloId, fileName }) => {
|
||||
try {
|
||||
const response = await api.get(`/api/v1/tablo-data/${tabloId}/${fileName}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const response = await api.get(
|
||||
`/api/v1/tablo-data/${tabloId}/${fileName}`
|
||||
);
|
||||
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to download file");
|
||||
|
|
@ -144,7 +140,7 @@ export function useDownloadTabloFile() {
|
|||
|
||||
// Hook to create a new file in a tablo
|
||||
export function useCreateTabloFile() {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
|
|
@ -153,11 +149,10 @@ export function useCreateTabloFile() {
|
|||
{ tabloId: string; fileName: string; data: FileUploadRequest }
|
||||
>({
|
||||
mutationFn: async ({ tabloId, fileName, data }) => {
|
||||
const response = await api.post(`/api/v1/tablo-data/${tabloId}/${fileName}`, data, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const response = await api.post(
|
||||
`/api/v1/tablo-data/${tabloId}/${fileName}`,
|
||||
data
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to create file");
|
||||
}
|
||||
|
|
@ -193,7 +188,7 @@ export function useCreateTabloFile() {
|
|||
|
||||
// Hook to update an existing file in a tablo
|
||||
export function useUpdateTabloFile() {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<
|
||||
|
|
@ -202,11 +197,10 @@ export function useUpdateTabloFile() {
|
|||
{ tabloId: string; fileName: string; data: FileUploadRequest }
|
||||
>({
|
||||
mutationFn: async ({ tabloId, fileName, data }) => {
|
||||
const response = await api.put(`/api/v1/tablo-data/${tabloId}/${fileName}`, data, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const response = await api.put(
|
||||
`/api/v1/tablo-data/${tabloId}/${fileName}`,
|
||||
data
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to update file");
|
||||
}
|
||||
|
|
@ -242,16 +236,18 @@ export function useUpdateTabloFile() {
|
|||
|
||||
// Hook to delete a file from a tablo
|
||||
export function useDeleteTabloFile() {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation<FileOperationResponse, Error, { tabloId: string; fileName: string }>({
|
||||
return useMutation<
|
||||
FileOperationResponse,
|
||||
Error,
|
||||
{ tabloId: string; fileName: string }
|
||||
>({
|
||||
mutationFn: async ({ tabloId, fileName }) => {
|
||||
const response = await api.delete(`/api/v1/tablo-data/${tabloId}/${fileName}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const response = await api.delete(
|
||||
`/api/v1/tablo-data/${tabloId}/${fileName}`
|
||||
);
|
||||
if (response.status !== 200) {
|
||||
throw new Error("Failed to delete file");
|
||||
}
|
||||
|
|
@ -286,7 +282,10 @@ export function useDeleteTabloFile() {
|
|||
}
|
||||
|
||||
// Utility function to invalidate all tablo data queries for a specific tablo
|
||||
export const invalidateTabloData = (queryClient: QueryClient, tabloId: string) => {
|
||||
export const invalidateTabloData = (
|
||||
queryClient: QueryClient,
|
||||
tabloId: string
|
||||
) => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: ["tablo-files", tabloId],
|
||||
});
|
||||
|
|
@ -315,7 +314,11 @@ export function useUploadTabloFile() {
|
|||
// Check if file exists first (unless overwrite is explicitly true)
|
||||
if (!overwrite) {
|
||||
try {
|
||||
const existingFile = queryClient.getQueryData(["tablo-file", tabloId, fileName]);
|
||||
const existingFile = queryClient.getQueryData([
|
||||
"tablo-file",
|
||||
tabloId,
|
||||
fileName,
|
||||
]);
|
||||
if (existingFile) {
|
||||
// File exists, use update
|
||||
return await updateFile.mutateAsync({ tabloId, fileName, data });
|
||||
|
|
@ -1,21 +1,19 @@
|
|||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { invalidatePublicSlots } from "@ui/hooks/public";
|
||||
import { api } from "@ui/lib/api";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||
import { Database } from "@ui/types/database.types";
|
||||
import { EventInsertInTablo } from "@ui/types/events.types";
|
||||
import { RemoveNullFromObject } from "@ui/types/removeNull";
|
||||
import { CreateTablo } from "@ui/types/tablos.types";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { supabase } from "./auth";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useUser } from "../providers/UserStoreProvider";
|
||||
import { Database } from "@xtablo/shared/types/database.types";
|
||||
import { RemoveNullFromObject } from "@xtablo/shared/types/removeNull";
|
||||
import { CreateTablo } from "@xtablo/shared/types/tablos.types";
|
||||
import { supabase } from "../lib/supabase";
|
||||
import { useAuthedApi } from "./auth";
|
||||
|
||||
type Tablo = Database["public"]["Tables"]["tablos"];
|
||||
|
||||
type TabloUpdate = Tablo["Update"];
|
||||
|
||||
type UserTablo = RemoveNullFromObject<Database["public"]["Views"]["user_tablos"]["Row"]>;
|
||||
type UserTablo = RemoveNullFromObject<
|
||||
Database["public"]["Views"]["user_tablos"]["Row"]
|
||||
>;
|
||||
|
||||
// Fetch all tablos
|
||||
export const useTablosList = () => {
|
||||
|
|
@ -23,7 +21,10 @@ export const useTablosList = () => {
|
|||
return useQuery({
|
||||
queryKey: ["tablos"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.from("user_tablos").select("*").eq("user_id", user.id);
|
||||
const { data, error } = await supabase
|
||||
.from("user_tablos")
|
||||
.select("*")
|
||||
.eq("user_id", user.id);
|
||||
if (error) throw error;
|
||||
const tablos = data as UserTablo[];
|
||||
return tablos;
|
||||
|
|
@ -49,17 +50,13 @@ export const useTablo = (id: string) => {
|
|||
|
||||
// Fetch tablo members
|
||||
export const useTabloMembers = (tabloId: string) => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["tablo-members", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<{
|
||||
members: { id: string; name: string; is_admin: boolean }[];
|
||||
}>(`/api/v1/tablos/members/${tabloId}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
}>(`/api/v1/tablos/members/${tabloId}`);
|
||||
return data.members;
|
||||
},
|
||||
});
|
||||
|
|
@ -69,16 +66,12 @@ export const useTabloMembers = (tabloId: string) => {
|
|||
|
||||
// Create new tablo
|
||||
export const useCreateTablo = () => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (tablo: CreateTablo) => {
|
||||
const { data } = await api.post("/api/v1/tablos/create", tablo, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
const { data } = await api.post("/api/v1/tablos/create", tablo);
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
|
|
@ -100,82 +93,17 @@ export const useCreateTablo = () => {
|
|||
});
|
||||
};
|
||||
|
||||
// Create tablo with owner
|
||||
export const useCreateTabloWithOwner = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return useMutation<
|
||||
{ id: string },
|
||||
unknown,
|
||||
CreateTablo & {
|
||||
owner_short_id: string;
|
||||
event: EventInsertInTablo;
|
||||
access_token: string;
|
||||
}
|
||||
>({
|
||||
mutationFn: async (
|
||||
tabloAndToken: CreateTablo & {
|
||||
owner_short_id: string;
|
||||
event: EventInsertInTablo;
|
||||
access_token: string;
|
||||
}
|
||||
) => {
|
||||
const { data } = await api.post(
|
||||
"/api/v1/tablos/create-and-invite",
|
||||
{
|
||||
owner_short_id: tabloAndToken.owner_short_id,
|
||||
event: tabloAndToken.event,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${tabloAndToken.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
return data;
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tablos"] });
|
||||
invalidatePublicSlots();
|
||||
navigate(`/chat/${data.id}`, { replace: true });
|
||||
navigate(0);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error);
|
||||
toast.add(
|
||||
{
|
||||
title: "Échec de la création du tablo",
|
||||
description: "Veuillez réessayer",
|
||||
type: "error",
|
||||
},
|
||||
{
|
||||
timeout: 5000,
|
||||
}
|
||||
);
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
// Update tablo
|
||||
export const useUpdateTablo = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ id, ...tablo }: TabloUpdate & { id: string }) => {
|
||||
const { data } = await api.patch(
|
||||
`/api/v1/tablos/update`,
|
||||
{
|
||||
id,
|
||||
...tablo,
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const { data } = await api.patch(`/api/v1/tablos/update`, {
|
||||
id,
|
||||
...tablo,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: (_, { id }) => {
|
||||
|
|
@ -203,16 +131,11 @@ export const useUpdateTablo = () => {
|
|||
// Delete tablo (soft delete)
|
||||
export const useDeleteTablo = () => {
|
||||
const queryClient = useQueryClient();
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
await api.delete(`/api/v1/tablos/delete`, {
|
||||
data: { id },
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
});
|
||||
await api.delete(`/api/v1/tablos/delete`, { data: { id } });
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ["tablos"] });
|
||||
|
|
@ -238,7 +161,10 @@ export const useGetAllTabloAccess = () => {
|
|||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ["tablo-access", user.id],
|
||||
queryFn: async () => {
|
||||
const { data } = await supabase.from("tablo_access").select("*").eq("user_id", user.id);
|
||||
const { data } = await supabase
|
||||
.from("tablo_access")
|
||||
.select("*")
|
||||
.eq("user_id", user.id);
|
||||
return data;
|
||||
},
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { api } from "@ui/lib/api";
|
||||
import { api } from "../lib/api";
|
||||
|
||||
interface UserMetadata {
|
||||
email: string;
|
||||
|
|
@ -1,7 +1,6 @@
|
|||
import { useMutation } from "@tanstack/react-query";
|
||||
import { useSession } from "@ui/contexts/SessionContext";
|
||||
import { api } from "@ui/lib/api";
|
||||
import { toast } from "@ui/lib/toast";
|
||||
import { toast } from "@xtablo/shared";
|
||||
import { useAuthedApi } from "./auth";
|
||||
|
||||
export interface WebcalToken {
|
||||
token: string;
|
||||
|
|
@ -18,26 +17,21 @@ export interface WebcalResponse {
|
|||
|
||||
// Generate a new webcal token
|
||||
export const useGenerateWebcalToken = () => {
|
||||
const { session } = useSession();
|
||||
const api = useAuthedApi();
|
||||
|
||||
const { mutate, isPending, data } = useMutation({
|
||||
mutationFn: async (tablo_id: string | null): Promise<WebcalResponse> => {
|
||||
const { data } = await api.post(
|
||||
"/api/v1/tablos/webcal/generate-url",
|
||||
{ tablo_id },
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${session?.access_token}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
const { data } = await api.post("/api/v1/tablos/webcal/generate-url", {
|
||||
tablo_id,
|
||||
});
|
||||
return data;
|
||||
},
|
||||
onSuccess: () => {
|
||||
toast.add(
|
||||
{
|
||||
title: "URL de synchronisation générée",
|
||||
description: "Vous pouvez maintenant ajouter ce calendrier à votre application",
|
||||
description:
|
||||
"Vous pouvez maintenant ajouter ce calendrier à votre application",
|
||||
type: "success",
|
||||
},
|
||||
{
|
||||
3
apps/main/src/lib/api.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import { buildApi } from "@xtablo/shared";
|
||||
|
||||
export const api = buildApi(import.meta.env.VITE_API_URL);
|
||||
4
apps/main/src/lib/env.ts
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
// Environment helpers using Vite's import.meta.env
|
||||
export const isStaging = import.meta.env.MODE === "staging";
|
||||
export const isProd = import.meta.env.MODE === "production";
|
||||
export const isDev = import.meta.env.DEV;
|
||||
|
|
@ -1,25 +1,25 @@
|
|||
import { AuthenticationGateway } from "@ui/components/AuthenticationGateway";
|
||||
import { EventModal } from "@ui/components/EventModal";
|
||||
import { Layout } from "@ui/components/Layout";
|
||||
import { ProtectedRoute } from "@ui/components/ProtectedRoute";
|
||||
import { AvailabilitiesPage } from "@ui/pages/availabilities";
|
||||
import { BookingsPage } from "@ui/pages/bookings";
|
||||
import { ChantiersPage } from "@ui/pages/chantiers";
|
||||
import { ChatPage } from "@ui/pages/chat";
|
||||
import { EventTypesPage } from "@ui/pages/event-types-page";
|
||||
import { FeedbackPage } from "@ui/pages/feedback";
|
||||
import { JoinPage } from "@ui/pages/join";
|
||||
import { LandingPage } from "@ui/pages/landing";
|
||||
import { LoginPage } from "@ui/pages/login";
|
||||
import { NotFoundPage } from "@ui/pages/NotFoundPage";
|
||||
import { OAuthSigninPage } from "@ui/pages/oauth-signin";
|
||||
import { PublicBookingPage } from "@ui/pages/PublicBookingPage";
|
||||
import { PlanningPage } from "@ui/pages/planning";
|
||||
import { ResetPasswordPage } from "@ui/pages/reset-password";
|
||||
import SettingsPage from "@ui/pages/settings";
|
||||
import { SignUpPage } from "@ui/pages/signup";
|
||||
import { TabloPage } from "@ui/pages/tablo";
|
||||
import ChatProvider from "@ui/providers/ChatProvider";
|
||||
import { AuthenticationGateway } from "../components/AuthenticationGateway";
|
||||
import { EventModal } from "../components/EventModal";
|
||||
import { Layout } from "../components/Layout";
|
||||
import { ProtectedRoute } from "../components/ProtectedRoute";
|
||||
import { AvailabilitiesPage } from "../pages/availabilities";
|
||||
import { BookingsPage } from "../pages/bookings";
|
||||
import { ChantiersPage } from "../pages/chantiers";
|
||||
import { ChatPage } from "../pages/chat";
|
||||
import { EventTypesPage } from "../pages/event-types-page";
|
||||
import { FeedbackPage } from "../pages/feedback";
|
||||
import { JoinPage } from "../pages/join";
|
||||
import { LandingPage } from "../pages/landing";
|
||||
import { LoginPage } from "../pages/login";
|
||||
import { NotFoundPage } from "../pages/NotFoundPage";
|
||||
import { OAuthSigninPage } from "../pages/oauth-signin";
|
||||
import { PublicBookingPage } from "../pages/PublicBookingPage";
|
||||
import { PlanningPage } from "../pages/planning";
|
||||
import { ResetPasswordPage } from "../pages/reset-password";
|
||||
import SettingsPage from "../pages/settings";
|
||||
import { SignUpPage } from "../pages/signup";
|
||||
import { TabloPage } from "../pages/tablo";
|
||||
import ChatProvider from "../providers/ChatProvider";
|
||||
import { RouteObject } from "react-router-dom";
|
||||
|
||||
export const routes: RouteObject[] = [
|
||||
10
apps/main/src/lib/supabase.ts
Normal file
|
|
@ -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);
|
||||
1271
apps/main/src/main.css
Normal file
|
|
@ -2,9 +2,10 @@ import { QueryClientProvider } from "@tanstack/react-query";
|
|||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { App } from "./App";
|
||||
import { queryClient } from "./lib/api";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
|
||||
import "stream-chat-react/dist/css/v2/index.css";
|
||||
import "@xtablo/ui/styles/globals.css";
|
||||
import "./main.css";
|
||||
|
||||
createRoot(document.getElementById("root")!).render(
|
||||