This commit is contained in:
2026-08-11 21:09:32 +07:00
parent e63c8f61aa
commit 94c0c47fd0
44 changed files with 5215 additions and 71 deletions
+29
View File
@@ -0,0 +1,29 @@
import { useEffect } from 'react';
import { Navigate, Route, Routes } from 'react-router-dom';
import { AppLayout } from '@/app/layouts/AppLayout';
import { HomePage } from '@/app/pages/HomePage';
import { NotFoundPage } from '@/app/pages/NotFoundPage';
import { ProtectedRoute } from '@/features/auth/components/ProtectedRoute';
import { LoginPage } from '@/features/auth/pages/LoginPage';
import { useAuthStore } from '@/features/auth/store/authStore';
export default function App() {
useEffect(() => {
void useAuthStore.getState().init();
}, []);
return (
<Routes>
<Route path="/login" element={<LoginPage />} />
<Route element={<ProtectedRoute />}>
<Route element={<AppLayout />}>
<Route index element={<HomePage />} />
<Route path="/learn" element={<div>Learn (placeholder)</div>} />
</Route>
</Route>
<Route path="/404" element={<NotFoundPage />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
);
}
+46
View File
@@ -0,0 +1,46 @@
import { Link, Outlet } from 'react-router-dom';
import env from '@/shared/config/env';
import { useLogout } from '@/features/auth/hooks/useLogout';
import { useAuthStore } from '@/features/auth/store/authStore';
export function AppLayout() {
const user = useAuthStore((s) => s.user);
const logout = useLogout();
return (
<div className="min-h-screen bg-gray-50">
<header className="border-b border-gray-200 bg-white">
<div className="mx-auto flex h-14 max-w-5xl items-center justify-between px-4">
<div className="flex items-center gap-6">
<Link to="/" className="text-lg font-semibold text-gray-900">
{env.VITE_APP_NAME}
</Link>
<nav className="flex items-center gap-4 text-sm text-gray-600">
<Link to="/" className="hover:text-gray-900">
Home
</Link>
<Link to="/learn" className="hover:text-gray-900">
Learn
</Link>
</nav>
</div>
<div className="flex items-center gap-4">
{user && <span className="text-sm text-gray-500">{user.email}</span>}
<button
type="button"
onClick={() => logout.mutate()}
disabled={logout.isPending}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm font-medium text-gray-700 hover:bg-gray-100 disabled:opacity-50"
>
{logout.isPending ? 'Signing out…' : 'Sign out'}
</button>
</div>
</div>
</header>
<main className="mx-auto max-w-5xl px-4 py-8">
<Outlet />
</main>
</div>
);
}
+26
View File
@@ -0,0 +1,26 @@
import env from '@/shared/config/env';
import { useAuthUser } from '@/features/auth/hooks/useAuthUser';
export function HomePage() {
const { user, isLoading, isError } = useAuthUser();
return (
<div>
<h1 className="text-2xl font-semibold text-gray-900">
Welcome{user ? `, ${user.name}` : ''} 👋
</h1>
<p className="mt-2 text-gray-600">{env.VITE_APP_NAME} your adaptive learning workspace.</p>
<div className="mt-6 rounded-lg border border-gray-200 bg-white p-6 text-sm text-gray-600">
{isLoading && <p>Loading profile</p>}
{isError && <p>Could not load profile details.</p>}
{user && !isLoading && (
<ul className="space-y-1">
<li>ID: {user.id}</li>
<li>Email: {user.email}</li>
<li>Name: {user.name}</li>
</ul>
)}
</div>
</div>
);
}
+13
View File
@@ -0,0 +1,13 @@
import { Link } from 'react-router-dom';
export function NotFoundPage() {
return (
<div className="flex min-h-screen flex-col items-center justify-center bg-gray-50 px-4 text-center">
<h1 className="text-4xl font-semibold text-gray-900">404</h1>
<p className="mt-2 text-gray-600">This page does not exist.</p>
<Link to="/" className="mt-4 text-sm font-medium text-indigo-600 hover:text-indigo-700">
Back home
</Link>
</div>
);
}
+29
View File
@@ -0,0 +1,29 @@
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { useState, type ReactNode } from 'react';
import { ErrorBoundary } from '@/shared/components/ErrorBoundary';
interface QueryProviderProps {
children: ReactNode;
}
export function QueryProvider({ children }: QueryProviderProps) {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
retry: 1,
refetchOnWindowFocus: false,
staleTime: 60_000,
},
},
}),
);
return (
<QueryClientProvider client={queryClient}>
<ErrorBoundary>{children}</ErrorBoundary>
</QueryClientProvider>
);
}