feat: add short description for team carousel cards

- Add shortDescription field to TeamMember type
- DB migration #11: add short_description column to team_members
- Admin editor: separate "Краткое описание (для карточки)" and "Полное описание"
- Carousel shows shortDescription with line-clamp-3, falls back to description
- Full description shown in "Подробнее" profile view

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-20 14:08:50 +03:00
parent 1acda847b7
commit 650f8dc719
4 changed files with 31 additions and 5 deletions

View File

@@ -20,6 +20,7 @@ interface MemberForm {
role: string; role: string;
image: string; image: string;
instagram: string; instagram: string;
shortDescription: string;
description: string; description: string;
experience: string[]; experience: string[];
victories: VictoryItem[]; victories: VictoryItem[];
@@ -36,6 +37,7 @@ export default function TeamMemberEditorPage() {
role: "", role: "",
image: "/images/team/placeholder.webp", image: "/images/team/placeholder.webp",
instagram: "", instagram: "",
shortDescription: "",
description: "", description: "",
experience: [], experience: [],
victories: [], victories: [],
@@ -116,6 +118,7 @@ export default function TeamMemberEditorPage() {
role: member.role, role: member.role,
image: member.image, image: member.image,
instagram: username, instagram: username,
shortDescription: member.shortDescription || "",
description: member.description || "", description: member.description || "",
experience: member.experience || [], experience: member.experience || [],
victories: member.victories || [], victories: member.victories || [],
@@ -302,7 +305,14 @@ export default function TeamMemberEditorPage() {
)} )}
</div> </div>
<TextareaField <TextareaField
label="Описание" label="Краткое описание (для карточки)"
value={data.shortDescription}
onChange={(v) => setData({ ...data, shortDescription: v })}
rows={2}
placeholder="1-2 предложения для карусели"
/>
<TextareaField
label="Полное описание (для страницы тренера)"
value={data.description} value={data.description}
onChange={(v) => setData({ ...data, description: v })} onChange={(v) => setData({ ...data, description: v })}
rows={6} rows={6}

View File

@@ -31,9 +31,9 @@ export function TeamMemberInfo({ members, activeIndex, onSelect, onOpenBio }: Te
</a> </a>
)} )}
{member.description && ( {(member.shortDescription || member.description) && (
<p className="mt-3 text-sm leading-relaxed text-white/55 line-clamp-3"> <p className="mt-3 text-sm leading-relaxed text-white/55 line-clamp-3">
{member.description} {member.shortDescription || member.description}
</p> </p>
)} )}

View File

@@ -219,6 +219,16 @@ const migrations: Migration[] = [
} }
}, },
}, },
{
version: 11,
name: "add_team_short_description",
up: (db) => {
const cols = db.prepare("PRAGMA table_info(team_members)").all() as { name: string }[];
if (!cols.some((c) => c.name === "short_description")) {
db.exec("ALTER TABLE team_members ADD COLUMN short_description TEXT");
}
},
},
]; ];
function runMigrations(db: Database.Database) { function runMigrations(db: Database.Database) {
@@ -278,6 +288,7 @@ interface TeamMemberRow {
role: string; role: string;
image: string; image: string;
instagram: string | null; instagram: string | null;
short_description: string | null;
description: string | null; description: string | null;
experience: string | null; experience: string | null;
victories: string | null; victories: string | null;
@@ -327,6 +338,7 @@ export function getTeamMembers(): (TeamMember & { id: number })[] {
role: r.role, role: r.role,
image: r.image, image: r.image,
instagram: r.instagram ?? undefined, instagram: r.instagram ?? undefined,
shortDescription: r.short_description ?? undefined,
description: r.description ?? undefined, description: r.description ?? undefined,
experience: parseJsonArray(r.experience), experience: parseJsonArray(r.experience),
victories: parseVictories(r.victories), victories: parseVictories(r.victories),
@@ -348,6 +360,7 @@ export function getTeamMember(
role: r.role, role: r.role,
image: r.image, image: r.image,
instagram: r.instagram ?? undefined, instagram: r.instagram ?? undefined,
shortDescription: r.short_description ?? undefined,
description: r.description ?? undefined, description: r.description ?? undefined,
experience: parseJsonArray(r.experience), experience: parseJsonArray(r.experience),
victories: parseVictories(r.victories), victories: parseVictories(r.victories),
@@ -364,14 +377,15 @@ export function createTeamMember(
.get() as { max: number }; .get() as { max: number };
const result = db const result = db
.prepare( .prepare(
`INSERT INTO team_members (name, role, image, instagram, description, experience, victories, education, sort_order) `INSERT INTO team_members (name, role, image, instagram, short_description, description, experience, victories, education, sort_order)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)` VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
) )
.run( .run(
data.name, data.name,
data.role, data.role,
data.image, data.image,
data.instagram ?? null, data.instagram ?? null,
data.shortDescription ?? null,
data.description ?? null, data.description ?? null,
data.experience?.length ? JSON.stringify(data.experience) : null, data.experience?.length ? JSON.stringify(data.experience) : null,
data.victories?.length ? JSON.stringify(data.victories) : null, data.victories?.length ? JSON.stringify(data.victories) : null,
@@ -393,6 +407,7 @@ export function updateTeamMember(
if (data.role !== undefined) { fields.push("role = ?"); values.push(data.role); } if (data.role !== undefined) { fields.push("role = ?"); values.push(data.role); }
if (data.image !== undefined) { fields.push("image = ?"); values.push(data.image); } if (data.image !== undefined) { fields.push("image = ?"); values.push(data.image); }
if (data.instagram !== undefined) { fields.push("instagram = ?"); values.push(data.instagram || null); } 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.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.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.victories !== undefined) { fields.push("victories = ?"); values.push(data.victories?.length ? JSON.stringify(data.victories) : null); }

View File

@@ -29,6 +29,7 @@ export interface TeamMember {
role: string; role: string;
image: string; image: string;
instagram?: string; instagram?: string;
shortDescription?: string;
description?: string; description?: string;
experience?: string[]; experience?: string[];
victories?: VictoryItem[]; victories?: VictoryItem[];