Skip to content

Commit 85dce84

Browse files
authored
Merge branch 'main' into feat
2 parents 9f4426a + 6ebc5cc commit 85dce84

20 files changed

Lines changed: 1104 additions & 62 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import React from "react";
2+
import Skeleton from "@/components/ui/Skeleton";
3+
4+
export default function Loading() {
5+
return (
6+
<div className="dashboard-main space-y-6 max-w-[1200px] mx-auto w-full">
7+
<div className="flex items-center gap-3">
8+
<Skeleton className="w-10 h-10 rounded-xl" />
9+
<div className="space-y-2">
10+
<Skeleton className="h-6 w-40 rounded-lg" />
11+
<Skeleton className="h-4 w-64 rounded-lg" />
12+
</div>
13+
</div>
14+
15+
<div className="bg-surface border border-border rounded-[24px] p-5 sm:p-6 space-y-4">
16+
<div className="flex items-center justify-between">
17+
<Skeleton className="h-4 w-28" />
18+
<Skeleton className="h-6 w-6 rounded-lg" />
19+
</div>
20+
21+
<div className="flex items-center gap-2">
22+
<Skeleton className="h-4 w-4 rounded-full" />
23+
<Skeleton className="h-7 w-12 rounded-lg" />
24+
<Skeleton className="h-7 w-20 rounded-lg" />
25+
<Skeleton className="h-7 w-16 rounded-lg" />
26+
</div>
27+
28+
<div className="space-y-3">
29+
{[1, 2, 3, 4, 5].map((i) => (
30+
<div key={i} className="flex items-center gap-3 p-3 rounded-xl border border-border bg-surface-hover/50">
31+
<Skeleton className="w-8 h-8 rounded-lg shrink-0" />
32+
<div className="flex-1 space-y-2">
33+
<div className="flex items-center justify-between">
34+
<Skeleton className="h-4 w-20" />
35+
<Skeleton className="h-4 w-24" />
36+
</div>
37+
<div className="flex items-center justify-between">
38+
<Skeleton className="h-3 w-32" />
39+
<Skeleton className="h-3 w-16" />
40+
</div>
41+
</div>
42+
</div>
43+
))}
44+
</div>
45+
</div>
46+
</div>
47+
);
48+
}

