team selector

This commit is contained in:
2025-04-01 19:04:13 +08:00
parent 1978e0224e
commit 53822f1087
5 changed files with 1081 additions and 53 deletions

View File

@@ -26,6 +26,8 @@ export default function AnalyticsPage() {
value={selectedTeamId} value={selectedTeamId}
onChange={setSelectedTeamId} onChange={setSelectedTeamId}
className="w-[200px]" className="w-[200px]"
variant="surface"
color="blue"
/> />
<DateRangePicker <DateRangePicker
value={dateRange} value={dateRange}

View File

@@ -1,80 +1,78 @@
"use client"; "use client";
import * as React from 'react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import type { Database } from '@/types/supabase'; import type { Database } from '@/types/supabase';
import { Select } from './Select';
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 } from 'lucide-react';
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';
interface TeamSelectorProps { // TeamSelector component using Radix Themes
export function TeamSelector({
value,
onChange,
className,
size = "2",
variant = "surface",
color,
radius,
}: {
value?: string; value?: string;
onChange?: (teamId: string) => void; onChange?: (teamId: string) => void;
className?: string; className?: string;
} size?: "1" | "2" | "3";
variant?: "classic" | "surface" | "soft" | "ghost";
export function TeamSelector({ value, onChange, className }: TeamSelectorProps) { color?: AccentColor;
const [teams, setTeams] = useState<Team[]>([]); 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[]>([]);
useEffect(() => { useEffect(() => {
let isMounted = true; let isMounted = true;
const fetchTeams = async (userId: string) => { const fetchTeams = async (userId: string) => {
if (!isMounted) return; if (!isMounted) return;
console.log('Starting to fetch teams for user:', userId);
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const supabase = getSupabaseClient(); const supabase = getSupabaseClient();
// 获取用户的团队成员关系
console.log('Fetching team memberships for user:', userId);
const { data: memberships, error: membershipError } = await supabase const { data: memberships, error: membershipError } = await supabase
.from('team_membership') .from('team_membership')
.select('team_id') .select('team_id')
.eq('user_id', userId); .eq('user_id', userId);
console.log('Team memberships result:', { memberships, membershipError }); if (membershipError) throw membershipError;
if (membershipError) {
console.error('Membership error:', membershipError);
throw membershipError;
}
if (!memberships || memberships.length === 0) { if (!memberships || memberships.length === 0) {
console.log('No team memberships found'); if (isMounted) setTeams([]);
if (isMounted) {
setTeams([]);
}
return; return;
} }
// 获取团队详细信息
const teamIds = memberships.map(m => m.team_id); const teamIds = memberships.map(m => m.team_id);
console.log('Fetching teams with IDs:', teamIds);
const { data: teamsData, error: teamsError } = await supabase const { data: teamsData, error: teamsError } = await supabase
.from('teams') .from('teams')
.select('*') .select('*')
.in('id', teamIds) .in('id', teamIds)
.is('deleted_at', null); .is('deleted_at', null);
console.log('Teams result:', { teamsData, teamsError }); if (teamsError) throw teamsError;
if (teamsError) {
console.error('Teams error:', teamsError);
throw teamsError;
}
if (isMounted && teamsData) { if (isMounted && teamsData) {
setTeams(teamsData); setTeams(teamsData);
} }
} catch (err) { } catch (err) {
console.error('Error fetching teams:', err);
if (isMounted) { if (isMounted) {
setError(err instanceof Error ? err.message : 'Failed to load teams'); setError(err instanceof Error ? err.message : 'Failed to load teams');
} }
@@ -86,10 +84,8 @@ export function TeamSelector({ value, onChange, className }: TeamSelectorProps)
}; };
const supabase = getSupabaseClient(); const supabase = getSupabaseClient();
// 设置认证状态监听器
const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => { const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent, session: Session | null) => {
console.log('Auth state changed:', event, session?.user);
if (event === 'SIGNED_IN' && session?.user?.id) { if (event === 'SIGNED_IN' && session?.user?.id) {
fetchTeams(session.user.id); fetchTeams(session.user.id);
} else if (event === 'SIGNED_OUT') { } else if (event === 'SIGNED_OUT') {
@@ -98,9 +94,7 @@ export function TeamSelector({ value, onChange, className }: TeamSelectorProps)
} }
}); });
// 初始检查session supabase.auth.getSession().then(({ data: { session } }) => {
supabase.auth.getSession().then(({ data: { session } }: { data: { session: Session | null } }) => {
console.log('Initial session check:', session?.user);
if (session?.user?.id) { if (session?.user?.id) {
fetchTeams(session.user.id); fetchTeams(session.user.id);
} }
@@ -113,28 +107,58 @@ export function TeamSelector({ value, onChange, className }: TeamSelectorProps)
}, []); }, []);
if (loading) { if (loading) {
return <div className="animate-pulse h-10 bg-gray-200 rounded-md dark:bg-gray-700" />; return (
<div className={cn(
"flex w-full items-center justify-between rounded-md border px-3 py-2",
className
)}>
<Loader2 className="h-4 w-4 animate-spin" />
</div>
);
} }
if (error) { if (error) {
return <div className="text-red-500 text-sm">{error}</div>; return (
<div className={cn(
"flex w-full items-center rounded-md border border-destructive bg-destructive/10 px-3 py-2 text-destructive",
className
)}>
{error}
</div>
);
} }
if (teams.length === 0) { if (teams.length === 0) {
return <div className="text-sm text-gray-500">No teams available</div>; return (
<div className={cn(
"flex w-full items-center rounded-md border px-3 py-2 text-muted-foreground",
className
)}>
No teams available
</div>
);
} }
return ( return (
<Select <Select.Root
className={className} value={value}
value={value} onValueChange={onChange}
onChange={(newValue: string) => onChange?.(newValue)} size={size}
options={teams.map(team => ({ >
value: team.id, <Select.Trigger
label: team.name, className={className}
icon: team.avatar_url || undefined variant={variant}
}))} color={color}
placeholder="Select a team" radius={radius}
/> placeholder="Select a team"
/>
<Select.Content position="popper">
{teams.map((team) => (
<Select.Item key={team.id} value={team.id}>
{team.name}
</Select.Item>
))}
</Select.Content>
</Select.Root>
); );
} }

