diff --git a/.cursor/rules/trustless-rule.mdc b/.cursor/rules/trustless-rule.mdc new file mode 100644 index 000000000..a9d529ad7 --- /dev/null +++ b/.cursor/rules/trustless-rule.mdc @@ -0,0 +1,55 @@ +--- +alwaysApply: true +--- +You are a **Senior Front-End Developer** and **Expert** in: +- ReactJS, NextJS, JavaScript, TypeScript +- TailwindCSS, Shadcn, Radix UI +- HTML, CSS, and modern UI/UX best practices + +You are methodical, precise, and a master at reasoning through complex requirements. You always provide correct, DRY, bug-free, production-ready code. + +## General Rules +- Follow the user’s requirements **exactly** as stated. +- Think step-by-step: + 1. **Analyze** the requirement. + 2. **Write detailed pseudocode** describing the implementation plan. + 3. **Confirm** the plan (if asked). + 4. **Write complete code** that matches the plan. +- Never guess. If something is unclear, ask for clarification. +- If an external library is mentioned, always refer to its official documentation before implementation. +- Always ensure the final code is fully functional, with no placeholders, `TODO`s, or missing parts. +- Prefer readability over performance. +- Use best practices for React & Next.js development. +- Do not use cd in order to access to determinate root, neither use &&, | or something like that in shell actions. +- Do not verify the build during the Trustless Work implementations. +- In each npm i, the name of the dependency must be enclosed in double quotation marks (“”). +- Do not ask for 2 o more ways to implement, just do it the best way possible. +- Do not plan or ask for steps; just implement the code without asking questions. + +## Trustless Work Integration Context +When working with Trustless Work: +- Documentation (I'll provide you the docs in the cursor docs management): + - React Library → + - Wallet Kit → + - Types → +- Ensure proper installation and configuration before usage. +- Use provided Types from the documentation when applicable. +- Follow the API and component usage exactly as described in the docs. +- Do not use any, instead always you must search for the Trustless Work entities. + +## Code Implementation Guidelines +- Use **TailwindCSS classes** for styling; avoid plain CSS. +- For conditional classes, prefer `clsx` or similar helper functions over ternary operators in JSX. +- Use **descriptive** variable, function, and component names. + - Event handlers start with `handle` (e.g., `handleClick`, `handleSubmit`). +- Prefer **const** arrow functions with explicit type annotations over `function` declarations. +- Always include all necessary imports at the top. +- Use early returns to improve code clarity. + +## Verification Before Delivery +Before finalizing: +1. Check that all required imports are present. +2. Ensure the code compiles in a Next.js 14+ environment. +3. Confirm that Tailwind and Shadcn styles render correctly. +4. Verify that Trustless Work components or hooks are properly initialized. +5. Ensure TypeScript types are correct and there are no type errors. diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..6f3a2913e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "liveServer.settings.port": 5501 +} \ No newline at end of file diff --git a/README.md b/README.md index e094cc199..e76dd4253 100644 --- a/README.md +++ b/README.md @@ -208,6 +208,10 @@ Before you begin, ensure you have the following installed: # Stellar Configuration NEXT_PUBLIC_STELLAR_NETWORK=testnet NEXT_PUBLIC_APP_URL=http://localhost:3000 + + # Trustless Work Configuration (optional) + # Get your API key from: https://docs.trustlesswork.com + NEXT_PUBLIC_TRUSTLESS_WORK_API_KEY=your-trustless-work-api-key ``` 6. **Run the development server:** diff --git a/app/(landing)/bounties/page.tsx b/app/(landing)/bounties/page.tsx new file mode 100644 index 000000000..c303997a7 --- /dev/null +++ b/app/(landing)/bounties/page.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('grants'); + +const GrantPage = () => { + return ( +
+ Bounties Page +
+ ); +}; + +export default GrantPage; diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx new file mode 100644 index 000000000..35fbb8dba --- /dev/null +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -0,0 +1,133 @@ +'use client'; + +import { useState, useEffect, useMemo } from 'react'; +import { useRouter, useSearchParams, useParams } from 'next/navigation'; +import { useHackathonData } from '@/lib/providers/hackathonProvider'; + +import { HackathonBanner } from '@/components/hackathons/hackathonBanner'; +import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs'; +import { HackathonOverview } from '@/components/hackathons/overview/hackathonOverview'; +import { HackathonParticipants } from '@/components/hackathons/participants/hackathonParticipant'; +import { HackathonResources } from '@/components/hackathons/resources/resources'; +import SubmissionTab from '@/components/hackathons/submissions/submissionTab'; +import { HackathonDiscussions } from '@/components/hackathons/discussion/comment'; +import LoadingScreen from '@/components/landing-page/project/CreateProjectModal/LoadingScreen'; + +export default function HackathonPage() { + const router = useRouter(); + const searchParams = useSearchParams(); + const params = useParams(); + + const { + currentHackathon, + content, + timelineEvents, + participants, + submissions, + prizes, + loading, + setCurrentHackathon, + } = useHackathonData(); + + const hackathonTabs = useMemo( + () => [ + { id: 'overview', label: 'Overview' }, + { id: 'participants', label: 'Participants', badge: participants.length }, + { id: 'resources', label: 'Resources' }, + { + id: 'submission', + label: 'Submissions', + badge: submissions.filter(p => p.status === 'Approved').length, + }, + { id: 'discussions', label: 'Discussions' }, + ], + [participants.length, submissions] + ); + + const hackathonId = params.slug as string; + const [activeTab, setActiveTab] = useState('overview'); + + useEffect(() => { + if (hackathonId) { + setCurrentHackathon(hackathonId); + } + }, [hackathonId, setCurrentHackathon]); + + useEffect(() => { + const tabFromUrl = searchParams.get('tab'); + if (tabFromUrl && hackathonTabs.some(tab => tab.id === tabFromUrl)) { + setActiveTab(tabFromUrl); + } + }, [searchParams, hackathonTabs]); + + const handleTabChange = (tabId: string) => { + setActiveTab(tabId); + const params = new URLSearchParams(searchParams.toString()); + params.set('tab', tabId); + router.push(`?${params.toString()}`, { scroll: false }); + }; + + if (loading) { + return ; + } + + if (!currentHackathon) { + return ( +
+
+

