fix: TS non-null assertion in seed + formatting cleanup across components
Some checks are pending
CI / Type Check (push) Waiting to run
CI / Lint (push) Waiting to run
CI / Unit Tests (push) Waiting to run
Deploy / Build & Push Image (push) Waiting to run
Deploy / Deploy to VPS (push) Blocked by required conditions

Fixes Object possibly undefined TS2532 in findOrUploadMedia (seed/route.ts:50)
that was blocking production build. All other changes are whitespace/ordering
only (Tailwind class order, SVG attribute expansion).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Vadym Samoilenko 2026-05-11 13:40:23 +01:00
parent 0895f25434
commit d015c07f7f
11 changed files with 68 additions and 54 deletions

View file

@ -35,15 +35,23 @@ for (const [base, variants] of byBase) {
if (!heavy) continue
const srcPath = path.join(ROOT, heavy)
const stat = await fs.stat(srcPath).catch(() => null)
if (!stat || stat.size < MIN_BYTES) { manifest.skipped.push(path.basename(srcPath)); continue }
if (!stat || stat.size < MIN_BYTES) {
manifest.skipped.push(path.basename(srcPath))
continue
}
const outPath = path.join(ROOT, `${base}.webp`)
const outStat = await fs.stat(outPath).catch(() => null)
if (outStat && outStat.mtimeMs > stat.mtimeMs) { manifest.skipped.push(path.basename(srcPath) + ' (webp newer)'); continue }
if (outStat && outStat.mtimeMs > stat.mtimeMs) {
manifest.skipped.push(path.basename(srcPath) + ' (webp newer)')
continue
}
await sharp(srcPath).webp({ quality: QUALITY }).toFile(outPath)
manifest.encoded.push({ src: path.basename(srcPath), out: `${base}.webp`, before: stat.size })
}
const manifestPath = path.join(process.cwd(), 'scripts/optimize-images.manifest.json')
await fs.writeFile(manifestPath, JSON.stringify(manifest, null, 2))
console.log(`Deleted ${manifest.deletedDuplicates.length} duplicates, encoded ${manifest.encoded.length} files, skipped ${manifest.skipped.length}.`)
console.log(
`Deleted ${manifest.deletedDuplicates.length} duplicates, encoded ${manifest.encoded.length} files, skipped ${manifest.skipped.length}.`
)
console.log('Manifest:', manifestPath)

View file

@ -47,7 +47,7 @@ async function findOrUploadMedia(
limit: 1,
overrideAccess: true,
})
if (existing.docs.length > 0) return existing.docs[0].id as string
if (existing.docs.length > 0) return existing.docs[0]!.id as string
const fullPath = path.resolve(process.cwd(), localPath)
if (!fs.existsSync(fullPath)) return null
const buffer = fs.readFileSync(fullPath)

View file

