feat(clients): add layout, auth callback, tablo page, and list page
Adds SessionProvider to main.tsx, creates ClientLayout with minimal top bar, AuthCallback for magic link handling, ClientTabloPage with all 7 tabs using tablo-views components, and ClientTabloListPage with auto-redirect for single tablo. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
118b23bfb1
commit
f3fb08c9b2
8 changed files with 536 additions and 9 deletions
67
apps/clients/src/components/ClientLayout.tsx
Normal file
67
apps/clients/src/components/ClientLayout.tsx
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { useSession } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { Avatar, AvatarFallback } from "@xtablo/ui/components/avatar";
|
||||
import { Button } from "@xtablo/ui/components/button";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { supabase } from "../lib/supabase";
|
||||
|
||||
function getInitials(email: string): string {
|
||||
const parts = email.split("@")[0].split(/[._-]/);
|
||||
return parts
|
||||
.slice(0, 2)
|
||||
.map((p) => p[0]?.toUpperCase() ?? "")
|
||||
.join("");
|
||||
}
|
||||
|
||||
export function ClientLayout() {
|
||||
const { session } = useSession();
|
||||
|
||||
if (!session) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-3">
|
||||
<p className="text-lg font-medium text-foreground">Accès non autorisé</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Veuillez utiliser le lien reçu dans votre email pour accéder à cette page.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const email = session.user.email ?? "";
|
||||
const initials = email ? getInitials(email) : "?";
|
||||
|
||||
const handleLogout = async () => {
|
||||
await supabase.auth.signOut();
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Top bar */}
|
||||
<header className="border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="flex h-14 items-center justify-between px-4 max-w-7xl mx-auto">
|
||||
{/* Brand */}
|
||||
<span className="text-lg font-bold text-foreground">Xtablo</span>
|
||||
|
||||
{/* User info + logout */}
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Avatar className="h-8 w-8">
|
||||
<AvatarFallback className="text-xs">{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
<span className="text-sm text-muted-foreground hidden sm:block">{email}</span>
|
||||
</div>
|
||||
<Button variant="outline" size="sm" onClick={handleLogout}>
|
||||
Déconnexion
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* Page content */}
|
||||
<main className="max-w-7xl mx-auto px-4 py-6">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
10
apps/clients/src/lib/supabase.ts
Normal file
10
apps/clients/src/lib/supabase.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { createSupabaseClient } from "@xtablo/shared";
|
||||
|
||||
const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
|
||||
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;
|
||||
|
||||
if (!supabaseUrl || !supabaseAnonKey) {
|
||||
throw new Error("Missing Supabase environment variables");
|
||||
}
|
||||
|
||||
export const supabase = createSupabaseClient(supabaseUrl, supabaseAnonKey);
|
||||
|
|
@ -1,11 +1,13 @@
|
|||
import { QueryClientProvider } from "@tanstack/react-query";
|
||||
import { queryClient } from "@xtablo/shared";
|
||||
import { SessionProvider } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { ThemeProvider } from "@xtablo/shared/contexts/ThemeContext";
|
||||
import { Toaster } from "@xtablo/ui/components/sonner";
|
||||
import { StrictMode } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { BrowserRouter as Router } from "react-router-dom";
|
||||
import App from "./App";
|
||||
import { supabase } from "./lib/supabase";
|
||||
|
||||
import "@xtablo/ui/styles/globals.css";
|
||||
import "./main.css";
|
||||
|
|
@ -14,12 +16,14 @@ import "./i18n";
|
|||
createRoot(document.getElementById("client-root")!).render(
|
||||
<StrictMode>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<ThemeProvider>
|
||||
<Toaster />
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
<SessionProvider supabase={supabase}>
|
||||
<ThemeProvider>
|
||||
<Toaster />
|
||||
<Router>
|
||||
<App />
|
||||
</Router>
|
||||
</ThemeProvider>
|
||||
</SessionProvider>
|
||||
</QueryClientProvider>
|
||||
</StrictMode>
|
||||
);
|
||||
|
|
|
|||
66
apps/clients/src/pages/AuthCallback.tsx
Normal file
66
apps/clients/src/pages/AuthCallback.tsx
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
import { useSession } from "@xtablo/shared/contexts/SessionContext";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
|
||||
export function AuthCallback() {
|
||||
const [searchParams] = useSearchParams();
|
||||
const token = searchParams.get("token");
|
||||
const { session } = useSession();
|
||||
const navigate = useNavigate();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const hasAccepted = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!session || !token || hasAccepted.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
hasAccepted.current = true;
|
||||
|
||||
const apiUrl = import.meta.env.VITE_API_URL as string;
|
||||
|
||||
fetch(`${apiUrl}/api/v1/client-invites/accept/${token}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then(async (res) => {
|
||||
if (!res.ok) {
|
||||
const body = await res.json().catch(() => ({}));
|
||||
throw new Error((body as { message?: string }).message ?? "Erreur lors de l'acceptation de l'invitation");
|
||||
}
|
||||
return res.json() as Promise<{ tabloId: string }>;
|
||||
})
|
||||
.then((data) => {
|
||||
navigate(`/tablo/${data.tabloId}`, { replace: true });
|
||||
})
|
||||
.catch((err: unknown) => {
|
||||
console.error("Accept invite error:", err);
|
||||
setError(
|
||||
"Une erreur est survenue lors de l'acceptation de l'invitation. Veuillez contacter la personne qui vous a invité."
|
||||
);
|
||||
});
|
||||
}, [session, token, navigate]);
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-3 max-w-md px-4">
|
||||
<p className="text-lg font-medium text-destructive">Erreur</p>
|
||||
<p className="text-sm text-muted-foreground">{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-background">
|
||||
<div className="text-center space-y-3">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary mx-auto" />
|
||||
<p className="text-sm text-muted-foreground">Authentification en cours...</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
63
apps/clients/src/pages/ClientTabloListPage.tsx
Normal file
63
apps/clients/src/pages/ClientTabloListPage.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import type { UserTablo } from "@xtablo/shared-types";
|
||||
import { Navigate, Link } from "react-router-dom";
|
||||
import { supabase } from "../lib/supabase";
|
||||
|
||||
function useClientTablosList() {
|
||||
return useQuery<UserTablo[]>({
|
||||
queryKey: ["client-tablos-list"],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase.from("user_tablos").select("*");
|
||||
if (error) throw error;
|
||||
return (data ?? []) as UserTablo[];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function ClientTabloListPage() {
|
||||
const { data: tablos, isLoading } = useClientTablosList();
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!tablos || tablos.length === 0) {
|
||||
return (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-muted-foreground">Aucun projet disponible.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (tablos.length === 1) {
|
||||
return <Navigate to={`/tablo/${tablos[0].id}`} replace />;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">Mes projets</h1>
|
||||
<p className="text-muted-foreground mt-1">Sélectionnez un projet pour y accéder.</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
||||
{tablos.map((tablo) => (
|
||||
<Link
|
||||
key={tablo.id}
|
||||
to={`/tablo/${tablo.id}`}
|
||||
className="block p-5 rounded-lg border border-border bg-card hover:bg-muted/50 transition-colors space-y-2"
|
||||
>
|
||||
{tablo.color && (
|
||||
<div className={`w-8 h-8 rounded-lg ${tablo.color}`} />
|
||||
)}
|
||||
<h2 className="font-semibold text-foreground">{tablo.name}</h2>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
310
apps/clients/src/pages/ClientTabloPage.tsx
Normal file
310
apps/clients/src/pages/ClientTabloPage.tsx
Normal file
|
|
@ -0,0 +1,310 @@
|
|||
import { useQuery } from "@tanstack/react-query";
|
||||
import { buildApi } from "@xtablo/shared";
|
||||
import { useSession } from "@xtablo/shared/contexts/SessionContext";
|
||||
import type { Etape, KanbanTask, TabloFolder, UserTablo } from "@xtablo/shared-types";
|
||||
import { CalendarIcon, FolderIcon, KanbanIcon, ListChecksIcon, MapIcon, MessageCircleIcon } from "lucide-react";
|
||||
import { useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import {
|
||||
EtapesSection,
|
||||
RoadmapSection,
|
||||
TabloDiscussionSection,
|
||||
TabloEventsSection,
|
||||
TabloFilesSection,
|
||||
TabloTasksSection,
|
||||
} from "@xtablo/tablo-views";
|
||||
import { supabase } from "../lib/supabase";
|
||||
|
||||
const API_URL = import.meta.env.VITE_API_URL as string;
|
||||
|
||||
// ─── Local hooks ──────────────────────────────────────────────────────────────
|
||||
|
||||
function useAuthedApi(accessToken: string | undefined) {
|
||||
return buildApi(API_URL).create({
|
||||
headers: {
|
||||
Authorization: `Bearer ${accessToken ?? ""}`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTablo(tabloId: string) {
|
||||
return useQuery<UserTablo>({
|
||||
queryKey: ["client-tablo", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("user_tablos")
|
||||
.select("*")
|
||||
.eq("id", tabloId)
|
||||
.single();
|
||||
if (error) throw error;
|
||||
return data as UserTablo;
|
||||
},
|
||||
enabled: !!tabloId,
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTabloTasks(tabloId: string) {
|
||||
return useQuery<KanbanTask[]>({
|
||||
queryKey: ["client-tasks", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("tasks_with_assignee")
|
||||
.select("*")
|
||||
.eq("tablo_id", tabloId)
|
||||
.eq("is_parent", false)
|
||||
.order("updated_at", { ascending: false });
|
||||
if (error) throw error;
|
||||
return (data ?? []) as KanbanTask[];
|
||||
},
|
||||
enabled: !!tabloId,
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTabloEtapes(tabloId: string) {
|
||||
return useQuery<Etape[]>({
|
||||
queryKey: ["client-etapes", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("tasks")
|
||||
.select("*")
|
||||
.eq("tablo_id", tabloId)
|
||||
.eq("is_parent", true)
|
||||
.order("position", { ascending: true });
|
||||
if (error) throw error;
|
||||
return (data ?? []) as Etape[];
|
||||
},
|
||||
enabled: !!tabloId,
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTabloEvents(tabloId: string) {
|
||||
return useQuery({
|
||||
queryKey: ["client-events", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data, error } = await supabase
|
||||
.from("events_and_tablos")
|
||||
.select("*")
|
||||
.eq("tablo_id", tabloId)
|
||||
.order("start_date", { ascending: false });
|
||||
if (error) throw error;
|
||||
return data ?? [];
|
||||
},
|
||||
enabled: !!tabloId,
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTabloMembers(tabloId: string, accessToken: string | undefined) {
|
||||
const api = useAuthedApi(accessToken);
|
||||
return useQuery({
|
||||
queryKey: ["client-members", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<{
|
||||
members: {
|
||||
id: string;
|
||||
name: string;
|
||||
is_admin: boolean;
|
||||
email: string;
|
||||
avatar_url: string | null;
|
||||
}[];
|
||||
}>(`/api/v1/tablos/members/${tabloId}`);
|
||||
return data.members;
|
||||
},
|
||||
enabled: !!tabloId && !!accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTabloFiles(tabloId: string, accessToken: string | undefined) {
|
||||
const api = useAuthedApi(accessToken);
|
||||
return useQuery<{ fileNames: string[] }>({
|
||||
queryKey: ["client-tablo-files", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get(`/api/v1/tablo-data/${tabloId}/filenames`);
|
||||
return data as { fileNames: string[] };
|
||||
},
|
||||
enabled: !!tabloId && !!accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
function useClientTabloFolders(tabloId: string, accessToken: string | undefined) {
|
||||
const api = useAuthedApi(accessToken);
|
||||
return useQuery<TabloFolder[]>({
|
||||
queryKey: ["client-tablo-folders", tabloId],
|
||||
queryFn: async () => {
|
||||
const { data } = await api.get<{ folders: TabloFolder[] }>(`/api/v1/tablo-folders/${tabloId}`);
|
||||
return data.folders ?? [];
|
||||
},
|
||||
enabled: !!tabloId && !!accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
// ─── Tabs ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
type TabId = "overview" | "etapes" | "tasks" | "files" | "discussion" | "events" | "roadmap";
|
||||
|
||||
const TABS: { id: TabId; label: string; icon: React.ElementType }[] = [
|
||||
{ id: "overview", label: "Aperçu", icon: ListChecksIcon },
|
||||
{ id: "etapes", label: "Étapes", icon: ListChecksIcon },
|
||||
{ id: "tasks", label: "Tâches", icon: KanbanIcon },
|
||||
{ id: "files", label: "Fichiers", icon: FolderIcon },
|
||||
{ id: "discussion", label: "Discussion", icon: MessageCircleIcon },
|
||||
{ id: "events", label: "Événements", icon: CalendarIcon },
|
||||
{ id: "roadmap", label: "Roadmap", icon: MapIcon },
|
||||
];
|
||||
|
||||
// ─── Page ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export function ClientTabloPage() {
|
||||
const { tabloId } = useParams<{ tabloId: string }>();
|
||||
const { session } = useSession();
|
||||
const [activeTab, setActiveTab] = useState<TabId>("overview");
|
||||
|
||||
const accessToken = session?.access_token;
|
||||
const currentUserId = session?.user.id ?? "";
|
||||
|
||||
const { data: tablo, isLoading: tabloLoading } = useClientTablo(tabloId ?? "");
|
||||
const { data: tasks = [] } = useClientTabloTasks(tabloId ?? "");
|
||||
const { data: etapes = [] } = useClientTabloEtapes(tabloId ?? "");
|
||||
const { data: events, isLoading: eventsLoading, error: eventsError } = useClientTabloEvents(tabloId ?? "");
|
||||
const { data: members = [] } = useClientTabloMembers(tabloId ?? "", accessToken);
|
||||
const { data: filesData, isLoading: filesLoading, error: filesError } = useClientTabloFiles(tabloId ?? "", accessToken);
|
||||
const { data: folders = [], isLoading: foldersLoading, error: foldersError } = useClientTabloFolders(tabloId ?? "", accessToken);
|
||||
|
||||
const fileNames = (filesData?.fileNames ?? []).filter((f) => !f.startsWith("."));
|
||||
|
||||
const currentUser = { id: currentUserId, avatar_url: null };
|
||||
|
||||
if (tabloLoading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!tablo) {
|
||||
return (
|
||||
<div className="text-center py-16">
|
||||
<p className="text-muted-foreground">Projet introuvable.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Tablo header */}
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-foreground">{tablo.name}</h1>
|
||||
</div>
|
||||
|
||||
{/* Tab bar */}
|
||||
<div className="border-b border-border">
|
||||
<nav className="flex gap-1 overflow-x-auto">
|
||||
{TABS.map((tab) => {
|
||||
const Icon = tab.icon;
|
||||
return (
|
||||
<button
|
||||
key={tab.id}
|
||||
type="button"
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-2.5 text-sm font-medium border-b-2 whitespace-nowrap transition-colors ${
|
||||
activeTab === tab.id
|
||||
? "border-primary text-primary"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground hover:border-muted-foreground"
|
||||
}`}
|
||||
>
|
||||
<Icon className="w-4 h-4" />
|
||||
{tab.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Tab content */}
|
||||
<div>
|
||||
{activeTab === "overview" && (
|
||||
<div className="space-y-6">
|
||||
{/* Simple overview: list etapes with progress */}
|
||||
<EtapesSection
|
||||
etapes={etapes}
|
||||
tabloTasks={tasks}
|
||||
tabloId={tablo.id}
|
||||
isAdmin={false}
|
||||
onCreateTask={() => {}}
|
||||
onCreateEtape={async () => {}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{activeTab === "etapes" && (
|
||||
<EtapesSection
|
||||
etapes={etapes}
|
||||
tabloTasks={tasks}
|
||||
tabloId={tablo.id}
|
||||
isAdmin={false}
|
||||
onCreateTask={() => {}}
|
||||
onCreateEtape={async () => {}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "tasks" && (
|
||||
<TabloTasksSection
|
||||
tablo={tablo}
|
||||
isAdmin={false}
|
||||
tasks={tasks}
|
||||
members={members}
|
||||
etapes={etapes}
|
||||
currentUser={currentUser}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "files" && (
|
||||
<TabloFilesSection
|
||||
tablo={tablo}
|
||||
isAdmin={false}
|
||||
isReadOnly={true}
|
||||
currentUserId={currentUserId}
|
||||
fileNames={fileNames}
|
||||
filesLoading={filesLoading}
|
||||
filesError={filesError instanceof Error ? filesError : null}
|
||||
folders={folders}
|
||||
foldersLoading={foldersLoading}
|
||||
foldersError={foldersError instanceof Error ? foldersError : null}
|
||||
currentUser={currentUser}
|
||||
members={members}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "discussion" && (
|
||||
<TabloDiscussionSection
|
||||
tablo={tablo}
|
||||
isAdmin={false}
|
||||
currentUserId={currentUserId}
|
||||
members={members}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "events" && (
|
||||
<TabloEventsSection
|
||||
tablo={tablo}
|
||||
isAdmin={false}
|
||||
isReadOnly={true}
|
||||
events={events as Parameters<typeof TabloEventsSection>[0]["events"]}
|
||||
isLoading={eventsLoading}
|
||||
error={eventsError instanceof Error ? eventsError : null}
|
||||
currentUser={currentUser}
|
||||
members={members}
|
||||
/>
|
||||
)}
|
||||
|
||||
{activeTab === "roadmap" && (
|
||||
<RoadmapSection
|
||||
tabloTasks={tasks}
|
||||
onDateClick={() => {}}
|
||||
onTaskStatusChange={() => {}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1,11 +1,17 @@
|
|||
import { Route, Routes } from "react-router-dom";
|
||||
import { ClientLayout } from "./components/ClientLayout";
|
||||
import { AuthCallback } from "./pages/AuthCallback";
|
||||
import { ClientTabloListPage } from "./pages/ClientTabloListPage";
|
||||
import { ClientTabloPage } from "./pages/ClientTabloPage";
|
||||
|
||||
export default function AppRoutes() {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path="/auth/callback" element={<div>Auth callback placeholder</div>} />
|
||||
<Route path="/tablo/:tabloId" element={<div>Tablo view placeholder</div>} />
|
||||
<Route path="/" element={<div>Client portal placeholder</div>} />
|
||||
<Route path="/auth/callback" element={<AuthCallback />} />
|
||||
<Route element={<ClientLayout />}>
|
||||
<Route path="/tablo/:tabloId" element={<ClientTabloPage />} />
|
||||
<Route path="/" element={<ClientTabloListPage />} />
|
||||
</Route>
|
||||
</Routes>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
"target": "ES2020",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||
"types": ["vite/client"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
|
|
|
|||
Loading…
Reference in a new issue