first commit

This commit is contained in:
2026-03-10 11:45:54 +08:00
commit e06d464a74
231 changed files with 15232 additions and 0 deletions

7
web/src/api/client.ts Normal file
View File

@@ -0,0 +1,7 @@
import { edenTreaty } from '@elysiajs/eden';
import type { App } from 'backend';
// 创建 Eden 客户端,自动推断类型
export const api = edenTreaty<App>('http://localhost:3001');

View File

@@ -0,0 +1,11 @@
import { Button } from "@heroui/react";
import { Link as RouterLink } from "react-router-dom";
export default function AboutPage() {
return (
<div className="py-16">
<h1>About</h1>
<Button as={RouterLink} to="/">Back</Button>
</div>
);
}

View File

@@ -0,0 +1,15 @@
import { Button } from "@heroui/react";
import { Link, useNavigate } from "react-router";
export default function DemoPage() {
const navigate = useNavigate();
return (
<div className="flex flex-col gap-3 py-16 max-w-3xl mx-auto">
<h1 className="text-2xl font-bold leading-loose">Demo Page</h1>
<button onClick={() => navigate("/")} className="p-3 rounded-lg bg-zinc-300 transition hover:scale-105 cursor-pointer">Back</button>
<Button color="primary" variant="flat" onPress={() => navigate("/")}>HeroUI Back</Button>
<Link to="/">Back</Link>
</div>
)
}

83
web/src/app/index.tsx Normal file
View File

@@ -0,0 +1,83 @@
import { useState } from 'react';
import { SiteNavbar } from '../components/SiteNavbar';
import { useResources, Resource } from '../hooks/useApi';
import { CustomCard } from '../components/CustomCard';
import { Button, Link } from '@heroui/react';
import { CreateResourceModal } from '../components/CreateResourceModal';
import { EditResourceModal } from '../components/EditResourceModal';
import { useSession } from '../lib/auth-client';
export default function HomePage() {
const { resources, isLoading } = useResources();
const [isModalOpen, setIsModalOpen] = useState(false);
const [isEditModalOpen, setIsEditModalOpen] = useState(false);
const [selectedResource, setSelectedResource] = useState<Resource | null>(null);
const { data: session } = useSession();
return (
<div className="min-h-screen bg-background text-foreground">
<SiteNavbar />
<main className="container mx-auto px-4 py-8">
<section className="mb-12">
<div className="flex justify-between items-center mb-6">
<h2 className="text-2xl font-bold"> & </h2>
{session && (
<Button onPress={() => setIsModalOpen(true)} color="primary" variant="secondary">
</Button>
)}
</div>
<CreateResourceModal isOpen={isModalOpen} onOpenChange={setIsModalOpen} />
<EditResourceModal
isOpen={isEditModalOpen}
onOpenChange={setIsEditModalOpen}
resource={selectedResource}
/>
{isLoading ? (
<div>...</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{resources?.map((res) => (
<CustomCard key={res.id} variant="glass" className="p-4">
<CustomCard.Header>
<div className="flex flex-col">
<p className="text-md">{res.title}</p>
{res.category && <p className="text-small text-default-500">{res.category}</p>}
</div>
</CustomCard.Header>
<CustomCard.Content>
<p className="text-sm text-gray-600 dark:text-gray-300 mb-4">{res.description}</p>
</CustomCard.Content>
<CustomCard.Footer className="flex justify-between items-center gap-2">
<Button as={Link} href={res.url} target="_blank" size="sm" color="primary">
访
</Button>
{session && (
<Button
size="sm"
variant="ghost"
onPress={() => {
setSelectedResource(res);
setIsEditModalOpen(true);
}}
>
</Button>
)}
</CustomCard.Footer>
</CustomCard>
))}
{!resources?.length && (
<div className="col-span-full text-center text-gray-500">
</div>
)}
</div>
)}
</section>
</main>
</div>
);
}

View File

