refactor: new layout and new router
This commit is contained in:
parent
0987ca37d5
commit
3f13447e1f
518
pnpm-lock.yaml
generated
518
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,22 @@ import { StatusPage } from './pages/Status';
|
||||
import { TelemetryPage } from './pages/Telemetry';
|
||||
import { LayoutV2 } from './pages/LayoutV2';
|
||||
import { isDev } from './utils/env';
|
||||
import { RouterProvider, createRouter } from '@tanstack/react-router';
|
||||
import { routeTree } from './routeTree.gen';
|
||||
|
||||
const router = createRouter({
|
||||
routeTree,
|
||||
context: {
|
||||
userInfo: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
// Register the router instance for type safety
|
||||
declare module '@tanstack/react-router' {
|
||||
interface Register {
|
||||
router: typeof router;
|
||||
}
|
||||
}
|
||||
|
||||
export const AppRoutes: React.FC = React.memo(() => {
|
||||
const { info: userInfo } = useUserStore();
|
||||
@ -64,21 +80,26 @@ export const App: React.FC = React.memo(() => {
|
||||
const colorScheme = useColorSchema();
|
||||
const algorithm =
|
||||
colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||
const { info: userInfo } = useUserStore();
|
||||
|
||||
return (
|
||||
<div ref={rootRef} className="App">
|
||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<BrowserRouter>
|
||||
<ConfigProvider
|
||||
theme={{ algorithm }}
|
||||
getPopupContainer={() => rootRef.current!}
|
||||
>
|
||||
<TokenLoginContainer>
|
||||
{isDev ? (
|
||||
<RouterProvider router={router} context={{ userInfo }} />
|
||||
) : (
|
||||
<BrowserRouter>
|
||||
<AppRoutes />
|
||||
</BrowserRouter>
|
||||
)}
|
||||
</TokenLoginContainer>
|
||||
</ConfigProvider>
|
||||
</BrowserRouter>
|
||||
</QueryClientProvider>
|
||||
</trpc.Provider>
|
||||
</div>
|
||||
|
84
src/client/components/CommonList.tsx
Normal file
84
src/client/components/CommonList.tsx
Normal file
@ -0,0 +1,84 @@
|
||||
import React, { ComponentProps } from 'react';
|
||||
import { ScrollArea } from './ui/scroll-area';
|
||||
import { cn } from '@/utils/style';
|
||||
import { Badge } from './ui/badge';
|
||||
import { useNavigate, useRouterState } from '@tanstack/react-router';
|
||||
|
||||
export interface CommonListItem {
|
||||
id: string;
|
||||
title: string;
|
||||
content: React.ReactNode;
|
||||
tags: string[];
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface CommonListProps {
|
||||
items: CommonListItem[];
|
||||
}
|
||||
export const CommonList: React.FC<CommonListProps> = React.memo((props) => {
|
||||
const { location } = useRouterState();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<ScrollArea className="h-screen">
|
||||
<div className="flex flex-col gap-2 p-4 pt-0">
|
||||
{props.items.map((item) => {
|
||||
const isSelected = item.href === location.pathname;
|
||||
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
className={cn(
|
||||
'flex flex-col items-start gap-2 rounded-lg border p-3 text-left text-sm transition-all hover:bg-accent',
|
||||
isSelected && 'bg-muted'
|
||||
)}
|
||||
onClick={() =>
|
||||
navigate({
|
||||
to: item.href,
|
||||
})
|
||||
}
|
||||
>
|
||||
<div className="flex w-full flex-col gap-1">
|
||||
<div className="flex items-center">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="font-semibold">{item.title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="line-clamp-2 text-xs text-muted-foreground">
|
||||
{item.content}
|
||||
</div>
|
||||
{item.tags.length > 0 ? (
|
||||
<div className="flex items-center gap-2">
|
||||
{item.tags.map((tag) => (
|
||||
<Badge key={tag} variant={getBadgeVariantFromLabel(tag)}>
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
);
|
||||
});
|
||||
CommonList.displayName = 'CommonList';
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
function getBadgeVariantFromLabel(
|
||||
label: string
|
||||
): ComponentProps<typeof Badge>['variant'] {
|
||||
if (['work'].includes(label.toLowerCase())) {
|
||||
return 'default';
|
||||
}
|
||||
|
||||
if (['personal'].includes(label.toLowerCase())) {
|
||||
return 'outline';
|
||||
}
|
||||
|
||||
return 'secondary';
|
||||
}
|
36
src/client/components/ui/badge.tsx
Normal file
36
src/client/components/ui/badge.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/utils/style"
|
||||
|
||||
const badgeVariants = cva(
|
||||
"inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default:
|
||||
"border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80",
|
||||
secondary:
|
||||
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
destructive:
|
||||
"border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80",
|
||||
outline: "text-foreground",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
export interface BadgeProps
|
||||
extends React.HTMLAttributes<HTMLDivElement>,
|
||||
VariantProps<typeof badgeVariants> {}
|
||||
|
||||
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||
return (
|
||||
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||
)
|
||||
}
|
||||
|
||||
export { Badge, badgeVariants }
|
46
src/client/components/ui/scroll-area.tsx
Normal file
46
src/client/components/ui/scroll-area.tsx
Normal file
@ -0,0 +1,46 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/utils/style"
|
||||
|
||||
const ScrollArea = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
|
||||
>(({ className, children, ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.Root
|
||||
ref={ref}
|
||||
className={cn("relative overflow-hidden", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
))
|
||||
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
|
||||
|
||||
const ScrollBar = React.forwardRef<
|
||||
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
|
||||
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
>(({ className, orientation = "vertical", ...props }, ref) => (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
ref={ref}
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none select-none transition-colors",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent p-[1px]",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
))
|
||||
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
53
src/client/components/ui/tabs.tsx
Normal file
53
src/client/components/ui/tabs.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import * as React from 'react';
|
||||
import * as TabsPrimitive from '@radix-ui/react-tabs';
|
||||
|
||||
import { cn } from '@/utils/style';
|
||||
|
||||
const Tabs = TabsPrimitive.Root;
|
||||
|
||||
const TabsList = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.List>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.List
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex h-9 items-center justify-center rounded-lg bg-muted p-1 text-muted-foreground',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||
|
||||
const TabsTrigger = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Trigger
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'inline-flex items-center justify-center whitespace-nowrap rounded-md px-3 py-1 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||
|
||||
const TabsContent = React.forwardRef<
|
||||
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||
>(({ className, ...props }, ref) => (
|
||||
<TabsPrimitive.Content
|
||||
ref={ref}
|
||||
className={cn(
|
||||
'mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2',
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
));
|
||||
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||
|
||||
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
@ -27,13 +27,17 @@
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.6",
|
||||
"@radix-ui/react-icons": "^1.3.0",
|
||||
"@radix-ui/react-menubar": "^1.0.4",
|
||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||
"@radix-ui/react-select": "^2.0.0",
|
||||
"@radix-ui/react-separator": "^1.0.3",
|
||||
"@radix-ui/react-slot": "^1.0.2",
|
||||
"@radix-ui/react-tabs": "^1.0.4",
|
||||
"@radix-ui/react-tooltip": "^1.0.7",
|
||||
"@tanstack/react-query": "4.33.0",
|
||||
"@tanstack/react-router": "^1.20.5",
|
||||
"@tanstack/react-table": "^8.13.2",
|
||||
"@tanstack/react-virtual": "^3.0.2",
|
||||
"@tanstack/router-devtools": "^1.20.5",
|
||||
"@tianji/shared": "workspace:^",
|
||||
"@trpc/client": "^10.45.0",
|
||||
"@trpc/react-query": "^10.45.0",
|
||||
@ -75,6 +79,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@i18next-toolkit/cli": "^1.1.0",
|
||||
"@tanstack/router-vite-plugin": "^1.20.5",
|
||||
"@types/leaflet": "^1.9.8",
|
||||
"@types/loadable__component": "^5.13.8",
|
||||
"@types/lodash-es": "^4.17.12",
|
||||
@ -88,7 +93,7 @@
|
||||
"less": "^4.2.0",
|
||||
"postcss": "^8.4.31",
|
||||
"shadcn-ui": "^0.8.0",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"vite": "^5.0.12",
|
||||
"vitest": "^1.2.1"
|
||||
|
@ -1,19 +1,9 @@
|
||||
import * as React from 'react';
|
||||
import {
|
||||
LuAlertCircle,
|
||||
LuArchive,
|
||||
LuAreaChart,
|
||||
LuFile,
|
||||
LuFilePieChart,
|
||||
LuInbox,
|
||||
LuMessagesSquare,
|
||||
LuMonitorDot,
|
||||
LuMoreVertical,
|
||||
LuSend,
|
||||
LuServer,
|
||||
LuShoppingCart,
|
||||
LuTrash2,
|
||||
LuUsers2,
|
||||
LuWifi,
|
||||
} from 'react-icons/lu';
|
||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||
@ -33,10 +23,14 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { useUserInfo } from '@/store/user';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { UserConfig } from './Layout/UserConfig';
|
||||
import { Outlet } from '@tanstack/react-router';
|
||||
import { CommonList, CommonListItem } from '@/components/CommonList';
|
||||
|
||||
const defaultLayout: [number, number, number] = [265, 440, 655];
|
||||
|
||||
export const LayoutV2: React.FC = React.memo(() => {
|
||||
export const LayoutV2: React.FC<{
|
||||
list: React.ReactNode;
|
||||
}> = React.memo((props) => {
|
||||
const [layout = defaultLayout, setLayout] = useLocalStorageState(
|
||||
'react-resizable-panels:layout',
|
||||
{ defaultValue: defaultLayout }
|
||||
@ -127,11 +121,13 @@ export const LayoutV2: React.FC = React.memo(() => {
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={layout[1]} minSize={30}>
|
||||
<div>1</div>
|
||||
<div>{props.list}</div>
|
||||
</ResizablePanel>
|
||||
<ResizableHandle withHandle />
|
||||
<ResizablePanel defaultSize={layout[2]}>
|
||||
<div>2</div>
|
||||
<div>
|
||||
<Outlet />
|
||||
</div>
|
||||
</ResizablePanel>
|
||||
</ResizablePanelGroup>
|
||||
</TooltipProvider>
|
||||
|
83
src/client/routeTree.gen.ts
Normal file
83
src/client/routeTree.gen.ts
Normal file
@ -0,0 +1,83 @@
|
||||
/* prettier-ignore-start */
|
||||
|
||||
/* eslint-disable */
|
||||
|
||||
// @ts-nocheck
|
||||
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
|
||||
// This file is auto-generated by TanStack Router
|
||||
|
||||
// Import Routes
|
||||
|
||||
import { Route as rootRoute } from './routes/__root'
|
||||
import { Route as WebsiteImport } from './routes/website'
|
||||
import { Route as RegisterImport } from './routes/register'
|
||||
import { Route as LoginImport } from './routes/login'
|
||||
import { Route as IndexImport } from './routes/index'
|
||||
import { Route as WebsiteWebsiteIdImport } from './routes/website/$websiteId'
|
||||
|
||||
// Create/Update Routes
|
||||
|
||||
const WebsiteRoute = WebsiteImport.update({
|
||||
path: '/website',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const RegisterRoute = RegisterImport.update({
|
||||
path: '/register',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const LoginRoute = LoginImport.update({
|
||||
path: '/login',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const IndexRoute = IndexImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => rootRoute,
|
||||
} as any)
|
||||
|
||||
const WebsiteWebsiteIdRoute = WebsiteWebsiteIdImport.update({
|
||||
path: '/$websiteId',
|
||||
getParentRoute: () => WebsiteRoute,
|
||||
} as any)
|
||||
|
||||
// Populate the FileRoutesByPath interface
|
||||
|
||||
declare module '@tanstack/react-router' {
|
||||
interface FileRoutesByPath {
|
||||
'/': {
|
||||
preLoaderRoute: typeof IndexImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/login': {
|
||||
preLoaderRoute: typeof LoginImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/register': {
|
||||
preLoaderRoute: typeof RegisterImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/website': {
|
||||
preLoaderRoute: typeof WebsiteImport
|
||||
parentRoute: typeof rootRoute
|
||||
}
|
||||
'/website/$websiteId': {
|
||||
preLoaderRoute: typeof WebsiteWebsiteIdImport
|
||||
parentRoute: typeof WebsiteImport
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create and export the route tree
|
||||
|
||||
export const routeTree = rootRoute.addChildren([
|
||||
IndexRoute,
|
||||
LoginRoute,
|
||||
RegisterRoute,
|
||||
WebsiteRoute.addChildren([WebsiteWebsiteIdRoute]),
|
||||
])
|
||||
|
||||
/* prettier-ignore-end */
|
20
src/client/routes/__root.tsx
Normal file
20
src/client/routes/__root.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
|
||||
import { TanStackRouterDevtools } from '@tanstack/router-devtools';
|
||||
|
||||
interface RouterContext {
|
||||
// The ReturnType of your useAuth hook or the value of your AuthContext
|
||||
userInfo: any;
|
||||
}
|
||||
|
||||
const defaultLayout: [number, number, number] = [265, 440, 655];
|
||||
|
||||
export const Route = createRootRouteWithContext<RouterContext>()({
|
||||
component: () => {
|
||||
return (
|
||||
<>
|
||||
<Outlet />
|
||||
<TanStackRouterDevtools position="bottom-right" />
|
||||
</>
|
||||
);
|
||||
},
|
||||
});
|
15
src/client/routes/index.tsx
Normal file
15
src/client/routes/index.tsx
Normal file
@ -0,0 +1,15 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: Index,
|
||||
});
|
||||
|
||||
function Index() {
|
||||
return (
|
||||
<div className="p-2">
|
||||
<h3>Welcome Home!</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
101
src/client/routes/login.tsx
Normal file
101
src/client/routes/login.tsx
Normal file
@ -0,0 +1,101 @@
|
||||
import {
|
||||
createFileRoute,
|
||||
getRouteApi,
|
||||
useNavigate,
|
||||
} from '@tanstack/react-router';
|
||||
import { useRequest } from '@/hooks/useRequest';
|
||||
import { setJWT } from '@/api/auth';
|
||||
import { useGlobalConfig } from '@/hooks/useConfig';
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { Button, Form, Input, Typography } from 'antd';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { setUserInfo } from '@/store/user';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const Route = createFileRoute('/login')({
|
||||
validateSearch: z.object({
|
||||
// redirect: z.string().catch('/'),
|
||||
redirect: z.string().optional(),
|
||||
}),
|
||||
component: LoginComponent,
|
||||
});
|
||||
|
||||
const routeApi = getRouteApi('/login');
|
||||
|
||||
function LoginComponent() {
|
||||
const navigate = useNavigate();
|
||||
const { t } = useTranslation();
|
||||
const loginMutation = trpc.user.login.useMutation();
|
||||
const search = routeApi.useSearch();
|
||||
|
||||
const [{ loading }, handleLogin] = useRequest(async (values: any) => {
|
||||
const res = await loginMutation.mutateAsync({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
|
||||
setJWT(res.token);
|
||||
setUserInfo(res.info);
|
||||
navigate({
|
||||
to: search.redirect ?? '/dashboard',
|
||||
});
|
||||
});
|
||||
const { allowRegister } = useGlobalConfig();
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center dark:bg-gray-900">
|
||||
<div className="w-80 -translate-y-1/4">
|
||||
<div className="text-center">
|
||||
<img className="w-24 h-24" src="/icon.svg" />
|
||||
</div>
|
||||
<Typography.Title className="text-center" level={2}>
|
||||
Tianji
|
||||
</Typography.Title>
|
||||
<Form layout="vertical" disabled={loading} onFinish={handleLogin}>
|
||||
<Form.Item
|
||||
label={t('Username')}
|
||||
name="username"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('Password')}
|
||||
name="password"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
block={true}
|
||||
loading={loading}
|
||||
>
|
||||
{t('Login')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
{allowRegister && (
|
||||
<Form.Item>
|
||||
<Button
|
||||
size="large"
|
||||
htmlType="button"
|
||||
block={true}
|
||||
onClick={() => {
|
||||
navigate({
|
||||
to: '/register',
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t('Register')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
71
src/client/routes/register.tsx
Normal file
71
src/client/routes/register.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { Button, Form, Input, Typography } from 'antd';
|
||||
import React from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useRequest } from '../hooks/useRequest';
|
||||
import { trpc } from '../api/trpc';
|
||||
import { setJWT } from '../api/auth';
|
||||
import { setUserInfo } from '../store/user';
|
||||
import { useTranslation } from '@i18next-toolkit/react';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/register')({
|
||||
component: RegisterComponent,
|
||||
});
|
||||
|
||||
function RegisterComponent() {
|
||||
const { t } = useTranslation();
|
||||
const navigate = useNavigate();
|
||||
|
||||
const mutation = trpc.user.register.useMutation();
|
||||
|
||||
const [{ loading }, handleRegister] = useRequest(async (values: any) => {
|
||||
const res = await mutation.mutateAsync({
|
||||
username: values.username,
|
||||
password: values.password,
|
||||
});
|
||||
setJWT(res.token);
|
||||
setUserInfo(res.info);
|
||||
|
||||
navigate('/dashboard');
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="w-full h-full flex justify-center items-center">
|
||||
<div className="w-80 -translate-y-1/4">
|
||||
<div className="text-center">
|
||||
<img className="w-24 h-24" src="/icon.svg" />
|
||||
</div>
|
||||
<Typography.Title className="text-center" level={2}>
|
||||
{t('Register Account')}
|
||||
</Typography.Title>
|
||||
<Form layout="vertical" disabled={loading} onFinish={handleRegister}>
|
||||
<Form.Item
|
||||
label={t('Username')}
|
||||
name="username"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('Password')}
|
||||
name="password"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Input.Password size="large" />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
htmlType="submit"
|
||||
block={true}
|
||||
loading={loading}
|
||||
>
|
||||
{t('Register')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
71
src/client/routes/website.tsx
Normal file
71
src/client/routes/website.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { trpc } from '@/api/trpc';
|
||||
import { CommonList } from '@/components/CommonList';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { LayoutV2 } from '@/pages/LayoutV2';
|
||||
import { useCurrentWorkspaceId } from '@/store/user';
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { createFileRoute } from '@tanstack/react-router';
|
||||
import { LuSearch } from 'react-icons/lu';
|
||||
|
||||
export const Route = createFileRoute('/website')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: WebsiteComponent,
|
||||
});
|
||||
|
||||
function WebsiteComponent() {
|
||||
const workspaceId = useCurrentWorkspaceId();
|
||||
const { data = [] } = trpc.website.all.useQuery({
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const items = data.map((item) => ({
|
||||
id: item.id,
|
||||
title: item.name,
|
||||
content: item.domain,
|
||||
tags: [],
|
||||
href: `/website/${item.id}`,
|
||||
}));
|
||||
|
||||
return (
|
||||
<LayoutV2
|
||||
list={
|
||||
<Tabs defaultValue="all">
|
||||
<div className="flex items-center px-4 py-2">
|
||||
<h1 className="text-xl font-bold">Website</h1>
|
||||
<TabsList className="ml-auto">
|
||||
<TabsTrigger
|
||||
value="all"
|
||||
className="text-zinc-600 dark:text-zinc-200"
|
||||
>
|
||||
All mail
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="unread"
|
||||
className="text-zinc-600 dark:text-zinc-200"
|
||||
>
|
||||
Unread
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
</div>
|
||||
<Separator />
|
||||
<div className="bg-background/95 p-4 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<form>
|
||||
<div className="relative">
|
||||
<LuSearch className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||
<Input placeholder="Search" className="pl-8" />
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<TabsContent value="all" className="m-0">
|
||||
<CommonList items={items} />
|
||||
</TabsContent>
|
||||
<TabsContent value="unread" className="m-0">
|
||||
<CommonList items={items} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
13
src/client/routes/website/$websiteId.tsx
Normal file
13
src/client/routes/website/$websiteId.tsx
Normal file
@ -0,0 +1,13 @@
|
||||
import { routeAuthBeforeLoad } from '@/utils/route';
|
||||
import { createFileRoute, getRouteApi } from '@tanstack/react-router';
|
||||
|
||||
export const Route = createFileRoute('/website/$websiteId')({
|
||||
beforeLoad: routeAuthBeforeLoad,
|
||||
component: WebsiteDetailComponent,
|
||||
});
|
||||
|
||||
function WebsiteDetailComponent() {
|
||||
const params = Route.useParams<{ websiteId: string }>();
|
||||
|
||||
return <div>website: {params.websiteId}</div>;
|
||||
}
|
@ -11,6 +11,7 @@ module.exports = {
|
||||
'./index.html',
|
||||
'./components/**/*.{js,jsx,ts,tsx}',
|
||||
'./pages/**/*.{js,jsx,ts,tsx}',
|
||||
'./routes/**/*.{js,jsx,ts,tsx}',
|
||||
],
|
||||
},
|
||||
theme: {
|
||||
@ -84,7 +85,7 @@ module.exports = {
|
||||
},
|
||||
darkMode: 'class',
|
||||
corePlugins: {
|
||||
preflight: false,
|
||||
preflight: true,
|
||||
},
|
||||
plugins: [animate],
|
||||
} satisfies Config;
|
||||
|
15
src/client/utils/route.ts
Normal file
15
src/client/utils/route.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { FileBaseRouteOptions, redirect } from '@tanstack/react-router';
|
||||
|
||||
export const routeAuthBeforeLoad: FileBaseRouteOptions['beforeLoad'] = ({
|
||||
context,
|
||||
location,
|
||||
}) => {
|
||||
if (!(context as any).userInfo) {
|
||||
throw redirect({
|
||||
to: '/login',
|
||||
search: {
|
||||
redirect: location.href,
|
||||
},
|
||||
});
|
||||
}
|
||||
};
|
@ -1,11 +1,18 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
import { resolve } from 'path';
|
||||
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
root: __dirname,
|
||||
plugins: [react()],
|
||||
plugins: [
|
||||
react(),
|
||||
TanStackRouterVite({
|
||||
routesDirectory: './routes',
|
||||
generatedRouteTree: './routeTree.gen.ts',
|
||||
}),
|
||||
],
|
||||
build: {
|
||||
outDir: '../server/public',
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user