diff --git a/public/images/team/20190816-164735-1774466974671.jpg b/public/images/team/20190816-164735-1774466974671.jpg new file mode 100644 index 0000000..d63f3a9 Binary files /dev/null and b/public/images/team/20190816-164735-1774466974671.jpg differ diff --git a/public/images/team/20190816-164735-1774466997272.jpg b/public/images/team/20190816-164735-1774466997272.jpg new file mode 100644 index 0000000..d63f3a9 Binary files /dev/null and b/public/images/team/20190816-164735-1774466997272.jpg differ diff --git a/public/images/team/20190816-164735-1774467131281.jpg b/public/images/team/20190816-164735-1774467131281.jpg new file mode 100644 index 0000000..d63f3a9 Binary files /dev/null and b/public/images/team/20190816-164735-1774467131281.jpg differ diff --git a/public/images/team/20190816-164735-1774467278915.jpg b/public/images/team/20190816-164735-1774467278915.jpg new file mode 100644 index 0000000..d63f3a9 Binary files /dev/null and b/public/images/team/20190816-164735-1774467278915.jpg differ diff --git a/public/images/team/20190816-164735-1774467770193.jpg b/public/images/team/20190816-164735-1774467770193.jpg new file mode 100644 index 0000000..d63f3a9 Binary files /dev/null and b/public/images/team/20190816-164735-1774467770193.jpg differ diff --git a/public/images/team/20190816-164735-1774467783564.jpg b/public/images/team/20190816-164735-1774467783564.jpg new file mode 100644 index 0000000..d63f3a9 Binary files /dev/null and b/public/images/team/20190816-164735-1774467783564.jpg differ diff --git a/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774467916236.jpg b/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774467916236.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774467916236.jpg differ diff --git a/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774467928377.jpg b/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774467928377.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774467928377.jpg differ diff --git a/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774468027961.jpg b/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774468027961.jpg new file mode 100644 index 0000000..2a5d46f Binary files /dev/null and b/public/images/team/saveclip-app-605404066-17852939781600031-474959415-1774468027961.jpg differ diff --git a/src/app/admin/_components/FormField.tsx b/src/app/admin/_components/FormField.tsx index 123f552..3c5119b 100644 --- a/src/app/admin/_components/FormField.tsx +++ b/src/app/admin/_components/FormField.tsx @@ -1,7 +1,7 @@ import { useRef, useEffect, useState, useMemo } from "react"; -import { Plus, X, Upload, Loader2, Link, ImageIcon, Calendar, AlertCircle, MapPin } from "lucide-react"; +import { Plus, X, Upload, Loader2, Link, ImageIcon, AlertCircle } from "lucide-react"; import { adminFetch } from "@/lib/csrf"; -import type { RichListItem, VictoryItem } from "@/types/content"; +import type { RichListItem } from "@/types/content"; interface InputFieldProps { label: string; @@ -414,9 +414,10 @@ interface VictoryListFieldProps { onChange: (items: RichListItem[]) => void; placeholder?: string; onLinkValidate?: (key: string, error: string | null) => void; + onUploadComplete?: () => void; } -export function VictoryListField({ label, items, onChange, placeholder, onLinkValidate }: VictoryListFieldProps) { +export function VictoryListField({ label, items, onChange, placeholder, onLinkValidate, onUploadComplete }: VictoryListFieldProps) { const [draft, setDraft] = useState(""); const [uploadingIndex, setUploadingIndex] = useState(null); @@ -455,6 +456,7 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa const result = await res.json(); if (result.path) { onChange(items.map((item, i) => (i === index ? { ...item, image: result.path } : item))); + onUploadComplete?.(); } } catch { /* upload failed */ } finally { setUploadingIndex(null); @@ -530,151 +532,6 @@ export function VictoryListField({ label, items, onChange, placeholder, onLinkVa ); } -// --- Date Range Picker --- -// Parses Russian date formats: "22.02.2025", "22-23.02.2025", "22.02-01.03.2025" -function parseDateRange(value: string): { start: string; end: string } { - if (!value) return { start: "", end: "" }; - - // "22-23.02.2025" → same month range - const sameMonth = value.match(/^(\d{1,2})-(\d{1,2})\.(\d{2})\.(\d{4})$/); - if (sameMonth) { - const [, d1, d2, m, y] = sameMonth; - return { - start: `${y}-${m}-${d1.padStart(2, "0")}`, - end: `${y}-${m}-${d2.padStart(2, "0")}`, - }; - } - - // "22.02-01.03.2025" → cross-month range - const crossMonth = value.match(/^(\d{1,2})\.(\d{2})-(\d{1,2})\.(\d{2})\.(\d{4})$/); - if (crossMonth) { - const [, d1, m1, d2, m2, y] = crossMonth; - return { - start: `${y}-${m1}-${d1.padStart(2, "0")}`, - end: `${y}-${m2}-${d2.padStart(2, "0")}`, - }; - } - - // "22.02.2025" → single date - const single = value.match(/^(\d{1,2})\.(\d{2})\.(\d{4})$/); - if (single) { - const [, d, m, y] = single; - const iso = `${y}-${m}-${d.padStart(2, "0")}`; - return { start: iso, end: "" }; - } - - return { start: "", end: "" }; -} - -function formatDateRange(start: string, end: string): string { - if (!start) return ""; - const [sy, sm, sd] = start.split("-"); - if (!end) return `${sd}.${sm}.${sy}`; - const [ey, em, ed] = end.split("-"); - if (sm === em && sy === ey) return `${sd}-${ed}.${sm}.${sy}`; - return `${sd}.${sm}-${ed}.${em}.${ey}`; -} - -interface DateRangeFieldProps { - value: string; - onChange: (value: string) => void; -} - -export function DateRangeField({ value, onChange }: DateRangeFieldProps) { - const { start, end } = parseDateRange(value); - - function handleChange(s: string, e: string) { - onChange(formatDateRange(s, e)); - } - - return ( -
- - handleChange(e.target.value, end)} - className="w-[130px] rounded-md border border-white/10 bg-neutral-800 px-1.5 py-1.5 text-xs text-white outline-none focus:border-gold transition-colors [color-scheme:dark]" - /> - - handleChange(start, e.target.value)} - className="w-[130px] rounded-md border border-white/10 bg-neutral-800 px-1.5 py-1.5 text-xs text-white outline-none focus:border-gold transition-colors [color-scheme:dark]" - /> -
- ); -} - -// --- City Autocomplete Field --- -interface CityFieldProps { - value: string; - onChange: (value: string) => void; - error?: string; - onSearch?: (query: string) => void; - suggestions?: string[]; - onSelectSuggestion?: (value: string) => void; -} - -export function CityField({ value, onChange, error, onSearch, suggestions, onSelectSuggestion }: CityFieldProps) { - const [focused, setFocused] = useState(false); - const containerRef = useRef(null); - - useEffect(() => { - if (!focused) return; - function handle(e: MouseEvent) { - if (containerRef.current && !containerRef.current.contains(e.target as Node)) { - setFocused(false); - } - } - document.addEventListener("mousedown", handle); - return () => document.removeEventListener("mousedown", handle); - }, [focused]); - - return ( -
-
- - { - onChange(e.target.value); - onSearch?.(e.target.value); - }} - onFocus={() => setFocused(true)} - placeholder="Город, страна" - className={`w-full rounded-md border bg-neutral-800 pl-6 pr-3 py-1.5 text-sm text-white placeholder-neutral-600 outline-none transition-colors ${ - error ? "border-red-500/50" : "border-white/10 focus:border-gold" - }`} - /> - {error && } -
- {error &&

{error}

} - {focused && suggestions && suggestions.length > 0 && ( -
- {suggestions.map((s) => ( - - ))} -
- )} -
- ); -} - // --- Link Field with Validation --- interface ValidatedLinkFieldProps { value: string; @@ -729,110 +586,6 @@ export function ValidatedLinkField({ value, onChange, onValidate, validationKey, ); } -interface VictoryItemListFieldProps { - label: string; - items: VictoryItem[]; - onChange: (items: VictoryItem[]) => void; - cityErrors?: Record; - citySuggestions?: { index: number; items: string[] } | null; - onCitySearch?: (index: number, query: string) => void; - onCitySelect?: (index: number, value: string) => void; - onLinkValidate?: (key: string, error: string | null) => void; -} - -export function VictoryItemListField({ label, items, onChange, cityErrors, citySuggestions, onCitySearch, onCitySelect, onLinkValidate }: VictoryItemListFieldProps) { - function add() { - onChange([...items, { type: "place", place: "", category: "", competition: "" }]); - } - - function remove(index: number) { - onChange(items.filter((_, i) => i !== index)); - } - - function update(index: number, field: keyof VictoryItem, value: string) { - onChange(items.map((item, i) => (i === index ? { ...item, [field]: value || undefined } : item))); - } - - return ( -
- -
- {items.map((item, i) => ( -
-
- - update(i, "place", e.target.value)} - placeholder="1 место, финалист..." - className={`w-28 shrink-0 ${smallInput}`} - /> - update(i, "category", e.target.value)} - placeholder="Категория" - className={`flex-1 ${smallInput}`} - /> - update(i, "competition", e.target.value)} - placeholder="Чемпионат" - className={`flex-1 ${smallInput}`} - /> - -
-
- update(i, "location", v)} - error={cityErrors?.[i]} - onSearch={(q) => onCitySearch?.(i, q)} - suggestions={citySuggestions?.index === i ? citySuggestions.items : undefined} - onSelectSuggestion={(v) => onCitySelect?.(i, v)} - /> - update(i, "date", v)} - /> -
- update(i, "link", v)} - validationKey={`victory-${i}`} - onValidate={onLinkValidate} - /> -
- ))} - -
-
- ); -} - // --- Autocomplete Multi-Select --- export function AutocompleteMulti({ label, diff --git a/src/app/admin/team/[id]/page.tsx b/src/app/admin/team/[id]/page.tsx index 627d853..764634a 100644 --- a/src/app/admin/team/[id]/page.tsx +++ b/src/app/admin/team/[id]/page.tsx @@ -4,10 +4,10 @@ import { useState, useEffect, useRef, useCallback } from "react"; import { useRouter, useParams } from "next/navigation"; import Image from "next/image"; import { Save, Loader2, Check, ArrowLeft, Upload, AlertCircle } from "lucide-react"; -import { InputField, TextareaField, ListField, VictoryListField, VictoryItemListField, AutocompleteMulti } from "../../_components/FormField"; +import { InputField, TextareaField, VictoryListField, AutocompleteMulti } from "../../_components/FormField"; import { useToast } from "../../_components/Toast"; import { adminFetch } from "@/lib/csrf"; -import type { RichListItem, VictoryItem } from "@/types/content"; +import type { RichListItem } from "@/types/content"; function extractUsername(value: string): string { if (!value) return ""; @@ -23,8 +23,7 @@ interface MemberForm { instagram: string; shortDescription: string; description: string; - experience: string[]; - victories: VictoryItem[]; + victories: RichListItem[]; education: RichListItem[]; } @@ -46,7 +45,6 @@ function TeamMemberEditor() { instagram: "", shortDescription: "", description: "", - experience: [], victories: [], education: [], }); @@ -78,43 +76,6 @@ function TeamMemberEditor() { // Link validation for bio const [linkErrors, setLinkErrors] = useState>({}); - function validateUrl(url: string): boolean { - if (!url) return true; - try { new URL(url); return true; } catch { return false; } - } - - // City validation for victories - const [cityErrors, setCityErrors] = useState>({}); - const [citySuggestions, setCitySuggestions] = useState<{ index: number; items: string[] } | null>(null); - const cityTimerRef = useRef | null>(null); - - const searchCity = useCallback((index: number, query: string) => { - if (cityTimerRef.current) clearTimeout(cityTimerRef.current); - if (!query || query.length < 2) { setCitySuggestions(null); return; } - cityTimerRef.current = setTimeout(async () => { - try { - const res = await fetch( - `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(query)}&format=json&addressdetails=1&limit=5&accept-language=ru`, - { headers: { "User-Agent": "BlackheartAdmin/1.0" } } - ); - const results = await res.json(); - const cities = results - .map((r: Record) => { - const addr = r.address as Record | undefined; - const city = addr?.city || addr?.town || addr?.village || addr?.state || (r.name as string); - const country = addr?.country || ""; - return country ? `${city}, ${country}` : city; - }) - .filter((v: string, i: number, a: string[]) => a.indexOf(v) === i) - .slice(0, 6); - setCitySuggestions(cities.length > 0 ? { index, items: cities } : null); - setCityErrors((prev) => { const n = { ...prev }; delete n[index]; return n; }); - } catch { - setCitySuggestions(null); - } - }, 500); - }, []); - // Fetch class styles for role autocomplete useEffect(() => { adminFetch("/api/admin/sections/classes") @@ -131,50 +92,75 @@ function TeamMemberEditor() { .then((r) => r.json()) .then((member) => { const username = extractUsername(member.instagram || ""); - setData({ + const loaded = { name: member.name, role: member.role, image: member.image, instagram: username, shortDescription: member.shortDescription || "", description: member.description || "", - experience: member.experience || [], victories: member.victories || [], education: member.education || [], - }); + }; + setData(loaded); + lastSavedRef.current = JSON.stringify(loaded); if (username) setIgStatus("valid"); // existing data is trusted }) .finally(() => setLoading(false)); }, [id, isNew]); - const hasErrors = igStatus === "invalid" || Object.keys(linkErrors).length > 0 || Object.keys(cityErrors).length > 0; - const saveTimerRef = useRef>(undefined); - const initialLoadRef = useRef(true); + const hasErrors = igStatus === "invalid" || Object.keys(linkErrors).length > 0; + const lastSavedRef = useRef(""); + const dataRef = useRef(data); + dataRef.current = data; - // Auto-save with 800ms debounce (existing members only) + // Shared save logic — compares snapshot, skips if unchanged + const saveIfDirty = useCallback(async () => { + if (isNew || loading) return; + const d = dataRef.current; + const snapshot = JSON.stringify(d); + if (snapshot === lastSavedRef.current) return; + if (!d.name || !d.role) return; + lastSavedRef.current = snapshot; + const payload = { ...d, instagram: d.instagram ? `https://instagram.com/${d.instagram}` : "" }; + const res = await adminFetch(`/api/admin/team/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + }); + if (res.ok) showSuccess("Сохранено"); + else showError("Ошибка сохранения"); + }, [isNew, loading, id, showSuccess, showError]); + + // Save when tab loses focus or user navigates away useEffect(() => { - if (isNew || loading || initialLoadRef.current) { - initialLoadRef.current = false; - return; + if (isNew || loading) return; + const onVisibilityChange = () => { if (document.hidden) saveIfDirty(); }; + const onBeforeUnload = () => { + const d = dataRef.current; + const snapshot = JSON.stringify(d); + if (snapshot === lastSavedRef.current || !d.name || !d.role) return; + const payload = { ...d, instagram: d.instagram ? `https://instagram.com/${d.instagram}` : "" }; + navigator.sendBeacon(`/api/admin/team/${id}`, JSON.stringify(payload)); + }; + document.addEventListener("visibilitychange", onVisibilityChange); + window.addEventListener("beforeunload", onBeforeUnload); + return () => { + document.removeEventListener("visibilitychange", onVisibilityChange); + window.removeEventListener("beforeunload", onBeforeUnload); + }; + }, [isNew, loading, id, saveIfDirty]); + + // Save on blur (when user leaves any field) + useEffect(() => { + if (isNew || loading) return; + function handleBlur() { + setTimeout(() => saveIfDirty(), 300); } - if (hasErrors || !data.name || !data.role) return; + document.addEventListener("focusout", handleBlur); + return () => document.removeEventListener("focusout", handleBlur); + }, [isNew, loading, saveIfDirty]); - clearTimeout(saveTimerRef.current); - saveTimerRef.current = setTimeout(async () => { - setSaving(true); - const payload = { ...data, instagram: data.instagram ? `https://instagram.com/${data.instagram}` : "" }; - const res = await adminFetch(`/api/admin/team/${id}`, { - method: "PUT", - headers: { "Content-Type": "application/json" }, - body: JSON.stringify(payload), - }); - if (res.ok) showSuccess("Сохранено"); - else showError("Ошибка сохранения"); - setSaving(false); - }, 800); - - return () => clearTimeout(saveTimerRef.current); - }, [data, isNew, loading, hasErrors, id]); // Manual save for new members async function handleSaveNew() { @@ -207,6 +193,7 @@ function TeamMemberEditor() { const result = await res.json(); if (result.path) { setData((prev) => ({ ...prev, image: result.path })); + setTimeout(saveIfDirty, 100); } } catch { // Upload failed silently @@ -365,31 +352,18 @@ function TeamMemberEditor() {