@@ -0,0 +1,65 @@
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Card, Input, Button, Form, toast } from '@heroui/react';
import { signIn } from '../../lib/auth-client';
export default function LoginPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await signIn.email({
email,
password,
}, {
onSuccess: () => {
navigate('/');
},
onError: (ctx) => {
toast.danger(ctx.error.message);
}
});
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="flex items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md p-6">
<h1 className="text-2xl font-bold mb-6 text-center"> HLAE </h1>
<Form validationBehavior="native" onSubmit={handleLogin} className="flex flex-col gap-4">
<Input
isRequired
label="邮箱"
placeholder="请输入您的邮箱"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input
isRequired
label="密码"
placeholder="请输入您的密码"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button color="primary" type="submit" isLoading={loading}>
</Button>
<div className="text-center text-sm">
<Link to="/register" className="text-primary hover:underline"></Link>
</div>
</Form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,99 @@
import { useParams, Link as RouterLink } from 'react-router-dom';
import { SiteNavbar } from '../../../components/SiteNavbar';
import { usePost } from '../../../hooks/useApi';
import { Card, Button, Avatar, Divider, Skeleton } from '@heroui/react';
import { Link } from '@heroui/react';
export default function PostDetailPage() {
const { id } = useParams<{ id: string }>();
const { post, isLoading, isError } = usePost(id || '');
if (isLoading) {
return (
<div className="min-h-screen bg-background text-foreground">
<SiteNavbar />
<main className="container mx-auto px-4 py-8">
<Card className="p-6 max-w-4xl mx-auto space-y-4">
<Skeleton className="rounded-lg">
<div className="h-8 w-3/4 rounded-lg bg-default-300"></div>
</Skeleton>
<Skeleton className="rounded-lg">
<div className="h-4 w-1/4 rounded-lg bg-default-300"></div>
</Skeleton>
<Skeleton className="rounded-lg">
<div className="h-40 w-full rounded-lg bg-default-300"></div>
</Skeleton>
</Card>
</main>
</div>
);
}
if (isError || !post) {
return (
<div className="min-h-screen bg-background text-foreground">
<SiteNavbar />
<main className="container mx-auto px-4 py-8 text-center">
<h1 className="text-2xl font-bold mb-4"></h1>
<Button as={RouterLink} to="/posts" color="primary">
</Button>
</main>
</div>
);
}
return (
<div className="min-h-screen bg-background text-foreground">
<SiteNavbar />
<main className="container mx-auto px-4 py-8">
<div className="max-w-4xl mx-auto">
<div className="mb-4">
<Link as={RouterLink} to="/posts" className="text-default-500 hover:text-foreground">
&larr;
</Link>
</div>
<Card className="p-6 mb-8">
<Card.Header className="flex flex-col items-start gap-2">
<h1 className="text-3xl font-bold">{post.title}</h1>
<div className="flex items-center gap-2 text-small text-default-500">
<Avatar
className="w-6 h-6"
name={post.user?.name || ''}
src={undefined} // Add user image if available in API
/>
<span>{post.user?.name || '匿名用户'}</span>
<span></span>
<span>{new Date(post.createdAt).toLocaleString()}</span>
</div>
</Card.Header>
<Divider className="my-4" />
<Card.Content>
<div className="whitespace-pre-wrap text-lg leading-relaxed">
{post.content}
</div>
</Card.Content>
</Card>
{post.comments && post.comments.length > 0 && (
<div className="space-y-4">
<h3 className="text-xl font-bold"> ({post.comments.length})</h3>
{post.comments.map((comment) => (
<Card key={comment.id} className="p-4">
<div className="flex flex-col gap-2">
<div className="flex items-center gap-2 text-small text-default-500">
<span className="font-semibold text-foreground">{comment.user?.name || '匿名用户'}</span>
<span>{new Date(comment.createdAt).toLocaleString()}</span>
</div>
<p>{comment.content}</p>
</div>
</Card>
))}
</div>
)}
</div>
</main>
</div>
);
}

101
web/src/app/posts/index.tsx Normal file
View File

