Skip to content

Commit c43e091

Browse files
339monIdrhas
andauthored
feat(campaign): aggregate and feature campaign success stories (#812)
* feat: feat(campaign): Campaign success stories - aggregate and fea (#787) * feat: feat(campaign): Campaign success stories - aggregate and fea (#787) * feat: feat(campaign): Campaign success stories - aggregate and fea (#787) * feat: feat(campaign): Campaign success stories - aggregate and fea (#787) * feat: feat(campaign): Campaign success stories - aggregate and fea (#787) * feat: feat(campaign): Campaign success stories - aggregate and fea (#787) * feat: feat(campaign): Campaign success stories - aggregate and fea (#787) --------- Co-authored-by: Aboogeeky <Idrishassan35@gmail.com>
1 parent 03ad499 commit c43e091

7 files changed

Lines changed: 459 additions & 11 deletions

File tree

apps/web/src/app/(overview)/campaigns/[id]/page.tsx

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
Share2,
1414
Edit,
1515
ShieldCheck,
16+
Trophy,
1617
BarChart3,
1718
Globe,
1819
AlertTriangle,
@@ -94,6 +95,15 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
9495
impactStatement: "Permanently offset 150 metric tons of CO2 while securing habitat for 200+ endangered species.",
9596
beneficiaries: "5,000 local indigenous community members",
9697
co2OffsetTons: "150",
98+
successStory: {
99+
headline: "From Rainforest Pledge to On-the-Ground Impact",
100+
creatorInterview: "Every XLM stream is tied to verifiable patrol hours and backers receive monthly GPS updates. The team shipped on every promise.",
101+
backerTestimonials: [
102+
{ name: "Marta L.", location: "Lisbon, Portugal", quote: "I could see exactly where my contribution went." },
103+
{ name: "Devon K.", location: "Austin, TX", quote: "You can tell this is a team that ships." },
104+
{ name: "Priya N.", location: "Bengaluru, India", quote: "More campaigns should publish stories like this." },
105+
],
106+
},
97107
treesPlanted: "1,500",
98108
};
99109

@@ -290,6 +300,7 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
290300
{/* Main Content Tabs (Overview, Sponsor Wall #724, Co-Creators #722, Series, Analytics) */}
291301
{/* Backer community spaces (#788) render inside the overview sidebar. */}
292302
<Tabs value={activeTab} onValueChange={setActiveTab} className="w-full space-y-6">
303+
<TabsList className="grid w-full grid-cols-4 bg-zinc-900 border border-zinc-800 p-1 rounded-xl">
293304
<TabsList className="grid w-full grid-cols-5 bg-zinc-900 border border-zinc-800 p-1 rounded-xl">
294305
<TabsTrigger value="overview" className="text-xs font-semibold data-[state=active]:bg-purple-600 data-[state=active]:text-white">
295306
<Target className="mr-1.5 h-4 w-4" /> Overview & Story
@@ -300,6 +311,8 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
300311
<TabsTrigger value="collaboration" className="text-xs font-semibold data-[state=active]:bg-purple-600 data-[state=active]:text-white">
301312
<Users className="mr-1.5 h-4 w-4 text-purple-400" /> Co-Creators (#722)
302313
</TabsTrigger>
314+
<TabsTrigger value="success" className="text-xs font-semibold data-[state=active]:bg-purple-600 data-[state=active]:text-white">
315+
<Trophy className="mr-1.5 h-4 w-4 text-amber-400" /> Success Story
303316
<TabsTrigger value="series" className="text-xs font-semibold data-[state=active]:bg-purple-600 data-[state=active]:text-white">
304317
<Sparkles className="mr-1.5 h-4 w-4 text-amber-400" /> Series & Sequels
305318
</TabsTrigger>
@@ -368,6 +381,35 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
368381
<TabsContent value="collaboration">
369382
<CampaignCollaboration campaignId={campaign.id} campaignTitle={campaign.title} />
370383
</TabsContent>
384+
{/* Tab 4: Success Story */}
385+
<TabsContent value="success" className="space-y-6">
386+
<div className="relative overflow-hidden rounded-2xl border border-emerald-800/60 bg-gradient-to-br from-emerald-950/40 via-zinc-900 to-zinc-900 p-6 md:p-8">
387+
<div className="flex items-center gap-2">
388+
<Badge className="bg-emerald-600 text-white font-semibold text-xs">
389+
<Trophy className="mr-1 h-3 w-3" /> Featured Success Story
390+
</Badge>
391+
</div>
392+
<h3 className="text-2xl font-extrabold text-zinc-50 mt-4">{campaign.successStory.headline}</h3>
393+
<div className="mt-4 space-y-2 rounded-lg border border-zinc-800 bg-zinc-950/40 p-4">
394+
<p className="text-xs font-bold uppercase tracking-wider text-purple-400">Creator Interview</p>
395+
<p className="text-sm text-zinc-300 leading-relaxed">{campaign.successStory.creatorInterview}</p>
396+
</div>
397+
</div>
398+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/60 p-6 space-y-4">
399+
<h4 className="text-sm font-bold text-zinc-100 flex items-center gap-2">
400+
<Users className="h-4 w-4 text-purple-400" /> Backer Testimonials
401+
</h4>
402+
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
403+
{campaign.successStory.backerTestimonials.map((testimonial) => (
404+
<figure key={testimonial.name} className="rounded-xl border border-zinc-800 bg-zinc-950/60 p-4">
405+
<blockquote className="text-sm text-zinc-300 leading-relaxed">"{testimonial.quote}"</blockquote>
406+
<figcaption className="mt-3 text-xs text-zinc-400">
407+
<strong className="text-zinc-200">{testimonial.name}</strong> · {testimonial.location}
408+
</figcaption>
409+
</figure>
410+
))}
411+
</div>
412+
</div>
371413

372414
{/* Tab 4: Series & Sequels */}
373415
<TabsContent value="series">
@@ -435,4 +477,4 @@ export default function CampaignDetailPage({ params }: { params: Promise<{ id: s
435477
)}
436478
</div>
437479
);
438-
}
480+
}