Биография

- setData({ ...data, experience: items })} - placeholder="Например: 10 лет в танцах" - /> - setData({ ...data, victories: items })} - cityErrors={cityErrors} - citySuggestions={citySuggestions} - onCitySearch={searchCity} - onCitySelect={(i, v) => { - const updated = data.victories.map((item, idx) => idx === i ? { ...item, location: v } : item); - setData({ ...data, victories: updated }); - setCitySuggestions(null); - setCityErrors((prev) => { const n = { ...prev }; delete n[i]; return n; }); - }} + placeholder="Например: 1 место, Чемпионат Беларуси 2024" onLinkValidate={(key, error) => { setLinkErrors((prev) => { if (error) return { ...prev, [key]: error }; const n = { ...prev }; delete n[key]; return n; }); }} + onUploadComplete={() => setTimeout(saveIfDirty, 100)} /> setTimeout(saveIfDirty, 100)} />
diff --git a/src/app/api/admin/team/route.ts b/src/app/api/admin/team/route.ts index 9068da6..c774e06 100644 --- a/src/app/api/admin/team/route.ts +++ b/src/app/api/admin/team/route.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from "next/server"; import { getTeamMembers, createTeamMember } from "@/lib/db"; import { revalidatePath } from "next/cache"; -import type { RichListItem, VictoryItem } from "@/types/content"; +import type { RichListItem } from "@/types/content"; export async function GET() { const members = getTeamMembers(); @@ -17,8 +17,7 @@ export async function POST(request: NextRequest) { image: string; instagram?: string; description?: string; - experience?: string[]; - victories?: VictoryItem[]; + victories?: RichListItem[]; education?: RichListItem[]; }; diff --git a/src/components/sections/team/TeamProfile.tsx b/src/components/sections/team/TeamProfile.tsx index 3ac941a..a4d0953 100644 --- a/src/components/sections/team/TeamProfile.tsx +++ b/src/components/sections/team/TeamProfile.tsx @@ -1,7 +1,7 @@ import { useState, useEffect, useRef, useCallback } from "react"; import Image from "next/image"; -import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, Award, Scale, Clock, MapPin } from "lucide-react"; -import type { TeamMember, RichListItem, VictoryItem, ScheduleLocation } from "@/types/content"; +import { ArrowLeft, Instagram, Trophy, GraduationCap, ExternalLink, X, Clock, MapPin } from "lucide-react"; +import type { TeamMember, RichListItem, ScheduleLocation } from "@/types/content"; import { SignupModal } from "@/components/ui/SignupModal"; interface TeamProfileProps { @@ -24,17 +24,7 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) { window.addEventListener("keydown", handleKeyDown); return () => window.removeEventListener("keydown", handleKeyDown); }, [lightbox, onBack]); - const places = member.victories?.filter(v => !v.type || v.type === 'place') ?? []; - const nominations = member.victories?.filter(v => v.type === 'nomination') ?? []; - const judging = member.victories?.filter(v => v.type === 'judge') ?? []; - const victoryTabs = [ - ...(places.length > 0 ? [{ key: 'place' as const, label: 'Достижения', icon: Trophy, items: places }] : []), - ...(nominations.length > 0 ? [{ key: 'nomination' as const, label: 'Номинации', icon: Award, items: nominations }] : []), - ...(judging.length > 0 ? [{ key: 'judge' as const, label: 'Судейство', icon: Scale, items: judging }] : []), - ]; - const hasVictories = victoryTabs.length > 0; - const [activeTab, setActiveTab] = useState(victoryTabs[0]?.key ?? 'place'); - const hasExperience = member.experience && member.experience.length > 0; + const hasVictories = member.victories && member.victories.length > 0; const hasEducation = member.education && member.education.length > 0; // Extract trainer's groups from schedule using groupId @@ -98,7 +88,7 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) { }); const hasGroups = uniqueGroups.length > 0; - const hasBio = hasVictories || hasExperience || hasEducation || hasGroups; + const hasBio = hasVictories || hasEducation || hasGroups; return (
- {/* Victory tabs */} + {/* Victories */} {hasVictories && (
-
- {victoryTabs.map(tab => ( - + + + Достижения + + + {member.victories!.map((item, i) => ( + ))} -
-
- {victoryTabs.map(tab => ( -
- - {tab.items.map((item, i) => ( - - ))} - -
- ))} -
+
)} @@ -270,23 +239,6 @@ export function TeamProfile({ member, onBack, schedule }: TeamProfileProps) {
)} - {/* Experience */} - {hasExperience && ( -
- - - Опыт - - - {member.experience!.map((item, i) => ( -
-

{item}

-
- ))} -
-
- )} - {/* Description */} {member.description && (

@@ -386,40 +338,6 @@ function ScrollRow({ children }: { children: React.ReactNode }) { ); } -function VictoryCard({ victory }: { victory: VictoryItem }) { - const hasLink = !!victory.link; - - return ( -

-
- {victory.place && ( -
- - {victory.place} - -
- )} -
- {victory.category && ( -

{victory.category}

- )} -

{victory.competition}

- {hasLink && ( - - - Подробнее - - )} -
-
- ); -} - function RichCard({ item, onImageClick }: { item: RichListItem; onImageClick: (src: string) => void }) { const hasImage = !!item.image; const hasLink = !!item.link; diff --git a/src/lib/db.ts b/src/lib/db.ts index ee0bd20..dffc1ea 100644 --- a/src/lib/db.ts +++ b/src/lib/db.ts @@ -1,6 +1,6 @@ import Database from "better-sqlite3"; import path from "path"; -import type { SiteContent, TeamMember, RichListItem, VictoryItem } from "@/types/content"; +import type { SiteContent, TeamMember, RichListItem } from "@/types/content"; import { MS_PER_DAY } from "@/lib/constants"; const DB_PATH = @@ -382,16 +382,18 @@ function parseRichList(val: string | null): RichListItem[] | undefined { } catch { return undefined; } } -function parseVictories(val: string | null): VictoryItem[] | undefined { +function parseVictoriesAsRichList(val: string | null): RichListItem[] | undefined { if (!val) return undefined; try { const arr = JSON.parse(val); if (!Array.isArray(arr) || arr.length === 0) return undefined; - // Handle old string[], old RichListItem[], and new VictoryItem[] formats + // Migrate old VictoryItem[] → RichListItem[] return arr.map((item: string | Record) => { - if (typeof item === "string") return { place: "", category: "", competition: item }; - if ("text" in item && !("competition" in item)) return { place: "", category: "", competition: item.text as string, image: item.image as string | undefined, link: item.link as string | undefined }; - return item as unknown as VictoryItem; + if (typeof item === "string") return { text: item }; + if ("text" in item) return { text: item.text as string, image: item.image as string | undefined, link: item.link as string | undefined }; + // Old VictoryItem format: combine place + category + competition into text + const parts = [item.place, item.category, item.competition].filter(Boolean); + return { text: parts.join(" · "), image: item.image as string | undefined, link: item.link as string | undefined }; }); } catch { return undefined; } } @@ -409,8 +411,7 @@ export function getTeamMembers(): (TeamMember & { id: number })[] { instagram: r.instagram ?? undefined, shortDescription: r.short_description ?? undefined, description: r.description ?? undefined, - experience: parseJsonArray(r.experience), - victories: parseVictories(r.victories), + victories: parseVictoriesAsRichList(r.victories), education: parseRichList(r.education), })); } @@ -431,8 +432,7 @@ export function getTeamMember( instagram: r.instagram ?? undefined, shortDescription: r.short_description ?? undefined, description: r.description ?? undefined, - experience: parseJsonArray(r.experience), - victories: parseVictories(r.victories), + victories: parseVictoriesAsRichList(r.victories), education: parseRichList(r.education), }; } @@ -456,7 +456,7 @@ export function createTeamMember( data.instagram ?? null, data.shortDescription ?? null, data.description ?? null, - data.experience?.length ? JSON.stringify(data.experience) : null, + null, data.victories?.length ? JSON.stringify(data.victories) : null, data.education?.length ? JSON.stringify(data.education) : null, maxOrder.max + 1 @@ -478,7 +478,6 @@ export function updateTeamMember( if (data.instagram !== undefined) { fields.push("instagram = ?"); values.push(data.instagram || null); } if (data.shortDescription !== undefined) { fields.push("short_description = ?"); values.push(data.shortDescription || null); } if (data.description !== undefined) { fields.push("description = ?"); values.push(data.description || null); } - if (data.experience !== undefined) { fields.push("experience = ?"); values.push(data.experience?.length ? JSON.stringify(data.experience) : null); } if (data.victories !== undefined) { fields.push("victories = ?"); values.push(data.victories?.length ? JSON.stringify(data.victories) : null); } if (data.education !== undefined) { fields.push("education = ?"); values.push(data.education?.length ? JSON.stringify(data.education) : null); } diff --git a/src/types/content.ts b/src/types/content.ts index c75affe..0c009c7 100644 --- a/src/types/content.ts +++ b/src/types/content.ts @@ -13,17 +13,6 @@ export interface RichListItem { link?: string; } -export interface VictoryItem { - type?: 'place' | 'nomination' | 'judge'; - place: string; - category: string; - competition: string; - location?: string; - date?: string; - image?: string; - link?: string; -} - export interface TeamMember { name: string; role: string; @@ -31,8 +20,7 @@ export interface TeamMember { instagram?: string; shortDescription?: string; description?: string; - experience?: string[]; - victories?: VictoryItem[]; + victories?: RichListItem[]; education?: RichListItem[]; }