From f21f0799cafbdb2650270f8905bd4ece5994e6d5 Mon Sep 17 00:00:00 2001 From: Mohammed Rayan A Date: Mon, 13 Jul 2026 11:56:16 +0530 Subject: [PATCH] Improve public profile page UI with logo, hero banner, and responsive card grid --- app/u/[username]/page.tsx | 454 +++++++++++++++++++++----------------- 1 file changed, 255 insertions(+), 199 deletions(-) diff --git a/app/u/[username]/page.tsx b/app/u/[username]/page.tsx index 22a689c..1713e45 100644 --- a/app/u/[username]/page.tsx +++ b/app/u/[username]/page.tsx @@ -2,7 +2,7 @@ import { useParams, useRouter } from 'next/navigation'; import { useEffect, useState, useMemo } from 'react'; -import { Search, RefreshCw, ExternalLink, Globe, Sparkles, BookOpen, AlertCircle, ArrowLeft } from 'lucide-react'; +import { Search, RefreshCw, ExternalLink, Globe, Sparkles, BookOpen, AlertCircle, ArrowLeft, ArrowRight } from 'lucide-react'; interface Resource { id: string; @@ -10,7 +10,7 @@ interface Resource { link: string; note?: string; tag: string; - created_at: any; + created_at: string; } interface CacheData { @@ -18,7 +18,45 @@ interface CacheData { resources: Resource[]; } -const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const CACHE_TTL_MS = 5 * 60 * 1000; + +const TAG_COLORS: Record = { + Tutorial: 'bg-amber-50 text-amber-700 border-amber-200 dark:bg-amber-950/30 dark:text-amber-400 dark:border-amber-900/50', + Article: 'bg-blue-50 text-blue-700 border-blue-200 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-900/50', + Video: 'bg-red-50 text-red-700 border-red-200 dark:bg-red-950/30 dark:text-red-400 dark:border-red-900/50', + Tool: 'bg-purple-50 text-purple-700 border-purple-200 dark:bg-purple-950/30 dark:text-purple-400 dark:border-purple-900/50', + Documentation: 'bg-slate-100 text-slate-700 border-slate-200 dark:bg-slate-800 dark:text-slate-300 dark:border-slate-700', + Course: 'bg-green-50 text-green-700 border-green-200 dark:bg-green-950/30 dark:text-green-400 dark:border-green-900/50', + GitHub: 'bg-gray-100 text-gray-700 border-gray-200 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-700', + Design: 'bg-pink-50 text-pink-700 border-pink-200 dark:bg-pink-950/30 dark:text-pink-400 dark:border-pink-900/50', + Library: 'bg-indigo-50 text-indigo-700 border-indigo-200 dark:bg-indigo-950/30 dark:text-indigo-400 dark:border-indigo-900/50', + Other: 'bg-slate-100 text-slate-600 border-slate-200 dark:bg-slate-800 dark:text-slate-400 dark:border-slate-700', +}; + +function getTagColor(tag: string): string { + return TAG_COLORS[tag] || TAG_COLORS['Other']; +} + +function getFavicon(url: string): string { + try { + const domain = new URL(url).hostname; + return `https://www.google.com/s2/favicons?domain=${domain}&sz=32`; + } catch { + return ''; + } +} + +function getDomain(url: string): string { + try { + return new URL(url).hostname.replace('www.', ''); + } catch { + return 'link'; + } +} + +function getInitials(name: string): string { + return name.substring(0, 2).toUpperCase(); +} export default function PublicProfilePage() { const params = useParams(); @@ -44,206 +82,197 @@ export default function PublicProfilePage() { const cached = sessionStorage.getItem(cacheKey); if (cached) { const parsed: CacheData = JSON.parse(cached); - const age = Date.now() - parsed.timestamp; - if (age < CACHE_TTL_MS) { + if (Date.now() - parsed.timestamp < CACHE_TTL_MS) { setResources(parsed.resources); setLoading(false); return; } } } catch (e) { - console.warn('Failed to parse cache from sessionStorage:', e); + console.warn('Failed to parse cache:', e); } } try { const response = await fetch(`/api/resources?username=${encodeURIComponent(username)}&public=true`); if (!response.ok) { - if (response.status === 404) { - throw new Error('User not found'); - } + if (response.status === 404) throw new Error('User not found'); throw new Error('Failed to load shared resources'); } - const data = await response.json(); - const fetchedResources: Resource[] = data.resources || []; - - setResources(fetchedResources); - - // Save to cache - const cacheData: CacheData = { - timestamp: Date.now(), - resources: fetchedResources - }; - sessionStorage.setItem(cacheKey, JSON.stringify(cacheData)); - } catch (err: any) { - setError(err.message || 'An error occurred'); + const fetched: Resource[] = data.resources || []; + setResources(fetched); + sessionStorage.setItem(cacheKey, JSON.stringify({ timestamp: Date.now(), resources: fetched })); + } catch (err: unknown) { + setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setLoading(false); setRefreshing(false); } }; - useEffect(() => { - fetchResources(); - }, [username]); + useEffect(() => { fetchResources(); }, [username]); - // Set page meta title dynamically useEffect(() => { - if (username) { - document.title = `@${username}'s Shared Library | DumpIt`; - } + if (username) document.title = `@${username}'s Library | DumpIt`; }, [username]); - const handleManualRefresh = () => { - setRefreshing(true); - fetchResources(true); - }; + const handleManualRefresh = () => { setRefreshing(true); fetchResources(true); }; - // Get unique tags from all resources const availableTags = useMemo(() => { const tags = new Set(); - resources.forEach((r) => { - if (r.tag) tags.add(r.tag); - }); + resources.forEach((r) => { if (r.tag) tags.add(r.tag); }); return Array.from(tags); }, [resources]); - // Filter resources by search query and tag selection const filteredResources = useMemo(() => { return resources.filter((resource) => { - const matchesSearch = - resource.title?.toLowerCase().includes(searchQuery.toLowerCase()) || - resource.note?.toLowerCase().includes(searchQuery.toLowerCase()) || - resource.tag?.toLowerCase().includes(searchQuery.toLowerCase()); - + const q = searchQuery.toLowerCase(); + const matchesSearch = !q || + resource.title?.toLowerCase().includes(q) || + resource.note?.toLowerCase().includes(q) || + resource.tag?.toLowerCase().includes(q) || + getDomain(resource.link).toLowerCase().includes(q); const matchesTag = !selectedTag || resource.tag === selectedTag; - return matchesSearch && matchesTag; }); }, [resources, searchQuery, selectedTag]); - // Helper to extract clean domain name - const getDomain = (url: string) => { - try { - return new URL(url).hostname.replace('www.', ''); - } catch { - return 'link'; - } - }; - if (loading && !refreshing) { return ( -
-
-
-

Loading library...

-
+
+ DumpIt +

Loading library...

); } if (error === 'User not found') { return ( -
-
- -

User Not Found

-

- The user profile @{username} does not exist or has no shared resources. -

- +
+
+ DumpIt + DumpIt +
+
+
+
+ +
+

User not found

+

+ No public library found for{' '} + @{username}. +

+ +
); } return ( -
- - {/* Navbar header */} -
-
-
- D -
-
-

DumpIt

- Shared Library +
+ + {/* Navbar */} +
+
+ + DumpIt logo + DumpIt + +
+ + + Open app + +
- -
- {/* Main Container */} -
- - {/* Profile Card Header */} -
-
-
- {username ? username.substring(0, 2) : 'U'} + {/* Hero / Profile banner */} +
+
+
+
+
+
+ {getInitials(username)} +
+
+
+

@{username}

+ + + Public + +
+

+ Publicly shared links and resources, powered by DumpIt AI knowledge vault. +

+
-
-
-

@{username}

- - Public - + +
+
+
{resources.length}
+
Public links
-

- Explore public links and resources saved in my AI-powered second brain. -

+ {selectedTag && ( + <> +
+
+
{filteredResources.length}
+
Filtered
+
+ + )}
+
+
-
-
{resources.length}
-
Shared Resources
-
-
- - {/* Filters and Search Bar */} -
-
- {/* Search Input */} -
- - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-3 bg-white dark:bg-slate-900 border border-slate-200 dark:border-slate-800 rounded-xl focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all text-sm placeholder-slate-400 dark:placeholder-slate-500" - placeholder="Search resources by title, tags, notes..." - /> -
+ {/* Main content */} +
+ + {/* Search + filters */} +
+
+ + setSearchQuery(e.target.value)} + placeholder="Search by title, domain, tag, or notes..." + className="w-full rounded-xl border border-slate-200 dark:border-slate-800 bg-white dark:bg-slate-900 py-3 pl-10 pr-4 text-sm outline-none placeholder-slate-400 dark:placeholder-slate-500 transition-all focus:border-blue-500 focus:ring-2 focus:ring-blue-500/20 dark:text-white" + />
- {/* Tag Pills */} {availableTags.length > 0 && ( -
- Tags: +
)} -
- - {/* Resources Grid */} -
- {filteredResources.length === 0 ? ( -
- -

No resources found

-

- {resources.length === 0 - ? 'This user has not shared any public resources yet.' - : 'Try adjusting your search terms or filters.'} -

+
+ + {/* Resource cards */} + {filteredResources.length === 0 ? ( +
+
+
- ) : ( -
- {filteredResources.map((resource) => ( -
-
-
- - {resource.title || 'Untitled Source'} - - - {resource.tag && ( - - {resource.tag} - - )} -
- - +

No resources found

+

+ {resources.length === 0 + ? "This user hasn't shared any public resources yet." + : 'Try adjusting your search or clearing the tag filter.'} +

+ {(searchQuery || selectedTag) && ( + + )} +
+ ) : ( +
- ))} -
- )} -
+

+ {resource.title || 'Untitled Resource'} +

+ + {resource.note && ( +

+ {resource.note} +

+ )} + +
+ + + Shared via DumpIt + + + Open + +
+
+ ))} +
+ )} - {/* Footer footer */} -
);