app/(dashboard)/analytics/page.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"use client";
2+
3+
import React, { useEffect, useRef, useState } from "react";
4+
import StatCard from "@/components/dashboard/StatCard";
5+
import Skeleton from "@/components/ui/Skeleton";
6+
import { Download, Eye, Clock, BarChart3 } from "lucide-react";
7+
import analytics from "@/app/lib/analytics";
8+
9+
type AnalyticsResponse = {
10+
totalViews: number;
11+
totalWatchTime: number;
12+
avgEngagement: number;
13+
byPlatform: { platform: string; views: number; engagement: number }[];
14+
top5: { clipId: string; title: string; views: number; platform: string }[];
15+
dateRange: { startDate: string | null; endDate: string | null };
16+
};
17+
18+
export default function AnalyticsPage() {
19+
const [data, setData] = useState<AnalyticsResponse | null>(null);
20+
const [loading, setLoading] = useState(true);
21+
const [error, setError] = useState<string | null>(null);
22+
const [range, setRange] = useState("30d");
23+
const [platform, setPlatform] = useState("all");
24+
const initialized = useRef(false);
25+
26+
useEffect(() => {
27+
if (initialized.current) return;
28+
initialized.current = true;
29+
analytics.trackPageView("/analytics");
30+
}, []);
31+
32+
useEffect(() => {
33+
let cancelled = false;
34+
async function load() {
35+
try {
36+
setLoading(true);
37+
const params = new URLSearchParams();
38+
if (range !== "all") params.set("startDate", new Date(Date.now() - Number(range.replace("d",""))*86400000).toISOString().split("T")[0]);
39+
if (platform !== "all") params.set("platform", platform);
40+
const res = await fetch(`/api/analytics?${params.toString()}`);
41+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
42+
const json = (await res.json()) as AnalyticsResponse;
43+
if (!cancelled) setData(json);
44+
} catch (err) {
45+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load analytics");
46+
} finally {
47+
if (!cancelled) setLoading(false);
48+
}
49+
}
50+
load();
51+
return () => { cancelled = true; };
52+
}, [range, platform]);
53+
54+
const exportCsv = () => {
55+
if (!data) return;
56+
const header = "clipId,title,views,watchTimeMinutes,engagementRate,platform\n";
57+
const rows = data.top5.map(t => `${t.clipId},"${t.title}",${t.views},,,${t.platform}`).join("\n");
58+
const blob = new Blob([header + rows], { type: "text/csv" });
59+
const url = URL.createObjectURL(blob);
60+
const a = document.createElement("a");
61+
a.href = url;
62+
a.download = `analytics-${range}.csv`;
63+
a.click();
64+
URL.revokeObjectURL(url);
65+
};
66+
67+
return (
68+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-10 py-10">
69+
<div className="space-y-8">
70+
<div className="flex flex-col lg:flex-row lg:items-center lg:justify-between gap-4">
71+
<div>
72+
<h1 className="text-[28px] sm:text-[32px] font-extrabold tracking-tight text-white">Clip Analytics</h1>
73+
<p className="text-muted text-[14px] mt-1">Views, watch time, engagement, and platform breakdown.</p>
74+
</div>
75+
<div className="flex items-center gap-3">
76+
<select value={range} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setRange(e.target.value)} className="bg-input text-white text-sm rounded-xl px-3 py-2 border border-white/10">
77+
<option value="7d">Last 7 days</option>
78+
<option value="30d">Last 30 days</option>
79+
<option value="90d">Last 90 days</option>
80+
<option value="all">All time</option>
81+
</select>
82+
<select value={platform} onChange={(e: React.ChangeEvent<HTMLSelectElement>) => setPlatform(e.target.value)} className="bg-input text-white text-sm rounded-xl px-3 py-2 border border-white/10">
83+
<option value="all">All platforms</option>
84+
<option value="YouTube">YouTube</option>
85+
<option value="TikTok">TikTok</option>
86+
<option value="Instagram">Instagram</option>
87+
<option value="Twitch">Twitch</option>
88+
</select>
89+
<button onClick={exportCsv} className="inline-flex items-center gap-2 px-4 py-2 rounded-xl bg-brand text-black font-bold text-sm hover:bg-brand-hover transition-colors">
90+
<Download className="w-4 h-4" /> Export
91+
</button>
92+
</div>
93+
</div>
94+
95+
{loading ? (
96+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
97+
{[1, 2, 3].map((i) => (<div key={i} className="h-32 rounded-2xl bg-white/6 animate-pulse" />))}
98+
</div>
99+
) : error ? (
100+
<div className="rounded-2xl border border-error/30 bg-error/10 p-6">
101+
<p className="text-error text-sm">{error}</p>
102+
</div>
103+
) : data ? (
104+
<>
105+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
106+
<StatCard label="Total Views" value={String(data.totalViews)} icon={Eye} trend={`${data.totalViews.toLocaleString()} views`} />
107+
<StatCard label="Watch Time" value={`${data.totalWatchTime.toLocaleString()}m`} icon={Clock} trend="Total minutes watched" />
108+
<StatCard label="Engagement" value={`${data.avgEngagement.toFixed(2)}%`} icon={BarChart3} trend="Average rate" />
109+
</div>
110+
111+
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
112+
<div className="bg-surface border border-white/5 rounded-2xl p-6">
113+
<h3 className="text-white font-bold mb-4">Engagement by Platform</h3>
114+
<div className="space-y-3">
115+
{data.byPlatform.map((p) => (
116+
<div key={p.platform} className="flex items-center justify-between">
117+
<span className="text-sm text-muted">{p.platform}</span>
118+
<div className="flex-1 mx-4">
119+
<div className="h-2 rounded-full bg-white/6 overflow-hidden">
120+
<div className="h-full bg-brand rounded-full" style={{ width: `${Math.min(p.engagement * 10, 100)}%` }} />
121+
</div>
122+
</div>
123+
<span className="text-sm text-white font-mono w-12 text-right">{p.engagement}%</span>
124+
</div>
125+
))}
126+
</div>
127+
</div>
128+
<div className="bg-surface border border-white/5 rounded-2xl p-6">
129+
<h3 className="text-white font-bold mb-4">Top 5 Clips</h3>
130+
<div className="space-y-3">
131+
{data.top5.map((clip, idx) => (
132+
<div key={clip.clipId} className="flex items-center justify-between">
133+
<div className="min-w-0">
134+
<p className="text-sm text-white font-semibold truncate">{clip.title}</p>
135+
<p className="text-xs text-muted">{clip.platform} · {clip.views.toLocaleString()} views</p>
136+
</div>
137+
<span className="text-xs text-muted w-6 text-right">#{idx + 1}</span>
138+
</div>
139+
))}
140+
</div>
141+
</div>
142+
</div>
143+
</>
144+
) : null}
145+
</div>
146+
</div>
147+
);
148+
}

