Migrate extension Create page to Nextjs

This commit is contained in:
Ben Grant 2023-05-28 19:24:35 +10:00
parent d2bee83db4
commit 2d9b1d7959
24 changed files with 214 additions and 391 deletions

View file

@ -16,7 +16,6 @@
"@microsoft/microsoft-graph-client": "^3.0.5",
"accept-language": "^3.0.18",
"gapi-script": "^1.2.0",
"goober": "^2.1.13",
"hue-map": "^1.0.0",
"i18next": "^22.5.0",
"i18next-browser-languagedetector": "^7.0.1",

View file

@ -0,0 +1,19 @@
'use client'
import { useEffect } from 'react'
import { useRouter } from 'next/navigation'
/** Check if the current page is running in an iframe, otherwise redirect home */
const Redirect = () => {
const router = useRouter()
useEffect(() => {
if (window.self === window.top) {
router.replace('/')
}
}, [])
return null
}
export default Redirect

View file

@ -0,0 +1,29 @@
import { Metadata } from 'next'
import Content from '/src/components/Content/Content'
import CreateForm from '/src/components/CreateForm/CreateForm'
import Header from '/src/components/Header/Header'
import Redirect from './Redirect'
export const metadata: Metadata = {
title: 'Create a Crab Fit',
}
/**
* Used in the Crab Fit browser extension, to be rendered only in an iframe
*/
const Page = async () => <>
<Content isSlim>
{/* @ts-expect-error Async Server Component */}
<Header isFull isSmall />
</Content>
<Content isSlim>
<CreateForm noRedirect />
</Content>
<Redirect />
</>
export default Page

View file

@ -6,6 +6,7 @@ import { range, rotateArray } from '@giraugh/tools'
import AvailabilityViewer from '/src/components/AvailabilityViewer/AvailabilityViewer'
import Button from '/src/components/Button/Button'
import Content from '/src/components/Content/Content'
import Footer from '/src/components/Footer/Footer'
import Header from '/src/components/Header/Header'
import { P } from '/src/components/Paragraph/Text'
import Section from '/src/components/Section/Section'
@ -80,6 +81,9 @@ const Page = async () => {
<Button href="/">{t('common:cta')}</Button>
</Content>
</Section>
{/* @ts-expect-error Async Server Component */}
<Footer />
</>
}

View file

@ -1,7 +1,6 @@
import { Metadata } from 'next'
import Egg from '/src/components/Egg/Egg'
import Footer from '/src/components/Footer/Footer'
import Settings from '/src/components/Settings/Settings'
import TranslateDialog from '/src/components/TranslateDialog/TranslateDialog'
import { fallbackLng } from '/src/i18n/options'
@ -40,9 +39,6 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
<TranslateDialog />
{children}
{/* @ts-expect-error Async Server Component */}
<Footer />
</body>
</html>
}

View file

@ -4,6 +4,7 @@ import Link from 'next/link'
import Content from '/src/components/Content/Content'
import CreateForm from '/src/components/CreateForm/CreateForm'
import DownloadButtons from '/src/components/DownloadButtons/DownloadButtons'
import Footer from '/src/components/Footer/Footer'
import Header from '/src/components/Header/Header'
import { P } from '/src/components/Paragraph/Text'
import Recents from '/src/components/Recents/Recents'
@ -46,6 +47,9 @@ const Page = async () => {
<P>{t('about.content.p5')}</P>
</Content>
</Section>
{/* @ts-expect-error Async Server Component */}
<Footer />
</>
}

View file

@ -3,6 +3,7 @@ import { Metadata } from 'next'
import GoogleTranslate from '/src/app/privacy/GoogleTranslate'
import Button from '/src/components/Button/Button'
import Content from '/src/components/Content/Content'
import Footer from '/src/components/Footer/Footer'
import Header from '/src/components/Header/Header'
import { P, Ul } from '/src/components/Paragraph/Text'
import Section from '/src/components/Section/Section'
@ -84,6 +85,9 @@ const Page = async () => {
<Button href="/">{t('common:cta')}</Button>
</Content>
</Section>
{/* @ts-expect-error Async Server Component */}
<Footer />
</>
}

View file

@ -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 })

View file

@ -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[]>([])

View file

@ -9,3 +9,8 @@
align-items: center;
justify-content: center;
}
.slim {
margin-block: 10px;
max-width: calc(100% - 30px);
}

View file

@ -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

View file

@ -1,7 +1,3 @@
.form {
margin: 0 0 60px;
}
.buttonWrapper {
display: flex;
justify-content: center;

View file

@ -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>

View file

@ -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);
}
}

