Skip to content

Commit 5ca04ae

Browse files
authored
Merge pull request #857 from favourawaku/main
Implemented four major features across mobile, backend, and frontend
2 parents f8c4f72 + d695e89 commit 5ca04ae

14 files changed

Lines changed: 1563 additions & 113 deletions

File tree

app/compare/page.tsx

Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
'use client';
2+
3+
import { useSearchParams, useRouter } from 'next/navigation';
4+
import { useEffect, useState } from 'react';
5+
import { Header } from '@/components/header';
6+
import { Footer } from '@/components/footer';
7+
import { Button } from '@/components/ui/button';
8+
import { ChevronLeft } from 'lucide-react';
9+
import { creators as allCreators, Creator } from '@/lib/creators-data';
10+
11+
export default function ComparePage() {
12+
const searchParams = useSearchParams();
13+
const router = useRouter();
14+
const [comparedCreators, setComparedCreators] = useState<Creator[]>([]);
15+
const [isLoading, setIsLoading] = useState(true);
16+
17+
useEffect(() => {
18+
const ids = searchParams.get('ids');
19+
if (!ids) {
20+
router.push('/creators');
21+
return;
22+
}
23+
24+
const idList = ids.split(',').filter(Boolean);
25+
const matched = allCreators.filter((c) => idList.includes(c.id));
26+
27+
if (matched.length < 2) {
28+
router.push('/creators');
29+
return;
30+
}
31+
32+
setComparedCreators(matched);
33+
setIsLoading(false);
34+
}, [searchParams, router]);
35+
36+
if (isLoading) {
37+
return (
38+
<div className="min-h-screen flex items-center justify-center">
39+
<p className="text-muted-foreground">Loading comparison...</p>
40+
</div>
41+
);
42+
}
43+
44+
// Calculate skills overlap
45+
const allSkills = Array.from(
46+
new Set(comparedCreators.flatMap((c) => c.skills || []))
47+
);
48+
49+
const getSkillOverlap = (skill: string) => {
50+
return comparedCreators.filter((c) => c.skills?.includes(skill)).length;
51+
};
52+
53+
const getSharedSkills = () => {
54+
return allSkills.filter((skill) => getSkillOverlap(skill) === comparedCreators.length);
55+
};
56+
57+
const sharedSkills = getSharedSkills();
58+
59+
return (
60+
<div className="min-h-screen flex flex-col bg-background">
61+
<Header />
62+
63+
<main className="flex-grow">
64+
{/* Header */}
65+
<section className="border-b border-border bg-muted/30 py-8">
66+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
67+
<div className="flex items-center gap-4 mb-4">
68+
<Button
69+
variant="ghost"
70+
size="sm"
71+
onClick={() => router.back()}
72+
className="gap-2"
73+
>
74+
<ChevronLeft size={16} />
75+
Back
76+
</Button>
77+
</div>
78+
<h1 className="text-4xl font-bold text-foreground mb-2">
79+
Creator Comparison
80+
</h1>
81+
<p className="text-lg text-muted-foreground">
82+
Side-by-side comparison of {comparedCreators.length} creators
83+
</p>
84+
</div>
85+
</section>
86+
87+
{/* Comparison Table */}
88+
<section className="py-12">
89+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
90+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
91+
{comparedCreators.map((creator) => (
92+
<div
93+
key={creator.id}
94+
className="bg-card border border-border rounded-lg overflow-hidden"
95+
>
96+
{/* Creator Header */}
97+
<div className="aspect-video bg-gradient-to-br from-primary/20 to-accent/20 overflow-hidden">
98+
{creator.coverImage && (
99+
<img
100+
src={creator.coverImage}
101+
alt={creator.name}
102+
className="w-full h-full object-cover"
103+
/>
104+
)}
105+
</div>
106+
107+
<div className="p-6">
108+
{/* Name & Title */}
109+
<h3 className="text-xl font-bold text-foreground mb-1">
110+
{creator.name}
111+
</h3>
112+
<p className="text-sm text-muted-foreground mb-4">
113+
{creator.title}
114+
</p>
115+
116+
{/* Stats */}
117+
{creator.stats && (
118+
<div className="grid grid-cols-3 gap-3 mb-6 py-4 border-y border-border">
119+
<div className="text-center">
120+
<div className="text-lg font-bold text-primary">
121+
{creator.stats.projects}
122+
</div>
123+
<div className="text-xs text-muted-foreground">
124+
Projects
125+
</div>
126+
</div>
127+
<div className="text-center">
128+
<div className="text-lg font-bold text-primary">
129+
{creator.stats.clients}
130+
</div>
131+
<div className="text-xs text-muted-foreground">
132+
Clients
133+
</div>
134+
</div>
135+
<div className="text-center">
136+
<div className="text-lg font-bold text-primary">
137+
{creator.stats.experience}y
138+
</div>
139+
<div className="text-xs text-muted-foreground">
140+
Experience
141+
</div>
142+
</div>
143+
</div>
144+
)}
145+
146+
{/* Rating */}
147+
{creator.rating !== undefined && (
148+
<div className="mb-4">
149+
<p className="text-xs text-muted-foreground font-semibold uppercase mb-2">
150+
Rating
151+
</p>
152+
<div className="flex items-center gap-2">
153+
<span className="text-lg font-bold text-primary">
154+
{creator.rating}
155+
</span>
156+
<span className="text-xs text-muted-foreground">
157+
({creator.rating === 0 ? 'No' : creator.rating} reviews)
158+
</span>
159+
</div>
160+
</div>
161+
)}
162+
163+
{/* Response Time */}
164+
<div className="mb-4">
165+
<p className="text-xs text-muted-foreground font-semibold uppercase mb-2">
166+
Response Time
167+
</p>
168+
<p className="text-sm font-medium text-foreground">
169+
Within 24 hours
170+
</p>
171+
</div>
172+
173+
{/* Price Range */}
174+
{creator.hourlyRate && (
175+
<div className="mb-4">
176+
<p className="text-xs text-muted-foreground font-semibold uppercase mb-2">
177+
Hourly Rate
178+
</p>
179+
<p className="text-sm font-medium text-foreground">
180+
${creator.hourlyRate}/hour
181+
</p>
182+
</div>
183+
)}
184+
185+
{/* Availability */}
186+
<div className="mb-6">
187+
<p className="text-xs text-muted-foreground font-semibold uppercase mb-2">
188+
Availability
189+
</p>
190+
<p className="text-sm font-medium text-green-600">
191+
✓ Available
192+
</p>
193+
</div>
194+
195+
{/* Skills */}
196+
<div className="mb-6">
197+
<p className="text-xs text-muted-foreground font-semibold uppercase mb-3">
198+
Skills
199+
</p>
200+
<div className="flex flex-wrap gap-2">
201+
{creator.skills?.map((skill) => (
202+
<span
203+
key={skill}
204+
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium transition-colors ${
205+
sharedSkills.includes(skill)
206+
? 'bg-green-100 text-green-800'
207+
: 'bg-secondary text-secondary-foreground'
208+
}`}
209+
>
210+
{skill}
211+
{sharedSkills.includes(skill) && ' ✓'}
212+
</span>
213+
))}
214+
</div>
215+
</div>
216+
217+
{/* Hire Button */}
218+
<Button
219+
className="w-full"
220+
onClick={() => router.push(`/creators/${creator.id}`)}
221+
>
222+
View Profile & Hire
223+
</Button>
224+
</div>
225+
</div>
226+
))}
227+
</div>
228+
229+
{/* Skills Summary */}
230+
{sharedSkills.length > 0 && (
231+
<div className="mt-12 bg-green-50 border border-green-200 rounded-lg p-6">
232+
<h3 className="text-lg font-bold text-green-900 mb-3">
233+
Shared Skills
234+
</h3>
235+
<div className="flex flex-wrap gap-2">
236+
{sharedSkills.map((skill) => (
237+
<span
238+
key={skill}
239+
className="inline-flex items-center px-3 py-1 rounded-full bg-green-200 text-green-800 text-sm font-medium"
240+
>
241+
{skill}
242+
</span>
243+
))}
244+
</div>
245+
</div>
246+
)}
247+
</div>
248+
</section>
249+
</main>
250+
251+
<Footer />
252+
</div>
253+
);
254+
}

backend/src/router.ts

Lines changed: 56 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { z } from 'zod';
22
import { protectedProcedure, publicProcedure, router } from './trpc-setup';
33
import { prisma } from '@/lib/prisma';
4+
import jwt from 'jsonwebtoken';
45

56
// Root router with Prisma-backed queries
67
export const appRouter = router({
@@ -320,7 +321,7 @@ export const appRouter = router({
320321
.query(async ({ ctx, input }) => {
321322
// Get user's analytics data
322323
const user = ctx.user!;
323-
324+
324325
// This would calculate real metrics from bounties, applications, etc.
325326
return {
326327
earnings: {
@@ -341,6 +342,60 @@ export const appRouter = router({
341342
};
342343
}),
343344
}),
345+
346+
// Identity/ZK endpoints
347+
identity: router({
348+
verifyZk: publicProcedure
349+
.input(
350+
z.object({
351+
proof: z.record(z.unknown()),
352+
publicInputs: z.record(z.unknown()),
353+
nullifier: z.string(),
354+
})
355+
)
356+
.mutation(async ({ input }) => {
357+
// Check if nullifier has already been used (replay protection)
358+
const existingNullifier = await prisma.zkNullifier.findUnique({
359+
where: { nullifier: input.nullifier },
360+
});
361+
362+
if (existingNullifier) {
363+
throw new Error('Proof already used');
364+
}
365+
366+
// Verify the proof (simplified: in production, call the Stellar contract or off-chain verifier)
367+
// For now, accept any proof with non-empty public inputs
368+
const publicInputs = input.publicInputs;
369+
if (!publicInputs || Object.keys(publicInputs).length === 0) {
370+
throw new Error('Invalid proof');
371+
}
372+
373+
// Store the nullifier to prevent replay
374+
await prisma.zkNullifier.create({
375+
data: {
376+
nullifier: input.nullifier,
377+
createdAt: new Date(),
378+
},
379+
});
380+
381+
// Issue a short-lived JWT with ZK verification claim
382+
const JWT_SECRET = process.env.JWT_SECRET || 'dev-secret-key';
383+
const token = jwt.sign(
384+
{
385+
zk_verified: true,
386+
claim: 'age_18+',
387+
iat: Math.floor(Date.now() / 1000),
388+
exp: Math.floor(Date.now() / 1000) + 86400, // 24 hours
389+
},
390+
JWT_SECRET
391+
);
392+
393+
return {
394+
token,
395+
expiresIn: 86400,
396+
};
397+
}),
398+
}),
344399
});
345400

346401
export type AppRouter = typeof appRouter;

0 commit comments

Comments
 (0)