apps/web/src/app/(overview)/campaigns/page.tsx

Lines changed: 20 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Link from "next/link";
55
import { Rocket, Plus, Heart, Users, Sparkles, ShieldCheck, ChevronRight, Trophy, Scale, ShoppingBag } from "lucide-react";
66
import { Button } from "@/components/ui/button";
77
import { Badge } from "@/components/ui/badge";
8+
import SuccessStories from "@/components/modules/campaign/success-stories/SuccessStories";
89

910
export default function CampaignsDirectoryPage() {
1011
const campaigns = [
@@ -36,6 +37,8 @@ export default function CampaignsDirectoryPage() {
3637

3738
return (
3839
<div className="container mx-auto px-4 py-8 max-w-6xl space-y-8">
40+
<!-- Header -->
41+
<div className="flex flex-col sm-flex-row sm-items-center sm-justify-between gap-4 border-b border-zinc-800 pb-6">
3942
{/* Header */}
4043
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4 border-b border-zinc-800 pb-6">
4144
<div>
@@ -48,6 +51,11 @@ export default function CampaignsDirectoryPage() {
4851
</p>
4952
</div>
5053

54+
<Link href="/campaigns/create">
55+
<Button className="bg-gradient-to-r from-purple-600 to-blue-600 font-semibold text-white hover-from-purple-700 hover-to-blue-700 shadow-lg shadow-purple-900/30">
56+
<Plus className="mr-2 h-4 w-4" /> Create Campaign Wizard (#720)
57+
</Button>
58+
</Link>
5159
<div className="flex flex-wrap items-center gap-2">
5260
<Link href="/campaigns/compare">
5361
<Button variant="outline" className="border-purple-800 bg-purple-950/40 text-purple-300 hover:bg-purple-900/60 font-semibold text-xs">
@@ -75,14 +83,18 @@ export default function CampaignsDirectoryPage() {
7583
</div>
7684
</div>
7785

78-
{/* Campaigns Grid */}
79-
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
86+
<!-- Success Stories Section -->
87+
<SuccessStories />
88+
89+
<!-- Campaigns Grid -->
90+
<div className="grid grid-cols-1 md-grid-cols-2 gap-6">
8091
{campaigns.map((c) => {
8192
const progress = Math.round((parseFloat(c.raisedAmount.replace(/,/g, "")) / parseFloat(c.goalAmount.replace(/,/g, ""))) * 100);
8293
return (
8394
<div
8495
key={c.id}
85-
className="group flex flex-col justify-between rounded-xl border border-zinc-800 bg-zinc-900/80 p-6 shadow-xl transition-all duration-300 hover:border-purple-500/50 hover:shadow-2xl"
96+
className="group flex flex-col justify-between rounded-xl border border-zinc-800 bg-zinc-900/80 p-6 shadow-xl transition-all duration-300 hover:border-purple-500/50
97+
hover:shadow-2xl"
8698
>
8799
<div className="space-y-3">
88100
<div className="flex items-center justify-between">
@@ -106,11 +118,11 @@ export default function CampaignsDirectoryPage() {
106118
<span className="text-emerald-400 font-bold">{progress}% Goal</span>
107119
</div>
108120

109-
{/* Progress bar */}
121+
<!-- Progress bar -->
110122
<div className="h-1.5 w-full overflow-hidden rounded-full bg-zinc-800">
111123
<div
112124
className="h-full rounded-full bg-gradient-to-r from-purple-500 to-emerald-400"
113-
style={{ width: `${progress}%` }}
125+
style={ width: `${progress}%` }
114126
/>
115127
</div>
116128

@@ -126,8 +138,8 @@ export default function CampaignsDirectoryPage() {
126138
</span>
127139
</div>
128140

129-
<Link href={`/campaigns/${c.id}`}>
130-
<Button size="sm" variant="ghost" className="text-xs text-purple-400 hover:text-purple-300 hover:bg-purple-950/40">
141+
<Link href={"/campaigns/${c.id}"}>
142+
<Button size="sm" variant="ghost" className="text-xs text-purple-400 hover-text-purple-300 hover-bg-purple-950/40">
131143
View Campaign <ChevronRight className="ml-1 h-3.5 w-3.5" />
132144
</Button>
133145
</Link>
@@ -139,4 +151,4 @@ export default function CampaignsDirectoryPage() {
139151
</div>
140152
</div>
141153
);
142-
}
154+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { NextResponse } from 'next/server';
2+
3+
// Mock data for campaign success stories
4+
// In a real implementation, this would come from a database or external service
5+
const successStories = [
6+
{
7+
id: 'story-001',
8+
campaignId: 'camp-101',
9+
title: 'Save the Amazon RainForest Reserve',
10+
category: 'Environmental & Reforestation',
11+
raisedAmount: '33,850',
12+
goalAmount: '50,000',
13+
token: 'XLM',
14+
sponsorCount: 6,
15+
collaboratorCount: 2,
16+
status: 'COMPLETED',
17+
description: 'Despite not reaching the full goal, the campaign successfully purchased 25,000 hectares of critical rainforest and implemented the first phase of community-led guardianship.',
18+
coverImageUrl: '/images/success-stories/amazon.jpg',
19+
creatorInterview: {
20+
id: 'int-001',
21+
quote: 'We proved that small-scale grassroots funding can make a real impact. The backers faith kept us going, and we delivered on our promise to protect the land.',
22+
author: 'Maria Alvarez',
23+
role: 'Lead Organizer, Amazon Reserve Initiative',
24+
avatarUrl: '/images/avatars/maria.jpg',
25+
},
26+
backerTestimonials: [
27+
{
28+
id: 'test-001',
29+
author: 'John Doe',
30+
content: 'Been part of this journey was incredible. The team kept us updated every step of the way and the land is now protected.',
31+
rating: 5,
32+
{
33+
id: 'test-002',
34+
author: 'Jane Smith',
35+
content: 'I’ve funded many campaigns, but this one truly shipped. Transparency and results exceeded my expectations.',
36+
rating: 5,
37+
},
38+
],
39+
},
40+
{
41+
id: 'story-002',
42+
campaignId: 'camp-102',
43+
title: 'Clean Water Wells for Sub-Saharan Communities',
44+
category: 'Community & Social Impact',
45+
raisedAmount: '18,200',
46+
goalAmount: '25,000',
47+
token: 'USDC',
48+
sponsorCount: 14,
49+
collaboratorCount: 3,
50+
status: 'SUCCESS',
51+
description: 'Successfully installed 8 of the 12 planned solar-powered water filtration wells, bringing clean water to over 5,000 people.',
52+
coverImageUrl: '/images/success-stories/water-wells.jpg',
53+
creatorInterview: {
54+
id: 'int-002',
55+
quote: 'The community support was overwhelming. We were able to scale our initial plan and now villages have access to safe drinking water.',
56+
author: 'David Ochieng',
57+
role: 'Project Director, Water for All',
58+
avatarUrl: '/images/avatars/david.jpg',
59+
},
60+
backerTestimonials: [
61+
{
62+
id: 'test-003',
63+
author: 'Alice Brown',
64+
content: 'I’m proud to support a campaign that delivered tangible results. The photos and stories from the field were heartwarming.',
65+
rating: 5,
66+
},
67+
],
68+
},
69+
];
70+
71+
export async function GET() {
72+
return NextResponse.json(successStories);
73+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
"use client";
2+
3+
import React, { useEffect, useState } from "react";
4+
import { Trophy, Star, Quote, Users, Heart } from "lucide-react";
5+
import { Badge } from "@/components/ui/badge";
6+
import { getSuccessStories, SuccessStory } from "@/services/campaign-success.service";
7+
8+
export default function SuccessStories() {
9+
const [stories, setStories] = useState<SuccessStory[]>([]);
10+
const [loading, setLoading] = useState(true);
11+
const [error, setError] = useState<string | null>(null);
12+
13+
useEffect(() => {
14+
const fetchStories = async () => {
15+
try {
16+
const data = await getSuccessStories();
17+
setStories(data);
18+
} catch (err) {
19+
setError(err instanceof Error ? err.message : "Failed to load success stories");
20+
} finally {
21+
setLoading(false);
22+
}
23+
};
24+
fetchStories();
25+
}, []);
26+
27+
if (loading) {
28+
return (
29+
<div className="rounded-xl border border-zinc-800 bg-zinc-900/80 p-6 shadow-xl animate-pulse">
30+
<div className="h-6 w-40 bg-zinc-800 rounded mb-4" />
31+
<div className="grid md-grid-cols-2 gap-6">
32+
<div className="h-48 bg-zinc-800 rounded" />
33+
<div className="h-48 bg-zinc-800 rounded" />
34+
</div>
35+
</div>
36+
);
37+
}
38+
39+
if (error) {
40+
return (
41+
<div className="rounded-xl border border-red-800/50 bg-red-950/20 p-6 text-red-300">
42+
<p className="text-sm font-semibold">Error loading success stories:</p>
43+
<p className="text-xs mt-1">{error}</p>
44+
</div>
45+
);
46+
}
47+
48+
if (stories.length === 0) {
49+
return null; // or some fallback
50+
}
51+
52+
return (
53+
<section className="space-y-6">
54+
<div className="border-b border-zinc-800 pb-4">
55+
<h2 className="text-2xl font-extrabold text-zinc-50 tracking-tight flex items-center gap-2">
56+
<Trophy className="h-6 w-6 text-amber-400" />
57+
Success Stories</h2>
58+
<p className="mt-1 text-sm text-zinc-400">
59+
Campaigns that shipped and delivered real impact. Hear directly from creators and backers.
60+
</p>
61+
</div>
62+
</div>
63+
64+
<div className="grid grid-cols-1 md-grid-cols-2 gap-6">
65+
{stories.map((story) => {
66+
const progress = Math.round(
67+
(parseFloat(story.raisedAmount.replace(/,/g, "")) /
68+
parseFloat(story.goalAmount.replace(/,/g, "")) ) *
69+
100
70+
);
71+
72+
return (
73+
<article
74+
key={story.id}
75+
className="flex flex-col rounded-xl border border-emerald-500/30 bg-zinc-900/80 p-6 shadow-xl transition-all duration-300 hover:border-emerald-400/50 hover-shadow-2xl"
76+
>
77+
<div className="flex items-start justify-between gap-4">
78+
<div className="space-y-2">
79+
<Badge className="bg-emerald-950/60 text-emerald-300 border-emerald-800 text-[11px]">
80+
{story.category}
81+
</Badge>
82+
<h3 className="text-xl font-bold text-zinc-100">{story.title}</h3>
83+
<p className="text-xs text-zinc-400 line-clamp-3 leading-relaxed">
84+
{story.description}
85+
</p>
86+
</div>
87+
</div>
88+
89+
<div className="mt-4 grid grid-cols-2 grid-gap-4 text-xs">
90+
<div className="rounded-lg bg-zinc-800/50 p-3">
91+
<p className="text-zinc-500">Raised</p>
92+
<p className="font-bold text-zinc-100">
93+
{story.raisedAmount} {story.token}
94+
</p>
95+
</div>
96+
<div className="rounded-lg bg-zinc-800/50 p-3">
97+
<p className="text-zinc-500">Progress</p>
98+
<p className="font-bold text-emerald-40">{progress}%</p>
99+
</div>
100+
</div>
101+
102+
<div className="mt-4 space-y-2 rounded-lg border border-purple-500/20 bg-purple-950/20 p-4">
103+
<div className="flex items-center gap-2 text-purple-300 text-xs font-semibold uppercase tracking-wide">
104+
<Quote className="h-3.5 w-3.5" /> Creator Interview
105+
</div>
106+
<blockquote className="text-sm text-zinc-300 italic leading-relaxed">
107+
{story.creatorInterview.quote}
108+
</blockquote>
109+
<figcaption className="text-xs text-zinc-500">
110+
{story.creatorInterview.author}, {story.creatorInterview.role}
111+
</figcaption>
112+
</div>
113+
114+
<div className="mt-4 space-y-3">
115+
<div className="flex items-center gap-2 text-amber-300 text-xs font-semibold uppercase tracking-wide">
116+
<Users className="h-3.5 w-3.5" /> Backer Testimonials
117+
</div>
118+
{story.backerTestimonials.slice(0, 2).map((testimonial) => (
119+
<div key={testimonial.id} className="rounded-lg bg-zinc-800/30 p-3 text-sm">
120+
<div className="flex items-center justify-between">
121+
<span className="font-semibold text-zinc-300">{testimonial.author}</span>
122+
<div className="flex items-center gap-0.5">
123+
{Array.from({length: testimonial.rating ?? 5}).map((_, i) => (
124+
<Star key={i} className="h-3 w-3 fill-amber-400 text-amber-400" />
125+
))}
126+
</div>
127+
</div>
128+
<p className="mt-1 text-xs text-zinc-400 leading-relaxed">{testimonial.content}</p>
129+
</div>
130+
))}
131+
</div>
132+
</article>
133+
);
134+
})}
135+
</div>
136+
</section>
137+
);
138+
}

0 commit comments

Comments
 (0)