From f37a2448927b59b16d18e0683d23d52aee93f896 Mon Sep 17 00:00:00 2001 From: Mohammed Rayan A Date: Sun, 12 Jul 2026 11:54:17 +0530 Subject: [PATCH] Implement public resource sharing by username under /u/[username] and integrate with ShareModal --- app/api/resources/route.ts | 37 +++- app/components/AddResource.tsx | 21 +- app/components/ui/ShareModal.tsx | 9 +- app/u/[username]/page.tsx | 336 +++++++++++++++++++++++++++++++ 4 files changed, 397 insertions(+), 6 deletions(-) create mode 100644 app/u/[username]/page.tsx diff --git a/app/api/resources/route.ts b/app/api/resources/route.ts index d9a6f16..25715f0 100644 --- a/app/api/resources/route.ts +++ b/app/api/resources/route.ts @@ -7,12 +7,45 @@ import { indexResource } from '../_utils/resourceIndexer'; // GET /api/resources - Get the authenticated user's resources export async function GET(request: NextRequest) { try { - const authUser = await requireAuth(request); const { searchParams } = new URL(request.url); - const collectionId = searchParams.get('collectionId'); + const username = searchParams.get('username'); + const isPublicParam = searchParams.get('public') === 'true'; const db = getServerFirestore(); + if (username && isPublicParam) { + const usernameQuery = await db.collection('users') + .where('username', '==', username.toLowerCase()) + .get(); + + if (usernameQuery.empty) { + return NextResponse.json( + { error: 'User not found' }, + { status: 404 } + ); + } + + const targetUid = usernameQuery.docs[0].id; + const resourcesQuery = await db.collection('resources') + .where('user_id', '==', targetUid) + .where('is_public', '==', true) + .orderBy('created_at', 'desc') + .get(); + + const resources = resourcesQuery.docs.map((doc: any) => ({ + id: doc.id, + ...doc.data() + })); + + return NextResponse.json({ + success: true, + resources + }); + } + + const authUser = await requireAuth(request); + const collectionId = searchParams.get('collectionId'); + let query: FirebaseFirestore.Query = db.collection('resources') .where('user_id', '==', authUser.uid); diff --git a/app/components/AddResource.tsx b/app/components/AddResource.tsx index bd963d0..7f26f6d 100644 --- a/app/components/AddResource.tsx +++ b/app/components/AddResource.tsx @@ -40,9 +40,27 @@ export function AddResource({ onSuccess }: AddResourceProps) { const lastEnrichedLinkRef = useRef('') const [showShareModal, setShowShareModal] = useState(false) const [sharedResourceData, setSharedResourceData] = useState<{ title: string; note?: string; link: string }>({ title: '', note: '', link: '' }) + const [username, setUsername] = useState('') useEffect(() => { - if (user) fetchCollections().catch(() => {}) + if (user) { + fetchCollections().catch(() => {}) + + const loadProfile = async () => { + try { + const response = await fetch('/api/user-profile') + if (response.ok) { + const data = await response.json() + if (data.profile?.username) { + setUsername(data.profile.username) + } + } + } catch (e) { + console.warn('Failed to load profile in AddResource:', e) + } + } + loadProfile() + } }, [user, fetchCollections]) const enrichResource = async () => { @@ -330,6 +348,7 @@ export function AddResource({ onSuccess }: AddResourceProps) { resourceTitle={sharedResourceData.title} resourceNote={sharedResourceData.note} resourceLink={sharedResourceData.link} + username={username} /> ) diff --git a/app/components/ui/ShareModal.tsx b/app/components/ui/ShareModal.tsx index bea89a9..1d42783 100644 --- a/app/components/ui/ShareModal.tsx +++ b/app/components/ui/ShareModal.tsx @@ -9,15 +9,18 @@ interface ShareModalProps { resourceTitle: string; resourceNote?: string; resourceLink: string; + username?: string; } -export function ShareModal({ isOpen, onClose, resourceTitle, resourceNote, resourceLink }: ShareModalProps) { +export function ShareModal({ isOpen, onClose, resourceTitle, resourceNote, resourceLink, username }: ShareModalProps) { const [copied, setCopied] = useState(false); if (!isOpen) return null; - // Use the actual resource link if available, fallback to the platform URL - const publicLink = resourceLink || `https://dumpit-three.vercel.app/`; + // Point to the user's public profile page if username is available, otherwise fallback to the resource's direct URL or the homepage + const publicLink = username + ? `https://dumpit-three.vercel.app/u/${username.toLowerCase()}` + : (resourceLink || `https://dumpit-three.vercel.app/`); const userHandle = '@DumpItApp'; const handleTwitterShare = () => { diff --git a/app/u/[username]/page.tsx b/app/u/[username]/page.tsx new file mode 100644 index 0000000..22a689c --- /dev/null +++ b/app/u/[username]/page.tsx @@ -0,0 +1,336 @@ +'use client' + +import { useParams, useRouter } from 'next/navigation'; +import { useEffect, useState, useMemo } from 'react'; +import { Search, RefreshCw, ExternalLink, Globe, Sparkles, BookOpen, AlertCircle, ArrowLeft } from 'lucide-react'; + +interface Resource { + id: string; + title: string; + link: string; + note?: string; + tag: string; + created_at: any; +} + +interface CacheData { + timestamp: number; + resources: Resource[]; +} + +const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes + +export default function PublicProfilePage() { + const params = useParams(); + const router = useRouter(); + const username = typeof params?.username === 'string' ? params.username : ''; + + const [resources, setResources] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(''); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedTag, setSelectedTag] = useState(null); + const [refreshing, setRefreshing] = useState(false); + + const fetchResources = async (forceRefetch = false) => { + if (!username) return; + setLoading(true); + setError(''); + + const cacheKey = `dumpit_shared_user_${username.toLowerCase()}`; + + if (!forceRefetch) { + try { + const cached = sessionStorage.getItem(cacheKey); + if (cached) { + const parsed: CacheData = JSON.parse(cached); + const age = Date.now() - parsed.timestamp; + if (age < CACHE_TTL_MS) { + setResources(parsed.resources); + setLoading(false); + return; + } + } + } catch (e) { + console.warn('Failed to parse cache from sessionStorage:', 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'); + } + 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'); + } finally { + setLoading(false); + setRefreshing(false); + } + }; + + useEffect(() => { + fetchResources(); + }, [username]); + + // Set page meta title dynamically + useEffect(() => { + if (username) { + document.title = `@${username}'s Shared Library | DumpIt`; + } + }, [username]); + + 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); + }); + 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 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...

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

User Not Found

+

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

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

DumpIt

+ Shared Library +
+
+ + +
+ + {/* Main Container */} +
+ + {/* Profile Card Header */} +
+
+
+ {username ? username.substring(0, 2) : 'U'} +
+
+
+

@{username}

+ + Public + +
+

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

+
+
+ +
+
{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..." + /> +
+
+ + {/* Tag Pills */} + {availableTags.length > 0 && ( +
+ Tags: + + {availableTags.map((tag) => ( + + ))} +
+ )} +
+ + {/* 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.'} +

+
+ ) : ( +
+ {filteredResources.map((resource) => ( + + ))} +
+ )} +
+
+ + {/* Footer footer */} + +
+ ); +}