Lint and format

This commit is contained in:
Arthur Belleville 2025-10-23 21:36:21 +02:00
parent 00c844e8d5
commit 7a0a5548f9
No known key found for this signature in database
110 changed files with 1079 additions and 1086 deletions

View file

@ -39,10 +39,16 @@ pnpm build
# Build only apps (main, external)
pnpm build:apps
# Build main app for specific environments
pnpm build:staging # Build for staging
pnpm build:prod # Build for production
```
**Note:** The `@xtablo/shared` and `@xtablo/ui` packages are source-only packages. They export TypeScript source files directly and are consumed by app bundlers (Vite) without a separate build step. This is faster and simpler for development.
**Environment Builds:** The main app supports environment-specific builds (`staging`, `production`) that are properly cached by Turborepo based on environment-specific inputs (`.env.staging`, `.env.production`, etc.).
### Development
```bash
@ -138,6 +144,22 @@ turbo build --filter='./packages/*'
turbo build --filter='!@xtablo/external'
```
### Package-Level Configuration
Packages can have their own `turbo.json` file to define custom tasks or override root configuration:
**Example:** `apps/main/turbo.json` defines environment-specific builds:
- `build:staging` - Builds for staging with `.env.staging`
- `build:prod` - Builds for production with `.env.production`
Each task:
- Extends the root config with `"extends": ["//"]`
- Defines specific inputs (including environment files)
- Configures caching with appropriate outputs
- Can pass environment variables to the build process
## Package Development Workflow
### 1. Working on Packages
@ -244,6 +266,8 @@ pnpm build
## CI/CD Considerations
### Basic Pipeline
For CI/CD pipelines:
```bash
@ -259,6 +283,45 @@ pnpm typecheck
pnpm test
```
### Environment-Specific Deployments
**Staging Pipeline:**
```bash
# Install dependencies
pnpm install --frozen-lockfile
# Build for staging
pnpm build:staging
# Deploy (example with wrangler)
cd apps/main && pnpm deploy:staging
```
**Production Pipeline:**
```bash
# Install dependencies
pnpm install --frozen-lockfile
# Run all checks
pnpm lint
pnpm typecheck
pnpm test
# Build for production
pnpm build:prod
# Deploy (example with wrangler)
cd apps/main && pnpm deploy:prod
```
**Benefits:**
- Turborepo caches builds per environment
- Environment-specific `.env` files are tracked as inputs
- Builds are only re-run when relevant files change
## Additional Resources
- [Turborepo Documentation](https://turbo.build/repo/docs)

View file

@ -48,6 +48,8 @@ pnpm dev:external # Run external app only
# Building
pnpm build # Build all apps
pnpm build:apps # Build apps only
pnpm build:staging # Build main app for staging
pnpm build:prod # Build main app for production
# Testing & Quality
pnpm test # Run all tests

View file

@ -1,22 +1,9 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false },
"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}"
]
"ignoreUnknown": true,
"includes": ["src/**/*", "*.{ts,tsx,js,jsx,json}"]
},
"formatter": {
"enabled": true,
@ -278,14 +265,7 @@
}
},
{
"includes": [
"ui/src/**/*.{ts,tsx}",
"ui/worker/**/*.{ts,tsx}",
"ui/*.{ts,tsx}",
"api/src/**/*.{ts,tsx}",
"api/*.{ts,tsx}",
"xtablo-expo/**/*.{ts,tsx}"
],
"includes": ["src/**/*.{ts,tsx}", "*.{ts,tsx}"],
"linter": {
"rules": {
"complexity": { "noArguments": "error" },
@ -314,29 +294,6 @@
}
}
}
},
{
"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"
}
}
}
}
]
}

View file

@ -39,4 +39,3 @@
"zustand": "^5.0.5"
}
}

View file

@ -1,5 +1,5 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog";
import { cn } from "@xtablo/shared";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog";
// Custom Modal Component - now using shadcn/ui Dialog
interface CustomModalProps {

View file

@ -1,16 +1,18 @@
import { CustomModal } from "./CustomModal";
import { LoadingSpinner } from "./LoadingSpinner";
import { useCreateTabloWithOwner } from "@xtablo/shared";
import { useSession } from "@xtablo/shared/contexts/SessionContext";
import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth";
import { TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public";
import { EventInsertInTablo } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import { FieldError } from "@xtablo/ui/components/field";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import { Text, TypographyH3, TypographyH4, TypographyMuted } from "@xtablo/ui/components/typography";
import { 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 {
Text,
TypographyH3,
TypographyH4,
TypographyMuted,
} from "@xtablo/ui/components/typography";
import {
CalendarIcon,
ChevronLeftIcon,
@ -22,8 +24,11 @@ import {
import { useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { supabase } from "./lib/supabase";
import { CustomModal } from "./CustomModal";
import { LoadingSpinner } from "./LoadingSpinner";
import { api } from "./lib/api";
import { supabase } from "./lib/supabase";
import { useMaybeUser } from "./UserStoreProvider";
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red";
@ -209,19 +214,16 @@ export function EmbeddedBookingPage() {
const shortUserId = userInfo?.substring(userInfo.lastIndexOf("-") + 1);
console.log({shortUserId, eventTypeStandardName})
console.log({ shortUserId, eventTypeStandardName });
const { data: publicSlots, isLoading: isLoadingSlots } = usePublicSlots(
api,
shortUserId || "",
eventTypeStandardName || ""
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api,
() => {
handleCloseModal();
}
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, () => {
handleCloseModal();
});
const userProfile = publicSlots?.user;
const eventType = publicSlots?.eventType;

View file

@ -1,16 +1,13 @@
import { CustomModal } from "./CustomModal";
import { LoadingSpinner } from "./LoadingSpinner";
import { useCreateTabloWithOwner } from "@xtablo/shared";
import { useSession } from "@xtablo/shared/contexts/SessionContext";
import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth";
import { TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public";
import { EventInsertInTablo } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import { FieldError } from "@xtablo/ui/components/field";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import { Text, 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,9 +20,12 @@ import {
import { useState } from "react";
import { useParams, useSearchParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { supabase } from "./lib/supabase";
import { CustomModal } from "./CustomModal";
import { LoadingSpinner } from "./LoadingSpinner";
import { api } from "./lib/api";
import { useCreateTabloWithOwner } from "@xtablo/shared";
import { supabase } from "./lib/supabase";
// import { useCreateTabloWithOwner } from "@xtablo/shared";
import { useMaybeUser } from "./UserStoreProvider";
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red";
@ -132,12 +132,10 @@ export function FloatingBookingWidget() {
eventTypeStandardName || ""
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api,
() => {
handleCloseModal();
setIsWidgetOpen(false);
}
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, () => {
handleCloseModal();
setIsWidgetOpen(false);
});
const userProfile = publicSlots?.user;
const eventType = publicSlots?.eventType;

View file

@ -1,9 +1,9 @@
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 { LoadingSpinner } from "./LoadingSpinner";
import { api } from "./lib/api";
export type User = Tables<"profiles"> & {
@ -12,7 +12,6 @@ export type User = Tables<"profiles"> & {
const UserStoreContext = React.createContext<StoreApi<User> | null>(null);
export const UserStoreProvider = ({ children }: { children: React.ReactNode }) => {
const { session } = useSession();
const shouldFetchUser = !!session?.access_token;

View file

@ -301,8 +301,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(150px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(150px)
rotate(-360deg);
transform: translate(-50%, -50%) rotate(360deg) translateX(150px) rotate(-360deg);
}
}
@ -311,8 +310,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(200px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg) translateX(200px)
rotate(360deg);
transform: translate(-50%, -50%) rotate(-360deg) translateX(200px) rotate(360deg);
}
}
@ -321,8 +319,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(100px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(100px)
rotate(-360deg);
transform: translate(-50%, -50%) rotate(360deg) translateX(100px) rotate(-360deg);
}
}
@ -500,8 +497,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(250px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(250px)
rotate(-360deg);
transform: translate(-50%, -50%) rotate(360deg) translateX(250px) rotate(-360deg);
}
}
@ -510,8 +506,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(120px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg) translateX(120px)
rotate(360deg);
transform: translate(-50%, -50%) rotate(-360deg) translateX(120px) rotate(360deg);
}
}

View file

@ -1,10 +1,9 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { queryClient } from "@xtablo/shared";
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
import { Toaster } from "@xtablo/ui/components/sonner";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter as Router } from "react-router-dom";
import AppRoutes from "./routes";

View file

@ -1,6 +1,6 @@
import { Routes, Route } from 'react-router-dom';
import { EmbeddedBookingPage } from './EmbeddedBookingPage';
import { FloatingBookingWidget } from './FloatingBookingWidget';
import { Route, Routes } from "react-router-dom";
import { EmbeddedBookingPage } from "./EmbeddedBookingPage";
import { FloatingBookingWidget } from "./FloatingBookingWidget";
export default function AppRoutes() {
return (

View file

@ -26,4 +26,3 @@
"include": ["src"],
"references": []
}

4
apps/main/.biomeignore Normal file
View file

@ -0,0 +1,4 @@
# Generated Wrangler files
worker-configuration.d.ts
worker/

View file

@ -1,22 +1,10 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": true },
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false },
"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}"
]
"ignoreUnknown": true,
"maxSize": 10485760,
"includes": ["src/**/*"]
},
"formatter": {
"enabled": true,
@ -278,14 +266,7 @@
}
},
{
"includes": [
"ui/src/**/*.{ts,tsx}",
"ui/worker/**/*.{ts,tsx}",
"ui/*.{ts,tsx}",
"api/src/**/*.{ts,tsx}",
"api/*.{ts,tsx}",
"xtablo-expo/**/*.{ts,tsx}"
],
"includes": ["src/**/*.{ts,tsx}", "worker/**/*.{ts,tsx}", "*.{ts,tsx}"],
"linter": {
"rules": {
"complexity": { "noArguments": "error" },
@ -314,29 +295,6 @@
}
}
}
},
{
"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"
}
}
}
}
]
}

View file

@ -13,8 +13,7 @@
"preview": "vite preview",
"build:staging": "tsc -b && vite build --mode staging",
"build:prod": "tsc -b && vite build --mode production",
"deploy:staging": "tsc -b && vite build --mode staging && wrangler deploy --env=\"\"",
"deploy:prod": "tsc -b && vite build --mode production && wrangler deploy --env=\"\"",
"deploy": "wrangler deploy --env=\"\"",
"cf-typegen": "wrangler types",
"test": "vitest run --mode dev --passWithNoTests",
"test:watch": "vitest watch --passWithNoTests",

View file

@ -1,11 +1,11 @@
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 { Toaster } from "@xtablo/ui/components/sonner";
import { BrowserRouter as Router, useRoutes } from "react-router-dom";
import { routes } from "./lib/routes";
import { supabase } from "./lib/supabase";
import { DatadogRumProvider } from "./providers/DatadogRumProvider";
import { UserStoreProvider } from "./providers/UserStoreProvider";
const AppRoutes = () => {
const element = useRoutes(routes);

View file

@ -1,8 +1,8 @@
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 "../providers/UserStoreProvider";
import { LoadingSpinner } from "./LoadingSpinner";
export const AuthenticationGateway = () => {
const user = useMaybeUser();

View file

@ -1,26 +1,28 @@
import { screen, waitFor } from "@testing-library/react";
import { AuthenticationGateway } from "@ui/components/AuthenticationGateway";
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
import { renderWithRouter } from "../utils/testHelpers";
import { Route, Routes } from "react-router-dom";
import { renderWithRouter } from "../utils/testHelpers";
describe("PublicRoute", () => {
it("shows loading state initially", () => {
renderWithRouter(
<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(),
}}>
<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>} />

View file

@ -1,7 +1,7 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useLoginGoogle } from "../../hooks/auth";
import { vi } from "vitest";
import { useLoginGoogle } from "../../hooks/auth";
vi.mock("../../hooks/auth", () => ({
useLoginGoogle: vi.fn(),

View file

@ -1,6 +1,6 @@
import { ChannelBadge } from "@ui/components/ChannelBadge";
import { Badge } from "@xtablo/ui/components/badge";
import { UserTablo } from "@xtablo/shared/types/tablos.types";
import { Badge } from "@xtablo/ui/components/badge";
import { ReactNode } from "react";
import { Channel } from "stream-chat";
import { twMerge } from "tailwind-merge";

View file

@ -1,5 +1,5 @@
import React from "react";
import { useClickOutside } from "@xtablo/shared/hooks/useClickOutside";
import React from "react";
interface ClickOutsideProps {
children: React.ReactNode;

View file

@ -1,5 +1,5 @@
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog";
import { cn } from "@xtablo/shared";
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@xtablo/ui/components/dialog";
// Custom Modal Component - now using shadcn/ui Dialog
interface CustomModalProps {

View file

@ -1,4 +1,5 @@
import { Button } from "@xtablo/ui/components/button";
import { CopyButton } from "@xtablo/ui/components/clipboard";
import {
Dialog,
DialogContent,
@ -14,9 +15,8 @@ import {
SelectTrigger,
SelectValue,
} from "@xtablo/ui/components/select";
import { CopyButton } from "@xtablo/ui/components/clipboard";
import { useState } from "react";
import { TypographyMuted, TypographyP } from "@xtablo/ui/components/typography";
import { useState } from "react";
type ColorVariant = "black" | "white" | "blue" | "purple" | "green" | "orange" | "red";

View file

@ -1,6 +1,6 @@
import { EventAndTablo } from "@xtablo/shared/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";

View file

@ -1,4 +1,5 @@
import { getLocalTimeZone, parseDate, today } from "@internationalized/date";
import { Event, EventInsert } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import { DatePicker } from "@xtablo/ui/components/date-picker";
import {
@ -20,12 +21,11 @@ import {
} from "@xtablo/ui/components/select";
import { Textarea } from "@xtablo/ui/components/textarea";
import { TimeInput } from "@xtablo/ui/components/time-input";
import { useEffect, useState } from "react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
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";
export const EventModal = ({ mode }: { mode: "create" | "edit" }) => {
const { event_id } = useParams();

View file

@ -1,3 +1,4 @@
import { EmbedConfigModal, EmbedType } from "@ui/components/EmbedConfigModal";
import { Button } from "@xtablo/ui/components/button";
import {
Card,
@ -7,8 +8,6 @@ import {
CardHeader,
CardTitle,
} from "@xtablo/ui/components/card";
import { EmbedConfigModal, EmbedType } from "@ui/components/EmbedConfigModal";
import { EventType, EventTypeConfig, useEventTypes } from "../hooks/event-types";
import {
CheckIcon,
EditIcon,
@ -18,8 +17,9 @@ import {
XIcon,
} from "lucide-react";
import { useState } from "react";
import { useUser } from "../providers/UserStoreProvider";
import { match } from "ts-pattern";
import { EventType, EventTypeConfig, useEventTypes } from "../hooks/event-types";
import { useUser } from "../providers/UserStoreProvider";
export function EventTypeCard({
eventType,

View file

@ -1,4 +1,5 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { toast } from "@xtablo/shared";
import { Button } from "@xtablo/ui/components/button";
import { ButtonGroup } from "@xtablo/ui/components/button-group";
import { DatePickerV1 } from "@xtablo/ui/components/date-picker";
@ -12,11 +13,10 @@ import {
} 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";
import { Exception } from "../hooks/availabilities";
const formSchema = z.object({
exceptionType: z.enum(["day", "hours"]),

View file

@ -1,6 +1,4 @@
import { useState, useCallback } from "react";
import Cropper from "react-easy-crop";
import type { Area } from "react-easy-crop";
import { Button } from "@xtablo/ui/components/button";
import {
Dialog,
DialogContent,
@ -9,9 +7,11 @@ import {
DialogHeader,
DialogTitle,
} 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";
import { Slider } from "@xtablo/ui/components/slider";
import { useCallback, useState } from "react";
import type { Area } from "react-easy-crop";
import Cropper from "react-easy-crop";
interface ImageCropDialogProps {
open: boolean;

View file

@ -1,3 +1,6 @@
import { ParsedICSEvent, parseICSFile, toast } from "@xtablo/shared";
import { EventInsert } from "@xtablo/shared/types/events.types";
import { CreateTablo } from "@xtablo/shared/types/tablos.types";
import {
Select,
SelectContent,
@ -5,14 +8,10 @@ import {
SelectTrigger,
SelectValue,
} from "@xtablo/ui/components/select";
import { useRef, useState } from "react";
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 {
onClose: () => void;

View file

@ -1,8 +1,8 @@
import { fireEvent, render, screen } from "@testing-library/react";
import { Layout } from "@ui/components/Layout";
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
import { renderWithProviders } from "../utils/testHelpers";
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
import { BrowserRouter } from "react-router-dom";
import { renderWithProviders } from "../utils/testHelpers";
describe("Layout", () => {
it("renders the layout with children", () => {

View file

@ -1,16 +1,17 @@
// shadcn components
import {
Avatar,
AvatarBadge,
AvatarFallback,
AvatarImage,
} from "@xtablo/ui/components/avatar";
import { cn } from "@xtablo/shared/lib/cn.ts";
import { 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 {
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 { cva, type VariantProps } from "class-variance-authority";
import {
CalendarCheckIcon,
CalendarIcon,
@ -31,10 +32,11 @@ import { useState } from "react";
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 { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@xtablo/shared/lib/cn.ts";
import { useLogout } from "../hooks/auth";
import { isProd, isStaging } from "../lib/env";
import { useUser } from "../providers/UserStoreProvider";
import { getXtabloIcon } from "../utils/iconHelpers";
import { ThemeSwitcher } from "./ThemeSwitcher";
type NavLinkItem = {
isActive?: boolean;
@ -161,7 +163,7 @@ export function UserMenuPopover({ isCollapsed }: { isCollapsed: boolean }) {
</div>
<MenuSeparator />
<MenuDropdownItem
icon={<LogOutIcon className="w-5 h-5" aria-hidden="true" />}
label="Se déconnecter"
@ -414,4 +416,4 @@ export function MainNavigation({ isCollapsed }: { isCollapsed: boolean }) {
</ul>
</nav>
);
}
}

View file

@ -1,9 +1,9 @@
import { screen, waitFor } from "@testing-library/react";
import { ProtectedRoute } from "@ui/components/ProtectedRoute";
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
import { renderWithRouter } from "../utils/testHelpers";
import { Route, Routes } from "react-router-dom";
import { TestUserStoreProvider } from "../providers/UserStoreProvider";
import { renderWithRouter } from "../utils/testHelpers";
describe("ProtectedRoute", () => {
beforeEach(() => {

View file

@ -1,8 +1,8 @@
import { useEffect, useState } from "react";
import { Navigate, Outlet } from "react-router-dom";
import { match } from "ts-pattern";
import { LoadingSpinner } from "./LoadingSpinner";
import { useMaybeUser } from "../providers/UserStoreProvider";
import { LoadingSpinner } from "./LoadingSpinner";
interface ProtectedRouteProps {
fallback?: string;

View file

@ -1,4 +1,9 @@
import { toast } from "@xtablo/shared";
import { TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types";
import { Button } from "@xtablo/ui/components/button";
import { DownloadIcon, Trash2Icon } from "lucide-react";
import { useEffect, useRef, useState } from "react";
import { FileTrigger } from "react-aria-components";
import { useInviteUser } from "../hooks/invite";
import {
useCreateTabloFile,
@ -7,12 +12,7 @@ import {
useTabloFileNames,
} 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";
import { ClickOutside } from "./ClickOutside";
import { ImageColorPicker } from "./ImageColorPicker";
import { StatusPicker } from "./StatusPicker";

View file

@ -1,6 +1,6 @@
import { useTheme } from "@xtablo/shared/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 = {

View file

@ -1,3 +1,5 @@
import { toast } from "@xtablo/shared";
import { Button } from "@xtablo/ui/components/button";
import {
Dialog,
DialogContent,
@ -5,16 +7,20 @@ import {
DialogHeader,
DialogTitle,
} 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 {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@xtablo/ui/components/select";
import { CopyIcon } from "lucide-react";
import { useState } from "react";
import { useTablosList } from "../hooks/tablos";
import { useGenerateWebcalToken } from "../hooks/webcal";
import { toast } from "@xtablo/shared";
import { useState } from "react";
import { CopyIcon } from "lucide-react";
interface WebcalModalProps {
open: boolean;
onOpenChange: (open: boolean) => void;
@ -37,7 +43,7 @@ export const WebcalModal = ({ open, onOpenChange }: WebcalModalProps) => {
},
{ timeout: 2000 }
);
} catch (error) {
} catch (_error) {
toast.add(
{
title: "Erreur",

View file

@ -1,7 +1,7 @@
import { Button } from "@xtablo/ui/components/button";
import { getXtabloIcon } from "../utils/iconHelpers";
import { Link } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { getXtabloIcon } from "../utils/iconHelpers";
export function Header() {
const logo = getXtabloIcon();

View file

@ -1,15 +1,13 @@
import { Session, User as SupabaseUser } from "@supabase/supabase-js";
import { useMutation } from "@tanstack/react-query";
import { api } from "../lib/api";
import { queryClient } from "@xtablo/shared";
import { toast } from "@xtablo/shared";
import { queryClient, toast, useSession } from "@xtablo/shared";
import { useSignUpToStream } from "@xtablo/shared/hooks/auth";
import { AxiosInstance } from "axios";
import { useState } from "react";
import { useNavigate } from "react-router-dom";
import { match } from "ts-pattern";
import { api } from "../lib/api";
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: {

View file

@ -1,9 +1,8 @@
import { useMutation, useQuery } from "@tanstack/react-query";
import { supabase } from "../lib/supabase";
import { queryClient } from "@xtablo/shared";
import { queryClient, toast } from "@xtablo/shared";
import { Database } from "@xtablo/shared/types/database.types";
import { useEffect, useState } from "react";
import { toast } from "@xtablo/shared";
import { supabase } from "../lib/supabase";
import { useUser } from "../providers/UserStoreProvider";
export type TimeRange = {
@ -34,23 +33,20 @@ export type Exception = {
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
(acc, day) => {
if (day === 5 || day === 6) {
acc[day] = {
enabled: false,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
} else {
acc[day] = {
enabled: true,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
}
return acc;
},
{} as WeeklyAvailability
);
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce((acc, day) => {
if (day === 5 || day === 6) {
acc[day] = {
enabled: false,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
} else {
acc[day] = {
enabled: true,
timeRanges: [{ start: "09:00", end: "17:00" }],
};
}
return acc;
}, {} as WeeklyAvailability);
export function useAvailabilities() {
const user = useUser();
@ -86,8 +82,7 @@ export function useAvailabilities() {
newException?: Exception | null;
}) => {
const newAvailabilities = updatedAvailabilities;
const newExceptions =
(availabilities?.exceptions as Exception[] | null) || [];
const newExceptions = (availabilities?.exceptions as Exception[] | null) || [];
if (newException) {
newExceptions.push(newException);
}
@ -120,22 +115,14 @@ export function useAvailabilities() {
},
});
const { mutate: deleteException } = useMutation<
void,
Error,
{ exceptionIndex: number }
>({
const { mutate: deleteException } = useMutation<void, Error, { exceptionIndex: number }>({
mutationFn: async ({ exceptionIndex }: { exceptionIndex: number }) => {
const currentExceptions =
(availabilities?.exceptions as Exception[] | null) || [];
const updatedExceptions = currentExceptions.filter(
(_, index) => index !== exceptionIndex
);
const currentExceptions = (availabilities?.exceptions as Exception[] | null) || [];
const updatedExceptions = currentExceptions.filter((_, index) => index !== exceptionIndex);
const { error } = await supabase.from("availabilities").upsert(
{
availability_data:
availabilities?.availability_data || DEFAULT_AVAILABILITIES,
availability_data: availabilities?.availability_data || DEFAULT_AVAILABILITIES,
exceptions: updatedExceptions,
user_id: user.id,
},
@ -155,14 +142,11 @@ export function useAvailabilities() {
},
});
const [draftAvailabilities, setDraftAvailabilities] =
useState<WeeklyAvailability | null>(null);
const [draftAvailabilities, setDraftAvailabilities] = useState<WeeklyAvailability | null>(null);
useEffect(() => {
if (availabilities?.availability_data) {
setDraftAvailabilities(
availabilities.availability_data as WeeklyAvailability
);
setDraftAvailabilities(availabilities.availability_data as WeeklyAvailability);
}
}, [availabilities?.availability_data]);

View file

@ -25,10 +25,7 @@ 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];
},

View file

@ -2,8 +2,8 @@ import { useMutation, useQuery } from "@tanstack/react-query";
import { queryClient } from "@xtablo/shared";
import { Database } from "@xtablo/shared/types/database.types";
import { useMemo } from "react";
import { supabase } from "../lib/supabase";
import { useUser } from "src/providers/UserStoreProvider";
import { supabase } from "../lib/supabase";
export type EventTypeData = {
id: string;
@ -95,13 +95,7 @@ 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({

View file

@ -1,13 +1,8 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "@xtablo/shared";
import { useUser } from "../providers/UserStoreProvider";
import {
Event,
EventAndTablo,
EventInsert,
EventUpdate,
} from "@xtablo/shared/types/events.types";
import { Event, EventAndTablo, EventInsert, EventUpdate } from "@xtablo/shared/types/events.types";
import { supabase } from "../lib/supabase";
import { useUser } from "../providers/UserStoreProvider";
// Fetch events for a specific tablo
export const useEventsByTablo = (tabloId: string | null) => {
@ -39,11 +34,7 @@ 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 () => {
@ -112,9 +103,7 @@ 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)`,

