Skip to content

Commit 1fc9037

Browse files
committed
Merge branch 'main' of https://github.com/Gonna-AI/Frontend
2 parents a3b8c13 + 1f4a3d1 commit 1fc9037

File tree

25 files changed

+4657
-54
lines changed

25 files changed

+4657
-54
lines changed

src/components/AppSidebar.tsx

Lines changed: 70 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ import {
2222
Users,
2323
Wand2,
2424
ShieldCheck,
25-
FileText
25+
FileText,
26+
TrendingUp,
27+
Activity,
28+
Webhook,
29+
Settings,
2630
} from "lucide-react"
2731

2832
import { useTheme } from "@/hooks/useTheme"
@@ -55,7 +59,7 @@ interface AppSidebarProps extends React.ComponentProps<typeof Sidebar> {
5559
}
5660

5761
// Tabs that are always accessible without an access code
58-
const ALWAYS_ACCESSIBLE_TABS = ['billing', 'keys', 'usage'];
62+
const ALWAYS_ACCESSIBLE_TABS = ['billing', 'keys', 'usage', 'settings'];
5963

6064
export function AppSidebar({ activeTab, setActiveTab, hasAccess = false, ...props }: AppSidebarProps) {
6165
const { t, language, setLanguage } = useLanguage();
@@ -184,6 +188,22 @@ export function AppSidebar({ activeTab, setActiveTab, hasAccess = false, ...prop
184188
<span>{t('sidebar.customerGraph')}</span>
185189
</SidebarMenuButton>
186190
</SidebarMenuItem>
191+
<SidebarMenuItem>
192+
<SidebarMenuButton
193+
isActive={activeTab === 'analytics'}
194+
onClick={() => handleTabClick('analytics')}
195+
tooltip={t('sidebar.analytics')}
196+
className={cn(
197+
"transition-colors",
198+
isDark
199+
? "text-white/80 hover:text-white hover:bg-white/10 data-[active=true]:bg-white/10 data-[active=true]:text-white"
200+
: "text-gray-600 hover:text-gray-900 hover:bg-black/5 data-[active=true]:bg-black/5 data-[active=true]:text-black"
201+
)}
202+
>
203+
<TrendingUp />
204+
<span>{t('sidebar.analytics')}</span>
205+
</SidebarMenuButton>
206+
</SidebarMenuItem>
187207
</SidebarMenu>
188208
</SidebarGroupContent>
189209
</SidebarGroup>
@@ -431,6 +451,54 @@ export function AppSidebar({ activeTab, setActiveTab, hasAccess = false, ...prop
431451
<span>{t('sidebar.integrations')}</span>
432452
</SidebarMenuButton>
433453
</SidebarMenuItem>
454+
<SidebarMenuItem className={getLockedStyles('webhooks')}>
455+
<SidebarMenuButton
456+
isActive={activeTab === 'webhooks'}
457+
onClick={() => handleTabClick('webhooks')}
458+
tooltip={t('sidebar.webhooks')}
459+
className={cn(
460+
"transition-colors",
461+
isDark
462+
? "text-white/80 hover:text-white hover:bg-white/10 data-[active=true]:bg-white/10 data-[active=true]:text-white"
463+
: "text-gray-600 hover:text-gray-900 hover:bg-black/5 data-[active=true]:bg-black/5 data-[active=true]:text-black"
464+
)}
465+
>
466+
<Webhook />
467+
<span>{t('sidebar.webhooks')}</span>
468+
</SidebarMenuButton>
469+
</SidebarMenuItem>
470+
<SidebarMenuItem className={getLockedStyles('activity_log')}>
471+
<SidebarMenuButton
472+
isActive={activeTab === 'activity_log'}
473+
onClick={() => handleTabClick('activity_log')}
474+
tooltip={t('sidebar.activityLog')}
475+
className={cn(
476+
"transition-colors",
477+
isDark
478+
? "text-white/80 hover:text-white hover:bg-white/10 data-[active=true]:bg-white/10 data-[active=true]:text-white"
479+
: "text-gray-600 hover:text-gray-900 hover:bg-black/5 data-[active=true]:bg-black/5 data-[active=true]:text-black"
480+
)}
481+
>
482+
<Activity />
483+
<span>{t('sidebar.activityLog')}</span>
484+
</SidebarMenuButton>
485+
</SidebarMenuItem>
486+
<SidebarMenuItem>
487+
<SidebarMenuButton
488+
isActive={activeTab === 'settings'}
489+
onClick={() => handleTabClick('settings')}
490+
tooltip={t('sidebar.settings')}
491+
className={cn(
492+
"transition-colors",
493+
isDark
494+
? "text-white/80 hover:text-white hover:bg-white/10 data-[active=true]:bg-white/10 data-[active=true]:text-white"
495+
: "text-gray-600 hover:text-gray-900 hover:bg-black/5 data-[active=true]:bg-black/5 data-[active=true]:text-black"
496+
)}
497+
>
498+
<Settings />
499+
<span>{t('sidebar.settings')}</span>
500+
</SidebarMenuButton>
501+
</SidebarMenuItem>
434502
</SidebarMenu>
435503
</SidebarGroupContent>
436504
</SidebarGroup>
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
1+
import { useState, useEffect, useCallback } from 'react';
2+
import {
3+
Activity, Search, Filter, Loader2, RefreshCw, ChevronDown,
4+
LogIn, CreditCard, Users, Key, Settings, FileText, Link2, Shield,
5+
} from 'lucide-react';
6+
import { cn } from '../../utils/cn';
7+
import { supabase } from '../../config/supabase';
8+
import { useLanguage } from '../../contexts/LanguageContext';
9+
10+
interface ActivityLogViewProps {
11+
isDark: boolean;
12+
}
13+
14+
interface ActivityEntry {
15+
id: string;
16+
event_type: string;
17+
action: string;
18+
description: string;
19+
metadata: Record<string, unknown>;
20+
ip_address: string | null;
21+
user_agent: string | null;
22+
created_at: string;
23+
}
24+
25+
const EVENT_TYPE_CONFIG: Record<string, { icon: React.ReactNode; color: string; label: string }> = {
26+
auth: { icon: <LogIn className="w-3.5 h-3.5" />, color: 'text-blue-400 bg-blue-500/10 border-blue-500/20', label: 'Auth' },
27+
billing: { icon: <CreditCard className="w-3.5 h-3.5" />, color: 'text-emerald-400 bg-emerald-500/10 border-emerald-500/20', label: 'Billing' },
28+
team: { icon: <Users className="w-3.5 h-3.5" />, color: 'text-purple-400 bg-purple-500/10 border-purple-500/20', label: 'Team' },
29+
api_keys: { icon: <Key className="w-3.5 h-3.5" />, color: 'text-orange-400 bg-orange-500/10 border-orange-500/20', label: 'API Keys' },
30+
config: { icon: <Settings className="w-3.5 h-3.5" />, color: 'text-cyan-400 bg-cyan-500/10 border-cyan-500/20', label: 'Config' },
31+
documents: { icon: <FileText className="w-3.5 h-3.5" />, color: 'text-pink-400 bg-pink-500/10 border-pink-500/20', label: 'Documents' },
32+
integrations: { icon: <Link2 className="w-3.5 h-3.5" />, color: 'text-indigo-400 bg-indigo-500/10 border-indigo-500/20', label: 'Integrations' },
33+
security: { icon: <Shield className="w-3.5 h-3.5" />, color: 'text-red-400 bg-red-500/10 border-red-500/20', label: 'Security' },
34+
system: { icon: <Activity className="w-3.5 h-3.5" />, color: 'text-gray-400 bg-gray-500/10 border-gray-500/20', label: 'System' },
35+
};
36+
37+
function timeAgo(dateStr: string): string {
38+
const now = new Date();
39+
const date = new Date(dateStr);
40+
const seconds = Math.floor((now.getTime() - date.getTime()) / 1000);
41+
if (seconds < 60) return 'just now';
42+
if (seconds < 3600) return `${Math.floor(seconds / 60)}m ago`;
43+
if (seconds < 86400) return `${Math.floor(seconds / 3600)}h ago`;
44+
if (seconds < 604800) return `${Math.floor(seconds / 86400)}d ago`;
45+
return date.toLocaleDateString();
46+
}
47+
48+
export default function ActivityLogView({ isDark }: ActivityLogViewProps) {
49+
const { t } = useLanguage();
50+
const [loading, setLoading] = useState(true);
51+
const [activities, setActivities] = useState<ActivityEntry[]>([]);
52+
const [totalCount, setTotalCount] = useState(0);
53+
const [search, setSearch] = useState('');
54+
const [filterType, setFilterType] = useState('');
55+
const [showFilter, setShowFilter] = useState(false);
56+
const [page, setPage] = useState(0);
57+
const pageSize = 30;
58+
59+
const fetchActivities = useCallback(async () => {
60+
setLoading(true);
61+
try {
62+
const session = await supabase.auth.getSession();
63+
const token = session.data.session?.access_token;
64+
if (!token) return;
65+
66+
const params = new URLSearchParams({
67+
limit: String(pageSize),
68+
offset: String(page * pageSize),
69+
});
70+
if (filterType) params.set('type', filterType);
71+
if (search) params.set('search', search);
72+
73+
const res = await fetch(
74+
`${import.meta.env.VITE_SUPABASE_URL}/functions/v1/api-activity-log?${params}`,
75+
{ headers: { Authorization: `Bearer ${token}` } }
76+
);
77+
78+
if (res.ok) {
79+
const data = await res.json();
80+
setActivities(data.activities || []);
81+
setTotalCount(data.count || 0);
82+
}
83+
} catch (err) {
84+
console.error('[ActivityLogView] fetch error:', err);
85+
} finally {
86+
setLoading(false);
87+
}
88+
}, [page, filterType, search]);
89+
90+
useEffect(() => { fetchActivities(); }, [fetchActivities]);
91+
92+
// Group activities by date
93+
const grouped: Record<string, ActivityEntry[]> = {};
94+
for (const a of activities) {
95+
const day = new Date(a.created_at).toLocaleDateString('en-US', { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' });
96+
if (!grouped[day]) grouped[day] = [];
97+
grouped[day].push(a);
98+
}
99+
100+
const totalPages = Math.ceil(totalCount / pageSize);
101+
102+
return (
103+
<div className="space-y-6">
104+
{/* Header */}
105+
<div className="flex items-center justify-between flex-wrap gap-4">
106+
<div>
107+
<h2 className={cn("text-2xl font-bold tracking-tight", isDark ? "text-white" : "text-gray-900")}>
108+
{t('activityLog.title') || 'Activity Log'}
109+
</h2>
110+
<p className={cn("text-sm mt-1", isDark ? "text-white/50" : "text-gray-500")}>
111+
{t('activityLog.subtitle') || 'Track all account actions and events'}
112+
</p>
113+
</div>
114+
<button
115+
onClick={fetchActivities}
116+
className={cn("p-2 rounded-lg border transition-colors", isDark ? "border-white/10 hover:bg-white/5 text-white/60" : "border-gray-200 hover:bg-gray-50 text-gray-500")}
117+
>
118+
<RefreshCw className="w-4 h-4" />
119+
</button>
120+
</div>
121+
122+
{/* Search & Filter */}
123+
<div className="flex items-center gap-3 flex-wrap">
124+
<div className={cn("flex items-center gap-2 flex-1 min-w-[200px] px-3 py-2 rounded-lg border", isDark ? "bg-white/5 border-white/10" : "bg-white border-gray-200")}>
125+
<Search className={cn("w-4 h-4 shrink-0", isDark ? "text-white/40" : "text-gray-400")} />
126+
<input
127+
type="text"
128+
value={search}
129+
onChange={(e) => { setSearch(e.target.value); setPage(0); }}
130+
placeholder="Search activities..."
131+
className={cn("flex-1 bg-transparent text-sm outline-none", isDark ? "text-white placeholder:text-white/30" : "text-gray-900 placeholder:text-gray-400")}
132+
/>
133+
</div>
134+
135+
<div className="relative">
136+
<button
137+
onClick={() => setShowFilter(!showFilter)}
138+
className={cn(
139+
"flex items-center gap-2 px-3 py-2 rounded-lg border text-sm transition-colors",
140+
filterType
141+
? isDark ? "border-purple-500/30 bg-purple-500/10 text-purple-400" : "border-purple-500/30 bg-purple-50 text-purple-600"
142+
: isDark ? "border-white/10 hover:bg-white/5 text-white/60" : "border-gray-200 hover:bg-gray-50 text-gray-500"
143+
)}
144+
>
145+
<Filter className="w-4 h-4" />
146+
{filterType ? EVENT_TYPE_CONFIG[filterType]?.label || filterType : 'Filter'}
147+
<ChevronDown className="w-3 h-3" />
148+
</button>
149+
150+
{showFilter && (
151+
<div className={cn(
152+
"absolute top-full mt-1 right-0 z-50 w-48 rounded-lg border shadow-lg overflow-hidden",
153+
isDark ? "bg-[#1a1a1a] border-white/10" : "bg-white border-gray-200"
154+
)}>
155+
<button
156+
onClick={() => { setFilterType(''); setShowFilter(false); setPage(0); }}
157+
className={cn("w-full text-left px-3 py-2 text-sm transition-colors", isDark ? "hover:bg-white/5 text-white/70" : "hover:bg-gray-50 text-gray-700")}
158+
>
159+
All Events
160+
</button>
161+
{Object.entries(EVENT_TYPE_CONFIG).map(([key, config]) => (
162+
<button
163+
key={key}
164+
onClick={() => { setFilterType(key); setShowFilter(false); setPage(0); }}
165+
className={cn(
166+
"w-full text-left px-3 py-2 text-sm transition-colors flex items-center gap-2",
167+
isDark ? "hover:bg-white/5 text-white/70" : "hover:bg-gray-50 text-gray-700",
168+
filterType === key && (isDark ? "bg-white/5" : "bg-gray-50")
169+
)}
170+
>
171+
{config.icon}
172+
{config.label}
173+
</button>
174+
))}
175+
</div>
176+
)}
177+
</div>
178+
</div>
179+
180+
{/* Activity List */}
181+
{loading ? (
182+
<div className="flex items-center justify-center py-20">
183+
<Loader2 className={cn("w-6 h-6 animate-spin", isDark ? "text-purple-400" : "text-purple-600")} />
184+
</div>
185+
) : activities.length === 0 ? (
186+
<div className={cn("text-center py-20 rounded-2xl border", isDark ? "bg-[#09090B] border-white/10" : "bg-white border-black/10")}>
187+
<Activity className={cn("w-10 h-10 mx-auto mb-3", isDark ? "text-white/20" : "text-gray-300")} />
188+
<p className={cn("text-sm font-medium", isDark ? "text-white/50" : "text-gray-500")}>No activity yet</p>
189+
<p className={cn("text-xs mt-1", isDark ? "text-white/30" : "text-gray-400")}>Account actions will appear here</p>
190+
</div>
191+
) : (
192+
<div className="space-y-6">
193+
{Object.entries(grouped).map(([date, entries]) => (
194+
<div key={date}>
195+
<h3 className={cn("text-xs font-semibold uppercase tracking-wider mb-3", isDark ? "text-white/30" : "text-gray-400")}>{date}</h3>
196+
<div className={cn("rounded-2xl border overflow-hidden divide-y", isDark ? "bg-[#09090B] border-white/10 divide-white/5" : "bg-white border-black/10 divide-gray-100")}>
197+
{entries.map((entry) => {
198+
const config = EVENT_TYPE_CONFIG[entry.event_type] || EVENT_TYPE_CONFIG.system;
199+
return (
200+
<div key={entry.id} className={cn("px-4 py-3 flex items-start gap-3 transition-colors", isDark ? "hover:bg-white/[0.02]" : "hover:bg-gray-50/50")}>
201+
<div className={cn("p-1.5 rounded-lg border shrink-0 mt-0.5", config.color)}>
202+
{config.icon}
203+
</div>
204+
<div className="flex-1 min-w-0">
205+
<div className="flex items-center gap-2 mb-0.5">
206+
<span className={cn("text-sm font-medium", isDark ? "text-white" : "text-gray-900")}>{entry.action.replace(/_/g, ' ')}</span>
207+
<span className={cn("text-xs px-1.5 py-0.5 rounded-md", isDark ? "bg-white/5 text-white/40" : "bg-gray-100 text-gray-500")}>{config.label}</span>
208+
</div>
209+
{entry.description && (
210+
<p className={cn("text-xs truncate", isDark ? "text-white/40" : "text-gray-500")}>{entry.description}</p>
211+
)}
212+
</div>
213+
<span className={cn("text-xs shrink-0", isDark ? "text-white/30" : "text-gray-400")}>{timeAgo(entry.created_at)}</span>
214+
</div>
215+
);
216+
})}
217+
</div>
218+
</div>
219+
))}
220+
221+
{/* Pagination */}
222+
{totalPages > 1 && (
223+
<div className="flex items-center justify-center gap-2 pt-4">
224+
<button
225+
onClick={() => setPage(p => Math.max(0, p - 1))}
226+
disabled={page === 0}
227+
className={cn("px-3 py-1.5 rounded-lg text-sm border transition-colors", isDark ? "border-white/10 text-white/60 hover:bg-white/5 disabled:opacity-30" : "border-gray-200 text-gray-600 hover:bg-gray-50 disabled:opacity-30")}
228+
>
229+
Previous
230+
</button>
231+
<span className={cn("text-xs", isDark ? "text-white/40" : "text-gray-400")}>
232+
Page {page + 1} of {totalPages}
233+
</span>
234+
<button
235+
onClick={() => setPage(p => Math.min(totalPages - 1, p + 1))}
236+
disabled={page >= totalPages - 1}
237+
className={cn("px-3 py-1.5 rounded-lg text-sm border transition-colors", isDark ? "border-white/10 text-white/60 hover:bg-white/5 disabled:opacity-30" : "border-gray-200 text-gray-600 hover:bg-gray-50 disabled:opacity-30")}
238+
>
239+
Next
240+
</button>
241+
</div>
242+
)}
243+
</div>
244+
)}
245+
</div>
246+
);
247+
}

0 commit comments

Comments
 (0)