diff --git a/.env.example b/.env.example index 82b3af2d..6fecbcaf 100644 --- a/.env.example +++ b/.env.example @@ -36,3 +36,9 @@ PUBLIC_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015" PUBLIC_STELLAR_RPC_URL="https://soroban-testnet.stellar.org:443" STELLAR_ACCOUNT="collins" STELLAR_NETWORK="testnet" + +# Cloudinary +CLOUDINARY_CLOUD_NAME= #neccesary +CLOUDINARY_API_KEY= #neccesary +CLOUDINARY_API_SECRET= #neccesary +CLOUDINARY_URL= #neccesary \ No newline at end of file diff --git a/app/(dashboard)/projects/[id]/page.tsx b/app/(dashboard)/projects/[id]/page.tsx index 770c155f..507dc3b8 100644 --- a/app/(dashboard)/projects/[id]/page.tsx +++ b/app/(dashboard)/projects/[id]/page.tsx @@ -1,33 +1,149 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useParams, useRouter } from "next/navigation"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Card, CardContent } from "@/components/ui/card"; import { Progress } from "@/components/ui/progress"; import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; -import { Users, Wallet, Plus } from "lucide-react"; +import { Users, Wallet } from "lucide-react"; import Image from "next/image"; import { ProjectActions } from "./project-actions"; import { MilestoneTracker } from "./milestone-tracker"; -// import { VotingSection } from "./voting-section" import { FundingSection } from "./funding-section"; +import { VotingSection } from "./voting-section"; +import { useSession } from "next-auth/react"; +import { TeamSection } from "./team-section"; +import type { Vote } from "@prisma/client"; + +type ValidationStatus = "PENDING" | "REJECTED" | "VALIDATED"; -// Note this should be uncommented when project id is provided. Blocked by DB creation and priject creation -// interface ProjectPageProps { -// params: { -// id: string -// } -// } +type Project = { + id: string; + userId: string; + title: string; + description: string; + fundingGoal: number; + category: string; + bannerUrl: string | null; + profileUrl: string | null; + blockchainTx: string | null; + ideaValidation: ValidationStatus; + createdAt: string; + user: { + id: string; + name: string | null; + image: string | null; + }; + votes: Vote[]; + teamMembers: { + id: string; + fullName: string; + role: string; + bio: string | null; + profileImage: string | null; + github: string | null; + twitter: string | null; + discord: string | null; + linkedin: string | null; + userId: string | null; + }[]; + _count: { + votes: number; + teamMembers: number; + }; +}; -// export default function ProjectPage({ params }: ProjectPageProps) { export default function ProjectPage() { - const isTeamMember = true; // This would come from your auth logic to be handled by Benjamin + const params = useParams(); + const router = useRouter(); + const { data: session } = useSession(); + const [project, setProject] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // Check if user is a team member + const isTeamMember = + project?.userId === session?.user?.id || + project?.teamMembers.some((member) => member.userId === session?.user?.id); + + useEffect(() => { + async function fetchProject() { + try { + const id = params?.id as string; + if (!id) return; + + const response = await fetch(`/api/projects/${id}`); + + if (response.status === 404) { + router.push("/projects"); + return; + } + + if (!response.ok) { + throw new Error("Failed to fetch project"); + } + + const data = await response.json(); + setProject(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + console.error(err); + } finally { + setLoading(false); + } + } + + fetchProject(); + }, [params, router]); + + if (loading) { + return
Loading project...
; + } + + if (error) { + return ( +
+ Error: {error} +
+ ); + } + + if (!project) { + return
Project not found
; + } + + // Calculate validation progress - this is just an example + const validationProgress = + project.ideaValidation === "VALIDATED" + ? 100 + : project.ideaValidation === "REJECTED" + ? 0 + : Math.min(project._count.votes, 100); + + // Determine validation phase + const getValidationPhase = () => { + switch (project.ideaValidation) { + case "VALIDATED": + return "Phase 4 of 4"; + case "REJECTED": + return "Rejected"; + case "PENDING": + if (project._count.votes >= 75) return "Phase 3 of 4"; + if (project._count.votes >= 50) return "Phase 2 of 4"; + if (project._count.votes >= 25) return "Phase 1 of 4"; + return "Initial Phase"; + } + }; return (
Project Banner
- - PJ + + + {project.title.substring(0, 2).toUpperCase()} +
-

Boundless

+

+ {project.title} +

- 50 Supporters + {project._count.votes}{" "} + Supporters - {/* - 1.5k Points - */} - 50 Funded + $ + {project.fundingGoal.toLocaleString()} Goal
- +
{/* Progress Section */}

Validation Progress

- Phase 4 of 4 + {getValidationPhase()}
- +

- Currently in Technical Review Phase + {project.ideaValidation === "VALIDATED" + ? "Project has been validated and is now in funding stage" + : project.ideaValidation === "REJECTED" + ? "Project did not receive enough community support" + : "Currently in community validation phase"}

