Skip to content

Commit a84594f

Browse files
authored
Merge pull request #3265 from Voyager-Ship/voyager-ship/improvements
[feat]: enhance medal components and refactor reward UI layout
2 parents c6bc78b + 22afbfd commit a84594f

File tree

13 files changed

+939
-90
lines changed

13 files changed

+939
-90
lines changed

app/api/projects/export/route.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { withAuthRole } from "@/lib/protectedRoute";
2+
import { exportShowcase } from "@/server/services/exportShowcase";
3+
import { NextRequest, NextResponse } from "next/server";
4+
5+
export const POST = withAuthRole('hackathonCreator', async (req: NextRequest) => {
6+
try {
7+
const body = await req.json();
8+
const buffer = await exportShowcase(body);
9+
if (!buffer) {
10+
return NextResponse.json(
11+
{ message: 'no projects found' },
12+
{ status: 404 }
13+
);
14+
}
15+
return new NextResponse(buffer, {
16+
headers: { 'Content-Type': 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' },
17+
});
18+
} catch (error: any) {
19+
console.error('Error POST /api/projects/export:', error.message);
20+
const wrappedError = error as Error;
21+
return NextResponse.json(
22+
{
23+
error: {
24+
message: wrappedError.message,
25+
stack: wrappedError.stack,
26+
cause: wrappedError.cause,
27+
name: wrappedError.name
28+
}
29+
},
30+
{ status: wrappedError.cause == 'ValidationError' ? 400 : 500 }
31+
);
32+
}
33+
});

components/profile/reward-board/component/auto-rotate-badge.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React, { useRef } from "react";
2+
import React, { useEffect, useRef, useState } from "react";
33
import { useFrame } from "@react-three/fiber";
44
import * as THREE from "three";
55
import { BackFace } from "./back-face";
@@ -20,6 +20,14 @@ export function AutoRotateMedal({
2020
speed?: number;
2121
}) {
2222
const groupRef = useRef<THREE.Group | null>(null);
23+
const badgeDefaultImage = "/wolfie/wolfie-hack.png";
24+
const [badgeImage, setBadgeImage] = useState(badgeDefaultImage);
25+
26+
useEffect(() => {
27+
if (image && image !== '') {
28+
setBadgeImage(image);
29+
}
30+
}, [image]);
2331

2432
useFrame((state, delta) => {
2533
const g = groupRef.current;
@@ -32,7 +40,7 @@ export function AutoRotateMedal({
3240
return (
3341
<group ref={groupRef}>
3442
<CircularFrame color="#999B9B" />
35-
<ImageDisc url={image} isUnlocked={!!is_unlocked} Disc={Disc} />
43+
<ImageDisc url={badgeImage} isUnlocked={!!is_unlocked} Disc={Disc} />
3644
<BackFace name={name} description={description} DISC={Disc} />
3745
</group>
3846
);

components/profile/reward-board/component/requirement-panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ export function RequirementsPanel({
2929
</h3>
3030
</div>
3131

32-
<div className="px-6 pb-6 h-35 overflow-y-auto">
32+
<div className="px-6 pb-6 ">
3333
<ul className="text-left ">
3434
{requirements.map((requirement) => (
3535
<li

components/profile/reward-board/component/reward-board.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,17 +6,18 @@ import { getRewardBoard } from "@/server/services/rewardBoard";
66
import { Separator } from "@/components/ui/separator";
77
import { Badge, UserBadge } from "@/types/badge";
88
import { getAllBadges } from "@/server/services/badge";
9+
import Link from "next/link";
910

1011
export default async function RewardBoard() {
1112
const session = await getAuthSession();
1213
const user_id = session?.user.id;
1314
if (!user_id) {
1415
return <div>Loading...</div>;
1516
}
16-
const userBadges:UserBadge[] = await getRewardBoard(user_id);
17+
const userBadges: UserBadge[] = await getRewardBoard(user_id);
1718
const badges = await getAllBadges();
18-
const academyBadges = badges.filter((badge) => badge.category == "academy");
19-
const hackathonBadges:Badge[] = badges.filter((badge) => badge.category == "hackathon");
19+
const academyBadges = badges.filter((badge) => badge.category == "academy")?.sort((a, b) => a.id.localeCompare(b.id));
20+
const hackathonBadges: Badge[] = badges.filter((badge) => badge.category == "hackathon")?.sort((a, b) => a.id.localeCompare(b.id));
2021
const totalPoints = userBadges.reduce((acc, userBadge) => acc + userBadge.points, 0);
2122
const hackathonBadgesUnlocked = hackathonBadges.map((badge) => {
2223
const userBadge = userBadges.find((userBadge) => userBadge.badge_id == badge.id);
@@ -25,16 +26,16 @@ export default async function RewardBoard() {
2526
is_unlocked: !!userBadge,
2627
requirements: userBadge?.requirements || badge.requirements,
2728
};
28-
}).sort(element=>element.is_unlocked ? -1 : 1);
29-
29+
});
30+
3031
const academyBadgesUnlocked = academyBadges.map((badge) => {
3132
const userBadge = userBadges.find((userBadge) => userBadge.badge_id == badge.id);
3233
return {
3334
...badge,
3435
is_unlocked: !!userBadge,
3536
requirements: userBadge?.requirements || badge.requirements,
3637
};
37-
}).sort(element=>element.is_unlocked ? -1 : 1);
38+
}).sort(element => element.is_unlocked ? -1 : 1);
3839
const rewards = hackathonBadgesUnlocked.map((reward) => (
3940
<RewardCard
4041
key={reward.name}
@@ -50,7 +51,7 @@ export default async function RewardBoard() {
5051
/>
5152
));
5253
const academyRewards = academyBadgesUnlocked.map((reward) => (
53-
54+
5455
<RewardCard
5556
key={reward.name}
5657
icon={reward.image_path}
@@ -61,14 +62,14 @@ export default async function RewardBoard() {
6162
requirements={reward.requirements}
6263
id={reward.id}
6364
is_unlocked={reward.is_unlocked}
64-
65+
6566
/>
6667
));
6768

6869
return (
6970
<div className="container mx-auto px-4 sm:px-6 lg:px-8 py-4 sm:py-6 lg:py-8">
7071
<div>
71-
72+
7273

7374
<div className="flex flex-col gap-4 sm:gap-6 mb-2 sm:mb-4">
7475
<div className="flex justify-between items-center">
@@ -86,7 +87,8 @@ export default async function RewardBoard() {
8687
{rewards.length === 0 ? (
8788
<div className="text-center py-12">
8889
<div className="text-gray-500 dark:text-gray-400 text-lg">
89-
No rewards available yet. Keep contributing to earn rewards!
90+
91+
<Link href="/hackathons" className="text-blue-500 hover:text-blue-700"> Your contributions matter. Explore our dev events here </Link>
9092
</div>
9193
</div>
9294
) : (
@@ -96,7 +98,7 @@ export default async function RewardBoard() {
9698
)}
9799
</div>
98100

99-
<div className="flex flex-col gap-4 sm:gap-6 mb-2 mt-8 ">
101+
<div className="flex flex-col gap-4 sm:gap-6 mb-2 mt-3 ">
100102
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold text-gray-900 dark:text-white">
101103
Academy Badges
102104
</h1>
@@ -105,7 +107,7 @@ export default async function RewardBoard() {
105107
{academyRewards.length === 0 ? (
106108
<div className="text-center py-12">
107109
<div className="text-gray-500 dark:text-gray-400 text-lg">
108-
Your contributions matter. Keep going to start earning rewards!
110+
<Link href="/hackathons" className="text-blue-500 hover:text-blue-700"> Your contributions matter. Explore our dev events here </Link>
109111
</div>
110112
</div>
111113
) : (

components/profile/reward-board/component/reward-card.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ export const RewardCard = ({
3030
<div
3131
style={{ width: "100%", height: 300, cursor: "pointer" }}
3232
onClick={() => setOpen(true)}
33-
title="details"
3433
>
3534
<Canvas
3635
shadows={false}
@@ -94,7 +93,7 @@ export const RewardCard = ({
9493
</div>
9594

9695
{requirements && requirements.length > 0 && (
97-
<div className="w-full">
96+
<div >
9897
<RequirementsPanel requirements={requirements as any} />
9998
</div>
10099
)}

components/profile/reward-board/component/static-metal.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
"use client";
2-
import React, { } from "react";
2+
import React, { useEffect, useState } from "react";
33
import { CircularFrame } from "./circular-frame";
44
import { ImageDisc } from "./image-disc";
55

@@ -13,10 +13,19 @@ export function StaticMedal({
1313
is_unlocked?: boolean;
1414
Disc: { radius: number; segments: number }
1515
}) {
16+
const badgeDefaultImage = "/wolfie/wolfie-hack.png";
17+
const [badgeImage, setBadgeImage] = useState(badgeDefaultImage);
18+
19+
useEffect(() => {
20+
if (image && image !== '') {
21+
setBadgeImage(image);
22+
}
23+
}, [image]);
24+
1625
return (
1726
<group rotation={[0,0,0]}>
1827
<CircularFrame color="#999B9B" />
19-
<ImageDisc url={image} isUnlocked={!!is_unlocked} Disc={Disc} />
28+
<ImageDisc url={badgeImage} isUnlocked={!!is_unlocked} Disc={Disc} />
2029
</group>
2130
);
2231
}

components/showcase/ShowCaseCard.tsx

Lines changed: 42 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,12 @@ import {
2525
} from '../ui/pagination';
2626
import React from 'react';
2727
import { ProjectCard } from './ProjectCard';
28-
import Link from 'next/link';
2928
import { ProjectFilters } from '@/types/project';
3029
import { useRouter } from 'next/navigation';
3130
import { HackathonHeader } from '@/types/hackathons';
31+
import { useExports } from './hooks/useExports';
32+
import { LoadingButton } from '../ui/loading-button';
33+
import { useSession } from 'next-auth/react';
3234
const tracks = ['AI', 'DeFi', 'RWA', 'Gaming', 'SocialFi', 'Tooling'];
3335

3436
type Props = {
@@ -47,16 +49,45 @@ export default function ShowCaseCard({
4749
const [searchValue, setSearchValue] = useState('');
4850
const [filters, setFilters] = useState<ProjectFilters>(initialFilters);
4951
const [currentPage, setCurrentPage] = useState(initialFilters.page ?? 1);
52+
const { data: session } = useSession();
5053
const [recordsByPage, setRecordsByPage] = useState(
5154
initialFilters.recordsByPage ?? 12
5255
);
5356
const [totalPages, setTotalPages] = useState<number>(
5457
Math.ceil(totalProjects / recordsByPage) || 1
5558
);
59+
const [isExporting, setIsExporting] = useState(false);
5660
const router = useRouter();
57-
61+
const { exportToExcel, isLoading, error } = useExports();
5862
const selectedHackathon = events.find(event => event.id === filters.event);
5963
const availableTracks = selectedHackathon?.content?.tracks?.map(track => track.name) ?? [];
64+
const [hasRole, setHasRole] = useState(false);
65+
const handleExport = async () => {
66+
try {
67+
setIsExporting(true);
68+
await exportToExcel({
69+
event: 'hackathon-2024',
70+
track: 'DeFi',
71+
winningProjects: true
72+
});
73+
} catch (err) {
74+
console.error('Error exporting:', error);
75+
}
76+
finally {
77+
setIsExporting(false);
78+
}
79+
};
80+
81+
useEffect(() => {
82+
if(session?.user) {
83+
if (session?.user?.custom_attributes?.includes('hackathonCreator')) {
84+
setHasRole(true);
85+
}
86+
}
87+
else {
88+
setHasRole(false);
89+
}
90+
}, [session]);
6091

6192

6293
const handleFilterChange = (type: keyof ProjectFilters, value: string) => {
@@ -112,6 +143,14 @@ export default function ShowCaseCard({
112143
</CardDescription>
113144
</CardHeader>
114145
<Separator className='mt-6 bg-zinc-300 dark:bg-zinc-800 h-[2px]' />
146+
<div className='flex justify-end'>
147+
{hasRole && <LoadingButton variant={'outline'}
148+
isLoading={isExporting}
149+
onClick={handleExport}
150+
className='bg-zinc-50 dark:bg-zinc-950 border border-zinc-300 dark:border-zinc-800 text-zinc-900 dark:text-zinc-50'>
151+
Export Projects
152+
</LoadingButton>}
153+
</div>
115154
<div className='grid grid-cols-1 md:grid-cols-2 xl:grid-cols-4 gap-4 mt-6'>
116155
<div className='w-full'>
117156
<Tabs
@@ -146,6 +185,7 @@ export default function ShowCaseCard({
146185
</TabsList>
147186
</Tabs>
148187
</div>
188+
149189
<div className='relative w-full'>
150190
<Search className='absolute left-3 top-1/2 transform -translate-y-1/2 h-[40px] w-5 text-zinc-400 stroke-zinc-700' />
151191
<Input

0 commit comments

Comments
 (0)