Skip to content
Open
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
35 changes: 35 additions & 0 deletions frontend/src/app/creator/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
"use client";

import { useWallet } from "@/context/WalletProvider";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import Sidebar from "@/components/creator/Sidebar";
import TopNav from "@/components/creator/TopNav";

Expand All @@ -6,6 +11,36 @@ export default function DashboardLayout({
}: {
children: React.ReactNode;
}) {
const { connected, publicKey } = useWallet();
const router = useRouter();
const [checked, setChecked] = useState(false);

useEffect(() => {
const timer = setTimeout(() => {
if (connected && publicKey) {
setChecked(true);
} else {
router.replace("/connect-wallet");
}
}, 500);
return () => clearTimeout(timer);
}, [connected, publicKey, router]);

if (!checked) {
return (
<div className="flex h-screen items-center justify-center bg-[#0D0B10] text-white">
<div className="text-center">
<div className="mx-auto mb-4 h-8 w-8 animate-spin rounded-full border-2 border-[#9011FF] border-t-transparent" />
<p className="text-sm text-[#8C86B8]">Checking wallet connection…</p>
</div>
</div>
);
}

if (!connected || !publicKey) {
return null;
}

return (
<div className="flex h-screen overflow-x-hidden bg-[#0D0B10] text-white">
<Sidebar />
Expand Down
68 changes: 35 additions & 33 deletions frontend/src/components/creator/TopNav.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
"use client";

import Image from "next/image";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Bell, ChevronDown } from "lucide-react";
import { useWallet } from "@/context/WalletProvider";
import { Bell, LogOut } from "lucide-react";
import { useState } from "react";

const topNavItems = [
{
Expand All @@ -20,8 +21,15 @@ const topNavItems = [
},
];

function truncateKey(key: string): string {
if (key.length <= 8) return key;
return `${key.slice(0, 4)}...${key.slice(-4)}`;
}

export default function TopNav() {
const pathname = usePathname();
const { publicKey, disconnect } = useWallet();
const [showLogout, setShowLogout] = useState(false);

const isActive = (href: string) =>
pathname === href ||
Expand All @@ -46,37 +54,31 @@ export default function TopNav() {

<div className="ml-auto flex items-center gap-4 text-sm font-semibold">
<Bell className="hidden size-5 text-white/85 sm:block" />
<span className="flex items-center gap-1">
<Image
src="/dashboard/Star.svg"
alt=""
width={18}
height={18}
className="h-[18px] w-[18px]"
/>
12
</span>
<span className="hidden items-center gap-1 sm:flex">
<Image
src="/dashboard/wallet.svg"
alt=""
width={18}
height={18}
className="h-[18px] w-[18px]"
/>
$0
</span>
<span className="flex items-center gap-2">
<Image
src="/dashboard/avatar.png"
alt="Ruze.stellar"
width={34}
height={34}
className="size-[34px] rounded-full"
/>
<span className="hidden sm:inline">Ruze.stellar</span>
<ChevronDown className="hidden size-4 sm:block" />
</span>
<div className="relative">
<button
type="button"
onClick={() => setShowLogout((prev) => !prev)}
className="flex items-center gap-2 rounded-lg border border-[#241B4A] bg-[#141026] px-3 py-1.5 text-xs font-mono text-[#CFC9FF] hover:border-[#9011FF] transition-colors"
>
<span className="h-2 w-2 rounded-full bg-green-400" />
{publicKey ? truncateKey(publicKey) : ""}
</button>
{showLogout && (
<div className="absolute right-0 mt-2 w-48 rounded-xl border border-[#241B4A] bg-[#141026] p-2 shadow-xl z-50">
<button
type="button"
onClick={() => {
disconnect();
setShowLogout(false);
}}
className="flex w-full items-center gap-2 rounded-lg px-3 py-2 text-sm text-red-400 hover:bg-red-500/10 transition-colors"
>
<LogOut className="size-4" />
Disconnect wallet
</button>
</div>
)}
</div>
</div>
</div>
</header>
Expand Down
82 changes: 59 additions & 23 deletions frontend/src/features/creators/CreatorQuestDetail.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,30 +20,36 @@ export default function CreatorQuestDetail({
}) {
const router = useRouter();
const [approvedSubmissions, setApprovedSubmissions] = useState<string[]>([]);
const [rejectedSubmissions, setRejectedSubmissions] = useState<string[]>([]);
const [rejectConfirm, setRejectConfirm] = useState<string | null>(null);
const [rejectReason, setRejectReason] = useState("");

const handleApprove = async (submissionId: string) => {
const handleApprove = (submissionId: string) => {
setApprovedSubmissions((prev) => {
if (prev.includes(submissionId)) {
return prev.filter((id) => id !== submissionId);
} else {
return [...prev, submissionId];
}
return [...prev, submissionId];
});
setRejectedSubmissions((prev) => prev.filter((id) => id !== submissionId));
};

try {
// TODO: Replace with actual API call
// const response = await fetch(`/api/submissions/${submissionId}/approve`, {
// method: "POST",
// headers: { "Content-Type": "application/json" },
// });
// if (response.ok) {
// console.log(`Submission ${submissionId} approved`);
// }

console.log(`Approval toggled for submission: ${submissionId}`);
} catch (error) {
console.error("Error approving submission:", error);
}
const handleReject = (submissionId: string) => {
setRejectedSubmissions((prev) => {
if (prev.includes(submissionId)) {
return prev.filter((id) => id !== submissionId);
}
return [...prev, submissionId];
});
setApprovedSubmissions((prev) => prev.filter((id) => id !== submissionId));
setRejectConfirm(null);
setRejectReason("");
};

const getEffectiveStatus = (sub: Submission): Submission["status"] => {
if (rejectedSubmissions.includes(sub.id)) return "rejected";
if (approvedSubmissions.includes(sub.id)) return "approved";
return sub.status;
};

const handleEditQuest = () => {
Expand Down Expand Up @@ -159,12 +165,42 @@ export default function CreatorQuestDetail({
<EmptyState message="No submissions yet." />
) : (
submissions.map((sub) => (
<SubmissionCard
key={sub.id}
submission={sub}
onApprove={() => handleApprove(sub.id)}
isApproved={approvedSubmissions.includes(sub.id)}
/>
<div key={sub.id}>
<SubmissionCard
submission={{ ...sub, status: getEffectiveStatus(sub) }}
onApprove={() => handleApprove(sub.id)}
onReject={() => setRejectConfirm(sub.id)}
isApproved={approvedSubmissions.includes(sub.id)}
/>
{rejectConfirm === sub.id && (
<div className="mx-4 mb-4 bg-[#1A1330] border border-red-500/30 rounded-xl p-4">
<p className="text-sm text-[#CFC9FF] mb-3">
Are you sure you want to reject this submission?
</p>
<textarea
value={rejectReason}
onChange={(e) => setRejectReason(e.target.value)}
placeholder="Optional reason for rejection…"
rows={2}
className="w-full bg-[#1B1540] border border-[#241B4A] rounded-lg px-3 py-2 text-white text-sm focus:outline-none focus:border-red-400 resize-none mb-3"
/>
<div className="flex gap-3">
<button
onClick={() => handleReject(sub.id)}
className="bg-red-500 hover:bg-red-600 text-white px-4 py-2 rounded-lg text-sm font-medium transition-colors"
>
Confirm Reject
</button>
<button
onClick={() => { setRejectConfirm(null); setRejectReason(""); }}
className="border border-[#241B4A] hover:bg-[#1B1540] text-white px-4 py-2 rounded-lg text-sm transition-colors"
>
Cancel
</button>
</div>
</div>
)}
</div>
))
)}
</div>
Expand Down
33 changes: 26 additions & 7 deletions frontend/src/features/creators/SubmissionCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { useState } from "react";
export default function SubmissionCard({
submission,
onApprove,
onReject,
isApproved,
}: {
submission: Submission;
onApprove: () => void;
onReject?: () => void;
isApproved?: boolean;
}) {
const [selectWinner, setSelectWinner] = useState<Record<string, boolean>>({});
Expand Down Expand Up @@ -89,13 +91,30 @@ export default function SubmissionCard({
</span>

{submission.status === "pending" && !isApproved && (
<button
onClick={onApprove}
className="bg-[#9011FF] hover:bg-[#7d0dd4] text-white px-3 py-1 rounded-lg text-sm font-medium transition-colors"
title="Approve submission"
>
✓ Approve
</button>
<>
<button
onClick={onApprove}
className="bg-[#9011FF] hover:bg-[#7d0dd4] text-white px-3 py-1 rounded-lg text-sm font-medium transition-colors"
title="Approve submission"
>
✓ Approve
</button>
{onReject && (
<button
onClick={onReject}
className="border border-red-500/50 text-red-400 hover:bg-red-500/10 px-3 py-1 rounded-lg text-sm font-medium transition-colors"
title="Reject submission"
>
✕ Reject
</button>
)}
</>
)}

{submission.status === "rejected" && (
<span className="px-3 py-1 rounded-lg text-sm font-medium bg-red-500/10 text-red-400 border border-red-500/30">
Rejected
</span>
)}

<button
Expand Down
Loading