app/(dashboard)/dashboard/page.tsx

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
import Link from "next/link";
44
import dynamic from "next/dynamic";
55
import StatCard from "@/components/dashboard/StatCard";
6-
import PlatformDistribution from "@/components/dashboard/PlatformDistribution";
76
import AIInsightCard from "@/components/dashboard/AIInsightCard";
87
import ProjectCard from "@/components/dashboard/ProjectCard";
98
import EarningsSummaryCards from "@/components/dashboard/EarningsSummaryCards";
@@ -16,6 +15,7 @@ import DashboardPageHeader from "./DashboardPageHeader";
1615

1716
// Lazy-load heavy client components for better performance
1817
const RevenueChart = dynamic(() => import("@/components/dashboard/RevenueChart"), {
18+
ssr: false,
1919
loading: () => (
2020
<div className="bg-surface border border-border rounded-[24px] p-8 h-[300px] flex items-center justify-center">
2121
<Skeleton className="w-full h-full" />
@@ -24,6 +24,7 @@ const RevenueChart = dynamic(() => import("@/components/dashboard/RevenueChart")
2424
});
2525

2626
const SendPaymentForm = dynamic(() => import("@/components/SendPaymentForm"), {
27+
ssr: false,
2728
loading: () => (
2829
<div className="bg-surface border border-border rounded-[24px] p-8 h-[300px] flex items-center justify-center">
2930
<Skeleton className="w-full h-full" />
@@ -32,13 +33,23 @@ const SendPaymentForm = dynamic(() => import("@/components/SendPaymentForm"), {
3233
});
3334

3435
const WalletHealthCard = dynamic(() => import("@/components/wallet/WalletHealthCard"), {
36+
ssr: false,
3537
loading: () => (
3638
<div className="bg-surface border border-border rounded-[24px] p-8 h-[200px] flex items-center justify-center">
3739
<Skeleton className="w-full h-full" />
3840
</div>
3941
),
4042
});
4143

44+
const PlatformDistribution = dynamic(() => import("@/components/dashboard/PlatformDistribution"), {
45+
ssr: false,
46+
loading: () => (
47+
<div className="bg-surface border border-border rounded-[24px] p-6 h-[300px] flex items-center justify-center">
48+
<Skeleton className="w-full h-full" />
49+
</div>
50+
),
51+
});
52+
4253
function StatCardSkeleton() {
4354
return (
4455
<div className="bg-surface border border-border rounded-[24px] p-8 flex flex-col gap-6">
@@ -56,7 +67,7 @@ function StatCardSkeleton() {
5667

5768
export default function DashboardPage() {
5869
const { publicKey } = useAutoStellarWallet();
59-
const { data, loading, error, retry } = useDashboardData();
70+
const { data, loading, error, retry } = useDashboardData({ enableStreaming: true });
6071
const stats = data?.stats;
6172
const recentProjects = data?.recentProjects ?? [];
6273

app/(dashboard)/referral/page.tsx

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
"use client";
2+
3+
import React, { useEffect, useRef, useState } from "react";
4+
import StatCard from "@/components/dashboard/StatCard";
5+
import Skeleton from "@/components/ui/Skeleton";
6+
import { Copy, Check, Users, DollarSign, Share2 } from "lucide-react";
7+
import analytics from "@/app/lib/analytics";
8+
9+
type ReferralStats = {
10+
code: string;
11+
link: string;
12+
referralCount: number;
13+
totalEarned: number;
14+
};
15+
16+
export default function ReferralPage() {
17+
const [stats, setStats] = useState<ReferralStats | null>(null);
18+
const [loading, setLoading] = useState(true);
19+
const [error, setError] = useState<string | null>(null);
20+
const [copied, setCopied] = useState(false);
21+
const inputRef = useRef<HTMLInputElement>(null);
22+
23+
useEffect(() => {
24+
let cancelled = false;
25+
async function load() {
26+
try {
27+
const res = await fetch("/api/referral");
28+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
29+
const data = (await res.json()) as ReferralStats;
30+
if (!cancelled) setStats(data);
31+
} catch (err) {
32+
if (!cancelled) setError(err instanceof Error ? err.message : "Failed to load referral stats");
33+
} finally {
34+
if (!cancelled) setLoading(false);
35+
}
36+
}
37+
load();
38+
return () => { cancelled = true; };
39+
}, []);
40+
41+
const handleCopy = async () => {
42+
if (!stats) return;
43+
try {
44+
await navigator.clipboard.writeText(stats.link);
45+
setCopied(true);
46+
setTimeout(() => setCopied(false), 1500);
47+
analytics.trackEvent("referral_link_shared", { code: stats.code });
48+
} catch {
49+
inputRef.current?.select();
50+
}
51+
};
52+
53+
const handleShare = () => {
54+
if (!stats) return;
55+
analytics.trackEvent("referral_link_shared", { code: stats.code });
56+
if (navigator.share) {
57+
navigator.share({ title: "Join ClipCash", text: "Use my referral link to sign up", url: stats.link }).catch(() => {});
58+
} else {
59+
handleCopy();
60+
}
61+
};
62+
63+
return (
64+
<div className="max-w-5xl mx-auto px-4 sm:px-6 lg:px-10 py-10">
65+
<div className="space-y-8">
66+
<div>
67+
<h1 className="text-[28px] sm:text-[32px] font-extrabold tracking-tight text-white">Referral Program</h1>
68+
<p className="text-muted text-[14px] mt-1">Share your unique link and earn bonuses when friends join.</p>
69+
</div>
70+
71+
{loading ? (
72+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
73+
{[1, 2, 3].map((i) => (
74+
<div key={i} className="h-32 rounded-2xl bg-white/6 animate-pulse" />
75+
))}
76+
</div>
77+
) : error ? (
78+
<div className="rounded-2xl border border-error/30 bg-error/10 p-6">
79+
<p className="text-error text-sm">{error}</p>
80+
</div>
81+
) : stats ? (
82+
<>
83+
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
84+
<StatCard
85+
label="Referrals"
86+
value={String(stats.referralCount)}
87+
trend={`${stats.referralCount} joined`}
88+
icon={Users}
89+
/>
90+
<StatCard
91+
label="Total Earned"
92+
value={`$${stats.totalEarned.toFixed(2)}`}
93+
trend="Referral bonuses"
94+
icon={DollarSign}
95+
/>
96+
<StatCard
97+
label="Your Code"
98+
value={stats.code}
99+
trend="Unique per user"
100+
icon={Share2}
101+
/>
102+
</div>
103+
104+
<div className="bg-surface border border-white/5 rounded-2xl p-6">
105+
<p className="text-[11px] font-bold text-muted-foreground uppercase tracking-widest mb-2">
106+
Your Referral Link
107+
</p>
108+
<div className="flex flex-col sm:flex-row gap-3">
109+
<input
110+
ref={inputRef}
111+
readOnly
112+
value={stats.link}
113+
className="flex-1 bg-input text-white text-sm rounded-xl px-4 py-3 border border-white/10"
114+
/>
115+
<button
116+
onClick={handleCopy}
117+
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl bg-brand hover:bg-brand-hover text-black font-bold transition-colors"
118+
>
119+
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
120+
{copied ? "Copied" : "Copy"}
121+
</button>
122+
<button
123+
onClick={handleShare}
124+
className="inline-flex items-center gap-2 px-5 py-2.5 rounded-xl border border-white/10 bg-surface hover:bg-input text-white font-bold transition-colors"
125+
>
126+
<Share2 className="w-4 h-4" />
127+
Share
128+
</button>
129+
</div>
130+
</div>
131+
</>
132+
) : null}
133+
</div>
134+
</div>
135+
);
136+
}

0 commit comments

Comments
 (0)