View File

@@ -1,6 +1,8 @@
import './globals.css'; import './globals.css';
import '@radix-ui/themes/styles.css';
import type { Metadata } from 'next'; import type { Metadata } from 'next';
import { AuthProvider } from '@/lib/auth'; import { AuthProvider } from '@/lib/auth';
import { Theme } from '@radix-ui/themes';
export const metadata: Metadata = { export const metadata: Metadata = {
title: 'ShortURL Analytics', title: 'ShortURL Analytics',
@@ -15,9 +17,11 @@ export default function RootLayout({
return ( return (
<html lang="en" suppressHydrationWarning> <html lang="en" suppressHydrationWarning>
<body> <body>
<AuthProvider> <Theme>
{children} <AuthProvider>
</AuthProvider> {children}
</AuthProvider>
</Theme>
</body> </body>
</html> </html>
); );

View File

@@ -25,6 +25,7 @@
"dependencies": { "dependencies": {
"@clickhouse/client": "^1.11.0", "@clickhouse/client": "^1.11.0",
"@radix-ui/react-select": "^2.1.6", "@radix-ui/react-select": "^2.1.6",
"@radix-ui/themes": "^3.2.1",
"@supabase/auth-helpers-nextjs": "^0.10.0", "@supabase/auth-helpers-nextjs": "^0.10.0",
"@types/chart.js": "^2.9.41", "@types/chart.js": "^2.9.41",
"@types/recharts": "^1.8.29", "@types/recharts": "^1.8.29",

997
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff