Landing page redesign, dashboard improvements & new settings sections #67

Merged
uz1mani merged 60 commits from staging into main 2026-03-22 16:17:38 +00:00
65 changed files with 8411 additions and 1610 deletions
Showing only changes of commit 82a201a043 - Show all commits

View File

@@ -1,6 +1,6 @@
'use client' 'use client'
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useRef, useState } from 'react'
import Image from 'next/image' import Image from 'next/image'
import { useParams, useSearchParams, useRouter } from 'next/navigation' import { useParams, useSearchParams, useRouter } from 'next/navigation'
import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, type DashboardData, type Stats, type DailyStat } from '@/lib/api/stats' import { getPublicDashboard, getPublicStats, getPublicDailyStats, getPublicRealtime, type DashboardData, type Stats, type DailyStat } from '@/lib/api/stats'
@@ -39,6 +39,7 @@ export default function PublicDashboardPage() {
const [loading, setLoading] = useState(true) const [loading, setLoading] = useState(true)
const [data, setData] = useState<DashboardData | null>(null) const [data, setData] = useState<DashboardData | null>(null)
const [password, setPassword] = useState(passwordParam || '') const [password, setPassword] = useState(passwordParam || '')
const [submittedPassword, setSubmittedPassword] = useState(passwordParam || '')
const [isPasswordProtected, setIsPasswordProtected] = useState(false) const [isPasswordProtected, setIsPasswordProtected] = useState(false)
// Captcha State // Captcha State
@@ -92,11 +93,11 @@ export default function PublicDashboardPage() {
const loadRealtime = useCallback(async () => { const loadRealtime = useCallback(async () => {
try { try {
const auth = { const auth = {
password, password: passwordRef.current,
captcha: { captcha: {
captcha_id: captchaId, captcha_id: captchaIdRef.current,
captcha_solution: captchaSolution, captcha_solution: captchaSolutionRef.current,
captcha_token: captchaToken captcha_token: captchaTokenRef.current
} }
} }
const realtimeData = await getPublicRealtime(siteId, auth) const realtimeData = await getPublicRealtime(siteId, auth)
@@ -109,19 +110,30 @@ export default function PublicDashboardPage() {
} catch (error) { } catch (error) {
// Silently fail for realtime updates // Silently fail for realtime updates
} }
}, [siteId, password, captchaId, captchaSolution, captchaToken, data]) }, [siteId, data])
// Use refs for auth values so loadDashboard doesn't recreate on every keystroke/captcha change
const passwordRef = useRef(submittedPassword)
const captchaIdRef = useRef(captchaId)
const captchaSolutionRef = useRef(captchaSolution)
const captchaTokenRef = useRef(captchaToken)
passwordRef.current = submittedPassword
captchaIdRef.current = captchaId
captchaSolutionRef.current = captchaSolution
captchaTokenRef.current = captchaToken
const loadDashboard = useCallback(async (silent = false) => { const loadDashboard = useCallback(async (silent = false) => {
try { try {
if (!silent) setLoading(true) if (!silent) setLoading(true)
const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval const interval = dateRange.start === dateRange.end ? todayInterval : multiDayInterval
const pw = passwordRef.current
const auth = { const auth = {
password, password: pw,
captcha: { captcha: {
captcha_id: captchaId, captcha_id: captchaIdRef.current,
captcha_solution: captchaSolution, captcha_solution: captchaSolutionRef.current,
captcha_token: captchaToken captcha_token: captchaTokenRef.current
} }
} }
@@ -132,7 +144,7 @@ export default function PublicDashboardPage() {
dateRange.end, dateRange.end,
10, 10,
interval, interval,
password, pw,
auth.captcha auth.captcha
), ),
(async () => { (async () => {
@@ -144,7 +156,7 @@ export default function PublicDashboardPage() {
return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth) return getPublicDailyStats(siteId, prevRange.start, prevRange.end, interval, auth)
})() })()
]) ])
setData(dashboardData) setData(dashboardData)
setPrevStats(prevStatsData) setPrevStats(prevStatsData)
setPrevDailyStats(prevDailyStatsData) setPrevDailyStats(prevDailyStatsData)
@@ -159,7 +171,7 @@ export default function PublicDashboardPage() {
const apiErr = error instanceof ApiError ? error : null const apiErr = error instanceof ApiError ? error : null
if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) { if (apiErr?.status === 401 && (apiErr.data as Record<string, unknown>)?.is_protected) {
setIsPasswordProtected(true) setIsPasswordProtected(true)
if (password) { if (passwordRef.current) {
toast.error('Invalid password or captcha') toast.error('Invalid password or captcha')
// Reset captcha on failure // Reset captcha on failure
setCaptchaId('') setCaptchaId('')
@@ -174,7 +186,7 @@ export default function PublicDashboardPage() {
} finally { } finally {
if (!silent) setLoading(false) if (!silent) setLoading(false)
} }
}, [siteId, dateRange, todayInterval, multiDayInterval, password, captchaId, captchaSolution, captchaToken]) }, [siteId, dateRange, todayInterval, multiDayInterval])
// * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds // * Auto-refresh interval: chart, KPIs, and realtime count update every 30 seconds
useEffect(() => { useEffect(() => {
@@ -185,7 +197,7 @@ export default function PublicDashboardPage() {
}, 30000) }, 30000)
return () => clearInterval(interval) return () => clearInterval(interval)
} }
}, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, password, loadDashboard, loadRealtime]) }, [data, isPasswordProtected, dateRange, todayInterval, multiDayInterval, loadDashboard, loadRealtime])
useEffect(() => { useEffect(() => {
loadDashboard() loadDashboard()
@@ -193,6 +205,9 @@ export default function PublicDashboardPage() {
const handlePasswordSubmit = (e: React.FormEvent) => { const handlePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault() e.preventDefault()
// Update ref immediately so loadDashboard reads the latest password
passwordRef.current = password
setSubmittedPassword(password)
loadDashboard() loadDashboard()
} }