Start setting up Next js with the new app router

This commit is contained in:
Ben Grant 2023-05-19 23:59:44 +10:00
parent 49c6281b74
commit 2adecd13f7
147 changed files with 2637 additions and 3667 deletions

View file

@ -1,33 +0,0 @@
import { Pressable } from './Button.styles'
const Button = ({
href,
type = 'button',
icon,
children,
secondary,
primaryColor,
secondaryColor,
small,
size,
isLoading,
...props
}) => (
<Pressable
type={type}
as={href ? 'a' : 'button'}
href={href}
$secondary={secondary}
$primaryColor={primaryColor}
$secondaryColor={secondaryColor}
$small={small}
$size={size}
$isLoading={isLoading}
{...props}
>
{icon}
{children}
</Pressable>
)
export default Button

View file

@ -0,0 +1,130 @@
.button {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
border: 0;
text-decoration: none;
font: inherit;
box-sizing: border-box;
background: var(--override-surface-color, var(--primary));
color: var(--override-text-color, var(--background));
font-weight: 600;
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1);
border-radius: 3px;
padding: .6em 1.5em;
transform-style: preserve-3d;
margin-bottom: 5px;
& svg, & img {
height: 1.2em;
width: 1.2em;
margin-right: .5em;
}
&::before {
content: '';
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
background: var(--override-shadow-color, var(--shadow));
border-radius: inherit;
transform: translate3d(0, 5px, -1em);
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1), box-shadow 150ms cubic-bezier(0, 0, 0.58, 1);
}
&:hover, &:focus {
transform: translate(0, 1px);
&::before {
transform: translate3d(0, 4px, -1em);
}
}
&:active {
transform: translate(0, 5px);
&::before {
transform: translate3d(0, 0, -1em);
}
}
@media print {
&::before {
display: none;
}
}
}
.small {
padding: .4em 1.3em;
}
.loading {
color: transparent;
cursor: wait;
& img {
opacity: 0;
}
@keyframes load {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
&::after {
content: '';
position: absolute;
top: calc(50% - 12px);
left: calc(50% - 12px);
height: 18px;
width: 18px;
border: 3px solid var(--override-text-color, var(--background));
border-left-color: transparent;
border-radius: 100px;
animation: load .5s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
&::after {
content: 'loading...';
color: var(--override-text-color, var(--background));
animation: none;
width: initial;
height: initial;
left: 50%;
transform: translateX(-50%);
border: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.secondary {
background: transparent;
border: 1px solid var(--override-surface-color, var(--secondary));
color: var(--override-surface-color, var(--secondary));
margin-bottom: 0;
&::before {
content: none;
}
&:hover, &:active, &:focus {
transform: none;
}
@media print {
box-shadow: 0 4px 0 0 var(--override-shadow-color, var(--secondary));
}
}

View file

@ -1,134 +0,0 @@
import { styled } from 'goober'
export const Pressable = styled('button')`
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
text-align: center;
cursor: pointer;
border: 0;
text-decoration: none;
font: inherit;
box-sizing: border-box;
background: ${props => props.$primaryColor || 'var(--primary)'};
color: ${props => props.$primaryColor ? '#FFF' : 'var(--background)'};
font-weight: 600;
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1);
border-radius: 3px;
padding: ${props => props.$small ? '.4em 1.3em' : '.6em 1.5em'};
transform-style: preserve-3d;
margin-bottom: 5px;
& svg, & img {
height: 1.2em;
width: 1.2em;
margin-right: .5em;
}
${props => props.$size && `
padding: 0;
height: ${props.$size};
width: ${props.$size};
`}
&::before {
content: '';
position: absolute;
height: 100%;
width: 100%;
top: 0;
left: 0;
background: ${props => props.$secondaryColor || 'var(--shadow)'};
border-radius: inherit;
transform: translate3d(0, 5px, -1em);
transition: transform 150ms cubic-bezier(0, 0, 0.58, 1), box-shadow 150ms cubic-bezier(0, 0, 0.58, 1);
}
&:hover, &:focus {
transform: translate(0, 1px);
&::before {
transform: translate3d(0, 4px, -1em);
}
}
&:active {
transform: translate(0, 5px);
&::before {
transform: translate3d(0, 0, -1em);
}
}
${props => props.$isLoading && `
color: transparent;
cursor: wait;
& img {
opacity: 0;
}
@keyframes load {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
&:after {
content: '';
position: absolute;
top: calc(50% - 12px);
left: calc(50% - 12px);
height: 18px;
width: 18px;
border: 3px solid ${props.$primaryColor ? '#FFF' : 'var(--background)'};
border-left-color: transparent;
border-radius: 100px;
animation: load .5s linear infinite;
}
@media (prefers-reduced-motion: reduce) {
&:after {
content: 'loading...';
color: ${props.$primaryColor ? '#FFF' : 'var(--background)'};
animation: none;
width: initial;
height: initial;
left: 50%;
transform: translateX(-50%);
border: 0;
top: 0;
bottom: 0;
display: flex;
align-items: center;
justify-content: center;
}
}
`}
${props => props.$secondary && `
background: transparent;
border: 1px solid ${props.$primaryColor || 'var(--secondary)'};
color: ${props.$primaryColor || 'var(--secondary)'};
margin-bottom: 0;
&::before {
content: none;
}
&:hover, &:active, &:focus {
transform: none;
}
`}
@media print {
${props => !props.$secondary && `
box-shadow: 0 4px 0 0 ${props.$secondaryColor || 'var(--secondary)'};
`}
&::before {
display: none;
}
}
`

View file

@ -0,0 +1,59 @@
import Link from 'next/link'
import { makeClass } from '/src/utils'
import styles from './Button.module.scss'
type ButtonProps = {
/** If provided, will render a link that looks like a button */
href?: string
icon?: React.ReactNode
children?: React.ReactNode
isSecondary?: boolean
isSmall?: boolean
isLoading?: boolean
/** Override the surface color of the button. Will force the text to #FFFFFF. */
surfaceColor?: string
/** Override the shadow color of the button */
shadowColor?: string
// TODO: evaluate
size?: string
} & Omit<React.ComponentProps<'button'> & React.ComponentProps<'a'>, 'ref'>
const Button: React.FC<ButtonProps> = ({
href,
type = 'button',
icon,
children,
isSecondary,
isSmall,
isLoading,
surfaceColor,
shadowColor,
size,
style,
...props
}) => {
const sharedProps = {
className: makeClass(
styles.button,
isSecondary && styles.secondary,
isSmall && styles.small,
isLoading && styles.loading,
),
style: {
...surfaceColor && { '--override-surface-color': surfaceColor, '--override-text-color': '#FFFFFF' },
...shadowColor && { '--override-shadow-color': shadowColor },
...size && { padding: 0, height: size, width: size },
...style,
},
children: [icon, children],
...props,
}
return href
? <Link href={href} {...sharedProps} />
: <button type={type} {...sharedProps} />
}
export default Button

View file

@ -1,159 +0,0 @@
import { useState, useEffect, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { Button } from '/src/components'
import { useTWAStore } from '/src/stores'
import {
Wrapper,
Options,
} from './Donate.styles'
import paypal_logo from '/src/res/paypal.svg'
const PAYMENT_METHOD = 'https://play.google.com/billing'
const SKU = 'crab_donation'
const Donate = () => {
const store = useTWAStore()
const { t } = useTranslation('common')
const firstLinkRef = useRef()
const modalRef = useRef()
const [isOpen, _setIsOpen] = useState(false)
const [closed, setClosed] = useState(false)
const setIsOpen = open => {
_setIsOpen(open)
if (open) {
window.setTimeout(() => firstLinkRef.current.focus(), 150)
}
}
const linkPressed = () => {
setIsOpen(false)
gtag('event', 'donate', { 'event_category': 'donate' })
}
useEffect(() => {
if (store.TWA === undefined) {
store.setTWA(document.referrer.includes('android-app://fit.crab'))
}
}, [store])
const acknowledge = async (token, type='repeatable', onComplete = () => {}) => {
try {
const service = await window.getDigitalGoodsService(PAYMENT_METHOD)
await service.acknowledge(token, type)
if ('acknowledge' in service) {
// DGAPI 1.0
service.acknowledge(token, type)
} else {
// DGAPI 2.0
service.consume(token)
}
onComplete()
} catch (error) {
console.error(error)
}
}
const purchase = () => {
if (!window.PaymentRequest) return false
if (!window.getDigitalGoodsService) return false
const supportedInstruments = [{
supportedMethods: PAYMENT_METHOD,
data: {
sku: SKU
}
}]
const details = {
total: {
label: 'Total',
amount: { currency: 'AUD', value: '0' }
},
}
const request = new PaymentRequest(supportedInstruments, details)
request.show()
.then(response => {
response
.complete('success')
.then(() => {
console.log(`Payment done: ${JSON.stringify(response, undefined, 2)}`)
if (response.details && response.details.token) {
const token = response.details.token
console.log(`Read Token: ${token.substring(0, 6)}...`)
alert(t('donate.messages.success'))
acknowledge(token)
}
})
.catch(e => {
console.error(e.message)
alert(t('donate.messages.error'))
})
})
.catch(e => {
console.error(e)
alert(t('donate.messages.error'))
})
}
return (
<Wrapper>
<Button
small
title={t('donate.title')}
onClick={event => {
if (closed) {
event.preventDefault()
return setClosed(false)
}
if (store.TWA) {
gtag('event', 'donate', { 'event_category': 'donate' })
event.preventDefault()
if (window.confirm(t('donate.messages.about'))) {
if (purchase() === false) {
alert(t('donate.messages.error'))
}
}
} else {
event.preventDefault()
setIsOpen(true)
}
}}
href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5"
target="_blank"
rel="noreferrer noopener payment"
id="donate_button"
role="button"
aria-expanded={isOpen ? 'true' : 'false'}
style={{ whiteSpace: 'nowrap' }}
>{t('donate.button')}</Button>
<Options
$isOpen={isOpen}
ref={modalRef}
onBlur={e => {
if (modalRef.current?.contains(e.relatedTarget)) return
setIsOpen(false)
if (e.relatedTarget && e.relatedTarget.id === 'donate_button') {
setClosed(true)
}
}}
>
<img src={paypal_logo} alt="Donate with PayPal" />
<a onClick={linkPressed} ref={firstLinkRef} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=2" target="_blank" rel="noreferrer noopener payment">{t('donate.options.$2')}</a>
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=5" target="_blank" rel="noreferrer noopener payment"><strong>{t('donate.options.$5')}</strong></a>
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD&amount=10" target="_blank" rel="noreferrer noopener payment">{t('donate.options.$10')}</a>
<a onClick={linkPressed} href="https://www.paypal.com/donate?business=N89X6YXRT5HKW&item_name=Crab+Fit+Donation&currency_code=AUD" target="_blank" rel="noreferrer noopener payment">{t('donate.options.choose')}</a>
</Options>
</Wrapper>
)
}
export default Donate

View file

@ -1,64 +0,0 @@
import { styled } from 'goober'
import { forwardRef } from 'react'
export const Wrapper = styled('div')`
margin-top: 6px;
margin-left: 12px;
position: relative;
`
export const Options = styled('div', forwardRef)`
position: absolute;
bottom: calc(100% + 20px);
right: 0;
background-color: var(--background);
border: 1px solid var(--surface);
z-index: 60;
padding: 4px 10px;
border-radius: 14px;
box-sizing: border-box;
max-width: calc(100vw - 20px);
box-shadow: 0 3px 6px 0 rgba(0,0,0,.3);
visibility: hidden;
pointer-events: none;
opacity: 0;
transform: translateY(5px);
transition: opacity .15s, transform .15s, visibility .15s;
${props => props.$isOpen && `
pointer-events: all;
opacity: 1;
transform: translateY(0);
visibility: visible;
`}
& img {
width: 80px;
margin: 10px auto 0;
display: block;
}
& a {
display: block;
white-space: nowrap;
text-align: center;
padding: 4px 20px;
margin: 6px 0;
text-decoration: none;
border-radius: 100px;
background-color: var(--primary);
color: var(--background);
&:hover {
text-decoration: underline;
}
& strong {
font-weight: 800;
}
}
@media (prefers-reduced-motion: reduce) {
transition: none;
}
`

View file

@ -1,17 +0,0 @@
import { X } from 'lucide-react'
import { Wrapper, CloseButton } from './Error.styles'
const Error = ({
children,
onClose,
open = true,
...props
}) => (
<Wrapper role="alert" open={open} {...props}>
{children}
<CloseButton type="button" onClick={onClose} title="Close error"><X /></CloseButton>
</Wrapper>
)
export default Error

View file

@ -1,6 +1,4 @@
import { styled } from 'goober'
export const Wrapper = styled('div')`
.error {
border-radius: 3px;
background-color: var(--error);
color: #FFFFFF;
@ -15,21 +13,21 @@ export const Wrapper = styled('div')`
visibility: hidden;
transition: margin .2s, padding .2s, max-height .2s;
${props => props.open && `
opacity: 1;
visibility: visible;
margin: 20px 0;
padding: 12px 16px;
max-height: 60px;
transition: opacity .15s .2s, max-height .2s, margin .2s, padding .2s, visibility .2s;
`}
@media (prefers-reduced-motion: reduce) {
transition: none;
}
`
}
export const CloseButton = styled('button')`
.open {
opacity: 1;
visibility: visible;
margin: 20px 0;
padding: 12px 16px;
max-height: 60px;
transition: opacity .15s .2s, max-height .2s, margin .2s, padding .2s, visibility .2s;
}
.closeButton {
border: 0;
background: none;
height: 30px;
@ -41,4 +39,4 @@ export const CloseButton = styled('button')`
justify-content: center;
margin-left: 16px;
padding: 0;
`
}

View file

@ -0,0 +1,25 @@
'use client'
import { X } from 'lucide-react'
import { makeClass } from '/src/utils'
import styles from './Error.module.scss'
interface ErrorProps {
children?: React.ReactNode
onClose: () => void
}
const Error = ({ children, onClose }: ErrorProps) =>
<div role="alert" className={makeClass(styles.error, children && styles.open)}>
{children}
<button
className={styles.closeButton}
type="button"
onClick={onClose}
title="Dismiss error"
><X /></button>
</div>
export default Error

View file

@ -1,17 +0,0 @@
import { useTranslation } from 'react-i18next'
import { Donate } from '/src/components'
import { Wrapper } from './Footer.styles'
const Footer = props => {
const { t } = useTranslation('common')
return (
<Wrapper id="donate" {...props}>
<span>{t('donate.info')}</span>
<Donate />
</Wrapper>
)
}
export default Footer

View file

@ -0,0 +1,24 @@
.footer {
width: 600px;
margin: 20px auto;
max-width: calc(100% - 60px);
display: flex;
align-items: center;
justify-content: space-between;
@media print {
display: none;
}
}
.small {
margin: 60px auto 0;
width: 250px;
max-width: initial;
display: block;
& span {
display: block;
margin-bottom: 20px;
}
}

View file

@ -1,26 +0,0 @@
import { styled } from 'goober'
export const Wrapper = styled('footer')`
width: 600px;
margin: 20px auto;
max-width: calc(100% - 60px);
display: flex;
align-items: center;
justify-content: space-between;
${props => props.small && `
margin: 60px auto 0;
width: 250px;
max-width: initial;
display: block;
& span {
display: block;
margin-bottom: 20px;
}
`}
@media print {
display: none;
}
`

View file

@ -0,0 +1,30 @@
import { Button } from '/src/components'
import { useTranslation } from '/src/i18n/server'
import { makeClass } from '/src/utils'
import styles from './Footer.module.scss'
interface FooterProps {
isSmall?: boolean
}
const Footer = async ({ isSmall }: FooterProps) => {
const { t } = await useTranslation('common')
return <footer
id="donate" // Required to allow scrolling directly to the footer
className={makeClass(styles.footer, isSmall && styles.small)}
>
<span>{t('donate.info')}</span>
<Button
isSmall
title={t<string>('donate.title')}
href="https://ko-fi.com/A06841WZ"
target="_blank"
rel="noreferrer noopener payment"
style={{ whiteSpace: 'nowrap' }}
>{t('donate.button')}</Button>
</footer>
}
export default Footer

View file

@ -0,0 +1,124 @@
.header {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
}
@keyframes jelly {
from,to {
transform: scale(1,1)
}
25% {
transform: scale(.9,1.1)
}
50% {
transform: scale(1.1,.9)
}
75% {
transform: scale(.95,1.05)
}
}
.link {
text-decoration: none;
&:hover img {
animation: jelly .5s 1;
}
@media (prefers-reduced-motion: reduce) {
&:hover img {
animation: none;
}
}
}
.top {
display: inline-flex;
justify-content: center;
align-items: center;
}
.logo {
width: 2.5rem;
margin-right: 16px;
}
.title {
display: block;
font-size: 2rem;
color: var(--primary);
font-family: 'Molot', sans-serif;
font-weight: 400;
text-shadow: 0 2px 0 var(--shadow);
line-height: 1em;
}
.tagline {
text-decoration: underline;
font-size: 14px;
padding-top: 2px;
display: flex;
align-items: center;
justify-content: center;
@media print {
display: none;
}
}
.subtitle {
display: block;
margin: 0;
font-size: 3rem;
text-align: center;
font-family: 'Samurai Bob', sans-serif;
font-weight: 400;
color: var(--secondary);
line-height: 1em;
text-transform: uppercase;
}
.hasAltChars {
font-family: sans-serif;
font-size: 2rem;
font-weight: 600;
line-height: 1.2em;
padding-top: .3em;
}
.bigTitle {
margin: 0;
font-size: 4rem;
text-align: center;
color: var(--primary);
font-family: 'Molot', sans-serif;
font-weight: 400;
text-shadow: 0 4px 0 var(--shadow);
line-height: 1em;
text-transform: uppercase;
@media (max-width: 350px) {
font-size: 3.5rem;
}
}
.bigLogo {
width: 80px;
transition: transform .15s;
animation: jelly .5s 1 .05s;
user-select: none;
&:active {
animation: none;
transform: scale(.85);
}
@media (prefers-reduced-motion: reduce) {
animation: none;
transition: none;
&:active {
transform: none;
}
}
}

View file

@ -0,0 +1,32 @@
import Link from 'next/link'
import { useTranslation } from '/src/i18n/server'
import logo from '/src/res/logo.svg'
import { makeClass } from '/src/utils'
import styles from './Header.module.scss'
interface HeaderProps {
/** Show the full header */
isFull?: boolean
}
const Header = async ({ isFull }: HeaderProps) => {
const { t } = await useTranslation(['common', 'home'])
return <header className={styles.header}>
{isFull ? <>
<img className={styles.bigLogo} src={logo.src} alt="" />
<span className={makeClass(styles.subtitle, !/^[A-Za-z ]+$/.test(t('home:create')) && styles.hasAltChars)}>{t('home:create')}</span>
<h1 className={styles.bigTitle}>CRAB FIT</h1>
</> : <Link href="/" className={styles.link}>
<div className={styles.top}>
<img className={styles.logo} src={logo.src} alt="" />
<span className={styles.title}>CRAB FIT</span>
</div>
<span className={styles.tagline}>{t('common:tagline')}</span>
</Link>}
</header>
}
export default Header

View file

@ -1,31 +0,0 @@
import { useTranslation } from 'react-i18next'
import { Link } from 'react-router-dom'
import {
Wrapper,
A,
Top,
Image,
Title,
Tagline,
} from './Logo.styles'
import image from '/src/res/logo.svg'
const Logo = () => {
const { t } = useTranslation('common')
return (
<Wrapper>
<A as={Link} to="/">
<Top>
<Image src={image} alt="" />
<Title>CRAB FIT</Title>
</Top>
<Tagline>{t('common:tagline')}</Tagline>
</A>
</Wrapper>
)
}
export default Logo

View file

@ -1,69 +0,0 @@
import { styled } from 'goober'
export const Wrapper = styled('div')`
display: flex;
align-items: center;
justify-content: center;
`
export const A = styled('a')`
text-decoration: none;
@keyframes jelly {
from,to {
transform: scale(1,1)
}
25% {
transform: scale(.9,1.1)
}
50% {
transform: scale(1.1,.9)
}
75% {
transform: scale(.95,1.05)
}
}
&:hover img {
animation: jelly .5s 1;
}
@media (prefers-reduced-motion: reduce) {
&:hover img {
animation: none;
}
}
`
export const Top = styled('div')`
display: inline-flex;
justify-content: center;
align-items: center;
`
export const Image = styled('img')`
width: 2.5rem;
margin-right: 16px;
`
export const Title = styled('span')`
display: block;
font-size: 2rem;
color: var(--primary);
font-family: 'Molot', sans-serif;
font-weight: 400;
text-shadow: 0 2px 0 var(--shadow);
line-height: 1em;
`
export const Tagline = styled('span')`
text-decoration: underline;
font-size: 14px;
padding-top: 2px;
display: flex;
align-items: center;
justify-content: center;
@media print {
display: none;
}
`

View file

@ -1,34 +0,0 @@
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useRecentsStore, useLocaleUpdateStore } from '/src/stores'
import { AboutSection, StyledMain } from '../../pages/Home/Home.styles'
import { Wrapper, Recent } from './Recents.styles'
dayjs.extend(relativeTime)
const Recents = ({ target }) => {
const recents = useRecentsStore(state => state.recents)
const locale = useLocaleUpdateStore(state => state.locale)
const { t } = useTranslation(['home', 'common'])
return !!recents.length && (
<Wrapper>
<AboutSection id="recents">
<StyledMain>
<h2>{t('home:recently_visited')}</h2>
{recents.map(event => (
<Recent href={`/${event.id}`} target={target} key={event.id}>
<span className="name">{event.name}</span>
<span locale={locale} className="date" title={dayjs.unix(event.created).format('D MMMM, YYYY')}>{t('common:created', { date: dayjs.unix(event.created).fromNow() })}</span>
</Recent>
))}
</StyledMain>
</AboutSection>
</Wrapper>
)
}
export default Recents

View file

@ -0,0 +1,35 @@
.recent {
text-decoration: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
flex-wrap: wrap;
&:hover .name {
text-decoration: underline;
}
@media (max-width: 500px) {
display: block;
}
}
.name {
font-weight: 700;
font-size: 1.1em;
color: var(--secondary);
flex: 1;
display: block;
}
.date {
font-weight: 400;
opacity: .8;
white-space: nowrap;
color: var(--text);
@media (max-width: 500px) {
white-space: normal;
}
}

View file

@ -1,42 +0,0 @@
import { styled } from 'goober'
export const Wrapper = styled('div')`
@media print {
display: none;
}
`
export const Recent = styled('a')`
text-decoration: none;
display: flex;
align-items: center;
justify-content: space-between;
padding: 5px 0;
flex-wrap: wrap;
& .name {
font-weight: 700;
font-size: 1.1em;
color: var(--secondary);
flex: 1;
display: block;
}
& .date {
font-weight: 400;
opacity: .8;
white-space: nowrap;
color: var(--text);
}
&:hover .name {
text-decoration: underline;
}
@media (max-width: 500px) {
display: block;
& .date {
white-space: normal;
}
}
`

View file

@ -0,0 +1,35 @@
'use client'
import Link from 'next/link'
import dayjs from '/src/config/dayjs'
import { useTranslation } from '/src/i18n/client'
import { useRecentsStore, useStore } from '/src/stores'
import styles from './Recents.module.scss'
interface RecentsProps {
target?: React.ComponentProps<'a'>['target']
}
const Recents = ({ target }: RecentsProps) => {
const recents = useStore(useRecentsStore, state => state.recents)
const { t } = useTranslation(['home', 'common'])
return recents?.length ? <section id="recents">
<div>
<h2>{t('home:recently_visited')}</h2>
{recents.map(event => (
<Link className={styles.recent} href={`/${event.id}`} target={target} key={event.id}>
<span className={styles.name}>{event.name}</span>
<span
className={styles.date}
title={dayjs.unix(event.created_at).format('D MMMM, YYYY')}
>{t('common:created', { date: dayjs.unix(event.created_at).fromNow() })}</span>
</Link>
))}
</div>
</section> : null
}
export default Recents

View file

@ -1,5 +1,4 @@
import { useState, useEffect, useRef } from 'react'
import { useLocation } from 'react-router-dom'
import { useTranslation } from 'react-i18next'
import dayjs from 'dayjs'
import { Settings as SettingsIcon } from 'lucide-react'
@ -18,6 +17,7 @@ import {
import locales from '/src/i18n/locales'
import { unhyphenate } from '/src/utils'
import { useRouter } from 'next/router'
// Language specific options
const setDefaults = (lang, store) => {
@ -28,7 +28,7 @@ const setDefaults = (lang, store) => {
}
const Settings = () => {
const { pathname } = useLocation()
const { pathname } = useRouter()
const store = useSettingsStore()
const [isOpen, _setIsOpen] = useState(false)
const { t, i18n } = useTranslation('common')

View file

@ -1,24 +0,0 @@
export { default as TextField } from './TextField/TextField'
export { default as SelectField } from './SelectField/SelectField'
export { default as CalendarField } from './CalendarField/CalendarField'
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 Donate } from './Donate/Donate'
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 Logo } from './Logo/Logo'
export { default as TranslateDialog } from './TranslateDialog/TranslateDialog'
export const _GoogleCalendar = () => import('./GoogleCalendar/GoogleCalendar')
export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar')

View file

@ -0,0 +1,23 @@
// export { default as TextField } from './TextField/TextField'
// export { default as SelectField } from './SelectField/SelectField'
// export { default as CalendarField } from './CalendarField/CalendarField'
// 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')
// export const _OutlookCalendar = () => import('./OutlookCalendar/OutlookCalendar')