Implement academy analytics, billing, and web stories updates

This commit is contained in:
2026-05-26 07:27:29 +02:00
parent 456c3d6bb0
commit 0b33a1b074
177 changed files with 27360 additions and 2685 deletions

View File

@@ -11,6 +11,9 @@ const MONTH_NAMES = [
'July', 'August', 'September', 'October', 'November', 'December',
]
const YEAR_MIN = 1900
const YEAR_MAX = 2105
const DAY_ABBR = ['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su']
function pad(value) {
@@ -40,16 +43,35 @@ function parseDatePart(value) {
return new Date(year, month - 1, day)
}
function splitDateTime(value) {
if (!value) {
return { date: '', time: '' }
function normalizeDateTimeInput(value) {
const raw = String(value || '').trim()
if (!raw) return { date: '', time: '' }
const match = raw.match(/^(\d{4}-\d{2}-\d{2})(?:[ T](\d{2}:\d{2})(?::\d{2})?)?(?:Z|[+-]\d{2}:?\d{2})?$/)
if (match) {
return {
date: match[1],
time: match[2] || '',
}
}
const [date = '', time = ''] = String(value).split('T')
const parsed = new Date(raw)
if (Number.isNaN(parsed.getTime())) {
return { date: raw, time: '' }
}
return {
date,
time: time.slice(0, 5),
date: toISODate(parsed),
time: `${pad(parsed.getHours())}:${pad(parsed.getMinutes())}`,
}
}
function splitDateTime(value) {
const normalized = normalizeDateTimeInput(value)
return {
date: normalized.date,
time: normalized.time.slice(0, 5),
}
}
@@ -416,7 +438,7 @@ export default function DateTimePicker({
className="fixed z-[500] overflow-hidden rounded-2xl border border-white/12 bg-nova-900 shadow-2xl shadow-black/50"
style={{ top: dropPos.top, left: dropPos.left, width: dropPos.width }}
>
<div className="flex items-center justify-between px-3 pt-3">
<div className="flex items-center justify-between gap-2 px-3 pt-3">
<button
type="button"
onClick={prevMonth}
@@ -428,7 +450,32 @@ export default function DateTimePicker({
</svg>
</button>
<span className="text-sm font-semibold text-white">{MONTH_NAMES[viewMonth]} {viewYear}</span>
<div className="flex min-w-0 flex-1 items-center justify-center gap-2">
<span className="whitespace-nowrap text-sm font-semibold text-white">{MONTH_NAMES[viewMonth]}</span>
<div className="flex items-center gap-1 rounded-xl border border-white/10 bg-white/[0.04] px-1 py-1">
<button
type="button"
onClick={() => setViewYear((current) => Math.max(YEAR_MIN, current - 1))}
disabled={viewYear <= YEAR_MIN}
className="flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 transition-all hover:bg-white/8 hover:text-white disabled:cursor-not-allowed disabled:opacity-30"
aria-label="Previous year"
>
<i className="fa-solid fa-minus text-[11px]" aria-hidden="true" />
</button>
<span className="min-w-[72px] px-2 text-center text-sm font-semibold text-white">{viewYear}</span>
<button
type="button"
onClick={() => setViewYear((current) => Math.min(YEAR_MAX, current + 1))}
disabled={viewYear >= YEAR_MAX}
className="flex h-8 w-8 items-center justify-center rounded-lg text-slate-400 transition-all hover:bg-white/8 hover:text-white disabled:cursor-not-allowed disabled:opacity-30"
aria-label="Next year"
>
<i className="fa-solid fa-plus text-[11px]" aria-hidden="true" />
</button>
</div>
</div>
<button
type="button"