team selector

This commit is contained in:
2025-04-01 19:51:30 +08:00
parent 1b901bda90
commit 0a881fd180
2 changed files with 155 additions and 49 deletions

View File

@@ -13,8 +13,8 @@ export default function AnalyticsPage() {
to: today // 今天 to: today // 今天
}); });
// 添加团队选择状态 // 添加团队选择状态 - 使用数组支持多选
const [selectedTeamId, setSelectedTeamId] = useState<string>(); const [selectedTeamIds, setSelectedTeamIds] = useState<string[]>([]);
return ( return (
<div className="container mx-auto px-4 py-8"> <div className="container mx-auto px-4 py-8">
@@ -23,11 +23,10 @@ export default function AnalyticsPage() {
<div className="flex flex-col gap-4 md:flex-row md:items-center"> <div className="flex flex-col gap-4 md:flex-row md:items-center">
<TeamSelector <TeamSelector
value={selectedTeamId} value={selectedTeamIds}
onChange={setSelectedTeamId} onChange={(value) => setSelectedTeamIds(Array.isArray(value) ? value : [value])}
className="w-[200px]" className="w-[250px]"
variant="surface" multiple={true}
color="blue"
/> />
<DateRangePicker <DateRangePicker
value={dateRange} value={dateRange}
@@ -37,18 +36,31 @@ export default function AnalyticsPage() {
</div> </div>
{/* 如果没有选择团队,显示提示信息 */} {/* 如果没有选择团队,显示提示信息 */}
{!selectedTeamId && ( {selectedTeamIds.length === 0 && (
<div className="flex items-center justify-center p-8 bg-gray-50 rounded-lg"> <div className="flex items-center justify-center p-8 bg-gray-50 rounded-lg">
<p className="text-gray-500"> <p className="text-gray-500">
Please select a team to view analytics Please select one or more teams to view analytics
</p> </p>
</div> </div>
)} )}
{/* 如果选择了团队,这里可以显示团队相关的分析数据 */} {/* 如果选择了团队,这里可以显示团队相关的分析数据 */}
{selectedTeamId && ( {selectedTeamIds.length > 0 && (
<div className="space-y-6"> <div className="space-y-6">
{/* 这里添加实际的分析数据组件 */} <div className="bg-white rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">
Analytics for {selectedTeamIds.length} selected {selectedTeamIds.length === 1 ? 'team' : 'teams'}
</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{/* You can map through selectedTeamIds and display data for each team */}
{selectedTeamIds.map((teamId) => (
<div key={teamId} className="p-4 border rounded-md">
<h3 className="font-medium text-gray-800">Team ID: {teamId}</h3>
<p className="text-gray-500 mt-2">Team analytics will appear here</p>
</div>
))}
</div>
</div>
</div> </div>
)} )}
</div> </div>

View File

@@ -1,41 +1,63 @@
"use client"; "use client";
import * as React from 'react'; import * as React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState, useRef } from 'react';
import type { Database } from '@/types/supabase'; import type { Database } from '@/types/supabase';
import { getSupabaseClient } from '../../utils/supabase'; import { getSupabaseClient } from '../../utils/supabase';
import { AuthChangeEvent, Session } from '@supabase/supabase-js'; import { AuthChangeEvent, Session } from '@supabase/supabase-js';
import { Select } from '@radix-ui/themes'; import { Loader2, X, Check } from 'lucide-react';
import { Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
type Team = Database['limq']['Tables']['teams']['Row']; type Team = Database['limq']['Tables']['teams']['Row'];
type AccentColor =
| 'ruby' | 'gray' | 'gold' | 'bronze' | 'brown' | 'yellow' | 'amber' | 'orange'
| 'tomato' | 'red' | 'crimson' | 'pink' | 'plum' | 'purple' | 'violet'
| 'iris' | 'indigo' | 'blue' | 'cyan' | 'teal' | 'jade' | 'green' | 'grass' | 'lime';
// TeamSelector component using Radix Themes // TeamSelector component with multi-select support
export function TeamSelector({ export function TeamSelector({
value, value,
onChange, onChange,
className, className,
size = "2", multiple = false,
variant = "surface",
color,
radius,
}: { }: {
value?: string; value?: string | string[];
onChange?: (teamId: string) => void; onChange?: (teamId: string | string[]) => void;
className?: string; className?: string;
size?: "1" | "2" | "3"; multiple?: boolean;
variant?: "classic" | "surface" | "soft" | "ghost";
color?: AccentColor;
radius?: "none" | "small" | "medium" | "large" | "full";
}) { }) {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const [teams, setTeams] = useState<Team[]>([]); const [teams, setTeams] = useState<Team[]>([]);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [isOpen, setIsOpen] = useState(false);
const selectorRef = useRef<HTMLDivElement>(null);
// Initialize selected teams based on value prop
useEffect(() => {
if (value) {
if (Array.isArray(value)) {
setSelectedIds(value);
} else {
setSelectedIds(value ? [value] : []);
}
} else {
setSelectedIds([]);
}
}, [value]);
// Add click outside listener to close dropdown
useEffect(() => {
function handleClickOutside(event: MouseEvent) {
if (selectorRef.current && !selectorRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
}
// Only add the event listener if the dropdown is open
if (isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}
}, [isOpen]);
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
@@ -106,6 +128,44 @@ export function TeamSelector({
}; };
}, []); }, []);
const handleToggle = () => {
if (!loading && !error && teams.length > 0) {
setIsOpen(!isOpen);
}
};
const handleTeamSelect = (teamId: string) => {
let newSelected: string[];
if (multiple) {
// For multi-select: toggle team in/out of selection
if (selectedIds.includes(teamId)) {
newSelected = selectedIds.filter(id => id !== teamId);
} else {
newSelected = [...selectedIds, teamId];
}
} else {
// For single-select: replace selection with the new team
newSelected = [teamId];
setIsOpen(false);
}
setSelectedIds(newSelected);
if (onChange) {
onChange(multiple ? newSelected : newSelected[0] || '');
}
};
const removeTeam = (e: React.MouseEvent, teamId: string) => {
e.stopPropagation();
const newSelected = selectedIds.filter(id => id !== teamId);
setSelectedIds(newSelected);
if (onChange) {
onChange(multiple ? newSelected : newSelected[0] || '');
}
};
if (loading) { if (loading) {
return ( return (
<div className={cn( <div className={cn(
@@ -139,26 +199,60 @@ export function TeamSelector({
); );
} }
const selectedTeams = teams.filter(team => selectedIds.includes(team.id));
return ( return (
<Select.Root <div className="relative" ref={selectorRef}>
value={value} <div
onValueChange={onChange} className={cn(
size={size} "flex w-full min-h-10 items-center flex-wrap rounded-md border p-1 cursor-pointer",
> isOpen && "ring-2 ring-offset-2 ring-blue-500",
<Select.Trigger className
className={className} )}
variant={variant} onClick={handleToggle}
color={color} >
radius={radius} {selectedTeams.length > 0 ? (
placeholder="Select a team" <div className="flex flex-wrap gap-1">
/> {selectedTeams.map(team => (
<Select.Content position="popper"> <div
{teams.map((team) => ( key={team.id}
<Select.Item key={team.id} value={team.id}> className="flex items-center gap-1 bg-blue-100 text-blue-800 rounded-md px-2 py-1 text-sm"
{team.name} >
</Select.Item> {team.name}
))} {multiple && (
</Select.Content> <X
</Select.Root> size={14}
className="cursor-pointer hover:text-blue-900"
onClick={(e) => removeTeam(e, team.id)}
/>
)}
</div>
))}
</div>
) : (
<div className="px-2 py-1 text-gray-500">Select a team</div>
)}
</div>
{isOpen && (
<div className="absolute w-full mt-1 max-h-60 overflow-auto rounded-md border bg-white shadow-lg z-10">
{teams.map(team => (
<div
key={team.id}
className={cn(
"px-3 py-2 cursor-pointer hover:bg-gray-100 flex items-center justify-between",
selectedIds.includes(team.id) && "bg-blue-50"
)}
onClick={() => handleTeamSelect(team.id)}
>
<span>{team.name}</span>
{selectedIds.includes(team.id) && (
<Check className="h-4 w-4 text-blue-600" />
)}
</div>
))}
</div>
)}
</div>
); );
} }