@@ -0,0 +1,101 @@
import { useState } from 'react';
import { SiteNavbar } from '../../components/SiteNavbar';
import { usePosts } from '../../hooks/useApi';
import { Link as RouterLink } from 'react-router-dom';
import { Card, Button, Input, TextArea, Form, Link, toast } from '@heroui/react';
import { useSession } from '../../lib/auth-client';
import { api } from '../../api/client';
export default function PostsPage() {
const { posts, isLoading, mutate } = usePosts();
const { data: session } = useSession();
const [title, setTitle] = useState('');
const [content, setContent] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!session) return;
setIsSubmitting(true);
try {
await api.posts.post({
title,
content,
userId: session.user.id,
published: true
});
setTitle('');
setContent('');
mutate(); // Refresh list
} catch (err) {
console.error(err);
toast.danger('发布失败');
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen bg-background text-foreground">
<SiteNavbar />
<main className="container mx-auto px-4 py-8">
<div className="flex flex-col md:flex-row gap-8">
<div className="flex-1">
<h2 className="text-2xl font-bold mb-6"></h2>
{isLoading ? (
<div>...</div>
) : (
<div className="flex flex-col gap-4">
{posts?.map((post: any) => (
<Card key={post.id} className="p-4">
<Card.Header>
<div className="flex flex-col">
<Link as={RouterLink} to={`/posts/${post.id}`} className="text-md font-bold text-foreground hover:text-primary">
{post.title}
</Link>
<p className="text-small text-default-500">
{post.user?.name || '匿名用户'} - {new Date(post.createdAt).toLocaleDateString()}
</p>
</div>
</Card.Header>
<Card.Content>
<p className="whitespace-pre-wrap line-clamp-3">{post.content}</p>
</Card.Content>
</Card>
))}
{!posts?.length && <div className="text-center text-gray-500"></div>}
</div>
)}
</div>
{session && (
<div className="w-full md:w-80">
<Card className="p-4 sticky top-4">
<h3 className="text-xl font-bold mb-4"></h3>
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
<Input
isRequired
label="标题"
placeholder="请输入标题"
value={title}
onValueChange={setTitle}
/>
<TextArea
isRequired
label="内容"
placeholder="请输入内容"
value={content}
onValueChange={setContent}
/>
<Button color="primary" type="submit" isLoading={isSubmitting}>
</Button>
</Form>
</Card>
</div>
)}
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,66 @@
import { useState } from 'react';
import { useNavigate, Link } from 'react-router-dom';
import { Card, Input, Button, Form, toast } from '@heroui/react';
import { signUp } from '../../lib/auth-client';
export default function RegisterPage() {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [loading, setLoading] = useState(false);
const navigate = useNavigate();
const handleRegister = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await signUp.email({
email,
password,
name: email.split('@')[0], // Default name
}, {
onSuccess: () => {
navigate('/');
},
onError: (ctx) => {
toast.danger(ctx.error.message);
}
});
} catch (err) {
console.error(err);
} finally {
setLoading(false);
}
};
return (
<div className="flex items-center justify-center min-h-screen p-4">
<Card className="w-full max-w-md p-6">
<h1 className="text-2xl font-bold mb-6 text-center"> HLAE </h1>
<Form validationBehavior="native" onSubmit={handleRegister} className="flex flex-col gap-4">
<Input
isRequired
label="邮箱"
placeholder="请输入您的邮箱"
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
/>
<Input
isRequired
label="密码"
placeholder="请输入您的密码"
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
/>
<Button color="primary" type="submit" isLoading={loading}>
</Button>
<div className="text-center text-sm">
<Link to="/login" className="text-primary hover:underline"></Link>
</div>
</Form>
</Card>
</div>
);
}

View File

