import { ContentStatus, ProficiencyLevel } from "@prisma/client"; export type AcademicStageId = "base" | "consolidacion" | "especializacion"; export type AvailabilityState = "published" | "upcoming" | "draft"; export type AcademicStageConfig = { id: AcademicStageId; tabLabel: string; sectionTitle: string; sectionDescription: string; levelLabel: string; }; export const ACADEMIC_STAGE_ORDER: AcademicStageId[] = ["base", "consolidacion", "especializacion"]; export const ACADEMIC_STAGE_CONFIG: Record = { base: { id: "base", tabLabel: "Base", sectionTitle: "Programas Base", sectionDescription: "Trayectos introductorios para fortalecer fundamentos del inglés jurídico y lenguaje técnico aplicado.", levelLabel: "Base", }, consolidacion: { id: "consolidacion", tabLabel: "Consolidación", sectionTitle: "Programas de Consolidación", sectionDescription: "Programas para consolidar precisión terminológica, comprensión de textos jurídicos y comunicación profesional.", levelLabel: "Consolidación", }, especializacion: { id: "especializacion", tabLabel: "Especialización", sectionTitle: "Programas de Especialización", sectionDescription: "Itinerarios avanzados orientados a práctica profesional internacional, redacción especializada y análisis complejo.", levelLabel: "Especialización", }, }; const STAGE_KEYWORDS: Record = { base: ["base", "fundamentos", "fundamentals", "intro", "introduccion", "beginner", "inicial"], consolidacion: ["consolidacion", "consolidación", "consolidation", "intermedio", "intermediate"], especializacion: ["especializacion", "especialización", "specialization", "avanzado", "advanced", "expert", "experto"], }; export const PUBLIC_UPCOMING_TAGS = ["upcoming", "coming-soon", "proximamente", "próximamente", "public-upcoming"]; const IMAGE_KEYS = ["coverImageUrl", "coverImage", "thumbnailUrl", "thumbnail", "imageUrl", "image", "cover"]; const SHORT_KEYS = ["shortEs", "shortEn", "short", "summary", "excerpt", "resumen"]; const LONG_KEYS = ["longEs", "longEn", "long", "description", "descripcion"]; type UnknownRecord = Record; function isRecord(value: unknown): value is UnknownRecord { return typeof value === "object" && value !== null && !Array.isArray(value); } function asString(value: unknown): string { return typeof value === "string" ? value.trim() : ""; } function fromRecord(record: UnknownRecord, keys: string[]): string { for (const key of keys) { const value = asString(record[key]); if (value) return value; } return ""; } export function getLocalizedText(value: unknown): string { if (!value) return ""; if (typeof value === "string") return value.trim(); if (isRecord(value)) { const preferred = fromRecord(value, ["es", "en"]); if (preferred) return preferred; } return ""; } export function getCourseDescriptions(description: unknown): { short: string; long: string } { if (!description) { return { short: "Programa en actualización académica.", long: "Programa en actualización académica.", }; } const localized = getLocalizedText(description); if (!isRecord(description)) { const normalized = localized || "Programa en actualización académica."; return { short: truncateText(normalized, 170), long: normalized }; } const shortCandidate = fromRecord(description, SHORT_KEYS); const longCandidate = fromRecord(description, LONG_KEYS) || localized; const longText = longCandidate || "Programa en actualización académica."; const shortText = shortCandidate || truncateText(longText, 170); return { short: shortText, long: longText, }; } function toTags(input: string[] | null | undefined): string[] { if (!Array.isArray(input)) return []; return input.filter((value): value is string => typeof value === "string"); } function getImageFromTags(tags: string[] | null | undefined): string { for (const rawTag of toTags(tags)) { const tag = rawTag.trim(); if (!tag) continue; const lower = tag.toLowerCase(); if (lower.startsWith("cover:") || lower.startsWith("cover-url:") || lower.startsWith("thumbnail:")) { const value = tag.slice(tag.indexOf(":") + 1).trim(); if (value.startsWith("http://") || value.startsWith("https://")) return value; } } return ""; } function getYouTubeThumbnail(url: string): string { if (!url) return ""; const normalized = url.trim(); if (!normalized) return ""; const patterns = [/v=([a-zA-Z0-9_-]{6,})/, /youtu\.be\/([a-zA-Z0-9_-]{6,})/, /embed\/([a-zA-Z0-9_-]{6,})/]; for (const pattern of patterns) { const match = normalized.match(pattern); const videoId = match?.[1]; if (videoId) return `https://i.ytimg.com/vi/${videoId}/hqdefault.jpg`; } return ""; } function getImageFromDescription(description: unknown): string { if (!isRecord(description)) return ""; const image = fromRecord(description, IMAGE_KEYS); if (!image) return ""; return image.startsWith("http://") || image.startsWith("https://") || image.startsWith("/") ? image : ""; } function getImageFromVideoUrl(url: string): string { if (!url) return ""; const normalized = url.trim(); if (!normalized) return ""; if (/\.(png|jpe?g|webp|gif)$/i.test(normalized)) return normalized; return ""; } export function resolveThumbnailUrl(args: { description: unknown; tags: string[] | null | undefined; youtubeUrl?: string | null; videoUrl?: string | null; }): string { const fromDescription = getImageFromDescription(args.description); if (fromDescription) return fromDescription; const fromTags = getImageFromTags(args.tags); if (fromTags) return fromTags; const fromYouTube = getYouTubeThumbnail(args.youtubeUrl ?? ""); if (fromYouTube) return fromYouTube; const fromVideo = getImageFromVideoUrl(args.videoUrl ?? ""); if (fromVideo) return fromVideo; return ""; } function normalizeTag(tag: string): string { return tag .trim() .toLowerCase() .normalize("NFD") .replace(/[\u0300-\u036f]/g, ""); } export function normalizeTags(tags: string[] | null | undefined): string[] { return toTags(tags).map(normalizeTag).filter(Boolean); } function includesKeyword(tags: string[], stage: AcademicStageId): boolean { return tags.some((tag) => STAGE_KEYWORDS[stage].some((keyword) => tag.includes(keyword))); } export function resolveAcademicStage(level: ProficiencyLevel, tags: string[] | null | undefined): AcademicStageConfig { const normalizedTags = normalizeTags(tags); if (includesKeyword(normalizedTags, "base")) return ACADEMIC_STAGE_CONFIG.base; if (includesKeyword(normalizedTags, "consolidacion")) return ACADEMIC_STAGE_CONFIG.consolidacion; if (includesKeyword(normalizedTags, "especializacion")) return ACADEMIC_STAGE_CONFIG.especializacion; if (level === "BEGINNER") return ACADEMIC_STAGE_CONFIG.base; if (level === "INTERMEDIATE") return ACADEMIC_STAGE_CONFIG.consolidacion; return ACADEMIC_STAGE_CONFIG.especializacion; } export function resolveAvailability( status: ContentStatus, tags: string[] | null | undefined, ): { state: AvailabilityState; label: string } { if (status === "PUBLISHED") return { state: "published", label: "Disponible" }; const normalizedTags = normalizeTags(tags); const hasUpcomingTag = normalizedTags.some((tag) => PUBLIC_UPCOMING_TAGS.includes(tag)); if (hasUpcomingTag) return { state: "upcoming", label: "Próximamente" }; return { state: "draft", label: "Borrador" }; } export function isUpcomingTagPresent(tags: string[] | null | undefined): boolean { const normalizedTags = normalizeTags(tags); return normalizedTags.some((tag) => PUBLIC_UPCOMING_TAGS.includes(tag)); } export function getProficiencyLabel(level: ProficiencyLevel): string { if (level === "BEGINNER") return "Inicial"; if (level === "INTERMEDIATE") return "Intermedio"; if (level === "ADVANCED") return "Avanzado"; return "Experto"; } export function formatDuration(totalSeconds: number): string { if (!Number.isFinite(totalSeconds) || totalSeconds <= 0) return "Duración por definir"; const totalMinutes = Math.max(1, Math.ceil(totalSeconds / 60)); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; if (hours === 0) return `${totalMinutes} min`; if (minutes === 0) return `${hours} h`; return `${hours} h ${minutes} min`; } export function formatMinutes(minutes: number): string { if (!Number.isFinite(minutes) || minutes <= 0) return "Sin duración"; if (minutes < 60) return `${minutes} min`; const hours = Math.floor(minutes / 60); const remainingMinutes = minutes % 60; if (remainingMinutes === 0) return `${hours} h`; return `${hours} h ${remainingMinutes} min`; } export function truncateText(value: string, maxChars: number): string { if (value.length <= maxChars) return value; return `${value.slice(0, Math.max(0, maxChars - 1)).trimEnd()}…`; }