Some good progress
This commit is contained in:
parent
51f8b89011
commit
59cd45c689
24 changed files with 3295 additions and 137 deletions
33
api/.gitignore
vendored
Normal file
33
api/.gitignore
vendored
Normal 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
21
api/README.md
Normal 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
1248
api/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
22
api/package.json
Normal file
22
api/package.json
Normal 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
1385
api/pnpm-lock.yaml
Normal file
File diff suppressed because it is too large
Load diff
4
api/pnpm-workspace.yaml
Normal file
4
api/pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
onlyBuiltDependencies:
|
||||||
|
- esbuild
|
||||||
|
- sharp
|
||||||
|
- workerd
|
||||||
42
api/src/index.ts
Normal file
42
api/src/index.ts
Normal 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
37
api/src/middleware.ts
Normal 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
30
api/src/routers.ts
Normal 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
5
api/src/types.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
type User = {
|
||||||
|
user_id: string;
|
||||||
|
email: string;
|
||||||
|
username: string;
|
||||||
|
};
|
||||||
31
api/src/user.ts
Normal file
31
api/src/user.ts
Normal 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
14
api/tsconfig.json
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ESNext",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "Bundler",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"lib": [
|
||||||
|
"ESNext"
|
||||||
|
],
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"jsxImportSource": "hono/jsx"
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@
|
||||||
"test:coverage": "vitest run --coverage"
|
"test:coverage": "vitest run --coverage"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@esbuild-plugins/node-globals-polyfill": "^0.2.3",
|
||||||
"@eslint/js": "^9.22.0",
|
"@eslint/js": "^9.22.0",
|
||||||
"@floating-ui/react": "^0.27.4",
|
"@floating-ui/react": "^0.27.4",
|
||||||
"@internationalized/date": "^3.7.0",
|
"@internationalized/date": "^3.7.0",
|
||||||
|
|
@ -69,6 +70,7 @@
|
||||||
"stream-chat": "^9.6.1",
|
"stream-chat": "^9.6.1",
|
||||||
"stream-chat-react": "^13.1.0",
|
"stream-chat-react": "^13.1.0",
|
||||||
"ts-pattern": "^5.6.2",
|
"ts-pattern": "^5.6.2",
|
||||||
"uuid": "^11.1.0"
|
"uuid": "^11.1.0",
|
||||||
|
"zustand": "^5.0.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -56,7 +56,13 @@ importers:
|
||||||
uuid:
|
uuid:
|
||||||
specifier: ^11.1.0
|
specifier: ^11.1.0
|
||||||
version: 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:
|
devDependencies:
|
||||||
|
'@esbuild-plugins/node-globals-polyfill':
|
||||||
|
specifier: ^0.2.3
|
||||||
|
version: 0.2.3(esbuild@0.25.1)
|
||||||
'@eslint/js':
|
'@eslint/js':
|
||||||
specifier: ^9.22.0
|
specifier: ^9.22.0
|
||||||
version: 9.22.0
|
version: 9.22.0
|
||||||
|
|
@ -365,6 +371,11 @@ packages:
|
||||||
'@braintree/sanitize-url@6.0.4':
|
'@braintree/sanitize-url@6.0.4':
|
||||||
resolution: {integrity: sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==}
|
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':
|
'@esbuild/aix-ppc64@0.25.1':
|
||||||
resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==}
|
resolution: {integrity: sha512-kfYGy8IdzTGy+z0vFGvExZtxkFlA4zAxgKEahG9KE1ScBjpQnFsNOX8KTU5ojNru5ed5CVoJYXFtoxaq5nFbjQ==}
|
||||||
engines: {node: '>=18'}
|
engines: {node: '>=18'}
|
||||||
|
|
@ -4734,6 +4745,24 @@ packages:
|
||||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||||
engines: {node: '>=10'}
|
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:
|
zwitch@2.0.4:
|
||||||
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
|
||||||
|
|
||||||
|
|
@ -4952,6 +4981,10 @@ snapshots:
|
||||||
|
|
||||||
'@braintree/sanitize-url@6.0.4': {}
|
'@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':
|
'@esbuild/aix-ppc64@0.25.1':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
|
|
@ -6597,7 +6630,7 @@ snapshots:
|
||||||
'@testing-library/dom@10.4.0':
|
'@testing-library/dom@10.4.0':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@babel/code-frame': 7.26.2
|
'@babel/code-frame': 7.26.2
|
||||||
'@babel/runtime': 7.27.0
|
'@babel/runtime': 7.27.6
|
||||||
'@types/aria-query': 5.0.4
|
'@types/aria-query': 5.0.4
|
||||||
aria-query: 5.3.0
|
aria-query: 5.3.0
|
||||||
chalk: 4.1.2
|
chalk: 4.1.2
|
||||||
|
|
@ -10701,4 +10734,10 @@ snapshots:
|
||||||
|
|
||||||
yocto-queue@0.1.0: {}
|
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: {}
|
zwitch@2.0.4: {}
|
||||||
|
|
|
||||||
146
ui/src/App.tsx
146
ui/src/App.tsx
|
|
@ -5,7 +5,6 @@ import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import { twMerge } from "tailwind-merge";
|
import { twMerge } from "tailwind-merge";
|
||||||
import { ResetPasswordPage } from "./pages/reset-password";
|
import { ResetPasswordPage } from "./pages/reset-password";
|
||||||
import { LandingPage } from "./pages/landing";
|
import { LandingPage } from "./pages/landing";
|
||||||
import { ProtectedRoute } from "./components/ProtectedRoute";
|
|
||||||
import { PublicRoute } from "./components/PublicRoute";
|
import { PublicRoute } from "./components/PublicRoute";
|
||||||
import { TabloPage } from "./pages/tablo";
|
import { TabloPage } from "./pages/tablo";
|
||||||
import { SessionProvider } from "./contexts/SessionContext";
|
import { SessionProvider } from "./contexts/SessionContext";
|
||||||
|
|
@ -19,6 +18,8 @@ import { ChantiersPage } from "./pages/chantiers";
|
||||||
import { ChatPage } from "./pages/chat";
|
import { ChatPage } from "./pages/chat";
|
||||||
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
import { AllCommunityModule, ModuleRegistry } from "ag-grid-community";
|
||||||
import ChatProvider from "./providers/ChatProvider";
|
import ChatProvider from "./providers/ChatProvider";
|
||||||
|
import { UserStoreProvider } from "./providers/UserStoreProvider";
|
||||||
|
import { ProtectedRoute } from "./components/ProtectedRoute";
|
||||||
|
|
||||||
// Register all Community features
|
// Register all Community features
|
||||||
ModuleRegistry.registerModules([AllCommunityModule]);
|
ModuleRegistry.registerModules([AllCommunityModule]);
|
||||||
|
|
@ -27,72 +28,76 @@ export const App = () => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<Router>
|
<UserStoreProvider>
|
||||||
<div className={twMerge("min-h-screen bg-white", "dark:bg-white")}>
|
<Router>
|
||||||
<Routes>
|
<div className={twMerge("min-h-screen bg-white", "dark:bg-white")}>
|
||||||
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
|
<Routes>
|
||||||
<Route
|
<Route path="/" element={<ProtectedRoute fallback="/login" />}>
|
||||||
index
|
<Route
|
||||||
element={
|
index
|
||||||
<Layout>
|
element={
|
||||||
<TabloPage />
|
<Layout>
|
||||||
</Layout>
|
<TabloPage />
|
||||||
}
|
</Layout>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="devis"
|
<Route
|
||||||
element={
|
path="devis"
|
||||||
<Layout>
|
element={
|
||||||
<DevisPage />
|
<Layout>
|
||||||
</Layout>
|
<DevisPage />
|
||||||
}
|
</Layout>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="factures"
|
<Route
|
||||||
element={
|
path="factures"
|
||||||
<Layout>
|
element={
|
||||||
<FacturesPage />
|
<Layout>
|
||||||
</Layout>
|
<FacturesPage />
|
||||||
}
|
</Layout>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="planning"
|
<Route
|
||||||
element={
|
path="planning"
|
||||||
<Layout>
|
element={
|
||||||
<PlanningPage />
|
<Layout>
|
||||||
</Layout>
|
<PlanningPage />
|
||||||
}
|
</Layout>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="chantiers"
|
<Route
|
||||||
element={
|
path="chantiers"
|
||||||
<Layout>
|
element={
|
||||||
<ChantiersPage />
|
<Layout>
|
||||||
</Layout>
|
<ChantiersPage />
|
||||||
}
|
</Layout>
|
||||||
/>
|
}
|
||||||
<Route
|
/>
|
||||||
path="chat"
|
<Route
|
||||||
element={
|
path="chat"
|
||||||
<Layout>
|
element={
|
||||||
<ChatProvider>
|
<Layout>
|
||||||
<ChatPage />
|
<ChatProvider>
|
||||||
</ChatProvider>
|
<ChatPage />
|
||||||
</Layout>
|
</ChatProvider>
|
||||||
}
|
</Layout>
|
||||||
/>
|
}
|
||||||
</Route>
|
/>
|
||||||
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
|
</Route>
|
||||||
<Route path="landing" element={<LandingPage />} />
|
<Route path="login-with-oauth" element={<OAuthSigninPage />} />
|
||||||
<Route element={<PublicRoute />}>
|
<Route path="landing" element={<LandingPage />} />
|
||||||
<Route path="login" element={<LoginPage />} />
|
<Route element={<PublicRoute />}>
|
||||||
<Route path="signup" element={<SignUpPage />} />
|
<Route path="login" element={<LoginPage />} />
|
||||||
<Route path="reset-password" element={<ResetPasswordPage />} />
|
<Route path="signup" element={<SignUpPage />} />
|
||||||
</Route>
|
<Route
|
||||||
<Route path="*" element={<NotFoundPage />} />
|
path="reset-password"
|
||||||
</Routes>
|
element={<ResetPasswordPage />}
|
||||||
<style>
|
/>
|
||||||
{`
|
</Route>
|
||||||
|
<Route path="*" element={<NotFoundPage />} />
|
||||||
|
</Routes>
|
||||||
|
<style>
|
||||||
|
{`
|
||||||
@keyframes slide {
|
@keyframes slide {
|
||||||
0% { transform: translateX(-100vw); }
|
0% { transform: translateX(-100vw); }
|
||||||
100% { transform: translateX(100vw); }
|
100% { transform: translateX(100vw); }
|
||||||
|
|
@ -101,9 +106,10 @@ export const App = () => {
|
||||||
animation: slide 24s linear infinite;
|
animation: slide 24s linear infinite;
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
</style>
|
</style>
|
||||||
</div>
|
</div>
|
||||||
</Router>
|
</Router>
|
||||||
|
</UserStoreProvider>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useSession } from "../contexts/SessionContext";
|
|
||||||
import { Navigate, Outlet } from "react-router-dom";
|
import { Navigate, Outlet } from "react-router-dom";
|
||||||
import { match } from "ts-pattern";
|
import { match } from "ts-pattern";
|
||||||
|
import { useSession } from "@ui/contexts/SessionContext";
|
||||||
|
|
||||||
interface ProtectedRouteProps {
|
interface ProtectedRouteProps {
|
||||||
fallback?: string;
|
fallback?: string;
|
||||||
|
|
@ -9,6 +9,7 @@ interface ProtectedRouteProps {
|
||||||
|
|
||||||
export const ProtectedRoute = ({ fallback }: ProtectedRouteProps) => {
|
export const ProtectedRoute = ({ fallback }: ProtectedRouteProps) => {
|
||||||
const { session } = useSession();
|
const { session } = useSession();
|
||||||
|
console.log({ session });
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -23,8 +24,10 @@ export const ProtectedRoute = ({ fallback }: ProtectedRouteProps) => {
|
||||||
| "should-land-user"
|
| "should-land-user"
|
||||||
| "should-redirect"
|
| "should-redirect"
|
||||||
| "should-pass" = "loading";
|
| "should-pass" = "loading";
|
||||||
|
|
||||||
const isFirstTimeUser =
|
const isFirstTimeUser =
|
||||||
localStorage.getItem("xtablo-has-seen-landing-page") === null;
|
localStorage.getItem("xtablo-has-seen-landing-page") === null;
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
status = "loading";
|
status = "loading";
|
||||||
} else if (!session?.user && isFirstTimeUser) {
|
} else if (!session?.user && isFirstTimeUser) {
|
||||||
|
|
|
||||||
|
|
@ -6,17 +6,9 @@ import { toast } from "../ui-library/toast/toast-queue";
|
||||||
|
|
||||||
export const SignOutButton = () => {
|
export const SignOutButton = () => {
|
||||||
const { mutate: logout, isPending, error } = useLogout();
|
const { mutate: logout, isPending, error } = useLogout();
|
||||||
const [isConfirming, setIsConfirming] = useState(false);
|
|
||||||
const [showSuccess, setShowSuccess] = useState(false);
|
const [showSuccess, setShowSuccess] = useState(false);
|
||||||
|
|
||||||
const handleLogout = () => {
|
const handleLogout = () => {
|
||||||
if (!isConfirming) {
|
|
||||||
setIsConfirming(true);
|
|
||||||
// Auto-reset confirmation after 3 seconds
|
|
||||||
setTimeout(() => setIsConfirming(false), 3000);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
logout(undefined, {
|
logout(undefined, {
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
setShowSuccess(true);
|
setShowSuccess(true);
|
||||||
|
|
@ -27,7 +19,6 @@ export const SignOutButton = () => {
|
||||||
position: "top-right",
|
position: "top-right",
|
||||||
});
|
});
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setIsConfirming(false);
|
|
||||||
setShowSuccess(false);
|
setShowSuccess(false);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
|
|
@ -39,19 +30,12 @@ export const SignOutButton = () => {
|
||||||
type: "error",
|
type: "error",
|
||||||
position: "top-right",
|
position: "top-right",
|
||||||
});
|
});
|
||||||
setIsConfirming(false);
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent) => {
|
|
||||||
if (event.key === "Escape" && isConfirming) {
|
|
||||||
setIsConfirming(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative inline-block group" onKeyDown={handleKeyDown}>
|
<div className="relative inline-block group">
|
||||||
<Button
|
<Button
|
||||||
onPress={handleLogout}
|
onPress={handleLogout}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -73,8 +57,6 @@ export const SignOutButton = () => {
|
||||||
${
|
${
|
||||||
showSuccess
|
showSuccess
|
||||||
? "bg-success/20 border-success/30 text-success hover:bg-success/25"
|
? "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"
|
: "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"
|
? "opacity-80 cursor-not-allowed"
|
||||||
: "hover:scale-[1.02] active:scale-[0.98]"
|
: "hover:scale-[1.02] active:scale-[0.98]"
|
||||||
}
|
}
|
||||||
${isConfirming ? "ring-2 ring-destructive/20" : ""}
|
|
||||||
group-hover:shadow-lg
|
group-hover:shadow-lg
|
||||||
`}
|
`}
|
||||||
isDisabled={isPending}
|
isDisabled={isPending}
|
||||||
aria-label={
|
aria-label={showSuccess ? "Déconnexion réussie" : "Se déconnecter"}
|
||||||
showSuccess
|
|
||||||
? "Déconnexion réussie"
|
|
||||||
: isConfirming
|
|
||||||
? "Confirmer la déconnexion - Cliquez à nouveau pour confirmer"
|
|
||||||
: "Se déconnecter"
|
|
||||||
}
|
|
||||||
tooltip={
|
tooltip={
|
||||||
showSuccess
|
showSuccess ? "Déconnexion réussie" : "Se déconnecter de votre compte"
|
||||||
? "Déconnexion réussie"
|
|
||||||
: isConfirming
|
|
||||||
? "Cliquez à nouveau pour confirmer la déconnexion"
|
|
||||||
: "Se déconnecter de votre compte"
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<div className="flex items-center justify-center gap-2.5 relative z-10">
|
<div className="flex items-center justify-center gap-2.5 relative z-10">
|
||||||
|
|
@ -127,8 +98,6 @@ export const SignOutButton = () => {
|
||||||
${
|
${
|
||||||
isPending
|
isPending
|
||||||
? "animate-spin text-destructive/60"
|
? "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"
|
: "text-destructive/70 group-hover:text-destructive group-hover:translate-x-[-1px] group-hover:scale-105"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
@ -147,8 +116,6 @@ export const SignOutButton = () => {
|
||||||
${
|
${
|
||||||
showSuccess
|
showSuccess
|
||||||
? "text-success"
|
? "text-success"
|
||||||
: isConfirming
|
|
||||||
? "text-destructive font-semibold"
|
|
||||||
: "text-destructive/80 group-hover:text-destructive"
|
: "text-destructive/80 group-hover:text-destructive"
|
||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
|
|
@ -157,8 +124,6 @@ export const SignOutButton = () => {
|
||||||
? "Déconnecté"
|
? "Déconnecté"
|
||||||
: isPending
|
: isPending
|
||||||
? "Déconnexion..."
|
? "Déconnexion..."
|
||||||
: isConfirming
|
|
||||||
? "Confirmer ?"
|
|
||||||
: "Déconnexion"}
|
: "Déconnexion"}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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" />
|
<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 && (
|
{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" />
|
<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>
|
</div>
|
||||||
</Button>
|
</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 */}
|
{/* Success indicator */}
|
||||||
{showSuccess && (
|
{showSuccess && (
|
||||||
<div className="absolute -bottom-0.5 left-0 right-0 flex justify-center">
|
<div className="absolute -bottom-0.5 left-0 right-0 flex justify-center">
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { createContext, useContext, useEffect, useState } from "react";
|
import { createContext, useContext, useEffect, useState } from "react";
|
||||||
import { Session, User } from "@supabase/supabase-js";
|
import { Session, User } from "@supabase/supabase-js";
|
||||||
import { supabase } from "../hooks/auth";
|
import { supabase } from "@ui/hooks/auth";
|
||||||
|
|
||||||
const SessionContext = createContext<{
|
const SessionContext = createContext<{
|
||||||
session: Session | null;
|
session: Session | null;
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,7 @@ import { IS_DEV } from "@ui/config";
|
||||||
|
|
||||||
// Create axios instance with default config
|
// Create axios instance with default config
|
||||||
export const api = axios.create({
|
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: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { SignOutButton } from "@ui/components/SignOutButton";
|
import { SignOutButton } from "@ui/components/SignOutButton";
|
||||||
|
import { useUser } from "@ui/providers/UserStoreProvider";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
interface Tablo {
|
interface Tablo {
|
||||||
|
|
@ -15,6 +16,8 @@ interface Folder {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TabloPage = () => {
|
export const TabloPage = () => {
|
||||||
|
const user = useUser();
|
||||||
|
console.log({ user });
|
||||||
const [hoveredTablo, setHoveredTablo] = useState<number | null>(null);
|
const [hoveredTablo, setHoveredTablo] = useState<number | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [newTabloName, setNewTabloName] = useState("");
|
const [newTabloName, setNewTabloName] = useState("");
|
||||||
|
|
@ -535,7 +538,9 @@ export const TabloPage = () => {
|
||||||
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
|
||||||
Vos tablos
|
Vos tablos
|
||||||
</h1>
|
</h1>
|
||||||
<SignOutButton />
|
<div className="flex items-center gap-3">
|
||||||
|
<SignOutButton />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
<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">
|
<div className="container mx-auto px-4 py-8">
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { Chat, useCreateChatClient } from "stream-chat-react";
|
import { Chat, useCreateChatClient } from "stream-chat-react";
|
||||||
|
import { useUser } from "./UserStoreProvider";
|
||||||
|
|
||||||
export default function ChatProvider({
|
export default function ChatProvider({
|
||||||
children,
|
children,
|
||||||
|
|
@ -6,14 +7,14 @@ export default function ChatProvider({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const apiKey = import.meta.env.VITE_STREAM_CHAT_API_KEY as string;
|
const apiKey = import.meta.env.VITE_STREAM_CHAT_API_KEY as string;
|
||||||
|
const user = useUser();
|
||||||
const client = useCreateChatClient({
|
const client = useCreateChatClient({
|
||||||
apiKey,
|
apiKey,
|
||||||
options: { timeout: 5000 },
|
options: { timeout: 5000 },
|
||||||
tokenOrProvider: "artslidd",
|
tokenOrProvider: user.streamToken,
|
||||||
userData: {
|
userData: {
|
||||||
id: "artslidd",
|
id: user.id,
|
||||||
name: "Arthur",
|
name: user.full_name || "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
64
ui/src/providers/UserStoreProvider.tsx
Normal file
64
ui/src/providers/UserStoreProvider.tsx
Normal 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);
|
||||||
|
};
|
||||||
220
ui/src/types/database.types.ts
Normal file
220
ui/src/types/database.types.ts
Normal 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
|
||||||
|
|
@ -1,7 +1,5 @@
|
||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
@import "~stream-chat-react/dist/css/v2/index.css";
|
|
||||||
|
|
||||||
@plugin 'tailwindcss-animate';
|
@plugin 'tailwindcss-animate';
|
||||||
@plugin '@tailwindcss/container-queries';
|
@plugin '@tailwindcss/container-queries';
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue