Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/(dashboard)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ConnectWalletButton from "@/components/connect-wallet";
import { MobileSidebar } from "@/components/mobile-sidebar";
import { Sidebar } from "@/components/sidebar";

Expand All @@ -18,6 +19,7 @@ export default function Layout({
{/* Mobile Header with Menu */}
<header className="flex h-14 items-center border-b px-4 lg:px-6">
<MobileSidebar />
<ConnectWalletButton />
</header>

<main className="flex-1 p-5">{children}</main>
Expand Down
12 changes: 12 additions & 0 deletions app/(dashboard)/projects/my-projects/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { MyProjectsList } from "@/components/projects/my-project-list";

export const dynamic = "force-dynamic";

export default function ProjectsPage() {
return (
<div className="container py-8">
<h1 className="text-3xl font-bold mb-8">Projects</h1>
<MyProjectsList />
</div>
);
}
5 changes: 4 additions & 1 deletion app/api/projects/create/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ export async function POST(request: Request) {
}
try {
const formData = await request.formData();
const projectId = formData.get("projectId") as string;
const signedTx = formData.get("signedTx") as string;
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const fundingGoal = Number(formData.get("fundingGoal"));
Expand Down Expand Up @@ -48,14 +50,15 @@ export async function POST(request: Request) {

const project = await prisma.project.create({
data: {
id: projectId,
userId: session.user.id,
title,
description,
fundingGoal,
category,
bannerUrl,
profileUrl,
blockchainTx: null,
blockchainTx: signedTx || null,
},
});

Expand Down
59 changes: 59 additions & 0 deletions app/api/projects/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { NextResponse } from "next/server";
import prisma from "@/lib/prisma";
import type { Prisma, ValidationStatus } from "@prisma/client";
import { getServerSession } from "next-auth";
import { authOptions } from "@/lib/auth.config";

export async function GET(request: Request) {
try {
Expand Down Expand Up @@ -53,3 +55,60 @@ export async function GET(request: Request) {
);
}
}

export async function POST(request: Request) {
try {
const { forUser } = await request.json();

const session = await getServerSession(authOptions);

const where: Prisma.ProjectWhereInput = {};

if (forUser && session?.user?.id) {
where.userId = session.user.id;
}

// if (category) {
// where.category = category;
// }

// if (status) {
// where.ideaValidation = status as ValidationStatus;
// }

const projects = await prisma.project.findMany({
where,
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(projects);
} catch (error) {
console.error("Error fetching projects:", error);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
}
83 changes: 83 additions & 0 deletions app/api/projects/upload-metadata/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { NextResponse } from "next/server";
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,
});

async function uploadImage(file: File): Promise<string | null> {
const buffer = await file.arrayBuffer();
const base64 = Buffer.from(buffer).toString("base64");
const dataURI = `data:${file.type};base64,${base64}`;

return new Promise<string | null>((resolve, reject) => {
cloudinary.uploader.upload(
dataURI,
{ resource_type: "auto", folder: "project_images" },
(error, result) => {
if (error) reject(null);
else resolve(result?.secure_url || null);
},
);
});
}

async function uploadMetadata(
metadata: object,
): Promise<{ secure_url: string }> {
const jsonStr = JSON.stringify(metadata);
const buffer = Buffer.from(jsonStr);
const base64 = buffer.toString("base64");
const dataURI = `data:application/json;base64,${base64}`;

return new Promise<{ secure_url: string }>((resolve, reject) => {
cloudinary.uploader.upload(
dataURI,
{ resource_type: "raw", folder: "project_metadata" },
(error, result) => {
if (error) reject(error);
else resolve(result as { secure_url: string });
},
);
});
}

export async function POST(request: Request) {
try {
const formData = await request.formData();
const title = formData.get("title") as string;
const description = formData.get("description") as string;
const category = formData.get("category") as string;
const bannerImage = formData.get("bannerImage") as File | null;
const profileImage = formData.get("profileImage") as File | null;

if (!title || !description || !category) {
return NextResponse.json(
{ error: "Missing required fields" },
{ status: 400 },
);
}

let bannerUrl = null;
let profileUrl = null;
if (bannerImage) bannerUrl = await uploadImage(bannerImage);
if (profileImage) profileUrl = await uploadImage(profileImage);

const metadata = { title, description, category, bannerUrl, profileUrl };

const metadataUpload = await uploadMetadata(metadata);

return NextResponse.json(
{ metadataUri: metadataUpload.secure_url },
{ status: 201 },
);
} catch (error) {
console.error("Metadata upload error:", error);
return NextResponse.json(
{ error: "Internal Server Error" },
{ status: 500 },
);
}
}
62 changes: 25 additions & 37 deletions components/connect-wallet.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,26 @@
"use client";

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Loader2, LogOut, Copy, Check } from "lucide-react";
import { toast } from "sonner";
import { connect, disconnect, getPublicKey } from "@/hooks/useStellarWallet";
import { useWalletStore } from "@/store/useWalletStore";
import { formatAddress } from "@/lib/utils";

const ConnectWalletButton = ({ className = "" }) => {
const {
publicKey,
connecting,
connect: connectWallet,
disconnect: disconnectWallet,
} = useWalletStore();

const [isChecking, setIsChecking] = useState(true);
const [isConnecting, setIsConnecting] = useState(false);
const [walletAddress, setWalletAddress] = useState<string | null>(null);
const [isCopied, setIsCopied] = useState(false);

useEffect(() => {
const checkConnection = async () => {
try {
const address = await getPublicKey();
if (address) {
setWalletAddress(address);
if (publicKey) {
toast.success("Wallet Reconnected", {
description: "Welcome back!",
});
Expand All @@ -30,49 +33,39 @@ const ConnectWalletButton = ({ className = "" }) => {
};

checkConnection();
}, []);
}, [publicKey]);

const connectWallet = async () => {
const handleConnectWallet = async () => {
try {
setIsConnecting(true);
await connect(async () => {
const address = await getPublicKey();
if (!address) throw new Error("Failed to retrieve wallet address");

setWalletAddress(address);
await connectWallet();
if (publicKey) {
toast.success("Wallet Connected", {
description: "Successfully connected to wallet",
});
});
}
} catch (error) {
console.log(error);
toast.error("Connection Failed", {
description: "Failed to connect to the wallet.",
});
} finally {
setIsConnecting(false);
}
};

const disconnectWallet = async () => {
await disconnect();
setWalletAddress(null);
const handleDisconnectWallet = async () => {
await disconnectWallet();
toast.info("Wallet Disconnected", {
description: "You have been disconnected.",
});
};

const copyToClipboard = async () => {
if (!walletAddress) return;

if (!publicKey) return;
try {
await navigator.clipboard.writeText(walletAddress);
await navigator.clipboard.writeText(publicKey);
setIsCopied(true);
toast.success("Address Copied", {
description: "Wallet address copied to clipboard",
});

// Reset the copied state after 2 seconds
setTimeout(() => {
setIsCopied(false);
}, 2000);
Expand All @@ -83,41 +76,36 @@ const ConnectWalletButton = ({ className = "" }) => {
}
};

const formatAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};

return (
<div className="flex items-center space-x-2">
{isChecking ? (
<Button disabled className={className}>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Checking...
</Button>
) : isConnecting ? (
) : connecting ? (
<Button disabled className={className}>
<Loader2 className="mr-2 h-4 w-4 animate-spin" /> Connecting...
</Button>
) : walletAddress ? (
) : publicKey ? (
<Button
variant="outline"
onClick={copyToClipboard}
className={`${className} flex items-center gap-2 cursor-pointer bg-primary text-white`}
>
{formatAddress(walletAddress)}
{formatAddress(publicKey)}
{isCopied ? (
<Check className="h-4 w-4 text-green-500" />
) : (
<Copy className="h-4 w-4" />
)}
</Button>
) : (
<Button onClick={connectWallet} className={className}>
<Button onClick={handleConnectWallet} className={className}>
Connect Wallet
</Button>
)}

{walletAddress && (
<Button onClick={disconnectWallet} variant="outline">
{publicKey && (
<Button onClick={handleDisconnectWallet} variant="outline">
<LogOut className="mr-2 h-4 w-4" /> Disconnect
</Button>
)}
Expand Down
Loading
Loading