import { useEffect, useMemo, useRef, useState } from 'react'; import { renderChart, useCollection, useIdentity, exportCSV } from '@deplixo/sdk'; import { LinkShortener } from './components/LinkShortener.jsx'; import { LinkList } from './components/LinkList.jsx'; import { Analytics } from './components/Analytics.jsx'; function App() { const { user, loading: identityLoading } = useIdentity(); const { items: linkItems, loading: linksLoading, add: addLink, update: updateLink, remove: removeLink, search: searchLinks, count: linkCount, collection: linksCollection } = useCollection('links', { personal: true }); const { items: analyticsItems, loading: analyticsLoading, add: addAnalytics, update: updateAnalytics, remove: removeAnalytics, search: searchAnalytics, count: analyticsCount, collection: analyticsCollection } = useCollection('link_analytics', { personal: true }); const [activeTab, setActiveTab] = useState('shorten'); const [selectedLink, setSelectedLink] = useState(null); const clicksChartRef = useRef(null); const topLinksChartRef = useRef(null); const tabs = [ { id: 'shorten', label: '✂️ Shorten', icon: '✂️' }, { id: 'links', label: '🔗 My Links', icon: '🔗' }, { id: 'analytics', label: '📊 Analytics', icon: '📊' } ]; useEffect(() => { if (activeTab === 'analytics' && selectedLink) { const latestSelected = linkItems?.find((link) => link.id === selectedLink.id || link.code === selectedLink.code); if (latestSelected) setSelectedLink(latestSelected); } }, [activeTab, selectedLink, linkItems]); const getLinkKey = (link) => link?.id || link?.code || link?.slug || link?.shortCode || ''; const getShortUrl = (link) => { if (!link) return ''; return link.shortUrl || link.short_url || link.url || link.link || ''; }; const normalizeTimestamp = (item) => { const value = item?.createdAt || item?.timestamp || item?.created_at || item?.date || item?.time; const date = value ? new Date(value) : null; return date && !Number.isNaN(date.getTime()) ? date : null; }; const generateQrUrl = (text) => { if (!text) return ''; return `https://api.qrserver.com/v1/create-qr-code/?size=180x180&data=${encodeURIComponent(text)}`; }; const canShare = typeof navigator !== 'undefined' && typeof navigator.share === 'function'; const handleShareLink = async (link) => { const shortUrl = getShortUrl(link) || link?.shortUrl || link?.url || link?.code || ''; const shareData = { title: 'Sniplink', text: `Check out this short link: ${shortUrl}`, url: shortUrl }; if (canShare) { try { await navigator.share(shareData); return; } catch (error) { // fall back to copy behavior below } } try { if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(shortUrl); } } catch (error) { // no-op } }; const analyticsForSelected = useMemo(() => { if (!selectedLink) return []; const selectedKey = getLinkKey(selectedLink); return (analyticsItems || []).filter((entry) => { const entryLink = entry?.linkId || entry?.link_id || entry?.linkCode || entry?.code || entry?.slug || entry?.shortCode; return entryLink === selectedKey || entryLink === selectedLink.code || entryLink === selectedLink.id; }); }, [analyticsItems, selectedLink]); const clicksOverTimeData = useMemo(() => { const grouped = new Map(); analyticsForSelected.forEach((entry) => { const date = normalizeTimestamp(entry) || new Date(); const label = date.toISOString().slice(0, 10); grouped.set(label, (grouped.get(label) || 0) + 1); }); return Array.from(grouped.entries()) .sort((a, b) => a[0].localeCompare(b[0])) .map(([label, value]) => ({ label, value })); }, [analyticsForSelected]); const topLinksData = useMemo(() => { const counts = new Map(); (analyticsItems || []).forEach((entry) => { const entryLink = entry?.linkId || entry?.link_id || entry?.linkCode || entry?.code || entry?.slug || entry?.shortCode; if (!entryLink) return; counts.set(entryLink, (counts.get(entryLink) || 0) + 1); }); const linkLookup = new Map((linkItems || []).map((link) => [getLinkKey(link), link])); return Array.from(counts.entries()) .map(([key, value]) => { const link = linkLookup.get(key); const label = link?.code || link?.shortCode || link?.slug || key; return { label, value }; }) .sort((a, b) => b.value - a.value) .slice(0, 8); }, [analyticsItems, linkItems]); useEffect(() => { if (clicksChartRef.current && clicksOverTimeData.length > 0) { renderChart(clicksChartRef.current, { type: 'line', data: { labels: clicksOverTimeData.map((d) => d.label), datasets: [{ label: 'Clicks', data: clicksOverTimeData.map((d) => d.value) }] } }); } }, [clicksOverTimeData]); useEffect(() => { if (topLinksChartRef.current && topLinksData.length > 0) { renderChart(topLinksChartRef.current, { type: 'bar', data: { labels: topLinksData.map((d) => d.label), datasets: [{ label: 'Total Clicks', data: topLinksData.map((d) => d.value) }] } }); } }, [topLinksData]); const getAnalyticsExportRows = (link = null) => { const selectedKey = link ? getLinkKey(link) : ''; const rows = (analyticsItems || []) .filter((entry) => { if (!link) return true; const entryLink = entry?.linkId || entry?.link_id || entry?.linkCode || entry?.code || entry?.slug || entry?.shortCode; return entryLink === selectedKey || entryLink === link.code || entryLink === link.id; }) .map((entry) => { const linked = link || (linkItems || []).find((item) => { const key = getLinkKey(item); const entryLink = entry?.linkId || entry?.link_id || entry?.linkCode || entry?.code || entry?.slug || entry?.shortCode; return entryLink === key || entryLink === item?.code || entryLink === item?.id; }); const timestamp = normalizeTimestamp(entry); return { link_code: linked?.code || linked?.shortCode || linked?.slug || entry?.linkCode || entry?.code || entry?.slug || entry?.shortCode || '', short_url: getShortUrl(linked), timestamp: timestamp ? timestamp.toISOString() : (entry?.createdAt || entry?.timestamp || entry?.created_at || entry?.date || entry?.time || ''), count: typeof entry?.count === 'number' ? entry.count : 1, referrer: entry?.referrer || entry?.source || entry?.utm_source || '', device: entry?.device || entry?.userAgent || entry?.platform || '' }; }); return rows; }; const handleExportAnalytics = (link = null) => { const rows = getAnalyticsExportRows(link); const filename = link ? `analytics_${(link?.code || link?.shortCode || link?.slug || getLinkKey(link) || 'selected').replace(/[^a-zA-Z0-9_-]/g, '_')}.csv` : 'analytics_full.csv'; exportCSV(rows, { filename, columns: ['link_code', 'short_url', 'timestamp', 'count', 'referrer', 'device'] }); }; const linkHelpers = useMemo(() => ({ user, addLink, updateLink, removeLink, searchLinks, linkItems: linkItems || [], linkCount: linkCount || 0, linksCollection, analyticsItems: analyticsItems || [], addAnalytics, updateAnalytics, removeAnalytics, searchAnalytics, analyticsCount: analyticsCount || 0, analyticsCollection }), [ user, addLink, updateLink, removeLink, searchLinks, linkItems, linkCount, linksCollection, analyticsItems, addAnalytics, updateAnalytics, removeAnalytics, searchAnalytics, analyticsCount, analyticsCollection ]); const handleViewAnalytics = (link) => { setSelectedLink(link); setActiveTab('analytics'); }; const handleCreated = () => { setActiveTab('links'); }; const appReady = !identityLoading && user; return (
Shorten, share, and track your links