+ Hackathon not found +

+

+ The hackathon you're looking for doesn't exist. +

+
+
+ ); + } + + return ( +
+ {/* Banner */} + + + {/* Tabs */} + + + {/* Content */} +
+ {activeTab === 'overview' && ( + + )} + + {activeTab === 'participants' && } + + {activeTab === 'resources' && } + + {activeTab === 'submission' && } + + {activeTab === 'discussions' && ( + + )} +
+
+ ); +} diff --git a/app/(landing)/hackathons/layout.tsx b/app/(landing)/hackathons/layout.tsx new file mode 100644 index 000000000..9bc3e940e --- /dev/null +++ b/app/(landing)/hackathons/layout.tsx @@ -0,0 +1,22 @@ +import { HackathonDataProvider } from '@/lib/providers/hackathonProvider'; +import { use } from 'react'; + +interface HackathonLayoutProps { + children: React.ReactNode; + params: Promise<{ + slug?: string; + }>; +} + +export default function HackathonLayout({ + children, + params, +}: HackathonLayoutProps) { + const resolvedParams = use(params); + + return ( + + {children} + + ); +} diff --git a/app/(landing)/hackathons/page.tsx b/app/(landing)/hackathons/page.tsx index d498e40bd..ddf1f491f 100644 --- a/app/(landing)/hackathons/page.tsx +++ b/app/(landing)/hackathons/page.tsx @@ -1,15 +1,15 @@ -import React from 'react'; import { Metadata } from 'next'; import { generatePageMetadata } from '@/lib/metadata'; +import HackathonsPageHero from '@/components/hackathons/HackathonsPageHero'; +import HackathonsPage from '@/components/hackathons/HackathonsPage'; export const metadata: Metadata = generatePageMetadata('hackathons'); -const HackathonsPage = () => { +export default function HackathonsPageRoute() { return ( -
- Hackathons Page +
+ +
); -}; - -export default HackathonsPage; +} diff --git a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx new file mode 100644 index 000000000..187f47e52 --- /dev/null +++ b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx @@ -0,0 +1,440 @@ +'use client'; + +import { useState, useEffect, useMemo } from 'react'; +import { useRouter, useSearchParams } from 'next/navigation'; +import { ArrowLeft } from 'lucide-react'; +import { + previewDraft, + transformPublicHackathonToHackathon, +} from '@/lib/api/hackathons'; +import { HackathonBanner } from '@/components/hackathons/hackathonBanner'; +import { HackathonNavTabs } from '@/components/hackathons/hackathonNavTabs'; +import { HackathonOverview } from '@/components/hackathons/overview/hackathonOverview'; +import { HackathonResources } from '@/components/hackathons/resources/resources'; +import LoadingScreen from '@/components/landing-page/project/CreateProjectModal/LoadingScreen'; +import { Badge } from '@/components/ui/badge'; +import { BoundlessButton } from '@/components/buttons'; +import type { Hackathon } from '@/types/hackathon'; + +// Mock data for preview (since drafts don't have participants/submissions yet) +const mockContent = ` +

This is a preview of your hackathon. Once published, participants will be able to see the full details here.

+`; + +const mockTimelineEvents = [ + { event: 'Registration Opens', date: new Date().toISOString() }, + { event: 'Submission Deadline', date: new Date().toISOString() }, + { event: 'Judging Period', date: new Date().toISOString() }, + { event: 'Winners Announced', date: new Date().toISOString() }, +]; + +const mockPrizes = [ + { + title: 'First Place', + rank: '1st', + prize: '$5,000', + icon: '🥇', + details: ['Cash prize', 'Certificate', 'Featured on homepage'], + }, + { + title: 'Second Place', + rank: '2nd', + prize: '$3,000', + icon: '🥈', + details: ['Cash prize', 'Certificate'], + }, + { + title: 'Third Place', + rank: '3rd', + prize: '$2,000', + icon: '🥉', + details: ['Cash prize', 'Certificate'], + }, +]; + +interface PreviewPageProps { + params: Promise<{ + orgId: string; + draftId: string; + }>; +} + +export default function DraftPreviewPage({ params }: PreviewPageProps) { + const router = useRouter(); + const searchParams = useSearchParams(); + const [resolvedParams, setResolvedParams] = useState<{ + orgId: string; + draftId: string; + } | null>(null); + const [previewHackathon, setPreviewHackathon] = useState( + null + ); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [errorType, setErrorType] = useState< + 'unauthorized' | 'forbidden' | 'not_found' | 'other' | null + >(null); + const [activeTab, setActiveTab] = useState('overview'); + + // Resolve params (Next.js 15 async params) + useEffect(() => { + params.then(p => setResolvedParams(p)); + }, [params]); + + // Fetch preview data + useEffect(() => { + const fetchPreview = async () => { + if (!resolvedParams) return; + + try { + setLoading(true); + setError(null); + + const response = await previewDraft( + resolvedParams.orgId, + resolvedParams.draftId + ); + + if (response.success && response.data) { + // Transform PublicHackathon to Hackathon type + const transformed = transformPublicHackathonToHackathon( + response.data, + response.data.organizer + ); + + // Map to the Hackathon type expected by components + const hackathon: Hackathon = { + id: transformed._id, + title: transformed.information.title, + subtitle: transformed.participation?.about || '', + description: transformed.information.description, + slug: transformed.information.slug || '', + imageUrl: transformed.information.banner, + status: + transformed.status === 'ongoing' + ? 'ongoing' + : transformed.status === 'completed' + ? 'ended' + : 'upcoming', + participants: 0, // Always 0 for drafts + totalPrizePool: response.data.totalPrizePool, + deadline: transformed.timeline.submissionDeadline, + categories: transformed.information.categories.map(cat => + cat.toString() + ), + startDate: transformed.timeline.startDate, + endDate: transformed.timeline.winnerAnnouncementDate, + organizer: response.data.organizer, + featured: transformed.featured || false, + resources: transformed.collaboration.socialLinks || [], + }; + + setPreviewHackathon(hackathon); + } else { + throw new Error(response.message || 'Failed to load preview'); + } + } catch (err) { + // Check if it's an axios error with status code + let errorMessage = 'Failed to load preview'; + let errorType: 'unauthorized' | 'forbidden' | 'not_found' | 'other' = + 'other'; + + // Check for axios error structure (has response property) + if ( + err && + typeof err === 'object' && + 'response' in err && + ( + err as { + response?: { status?: number; data?: { message?: string } }; + } + ).response + ) { + const response = ( + err as { + response?: { status?: number; data?: { message?: string } }; + } + ).response; + const status = response?.status; + + if (status === 401) { + errorMessage = + 'Authentication required. Please log in to view this preview.'; + errorType = 'unauthorized'; + } else if (status === 403) { + errorMessage = + 'Access denied. Only owners and admins of this organization can preview hackathon drafts.'; + errorType = 'forbidden'; + } else if (status === 404) { + errorMessage = + "Draft not found. The hackathon draft you're looking for doesn't exist."; + errorType = 'not_found'; + } else if (response?.data?.message) { + errorMessage = response.data.message; + } + } + // Check for transformed ApiError structure (has status property directly) + else if (err && typeof err === 'object' && 'status' in err) { + const status = (err as { status?: number }).status; + if (status === 401) { + errorMessage = + 'Authentication required. Please log in to view this preview.'; + errorType = 'unauthorized'; + } else if (status === 403) { + errorMessage = + 'Access denied. Only owners and admins of this organization can preview hackathon drafts.'; + errorType = 'forbidden'; + } else if (status === 404) { + errorMessage = + "Draft not found. The hackathon draft you're looking for doesn't exist."; + errorType = 'not_found'; + } else if ( + 'message' in err && + typeof (err as { message: string }).message === 'string' + ) { + errorMessage = (err as { message: string }).message; + } + } else if (err instanceof Error) { + errorMessage = err.message; + } + + setError(errorMessage); + setErrorType(errorType); + } finally { + setLoading(false); + } + }; + + if (resolvedParams) { + fetchPreview(); + } + }, [resolvedParams]); + + const hackathonTabs = useMemo( + () => [ + { id: 'overview', label: 'Overview' }, + { id: 'participants', label: 'Participants', badge: 0 }, + { id: 'resources', label: 'Resources' }, + { id: 'submission', label: 'Submissions', badge: 0 }, + { id: 'discussions', label: 'Discussions' }, + ], + [] + ); + + useEffect(() => { + const tabFromUrl = searchParams.get('tab'); + if (tabFromUrl && hackathonTabs.some(tab => tab.id === tabFromUrl)) { + setActiveTab(tabFromUrl); + } + }, [searchParams, hackathonTabs]); + + const handleTabChange = (tabId: string) => { + setActiveTab(tabId); + const params = new URLSearchParams(searchParams.toString()); + params.set('tab', tabId); + router.push(`?${params.toString()}`, { scroll: false }); + }; + + if (loading) { + return ; + } + + if (error) { + return ( +
+
+
+ {errorType === 'unauthorized' && ( +
+ + + +
+ )} + {errorType === 'forbidden' && ( +
+ + + +
+ )} + {(errorType === 'not_found' || errorType === 'other') && ( +
+ + + +
+ )} +
+

+ {errorType === 'unauthorized' + ? 'Authentication Required' + : errorType === 'forbidden' + ? 'Access Denied' + : errorType === 'not_found' + ? 'Preview Not Found' + : 'Error Loading Preview'} +

+

{error}

+
+ {errorType === 'unauthorized' && ( + router.push('/auth?mode=signin')} + className='w-full sm:w-auto' + > + Sign In + + )} + {resolvedParams && ( + + router.push( + `/organizations/${resolvedParams.orgId}/hackathons` + ) + } + variant='outline' + className='w-full border-gray-700 hover:border-gray-600 hover:bg-gray-800 sm:w-auto' + > + Back to Hackathons + + )} +
+
+
+ ); + } + + if (!previewHackathon) { + return ( +
+
+

+ Preview not found +

+

+ The draft preview you're looking for doesn't exist. +

+
+
+ ); + } + + const handleBackToEdit = () => { + if (resolvedParams) { + router.push( + `/organizations/${resolvedParams.orgId}/hackathons/drafts/${resolvedParams.draftId}` + ); + } + }; + + return ( +
+ {/* Preview Banner */} +
+ + + Back to Edit + + + ⚠️ PREVIEW MODE - This is how your hackathon will appear to users + +
+ + {/* Banner */} + + + {/* Tabs */} + + + {/* Content */} +
+ {activeTab === 'overview' && ( + + )} + + {activeTab === 'participants' && ( +
+

+ Participants will appear here once the hackathon is published and + people start registering. +

+
+ )} + + {activeTab === 'resources' && } + + {activeTab === 'submission' && ( +
+

+ Submissions will appear here once the hackathon is published and + participants start submitting their projects. +

+
+ )} + + {activeTab === 'discussions' && ( +
+

+ Discussions will appear here once the hackathon is published and + participants start engaging. +

+
+ )} +
+
+ ); +} diff --git a/app/(landing)/layout.tsx b/app/(landing)/layout.tsx index eebb9eb7d..7fd4373fa 100644 --- a/app/(landing)/layout.tsx +++ b/app/(landing)/layout.tsx @@ -12,7 +12,7 @@ interface LandingLayoutProps { export default function LandingLayout({ children }: LandingLayoutProps) { return ( -
+
{children}