feat: sync Sidebar and BottomNav, standardize user profile API

Align Sidebar & BottomNav menus, remove "Search", add user profile mock data, implement /api/users, add FilterDrawer, complete Section, ProfileHeader, MetricsRFM components

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-08-08 07:00:12 +00:00
parent 4eed69520c
commit f0a6a364f2
85 changed files with 3318 additions and 6786 deletions

View File

@@ -1,567 +1,239 @@
"use client"
import { useState } from "react"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { useEffect, useMemo, useState } from "react"
import Link from "next/link"
import MobileHeader from "@/app/components/MobileHeader"
import BottomNav from "@/app/components/BottomNav"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Card, CardContent } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Badge } from "@/components/ui/badge"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { Label } from "@/components/ui/label"
import { Textarea } from "@/components/ui/textarea"
import { Switch } from "@/components/ui/switch"
import { Users, UserPlus, Tags, Target, Activity, Plus, Edit, Search, Filter } from "lucide-react"
import { TooltipHelp } from "@/components/ui/tooltip-help"
import { TooltipProvider } from "@/components/ui/tooltip"
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog"
import { Checkbox } from "@/components/ui/checkbox"
import { Search, Filter, Plus } from 'lucide-react'
import FilterDrawer, { type FilterValues } from "@/components/user-portrait/filter-drawer"
type User = {
id: string
name: string
phone: string
email: string
tags: string[]
rfmScore: number
lastActivity: string
status: "活跃" | "沉睡" | "已封禁"
}
type UsersResponse = { success: true; data: { items: User[]; total: number; page: number; pageSize: number } }
export default function UserPortraitPage() {
const [selectedSegment, setSelectedSegment] = useState("all")
const [isAddingUser, setIsAddingUser] = useState(false)
const [isAddingTag, setIsAddingTag] = useState(false)
const [users, setUsers] = useState<User[]>([])
const [total, setTotal] = useState(0)
const [searchQuery, setSearchQuery] = useState("")
const [isAddingUser, setIsAddingUser] = useState(false)
const [newUser, setNewUser] = useState({ name: "", phone: "", email: "", tags: [] as string[] })
// 用户数据
const [users, setUsers] = useState([
{
id: "U001",
name: "张三",
phone: "13800138001",
email: "zhangsan@example.com",
tags: ["高价值", "活跃用户", "科技爱好者"],
value: 12580,
lastActive: "2小时前",
source: "微信",
registerDate: "2023-01-15",
},
{
id: "U002",
name: "李四",
phone: "13800138002",
email: "lisi@example.com",
tags: ["潜在客户", "新用户", "价格敏感"],
value: 3240,
lastActive: "1天前",
source: "抖音",
registerDate: "2024-01-10",
},
{
id: "U003",
name: "王五",
phone: "13800138003",
email: "wangwu@example.com",
tags: ["高价值", "忠实用户", "奢侈品偏好"],
value: 25600,
lastActive: "30分钟前",
source: "小红书",
registerDate: "2022-08-20",
},
])
const [filterOpen, setFilterOpen] = useState(false)
const [allTags, setAllTags] = useState<string[]>([])
const [filters, setFilters] = useState<FilterValues>({ tags: [], status: [], rfm: [0, 100] })
// 标签分类数据
const [tagCategories, setTagCategories] = useState([
{
id: "1",
name: "用户价值",
description: "基于用户消费能力和贡献度的分类",
tags: [
{ name: "高价值用户", count: 2847, color: "red" },
{ name: "中价值用户", count: 12456, color: "blue" },
{ name: "低价值用户", count: 8234, color: "gray" },
{ name: "潜在价值用户", count: 5287, color: "yellow" },
],
},
{
id: "2",
name: "活跃度",
description: "基于用户活跃程度和互动频率的分类",
tags: [
{ name: "超级活跃", count: 1234, color: "green" },
{ name: "活跃用户", count: 8429, color: "blue" },
{ name: "一般活跃", count: 15678, color: "yellow" },
{ name: "不活跃", count: 3456, color: "gray" },
],
},
{
id: "3",
name: "消费行为",
description: "基于用户购买习惯和消费偏好的分类",
tags: [
{ name: "冲动消费", count: 4567, color: "red" },
{ name: "理性消费", count: 12345, color: "blue" },
{ name: "价格敏感", count: 8901, color: "yellow" },
{ name: "品质优先", count: 6789, color: "purple" },
{ name: "奢侈品偏好", count: 2345, color: "pink" },
],
},
{
id: "4",
name: "兴趣偏好",
description: "基于用户兴趣爱好和内容偏好的分类",
tags: [
{ name: "科技爱好者", count: 5678, color: "blue" },
{ name: "时尚达人", count: 7890, color: "pink" },
{ name: "旅游爱好者", count: 4321, color: "green" },
{ name: "美食家", count: 6543, color: "orange" },
{ name: "运动健身", count: 3210, color: "red" },
{ name: "文艺青年", count: 2109, color: "purple" },
],
},
{
id: "5",
name: "生命周期",
description: "基于用户在平台的生命周期阶段分类",
tags: [
{ name: "新用户", count: 8765, color: "green" },
{ name: "成长期用户", count: 12345, color: "blue" },
{ name: "成熟期用户", count: 15678, color: "purple" },
{ name: "衰退期用户", count: 4321, color: "yellow" },
{ name: "流失用户", count: 2109, color: "gray" },
],
},
{
id: "6",
name: "地域分布",
description: "基于用户地理位置的分类",
tags: [
{ name: "一线城市", count: 18765, color: "red" },
{ name: "二线城市", count: 15432, color: "blue" },
{ name: "三线城市", count: 9876, color: "green" },
{ name: "四线及以下", count: 5432, color: "gray" },
{ name: "海外用户", count: 1234, color: "purple" },
],
},
{
id: "7",
name: "设备偏好",
description: "基于用户使用设备类型的分类",
tags: [
{ name: "iOS用户", count: 16789, color: "blue" },
{ name: "Android用户", count: 23456, color: "green" },
{ name: "PC用户", count: 8765, color: "gray" },
{ name: "平板用户", count: 4321, color: "purple" },
],
},
{
id: "8",
name: "社交属性",
description: "基于用户社交行为和影响力的分类",
tags: [
{ name: "意见领袖", count: 1234, color: "red" },
{ name: "活跃分享者", count: 5678, color: "blue" },
{ name: "默默关注者", count: 18765, color: "gray" },
{ name: "社交新手", count: 8901, color: "green" },
],
},
])
const queryString = useMemo(() => {
const p = new URLSearchParams()
if (searchQuery) p.set("q", searchQuery)
if (filters.tags.length) p.set("tags", filters.tags.join(","))
if (filters.status.length) p.set("status", filters.status.join(","))
p.set("rfmMin", String(filters.rfm[0]))
p.set("rfmMax", String(filters.rfm[1]))
p.set("page", "1")
p.set("pageSize", "50")
return p.toString()
}, [searchQuery, filters])
const getTagColor = (color: string) => {
const colors = {
red: "bg-red-100 text-red-800",
blue: "bg-blue-100 text-blue-800",
green: "bg-green-100 text-green-800",
yellow: "bg-yellow-100 text-yellow-800",
purple: "bg-purple-100 text-purple-800",
pink: "bg-pink-100 text-pink-800",
orange: "bg-orange-100 text-orange-800",
gray: "bg-gray-100 text-gray-800",
useEffect(() => {
fetch(`/api/users?${queryString}`)
.then((r) => r.json())
.then((res: UsersResponse) => {
if (res?.success) {
setUsers(res.data.items)
setTotal(res.data.total)
}
})
.catch(() => {})
}, [queryString])
useEffect(() => {
fetch("/api/users?meta=tags")
.then((r) => r.json())
.then((res: any) => setAllTags(res?.data?.tags ?? []))
.catch(() => {})
}, [])
const handleAddUser = async () => {
const resp = await fetch("/api/users", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(newUser),
})
const data = await resp.json()
if (data?.success) {
setIsAddingUser(false)
setNewUser({ name: "", phone: "", email: "", tags: [] })
// 触发刷新
fetch(`/api/users?${queryString}`)
.then((r) => r.json())
.then((res: UsersResponse) => {
if (res?.success) {
setUsers(res.data.items)
setTotal(res.data.total)
}
})
}
return colors[color as keyof typeof colors] || colors.gray
}
return (
<TooltipProvider>
<div className="container mx-auto p-6 space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold"></h1>
<p className="text-muted-foreground mt-2"></p>
<div className="min-h-screen bg-gradient-to-br from-slate-50 via-white to-purple-50">
<MobileHeader onMenuToggle={() => {}} title="用户画像" />
<main className="container mx-auto px-4 pb-24 space-y-4">
<div className="rounded-2xl bg-white/60 backdrop-blur-md p-4 shadow-sm border">
<div className="flex items-baseline justify-between">
<div>
<h1 className="text-2xl font-bold"></h1>
<p className="text-sm text-muted-foreground"></p>
</div>
<div className="text-sm text-muted-foreground"> {total} </div>
</div>
</div>
{/* 核心指标概览 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6">
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<TooltipHelp content="系统中所有已识别和分析的用户总数,包括活跃和非活跃用户" />
</CardTitle>
<Users className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">24,563</div>
<p className="text-xs text-muted-foreground">
<span className="text-green-600">+12.5%</span>
</p>
</CardContent>
</Card>
<div className="mt-4">
<Tabs defaultValue="users" className="w-full">
<TabsList className="grid grid-cols-2 w-full">
<TabsTrigger value="users" className="data-[state=active]:bg-white"></TabsTrigger>
<TabsTrigger value="tags" className="data-[state=active]:bg-white"></TabsTrigger>
</TabsList>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<TooltipHelp content="近30天内有过互动行为的用户数量包括点击、浏览、购买等行为" />
</CardTitle>
<Activity className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">18,429</div>
<p className="text-xs text-muted-foreground">
<span className="text-green-600">+8.2%</span>
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<TooltipHelp content="基于用户行为和特征自动创建的用户群组数量,用于精准营销" />
</CardTitle>
<Target className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">156</div>
<p className="text-xs text-muted-foreground">
<span className="text-blue-600">+23</span>
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium flex items-center gap-2">
<TooltipHelp content="已打标签用户占总用户的比例,反映用户画像的完整程度" />
</CardTitle>
<Tags className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">87.3%</div>
<p className="text-xs text-muted-foreground">
<span className="text-green-600">+2.1%</span>
</p>
</CardContent>
</Card>
</div>
<Tabs defaultValue="users" className="space-y-6">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="users"></TabsTrigger>
<TabsTrigger value="tags"></TabsTrigger>
</TabsList>
<TabsContent value="users" className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<TooltipHelp content="管理真实用户信息,支持添加、编辑和查看用户详细资料" />
</CardTitle>
<CardDescription></CardDescription>
</CardHeader>
<CardContent>
<div className="flex items-center gap-4 mb-6">
<div className="relative">
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索用户..."
className="pl-8 max-w-sm"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
<TabsContent value="users" className="space-y-4">
<div className="flex items-center gap-2 mt-3">
<div className="relative flex-1">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-gray-500" />
<Input className="pl-8" placeholder="搜索用户…" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} />
</div>
<Select value={selectedSegment} onValueChange={setSelectedSegment}>
<SelectTrigger className="w-48">
<SelectValue placeholder="选择用户分群" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="high-value"></SelectItem>
<SelectItem value="active"></SelectItem>
<SelectItem value="potential"></SelectItem>
</SelectContent>
</Select>
<Button variant="outline" size="sm">
<Filter className="h-4 w-4 mr-2" />
<Button variant="outline" onClick={() => setFilterOpen(true)}>
<Filter className="h-4 w-4 mr-1" />
</Button>
<Button onClick={() => setIsAddingUser(true)}>
<UserPlus className="h-4 w-4 mr-2" />
<Plus className="h-4 w-4 mr-1" />
</Button>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead>ID</TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{users.map((user) => (
<TableRow key={user.id}>
<TableCell className="font-medium">{user.id}</TableCell>
<TableCell>{user.name}</TableCell>
<TableCell>
<div className="text-sm">
<div>{user.phone}</div>
<div className="text-muted-foreground">{user.email}</div>
<div className="grid gap-3">
{users.map((u) => (
<Card key={u.id} className="border bg-white/70 backdrop-blur-md shadow-sm">
<CardContent className="p-4">
<div className="grid grid-cols-12 gap-3 items-center">
<div className="col-span-5">
<Link href={`/user-portrait/${u.id}`} className="font-medium hover:underline">
{u.name}
</Link>
<p className="text-xs text-muted-foreground mt-1">
{new Date(u.lastActivity).toLocaleDateString("zh-CN")}
</p>
</div>
</TableCell>
<TableCell>
<div className="flex flex-wrap gap-1">
{user.tags.slice(0, 2).map((tag, index) => (
<Badge key={index} variant="secondary" className="text-xs">
{tag}
</Badge>
))}
{user.tags.length > 2 && (
<Badge variant="outline" className="text-xs">
+{user.tags.length - 2}
</Badge>
)}
<div className="col-span-3">
<div className="text-sm">{u.phone}</div>
<div className="text-xs text-muted-foreground">{u.email}</div>
</div>
</TableCell>
<TableCell>¥{user.value.toLocaleString()}</TableCell>
<TableCell>{user.lastActive}</TableCell>
<TableCell>
<Badge variant="outline">{user.source}</Badge>
</TableCell>
<TableCell>
<div className="flex space-x-2">
<Button variant="ghost" size="sm">
<Edit className="h-4 w-4" />
</Button>
<Button variant="ghost" size="sm">
</Button>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="tags" className="space-y-6">
<Card>
<CardHeader>
<div className="flex justify-between items-center">
<div>
<CardTitle className="flex items-center gap-2">
<TooltipHelp content="管理用户标签体系,包括标签创建、分类、规则设置和自动化打标" />
</CardTitle>
<CardDescription></CardDescription>
</div>
<Button onClick={() => setIsAddingTag(true)}>
<Plus className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent>
<div className="space-y-6">
{tagCategories.map((category) => (
<Card key={category.id}>
<CardHeader className="pb-3">
<div className="flex justify-between items-center">
<div>
<CardTitle className="text-base">{category.name}</CardTitle>
<CardDescription>{category.description}</CardDescription>
</div>
<Badge variant="outline">{category.tags.length} </Badge>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
{category.tags.map((tag, index) => (
<div
key={index}
className="flex items-center justify-between p-3 border rounded-lg hover:bg-gray-50"
>
<div className="flex items-center gap-2">
<Badge className={getTagColor(tag.color)}>{tag.name}</Badge>
</div>
<div className="text-sm text-muted-foreground">{tag.count.toLocaleString()}</div>
<div className="col-span-2">
<div className="flex flex-wrap gap-1">
{u.tags.slice(0, 2).map((t) => (
<Badge key={t} variant="secondary" className="text-xs">{t}</Badge>
))}
{u.tags.length > 2 && (
<Badge variant="outline" className="text-xs">+{u.tags.length - 2}</Badge>
)}
</div>
))}
</div>
<div className="col-span-1 text-sm font-semibold">{u.rfmScore}</div>
<div className="col-span-1">
<span className={`text-xs px-2 py-1 rounded-full ${
u.status === "活跃" ? "bg-green-100 text-green-700" :
u.status === "沉睡" ? "bg-yellow-100 text-yellow-800" : "bg-red-100 text-red-700"
}`}>
{u.status}
</span>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</TabsContent>
{/* 添加用户对话框 */}
<Dialog open={isAddingUser} onOpenChange={setIsAddingUser}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="user-name" className="text-right">
</Label>
<Input id="user-name" className="col-span-3" placeholder="请输入真实姓名" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="user-phone" className="text-right">
</Label>
<Input id="user-phone" className="col-span-3" placeholder="请输入手机号码" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="user-email" className="text-right">
</Label>
<Input id="user-email" className="col-span-3" placeholder="请输入邮箱地址" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="user-source" className="text-right">
</Label>
<Select>
<SelectTrigger id="user-source" className="col-span-3">
<SelectValue placeholder="选择用户来源" />
</SelectTrigger>
<SelectContent>
<SelectItem value="wechat"></SelectItem>
<SelectItem value="douyin"></SelectItem>
<SelectItem value="xiaohongshu"></SelectItem>
<SelectItem value="website"></SelectItem>
<SelectItem value="offline">线</SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="user-tags" className="text-right pt-2">
</Label>
<Textarea id="user-tags" className="col-span-3" placeholder="请输入用户标签,用逗号分隔" rows={3} />
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="user-notes" className="text-right pt-2">
</Label>
<Textarea id="user-notes" className="col-span-3" placeholder="用户备注信息" rows={3} />
</div>
<TabsContent value="tags">
<div className="text-sm text-muted-foreground py-6 text-center"></div>
</TabsContent>
</Tabs>
</div>
</div>
</main>
<BottomNav />
{/* 筛选抽屉 */}
<FilterDrawer
open={filterOpen}
onOpenChange={setFilterOpen}
allTags={allTags}
value={filters}
onApply={(v) => setFilters(v)}
/>
{/* 添加用户 */}
<Dialog open={isAddingUser} onOpenChange={setIsAddingUser}>
<DialogContent>
<DialogHeader><DialogTitle></DialogTitle></DialogHeader>
<div className="space-y-3 py-2">
<div className="space-y-2">
<Label htmlFor="name"></Label>
<Input id="name" value={newUser.name} onChange={(e) => setNewUser((p) => ({ ...p, name: e.target.value }))} />
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingUser(false)}>
</Button>
<Button onClick={() => setIsAddingUser(false)}>
<UserPlus className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* 添加标签对话框 */}
<Dialog open={isAddingTag} onOpenChange={setIsAddingTag}>
<DialogContent className="sm:max-w-[500px]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription></DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-category" className="text-right">
</Label>
<Select>
<SelectTrigger id="tag-category" className="col-span-3">
<SelectValue placeholder="选择标签分类" />
</SelectTrigger>
<SelectContent>
{tagCategories.map((category) => (
<SelectItem key={category.id} value={category.id}>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-name" className="text-right">
</Label>
<Input id="tag-name" className="col-span-3" placeholder="请输入标签名称" />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="tag-color" className="text-right">
</Label>
<Select>
<SelectTrigger id="tag-color" className="col-span-3">
<SelectValue placeholder="选择标签颜色" />
</SelectTrigger>
<SelectContent>
<SelectItem value="red"></SelectItem>
<SelectItem value="blue"></SelectItem>
<SelectItem value="green">绿</SelectItem>
<SelectItem value="yellow"></SelectItem>
<SelectItem value="purple"></SelectItem>
<SelectItem value="pink"></SelectItem>
<SelectItem value="orange"></SelectItem>
<SelectItem value="gray"></SelectItem>
</SelectContent>
</Select>
</div>
<div className="grid grid-cols-4 items-start gap-4">
<Label htmlFor="tag-description" className="text-right pt-2">
</Label>
<Textarea id="tag-description" className="col-span-3" placeholder="标签描述信息" rows={3} />
</div>
<div className="grid grid-cols-4 items-center gap-4">
<div className="text-right">
<Label htmlFor="auto-tag"></Label>
</div>
<div className="flex items-center space-x-2 col-span-3">
<Switch id="auto-tag" />
<Label htmlFor="auto-tag"></Label>
<div className="space-y-2">
<Label htmlFor="phone"></Label>
<Input id="phone" value={newUser.phone} onChange={(e) => setNewUser((p) => ({ ...p, phone: e.target.value }))} />
</div>
<div className="space-y-2">
<Label htmlFor="email"></Label>
<Input id="email" type="email" value={newUser.email} onChange={(e) => setNewUser((p) => ({ ...p, email: e.target.value }))} />
</div>
{!!allTags.length && (
<div className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2 max-h-40 overflow-auto">
{allTags.map((t) => (
<label key={t} className="flex items-center gap-2 text-sm">
<Checkbox
checked={newUser.tags.includes(t)}
onCheckedChange={(ck) =>
setNewUser((p) => ({ ...p, tags: ck ? [...p.tags, t] : p.tags.filter((x) => x !== t) }))
}
/>
<span className="truncate">{t}</span>
</label>
))}
</div>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsAddingTag(false)}>
</Button>
<Button onClick={() => setIsAddingTag(false)}>
<Tags className="mr-2 h-4 w-4" />
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</TooltipProvider>
)}
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => setIsAddingUser(false)}></Button>
<Button onClick={handleAddUser}></Button>
</div>
</DialogContent>
</Dialog>
</div>
)
}