Some good progress

This commit is contained in:
Arthur Belleville 2025-06-23 11:34:30 +02:00
parent 51f8b89011
commit 59cd45c689
No known key found for this signature in database
24 changed files with 3295 additions and 137 deletions

33
api/.gitignore vendored Normal file
View file

@ -0,0 +1,33 @@
# prod
dist/
# dev
.yarn/
!.yarn/releases
.vscode/*
!.vscode/launch.json
!.vscode/*.code-snippets
.idea/workspace.xml
.idea/usage.statistics.xml
.idea/shelf
# deps
node_modules/
.wrangler
# env
.env
.env.production
.dev.vars
# logs
logs/
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
# misc
.DS_Store

21
api/README.md Normal file
View file

@ -0,0 +1,21 @@
```txt
npm install
npm run dev
```
```txt
npm run deploy
```
[For generating/synchronizing types based on your Worker configuration run](https://developers.cloudflare.com/workers/wrangler/commands/#types):
```txt
npm run cf-typegen
```
Pass the `CloudflareBindings` as generics when instantiation `Hono`:
```ts
// src/index.ts
const app = new Hono<{ Bindings: CloudflareBindings }>()
```

1248
api/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

22
api/package.json Normal file
View file

@ -0,0 +1,22 @@
{
"type": "module",
"name": "xtablo-api",
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
},
"dependencies": {
"@hono/node-server": "^1.14.4",
"@supabase/supabase-js": "^2.49.4",
"dotenv": "^16.5.0",
"hono": "^4.7.7",
"hono-sessions": "^0.7.2",
"stream-chat": "^9.8.0"
},
"devDependencies": {
"@types/node": "^20.11.17",
"tsx": "^4.7.1",
"typescript": "^5.8.3"
}
}

1385
api/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load diff

4
api/pnpm-workspace.yaml Normal file
View file

@ -0,0 +1,4 @@
onlyBuiltDependencies:
- esbuild
- sharp
- workerd

42
api/src/index.ts Normal file
View file

@ -0,0 +1,42 @@
import "dotenv/config";
import { Hono } from "hono";
import { serve } from "@hono/node-server";
import { logger } from "hono/logger";
import { mainRouter } from "./routers";
import { cors } from "hono/cors";
const app = new Hono();
app.use(logger());
app.use("*", async (c, next) => {
const corsMiddleware = cors({
origin: process.env.FRONTEND_URL || "http://localhost:5173",
allowHeaders: [
"Authorization",
"Content-Type",
"Access-Control-Allow-Origin",
"Access-Control-Allow-Credentials",
"Access-Control-Expose-Headers",
],
allowMethods: ["GET", "POST", "PATCH", "OPTIONS", "DELETE"],
exposeHeaders: ["set-cookie"],
credentials: true,
});
return corsMiddleware(c, next);
});
app.route("/api/v1", mainRouter);
serve(
{
fetch: app.fetch,
port: 8080,
},
(info) => {
console.log(`Server is running on http://localhost:${info.port}`);
}
);

37
api/src/middleware.ts Normal file
View file

@ -0,0 +1,37 @@
import { createClient, User } from "@supabase/supabase-js";
import { Context, Next } from "hono";
// Create authentication middleware
export const authMiddleware = async (c: Context, next: Next) => {
const supabase = c.get("supabase");
// Extract Bearer token from Authorization header
const authHeader = c.req.header("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return c.json({ error: "Missing or invalid authorization header" }, 401);
}
const token = authHeader.substring(7); // Remove "Bearer " prefix
const {
data: { user },
error,
} = await supabase.auth.getUser(token);
if (error || !user) {
return c.json({ error: "Invalid or expired token" }, 401);
}
const userTyped = user as User;
c.set("user", userTyped);
await next();
};
export const supabaseMiddleware = async (c: Context, next: Next) => {
const supabase = createClient(
process.env.SUPABASE_URL as string,
process.env.SUPABASE_ANON_KEY as string
);
c.set("supabase", supabase);
await next();
};

30
api/src/routers.ts Normal file
View file

@ -0,0 +1,30 @@
import { Hono } from "hono";
import { userRouter } from "./user";
import { supabaseMiddleware } from "./middleware";
export const mainRouter = new Hono<{
Bindings: {
SESSION_ENCRYPTION_KEY: string;
};
}>();
// const store = new CookieStore();
mainRouter.use(supabaseMiddleware);
// mainRouter.use("*", (c, next) =>
// sessionMiddleware({
// store,
// encryptionKey: c.env.SESSION_ENCRYPTION_KEY,
// expireAfterSeconds: 900,
// sessionCookieName: "xtablo_session",
// cookieOptions: {
// sameSite: "Lax",
// path: "/",
// httpOnly: true,
// secure: false,
// // secure: process.env.NODE_ENV === "production",
// },
// })(c, next)
// );
mainRouter.route("/users", userRouter);

5
api/src/types.ts Normal file
View file

@ -0,0 +1,5 @@
type User = {
user_id: string;
email: string;
username: string;
};

31
api/src/user.ts Normal file
View file

@ -0,0 +1,31 @@
import { Hono } from "hono";
import { authMiddleware } from "./middleware";
import { User } from "@supabase/supabase-js";
import { StreamChat } from "stream-chat";
export const userRouter = new Hono<{
Variables: {
user: User;
};
}>();
userRouter.use(authMiddleware);
userRouter.get("/get-stream-token", async (c) => {
const user = c.get("user");
const user_id = user.id;
const serverClient = new StreamChat(
process.env.STREAM_CHAT_API_KEY as string,
process.env.STREAM_CHAT_API_SECRET as string,
{
disableCache: true,
}
);
console.log({ user_id });
const token = serverClient.createToken(user_id);
return c.json({
token,
});
});

14
api/tsconfig.json Normal file
View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"skipLibCheck": true,
"lib": [
"ESNext"
],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx"
},
}

View file

@ -14,6 +14,7 @@
"test:coverage": "vitest run --coverage"
},
"devDependencies": {
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
"@eslint/js": "^9.22.0",
"@floating-ui/react": "^0.27.4",
"@internationalized/date": "^3.7.0",
@ -69,6 +70,7 @@
"stream-chat": "^9.6.1",
"stream-chat-react": "^13.1.0",
"ts-pattern": "^5.6.2",
"uuid": "^11.1.0"
"uuid": "^11.1.0",
"zustand": "^5.0.5"
}
}

View file

@ -56,7 +56,13 @@ importers:
uuid:
specifier: ^11.1.0
version: 11.1.0
zustand:
specifier: ^5.0.5
version: 5.0.5(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0))
devDependencies:
'@esbuild-plugins/node-globals-polyfill':
specifier: ^0.2.3
version: 0.2.3(esbuild@0.25.1)
'@eslint/js':
specifier: ^9.22.0
version: 9.22.0
@ -365,6 +371,11 @@ packages:
'@braintree/sanitize-url@6.0.4':
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
'@esbuild-plugins/node-globals-polyfill@0.2.3':
resolution: {integrity: sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw==}
peerDependencies:
esbuild: '*'
'@esbuild/aix-ppc64@0.25.1':
resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==}
engines: {node: '>=18'}
@ -4734,6 +4745,24 @@ packages:
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
engines: {node: '>=10'}
zustand@5.0.5:
resolution: {integrity: sha512-mILtRfKW9xM47hqxGIxCv12gXusoY/xTSHBYApXozR0HmQv299whhBeeAcRy+KrPPybzosvJBCOmVjq6x12fCg==}
engines: {node: '>=12.20.0'}
peerDependencies:
'@types/react': '>=18.0.0'
immer: '>=9.0.6'
react: '>=18.0.0'
use-sync-external-store: '>=1.2.0'
peerDependenciesMeta:
'@types/react':
optional: true
immer:
optional: true
react:
optional: true
use-sync-external-store:
optional: true
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@ -4952,6 +4981,10 @@ snapshots:
'@braintree/sanitize-url@6.0.4': {}
'@esbuild-plugins/node-globals-polyfill@0.2.3(esbuild@0.25.1)':
dependencies:
esbuild: 0.25.1
'@esbuild/aix-ppc64@0.25.1':
optional: true
@ -6597,7 +6630,7 @@ snapshots:
'@testing-library/dom@10.4.0':
dependencies:
'@babel/code-frame': 7.26.2
'@babel/runtime': 7.27.0
'@babel/runtime': 7.27.6
'@types/aria-query': 5.0.4
aria-query: 5.3.0
chalk: 4.1.2
@ -10701,4 +10734,10 @@ snapshots:
yocto-queue@0.1.0: {}
zustand@5.0.5(@types/react@19.0.10)(react@19.0.0)(use-sync-external-store@1.4.0(react@19.0.0)):
optionalDependencies:
'@types/react': 19.0.10
react: 19.0.0
use-sync-external-store: 1.4.0(react@19.0.0)
zwitch@2.0.4: {}

View file

@ -5,7 +5,6 @@ import { ThemeProvider } from "./contexts/ThemeContext";
import { twMerge } from "tailwind-merge";
import { ResetPasswordPage } from "./pages/reset-password";
import { LandingPage } from "./pages/landing";
import { ProtectedRoute } from "./components/ProtectedRoute";
import { PublicRoute } from "./components/PublicRoute";
import { TabloPage } from "./pages/tablo";
import { SessionProvider } from "./contexts/SessionContext";
@ -19,6 +18,8 @@ import { ChantiersPage } from "./pages/chantiers";
import { ChatPage } from "./pages/chat";
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
import ChatProvider from "./providers/ChatProvider";
import { UserStoreProvider } from "./providers/UserStoreProvider";
import { ProtectedRoute } from "./components/ProtectedRoute";
// Register all Community features
ModuleRegistry.registerModules([AllCommunityModule]);
@ -27,72 +28,76 @@ export const App = () => {
return (
<ThemeProvider>
<SessionProvider>
<Router>
<div className={twMerge("min-h-screen bg-white", "dark:bg-white")}>
<Routes>
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
<Route
index
element={
<Layout>
<TabloPage />
</Layout>
}
/>
<Route
path="devis"
element={
<Layout>
<DevisPage />
</Layout>
}
/>
<Route
path="factures"
element={
<Layout>
<FacturesPage />
</Layout>
}
/>
<Route
path="planning"
element={
<Layout>
<PlanningPage />
</Layout>
}
/>
<Route
path="chantiers"
element={
<Layout>
<ChantiersPage />
</Layout>
}
/>
<Route
path="chat"
element={
<Layout>
<ChatProvider>
<ChatPage />
</ChatProvider>
</Layout>
}
/>
</Route>
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
<Route path="landing" element={<LandingPage />} />
<Route element={<PublicRoute />}>
<Route path="login" element={<LoginPage />} />
<Route path="signup" element={<SignUpPage />} />
<Route path="reset-password" element={<ResetPasswordPage />} />
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
<style>
{`
<UserStoreProvider>
<Router>
<div className={twMerge("min-h-screen bg-white", "dark:bg-white")}>
<Routes>
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
<Route
index
element={
<Layout>
<TabloPage />
</Layout>
}
/>
<Route
path="devis"
element={
<Layout>
<DevisPage />
</Layout>
}
/>
<Route
path="factures"
element={
<Layout>
<FacturesPage />
</Layout>
}
/>
<Route
path="planning"
element={
<Layout>
<PlanningPage />
</Layout>
}
/>
<Route
path="chantiers"
element={
<Layout>
<ChantiersPage />
</Layout>
}
/>
<Route
path="chat"
element={
<Layout>
<ChatProvider>
<ChatPage />
</ChatProvider>
</Layout>
}
/>
</Route>
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
<Route path="landing" element={<LandingPage />} />
<Route element={<PublicRoute />}>
<Route path="login" element={<LoginPage />} />
<Route path="signup" element={<SignUpPage />} />
<Route
path="reset-password"
element={<ResetPasswordPage />}
/>
</Route>
<Route path="*" element={<NotFoundPage />} />
</Routes>
<style>
{`
@keyframes slide {
0% { transform: translateX(-100vw); }
100% { transform: translateX(100vw); }
@ -101,9 +106,10 @@ export const App = () => {
animation: slide 24s linear infinite;
}
`}
</style>
</div>
</Router>
</style>
</div>
</Router>
</UserStoreProvider>
</SessionProvider>
</ThemeProvider>
);

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from "react";
import { useSession } from "../contexts/SessionContext";
import { Navigate, Outlet } from "react-router-dom";
import { match } from "ts-pattern";
import { useSession } from "@ui/contexts/SessionContext";
interface ProtectedRouteProps {
fallback?: string;
@ -9,6 +9,7 @@ interface ProtectedRouteProps {
export const ProtectedRoute = ({ fallback }: ProtectedRouteProps) => {
const { session } = useSession();
console.log({ session });
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
@ -23,8 +24,10 @@ export const ProtectedRoute = ({ fallback }: ProtectedRouteProps) => {
| "should-land-user"
| "should-redirect"
| "should-pass" = "loading";
const isFirstTimeUser =
localStorage.getItem("xtablo-has-seen-landing-page") === null;
if (isLoading) {
status = "loading";
} else if (!session?.user && isFirstTimeUser) {

View file

@ -6,17 +6,9 @@ import { toast } from "../ui-library/toast/toast-queue";
export const SignOutButton = () => {
const { mutate: logout, isPending, error } = useLogout();
const [isConfirming, setIsConfirming] = useState(false);
const [showSuccess, setShowSuccess] = useState(false);
const handleLogout = () => {
if (!isConfirming) {
setIsConfirming(true);
// Auto-reset confirmation after 3 seconds
setTimeout(() => setIsConfirming(false), 3000);
return;
}
logout(undefined, {
onSuccess: () => {
setShowSuccess(true);
@ -27,7 +19,6 @@ export const SignOutButton = () => {
position: "top-right",
});
setTimeout(() => {
setIsConfirming(false);
setShowSuccess(false);
}, 1000);
},
@ -39,19 +30,12 @@ export const SignOutButton = () => {
type: "error",
position: "top-right",
});
setIsConfirming(false);
},
});
};
const handleKeyDown = (event: React.KeyboardEvent) => {
if (event.key === "Escape" && isConfirming) {
setIsConfirming(false);
}
};
return (
<div className="relative inline-block group" onKeyDown={handleKeyDown}>
<div className="relative inline-block group">
<Button
onPress={handleLogout}
variant="outline"
@ -73,8 +57,6 @@ export const SignOutButton = () => {
${
showSuccess
? "bg-success/20 border-success/30 text-success hover:bg-success/25"
: isConfirming
? "bg-destructive/15 border-destructive/40 text-destructive hover:bg-destructive/20 shadow-lg shadow-destructive/10"
: "bg-destructive/5 border-destructive/20 text-destructive/80 hover:bg-destructive/10 hover:border-destructive/30 hover:text-destructive hover:shadow-md hover:shadow-destructive/5"
}
${
@ -82,23 +64,12 @@ export const SignOutButton = () => {
? "opacity-80 cursor-not-allowed"
: "hover:scale-[1.02] active:scale-[0.98]"
}
${isConfirming ? "ring-2 ring-destructive/20" : ""}
group-hover:shadow-lg
`}
isDisabled={isPending}
aria-label={
showSuccess
? "Déconnexion réussie"
: isConfirming
? "Confirmer la déconnexion - Cliquez à nouveau pour confirmer"
: "Se déconnecter"
}
aria-label={showSuccess ? "Déconnexion réussie" : "Se déconnecter"}
tooltip={
showSuccess
? "Déconnexion réussie"
: isConfirming
? "Cliquez à nouveau pour confirmer la déconnexion"
: "Se déconnecter de votre compte"
showSuccess ? "Déconnexion réussie" : "Se déconnecter de votre compte"
}
>
<div className="flex items-center justify-center gap-2.5 relative z-10">
@ -127,8 +98,6 @@ export const SignOutButton = () => {
${
isPending
? "animate-spin text-destructive/60"
: isConfirming
? "animate-pulse text-destructive scale-110"
: "text-destructive/70 group-hover:text-destructive group-hover:translate-x-[-1px] group-hover:scale-105"
}
`}
@ -147,8 +116,6 @@ export const SignOutButton = () => {
${
showSuccess
? "text-success"
: isConfirming
? "text-destructive font-semibold"
: "text-destructive/80 group-hover:text-destructive"
}
`}
@ -157,8 +124,6 @@ export const SignOutButton = () => {
? "Déconnecté"
: isPending
? "Déconnexion..."
: isConfirming
? "Confirmer ?"
: "Déconnexion"}
</span>
</div>
@ -168,13 +133,6 @@ export const SignOutButton = () => {
<div className="absolute inset-0 bg-gradient-to-r from-destructive/5 via-destructive/10 to-destructive/5 animate-pulse rounded-lg" />
)}
{isConfirming && !isPending && !showSuccess && (
<>
<div className="absolute inset-0 bg-destructive/10 rounded-lg animate-pulse" />
<div className="absolute inset-0 bg-gradient-to-r from-transparent via-destructive/5 to-transparent animate-ping rounded-lg" />
</>
)}
{showSuccess && (
<div className="absolute inset-0 bg-gradient-to-r from-success/10 via-success/15 to-success/10 animate-in fade-in duration-300 rounded-lg" />
)}
@ -185,16 +143,6 @@ export const SignOutButton = () => {
</div>
</Button>
{/* Confirmation indicator bar */}
{isConfirming && !showSuccess && (
<div className="absolute -bottom-0.5 left-0 right-0 flex justify-center">
<div
className="h-0.5 bg-destructive/40 rounded-full animate-pulse shadow-sm shadow-destructive/20"
style={{ width: "80%" }}
/>
</div>
)}
{/* Success indicator */}
{showSuccess && (
<div className="absolute -bottom-0.5 left-0 right-0 flex justify-center">

View file

@ -1,6 +1,6 @@
import { createContext, useContext, useEffect, useState } from "react";
import { Session, User } from "@supabase/supabase-js";
import { supabase } from "../hooks/auth";
import { supabase } from "@ui/hooks/auth";
const SessionContext = createContext<{
session: Session | null;

View file

@ -4,7 +4,7 @@ import { IS_DEV } from "@ui/config";
// Create axios instance with default config
export const api = axios.create({
baseURL: IS_DEV ? "http://127.0.0.1:8000" : "https://api.xtablo.com",
baseURL: IS_DEV ? "http://127.0.0.1:8080" : "https://api.xtablo.com",
headers: {
"Content-Type": "application/json",
},

View file

@ -1,4 +1,5 @@
import { SignOutButton } from "@ui/components/SignOutButton";
import { useUser } from "@ui/providers/UserStoreProvider";
import { useState } from "react";
interface Tablo {
@ -15,6 +16,8 @@ interface Folder {
}
export const TabloPage = () => {
const user = useUser();
console.log({ user });
const [hoveredTablo, setHoveredTablo] = useState<number | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [newTabloName, setNewTabloName] = useState("");
@ -535,7 +538,9 @@ export const TabloPage = () => {
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Vos tablos
</h1>
<SignOutButton />
<div className="flex items-center gap-3">
<SignOutButton />
</div>
</div>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
<div className="container mx-auto px-4 py-8">

View file

@ -1,4 +1,5 @@
import { Chat, useCreateChatClient } from "stream-chat-react";
import { useUser } from "./UserStoreProvider";
export default function ChatProvider({
children,
@ -6,14 +7,14 @@ export default function ChatProvider({
children: React.ReactNode;
}) {
const apiKey = import.meta.env.VITE_STREAM_CHAT_API_KEY as string;
const user = useUser();
const client = useCreateChatClient({
apiKey,
options: { timeout: 5000 },
tokenOrProvider: "artslidd",
tokenOrProvider: user.streamToken,
userData: {
id: "artslidd",
name: "Arthur",
id: user.id,
name: user.full_name || "",
},
});

View file

@ -0,0 +1,64 @@
import { createStore, StoreApi, useStore } from "zustand";
import React from "react";
import { supabase } from "@ui/hooks/auth";
import { useQuery } from "@tanstack/react-query";
import { Tables } from "@ui/types/database.types";
import { useSession } from "@ui/contexts/SessionContext";
import { api } from "@ui/lib/api";
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 { data, isPending } = useQuery<User | null>({
queryKey: ["user"],
queryFn: async () => {
const { data, error } = await supabase.from("profiles").select("*");
if (error) throw error;
const {
data: { token },
} = await api.get("/api/v1/users/get-stream-token", {
headers: {
Authorization: `Bearer ${session?.access_token}`,
},
});
console.log({ token, data });
return {
...data[0],
streamToken: token,
};
},
});
if (isPending) {
return <div>Loading...</div>;
}
if (!data) {
return children;
}
const store = createStore<User>()(() => data);
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);
};

View file

@ -0,0 +1,220 @@
export type Json =
| string
| number
| boolean
| null
| { [key: string]: Json | undefined }
| Json[]
export type Database = {
public: {
Tables: {
devis: {
Row: {
client_email: string
created_at: string
date: string
due_date: string
id: string
items: Json
notes: string | null
number: string
status: Database["public"]["Enums"]["devis_status"]
subtotal: number
tax: number
terms: string | null
total: number
updated_at: string
user_id: string
}
Insert: {
client_email: string
created_at?: string
date: string
due_date: string
id?: string
items?: Json
notes?: string | null
number: string
status?: Database["public"]["Enums"]["devis_status"]
subtotal: number
tax: number
terms?: string | null
total: number
updated_at?: string
user_id: string
}
Update: {
client_email?: string
created_at?: string
date?: string
due_date?: string
id?: string
items?: Json
notes?: string | null
number?: string
status?: Database["public"]["Enums"]["devis_status"]
subtotal?: number
tax?: number
terms?: string | null
total?: number
updated_at?: string
user_id?: string
}
Relationships: []
}
profiles: {
Row: {
avatar_url: string | null
email: string | null
full_name: string | null
id: string
updated_at: string | null
website: string | null
}
Insert: {
avatar_url?: string | null
email?: string | null
full_name?: string | null
id: string
updated_at?: string | null
website?: string | null
}
Update: {
avatar_url?: string | null
email?: string | null
full_name?: string | null
id?: string
updated_at?: string | null
website?: string | null
}
Relationships: []
}
}
Views: {
[_ in never]: never
}
Functions: {
[_ in never]: never
}
Enums: {
devis_status: "draft" | "sent" | "accepted" | "rejected" | "expired"
}
CompositeTypes: {
[_ in never]: never
}
}
}
type DefaultSchema = Database[Extract<keyof Database, "public">]
export type Tables<
DefaultSchemaTableNameOrOptions extends
| keyof (DefaultSchema["Tables"] & DefaultSchema["Views"])
| { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
}
? keyof (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])
: never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? (Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"] &
Database[DefaultSchemaTableNameOrOptions["schema"]]["Views"])[TableName] extends {
Row: infer R
}
? R
: never
: DefaultSchemaTableNameOrOptions extends keyof (DefaultSchema["Tables"] &
DefaultSchema["Views"])
? (DefaultSchema["Tables"] &
DefaultSchema["Views"])[DefaultSchemaTableNameOrOptions] extends {
Row: infer R
}
? R
: never
: never
export type TablesInsert<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
}
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Insert: infer I
}
? I
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Insert: infer I
}
? I
: never
: never
export type TablesUpdate<
DefaultSchemaTableNameOrOptions extends
| keyof DefaultSchema["Tables"]
| { schema: keyof Database },
TableName extends DefaultSchemaTableNameOrOptions extends {
schema: keyof Database
}
? keyof Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"]
: never = never,
> = DefaultSchemaTableNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaTableNameOrOptions["schema"]]["Tables"][TableName] extends {
Update: infer U
}
? U
: never
: DefaultSchemaTableNameOrOptions extends keyof DefaultSchema["Tables"]
? DefaultSchema["Tables"][DefaultSchemaTableNameOrOptions] extends {
Update: infer U
}
? U
: never
: never
export type Enums<
DefaultSchemaEnumNameOrOptions extends
| keyof DefaultSchema["Enums"]
| { schema: keyof Database },
EnumName extends DefaultSchemaEnumNameOrOptions extends {
schema: keyof Database
}
? keyof Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"]
: never = never,
> = DefaultSchemaEnumNameOrOptions extends { schema: keyof Database }
? Database[DefaultSchemaEnumNameOrOptions["schema"]]["Enums"][EnumName]
: DefaultSchemaEnumNameOrOptions extends keyof DefaultSchema["Enums"]
? DefaultSchema["Enums"][DefaultSchemaEnumNameOrOptions]
: never
export type CompositeTypes<
PublicCompositeTypeNameOrOptions extends
| keyof DefaultSchema["CompositeTypes"]
| { schema: keyof Database },
CompositeTypeName extends PublicCompositeTypeNameOrOptions extends {
schema: keyof Database
}
? keyof Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"]
: never = never,
> = PublicCompositeTypeNameOrOptions extends { schema: keyof Database }
? Database[PublicCompositeTypeNameOrOptions["schema"]]["CompositeTypes"][CompositeTypeName]
: PublicCompositeTypeNameOrOptions extends keyof DefaultSchema["CompositeTypes"]
? DefaultSchema["CompositeTypes"][PublicCompositeTypeNameOrOptions]
: never
export const Constants = {
public: {
Enums: {
devis_status: ["draft", "sent", "accepted", "rejected", "expired"],
},
},
} as const

View file

@ -1,7 +1,5 @@
@import "tailwindcss";
@import "~stream-chat-react/dist/css/v2/index.css";
@plugin 'tailwindcss-animate';
@plugin '@tailwindcss/container-queries';