@@ -96,18 +222,9 @@ export default function ProjectPage() {
-

About the Project

-

Detailed project description...

- -

Project Goals

-
    -
  • Goal 1
  • -
  • Goal 2
  • -
  • Goal 3
  • -
- -

Resources

-
+

About the Project

+

{project.description}

+
@@ -121,11 +238,18 @@ export default function ProjectPage() { - + - {/* */} + vote.userId === session?.user?.id, + )} + ideaValidation={project.ideaValidation} + /> @@ -133,39 +257,11 @@ export default function ProjectPage() { - - -
- Team Members - {isTeamMember && ( - - )} -
-
- -
- {Array.from({ length: 3 }).map((_, i) => ( -
- - - TM - -
-
Team Member Name
-
- Role -
-
-
- ))} -
-
-
+
diff --git a/app/(dashboard)/projects/[id]/project-actions.tsx b/app/(dashboard)/projects/[id]/project-actions.tsx index 75a24795..0c9da922 100644 --- a/app/(dashboard)/projects/[id]/project-actions.tsx +++ b/app/(dashboard)/projects/[id]/project-actions.tsx @@ -8,7 +8,8 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; -import { Edit, MoreVertical, Plus, Share2, Flag } from "lucide-react"; +import { Edit, MoreVertical, Share2, Flag, Plus } from "lucide-react"; +import Link from "next/link"; interface ProjectActionsProps { isTeamMember: boolean; @@ -22,9 +23,11 @@ export function ProjectActions({ isTeamMember }: ProjectActionsProps) { - + + + ) : ( diff --git a/app/(dashboard)/projects/[id]/team-section.tsx b/app/(dashboard)/projects/[id]/team-section.tsx new file mode 100644 index 00000000..a47a2298 --- /dev/null +++ b/app/(dashboard)/projects/[id]/team-section.tsx @@ -0,0 +1,177 @@ +"use client"; + +import { useState } from "react"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Button } from "@/components/ui/button"; +import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar"; +import { + Plus, + Github, + Twitter, + Linkedin, + MessageSquare, + ChevronDown, + ChevronUp, +} from "lucide-react"; +import Link from "next/link"; +import type { TeamMember } from "@/types/project"; + +interface TeamSectionProps { + teamMembers: TeamMember[]; + isTeamMember: boolean; + projectId: string; +} + +export function TeamSection({ + teamMembers, + isTeamMember, + projectId, +}: TeamSectionProps) { + return ( + + +
+ Team Members + {isTeamMember && ( + + )} +
+
+ +
+ {teamMembers.length > 0 ? ( + teamMembers.map((member) => ( + + )) + ) : ( +
+ No team members added yet +
+ )} +
+
+
+ ); +} + +function TeamMemberCard({ + member, + isTeamMember, +}: { member: TeamMember; isTeamMember: boolean }) { + const [expanded, setExpanded] = useState(false); + + // Check if member has any social links + const hasSocialLinks = + member.github || member.twitter || member.discord || member.linkedin; + + return ( +
+
+ + + + {member.fullName.substring(0, 2).toUpperCase()} + + +
+
{member.fullName}
+
{member.role}
+ + {/* Social Links */} + {hasSocialLinks && ( +
+ {member.github && ( + + + + )} + {member.twitter && ( + + + + )} + {member.discord && ( +
+ +
+ )} + {member.linkedin && ( + + + + )} +
+ )} +
+ + {/* Expand/Collapse button for bio */} + {member.bio && ( + + )} +
+ + {/* Bio section (expandable) */} + {member.bio && expanded && ( +
+

{member.bio}

+
+ )} + + {/* Edit button for team members */} + {isTeamMember && ( +
+ +
+ )} +
+ ); +} diff --git a/app/(dashboard)/projects/[id]/voting-section.tsx b/app/(dashboard)/projects/[id]/voting-section.tsx new file mode 100644 index 00000000..267315fe --- /dev/null +++ b/app/(dashboard)/projects/[id]/voting-section.tsx @@ -0,0 +1,166 @@ +"use client"; + +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import { Progress } from "@/components/ui/progress"; +import { Users, AlertTriangle } from "lucide-react"; +import { VoteButton } from "@/components/shared/vote-button"; + +interface VotingSectionProps { + projectId: string; + initialVoteCount: number; + initialUserVoted: boolean; + ideaValidation: "PENDING" | "REJECTED" | "VALIDATED"; +} + +export function VotingSection({ + projectId, + initialVoteCount, + initialUserVoted, + ideaValidation, +}: VotingSectionProps) { + // Determine if voting is allowed based on validation status + const votingEnabled = ideaValidation === "PENDING"; + + // Calculate progress - this is just an example, adjust as needed + // Assuming 100 votes is the goal for validation + const votingGoal = 100; + const progressPercentage = Math.min( + Math.round((initialVoteCount / votingGoal) * 100), + 100, + ); + + return ( +
+ + + Project Validation Voting + + +
+ {/* Validation Status */} +
+
+ + {ideaValidation.charAt(0) + + ideaValidation.slice(1).toLowerCase()} + + + {ideaValidation === "PENDING" + ? "This project is awaiting community validation" + : ideaValidation === "VALIDATED" + ? "This project has been validated by the community" + : "This project was rejected by the community"} + +
+
+ + {/* Voting Progress */} + {ideaValidation === "PENDING" && ( +
+
+ Validation Progress + + {initialVoteCount} / {votingGoal} votes + +
+ +

+ This project needs {votingGoal - initialVoteCount} more votes + to reach the validation threshold +

+
+ )} + + {/* Vote Button */} +
+
+ + + Community support helps projects move to the funding stage + +
+ + {votingEnabled ? ( + + ) : ( +
+ Voting is{" "} + {ideaValidation === "VALIDATED" ? "complete" : "closed"} for + this project +
+ )} +
+ + {/* Info Box */} + {ideaValidation !== "PENDING" && ( +
+ +
+

+ Voting is{" "} + {ideaValidation === "VALIDATED" ? "complete" : "closed"} +

+

+ {ideaValidation === "VALIDATED" + ? "This project has been validated and has moved to the funding stage." + : "This project did not receive enough votes for validation."} +

+
+
+ )} +
+
+
+ + {/* Recent Voters */} + {initialVoteCount > 0 && ( + + + Recent Supporters + + +
+ {/* This would ideally be populated with actual voter data */} + {Array.from({ length: Math.min(3, initialVoteCount) }).map( + (_, i) => { + const placeholderKey = `voter-placeholder-${i}-${projectId}`; + return ( +
+
+
Community Member
+
+ {i === 0 + ? "Just now" + : i === 1 + ? "2 hours ago" + : "1 day ago"} +
+
+ Supporter +
+ ); + }, + )} +
+
+
+ )} +
+ ); +} diff --git a/app/(dashboard)/projects/page.tsx b/app/(dashboard)/projects/page.tsx index 1adfb171..638a37f8 100644 --- a/app/(dashboard)/projects/page.tsx +++ b/app/(dashboard)/projects/page.tsx @@ -1,7 +1,12 @@ -import React from "react"; +import { ProjectsList } from "@/components/projects/project-list"; -const page = () => { - return
page
; -}; +export const dynamic = "force-dynamic"; -export default page; +export default function ProjectsPage() { + return ( +
+

Projects

+ +
+ ); +} diff --git a/app/api/projects/[id]/route.ts b/app/api/projects/[id]/route.ts new file mode 100644 index 00000000..05ce1b82 --- /dev/null +++ b/app/api/projects/[id]/route.ts @@ -0,0 +1,69 @@ +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import type { Project, TeamMember, Vote } from "@prisma/client"; + +// Define a type for the project with included relations +type ProjectWithRelations = Project & { + user: { + id: string; + name: string | null; + image: string | null; + }; + votes: Vote[]; + teamMembers: TeamMember[]; + _count: { + votes: number; + }; +}; + +// Only use the request parameter and extract the ID from the URL +export async function GET(request: Request) { + try { + // Extract the ID from the URL path + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const id = pathParts[pathParts.length - 1]; // Get the last segment of the path + + const project = (await prisma.project.findUnique({ + where: { id }, + include: { + user: { + select: { + id: true, + name: true, + image: true, + }, + }, + votes: true, + teamMembers: true, + _count: { + select: { + votes: true, + }, + }, + }, + })) as ProjectWithRelations | null; + + if (!project) { + return NextResponse.json({ error: "Project not found" }, { status: 404 }); + } + + // Calculate team members count manually + const teamMembersCount = project.teamMembers.length; + + // Return the project with the manually calculated team members count + return NextResponse.json({ + ...project, + _count: { + ...project._count, + teamMembers: teamMembersCount, + }, + }); + } catch (error) { + console.error("Error fetching project:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} diff --git a/app/api/projects/[id]/vote/route.ts b/app/api/projects/[id]/vote/route.ts new file mode 100644 index 00000000..cb20cedf --- /dev/null +++ b/app/api/projects/[id]/vote/route.ts @@ -0,0 +1,120 @@ +import { type NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/prisma"; +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth.config"; + +export async function POST(request: Request) { + try { + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const id = pathParts[pathParts.length - 1]; + const session = await getServerSession(authOptions); + + if (!session || !session.user || !session.user.id) { + return NextResponse.json( + { error: "You must be logged in to vote" }, + { status: 401 }, + ); + } + + const userId = session.user.id; + const projectId = id; + + // Check if project exists + const project = await prisma.project.findUnique({ + where: { id: projectId }, + }); + + if (!project) { + return NextResponse.json({ error: "Project not found" }, { status: 404 }); + } + + // Check if user has already voted + const existingVote = await prisma.vote.findUnique({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }); + + if (existingVote) { + // User has already voted, so remove their vote + await prisma.vote.delete({ + where: { + id: existingVote.id, + }, + }); + + const count = await prisma.vote.count({ + where: { projectId }, + }); + + return NextResponse.json({ success: true, voted: false, count }); + } + + // Create a new vote + await prisma.vote.create({ + data: { + project: { + connect: { id: projectId }, + }, + user: { + connect: { id: userId }, + }, + }, + }); + + const count = await prisma.vote.count({ + where: { projectId }, + }); + + return NextResponse.json({ success: true, voted: true, count }); + } catch (error) { + console.error("Error voting for project:", error); + return NextResponse.json( + { error: "Failed to register vote" }, + { status: 500 }, + ); + } +} + +export async function GET(request: NextRequest) { + try { + const url = new URL(request.url); + const pathParts = url.pathname.split("/"); + const id = pathParts[pathParts.length - 1]; + const projectId = id; + + // Get vote count + const count = await prisma.vote.count({ + where: { projectId }, + }); + + // Check if current user has voted + const session = await getServerSession(authOptions); + let hasVoted = false; + + if (session?.user?.id) { + const vote = await prisma.vote.findUnique({ + where: { + projectId_userId: { + projectId, + userId: session.user.id, + }, + }, + }); + + hasVoted = !!vote; + } + + return NextResponse.json({ count, hasVoted }); + } catch (error) { + console.error("Error getting vote data:", error); + return NextResponse.json( + { error: "Failed to get vote data" }, + { status: 500 }, + ); + } +} diff --git a/app/api/projects/create/route.ts b/app/api/projects/create/route.ts new file mode 100644 index 00000000..6d2c6b0e --- /dev/null +++ b/app/api/projects/create/route.ts @@ -0,0 +1,90 @@ +import { getServerSession } from "next-auth/next"; +import { NextResponse } from "next/server"; +import prisma from "@/lib/prisma"; +import { authOptions } from "@/lib/auth.config"; +import { v2 as cloudinary } from "cloudinary"; + +cloudinary.config({ + cloud_name: process.env.CLOUDINARY_CLOUD_NAME, + api_key: process.env.CLOUDINARY_API_KEY, + api_secret: process.env.CLOUDINARY_API_SECRET, +}); +export async function POST(request: Request) { + const session = await getServerSession(authOptions); + + if (!session || !session.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + try { + const formData = await request.formData(); + const title = formData.get("title") as string; + const description = formData.get("description") as string; + const fundingGoal = Number(formData.get("fundingGoal")); + const category = formData.get("category") as string; + const bannerImage = formData.get("bannerImage") as File | null; + const bannerImageUrl = formData.get("bannerImageUrl") as string | null; + const profileImage = formData.get("profileImage") as File | null; + const profileImageUrl = formData.get("profileImageUrl") as string | null; + + if (!title || !description || !fundingGoal || !category) { + return NextResponse.json( + { error: "Missing required fields" }, + { status: 400 }, + ); + } + + let bannerUrl = bannerImageUrl; + let profileUrl = profileImageUrl; + + if (bannerImage) { + const bannerResult = await uploadToCloudinary(bannerImage); + bannerUrl = bannerResult.secure_url; + } + + if (profileImage) { + const profileResult = await uploadToCloudinary(profileImage); + profileUrl = profileResult.secure_url; + } + + const project = await prisma.project.create({ + data: { + userId: session.user.id, + title, + description, + fundingGoal, + category, + bannerUrl, + profileUrl, + blockchainTx: null, + }, + }); + + return NextResponse.json({ success: true, project }, { status: 201 }); + } catch (error) { + console.error("Project creation error:", error); + return NextResponse.json( + { error: "Internal Server Error" }, + { status: 500 }, + ); + } +} + +async function uploadToCloudinary(file: File) { + const buffer = await file.arrayBuffer(); + const base64 = Buffer.from(buffer).toString("base64"); + const dataURI = `data:${file.type};base64,${base64}`; + + return new Promise<{ secure_url: string }>((resolve, reject) => { + cloudinary.uploader.upload( + dataURI, + { + resource_type: "auto", + folder: "project_images", + }, + (error, result) => { + if (error) reject(error); + else resolve(result as { secure_url: string }); + }, + ); + }); +} diff --git a/app/api/projects/route.ts b/app/api/projects/route.ts index 227c9f34..a614de84 100644 --- a/app/api/projects/route.ts +++ b/app/api/projects/route.ts @@ -1,48 +1,52 @@ -import { getServerSession } from "next-auth/next"; import { NextResponse } from "next/server"; import prisma from "@/lib/prisma"; -import { authOptions } from "@/lib/auth.config"; +import type { Prisma, ValidationStatus } from "@prisma/client"; -export async function POST(request: Request) { - const session = await getServerSession(authOptions); +export async function GET(request: Request) { + try { + const { searchParams } = new URL(request.url); + const category = searchParams.get("category"); + const status = searchParams.get("status"); - if (!session || !session.user?.id) { - return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); - } + const where: Prisma.ProjectWhereInput = {}; - try { - const { - title, - description, - fundingGoal, - category, - bannerPath, - profilePath, - } = await request.json(); + if (category) { + where.category = category; + } - if (!title || !description || !fundingGoal || !category) { - return NextResponse.json( - { error: "Missing required fields" }, - { status: 400 }, - ); + if (status) { + where.ideaValidation = status as ValidationStatus; } - const project = await prisma.project.create({ - data: { - userId: session.user.id, - title, - description, - fundingGoal, - category, - bannerUrl: bannerPath || null, - profileUrl: profilePath || null, - blockchainTx: null, + const projects = await prisma.project.findMany({ + include: { + user: { + select: { + id: true, + name: true, + image: true, + }, + }, + votes: { + select: { + id: true, + userId: true, + }, + }, + _count: { + select: { + votes: true, + }, + }, + }, + orderBy: { + createdAt: "desc", }, }); - return NextResponse.json({ success: true, project }, { status: 201 }); + return NextResponse.json(projects); } catch (error) { - console.error("Project creation error:", error); + console.error("Error fetching projects:", error); return NextResponse.json( { error: "Internal Server Error" }, { status: 500 }, diff --git a/components/connect-wallet.tsx b/components/connect-wallet.tsx index a86d6479..3aa983e4 100644 --- a/components/connect-wallet.tsx +++ b/components/connect-wallet.tsx @@ -1,8 +1,8 @@ "use client"; -import React, { useState, useEffect } from "react"; +import { useState, useEffect } from "react"; import { Button } from "@/components/ui/button"; -import { Loader2, LogOut } from "lucide-react"; +import { Loader2, LogOut, Copy, Check } from "lucide-react"; import { toast } from "sonner"; import { connect, disconnect, getPublicKey } from "@/hooks/useStellarWallet"; @@ -10,6 +10,7 @@ const ConnectWalletButton = ({ className = "" }) => { const [isChecking, setIsChecking] = useState(true); const [isConnecting, setIsConnecting] = useState(false); const [walletAddress, setWalletAddress] = useState(null); + const [isCopied, setIsCopied] = useState(false); useEffect(() => { const checkConnection = async () => { @@ -18,7 +19,7 @@ const ConnectWalletButton = ({ className = "" }) => { if (address) { setWalletAddress(address); toast.success("Wallet Reconnected", { - description: `Welcome back! Address: ${address}`, + description: "Welcome back!", }); } } catch (error) { @@ -40,7 +41,7 @@ const ConnectWalletButton = ({ className = "" }) => { setWalletAddress(address); toast.success("Wallet Connected", { - description: `Connected. Address: ${address}`, + description: "Successfully connected to wallet", }); }); } catch (error) { @@ -61,27 +62,59 @@ const ConnectWalletButton = ({ className = "" }) => { }); }; + const copyToClipboard = async () => { + if (!walletAddress) return; + + try { + await navigator.clipboard.writeText(walletAddress); + setIsCopied(true); + toast.success("Address Copied", { + description: "Wallet address copied to clipboard", + }); + + // Reset the copied state after 2 seconds + setTimeout(() => { + setIsCopied(false); + }, 2000); + } catch (error) { + toast.error(error as string, { + description: "Failed to copy address to clipboard", + }); + } + }; + + const formatAddress = (address: string) => { + return `${address.slice(0, 6)}...${address.slice(-4)}`; + }; + return (
- + {isChecking ? ( + + ) : isConnecting ? ( + + ) : walletAddress ? ( + + ) : ( + + )} {walletAddress && ( diff --git a/components/projects/project-list.tsx b/components/projects/project-list.tsx new file mode 100644 index 00000000..bdd057a0 --- /dev/null +++ b/components/projects/project-list.tsx @@ -0,0 +1,182 @@ +"use client"; + +import { useEffect, useState } from "react"; +import Link from "next/link"; +import { Card, CardFooter, CardHeader } from "@/components/ui/card"; +import { Badge } from "@/components/ui/badge"; +import Image from "next/image"; +import { VoteButton } from "../shared/vote-button"; +import { useSession } from "next-auth/react"; + +type ValidationStatus = "PENDING" | "REJECTED" | "VALIDATED"; + +type Project = { + id: string; + userId: string; + title: string; + description: string; + fundingGoal: number; + category: string; + bannerUrl: string | null; + profileUrl: string | null; + blockchainTx: string | null; + ideaValidation: ValidationStatus; + createdAt: string; + user: { + id: string; + name: string | null; + image: string | null; + }; + votes: { + id: string; + userId: string; + }[]; + _count: { + votes: number; + }; +}; + +export function ProjectsList() { + const [projects, setProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const { data: session } = useSession(); + + useEffect(() => { + async function fetchProjects() { + try { + const response = await fetch("/api/projects"); + + if (!response.ok) { + throw new Error("Failed to fetch projects"); + } + + const data = await response.json(); + setProjects(data); + } catch (err) { + setError(err instanceof Error ? err.message : "An error occurred"); + console.error(err); + } finally { + setLoading(false); + } + } + + fetchProjects(); + }, []); + + if (loading) { + return
Loading projects...
; + } + + if (error) { + return ( +
Error: {error}
+ ); + } + + if (!projects.length) { + return
No projects found
; + } + + return ( +
+ {projects.map((project) => { + // Check if the current user has voted for this project + const userVoted = session?.user?.id + ? project.votes.some((vote) => vote.userId === session.user?.id) + : false; + + return ( + + + {project.bannerUrl ? ( +
+ {project.title} +
+ ) : ( +
+ No banner image +
+ )} + + + {project.profileUrl && ( +
+ {`${project.title} +
+ )} +
+
+

+ {project.title} +

+
+

+ {project.description} +

+
+
+ + + +
+ + {project.category.slice(0, 1).toUpperCase() + + project.category.slice(1).toLowerCase()} + + + {formatValidationStatus(project.ideaValidation)} + +
+
+ + {project.ideaValidation === "VALIDATED" + ? "FUNDING STAGE" + : "IDEA VALIDATION STAGE"} + +
+ + {/* Vote Button - only show if project is in PENDING state */} + {project.ideaValidation === "PENDING" && ( +
+ +
+ )} +
+
+ ); + })} +
+ ); +} + +function formatValidationStatus(status: ValidationStatus | null | undefined) { + if (!status) return "Unknown"; + return status.charAt(0) + status.slice(1).toLowerCase(); +} diff --git a/components/registeration-form.tsx b/components/registeration-form.tsx index cf5dd0dd..03350525 100644 --- a/components/registeration-form.tsx +++ b/components/registeration-form.tsx @@ -1,101 +1,102 @@ -"use client" +"use client"; -import { useState } from "react" -import { useRouter } from "next/navigation" +import { useState } from "react"; +import { useRouter } from "next/navigation"; export default function RegistrationForm() { - const [name, setName] = useState("") - const [email, setEmail] = useState("") - const [password, setPassword] = useState("") - const [error, setError] = useState("") - const router = useRouter() + const [name, setName] = useState(""); + const [email, setEmail] = useState(""); + const [password, setPassword] = useState(""); + const [error, setError] = useState(""); + const router = useRouter(); - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault() - setError("") + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(""); - try { - const response = await fetch("/api/auth/register", { - method: "POST", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify({ name, email, password }), - }) + try { + const response = await fetch("/api/auth/register", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ name, email, password }), + }); - if (response.ok) { - // Redirect to OTP verification page with the user's email - router.push(`/auth/verify-otp?email=${encodeURIComponent(email)}`) - } else { - const data = await response.json() - setError(data.message || "An error occurred during registration.") - } - } catch (error) { - setError(`An error occurred during registration: ${error instanceof Error ? error.message : String(error)}`) - } - } + if (response.ok) { + // Redirect to OTP verification page with the user's email + router.push(`/auth/verify-otp?email=${encodeURIComponent(email)}`); + } else { + const data = await response.json(); + setError(data.message || "An error occurred during registration."); + } + } catch (error) { + setError( + `An error occurred during registration: ${error instanceof Error ? error.message : String(error)}`, + ); + } + }; - return ( -
-
-
- - setName(e.target.value)} - /> -
-
- - setEmail(e.target.value)} - /> -
-
- - setPassword(e.target.value)} - /> -
-
+ return ( + +
+
+ + setName(e.target.value)} + /> +
+
+ + setEmail(e.target.value)} + /> +
+
+ + setPassword(e.target.value)} + /> +
+
- {error &&

{error}

} + {error &&

{error}

} -
- -
-
- ) +
+ +
+ + ); } - diff --git a/components/shared/vote-button.tsx b/components/shared/vote-button.tsx new file mode 100644 index 00000000..be9295bd --- /dev/null +++ b/components/shared/vote-button.tsx @@ -0,0 +1,87 @@ +"use client"; + +import type React from "react"; + +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { ThumbsUp } from "lucide-react"; +import { voteForProject } from "@/lib/actions/vote"; +import { toast } from "sonner"; +import { useRouter } from "next/navigation"; + +interface VoteButtonProps { + projectId: string; + initialVoteCount: number; + initialUserVoted: boolean; + compact?: boolean; +} + +export function VoteButton({ + projectId, + initialVoteCount, + initialUserVoted, + compact = false, +}: VoteButtonProps) { + const [voteCount, setVoteCount] = useState( + initialVoteCount, + ); + const [userVoted, setUserVoted] = useState( + initialUserVoted, + ); + const [isLoading, setIsLoading] = useState(false); + const router = useRouter(); + + const handleVote = async (e: React.MouseEvent) => { + // Prevent the click from navigating to the project page if inside a link + e.stopPropagation(); + + try { + setIsLoading(true); + const result = await voteForProject(projectId); + + if (result.success) { + setVoteCount(result.count); + setUserVoted(result.voted); + + toast.success(result.voted ? "Vote registered!" : "Vote removed!", { + description: result.voted + ? "Thank you for supporting this project." + : "Your vote has been removed.", + }); + + // Refresh the page to update data + router.refresh(); + } else { + toast.error("Error", { + description: result.error || "Something went wrong", + }); + } + } catch (error) { + toast.error(`Error:${error}`, { + description: "Failed to register your vote", + }); + } finally { + setIsLoading(false); + } + }; + + return ( +
+ + + {voteCount} {voteCount === 1 ? "vote" : "votes"} + +
+ ); +} diff --git a/components/sidebar.tsx b/components/sidebar.tsx index f6ce1764..a3b6c485 100644 --- a/components/sidebar.tsx +++ b/components/sidebar.tsx @@ -2,136 +2,147 @@ import * as React from "react"; import { - Home, - Briefcase, - Bell, - BarChart2, - Cpu, - Crown, - Settings, - Sun, - Moon, + Home, + Briefcase, + Bell, + BarChart2, + Cpu, + Crown, + Settings, + Sun, + Moon, } from "lucide-react"; import { cn } from "@/lib/utils"; import { Button } from "@/components/ui/button"; import { Card, CardContent, CardFooter } from "@/components/ui/card"; import Image from "next/image"; +import { useRouter, usePathname } from "next/navigation"; interface NavItem { - icon: React.ElementType; - label: string; - href: string; - isActive?: boolean; + icon: React.ElementType; + label: string; + href: string; + isActive?: boolean; } const navItems: NavItem[] = [ - { icon: Home, label: "Dashboard", href: "/", isActive: true }, - { icon: Briefcase, label: "My Projects", href: "projects/my-projects" }, - { icon: Bell, label: "Explore", href: "projects/explore" }, - { icon: BarChart2, label: "Funded Projects", href: "/projects/funded" }, - { icon: Cpu, label: "Profile", href: "/profile" }, - { icon: Crown, label: "My Votes", href: "/votes" }, - { icon: Settings, label: "Settings", href: "/settings" }, + { icon: Home, label: "Dashboard", href: "/dashboard", isActive: true }, + { icon: Briefcase, label: "My Projects", href: "projects/my-projects" }, + { icon: Bell, label: "Explore", href: "/projects" }, + { icon: BarChart2, label: "Funded Projects", href: "/projects/funded" }, + { icon: Cpu, label: "Profile", href: "/profile" }, + { icon: Crown, label: "My Votes", href: "/votes" }, + { icon: Settings, label: "Settings", href: "/settings" }, ]; interface SidebarProps extends React.HTMLAttributes { - className?: string; + className?: string; } export function Sidebar({ className, ...props }: SidebarProps) { - const [theme, setTheme] = React.useState<"light" | "dark">("light"); + const [theme, setTheme] = React.useState<"light" | "dark">("light"); + const router = useRouter(); + const pathname = usePathname(); - const toggleTheme = () => { - const newTheme = theme === "light" ? "dark" : "light"; - setTheme(newTheme); - document.documentElement.classList.toggle("dark"); - }; + const toggleTheme = () => { + const newTheme = theme === "light" ? "dark" : "light"; + setTheme(newTheme); + document.documentElement.classList.toggle("dark"); + }; - return ( - + ); } diff --git a/lib/actions/vote.ts b/lib/actions/vote.ts index d654139f..87a46ff0 100644 --- a/lib/actions/vote.ts +++ b/lib/actions/vote.ts @@ -1,59 +1,99 @@ -"use server" - -import { getServerSession } from "next-auth" -import { authOptions } from "@/lib/auth.config" -import { prisma } from "@/lib/prisma" -import { revalidatePath } from "next/cache" - -export async function toggleVote(projectId: string) { - const session = await getServerSession(authOptions) - - if (!session?.user?.email) { - throw new Error("Unauthorized") - } - - // Get user ID from email since we're using JWT strategy - const user = await prisma.user.findUnique({ - where: { - email: session.user.email, - }, - select: { - id: true, - }, - }) - - if (!user) { - throw new Error("User not found") - } - - // Check if user has already voted - const existingVote = await prisma.vote.findUnique({ - where: { - projectId_userId: { - projectId, - userId: user.id, - }, - }, - }) - - if (existingVote) { - // Remove vote if it exists - await prisma.vote.delete({ - where: { - id: existingVote.id, - }, - }) - } else { - // Add new vote - await prisma.vote.create({ - data: { - projectId, - userId: user.id, - }, - }) - } - - revalidatePath(`/projects/${projectId}`) - return { success: true } +"use server"; + +import { getServerSession } from "next-auth"; +import { authOptions } from "@/lib/auth.config"; +import { prisma } from "@/lib/prisma"; +import { revalidatePath } from "next/cache"; + +export async function voteForProject(projectId: string) { + try { + // Get the session using your authOptions + const session = await getServerSession(authOptions); + + // Check if user is authenticated + if (!session || !session.user || !session.user.id) { + return { success: false, error: "You must be logged in to vote" }; + } + + const userId = session.user.id; + + // Check if user has already voted for this project + const existingVote = await prisma.vote.findUnique({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }); + + if (existingVote) { + // User has already voted, so remove their vote (toggle functionality) + await prisma.vote.delete({ + where: { + id: existingVote.id, + }, + }); + + revalidatePath(`/projects/${projectId}`); + return { + success: true, + voted: false, + count: await getVoteCount(projectId), + }; + } + + // Create a new vote + await prisma.vote.create({ + data: { + project: { + connect: { id: projectId }, + }, + user: { + connect: { id: userId }, + }, + }, + }); + + revalidatePath(`/projects/${projectId}`); + return { success: true, voted: true, count: await getVoteCount(projectId) }; + } catch (error) { + console.error("Error voting for project:", error); + return { success: false, error: "Failed to register vote" }; + } } +export async function getVoteCount(projectId: string): Promise { + const count = await prisma.vote.count({ + where: { + projectId, + }, + }); + + return count; +} + +export async function hasUserVoted(projectId: string): Promise { + try { + const session = await getServerSession(authOptions); + + if (!session || !session.user || !session.user.id) { + return false; + } + + const userId = session.user.id; + + const vote = await prisma.vote.findUnique({ + where: { + projectId_userId: { + projectId, + userId, + }, + }, + }); + + return !!vote; + } catch { + return false; + } +} diff --git a/lib/utils.ts b/lib/utils.ts index bd0c391d..4b138ca6 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,6 +1,14 @@ -import { clsx, type ClassValue } from "clsx" -import { twMerge } from "tailwind-merge" +import type { ValidationStatus } from "@/types/project"; +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { - return twMerge(clsx(inputs)) + return twMerge(clsx(inputs)); +} + +export function formatValidationStatus( + status: ValidationStatus | null | undefined, +) { + if (!status) return "Unknown"; + return status.charAt(0) + status.slice(1).toLowerCase(); } diff --git a/next.config.ts b/next.config.ts index 5dde93d2..47e84bd5 100644 --- a/next.config.ts +++ b/next.config.ts @@ -1,10 +1,10 @@ import type { NextConfig } from "next"; const nextConfig: NextConfig = { - /* config options here */ - images: { - domains: ['i.postimg.cc'], - } + /* config options here */ + images: { + domains: ["i.postimg.cc", "res.cloudinary.com", "example.com"], + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index ac95697f..4c3f2350 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "@vercel/blob": "^0.27.1", "bcrypt": "^5.1.1", "class-variance-authority": "^0.7.1", + "cloudinary": "^2.5.1", "clsx": "^2.1.1", "dotenv": "^16.4.7", "framer-motion": "^12.4.2", @@ -1892,9 +1893,9 @@ } }, "node_modules/@next/env": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/env/-/env-15.1.6.tgz", - "integrity": "sha512-d9AFQVPEYNr+aqokIiPLNK/MTyt3DWa/dpKveiAaVccUadFbhFEvY6FXYX2LJO2Hv7PHnLBu2oWwB4uBuHjr/w==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.2.1.tgz", + "integrity": "sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { @@ -1908,9 +1909,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.1.6.tgz", - "integrity": "sha512-u7lg4Mpl9qWpKgy6NzEkz/w0/keEHtOybmIl0ykgItBxEM5mYotS5PmqTpo+Rhg8FiOiWgwr8USxmKQkqLBCrw==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.2.1.tgz", + "integrity": "sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==", "cpu": [ "arm64" ], @@ -1924,9 +1925,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.1.6.tgz", - "integrity": "sha512-x1jGpbHbZoZ69nRuogGL2MYPLqohlhnT9OCU6E6QFewwup+z+M6r8oU47BTeJcWsF2sdBahp5cKiAcDbwwK/lg==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.2.1.tgz", + "integrity": "sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==", "cpu": [ "x64" ], @@ -1940,9 +1941,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.1.6.tgz", - "integrity": "sha512-jar9sFw0XewXsBzPf9runGzoivajeWJUc/JkfbLTC4it9EhU8v7tCRLH7l5Y1ReTMN6zKJO0kKAGqDk8YSO2bg==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.2.1.tgz", + "integrity": "sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==", "cpu": [ "arm64" ], @@ -1956,9 +1957,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.1.6.tgz", - "integrity": "sha512-+n3u//bfsrIaZch4cgOJ3tXCTbSxz0s6brJtU3SzLOvkJlPQMJ+eHVRi6qM2kKKKLuMY+tcau8XD9CJ1OjeSQQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.2.1.tgz", + "integrity": "sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==", "cpu": [ "arm64" ], @@ -1972,9 +1973,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.1.6.tgz", - "integrity": "sha512-SpuDEXixM3PycniL4iVCLyUyvcl6Lt0mtv3am08sucskpG0tYkW1KlRhTgj4LI5ehyxriVVcfdoxuuP8csi3kQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.2.1.tgz", + "integrity": "sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==", "cpu": [ "x64" ], @@ -1988,9 +1989,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.1.6.tgz", - "integrity": "sha512-L4druWmdFSZIIRhF+G60API5sFB7suTbDRhYWSjiw0RbE+15igQvE2g2+S973pMGvwN3guw7cJUjA/TmbPWTHQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.2.1.tgz", + "integrity": "sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==", "cpu": [ "x64" ], @@ -2004,9 +2005,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.1.6.tgz", - "integrity": "sha512-s8w6EeqNmi6gdvM19tqKKWbCyOBvXFbndkGHl+c9YrzsLARRdCHsD9S1fMj8gsXm9v8vhC8s3N8rjuC/XrtkEg==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.2.1.tgz", + "integrity": "sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==", "cpu": [ "arm64" ], @@ -2020,9 +2021,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.1.6.tgz", - "integrity": "sha512-6xomMuu54FAFxttYr5PJbEfu96godcxBTRk1OhAvJq0/EnmFU/Ybiax30Snis4vdWZ9LGpf7Roy5fSs7v/5ROQ==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.2.1.tgz", + "integrity": "sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==", "cpu": [ "x64" ], @@ -6773,6 +6774,19 @@ "node": ">=8" } }, + "node_modules/cloudinary": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.5.1.tgz", + "integrity": "sha512-CNg6uU53Hl4FEVynkTGpt5bQEAQWDHi3H+Sm62FzKf5uQHipSN2v7qVqS8GRVqeb0T1WNV+22+75DOJeRXYeSQ==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/clsx": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", @@ -10936,12 +10950,12 @@ "license": "MIT" }, "node_modules/next": { - "version": "15.1.6", - "resolved": "https://registry.npmjs.org/next/-/next-15.1.6.tgz", - "integrity": "sha512-Hch4wzbaX0vKQtalpXvUiw5sYivBy4cm5rzUKrBnUB/y436LGrvOUqYvlSeNVCWFO/770gDlltR9gqZH62ct4Q==", + "version": "15.2.1", + "resolved": "https://registry.npmjs.org/next/-/next-15.2.1.tgz", + "integrity": "sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==", "license": "MIT", "dependencies": { - "@next/env": "15.1.6", + "@next/env": "15.2.1", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", @@ -10956,14 +10970,14 @@ "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "15.1.6", - "@next/swc-darwin-x64": "15.1.6", - "@next/swc-linux-arm64-gnu": "15.1.6", - "@next/swc-linux-arm64-musl": "15.1.6", - "@next/swc-linux-x64-gnu": "15.1.6", - "@next/swc-linux-x64-musl": "15.1.6", - "@next/swc-win32-arm64-msvc": "15.1.6", - "@next/swc-win32-x64-msvc": "15.1.6", + "@next/swc-darwin-arm64": "15.2.1", + "@next/swc-darwin-x64": "15.2.1", + "@next/swc-linux-arm64-gnu": "15.2.1", + "@next/swc-linux-arm64-musl": "15.2.1", + "@next/swc-linux-x64-gnu": "15.2.1", + "@next/swc-linux-x64-musl": "15.2.1", + "@next/swc-win32-arm64-msvc": "15.2.1", + "@next/swc-win32-x64-msvc": "15.2.1", "sharp": "^0.33.5" }, "peerDependencies": { @@ -12118,10 +12132,6 @@ "integrity": "sha512-du4wfLyj4yCZq1VupnVSZmRsPJsNuxoDQFdCFHLaYiEbFBD7QE0a+I4D7hOxrVnh78QE/YipFAj9lXHiXocV+Q==", "license": "MIT" }, - "node_modules/project_contract": { - "resolved": "packages/project_contract", - "link": true - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -12200,6 +12210,17 @@ "bitcoin-ops": "^1.3.0" } }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "deprecated": "You or someone you depend on is using Q, the JavaScript Promise library that gave JavaScript developers strong feelings about promises. They can almost certainly migrate to the native JavaScript promise now. Thank you literally everyone for joining me in this bet against the odds. Be excellent to each other.\n\n(For a CapTP with native promises, see @endo/eventual-send and @endo/captp)", + "license": "MIT", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qrcode": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.3.tgz", @@ -15131,6 +15152,7 @@ }, "packages/project_contract": { "version": "0.0.0", + "extraneous": true, "dependencies": { "@stellar/stellar-sdk": "13.0.0", "buffer": "6.0.3" @@ -15138,48 +15160,6 @@ "devDependencies": { "typescript": "^5.6.2" } - }, - "packages/project_contract/node_modules/@stellar/stellar-base": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-13.0.1.tgz", - "integrity": "sha512-Xbd12mc9Oj/130Tv0URmm3wXG77XMshZtZ2yNCjqX5ZbMD5IYpbBs3DVCteLU/4SLj/Fnmhh1dzhrQXnk4r+pQ==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/js-xdr": "^3.1.2", - "base32.js": "^0.1.0", - "bignumber.js": "^9.1.2", - "buffer": "^6.0.3", - "sha.js": "^2.3.6", - "tweetnacl": "^1.0.3" - }, - "optionalDependencies": { - "sodium-native": "^4.3.0" - } - }, - "packages/project_contract/node_modules/@stellar/stellar-sdk": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/@stellar/stellar-sdk/-/stellar-sdk-13.0.0.tgz", - "integrity": "sha512-+wvmKi+XWwu27nLYTM17EgBdpbKohEkOfCIK4XKfsI4WpMXAqvnqSm98i9h5dAblNB+w8BJqzGs1JY0PtTGm4A==", - "license": "Apache-2.0", - "dependencies": { - "@stellar/stellar-base": "^13.0.1", - "axios": "^1.7.7", - "bignumber.js": "^9.1.2", - "eventsource": "^2.0.2", - "feaxios": "^0.0.20", - "randombytes": "^2.1.0", - "toml": "^3.0.0", - "urijs": "^1.19.1" - } - }, - "packages/project_contract/node_modules/feaxios": { - "version": "0.0.20", - "resolved": "https://registry.npmjs.org/feaxios/-/feaxios-0.0.20.tgz", - "integrity": "sha512-g3hm2YDNffNxA3Re3Hd8ahbpmDee9Fv1Pb1C/NoWsjY7mtD8nyNeJytUzn+DK0Hyl9o6HppeWOrtnqgmhOYfWA==", - "license": "MIT", - "dependencies": { - "is-retry-allowed": "^3.0.0" - } } } } diff --git a/package.json b/package.json index 6d666004..a1c59999 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ ] }, "prisma": { - "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" + "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.js" }, "dependencies": { "@auth/prisma-adapter": "^2.7.4", @@ -49,6 +49,7 @@ "@vercel/blob": "^0.27.1", "bcrypt": "^5.1.1", "class-variance-authority": "^0.7.1", + "cloudinary": "^2.5.1", "clsx": "^2.1.1", "dotenv": "^16.4.7", "framer-motion": "^12.4.2", diff --git a/prisma/migrations/20250211123141_boundless/migration.sql b/prisma/migrations/20250211123141_boundless/migration.sql deleted file mode 100644 index cbbf8be4..00000000 --- a/prisma/migrations/20250211123141_boundless/migration.sql +++ /dev/null @@ -1,88 +0,0 @@ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); - --- CreateTable -CREATE TABLE "Account" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "type" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "providerAccountId" TEXT NOT NULL, - "refresh_token" TEXT, - "access_token" TEXT, - "expires_at" INTEGER, - "token_type" TEXT, - "scope" TEXT, - "id_token" TEXT, - "session_state" TEXT, - - CONSTRAINT "Account_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Session" ( - "id" TEXT NOT NULL, - "sessionToken" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Session_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "User" ( - "id" TEXT NOT NULL, - "name" TEXT, - "email" TEXT, - "emailVerified" TIMESTAMP(3), - "image" TEXT, - "password" TEXT, - "role" "Role" NOT NULL DEFAULT 'USER', - - CONSTRAINT "User_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "VerificationToken" ( - "id" TEXT NOT NULL, - "identifier" TEXT NOT NULL, - "token" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "OTP" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "token" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "OTP_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); - --- CreateIndex -CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); - --- CreateIndex -CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); - --- CreateIndex -CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); - --- CreateIndex -CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); - --- CreateIndex -CREATE UNIQUE INDEX "OTP_token_key" ON "OTP"("token"); - --- AddForeignKey -ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250212211904_boundless/migration.sql b/prisma/migrations/20250212211904_boundless/migration.sql deleted file mode 100644 index 947d6f19..00000000 --- a/prisma/migrations/20250212211904_boundless/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AddForeignKey -ALTER TABLE "OTP" ADD CONSTRAINT "OTP_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250220000001_boundless/migration.sql b/prisma/migrations/20250220000001_boundless/migration.sql deleted file mode 100644 index f40057f9..00000000 --- a/prisma/migrations/20250220000001_boundless/migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- CreateTable -CREATE TABLE "Project" ( - "id" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updatedAt" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "Project_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Vote" ( - "id" TEXT NOT NULL, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "projectId" TEXT NOT NULL, - "userId" TEXT NOT NULL, - - CONSTRAINT "Vote_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE INDEX "Vote_projectId_idx" ON "Vote"("projectId"); - --- CreateIndex -CREATE INDEX "Vote_userId_idx" ON "Vote"("userId"); - --- CreateIndex -CREATE UNIQUE INDEX "Vote_projectId_userId_key" ON "Vote"("projectId", "userId"); - --- AddForeignKey -ALTER TABLE "Vote" ADD CONSTRAINT "Vote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Vote" ADD CONSTRAINT "Vote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250222130151_benji/migration.sql b/prisma/migrations/20250222130151_benji/migration.sql deleted file mode 100644 index f1f7b3a0..00000000 --- a/prisma/migrations/20250222130151_benji/migration.sql +++ /dev/null @@ -1,18 +0,0 @@ --- CreateTable -CREATE TABLE "Project" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "title" TEXT NOT NULL, - "description" TEXT NOT NULL, - "fundingGoal" INTEGER NOT NULL, - "category" TEXT NOT NULL, - "bannerUrl" TEXT, - "profileUrl" TEXT, - "blockchainTx" TEXT, - "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Project_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20250307015524_add_idea_validation_and_team_members/migration.sql b/prisma/migrations/20250307015524_add_idea_validation_and_team_members/migration.sql new file mode 100644 index 00000000..da31214e --- /dev/null +++ b/prisma/migrations/20250307015524_add_idea_validation_and_team_members/migration.sql @@ -0,0 +1,170 @@ +-- CreateEnum +CREATE TYPE "ValidationStatus" AS ENUM ('PENDING', 'REJECTED', 'VALIDATED'); + +-- CreateEnum +CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN'); + +-- CreateTable +CREATE TABLE "Account" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "type" TEXT NOT NULL, + "provider" TEXT NOT NULL, + "providerAccountId" TEXT NOT NULL, + "refresh_token" TEXT, + "access_token" TEXT, + "expires_at" INTEGER, + "token_type" TEXT, + "scope" TEXT, + "id_token" TEXT, + "session_state" TEXT, + + CONSTRAINT "Account_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Session" ( + "id" TEXT NOT NULL, + "sessionToken" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "Session_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "User" ( + "id" TEXT NOT NULL, + "name" TEXT, + "email" TEXT, + "emailVerified" TIMESTAMP(3), + "image" TEXT, + "password" TEXT, + "role" "Role" NOT NULL DEFAULT 'USER', + + CONSTRAINT "User_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "VerificationToken" ( + "id" TEXT NOT NULL, + "identifier" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "VerificationToken_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "OTP" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "token" TEXT NOT NULL, + "expires" TIMESTAMP(3) NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "OTP_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Project" ( + "id" TEXT NOT NULL, + "userId" TEXT NOT NULL, + "title" TEXT NOT NULL, + "description" TEXT NOT NULL, + "fundingGoal" INTEGER NOT NULL, + "category" TEXT NOT NULL, + "bannerUrl" TEXT, + "profileUrl" TEXT, + "blockchainTx" TEXT, + "ideaValidation" "ValidationStatus" NOT NULL DEFAULT 'PENDING', + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + + CONSTRAINT "Project_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "TeamMember" ( + "id" TEXT NOT NULL, + "fullName" TEXT NOT NULL, + "role" TEXT NOT NULL, + "bio" TEXT, + "profileImage" TEXT, + "github" TEXT, + "twitter" TEXT, + "discord" TEXT, + "linkedin" TEXT, + "projectId" TEXT NOT NULL, + "userId" TEXT, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" TIMESTAMP(3) NOT NULL, + + CONSTRAINT "TeamMember_pkey" PRIMARY KEY ("id") +); + +-- CreateTable +CREATE TABLE "Vote" ( + "id" TEXT NOT NULL, + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, + "projectId" TEXT NOT NULL, + "userId" TEXT NOT NULL, + + CONSTRAINT "Vote_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "Account_provider_providerAccountId_key" ON "Account"("provider", "providerAccountId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Session_sessionToken_key" ON "Session"("sessionToken"); + +-- CreateIndex +CREATE UNIQUE INDEX "User_email_key" ON "User"("email"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_token_key" ON "VerificationToken"("token"); + +-- CreateIndex +CREATE UNIQUE INDEX "VerificationToken_identifier_token_key" ON "VerificationToken"("identifier", "token"); + +-- CreateIndex +CREATE UNIQUE INDEX "OTP_token_key" ON "OTP"("token"); + +-- CreateIndex +CREATE INDEX "TeamMember_projectId_idx" ON "TeamMember"("projectId"); + +-- CreateIndex +CREATE INDEX "TeamMember_userId_idx" ON "TeamMember"("userId"); + +-- CreateIndex +CREATE INDEX "Vote_projectId_idx" ON "Vote"("projectId"); + +-- CreateIndex +CREATE INDEX "Vote_userId_idx" ON "Vote"("userId"); + +-- CreateIndex +CREATE UNIQUE INDEX "Vote_projectId_userId_key" ON "Vote"("projectId", "userId"); + +-- AddForeignKey +ALTER TABLE "Account" ADD CONSTRAINT "Account_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Session" ADD CONSTRAINT "Session_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "OTP" ADD CONSTRAINT "OTP_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TeamMember" ADD CONSTRAINT "TeamMember_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE SET NULL ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Vote" ADD CONSTRAINT "Vote_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "Vote" ADD CONSTRAINT "Vote_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 932852ac..bf0e07ac 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -35,18 +35,19 @@ model Session { } model User { - id String @id @default(cuid()) + id String @id @default(cuid()) name String? - email String? @unique + email String? @unique emailVerified DateTime? image String? password String? - role Role @default(USER) + role Role @default(USER) accounts Account[] sessions Session[] OTP OTP[] projects Project[] Vote Vote[] + TeamMember TeamMember[] } model VerificationToken { @@ -67,29 +68,50 @@ model OTP { user User @relation(fields: [userId], references: [id], onDelete: Cascade) } -// model Project { -// id String @id @default(cuid()) -// title String -// description String -// createdAt DateTime @default(now()) -// updatedAt DateTime @updatedAt -// votes Vote[] -// } +enum ValidationStatus { + PENDING + REJECTED + VALIDATED +} model Project { + id String @id @default(cuid()) + userId String + title String + description String + fundingGoal Int + category String + bannerUrl String? + profileUrl String? + blockchainTx String? + ideaValidation ValidationStatus @default(PENDING) + createdAt DateTime @default(now()) + votes Vote[] + teamMembers TeamMember[] + + user User @relation(fields: [userId], references: [id], onDelete: Cascade) +} + +model TeamMember { id String @id @default(cuid()) - userId String - title String - description String - fundingGoal Int - category String - bannerUrl String? - profileUrl String? - blockchainTx String? + fullName String + role String + bio String? @db.Text + profileImage String? + github String? + twitter String? + discord String? + linkedin String? + projectId String + userId String? createdAt DateTime @default(now()) - votes Vote[] + updatedAt DateTime @updatedAt - user User @relation(fields: [userId], references: [id], onDelete: Cascade) + project Project @relation(fields: [projectId], references: [id], onDelete: Cascade) + user User? @relation(fields: [userId], references: [id], onDelete: SetNull) + + @@index([projectId]) + @@index([userId]) } model Vote { diff --git a/prisma/seed.js b/prisma/seed.js new file mode 100644 index 00000000..bbcd97ee --- /dev/null +++ b/prisma/seed.js @@ -0,0 +1,107 @@ +import { PrismaClient } from "@prisma/client"; +import { hash } from "bcrypt"; + +const prisma = new PrismaClient(); + +async function main() { + const password = await hash("adminpassword", 12); + const admin = await prisma.user.upsert({ + where: { email: "admin@example.com" }, + update: {}, + create: { + email: "admin@example.com", + name: "Admin", + password, + role: "ADMIN", + }, + }); + console.log({ admin }); + + // Seed Projects + const projects = await Promise.all([ + prisma.project.create({ + data: { + userId: "cm7y4qb140007rqxnx1zfd8pb", + title: "Eco-Friendly Water Purifier", + description: + "A sustainable water purification system for developing countries.", + fundingGoal: 50000, + category: "Environment", + bannerUrl: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345548/project_images/uq7sf4tjkckc62mgohic.avif", + profileUrl: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345550/project_images/arnuhsp5cct8js56zq01.png", + ideaValidation: "PENDING", + }, + }), + prisma.project.create({ + data: { + userId: "cm7y4qb140007rqxnx1zfd8pb", + title: "AI-Powered Education Platform", + description: + "An adaptive learning platform using artificial intelligence.", + fundingGoal: 75000, + category: "Education", + bannerUrl: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345548/project_images/uq7sf4tjkckc62mgohic.avif", + profileUrl: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345550/project_images/arnuhsp5cct8js56zq01.png", + ideaValidation: "VALIDATED", + }, + }), + ]); + console.log({ projects }); + + // Seed TeamMembers + const teamMembers = await Promise.all([ + prisma.teamMember.create({ + data: { + fullName: "John Doe", + role: "Project Lead", + bio: "Experienced environmental engineer with a passion for clean water.", + profileImage: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345550/project_images/arnuhsp5cct8js56zq01.png", + github: "johndoe", + twitter: "johndoe_eco", + linkedin: "johndoe-eco", + projectId: projects[0].id, + }, + }), + prisma.teamMember.create({ + data: { + fullName: "Jane Smith", + role: "AI Specialist", + bio: "Machine learning expert with a focus on educational technology.", + profileImage: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345550/project_images/arnuhsp5cct8js56zq01.png", + github: "janesmith", + twitter: "janesmith_ai", + linkedin: "janesmith-ai", + projectId: projects[1].id, + }, + }), + prisma.teamMember.create({ + data: { + fullName: "Alex Johnson", + role: "UX Designer", + bio: "User experience designer with a knack for creating intuitive interfaces.", + profileImage: + "https://res.cloudinary.com/dmsphf4d3/image/upload/v1741345550/project_images/arnuhsp5cct8js56zq01.png", + github: "alexj", + twitter: "alexj_design", + linkedin: "alexjohnson-ux", + projectId: projects[1].id, + }, + }), + ]); + console.log({ teamMembers }); +} + +main() + .catch((e) => { + console.error(e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/prisma/seed.ts b/prisma/seed.ts deleted file mode 100644 index 37a96efd..00000000 --- a/prisma/seed.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { PrismaClient } from "@prisma/client" -import { hash } from "bcrypt" - -const prisma = new PrismaClient() - -async function main() { - const password = await hash("adminpassword", 12) - const admin = await prisma.user.upsert({ - where: { email: "admin@example.com" }, - update: {}, - create: { - email: "admin@example.com", - name: "Admin", - password, - role: "ADMIN", - }, - }) - console.log({ admin }) -} - -main() - .catch((e) => { - console.error(e) - process.exit(1) - }) - .finally(async () => { - await prisma.$disconnect() - }) - diff --git a/tsconfig.json b/tsconfig.json index eedb14e2..7fa8619b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -27,7 +27,8 @@ "**/*.ts", "**/*.tsx", ".next/types/**/*.ts", - "initialize.js" + "initialize.js", + "prisma/seed.js" ], "exclude": ["node_modules", "packages"] } diff --git a/types/project.ts b/types/project.ts index 82942c5d..df40fdae 100644 --- a/types/project.ts +++ b/types/project.ts @@ -32,5 +32,48 @@ export interface ActivityDataPoint { participants: number; } +export type Project = { + id: string; + userId: string; + title: string; + description: string; + fundingGoal: number; + category: string; + bannerUrl: string | null; + profileUrl: string | null; + blockchainTx: string | null; + ideaValidation: ValidationStatus; + createdAt: string; + user: { + id: string; + name: string | null; + image: string | null; + }; + votes: { + id: string; + userId: string; + }[]; + _count: { + votes: number; + }; +}; + +export interface TeamMember { + id: string; + fullName: string; + role: string; + bio: string | null; + profileImage: string | null; + github: string | null; + twitter: string | null; + discord: string | null; + linkedin: string | null; + userId?: string | null; // Optional + projectId?: string; // Optional + createdAt?: string | Date; // Optional + updatedAt?: string | Date; // Optional +} + export type ExploreFilter = "newest" | "popular" | "ending"; export type CompletedSort = "date" | "size" | "category"; +export type ValidationStatus = "PENDING" | "REJECTED" | "VALIDATED";