Files
kycnotme/web/src/layouts/BaseLayout.astro
2025-07-03 08:38:11 +00:00

168 lines
4.6 KiB
Plaintext

---
import { differenceInCalendarDays } from 'date-fns'
import AnnouncementBanner from '../components/AnnouncementBanner.astro'
import BaseHead from '../components/BaseHead.astro'
import Footer from '../components/Footer.astro'
import Header from '../components/Header.astro'
import { cn } from '../lib/cn'
import { pluralize } from '../lib/pluralize'
import { prisma } from '../lib/prisma'
import type { AstroChildren } from '../lib/astro'
import type { Prisma } from '@prisma/client'
import type { ComponentProps } from 'astro/types'
import '@fontsource-variable/space-grotesk'
import '../styles/global.css'
type Props = ComponentProps<typeof BaseHead> & {
children: AstroChildren
errors?: string[]
success?: string[]
classNames?: {
body?: string
main?: string
footer?: string
}
showSplashText?: boolean
widthClassName?:
| 'container'
| 'max-w-none'
| 'max-w-screen-2xl'
| 'max-w-screen-lg'
| 'max-w-screen-md'
| 'max-w-screen-sm'
| 'max-w-screen-xl'
| 'max-w-screen-xs'
isErrorPage?: boolean
}
const {
errors = [],
success = [],
classNames,
widthClassName = 'max-w-screen-2xl',
showSplashText,
isErrorPage,
...baseHeadProps
} = Astro.props
const actualErrors = [...errors, ...Astro.locals.banners.errors]
const actualSuccess = [...success, ...Astro.locals.banners.successes]
const currentDate = new Date()
const announcement = await Astro.locals.banners.try(
'Unable to load announcements.',
() =>
prisma.announcement.findFirst({
where: {
isActive: true,
startDate: { lte: currentDate },
OR: [{ endDate: null }, { endDate: { gt: currentDate } }],
},
select: {
id: true,
content: true,
type: true,
link: true,
linkText: true,
startDate: true,
endDate: true,
isActive: true,
},
orderBy: [{ type: 'desc' }, { createdAt: 'desc' }],
}),
null
)
function getDeletionAnnouncement(
user: Prisma.UserGetPayload<{ select: { scheduledDeletionAt: true } }> | null,
currentDate: Date = new Date()
) {
if (!user?.scheduledDeletionAt) return null
const daysUntilDeletion = differenceInCalendarDays(user.scheduledDeletionAt, currentDate)
return {
id: 0,
content: `Your account will be deleted ${daysUntilDeletion <= 0 ? 'today' : `in ${daysUntilDeletion.toLocaleString()} ${pluralize('day', daysUntilDeletion)}`} due to inactivity.`,
type: 'ALERT' as const,
link: '/account',
linkText: 'Prevent deletion',
startDate: currentDate,
endDate: null,
isActive: true,
}
}
const deletionAnnouncement = getDeletionAnnouncement(Astro.locals.user, currentDate)
---
<html lang="en" transition:name="root" transition:animate="none">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<BaseHead {...baseHeadProps} />
</head>
<body
class={cn('bg-night-700 text-day-300 flex min-h-dvh flex-col *:shrink-0', classNames?.body)}
data-is-error-page={isErrorPage ? '' : undefined}
data-is-logged-in={Astro.locals.user !== null ? '' : undefined}
>
{announcement && <AnnouncementBanner announcement={announcement} transition:name="header-announcement" />}
{
deletionAnnouncement && (
<AnnouncementBanner
announcement={deletionAnnouncement}
transition:name="deletion-warning-announcement"
/>
)
}
<Header
classNames={{
nav: cn(
(widthClassName === 'max-w-none' || widthClassName === 'max-w-screen-2xl') && 'lg:px-8 2xl:px-12',
widthClassName
),
}}
showSplashText={showSplashText}
/>
{
actualSuccess.length > 0 && (
<ul class="container mx-auto my-4 space-y-4 px-4">
{actualSuccess.map((message) => (
<li class="font-title rounded-lg border border-green-500/30 bg-green-500/20 px-4 py-3 text-green-400">
{message}
</li>
))}
</ul>
)
}
{
actualErrors.length > 0 && (
<ul class="container mx-auto my-4 space-y-4 px-4">
{actualErrors.map((error) => (
<li class="font-title rounded-lg border border-red-500/30 bg-red-500/20 px-4 py-3 text-red-400">
{error}
</li>
))}
</ul>
)
}
<main
class={cn(
'container mx-auto mt-4 mb-12 grow px-4',
classNames?.main,
(widthClassName === 'max-w-none' || widthClassName === 'max-w-screen-2xl') && 'lg:px-8 2xl:px-12',
widthClassName
)}
>
<slot />
</main>
<Footer class={classNames?.footer} />
</body>
</html>