View file

@ -1,7 +1,7 @@
import { useMutation } from "@tanstack/react-query";
import { supabase } from "../lib/supabase";
import { FeedbackData } from "../pages/feedback";
import { useUser } from "../providers/UserStoreProvider";
import { supabase } from "../lib/supabase";
// Create new feedback
export const useCreateFeedback = () => {

View file

@ -1,10 +1,9 @@
import { QueryKey, useMutation, useQuery } from "@tanstack/react-query";
import { supabase } from "../lib/supabase";
import { toast } from "@xtablo/shared";
import { useUser } from "../providers/UserStoreProvider";
import { queryClient } from "@xtablo/shared";
import { queryClient, toast } from "@xtablo/shared";
import { Tables } from "@xtablo/shared/types/database.types";
import { useEffect, useState } from "react";
import { supabase } from "../lib/supabase";
import { useUser } from "../providers/UserStoreProvider";
type IntroductionConfig = {
intro_email: string;
@ -24,8 +23,7 @@ export const useIntroduction = (): {
const { mutate: updateIntroduction, isPending: updateIntroductionPending } =
useUpsertIntroduction();
const [draftIntroduction, setDraftIntroduction] =
useState<IntroductionConfig | null>(null);
const [draftIntroduction, setDraftIntroduction] = useState<IntroductionConfig | null>(null);
useEffect(() => {
if (data) {
@ -112,8 +110,7 @@ export function useUpsertIntroduction() {
onError: () => {
toast.add({
title: "Erreur",
description:
"Une erreur est survenue lors de la mise à jour de votre introduction",
description: "Une erreur est survenue lors de la mise à jour de votre introduction",
type: "error",
position: "top-center",
});
@ -130,10 +127,7 @@ export function useDeleteIntroduction() {
const { mutate, isPending } = useMutation({
mutationFn: async () => {
const { error } = await supabase
.from("user_introductions")
.delete()
.eq("user_id", user.id);
const { error } = await supabase.from("user_introductions").delete().eq("user_id", user.id);
if (error) {
throw new Error(error.message);
@ -154,8 +148,7 @@ export function useDeleteIntroduction() {
onError: () => {
toast.add({
title: "Erreur",
description:
"Une erreur est survenue lors de la suppression de votre introduction",
description: "Une erreur est survenue lors de la suppression de votre introduction",
type: "error",
position: "top-center",
});

View file

@ -6,13 +6,7 @@ import { useAuthedApi } from "./auth";
export const useInviteUser = () => {
const api = useAuthedApi();
const { mutate, isPending } = useMutation({
mutationFn: async ({
email,
tablo_id,
}: {
email: string;
tablo_id: string;
}) => {
mutationFn: async ({ email, tablo_id }: { email: string; tablo_id: string }) => {
const { data } = await api.post("/api/v1/tablos/invite", {
email,
tablo_id,

View file

@ -1,7 +1,6 @@
import { QueryKey, useMutation } from "@tanstack/react-query";
import { queryClient, toast } from "@xtablo/shared";
import { supabase } from "../lib/supabase";
import { queryClient } from "@xtablo/shared";
import { toast } from "@xtablo/shared";
import { useUser } from "../providers/UserStoreProvider";
import { useAuthedApi } from "./auth";
@ -13,13 +12,7 @@ export function useUpdateProfile() {
const user = useUser();
const { mutate, isPending } = useMutation({
mutationFn: async ({
firstName,
lastName,
}: {
firstName: string;
lastName: string;
}) => {
mutationFn: async ({ firstName, lastName }: { firstName: string; lastName: string }) => {
// Build the name from first_name and last_name if provided
const fullName = [firstName, lastName].filter(Boolean).join(" ");
@ -49,8 +42,7 @@ export function useUpdateProfile() {
onError: () => {
toast.add({
title: "Erreur",
description:
"Une erreur est survenue lors de la mise à jour de votre profil",
description: "Une erreur est survenue lors de la mise à jour de votre profil",
type: "error",
position: "top-center",
});
@ -102,10 +94,7 @@ export const useUploadAvatar = () => {
};
// Upload to backend
const response = await api.post(
"/api/v1/users/profile/avatar",
uploadRequest
);
const response = await api.post("/api/v1/users/profile/avatar", uploadRequest);
if (response.status !== 200) {
throw new Error("Failed to upload avatar");
@ -126,8 +115,7 @@ export const useUploadAvatar = () => {
onError: (error: Error) => {
toast.add({
title: "Erreur",
description:
error.message || "Une erreur est survenue lors de l'upload",
description: error.message || "Une erreur est survenue lors de l'upload",
type: "error",
position: "top-center",
});

View file

@ -1,9 +1,4 @@
import {
QueryClient,
useMutation,
useQuery,
useQueryClient,
} from "@tanstack/react-query";
import { QueryClient, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { toast } from "@xtablo/shared";
import { useAuthedApi } from "./auth";
@ -59,9 +54,7 @@ export function useTabloFile(tabloId: string, fileName: string) {
return useQuery<TabloFile>({
queryKey: ["tablo-file", tabloId, fileName],
queryFn: async () => {
const response = await api.get(
`/api/v1/tablo-data/${tabloId}/${fileName}`
);
const response = await api.get(`/api/v1/tablo-data/${tabloId}/${fileName}`);
if (response.status !== 200) {
throw new Error("Failed to fetch file");
}
@ -78,9 +71,7 @@ export function useDownloadTabloFile() {
return useMutation<void, Error, { tabloId: string; fileName: string }>({
mutationFn: async ({ tabloId, fileName }) => {
try {
const response = await api.get(
`/api/v1/tablo-data/${tabloId}/${fileName}`
);
const response = await api.get(`/api/v1/tablo-data/${tabloId}/${fileName}`);
if (response.status !== 200) {
throw new Error("Failed to download file");
@ -149,10 +140,7 @@ 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
);
const response = await api.post(`/api/v1/tablo-data/${tabloId}/${fileName}`, data);
if (response.status !== 200) {
throw new Error("Failed to create file");
}
@ -197,10 +185,7 @@ 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
);
const response = await api.put(`/api/v1/tablo-data/${tabloId}/${fileName}`, data);
if (response.status !== 200) {
throw new Error("Failed to update file");
}
@ -239,15 +224,9 @@ export function useDeleteTabloFile() {
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}`
);
const response = await api.delete(`/api/v1/tablo-data/${tabloId}/${fileName}`);
if (response.status !== 200) {
throw new Error("Failed to delete file");
}
@ -282,10 +261,7 @@ 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],
});
@ -314,11 +290,7 @@ 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 });

View file

@ -1,19 +1,17 @@
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
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 { useUser } from "../providers/UserStoreProvider";
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 = () => {
@ -21,10 +19,7 @@ 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;
@ -161,10 +156,7 @@ 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;
},
});

View file

@ -30,8 +30,7 @@ export const useGenerateWebcalToken = () => {
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",
},
{

View file

@ -1,3 +1,4 @@
import { RouteObject } from "react-router-dom";
import { AuthenticationGateway } from "../components/AuthenticationGateway";
import { EventModal } from "../components/EventModal";
import { Layout } from "../components/Layout";
@ -20,7 +21,6 @@ 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[] = [
// Protected routes

View file

@ -301,8 +301,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(150px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(150px)
rotate(-360deg);
transform: translate(-50%, -50%) rotate(360deg) translateX(150px) rotate(-360deg);
}
}
@ -311,8 +310,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(200px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg) translateX(200px)
rotate(360deg);
transform: translate(-50%, -50%) rotate(-360deg) translateX(200px) rotate(360deg);
}
}
@ -321,8 +319,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(100px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(100px)
rotate(-360deg);
transform: translate(-50%, -50%) rotate(360deg) translateX(100px) rotate(-360deg);
}
}
@ -500,8 +497,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(250px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(360deg) translateX(250px)
rotate(-360deg);
transform: translate(-50%, -50%) rotate(360deg) translateX(250px) rotate(-360deg);
}
}
@ -510,8 +506,7 @@
transform: translate(-50%, -50%) rotate(0deg) translateX(120px) rotate(0deg);
}
100% {
transform: translate(-50%, -50%) rotate(-360deg) translateX(120px)
rotate(360deg);
transform: translate(-50%, -50%) rotate(-360deg) translateX(120px) rotate(360deg);
}
}

View file

@ -1,8 +1,8 @@
import { QueryClientProvider } from "@tanstack/react-query";
import { queryClient } from "@xtablo/shared";
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { App } from "./App";
import { queryClient } from "@xtablo/shared";
import "stream-chat-react/dist/css/v2/index.css";
import "@xtablo/ui/styles/globals.css";

View file

@ -1,17 +1,17 @@
import { useQueryClient } from "@tanstack/react-query";
import { CustomModal } from "@ui/components/CustomModal";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
import { useCreateTabloWithOwner } from "@xtablo/shared";
import { useSession } from "@xtablo/shared/contexts/SessionContext";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth";
import { invalidatePublicSlots, TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public";
import { EventInsertInTablo } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import { FieldError } from "@xtablo/ui/components/field";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import { Strong, Text } from "@xtablo/ui/components/typography";
import { useSession } from "@xtablo/shared/contexts/SessionContext";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { useSignUpWithoutPassword } from "@xtablo/shared/hooks/auth";
import { invalidatePublicSlots, TimeSlot, usePublicSlots } from "@xtablo/shared/hooks/public";
import { useCreateTabloWithOwner } from "@xtablo/shared";
import { useMaybeUser } from "../providers/UserStoreProvider";
import { EventInsertInTablo } from "@xtablo/shared/types/events.types";
import {
CalendarIcon,
ChevronLeftIcon,
@ -27,7 +27,7 @@ import { useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { api } from "../lib/api";
import { supabase } from "../lib/supabase";
import { useQueryClient } from "@tanstack/react-query";
import { useMaybeUser } from "../providers/UserStoreProvider";
export function PublicBookingPage() {
const { user_info, event_type_standard_name } = useParams<{
@ -47,14 +47,12 @@ export function PublicBookingPage() {
event_type_standard_name || ""
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api,
(data) => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
invalidatePublicSlots();
navigate(`/chat/${data.id}`, { replace: true });
navigate(0);
}
);
const { mutateAsync: createTabloWithOwner } = useCreateTabloWithOwner(api, (data) => {
queryClient.invalidateQueries({ queryKey: ["tablos"] });
invalidatePublicSlots();
navigate(`/chat/${data.id}`, { replace: true });
navigate(0);
});
const userProfile = publicSlots?.user;
const eventType = publicSlots?.eventType;

View file

@ -1,7 +1,9 @@
import { AvailabilityCard } from "@ui/components/AvailabilityCard";
import { AvailabilityVisualization } from "@ui/components/AvailabilityVisualization";
import { toast } from "@xtablo/shared";
import { Button } from "@xtablo/ui/components/button";
import { Card } from "@xtablo/ui/components/card";
import { Card, CardContent } from "@xtablo/ui/components/card";
import { Checkbox } from "@xtablo/ui/components/checkbox";
import {
Dialog,
DialogContent,
@ -17,21 +19,18 @@ import {
EmptyHeader,
EmptyTitle,
} from "@xtablo/ui/components/empty";
import { Label } from "@xtablo/ui/components/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@xtablo/ui/components/tabs";
import { Strong, Text, TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography";
import { Plus as PlusIcon, SaveIcon } from "lucide-react";
import { useState } from "react";
import { ExceptionModal } from "../components/ExceptionModal";
import {
DEFAULT_AVAILABILITIES,
Exception,
useAvailabilities,
WeeklyAvailability,
} from "../hooks/availabilities";
import { toast } from "@xtablo/shared";
import { Checkbox } from "@xtablo/ui/components/checkbox";
import { Plus as PlusIcon, SaveIcon } from "lucide-react";
import { useState } from "react";
import { ExceptionModal } from "../components/ExceptionModal";
import { CardContent } from "@xtablo/ui/components/card";
import { Label } from "@xtablo/ui/components/label";
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
const DAYS_OF_WEEK_DISPLAY = [

View file

@ -1,5 +1,7 @@
import { EventDetailsModal } from "@ui/components/EventDetailsModal";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
import { getTextColorFromTabloColor } from "@xtablo/shared";
import { EventAndTablo } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import { ButtonGroup } from "@xtablo/ui/components/button-group";
import { Input } from "@xtablo/ui/components/input";
@ -11,14 +13,12 @@ import {
SelectValue,
} from "@xtablo/ui/components/select";
import { Strong, Text, TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography";
import { useEventsByTablo } from "../hooks/events";
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
import { EventAndTablo } from "@xtablo/shared/types/events.types";
import { getTextColorFromTabloColor } from "@xtablo/shared";
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, SearchIcon } from "lucide-react";
import { useEffect, useMemo, useState } from "react";
import { useNavigate, useParams } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { useEventsByTablo } from "../hooks/events";
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
type BookingStatus = "all" | "upcoming" | "past";

View file

@ -1,8 +1,5 @@
import { ChannelPreview } from "@ui/components/ChannelPreview";
import { CustomChannelHeader } from "@ui/components/CustomChannelHeader";
import { useChannelFromUrl } from "../hooks/channel";
import { useTablosList } from "../hooks/tablos";
import { useUser } from "../providers/UserStoreProvider";
import { useEffect, useState } from "react";
import {
Channel,
@ -12,6 +9,9 @@ import {
useChatContext,
Window,
} from "stream-chat-react";
import { useChannelFromUrl } from "../hooks/channel";
import { useTablosList } from "../hooks/tablos";
import { useUser } from "../providers/UserStoreProvider";
export function ChatPage() {
const user = useUser();

View file

@ -1,11 +1,11 @@
import { EventTypeModal } from "@ui/components/EventTypeModal";
import { toast } from "@xtablo/shared";
import { Button } from "@xtablo/ui/components/button";
import { Text, TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography";
import { EventTypeConfig, useEventTypes } from "../hooks/event-types";
import { toast } from "@xtablo/shared";
import { PlusIcon } from "lucide-react";
import { useState } from "react";
import { EventTypeCard } from "../components/EventTypeCard";
import { EventTypeConfig, useEventTypes } from "../hooks/event-types";
export function EventTypesPage() {
const [isModalOpen, setIsModalOpen] = useState(false);

View file

@ -10,11 +10,11 @@ import {
} from "@xtablo/ui/components/select";
import { Textarea } from "@xtablo/ui/components/textarea";
import { Text, TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography";
import { useCreateFeedback } from "../hooks/feedback";
import { ArrowLeftIcon, SendIcon } from "lucide-react";
import React, { useState } from "react";
import { Separator } from "react-aria-components";
import { useNavigate } from "react-router-dom";
import { useCreateFeedback } from "../hooks/feedback";
export interface FeedbackData {
fd_type: "bug" | "feature" | "improvement" | "other";

View file

@ -1,10 +1,16 @@
import { Button } from "@xtablo/ui/components/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@xtablo/ui/components/card";
import { useJoinTablo } from "../hooks/invite";
import { toast } from "@xtablo/shared";
import { useUser } from "../providers/UserStoreProvider";
import { Button } from "@xtablo/ui/components/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@xtablo/ui/components/card";
import { CheckCircle2Icon, XCircleIcon } from "lucide-react";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { useJoinTablo } from "../hooks/invite";
import { useUser } from "../providers/UserStoreProvider";
export const JoinPage = () => {
const { tablo_name } = useParams<{ tablo_name: string }>();

View file

@ -1,15 +1,15 @@
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
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 { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { useLoginEmail } from "../hooks/auth";
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { useRef, useState } from "react";
import { Link } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { useLoginEmail } from "../hooks/auth";
export function LoginPage() {
const redirectUrl = localStorage.getItem("redirectUrl");

View file

@ -1,4 +1,8 @@
import { ImportICSModal } from "@ui/components/ImportICSModal";
import { WebcalModal } from "@ui/components/WebcalModal";
import { downloadICSFile, generateICSFromEvents } from "@xtablo/shared";
import { EventAndTablo } from "@xtablo/shared/types/events.types";
import { Button } from "@xtablo/ui/components/button";
import {
Select,
SelectContent,
@ -7,15 +11,11 @@ import {
SelectValue,
} from "@xtablo/ui/components/select";
import { TypographyH3 } from "@xtablo/ui/components/typography";
import { Button } from "@xtablo/ui/components/button";
import { WebcalModal } from "@ui/components/WebcalModal";
import { useDeleteEvent, useEventsByTablo } from "../hooks/events";
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
import { EventAndTablo } from "@xtablo/shared/types/events.types";
import { downloadICSFile, generateICSFromEvents } from "@xtablo/shared";
import { Download, FolderInputIcon, PlusIcon, RefreshCcw } from "lucide-react";
import { useEffect, useState } from "react";
import { Outlet, useNavigate, useParams } from "react-router-dom";
import { useDeleteEvent, useEventsByTablo } from "../hooks/events";
import { useGetAllTabloAccess, useTablosList } from "../hooks/tablos";
type ViewType = "month" | "week" | "day";
@ -43,7 +43,12 @@ export const PlanningPage = () => {
// Check if an event can be deleted (e.g., based on permissions, event status, etc.)
const canDeleteEvent = (event: EventAndTablo) => {
if (tabloAccess?.find((access: { tablo_id: string; is_admin: boolean }) => access.tablo_id === event.tablo_id && access.is_admin)) {
if (
tabloAccess?.find(
(access: { tablo_id: string; is_admin: boolean }) =>
access.tablo_id === event.tablo_id && access.is_admin
)
) {
return true;
}
return false;

View file

@ -1,9 +1,14 @@
import { Button } from "@xtablo/ui/components/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@xtablo/ui/components/card";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import { Textarea } from "@xtablo/ui/components/textarea";
import { ImageCropDialog } from "@ui/components/ImageCropDialog";
import { toast } from "@xtablo/shared";
import { Avatar, AvatarFallback, AvatarImage } from "@xtablo/ui/components/avatar";
import { Button } from "@xtablo/ui/components/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@xtablo/ui/components/card";
import {
Dialog,
DialogContent,
@ -12,14 +17,15 @@ import {
DialogHeader,
DialogTitle,
} from "@xtablo/ui/components/dialog";
import { useUser } from "../providers/UserStoreProvider";
import { useState, useRef } from "react";
import { Input } from "@xtablo/ui/components/input";
import { Label } from "@xtablo/ui/components/label";
import { Textarea } from "@xtablo/ui/components/textarea";
import { TypographyH3, TypographyMuted, TypographySmall } from "@xtablo/ui/components/typography";
import { CameraIcon, Loader2Icon, Trash2Icon, UploadIcon } from "lucide-react";
import { useRef, useState } from "react";
import { useIntroduction } from "../hooks/intros";
import { useRemoveAvatar, useUpdateProfile, useUploadAvatar } from "../hooks/profile";
import { CameraIcon, Loader2Icon, Trash2Icon, UploadIcon } from "lucide-react";
import { toast } from "@xtablo/shared";
import { ImageCropDialog } from "@ui/components/ImageCropDialog";
import { useUser } from "../providers/UserStoreProvider";
export default function SettingsPage() {
const user = useUser();

View file

@ -1,15 +1,15 @@
import { AnimatedBackground } from "@ui/components/AnimatedBackground";
import { LoginWithGoogle } from "@ui/components/BrandButtons/LoginWithGoogle";
import { useTheme } from "@xtablo/shared/contexts/ThemeContext";
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 { useTheme } from "@xtablo/shared/contexts/ThemeContext";
import { useSignUp } from "../hooks/auth";
import { MonitorIcon, MoonIcon, SunIcon } from "lucide-react";
import { useRef, useState } from "react";
import { Link, useNavigate } from "react-router-dom";
import { twMerge } from "tailwind-merge";
import { useSignUp } from "../hooks/auth";
export function SignUpPage() {
const navigate = useNavigate();

View file

@ -3,6 +3,7 @@ import { DeleteTabloModal } from "@ui/components/DeleteTabloModal";
import { LoadingSpinner } from "@ui/components/LoadingSpinner";
import { TabloModal } from "@ui/components/TabloModal";
import { TabloTutorial } from "@ui/components/TabloTutorial";
import { TabloInsert, TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types";
import { Button } from "@xtablo/ui/components/button";
import {
Empty,
@ -20,8 +21,6 @@ import {
SelectValue,
} from "@xtablo/ui/components/select";
import { Text, TypographyH3, TypographyMuted } from "@xtablo/ui/components/typography";
import { useCreateTablo, useDeleteTablo, useTablosList, useUpdateTablo } from "../hooks/tablos";
import { TabloInsert, TabloUpdate, UserTablo } from "@xtablo/shared/types/tablos.types";
import {
CheckCircle2,
Clock,
@ -37,6 +36,7 @@ import {
} from "lucide-react";
import { useEffect, useState } from "react";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useCreateTablo, useDeleteTablo, useTablosList, useUpdateTablo } from "../hooks/tablos";
type FilterOption = {
id: "all" | "todo" | "in_progress" | "done";

View file

@ -1,8 +1,8 @@
import { datadogRum } from "@datadog/browser-rum";
import { routes } from "../lib/routes";
import { initRum } from "../lib/rum";
import { useEffect } from "react";
import { matchRoutes, RouteMatch, useLocation } from "react-router-dom";
import { routes } from "../lib/routes";
import { initRum } from "../lib/rum";
function computeViewName(routeMatches: RouteMatch[]) {
let viewName = "";

View file

@ -1,10 +1,10 @@
import { useQuery } from "@tanstack/react-query";
import { useSession } from "@xtablo/shared/contexts/SessionContext";
import { api } from "../lib/api";
import { Tables } from "@xtablo/shared/types/database.types";
import React from "react";
import { createStore, StoreApi, useStore } from "zustand";
import { LoadingSpinner } from "../components/LoadingSpinner";
import { api } from "../lib/api";
export type User = Tables<"profiles"> & {
streamToken: string | null;

View file

@ -3,9 +3,9 @@ import { RenderResult, render, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { SessionTestProvider } from "@xtablo/shared/contexts/SessionContext";
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
import { TestUserStoreProvider } from "../providers/UserStoreProvider";
import React from "react";
import { BrowserRouter } from "react-router-dom";
import { TestUserStoreProvider } from "../providers/UserStoreProvider";
const defaultUser = {
id: "123",

File diff suppressed because one or more lines are too long

View file

@ -26,14 +26,9 @@
"paths": {
"*": ["./*"],
"@ui/*": ["./src/*"],
"@xtablo/ui/*": ["../../packages/ui/src/*"],
// "@external/*": ["./src/external/*"],
// "@xtablo/ui/components": ["../../packages/ui-components/src"],
// "@xtablo/ui/components/*": ["../../packages/ui-components/src/*"],
// "@xtablo/shared": ["../../packages/shared/src"],
// "@xtablo/shared/*": ["../../packages/shared/src/*"]
"@xtablo/ui/*": ["../../packages/ui/src/*"]
}
},
"include": ["src", "worker"],
"exclude": ["node_modules", "dist"],
"exclude": ["node_modules", "dist"]
}

34
apps/main/turbo.json Normal file
View file

@ -0,0 +1,34 @@
{
"$schema": "https://turbo.build/schema.json",
"extends": ["//"],
"tasks": {
"build:staging": {
"inputs": [
"src/**",
"tsconfig.json",
"tsconfig.*.json",
"vite.config.ts",
"package.json",
".env.staging",
".env.*.staging"
],
"outputs": ["dist/**", "tsconfig.tsbuildinfo"],
"outputLogs": "new-only",
"env": ["NODE_ENV", "VITE_*"]
},
"build:prod": {
"inputs": [
"src/**",
"tsconfig.json",
"tsconfig.*.json",
"vite.config.ts",
"package.json",
".env.production",
".env.*.production"
],
"outputs": ["dist/**", "tsconfig.tsbuildinfo"],
"outputLogs": "new-only",
"env": ["NODE_ENV", "VITE_*"]
}
}
}

View file

@ -1,22 +1,16 @@
/// <reference types="vitest" />
import { cloudflare } from "@cloudflare/vite-plugin";
// import { cloudflare } from "@cloudflare/vite-plugin";
import tailwindcss from "@tailwindcss/vite";
import react from "@vitejs/plugin-react";
import { visualizer } from "rollup-plugin-visualizer";
import { defineConfig, type PluginOption } from "vite";
import tsconfigPaths from "vite-tsconfig-paths";
import { cloudflare } from "@cloudflare/vite-plugin";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
react(),
visualizer() as PluginOption,
tailwindcss(),
tsconfigPaths(),
cloudflare(),
],
plugins: [react(), visualizer() as PluginOption, tailwindcss(), tsconfigPaths(), cloudflare()],
server: {
cors: false,
},

View file

@ -1,5 +1,5 @@
// @ts-nocheck
/* biome-ignore-file */
// biome-ignore-file: Generated file from Wrangler
/* eslint-disable */
// Generated by Wrangler by running `wrangler types` (hash: a253eed42b727ba5d2d964ef20087016)
@ -33,7 +33,7 @@ and limitations under the License.
***************************************************************************** */
/* eslint-disable */
// noinspection JSUnusedGlobalSymbols
declare var onmessage: never;
declare let onmessage: never;
/**
* An abnormal event (called an exception) which occurs as a result of calling a method or accessing a property of a web API.
*
@ -91,7 +91,7 @@ declare abstract class WorkerGlobalScope extends EventTarget<WorkerGlobalScopeEv
}
/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console) */
interface Console {
"assert"(condition?: boolean, ...data: any[]): void;
assert(condition?: boolean, ...data: any[]): void;
/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/clear_static) */
clear(): void;
/* [MDN Reference](https://developer.mozilla.org/docs/Web/API/console/count_static) */
@ -1650,7 +1650,7 @@ declare abstract class Body {
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Response)
*/
declare var Response: {
declare let Response: {
prototype: Response;
new (body?: BodyInit | null, init?: ResponseInit): Response;
error(): Response;
@ -1698,7 +1698,7 @@ type RequestInfo<CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>> =
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request)
*/
declare var Request: {
declare let Request: {
prototype: Request;
new <CfHostMetadata = unknown, Cf = CfProperties<CfHostMetadata>>(
input: RequestInfo<CfProperties> | URL,
@ -2891,7 +2891,7 @@ type WebSocketEventMap = {
*
* [MDN Reference](https://developer.mozilla.org/docs/Web/API/WebSocket)
*/
declare var WebSocket: {
declare let WebSocket: {
prototype: WebSocket;
new (url: string, protocols?: string[] | string): WebSocket;
readonly READY_STATE_CONNECTING: number;
@ -8743,20 +8743,48 @@ declare namespace Rpc {
| Headers;
// Recursively rewrite all `Stubable` types with `Stub`s
// prettier-ignore
type Stubify<T> = T extends Stubable ? Stub<T> : T extends Map<infer K, infer V> ? Map<Stubify<K>, Stubify<V>> : T extends Set<infer V> ? Set<Stubify<V>> : T extends Array<infer V> ? Array<Stubify<V>> : T extends ReadonlyArray<infer V> ? ReadonlyArray<Stubify<V>> : T extends BaseType ? T : T extends {
[key: string | number]: any;
} ? {
[K in keyof T]: Stubify<T[K]>;
} : T;
type Stubify<T> = T extends Stubable
? Stub<T>
: T extends Map<infer K, infer V>
? Map<Stubify<K>, Stubify<V>>
: T extends Set<infer V>
? Set<Stubify<V>>
: T extends Array<infer V>
? Array<Stubify<V>>
: T extends ReadonlyArray<infer V>
? ReadonlyArray<Stubify<V>>
: T extends BaseType
? T
: T extends {
[key: string | number]: any;
}
? {
[K in keyof T]: Stubify<T[K]>;
}
: T;
// Recursively rewrite all `Stub<T>`s with the corresponding `T`s.
// Note we use `StubBase` instead of `Stub` here to avoid circular dependencies:
// `Stub` depends on `Provider`, which depends on `Unstubify`, which would depend on `Stub`.
// prettier-ignore
type Unstubify<T> = T extends StubBase<infer V> ? V : T extends Map<infer K, infer V> ? Map<Unstubify<K>, Unstubify<V>> : T extends Set<infer V> ? Set<Unstubify<V>> : T extends Array<infer V> ? Array<Unstubify<V>> : T extends ReadonlyArray<infer V> ? ReadonlyArray<Unstubify<V>> : T extends BaseType ? T : T extends {
[key: string | number]: unknown;
} ? {
[K in keyof T]: Unstubify<T[K]>;
} : T;
type Unstubify<T> = T extends StubBase<infer V>
? V
: T extends Map<infer K, infer V>
? Map<Unstubify<K>, Unstubify<V>>
: T extends Set<infer V>
? Set<Unstubify<V>>
: T extends Array<infer V>
? Array<Unstubify<V>>
: T extends ReadonlyArray<infer V>
? ReadonlyArray<Unstubify<V>>
: T extends BaseType
? T
: T extends {
[key: string | number]: unknown;
}
? {
[K in keyof T]: Unstubify<T[K]>;
}
: T;
type UnstubifyAll<A extends any[]> = {
[I in keyof A]: Unstubify<A[I]>;
};
@ -8772,7 +8800,11 @@ declare namespace Rpc {
// Technically, we use custom thenables here, but they quack like `Promise`s.
// Intersecting with `(Maybe)Provider` allows pipelining.
// prettier-ignore
type Result<R> = R extends Stubable ? Promise<Stub<R>> & Provider<R> : R extends Serializable<R> ? Promise<Stubify<R> & MaybeDisposable<R>> & MaybeProvider<R> : never;
type Result<R> = R extends Stubable
? Promise<Stub<R>> & Provider<R>
: R extends Serializable<R>
? Promise<Stubify<R> & MaybeDisposable<R>> & MaybeProvider<R>
: never;
// Type for method or property on an RPC interface.
// For methods, unwrap `Stub`s in parameters, and rewrite returns to be `Result`s.
// Unwrapping `Stub`s allows calling with `Stubable` arguments.

View file

@ -1,4 +1,4 @@
declare const _default: {
fetch(request: any): Response;
fetch(request: any): Response;
};
export default _default;

View file

@ -1,5 +1,5 @@
// @ts-nocheck
/* biome-ignore-file */
// biome-ignore-file: Worker entry point with dynamic imports
/* eslint-disable */
export default {

View file

@ -7,6 +7,8 @@
"scripts": {
"build": "turbo build",
"build:apps": "turbo build --filter='./apps/*'",
"build:staging": "turbo build:staging --filter=@xtablo/main",
"build:prod": "turbo build:prod --filter=@xtablo/main",
"dev": "turbo dev",
"dev:main": "turbo dev --filter=@xtablo/main",
"dev:external": "turbo dev --filter=@xtablo/external",

View file

@ -0,0 +1,40 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false },
"files": {
"ignoreUnknown": true
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto"
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "double",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"json": {
"parser": { "allowComments": true, "allowTrailingCommas": false },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"trailingCommas": "none"
}
}
}

View file

@ -46,4 +46,3 @@
"typescript": "^5.7.0"
}
}

View file

@ -1,4 +1,4 @@
import { Session, SupabaseClient, User } from "@supabase/supabase-js";
import type { Session, SupabaseClient, User } from "@supabase/supabase-js";
import { createContext, useContext, useEffect, useState } from "react";
const SessionContext = createContext<{
@ -15,7 +15,7 @@ export const useSession = () => {
return context;
};
type Props = { supabase: SupabaseClient, children: React.ReactNode };
type Props = { supabase: SupabaseClient; children: React.ReactNode };
export const SessionProvider = ({ supabase, children }: Props) => {
const [session, setSession] = useState<Session | null>(null);
@ -33,7 +33,7 @@ export const SessionProvider = ({ supabase, children }: Props) => {
return <SessionContext.Provider value={{ session }}>{children}</SessionContext.Provider>;
};
type TestProps = { children: React.ReactNode, testUser?: User };
type TestProps = { children: React.ReactNode; testUser?: User };
export const SessionTestProvider = ({ children, testUser }: TestProps) => {
const session = testUser

View file

@ -1,20 +1,16 @@
import { useState } from "react";
import type { Session, SupabaseClient, User as SupabaseUser } from "@supabase/supabase-js";
import { useMutation } from "@tanstack/react-query";
import { toast } from "../lib/toast";
import type { AxiosInstance } from "axios";
import { useState } from "react";
import { match } from "ts-pattern";
import { SupabaseClient, User as SupabaseUser } from "@supabase/supabase-js";
import { Session } from "@supabase/supabase-js";
import { AxiosInstance } from "axios";
import { toast } from "../lib/toast";
interface AuthResponse {
user: SupabaseUser | null;
session: Session | null;
}
export function useSignUpWithoutPassword(
supabase: SupabaseClient,
api: AxiosInstance
) {
export function useSignUpWithoutPassword(supabase: SupabaseClient, api: AxiosInstance) {
const [errors, setErrors] = useState<Record<string, string>>({});
const { signUpToStream } = useSignUpToStream(api);
const { mutateAsync, isPending } = useMutation<
@ -25,8 +21,7 @@ export function useSignUpWithoutPassword(
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);
Math.random().toString(36).slice(-8) + Math.random().toString(36).slice(-8);
const { data: response, error } = await supabase.auth.signUp({
email: data.email.trim(),
@ -65,7 +60,7 @@ export function useSignUpWithoutPassword(
match(error.code)
.with("user_already_exists", () => {
errMap["email"] = "Cette adresse email est déjà utilisée";
errMap.email = "Cette adresse email est déjà utilisée";
})
.otherwise(() => {
toast.add(

View file

@ -1,6 +1,6 @@
import { useQuery } from "@tanstack/react-query";
import type { AxiosInstance } from "axios";
import { queryClient } from "../lib/api";
import { AxiosInstance } from "axios";
type EventTypeConfig = {
name: string;
@ -23,11 +23,7 @@ export type TimeSlot = {
available: boolean;
};
export function usePublicSlots(
api: AxiosInstance,
shortUserId: string,
standardName: string
) {
export function usePublicSlots(api: AxiosInstance, shortUserId: string, standardName: string) {
return useQuery<{
user: { name: string };
eventType: EventTypeConfig;
@ -36,9 +32,7 @@ export function usePublicSlots(
}>({
queryKey: ["public-slots", shortUserId, standardName],
queryFn: async () => {
const res = await api.get(
`/api/public/slots/${shortUserId}/${standardName}`
);
const res = await api.get(`/api/public/slots/${shortUserId}/${standardName}`);
if (res.status !== 200) {
throw new Error("Failed to fetch public slots");
}

View file

@ -1,8 +1,8 @@
import { AxiosInstance } from "axios";
import { useMutation } from "@tanstack/react-query";
import { CreateTablo } from "../types/tablos.types";
import { EventInsertInTablo } from "../types/events.types";
import type { AxiosInstance } from "axios";
import { toast } from "../lib/toast";
import type { EventInsertInTablo } from "../types/events.types";
import type { CreateTablo } from "../types/tablos.types";
// Create tablo with owner
export const useCreateTabloWithOwner = (

View file

@ -1,25 +1,21 @@
// Export contexts
export * from "./contexts/SessionContext";
export * from "./contexts/ThemeContext";
// Export hooks
export * from "./hooks/auth";
export * from "./hooks/public";
export * from "./hooks/tablos";
export * from "./hooks/useClickOutside";
export * from "./lib/api";
// Export lib
export * from "./lib/cn";
export * from "./lib/supabase";
export * from "./lib/toast";
// Export types
export * from "./types/database.types";
export * from "./types/events.types";
export * from "./types/kanban.types";
export * from "./types/removeNull";
export * from "./types/tablos.types";
// Export hooks
export * from "./hooks/auth";
export * from "./hooks/public";
export * from "./hooks/tablos";
export * from "./hooks/useClickOutside";
// Export utils
export * from "./utils/helpers";
// Export lib
export * from "./lib/cn";
export * from "./lib/api";
export * from "./lib/toast";
export * from "./lib/supabase";

File diff suppressed because it is too large Load diff

View file

@ -1,5 +1,5 @@
import { Tables, TablesInsert, TablesUpdate } from "../types/database.types";
import { RemoveNullFromObject } from "../types/removeNull";
import type { Tables, TablesInsert, TablesUpdate } from "../types/database.types";
import type { RemoveNullFromObject } from "../types/removeNull";
export type Event = RemoveNullFromObject<Tables<"events">, "created_at" | "end_time">;
export type EventInsert = TablesInsert<"events">;

View file

@ -1,6 +1,6 @@
import { Database } from "../types/database.types";
import { EventInsertInTablo } from "../types/events.types";
import { RemoveNullFromObject } from "../types/removeNull";
import type { Database } from "../types/database.types";
import type { EventInsertInTablo } from "../types/events.types";
import type { RemoveNullFromObject } from "../types/removeNull";
export type UserTablo = RemoveNullFromObject<
Database["public"]["Views"]["user_tablos"]["Row"],

View file

@ -1,6 +1,6 @@
import { Database } from "../types/database.types";
import { EventAndTablo } from "../types/events.types";
import jsPDF from "jspdf";
import type { Database } from "../types/database.types";
import type { EventAndTablo } from "../types/events.types";
export const calculateTax = (amount: number, taxRate: number) => {
return (amount * taxRate) / 100;
@ -30,16 +30,8 @@ export const exportDevisToPdf = (devis: Devis) => {
doc.setFontSize(12);
doc.text(`Client: ${devis.client_email}`, 20, 40);
doc.text(
`Date: ${new Date(devis.date).toLocaleDateString("fr-FR")}`,
140,
40
);
doc.text(
`Date d'échéance: ${new Date(devis.due_date).toLocaleDateString("fr-FR")}`,
140,
48
);
doc.text(`Date: ${new Date(devis.date).toLocaleDateString("fr-FR")}`, 140, 40);
doc.text(`Date d'échéance: ${new Date(devis.due_date).toLocaleDateString("fr-FR")}`, 140, 48);
// --- Amounts ---
const startYAmounts = 70;
@ -143,10 +135,7 @@ export const generateICSFromEvents = (
return icsContent;
};
export const downloadICSFile = (
icsContent: string,
filename: string = "planning.ics"
) => {
export const downloadICSFile = (icsContent: string, filename: string = "planning.ics") => {
const blob = new Blob([icsContent], { type: "text/calendar;charset=utf-8" });
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
@ -215,11 +204,7 @@ export const parseICSFile = (icsContent: string): ParsedICSEvent[] => {
if (line === "BEGIN:VEVENT") {
currentEvent = {};
} else if (line === "END:VEVENT" && currentEvent) {
if (
currentEvent.title &&
currentEvent.start_date &&
currentEvent.start_time
) {
if (currentEvent.title && currentEvent.start_date && currentEvent.start_time) {
events.push(currentEvent as ParsedICSEvent);
}
currentEvent = null;

View file

@ -1,22 +1,21 @@
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleDetection": "force",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022",
"jsx": "react-jsx"
},
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"esModuleInterop": true,
"incremental": false,
"isolatedModules": true,
"lib": ["es2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"moduleDetection": "force",
"moduleResolution": "bundler",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"target": "ES2022",
"jsx": "react-jsx"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

3
packages/ui/.biomeignore Normal file
View file

@ -0,0 +1,3 @@
# Tailwind CSS v4 files (uses custom at-rules not recognized by Biome)
src/styles/globals.css

50
packages/ui/biome.json Normal file
View file

@ -0,0 +1,50 @@
{
"$schema": "https://biomejs.dev/schemas/2.2.5/schema.json",
"vcs": { "enabled": true, "clientKind": "git", "useIgnoreFile": false },
"files": {
"ignoreUnknown": true,
"experimentalScannerIgnores": ["**/globals.css"]
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"attributePosition": "auto"
},
"javascript": {
"formatter": {
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"trailingCommas": "es5",
"semicolons": "always",
"arrowParentheses": "always",
"bracketSameLine": false,
"quoteStyle": "double",
"attributePosition": "auto",
"bracketSpacing": true
}
},
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"suspicious": {
"noUnknownAtRules": "off"
}
}
},
"json": {
"parser": { "allowComments": true, "allowTrailingCommas": false },
"formatter": {
"enabled": true,
"indentStyle": "space",
"indentWidth": 2,
"lineEnding": "lf",
"lineWidth": 100,
"trailingCommas": "none"
}
}
}

View file

@ -56,4 +56,3 @@
"typescript": "^5.7.0"
}
}

View file

@ -2,7 +2,7 @@
import * as AvatarPrimitive from "@radix-ui/react-avatar";
import { cn } from "@xtablo/shared";
import * as React from "react";
import type * as React from "react";
function Avatar({ className, ...props }: React.ComponentProps<typeof AvatarPrimitive.Root>) {
return (

View file

@ -1,7 +1,7 @@
import { Slot } from "@radix-ui/react-slot";
import { cn } from "@xtablo/shared";
import { cva } from "class-variance-authority";
import * as React from "react";
import type * as React from "react";
const badgeVariants = cva(
"inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden",

View file

@ -1,8 +1,7 @@
import { Slot } from "@radix-ui/react-slot";
import { Separator } from "./separator";
import { cn } from "@xtablo/shared";
import { cva, type VariantProps } from "class-variance-authority";
import { Separator } from "./separator";
const buttonGroupVariants = cva(
"flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2",

View file

@ -1,10 +1,10 @@
"use client";
import { Button, buttonVariants } from "./button";
import { cn } from "@xtablo/shared";
import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
import * as React from "react";
import { DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { type DayButton, DayPicker, getDefaultClassNames } from "react-day-picker";
import { Button, buttonVariants } from "./button";
function Calendar({
className,

View file

@ -1,5 +1,5 @@
import { cn } from "@xtablo/shared";
import * as React from "react";
import type * as React from "react";
function Card({ className, ...props }: React.ComponentProps<"div">) {
return (

View file

@ -1,10 +1,9 @@
"use client";
import * as React from "react";
import * as CheckboxPrimitive from "@radix-ui/react-checkbox";
import { CheckIcon } from "lucide-react";
import { cn } from "@xtablo/shared";
import { CheckIcon } from "lucide-react";
import type * as React from "react";
function Checkbox({ className, ...props }: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
return (

View file

@ -1,8 +1,8 @@
import { cn } from "@xtablo/shared";
import { useCopyToClipboard } from "../hooks/use-clipboard";
import { Check, Copy } from "lucide-react";
import React from "react";
import { Button, ButtonProps } from "./button";
import { useCopyToClipboard } from "../hooks/use-clipboard";
import { Button, type ButtonProps } from "./button";
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./tooltip";
export type ClipboardProps = {

View file

@ -1,14 +1,13 @@
"use client";
import { CalendarDate } from "@internationalized/date";
import { Button } from "./button";
import { Calendar } from "./calendar";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { cn } from "@xtablo/shared";
import { format } from "date-fns";
import { Calendar as CalendarIcon } from "lucide-react";
import * as React from "react";
import { Button } from "./button";
import { Calendar } from "./calendar";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
interface DateFieldProps {
name?: string;

View file

@ -1,13 +1,13 @@
"use client";
import { CalendarDate } from "@internationalized/date";
import { Button } from "./button";
import { Label } from "./label";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import { cn } from "@xtablo/shared";
import { Calendar as CalendarIcon, ChevronDownIcon } from "lucide-react";
import { useState } from "react";
import { Button } from "./button";
import { Calendar } from "./calendar";
import { Label } from "./label";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
interface DatePickerProps {
value?: Date | CalendarDate;

View file

@ -1,9 +1,8 @@
import { Label } from "./label";
import { Separator } from "./separator";
import { cn } from "@xtablo/shared";
import { cva, type VariantProps } from "class-variance-authority";
import { useMemo } from "react";
import { Label } from "./label";
import { Separator } from "./separator";
function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) {
return (
@ -189,7 +188,7 @@ function FieldError({
return null;
}
if (errors?.length == 1) {
if (errors?.length === 1) {
return errors[0]?.message;
}

Some files were not shown because too many files have changed in this diff Show more