mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 04:48:53 +00:00
feat(admin): init project (#7197)
This commit is contained in:
102
packages/frontend/admin/src/modules/auth/index.tsx
Normal file
102
packages/frontend/admin/src/modules/auth/index.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import { Input } from '@affine/admin/components/ui/input';
|
||||
import { Label } from '@affine/admin/components/ui/label';
|
||||
import { FeatureType, getUserFeaturesQuery } from '@affine/graphql';
|
||||
import { useCallback, useRef } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import logo from './logo.svg';
|
||||
|
||||
export function Auth() {
|
||||
const emailRef = useRef<HTMLInputElement>(null);
|
||||
const passwordRef = useRef<HTMLInputElement>(null);
|
||||
const navigate = useNavigate();
|
||||
const login = useCallback(() => {
|
||||
if (!emailRef.current || !passwordRef.current) return;
|
||||
fetch('/api/auth/sign-in', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
email: emailRef.current?.value,
|
||||
password: passwordRef.current?.value,
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(() =>
|
||||
fetch('/graphql', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
operationName: getUserFeaturesQuery.operationName,
|
||||
query: getUserFeaturesQuery.query,
|
||||
variables: {},
|
||||
}),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
})
|
||||
)
|
||||
.then(res => res.json())
|
||||
.then(
|
||||
({
|
||||
data: {
|
||||
currentUser: { features },
|
||||
},
|
||||
}) => {
|
||||
if (features.includes(FeatureType.Admin)) {
|
||||
navigate('/admin');
|
||||
} else {
|
||||
toast.error('You are not an admin');
|
||||
}
|
||||
}
|
||||
)
|
||||
.catch(err => {
|
||||
toast.error(`Failed to login: ${err.message}`);
|
||||
});
|
||||
}, [navigate]);
|
||||
return (
|
||||
<div className="w-full lg:grid lg:min-h-[600px] lg:grid-cols-2 xl:min-h-[800px]">
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="mx-auto grid w-[350px] gap-6">
|
||||
<div className="grid gap-2 text-center">
|
||||
<h1 className="text-3xl font-bold">Login</h1>
|
||||
<p className="text-balance text-muted-foreground">
|
||||
Enter your email below to login to your account
|
||||
</p>
|
||||
</div>
|
||||
<div className="grid gap-4">
|
||||
<div className="grid gap-2">
|
||||
<Label htmlFor="email">Email</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
ref={emailRef}
|
||||
placeholder="m@example.com"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
<div className="grid gap-2">
|
||||
<div className="flex items-center">
|
||||
<Label htmlFor="password">Password</Label>
|
||||
</div>
|
||||
<Input id="password" type="password" ref={passwordRef} required />
|
||||
</div>
|
||||
<Button onClick={login} type="submit" className="w-full">
|
||||
Login
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="hidden bg-muted lg:block">
|
||||
<img
|
||||
src={logo}
|
||||
alt="Image"
|
||||
className="w-1/2 h-1/2 object-cover dark:brightness-[0.2] dark:grayscale relative top-1/4 left-1/4"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export { Auth as Component };
|
||||
18
packages/frontend/admin/src/modules/auth/logo.svg
Normal file
18
packages/frontend/admin/src/modules/auth/logo.svg
Normal file
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg id="Layer_2" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 470 526.48">
|
||||
<g id="_6_export">
|
||||
<rect id="box_geometric" fill="none" width="470" height="526.48" />
|
||||
<path id="logo-black"
|
||||
d="m404.14,313.34c-5.95-10.33-15.86-27.48-25.65-44.42-2.97-5.14-5.92-10.25-8.75-15.15-5.82-10.08-11.12-19.27-14.9-25.83-26.23-45.33-77.87-135.08-103.67-179.46-7.95-12.37-26.37-11.39-33.21,1.42-7.76,13.45-16.3,28.24-25.32,43.87-2.86,4.96-5.78,10.01-8.73,15.12-37.58,65.08-81.56,141.26-112.89,195.52-1.6,2.9-4.58,7.63-6.07,10.76-2.61,5.65-2.1,12.69,1.19,17.91,3.71,6.18,10.65,9.57,17.74,9.22,8.53,0,26.62-.01,50.01,0,5.55,0,11.4,0,17.49,0,81.33,0,205.57.05,236.06,0,14.81.03,24.1-16.21,16.72-28.97Zm-175.07-57.64l-14.97-25.93c-2.63-4.56.66-10.26,5.92-10.26h29.94c5.27,0,8.56,5.7,5.92,10.26l-14.97,25.93c-2.63,4.56-9.21,4.56-11.85,0Zm-25.08-48.29c-1.24-3.16-2.31-6.37-3.19-9.63l49.53,9.63h-46.34Zm22.62,68.22c-2.11,2.66-4.36,5.19-6.74,7.59l-16.43-47.71,23.16,40.12Zm47.78-53.7c3.35.5,6.67,1.19,9.93,2.05l-33.1,38.08,23.17-40.13Zm-76.64-40.4c-.53-4.82-.76-9.69-.74-14.57l65.41,31.91-64.68-17.33Zm-7.76,47.77l17.32,64.65c-3.91,2.87-8.01,5.51-12.25,7.93l-5.08-72.58Zm109.93.17c4.44,1.95,8.77,4.19,12.99,6.65l-60.34,40.7,47.35-47.35Zm-101.41-83.09c1.2-8.86,3.05-17.59,5.29-25.96l99.37,86.38-104.65-60.42Zm-22.02,164.51c-8.27,3.39-16.75,6.15-25.12,8.39l25.12-129.23v120.84Zm153.49-63.19c7.07,5.47,13.71,11.44,19.84,17.56l-124.49,42.86,104.65-60.42Zm-90.64-175.85c18.32,31.79,44.92,77.89,70.26,121.77l-94.61-94.61c5.56-9.62,10.83-18.75,15.69-27.18,1.93-3.33,6.73-3.33,8.66,0Zm-147.77,240.92c5.21-8.99,12.37-21.32,13.96-24.16,15.08-26.12,35.39-61.29,56.37-97.63l-34.65,129.3c-12.41,0-23.11,0-31.35,0-3.85,0-6.26-4.17-4.33-7.5Zm282.54,7.53c-28.89,0-84.98,0-140.67,0l129.31-34.65c6.78,11.74,12.21,21.14,15.69,27.16,1.93,3.33-.48,7.49-4.32,7.49Z" />
|
||||
<path
|
||||
d="m115.94,486.75c-2.9,0-5.39-1.98-6.05-4.81l-4.72-20.29c-.23-1.01-1.12-1.71-2.16-1.71h-20.5c-1.04,0-1.92.7-2.16,1.71l-4.72,20.29c-.66,2.83-3.15,4.81-6.05,4.81h-2.26c-1.92,0-3.71-.87-4.89-2.39-1.19-1.52-1.6-3.46-1.14-5.32l22.17-89.41c.69-2.78,3.17-4.72,6.03-4.72h6.54c2.86,0,5.34,1.94,6.03,4.72l22.17,89.41c.46,1.87.05,3.81-1.14,5.33-1.19,1.52-2.97,2.38-4.89,2.38h-2.26Zm-23.18-82.26s-.08.01-.08.02l-8.17,39.3c-.16.71,0,1.39.41,1.93.42.53,1.05.84,1.73.84h12.21c.68,0,1.31-.31,1.73-.84.42-.53.58-1.22.42-1.88l-8.17-39.32-.09-.04Z" />
|
||||
<path
|
||||
d="m269.14,486.8c-3.43,0-6.21-2.79-6.21-6.21v-33.53c0-3.31-2.7-6.01-6.01-6.01h-47.88c-1.22,0-2.21.99-2.21,2.21v37.27c0,3.43-2.79,6.21-6.21,6.21h-2.56c-3.43,0-6.21-2.79-6.21-6.21v-39.49h-43.04c-1.22,0-2.21.99-2.21,2.21v37.27c0,3.43-2.79,6.21-6.21,6.21h-2.56c-3.43,0-6.21-2.79-6.21-6.21v-77.61c0-9.93,8.08-18.02,18.02-18.02h28.02c3.43,0,6.21,2.79,6.21,6.21v.95c0,3.43-2.79,6.21-6.21,6.21h-25.04c-3.31,0-6.01,2.7-6.01,6.01v21.15c0,1.22.99,2.21,2.21,2.21h43.04v-24.74c0-9.93,8.08-18.02,18.02-18.02h28.02c3.43,0,6.21,2.79,6.21,6.21v.95c0,3.43-2.79,6.21-6.21,6.21h-25.04c-3.31,0-6.01,2.7-6.01,6.01v21.15c0,1.22.99,2.21,2.21,2.21h50.59c9.93,0,18.02,8.08,18.02,18.02v34.9c0,3.43-2.79,6.21-6.21,6.21h-2.29Z" />
|
||||
<path
|
||||
d="m332.61,485.75c-2.32,0-4.39-1.56-5.02-3.8l-19.11-67.64-1.96.27-.21,65.97c0,2.87-2.35,5.2-5.21,5.2h-2.31c-2.88,0-5.21-2.34-5.21-5.21v-89.41c0-2.88,2.34-5.21,5.21-5.21h10.02c2.32,0,4.39,1.56,5.02,3.8l19.11,67.64,1.96-.27.21-65.97c0-2.87,2.35-5.2,5.21-5.2h2.31c2.88,0,5.21,2.34,5.21,5.21v89.41c0,2.88-2.34,5.21-5.21,5.21h-10.02Z" />
|
||||
<path
|
||||
d="m376.06,486.75c-9.93,0-18.02-8.08-18.02-18.02v-65.81c0-9.93,8.08-18.02,18.02-18.02h26.68c3.43,0,6.21,2.79,6.21,6.21v.95c0,3.43-2.79,6.21-6.21,6.21h-23.83c-3.31,0-6.01,2.7-6.01,6.01v19.81c0,1.22.99,2.21,2.21,2.21h26.28c3.43,0,6.21,2.79,6.21,6.21v.95c0,3.43-2.79,6.21-6.21,6.21h-26.28c-1.22,0-2.21.99-2.21,2.21v25.44c0,3.31,2.7,6.01,6.01,6.01h23.83c3.43,0,6.21,2.79,6.21,6.21v.95c0,3.43-2.79,6.21-6.21,6.21h-26.68Z" />
|
||||
<path
|
||||
d="m263.69,418.27c-1.67,0-3.28-.8-4.3-2.14-1.03-1.35-1.37-3.06-.93-4.71l3.1-11.56c.64-2.37,2.8-4.03,5.27-4.03,1.44,0,2.8.57,3.83,1.59l8.46,8.46c1.39,1.39,1.92,3.35,1.41,5.25-.51,1.9-1.95,3.34-3.84,3.84l-11.56,3.1c-.47.13-.95.19-1.42.19h0Z" />
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
304
packages/frontend/admin/src/modules/home/index.tsx
Normal file
304
packages/frontend/admin/src/modules/home/index.tsx
Normal file
@@ -0,0 +1,304 @@
|
||||
import {
|
||||
Avatar,
|
||||
AvatarFallback,
|
||||
AvatarImage,
|
||||
} from '@affine/admin/components/ui/avatar';
|
||||
import { Badge } from '@affine/admin/components/ui/badge';
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@affine/admin/components/ui/card';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@affine/admin/components/ui/table';
|
||||
import {
|
||||
Activity,
|
||||
ArrowUpRight,
|
||||
CreditCard,
|
||||
DollarSign,
|
||||
Users,
|
||||
} from 'lucide-react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { Nav } from '../nav';
|
||||
|
||||
export function Dashboard() {
|
||||
return (
|
||||
<Nav>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
|
||||
<MainDashBoard />
|
||||
</main>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
|
||||
function MainDashBoard() {
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-4 md:grid-cols-2 md:gap-8 lg:grid-cols-4">
|
||||
<Card x-chunk="dashboard-01-chunk-0">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
|
||||
<DollarSign className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">$45,231.89</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+20.1% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Subscriptions</CardTitle>
|
||||
<Users className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+2350</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+180.1% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-2">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Sales</CardTitle>
|
||||
<CreditCard className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+12,234</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+19% from last month
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-3">
|
||||
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||
<CardTitle className="text-sm font-medium">Active Now</CardTitle>
|
||||
<Activity className="h-4 w-4 text-muted-foreground" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold">+573</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
+201 since last hour
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
<div className="grid gap-4 md:gap-8 lg:grid-cols-2 xl:grid-cols-3">
|
||||
<Card className="xl:col-span-2" x-chunk="dashboard-01-chunk-4">
|
||||
<CardHeader className="flex flex-row items-center">
|
||||
<div className="grid gap-2">
|
||||
<CardTitle>Transactions</CardTitle>
|
||||
<CardDescription>
|
||||
Recent transactions from your store.
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Button asChild size="sm" className="ml-auto gap-1">
|
||||
<Link to="/">
|
||||
View All
|
||||
<ArrowUpRight className="h-4 w-4" />
|
||||
</Link>
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Customer</TableHead>
|
||||
<TableHead className="hidden xl:table-column">Type</TableHead>
|
||||
<TableHead className="hidden xl:table-column">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="hidden xl:table-column">Date</TableHead>
|
||||
<TableHead className="text-right">Amount</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">Sale</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-23
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$250.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Olivia Smith</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
olivia@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Refund
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Declined
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-24
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$150.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Noah Williams</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
noah@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
Subscription
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-25
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$350.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Emma Brown</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
emma@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">Sale</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-26
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$450.00</TableCell>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableCell>
|
||||
<div className="font-medium">Liam Johnson</div>
|
||||
<div className="hidden text-sm text-muted-foreground md:inline">
|
||||
liam@example.com
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="hidden xl:table-column">Sale</TableCell>
|
||||
<TableCell className="hidden xl:table-column">
|
||||
<Badge className="text-xs" variant="outline">
|
||||
Approved
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="hidden md:table-cell lg:hidden xl:table-column">
|
||||
2023-06-27
|
||||
</TableCell>
|
||||
<TableCell className="text-right">$550.00</TableCell>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<Card x-chunk="dashboard-01-chunk-5">
|
||||
<CardHeader>
|
||||
<CardTitle>Recent Sales</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="grid gap-8">
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/01.png" alt="Avatar" />
|
||||
<AvatarFallback>OM</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
Olivia Martin
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
olivia.martin@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$1,999.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/02.png" alt="Avatar" />
|
||||
<AvatarFallback>JL</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">Jackson Lee</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
jackson.lee@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$39.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/03.png" alt="Avatar" />
|
||||
<AvatarFallback>IN</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">
|
||||
Isabella Nguyen
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
isabella.nguyen@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$299.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/04.png" alt="Avatar" />
|
||||
<AvatarFallback>WK</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">William Kim</p>
|
||||
<p className="text-sm text-muted-foreground">will@email.com</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$99.00</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<Avatar className="hidden h-9 w-9 sm:flex">
|
||||
<AvatarImage src="/avatars/05.png" alt="Avatar" />
|
||||
<AvatarFallback>SD</AvatarFallback>
|
||||
</Avatar>
|
||||
<div className="grid gap-1">
|
||||
<p className="text-sm font-medium leading-none">Sofia Davis</p>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
sofia.davis@email.com
|
||||
</p>
|
||||
</div>
|
||||
<div className="ml-auto font-medium">+$39.00</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export { Dashboard as Component };
|
||||
113
packages/frontend/admin/src/modules/nav/index.tsx
Normal file
113
packages/frontend/admin/src/modules/nav/index.tsx
Normal file
@@ -0,0 +1,113 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetTrigger,
|
||||
} from '@affine/admin/components/ui/sheet';
|
||||
import { Menu, Package2 } from 'lucide-react';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
import { UserDropdown } from './user-dropdown';
|
||||
|
||||
export function Nav({ children }: PropsWithChildren<unknown>) {
|
||||
return (
|
||||
<div className="flex min-h-screen w-full flex-col">
|
||||
<header className="sticky top-0 flex h-16 items-center gap-4 border-b bg-background px-4 md:px-6">
|
||||
<nav className="hidden flex-col gap-6 text-lg font-medium md:flex md:flex-row md:items-center md:gap-5 md:text-sm lg:gap-6">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold md:text-base"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">AFFiNE</span>
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/admin/users"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Users
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Configs
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Backups
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground transition-colors hover:text-foreground"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
<Sheet>
|
||||
<SheetTrigger asChild>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="shrink-0 md:hidden"
|
||||
>
|
||||
<Menu className="h-5 w-5" />
|
||||
<span className="sr-only">Toggle navigation menu</span>
|
||||
</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent side="left">
|
||||
<nav className="grid gap-6 text-lg font-medium">
|
||||
<Link
|
||||
to="/"
|
||||
className="flex items-center gap-2 text-lg font-semibold"
|
||||
>
|
||||
<Package2 className="h-6 w-6" />
|
||||
<span className="sr-only">Acme Inc</span>
|
||||
</Link>
|
||||
<Link to="/" className="hover:text-foreground">
|
||||
Dashboard
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Orders
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Products
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Customers
|
||||
</Link>
|
||||
<Link
|
||||
to="/"
|
||||
className="text-muted-foreground hover:text-foreground"
|
||||
>
|
||||
Analytics
|
||||
</Link>
|
||||
</nav>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
<div className="flex w-full items-center justify-end gap-4 md:ml-auto md:gap-2 lg:gap-4">
|
||||
<UserDropdown />
|
||||
</div>
|
||||
</header>
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
59
packages/frontend/admin/src/modules/nav/user-dropdown.tsx
Normal file
59
packages/frontend/admin/src/modules/nav/user-dropdown.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@affine/admin/components/ui/dropdown-menu';
|
||||
import { useQuery } from '@affine/core/hooks/use-query';
|
||||
import { FeatureType, getCurrentUserFeaturesQuery } from '@affine/graphql';
|
||||
import { CircleUser } from 'lucide-react';
|
||||
import { useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
export function UserDropdown() {
|
||||
const {
|
||||
data: { currentUser },
|
||||
} = useQuery({
|
||||
query: getCurrentUserFeaturesQuery,
|
||||
});
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentUser) {
|
||||
navigate('/admin/auth');
|
||||
return;
|
||||
}
|
||||
if (!currentUser?.features.includes?.(FeatureType.Admin)) {
|
||||
toast.error('You are not an admin, please login the admin account.');
|
||||
navigate('/admin/auth');
|
||||
return;
|
||||
}
|
||||
}, [currentUser, navigate]);
|
||||
const avatar = currentUser?.avatarUrl ? (
|
||||
<img src={currentUser?.avatarUrl} />
|
||||
) : (
|
||||
<CircleUser size={24} />
|
||||
);
|
||||
|
||||
return (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="secondary" size="icon" className="rounded-full">
|
||||
{avatar}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>{currentUser?.name}</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Settings</DropdownMenuItem>
|
||||
<DropdownMenuItem>Support</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem>Logout</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
);
|
||||
}
|
||||
194
packages/frontend/admin/src/modules/users/index.tsx
Normal file
194
packages/frontend/admin/src/modules/users/index.tsx
Normal file
@@ -0,0 +1,194 @@
|
||||
import { Badge } from '@affine/admin/components/ui/badge';
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from '@affine/admin/components/ui/card';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuCheckboxItem,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from '@affine/admin/components/ui/dropdown-menu';
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@affine/admin/components/ui/table';
|
||||
import {
|
||||
Tabs,
|
||||
TabsContent,
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from '@affine/admin/components/ui/tabs';
|
||||
import { useQuery } from '@affine/core/hooks/use-query';
|
||||
import { listUsersQuery } from '@affine/graphql';
|
||||
import {
|
||||
CircleUser,
|
||||
File,
|
||||
ListFilter,
|
||||
MoreHorizontal,
|
||||
PlusCircle,
|
||||
} from 'lucide-react';
|
||||
|
||||
import { Nav } from '../nav';
|
||||
|
||||
export function Users() {
|
||||
const {
|
||||
data: { users },
|
||||
} = useQuery({
|
||||
query: listUsersQuery,
|
||||
variables: {
|
||||
filter: {
|
||||
first: 10,
|
||||
skip: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const usersCells = users.map(user => {
|
||||
const avatar = user.avatarUrl ? (
|
||||
<img
|
||||
alt="User avatar"
|
||||
className="aspect-square rounded-md object-cover"
|
||||
height="64"
|
||||
src={user.avatarUrl}
|
||||
width="64"
|
||||
/>
|
||||
) : (
|
||||
<CircleUser size={36} />
|
||||
);
|
||||
return (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="hidden sm:table-cell">{avatar}</TableCell>
|
||||
<TableCell className="hidden md:table-cell">{user.id}</TableCell>
|
||||
<TableCell className="font-medium">{user.name}</TableCell>
|
||||
<TableCell>
|
||||
<Badge variant="outline">
|
||||
{user.emailVerified ? 'Email Verified' : 'Not yet verified'}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell className="hidden md:table-cell">
|
||||
{user.hasPassword ? '✅' : '❌'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button aria-haspopup="true" size="icon" variant="ghost">
|
||||
<MoreHorizontal className="h-4 w-4" />
|
||||
<span className="sr-only">Toggle menu</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||
<DropdownMenuItem>Edit</DropdownMenuItem>
|
||||
<DropdownMenuItem>Delete</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<Nav>
|
||||
<main className="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
|
||||
<Tabs defaultValue="all">
|
||||
<div className="flex items-center">
|
||||
<TabsList>
|
||||
<TabsTrigger value="all">All</TabsTrigger>
|
||||
<TabsTrigger value="active">Active</TabsTrigger>
|
||||
<TabsTrigger value="draft">Draft</TabsTrigger>
|
||||
<TabsTrigger value="archived" className="hidden sm:flex">
|
||||
Archived
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="h-7 gap-1">
|
||||
<ListFilter className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
||||
Filter
|
||||
</span>
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuLabel>Filter by</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuCheckboxItem checked>
|
||||
Active
|
||||
</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>Draft</DropdownMenuCheckboxItem>
|
||||
<DropdownMenuCheckboxItem>Archived</DropdownMenuCheckboxItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button size="sm" variant="outline" className="h-7 gap-1">
|
||||
<File className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
||||
Export
|
||||
</span>
|
||||
</Button>
|
||||
<Button size="sm" className="h-7 gap-1">
|
||||
<PlusCircle className="h-3.5 w-3.5" />
|
||||
<span className="sr-only sm:not-sr-only sm:whitespace-nowrap">
|
||||
Add User
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<TabsContent value="all">
|
||||
<Card x-chunk="dashboard-06-chunk-0">
|
||||
<CardHeader>
|
||||
<CardTitle>Users</CardTitle>
|
||||
<CardDescription>
|
||||
Manage your users and edit their properties.
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead className="hidden w-[100px] sm:table-cell">
|
||||
<span className="sr-only">Image</span>
|
||||
</TableHead>
|
||||
<TableHead className="hidden md:table-cell">Id</TableHead>
|
||||
<TableHead>Name</TableHead>
|
||||
<TableHead>Status</TableHead>
|
||||
<TableHead>Email</TableHead>
|
||||
<TableHead className="hidden md:table-cell">
|
||||
Has password
|
||||
</TableHead>
|
||||
<TableHead>
|
||||
<span className="sr-only">Actions</span>
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>{usersCells}</TableBody>
|
||||
</Table>
|
||||
</CardContent>
|
||||
<CardFooter>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
Showing <strong>1-10</strong> of <strong>32</strong> products
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</main>
|
||||
</Nav>
|
||||
);
|
||||
}
|
||||
|
||||
export { Users as Component };
|
||||
Reference in New Issue
Block a user