From 61bd31eb7e4564d43fe1abd499db5188ca2fd08d Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Sat, 20 May 2023 01:52:44 +1000 Subject: [PATCH] Set up API spec and basic components --- frontend/.env.local | 1 + frontend/package.json | 1 + frontend/src/app/page.tsx | 94 +++++++++++++++++-- .../components/Content/Content.module.scss | 5 + frontend/src/components/Content/Content.tsx | 10 ++ frontend/src/components/Footer/Footer.tsx | 2 +- .../Paragraph/Paragraph.module.scss | 4 + .../src/components/Paragraph/Paragraph.tsx | 10 ++ frontend/src/components/Recents/Recents.tsx | 10 +- .../components/Section/Section.module.scss | 9 ++ frontend/src/components/Section/Section.tsx | 11 +++ .../src/components/Stats/Stats.module.scss | 24 +++++ frontend/src/components/Stats/Stats.tsx | 33 +++++++ .../src/components/Video/Video.module.scss | 64 +++++++++++++ frontend/src/components/Video/Video.tsx | 43 +++++++++ frontend/src/components/index.ts | 5 - frontend/src/config/api.ts | 44 +++++++++ frontend/src/i18n/options.ts | 2 +- frontend/src/i18n/server.ts | 5 +- frontend/yarn.lock | 2 +- 20 files changed, 353 insertions(+), 26 deletions(-) create mode 100644 frontend/.env.local create mode 100644 frontend/src/components/Content/Content.module.scss create mode 100644 frontend/src/components/Content/Content.tsx create mode 100644 frontend/src/components/Paragraph/Paragraph.module.scss create mode 100644 frontend/src/components/Paragraph/Paragraph.tsx create mode 100644 frontend/src/components/Section/Section.module.scss create mode 100644 frontend/src/components/Section/Section.tsx create mode 100644 frontend/src/components/Stats/Stats.module.scss create mode 100644 frontend/src/components/Stats/Stats.tsx create mode 100644 frontend/src/components/Video/Video.module.scss create mode 100644 frontend/src/components/Video/Video.tsx create mode 100644 frontend/src/config/api.ts diff --git a/frontend/.env.local b/frontend/.env.local new file mode 100644 index 0000000..5484c2c --- /dev/null +++ b/frontend/.env.local @@ -0,0 +1 @@ +NEXT_PUBLIC_API_URL="http://127.0.0.1:3000" diff --git a/frontend/package.json b/frontend/package.json index 9057510..8f61d33 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,6 +27,7 @@ "react-dom": "^18.2.0", "react-hook-form": "^7.43.9", "react-i18next": "^12.3.1", + "zod": "^3.21.4", "zustand": "^4.3.8" }, "devDependencies": { diff --git a/frontend/src/app/page.tsx b/frontend/src/app/page.tsx index 85deea2..1cb8b63 100644 --- a/frontend/src/app/page.tsx +++ b/frontend/src/app/page.tsx @@ -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 styles from './home.module.scss' @@ -6,19 +17,82 @@ import styles from './home.module.scss' const Page = async () => { const { t } = await useTranslation('home') - return
-
+ return <> + + {/* @ts-expect-error Async Server Component */} +
- + + - + + + Form here + + + +
+ +

{t('about.name')}

+ + {/* @ts-expect-error Async Server Component */} + + +

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.

+ +
+
+ + {/* @ts-expect-error Async Server Component */}
-
+ } export default Page diff --git a/frontend/src/components/Content/Content.module.scss b/frontend/src/components/Content/Content.module.scss new file mode 100644 index 0000000..53d571b --- /dev/null +++ b/frontend/src/components/Content/Content.module.scss @@ -0,0 +1,5 @@ +.content { + width: 600px; + margin: 20px auto; + max-width: calc(100% - 60px); +} diff --git a/frontend/src/components/Content/Content.tsx b/frontend/src/components/Content/Content.tsx new file mode 100644 index 0000000..0da66bd --- /dev/null +++ b/frontend/src/components/Content/Content.tsx @@ -0,0 +1,10 @@ +import styles from './Content.module.scss' + +interface ContentProps { + children: React.ReactNode +} + +const Content = (props: ContentProps) => +
+ +export default Content diff --git a/frontend/src/components/Footer/Footer.tsx b/frontend/src/components/Footer/Footer.tsx index ef5eb6c..23bcff4 100644 --- a/frontend/src/components/Footer/Footer.tsx +++ b/frontend/src/components/Footer/Footer.tsx @@ -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' diff --git a/frontend/src/components/Paragraph/Paragraph.module.scss b/frontend/src/components/Paragraph/Paragraph.module.scss new file mode 100644 index 0000000..1b87409 --- /dev/null +++ b/frontend/src/components/Paragraph/Paragraph.module.scss @@ -0,0 +1,4 @@ +.p { + font-weight: 500; + line-height: 1.6em; +} diff --git a/frontend/src/components/Paragraph/Paragraph.tsx b/frontend/src/components/Paragraph/Paragraph.tsx new file mode 100644 index 0000000..16a84fa --- /dev/null +++ b/frontend/src/components/Paragraph/Paragraph.tsx @@ -0,0 +1,10 @@ +import styles from './Paragraph.module.scss' + +interface ParagraphProps { + children: React.ReactNode +} + +const Paragraph = (props: ParagraphProps) => +

+ +export default Paragraph diff --git a/frontend/src/components/Recents/Recents.tsx b/frontend/src/components/Recents/Recents.tsx index 502eb75..4740444 100644 --- a/frontend/src/components/Recents/Recents.tsx +++ b/frontend/src/components/Recents/Recents.tsx @@ -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 ?

-
+ return recents?.length ?
+

{t('home:recently_visited')}

{recents.map(event => ( @@ -28,8 +30,8 @@ const Recents = ({ target }: RecentsProps) => { >{t('common:created', { date: dayjs.unix(event.created_at).fromNow() })} ))} -
-
: null + + : null } export default Recents diff --git a/frontend/src/components/Section/Section.module.scss b/frontend/src/components/Section/Section.module.scss new file mode 100644 index 0000000..7d6bc65 --- /dev/null +++ b/frontend/src/components/Section/Section.module.scss @@ -0,0 +1,9 @@ +.section { + margin: 30px 0 0; + background-color: var(--surface); + padding: 20px 0; + + & a { + color: var(--secondary); + } +} diff --git a/frontend/src/components/Section/Section.tsx b/frontend/src/components/Section/Section.tsx new file mode 100644 index 0000000..56f8fe0 --- /dev/null +++ b/frontend/src/components/Section/Section.tsx @@ -0,0 +1,11 @@ +import styles from './Section.module.scss' + +interface SectionProps { + children: React.ReactNode + id?: string +} + +const Section = (props: SectionProps) => +
+ +export default Section diff --git a/frontend/src/components/Stats/Stats.module.scss b/frontend/src/components/Stats/Stats.module.scss new file mode 100644 index 0000000..2b6af4d --- /dev/null +++ b/frontend/src/components/Stats/Stats.module.scss @@ -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; +} diff --git a/frontend/src/components/Stats/Stats.tsx b/frontend/src/components/Stats/Stats.tsx new file mode 100644 index 0000000..e15493a --- /dev/null +++ b/frontend/src/components/Stats/Stats.tsx @@ -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
+
+ + {new Intl.NumberFormat().format(stats?.event_count || 17000)}{!stats?.event_count && '+'} + + {t('about.events')} +
+
+ + {new Intl.NumberFormat().format(stats?.person_count || 65000)}{!stats?.person_count && '+'} + + {t('about.availabilities')} +
+
+} + +export default Stats diff --git a/frontend/src/components/Video/Video.module.scss b/frontend/src/components/Video/Video.module.scss new file mode 100644 index 0000000..b8423de --- /dev/null +++ b/frontend/src/components/Video/Video.module.scss @@ -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); + } + } +} diff --git a/frontend/src/components/Video/Video.tsx b/frontend/src/components/Video/Video.tsx new file mode 100644 index 0000000..902f77e --- /dev/null +++ b/frontend/src/components/Video/Video.tsx @@ -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 ? ( +
+