Frontend update
This commit is contained in:
66
client/src/lib/format.js
Normal file
66
client/src/lib/format.js
Normal file
@@ -0,0 +1,66 @@
|
||||
// Date + label helpers shared across pages.
|
||||
|
||||
const MONTHS = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December',
|
||||
]
|
||||
|
||||
function parse(value) {
|
||||
if (!value) return null
|
||||
const d = new Date(value)
|
||||
return isNaN(d.getTime()) ? null : d
|
||||
}
|
||||
|
||||
// "June 24, 2026"
|
||||
export function longDate(value) {
|
||||
const d = parse(value)
|
||||
if (!d) return ''
|
||||
return `${MONTHS[d.getMonth()]} ${d.getDate()}, ${d.getFullYear()}`
|
||||
}
|
||||
|
||||
// "Jun 24"
|
||||
export function shortDate(value) {
|
||||
const d = parse(value)
|
||||
if (!d) return ''
|
||||
return `${MONTHS[d.getMonth()].slice(0, 3)} ${d.getDate()}`
|
||||
}
|
||||
|
||||
// "Jun 26 18:55"
|
||||
export function dateTime(value) {
|
||||
const d = parse(value)
|
||||
if (!d) return ''
|
||||
const hh = String(d.getHours()).padStart(2, '0')
|
||||
const mm = String(d.getMinutes()).padStart(2, '0')
|
||||
return `${MONTHS[d.getMonth()].slice(0, 3)} ${d.getDate()} ${hh}:${mm}`
|
||||
}
|
||||
|
||||
// { mon: 'JUN', num: "'26" } for the newsletter month tile
|
||||
export function monthTile(value) {
|
||||
const d = parse(value)
|
||||
if (!d) return { mon: '—', num: '' }
|
||||
return { mon: MONTHS[d.getMonth()].slice(0, 3).toUpperCase(), num: `'${String(d.getFullYear()).slice(2)}` }
|
||||
}
|
||||
|
||||
// Compact relative time: "5m ago", "2h ago", "1d ago"
|
||||
export function ago(value) {
|
||||
const d = parse(value)
|
||||
if (!d) return ''
|
||||
const secs = Math.max(1, Math.floor((Date.now() - d.getTime()) / 1000))
|
||||
if (secs < 60) return `${secs}s ago`
|
||||
const mins = Math.floor(secs / 60)
|
||||
if (mins < 60) return `${mins}m ago`
|
||||
const hrs = Math.floor(mins / 60)
|
||||
if (hrs < 24) return `${hrs}h ago`
|
||||
const days = Math.floor(hrs / 24)
|
||||
return `${days}d ago`
|
||||
}
|
||||
|
||||
const CATEGORY_LABELS = {
|
||||
news: 'News',
|
||||
five_on_friday: 'Five on Friday',
|
||||
newsletter: 'Newsletter',
|
||||
screenshot: 'Screenshot',
|
||||
}
|
||||
export function categoryLabel(dbCategory) {
|
||||
return CATEGORY_LABELS[dbCategory] || dbCategory
|
||||
}
|
||||
20
client/src/lib/useAsync.js
Normal file
20
client/src/lib/useAsync.js
Normal file
@@ -0,0 +1,20 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
// Minimal data-fetching hook: runs `fn` on mount / when deps change.
|
||||
export function useAsync(fn, deps = []) {
|
||||
const [state, setState] = useState({ loading: true, error: null, data: null })
|
||||
|
||||
useEffect(() => {
|
||||
let active = true
|
||||
setState({ loading: true, error: null, data: null })
|
||||
fn()
|
||||
.then((data) => active && setState({ loading: false, error: null, data }))
|
||||
.catch((error) => active && setState({ loading: false, error, data: null }))
|
||||
return () => {
|
||||
active = false
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, deps)
|
||||
|
||||
return state
|
||||
}
|
||||
Reference in New Issue
Block a user