team selector
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user