Set up API spec and basic components

This commit is contained in:
Ben Grant 2023-05-20 01:52:44 +10:00
parent 2adecd13f7
commit 61bd31eb7e
20 changed files with 353 additions and 26 deletions

View file

@ -0,0 +1,5 @@
.content {
width: 600px;
margin: 20px auto;
max-width: calc(100% - 60px);
}

View 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

View file

@ -1,4 +1,4 @@
import { Button } from '/src/components'
import Button from '/src/components/Button/Button'
import { useTranslation } from '/src/i18n/server'
import { makeClass } from '/src/utils'

View file

@ -0,0 +1,4 @@
.p {
font-weight: 500;
line-height: 1.6em;
}

View 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

View file

@ -2,6 +2,8 @@
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 { useTranslation } from '/src/i18n/client'
import { useRecentsStore, useStore } from '/src/stores'
@ -16,8 +18,8 @@ const Recents = ({ target }: RecentsProps) => {
const recents = useStore(useRecentsStore, state => state.recents)
const { t } = useTranslation(['home', 'common'])
return recents?.length ? <section id="recents">
<div>
return recents?.length ? <Section id="recents">
<Content>
<h2>{t('home:recently_visited')}</h2>
{recents.map(event => (
<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>
</Link>
))}
</div>
</section> : null
</Content>
</Section> : null
}
export default Recents

View file

@ -0,0 +1,9 @@
.section {
margin: 30px 0 0;
background-color: var(--surface);
padding: 20px 0;
& a {
color: var(--secondary);
}
}

View 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

View 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;
}

View 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

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

View 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

View file

@ -4,19 +4,14 @@
// export { default as TimeRangeField } from './TimeRangeField/TimeRangeField'
// export { default as ToggleField } from './ToggleField/ToggleField'
export { default as Button } from './Button/Button'
// export { default as Legend } from './Legend/Legend'
// export { default as AvailabilityViewer } from './AvailabilityViewer/AvailabilityViewer'
// export { default as AvailabilityEditor } from './AvailabilityEditor/AvailabilityEditor'
export { default as Error } from './Error/Error'
// export { default as Loading } from './Loading/Loading'
// export { default as Center } from './Center/Center'
// export { default as Settings } from './Settings/Settings'
// 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 const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar')