From 1be6a6dbf0a4c4cdf621e0903fd189df18ea97cf Mon Sep 17 00:00:00 2001 From: William Tso Date: Tue, 1 Apr 2025 22:18:25 +0800 Subject: [PATCH] move page --- app/(app)/AppLayoutClient.tsx | 102 ++++++------- app/(app)/analytics/geo/page.tsx | 137 ----------------- app/(app)/dashboard/page.tsx | 162 +++++++++++++++++++- app/(app)/events/page.tsx | 250 ------------------------------- 4 files changed, 210 insertions(+), 441 deletions(-) delete mode 100644 app/(app)/analytics/geo/page.tsx delete mode 100644 app/(app)/events/page.tsx diff --git a/app/(app)/AppLayoutClient.tsx b/app/(app)/AppLayoutClient.tsx index d2018c8..1d490fd 100644 --- a/app/(app)/AppLayoutClient.tsx +++ b/app/(app)/AppLayoutClient.tsx @@ -1,7 +1,10 @@ 'use client'; +import React from 'react'; +import { usePathname } from 'next/navigation'; import Link from 'next/link'; import { ProtectedRoute, useAuth } from '@/lib/auth'; +import { LayoutDashboard, BarChart3, UserCircle } from 'lucide-react'; export default function AppLayoutClient({ children, @@ -9,6 +12,13 @@ export default function AppLayoutClient({ children: React.ReactNode; }) { const { signOut, user } = useAuth(); + const pathname = usePathname(); + + const navigationItems = [ + { name: 'Dashboard', href: '/dashboard', icon: }, + { name: 'Analytics', href: '/analytics', icon: }, + { name: 'Account', href: '/account', icon: }, + ]; const handleSignOut = async () => { await signOut(); @@ -16,64 +26,56 @@ export default function AppLayoutClient({ return ( -
- -
- {children} -
+ + + {/* 页面内容 */} +
+ {children} +
+
); diff --git a/app/(app)/analytics/geo/page.tsx b/app/(app)/analytics/geo/page.tsx deleted file mode 100644 index ebe5c46..0000000 --- a/app/(app)/analytics/geo/page.tsx +++ /dev/null @@ -1,137 +0,0 @@ -"use client"; - -import { useState, useEffect } from 'react'; -import { GeoData } from '../../api/types'; - -export default function GeoAnalyticsPage() { - const [geoData, setGeoData] = useState([]); - const [isLoading, setIsLoading] = useState(true); - const [error, setError] = useState(null); - const [dateRange, setDateRange] = useState({ - from: new Date('2024-02-01'), - to: new Date('2025-03-05') - }); - - useEffect(() => { - const fetchGeoData = async () => { - try { - setIsLoading(true); - setError(null); - - const response = await fetch(`/api/events/geo?startTime=${dateRange.from.toISOString().split('T')[0]}T00:00:00Z&endTime=${dateRange.to.toISOString().split('T')[0]}T23:59:59Z`); - if (!response.ok) throw new Error('Failed to fetch geographic data'); - - const data = await response.json(); - setGeoData(data.data); - } catch (err) { - setError(err instanceof Error ? err.message : 'An error occurred'); - } finally { - setIsLoading(false); - } - }; - - fetchGeoData(); - }, [dateRange]); - - return ( -
- {/* 页面标题 */} -
-

Geographic Analysis

-

Analyze visitor distribution by location

-
- - {/* 时间范围选择器 */} -
-
-
- - setDateRange(prev => ({ ...prev, from: new Date(e.target.value) }))} - /> -
-
- - setDateRange(prev => ({ ...prev, to: new Date(e.target.value) }))} - /> -
-
-
- - {/* 地理数据表格 */} -
-
- - - - - - - - - - - {geoData.map(item => ( - - - - - - - ))} - -
LocationVisitsUnique VisitorsPercentage
- {item.location} - - {item.visits} - - {item.visitors} - -
- {item.percentage.toFixed(1)}% -
-
-
-
-
-
- - {/* 加载状态 */} - {isLoading && ( -
-
-
- )} - - {/* 错误状态 */} - {error && ( -
-

{error}

-
- )} - - {/* 无数据状态 */} - {!isLoading && !error && geoData.length === 0 && ( -
-

No geographic data available

-
- )} -
- - {/* 提示信息 */} -
-

Note: Geographic data is based on IP addresses and may not be 100% accurate.

-
-
- ); -} \ No newline at end of file diff --git a/app/(app)/dashboard/page.tsx b/app/(app)/dashboard/page.tsx index 016f2df..e53f2bb 100644 --- a/app/(app)/dashboard/page.tsx +++ b/app/(app)/dashboard/page.tsx @@ -8,6 +8,62 @@ import GeoAnalytics from '@/app/components/analytics/GeoAnalytics'; import DevicePieCharts from '@/app/components/charts/DevicePieCharts'; import { EventsSummary, TimeSeriesData, GeoData, DeviceAnalytics as DeviceAnalyticsType } from '@/app/api/types'; +// 事件类型定义 +interface Event { + event_id?: string; + url_id: string; + url: string; + event_type: string; + visitor_id: string; + created_at: string; + referrer?: string; + browser?: string; + os?: string; + device_type?: string; + country?: string; + city?: string; +} + +// 获取事件的函数 +const fetchEvents = async ( + startTime?: string, + endTime?: string, + urlId?: string, + eventType?: string +): Promise => { + try { + // 构建查询参数 + const params = new URLSearchParams(); + if (startTime) params.append('startTime', startTime); + if (endTime) params.append('endTime', endTime); + if (urlId) params.append('urlId', urlId); + if (eventType) params.append('eventType', eventType); + + // 发送请求 + const response = await fetch(`/api/events?${params.toString()}`); + + if (!response.ok) { + throw new Error('Failed to fetch events'); + } + + const data = await response.json(); + return data.data || []; + } catch (error) { + console.error('Error fetching events:', error); + return []; + } +}; + +// 格式化日期函数 +const formatDate = (dateString: string) => { + if (!dateString) return ''; + try { + return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss'); + } catch { + return dateString; + } +}; + export default function DashboardPage() { const [dateRange, setDateRange] = useState({ from: new Date('2024-02-01'), @@ -20,6 +76,8 @@ export default function DashboardPage() { const [timeSeriesData, setTimeSeriesData] = useState([]); const [geoData, setGeoData] = useState([]); const [deviceData, setDeviceData] = useState(null); + const [events, setEvents] = useState([]); + const [urlFilter, setUrlFilter] = useState(''); useEffect(() => { const fetchData = async () => { @@ -31,11 +89,12 @@ export default function DashboardPage() { const endTime = format(dateRange.to, "yyyy-MM-dd'T'HH:mm:ss'Z'"); // 并行获取所有数据 - const [summaryRes, timeSeriesRes, geoRes, deviceRes] = await Promise.all([ + const [summaryRes, timeSeriesRes, geoRes, deviceRes, eventsRes] = await Promise.all([ fetch(`/api/events/summary?startTime=${startTime}&endTime=${endTime}`), fetch(`/api/events/time-series?startTime=${startTime}&endTime=${endTime}`), fetch(`/api/events/geo?startTime=${startTime}&endTime=${endTime}`), - fetch(`/api/events/devices?startTime=${startTime}&endTime=${endTime}`) + fetch(`/api/events/devices?startTime=${startTime}&endTime=${endTime}`), + fetchEvents(startTime, endTime, urlFilter || undefined) ]); const [summaryData, timeSeriesData, geoData, deviceData] = await Promise.all([ @@ -54,6 +113,7 @@ export default function DashboardPage() { setTimeSeriesData(timeSeriesData.data); setGeoData(geoData.data); setDeviceData(deviceData.data); + setEvents(eventsRes || []); } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred while fetching data'); } finally { @@ -62,7 +122,7 @@ export default function DashboardPage() { }; fetchData(); - }, [dateRange]); + }, [dateRange, urlFilter]); if (loading) { return ( @@ -131,10 +191,104 @@ export default function DashboardPage() { {deviceData && } -
+

Geographic Distribution

+ + {/* 事件列表部分 */} +
+
+

Recent Events

+ +
+
+ + setUrlFilter(e.target.value)} + className="w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm text-gray-900" + placeholder="Enter URL ID to filter" + /> +
+
+
+ +
+ + + + + + + + + + + + + + {events.map((event, index) => ( + + + + + + + + + + ))} + +
+ Time + + URL ID + + URL + + Event Type + + Visitor ID + + Referrer + + Location +
+ {formatDate(event.created_at)} + + {event.url_id} + + + {event.url} + + + + {event.event_type} + + + {event.visitor_id.substring(0, 8)}... + + {event.referrer || '-'} + + {event.country && event.city ? `${event.city}, ${event.country}` : (event.country || '-')} +
+
+ + {/* 表格为空状态 */} + {!loading && events.length === 0 && ( +
+ No events found +
+ )} +
); } \ No newline at end of file diff --git a/app/(app)/events/page.tsx b/app/(app)/events/page.tsx deleted file mode 100644 index 50ca432..0000000 --- a/app/(app)/events/page.tsx +++ /dev/null @@ -1,250 +0,0 @@ -"use client"; - -import { useState, useEffect } from 'react'; -import { format } from 'date-fns'; - -// 更复杂的事件类型定义 -interface Event { - event_id?: string; - url_id: string; - url: string; - event_type: string; - visitor_id: string; - created_at: string; - referrer?: string; - browser?: string; - os?: string; - device_type?: string; - country?: string; - city?: string; -} - -// 创建获取事件的函数 -const fetchEvents = async ( - startTime?: string, - endTime?: string, - urlId?: string, - eventType?: string -): Promise => { - try { - // 构建查询参数 - const params = new URLSearchParams(); - if (startTime) params.append('startTime', startTime); - if (endTime) params.append('endTime', endTime); - if (urlId) params.append('urlId', urlId); - if (eventType) params.append('eventType', eventType); - - // 发送请求 - const response = await fetch(`/api/events?${params.toString()}`); - - if (!response.ok) { - throw new Error('Failed to fetch events'); - } - - const data = await response.json(); - return data.data || []; - } catch (error) { - console.error('Error fetching events:', error); - return []; - } -}; - -const formatDate = (dateString: string) => { - if (!dateString) return ''; - try { - return format(new Date(dateString), 'yyyy-MM-dd HH:mm:ss'); - } catch { - return dateString; - } -}; - -export default function EventsPage() { - // 状态定义 - const [events, setEvents] = useState([]); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); - const [filters, setFilters] = useState({ - startDate: format(new Date(Date.now() - 7 * 24 * 60 * 60 * 1000), 'yyyy-MM-dd'), - endDate: format(new Date(), 'yyyy-MM-dd'), - urlId: '', - eventType: '' - }); - - // 加载事件数据 - useEffect(() => { - const loadEvents = async () => { - setLoading(true); - setError(null); - - try { - const startTime = `${filters.startDate}T00:00:00Z`; - const endTime = `${filters.endDate}T23:59:59Z`; - - const eventsData = await fetchEvents( - startTime, - endTime, - filters.urlId || undefined, - filters.eventType || undefined - ); - - setEvents(eventsData); - } catch (err) { - setError('Failed to load events'); - console.error(err); - } finally { - setLoading(false); - } - }; - - loadEvents(); - }, [filters]); - - // 处理筛选条件变化 - const handleFilterChange = (name: string, value: string) => { - setFilters(prev => ({ - ...prev, - [name]: value - })); - }; - - return ( -
- {/* 页面标题 */} -
-

Events

-

View and analyze all events for your URLs

-
- - {/* 过滤器面板 */} -
-
-
- - handleFilterChange('startDate', e.target.value)} - className="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm text-gray-900" - /> -
- -
- - handleFilterChange('endDate', e.target.value)} - className="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm text-gray-900" - /> -
- -
- - handleFilterChange('urlId', e.target.value)} - className="block w-full px-3 py-2 bg-white border border-gray-300 rounded-md text-sm text-gray-900" - placeholder="Filter by URL ID" - /> -
-
-
- - {/* 事件表格 */} -
-
- - - - - - - - - - - - - - {events.map((event, index) => ( - - - - - - - - - - ))} - -
- Time - - URL ID - - URL - - Event Type - - Visitor ID - - Referrer - - Location -
- {formatDate(event.created_at)} - - {event.url_id} - - - {event.url} - - - - {event.event_type} - - - {event.visitor_id.substring(0, 8)}... - - {event.referrer || '-'} - - {event.country && event.city ? `${event.city}, ${event.country}` : (event.country || '-')} -
-
- - {/* 加载状态 */} - {loading && ( -
-
-
- )} - - {/* 错误状态 */} - {error && ( -
- {error} -
- )} - - {/* 空状态 */} - {!loading && !error && events.length === 0 && ( -
- No events found -
- )} -
-
- ); -}