Overview
The dashboard is your central hub for managing short links. View all your links, track performance with click analytics, edit custom slugs and expiration dates, and delete links you no longer need.
The dashboard is only available to authenticated users . See the Authentication guide to learn how to sign in or create an account.
Accessing the Dashboard
Navigate to the dashboard:
Click Dashboard in the navigation bar
Visit /dashboard directly
Click My links from anywhere in the app
// From app/dashboard/page.tsx:134-141
< div >
< h1 className = "text-2xl font-semibold tracking-tight md:text-3xl" > My links </ h1 >
< p className = "mt-1 text-sm text-muted-foreground" >
Manage your short links and see how they perform .
</ p >
</ div >
< Button asChild className = "shrink-0" >
< Link href = "/" > Create short link </ Link >
</ Button >
Dashboard Layout
The dashboard includes:
Header : Page title, description, and “Create short link” button
Links table/cards : All your links with details and actions
Edit dialog : Modal for editing link properties
Responsive Design
Desktop (≥768px) : Data table with columns for all details
Mobile (<768px) : Card-based layout for touch-friendly interaction
// From app/dashboard/page.tsx:177-178
{ /* Desktop: table */ }
< Card className = "hidden overflow-hidden md:block" >
// From app/dashboard/page.tsx:248-249
{ /* Mobile: cards */ }
< div className = "space-y-3 md:hidden" >
Viewing Your Links
Desktop Table View
The table displays five columns:
Column Content Description Short link /{short_code}Clickable link with copy button on hover Destination Original URL Truncated with full URL on hover Expires Expiration date Formatted as “MMM d, yyyy” Clicks Click count Total lifetime clicks Actions Edit, Delete Quick action buttons
// From app/dashboard/page.tsx:182-188
< TableHeader >
< TableRow className = "hover:bg-transparent" >
< TableHead className = "font-medium" > Short link </ TableHead >
< TableHead className = "font-medium" > Destination </ TableHead >
< TableHead className = "font-medium" > Expires </ TableHead >
< TableHead className = "font-medium" > Clicks </ TableHead >
< TableHead className = "w-[140px] font-medium" > Actions </ TableHead >
</ TableRow >
</ TableHeader >
Mobile Card View
Each link is displayed as a card showing:
Short URL : Full clickable link
Original URL : Truncated destination
Metadata : Expiration date and click count
Actions : Full-width Edit and Delete buttons
// From app/dashboard/page.tsx:250-295
{ links . map (( link ) => (
< Card key = {link. id } className = "overflow-hidden transition-shadow hover:shadow-md" >
< CardContent className = "p-4" >
< div className = "flex items-start justify-between gap-2" >
< a href = { ` ${ BASE_URL } / ${ link . short_code } ` } target = "_blank" rel = "noopener noreferrer" >
{ BASE_URL } / {link. short_code }
</ a >
< Button variant = "ghost" size = "icon" onClick = {() => copyShortUrl ( link )} >
< Copy className = "size-4" />
</ Button >
</ div >
< p className = "mt-1 truncate text-sm text-muted-foreground" > {link. original_url } </ p >
< div className = "mt-3 flex flex-wrap items-center gap-x-4 gap-y-1 text-xs text-muted-foreground" >
< span >{link.expires_at ? format ( new Date (link.expires_at), 'MMM d, yyyy' ) : 'No expiry' } </ span >
< span className = "tabular-nums" > {link. clicks } clicks </ span >
</ div >
< div className = "mt-3 flex gap-2" >
< Button variant = "outline" size = "sm" className = "flex-1" onClick = {() => openEdit ( link )} > Edit </ Button >
< Button variant = "outline" size = "sm" className = "flex-1" onClick = {() => handleDelete ( link )} > Delete </ Button >
</ div >
</ CardContent >
</ Card >
))}
Empty State
When you haven’t created any links yet:
Icon : Large copy icon in a muted circle
Message : “No links yet” with helpful subtext
Call to action : “Create short link” button
// From app/dashboard/page.tsx:160-174
links . length === 0 ? (
< Card className = "border-dashed" >
< CardContent className = "flex flex-col items-center justify-center py-12 text-center" >
< div className = "rounded-full bg-muted p-4" >
< Copy className = "size-8 text-muted-foreground" />
</ div >
< h2 className = "mt-4 text-lg font-medium" > No links yet </ h2 >
< p className = "mt-1 max-w-sm text-sm text-muted-foreground" >
Create your first short link from the home page , then manage it here .
</ p >
< Button asChild className = "mt-6" >
< Link href = "/" > Create short link </ Link >
</ Button >
</ CardContent >
</ Card >
)
The empty state encourages users to create their first link with clear next steps.
Quick Actions
Copy Short URL
On desktop, hover over a short link to reveal the copy button:
// From app/dashboard/page.tsx:203-211
< Button
variant = "ghost"
size = "icon"
className = "size-8 shrink-0 opacity-0 transition-opacity group-hover:opacity-100"
onClick = {() => copyShortUrl ( link )}
>
< Copy className = "size-4" />
</ Button >
Clicking copies the full short URL to your clipboard:
// From app/dashboard/page.tsx:125-128
async function copyShortUrl ( link : LinkRow ) {
const url = ` ${ BASE_URL } / ${ link . short_code } ` ;
await navigator . clipboard . writeText ( url ). catch (() => {});
}
Open Short Link
Click the short link text to open it in a new tab:
// From app/dashboard/page.tsx:195-202
< a
href = { ` ${ BASE_URL } / ${ link . short_code } ` }
target = "_blank"
rel = "noopener noreferrer"
className = "font-medium text-primary underline-offset-4 hover:underline"
>
/ { link . short_code }
</ a >
Short links open in a new tab and include noopener noreferrer for security.
Editing Links
Modify link properties after creation:
Click Edit button
Click the Edit button on any link to open the edit dialog.
Modify properties
Change the custom slug and/or expiration date using the form fields.
Save changes
Click Save to apply changes or Cancel to discard.
View updated link
The dashboard refreshes automatically with your updated link.
Edit Dialog
The edit dialog includes:
Title : “Edit short link”
Description : “Change the slug or expiry date.”
Custom slug field : Text input with validation hint
Expiration picker : Calendar popover for date selection
Action buttons : Cancel and Save
// From app/dashboard/page.tsx:300-357
< Dialog open = {!! editing } onOpenChange = {(open) => !open && setEditing ( null )} >
< DialogContent className = "sm:max-w-md" >
< DialogHeader >
< DialogTitle > Edit short link </ DialogTitle >
< DialogDescription > Change the slug or expiry date . </ DialogDescription >
</ DialogHeader >
{ editing && (
< div className = "space-y-4 py-4" >
< div className = "space-y-2" >
< Label > Custom slug </ Label >
< Input
value = { editSlug }
onChange = {(e) => setEditSlug (e.target.value)}
placeholder = "my-custom-slug"
/>
< p className = "text-xs text-muted-foreground" >
Letters , numbers , _ and - only ( 1 – 20 chars )
</ p >
</ div >
{ /* Expiration date picker */ }
</ div >
)}
< DialogFooter >
< Button variant = "outline" onClick = {() => setEditing ( null )} > Cancel </ Button >
< Button onClick = { handleSave } disabled = { saving } >
{ saving ? 'Saving…' : 'Save' }
</ Button >
</ DialogFooter >
</ DialogContent >
</ Dialog >
Custom Slug Editing
Edit or add a custom slug:
Input field : Shows current slug (editable)
Validation hint : “Letters, numbers, _ and - only (1–20 chars)”
Real-time updates : Slug updates when you save
// From app/dashboard/page.tsx:75-80
function openEdit ( link : LinkRow ) {
setEditing ( link );
setEditSlug ( link . short_code );
setEditExpires ( link . expires_at ? new Date ( link . expires_at ) : null );
setError ( '' );
}
Expiration Date Editing
Change when the link expires:
Calendar popover : Click to open date picker
Date constraints : Today to 30 days from today
Clear option : Remove date to clear expiration (may not work)
// From app/dashboard/page.tsx:319-345
< div className = "space-y-2" >
< Label > Expires ( optional , max 30 days from today ) </ Label >
< Popover >
< PopoverTrigger asChild >
< Button variant = "outline" className = "w-full justify-start text-left font-normal" >
{ editExpires ? format ( editExpires , 'PPP' ) : 'Pick a date' }
</ Button >
</ PopoverTrigger >
< PopoverContent className = "w-auto p-0" align = "start" >
< Calendar
mode = "single"
selected = {editExpires ?? undefined }
onSelect = {(d) => setEditExpires ( d ? ? null )}
disabled = {(d) => d < minDate || d > maxDate }
initialFocus
/>
</ PopoverContent >
</ Popover >
</ div >
Save Handler
Submits changes to the API:
// From app/dashboard/page.tsx:82-107
async function handleSave () {
if ( ! editing ) return ;
setSaving ( true );
setError ( '' );
try {
const res = await fetch ( `/api/links/ ${ editing . id } ` , {
method: 'PATCH' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ({
shortCode: editSlug . trim () || undefined ,
expiresAt: editExpires ? editExpires . toISOString () : null ,
}),
});
const data = await res . json (). catch (() => ({}));
if ( ! res . ok ) {
setError ( data ?. error ?? 'Failed to update' );
return ;
}
setEditing ( null );
fetchLinks ();
} catch {
setError ( 'Failed to update' );
} finally {
setSaving ( false );
}
}
Changing a custom slug makes the old short URL stop working immediately. Make sure to update any places where you’ve shared the old link.
Deleting Links
Permanently remove links you no longer need:
Click Delete button
Click the Delete button on the link you want to remove.
Confirm deletion
A browser confirmation dialog asks: “Delete this short link?”
Link removed
The link is deleted from the database and cache, and the dashboard refreshes.
// From app/dashboard/page.tsx:109-119
async function handleDelete ( link : LinkRow ) {
if ( ! confirm ( 'Delete this short link?' )) return ;
try {
const res = await fetch ( `/api/links/ ${ link . id } ` , { method: 'DELETE' });
if ( ! res . ok ) throw new Error ( 'Failed' );
setEditing ( null );
fetchLinks ();
} catch {
setError ( 'Failed to delete' );
}
}
Deletion is permanent and immediate . The short link will stop working right away and cannot be recovered.
Loading States
The dashboard shows loading skeletons while fetching links:
// From app/dashboard/page.tsx:150-159
loading ? (
< Card >
< CardContent className = "pt-6" >
< div className = "space-y-3" >
{ [ 1 , 2 , 3 , 4 ].map((i) => (
<Skeleton key={i} className="h-14 w-full rounded-lg" />
))}
</div>
</CardContent>
</Card>
)
Skeleton screens improve perceived performance by showing content structure before data loads.
Error Handling
Errors are displayed in a dismissible banner:
// From app/dashboard/page.tsx:144-148
{ error && (
< div className = "rounded-lg border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive" >
{ error }
</ div >
)}
Common error messages:
“Failed to load links” : Couldn’t fetch your links from the server
“Failed to update” : Edit operation failed (e.g., slug conflict)
“Failed to delete” : Deletion operation failed
“Slug already in use” : Another link is using that custom slug
Data Fetching
The dashboard fetches links on mount and after updates:
// From app/dashboard/page.tsx:58-69
const fetchLinks = useCallback ( async () => {
try {
const res = await fetch ( '/api/links' );
if ( ! res . ok ) throw new Error ( 'Failed to load' );
const data = await res . json ();
setLinks ( data );
} catch {
setError ( 'Failed to load links' );
} finally {
setLoading ( false );
}
}, []);
useEffect (() => {
fetchLinks ();
}, [ fetchLinks ]);
// From app/lib/links.ts:111-120
export type LinkRow = {
id : number ;
short_code : string ;
original_url : string ;
created_at : Date ;
expires_at : Date | null ;
custom_slug : boolean ;
clicks : number ;
user_id : string | null ;
};
Best Practices
Dashboard Tips
Regular cleanup : Delete expired or unused links to keep your dashboard organized
Descriptive slugs : Use custom slugs that clearly identify the campaign or purpose
Check analytics : Review click counts to see which links perform best
Extend expiration : Edit links before they expire if you still need them
Organize campaigns : Use consistent slug naming (e.g., campaign-month-year)
Keyboard & Accessibility
Tab navigation : Navigate between links and actions with Tab
Enter/Space : Activate buttons and open dialogs
Escape : Close edit dialog
ARIA labels : Semantic HTML and proper labeling for screen readers
// From app/dashboard/page.tsx:107
initialFocus // Calendar auto-focuses when opened
Hover-only copy button : Reduces visual clutter on desktop
Responsive breakpoints : Optimized layouts for mobile and desktop
Truncated URLs : Long URLs don’t break layout (full text on hover)
Skeleton loading : Perceived faster load times
useCallback : Prevents unnecessary re-renders
Common Workflows
Creating and Managing a Campaign
Create link with custom slug: /summer-sale
View in dashboard: Check it appears correctly
Monitor clicks: Watch performance over time
Extend if needed: Edit expiration before campaign ends
Delete after campaign: Clean up when done
Quick Link Creation
Create link on home page (auto-generated slug)
Open dashboard to verify
Edit to add custom slug if needed
Copy short URL and share
Bulk Management
Review all links in dashboard
Delete expired or low-performing links
Extend expiration on active campaigns
Update slugs for better organization