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
|
### 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.
|
- **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.
|
- **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.
|
- **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 } {
|
function getInitialDateRange(): { start: string; end: string } {
|
||||||
const settings = loadSavedSettings()
|
const settings = loadSavedSettings()
|
||||||
if (settings?.type === 'today') {
|
if (settings?.type === 'today') {
|
||||||
@@ -71,10 +85,16 @@ function getInitialDateRange(): { start: string; end: string } {
|
|||||||
return { start: today, end: today }
|
return { start: today, end: today }
|
||||||
}
|
}
|
||||||
if (settings?.type === '7') return getDateRange(7)
|
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
|
if (settings?.type === 'custom' && settings.dateRange) return settings.dateRange
|
||||||
return getDateRange(30)
|
return getDateRange(30)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getInitialPeriod(): string {
|
||||||
|
return loadSavedSettings()?.type || '30'
|
||||||
|
}
|
||||||
|
|
||||||
export default function SiteDashboardPage() {
|
export default function SiteDashboardPage() {
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +104,7 @@ export default function SiteDashboardPage() {
|
|||||||
const siteId = params.id as string
|
const siteId = params.id as string
|
||||||
|
|
||||||
// UI state - initialized from localStorage synchronously to avoid double-fetch
|
// UI state - initialized from localStorage synchronously to avoid double-fetch
|
||||||
|
const [period, setPeriod] = useState(getInitialPeriod)
|
||||||
const [dateRange, setDateRange] = useState(getInitialDateRange)
|
const [dateRange, setDateRange] = useState(getInitialDateRange)
|
||||||
const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>(
|
const [todayInterval, setTodayInterval] = useState<'minute' | 'hour'>(
|
||||||
() => loadSavedSettings()?.todayInterval || 'hour'
|
() => loadSavedSettings()?.todayInterval || 'hour'
|
||||||
@@ -457,40 +478,44 @@ export default function SiteDashboardPage() {
|
|||||||
<Select
|
<Select
|
||||||
variant="input"
|
variant="input"
|
||||||
className="min-w-[140px]"
|
className="min-w-[140px]"
|
||||||
value={
|
value={period}
|
||||||
dateRange.start === formatDate(new Date()) && dateRange.end === formatDate(new Date())
|
|
||||||
? 'today'
|
|
||||||
: dateRange.start === getDateRange(7).start
|
|
||||||
? '7'
|
|
||||||
: dateRange.start === getDateRange(30).start
|
|
||||||
? '30'
|
|
||||||
: 'custom'
|
|
||||||
}
|
|
||||||
onChange={(value) => {
|
onChange={(value) => {
|
||||||
if (value === '7') {
|
if (value === 'today') {
|
||||||
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') {
|
|
||||||
const today = formatDate(new Date())
|
const today = formatDate(new Date())
|
||||||
const range = { start: today, end: today }
|
const range = { start: today, end: today }
|
||||||
setDateRange(range)
|
setDateRange(range)
|
||||||
|
setPeriod('today')
|
||||||
saveSettings('today', range)
|
saveSettings('today', range)
|
||||||
}
|
} else if (value === '7') {
|
||||||
else if (value === 'custom') {
|
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)
|
setIsDatePickerOpen(true)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
options={[
|
options={[
|
||||||
{ value: 'today', label: 'Today' },
|
{ value: 'today', label: 'Today' },
|
||||||
{ value: '7', label: 'Last 7 days' },
|
{ value: '7', label: 'Last 7 days' },
|
||||||
|
{ value: 'week', label: 'This week' },
|
||||||
{ value: '30', label: 'Last 30 days' },
|
{ value: '30', label: 'Last 30 days' },
|
||||||
|
{ value: 'month', label: 'This month' },
|
||||||
{ value: 'custom', label: 'Custom' },
|
{ value: 'custom', label: 'Custom' },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
@@ -514,6 +539,7 @@ export default function SiteDashboardPage() {
|
|||||||
prevStats={prevStats}
|
prevStats={prevStats}
|
||||||
interval={dateRange.start === dateRange.end ? todayInterval : multiDayInterval}
|
interval={dateRange.start === dateRange.end ? todayInterval : multiDayInterval}
|
||||||
dateRange={dateRange}
|
dateRange={dateRange}
|
||||||
|
period={period}
|
||||||
todayInterval={todayInterval}
|
todayInterval={todayInterval}
|
||||||
setTodayInterval={setTodayInterval}
|
setTodayInterval={setTodayInterval}
|
||||||
multiDayInterval={multiDayInterval}
|
multiDayInterval={multiDayInterval}
|
||||||
@@ -613,6 +639,7 @@ export default function SiteDashboardPage() {
|
|||||||
onClose={() => setIsDatePickerOpen(false)}
|
onClose={() => setIsDatePickerOpen(false)}
|
||||||
onApply={(range) => {
|
onApply={(range) => {
|
||||||
setDateRange(range)
|
setDateRange(range)
|
||||||
|
setPeriod('custom')
|
||||||
saveSettings('custom', range)
|
saveSettings('custom', range)
|
||||||
setIsDatePickerOpen(false)
|
setIsDatePickerOpen(false)
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ interface ChartProps {
|
|||||||
prevStats?: Stats
|
prevStats?: Stats
|
||||||
interval: 'minute' | 'hour' | 'day' | 'month'
|
interval: 'minute' | 'hour' | 'day' | 'month'
|
||||||
dateRange: { start: string, end: string }
|
dateRange: { start: string, end: string }
|
||||||
|
period?: string
|
||||||
todayInterval: 'minute' | 'hour'
|
todayInterval: 'minute' | 'hour'
|
||||||
setTodayInterval: (interval: 'minute' | 'hour') => void
|
setTodayInterval: (interval: 'minute' | 'hour') => void
|
||||||
multiDayInterval: 'hour' | 'day'
|
multiDayInterval: 'hour' | 'day'
|
||||||
@@ -145,6 +146,7 @@ export default function Chart({
|
|||||||
prevStats,
|
prevStats,
|
||||||
interval,
|
interval,
|
||||||
dateRange,
|
dateRange,
|
||||||
|
period,
|
||||||
todayInterval,
|
todayInterval,
|
||||||
setTodayInterval,
|
setTodayInterval,
|
||||||
multiDayInterval,
|
multiDayInterval,
|
||||||
@@ -358,10 +360,17 @@ export default function Chart({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-neutral-400 dark:text-neutral-500 mt-1">{(() => {
|
<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)
|
period === 'today' ? 'vs yesterday'
|
||||||
return days === 0 ? 'vs yesterday' : `vs previous ${days} days`
|
: period === 'week' ? 'vs last week'
|
||||||
})()}</div>
|
: 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 && (
|
{metric === m.key && (
|
||||||
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-orange" />
|
<div className="absolute bottom-0 left-0 right-0 h-0.5 bg-brand-orange" />
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user