Files
ridebus-website/modules/home/changesCard/changesCard.tsx
2026-10-02 20:31:49 +03:00

190 lines
6.6 KiB
TypeScript

import { useState } from 'react';
import { useRouter } from 'next/router';
import clsx from 'clsx';
import { Card } from '@/components/card';
import { CursorEffect } from '@/components/cursor';
import Star from '@/assets/images/thick-round-star-6.svg';
import NoteIcon from '@/assets/icons/note.svg';
import type { Alerts, City } from '@/lib/ridebus';
import useLocale from '@/utils/useLocale';
import { SelectMenu } from '../departuresCard/selectMenu';
import { useResource } from '../departuresCard/useResource';
import { locales } from './locales';
import styles from './changesCard.module.scss';
export function ChangesCard() {
const t = useLocale(locales);
const { locale } = useRouter();
const [revision, setRevision] = useState(0);
const [chosenCity, setChosenCity] = useState('');
const [expanded, setExpanded] = useState(false);
const cities = useResource<City[]>(
'/api/departures?resource=cities',
revision
);
const cityId = String(
cities.data?.find((city) => String(city.id) === chosenCity)?.id ??
cities.data?.find((city) => city.slug === 'polotsk')?.id ??
cities.data?.[0]?.id ??
''
);
const notices = useResource<Alerts>(
cityId ? `/api/departures?resource=alerts&cityId=${cityId}` : null,
revision,
true
);
const failed = cities.status === 'error' || notices.status === 'error';
const loading = cities.status === 'loading' || notices.status === 'loading';
const alerts = notices.data?.alerts ?? [];
const dateFormat = new Intl.DateTimeFormat(
locale === 'ru' ? 'ru-BY' : 'en-GB',
{
day: 'numeric',
month: 'short',
year: 'numeric',
timeZone: 'Europe/Minsk',
}
);
const updateTime = notices.data
? new Intl.DateTimeFormat(locale === 'ru' ? 'ru-BY' : 'en-GB', {
hour: '2-digit',
minute: '2-digit',
timeZone: 'Europe/Minsk',
}).format(new Date(notices.data.updatedAt))
: '';
return (
<section aria-label={t('title')}>
<Card
className={styles.card}
title={t('title')}
subtitle={t('description')}
classes={{ content: styles.content }}
>
<Star className={styles.star} aria-hidden="true" />
<div className={styles.layout}>
<div className={styles.controls}>
<SelectMenu
label={t('city')}
value={cityId}
options={
cities.data?.map((city) => ({
value: String(city.id),
label: city.name,
})) ?? []
}
placeholder={
cities.status === 'loading'
? t('loadingCities')
: t('selectCity')
}
searchLabel={t('searchCity')}
noResults={t('noResults')}
onChange={(value) => {
setChosenCity(value);
setExpanded(false);
}}
/>
<p className={styles.note}>{t('note')}</p>
{updateTime && !failed && !loading && (
<p className={styles.updated}>
{t('updated')} {updateTime}
</p>
)}
</div>
<div
className={styles.notices}
aria-live="polite"
aria-busy={loading}
>
{failed || loading || !cities.data?.length ? (
<div className={styles.state}>
{loading && (
<div className={styles.loadingDots} aria-hidden="true">
<i />
<i />
<i />
</div>
)}
<p>
{t(failed ? 'unavailable' : loading ? 'loading' : 'noCities')}
</p>
{failed && (
<CursorEffect cursorBorderRadius={16} cursorPadding={4}>
<button
type="button"
className={styles.action}
onClick={() => setRevision((r) => r + 1)}
>
{t('retry')}
</button>
</CursorEffect>
)}
</div>
) : alerts.length ? (
<>
{(expanded ? alerts : alerts.slice(0, 3)).map((alert) => (
<article
className={styles.notice}
key={`${cityId}:${alert.id}`}
>
<div className={styles.badges}>
<span className={clsx(styles.type, styles[alert.type])}>
{t(alert.type)}
</span>
{alert.routeNumber && (
<span className={styles.route}>
{t('route')} №{alert.routeNumber}
</span>
)}
</div>
<h4>{alert.title}</h4>
<p className={styles.message}>{alert.message}</p>
<div className={styles.dates}>
<span>
{t('published')}{' '}
<time dateTime={alert.createdAt}>
{dateFormat.format(new Date(alert.createdAt))}
</time>
</span>
{alert.endTime && (
<span>
{t('until')}{' '}
<time dateTime={alert.endTime}>
{dateFormat.format(new Date(alert.endTime))}
</time>
</span>
)}
</div>
</article>
))}
{alerts.length > 3 && (
<CursorEffect cursorBorderRadius={16} cursorPadding={4}>
<button
type="button"
className={styles.action}
aria-expanded={expanded}
onClick={() => setExpanded((value) => !value)}
>
{t(expanded ? 'showLess' : 'showAll')}
{!expanded && ` (${alerts.length})`}
</button>
</CursorEffect>
)}
</>
) : (
<div className={styles.state} key={cityId}>
<span className={styles.emptyIcon} aria-hidden="true">
<NoteIcon />
</span>
<h4>{t('emptyTitle')}</h4>
<p>{t('emptyDescription')}</p>
</div>
)}
</div>
</div>
</Card>
</section>
);
}