"use client"; import { useEffect, useState } from 'react'; import { getSupabaseClient } from '../utils/supabase'; import { AuthChangeEvent } from '@supabase/supabase-js'; import { Loader2, ExternalLink, Copy, Search } from 'lucide-react'; // Define attribute type to avoid using 'any' interface LinkAttributes { title?: string; name?: string; slug?: string; original_url?: string; originalUrl?: string; visits?: number; click_count?: number; team_id?: string; team_name?: string; tags?: string[]; [key: string]: unknown; } // Define Link type based on the database schema interface ShortLink { id: string; external_id: string | null; attributes: LinkAttributes | null; type: string | null; creator_id: string | null; created_at: string | null; updated_at: string | null; deleted_at: string | null; schema_version: number | null; } // Define ClickHouse shorturl type interface ClickHouseShortUrl { id: string; external_id: string; type: string; slug: string; original_url: string; title: string; description: string; attributes: string; // JSON string schema_version: number; creator_id: string; creator_email: string; creator_name: string; created_at: string; updated_at: string; deleted_at: string | null; projects: string; // JSON string teams: string; // JSON string tags: string; // JSON string qr_codes: string; // JSON string channels: string; // JSON string favorites: string; // JSON string expires_at: string | null; click_count: number; unique_visitors: number; link_attributes?: string; // Optional JSON string containing link-specific attributes } // Convert ClickHouse data to ShortLink format function convertClickHouseToShortLink(chData: ClickHouseShortUrl): ShortLink { // Parse JSON strings const attributesJson = JSON.parse(chData.attributes || '{}'); const teamsJson = JSON.parse(chData.teams || '[]'); const tagsJson = JSON.parse(chData.tags || '[]'); // Extract team info from the first team if available const teamInfo = teamsJson.length > 0 ? { team_id: teamsJson[0].team_id, team_name: teamsJson[0].team_name } : {}; // Extract tag names const tagNames = tagsJson.map((tag: { tag_name: string }) => tag.tag_name); // Parse link_attributes to get domain if available let domain = 'shorturl.example.com'; try { if (chData.link_attributes) { const linkAttrObj = JSON.parse(chData.link_attributes); if (linkAttrObj.domain) { domain = linkAttrObj.domain; } } // Extract domain from shortUrl in attributes if available const attributesObj = JSON.parse(chData.attributes || '{}'); if (attributesObj.shortUrl) { try { const urlObj = new URL(attributesObj.shortUrl); domain = urlObj.hostname; } catch (e) { console.error('Error parsing shortUrl:', e); } } } catch (e) { console.error('Error parsing link_attributes:', e); } return { id: chData.id, external_id: chData.external_id, type: chData.type, creator_id: chData.creator_id, created_at: chData.created_at, updated_at: chData.updated_at, deleted_at: chData.deleted_at, schema_version: chData.schema_version, attributes: { ...attributesJson, title: chData.title || attributesJson.title || '', slug: chData.slug, original_url: chData.original_url, click_count: chData.click_count, visits: chData.click_count, unique_visitors: chData.unique_visitors, domain: attributesJson.domain || domain, ...teamInfo, tags: tagNames } }; } export default function LinksPage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [links, setLinks] = useState([]); const [searchQuery, setSearchQuery] = useState(''); const [teamFilter, setTeamFilter] = useState(null); // Copy link to clipboard const copyToClipboard = (text: string) => { navigator.clipboard.writeText(text).then(() => { // Could add a notification here console.log('Copied to clipboard'); }); }; // Extract link metadata from attributes const getLinkMetadata = (link: ShortLink) => { const attrs = link.attributes || {}; return { title: attrs.title || attrs.name || 'Untitled Link', slug: attrs.slug || 'no-slug', originalUrl: attrs.original_url || attrs.originalUrl || '#', visits: attrs.visits || attrs.click_count || 0, teamId: attrs.team_id || '', teamName: attrs.team_name || 'Personal', createdAt: new Date(link.created_at || Date.now()).toLocaleDateString(), tags: attrs.tags || [], domain: attrs.domain || 'shorturl.example.com', }; }; // Filter links by search query const filteredLinks = links.length > 0 ? links.filter(link => { if (!searchQuery && !teamFilter) return true; const metadata = getLinkMetadata(link); const matchesSearch = searchQuery ? (metadata.title.toLowerCase().includes(searchQuery.toLowerCase()) || metadata.slug.toLowerCase().includes(searchQuery.toLowerCase()) || metadata.originalUrl.toLowerCase().includes(searchQuery.toLowerCase())) : true; const matchesTeam = teamFilter ? metadata.teamId === teamFilter : true; return matchesSearch && matchesTeam; }) : []; // Get unique teams for filtering const teams = links.length > 0 ? Array.from( links.reduce((teams, link) => { const metadata = getLinkMetadata(link); if (metadata.teamId) { teams.set(metadata.teamId, metadata.teamName); } return teams; }, new Map()) ).map(([id, name]) => ({ id, name })) : []; useEffect(() => { let isMounted = true; const fetchLinks = async () => { if (!isMounted) return; setLoading(true); setError(null); try { // Fetch data from ClickHouse API instead of Supabase const response = await fetch('/api/shortlinks'); if (!response.ok) { throw new Error(`Failed to fetch links: ${response.status} ${response.statusText}`); } const data = await response.json(); if (!data || !data.links || data.links.length === 0) { if (isMounted) setLinks([]); return; } // Convert ClickHouse data format to ShortLink format const convertedLinks = data.links.map(convertClickHouseToShortLink); if (isMounted) { setLinks(convertedLinks); } } catch (err) { if (isMounted) { setError(err instanceof Error ? err.message : 'Failed to load short URLs'); console.error("Error fetching links:", err); } } finally { if (isMounted) { setLoading(false); } } }; const supabase = getSupabaseClient(); const { data: { subscription } } = supabase.auth.onAuthStateChange((event: AuthChangeEvent) => { if (event === 'SIGNED_IN') { fetchLinks(); } else if (event === 'SIGNED_OUT') { setLinks([]); setError(null); } }); supabase.auth.getSession().then(() => { fetchLinks(); }); return () => { isMounted = false; subscription.unsubscribe(); }; }, []); if (loading) { return (
); } if (error) { return (

Error: {error}

); } if (links.length === 0) { return (

No short links found

Create your first short URL to get started

); } return (

Short URL Links

{/* Search and filters */}
setSearchQuery(e.target.value)} className="w-full rounded-md border border-gray-300 py-2 pl-10 pr-4 text-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500" />
{/* Links table */}
{filteredLinks.map(link => { const metadata = getLinkMetadata(link); const shortUrl = `https://${metadata.domain}/${metadata.slug}`; return ( ); })}
Link Original URL Team Created Visits Actions
{metadata.title} {shortUrl}
{metadata.originalUrl} {metadata.teamName} {metadata.createdAt} {metadata.visits}
{/* Pagination or "Load More" could be added here */} {filteredLinks.length === 0 && links.length > 0 && (
No links match your search criteria
)}
); }