Set up API spec and basic components
This commit is contained in:
parent
2adecd13f7
commit
61bd31eb7e
1
frontend/.env.local
Normal file
1
frontend/.env.local
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
NEXT_PUBLIC_API_URL="http://127.0.0.1:3000"
|
||||||
|
|
@ -27,6 +27,7 @@
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hook-form": "^7.43.9",
|
"react-hook-form": "^7.43.9",
|
||||||
"react-i18next": "^12.3.1",
|
"react-i18next": "^12.3.1",
|
||||||
|
"zod": "^3.21.4",
|
||||||
"zustand": "^4.3.8"
|
"zustand": "^4.3.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,15 @@
|
||||||
import { Button, Footer, Header, Recents } from '/src/components'
|
import { Trans } from 'react-i18next/TransWithoutContext'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
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 { default as P } from '/src/components/Paragraph/Paragraph'
|
||||||
|
import Recents from '/src/components/Recents/Recents'
|
||||||
|
import Section from '/src/components/Section/Section'
|
||||||
|
import Stats from '/src/components/Stats/Stats'
|
||||||
|
import Video from '/src/components/Video/Video'
|
||||||
import { useTranslation } from '/src/i18n/server'
|
import { useTranslation } from '/src/i18n/server'
|
||||||
|
|
||||||
import styles from './home.module.scss'
|
import styles from './home.module.scss'
|
||||||
|
|
@ -6,19 +17,82 @@ import styles from './home.module.scss'
|
||||||
const Page = async () => {
|
const Page = async () => {
|
||||||
const { t } = await useTranslation('home')
|
const { t } = await useTranslation('home')
|
||||||
|
|
||||||
return <div>
|
return <>
|
||||||
|
<Content>
|
||||||
|
{/* @ts-expect-error Async Server Component */}
|
||||||
<Header isFull />
|
<Header isFull />
|
||||||
|
|
||||||
<nav className={styles.nav}>
|
<nav className={styles.nav}>
|
||||||
<a href="#about">{t('home:nav.about')}</a>
|
<a href="#about">{t('nav.about')}</a>
|
||||||
{' / '}
|
{' / '}
|
||||||
<a href="#donate">{t('home:nav.donate')}</a>
|
<a href="#donate">{t('nav.donate')}</a>
|
||||||
</nav>
|
</nav>
|
||||||
|
</Content>
|
||||||
|
|
||||||
<Recents />
|
<Recents />
|
||||||
<Button>Hey there!</Button>
|
|
||||||
|
<Content>
|
||||||
|
<span>Form here</span>
|
||||||
|
<Button>Create</Button>
|
||||||
|
</Content>
|
||||||
|
|
||||||
|
<Section id="about">
|
||||||
|
<Content>
|
||||||
|
<h2>{t('about.name')}</h2>
|
||||||
|
|
||||||
|
{/* @ts-expect-error Async Server Component */}
|
||||||
|
<Stats />
|
||||||
|
|
||||||
|
<P><Trans i18nKey="home:about.content.p1" t={t}>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.<br /><Link href="/how-to" rel="help">Learn more about how to Crab Fit</Link>.</Trans></P>
|
||||||
|
|
||||||
|
<Video />
|
||||||
|
|
||||||
|
{/*
|
||||||
|
{!document.referrer.includes('android-app://fit.crab') && (
|
||||||
|
<ButtonArea>
|
||||||
|
{['chrome', 'firefox', 'safari'].includes(browser) && (
|
||||||
|
<Button
|
||||||
|
href={{
|
||||||
|
chrome: 'https://chrome.google.com/webstore/detail/crab-fit/pnafiibmjbiljofcpjlbonpgdofjhhkj',
|
||||||
|
firefox: 'https://addons.mozilla.org/en-US/firefox/addon/crab-fit/',
|
||||||
|
safari: 'https://apps.apple.com/us/app/crab-fit/id1570803259',
|
||||||
|
}[browser]}
|
||||||
|
icon={{
|
||||||
|
chrome: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M12,20L15.46,14H15.45C15.79,13.4 16,12.73 16,12C16,10.8 15.46,9.73 14.62,9H19.41C19.79,9.93 20,10.94 20,12A8,8 0 0,1 12,20M4,12C4,10.54 4.39,9.18 5.07,8L8.54,14H8.55C9.24,15.19 10.5,16 12,16C12.45,16 12.88,15.91 13.29,15.77L10.89,19.91C7,19.37 4,16.04 4,12M15,12A3,3 0 0,1 12,15A3,3 0 0,1 9,12A3,3 0 0,1 12,9A3,3 0 0,1 15,12M12,4C14.96,4 17.54,5.61 18.92,8H12C10.06,8 8.45,9.38 8.08,11.21L5.7,7.08C7.16,5.21 9.44,4 12,4M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2Z" /></svg>,
|
||||||
|
firefox: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M9.27 7.94C9.27 7.94 9.27 7.94 9.27 7.94M6.85 6.74C6.86 6.74 6.86 6.74 6.85 6.74M21.28 8.6C20.85 7.55 19.96 6.42 19.27 6.06C19.83 7.17 20.16 8.28 20.29 9.1L20.29 9.12C19.16 6.3 17.24 5.16 15.67 2.68C15.59 2.56 15.5 2.43 15.43 2.3C15.39 2.23 15.36 2.16 15.32 2.09C15.26 1.96 15.2 1.83 15.17 1.69C15.17 1.68 15.16 1.67 15.15 1.67H15.13L15.12 1.67L15.12 1.67L15.12 1.67C12.9 2.97 11.97 5.26 11.74 6.71C11.05 6.75 10.37 6.92 9.75 7.22C9.63 7.27 9.58 7.41 9.62 7.53C9.67 7.67 9.83 7.74 9.96 7.68C10.5 7.42 11.1 7.27 11.7 7.23L11.75 7.23C11.83 7.22 11.92 7.22 12 7.22C12.5 7.21 12.97 7.28 13.44 7.42L13.5 7.44C13.6 7.46 13.67 7.5 13.75 7.5C13.8 7.54 13.86 7.56 13.91 7.58L14.05 7.64C14.12 7.67 14.19 7.7 14.25 7.73C14.28 7.75 14.31 7.76 14.34 7.78C14.41 7.82 14.5 7.85 14.54 7.89C14.58 7.91 14.62 7.94 14.66 7.96C15.39 8.41 16 9.03 16.41 9.77C15.88 9.4 14.92 9.03 14 9.19C17.6 11 16.63 17.19 11.64 16.95C11.2 16.94 10.76 16.85 10.34 16.7C10.24 16.67 10.14 16.63 10.05 16.58C10 16.56 9.93 16.53 9.88 16.5C8.65 15.87 7.64 14.68 7.5 13.23C7.5 13.23 8 11.5 10.83 11.5C11.14 11.5 12 10.64 12.03 10.4C12.03 10.31 10.29 9.62 9.61 8.95C9.24 8.59 9.07 8.42 8.92 8.29C8.84 8.22 8.75 8.16 8.66 8.1C8.43 7.3 8.42 6.45 8.63 5.65C7.6 6.12 6.8 6.86 6.22 7.5H6.22C5.82 7 5.85 5.35 5.87 5C5.86 5 5.57 5.16 5.54 5.18C5.19 5.43 4.86 5.71 4.56 6C4.21 6.37 3.9 6.74 3.62 7.14C3 8.05 2.5 9.09 2.28 10.18C2.28 10.19 2.18 10.59 2.11 11.1L2.08 11.33C2.06 11.5 2.04 11.65 2 11.91L2 11.94L2 12.27L2 12.32C2 17.85 6.5 22.33 12 22.33C16.97 22.33 21.08 18.74 21.88 14C21.9 13.89 21.91 13.76 21.93 13.63C22.13 11.91 21.91 10.11 21.28 8.6Z" /></svg>,
|
||||||
|
safari: <svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12C4,14.09 4.8,16 6.11,17.41L9.88,9.88L17.41,6.11C16,4.8 14.09,4 12,4M12,20A8,8 0 0,0 20,12C20,9.91 19.2,8 17.89,6.59L14.12,14.12L6.59,17.89C8,19.2 9.91,20 12,20M12,12L11.23,11.23L9.7,14.3L12.77,12.77L12,12M12,17.5H13V19H12V17.5M15.88,15.89L16.59,15.18L17.65,16.24L16.94,16.95L15.88,15.89M17.5,12V11H19V12H17.5M12,6.5H11V5H12V6.5M8.12,8.11L7.41,8.82L6.35,7.76L7.06,7.05L8.12,8.11M6.5,12V13H5V12H6.5Z" /></svg>,
|
||||||
|
}[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]}</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
href="https://play.google.com/store/apps/details?id=fit.crab"
|
||||||
|
icon={<svg aria-hidden="true" focusable="false" viewBox="0 0 24 24"><path fill="currentColor" d="M3,20.5V3.5C3,2.91 3.34,2.39 3.84,2.15L13.69,12L3.84,21.85C3.34,21.6 3,21.09 3,20.5M16.81,15.12L6.05,21.34L14.54,12.85L16.81,15.12M20.16,10.81C20.5,11.08 20.75,11.5 20.75,12C20.75,12.5 20.53,12.9 20.18,13.18L17.89,14.5L15.39,12L17.89,9.5L20.16,10.81M6.05,2.66L16.81,8.88L14.54,11.15L6.05,2.66Z" /></svg>}
|
||||||
|
onClick={() => gtag('event', 'download_android_app', { 'event_category': 'home' })}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer noopener"
|
||||||
|
secondary
|
||||||
|
>{t('home:about.android_app')}</Button>
|
||||||
|
</ButtonArea>
|
||||||
|
)} */}
|
||||||
|
|
||||||
|
<P><Trans i18nKey="about.content.p3" t={t}>Created by <a href="https://bengrant.dev" target="_blank" rel="noreferrer noopener author">Ben Grant</a>, Crab Fit is the modern-day solution to your group event planning debates.</Trans></P>
|
||||||
|
<P><Trans i18nKey="about.content.p4" t={t}>The code for Crab Fit is open source, if you find any issues or want to contribute, you can visit the <a href="https://github.com/GRA0007/crab.fit" target="_blank" rel="noreferrer noopener">repository</a>. By using Crab Fit you agree to the <Link href="/privacy" rel="license">privacy policy</Link>.</Trans></P>
|
||||||
|
<P>{t('about.content.p6')}</P>
|
||||||
|
<P>{t('about.content.p5')}</P>
|
||||||
|
</Content>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* @ts-expect-error Async Server Component */}
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</>
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Page
|
export default Page
|
||||||
|
|
|
||||||
5
frontend/src/components/Content/Content.module.scss
Normal file
5
frontend/src/components/Content/Content.module.scss
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
.content {
|
||||||
|
width: 600px;
|
||||||
|
margin: 20px auto;
|
||||||
|
max-width: calc(100% - 60px);
|
||||||
|
}
|
||||||
10
frontend/src/components/Content/Content.tsx
Normal file
10
frontend/src/components/Content/Content.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import styles from './Content.module.scss'
|
||||||
|
|
||||||
|
interface ContentProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Content = (props: ContentProps) =>
|
||||||
|
<div className={styles.content} {...props} />
|
||||||
|
|
||||||
|
export default Content
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { Button } from '/src/components'
|
import Button from '/src/components/Button/Button'
|
||||||
import { useTranslation } from '/src/i18n/server'
|
import { useTranslation } from '/src/i18n/server'
|
||||||
import { makeClass } from '/src/utils'
|
import { makeClass } from '/src/utils'
|
||||||
|
|
||||||
|
|
|
||||||
4
frontend/src/components/Paragraph/Paragraph.module.scss
Normal file
4
frontend/src/components/Paragraph/Paragraph.module.scss
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
.p {
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 1.6em;
|
||||||
|
}
|
||||||
10
frontend/src/components/Paragraph/Paragraph.tsx
Normal file
10
frontend/src/components/Paragraph/Paragraph.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import styles from './Paragraph.module.scss'
|
||||||
|
|
||||||
|
interface ParagraphProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
const Paragraph = (props: ParagraphProps) =>
|
||||||
|
<p className={styles.p} {...props} />
|
||||||
|
|
||||||
|
export default Paragraph
|
||||||
|
|
@ -2,6 +2,8 @@
|
||||||
|
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
|
|
||||||
|
import Content from '/src/components/Content/Content'
|
||||||
|
import Section from '/src/components/Section/Section'
|
||||||
import dayjs from '/src/config/dayjs'
|
import dayjs from '/src/config/dayjs'
|
||||||
import { useTranslation } from '/src/i18n/client'
|
import { useTranslation } from '/src/i18n/client'
|
||||||
import { useRecentsStore, useStore } from '/src/stores'
|
import { useRecentsStore, useStore } from '/src/stores'
|
||||||
|
|
@ -16,8 +18,8 @@ const Recents = ({ target }: RecentsProps) => {
|
||||||
const recents = useStore(useRecentsStore, state => state.recents)
|
const recents = useStore(useRecentsStore, state => state.recents)
|
||||||
const { t } = useTranslation(['home', 'common'])
|
const { t } = useTranslation(['home', 'common'])
|
||||||
|
|
||||||
return recents?.length ? <section id="recents">
|
return recents?.length ? <Section id="recents">
|
||||||
<div>
|
<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}`} target={target} key={event.id}>
|
||||||
|
|
@ -28,8 +30,8 @@ const Recents = ({ target }: RecentsProps) => {
|
||||||
>{t('common:created', { date: dayjs.unix(event.created_at).fromNow() })}</span>
|
>{t('common:created', { date: dayjs.unix(event.created_at).fromNow() })}</span>
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</Content>
|
||||||
</section> : null
|
</Section> : null
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Recents
|
export default Recents
|
||||||
|
|
|
||||||
9
frontend/src/components/Section/Section.module.scss
Normal file
9
frontend/src/components/Section/Section.module.scss
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
.section {
|
||||||
|
margin: 30px 0 0;
|
||||||
|
background-color: var(--surface);
|
||||||
|
padding: 20px 0;
|
||||||
|
|
||||||
|
& a {
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
11
frontend/src/components/Section/Section.tsx
Normal file
11
frontend/src/components/Section/Section.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import styles from './Section.module.scss'
|
||||||
|
|
||||||
|
interface SectionProps {
|
||||||
|
children: React.ReactNode
|
||||||
|
id?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const Section = (props: SectionProps) =>
|
||||||
|
<section className={styles.section} {...props} />
|
||||||
|
|
||||||
|
export default Section
|
||||||
24
frontend/src/components/Stats/Stats.module.scss
Normal file
24
frontend/src/components/Stats/Stats.module.scss
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-around;
|
||||||
|
align-items: flex-start;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
text-align: center;
|
||||||
|
padding: 0 6px;
|
||||||
|
min-width: 160px;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.number {
|
||||||
|
display: block;
|
||||||
|
font-weight: 900;
|
||||||
|
color: var(--secondary);
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.label {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
33
frontend/src/components/Stats/Stats.tsx
Normal file
33
frontend/src/components/Stats/Stats.tsx
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { API_BASE, StatsResponse } from '/src/config/api'
|
||||||
|
import { useTranslation } from '/src/i18n/server'
|
||||||
|
|
||||||
|
import styles from './Stats.module.scss'
|
||||||
|
|
||||||
|
const getStats = async () => {
|
||||||
|
const res = await fetch(new URL('/stats', API_BASE))
|
||||||
|
.catch(console.warn)
|
||||||
|
if (!res?.ok) return
|
||||||
|
return StatsResponse.parse(await res.json())
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stats = async () => {
|
||||||
|
const stats = await getStats()
|
||||||
|
const { t } = await useTranslation('home')
|
||||||
|
|
||||||
|
return <div className={styles.wrapper}>
|
||||||
|
<div>
|
||||||
|
<span className={styles.number}>
|
||||||
|
{new Intl.NumberFormat().format(stats?.event_count || 17000)}{!stats?.event_count && '+'}
|
||||||
|
</span>
|
||||||
|
<span className={styles.label}>{t('about.events')}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span className={styles.number}>
|
||||||
|
{new Intl.NumberFormat().format(stats?.person_count || 65000)}{!stats?.person_count && '+'}
|
||||||
|
</span>
|
||||||
|
<span className={styles.label}>{t('about.availabilities')}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Stats
|
||||||
64
frontend/src/components/Video/Video.module.scss
Normal file
64
frontend/src/components/Video/Video.module.scss
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
.videoWrapper {
|
||||||
|
margin: 0 auto;
|
||||||
|
position: relative;
|
||||||
|
padding-bottom: 56.4%;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
iframe {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
border-radius: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
display: block;
|
||||||
|
text-decoration: none;
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 400px;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: transform .15s;
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
border-radius: 10px;
|
||||||
|
background-color: #CCC;
|
||||||
|
}
|
||||||
|
span {
|
||||||
|
color: #FFFFFF;
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
text-shadow: 0 0 20px rgba(0,0,0,.8);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
height: 2em;
|
||||||
|
width: 2em;
|
||||||
|
background: currentColor;
|
||||||
|
border-radius: 100%;
|
||||||
|
margin: 0 auto .4em;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='24' viewBox='0 0 24 24' fill='none' stroke='%23F79E00' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' class='feather feather-play'%3E%3Cpolygon points='5 3 19 12 5 21 5 3'%3E%3C/polygon%3E%3C/svg%3E");
|
||||||
|
background-position: center;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: 1em;
|
||||||
|
box-shadow: 0 0 20px 0 rgba(0,0,0,.3);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
frontend/src/components/Video/Video.tsx
Normal file
43
frontend/src/components/Video/Video.tsx
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
'use client'
|
||||||
|
|
||||||
|
import { useState } from 'react'
|
||||||
|
|
||||||
|
import { useTranslation } from '/src/i18n/client'
|
||||||
|
import video_thumb from '/src/res/video_thumb.jpg'
|
||||||
|
|
||||||
|
import styles from './Video.module.scss'
|
||||||
|
|
||||||
|
const Video = () => {
|
||||||
|
const { t } = useTranslation('common')
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false)
|
||||||
|
|
||||||
|
return isPlaying ? (
|
||||||
|
<div className={styles.videoWrapper}>
|
||||||
|
<iframe
|
||||||
|
width="560"
|
||||||
|
height="315"
|
||||||
|
src="https://www.youtube.com/embed/yXGd4VXZzcY?modestbranding=1&rel=0&autoplay=1"
|
||||||
|
title={t<string>('video.title')}
|
||||||
|
frameBorder="0"
|
||||||
|
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<a
|
||||||
|
className={styles.preview}
|
||||||
|
href="https://www.youtube.com/watch?v=yXGd4VXZzcY"
|
||||||
|
target="_blank"
|
||||||
|
rel="nofollow noreferrer"
|
||||||
|
onClick={e => {
|
||||||
|
e.preventDefault()
|
||||||
|
setIsPlaying(true)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<img src={video_thumb.src} alt={t<string>('video.button')} />
|
||||||
|
<span>{t('video.button')}</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Video
|
||||||
|
|
@ -4,19 +4,14 @@
|
||||||
// export { default as TimeRangeField } from './TimeRangeField/TimeRangeField'
|
// export { default as TimeRangeField } from './TimeRangeField/TimeRangeField'
|
||||||
// export { default as ToggleField } from './ToggleField/ToggleField'
|
// export { default as ToggleField } from './ToggleField/ToggleField'
|
||||||
|
|
||||||
export { default as Button } from './Button/Button'
|
|
||||||
// export { default as Legend } from './Legend/Legend'
|
// export { default as Legend } from './Legend/Legend'
|
||||||
// export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer'
|
// export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer'
|
||||||
// export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor'
|
// export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor'
|
||||||
export { default as Error } from './Error/Error'
|
|
||||||
// export { default as Loading } from './Loading/Loading'
|
// export { default as Loading } from './Loading/Loading'
|
||||||
|
|
||||||
// export { default as Center } from './Center/Center'
|
// export { default as Center } from './Center/Center'
|
||||||
// export { default as Settings } from './Settings/Settings'
|
// export { default as Settings } from './Settings/Settings'
|
||||||
// export { default as Egg } from './Egg/Egg'
|
// export { default as Egg } from './Egg/Egg'
|
||||||
export { default as Footer } from './Footer/Footer'
|
|
||||||
export { default as Recents } from './Recents/Recents'
|
|
||||||
export { default as Header } from './Header/Header'
|
|
||||||
// export { default as TranslateDialog } from './TranslateDialog/TranslateDialog'
|
// export { default as TranslateDialog } from './TranslateDialog/TranslateDialog'
|
||||||
|
|
||||||
// export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar')
|
// export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar')
|
||||||
|
|
|
||||||
44
frontend/src/config/api.ts
Normal file
44
frontend/src/config/api.ts
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
// TODO: Write a simple rust crate that generates these from the OpenAPI spec
|
||||||
|
|
||||||
|
import { z } from 'zod'
|
||||||
|
|
||||||
|
if (process.env.NEXT_PUBLIC_API_URL === undefined) {
|
||||||
|
throw new Error('Expected API url environment variable')
|
||||||
|
}
|
||||||
|
|
||||||
|
export const API_BASE = new URL(process.env.NEXT_PUBLIC_API_URL)
|
||||||
|
|
||||||
|
export const EventInput = z.object({
|
||||||
|
name: z.string().optional(),
|
||||||
|
times: z.string().array(),
|
||||||
|
timezone: z.string(),
|
||||||
|
})
|
||||||
|
export type EventInput = z.infer<typeof EventInput>
|
||||||
|
|
||||||
|
export const EventResponse = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
times: z.string().array(),
|
||||||
|
timezone: z.string(),
|
||||||
|
created_at: z.number(),
|
||||||
|
})
|
||||||
|
export type EventResponse = z.infer<typeof EventResponse>
|
||||||
|
|
||||||
|
export const PersonInput = z.object({
|
||||||
|
availability: z.string().array(),
|
||||||
|
})
|
||||||
|
export type PersonInput = z.infer<typeof PersonInput>
|
||||||
|
|
||||||
|
export const PersonResponse = z.object({
|
||||||
|
name: z.string(),
|
||||||
|
availability: z.string().array(),
|
||||||
|
created_at: z.number(),
|
||||||
|
})
|
||||||
|
export type PersonResponse = z.infer<typeof PersonResponse>
|
||||||
|
|
||||||
|
export const StatsResponse = z.object({
|
||||||
|
event_count: z.number(),
|
||||||
|
person_count: z.number(),
|
||||||
|
version: z.string(),
|
||||||
|
})
|
||||||
|
export type StatsResponse = z.infer<typeof StatsResponse>
|
||||||
|
|
@ -15,7 +15,7 @@ export const getOptions = (lng = fallbackLng, ns: InitOptions['ns'] = defaultNS)
|
||||||
ns,
|
ns,
|
||||||
fallbackNS: defaultNS,
|
fallbackNS: defaultNS,
|
||||||
defaultNS,
|
defaultNS,
|
||||||
debug: process.env.NODE_ENV !== 'production',
|
// debug: true,
|
||||||
interpolation: {
|
interpolation: {
|
||||||
escapeValue: false,
|
escapeValue: false,
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,7 @@ const initI18next = async (language: string, ns: string | string []) => {
|
||||||
.use(resourcesToBackend((language: string, namespace: string) =>
|
.use(resourcesToBackend((language: string, namespace: string) =>
|
||||||
import(`./locales/${language}/${namespace}.json`)
|
import(`./locales/${language}/${namespace}.json`)
|
||||||
))
|
))
|
||||||
.init({
|
.init(getOptions(language, ns))
|
||||||
...getOptions(language, ns),
|
|
||||||
debug: false,
|
|
||||||
})
|
|
||||||
return i18nInstance
|
return i18nInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2802,7 +2802,7 @@ yocto-queue@^0.1.0:
|
||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
|
||||||
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
|
||||||
|
|
||||||
zod@3.21.4:
|
zod@3.21.4, zod@^3.21.4:
|
||||||
version "3.21.4"
|
version "3.21.4"
|
||||||
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
|
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
|
||||||
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
|
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue