diff --git a/.env.example b/.env.example index 50e04a86..84b16ff8 100644 --- a/.env.example +++ b/.env.example @@ -22,12 +22,18 @@ NEXT_PUBLIC_GOOGLE_CLIENT_ID="" NEXT_PUBLIC_HORIZON_PUBLIC_URL="https://horizon.stellar.org" NEXT_PUBLIC_HORIZON_TESTNET_URL="https://horizon-testnet.stellar.org" NEXT_PUBLIC_STELLAR_NETWORK="testnet" +# Smart Wallet (Passkey) — OpenZeppelin smart accounts via smart-account-kit +NEXT_PUBLIC_STELLAR_RPC_URL="https://soroban-testnet.stellar.org" +NEXT_PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" +NEXT_PUBLIC_SMART_ACCOUNT_WASM_HASH="a12e8fa9621efd20315753bd4007d974390e31fbcb4a7ddc4dd0a0dec728bf2e" +NEXT_PUBLIC_WEBAUTHN_VERIFIER_ADDRESS="CBSHV66WG7UV6FQVUTB67P3DZUEJ2KJ5X6JKQH5MFRAAFNFJUAJVXJYV" +NEXT_PUBLIC_NATIVE_TOKEN_CONTRACT="CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC" NEXT_PUBLIC_TRUSTLESS_WORK_API_KEY="" NEXT_PUBLIC_WALLET_CONNECT_PROJECT_ID="your_wallet_connect_project_id" # Error reporting (optional). When set, errors are sent to Sentry. NEXT_PUBLIC_SENTRY_DSN="" SENTRY_DSN="" SENTRY_ORG="" -SENTRY_PROJECT="boundless-next" -SENTRY_AUTH_TOKEN="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" +SENTRY_PROJECT="" +SENTRY_AUTH_TOKEN="" NODE_ENV="dev" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 925a8879..6ce85daf 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts + +# Sentry Config File +.env.sentry-build-plugin diff --git a/.npmrc b/.npmrc new file mode 100644 index 00000000..41583e36 --- /dev/null +++ b/.npmrc @@ -0,0 +1 @@ +@jsr:registry=https://npm.jsr.io diff --git a/app/(landing)/about/AboutUsHero.tsx b/app/(landing)/about/AboutUsHero.tsx index 53c6af0f..fa29f109 100644 --- a/app/(landing)/about/AboutUsHero.tsx +++ b/app/(landing)/about/AboutUsHero.tsx @@ -88,7 +88,7 @@ export default function AboutUsHero() { />
-
+
{ className='mx-auto mb-4 w-fit bg-clip-text text-sm font-medium text-transparent md:text-base' style={{ backgroundImage: - 'linear-gradient(272.61deg, #A7F95080 13.84%, #3AE6B2 73.28%)', + 'linear-gradient(272.61deg, #2EEDAA80 13.84%, #3AE6B2 73.28%)', }} > Our Team diff --git a/app/(landing)/campaigns/[slug]/contributions/page.tsx b/app/(landing)/campaigns/[slug]/contributions/page.tsx new file mode 100644 index 00000000..f1c7f7fe --- /dev/null +++ b/app/(landing)/campaigns/[slug]/contributions/page.tsx @@ -0,0 +1,92 @@ +'use client'; + +import { use, useEffect, useState } from 'react'; +import { getCrowdfundingProject } from '@/features/projects/api'; +import type { Crowdfunding } from '@/features/projects/types'; +import { ContributionsDataTable } from '@/features/projects/components/Contributions/ContributionsDataTable'; +import { ArrowLeft } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useRouter } from 'next/navigation'; + +interface ContributionsPageProps { + params: Promise<{ + slug: string; + }>; +} + +export default function ContributionsPage({ params }: ContributionsPageProps) { + const router = useRouter(); + const resolvedParams = use(params); + const slug = resolvedParams.slug; + + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchProject = async () => { + try { + setLoading(true); + const data = await getCrowdfundingProject(slug); + setProject(data); + } catch { + // Error handled by UI state + } finally { + setLoading(false); + } + }; + + fetchProject(); + }, [slug]); + + return ( +
+ {/* Header */} +
+ + +
+

Contributions

+ {project && ( +
+

+ Project:{' '} + + {project.project.title} + +

+ +

+ {project.contributors.length}{' '} + {project.contributors.length === 1 + ? 'Contributor' + : 'Contributors'} +

+
+ )} +
+
+ + {/* Table */} +
+ {loading ? ( +
+
+
+ ) : project ? ( + + ) : ( +
+

Failed to load contributions

+
+ )} +
+
+ ); +} diff --git a/app/(landing)/campaigns/[slug]/milestone/[milestoneId]/page.tsx b/app/(landing)/campaigns/[slug]/milestone/[milestoneId]/page.tsx new file mode 100644 index 00000000..7d96bdc6 --- /dev/null +++ b/app/(landing)/campaigns/[slug]/milestone/[milestoneId]/page.tsx @@ -0,0 +1,140 @@ +import React from 'react'; +import MilstoneOverview from '@/components/project-details/project-milestone/milestone-details/MilstoneOverview'; +import MilestoneDetails from '@/components/project-details/project-milestone/milestone-details/MilestoneDetails'; +import MilestoneLinks from '@/components/project-details/project-milestone/milestone-details/MilestoneLinks'; +import { MilestoneStatusCard } from '@/features/projects/components/Milestone/MilestoneStatusCard'; +import { MilestoneEvidence } from '@/features/projects/components/Milestone/MilestoneEvidence'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { + getCrowdfundingProject, + getCrowdfundingMilestone, +} from '@/features/projects/api'; +import { Crowdfunding } from '@/features/projects/types'; + +interface MilestonePageProps { + params: Promise<{ + slug: string; // Project slug + milestoneId: string; // Milestone index + }>; +} + +const MilestonePage = async ({ params }: MilestonePageProps) => { + const { slug, milestoneId } = await params; + + // Fetch project data and specific milestone + let project: Crowdfunding | null = null; + let milestone = null; + + try { + // Fetch both project and milestone data + const [crowdfundingProject, milestoneData] = await Promise.all([ + getCrowdfundingProject(slug), + getCrowdfundingMilestone(slug, milestoneId), + ]); + + project = crowdfundingProject; + + // Transform milestone to match component expectations + milestone = milestoneData + ? { + _id: milestoneData.id || milestoneData.name, + title: milestoneData.name, + description: milestoneData.description, + status: milestoneData.status, + dueDate: milestoneData.endDate, + amount: milestoneData.amount, + // Proof and submission data (optional) + ...(milestoneData.submittedAt && { + submittedAt: milestoneData.submittedAt, + }), + ...(milestoneData.approvedAt && { + approvedAt: milestoneData.approvedAt, + }), + ...(milestoneData.rejectedAt && { + rejectedAt: milestoneData.rejectedAt, + }), + ...(milestoneData.evidence && { evidence: milestoneData.evidence }), + // Voting data (optional) + ...(milestoneData.votes && { votes: milestoneData.votes }), + ...(milestoneData.userHasVoted !== undefined && { + userHasVoted: milestoneData.userHasVoted, + }), + ...(milestoneData.userVote && { userVote: milestoneData.userVote }), + } + : null; + } catch { + // Handle error silently - milestone will be null + } + + // If milestone not found, show error state + if (!milestone) { + return ( +
+
+

+ Milestone Not Found +

+

+ The requested milestone could not be found. +

+
+
+ ); + } + + return ( +
+
+ +
+ +
+ + + Details + + + Proof & Status + + + Links + + +
+ + + + + + {milestone.evidence && ( + + )} + + + + +
+
+ ); +}; + +export default MilestonePage; diff --git a/app/(landing)/campaigns/[slug]/page.tsx b/app/(landing)/campaigns/[slug]/page.tsx new file mode 100644 index 00000000..5d17f1ae --- /dev/null +++ b/app/(landing)/campaigns/[slug]/page.tsx @@ -0,0 +1,139 @@ +'use client'; + +import { ProjectLayout } from '@/components/project-details/project-layout'; +import { reportError } from '@/lib/error-reporting'; +import { ProjectLoading } from '@/components/project-details/project-loading'; +import { getCrowdfundingProject } from '@/features/projects/api'; +import { useEffect, useState } from 'react'; +import { useSearchParams, notFound } from 'next/navigation'; +import { + getSubmissionDetails, + getHackathon, + type ParticipantSubmission, +} from '@/lib/api/hackathons'; +import type { Hackathon } from '@/lib/api/hackathons'; +import type { ProjectViewModel } from '@/features/projects/types/view-model'; +import { + buildFromCrowdfunding, + buildFromSubmission, +} from '@/features/projects/lib/build-view-model'; + +interface ProjectPageProps { + params: Promise<{ + slug: string; + }>; +} + +function ProjectContent({ + id, + isSubmission = false, +}: { + id: string; + isSubmission?: boolean; +}) { + const [vm, setVm] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + let cancelled = false; + + const fetchSubmission = async ( + submissionId: string + ): Promise => { + const submissionRes = await getSubmissionDetails(submissionId); + if (!submissionRes?.data) throw new Error('Submission not found'); + + const submission = submissionRes.data; + const subData = submission as unknown as Record; + + let hackathon: Hackathon | null = null; + if (subData.hackathonId) { + try { + const hackathonRes = await getHackathon( + subData.hackathonId as string + ); + hackathon = hackathonRes.data; + } catch (err) { + reportError(err, { + context: 'project-fetchHackathonDetails', + submissionId: id, + }); + } + } + + if (!hackathon) throw new Error('Hackathon details not found'); + + return buildFromSubmission( + submission as ParticipantSubmission & { members?: unknown[] }, + hackathon + ); + }; + + const fetchProjectData = async () => { + try { + setLoading(true); + setError(null); + + if (isSubmission) { + const result = await fetchSubmission(id); + if (!cancelled) setVm(result); + return; + } + + try { + const projectData = await getCrowdfundingProject(id); + if (!cancelled && projectData) { + setVm(buildFromCrowdfunding(projectData)); + return; + } + } catch { + const result = await fetchSubmission(id); + if (!cancelled) setVm(result); + } + } catch (err) { + reportError(err, { context: 'project-fetch', id }); + if (!cancelled) setError('Failed to fetch project data'); + } finally { + if (!cancelled) setLoading(false); + } + }; + + fetchProjectData(); + return () => { + cancelled = true; + }; + }, [id, isSubmission]); + + if (loading) { + return ; + } + + if (error || !vm) { + notFound(); + } + + return ( +
+
+ +
+
+ ); +} + +export default function ProjectPage({ params }: ProjectPageProps) { + const [id, setId] = useState(null); + const searchParams = useSearchParams(); + const isSubmission = searchParams.get('type') === 'submission'; + + useEffect(() => { + params.then(resolved => setId(resolved.slug)); + }, [params]); + + if (!id) { + return ; + } + + return ; +} diff --git a/app/(landing)/campaigns/layout.tsx b/app/(landing)/campaigns/layout.tsx new file mode 100644 index 00000000..3a31c0ce --- /dev/null +++ b/app/(landing)/campaigns/layout.tsx @@ -0,0 +1,12 @@ +import { Metadata } from 'next'; +import { generatePageMetadata } from '@/lib/metadata'; + +export const metadata: Metadata = generatePageMetadata('projects'); + +export default function ProjectsLayout({ + children, +}: { + children: React.ReactNode; +}) { + return <>{children}; +} diff --git a/app/(landing)/campaigns/page.tsx b/app/(landing)/campaigns/page.tsx new file mode 100644 index 00000000..0c7b9787 --- /dev/null +++ b/app/(landing)/campaigns/page.tsx @@ -0,0 +1,16 @@ +import React from 'react'; +import CampaignPageHero from '@/features/projects/components/CampaignPageHero'; +import ProjectsClient from '@/features/projects/components/ProjectsPage'; + +export default function CampaignsPage() { + return ( +
+
+
+ + +
+
+
+ ); +} diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/Overview.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/Overview.tsx index c1861629..b354a16d 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/Overview.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/Overview.tsx @@ -164,7 +164,7 @@ const Overview = () => { className={cn( 'relative z-10 mt-1.5 h-[10px] w-[10px] shrink-0 rounded-full border-2 transition-all duration-500', status === 'active' - ? 'ring-primary/10 bg-primary/40 shadow-[0_0_20px_rgba(167,249,80,0.5)] ring' + ? 'ring-primary/10 bg-primary/40 shadow-[0_0_20px_rgba(46,237,170,0.5)] ring' : status === 'completed' ? 'bg-primary ring-primary/10 ring-8' : 'border-[#262626] bg-[#262626]' diff --git a/app/(landing)/hackathons/[slug]/components/tabs/contents/resources/header.tsx b/app/(landing)/hackathons/[slug]/components/tabs/contents/resources/header.tsx index 582a4e74..edeb4669 100644 --- a/app/(landing)/hackathons/[slug]/components/tabs/contents/resources/header.tsx +++ b/app/(landing)/hackathons/[slug]/components/tabs/contents/resources/header.tsx @@ -31,7 +31,7 @@ export const ResourceHeader = ({ className={cn( 'rounded-lg px-4 py-2 text-sm font-bold transition-all duration-200 sm:px-6', activeTab === tab - ? 'bg-primary text-black shadow-[0_0_20px_rgba(167,249,80,0.2)]' + ? 'bg-primary text-black shadow-[0_0_20px_rgba(46,237,170,0.2)]' : 'text-gray-500 hover:bg-white/5 hover:text-white' )} > diff --git a/app/(landing)/layout.tsx b/app/(landing)/layout.tsx index 2bbf0d56..08310f90 100644 --- a/app/(landing)/layout.tsx +++ b/app/(landing)/layout.tsx @@ -4,6 +4,7 @@ import { Footer, Navbar } from '@/components/landing-page'; import { generatePageMetadata } from '@/lib/metadata'; // import { GoogleOneTap } from '@/components/auth/GoogleOneTap'; import { LandingWalletWrapper } from '@/components/wallet/LandingWalletWrapper'; +import { ProfileIncompleteBanner } from '@/components/ProfileIncompleteBanner'; export const metadata: Metadata = generatePageMetadata('home'); @@ -14,6 +15,7 @@ interface LandingLayoutProps { export default function LandingLayout({ children }: LandingLayoutProps) { return (
+
{children}