team selector
This commit is contained in:
@@ -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}
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
997
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user