Skip to main content

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:
  1. Header: Page title, description, and “Create short link” button
  2. Links table/cards: All your links with details and actions
  3. 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">

Desktop Table View

The table displays five columns:
ColumnContentDescription
Short link/{short_code}Clickable link with copy button on hover
DestinationOriginal URLTruncated with full URL on hover
ExpiresExpiration dateFormatted as “MMM d, yyyy”
ClicksClick countTotal lifetime clicks
ActionsEdit, DeleteQuick 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(() => {});
}
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.
Modify link properties after creation:
1

Click Edit button

Click the Edit button on any link to open the edit dialog.
2

Modify properties

Change the custom slug and/or expiration date using the form fields.
3

Save changes

Click Save to apply changes or Cancel to discard.
4

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 (120 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.
Permanently remove links you no longer need:
1

Click Delete button

Click the Delete button on the link you want to remove.
2

Confirm deletion

A browser confirmation dialog asks: “Delete this short link?”
3

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]);

API Response Format

// 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

Performance Optimizations

  • 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

  1. Create link with custom slug: /summer-sale
  2. View in dashboard: Check it appears correctly
  3. Monitor clicks: Watch performance over time
  4. Extend if needed: Edit expiration before campaign ends
  5. Delete after campaign: Clean up when done
  1. Create link on home page (auto-generated slug)
  2. Open dashboard to verify
  3. Edit to add custom slug if needed
  4. Copy short URL and share

Bulk Management

  1. Review all links in dashboard
  2. Delete expired or low-performing links
  3. Extend expiration on active campaigns
  4. Update slugs for better organization

Build docs developers (and LLMs) love