feat(admin): init project (#7197)

This commit is contained in:
Brooooooklyn
2024-06-18 06:01:13 +00:00
parent d216606193
commit 0fe672efa5
92 changed files with 6392 additions and 175 deletions

View 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 };

View 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

View 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 };

View 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>
);
}

View 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>
);
}

View 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 };