From 9aaf94625afc9893c74ac8a0157502408360276f Mon Sep 17 00:00:00 2001 From: Mohan Ki Date: Mon, 27 Jul 2026 17:00:54 +0530 Subject: [PATCH] feat: add SVG/PNG download options, track geolocation from CF headers, and fix analytics pie charts by parsing counts as integers --- api/routes/analytics.js | 21 +++++-- api/routes/redirect.js | 9 +-- api/test.js | 57 ++++++++++++++++++ client/src/app/dashboard/analytics/page.tsx | 62 ++++++++++++++++---- client/src/app/dashboard/create/page.tsx | 25 +++++--- client/src/app/dashboard/page.tsx | 27 ++++++--- logs.txt | Bin 0 -> 65734 bytes users.json | Bin 0 -> 830 bytes 8 files changed, 163 insertions(+), 38 deletions(-) create mode 100644 api/test.js create mode 100644 logs.txt create mode 100644 users.json diff --git a/api/routes/analytics.js b/api/routes/analytics.js index 9133d2a..b0eae7f 100644 --- a/api/routes/analytics.js +++ b/api/routes/analytics.js @@ -22,8 +22,8 @@ router.get('/:qrCodeId', verifyToken, async (req, res) => { return res.status(403).json({ error: 'Unauthorized or QR code not found' }); } - // Get total scans - const totalQuery = await pgPool.query('SELECT COUNT(*) as total FROM qr_scans WHERE qr_code_id = $1', [qrCodeId]); + // Get total scans and unique scanners + const totalQuery = await pgPool.query('SELECT COUNT(*) as total, COUNT(DISTINCT ip_address) as unique_scanners FROM qr_scans WHERE qr_code_id = $1', [qrCodeId]); // Get OS breakdown const osQuery = await pgPool.query( @@ -37,6 +37,12 @@ router.get('/:qrCodeId', verifyToken, async (req, res) => { [qrCodeId] ); + // Get Country breakdown + const countryQuery = await pgPool.query( + 'SELECT country, COUNT(*) as count FROM qr_scans WHERE qr_code_id = $1 GROUP BY country ORDER BY count DESC', + [qrCodeId] + ); + // Get Scans over time (last 7 days grouped by date) const timeQuery = await pgPool.query( `SELECT DATE(scanned_at) as date, COUNT(*) as count @@ -46,11 +52,14 @@ router.get('/:qrCodeId', verifyToken, async (req, res) => { [qrCodeId] ); + // Parse string counts from PG back to integers for Recharts res.json({ - totalScans: parseInt(totalQuery.rows[0].total), - osBreakdown: osQuery.rows, - deviceBreakdown: deviceQuery.rows, - timeline: timeQuery.rows + totalScans: parseInt(totalQuery.rows[0].total) || 0, + uniqueScanners: parseInt(totalQuery.rows[0].unique_scanners) || 0, + osBreakdown: osQuery.rows.map(r => ({ ...r, count: parseInt(r.count) })), + deviceBreakdown: deviceQuery.rows.map(r => ({ ...r, count: parseInt(r.count) })), + countryBreakdown: countryQuery.rows.map(r => ({ country: r.country || 'Unknown', count: parseInt(r.count) })), + timeline: timeQuery.rows.map(r => ({ ...r, count: parseInt(r.count) })) }); } catch (err) { console.error(err); diff --git a/api/routes/redirect.js b/api/routes/redirect.js index ecfbdc7..e254ab7 100644 --- a/api/routes/redirect.js +++ b/api/routes/redirect.js @@ -53,12 +53,13 @@ router.get('/:shortUrlId', async (req, res) => { const parser = new UAParser(req.headers['user-agent']); const os = parser.getOS().name || 'Unknown'; const deviceType = parser.getDevice().type || 'desktop'; - const ip = req.ip || req.connection.remoteAddress; + const ip = req.headers['x-forwarded-for'] || req.connection.remoteAddress; + const country = req.headers['cf-ipcountry'] || 'Unknown'; pgPool.query( - `INSERT INTO qr_scans (qr_code_id, ip_address, user_agent, os, device_type) - VALUES ($1, $2, $3, $4, $5)`, - [qrData.qr_id, ip, req.headers['user-agent'], os, deviceType] + `INSERT INTO qr_scans (qr_code_id, ip_address, user_agent, os, device_type, country) + VALUES ($1, $2, $3, $4, $5, $6)`, + [qrData.qr_id, ip, req.headers['user-agent'], os, deviceType, country] ).catch(err => console.error('Failed to log scan:', err)); // 4. Check Tier & Redirect diff --git a/api/test.js b/api/test.js new file mode 100644 index 0000000..736af28 --- /dev/null +++ b/api/test.js @@ -0,0 +1,57 @@ +const http = require('http'); + +const options = { + hostname: 'localhost', + port: 4001, + path: '/api/auth/login', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, +}; + +const req = http.request(options, (res) => { + let data = ''; + res.on('data', (chunk) => data += chunk); + res.on('end', () => { + if (!res.headers['set-cookie']) { + console.log('No cookie', data); return; + } + const cookie = res.headers['set-cookie'][0]; + + // Generate QR + const largeStr = 'a'.repeat(200000); + const postData = JSON.stringify({ + name: 'Test QR', + type: 'dynamic', + dataType: 'URL', + destinationUrl: 'https://google.com', + designData: { image: 'data:image/png;base64,' + largeStr } + }); + + const options2 = { + hostname: 'localhost', + port: 4001, + path: '/api/qr/generate', + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Content-Length': Buffer.byteLength(postData), + 'Cookie': cookie + } + }; + + const req2 = http.request(options2, (res2) => { + let d = ''; + res2.on('data', c => d+=c); + res2.on('end', () => { + console.log('Status:', res2.statusCode); + console.log('Body:', d); + }); + }); + req2.write(postData); + req2.end(); + }); +}); +req.write(JSON.stringify({email: 'admin@ckqr.com', password: 'password123'})); +req.end(); diff --git a/client/src/app/dashboard/analytics/page.tsx b/client/src/app/dashboard/analytics/page.tsx index 1851478..0fe2c97 100644 --- a/client/src/app/dashboard/analytics/page.tsx +++ b/client/src/app/dashboard/analytics/page.tsx @@ -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() {
{analytics.totalScans.toLocaleString()}
- {/* Adding placeholders for conversion rate / unique scanners since backend doesn't track unique yet */} -
+

Unique Scanners

-
--
-

Available in Pro

+
{analytics.uniqueScanners.toLocaleString()}
+

Distinct IP Addresses

-
+

Top Location

-
Unknown
-

Available in Pro

+
+ {analytics.countryBreakdown?.length > 0 ? analytics.countryBreakdown[0].country : 'Unknown'} +
+

By Country

@@ -243,6 +246,45 @@ export default function AnalyticsPage() {
+ {/* Country Chart */} +
+

Locations (Country)

+
+ {analytics.countryBreakdown?.length > 0 ? ( + + + + {analytics.countryBreakdown.map((entry, index) => ( + + ))} + + + + + ) : ( + No location data + )} +
+ {/* Legend */} +
+ {analytics.countryBreakdown?.map((entry, index) => ( +
+ + {entry.country || 'Unknown'} ({entry.count}) +
+ ))} +
+
+ )} diff --git a/client/src/app/dashboard/create/page.tsx b/client/src/app/dashboard/create/page.tsx index 3d720d2..5c253b3 100644 --- a/client/src/app/dashboard/create/page.tsx +++ b/client/src/app/dashboard/create/page.tsx @@ -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() {

Design a beautiful, dynamic QR code

- +
+ + +
- +
+ + +