From 638049726453f6ffc90088dc5805491cf3d9b75c Mon Sep 17 00:00:00 2001 From: Prz-droid Date: Mon, 27 Apr 2026 20:40:45 +0100 Subject: [PATCH 1/2] feat: implement discovery interface with loading and empty states - Add initial loading state with spinner during data fetch - Implement empty state for when no projects exist - Implement filtered empty state with clear filters action - Add loading state for 'Load More' button - Disable controls during initial loading - Maintain responsive grid layout (1/2/3 columns) - Keep search, category filter, and sort functionality - Add proper visual feedback for all states --- dongle/app/discover/page.tsx | 141 ++++++++++++++++++++++------------- 1 file changed, 88 insertions(+), 53 deletions(-) diff --git a/dongle/app/discover/page.tsx b/dongle/app/discover/page.tsx index 4b63062..45af8ed 100644 --- a/dongle/app/discover/page.tsx +++ b/dongle/app/discover/page.tsx @@ -1,23 +1,31 @@ "use client"; -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import LayoutWrapper from "@/components/layout/LayoutWrapper"; import { mockProjects } from "@/data/mockProjects"; import { ProjectCard } from "@/components/projects/ProjectCard"; import { Button } from "@/components/ui/Button"; -import { Search, Filter } from "lucide-react"; +import { Spinner } from "@/components/ui/Spinner"; +import { Search, Filter, Package } from "lucide-react"; const ITEMS_PER_PAGE = 9; export default function DiscoverPage() { + const [isInitialLoading, setIsInitialLoading] = useState(true); const [searchQuery, setSearchQuery] = useState(""); const [selectedCategory, setSelectedCategory] = useState("All"); - const [sortBy, setSortBy] = useState<"rating" | "newest" | "popular">( - "rating", - ); + const [sortBy, setSortBy] = useState<"rating" | "newest" | "popular">("rating"); const [visibleCount, setVisibleCount] = useState(ITEMS_PER_PAGE); const [isLoadingMore, setIsLoadingMore] = useState(false); + // Simulate initial data loading + useEffect(() => { + const timer = setTimeout(() => { + setIsInitialLoading(false); + }, 800); + return () => clearTimeout(timer); + }, []); + const categories = [ "All", ...Array.from(new Set(mockProjects.map((p) => p.category))), @@ -61,7 +69,6 @@ export default function DiscoverPage() { const handleLoadMore = () => { setIsLoadingMore(true); - // Simulate network delay for loading chunks setTimeout(() => { setVisibleCount((prev) => prev + ITEMS_PER_PAGE); setIsLoadingMore(false); @@ -69,7 +76,7 @@ export default function DiscoverPage() { }; // Reset pagination when filters change - React.useEffect(() => { + useEffect(() => { setVisibleCount(ITEMS_PER_PAGE); }, [searchQuery, selectedCategory, sortBy]); @@ -96,6 +103,7 @@ export default function DiscoverPage() { className="w-full pl-12 pr-4 py-3 bg-zinc-50 dark:bg-zinc-900/50 border border-zinc-200 dark:border-zinc-800 rounded-2xl focus:outline-none focus:ring-2 focus:ring-blue-500/20" value={searchQuery} onChange={(e) => setSearchQuery(e.target.value)} + disabled={isInitialLoading} /> @@ -105,7 +113,8 @@ export default function DiscoverPage() { - - )} - - {/* Load More Pagination */} - {hasMore && visibleProjects.length > 0 && ( -
- -
+ <> + {/* Empty State - No Projects at All */} + {mockProjects.length === 0 ? ( +
+ +

No Projects Yet

+

+ Be the first to submit a project to the platform. +

+ +
+ ) : visibleProjects.length > 0 ? ( + <> + {/* Project Grid */} +
+ {visibleProjects.map((project) => ( + + ))} +
+ + {/* Load More Button */} + {hasMore && ( +
+ +
+ )} + + {/* Results Counter */} +
+ Showing {visibleProjects.length} of{" "} + {filteredAndSortedProjects.length} projects +
+ + ) : ( + /* Empty State - No Results from Filters */ +
+ +

No projects found

+

+ Try adjusting your search or filters to find what you're + looking for. +

+ +
+ )} + )} - -
- Showing {visibleProjects.length} of{" "} - {filteredAndSortedProjects.length} projects -
From 01b62d2ae654ab6230298ba815e1d32f6e54b954 Mon Sep 17 00:00:00 2001 From: Prz-droid Date: Mon, 27 Apr 2026 20:46:53 +0100 Subject: [PATCH 2/2] feat: implement project detail view page - Create dynamic project detail page with full information display - Add loading state with spinner during data fetch - Implement 404 error state for invalid/missing projects - Display project header with category, rating, and creation date - Show project image placeholder and description - Include external links section (website, GitHub) - Add verification status sidebar component - Display reviews section with ReviewList component - Add quick stats sidebar with key metrics - Include action buttons for reviews and verification - Make ProjectCard clickable with navigation to detail page - Responsive layout with sidebar on desktop --- dongle/app/projects/[id]/page.tsx | 268 +++++++++++++++++++++ dongle/components/projects/ProjectCard.tsx | 5 +- 2 files changed, 271 insertions(+), 2 deletions(-) create mode 100644 dongle/app/projects/[id]/page.tsx diff --git a/dongle/app/projects/[id]/page.tsx b/dongle/app/projects/[id]/page.tsx new file mode 100644 index 0000000..a6ab2f0 --- /dev/null +++ b/dongle/app/projects/[id]/page.tsx @@ -0,0 +1,268 @@ +"use client"; + +import React, { useState, useEffect } from "react"; +import { useParams, useRouter } from "next/navigation"; +import LayoutWrapper from "@/components/layout/LayoutWrapper"; +import { mockProjects } from "@/data/mockProjects"; +import { Badge } from "@/components/ui/Badge"; +import { Button } from "@/components/ui/Button"; +import { Spinner } from "@/components/ui/Spinner"; +import VerificationStatus from "@/components/verify/VerificationStatus"; +import ReviewList from "@/components/reviews/ReviewList"; +import { Review } from "@/types/review"; +import { + ArrowLeft, + ExternalLink, + Github, + Globe, + Star, + MessageSquare, + Calendar, + AlertCircle, +} from "lucide-react"; + +export default function ProjectDetailPage() { + const params = useParams(); + const router = useRouter(); + const projectId = params.id as string; + + const [isLoading, setIsLoading] = useState(true); + const [project, setProject] = useState(null); + const [reviews, setReviews] = useState([]); + const [currentUserAddress] = useState(null); + + useEffect(() => { + // Simulate data loading + const timer = setTimeout(() => { + const foundProject = mockProjects.find((p) => p.id === projectId); + setProject(foundProject || null); + + // Mock reviews data + if (foundProject) { + setReviews([ + { + id: "rev-1", + projectId: foundProject.id, + projectName: foundProject.name, + userAddress: "GABC...XYZ1", + rating: 5, + comment: "Excellent project! The team is very responsive and the product works flawlessly.", + createdAt: new Date(Date.now() - 86400000 * 2).toISOString(), + }, + { + id: "rev-2", + projectId: foundProject.id, + projectName: foundProject.name, + userAddress: "GDEF...XYZ2", + rating: 4, + comment: "Great concept and execution. Looking forward to future updates.", + createdAt: new Date(Date.now() - 86400000 * 5).toISOString(), + }, + ]); + } + + setIsLoading(false); + }, 600); + + return () => clearTimeout(timer); + }, [projectId]); + + const handleEdit = (review: Review) => { + console.log("Edit review:", review); + }; + + const handleDelete = (id: string) => { + console.log("Delete review:", id); + }; + + if (isLoading) { + return ( + +
+
+
+ +

Loading project details...

+
+
+
+
+ ); + } + + if (!project) { + return ( + +
+
+
+ +

Project Not Found

+

+ The project you're looking for doesn't exist or has been removed. +

+ +
+
+
+
+ ); + } + + return ( + +
+
+ {/* Back Button */} + + +
+ {/* Main Content */} +
+ {/* Project Header */} +
+
+
+ + {project.category} + +

{project.name}

+
+
+ + + {project.rating} + + ({project.reviews} reviews) +
+
+ + + {new Date(project.createdAt).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + })} + +
+
+
+
+ + {/* Project Image Placeholder */} +
+
+ {project.name[0]} +
+
+ + {/* Description */} +
+

