Migrate extension Create page to Nextjs
This commit is contained in:
parent
d2bee83db4
commit
2d9b1d7959
24 changed files with 214 additions and 391 deletions
|
|
@ -20,7 +20,7 @@ interface MonthProps {
|
|||
const Month = ({ value, onChange }: MonthProps) => {
|
||||
const { t, i18n } = useTranslation('home')
|
||||
|
||||
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 1
|
||||
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 0
|
||||
|
||||
const [page, setPage] = useState<Temporal.PlainYearMonth>(Temporal.Now.plainDateISO().toPlainYearMonth())
|
||||
const dates = useMemo(() => calculateMonth(page, weekStart), [page, weekStart])
|
||||
|
|
@ -61,7 +61,7 @@ const Month = ({ value, onChange }: MonthProps) => {
|
|||
</div>
|
||||
|
||||
<div className={styles.dayLabels}>
|
||||
{(rotateArray(getWeekdayNames(i18n.language, 'short'), weekStart)).map(name =>
|
||||
{(rotateArray(getWeekdayNames(i18n.language, 'short'), weekStart ? 0 : 1)).map(name =>
|
||||
<label key={name}>{name}</label>
|
||||
)}
|
||||
</div>
|
||||
|
|
@ -119,8 +119,8 @@ export default Month
|
|||
|
||||
/** Calculate the dates to show for the month in a 2d array */
|
||||
const calculateMonth = (month: Temporal.PlainYearMonth, weekStart: 0 | 1) => {
|
||||
const daysBefore = month.toPlainDate({ day: 1 }).dayOfWeek - (weekStart ? 0 : 1)
|
||||
const daysAfter = 6 - month.toPlainDate({ day: month.daysInMonth }).dayOfWeek + (weekStart ? 0 : 1)
|
||||
const daysBefore = month.toPlainDate({ day: 1 }).dayOfWeek - weekStart
|
||||
const daysAfter = 6 - month.toPlainDate({ day: month.daysInMonth }).dayOfWeek + weekStart
|
||||
|
||||
const dates: Temporal.PlainDate[][] = []
|
||||
let curDate = month.toPlainDate({ day: 1 }).subtract({ days: daysBefore })
|
||||
|
|
|
|||
|
|
@ -19,9 +19,9 @@ interface WeekdaysProps {
|
|||
const Weekdays = ({ value, onChange }: WeekdaysProps) => {
|
||||
const { t, i18n } = useTranslation('home')
|
||||
|
||||
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 1
|
||||
const weekStart = useStore(useSettingsStore, state => state.weekStart) ?? 0
|
||||
|
||||
const weekdays = useMemo(() => rotateArray(range(1, 7).map(i => Temporal.Now.plainDateISO().add({ days: i - Temporal.Now.plainDateISO().dayOfWeek })), weekStart), [weekStart])
|
||||
const weekdays = useMemo(() => rotateArray(range(1, 7).map(i => Temporal.Now.plainDateISO().add({ days: i - Temporal.Now.plainDateISO().dayOfWeek })), weekStart ? 0 : 1), [weekStart])
|
||||
|
||||
// Ref and state required to rerender but also access static version in callbacks
|
||||
const selectingRef = useRef<string[]>([])
|
||||
|
|
|
|||
|
|
@ -9,3 +9,8 @@
|
|||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.slim {
|
||||
margin-block: 10px;
|
||||
max-width: calc(100% - 30px);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,9 +5,17 @@ import styles from './Content.module.scss'
|
|||
interface ContentProps {
|
||||
children: React.ReactNode
|
||||
isCentered?: boolean
|
||||
isSlim?: boolean
|
||||
}
|
||||
|
||||
const Content = ({ isCentered, ...props }: ContentProps) =>
|
||||
<div className={makeClass(styles.content, isCentered && styles.centered)} {...props} />
|
||||
const Content = ({ isCentered, isSlim, ...props }: ContentProps) =>
|
||||
<div
|
||||
className={makeClass(
|
||||
styles.content,
|
||||
isCentered && styles.centered,
|
||||
isSlim && styles.slim,
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
|
||||
export default Content
|
||||
|
|
|
|||
|
|
@ -1,7 +1,3 @@
|
|||
.form {
|
||||
margin: 0 0 60px;
|
||||
}
|
||||
|
||||
.buttonWrapper {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
import { useState } from 'react'
|
||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { range } from '@giraugh/tools'
|
||||
import { Temporal } from '@js-temporal/polyfill'
|
||||
|
||||
import Button from '/src/components/Button/Button'
|
||||
|
|
@ -11,14 +12,17 @@ import { default as ErrorAlert } from '/src/components/Error/Error'
|
|||
import SelectField from '/src/components/SelectField/SelectField'
|
||||
import TextField from '/src/components/TextField/TextField'
|
||||
import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField'
|
||||
import { API_BASE } from '/src/config/api'
|
||||
import { API_BASE, EventResponse } from '/src/config/api'
|
||||
import { useTranslation } from '/src/i18n/client'
|
||||
import timezones from '/src/res/timezones.json'
|
||||
import useRecentsStore from '/src/stores/recentsStore'
|
||||
|
||||
import EventInfo from './components/EventInfo/EventInfo'
|
||||
import styles from './CreateForm.module.scss'
|
||||
|
||||
interface Fields {
|
||||
name: string
|
||||
/** As `YYYY-MM-DD` or `d` */
|
||||
dates: string[]
|
||||
time: {
|
||||
start: number
|
||||
|
|
@ -34,10 +38,12 @@ const defaultValues: Fields = {
|
|||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||
}
|
||||
|
||||
const CreateForm = () => {
|
||||
const CreateForm = ({ noRedirect }: { noRedirect?: boolean }) => {
|
||||
const { t } = useTranslation('home')
|
||||
const { push } = useRouter()
|
||||
|
||||
const addRecent = useRecentsStore(state => state.addRecent)
|
||||
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
|
|
@ -45,6 +51,7 @@ const CreateForm = () => {
|
|||
} = useForm({ defaultValues })
|
||||
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
const [createdEvent, setCreatedEvent] = useState<EventResponse>()
|
||||
const [error, setError] = useState<React.ReactNode>()
|
||||
|
||||
const onSubmit: SubmitHandler<Fields> = async values => {
|
||||
|
|
@ -64,36 +71,24 @@ const CreateForm = () => {
|
|||
// If format is `YYYY-MM-DD` or `d`
|
||||
const isSpecificDates = dates[0].length !== 1
|
||||
|
||||
const times = dates.reduce((times, dateStr) => {
|
||||
const day = []
|
||||
const times = dates.flatMap(dateStr => {
|
||||
const date = isSpecificDates
|
||||
? Temporal.PlainDate.from(dateStr)
|
||||
: Temporal.Now.plainDateISO().add({ days: Number(dateStr) - Temporal.Now.plainDateISO().dayOfWeek })
|
||||
|
||||
for (let i = time.start; i < (time.start > time.end ? 24 : time.end); i++) {
|
||||
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour: i }) }).withTimeZone('UTC')
|
||||
const hours = time.start > time.end ? [...range(0, time.end - 1), ...range(time.start, 23)] : range(time.start, time.end - 1)
|
||||
|
||||
return hours.map(hour => {
|
||||
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour }) }).withTimeZone('UTC')
|
||||
if (isSpecificDates) {
|
||||
// Format as `HHmm-DDMMYYYY`
|
||||
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${dateTime.day.toString().padStart(2, '0')}${dateTime.month.toString().padStart(2, '0')}${dateTime.year.toString().padStart(4, '0')}`)
|
||||
return `${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${dateTime.day.toString().padStart(2, '0')}${dateTime.month.toString().padStart(2, '0')}${dateTime.year.toString().padStart(4, '0')}`
|
||||
} else {
|
||||
// Format as `HHmm-d`
|
||||
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${String(dateTime.dayOfWeek === 7 ? 0 : dateTime.dayOfWeek)}`)
|
||||
return `${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${String(dateTime.dayOfWeek === 7 ? 0 : dateTime.dayOfWeek)}`
|
||||
}
|
||||
}
|
||||
if (time.start > time.end) {
|
||||
for (let i = 0; i < time.end; i++) {
|
||||
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour: i }) }).withTimeZone('UTC')
|
||||
if (isSpecificDates) {
|
||||
// Format as `HHmm-DDMMYYYY`
|
||||
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${dateTime.day.toString().padStart(2, '0')}${dateTime.month.toString().padStart(2, '0')}${dateTime.year.toString().padStart(4, '0')}`)
|
||||
} else {
|
||||
// Format as `HHmm-d`
|
||||
day.push(`${dateTime.hour.toString().padStart(2, '0')}${dateTime.minute.toString().padStart(2, '0')}-${String(dateTime.dayOfWeek === 7 ? 0 : dateTime.dayOfWeek)}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
return [...times, ...day]
|
||||
}, [] as string[])
|
||||
})
|
||||
})
|
||||
|
||||
if (times.length === 0) {
|
||||
return setError(t('form.errors.no_time'))
|
||||
|
|
@ -114,10 +109,20 @@ const CreateForm = () => {
|
|||
throw new Error('Failed to create event')
|
||||
}
|
||||
|
||||
const { id } = await res.json()
|
||||
const newEvent = EventResponse.parse(await res.json())
|
||||
|
||||
// Navigate to the new event
|
||||
push(`/${id}`)
|
||||
if (noRedirect) {
|
||||
// Show event link
|
||||
setCreatedEvent(newEvent)
|
||||
addRecent({
|
||||
id: newEvent.id,
|
||||
name: newEvent.name,
|
||||
created_at: newEvent.created_at,
|
||||
})
|
||||
} else {
|
||||
// Navigate to the new event
|
||||
push(`/${newEvent.id}`)
|
||||
}
|
||||
} catch (e) {
|
||||
setError(t('form.errors.unknown'))
|
||||
console.error(e)
|
||||
|
|
@ -126,7 +131,11 @@ const CreateForm = () => {
|
|||
}
|
||||
}
|
||||
|
||||
return <form className={styles.form} onSubmit={handleSubmit(onSubmit)} id="create">
|
||||
return createdEvent ? <EventInfo event={createdEvent} /> : <form
|
||||
style={{ marginBlockEnd: noRedirect ? 30 : 60 }}
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
id="create"
|
||||
>
|
||||
<TextField
|
||||
label={t('form.name.label')}
|
||||
description={t('form.name.sublabel')}
|
||||
|
|
@ -163,6 +172,7 @@ const CreateForm = () => {
|
|||
type="submit"
|
||||
isLoading={isLoading}
|
||||
disabled={isLoading}
|
||||
style={noRedirect ? { width: '100%' } : undefined}
|
||||
>{t('form.button')}</Button>
|
||||
</div>
|
||||
</form>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,19 @@
|
|||
.wrapper {
|
||||
text-align: center;
|
||||
margin: 50px 0 20px;
|
||||
}
|
||||
|
||||
.info {
|
||||
margin: 6px 0;
|
||||
text-align: center;
|
||||
font-size: 15px;
|
||||
padding: 10px 0;
|
||||
}
|
||||
|
||||
.copyable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
color: var(--secondary);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { useState } from 'react'
|
||||
import { Trans } from 'react-i18next/TransWithoutContext'
|
||||
|
||||
import { EventResponse } from '/src/config/api'
|
||||
import { useTranslation } from '/src/i18n/client'
|
||||
import { makeClass } from '/src/utils'
|
||||
|
||||
import styles from './EventInfo.module.scss'
|
||||
|
||||
interface EventInfoProps {
|
||||
event: EventResponse
|
||||
}
|
||||
|
||||
const EventInfo = ({ event }: EventInfoProps) => {
|
||||
const { t, i18n } = useTranslation('event')
|
||||
|
||||
const [copied, setCopied] = useState<React.ReactNode>()
|
||||
|
||||
return <div className={styles.wrapper}>
|
||||
<h2>{event.name}</h2>
|
||||
<p
|
||||
className={makeClass(styles.info, styles.copyable)}
|
||||
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${event.id}`)
|
||||
.then(() => {
|
||||
setCopied(t('nav.copied'))
|
||||
setTimeout(() => setCopied(undefined), 1000)
|
||||
})
|
||||
.catch(e => console.error('Failed to copy', e))
|
||||
}
|
||||
title={navigator.clipboard ? t<string>('nav.title') : undefined}
|
||||
>{copied ?? `https://crab.fit/${event.id}`}</p>
|
||||
<p className={styles.info}>
|
||||
<Trans i18nKey="event:nav.shareinfo_alt" t={t} i18n={i18n}>_<a href={`mailto:?subject=${encodeURIComponent(t<string>('nav.email_subject', { event_name: event.name }))}&body=${encodeURIComponent(`${t('nav.email_body')} https://crab.fit/${event.id}`)}`} target="_blank">_</a>_</Trans>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
export default EventInfo
|
||||
|
|
@ -77,6 +77,10 @@
|
|||
color: var(--secondary);
|
||||
line-height: 1em;
|
||||
text-transform: uppercase;
|
||||
|
||||
[data-small=true] & {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.hasAltChars {
|
||||
|
|
@ -101,6 +105,13 @@
|
|||
@media (max-width: 350px) {
|
||||
font-size: 3.5rem;
|
||||
}
|
||||
|
||||
[data-small=true] & {
|
||||
font-size: 2rem;
|
||||
@media (max-width: 350px) {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bigLogo {
|
||||
|
|
|
|||
|
|
@ -9,14 +9,15 @@ import styles from './Header.module.scss'
|
|||
interface HeaderProps {
|
||||
/** Show the full header */
|
||||
isFull?: boolean
|
||||
isSmall?: boolean
|
||||
}
|
||||
|
||||
const Header = async ({ isFull }: HeaderProps) => {
|
||||
const Header = async ({ isFull, isSmall }: HeaderProps) => {
|
||||
const { t } = await useTranslation(['common', 'home'])
|
||||
|
||||
return <header className={styles.header}>
|
||||
return <header className={styles.header} data-small={isSmall}>
|
||||
{isFull ? <>
|
||||
<img className={styles.bigLogo} src={logo.src} alt="" />
|
||||
{!isSmall && <img className={styles.bigLogo} src={logo.src} alt="" />}
|
||||
<span className={makeClass(styles.subtitle, !/^[A-Za-z ]+$/.test(t('home:create')) && styles.hasAltChars)}>{t('home:create')}</span>
|
||||
<h1 className={styles.bigTitle}>CRAB FIT</h1>
|
||||
</> : <Link href="/" className={styles.link}>
|
||||
|
|
|
|||
|
|
@ -12,11 +12,7 @@ import { relativeTimeFormat } from '/src/utils'
|
|||
|
||||
import styles from './Recents.module.scss'
|
||||
|
||||
interface RecentsProps {
|
||||
target?: React.ComponentProps<'a'>['target']
|
||||
}
|
||||
|
||||
const Recents = ({ target }: RecentsProps) => {
|
||||
const Recents = () => {
|
||||
const recents = useStore(useRecentsStore, state => state.recents)
|
||||
const { t, i18n } = useTranslation(['home', 'common'])
|
||||
|
||||
|
|
@ -24,7 +20,7 @@ const Recents = ({ target }: RecentsProps) => {
|
|||
<Content>
|
||||
<h2>{t('home:recently_visited')}</h2>
|
||||
{recents.map(event => (
|
||||
<Link className={styles.recent} href={`/${event.id}`} target={target} key={event.id}>
|
||||
<Link className={styles.recent} href={`/${event.id}`} key={event.id}>
|
||||
<span className={styles.name}>{event.name}</span>
|
||||
<span
|
||||
className={styles.date}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { isKeyOfObject } from '@giraugh/tools'
|
||||
import { maps } from 'hue-map'
|
||||
import { MapKey } from 'hue-map/dist/maps'
|
||||
import { Settings as SettingsIcon } from 'lucide-react'
|
||||
|
|
@ -65,8 +66,8 @@ const Settings = () => {
|
|||
'Sunday': t('options.weekStart.options.Sunday'),
|
||||
'Monday': t('options.weekStart.options.Monday'),
|
||||
}}
|
||||
value={store?.weekStart === 1 ? 'Sunday' : 'Monday'}
|
||||
onChange={value => store?.setWeekStart(value === 'Sunday' ? 1 : 0)}
|
||||
value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
|
||||
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
|
|
@ -76,8 +77,8 @@ const Settings = () => {
|
|||
'12h': t('options.timeFormat.options.12h'),
|
||||
'24h': t('options.timeFormat.options.24h'),
|
||||
}}
|
||||
value={store?.timeFormat ?? '12h'}
|
||||
onChange={value => store?.setTimeFormat(value)}
|
||||
value={store.timeFormat ?? '12h'}
|
||||
onChange={value => store.setTimeFormat(value)}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
|
|
@ -88,8 +89,8 @@ const Settings = () => {
|
|||
'Light': t('options.theme.options.Light'),
|
||||
'Dark': t('options.theme.options.Dark'),
|
||||
}}
|
||||
value={store?.theme ?? 'System'}
|
||||
onChange={value => store?.setTheme(value)}
|
||||
value={store.theme ?? 'System'}
|
||||
onChange={value => store.setTheme(value)}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
|
|
@ -103,8 +104,8 @@ const Settings = () => {
|
|||
])),
|
||||
}}
|
||||
isSmall
|
||||
value={store?.colormap}
|
||||
onChange={event => store?.setColormap(event.target.value as MapKey)}
|
||||
value={store.colormap}
|
||||
onChange={event => store.setColormap(event.target.value as MapKey)}
|
||||
/>
|
||||
|
||||
<ToggleField
|
||||
|
|
@ -115,8 +116,8 @@ const Settings = () => {
|
|||
'Off': t('options.highlight.options.Off'),
|
||||
'On': t('options.highlight.options.On'),
|
||||
}}
|
||||
value={store?.highlight ? 'On' : 'Off'}
|
||||
onChange={value => store?.setHighlight(value === 'On')}
|
||||
value={store.highlight ? 'On' : 'Off'}
|
||||
onChange={value => store.setHighlight(value === 'On')}
|
||||
/>
|
||||
|
||||
<SelectField
|
||||
|
|
@ -129,7 +130,15 @@ const Settings = () => {
|
|||
}}
|
||||
isSmall
|
||||
value={i18n.language}
|
||||
onChange={e => i18n.changeLanguage(e.target.value).then(() => router.refresh())}
|
||||
onChange={e => {
|
||||
// Set language defaults
|
||||
if (isKeyOfObject(e.target.value, languageDetails)) {
|
||||
store.setTimeFormat(languageDetails[e.target.value].timeFormat)
|
||||
store.setWeekStart(languageDetails[e.target.value].weekStart)
|
||||
}
|
||||
|
||||
i18n.changeLanguage(e.target.value).then(() => router.refresh())
|
||||
}}
|
||||
/>
|
||||
</>}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue