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
48 changes: 47 additions & 1 deletion app/components/Profile.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { CheckCircle2, Globe, Loader2, Save, ShieldCheck, User } from 'lucide-react'
import { CheckCircle2, Globe, Loader2, Save, ShieldCheck, User, Copy, ExternalLink, Share2 } from 'lucide-react'
import { ReactNode, useEffect, useState } from 'react'
import { useAuth } from '../contexts/AuthContext'
import { authFetch, jsonAuthFetch } from '../lib/authFetch'
Expand All @@ -22,6 +22,19 @@ export function Profile() {
const [username, setUsername] = useState('')
const [shareByDefault, setShareByDefault] = useState(false)
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null)
const [copied, setCopied] = useState(false)

const handleCopyLink = async () => {
if (!profile?.username) return
const link = `https://dumpit-three.vercel.app/u/${profile.username.toLowerCase()}`
try {
await navigator.clipboard.writeText(link)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
} catch (err) {
console.error('Failed to copy link:', err)
}
}

useEffect(() => {
if (user) {
Expand Down Expand Up @@ -182,6 +195,39 @@ export function Profile() {
</div>
</section>

{profile?.username && (
<section className="app-panel p-4">
<div className="mb-3 flex items-center gap-2 text-sm font-bold text-slate-950 dark:text-white">
<Share2 className="h-4 w-4 text-blue-600" />
Public Library Link
</div>
<p className="text-xs text-slate-500 dark:text-slate-400 mb-3">
Anyone can view your publicly shared resources by visiting your personal link.
</p>
<div className="app-muted-panel p-2 mb-3 truncate text-xs text-slate-600 dark:text-slate-300 select-all font-mono">
https://dumpit-three.vercel.app/u/{profile.username.toLowerCase()}
</div>
<div className="grid grid-cols-2 gap-2">
<button
onClick={handleCopyLink}
className="inline-flex min-h-9 items-center justify-center gap-1.5 rounded-lg border border-slate-200 hover:bg-slate-50 dark:border-slate-850 dark:hover:bg-slate-900 text-xs font-bold text-slate-700 dark:text-slate-200 transition-colors"
>
<Copy className="h-3.5 w-3.5" />
{copied ? 'Copied!' : 'Copy Link'}
</button>
<a
href={`/u/${profile.username.toLowerCase()}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-h-9 items-center justify-center gap-1.5 rounded-lg bg-blue-600 hover:bg-blue-750 text-xs font-bold text-white transition-colors"
>
<ExternalLink className="h-3.5 w-3.5" />
View Library
</a>
</div>
</section>
)}

{profile && (
<section className="app-panel p-4">
<h2 className="mb-4 text-sm font-bold text-slate-950 dark:text-white">Vault stats</h2>
Expand Down
72 changes: 68 additions & 4 deletions app/components/SharedDump.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { CheckCircle2, ExternalLink, Filter, Globe2, Loader2, Plus, Search, Users } from 'lucide-react'
import { useEffect, useMemo, useState } from 'react'
import { useRouter } from 'next/navigation'
import { useAuth } from '../contexts/AuthContext'
import { authFetch, jsonAuthFetch } from '../lib/authFetch'

Expand Down Expand Up @@ -39,10 +40,28 @@ interface Resource {

export function SharedDump() {
const { user } = useAuth()
const router = useRouter()
const [resources, setResources] = useState<Resource[]>([])
const [loading, setLoading] = useState(true)
const [searchQuery, setSearchQuery] = useState('')
const [selectedTag, setSelectedTag] = useState('all')
const [userSearchInput, setUserSearchInput] = useState('')
const [searchUserError, setSearchUserError] = useState('')

const handleUserSearchSubmit = (e: React.FormEvent) => {
e.preventDefault()
const cleaned = userSearchInput.trim().replace(/^@/, '').toLowerCase()
if (!cleaned) return

const usernameRegex = /^[a-z0-9_-]{3,20}$/
if (!usernameRegex.test(cleaned)) {
setSearchUserError('Username must be 3-20 characters, lowercase letters, numbers, underscores, or hyphens.')
return
}

setSearchUserError('')
router.push(`/u/${cleaned}`)
}
const [savedResources, setSavedResources] = useState<Set<string>>(new Set())
const [savingId, setSavingId] = useState<string | null>(null)
const [error, setError] = useState('')
Expand Down Expand Up @@ -158,6 +177,38 @@ export function SharedDump() {
</div>
</header>

<section className="app-panel p-5 border border-slate-200 dark:border-slate-800 bg-slate-50/50 dark:bg-slate-900/40">
<h2 className="text-base font-bold text-slate-950 dark:text-white flex items-center gap-2 mb-1.5">
<Users className="h-4.5 w-4.5 text-blue-600 dark:text-blue-400" />
Browse a Contributor's Library
</h2>
<p className="text-xs text-slate-555 dark:text-slate-400 mb-4">
Enter a user's unique username to explore all of their publicly shared links and bookmarks.
</p>
<form onSubmit={handleUserSearchSubmit} className="flex gap-2 max-w-md">
<div className="relative flex-1">
<span className="absolute left-3 top-1/2 -translate-y-1/2 text-sm font-semibold text-slate-450 dark:text-slate-500">@</span>
<input
type="text"
placeholder="username"
value={userSearchInput}
onChange={(e) => setUserSearchInput(e.target.value)}
className="app-input pl-7 text-sm"
required
/>
</div>
<button
type="submit"
className="inline-flex min-h-10 items-center justify-center rounded-lg bg-blue-600 hover:bg-blue-700 px-4 text-xs font-bold text-white transition-colors"
>
Browse
</button>
</form>
{searchUserError && (
<p className="text-xs text-red-650 dark:text-red-400 mt-2 font-semibold">{searchUserError}</p>
)}
</section>

{error && (
<div className="rounded-lg border border-red-200 bg-red-50 p-4 text-sm text-red-700 dark:border-red-900 dark:bg-red-950/40 dark:text-red-300">
{error}
Expand Down Expand Up @@ -254,10 +305,23 @@ export function SharedDump() {
<span className="truncate">{resource.link}</span>
</a>
<div className="mt-4 flex items-center justify-between gap-3 border-t border-slate-100 pt-3 text-xs text-slate-500 dark:border-slate-800 dark:text-slate-400">
<span className="inline-flex min-w-0 items-center gap-1">
<Users className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">{resource.username || 'Anonymous'}</span>
</span>
{resource.username ? (
<a
href={`/u/${resource.username.toLowerCase()}`}
target="_blank"
rel="noopener noreferrer"
className="inline-flex min-w-0 items-center gap-1 hover:text-blue-600 dark:hover:text-blue-400 font-semibold transition-colors"
title={`View @${resource.username}'s public library`}
>
<Users className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">@{resource.username}</span>
</a>
) : (
<span className="inline-flex min-w-0 items-center gap-1 font-medium">
<Users className="h-3.5 w-3.5 shrink-0" />
<span className="truncate">Anonymous</span>
</span>
)}
<span>{formatDate(resource.created_at)}</span>
</div>
</div>
Expand Down
Loading