@@ -0,0 +1,89 @@
import { useState } from 'react';
import { Modal, Button, Input, Form, toast } from "@heroui/react";
import { api } from '../api/client';
import { useSWRConfig } from 'swr';
export function CreateResourceModal({ isOpen, onOpenChange }: { isOpen: boolean, onOpenChange: (isOpen: boolean) => void }) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [url, setUrl] = useState('');
const [category, setCategory] = useState('');
const [loading, setLoading] = useState(false);
const { mutate } = useSWRConfig();
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
await api.resources.post({
title,
description,
url,
category
});
mutate('/resources');
onOpenChange(false);
setTitle('');
setDescription('');
setUrl('');
setCategory('');
} catch (err) {
console.error(err);
toast.danger('创建失败');
} finally {
setLoading(false);
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<Modal.Backdrop />
<Modal.Container>
<Modal.Dialog>
<Modal.Header>
<Modal.Heading></Modal.Heading>
<Modal.CloseTrigger />
</Modal.Header>
<Modal.Body>
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
<Input
isRequired
label="标题"
placeholder="资源名称"
value={title}
onValueChange={setTitle}
/>
<Input
label="描述"
placeholder="简短描述"
value={description}
onValueChange={setDescription}
/>
<Input
isRequired
label="链接"
placeholder="https://..."
value={url}
onValueChange={setUrl}
/>
<Input
label="分类"
placeholder="如: 工具, 文档"
value={category}
onValueChange={setCategory}
/>
<Button color="primary" type="submit" isLoading={loading}>
</Button>
</Form>
</Modal.Body>
<Modal.Footer>
<Button color="danger" variant="ghost" onPress={() => onOpenChange(false)}>
</Button>
</Modal.Footer>
</Modal.Dialog>
</Modal.Container>
</Modal>
);
}

View File

@@ -0,0 +1,29 @@
import { Card } from "@heroui/react";
import { tv, type VariantProps } from "tailwind-variants";
import React from "react";
// Define custom styles
export const customStyles = tv({
variants: {
variant: {
glass: "bg-white/70 backdrop-blur-md border-white/20 shadow-xl dark:bg-black/70 dark:border-white/10",
neon: "border border-blue-500 shadow-[0_0_15px_rgba(59,130,246,0.5)] bg-gray-900/90 text-white",
minimal: "border-none shadow-none bg-transparent hover:bg-gray-100 dark:hover:bg-gray-800 transition-colors",
},
},
});
type CustomCardProps = React.ComponentProps<typeof Card> & VariantProps<typeof customStyles>;
// Create the custom component
export function CustomCard({ className, variant, ...props }: CustomCardProps) {
// Use custom styles and merge with className
return <Card className={customStyles({ variant, className })} {...props} />;
}
// Attach subcomponents to the custom component for easier usage
CustomCard.Header = Card.Header;
CustomCard.Content = Card.Content;
CustomCard.Footer = Card.Footer;
CustomCard.Title = Card.Title;
CustomCard.Description = Card.Description;

View File

