Clone
<template>
<div class="min-h-screen bg-gray-50">
<!-- Header -->
<header class="bg-white shadow-sm border-b">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex justify-between items-center h-16">
<div class="flex items-center space-x-4">
<router-link to="/dashboard" class="flex items-center space-x-2">
<LinkIcon class="h-6 w-6 text-indigo-600" />
<span class="text-xl font-bold text-gray-900">QRurl</span>
</router-link>
<span class="text-gray-400">/</span>
<span class="text-gray-600">Analytics</span>
</div>
<router-link
to="/dashboard"
class="text-gray-500 hover:text-gray-700 transition flex items-center space-x-2"
>
<ArrowLeftIcon class="h-4 w-4" />
<span>Back to Dashboard</span>
</router-link>
</div>
</div>
</header>
<!-- Main Content -->
<main class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div v-if="loading" class="bg-white rounded-lg shadow-sm p-8">
<div class="text-center">
<Loader2Icon class="h-8 w-8 text-gray-400 animate-spin mx-auto mb-2" />
<p class="text-gray-500">Loading analytics...</p>
</div>
</div>
<div v-else-if="error" class="bg-white rounded-lg shadow-sm p-8">
<div class="text-center">
<AlertCircleIcon class="h-12 w-12 text-red-400 mx-auto mb-3" />
<p class="text-gray-900 font-semibold">Failed to load analytics</p>
<p class="text-gray-500 mt-1">{{ error }}</p>
</div>
</div>
<div v-else>
<!-- Link Info -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h1 class="text-2xl font-bold text-gray-900 mb-2">{{ analyticsData.entry.name }}</h1>
<div class="space-y-2">
<p class="text-sm text-gray-600">
<span class="font-medium">Short URL:</span>
<a :href="getShortUrl(analyticsData.entry.slug)" target="_blank" class="text-indigo-600 hover:text-indigo-700 ml-2">
{{ getShortUrl(analyticsData.entry.slug) }}
</a>
</p>
</div>
</div>
<!-- Stats Cards -->
<div class="grid md:grid-cols-3 gap-6 mb-6">
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Total Clicks</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{{ analyticsData.entry.clickCount || 0 }}</p>
</div>
<div class="p-3 bg-indigo-100 rounded-lg">
<MousePointerClickIcon class="h-6 w-6 text-indigo-600" />
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Unique Visitors</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{{ uniqueVisitors }}</p>
</div>
<div class="p-3 bg-green-100 rounded-lg">
<UsersIcon class="h-6 w-6 text-green-600" />
</div>
</div>
</div>
<div class="bg-white rounded-lg shadow-sm p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600">Countries</p>
<p class="text-3xl font-bold text-gray-900 mt-1">{{ uniqueCountries }}</p>
</div>
<div class="p-3 bg-purple-100 rounded-lg">
<GlobeIcon class="h-6 w-6 text-purple-600" />
</div>
</div>
</div>
</div>
<!-- Clicks by Date -->
<div class="bg-white rounded-lg shadow-sm p-6 mb-6">
<h2 class="text-lg font-semibold mb-4">Clicks Over Time</h2>
<div v-if="analyticsData.analytics.length === 0" class="text-center py-8 text-gray-500">
No click data available yet
</div>
<div v-else class="space-y-2">
<div v-for="day in clicksByDate" :key="day.date" class="flex items-center">
<span class="text-sm text-gray-600 w-24">{{ formatDate(day.date) }}</span>
<div class="flex-1 mx-4">
<div class="h-6 bg-gray-100 rounded-full overflow-hidden">
<div
class="h-full bg-indigo-600 rounded-full"
:style="`width: ${(day.clicks / maxClicks) * 100}%`"
></div>
</div>
</div>
<span class="text-sm font-medium text-gray-900 w-12 text-right">{{ day.clicks }}</span>
</div>
</div>
</div>
<!-- Countries -->
<div class="bg-white rounded-lg shadow-sm p-6">
<h2 class="text-lg font-semibold mb-4">Top Countries</h2>
<div v-if="countriesData.length === 0" class="text-center py-8 text-gray-500">
No geographic data available yet
</div>
<div v-else class="space-y-3">
<div v-for="country in countriesData" :key="country.name" class="flex items-center justify-between">
<span class="text-sm text-gray-700">{{ country.name || 'Unknown' }}</span>
<span class="text-sm font-medium text-gray-900">{{ country.clicks }} clicks</span>
</div>
</div>
</div>
</div>
</main>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import {
Link as LinkIcon,
ArrowLeft as ArrowLeftIcon,
Loader2 as Loader2Icon,
AlertCircle as AlertCircleIcon,
MousePointerClick as MousePointerClickIcon,
Users as UsersIcon,
Globe as GlobeIcon
} from 'lucide-vue-next'
import { useEntriesStore } from '../stores/entries'
const route = useRoute()
const entriesStore = useEntriesStore()
const loading = ref(true)
const error = ref('')
const analyticsData = ref(null)
const uniqueVisitors = computed(() => {
if (!analyticsData.value?.analytics) return 0
const unique = new Set(analyticsData.value.analytics.map(a => a.ip_hash))
return unique.size
})
const uniqueCountries = computed(() => {
if (!analyticsData.value?.analytics) return 0
const countries = new Set(analyticsData.value.analytics.map(a => a.country).filter(Boolean))
return countries.size
})
const clicksByDate = computed(() => {
if (!analyticsData.value?.analytics) return []
const grouped = {}
analyticsData.value.analytics.forEach(item => {
const date = item.date
if (!grouped[date]) {
grouped[date] = 0
}
grouped[date] += item.clicks || 1
})
return Object.entries(grouped)
.map(([date, clicks]) => ({ date, clicks }))
.sort((a, b) => new Date(b.date) - new Date(a.date))
.slice(0, 7)
})
const maxClicks = computed(() => {
return Math.max(...clicksByDate.value.map(d => d.clicks), 1)
})
const countriesData = computed(() => {
if (!analyticsData.value?.analytics) return []
const grouped = {}
analyticsData.value.analytics.forEach(item => {
const country = item.country || 'Unknown'
if (!grouped[country]) {
grouped[country] = 0
}
grouped[country] += item.clicks || 1
})
return Object.entries(grouped)
.map(([name, clicks]) => ({ name, clicks }))
.sort((a, b) => b.clicks - a.clicks)
.slice(0, 10)
})
onMounted(async () => {
try {
const id = route.params.id
analyticsData.value = await entriesStore.fetchAnalytics(id)
} catch (err) {
error.value = err.response?.data?.error || 'Failed to load analytics'
} finally {
loading.value = false
}
})
function getShortUrl(slug) {
return `${import.meta.env.VITE_SHORT_URL || 'http://localhost:8787'}/${slug}`
}
function formatDate(dateString) {
const date = new Date(dateString)
return date.toLocaleDateString('en-US', { month: 'short', day: 'numeric' })
}
</script>