@ -47,8 +47,7 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
backgroundColor: 'rgba(34, 62, 13, 0.42)',
backdropFilter: 'blur(22px) saturate(160%)',
WebkitBackdropFilter: 'blur(22px) saturate(160%)',
boxShadow:
'inset 0 1px 0 0 rgba(255,255,255,0.18), 0 1px 0 0 rgba(34,62,13,0.18)',
boxShadow: 'inset 0 1px 0 0 rgba(255,255,255,0.18), 0 1px 0 0 rgba(34,62,13,0.18)',
}}
>
{/* Highlight sheen (top edge) — gives the glass-y look from Figma */}
@ -56,12 +55,11 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
aria-hidden="true"
className="pointer-events-none absolute inset-x-0 top-0 h-[34px]"
style={{
background:
'linear-gradient(to bottom, rgba(255,255,255,0.10), rgba(255,255,255,0))',
background: 'linear-gradient(to bottom, rgba(255,255,255,0.10), rgba(255,255,255,0))',
}}
/>
<div className="relative flex items-center justify-between h-[60px] lg:h-[120px] px-[20px] lg:px-[30px]">
<div className="relative flex h-[60px] items-center justify-between px-[20px] lg:h-[120px] lg:px-[30px]">
{/* Logo */}
<Link href="/" aria-label="Шуміленд — на головну" className="shrink-0">
{logo ? (
@ -78,19 +76,19 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
className="absolute"
style={{ top: '91.32%', right: '21.81%', bottom: '0.97%', left: '22.26%' }}
>
<img src={LOGO_G1} alt="" aria-hidden="true" className="block w-full h-full" />
<img src={LOGO_G1} alt="" aria-hidden="true" className="block h-full w-full" />
</div>
<div
className="absolute"
style={{ top: '71.76%', right: '2.82%', bottom: '7.3%', left: '1.41%' }}
>
<img src={LOGO_G2} alt="" aria-hidden="true" className="block w-full h-full" />
<img src={LOGO_G2} alt="" aria-hidden="true" className="block h-full w-full" />
</div>
<div
className="absolute"
style={{ top: '1.61%', right: '2.82%', bottom: '38.73%', left: '21.27%' }}
>
<img src={LOGO_G3} alt="" aria-hidden="true" className="block w-full h-full" />
<img src={LOGO_G3} alt="" aria-hidden="true" className="block h-full w-full" />
</div>
</div>
)}
@ -98,14 +96,14 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
{/* Nav — desktop */}
<nav aria-label="Головна навігація" className="hidden lg:flex">
<ul className="flex items-center gap-[24px] list-none m-0 p-0">
<ul className="m-0 flex list-none items-center gap-[24px] p-0">
{navLinks.map((link) => (
<li key={link.href} className="relative group">
<li key={link.href} className="group relative">
<NavLink href={link.href}>{link.label}</NavLink>
{link.children && (
<div
className="pointer-events-none absolute top-full left-0 mt-2 min-w-[220px] overflow-hidden rounded-[16px] py-2 z-50 opacity-0 translate-y-1 transition-all duration-150 group-hover:pointer-events-auto group-hover:opacity-100 group-hover:translate-y-0"
className="pointer-events-none absolute top-full left-0 z-50 mt-2 min-w-[220px] translate-y-1 overflow-hidden rounded-[16px] py-2 opacity-0 transition-all duration-150 group-hover:pointer-events-auto group-hover:translate-y-0 group-hover:opacity-100"
style={{
backgroundColor: 'rgba(34, 62, 13, 0.42)',
backdropFilter: 'blur(22px) saturate(160%)',
@ -127,7 +125,7 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
<Link
key={child.href}
href={child.href}
className="relative block px-5 py-[10px] text-white font-bold text-[16px] transition-colors hover:text-[#f28b4a]"
className="relative block px-5 py-[10px] text-[16px] font-bold text-white transition-colors hover:text-[#f28b4a]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{child.label}
@ -148,7 +146,7 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
{/* Hamburger — mobile */}
<button
onClick={() => setMenuOpen((v) => !v)}
className="lg:hidden text-white p-2 -mr-2"
className="-mr-2 p-2 text-white lg:hidden"
aria-label={menuOpen ? 'Закрити меню' : 'Відкрити меню'}
aria-expanded={menuOpen}
>
@ -176,32 +174,32 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
{/* Mobile dropdown */}
{menuOpen && (
<div
className="lg:hidden mx-auto max-w-[1204px] mt-1 rounded-[20px] px-6 py-5"
className="mx-auto mt-1 max-w-[1204px] rounded-[20px] px-6 py-5 lg:hidden"
style={{
backgroundColor: 'rgba(34,62,13,0.85)',
backdropFilter: 'blur(22px) saturate(160%)',
WebkitBackdropFilter: 'blur(22px) saturate(160%)',
}}
>
<ul className="flex flex-col gap-5 list-none m-0 p-0">
<ul className="m-0 flex list-none flex-col gap-5 p-0">
{navLinks.map((link) => (
<li key={link.href}>
<Link
href={link.href}
onClick={() => setMenuOpen(false)}
className="text-white font-bold text-[18px]"
className="text-[18px] font-bold text-white"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{link.label}
</Link>
{link.children && (
<ul className="flex flex-col gap-2 mt-2 ml-4 list-none p-0">
<ul className="mt-2 ml-4 flex list-none flex-col gap-2 p-0">
{link.children.map((child) => (
<li key={child.href}>
<Link
href={child.href}
onClick={() => setMenuOpen(false)}
className="text-white/70 font-medium text-[16px] hover:text-[#f28b4a] transition-colors"
className="text-[16px] font-medium text-white/70 transition-colors hover:text-[#f28b4a]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{child.label}
@ -216,7 +214,7 @@ export function HeaderClient({ navLinks, ctaLabel, ctaHref, logo }: HeaderClient
<Link
href={ctaHref}
onClick={() => setMenuOpen(false)}
className="inline-flex items-center bg-[#f28b4a] text-white font-bold text-[18px] px-7 py-[10px] rounded-[64px] transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
className="inline-flex items-center rounded-[64px] bg-[#f28b4a] px-7 py-[10px] text-[18px] font-bold text-white transition-shadow hover:shadow-[0_0_20px_0_#f28b4a]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{ctaLabel}

View file

@ -61,15 +61,23 @@ export function LocationsSlider({ locations }: { locations: LocationData[] }) {
aria-label="Попередня локація"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M13 4L7 10L13 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M13 4L7 10L13 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
<div
className="relative overflow-hidden"
style={{
maskImage: 'linear-gradient(90deg, transparent 0%, black 5%, black 95%, transparent 100%)',
WebkitMaskImage: 'linear-gradient(90deg, transparent 0%, black 5%, black 95%, transparent 100%)',
maskImage:
'linear-gradient(90deg, transparent 0%, black 5%, black 95%, transparent 100%)',
WebkitMaskImage:
'linear-gradient(90deg, transparent 0%, black 5%, black 95%, transparent 100%)',
}}
>
<div
@ -90,22 +98,22 @@ export function LocationsSlider({ locations }: { locations: LocationData[] }) {
className="object-cover transition-transform duration-700 ease-out group-hover:scale-105"
priority={idx < 2}
/>
<div className="absolute right-0 top-0 flex h-full w-[327px] max-w-[60%] flex-col justify-center gap-[28px] bg-[rgba(34,62,13,0.8)] px-[30px]">
<div className="absolute top-0 right-0 flex h-full w-[327px] max-w-[60%] flex-col justify-center gap-[28px] bg-[rgba(34,62,13,0.8)] px-[30px]">
<div className="flex flex-col gap-3 text-white">
<h3
className="text-[24px] font-bold leading-[1.1] uppercase"
className="text-[24px] leading-[1.1] font-bold uppercase"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{loc.name}
</h3>
<p
className="text-[20px] font-medium leading-[1.5] text-[#fdcf54]"
className="text-[20px] leading-[1.5] font-medium text-[#fdcf54]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{loc.tagline}
</p>
<p
className="line-clamp-5 text-[15px] font-normal leading-[1.5] text-white/90"
className="line-clamp-5 text-[15px] leading-[1.5] font-normal text-white/90"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{loc.description}
@ -124,7 +132,13 @@ export function LocationsSlider({ locations }: { locations: LocationData[] }) {
aria-label="Наступна локація"
>
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" aria-hidden="true">
<path d="M7 4L13 10L7 16" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" />
<path
d="M7 4L13 10L7 16"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</button>
</div>

View file

@ -137,7 +137,11 @@ export function Reviews({ data, title }: ReviewsProps) {
>
{review.ago}
</span>
<StarRating value={review.rating ?? 5} size={16} className="flex gap-[2px]" />
<StarRating
value={review.rating ?? 5}
size={16}
className="flex gap-[2px]"
/>
</div>
</div>
</div>

View file

@ -167,7 +167,7 @@ export function WhyParents({ items, sideGallery, title }: WhyParentsProps) {
>
<div className="overflow-hidden">
<p
className="pt-2 text-[16px] font-light leading-[1.6] text-[#272727]"
className="pt-2 text-[16px] leading-[1.6] font-light text-[#272727]"
style={{ fontFamily: 'var(--font-poppins, Poppins), sans-serif' }}
>
{item.description}

View file

@ -18,7 +18,7 @@ interface PageHeroProps {
export function PageHero({ title, subtitle, bgSrc, children }: PageHeroProps) {
const src = bgSrc ?? DEFAULT_BG
return (
<div className="relative -mt-[60px] overflow-hidden px-8 pb-16 pt-[calc(60px+48px)] lg:-mt-[120px] lg:pt-[calc(120px+64px)]">
<div className="relative -mt-[60px] overflow-hidden px-8 pt-[calc(60px+48px)] pb-16 lg:-mt-[120px] lg:pt-[calc(120px+64px)]">
<Image
src={src}
alt=""
@ -28,13 +28,10 @@ export function PageHero({ title, subtitle, bgSrc, children }: PageHeroProps) {
className="object-cover"
style={{ zIndex: -2 }}
/>
<div
className="absolute inset-0"
style={{ zIndex: -1, background: 'rgba(34,62,13,0.55)' }}
/>
<div className="absolute inset-0" style={{ zIndex: -1, background: 'rgba(34,62,13,0.55)' }} />
<div className="relative mx-auto max-w-[1140px] text-white">
<h1
className="text-[40px] font-bold uppercase leading-[1.1] lg:text-[56px]"
className="text-[40px] leading-[1.1] font-bold uppercase lg:text-[56px]"
style={{ fontFamily: 'var(--font-montserrat, Montserrat), sans-serif' }}
>
{title}

View file

@ -93,8 +93,7 @@ export const HomePage: GlobalConfig = {
name: 'images',
type: 'array',
admin: {
description:
'Завантажте 9 фото для слайдера Фотогалерея — порядок і alt керуйте тут.',
description: 'Завантажте 9 фото для слайдера Фотогалерея — порядок і alt керуйте тут.',
},
fields: [
{ name: 'image', type: 'upload', relationTo: 'media' },

View file

@ -19,13 +19,7 @@ interface Options {
*/
export function useAutoScroll<T extends HTMLElement>(
ref: RefObject<T | null>,
{
speed = 1,
intervalMs = 16,
step = false,
pauseOnHover = true,
disabled = false,
}: Options = {},
{ speed = 1, intervalMs = 16, step = false, pauseOnHover = true, disabled = false }: Options = {}
) {
useEffect(() => {
if (disabled) return