@@ -0,0 +1,124 @@
import { useState, useEffect } from 'react';
import { Modal, Button, Input, Form, toast } from "@heroui/react";
import { api } from '../api/client';
import { useSWRConfig } from 'swr';
import { Resource } from '../hooks/useApi';
export function EditResourceModal({
isOpen,
onOpenChange,
resource
}: {
isOpen: boolean,
onOpenChange: (isOpen: boolean) => void,
resource: Resource | null
}) {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const [url, setUrl] = useState('');
const [category, setCategory] = useState('');
const [loading, setLoading] = useState(false);
const { mutate } = useSWRConfig();
useEffect(() => {
if (resource) {
setTitle(resource.title);
setDescription(resource.description || '');
setUrl(resource.url);
setCategory(resource.category || '');
}
}, [resource]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!resource) return;
setLoading(true);
try {
await api.resources({ id: resource.id }).put({
title,
description,
url,
category
});
mutate('/resources');
onOpenChange(false);
} catch (err) {
console.error(err);
toast.danger('更新失败');
} finally {
setLoading(false);
}
};
const handleDelete = async () => {
if (!resource || !confirm('确定要删除这个资源吗?')) return;
setLoading(true);
try {
await api.resources({ id: resource.id }).delete();
mutate('/resources');
onOpenChange(false);
} catch (err) {
console.error(err);
toast.danger('删除失败');
} finally {
setLoading(false);
}
};
return (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}>
<Modal.Backdrop />
<Modal.Container>
<Modal.Dialog>
<Modal.Header>
<Modal.Heading></Modal.Heading>
<Modal.CloseTrigger />
</Modal.Header>
<Modal.Body>
<Form validationBehavior="native" onSubmit={handleSubmit} className="flex flex-col gap-4">
<Input
isRequired
label="标题"
placeholder="资源名称"
value={title}
onValueChange={setTitle}
/>
<Input
label="描述"
placeholder="简短描述"
value={description}
onValueChange={setDescription}
/>
<Input
isRequired
label="链接"
placeholder="https://..."
value={url}
onValueChange={setUrl}
/>
<Input
label="分类"
placeholder="如: 工具, 文档"
value={category}
onValueChange={setCategory}
/>
<div className="flex justify-between gap-2 mt-4">
<Button color="danger" variant="flat" onPress={handleDelete} isLoading={loading}>
</Button>
<div className="flex gap-2">
<Button color="danger" variant="ghost" onPress={() => onOpenChange(false)}>
</Button>
<Button color="primary" type="submit" isLoading={loading}>
</Button>
</div>
</div>
</Form>
</Modal.Body>
</Modal.Dialog>
</Modal.Container>
</Modal>
);
}

View File

@@ -0,0 +1,93 @@
import {
Link,
Button,
Avatar,
Dropdown,
Label
} from "@heroui/react";
import { useEffect, useState } from "react";
import { useSession, signOut } from "../lib/auth-client";
export function SiteNavbar() {
const { data: session } = useSession();
const [theme, setTheme] = useState<"light" | "dark">("light");
useEffect(() => {
const currentTheme = document.documentElement.classList.contains("dark") ? "dark" : "light";
setTheme(currentTheme);
}, []);
const toggleTheme = () => {
const nextTheme = theme === "dark" ? "light" : "dark";
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(nextTheme);
document.documentElement.setAttribute("data-theme", nextTheme);
localStorage.setItem("theme", nextTheme);
setTheme(nextTheme);
};
return (
<nav className="flex items-center justify-between px-6 py-3 bg-background/70 backdrop-blur-md border-b border-white/10 sticky top-0 z-50 w-full">
<div className="flex items-center gap-4">
<Link className="font-bold text-inherit text-lg no-underline text-foreground" href="/">
HLAE
</Link>
</div>
<div className="hidden sm:flex gap-6 justify-center flex-1">
<Link className="text-foreground hover:text-primary transition-colors" href="/">
</Link>
<Link className="text-foreground hover:text-primary transition-colors" href="/posts">
</Link>
</div>
<div className="flex items-center gap-4 justify-end">
<Button variant="ghost" onPress={toggleTheme}>
{theme === "dark" ? "☀️" : "🌙"}
</Button>
{session ? (
<Dropdown>
<Dropdown.Trigger>
<button className="outline-none focus:outline-none rounded-full">
<Avatar size="sm">
<Avatar.Image
alt={session.user.name || "用户头像"}
src={session.user.image || ""}
/>
<Avatar.Fallback>
{(session.user.name || "U").slice(0, 2).toUpperCase()}
</Avatar.Fallback>
</Avatar>
</button>
</Dropdown.Trigger>
<Dropdown.Popover>
<Dropdown.Menu aria-label="Profile Actions">
<Dropdown.Item id="profile" textValue="Profile">
<Label className="font-semibold"> {session.user.email}</Label>
</Dropdown.Item>
<Dropdown.Item id="logout" textValue="Logout" onPress={() => signOut()}>
<Label className="text-danger">退</Label>
</Dropdown.Item>
</Dropdown.Menu>
</Dropdown.Popover>
</Dropdown>
) : (
<div className="flex gap-4 items-center">
<Link href="/login">
<Button variant="ghost" className="text-foreground hover:text-primary">
</Button>
</Link>
<Link href="/register">
<Button variant="primary" className="text-foreground hover:text-primary">
</Button>
</Link>
</div>
)}
</div>
</nav>
);
}

