diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 27bbd99f9..000000000 --- a/.eslintrc.js +++ /dev/null @@ -1,43 +0,0 @@ -module.exports = { - env: { - browser: true, - es2021: true, - node: true, - }, - extends: ['eslint:recommended'], - parser: '@typescript-eslint/parser', - parserOptions: { - ecmaVersion: 12, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - plugins: ['@typescript-eslint', 'react', 'react-hooks'], - rules: { - // TypeScript specific rules - '@typescript-eslint/no-unused-vars': 'error', - '@typescript-eslint/no-explicit-any': 'warn', - - // React specific rules - 'react-hooks/exhaustive-deps': 'warn', - 'react/no-unescaped-entities': 'off', // Allow apostrophes in JSX - - // General rules - 'no-console': 'warn', - 'prefer-const': 'error', - 'no-var': 'error', - }, - settings: { - react: { - version: 'detect', - }, - }, - ignorePatterns: [ - 'node_modules/**', - '.next/**', - 'out/**', - 'build/**', - 'next-env.d.ts', - ], -}; diff --git a/app/(landing)/about/page.tsx b/app/(landing)/about/page.tsx index f956d5f81..92382ce7a 100644 --- a/app/(landing)/about/page.tsx +++ b/app/(landing)/about/page.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { Metadata } from 'next'; import { generatePageMetadata } from '@/lib/metadata'; import Missionpage from '@/components/About-Mission/Missionpage'; @@ -9,7 +8,7 @@ import Timeline from '@/components/landing-page/about/timeline/Timeline'; import TestimonialsSection from '@/components/testimonials/TestimonialsSection'; import { testimonials } from '@/components/testimonials/data/testimonial'; import AboutUsHero2 from './AboutUsHero2'; -import OurTeam from './OurTeam'; +// import OurTeam from './OurTeam'; import Partners from './Partners'; export const metadata: Metadata = generatePageMetadata('about'); @@ -22,7 +21,7 @@ const AboutPage = () => { - + {/* */} diff --git a/app/(landing)/hackathons/[slug]/page.tsx b/app/(landing)/hackathons/[slug]/page.tsx index 765334b06..959d07a63 100644 --- a/app/(landing)/hackathons/[slug]/page.tsx +++ b/app/(landing)/hackathons/[slug]/page.tsx @@ -13,7 +13,7 @@ import { HackathonResources } from '@/components/hackathons/resources/resources' import SubmissionTab from '@/components/hackathons/submissions/submissionTab'; import { HackathonDiscussions } from '@/components/hackathons/discussion/comment'; import { TeamFormationTab } from '@/components/hackathons/team-formation/TeamFormationTab'; -import LoadingScreen from '@/components/landing-page/project/CreateProjectModal/LoadingScreen'; +import LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen'; import { useTimelineEvents } from '@/hooks/hackathon/use-timeline-events'; import { toast } from 'sonner'; import type { Participant } from '@/lib/api/hackathons'; diff --git a/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx b/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx index b5b6fa706..8a6a064a7 100644 --- a/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx +++ b/app/(landing)/hackathons/[slug]/team-invitations/[token]/accept/page.tsx @@ -72,8 +72,6 @@ const AcceptTeamInvitationPage = () => { }, 2000); } } catch (err: any) { - console.error('Failed to accept invitation:', err); - const errorMessage = err?.message || 'Failed to accept invitation'; setError(errorMessage); diff --git a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx index 38260de13..2cc8283af 100644 --- a/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx +++ b/app/(landing)/hackathons/preview/[orgId]/[draftId]/page.tsx @@ -8,7 +8,7 @@ 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 LoadingScreen from '@/features/projects/components/CreateProjectModal/LoadingScreen'; import { Badge } from '@/components/ui/badge'; import { BoundlessButton } from '@/components/buttons'; import type { Hackathon } from '@/lib/api/hackathons'; diff --git a/app/(landing)/me/layout.tsx b/app/(landing)/me/layout.tsx deleted file mode 100644 index 38a6e1452..000000000 --- a/app/(landing)/me/layout.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import { Metadata } from 'next'; - -export const metadata: Metadata = { - title: 'My Profile | Boundless', - description: 'View and manage your profile on Boundless', -}; - -export default function MeLayout({ children }: { children: React.ReactNode }) { - return ( -
- {children} -
- ); -} diff --git a/app/(landing)/notifications/page.tsx b/app/(landing)/notifications/page.tsx index 14dc5a6e8..067a71879 100644 --- a/app/(landing)/notifications/page.tsx +++ b/app/(landing)/notifications/page.tsx @@ -1,7 +1,7 @@ 'use client'; import { useState } from 'react'; -import { useNotifications } from '@/hooks/use-notifications'; +import { useNotifications } from '@/hooks/useNotifications'; import { useNotificationPolling } from '@/hooks/use-notification-polling'; import { NotificationList } from '@/components/notifications/NotificationList'; import { Button } from '@/components/ui/button'; @@ -81,7 +81,7 @@ export default function NotificationsPage() {

Notifications

-

+

{loading ? ( ) : ( @@ -89,7 +89,7 @@ export default function NotificationsPage() { {unreadCount} unread of {total} total )} -

+
{unreadCount > 0 && !loading && ( + +
+

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)/projects/[id]/milestone/[milestoneId]/page.tsx b/app/(landing)/projects/[slug]/milestone/[milestoneId]/page.tsx similarity index 52% rename from app/(landing)/projects/[id]/milestone/[milestoneId]/page.tsx rename to app/(landing)/projects/[slug]/milestone/[milestoneId]/page.tsx index 015908369..7d96bdc60 100644 --- a/app/(landing)/projects/[id]/milestone/[milestoneId]/page.tsx +++ b/app/(landing)/projects/[slug]/milestone/[milestoneId]/page.tsx @@ -2,46 +2,68 @@ 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 } from '@/lib/api/project'; -import { Crowdfunding } from '@/types/project'; +import { + getCrowdfundingProject, + getCrowdfundingMilestone, +} from '@/features/projects/api'; +import { Crowdfunding } from '@/features/projects/types'; interface MilestonePageProps { params: Promise<{ - id: string; // Project ID - milestoneId: string; // Milestone ID + slug: string; // Project slug + milestoneId: string; // Milestone index }>; } const MilestonePage = async ({ params }: MilestonePageProps) => { - const { id: projectId, milestoneId } = await params; + const { slug, milestoneId } = await params; - // Fetch project data + // Fetch project data and specific milestone let project: Crowdfunding | null = null; let milestone = null; try { - const crowdfundingProject = await getCrowdfundingProject(projectId); - project = crowdfundingProject; + // Fetch both project and milestone data + const [crowdfundingProject, milestoneData] = await Promise.all([ + getCrowdfundingProject(slug), + getCrowdfundingMilestone(slug, milestoneId), + ]); - // Find the specific milestone - const foundMilestone = project?.milestones?.find( - m => m.id === milestoneId || m.name === milestoneId - ); + project = crowdfundingProject; // Transform milestone to match component expectations - milestone = foundMilestone + milestone = milestoneData ? { - _id: foundMilestone.id || foundMilestone.name, - title: foundMilestone.name, - description: foundMilestone.description, - status: foundMilestone.status, - dueDate: foundMilestone.endDate, - amount: foundMilestone.amount, + _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 + // Handle error silently - milestone will be null } // If milestone not found, show error state @@ -74,9 +96,15 @@ const MilestonePage = async ({ params }: MilestonePageProps) => { > Details + + Proof & Status + Links @@ -85,12 +113,24 @@ const MilestonePage = async ({ params }: MilestonePageProps) => { + + + {milestone.evidence && ( + + )} + - + diff --git a/app/(landing)/projects/[id]/page.tsx b/app/(landing)/projects/[slug]/page.tsx similarity index 88% rename from app/(landing)/projects/[id]/page.tsx rename to app/(landing)/projects/[slug]/page.tsx index 1052d5d18..2ba0f1d24 100644 --- a/app/(landing)/projects/[id]/page.tsx +++ b/app/(landing)/projects/[slug]/page.tsx @@ -1,22 +1,25 @@ 'use client'; import { ProjectLayout } from '@/components/project-details/project-layout'; import { ProjectLoading } from '@/components/project-details/project-loading'; -import { notFound } from 'next/navigation'; -import { getCrowdfundingProject } from '@/lib/api/project'; -import type { Crowdfunding } from '@/types/project'; -import { useEffect, useState } from 'react'; -import { useSearchParams } from 'next/navigation'; +import { getCrowdfundingProject } from '@/features/projects/api'; +import type { Crowdfunding } from '@/features/projects/types'; +import { use, 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 { Milestone, TeamMember, SocialLink } from '@/types/project'; +import type { + Milestone, + TeamMember, + SocialLink, +} from '@/features/projects/types'; interface ProjectPageProps { params: Promise<{ - id: string; + slug: string; }>; } @@ -118,7 +121,7 @@ function ProjectContent({ } return ( -
+
{ const getParams = async () => { const resolvedParams = await params; - setId(resolvedParams.id); + setId(resolvedParams.slug); }; getParams(); }, [params]); diff --git a/app/(landing)/projects/page.tsx b/app/(landing)/projects/page.tsx index f68ed1ac0..2e777d750 100644 --- a/app/(landing)/projects/page.tsx +++ b/app/(landing)/projects/page.tsx @@ -1,11 +1,16 @@ -import ProjectPageHero from '@/components/Project-Page-Hero'; -import ProjectsClient from '@/components/project/ProjectsPage'; +import React from 'react'; +import ProjectPageHero from '@/features/projects/components/ProjectPageHero'; +import ProjectsClient from '@/features/projects/components/ProjectsPage'; export default function ProjectsPage() { return ( -
- - +
+
+
+ + +
+
); } diff --git a/app/dashboard/data.json b/app/dashboard/data.json new file mode 100644 index 000000000..ec0873641 --- /dev/null +++ b/app/dashboard/data.json @@ -0,0 +1,614 @@ +[ + { + "id": 1, + "header": "Cover page", + "type": "Cover page", + "status": "In Process", + "target": "18", + "limit": "5", + "reviewer": "Eddie Lake" + }, + { + "id": 2, + "header": "Table of contents", + "type": "Table of contents", + "status": "Done", + "target": "29", + "limit": "24", + "reviewer": "Eddie Lake" + }, + { + "id": 3, + "header": "Executive summary", + "type": "Narrative", + "status": "Done", + "target": "10", + "limit": "13", + "reviewer": "Eddie Lake" + }, + { + "id": 4, + "header": "Technical approach", + "type": "Narrative", + "status": "Done", + "target": "27", + "limit": "23", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 5, + "header": "Design", + "type": "Narrative", + "status": "In Process", + "target": "2", + "limit": "16", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 6, + "header": "Capabilities", + "type": "Narrative", + "status": "In Process", + "target": "20", + "limit": "8", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 7, + "header": "Integration with existing systems", + "type": "Narrative", + "status": "In Process", + "target": "19", + "limit": "21", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 8, + "header": "Innovation and Advantages", + "type": "Narrative", + "status": "Done", + "target": "25", + "limit": "26", + "reviewer": "Assign reviewer" + }, + { + "id": 9, + "header": "Overview of EMR's Innovative Solutions", + "type": "Technical content", + "status": "Done", + "target": "7", + "limit": "23", + "reviewer": "Assign reviewer" + }, + { + "id": 10, + "header": "Advanced Algorithms and Machine Learning", + "type": "Narrative", + "status": "Done", + "target": "30", + "limit": "28", + "reviewer": "Assign reviewer" + }, + { + "id": 11, + "header": "Adaptive Communication Protocols", + "type": "Narrative", + "status": "Done", + "target": "9", + "limit": "31", + "reviewer": "Assign reviewer" + }, + { + "id": 12, + "header": "Advantages Over Current Technologies", + "type": "Narrative", + "status": "Done", + "target": "12", + "limit": "0", + "reviewer": "Assign reviewer" + }, + { + "id": 13, + "header": "Past Performance", + "type": "Narrative", + "status": "Done", + "target": "22", + "limit": "33", + "reviewer": "Assign reviewer" + }, + { + "id": 14, + "header": "Customer Feedback and Satisfaction Levels", + "type": "Narrative", + "status": "Done", + "target": "15", + "limit": "34", + "reviewer": "Assign reviewer" + }, + { + "id": 15, + "header": "Implementation Challenges and Solutions", + "type": "Narrative", + "status": "Done", + "target": "3", + "limit": "35", + "reviewer": "Assign reviewer" + }, + { + "id": 16, + "header": "Security Measures and Data Protection Policies", + "type": "Narrative", + "status": "In Process", + "target": "6", + "limit": "36", + "reviewer": "Assign reviewer" + }, + { + "id": 17, + "header": "Scalability and Future Proofing", + "type": "Narrative", + "status": "Done", + "target": "4", + "limit": "37", + "reviewer": "Assign reviewer" + }, + { + "id": 18, + "header": "Cost-Benefit Analysis", + "type": "Plain language", + "status": "Done", + "target": "14", + "limit": "38", + "reviewer": "Assign reviewer" + }, + { + "id": 19, + "header": "User Training and Onboarding Experience", + "type": "Narrative", + "status": "Done", + "target": "17", + "limit": "39", + "reviewer": "Assign reviewer" + }, + { + "id": 20, + "header": "Future Development Roadmap", + "type": "Narrative", + "status": "Done", + "target": "11", + "limit": "40", + "reviewer": "Assign reviewer" + }, + { + "id": 21, + "header": "System Architecture Overview", + "type": "Technical content", + "status": "In Process", + "target": "24", + "limit": "18", + "reviewer": "Maya Johnson" + }, + { + "id": 22, + "header": "Risk Management Plan", + "type": "Narrative", + "status": "Done", + "target": "15", + "limit": "22", + "reviewer": "Carlos Rodriguez" + }, + { + "id": 23, + "header": "Compliance Documentation", + "type": "Legal", + "status": "In Process", + "target": "31", + "limit": "27", + "reviewer": "Sarah Chen" + }, + { + "id": 24, + "header": "API Documentation", + "type": "Technical content", + "status": "Done", + "target": "8", + "limit": "12", + "reviewer": "Raj Patel" + }, + { + "id": 25, + "header": "User Interface Mockups", + "type": "Visual", + "status": "In Process", + "target": "19", + "limit": "25", + "reviewer": "Leila Ahmadi" + }, + { + "id": 26, + "header": "Database Schema", + "type": "Technical content", + "status": "Done", + "target": "22", + "limit": "20", + "reviewer": "Thomas Wilson" + }, + { + "id": 27, + "header": "Testing Methodology", + "type": "Technical content", + "status": "In Process", + "target": "17", + "limit": "14", + "reviewer": "Assign reviewer" + }, + { + "id": 28, + "header": "Deployment Strategy", + "type": "Narrative", + "status": "Done", + "target": "26", + "limit": "30", + "reviewer": "Eddie Lake" + }, + { + "id": 29, + "header": "Budget Breakdown", + "type": "Financial", + "status": "In Process", + "target": "13", + "limit": "16", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 30, + "header": "Market Analysis", + "type": "Research", + "status": "Done", + "target": "29", + "limit": "32", + "reviewer": "Sophia Martinez" + }, + { + "id": 31, + "header": "Competitor Comparison", + "type": "Research", + "status": "In Process", + "target": "21", + "limit": "19", + "reviewer": "Assign reviewer" + }, + { + "id": 32, + "header": "Maintenance Plan", + "type": "Technical content", + "status": "Done", + "target": "16", + "limit": "23", + "reviewer": "Alex Thompson" + }, + { + "id": 33, + "header": "User Personas", + "type": "Research", + "status": "In Process", + "target": "27", + "limit": "24", + "reviewer": "Nina Patel" + }, + { + "id": 34, + "header": "Accessibility Compliance", + "type": "Legal", + "status": "Done", + "target": "18", + "limit": "21", + "reviewer": "Assign reviewer" + }, + { + "id": 35, + "header": "Performance Metrics", + "type": "Technical content", + "status": "In Process", + "target": "23", + "limit": "26", + "reviewer": "David Kim" + }, + { + "id": 36, + "header": "Disaster Recovery Plan", + "type": "Technical content", + "status": "Done", + "target": "14", + "limit": "17", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 37, + "header": "Third-party Integrations", + "type": "Technical content", + "status": "In Process", + "target": "25", + "limit": "28", + "reviewer": "Eddie Lake" + }, + { + "id": 38, + "header": "User Feedback Summary", + "type": "Research", + "status": "Done", + "target": "20", + "limit": "15", + "reviewer": "Assign reviewer" + }, + { + "id": 39, + "header": "Localization Strategy", + "type": "Narrative", + "status": "In Process", + "target": "12", + "limit": "19", + "reviewer": "Maria Garcia" + }, + { + "id": 40, + "header": "Mobile Compatibility", + "type": "Technical content", + "status": "Done", + "target": "28", + "limit": "31", + "reviewer": "James Wilson" + }, + { + "id": 41, + "header": "Data Migration Plan", + "type": "Technical content", + "status": "In Process", + "target": "19", + "limit": "22", + "reviewer": "Assign reviewer" + }, + { + "id": 42, + "header": "Quality Assurance Protocols", + "type": "Technical content", + "status": "Done", + "target": "30", + "limit": "33", + "reviewer": "Priya Singh" + }, + { + "id": 43, + "header": "Stakeholder Analysis", + "type": "Research", + "status": "In Process", + "target": "11", + "limit": "14", + "reviewer": "Eddie Lake" + }, + { + "id": 44, + "header": "Environmental Impact Assessment", + "type": "Research", + "status": "Done", + "target": "24", + "limit": "27", + "reviewer": "Assign reviewer" + }, + { + "id": 45, + "header": "Intellectual Property Rights", + "type": "Legal", + "status": "In Process", + "target": "17", + "limit": "20", + "reviewer": "Sarah Johnson" + }, + { + "id": 46, + "header": "Customer Support Framework", + "type": "Narrative", + "status": "Done", + "target": "22", + "limit": "25", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 47, + "header": "Version Control Strategy", + "type": "Technical content", + "status": "In Process", + "target": "15", + "limit": "18", + "reviewer": "Assign reviewer" + }, + { + "id": 48, + "header": "Continuous Integration Pipeline", + "type": "Technical content", + "status": "Done", + "target": "26", + "limit": "29", + "reviewer": "Michael Chen" + }, + { + "id": 49, + "header": "Regulatory Compliance", + "type": "Legal", + "status": "In Process", + "target": "13", + "limit": "16", + "reviewer": "Assign reviewer" + }, + { + "id": 50, + "header": "User Authentication System", + "type": "Technical content", + "status": "Done", + "target": "28", + "limit": "31", + "reviewer": "Eddie Lake" + }, + { + "id": 51, + "header": "Data Analytics Framework", + "type": "Technical content", + "status": "In Process", + "target": "21", + "limit": "24", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 52, + "header": "Cloud Infrastructure", + "type": "Technical content", + "status": "Done", + "target": "16", + "limit": "19", + "reviewer": "Assign reviewer" + }, + { + "id": 53, + "header": "Network Security Measures", + "type": "Technical content", + "status": "In Process", + "target": "29", + "limit": "32", + "reviewer": "Lisa Wong" + }, + { + "id": 54, + "header": "Project Timeline", + "type": "Planning", + "status": "Done", + "target": "14", + "limit": "17", + "reviewer": "Eddie Lake" + }, + { + "id": 55, + "header": "Resource Allocation", + "type": "Planning", + "status": "In Process", + "target": "27", + "limit": "30", + "reviewer": "Assign reviewer" + }, + { + "id": 56, + "header": "Team Structure and Roles", + "type": "Planning", + "status": "Done", + "target": "20", + "limit": "23", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 57, + "header": "Communication Protocols", + "type": "Planning", + "status": "In Process", + "target": "15", + "limit": "18", + "reviewer": "Assign reviewer" + }, + { + "id": 58, + "header": "Success Metrics", + "type": "Planning", + "status": "Done", + "target": "30", + "limit": "33", + "reviewer": "Eddie Lake" + }, + { + "id": 59, + "header": "Internationalization Support", + "type": "Technical content", + "status": "In Process", + "target": "23", + "limit": "26", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 60, + "header": "Backup and Recovery Procedures", + "type": "Technical content", + "status": "Done", + "target": "18", + "limit": "21", + "reviewer": "Assign reviewer" + }, + { + "id": 61, + "header": "Monitoring and Alerting System", + "type": "Technical content", + "status": "In Process", + "target": "25", + "limit": "28", + "reviewer": "Daniel Park" + }, + { + "id": 62, + "header": "Code Review Guidelines", + "type": "Technical content", + "status": "Done", + "target": "12", + "limit": "15", + "reviewer": "Eddie Lake" + }, + { + "id": 63, + "header": "Documentation Standards", + "type": "Technical content", + "status": "In Process", + "target": "27", + "limit": "30", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 64, + "header": "Release Management Process", + "type": "Planning", + "status": "Done", + "target": "22", + "limit": "25", + "reviewer": "Assign reviewer" + }, + { + "id": 65, + "header": "Feature Prioritization Matrix", + "type": "Planning", + "status": "In Process", + "target": "19", + "limit": "22", + "reviewer": "Emma Davis" + }, + { + "id": 66, + "header": "Technical Debt Assessment", + "type": "Technical content", + "status": "Done", + "target": "24", + "limit": "27", + "reviewer": "Eddie Lake" + }, + { + "id": 67, + "header": "Capacity Planning", + "type": "Planning", + "status": "In Process", + "target": "21", + "limit": "24", + "reviewer": "Jamik Tashpulatov" + }, + { + "id": 68, + "header": "Service Level Agreements", + "type": "Legal", + "status": "Done", + "target": "26", + "limit": "29", + "reviewer": "Assign reviewer" + } +] diff --git a/app/dashboard/page copy.tsx b/app/dashboard/page copy.tsx new file mode 100644 index 000000000..7546289b3 --- /dev/null +++ b/app/dashboard/page copy.tsx @@ -0,0 +1,225 @@ +'use client'; + +import { useRouter } from 'next/navigation'; +import { useEffect } from 'react'; +import { Button } from '@/components/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + Loader2, + LogOut, + User, + Mail, + Shield, + CheckCircle2, +} from 'lucide-react'; +import { authClient } from '@/lib/auth-client'; +import { useAuthActions } from '@/hooks/use-auth'; + +export default function DashboardPage() { + const { data: session, isPending: sessionPending } = authClient.useSession(); + const router = useRouter(); + const { logout } = useAuthActions(); + + useEffect(() => { + if (!sessionPending && !session?.user) { + router.push('/auth?mode=signin'); + } + }, [session, sessionPending, router]); + + const handleSignOut = async () => { + await logout(); + router.push('/'); + }; + + if (sessionPending) { + return ( +
+
+ + Loading... +
+
+ ); + } + + if (!session?.user) { + return null; + } + + return ( +
+
+ {/* Header */} +
+
+

Dashboard

+

+ Welcome back, {session.user.name || session.user.email} +

+
+ +
+ + {/* Stats Cards Grid */} +
+ {/* Profile Card */} + +
+ + +
+ +
+ Profile +
+
+ +
+ + + + {session.user.name?.charAt(0) || + session.user.email.charAt(0)} + + +
+

+ {session.user.name || 'No name'} +

+

+ {session.user.email} +

+
+
+
+
+ + {/* Account Details Card */} + +
+ + +
+ +
+ Account Details +
+
+ +
+ User ID + + {session.user.id} + +
+
+ Email + + {session.user.email} + +
+
+
+ + {/* Status Card */} + +
+ + +
+ +
+ Status & Verification +
+
+ +
+ Email Status +
+ {session.user.emailVerified ? ( + <> + + + Verified + + + ) : ( + + Unverified + + )} +
+
+
+ Account Status +
+
+ + Active + +
+
+ {(session.user as { lastLoginMethod?: string | null }) + ?.lastLoginMethod && ( +
+ + Last Login Method + + + {(() => { + const method = ( + session.user as { lastLoginMethod?: string | null } + ).lastLoginMethod; + return method === 'google' + ? 'Google' + : method === 'email' + ? 'Email' + : method || 'N/A'; + })()} + +
+ )} +
+
+
+ + {/* Welcome Card */} +
+ +
+
+ + + Welcome to Boundless + + + Your platform for crowdfunding and grants + + + +

+ This is your dashboard where you can manage your projects, view + contributions, and access all the features of the platform. The + authentication system is now working properly! +

+
+
+
+
+
+ ); +} diff --git a/app/dashboard/page.tsx b/app/dashboard/page.tsx index 7546289b3..69a6c55dc 100644 --- a/app/dashboard/page.tsx +++ b/app/dashboard/page.tsx @@ -1,225 +1,6 @@ -'use client'; +import { DashboardContent } from '@/components/dashboard-content'; +import React from 'react'; -import { useRouter } from 'next/navigation'; -import { useEffect } from 'react'; -import { Button } from '@/components/ui/button'; -import { - Card, - CardContent, - CardDescription, - CardHeader, - CardTitle, -} from '@/components/ui/card'; -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; -import { - Loader2, - LogOut, - User, - Mail, - Shield, - CheckCircle2, -} from 'lucide-react'; -import { authClient } from '@/lib/auth-client'; -import { useAuthActions } from '@/hooks/use-auth'; - -export default function DashboardPage() { - const { data: session, isPending: sessionPending } = authClient.useSession(); - const router = useRouter(); - const { logout } = useAuthActions(); - - useEffect(() => { - if (!sessionPending && !session?.user) { - router.push('/auth?mode=signin'); - } - }, [session, sessionPending, router]); - - const handleSignOut = async () => { - await logout(); - router.push('/'); - }; - - if (sessionPending) { - return ( -
-
- - Loading... -
-
- ); - } - - if (!session?.user) { - return null; - } - - return ( -
-
- {/* Header */} -
-
-

Dashboard

-

- Welcome back, {session.user.name || session.user.email} -

-
- -
- - {/* Stats Cards Grid */} -
- {/* Profile Card */} - -
- - -
- -
- Profile -
-
- -
- - - - {session.user.name?.charAt(0) || - session.user.email.charAt(0)} - - -
-

- {session.user.name || 'No name'} -

-

- {session.user.email} -

-
-
-
-
- - {/* Account Details Card */} - -
- - -
- -
- Account Details -
-
- -
- User ID - - {session.user.id} - -
-
- Email - - {session.user.email} - -
-
-
- - {/* Status Card */} - -
- - -
- -
- Status & Verification -
-
- -
- Email Status -
- {session.user.emailVerified ? ( - <> - - - Verified - - - ) : ( - - Unverified - - )} -
-
-
- Account Status -
-
- - Active - -
-
- {(session.user as { lastLoginMethod?: string | null }) - ?.lastLoginMethod && ( -
- - Last Login Method - - - {(() => { - const method = ( - session.user as { lastLoginMethod?: string | null } - ).lastLoginMethod; - return method === 'google' - ? 'Google' - : method === 'email' - ? 'Email' - : method || 'N/A'; - })()} - -
- )} -
-
-
- - {/* Welcome Card */} -
- -
-
- - - Welcome to Boundless - - - Your platform for crowdfunding and grants - - - -

- This is your dashboard where you can manage your projects, view - contributions, and access all the features of the platform. The - authentication system is now working properly! -

-
-
-
-
-
- ); +export default function Page() { + return ; } diff --git a/app/docs/how-to-write-evidence/page.tsx b/app/docs/how-to-write-evidence/page.tsx new file mode 100644 index 000000000..deec98664 --- /dev/null +++ b/app/docs/how-to-write-evidence/page.tsx @@ -0,0 +1,273 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Alert, AlertDescription } from '@/components/ui/alert'; +import { + CheckCircle2, + FileText, + Link as LinkIcon, + AlertCircle, +} from 'lucide-react'; +import Link from 'next/link'; + +export default function HowToWriteEvidencePage() { + return ( +
+
+

+ How to Write Milestone Evidence +

+

+ A comprehensive guide to documenting your milestone achievements + effectively +

+
+ +
+ {/* What is Evidence */} + + + + + What is Milestone Evidence? + + + +

+ Milestone evidence is documentation that proves you have completed + the work outlined in your milestone. It helps stakeholders, + contributors, and reviewers verify that the milestone objectives + have been met. +

+ + + Pro Tip: Good evidence is specific, verifiable, + and clearly demonstrates progress toward your milestone goals. + + +
+
+ + {/* Key Components */} + + + + + Key Components of Good Evidence + + + +
+
+

1. Clear Description

+

+ Explain what was accomplished in clear, concise language. + Avoid jargon and assume your reader may not be familiar with + technical details. +

+
+ +
+

2. Deliverables

+

+ List specific deliverables that were completed. This could + include code, designs, documentation, or other tangible + outputs. +

+
+ +
+

3. Supporting Links

+

+ Include relevant links to GitHub commits, pull requests, + deployed demos, documents, or other resources that verify your + work. +

+
+ +
+

4. Metrics & Results

+

+ When applicable, include quantifiable results such as + performance improvements, test coverage, or other measurable + outcomes. +

+
+
+
+
+ + {/* Example Template */} + + + + + Evidence Template + + + +
+
+

## Summary

+

+ Brief overview of what was accomplished in this milestone. +

+
+ +
+

## Deliverables

+

+ - Implemented user authentication system +
+ - Created API documentation +
- Added unit tests with 85% coverage +

+
+ +
+

## Links & Resources

+

+ - GitHub PR: https://github.com/... +
+ - Live Demo: https://demo.example.com +
- Documentation: https://docs.example.com +

+
+ +
+

## Results

+

+ - All acceptance criteria met +
+ - 20% improvement in load time +
- Zero critical bugs in testing +

+
+
+
+
+ + {/* Best Practices */} + + + Best Practices + + +
    +
  • + +
    +

    Be Specific

    +

    + Provide concrete examples and specific details rather than + vague statements +

    +
    +
  • +
  • + +
    +

    Include Proof

    +

    + Always link to verifiable sources like code repositories, + demos, or documents +

    +
    +
  • +
  • + +
    +

    Keep It Professional

    +

    + Use clear, professional language and proper formatting +

    +
    +
  • +
  • + +
    +

    Address Challenges

    +

    + If you faced obstacles, briefly mention them and how you + overcame them +

    +
    +
  • +
+
+
+ + {/* What to Avoid */} + + + + + What to Avoid + + + +
    +
  • + +
    +

    Vague Descriptions

    +

    + "Made some progress" or "Did some work" + don't provide useful information +

    +
    +
  • +
  • + +
    +

    Missing Links

    +

    + Evidence without verifiable links makes it difficult to + review your work +

    +
    +
  • +
  • + +
    +

    Excessive Length

    +

    + Keep it concise - reviewers appreciate brevity and clarity +

    +
    +
  • +
  • + +
    +

    Broken Links

    +

    + Always verify that your links work before submitting +

    +
    +
  • +
+
+
+ + {/* Call to Action */} + + +
+

+ Ready to Submit Evidence? +

+

+ Use the template and guidelines above to document your milestone + achievements. +

+ + Go to My Campaigns + +
+
+
+
+
+ ); +} diff --git a/app/globals.css b/app/globals.css index 83de59016..7bddde3b2 100644 --- a/app/globals.css +++ b/app/globals.css @@ -206,42 +206,47 @@ body { :root { --radius: 0.625rem; - --background: #030303; - --foreground: oklch(0.145 0 0); - --card: oklch(1 0 0); - --card-foreground: oklch(0.145 0 0); - --popover: oklch(1 0 0); + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); --popover-foreground: oklch(0.145 0 0); --primary: #a7f950; - --primary-foreground: #020502; - --secondary: oklch(0.97 0 0); - --secondary-foreground: oklch(0.205 0 0); - --muted: oklch(0.97 0 0); - --muted-foreground: oklch(0.556 0 0); - --accent: oklch(0.97 0 0); - --accent-foreground: oklch(0.205 0 0); - --destructive: oklch(0.577 0.245 27.325); - --border: oklch(0.922 0 0); - --input: oklch(0.922 0 0); - --ring: oklch(0.708 0 0); - --chart-1: oklch(0.646 0.222 41.116); - --chart-2: oklch(0.6 0.118 184.704); - --chart-3: oklch(0.398 0.07 227.392); - --chart-4: oklch(0.828 0.189 84.429); - --chart-5: oklch(0.769 0.188 70.08); - --sidebar: oklch(0.985 0 0); - --stepper-foreground: oklch(0.2046 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); --stepper-border: oklch(0.2264 0 0); --stepper-border-active: oklch(0.3501 0.0381 190.35); --stepper-text-inactive: oklch(0.7731 0 0); --placeholder: oklch(0.6567 0 0); - --sidebar-foreground: oklch(0.145 0 0); - --sidebar-primary: oklch(0.205 0 0); + --sidebar-foreground: #fff; + --sidebar-primary: oklch(0.488 0.243 264.376); --sidebar-primary-foreground: oklch(0.985 0 0); - --sidebar-accent: oklch(0.97 0 0); - --sidebar-accent-foreground: oklch(0.205 0 0); - --sidebar-border: oklch(0.922 0 0); - --sidebar-ring: oklch(0.708 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); --office-brown: #e4dbdb; --office-brown-darker: #645d5d; --warning-orange: #fbe2b7; diff --git a/app/me/crowdfunding/[slug]/components/CampaignBanner.tsx b/app/me/crowdfunding/[slug]/components/CampaignBanner.tsx new file mode 100644 index 000000000..09e9215b5 --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/CampaignBanner.tsx @@ -0,0 +1,27 @@ +import { Card, CardContent } from '@/components/ui/card'; +import Image from 'next/image'; + +interface CampaignBannerProps { + project: { + banner: string | null; + title: string; + }; +} + +export function CampaignBanner({ project }: CampaignBannerProps) { + if (!project.banner) return null; + + return ( + + + {project.title} + + + ); +} diff --git a/app/me/crowdfunding/[slug]/components/CampaignTabs.tsx b/app/me/crowdfunding/[slug]/components/CampaignTabs.tsx new file mode 100644 index 000000000..0e2f4a6a1 --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/CampaignTabs.tsx @@ -0,0 +1,74 @@ +import { Target, DollarSign, Users } from 'lucide-react'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; +import { CampaignTeamTab } from '@/components/crowdfunding/campaign-team-tab'; +import { CampaignMilestonesTab } from '@/components/crowdfunding/campaign-milestones-tab'; +import { CampaignCommentsTab } from '@/components/crowdfunding/campaign-comments-tab'; +import { CampaignFundingTab } from '@/components/crowdfunding/campaign-funding-tab'; +import { Crowdfunding } from '@/features/projects/types'; + +interface CampaignTabsProps { + campaign: Crowdfunding; +} + +export function CampaignTabs({ campaign }: CampaignTabsProps) { + return ( + + + + + Team + + + + Milestones + + + + + + Comments + + + + Funding + + + + + + + + + + + + + + + + + + + + ); +} diff --git a/app/me/crowdfunding/[slug]/components/FundingProgress.tsx b/app/me/crowdfunding/[slug]/components/FundingProgress.tsx new file mode 100644 index 000000000..188ced83b --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/FundingProgress.tsx @@ -0,0 +1,76 @@ +import { format } from 'date-fns'; +import { Target } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Progress } from '@/components/ui/progress'; +import { Separator } from '@/components/ui/separator'; +import { Crowdfunding } from '@/features/projects/types'; + +interface FundingProgressProps { + campaign: Crowdfunding; +} + +export function FundingProgress({ campaign }: FundingProgressProps) { + const fundingProgress = + campaign.fundingGoal > 0 + ? (campaign.fundingRaised / campaign.fundingGoal) * 100 + : 0; + const daysLeft = Math.ceil( + (new Date(campaign.fundingEndDate).getTime() - new Date().getTime()) / + (1000 * 60 * 60 * 24) + ); + + return ( + + + + + Funding Progress + + + +
+
+ ${campaign.fundingRaised.toLocaleString()} +
+
+ of ${campaign.fundingGoal.toLocaleString()} goal +
+
+ + + +
+
+
+ {fundingProgress.toFixed(1)}% +
+
Funded
+
+
+
+ {daysLeft > 0 ? daysLeft : 0} +
+
+ {daysLeft > 0 ? 'Days Left' : 'Ended'} +
+
+
+ + + +
+
+ Currency + {campaign.fundingCurrency} +
+
+ End Date + + {format(new Date(campaign.fundingEndDate), 'MMM dd, yyyy')} + +
+
+
+
+ ); +} diff --git a/app/me/crowdfunding/[slug]/components/ProjectDetails.tsx b/app/me/crowdfunding/[slug]/components/ProjectDetails.tsx new file mode 100644 index 000000000..af759ba25 --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/ProjectDetails.tsx @@ -0,0 +1,89 @@ +import { format } from 'date-fns'; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { useMarkdown } from '@/hooks/use-markdown'; +import { Crowdfunding } from '@/features/projects/types'; +import Image from 'next/image'; + +interface ProjectDetailsProps { + campaign: Crowdfunding; + project: Crowdfunding['project']; +} + +export function ProjectDetails({ campaign, project }: ProjectDetailsProps) { + const { styledContent } = useMarkdown(project.description || '', { + breaks: true, + gfm: true, + pedantic: true, + loadingDelay: 0, + }); + + const getStatusColor = (status: string) => { + switch (status.toLowerCase()) { + case 'active': + case 'funding': + return 'bg-green-500/20 text-green-400 border-green-500/30'; + case 'completed': + return 'bg-blue-500/20 text-blue-400 border-blue-500/30'; + case 'draft': + return 'bg-yellow-500/20 text-yellow-400 border-yellow-500/30'; + default: + return 'bg-gray-500/20 text-gray-400 border-gray-500/30'; + } + }; + + return ( + + +
+
+ {project.logo && ( + {project.title} + )} +
+ {project.title} + + Created {format(new Date(campaign.createdAt), 'MMM dd, yyyy')} + +
+
+ + {project.status} + +
+
+ +
+

Description

+
{styledContent}
+
+ + {project.details && ( +
+

Details

+

{project.details}

+
+ )} + + {project.vision && ( +
+

Vision

+

{project.vision}

+
+ )} +
+
+ ); +} diff --git a/app/me/crowdfunding/[slug]/components/ProjectLinks.tsx b/app/me/crowdfunding/[slug]/components/ProjectLinks.tsx new file mode 100644 index 000000000..775279d6b --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/ProjectLinks.tsx @@ -0,0 +1,69 @@ +import { ExternalLink, Github, Globe } from 'lucide-react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; + +interface ProjectLinksProps { + project: { + projectWebsite?: string; + githubUrl?: string; + demoVideo?: string; + }; +} + +export function ProjectLinks({ project }: ProjectLinksProps) { + const hasLinks = + project.projectWebsite || project.githubUrl || project.demoVideo; + + if (!hasLinks) return null; + + return ( + + + Project Links + + + {project.projectWebsite && ( + + )} + + {project.githubUrl && ( + + )} + + {project.demoVideo && ( + + )} + + + ); +} diff --git a/app/me/crowdfunding/[slug]/components/TagsSection.tsx b/app/me/crowdfunding/[slug]/components/TagsSection.tsx new file mode 100644 index 000000000..1acd661ac --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/TagsSection.tsx @@ -0,0 +1,33 @@ +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; + +interface TagsSectionProps { + project: { + tags?: string[]; + }; +} + +export function TagsSection({ project }: TagsSectionProps) { + if (!project.tags || project.tags.length === 0) return null; + + return ( + + + Tags + + +
+ {project.tags.map((tag, index) => ( + + {tag} + + ))} +
+
+
+ ); +} diff --git a/app/me/crowdfunding/[slug]/components/index.ts b/app/me/crowdfunding/[slug]/components/index.ts new file mode 100644 index 000000000..da09ad809 --- /dev/null +++ b/app/me/crowdfunding/[slug]/components/index.ts @@ -0,0 +1,6 @@ +export { CampaignBanner } from './CampaignBanner'; +export { ProjectDetails } from './ProjectDetails'; +export { CampaignTabs } from './CampaignTabs'; +export { FundingProgress } from './FundingProgress'; +export { ProjectLinks } from './ProjectLinks'; +export { TagsSection } from './TagsSection'; diff --git a/app/me/crowdfunding/[slug]/contributions/contributions-data-table.tsx b/app/me/crowdfunding/[slug]/contributions/contributions-data-table.tsx new file mode 100644 index 000000000..b8eac0a5a --- /dev/null +++ b/app/me/crowdfunding/[slug]/contributions/contributions-data-table.tsx @@ -0,0 +1,170 @@ +'use client'; + +import * as React from 'react'; +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from '@/components/ui/table'; +import { + ColumnFiltersState, + SortingState, + VisibilityState, + flexRender, + getCoreRowModel, + getFilteredRowModel, + getPaginationRowModel, + getSortedRowModel, + useReactTable, +} from '@tanstack/react-table'; +import { DataTablePagination } from '@/components/ui/data-table-pagination'; +import { Contributor } from '@/features/projects/types'; +import { contributionsTableColumns } from './contributions-table-columns'; +import { Input } from '@/components/ui/input'; +import { Search } from 'lucide-react'; + +interface ContributionsDataTableProps { + data: Contributor[]; + loading?: boolean; +} + +export function ContributionsDataTable({ + data, + loading = false, +}: ContributionsDataTableProps) { + const [sorting, setSorting] = React.useState([ + { id: 'date', desc: true }, // Sort by date descending by default + ]); + const [columnFilters, setColumnFilters] = React.useState( + [] + ); + const [columnVisibility, setColumnVisibility] = + React.useState({}); + const [rowSelection, setRowSelection] = React.useState({}); + const [globalFilter, setGlobalFilter] = React.useState(''); + + const table = useReactTable({ + data, + columns: contributionsTableColumns, + onSortingChange: setSorting, + onColumnFiltersChange: setColumnFilters, + getCoreRowModel: getCoreRowModel(), + getPaginationRowModel: getPaginationRowModel(), + getSortedRowModel: getSortedRowModel(), + getFilteredRowModel: getFilteredRowModel(), + onColumnVisibilityChange: setColumnVisibility, + onRowSelectionChange: setRowSelection, + onGlobalFilterChange: setGlobalFilter, + globalFilterFn: 'includesString', + state: { + sorting, + columnFilters, + columnVisibility, + rowSelection, + globalFilter, + }, + initialState: { + pagination: { + pageSize: 10, + }, + }, + }); + + return ( +
+ {/* Search */} +
+
+ + setGlobalFilter(String(event.target.value))} + className='focus-visible:border-primary border-[#2B2B2B] bg-[#101010] pl-9 text-white placeholder:text-[#919191]' + /> +
+
+ + {/* Table */} +
+ + + {table.getHeaderGroups().map(headerGroup => ( + + {headerGroup.headers.map(header => ( + + {header.isPlaceholder + ? null + : flexRender( + header.column.columnDef.header, + header.getContext() + )} + + ))} + + ))} + + + {loading ? ( + + +
+
+
+ + + ) : table.getRowModel().rows?.length ? ( + table.getRowModel().rows.map(row => ( + + {row.getVisibleCells().map(cell => ( + + {flexRender( + cell.column.columnDef.cell, + cell.getContext() + )} + + ))} + + )) + ) : ( + + +
+

No contributions yet

+

+ Waiting for contributions to this project +

+
+
+
+ )} + +
+
+ + {/* Pagination */} + {table.getRowModel().rows?.length > 0 && ( + + )} +
+ ); +} diff --git a/app/me/crowdfunding/[slug]/contributions/contributions-table-columns.tsx b/app/me/crowdfunding/[slug]/contributions/contributions-table-columns.tsx new file mode 100644 index 000000000..e355b9421 --- /dev/null +++ b/app/me/crowdfunding/[slug]/contributions/contributions-table-columns.tsx @@ -0,0 +1,136 @@ +'use client'; + +import { ColumnDef } from '@tanstack/react-table'; +import { Contributor } from '@/features/projects/types'; +import { formatNumber } from '@/lib/utils'; +import { ExternalLink } from 'lucide-react'; +import { getTransactionExplorerUrl } from '@/lib/wallet-utils'; +import { Button } from '@/components/ui/button'; +import { format } from 'date-fns'; +import Link from 'next/link'; + +export const contributionsTableColumns: ColumnDef[] = [ + { + accessorKey: 'name', + header: 'Contributor', + cell: ({ row }) => { + const contributor = row.original; + const isAnonymous = !contributor.name || contributor.name === 'Anonymous'; + + if (isAnonymous) { + return ( +
+
+ ? +
+ Anonymous +
+ ); + } + + const profileUrl = `/profile/${contributor.username}`; + + return ( + + {contributor.image ? ( +
+ ) : ( +
+ + {contributor.name.charAt(0).toUpperCase()} + +
+ )} +
+ + {contributor.name} + + {contributor.username && ( + + @{contributor.username} + + )} +
+ + ); + }, + }, + { + accessorKey: 'amount', + header: 'Amount', + cell: ({ row }) => { + const amount = row.getValue('amount') as number; + return ( +
+ {formatNumber(amount)} USD +
+ ); + }, + }, + { + accessorKey: 'message', + header: 'Message', + cell: ({ row }) => { + const message = row.original.message; + + if (!message) { + return No message; + } + + return ( +
+

{message}

+
+ ); + }, + }, + { + accessorKey: 'date', + header: 'Date', + cell: ({ row }) => { + const date = row.getValue('date') as string; + try { + return ( + + {format(new Date(date), 'MMM dd, yyyy')} + + ); + } catch { + return Invalid date; + } + }, + }, + { + accessorKey: 'transactionHash', + header: 'Transaction', + cell: ({ row }) => { + const txHash = row.getValue('transactionHash') as string; + const explorerUrl = getTransactionExplorerUrl(txHash); + + return ( + + ); + }, + }, +]; diff --git a/app/me/crowdfunding/[slug]/contributions/page.tsx b/app/me/crowdfunding/[slug]/contributions/page.tsx new file mode 100644 index 000000000..7f55c1838 --- /dev/null +++ b/app/me/crowdfunding/[slug]/contributions/page.tsx @@ -0,0 +1,102 @@ +'use client'; + +import { use, useEffect, useState } from 'react'; +import { getCrowdfundingProject } from '@/features/projects/api'; +import type { Crowdfunding } from '@/features/projects/types'; +import { ContributionsDataTable } from './contributions-data-table'; +import { ArrowLeft } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { useRouter } from 'next/navigation'; +import { ContributionsMetrics } from '@/components/crowdfunding/contributions-metrics'; + +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/me/crowdfunding/[slug]/edit/components/BasicInfoSection.tsx b/app/me/crowdfunding/[slug]/edit/components/BasicInfoSection.tsx new file mode 100644 index 000000000..b1dfceac9 --- /dev/null +++ b/app/me/crowdfunding/[slug]/edit/components/BasicInfoSection.tsx @@ -0,0 +1,233 @@ +'use client'; + +import React, { useState } from 'react'; +import { Input } from '@/components/ui/input'; +import { Label } from '@/components/ui/label'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { Textarea } from '@/components/ui/textarea'; +import { Button } from '@/components/ui/button'; +import { Upload, X } from 'lucide-react'; +import Image from 'next/image'; +import { uploadService } from '@/lib/api/upload'; +import { cn } from '@/lib/utils'; + +interface BasicInfoSectionProps { + data: { + title: string; + logo: string; + vision: string; + category: string; + }; + onChange: (field: string, value: any) => void; +} + +const categories = [ + 'DeFi & Finance', + 'Gaming & Metaverse', + 'Social & Community', + 'Infrastructure & Tooling', + 'AI & Machine Learning', + 'Sustainability & Impact', + 'Other', +]; + +export function BasicInfoSection({ data, onChange }: BasicInfoSectionProps) { + const [uploading, setUploading] = useState(false); + const [isDragOver, setIsDragOver] = useState(false); + + const handleLogoUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + setUploading(true); + try { + const result = await uploadService.uploadSingle(file); + onChange('logo', result.data.url); + } catch { + // Error handled silently + } finally { + setUploading(false); + } + }; + + const handleDragOver = (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(true); + }; + + const handleDragLeave = () => { + setIsDragOver(false); + }; + + const handleDrop = async (e: React.DragEvent) => { + e.preventDefault(); + setIsDragOver(false); + + const file = e.dataTransfer.files?.[0]; + if (!file) return; + + setUploading(true); + try { + const result = await uploadService.uploadSingle(file); + onChange('logo', result.data.url); + } catch { + // Error handled silently + } finally { + setUploading(false); + } + }; + + const removeLogo = () => { + onChange('logo', ''); + }; + + return ( +
+ {/* Two Column Grid Layout */} +
+ {/* Left Column - Title and Logo */} +
+ {/* Title */} +
+ + onChange('title', e.target.value)} + placeholder='Enter project name/title' + className='bg-card text-foreground placeholder:text-muted-foreground' + /> +
+ + {/* Logo */} +
+ +
+ {data.logo ? ( +
+
+ Project logo +
+ +
+ ) : null} + +
+
+
+ + {/* Right Column - Vision and Category */} +
+ {/* Vision */} +
+ +