fix: remove fallback content, fix video upload and positioning
- Remove hardcoded fallback data — DB is sole content source - Sections render conditionally when data exists - Hero video slots save after each upload (not only when all 3 filled) - Video positions preserved (left/center/right) with empty string slots - Client-side 10MB hard limit on video uploads with clear error - Server-side upload error handling for body size limits - Guard Team section against empty members array - Clean up old uploaded images and videos
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 481 KiB |
|
Before Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 27 KiB |
@@ -1,3 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 234 192" fill="currentColor" fill-rule="evenodd">
|
||||
<path d="M118.02,188.43 C118.04,184.10 120.51,173.30 122.96,166.79 C126.11,158.42 133.55,147.62 144.55,135.42 C165.53,112.15 170.96,101.38 170.99,82.98 C171.00,72.35 168.51,62.96 162.47,50.94 C160.00,46.02 157.78,42.00 157.53,42.00 C157.29,42.00 158.24,45.04 159.64,48.75 C163.04,57.78 165.96,71.24 165.96,78.00 C165.97,85.89 163.51,95.22 159.27,103.40 C156.31,109.09 152.52,113.57 140.17,126.00 C131.69,134.53 123.42,143.44 121.79,145.81 C116.23,153.88 110.87,167.99 109.81,177.28 C109.51,179.99 109.37,179.90 105.02,174.28 C102.55,171.10 98.74,166.54 96.55,164.15 L92.56,159.80 L95.53,157.70 C100.61,154.12 105.90,148.12 108.76,142.70 C111.22,138.02 111.50,136.50 111.47,127.50 C111.45,118.43 111.02,116.15 106.86,103.00 C101.17,85.06 99.60,76.75 100.25,68.17 C100.75,61.51 104.60,48.83 107.29,45.00 C108.57,43.16 108.76,43.69 109.31,50.84 C110.42,65.22 115.99,75.08 126.37,81.04 C133.31,85.02 133.82,84.76 128.92,79.75 C124.20,74.93 119.44,65.68 118.16,58.84 C116.46,49.75 119.09,39.24 125.73,28.59 L128.79,23.69 L130.02,29.59 C130.69,32.84 133.23,39.92 135.65,45.33 C143.15,62.02 144.36,69.90 141.53,83.29 C140.04,90.28 134.00,104.04 127.66,114.86 C125.68,118.24 124.39,120.97 124.78,120.93 C125.18,120.90 128.41,117.53 131.97,113.46 C145.06,98.47 150.50,85.84 150.46,70.50 C150.43,59.80 149.70,57.36 141.46,40.79 C137.98,33.80 134.83,26.02 134.45,23.49 C133.85,19.50 134.07,18.56 136.10,16.40 C139.50,12.77 147.93,7.39 153.86,5.06 L159.00,3.03 L159.00,9.32 C159.00,18.37 162.11,24.01 172.06,33.00 C176.46,36.97 180.72,41.50 181.53,43.06 C183.67,47.20 183.39,56.94 180.95,63.13 C178.14,70.25 180.87,67.95 184.97,59.74 C190.78,48.12 188.70,39.73 177.15,28.15 C173.28,24.27 169.43,20.06 168.61,18.80 C166.51,15.58 164.79,7.21 165.54,3.83 C166.16,1.00 166.17,1.00 174.33,1.01 C178.82,1.02 184.15,1.29 186.17,1.63 C189.69,2.21 189.81,2.37 189.29,5.62 C188.42,10.94 190.83,20.71 195.10,29.20 C197.26,33.49 199.19,37.00 199.40,37.00 C199.62,37.00 198.67,33.51 197.31,29.25 C195.35,23.12 194.91,19.90 195.17,13.83 C195.36,9.61 195.85,5.81 196.28,5.39 C197.35,4.31 205.52,8.43 211.67,13.15 C218.45,18.35 221.00,23.72 220.99,32.74 C220.98,36.46 220.28,41.98 219.42,45.00 C218.57,48.02 217.63,51.40 217.34,52.50 C216.30,56.51 222.34,45.32 224.52,39.20 C225.76,35.73 227.01,32.66 227.29,32.38 C227.57,32.09 228.79,34.48 229.99,37.68 C238.21,59.57 232.78,83.80 215.76,101.15 C209.43,107.60 207.42,108.54 209.17,104.25 C210.91,100.00 210.37,85.54 208.08,74.64 C206.93,69.22 205.95,62.24 205.90,59.14 C205.80,53.85 205.74,53.72 204.93,57.00 C204.45,58.92 204.13,68.15 204.22,77.50 C204.37,93.11 204.20,94.91 202.15,99.45 C198.99,106.51 192.06,115.46 190.76,114.16 C188.49,111.89 189.93,84.88 192.72,77.19 C194.09,73.45 189.30,79.05 186.68,84.26 C182.02,93.55 180.69,101.03 181.41,113.97 L182.05,125.50 L169.94,135.00 C153.90,147.58 132.06,170.01 124.13,182.05 C118.83,190.09 118.00,190.96 118.02,188.43 Z M83.09,150.59 C78.00,145.44 77.78,144.99 78.44,141.34 C78.82,139.23 81.24,133.00 83.81,127.50 C88.47,117.57 88.50,117.43 88.49,107.00 C88.47,99.38 87.96,94.99 86.59,91.00 C84.28,84.21 77.06,69.61 76.36,70.30 C76.08,70.58 76.56,72.19 77.43,73.87 C79.91,78.65 82.99,92.88 82.99,99.57 C83.00,108.39 80.69,114.86 73.96,124.82 C70.68,129.67 68.00,134.17 68.00,134.82 C68.00,135.47 67.62,136.00 67.16,136.00 C66.07,136.00 57.00,128.93 57.00,128.07 C57.00,127.71 59.03,123.47 61.50,118.66 C66.60,108.75 67.24,103.18 64.48,92.59 C62.01,83.09 61.32,83.22 61.40,93.17 C61.45,100.19 61.02,103.39 59.69,106.10 C57.49,110.57 48.29,121.00 46.56,121.00 C44.40,121.00 39.79,109.24 39.24,102.34 C38.56,93.90 40.48,89.09 48.68,78.77 C62.32,61.60 65.53,49.22 60.98,31.41 C58.70,22.51 54.61,13.20 50.08,6.62 C47.54,2.92 47.30,2.10 48.60,1.60 C50.50,0.87 66.31,0.80 68.17,1.51 C69.18,1.90 69.42,4.19 69.15,10.95 C68.88,18.05 69.27,21.45 71.06,27.48 C72.30,31.66 73.77,35.36 74.33,35.70 C74.97,36.10 75.06,35.62 74.57,34.41 C74.15,33.36 73.57,28.88 73.27,24.45 C72.70,15.76 74.85,5.38 77.44,4.39 C79.59,3.56 92.09,10.37 97.73,15.45 C100.57,18.00 103.83,21.61 104.99,23.48 L107.09,26.88 L103.66,31.69 C94.93,43.97 91.54,55.17 91.64,71.50 C91.72,84.83 92.69,89.79 99.08,109.50 C105.86,130.41 104.79,139.90 94.39,151.01 C91.83,153.75 89.44,156.00 89.08,156.00 C88.72,156.00 86.03,153.57 83.09,150.59 Z M29.50,109.90 C26.20,107.67 21.64,104.05 19.38,101.84 L15.26,97.83 L17.24,92.67 C19.86,85.83 19.20,74.50 15.57,64.04 C12.16,54.24 10.98,53.26 12.75,61.71 C14.48,69.97 13.94,81.02 11.53,86.50 L9.77,90.50 L6.92,84.50 C2.82,75.84 1.00,67.75 1.00,58.18 C1.00,42.04 6.09,29.69 17.39,18.40 C23.48,12.31 32.07,6.09 30.82,8.67 C30.59,9.13 28.88,12.62 27.02,16.44 C21.43,27.90 22.74,38.16 31.08,48.28 C35.14,53.21 36.55,53.01 33.93,47.87 C31.25,42.61 30.40,34.47 31.90,28.31 C33.17,23.08 42.81,3.00 44.05,3.00 C44.47,3.00 46.18,6.79 47.86,11.42 C58.07,39.63 56.90,53.23 42.67,72.14 C31.96,86.38 29.60,96.21 33.92,108.52 C34.98,111.54 35.77,113.99 35.67,113.97 C35.58,113.96 32.80,112.12 29.50,109.90 Z"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 5.0 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 579 KiB |
|
Before Width: | Height: | Size: 43 KiB |
|
Before Width: | Height: | Size: 32 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 313 KiB |
|
Before Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 281 KiB |
|
Before Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
Before Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 105 KiB |
|
Before Width: | Height: | Size: 301 KiB |
|
Before Width: | Height: | Size: 51 KiB |
@@ -6,7 +6,7 @@ import { InputField } from "../_components/FormField";
|
||||
import { adminFetch } from "@/lib/csrf";
|
||||
import { Upload, X, Loader2, Smartphone, Monitor, Star } from "lucide-react";
|
||||
|
||||
const MAX_VIDEO_SIZE_MB = 8;
|
||||
const MAX_VIDEO_SIZE_MB = 10;
|
||||
const MAX_VIDEO_SIZE_BYTES = MAX_VIDEO_SIZE_MB * 1024 * 1024;
|
||||
|
||||
function formatFileSize(bytes: number): string {
|
||||
@@ -209,10 +209,8 @@ function VideoManager({
|
||||
const syncToParent = useCallback(
|
||||
(updated: (string | null)[]) => {
|
||||
setSlots(updated);
|
||||
// Only propagate when all 3 are filled
|
||||
if (updated.every((s) => s !== null)) {
|
||||
onChange(updated as string[]);
|
||||
}
|
||||
// Save all 3 slots (empty string for unfilled) to preserve positions
|
||||
onChange(updated.map((s) => s || ""));
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
@@ -246,10 +244,10 @@ function VideoManager({
|
||||
async function handleUpload(idx: number, file: File) {
|
||||
if (file.size > MAX_VIDEO_SIZE_BYTES) {
|
||||
const sizeMb = (file.size / (1024 * 1024)).toFixed(1);
|
||||
setSizeWarning(`Видео ${sizeMb} МБ — рекомендуем до ${MAX_VIDEO_SIZE_MB} МБ для быстрой загрузки`);
|
||||
} else {
|
||||
setSizeWarning(null);
|
||||
alert(`Видео ${sizeMb} МБ — максимум ${MAX_VIDEO_SIZE_MB} МБ. Сожмите видео и попробуйте снова.`);
|
||||
return;
|
||||
}
|
||||
setSizeWarning(null);
|
||||
setUploadingIdx(idx);
|
||||
try {
|
||||
const form = new FormData();
|
||||
@@ -260,14 +258,21 @@ function VideoManager({
|
||||
body: form,
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.json();
|
||||
alert(err.error || "Ошибка загрузки");
|
||||
const text = await res.text();
|
||||
let msg = "Ошибка загрузки";
|
||||
try {
|
||||
const err = JSON.parse(text);
|
||||
msg = err.error || msg;
|
||||
} catch { /* empty response */ }
|
||||
alert(`${msg} (${res.status})`);
|
||||
return;
|
||||
}
|
||||
const { path } = await res.json();
|
||||
const updated = [...slots];
|
||||
updated[idx] = path;
|
||||
syncToParent(updated);
|
||||
} catch (e) {
|
||||
alert(`Ошибка сети: ${e instanceof Error ? e.message : "попробуйте снова"}`);
|
||||
} finally {
|
||||
setUploadingIdx(null);
|
||||
}
|
||||
@@ -276,8 +281,7 @@ function VideoManager({
|
||||
function handleRemove(idx: number) {
|
||||
const updated = [...slots];
|
||||
updated[idx] = null;
|
||||
setSlots(updated);
|
||||
// Don't propagate incomplete state — keep old saved videos in DB
|
||||
syncToParent(updated);
|
||||
}
|
||||
|
||||
const allFilled = slots.every((s) => s !== null);
|
||||
@@ -351,17 +355,17 @@ export default function HeroEditorPage() {
|
||||
|
||||
<InputField
|
||||
label="Заголовок"
|
||||
value={data.headline}
|
||||
value={data.headline || ""}
|
||||
onChange={(v) => update({ ...data, headline: v })}
|
||||
/>
|
||||
<InputField
|
||||
label="Подзаголовок"
|
||||
value={data.subheadline}
|
||||
value={data.subheadline || ""}
|
||||
onChange={(v) => update({ ...data, subheadline: v })}
|
||||
/>
|
||||
<InputField
|
||||
label="Текст кнопки"
|
||||
value={data.ctaText}
|
||||
value={data.ctaText || ""}
|
||||
onChange={(v) => update({ ...data, ctaText: v })}
|
||||
/>
|
||||
</>
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getSection, setSection, SECTION_KEYS } from "@/lib/db";
|
||||
import { siteContent } from "@/data/content";
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { invalidateContentCache } from "@/lib/content";
|
||||
|
||||
@@ -12,16 +11,10 @@ export async function GET(_request: NextRequest, { params }: Params) {
|
||||
return NextResponse.json({ error: "Invalid section key" }, { status: 400 });
|
||||
}
|
||||
|
||||
let data = getSection(key);
|
||||
const data = getSection(key);
|
||||
if (!data) {
|
||||
// Auto-seed from fallback content if section doesn't exist yet
|
||||
const fallback = (siteContent as unknown as Record<string, unknown>)[key];
|
||||
if (fallback) {
|
||||
setSection(key, fallback);
|
||||
data = fallback;
|
||||
} else {
|
||||
return NextResponse.json({ error: "Section not found" }, { status: 404 });
|
||||
}
|
||||
// Return empty object so admin editor can initialize fields
|
||||
return NextResponse.json({});
|
||||
}
|
||||
|
||||
return NextResponse.json(data, {
|
||||
|
||||
@@ -10,10 +10,19 @@ const IMAGE_FOLDERS = ["team", "master-classes", "news", "classes"];
|
||||
const VIDEO_FOLDERS = ["hero"];
|
||||
const ALL_FOLDERS = [...IMAGE_FOLDERS, ...VIDEO_FOLDERS];
|
||||
const IMAGE_MAX_SIZE = 5 * 1024 * 1024; // 5MB
|
||||
const VIDEO_MAX_SIZE = 50 * 1024 * 1024; // 50MB
|
||||
const VIDEO_MAX_SIZE = 10 * 1024 * 1024; // 10MB
|
||||
|
||||
export async function POST(request: NextRequest) {
|
||||
const formData = await request.formData();
|
||||
let formData: FormData;
|
||||
try {
|
||||
formData = await request.formData();
|
||||
} catch (e) {
|
||||
const msg = e instanceof Error ? e.message : "Upload failed";
|
||||
return NextResponse.json(
|
||||
{ error: msg.includes("size") || msg.includes("multipart") ? "Файл слишком большой (макс. 10 МБ)" : `Ошибка загрузки: ${msg}` },
|
||||
{ status: 413 }
|
||||
);
|
||||
}
|
||||
const file = formData.get("file") as File | null;
|
||||
const rawFolder = (formData.get("folder") as string) || "team";
|
||||
const folder = ALL_FOLDERS.includes(rawFolder) ? rawFolder : "team";
|
||||
@@ -26,16 +35,16 @@ export async function POST(request: NextRequest) {
|
||||
const allowedTypes = isVideoFolder ? VIDEO_TYPES : IMAGE_TYPES;
|
||||
if (!allowedTypes.includes(file.type)) {
|
||||
const msg = isVideoFolder
|
||||
? "Only MP4 and WebM videos are allowed"
|
||||
: "Only JPEG, PNG, WebP, and AVIF are allowed";
|
||||
? "Допустимы только MP4 и WebM"
|
||||
: "Допустимы только JPEG, PNG, WebP и AVIF";
|
||||
return NextResponse.json({ error: msg }, { status: 400 });
|
||||
}
|
||||
|
||||
const maxSize = isVideoFolder ? VIDEO_MAX_SIZE : IMAGE_MAX_SIZE;
|
||||
if (file.size > maxSize) {
|
||||
const label = isVideoFolder ? "50MB" : "5MB";
|
||||
const label = isVideoFolder ? "10 МБ" : "5 МБ";
|
||||
return NextResponse.json(
|
||||
{ error: `File too large (max ${label})` },
|
||||
{ error: `Файл слишком большой (макс. ${label})` },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
@@ -45,7 +54,7 @@ export async function POST(request: NextRequest) {
|
||||
const allowedExts = isVideoFolder ? VIDEO_EXTENSIONS : IMAGE_EXTENSIONS;
|
||||
if (!allowedExts.includes(ext)) {
|
||||
return NextResponse.json(
|
||||
{ error: "Invalid file extension" },
|
||||
{ error: "Недопустимое расширение файла" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -22,13 +22,15 @@ const oswald = localFont({
|
||||
});
|
||||
|
||||
export function generateMetadata(): Metadata {
|
||||
const { meta } = getContent();
|
||||
const content = getContent();
|
||||
const title = content?.meta?.title || "BLACK HEART DANCE HOUSE";
|
||||
const description = content?.meta?.description || "";
|
||||
return {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title,
|
||||
description,
|
||||
openGraph: {
|
||||
title: meta.title,
|
||||
description: meta.description,
|
||||
title,
|
||||
description,
|
||||
locale: "ru_RU",
|
||||
type: "website",
|
||||
},
|
||||
|
||||
@@ -33,24 +33,26 @@ export default function HomePage() {
|
||||
<ClientShell>
|
||||
<Header />
|
||||
<main id="main-content">
|
||||
<Hero data={content.hero} />
|
||||
<About
|
||||
data={content.about}
|
||||
stats={{
|
||||
trainers: content.team.members.length,
|
||||
classes: content.classes.items.length,
|
||||
locations: content.schedule.locations.length,
|
||||
}}
|
||||
/>
|
||||
<Classes data={content.classes} />
|
||||
<Team data={content.team} schedule={content.schedule.locations} />
|
||||
{openDayData && <OpenDay data={openDayData} popups={content.popups} teamMembers={content.team.members} />}
|
||||
<Schedule data={content.schedule} scheduleConfig={content.scheduleConfig} classItems={content.classes.items} teamMembers={content.team.members} />
|
||||
<Pricing data={content.pricing} />
|
||||
<MasterClasses data={content.masterClasses} regCounts={mcRegCounts} popups={content.popups} />
|
||||
<News data={content.news} />
|
||||
<FAQ data={content.faq} />
|
||||
<Contact data={content.contact} />
|
||||
{content?.hero && <Hero data={content.hero} />}
|
||||
{content?.about && (
|
||||
<About
|
||||
data={content.about}
|
||||
stats={{
|
||||
trainers: content.team?.members?.length ?? 0,
|
||||
classes: content.classes?.items?.length ?? 0,
|
||||
locations: content.schedule?.locations?.length ?? 0,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{content?.classes && <Classes data={content.classes} />}
|
||||
{content?.team && <Team data={content.team} schedule={content.schedule?.locations} />}
|
||||
{openDayData && content?.popups && <OpenDay data={openDayData} popups={content.popups} teamMembers={content.team?.members ?? []} />}
|
||||
{content?.schedule && <Schedule data={content.schedule} scheduleConfig={content.scheduleConfig} classItems={content.classes?.items ?? []} teamMembers={content.team?.members ?? []} />}
|
||||
{content?.pricing && <Pricing data={content.pricing} />}
|
||||
{content?.masterClasses && <MasterClasses data={content.masterClasses} regCounts={mcRegCounts} popups={content.popups} />}
|
||||
{content?.news && <News data={content.news} />}
|
||||
{content?.faq && <FAQ data={content.faq} />}
|
||||
{content?.contact && <Contact data={content.contact} />}
|
||||
<BackToTop />
|
||||
<FloatingContact />
|
||||
</main>
|
||||
|
||||
@@ -20,9 +20,10 @@ export function Hero({ data: hero }: HeroProps) {
|
||||
const overlayRef = useRef<HTMLDivElement>(null);
|
||||
const readyCount = useRef(0);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const videos = hero.videos?.length ? hero.videos : DEFAULT_VIDEOS;
|
||||
const centerVideo = videos[Math.floor(videos.length / 2)] || videos[0];
|
||||
const totalVideos = videos.slice(0, 3).length + 1; // desktop (3) + mobile (1)
|
||||
const hasVideos = hero.videos?.some(Boolean);
|
||||
const videos = hasVideos ? hero.videos! : DEFAULT_VIDEOS;
|
||||
const centerVideo = videos[1] || videos[0];
|
||||
const totalVideos = videos.slice(0, 3).filter(Boolean).length + 1; // desktop (filled) + mobile (1)
|
||||
|
||||
const prefersReducedMotion = useRef(false);
|
||||
|
||||
@@ -106,6 +107,7 @@ export function Hero({ data: hero }: HeroProps) {
|
||||
{/* Desktop: diagonal split with all videos */}
|
||||
<div className="absolute inset-0 hidden md:block">
|
||||
{videos.slice(0, 3).map((src, i) => {
|
||||
if (!src) return null;
|
||||
const positions = [
|
||||
{ left: "0%", width: "38%" },
|
||||
{ left: "31%", width: "38%" },
|
||||
@@ -118,7 +120,7 @@ export function Hero({ data: hero }: HeroProps) {
|
||||
];
|
||||
return (
|
||||
<div
|
||||
key={src}
|
||||
key={`video-${i}`}
|
||||
className="absolute top-0 bottom-0 overflow-hidden"
|
||||
style={{
|
||||
left: positions[i].left,
|
||||
|
||||
@@ -14,6 +14,7 @@ interface TeamProps {
|
||||
}
|
||||
|
||||
export function Team({ data: team, schedule }: TeamProps) {
|
||||
if (!team?.members?.length) return null;
|
||||
const [activeIndex, setActiveIndex] = useState(0);
|
||||
const [showProfile, setShowProfile] = useState(false);
|
||||
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { getSiteContent } from "@/lib/db";
|
||||
import { siteContent as fallback } from "@/data/content";
|
||||
import type { SiteContent } from "@/types/content";
|
||||
|
||||
let cached: { data: SiteContent; expiresAt: number } | null = null;
|
||||
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
||||
|
||||
export function getContent(): SiteContent {
|
||||
export function getContent(): SiteContent | null {
|
||||
const now = Date.now();
|
||||
if (cached && now < cached.expiresAt) {
|
||||
return cached.data;
|
||||
@@ -17,9 +16,9 @@ export function getContent(): SiteContent {
|
||||
cached = { data: content, expiresAt: now + CACHE_TTL };
|
||||
return content;
|
||||
}
|
||||
return fallback;
|
||||
return null;
|
||||
} catch {
|
||||
return fallback;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||