7
web/src/globals.css Normal file
View File

@@ -0,0 +1,7 @@
@import "tailwindcss";
@import "@heroui/styles";
@layer base {
/* :root {} */
}

47
web/src/hooks/useApi.ts Normal file
View File

@@ -0,0 +1,47 @@
import useSWR from 'swr';
import { api } from '../api/client';
export function useResources() {
const { data, error, isLoading, mutate } = useSWR('/resources', async () => {
const { data, error } = await api.resources.get();
if (error) throw error;
return data;
});
return {
resources: data,
isLoading,
isError: error,
mutate
};
}
export function usePosts() {
const { data, error, isLoading, mutate } = useSWR('/posts', async () => {
const { data, error } = await api.posts.get();
if (error) throw error;
return data;
});
return {
posts: data,
isLoading,
isError: error,
mutate
};
}
export function usePost(id: string) {
const { data, error, isLoading, mutate } = useSWR(id ? `/posts/${id}` : null, async () => {
const { data, error } = await api.posts({ id }).get();
if (error) throw error;
return data;
});
return {
post: data,
isLoading,
isError: error,
mutate
};
}

19
web/src/layout.tsx Normal file
View File

@@ -0,0 +1,19 @@
import { Toast } from "@heroui/react";
import "./globals.css";
// import Footer from "./components/footer";
// import Header from "./components/header";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div className="min-h-screen">
<Toast.Provider />
{/* <Header /> */}
<main className="min-h-screen h-full">{children}</main>
{/* <Footer /> */}
</div>
);
}

View File

@@ -0,0 +1,7 @@
import { createAuthClient } from 'better-auth/react';
export const authClient = createAuthClient({
baseURL: import.meta.env.VITE_API_URL,
});
export const { useSession, signIn, signUp, signOut } = authClient;

14
web/src/providers.tsx Normal file
View File

@@ -0,0 +1,14 @@
// import { ThemeProvider } from "next-themes";
// import { HeroUIProvider, ToastProvider } from '@heroui/react'
export function Providers( { children }: { children: React.ReactNode } ) {
return (
// <HeroUIProvider>
// <ThemeProvider attribute="class" defaultTheme="dark">
// <ToastProvider />
{children}
// </ThemeProvider>
// </HeroUIProvider>
);
}

38
web/src/root.tsx Normal file
View File

@@ -0,0 +1,38 @@
import { StrictMode, Suspense, useEffect } from "react";
import { createRoot } from "react-dom/client";
import { BrowserRouter, useRoutes } from "react-router-dom";
// import { Providers } from "./providers";
import RootLayout from "./layout";
import routes from "~react-pages";
function App() {
useEffect(() => {
const storedTheme = localStorage.getItem("theme");
const systemPrefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
const theme = storedTheme === "dark" || storedTheme === "light"
? storedTheme
: systemPrefersDark
? "dark"
: "light";
document.documentElement.classList.remove("light", "dark");
document.documentElement.classList.add(theme);
document.documentElement.setAttribute("data-theme", theme);
}, []);
return <Suspense fallback={<p>Loading...</p>}>{useRoutes(routes)}</Suspense>;
}
const app = createRoot(document.getElementById("root")!);
app.render(
<StrictMode>
<BrowserRouter>
{/* <Providers> */}
<RootLayout>
<App />
</RootLayout>
{/* </Providers> */}
</BrowserRouter>
</StrictMode>,
);

3
web/src/vite-env.d.ts vendored Normal file
View File

@@ -0,0 +1,3 @@
// vite-env.d.ts
/// <reference types="vite-plugin-pages/client-react" />
/// <reference types="vite/client" />