diff --git a/app/(landing)/hackathons/[slug]/submit/page.tsx b/app/(landing)/hackathons/[slug]/submit/page.tsx
index f5ca054e2..61e394970 100644
--- a/app/(landing)/hackathons/[slug]/submit/page.tsx
+++ b/app/(landing)/hackathons/[slug]/submit/page.tsx
@@ -1,6 +1,6 @@
'use client';
-import { use, useEffect } from 'react';
+import { use, useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
import { useHackathonData } from '@/lib/providers/hackathonProvider';
import { useAuthStatus } from '@/hooks/use-auth';
@@ -70,15 +70,34 @@ export default function SubmitProjectPage({
router.push(`/hackathons/${hackathonSlug}?tab=submission`);
};
- if (
- isLoading ||
- hackathonLoading ||
- isLoadingMySubmission ||
- !currentHackathon
- ) {
+ const [hasInitialLoaded, setHasInitialLoaded] = useState(false);
+
+ useEffect(() => {
+ if (!isLoading && !hackathonLoading && !isLoadingMySubmission) {
+ setHasInitialLoaded(true);
+ }
+ }, [isLoading, hackathonLoading, isLoadingMySubmission]);
+
+ if (!hasInitialLoaded) {
return ;
}
+ if (!currentHackathon) {
+ return (
+
+
Hackathon Not Found
+
+
+ );
+ }
+
return (
@@ -107,6 +126,8 @@ export default function SubmitProjectPage({
introduction: mySubmission.introduction,
links: mySubmission.links,
participationType: (mySubmission as any).participationType,
+ teamName: (mySubmission as any).teamName,
+ teamMembers: (mySubmission as any).teamMembers,
}
: undefined
}
diff --git a/app/me/projects/page.tsx b/app/me/projects/page.tsx
index 4c61e8f1e..fd6c819bf 100644
--- a/app/me/projects/page.tsx
+++ b/app/me/projects/page.tsx
@@ -102,14 +102,23 @@ export default function MyProjectsPage() {
) : (
- {sortedProjects.map(project => (
-
{
+ // Find if this project is a hackathon submission
+ const submission =
+ meData.user.hackathonSubmissionsAsParticipant?.find(
+ s => s.projectId === project.id
+ );
+
+ // If it's a submission, use the submission ID for the slug to ensure correct redirection from ProjectCard
+ const displayId = submission?.id || project.id;
+
+ return (
+
- ))}
+ isSubmission: !!submission,
+ submissionStatus: submission?.status,
+ }}
+ />
+ );
+ })}
)}
diff --git a/components/hackathons/submissions/SubmissionForm.tsx b/components/hackathons/submissions/SubmissionForm.tsx
index 1604189b6..12d5273f6 100644
--- a/components/hackathons/submissions/SubmissionForm.tsx
+++ b/components/hackathons/submissions/SubmissionForm.tsx
@@ -34,6 +34,7 @@ import {
ExpandableScreenContent,
ExpandableScreenTrigger,
useExpandableScreen,
+ useOptionalExpandableScreen,
} from '@/components/ui/expandable-screen';
import Stepper from '@/components/stepper/Stepper';
import { uploadService } from '@/lib/api/upload';
@@ -201,16 +202,9 @@ const SubmissionFormContent: React.FC
= ({
onSuccess,
onClose,
}) => {
- // Use context carefully since it might not be available when used standalone
- let collapse = () => {};
- let open = true;
- try {
- const expandableCtx = useExpandableScreen();
- collapse = expandableCtx.collapse;
- open = expandableCtx.isExpanded;
- } catch (e) {
- // Standalone mode, not in ExpandableScreen
- }
+ const expandableCtx = useOptionalExpandableScreen();
+ const collapse = expandableCtx?.collapse ?? (() => {});
+ const open = expandableCtx?.isExpanded ?? true;
const { currentHackathon } = useHackathonData();
const { user } = useAuthStatus();
@@ -416,7 +410,7 @@ const SubmissionFormContent: React.FC = ({
description:
'An intelligent task management application that uses machine learning to prioritize tasks...',
logo: '',
- videoUrl: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
+ videoUrl: 'https://youtu.be/rOSZhhblE_8?si=Hf_YvPTMmyWTUOKQ',
introduction: 'This project leverages advanced AI algorithms...',
links: [
{ type: 'github', url: 'https://github.com/example/ai-task-manager' },
@@ -785,12 +779,14 @@ const SubmissionFormContent: React.FC = ({
} else {
await create(submissionData);
}
- if (onClose) {
+
+ if (onSuccess) {
+ onSuccess();
+ } else if (onClose) {
onClose();
} else {
collapse();
}
- onSuccess?.();
} catch {
// Error handled in hook
}
diff --git a/components/hackathons/submissions/submissionCard.tsx b/components/hackathons/submissions/submissionCard.tsx
index 1f0d6e59a..f5b511622 100644
--- a/components/hackathons/submissions/submissionCard.tsx
+++ b/components/hackathons/submissions/submissionCard.tsx
@@ -127,6 +127,16 @@ const SubmissionCard = ({
return formatDistanceToNow(new Date(dateString), { addSuffix: true });
};
+ const handleEditSelect = (e: Event) => {
+ e.stopPropagation();
+ onEditClick?.();
+ };
+
+ const handleDeleteSelect = (e: Event) => {
+ e.stopPropagation();
+ onDeleteClick?.();
+ };
+
return (
@@ -181,14 +192,14 @@ const SubmissionCard = ({
className='border-gray-800 bg-black text-white'
>
onEditClick?.()}
+ onSelect={handleEditSelect}
className='cursor-pointer text-gray-300 focus:bg-gray-800 focus:text-white'
>
Edit Submission
onDeleteClick?.()}
+ onSelect={handleDeleteSelect}
className='cursor-pointer text-red-500 focus:bg-red-900/20 focus:text-red-400'
>
diff --git a/components/hackathons/submissions/submissionTab.tsx b/components/hackathons/submissions/submissionTab.tsx
index fff6a4974..29193c83a 100644
--- a/components/hackathons/submissions/submissionTab.tsx
+++ b/components/hackathons/submissions/submissionTab.tsx
@@ -83,6 +83,7 @@ const SubmissionTabContent: React.FC = ({
setSelectedSort,
setSelectedCategory,
} = useSubmissions();
+
const { currentHackathon, loading: isHackathonDataLoading } =
useHackathonData();
const { status } = useHackathonStatus(
@@ -276,6 +277,7 @@ const SubmissionTabContent: React.FC = ({
{/* Submissions Grid with Create Button if no submission */}
{!isLoadingMySubmission &&
+ !isHackathonDataLoading &&
!mySubmission &&
isAuthenticated &&
isRegistered &&
diff --git a/components/organization/hackathons/settings/GeneralSettingsTab.tsx b/components/organization/hackathons/settings/GeneralSettingsTab.tsx
index e0b2ce74d..1c1e84e65 100644
--- a/components/organization/hackathons/settings/GeneralSettingsTab.tsx
+++ b/components/organization/hackathons/settings/GeneralSettingsTab.tsx
@@ -55,6 +55,8 @@ interface GeneralSettingsTabProps {
isPublished?: boolean;
}
+import TurndownService from 'turndown';
+
export default function GeneralSettingsTab({
organizationId,
hackathonId,
@@ -78,6 +80,24 @@ export default function GeneralSettingsTab({
address: string;
} | null>(null);
+ // Normalize HTML to Markdown for existing descriptions
+ const normalizedDescription = React.useMemo(() => {
+ let desc = initialData?.description || '';
+ if (desc && /<[a-z][\\s\\S]*>/i.test(desc)) {
+ try {
+ const turndownService = new TurndownService({
+ headingStyle: 'atx',
+ codeBlockStyle: 'fenced',
+ bulletListMarker: '-',
+ });
+ desc = turndownService.turndown(desc);
+ } catch (err) {
+ console.error('Failed to convert HTML to Markdown', err);
+ }
+ }
+ return desc;
+ }, [initialData?.description]);
+
const form = useForm({
resolver: zodResolver(infoSchema),
defaultValues: {
@@ -85,7 +105,7 @@ export default function GeneralSettingsTab({
tagline: initialData?.tagline || '',
slug: initialData?.slug || '',
banner: initialData?.banner || '',
- description: initialData?.description || '',
+ description: normalizedDescription,
categories: Array.isArray(initialData?.categories)
? initialData.categories
: [],
diff --git a/components/profile/ProjectsTab.tsx b/components/profile/ProjectsTab.tsx
index ff2808d52..4758ce189 100644
--- a/components/profile/ProjectsTab.tsx
+++ b/components/profile/ProjectsTab.tsx
@@ -100,38 +100,47 @@ export default function ProjectsTab({ user }: ProjectsTabProps) {
onScrollCapture={handleScroll}
>
- {projects.map(project => (
-
-
{
+ // Find if this project is a hackathon submission
+ // We need to check if the user object has submissions
+ const submission =
+ user.user.hackathonSubmissionsAsParticipant?.find(
+ s => s.projectId === project.id
+ );
+
+ // If it's a submission, use the submission ID for the URL and add ?type=submission
+ const displayId = submission?.id || project.id;
+ const href = submission
+ ? `/projects/${displayId}?type=submission`
+ : `/projects/${displayId}`;
+
+ return (
+
+
-
- ))}
+ },
+ isSubmission: !!submission,
+ submissionStatus: submission?.status,
+ }}
+ />
+
+ );
+ })}
{!hasMore && projects.length > 0 && (
diff --git a/components/profile/ProjectsTabPublic.tsx b/components/profile/ProjectsTabPublic.tsx
index 478c1fafd..ccbf7194a 100644
--- a/components/profile/ProjectsTabPublic.tsx
+++ b/components/profile/ProjectsTabPublic.tsx
@@ -33,14 +33,26 @@ export default function ProjectsTabPublic({ user }: ProjectsTabProps) {
- {user.projects.map(project => (
-
-
{
+ // Find if this project is a hackathon submission
+ const submission = user.hackathonSubmissionsAsParticipant?.find(
+ s => s.projectId === project.id
+ );
+
+ // If it's a submission, use the submission ID for the URL and add ?type=submission
+ const displayId = submission?.id || project.id;
+ const href = submission
+ ? `/projects/${displayId}?type=submission`
+ : `/projects/${displayId}`;
+
+ return (
+
+
-
- ))}
+ // Add a custom property to indicate it's a submission for ProjectCard
+ isSubmission: !!submission,
+ submissionStatus: submission?.status,
+ }}
+ />
+
+ );
+ })}
);
diff --git a/components/ui/expandable-screen.tsx b/components/ui/expandable-screen.tsx
index 7eaf54e96..4894a27b7 100644
--- a/components/ui/expandable-screen.tsx
+++ b/components/ui/expandable-screen.tsx
@@ -34,6 +34,10 @@ function useExpandableScreen() {
return context;
}
+export function useOptionalExpandableScreen() {
+ return useContext(ExpandableScreenContext);
+}
+
// Root Component
interface ExpandableScreenProps {
children: ReactNode;
diff --git a/features/projects/components/ProjectCard.tsx b/features/projects/components/ProjectCard.tsx
index 8dd194c84..155c4ec3a 100644
--- a/features/projects/components/ProjectCard.tsx
+++ b/features/projects/components/ProjectCard.tsx
@@ -3,10 +3,36 @@ import { formatNumber, cn } from '@/lib/utils';
import { useRouter } from 'nextjs-toploader/app';
import Image from 'next/image';
import { CountdownTimer } from '@/components/ui/timer';
-import { Crowdfunding } from '@/features/projects/types';
+
+export type ProjectCardData = {
+ id: string;
+ slug: string;
+ project: {
+ id?: string;
+ title: string;
+ vision?: string | null;
+ banner?: string | null;
+ logo?: string | null;
+ creator?: { name: string; image?: string | null };
+ category?: string | null;
+ status: string;
+ _count?: { votes?: number };
+ [key: string]: any;
+ };
+ fundingGoal?: number;
+ fundingRaised?: number;
+ fundingCurrency?: string;
+ fundingEndDate?: string | null;
+ milestones?: any[];
+ voteGoal?: number;
+ voteProgress?: number;
+ isSubmission?: boolean;
+ submissionStatus?: string | null;
+ [key: string]: any;
+};
type ProjectCardProps = {
- data: Crowdfunding;
+ data: ProjectCardData;
newTab?: boolean;
isFullWidth?: boolean;
className?: string;
@@ -23,13 +49,15 @@ function ProjectCard({
const {
slug,
project,
- fundingGoal,
- fundingRaised,
- fundingCurrency,
- fundingEndDate,
- milestones,
- voteGoal,
- voteProgress,
+ fundingGoal = 0,
+ fundingRaised = 0,
+ fundingCurrency = 'USDC',
+ fundingEndDate = null,
+ milestones = [],
+ voteGoal = 0,
+ voteProgress = 0,
+ isSubmission,
+ submissionStatus,
} = data;
const {
@@ -47,11 +75,22 @@ function ProjectCard({
banner || '/images/placeholders/project-banner-placeholder.png';
const handleClick = () => {
- router.push(`/projects/${slug}`);
+ const url = isSubmission
+ ? `/projects/${slug}?type=submission`
+ : `/projects/${slug}`;
+ router.push(url);
};
// Determine display status
const getDisplayStatus = () => {
+ if (isSubmission) {
+ if (submissionStatus === 'SHORTLISTED' || submissionStatus === 'ACCEPTED')
+ return 'Shortlisted';
+ if (submissionStatus === 'SUBMITTED') return 'Submitted';
+ if (submissionStatus === 'DISQUALIFIED') return 'Disqualified';
+ return 'Submission';
+ }
+
if (projectStatus === 'IDEA') return 'Validation';
if (projectStatus === 'ACTIVE') return 'Funding';
if (projectStatus === 'LIVE') return 'Funded';
diff --git a/features/projects/types/index.ts b/features/projects/types/index.ts
index bce8bcc40..7d0418dab 100644
--- a/features/projects/types/index.ts
+++ b/features/projects/types/index.ts
@@ -82,6 +82,13 @@ export interface PublicUserProfile {
logo: string;
createdAt: string;
}>;
+ hackathonSubmissionsAsParticipant?: Array<{
+ id: string;
+ projectId: string;
+ status: string;
+ hackathonId: string;
+ projectName: string;
+ }>;
badges: any[];
stats: {
projectsCreated: number;
diff --git a/hooks/hackathon/use-participants.ts b/hooks/hackathon/use-participants.ts
index f6fe782bc..45e43122b 100644
--- a/hooks/hackathon/use-participants.ts
+++ b/hooks/hackathon/use-participants.ts
@@ -12,17 +12,17 @@ export function useParticipants() {
const [apiParticipants, setApiParticipants] = useState([]);
const [isLoading, setIsLoading] = useState(false);
- const hackathonId = currentHackathon?.id || (params?.slug as string);
+ const hackathonId = currentHackathon?.id;
// Fetch teams to get accurate team info and roles
useEffect(() => {
if (hackathonId) {
setIsLoading(true);
- Promise.all([
- getTeamPosts(hackathonId, { limit: 50 }),
- getHackathonParticipants(hackathonId, { limit: 100 }),
- ])
- .then(([teamsResponse, participantsResponse]) => {
+
+ const fetchAllData = async () => {
+ try {
+ // Fetch teams
+ const teamsResponse = await getTeamPosts(hackathonId, { limit: 50 });
if (teamsResponse.success && teamsResponse.data) {
const teamsArray =
(teamsResponse.data as any).teams ||
@@ -30,16 +30,35 @@ export function useParticipants() {
setTeams(teamsArray);
}
- if (participantsResponse.success && participantsResponse.data) {
- setApiParticipants(participantsResponse.data.participants || []);
+ // Fetch participants with pagination
+ let allParticipants: any[] = [];
+ let page = 1;
+ let hasMore = true;
+
+ while (hasMore) {
+ const participantsResponse = await getHackathonParticipants(
+ hackathonId,
+ { limit: 100, page }
+ );
+ if (participantsResponse.success && participantsResponse.data) {
+ const newParticipants =
+ participantsResponse.data.participants || [];
+ allParticipants = [...allParticipants, ...newParticipants];
+ hasMore = participantsResponse.data.pagination?.hasNext || false;
+ page++;
+ } else {
+ hasMore = false;
+ }
}
- })
- .catch(err => {
+ setApiParticipants(allParticipants);
+ } catch (err) {
reportError(err, { context: 'participants-fetchData', hackathonId });
- })
- .finally(() => {
+ } finally {
setIsLoading(false);
- });
+ }
+ };
+
+ fetchAllData();
}
}, [hackathonId]);
diff --git a/lib/api/types.ts b/lib/api/types.ts
index 39010e576..d418161a3 100644
--- a/lib/api/types.ts
+++ b/lib/api/types.ts
@@ -122,6 +122,7 @@ export interface User {
rank?: number | null;
submittedAt: string;
hackathonId: string;
+ projectId: string;
hackathon?: Hackathon;
}>;
joinedHackathons?: Array<{
diff --git a/lib/providers/hackathonProvider.tsx b/lib/providers/hackathonProvider.tsx
index e6b26fbbe..e21a12c16 100644
--- a/lib/providers/hackathonProvider.tsx
+++ b/lib/providers/hackathonProvider.tsx
@@ -99,7 +99,7 @@ interface HackathonDataContextType {
getHackathonById: (id: string) => Hackathon | undefined;
getHackathonBySlug: (slug: string) => Promise;
- setCurrentHackathon: (slug: string) => void;
+ setCurrentHackathon: (slug: string) => Promise;
addDiscussion: (content: string) => Promise;
addReply: (parentCommentId: string, content: string) => Promise;
diff --git a/package-lock.json b/package-lock.json
index 32c85f04a..74be24bf5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -125,6 +125,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"three": "^0.180.0",
+ "turndown": "^7.2.2",
"tw-animate-css": "^1.3.6",
"uuid": "^13.0.0",
"vaul": "^1.1.2",
@@ -137,6 +138,7 @@
"@types/p-limit": "^2.1.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
+ "@types/turndown": "^5.0.6",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",
@@ -1609,6 +1611,12 @@
"langium": "^4.0.0"
}
},
+ "node_modules/@mixmark-io/domino": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz",
+ "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==",
+ "license": "BSD-2-Clause"
+ },
"node_modules/@monogrid/gainmap-js": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
@@ -6352,6 +6360,13 @@
"license": "MIT",
"optional": true
},
+ "node_modules/@types/turndown": {
+ "version": "5.0.6",
+ "resolved": "https://registry.npmjs.org/@types/turndown/-/turndown-5.0.6.tgz",
+ "integrity": "sha512-ru00MoyeeouE5BX4gRL+6m/BsDfbRayOskWqUvh7CLGW+UXxHQItqALa38kKnOiZPqJrtzJUgAC2+F0rL1S4Pg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -16839,6 +16854,15 @@
}
}
},
+ "node_modules/turndown": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz",
+ "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@mixmark-io/domino": "^2.2.0"
+ }
+ },
"node_modules/tw-animate-css": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/tw-animate-css/-/tw-animate-css-1.4.0.tgz",
diff --git a/package.json b/package.json
index 8e73c10a0..a557459e3 100644
--- a/package.json
+++ b/package.json
@@ -141,6 +141,7 @@
"sonner": "^2.0.7",
"tailwind-merge": "^3.3.1",
"three": "^0.180.0",
+ "turndown": "^7.2.2",
"tw-animate-css": "^1.3.6",
"uuid": "^13.0.0",
"vaul": "^1.1.2",
@@ -153,6 +154,7 @@
"@types/p-limit": "^2.1.0",
"@types/react": "19.2.7",
"@types/react-dom": "19.2.3",
+ "@types/turndown": "^5.0.6",
"@types/uuid": "^10.0.0",
"@typescript-eslint/eslint-plugin": "^8.38.0",
"@typescript-eslint/parser": "^8.38.0",