About

+

+ {project.description} +

+
+ + {/* Links */} +
+ + +
+
+ + {/* Reviews Section */} +
+
+

+ + Reviews +

+ +
+ +
+
+ + {/* Sidebar */} +
+ {/* Verification Status */} +
+

Verification Status

+ +
+ + {/* Quick Stats */} +
+

Quick Stats

+
+
+ Rating + {project.rating} / 5.0 +
+
+ Total Reviews + {project.reviews} +
+
+ Category + {project.category} +
+
+
+ + {/* Actions */} +
+

Actions

+
+ + +
+
+
+
+
+
+
+ ); +} diff --git a/dongle/components/projects/ProjectCard.tsx b/dongle/components/projects/ProjectCard.tsx index d8b3002..5c4734c 100644 --- a/dongle/components/projects/ProjectCard.tsx +++ b/dongle/components/projects/ProjectCard.tsx @@ -1,4 +1,5 @@ import React from "react"; +import Link from "next/link"; import { Project } from "@/data/mockProjects"; interface ProjectCardProps { @@ -7,7 +8,7 @@ interface ProjectCardProps { export const ProjectCard = ({ project }: ProjectCardProps) => { return ( -
+
{project.name[0]} @@ -33,6 +34,6 @@ export const ProjectCard = ({ project }: ProjectCardProps) => {
{project.reviews} reviews
-
+ ); };