import { createHash } from "crypto"; import { parse } from "date-fns"; import { enUS } from "date-fns/locale"; import type { StrengthWorkoutInput } from "@/lib/models/strength"; const DATE_FORMAT = "EEEE, d MMMM yyyy 'at' HH:mm"; const HEADER_DATE_RE = /^[A-Za-z]+,\s+\d{1,2}\s+[A-Za-z]+\s+\d{4}\s+at\s+\d{1,2}:\d{2}$/; const SET_RE = /^Set\s+\d+:\s*(?:([\d.,]+)\s*kg\s*[×x]\s*)?(\d+)$/i; const SOURCE_URL_RE = /^https:\/\/link\.strong\.app\/\S+$/; type ParsedBlock = string[]; function splitBlocks(text: string): ParsedBlock[] { return text .replace(/\r\n/g, "\n") .split(/\n\s*\n+/) .map((block) => block .split("\n") .map((line) => line.trim()) .filter((line) => line.length > 0) ) .filter((block) => block.length > 0); } function parseWeight(raw: string | undefined): number | undefined { if (!raw) return undefined; const value = Number.parseFloat(raw.replace(",", ".")); return Number.isFinite(value) ? value : undefined; } function makeSourceKey(workout: Omit): string { if (workout.sourceUrl) return workout.sourceUrl; return createHash("sha256") .update(`${workout.date.toISOString()}|${workout.name}`) .digest("hex"); } export function parseStrongShareText(text: string): StrengthWorkoutInput[] { const blocks = splitBlocks(text); const workouts: Omit[] = []; for (const block of blocks) { const isHeader = block.length === 2 && HEADER_DATE_RE.test(block[1]); if (isHeader) { const date = parse(block[1], DATE_FORMAT, new Date(), { locale: enUS }); workouts.push({ date, name: block[0], exercises: [] }); continue; } const current = workouts[workouts.length - 1]; if (!current) { throw new Error(`Nieoczekiwany blok przed nagłówkiem treningu: "${block[0]}"`); } const lines = [...block]; const lastLine = lines[lines.length - 1]; if (SOURCE_URL_RE.test(lastLine)) { current.sourceUrl = lastLine; lines.pop(); } if (lines.length === 0) continue; if (/^Notes:/i.test(lines[0])) { const note = lines.join(" ").replace(/^Notes:\s*/i, ""); const lastExercise = current.exercises[current.exercises.length - 1]; if (lastExercise) { lastExercise.notes = note; } else { current.notes = note; } continue; } const [exerciseName, ...setLines] = lines; const sets = setLines .map((line, index) => { const match = SET_RE.exec(line); if (!match) return null; return { order: index + 1, weightKg: parseWeight(match[1]), reps: Number.parseInt(match[2], 10), }; }) .filter((set): set is NonNullable => set !== null); current.exercises.push({ name: exerciseName, sets }); } return workouts.map((workout) => ({ ...workout, sourceKey: makeSourceKey(workout), })); }