Migrate extension Create page to Nextjs
This commit is contained in:
parent
d2bee83db4
commit
2d9b1d7959
|
|
@ -16,7 +16,6 @@
|
||||||
"@microsoft/microsoft-graph-client": "^3.0.5",
|
"@microsoft/microsoft-graph-client": "^3.0.5",
|
||||||
"accept-language": "^3.0.18",
|
"accept-language": "^3.0.18",
|
||||||
"gapi-script": "^1.2.0",
|
"gapi-script": "^1.2.0",
|
||||||
"goober": "^2.1.13",
|
|
||||||
"hue-map": "^1.0.0",
|
"hue-map": "^1.0.0",
|
||||||
"i18next": "^22.5.0",
|
"i18next": "^22.5.0",
|
||||||
"i18next-browser-languagedetector": "^7.0.1",
|
"i18next-browser-languagedetector": "^7.0.1",
|
||||||
|
|
|
||||||
19
frontend/src/app/create/Redirect.tsx
Normal file
19
frontend/src/app/create/Redirect.tsx
Normal 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
|
||||||
29
frontend/src/app/create/page.tsx
Normal file
29
frontend/src/app/create/page.tsx
Normal 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
|
||||||
|
|
@ -6,6 +6,7 @@ import { range, rotateArray } from '@giraugh/tools'
|
||||||
import AvailabilityViewer from '/src/components/AvailabilityViewer/AvailabilityViewer'
|
import AvailabilityViewer from '/src/components/AvailabilityViewer/AvailabilityViewer'
|
||||||
import Button from '/src/components/Button/Button'
|
import Button from '/src/components/Button/Button'
|
||||||
import Content from '/src/components/Content/Content'
|
import Content from '/src/components/Content/Content'
|
||||||
|
import Footer from '/src/components/Footer/Footer'
|
||||||
import Header from '/src/components/Header/Header'
|
import Header from '/src/components/Header/Header'
|
||||||
import { P } from '/src/components/Paragraph/Text'
|
import { P } from '/src/components/Paragraph/Text'
|
||||||
import Section from '/src/components/Section/Section'
|
import Section from '/src/components/Section/Section'
|
||||||
|
|
@ -80,6 +81,9 @@ const Page = async () => {
|
||||||
<Button href="/">{t('common:cta')}</Button>
|
<Button href="/">{t('common:cta')}</Button>
|
||||||
</Content>
|
</Content>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
{/* @ts-expect-error Async Server Component */}
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
import { Metadata } from 'next'
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
import Egg from '/src/components/Egg/Egg'
|
import Egg from '/src/components/Egg/Egg'
|
||||||
import Footer from '/src/components/Footer/Footer'
|
|
||||||
import Settings from '/src/components/Settings/Settings'
|
import Settings from '/src/components/Settings/Settings'
|
||||||
import TranslateDialog from '/src/components/TranslateDialog/TranslateDialog'
|
import TranslateDialog from '/src/components/TranslateDialog/TranslateDialog'
|
||||||
import { fallbackLng } from '/src/i18n/options'
|
import { fallbackLng } from '/src/i18n/options'
|
||||||
|
|
@ -40,9 +39,6 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => {
|
||||||
<TranslateDialog />
|
<TranslateDialog />
|
||||||
|
|
||||||
{children}
|
{children}
|
||||||
|
|
||||||
{/* @ts-expect-error Async Server Component */}
|
|
||||||
<Footer />
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import Link from 'next/link'
|
||||||
import Content from '/src/components/Content/Content'
|
import Content from '/src/components/Content/Content'
|
||||||
import CreateForm from '/src/components/CreateForm/CreateForm'
|
import CreateForm from '/src/components/CreateForm/CreateForm'
|
||||||
import DownloadButtons from '/src/components/DownloadButtons/DownloadButtons'
|
import DownloadButtons from '/src/components/DownloadButtons/DownloadButtons'
|
||||||
|
import Footer from '/src/components/Footer/Footer'
|
||||||
import Header from '/src/components/Header/Header'
|
import Header from '/src/components/Header/Header'
|
||||||
import { P } from '/src/components/Paragraph/Text'
|
import { P } from '/src/components/Paragraph/Text'
|
||||||
import Recents from '/src/components/Recents/Recents'
|
import Recents from '/src/components/Recents/Recents'
|
||||||
|
|
@ -46,6 +47,9 @@ const Page = async () => {
|
||||||
<P>{t('about.content.p5')}</P>
|
<P>{t('about.content.p5')}</P>
|
||||||
</Content>
|
</Content>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
{/* @ts-expect-error Async Server Component */}
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@ import { Metadata } from 'next'
|
||||||
import GoogleTranslate from '/src/app/privacy/GoogleTranslate'
|
import GoogleTranslate from '/src/app/privacy/GoogleTranslate'
|
||||||
import Button from '/src/components/Button/Button'
|
import Button from '/src/components/Button/Button'
|
||||||
import Content from '/src/components/Content/Content'
|
import Content from '/src/components/Content/Content'
|
||||||
|
import Footer from '/src/components/Footer/Footer'
|
||||||
import Header from '/src/components/Header/Header'
|
import Header from '/src/components/Header/Header'
|
||||||
import { P, Ul } from '/src/components/Paragraph/Text'
|
import { P, Ul } from '/src/components/Paragraph/Text'
|
||||||
import Section from '/src/components/Section/Section'
|
import Section from '/src/components/Section/Section'
|
||||||
|
|
@ -84,6 +85,9 @@ const Page = async () => {
|
||||||
<Button href="/">{t('common:cta')}</Button>
|
<Button href="/">{t('common:cta')}</Button>
|
||||||
</Content>
|
</Content>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
{/* @ts-expect-error Async Server Component */}
|
||||||
|
<Footer />
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ interface MonthProps {
|
||||||
const Month = ({ value, onChange }: MonthProps) => {
|
const Month = ({ value, onChange }: MonthProps) => {
|
||||||
const { t, i18n } = useTranslation('home')
|
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 [page, setPage] = useState<Temporal.PlainYearMonth>(Temporal.Now.plainDateISO().toPlainYearMonth())
|
||||||
const dates = useMemo(() => calculateMonth(page, weekStart), [page, weekStart])
|
const dates = useMemo(() => calculateMonth(page, weekStart), [page, weekStart])
|
||||||
|
|
@ -61,7 +61,7 @@ const Month = ({ value, onChange }: MonthProps) => {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.dayLabels}>
|
<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>
|
<label key={name}>{name}</label>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -119,8 +119,8 @@ export default Month
|
||||||
|
|
||||||
/** Calculate the dates to show for the month in a 2d array */
|
/** Calculate the dates to show for the month in a 2d array */
|
||||||
const calculateMonth = (month: Temporal.PlainYearMonth, weekStart: 0 | 1) => {
|
const calculateMonth = (month: Temporal.PlainYearMonth, weekStart: 0 | 1) => {
|
||||||
const daysBefore = month.toPlainDate({ day: 1 }).dayOfWeek - (weekStart ? 0 : 1)
|
const daysBefore = month.toPlainDate({ day: 1 }).dayOfWeek - weekStart
|
||||||
const daysAfter = 6 - month.toPlainDate({ day: month.daysInMonth }).dayOfWeek + (weekStart ? 0 : 1)
|
const daysAfter = 6 - month.toPlainDate({ day: month.daysInMonth }).dayOfWeek + weekStart
|
||||||
|
|
||||||
const dates: Temporal.PlainDate[][] = []
|
const dates: Temporal.PlainDate[][] = []
|
||||||
let curDate = month.toPlainDate({ day: 1 }).subtract({ days: daysBefore })
|
let curDate = month.toPlainDate({ day: 1 }).subtract({ days: daysBefore })
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,9 @@ interface WeekdaysProps {
|
||||||
const Weekdays = ({ value, onChange }: WeekdaysProps) => {
|
const Weekdays = ({ value, onChange }: WeekdaysProps) => {
|
||||||
const { t, i18n } = useTranslation('home')
|
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
|
// Ref and state required to rerender but also access static version in callbacks
|
||||||
const selectingRef = useRef<string[]>([])
|
const selectingRef = useRef<string[]>([])
|
||||||
|
|
|
||||||
|
|
@ -9,3 +9,8 @@
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: 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 {
|
interface ContentProps {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
isCentered?: boolean
|
isCentered?: boolean
|
||||||
|
isSlim?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Content = ({ isCentered, ...props }: ContentProps) =>
|
const Content = ({ isCentered, isSlim, ...props }: ContentProps) =>
|
||||||
<div className={makeClass(styles.content, isCentered && styles.centered)} {...props} />
|
<div
|
||||||
|
className={makeClass(
|
||||||
|
styles.content,
|
||||||
|
isCentered && styles.centered,
|
||||||
|
isSlim && styles.slim,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
|
||||||
export default Content
|
export default Content
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,3 @@
|
||||||
.form {
|
|
||||||
margin: 0 0 60px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttonWrapper {
|
.buttonWrapper {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { SubmitHandler, useForm } from 'react-hook-form'
|
import { SubmitHandler, useForm } from 'react-hook-form'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { range } from '@giraugh/tools'
|
||||||
import { Temporal } from '@js-temporal/polyfill'
|
import { Temporal } from '@js-temporal/polyfill'
|
||||||
|
|
||||||
import Button from '/src/components/Button/Button'
|
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 SelectField from '/src/components/SelectField/SelectField'
|
||||||
import TextField from '/src/components/TextField/TextField'
|
import TextField from '/src/components/TextField/TextField'
|
||||||
import TimeRangeField from '/src/components/TimeRangeField/TimeRangeField'
|
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 { useTranslation } from '/src/i18n/client'
|
||||||
import timezones from '/src/res/timezones.json'
|
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'
|
import styles from './CreateForm.module.scss'
|
||||||
|
|
||||||
interface Fields {
|
interface Fields {
|
||||||
name: string
|
name: string
|
||||||
|
/** As `YYYY-MM-DD` or `d` */
|
||||||
dates: string[]
|
dates: string[]
|
||||||
time: {
|
time: {
|
||||||
start: number
|
start: number
|
||||||
|
|
@ -34,10 +38,12 @@ const defaultValues: Fields = {
|
||||||
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
||||||
}
|
}
|
||||||
|
|
||||||
const CreateForm = () => {
|
const CreateForm = ({ noRedirect }: { noRedirect?: boolean }) => {
|
||||||
const { t } = useTranslation('home')
|
const { t } = useTranslation('home')
|
||||||
const { push } = useRouter()
|
const { push } = useRouter()
|
||||||
|
|
||||||
|
const addRecent = useRecentsStore(state => state.addRecent)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
|
@ -45,6 +51,7 @@ const CreateForm = () => {
|
||||||
} = useForm({ defaultValues })
|
} = useForm({ defaultValues })
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false)
|
const [isLoading, setIsLoading] = useState(false)
|
||||||
|
const [createdEvent, setCreatedEvent] = useState<EventResponse>()
|
||||||
const [error, setError] = useState<React.ReactNode>()
|
const [error, setError] = useState<React.ReactNode>()
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<Fields> = async values => {
|
const onSubmit: SubmitHandler<Fields> = async values => {
|
||||||
|
|
@ -64,36 +71,24 @@ const CreateForm = () => {
|
||||||
// If format is `YYYY-MM-DD` or `d`
|
// If format is `YYYY-MM-DD` or `d`
|
||||||
const isSpecificDates = dates[0].length !== 1
|
const isSpecificDates = dates[0].length !== 1
|
||||||
|
|
||||||
const times = dates.reduce((times, dateStr) => {
|
const times = dates.flatMap(dateStr => {
|
||||||
const day = []
|
|
||||||
const date = isSpecificDates
|
const date = isSpecificDates
|
||||||
? Temporal.PlainDate.from(dateStr)
|
? Temporal.PlainDate.from(dateStr)
|
||||||
: Temporal.Now.plainDateISO().add({ days: Number(dateStr) - Temporal.Now.plainDateISO().dayOfWeek })
|
: 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 hours = time.start > time.end ? [...range(0, time.end - 1), ...range(time.start, 23)] : range(time.start, time.end - 1)
|
||||||
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour: i }) }).withTimeZone('UTC')
|
|
||||||
|
return hours.map(hour => {
|
||||||
|
const dateTime = date.toZonedDateTime({ timeZone: timezone, plainTime: Temporal.PlainTime.from({ hour }) }).withTimeZone('UTC')
|
||||||
if (isSpecificDates) {
|
if (isSpecificDates) {
|
||||||
// Format as `HHmm-DDMMYYYY`
|
// 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 {
|
} else {
|
||||||
// Format as `HHmm-d`
|
// 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) {
|
if (times.length === 0) {
|
||||||
return setError(t('form.errors.no_time'))
|
return setError(t('form.errors.no_time'))
|
||||||
|
|
@ -114,10 +109,20 @@ const CreateForm = () => {
|
||||||
throw new Error('Failed to create event')
|
throw new Error('Failed to create event')
|
||||||
}
|
}
|
||||||
|
|
||||||
const { id } = await res.json()
|
const newEvent = EventResponse.parse(await res.json())
|
||||||
|
|
||||||
// Navigate to the new event
|
if (noRedirect) {
|
||||||
push(`/${id}`)
|
// 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) {
|
} catch (e) {
|
||||||
setError(t('form.errors.unknown'))
|
setError(t('form.errors.unknown'))
|
||||||
console.error(e)
|
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
|
<TextField
|
||||||
label={t('form.name.label')}
|
label={t('form.name.label')}
|
||||||
description={t('form.name.sublabel')}
|
description={t('form.name.sublabel')}
|
||||||
|
|
@ -163,6 +172,7 @@ const CreateForm = () => {
|
||||||
type="submit"
|
type="submit"
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
|
style={noRedirect ? { width: '100%' } : undefined}
|
||||||
>{t('form.button')}</Button>
|
>{t('form.button')}</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</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);
|
color: var(--secondary);
|
||||||
line-height: 1em;
|
line-height: 1em;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
|
|
||||||
|
[data-small=true] & {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.hasAltChars {
|
.hasAltChars {
|
||||||
|
|
@ -101,6 +105,13 @@
|
||||||
@media (max-width: 350px) {
|
@media (max-width: 350px) {
|
||||||
font-size: 3.5rem;
|
font-size: 3.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-small=true] & {
|
||||||
|
font-size: 2rem;
|
||||||
|
@media (max-width: 350px) {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.bigLogo {
|
.bigLogo {
|
||||||
|
|
|
||||||
|
|
@ -9,14 +9,15 @@ import styles from './Header.module.scss'
|
||||||
interface HeaderProps {
|
interface HeaderProps {
|
||||||
/** Show the full header */
|
/** Show the full header */
|
||||||
isFull?: boolean
|
isFull?: boolean
|
||||||
|
isSmall?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const Header = async ({ isFull }: HeaderProps) => {
|
const Header = async ({ isFull, isSmall }: HeaderProps) => {
|
||||||
const { t } = await useTranslation(['common', 'home'])
|
const { t } = await useTranslation(['common', 'home'])
|
||||||
|
|
||||||
return <header className={styles.header}>
|
return <header className={styles.header} data-small={isSmall}>
|
||||||
{isFull ? <>
|
{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>
|
<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>
|
<h1 className={styles.bigTitle}>CRAB FIT</h1>
|
||||||
</> : <Link href="/" className={styles.link}>
|
</> : <Link href="/" className={styles.link}>
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,7 @@ import { relativeTimeFormat } from '/src/utils'
|
||||||
|
|
||||||
import styles from './Recents.module.scss'
|
import styles from './Recents.module.scss'
|
||||||
|
|
||||||
interface RecentsProps {
|
const Recents = () => {
|
||||||
target?: React.ComponentProps<'a'>['target']
|
|
||||||
}
|
|
||||||
|
|
||||||
const Recents = ({ target }: RecentsProps) => {
|
|
||||||
const recents = useStore(useRecentsStore, state => state.recents)
|
const recents = useStore(useRecentsStore, state => state.recents)
|
||||||
const { t, i18n } = useTranslation(['home', 'common'])
|
const { t, i18n } = useTranslation(['home', 'common'])
|
||||||
|
|
||||||
|
|
@ -24,7 +20,7 @@ const Recents = ({ target }: RecentsProps) => {
|
||||||
<Content>
|
<Content>
|
||||||
<h2>{t('home:recently_visited')}</h2>
|
<h2>{t('home:recently_visited')}</h2>
|
||||||
{recents.map(event => (
|
{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.name}>{event.name}</span>
|
||||||
<span
|
<span
|
||||||
className={styles.date}
|
className={styles.date}
|
||||||
|
|
|
||||||
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { isKeyOfObject } from '@giraugh/tools'
|
||||||
import { maps } from 'hue-map'
|
import { maps } from 'hue-map'
|
||||||
import { MapKey } from 'hue-map/dist/maps'
|
import { MapKey } from 'hue-map/dist/maps'
|
||||||
import { Settings as SettingsIcon } from 'lucide-react'
|
import { Settings as SettingsIcon } from 'lucide-react'
|
||||||
|
|
@ -65,8 +66,8 @@ const Settings = () => {
|
||||||
'Sunday': t('options.weekStart.options.Sunday'),
|
'Sunday': t('options.weekStart.options.Sunday'),
|
||||||
'Monday': t('options.weekStart.options.Monday'),
|
'Monday': t('options.weekStart.options.Monday'),
|
||||||
}}
|
}}
|
||||||
value={store?.weekStart === 1 ? 'Sunday' : 'Monday'}
|
value={store.weekStart === 0 ? 'Sunday' : 'Monday'}
|
||||||
onChange={value => store?.setWeekStart(value === 'Sunday' ? 1 : 0)}
|
onChange={value => store.setWeekStart(value === 'Sunday' ? 0 : 1)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleField
|
<ToggleField
|
||||||
|
|
@ -76,8 +77,8 @@ const Settings = () => {
|
||||||
'12h': t('options.timeFormat.options.12h'),
|
'12h': t('options.timeFormat.options.12h'),
|
||||||
'24h': t('options.timeFormat.options.24h'),
|
'24h': t('options.timeFormat.options.24h'),
|
||||||
}}
|
}}
|
||||||
value={store?.timeFormat ?? '12h'}
|
value={store.timeFormat ?? '12h'}
|
||||||
onChange={value => store?.setTimeFormat(value)}
|
onChange={value => store.setTimeFormat(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleField
|
<ToggleField
|
||||||
|
|
@ -88,8 +89,8 @@ const Settings = () => {
|
||||||
'Light': t('options.theme.options.Light'),
|
'Light': t('options.theme.options.Light'),
|
||||||
'Dark': t('options.theme.options.Dark'),
|
'Dark': t('options.theme.options.Dark'),
|
||||||
}}
|
}}
|
||||||
value={store?.theme ?? 'System'}
|
value={store.theme ?? 'System'}
|
||||||
onChange={value => store?.setTheme(value)}
|
onChange={value => store.setTheme(value)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|
@ -103,8 +104,8 @@ const Settings = () => {
|
||||||
])),
|
])),
|
||||||
}}
|
}}
|
||||||
isSmall
|
isSmall
|
||||||
value={store?.colormap}
|
value={store.colormap}
|
||||||
onChange={event => store?.setColormap(event.target.value as MapKey)}
|
onChange={event => store.setColormap(event.target.value as MapKey)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ToggleField
|
<ToggleField
|
||||||
|
|
@ -115,8 +116,8 @@ const Settings = () => {
|
||||||
'Off': t('options.highlight.options.Off'),
|
'Off': t('options.highlight.options.Off'),
|
||||||
'On': t('options.highlight.options.On'),
|
'On': t('options.highlight.options.On'),
|
||||||
}}
|
}}
|
||||||
value={store?.highlight ? 'On' : 'Off'}
|
value={store.highlight ? 'On' : 'Off'}
|
||||||
onChange={value => store?.setHighlight(value === 'On')}
|
onChange={value => store.setHighlight(value === 'On')}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<SelectField
|
<SelectField
|
||||||
|
|
@ -129,7 +130,15 @@ const Settings = () => {
|
||||||
}}
|
}}
|
||||||
isSmall
|
isSmall
|
||||||
value={i18n.language}
|
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>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
`
|
|
||||||
|
|
@ -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'))
|
|
||||||
|
|
@ -6,7 +6,7 @@ type TimeFormat = '12h' | '24h'
|
||||||
type Theme = 'System' | 'Light' | 'Dark'
|
type Theme = 'System' | 'Light' | 'Dark'
|
||||||
|
|
||||||
interface SettingsStore {
|
interface SettingsStore {
|
||||||
/** 0: Monday, 1: Sunday */
|
/** 0: Sunday, 1: Monday */
|
||||||
weekStart: 0 | 1
|
weekStart: 0 | 1
|
||||||
timeFormat: TimeFormat
|
timeFormat: TimeFormat
|
||||||
theme: Theme
|
theme: Theme
|
||||||
|
|
@ -34,18 +34,7 @@ const useSettingsStore = create<SettingsStore>()(persist(
|
||||||
setHighlight: highlight => set({ highlight }),
|
setHighlight: highlight => set({ highlight }),
|
||||||
setColormap: colormap => set({ colormap }),
|
setColormap: colormap => set({ colormap }),
|
||||||
}),
|
}),
|
||||||
{
|
{ name: 'crabfit-settings' },
|
||||||
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
|
|
||||||
},
|
|
||||||
},
|
|
||||||
))
|
))
|
||||||
|
|
||||||
export default useSettingsStore
|
export default useSettingsStore
|
||||||
|
|
|
||||||
|
|
@ -1317,11 +1317,6 @@ globby@^13.1.3:
|
||||||
merge2 "^1.4.1"
|
merge2 "^1.4.1"
|
||||||
slash "^4.0.0"
|
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:
|
gopd@^1.0.1:
|
||||||
version "1.0.1"
|
version "1.0.1"
|
||||||
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.0.1.tgz#29ff76de69dac7489b7c0918a5788e56477c332c"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue