feat: add SVG/PNG download options, track geolocation from CF headers, and fix analytics pie charts by parsing counts as integers

This commit is contained in:
Mohan Ki
2026-07-27 17:00:54 +05:30
parent fba969dd55
commit 9aaf94625a
8 changed files with 163 additions and 38 deletions
+52 -10
View File
@@ -22,9 +22,11 @@ interface QRCode {
interface AnalyticsData {
totalScans: number;
osBreakdown: { os: string; count: string }[];
deviceBreakdown: { device_type: string; count: string }[];
timeline: { date: string; count: string }[];
uniqueScanners: number;
osBreakdown: { os: string; count: number }[];
deviceBreakdown: { device_type: string; count: number }[];
countryBreakdown: { country: string; count: number }[];
timeline: { date: string; count: number }[];
}
const COLORS = ['#4f46e5', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6'];
@@ -126,17 +128,18 @@ export default function AnalyticsPage() {
<div className="text-5xl font-black text-indigo-600">{analytics.totalScans.toLocaleString()}</div>
</div>
{/* Adding placeholders for conversion rate / unique scanners since backend doesn't track unique yet */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 opacity-50">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
<h3 className="text-sm font-bold text-gray-500 uppercase tracking-wider mb-2">Unique Scanners</h3>
<div className="text-5xl font-black text-gray-900">--</div>
<p className="text-xs text-gray-400 mt-2">Available in Pro</p>
<div className="text-5xl font-black text-indigo-600">{analytics.uniqueScanners.toLocaleString()}</div>
<p className="text-xs text-gray-400 mt-2">Distinct IP Addresses</p>
</div>
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200 opacity-50">
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
<h3 className="text-sm font-bold text-gray-500 uppercase tracking-wider mb-2">Top Location</h3>
<div className="text-3xl font-black text-gray-900 mt-2">Unknown</div>
<p className="text-xs text-gray-400 mt-2">Available in Pro</p>
<div className="text-3xl font-black text-gray-900 mt-2">
{analytics.countryBreakdown?.length > 0 ? analytics.countryBreakdown[0].country : 'Unknown'}
</div>
<p className="text-xs text-gray-400 mt-2">By Country</p>
</div>
</div>
@@ -243,6 +246,45 @@ export default function AnalyticsPage() {
</div>
{/* Country Chart */}
<div className="bg-white p-6 rounded-2xl shadow-sm border border-gray-200">
<h3 className="text-lg font-bold text-gray-900 mb-6">Locations (Country)</h3>
<div className="h-64 flex items-center justify-center">
{analytics.countryBreakdown?.length > 0 ? (
<ResponsiveContainer width="100%" height="100%">
<PieChart>
<Pie
data={analytics.countryBreakdown}
cx="50%"
cy="50%"
innerRadius={60}
outerRadius={80}
paddingAngle={5}
dataKey="count"
nameKey="country"
>
{analytics.countryBreakdown.map((entry, index) => (
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
))}
</Pie>
<Tooltip contentStyle={{ borderRadius: '12px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
</PieChart>
</ResponsiveContainer>
) : (
<span className="text-gray-400">No location data</span>
)}
</div>
{/* Legend */}
<div className="flex justify-center gap-4 flex-wrap mt-2">
{analytics.countryBreakdown?.map((entry, index) => (
<div key={index} className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full" style={{backgroundColor: COLORS[index % COLORS.length]}}></span>
<span className="text-sm font-medium text-gray-700 capitalize">{entry.country || 'Unknown'} ({entry.count})</span>
</div>
))}
</div>
</div>
</div>
)}
</div>
+16 -9
View File
@@ -82,11 +82,11 @@ export default function CreateQRPage() {
}
};
const handleDownload = async () => {
const handleDownload = async (extension: 'png' | 'svg' = 'png') => {
try {
const QRCodeStyling = (await import('qr-code-styling')).default;
const qrCode = new QRCodeStyling(qrOptions);
qrCode.download({ name: qrName || 'QR-Code', extension: 'png' });
qrCode.download({ name: qrName || 'QR-Code', extension });
} catch (err) {
console.error('Download failed', err);
alert('Failed to download QR code');
@@ -101,13 +101,20 @@ export default function CreateQRPage() {
<p className="text-gray-500 mt-1">Design a beautiful, dynamic QR code</p>
</div>
<div className="flex gap-3">
<button
onClick={handleDownload}
className="bg-white border border-gray-300 text-gray-700 px-6 py-2.5 rounded-lg hover:bg-gray-50 transition-colors font-semibold shadow-sm flex items-center gap-2"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
Download
</button>
<div className="flex gap-2">
<button
onClick={() => handleDownload('png')}
className="bg-white border border-gray-300 text-gray-700 px-6 py-2.5 rounded-lg hover:bg-gray-50 transition-colors font-semibold shadow-sm"
>
Download PNG
</button>
<button
onClick={() => handleDownload('svg')}
className="bg-white border border-gray-300 text-gray-700 px-6 py-2.5 rounded-lg hover:bg-gray-50 transition-colors font-semibold shadow-sm"
>
Download SVG
</button>
</div>
<button
onClick={handleGenerate}
disabled={isGenerating}
+18 -9
View File
@@ -80,7 +80,7 @@ export default function DashboardPage() {
}
};
const handleDownload = async (qr: QRCode) => {
const handleDownload = async (qr: QRCode, extension: 'png' | 'svg' = 'png') => {
try {
const QRCodeStyling = (await import('qr-code-styling')).default;
const baseUrl = process.env.NEXT_PUBLIC_APP_URL || 'https://qr.houseofwebsites.ai';
@@ -90,7 +90,7 @@ export default function DashboardPage() {
...qr.design_data,
data: shortUrl
});
qrCode.download({ name: qr.name || 'QR-Code', extension: 'png' });
qrCode.download({ name: qr.name || 'QR-Code', extension: extension });
} catch (err) {
console.error('Failed to download QR code', err);
alert('Failed to download QR code');
@@ -187,13 +187,22 @@ export default function DashboardPage() {
</div>
<div className="flex flex-col gap-2 w-full sm:w-48">
<button
onClick={() => handleDownload(qr)}
className="w-full bg-green-500 hover:bg-green-600 text-white font-bold py-2.5 px-4 rounded-lg transition-colors flex justify-center items-center gap-2 shadow-sm border border-transparent"
>
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" /></svg>
Download
</button>
<div className="flex gap-2 w-full">
<button
onClick={() => handleDownload(qr, 'png')}
className="flex-1 bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded-lg transition-colors flex justify-center items-center text-sm shadow-sm"
title="Download PNG"
>
PNG
</button>
<button
onClick={() => handleDownload(qr, 'svg')}
className="flex-1 bg-green-500 hover:bg-green-600 text-white font-bold py-2 px-2 rounded-lg transition-colors flex justify-center items-center text-sm shadow-sm"
title="Download SVG"
>
SVG
</button>
</div>
<div className="flex gap-2">
<button onClick={() => openEditModal(qr)} className="flex-1 bg-white border border-gray-300 hover:bg-gray-50 text-gray-700 font-semibold py-2 px-3 rounded-lg transition-colors text-sm">