feat: add This week / This month period options and fix comparison labels
This commit is contained in:
@@ -8,6 +8,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
||||
|
||||
### Improved
|
||||
|
||||
- **New date range options.** The period selector now includes "This week" (Monday to today) and "This month" (1st to today) alongside the existing rolling windows. Your selection is remembered between sessions.
|
||||
- **Smarter comparison labels.** The "vs …" label under each stat now matches the period you're viewing — "vs yesterday" for today, "vs last week" for this week, "vs last month" for this month, and "vs previous N days" for rolling windows.
|
||||
- **Refreshed stat headers.** The Unique Visitors, Total Pageviews, Bounce Rate, and Visit Duration stats at the top of the chart have a new look — uppercase labels, the percentage change shown inline next to the number, and an orange underline on whichever metric you're currently graphing.
|
||||
- **Consistent green and red colors.** The up/down percentage indicators now use the same green and red as the rest of the app, instead of slightly different shades.
|
||||
- **Scroll Depth is now a radar chart.** The Scroll Depth panel has been redesigned from a bar chart into a radar chart. The four scroll milestones (25%, 50%, 75%, 100%) are plotted as axes, with the filled shape showing how far visitors are getting through your pages at a glance.
|
||||
- **Polished Goals & Events panel.** The Goals & Events block on your dashboard got a visual refresh to match the style of the Pages, Referrers, and Locations panels. Events are now ranked with a number on the left, counts are shown in a consistent style, and hovering any row reveals what percentage of total events that action accounts for — sliding in smoothly from the right.
|
||||
- **Smarter bot protection.** The security checks on shared dashboard access and organization settings now use action-specific tokens tied to each page. A token earned on one page can't be reused on another, making it harder for automated tools to bypass the captcha.
|
||||
|
||||
@@ -64,6 +64,20 @@ function loadSavedSettings(): {
|
||||
}
|
||||
}
|
||||
|
||||
function getThisWeekRange(): { start: string; end: string } {
|
||||
const today = new Date()
|
||||
const dayOfWeek = today.getDay()
|
||||
const monday = new Date(today)
|
||||
monday.setDate(today.getDate() - (dayOfWeek === 0 ? 6 : dayOfWeek - 1))
|
||||
return { start: formatDate(monday), end: formatDate(today) }
|
||||
}
|
||||
|
||||
function getThisMonthRange(): { start: string; end: string } {
|
||||
const today = new Date()
|
||||
const firstOfMonth = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||
return { start: formatDate(firstOfMonth), end: formatDate(today) }
|
||||
}
|
||||
|
||||
function getInitialDateRange(): { start: string; end: string } {
|
||||
const settings = loadSavedSettings()
|
||||
if (settings?.type === 'today') {
|
||||
@@ -71,10 +85,16 @@ function getInitialDateRange(): { start: string; end: string } {
|
||||
return { start: today, end: today }
|
||||
}
|
||||
if (settings?.type === '7') return getDateRange(7)
|
||||
if (settings?.type === 'week') return getThisWeekRange()
|
||||
if (settings?.type === 'month') return getThisMonthRange()
|
||||
if (settings?.type === 'custom' && settings.dateRange) return settings.dateRange
|
||||
return getDateRange(30)
|
||||
}
|
||||
|
||||
function getInitialPeriod(): string {
|
||||
return loadSavedSettings()?.type || '30'
|
||||
}
|
||||
|
||||
export default function SiteDashboardPage() {
|
||||
|
||||
|
||||
@@ -84,6 +104,7 @@ export default function SiteDashboardPage() {
|
||||
const siteId = params.id as string
|
||||
|
||||
// UI state - initialized from localStorage synchronously to avoid double-fetch
|
||||
const [period, setPeriod] = useState(getInitialPeriod)
|
||||
const [dateRange, setDateRange] = useState(getInitialDateRange)
|
||||
const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>(
|
||||
() => loadSavedSettings()?.todayInterval || 'hour'
|
||||
@@ -457,40 +478,44 @@ export default function SiteDashboardPage() {
|
||||
<Select
|
||||
variant="input"
|
||||
className="min-w-[140px]"
|
||||
value={
|
||||
dateRange.start === formatDate(new Date()) && dateRange.end === formatDate(new Date())
|
||||
? 'today'
|
||||
: dateRange.start === getDateRange(7).start
|
||||
? '7'
|
||||
: dateRange.start === getDateRange(30).start
|
||||
? '30'
|
||||
: 'custom'
|
||||
}
|
||||
value={period}
|
||||
onChange={(value) => {
|
||||
if (value === '7') {
|
||||
const range = getDateRange(7)
|
||||
setDateRange(range)
|
||||
saveSettings('7', range)
|
||||
}
|
||||
else if (value === '30') {
|
||||
const range = getDateRange(30)
|
||||
setDateRange(range)
|
||||
saveSettings('30', range)
|
||||
}
|
||||
else if (value === 'today') {
|
||||
if (value === 'today') {
|
||||
const today = formatDate(new Date())
|
||||
const range = { start: today, end: today }
|
||||
setDateRange(range)
|
||||
setPeriod('today')
|
||||
saveSettings('today', range)
|
||||
}
|
||||
else if (value === 'custom') {
|
||||
} else if (value === '7') {
|
||||
const range = getDateRange(7)
|
||||
setDateRange(range)
|
||||
setPeriod('7')
|
||||
saveSettings('7', range)
|
||||
} else if (value === 'week') {
|
||||
const range = getThisWeekRange()
|
||||
setDateRange(range)
|
||||
setPeriod('week')
|
||||
saveSettings('week', range)
|
||||
} else if (value === '30') {
|
||||
const range = getDateRange(30)
|
||||
setDateRange(range)
|
||||
setPeriod('30')
|
||||
saveSettings('30', range)
|
||||
} else if (value === 'month') {
|
||||
const range = getThisMonthRange()
|
||||
setDateRange(range)
|
||||
setPeriod('month')
|
||||
saveSettings('month', range)
|
||||
} else if (value === 'custom') {
|
||||
setIsDatePickerOpen(true)
|
||||
}
|
||||
}}
|
||||
options={[
|
||||
{ value: 'today', label: 'Today' },
|
||||
{ value: '7', label: 'Last 7 days' },
|
||||
{ value: 'week', label: 'This week' },
|
||||
{ value: '30', label: 'Last 30 days' },
|
||||
{ value: 'month', label: 'This month' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
]}
|
||||
/>
|
||||
@@ -514,6 +539,7 @@ export default function SiteDashboardPage() {
|
||||
prevStats={prevStats}
|
||||
interval={dateRange.start === dateRange.end ? todayInterval : multiDayInterval}
|
||||
dateRange={dateRange}
|
||||
period={period}
|
||||
todayInterval={todayInterval}
|
||||
setTodayInterval={setTodayInterval}
|
||||
multiDayInterval={multiDayInterval}
|
||||
@@ -613,6 +639,7 @@ export default function SiteDashboardPage() {
|
||||
onClose={() => setIsDatePickerOpen(false)}
|
||||
onApply={(range) => {
|
||||
setDateRange(range)
|
||||
setPeriod('custom')
|
||||
saveSettings('custom', range)
|
||||
setIsDatePickerOpen(false)
|
||||
}}
|
||||
|
||||
@@ -64,6 +64,7 @@ interface ChartProps {
|
||||
prevStats?: Stats
|
||||
interval: 'minute' | 'hour' | 'day' | 'month'
|
||||
dateRange: { start: string, end: string }
|
||||
period?: string
|
||||
todayInterval: 'minute' | 'hour'
|
||||
setTodayInterval: (interval: 'minute' | 'hour') => void
|
||||
multiDayInterval: 'hour' | 'day'
|
||||
@@ -145,6 +146,7 @@ export default function Chart({
|
||||
prevStats,
|
||||
interval,
|
||||
dateRange,
|
||||
period,
|
||||
todayInterval,
|
||||
setTodayInterval,
|
||||
multiDayInterval,
|
||||
@@ -358,10 +360,17 @@ export default function Chart({
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">{(() => {
|
||||
const days = Math.round((new Date(dateRange.end).getTime() - new Date(dateRange.start).getTime()) / 86400000)
|
||||
return days === 0 ? 'vs yesterday' : `vs previous ${days} days`
|
||||
})()}</div>
|
||||
<div className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">{
|
||||
period === 'today' ? 'vs yesterday'
|
||||
: period === 'week' ? 'vs last week'
|
||||
: period === 'month' ? 'vs last month'
|
||||
: period === '7' ? 'vs previous 7 days'
|
||||
: period === '30' ? 'vs previous 30 days'
|
||||
: (() => {
|
||||
const days = Math.round((new Date(dateRange.end).getTime() - new Date(dateRange.start).getTime()) / 86400000)
|
||||
return days === 0 ? 'vs yesterday' : `vs previous ${days} days`
|
||||
})()
|
||||
}</div>
|
||||
{metric === m.key && (
|
||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-orange" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user