feat(analytics): replace native date select with custom UI component
This commit is contained in:
@@ -7,6 +7,7 @@ import { getStats, getRealtime, getDailyStats, getTopPages, getTopReferrers, get
|
||||
import { formatNumber, formatDuration, getDateRange } from '@/lib/utils/format'
|
||||
import { toast } from 'sonner'
|
||||
import LoadingOverlay from '@/components/LoadingOverlay'
|
||||
import Select from '@/components/ui/Select'
|
||||
import ContentStats from '@/components/dashboard/ContentStats'
|
||||
import TopReferrers from '@/components/dashboard/TopReferrers'
|
||||
import Locations from '@/components/dashboard/Locations'
|
||||
@@ -161,7 +162,7 @@ export default function SiteDashboardPage() {
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<select
|
||||
<Select
|
||||
value={
|
||||
dateRange.start === new Date().toISOString().split('T')[0] && dateRange.end === new Date().toISOString().split('T')[0]
|
||||
? 'today'
|
||||
@@ -171,21 +172,21 @@ export default function SiteDashboardPage() {
|
||||
? '30'
|
||||
: 'custom'
|
||||
}
|
||||
onChange={(e) => {
|
||||
if (e.target.value === '7') setDateRange(getDateRange(7))
|
||||
else if (e.target.value === '30') setDateRange(getDateRange(30))
|
||||
else if (e.target.value === 'today') {
|
||||
onChange={(value) => {
|
||||
if (value === '7') setDateRange(getDateRange(7))
|
||||
else if (value === '30') setDateRange(getDateRange(30))
|
||||
else if (value === 'today') {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
setDateRange({ start: today, end: today })
|
||||
}
|
||||
}}
|
||||
className="btn-secondary text-sm"
|
||||
>
|
||||
<option value="today">Today</option>
|
||||
<option value="7">Last 7 days</option>
|
||||
<option value="30">Last 30 days</option>
|
||||
<option value="custom">Custom</option>
|
||||
</select>
|
||||
options={[
|
||||
{ value: 'today', label: 'Today' },
|
||||
{ value: '7', label: 'Last 7 days' },
|
||||
{ value: '30', label: 'Last 30 days' },
|
||||
{ value: 'custom', label: 'Custom' },
|
||||
]}
|
||||
/>
|
||||
<button
|
||||
onClick={() => router.push(`/sites/${siteId}/settings`)}
|
||||
className="btn-secondary text-sm"
|
||||
|
||||
79
components/ui/Select.tsx
Normal file
79
components/ui/Select.tsx
Normal file
@@ -0,0 +1,79 @@
|
||||
'use client'
|
||||
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
|
||||
interface Option {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
interface SelectProps {
|
||||
value: string
|
||||
onChange: (value: string) => void
|
||||
options: Option[]
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function Select({ value, onChange, options, className = '' }: SelectProps) {
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const ref = useRef<HTMLDivElement>(null)
|
||||
|
||||
useEffect(() => {
|
||||
function handleClickOutside(event: MouseEvent) {
|
||||
if (ref.current && !ref.current.contains(event.target as Node)) {
|
||||
setIsOpen(false)
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [])
|
||||
|
||||
const selectedOption = options.find(o => o.value === value) || options.find(o => o.value === 'custom')
|
||||
|
||||
return (
|
||||
<div className={`relative ${className}`} ref={ref}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="btn-secondary min-w-[140px] flex items-center justify-between gap-2"
|
||||
>
|
||||
<span>{selectedOption?.label || value}</span>
|
||||
<svg
|
||||
className={`w-4 h-4 text-neutral-500 transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`}
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
{isOpen && (
|
||||
<div className="absolute right-0 mt-2 w-full min-w-[140px] bg-white dark:bg-neutral-900 border border-neutral-200 dark:border-neutral-800 rounded-xl shadow-lg z-50 overflow-hidden py-1">
|
||||
{options.map((option) => (
|
||||
<button
|
||||
key={option.value}
|
||||
onClick={() => {
|
||||
onChange(option.value)
|
||||
setIsOpen(false)
|
||||
}}
|
||||
className={`w-full text-left px-4 py-2.5 text-sm transition-colors flex items-center justify-between
|
||||
${value === option.value
|
||||
? 'bg-neutral-50 dark:bg-neutral-800 text-brand-orange font-medium'
|
||||
: 'text-neutral-700 dark:text-neutral-300 hover:bg-neutral-50 dark:hover:bg-neutral-800'
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.label}
|
||||
{value === option.value && (
|
||||
<svg className="w-4 h-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user