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 { TelemetryPage } from './pages/Telemetry';
|
||||||
import { LayoutV2 } from './pages/LayoutV2';
|
import { LayoutV2 } from './pages/LayoutV2';
|
||||||
import { isDev } from './utils/env';
|
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(() => {
|
export const AppRoutes: React.FC = React.memo(() => {
|
||||||
const { info: userInfo } = useUserStore();
|
const { info: userInfo } = useUserStore();
|
||||||
@ -64,21 +80,26 @@ export const App: React.FC = React.memo(() => {
|
|||||||
const colorScheme = useColorSchema();
|
const colorScheme = useColorSchema();
|
||||||
const algorithm =
|
const algorithm =
|
||||||
colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
colorScheme === 'dark' ? theme.darkAlgorithm : theme.defaultAlgorithm;
|
||||||
|
const { info: userInfo } = useUserStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={rootRef} className="App">
|
<div ref={rootRef} className="App">
|
||||||
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
<trpc.Provider client={trpcClient} queryClient={queryClient}>
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<BrowserRouter>
|
|
||||||
<ConfigProvider
|
<ConfigProvider
|
||||||
theme={{ algorithm }}
|
theme={{ algorithm }}
|
||||||
getPopupContainer={() => rootRef.current!}
|
getPopupContainer={() => rootRef.current!}
|
||||||
>
|
>
|
||||||
<TokenLoginContainer>
|
<TokenLoginContainer>
|
||||||
|
{isDev ? (
|
||||||
|
<RouterProvider router={router} context={{ userInfo }} />
|
||||||
|
) : (
|
||||||
|
<BrowserRouter>
|
||||||
<AppRoutes />
|
<AppRoutes />
|
||||||
|
</BrowserRouter>
|
||||||
|
)}
|
||||||
</TokenLoginContainer>
|
</TokenLoginContainer>
|
||||||
</ConfigProvider>
|
</ConfigProvider>
|
||||||
</BrowserRouter>
|
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
</trpc.Provider>
|
</trpc.Provider>
|
||||||
</div>
|
</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-dropdown-menu": "^2.0.6",
|
||||||
"@radix-ui/react-icons": "^1.3.0",
|
"@radix-ui/react-icons": "^1.3.0",
|
||||||
"@radix-ui/react-menubar": "^1.0.4",
|
"@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-select": "^2.0.0",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
"@tanstack/react-query": "4.33.0",
|
"@tanstack/react-query": "4.33.0",
|
||||||
|
"@tanstack/react-router": "^1.20.5",
|
||||||
"@tanstack/react-table": "^8.13.2",
|
"@tanstack/react-table": "^8.13.2",
|
||||||
"@tanstack/react-virtual": "^3.0.2",
|
"@tanstack/react-virtual": "^3.0.2",
|
||||||
|
"@tanstack/router-devtools": "^1.20.5",
|
||||||
"@tianji/shared": "workspace:^",
|
"@tianji/shared": "workspace:^",
|
||||||
"@trpc/client": "^10.45.0",
|
"@trpc/client": "^10.45.0",
|
||||||
"@trpc/react-query": "^10.45.0",
|
"@trpc/react-query": "^10.45.0",
|
||||||
@ -75,6 +79,7 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@i18next-toolkit/cli": "^1.1.0",
|
"@i18next-toolkit/cli": "^1.1.0",
|
||||||
|
"@tanstack/router-vite-plugin": "^1.20.5",
|
||||||
"@types/leaflet": "^1.9.8",
|
"@types/leaflet": "^1.9.8",
|
||||||
"@types/loadable__component": "^5.13.8",
|
"@types/loadable__component": "^5.13.8",
|
||||||
"@types/lodash-es": "^4.17.12",
|
"@types/lodash-es": "^4.17.12",
|
||||||
@ -88,7 +93,7 @@
|
|||||||
"less": "^4.2.0",
|
"less": "^4.2.0",
|
||||||
"postcss": "^8.4.31",
|
"postcss": "^8.4.31",
|
||||||
"shadcn-ui": "^0.8.0",
|
"shadcn-ui": "^0.8.0",
|
||||||
"tailwindcss": "^3.3.5",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"vite": "^5.0.12",
|
"vite": "^5.0.12",
|
||||||
"vitest": "^1.2.1"
|
"vitest": "^1.2.1"
|
||||||
|
@ -1,19 +1,9 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import {
|
import {
|
||||||
LuAlertCircle,
|
|
||||||
LuArchive,
|
|
||||||
LuAreaChart,
|
LuAreaChart,
|
||||||
LuFile,
|
|
||||||
LuFilePieChart,
|
LuFilePieChart,
|
||||||
LuInbox,
|
|
||||||
LuMessagesSquare,
|
|
||||||
LuMonitorDot,
|
LuMonitorDot,
|
||||||
LuMoreVertical,
|
|
||||||
LuSend,
|
|
||||||
LuServer,
|
LuServer,
|
||||||
LuShoppingCart,
|
|
||||||
LuTrash2,
|
|
||||||
LuUsers2,
|
|
||||||
LuWifi,
|
LuWifi,
|
||||||
} from 'react-icons/lu';
|
} from 'react-icons/lu';
|
||||||
import { TooltipProvider } from '@/components/ui/tooltip';
|
import { TooltipProvider } from '@/components/ui/tooltip';
|
||||||
@ -33,10 +23,14 @@ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
|
|||||||
import { useUserInfo } from '@/store/user';
|
import { useUserInfo } from '@/store/user';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { UserConfig } from './Layout/UserConfig';
|
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];
|
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(
|
const [layout = defaultLayout, setLayout] = useLocalStorageState(
|
||||||
'react-resizable-panels:layout',
|
'react-resizable-panels:layout',
|
||||||
{ defaultValue: defaultLayout }
|
{ defaultValue: defaultLayout }
|
||||||
@ -127,11 +121,13 @@ export const LayoutV2: React.FC = React.memo(() => {
|
|||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel defaultSize={layout[1]} minSize={30}>
|
<ResizablePanel defaultSize={layout[1]} minSize={30}>
|
||||||
<div>1</div>
|
<div>{props.list}</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
<ResizableHandle withHandle />
|
<ResizableHandle withHandle />
|
||||||
<ResizablePanel defaultSize={layout[2]}>
|
<ResizablePanel defaultSize={layout[2]}>
|
||||||
<div>2</div>
|
<div>
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
</ResizablePanel>
|
</ResizablePanel>
|
||||||
</ResizablePanelGroup>
|
</ResizablePanelGroup>
|
||||||
</TooltipProvider>
|
</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',
|
'./index.html',
|
||||||
'./components/**/*.{js,jsx,ts,tsx}',
|
'./components/**/*.{js,jsx,ts,tsx}',
|
||||||
'./pages/**/*.{js,jsx,ts,tsx}',
|
'./pages/**/*.{js,jsx,ts,tsx}',
|
||||||
|
'./routes/**/*.{js,jsx,ts,tsx}',
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
theme: {
|
theme: {
|
||||||
@ -84,7 +85,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
darkMode: 'class',
|
darkMode: 'class',
|
||||||
corePlugins: {
|
corePlugins: {
|
||||||
preflight: false,
|
preflight: true,
|
||||||
},
|
},
|
||||||
plugins: [animate],
|
plugins: [animate],
|
||||||
} satisfies Config;
|
} 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 { defineConfig } from 'vite';
|
||||||
import react from '@vitejs/plugin-react';
|
import react from '@vitejs/plugin-react';
|
||||||
import { resolve } from 'path';
|
import { resolve } from 'path';
|
||||||
|
import { TanStackRouterVite } from '@tanstack/router-vite-plugin';
|
||||||
|
|
||||||
// https://vitejs.dev/config/
|
// https://vitejs.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
root: __dirname,
|
root: __dirname,
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
TanStackRouterVite({
|
||||||
|
routesDirectory: './routes',
|
||||||
|
generatedRouteTree: './routeTree.gen.ts',
|
||||||
|
}),
|
||||||
|
],
|
||||||
build: {
|
build: {
|
||||||
outDir: '../server/public',
|
outDir: '../server/public',
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user