View file

@ -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

View file

@ -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 {

View file

@ -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}>

View file

@ -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}

View file

@ -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>

View file

@ -1,242 +0,0 @@
import { useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useForm } from 'react-hook-form'
import { useTranslation, Trans } from 'react-i18next'
import dayjs from 'dayjs'
import utc from 'dayjs/plugin/utc'
import timezone from 'dayjs/plugin/timezone'
import customParseFormat from 'dayjs/plugin/customParseFormat'
import {
TextField,
CalendarField,
TimeRangeField,
SelectField,
Button,
Error,
Recents,
Footer,
} from '/src/components'
import {
StyledMain,
CreateForm,
TitleSmall,
TitleLarge,
P,
OfflineMessage,
ShareInfo,
} from './Create.styles'
import api from '/src/services'
import { useRecentsStore } from '/src/stores'
import timezones from '/src/res/timezones.json'
dayjs.extend(utc)
dayjs.extend(timezone)
dayjs.extend(customParseFormat)
const Create = ({ offline }) => {
const { register, handleSubmit, setValue } = useForm({
defaultValues: {
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
},
})
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState(null)
const [createdEvent, setCreatedEvent] = useState(null)
const [copied, setCopied] = useState(null)
const [showFooter, setShowFooter] = useState(true)
const navigate = useNavigate()
const { t } = useTranslation(['common', 'home', 'event'])
const addRecent = useRecentsStore(state => state.addRecent)
useEffect(() => {
if (window.self === window.top) {
navigate('/')
}
document.title = 'Create a Crab Fit'
if (window.parent) {
window.parent.postMessage('crabfit-create', '*')
window.addEventListener('message', e => {
if (e.data === 'safari-extension') {
setShowFooter(false)
}
}, {
once: true
})
}
}, [navigate])
const onSubmit = async data => {
setIsLoading(true)
setError(null)
try {
const { start, end } = JSON.parse(data.times)
const dates = JSON.parse(data.dates)
if (dates.length === 0) {
return setError(t('home:form.errors.no_dates'))
}
const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8
if (start === end) {
return setError(t('home:form.errors.same_times'))
}
const times = dates.reduce((times, date) => {
const day = []
for (let i = start; i < (start > end ? 24 : end); i++) {
if (isSpecificDates) {
day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
)
} else {
day.push(
dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d')
)
}
}
if (start > end) {
for (let i = 0; i < end; i++) {
if (isSpecificDates) {
day.push(
dayjs.tz(date, 'DDMMYYYY', data.timezone)
.hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
)
} else {
day.push(
dayjs().tz(data.timezone)
.day(date).hour(i).minute(0).utc().format('HHmm-d')
)
}
}
}
return [...times, ...day]
}, [])
if (times.length === 0) {
return setError(t('home:form.errors.no_time'))
}
const event = await api.post('/event', {
event: {
name: data.name,
times: times,
timezone: data.timezone,
},
})
setCreatedEvent(event)
addRecent({
id: event.id,
created: event.created,
name: event.name,
})
gtag('event', 'create_event', {
'event_category': 'create',
})
} catch (e) {
setError(t('home:form.errors.unknown'))
console.error(e)
} finally {
setIsLoading(false)
}
}
return (
<>
<StyledMain>
<TitleSmall>{t('home:create')}</TitleSmall>
<TitleLarge>CRAB FIT</TitleLarge>
</StyledMain>
{createdEvent ? (
<StyledMain>
<OfflineMessage>
<h2>{createdEvent?.name}</h2>
<ShareInfo
onClick={() => navigator.clipboard?.writeText(`https://crab.fit/${createdEvent.id}`)
.then(() => {
setCopied(t('event:nav.copied'))
setTimeout(() => setCopied(null), 1000)
gtag('event', 'copy_link', {
'event_category': 'event',
})
})
.catch(e => console.error('Failed to copy', e))
}
title={navigator.clipboard ? t('event:nav.title') : ''}
>{copied ?? `https://crab.fit/${createdEvent?.id}`}</ShareInfo>
<ShareInfo>
{/* eslint-disable-next-line */}
<Trans i18nKey="event:nav.shareinfo_alt">Click the link above to copy it to your clipboard, or share via <a onClick={() => gtag('event', 'send_email', { 'event_category': 'event' })} href={`mailto:?subject=${encodeURIComponent(t('event:nav.email_subject', { event_name: createdEvent?.name }))}&body=${encodeURIComponent(`${t('event:nav.email_body')} https://crab.fit/${createdEvent?.id}`)}`} target="_blank">email</a>.</Trans>
</ShareInfo>
{showFooter && <Footer small />}
</OfflineMessage>
</StyledMain>
) : (
<>
<Recents target="_blank" />
<StyledMain>
{offline ? (
<OfflineMessage>
<h1>🦀📵</h1>
<P>{t('home:offline')}</P>
</OfflineMessage>
) : (
<CreateForm onSubmit={handleSubmit(onSubmit)} id="create">
<TextField
label={t('home:form.name.label')}
subLabel={t('home:form.name.sublabel')}
type="text"
id="name"
{...register('name')}
/>
<CalendarField
label={t('home:form.dates.label')}
subLabel={t('home:form.dates.sublabel')}
id="dates"
required
setValue={setValue}
{...register('dates')}
/>
<TimeRangeField
label={t('home:form.times.label')}
subLabel={t('home:form.times.sublabel')}
id="times"
required
setValue={setValue}
{...register('times')}
/>
<SelectField
label={t('home:form.timezone.label')}
id="timezone"
options={timezones}
required
{...register('timezone')}
defaultOption={t('home:form.timezone.defaultOption')}
/>
<Error open={!!error} onClose={() => setError(null)}>{error}</Error>
<Button type="submit" isLoading={isLoading} disabled={isLoading} style={{ width: '100%' }}>{t('home:form.button')}</Button>
</CreateForm>
)}
</StyledMain>
</>
)}
</>
)
}
export default Create

View file

@ -1,60 +0,0 @@
import { styled } from 'goober'
export const StyledMain = styled('div')`
width: 600px;
margin: 10px auto;
max-width: calc(100% - 30px);
`
export const CreateForm = styled('form')`
margin: 0 0 30px;
`
export const TitleSmall = styled('span')`
display: block;
margin: 0;
font-size: 2rem;
text-align: center;
font-family: 'Samurai Bob', sans-serif;
font-weight: 400;
color: var(--secondary);
line-height: 1em;
text-transform: uppercase;
`
export const TitleLarge = styled('h1')`
margin: 0;
font-size: 2rem;
text-align: center;
color: var(--primary);
font-family: 'Molot', sans-serif;
font-weight: 400;
text-shadow: 0 3px 0 var(--secondary);
line-height: 1em;
text-transform: uppercase;
`
export const P = styled('p')`
font-weight: 500;
line-height: 1.6em;
`
export const OfflineMessage = styled('div')`
text-align: center;
margin: 50px 0 20px;
`
export const ShareInfo = styled('p')`
margin: 6px 0;
text-align: center;
font-size: 15px;
padding: 10px 0;
${props => props.onClick && `
cursor: pointer;
&:hover {
color: var(--secondary);
}
`}
`

View file

@ -1,7 +0,0 @@
import { lazy } from 'react'
export const Home = lazy(() => import('./Home/Home'))
export const Event = lazy(() => import('./Event/Event'))
export const Create = lazy(() => import('./Create/Create'))
export const Help = lazy(() => import('./Help/Help'))
export const Privacy = lazy(() => import('./Privacy/Privacy'))

View file

@ -6,7 +6,7 @@ type TimeFormat = '12h' | '24h'
type Theme = 'System' | 'Light' | 'Dark'
interface SettingsStore {
/** 0: Monday, 1: Sunday */
/** 0: Sunday, 1: Monday */
weekStart: 0 | 1
timeFormat: TimeFormat
theme: Theme
@ -34,18 +34,7 @@ const useSettingsStore = create<SettingsStore>()(persist(
setHighlight: highlight => set({ highlight }),
setColormap: colormap => set({ colormap }),
}),
{
name: 'crabfit-settings',
version: 1,
migrate: (persistedState, version) => {
if (version === 0) {
// Weekstart used to be 0 for Sunday, but now it's been swapped
(persistedState as SettingsStore).weekStart = (persistedState as SettingsStore).weekStart === 1 ? 0 : 1
return persistedState as SettingsStore
}
return persistedState as SettingsStore
},
},
{ name: 'crabfit-settings' },
))
export default useSettingsStore

View file

@ -1317,11 +1317,6 @@ globby@^13.1.3:
merge2 "^1.4.1"
slash "^4.0.0"
goober@^2.1.13:
version "2.1.13"
resolved "https://registry.yarnpkg.com/goober/-/goober-2.1.13.tgz#e3c06d5578486212a76c9eba860cbc3232ff6d7c"
integrity sha512-jFj3BQeleOoy7t93E9rZ2de+ScC4lQICLwiAQmKMg9F6roKGaLSHoCDYKkWlSafg138jejvq/mTdvmnwDQgqoQ==
gopd@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"