([])
+ const setSelecting = useCallback((v: string[]) => {
+ selectingRef.current = v
+ _setSelecting(v)
+ }, [])
+
+ const startPos = useRef({ x: 0, y: 0 })
+ const mode = useRef<'add' | 'remove'>()
+
+ // Update month view
+ useEffect(() => {
+ if (dayjs.Ls?.[locale] && weekStart !== dayjs.Ls[locale].weekStart) {
+ dayjs.updateLocale(locale, { weekStart })
+ }
+ setDates(calculateMonth(page, weekStart))
+ }, [weekStart, page, locale])
+
+ const handleFinishSelection = useCallback(() => {
+ if (mode.current === 'add') {
+ onChange([...value, ...selectingRef.current])
+ } else {
+ onChange(value.filter(d => !selectingRef.current.includes(d)))
+ }
+ mode.current = undefined
+ }, [value])
+
+ return <>
+
+ ('form.dates.tooltips.previous')}
+ onClick={() => {
+ if (page.month - 1 < 0) {
+ setPage({ month: 11, year: page.year - 1 })
+ } else {
+ setPage({ ...page, month: page.month - 1 })
+ }
+ }}
+ icon={ }
+ />
+ {dayjs.months()[page.month]} {page.year}
+ ('form.dates.tooltips.next')}
+ onClick={() => {
+ if (page.month + 1 > 11) {
+ setPage({ month: 0, year: page.year + 1 })
+ } else {
+ setPage({ ...page, month: page.month + 1 })
+ }
+ }}
+ icon={ }
+ />
+
+
+
+ {(rotateArray(dayjs.weekdaysShort(), -weekStart)).map(name =>
+ {name}
+ )}
+
+
+
+ {dates.length > 0 && dates.map((dateRow, y) =>
+ dateRow.map((date, x) => {
+ if (e.key === ' ' || e.key === 'Enter') {
+ if (value.includes(date.str)) {
+ onChange(value.filter(d => d !== date.str))
+ } else {
+ onChange([...value, date.str])
+ }
+ }
+ }}
+ onPointerDown={e => {
+ startPos.current = { x, y }
+ mode.current = value.includes(date.str) ? 'remove' : 'add'
+ setSelecting([date.str])
+ e.currentTarget.releasePointerCapture(e.pointerId)
+
+ document.addEventListener('pointerup', handleFinishSelection, { once: true })
+ }}
+ onPointerEnter={() => {
+ if (mode) {
+ const found = []
+ for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y) + 1; cy++) {
+ for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x) + 1; cx++) {
+ found.push({ y: cy, x: cx })
+ }
+ }
+ setSelecting(found.map(d => dates[d.y][d.x].str))
+ }
+ }}
+ >{date.day} )
+ )}
+
+ >
+}
+
+export default Month
+
+interface Date {
+ str: string
+ day: number
+ month: number
+ isToday: boolean
+}
+
+/** Calculate the dates to show for the month in a 2d array */
+const calculateMonth = ({ month, year }: { month: number, year: number }, weekStart: 0 | 1) => {
+ const date = dayjs().month(month).year(year)
+ const daysInMonth = date.daysInMonth()
+ const daysBefore = date.date(1).day() - weekStart
+ const daysAfter = 6 - date.date(daysInMonth).day() + weekStart
+
+ const dates: Date[][] = []
+ let curDate = date.date(1).subtract(daysBefore, 'day')
+ let y = 0
+ let x = 0
+ for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
+ if (x === 0) dates[y] = []
+ dates[y][x] = {
+ str: curDate.format('DDMMYYYY'),
+ day: curDate.date(),
+ month: curDate.month(),
+ isToday: curDate.isToday(),
+ }
+ curDate = curDate.add(1, 'day')
+ x++
+ if (x > 6) {
+ x = 0
+ y++
+ }
+ }
+
+ return dates
+}
diff --git a/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx b/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx
new file mode 100644
index 0000000..3fe591a
--- /dev/null
+++ b/frontend/src/components/CalendarField/components/Weekdays/Weekdays.tsx
@@ -0,0 +1,97 @@
+import { useCallback, useMemo, useRef, useState } from 'react'
+
+import dayjs from '/src/config/dayjs'
+import { useTranslation } from '/src/i18n/client'
+import useSettingsStore from '/src/stores/settingsStore'
+import { makeClass } from '/src/utils'
+
+// Use styles from Month picker
+import styles from '../Month/Month.module.scss'
+
+// TODO: use from giraugh tools
+export const rotateArray = (arr: T[], amount = 1): T[] =>
+ arr.map((_, i) => arr[((( -amount + i ) % arr.length) + arr.length) % arr.length])
+
+interface WeekdaysProps {
+ /** Array of weekdays as numbers from 0-6 (as strings) */
+ value: string[]
+ onChange: (value: string[]) => void
+}
+
+const Weekdays = ({ value, onChange }: WeekdaysProps) => {
+ const { t } = useTranslation('home')
+
+ const weekStart = useSettingsStore(state => state.weekStart)
+
+ const weekdays = useMemo(() => rotateArray(dayjs.weekdaysShort().map((name, i) => ({
+ name,
+ isToday: dayjs().day() === i,
+ str: String(i),
+ })), -weekStart), [weekStart])
+
+ // Ref and state required to rerender but also access static version in callbacks
+ const selectingRef = useRef([])
+ const [selecting, _setSelecting] = useState([])
+ const setSelecting = useCallback((v: string[]) => {
+ selectingRef.current = v
+ _setSelecting(v)
+ }, [])
+
+ const startPos = useRef(0)
+ const mode = useRef<'add' | 'remove'>()
+
+ const handleFinishSelection = useCallback(() => {
+ if (mode.current === 'add') {
+ onChange([...value, ...selectingRef.current])
+ } else {
+ onChange(value.filter(d => !selectingRef.current.includes(d)))
+ }
+ mode.current = undefined
+ }, [value])
+
+ return
+ {weekdays.map((day, i) =>
+ ('form.dates.tooltips.today') : undefined}
+ onKeyDown={e => {
+ if (e.key === ' ' || e.key === 'Enter') {
+ if (value.includes(day.str)) {
+ onChange(value.filter(d => d !== day.str))
+ } else {
+ onChange([...value, day.str])
+ }
+ }
+ }}
+ onPointerDown={e => {
+ startPos.current = i
+ mode.current = value.includes(day.str) ? 'remove' : 'add'
+ setSelecting([day.str])
+ e.currentTarget.releasePointerCapture(e.pointerId)
+
+ document.addEventListener('pointerup', handleFinishSelection, { once: true })
+ }}
+ onPointerEnter={() => {
+ if (mode.current) {
+ const found = []
+ for (let ci = Math.min(startPos.current, i); ci < Math.max(startPos.current, i) + 1; ci++) {
+ found.push(weekdays[ci].str)
+ }
+ setSelecting(found)
+ }
+ }}
+ >{day.name}
+ )}
+
+}
+
+export default Weekdays
diff --git a/frontend/src/components/CreateForm/CreateForm.module.scss b/frontend/src/components/CreateForm/CreateForm.module.scss
new file mode 100644
index 0000000..66f3f75
--- /dev/null
+++ b/frontend/src/components/CreateForm/CreateForm.module.scss
@@ -0,0 +1,8 @@
+.form {
+ margin: 0 0 60px;
+}
+
+.buttonWrapper {
+ display: flex;
+ justify-content: center;
+}
diff --git a/frontend/src/components/CreateForm/CreateForm.tsx b/frontend/src/components/CreateForm/CreateForm.tsx
new file mode 100644
index 0000000..690fb31
--- /dev/null
+++ b/frontend/src/components/CreateForm/CreateForm.tsx
@@ -0,0 +1,172 @@
+'use client'
+
+import { useEffect, useState } from 'react'
+import { SubmitHandler, useForm } from 'react-hook-form'
+import { useRouter } from 'next/navigation'
+
+import Button from '/src/components/Button/Button'
+import CalendarField from '/src/components/CalendarField/CalendarField'
+import { default as ErrorAlert } from '/src/components/Error/Error'
+import TextField from '/src/components/TextField/TextField'
+import ToggleField from '/src/components/ToggleField/ToggleField'
+import { API_BASE } from '/src/config/api'
+import dayjs from '/src/config/dayjs'
+import { useTranslation } from '/src/i18n/client'
+import timezones from '/src/res/timezones.json'
+
+import styles from './CreateForm.module.scss'
+
+interface Fields {
+ name: string
+ dates: string[]
+ time: {
+ start: number
+ end: number
+ }
+ timezone: string
+}
+
+const defaultValues: Fields = {
+ name: '',
+ dates: [],
+ time: { start: 9, end: 17 },
+ timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
+}
+
+const CreateForm = () => {
+ const { t } = useTranslation('home')
+ const { push } = useRouter()
+
+ const {
+ register,
+ handleSubmit,
+ control,
+ } = useForm({ defaultValues })
+
+ const [isLoading, setIsLoading] = useState(false)
+ const [error, setError] = useState()
+
+ const onSubmit: SubmitHandler = async values => {
+ console.log({values})
+ setIsLoading(true)
+ setError(undefined)
+
+ const { name, dates, time, timezone } = values
+
+ try {
+ if (dates.length === 0) {
+ return setError(t('form.errors.no_dates'))
+ }
+ const isSpecificDates = typeof dates[0] === 'string' && dates[0].length === 8
+ if (time.start === time.end) {
+ return setError(t('form.errors.same_times'))
+ }
+
+ const times = dates.reduce((times, date) => {
+ const day = []
+ for (let i = time.start; i < (time.start > time.end ? 24 : time.end); i++) {
+ if (isSpecificDates) {
+ day.push(
+ dayjs.tz(date, 'DDMMYYYY', timezone)
+ .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
+ )
+ } else {
+ day.push(
+ dayjs().tz(timezone)
+ .day(date).hour(i).minute(0).utc().format('HHmm-d')
+ )
+ }
+ }
+ if (time.start > time.end) {
+ for (let i = 0; i < time.end; i++) {
+ if (isSpecificDates) {
+ day.push(
+ dayjs.tz(date, 'DDMMYYYY', timezone)
+ .hour(i).minute(0).utc().format('HHmm-DDMMYYYY')
+ )
+ } else {
+ day.push(
+ dayjs().tz(timezone)
+ .day(date).hour(i).minute(0).utc().format('HHmm-d')
+ )
+ }
+ }
+ }
+ return [...times, ...day]
+ }, [])
+
+ if (times.length === 0) {
+ return setError(t('form.errors.no_time'))
+ }
+
+ const res = await fetch(new URL('/event', API_BASE), {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({
+ name,
+ times,
+ timezone,
+ }),
+ })
+
+ if (!res.ok) {
+ console.error(res)
+ throw new Error('Failed to create event')
+ }
+
+ const { id } = await res.json()
+
+ // Navigate to the new event
+ push(`/${id}`)
+ } catch (e) {
+ setError(t('form.errors.unknown'))
+ console.error(e)
+ } finally {
+ setIsLoading(false)
+ }
+ }
+
+ return
+}
+
+export default CreateForm
diff --git a/frontend/src/components/Field/Field.module.scss b/frontend/src/components/Field/Field.module.scss
new file mode 100644
index 0000000..39c7a00
--- /dev/null
+++ b/frontend/src/components/Field/Field.module.scss
@@ -0,0 +1,16 @@
+.wrapper {
+ margin: 30px 0;
+}
+
+.label {
+ display: block;
+ padding-bottom: 4px;
+ font-size: 18px;
+}
+
+.description {
+ display: block;
+ padding-bottom: 6px;
+ font-size: 13px;
+ opacity: .7;
+}
diff --git a/frontend/src/components/Field/Field.tsx b/frontend/src/components/Field/Field.tsx
new file mode 100644
index 0000000..0b49696
--- /dev/null
+++ b/frontend/src/components/Field/Field.tsx
@@ -0,0 +1,22 @@
+import styles from './Field.module.scss'
+
+interface WrapperProps {
+ children: React.ReactNode
+ style?: React.CSSProperties
+}
+
+export const Wrapper = (props: WrapperProps) =>
+
+
+interface LabelProps {
+ htmlFor?: string
+ children: React.ReactNode
+ style?: React.CSSProperties
+ title?: string
+}
+
+export const Label = (props: LabelProps) =>
+
+
+export const Description = (props: LabelProps) =>
+
diff --git a/frontend/src/components/Recents/Recents.tsx b/frontend/src/components/Recents/Recents.tsx
index 4740444..79a9d19 100644
--- a/frontend/src/components/Recents/Recents.tsx
+++ b/frontend/src/components/Recents/Recents.tsx
@@ -6,7 +6,8 @@ import Content from '/src/components/Content/Content'
import Section from '/src/components/Section/Section'
import dayjs from '/src/config/dayjs'
import { useTranslation } from '/src/i18n/client'
-import { useRecentsStore, useStore } from '/src/stores'
+import { useStore } from '/src/stores'
+import useRecentsStore from '/src/stores/recentsStore'
import styles from './Recents.module.scss'
diff --git a/frontend/src/components/TextField/TextField.jsx b/frontend/src/components/TextField/TextField.jsx
deleted file mode 100644
index a0d7a85..0000000
--- a/frontend/src/components/TextField/TextField.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import { forwardRef } from 'react'
-
-import {
- Wrapper,
- StyledLabel,
- StyledSubLabel,
- StyledInput,
-} from './TextField.styles'
-
-const TextField = forwardRef(({
- label,
- subLabel,
- id,
- inline = false,
- ...props
-}, ref) => (
-
- {label && {label} }
- {subLabel && {subLabel} }
-
-
-))
-
-export default TextField
diff --git a/frontend/src/components/TextField/TextField.module.scss b/frontend/src/components/TextField/TextField.module.scss
new file mode 100644
index 0000000..965c5cc
--- /dev/null
+++ b/frontend/src/components/TextField/TextField.module.scss
@@ -0,0 +1,19 @@
+.input {
+ width: 100%;
+ box-sizing: border-box;
+ font: inherit;
+ background: var(--surface);
+ color: inherit;
+ padding: 10px 14px;
+ border: 1px solid var(--primary);
+ box-shadow: inset 0 0 0 0 var(--primary);
+ border-radius: 3px;
+ font-size: 18px;
+ outline: none;
+ transition: border-color .15s, box-shadow .15s;
+
+ &:focus {
+ border: 1px solid var(--secondary);
+ box-shadow: inset 0 -3px 0 0 var(--secondary);
+ }
+}
diff --git a/frontend/src/components/TextField/TextField.styles.js b/frontend/src/components/TextField/TextField.styles.js
deleted file mode 100644
index 8fb60f2..0000000
--- a/frontend/src/components/TextField/TextField.styles.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { styled } from 'goober'
-import { forwardRef } from 'react'
-
-export const Wrapper = styled('div')`
- margin: 30px 0;
-
- ${props => props.$inline && `
- margin: 0;
- `}
-`
-
-export const StyledLabel = styled('label')`
- display: block;
- padding-bottom: 4px;
- font-size: 18px;
-
- ${props => props.$inline && `
- font-size: 16px;
- `}
-`
-
-export const StyledSubLabel = styled('label')`
- display: block;
- padding-bottom: 6px;
- font-size: 13px;
- opacity: .6;
-`
-
-export const StyledInput = styled('input', forwardRef)`
- width: 100%;
- box-sizing: border-box;
- font: inherit;
- background: var(--surface);
- color: inherit;
- padding: 10px 14px;
- border: 1px solid var(--primary);
- box-shadow: inset 0 0 0 0 var(--primary);
- border-radius: 3px;
- font-size: 18px;
- outline: none;
- transition: border-color .15s, box-shadow .15s;
-
- &:focus {
- border: 1px solid var(--secondary);
- box-shadow: inset 0 -3px 0 0 var(--secondary);
- }
-`
diff --git a/frontend/src/components/TextField/TextField.tsx b/frontend/src/components/TextField/TextField.tsx
new file mode 100644
index 0000000..f6acad0
--- /dev/null
+++ b/frontend/src/components/TextField/TextField.tsx
@@ -0,0 +1,31 @@
+import { forwardRef } from 'react'
+
+import { Description, Label, Wrapper } from '/src/components/Field/Field'
+
+import styles from './TextField.module.scss'
+
+interface TextFieldProps extends React.ComponentProps<'input'> {
+ label?: React.ReactNode
+ description?: React.ReactNode
+ isInline?: boolean
+}
+
+const TextField = forwardRef(({
+ label,
+ description,
+ isInline,
+ ...props
+}, ref) => (
+
+ {label && {label} }
+
+ {description && {description} }
+
+
+
+))
+
+export default TextField
diff --git a/frontend/src/components/ToggleField/ToggleField.jsx b/frontend/src/components/ToggleField/ToggleField.jsx
deleted file mode 100644
index 67fdfe7..0000000
--- a/frontend/src/components/ToggleField/ToggleField.jsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { Info } from 'lucide-react'
-
-import {
- Wrapper,
- ToggleContainer,
- StyledLabel,
- Option,
- HiddenInput,
- LabelButton,
-} from './ToggleField.styles'
-
-const ToggleField = ({
- label,
- name,
- title = '',
- options = [],
- value,
- onChange,
- inputRef,
-}) => (
-
- {label && {label} {title !== '' && } }
-
-
- {Object.entries(options).map(([key, label]) =>
-
- onChange(key)}
- ref={inputRef}
- />
- {label}
-
- )}
-
-
-)
-
-export default ToggleField
diff --git a/frontend/src/components/ToggleField/ToggleField.styles.js b/frontend/src/components/ToggleField/ToggleField.module.scss
similarity index 65%
rename from frontend/src/components/ToggleField/ToggleField.styles.js
rename to frontend/src/components/ToggleField/ToggleField.module.scss
index 39bfc78..bbc2619 100644
--- a/frontend/src/components/ToggleField/ToggleField.styles.js
+++ b/frontend/src/components/ToggleField/ToggleField.module.scss
@@ -1,11 +1,4 @@
-import { styled } from 'goober'
-import { forwardRef } from 'react'
-
-export const Wrapper = styled('div')`
- margin: 10px 0;
-`
-
-export const ToggleContainer = styled('div')`
+.toggleContainer {
display: flex;
border: 1px solid var(--primary);
border-radius: 3px;
@@ -27,26 +20,14 @@ export const ToggleContainer = styled('div')`
& > div:last-of-type label {
border-end-end-radius: 2px;
}
-`
+}
-export const StyledLabel = styled('label')`
- display: block;
- padding-bottom: 4px;
- font-size: .9rem;
-
- & svg {
- height: 1em;
- width: 1em;
- vertical-align: middle;
- }
-`
-
-export const Option = styled('div')`
+.option {
flex: 1;
position: relative;
-`
+}
-export const HiddenInput = styled('input', forwardRef)`
+.hiddenInput {
height: 0;
width: 0;
position: absolute;
@@ -59,9 +40,9 @@ export const HiddenInput = styled('input', forwardRef)`
color: var(--background);
background-color: var(--focus-color);
}
-`
+}
-export const LabelButton = styled('label')`
+.button {
padding: 6px;
display: flex;
text-align: center;
@@ -72,4 +53,4 @@ export const LabelButton = styled('label')`
align-items: center;
justify-content: center;
transition: box-shadow .15s, background-color .15s;
-`
+}
diff --git a/frontend/src/components/ToggleField/ToggleField.tsx b/frontend/src/components/ToggleField/ToggleField.tsx
new file mode 100644
index 0000000..1b0cbf8
--- /dev/null
+++ b/frontend/src/components/ToggleField/ToggleField.tsx
@@ -0,0 +1,47 @@
+import { Info } from 'lucide-react'
+
+import { Label, Wrapper } from '/src/components/Field/Field'
+
+import styles from './ToggleField.module.scss'
+
+interface ToggleFieldProps {
+ label?: React.ReactNode
+ description?: React.ReactNode
+ name: string
+ value: TValue
+ onChange: (value: TValue) => void
+ options: Record
+}
+
+const ToggleField = ({
+ label,
+ description,
+ name,
+ options,
+ value,
+ onChange,
+}: ToggleFieldProps) =>
+ {/* TODO: Better description viewer */}
+ {label &&
+ {label} {description && }
+ }
+
+
+ {Object.entries(options).map(([key, label]) =>
+
+ onChange(key as TValue)}
+ />
+ {label as React.ReactNode}
+
+ )}
+
+
+
+export default ToggleField
diff --git a/frontend/src/config/dayjs.ts b/frontend/src/config/dayjs.ts
index 0cd2d81..2df448c 100644
--- a/frontend/src/config/dayjs.ts
+++ b/frontend/src/config/dayjs.ts
@@ -1,6 +1,18 @@
import dayjs from 'dayjs'
+import customParseFormat from 'dayjs/plugin/customParseFormat'
+import isToday from 'dayjs/plugin/isToday'
+import localeData from 'dayjs/plugin/localeData'
import relativeTime from 'dayjs/plugin/relativeTime'
+import timezone from 'dayjs/plugin/timezone'
+import updateLocale from 'dayjs/plugin/updateLocale'
+import utc from 'dayjs/plugin/utc'
+dayjs.extend(customParseFormat)
+dayjs.extend(isToday)
+dayjs.extend(localeData)
dayjs.extend(relativeTime)
+dayjs.extend(timezone)
+dayjs.extend(updateLocale)
+dayjs.extend(utc)
export default dayjs
diff --git a/frontend/src/i18n/locales/en/home.json b/frontend/src/i18n/locales/en/home.json
index 689b637..468d467 100644
--- a/frontend/src/i18n/locales/en/home.json
+++ b/frontend/src/i18n/locales/en/home.json
@@ -1,10 +1,6 @@
{
"create": "CREATE A",
"recently_visited": "Recently visited",
- "nav": {
- "about": "About",
- "donate": "Donate"
- },
"form": {
"name": {
"label": "Give your event a name!",
@@ -40,7 +36,6 @@
"unknown": "Something went wrong. Please try again later."
}
},
- "offline": "You can't create a Crab Fit when you don't have an internet connection. Please make sure you're connected.",
"about": {
"name": "About Crab Fit",
diff --git a/frontend/src/pages-old/Home/Home.jsx b/frontend/src/pages-old/Home/Home.jsx
deleted file mode 100644
index 86051f2..0000000
--- a/frontend/src/pages-old/Home/Home.jsx
+++ /dev/null
@@ -1,301 +0,0 @@
-import dayjs from 'dayjs'
-import customParseFormat from 'dayjs/plugin/customParseFormat'
-import timezone from 'dayjs/plugin/timezone'
-import utc from 'dayjs/plugin/utc'
-import { useEffect, useState } from 'react'
-import { useForm } from 'react-hook-form'
-import { Trans,useTranslation } from 'react-i18next'
-import { Link,useNavigate } from 'react-router-dom'
-
-import {
- Button,
- CalendarField,
- Center,
- Error,
- Footer,
- Recents,
- SelectField,
- TextField,
- TimeRangeField,
-} from '/src/components'
-import logo from '/src/res/logo.svg'
-import timezones from '/src/res/timezones.json'
-import video_thumb from '/src/res/video_thumb.jpg'
-import api from '/src/services'
-import { useTWAStore } from '/src/stores'
-import { detect_browser } from '/src/utils'
-
-import {
- AboutSection,
- ButtonArea,
- CreateForm,
- Links,
- Logo,
- OfflineMessage,
- P,
- Stat,
- StatLabel,
- StatNumber,
- Stats,
- StyledMain,
- TitleLarge,
- TitleSmall,
- VideoLink,
- VideoWrapper,
-} from './Home.styles'
-
-dayjs.extend(utc)
-dayjs.extend(timezone)
-dayjs.extend(customParseFormat)
-
-const Home = ({ offline }) => {
- const { register, handleSubmit, setValue } = useForm({
- defaultValues: {
- timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
- },
- })
- const [isLoading, setIsLoading] = useState(false)
- const [error, setError] = useState(null)
- const [stats, setStats] = useState({
- eventCount: null,
- personCount: null,
- version: 'loading...',
- })
- const [browser, setBrowser] = useState(undefined)
- const [videoPlay, setVideoPlay] = useState(false)
- const navigate = useNavigate()
- const { t } = useTranslation(['common', 'home'])
- const isTWA = useTWAStore(state => state.TWA)
-
- useEffect(() => {
- const fetch = async () => {
- try {
- const response = await api.get('/stats')
- setStats(response)
- } catch (e) {
- console.error(e)
- }
- }
-
- fetch()
- document.title = 'Crab Fit'
- setBrowser(detect_browser())
- }, [])
-
- 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 response = await api.post('/event', {
- event: {
- name: data.name,
- times: times,
- timezone: data.timezone,
- },
- })
- navigate(`/${response.id}`)
- gtag('event', 'create_event', {
- 'event_category': 'home',
- })
- } catch (e) {
- setError(t('home:form.errors.unknown'))
- console.error(e)
- } finally {
- setIsLoading(false)
- }
- }
-
- return (
- <>
-
-
-
-
- {t('home:create')}
- CRAB FIT
-
- {t('home:nav.about')} / {t('home:nav.donate')}
-
-
-
-
-
-
- {offline ? (
-
- 🦀📵
- {t('home:offline')}
-
- ) : (
-
-
-
-
-
-
-
-
-
- setError(null)}>{error}
-
-
- {t('home:form.button')}
-
-
- )}
-
-
-
-
- {t('home:about.name')}
-
-
- {new Intl.NumberFormat().format(stats.eventCount ?? 7000)}{!stats.eventCount && '+'}
- {t('home:about.events')}
-
-
- {new Intl.NumberFormat().format(stats.personCount ?? 25000)}{!stats.personCount && '+'}
- {t('home:about.availabilities')}
-
-
- Crab Fit helps you fit your event around everyone's schedules. Simply create an event above and send the link to everyone that is participating. Results update live and you will be able to see a heat-map of when everyone is free. Learn more about how to Crab Fit.
-
- {videoPlay ? (
-
- VIDEO
-
- ) : (
- {
- e.preventDefault()
- setVideoPlay(true)
- }}
- >
-
- {t('common:video.button')}
-
- )}
-
- {!document.referrer.includes('android-app://fit.crab') && (
-
- {['chrome', 'firefox', 'safari'].includes(browser) && (
- ,
- firefox: ,
- safari: ,
- }[browser]}
- onClick={() => gtag('event', `download_extension_${browser}`, { 'event_category': 'home'})}
- target="_blank"
- rel="noreferrer noopener"
- secondary
- >{{
- chrome: t('home:about.chrome_extension'),
- firefox: t('home:about.firefox_extension'),
- safari: t('home:about.safari_extension'),
- }[browser]}
- )}
- }
- onClick={() => gtag('event', 'download_android_app', { 'event_category': 'home' })}
- target="_blank"
- rel="noreferrer noopener"
- secondary
- >{t('home:about.android_app')}
-
- )}
- Created by Ben Grant , Crab Fit is the modern-day solution to your group event planning debates.
- The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the repository . By using Crab Fit you agree to the privacy policy.
- {t('home:about.content.p6')}
- {t('home:about.content.p5')}
-
-
-
-
- >
- )
-}
-
-export default Home
diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts
index 497ba64..6dcd993 100644
--- a/frontend/src/stores/index.ts
+++ b/frontend/src/stores/index.ts
@@ -1,11 +1,5 @@
import { useEffect, useState } from 'react'
-// export { default as useSettingsStore } from './settingsStore'
-export { default as useRecentsStore } from './recentsStore'
-// export { default as useTWAStore } from './twaStore'
-// export { default as useLocaleUpdateStore } from './localeUpdateStore'
-// export { default as useTranslateStore } from './translateStore'
-
/** Helper to use a persisted store in zustand with Next js without causing a hydration error */
export const useStore = (
store: (callback?: (state: T) => unknown) => unknown,
diff --git a/frontend/src/stores/settingsStore.ts b/frontend/src/stores/settingsStore.ts
index 57991b6..a6d89be 100644
--- a/frontend/src/stores/settingsStore.ts
+++ b/frontend/src/stores/settingsStore.ts
@@ -5,13 +5,13 @@ type TimeFormat = '12h' | '24h'
type Theme = 'System' | 'Light' | 'Dark'
interface SettingsStore {
- weekStart: number
+ weekStart: 0 | 1
timeFormat: TimeFormat
theme: Theme
highlight: boolean
colormap: string
- setWeekStart: (weekStart: number) => void
+ setWeekStart: (weekStart: 0 | 1) => void
setTimeFormat: (timeFormat: TimeFormat) => void
setTheme: (theme: Theme) => void
setHighlight: (highlight: boolean) => void