diff --git a/app/(app)/AppLayoutClient.tsx b/app/(app)/AppLayoutClient.tsx
index 6686067..b0a665f 100644
--- a/app/(app)/AppLayoutClient.tsx
+++ b/app/(app)/AppLayoutClient.tsx
@@ -32,6 +32,12 @@ export default function AppLayoutClient({
>
Dashboard
+
+ Analytics
+
();
+
+ return (
+
+
+
+ {/* 如果没有选择团队,显示提示信息 */}
+ {!selectedTeamId && (
+
+
+ Please select a team to view analytics
+
+
+ )}
+
+ {/* 如果选择了团队,这里可以显示团队相关的分析数据 */}
+ {selectedTeamId && (
+
+ {/* 这里添加实际的分析数据组件 */}
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/api/teams/list/route.ts b/app/api/teams/list/route.ts
new file mode 100644
index 0000000..cb2d078
--- /dev/null
+++ b/app/api/teams/list/route.ts
@@ -0,0 +1,41 @@
+import { createRouteHandlerClient } from '@supabase/auth-helpers-nextjs';
+import { cookies } from 'next/headers';
+import { NextResponse } from 'next/server';
+
+export async function GET() {
+ try {
+ const supabase = createRouteHandlerClient({ cookies });
+
+ // 获取当前用户
+ const { data: { user }, error: userError } = await supabase.auth.getUser();
+ if (userError || !user) {
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
+ }
+
+ // 获取用户所属的所有团队
+ const { data: teams, error: teamsError } = await supabase
+ .from('teams')
+ .select(`
+ id,
+ name,
+ description,
+ avatar_url
+ `)
+ .innerJoin('team_membership', 'teams.id = team_membership.team_id')
+ .eq('team_membership.user_id', user.id)
+ .is('teams.deleted_at', null);
+
+ if (teamsError) {
+ console.error('Error fetching teams:', teamsError);
+ return NextResponse.json({ error: 'Failed to fetch teams' }, { status: 500 });
+ }
+
+ return NextResponse.json(teams);
+ } catch (error) {
+ console.error('Error in /api/teams/list:', error);
+ return NextResponse.json(
+ { error: 'Internal server error' },
+ { status: 500 }
+ );
+ }
+}
\ No newline at end of file
diff --git a/app/components/ui/Select.tsx b/app/components/ui/Select.tsx
new file mode 100644
index 0000000..7448aca
--- /dev/null
+++ b/app/components/ui/Select.tsx
@@ -0,0 +1,88 @@
+"use client";
+
+import * as React from 'react';
+import { ChevronDown } from 'lucide-react';
+
+interface SelectOption {
+ value: string;
+ label: string;
+ icon?: string;
+}
+
+interface SelectProps {
+ value?: string;
+ onChange?: (value: string) => void;
+ options: SelectOption[];
+ placeholder?: string;
+ className?: string;
+}
+
+export function Select({ value, onChange, options, placeholder, className = '' }: SelectProps) {
+ const [isOpen, setIsOpen] = React.useState(false);
+ const containerRef = React.useRef(null);
+
+ React.useEffect(() => {
+ function handleClickOutside(event: MouseEvent) {
+ if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
+ setIsOpen(false);
+ }
+ }
+
+ document.addEventListener('mousedown', handleClickOutside);
+ return () => {
+ document.removeEventListener('mousedown', handleClickOutside);
+ };
+ }, []);
+
+ const selectedOption = options.find(option => option.value === value);
+
+ return (
+
+
+
+ {isOpen && (
+
+
+ {options.map((option) => (
+
+ ))}
+
+
+ )}
+
+ );
+}
\ No newline at end of file
diff --git a/app/components/ui/TeamSelector.tsx b/app/components/ui/TeamSelector.tsx
new file mode 100644
index 0000000..dd031a4
--- /dev/null
+++ b/app/components/ui/TeamSelector.tsx
@@ -0,0 +1,137 @@
+"use client";
+
+import { useEffect, useState } from 'react';
+import { createClientComponentClient } from '@supabase/auth-helpers-nextjs';
+import type { Database } from '@/types/supabase';
+import { Select } from './Select';
+
+type Team = Database['public']['Tables']['teams']['Row'];
+
+interface TeamSelectorProps {
+ value?: string;
+ onChange?: (teamId: string) => void;
+ className?: string;
+}
+
+export function TeamSelector({ value, onChange, className }: TeamSelectorProps) {
+ const [teams, setTeams] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ const supabase = createClientComponentClient();
+
+ useEffect(() => {
+ let isMounted = true;
+
+ const fetchTeams = async (userId: string) => {
+ if (!isMounted) return;
+
+ console.log('Starting to fetch teams for user:', userId);
+ setLoading(true);
+ setError(null);
+
+ try {
+ // 获取用户的团队成员关系
+ console.log('Fetching team memberships for user:', userId);
+ const { data: memberships, error: membershipError } = await supabase
+ .from('team_membership')
+ .select('team_id')
+ .eq('user_id', userId);
+
+ console.log('Team memberships result:', { memberships, membershipError });
+
+ if (membershipError) {
+ console.error('Membership error:', membershipError);
+ throw membershipError;
+ }
+
+ if (!memberships || memberships.length === 0) {
+ console.log('No team memberships found');
+ if (isMounted) {
+ setTeams([]);
+ }
+ return;
+ }
+
+ // 获取团队详细信息
+ const teamIds = memberships.map(m => m.team_id);
+ console.log('Fetching teams with IDs:', teamIds);
+ const { data: teamsData, error: teamsError } = await supabase
+ .from('teams')
+ .select('*')
+ .in('id', teamIds)
+ .is('deleted_at', null);
+
+ console.log('Teams result:', { teamsData, teamsError });
+
+ if (teamsError) {
+ console.error('Teams error:', teamsError);
+ throw teamsError;
+ }
+
+ if (isMounted && teamsData) {
+ setTeams(teamsData);
+ }
+ } catch (err) {
+ console.error('Error fetching teams:', err);
+ if (isMounted) {
+ setError(err instanceof Error ? err.message : 'Failed to load teams');
+ }
+ } finally {
+ if (isMounted) {
+ setLoading(false);
+ }
+ }
+ };
+
+ // 设置认证状态监听器
+ const { data: { subscription } } = supabase.auth.onAuthStateChange((event, session) => {
+ console.log('Auth state changed:', event, session?.user);
+ if (event === 'SIGNED_IN' && session?.user?.id) {
+ fetchTeams(session.user.id);
+ } else if (event === 'SIGNED_OUT') {
+ setTeams([]);
+ setError(null);
+ }
+ });
+
+ // 初始检查session
+ supabase.auth.getSession().then(({ data: { session } }) => {
+ console.log('Initial session check:', session?.user);
+ if (session?.user?.id) {
+ fetchTeams(session.user.id);
+ }
+ });
+
+ return () => {
+ isMounted = false;
+ subscription.unsubscribe();
+ };
+ }, [supabase]);
+
+ if (loading) {
+ return ;
+ }
+
+ if (error) {
+ return {error}
;
+ }
+
+ if (teams.length === 0) {
+ return No teams available
;
+ }
+
+ return (
+