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"
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {}
|
||||
|
|
|
|||
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 { 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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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 || "",
|
||||
},
|
||||
});
|
||||
|
||||
|
|
|
|||
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 "~stream-chat-react/dist/css/v2/index.css";
|
||||
|
||||
@plugin 'tailwindcss-animate';
|
||||
@plugin '@tailwindcss/container-queries';
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue