diff --git a/TODO.md b/TODO.md index c7c0808..e7e6721 100644 --- a/TODO.md +++ b/TODO.md @@ -6,3 +6,8 @@ - [x] Add `console.log` tracing on JS submit interception - [x] Add `error_log` entry-point logging to all 16 PHP action files - [x] Add double-submit guard (`_xamxamActiveSubmit`) +- [x] Fix spurious HTMX console warnings from checkbox-list default hx-include +- [x] Fix duplicate language entries (accented vs non-accented variants) + - [x] Deduplicate getPredefinedLanguages() query + - [x] Accent-tolerant getOrCreateLanguage() to prevent future duplicates + - [x] Delete orphan non-accented language rows from DB diff --git a/app/src/Database.php b/app/src/Database.php index 34b6ce8..edffb52 100644 --- a/app/src/Database.php +++ b/app/src/Database.php @@ -755,16 +755,40 @@ class Database /** * Return only the predefined / hardcoded languages used as checkboxes * in the form. All other languages go into the "Autre langue" input. + * + * De-duplicates accent variants (e.g. 'francais' + 'français') by + * returning the accented row. No REGEXP in SQLite, so we use a + * priority-window approach. */ public function getPredefinedLanguages(): array { - $stmt = $this->pdo->query( - "SELECT id, UPPER(SUBSTR(name,1,1)) || SUBSTR(name,2) as name, created_at + $langs = $this->pdo->query( + "SELECT id, name, created_at, + CASE + WHEN LOWER(name) IN ('français', 'francais') THEN 1 + WHEN LOWER(name) = 'anglais' THEN 2 + WHEN LOWER(name) IN ('néerlandais', 'neerlandais') THEN 3 + END AS grp, + CASE WHEN name GLOB '*[À-ÿ]*' THEN 0 ELSE 1 END AS is_ascii FROM languages WHERE LOWER(name) IN ('français', 'anglais', 'néerlandais', 'francais', 'neerlandais') - ORDER BY name" - ); - return $stmt->fetchAll(); + ORDER BY grp, is_ascii" + )->fetchAll(); + + // De-duplicate: keep first row per grp (accented variant wins due to ORDER BY) + $seen = []; + $dedup = []; + foreach ($langs as $l) { + $g = $l['grp']; + if (isset($seen[$g])) continue; + $seen[$g] = true; + $dedup[] = [ + 'id' => $l['id'], + 'name' => $l['name'], + 'created_at' => $l['created_at'], + ]; + } + return $dedup; } // ======================================================================== @@ -1681,15 +1705,41 @@ class Database * Return the ID of an existing language by name, inserting it if absent. * Name is stored lowercase and displayed with first letter capitalized. */ + /** + * Find or create a language by name (case-insensitive, accent-tolerant). + * + * Normalises the name to lowercase. Before creating a new row, checks + * whether the name differs from an existing row only by accents (e.g. + * 'francais' → matches existing 'français') and returns the existing ID. + */ public function getOrCreateLanguage(string $name): int { $name = strtolower(trim($name)); + if ($name === '') { + throw new \InvalidArgumentException('Language name must not be empty.'); + } + + // 1. Exact lowercase match $stmt = $this->pdo->prepare('SELECT id FROM languages WHERE LOWER(name) = LOWER(?) LIMIT 1'); $stmt->execute([$name]); $id = $stmt->fetchColumn(); if ($id !== false) { return (int)$id; } + + // 2. Accent-tolerant fallback: strip accents and re-compare. + // iconv 'ASCII//TRANSLIT' turns é→e, ç→c, etc. + $asciiName = @iconv('UTF-8', 'ASCII//TRANSLIT', $name); + if ($asciiName !== false && $asciiName !== $name) { + $all = $this->pdo->query('SELECT id, name FROM languages')->fetchAll(); + foreach ($all as $row) { + $rowAscii = @iconv('UTF-8', 'ASCII//TRANSLIT', strtolower($row['name'])); + if ($rowAscii !== false && $rowAscii === $asciiName) { + return (int)$row['id']; + } + } + } + $this->pdo->prepare('INSERT INTO languages (name) VALUES (?)')->execute([$name]); return (int)$this->pdo->lastInsertId(); } diff --git a/app/storage/xamxam.sqlite b/app/storage/xamxam.sqlite new file mode 100644 index 0000000..e69de29 diff --git a/app/templates/admin/acces.php b/app/templates/admin/acces.php index a2a9ff5..af26e38 100644 --- a/app/templates/admin/acces.php +++ b/app/templates/admin/acces.php @@ -272,6 +272,19 @@ +%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +\\\\\\\ to: muzswpkw dbe1ac28 "fix: repair form submission with queued files + add comprehensive debug logging" (rebased revision) ++ $linkName = $link['name'] ?? ''; +++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: muzswpkw dbe1ac28 "fix: repair form submission with queued files + add comprehensive debug logging" (rebased revision) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) +- $linkName = $link['name'] ?? ''; +- $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; +%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% diff from: somsyvxz 14a3cd10 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebase destination) +\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\\ to: ymrzpvln f17c59a8 "fix: spurious HTMX console warnings from checkbox-list default hx-include" (rebased revision) + $linkName = $link['name'] ?? ''; + $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; + $linkLockedYear = $link['locked_year'] ?? null; ++%%%%%%% diff from: somsyvxz 249f7943 "Bulk bar anti-shift, tags icons, AP no-wrap, credits reorder" (rebased revision) ++\\\\\\\ to: ymrzpvln 01fd03c9 "fix: spurious HTMX console warnings from checkbox-list default hx-include" (rebased revision) +++ $linkName = $link['name'] ?? ''; ++ $linkExpiresVal = $link['expires_at'] ? date('Y-m-d\TH:i', strtotime($link['expires_at'])) : ''; ?>