Set up API spec and basic components
This commit is contained in:
parent
2adecd13f7
commit
61bd31eb7e
20 changed files with 353 additions and 26 deletions
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 { 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 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
|
||||
|
|
|
|||
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 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')
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue