diff --git a/database/migration_003_profile_slug.sql b/database/migration_003_profile_slug.sql new file mode 100644 index 0000000..e8f9911 --- /dev/null +++ b/database/migration_003_profile_slug.sql @@ -0,0 +1,2 @@ +ALTER TABLE user_profiles ADD COLUMN IF NOT EXISTS profile_slug TEXT NOT NULL DEFAULT ''; +CREATE UNIQUE INDEX IF NOT EXISTS user_profiles_profile_slug_idx ON user_profiles (profile_slug) WHERE profile_slug <> ''; diff --git a/public/.htaccess b/public/.htaccess index 5fe8181..2a62dab 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -41,6 +41,9 @@ RewriteRule ^categories/?$ /index.php?action=categories [L,QSA] RewriteRule ^profile/?$ /index.php?action=profile [L,QSA] RewriteRule ^search/?$ /index.php?action=search [L,QSA] +# Profil public auteur +RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=author&slug=$1 [L,QSA] + # Pages statiques RewriteRule ^about/?$ /index.php?action=about [L,QSA] RewriteRule ^legal/?$ /index.php?action=legal [L,QSA] diff --git a/public/assets/css/style.css b/public/assets/css/style.css index 60b302e..c4a13b1 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1242,3 +1242,38 @@ footer.mt-5 { margin-top: 0 !important; } } .tag-cloud-reset:hover { color: var(--vl-accent); } + +/* ─── Profil public auteur ───────────────── */ + +.author-profile-hero { + display: flex; + align-items: center; + gap: 1.5rem; +} + +.author-avatar { + flex-shrink: 0; + width: 4rem; + height: 4rem; + border-radius: 50%; + background: var(--vl-accent); + color: #fff; + font-size: 1.75rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.author-profile-name { + font-size: 1.5rem; + font-weight: 700; + margin: 0 0 .25rem; +} + +.author-profile-link { + font-size: .875rem; + color: var(--vl-muted); +} + +.author-profile-link:hover { color: var(--vl-accent); } diff --git a/public/index.php b/public/index.php index fb052bb..d2d88bb 100644 --- a/public/index.php +++ b/public/index.php @@ -851,6 +851,36 @@ switch ($action) { header('Location: /categories'); exit; + case 'author': + $authorSlug = trim($_GET['slug'] ?? ''); + $authorRow = profileBySlug($authorSlug); + if (!$authorRow) { + http_response_code(404); + $content = '

Profil introuvable.

'; + $title = 'Profil introuvable'; + include BASE_PATH . '/templates/layout.php'; + break; + } + $privateCats = $articles->getPrivateCategories(); + $authorArticles = array_values(array_filter( + $articles->getAll(publishedOnly: true), + static function (array $a) use ($authorRow, $privateCats): bool { + if (($a['author'] ?? '') !== $authorRow['email']) { + return false; + } + $cat = trim($a['category'] ?? ''); + if ($cat !== '' && in_array($cat, $privateCats, true) && !isLoggedIn()) { + return false; + } + if (strtotime((string)($a['published_at'] ?? '')) > time() && !hasCapability('view_previews')) { + return false; + } + return true; + } + )); + include BASE_PATH . '/templates/author_profile.php'; + break; + case 'about': include BASE_PATH . '/templates/about.php'; break; @@ -1785,12 +1815,13 @@ switch ($action) { $pdo = dbPdo(); if ($pdo) { try { + $newSlug = slugify($newName); $st = $pdo->prepare( - 'INSERT INTO user_profiles (email, display_name, profile_url, updated_at) - VALUES (:e, :n, :u, now()) - ON CONFLICT (email) DO UPDATE SET display_name = :n, profile_url = :u, updated_at = now()' + 'INSERT INTO user_profiles (email, display_name, profile_url, profile_slug, updated_at) + VALUES (:e, :n, :u, :s, now()) + ON CONFLICT (email) DO UPDATE SET display_name = :n, profile_url = :u, profile_slug = :s, updated_at = now()' ); - $st->execute([':e' => currentUserEmail(), ':n' => $newName, ':u' => $newUrl]); + $st->execute([':e' => currentUserEmail(), ':n' => $newName, ':u' => $newUrl, ':s' => $newSlug]); $_SESSION['user_display_name'] = $newName; $profileSuccess = true; } catch (\Throwable $ex) { diff --git a/src/auth.php b/src/auth.php index 97a39ed..ddb3113 100644 --- a/src/auth.php +++ b/src/auth.php @@ -49,7 +49,7 @@ function authorProfile(string $email): array static $cache = []; $key = strtolower(trim($email)); if ($key === '') { - return ['name' => '', 'url' => '']; + return ['name' => '', 'url' => '', 'slug' => '']; } if (array_key_exists($key, $cache)) { return $cache[$key]; @@ -57,23 +57,64 @@ function authorProfile(string $email): array $pdo = dbPdo(); if ($pdo) { try { - $st = $pdo->prepare('SELECT display_name, profile_url FROM user_profiles WHERE email = :e'); + $st = $pdo->prepare('SELECT display_name, profile_url, profile_slug FROM user_profiles WHERE email = :e'); $st->execute([':e' => $key]); $row = $st->fetch(PDO::FETCH_ASSOC); if ($row) { $cache[$key] = [ 'name' => ($row['display_name'] !== '') ? $row['display_name'] : explode('@', $key)[0], 'url' => $row['profile_url'] ?? '', + 'slug' => $row['profile_slug'] ?? '', ]; return $cache[$key]; } } catch (\Throwable) { } } - $cache[$key] = ['name' => explode('@', $key)[0], 'url' => '']; + $cache[$key] = ['name' => explode('@', $key)[0], 'url' => '', 'slug' => '']; return $cache[$key]; } +function authorSlug(string $email): string +{ + return authorProfile($email)['slug']; +} + +function profileBySlug(string $slug): ?array +{ + if ($slug === '') { + return null; + } + $pdo = dbPdo(); + if (!$pdo) { + return null; + } + try { + $st = $pdo->prepare('SELECT email, display_name, profile_url, profile_slug FROM user_profiles WHERE profile_slug = :s'); + $st->execute([':s' => $slug]); + $row = $st->fetch(PDO::FETCH_ASSOC); + return $row ?: null; + } catch (\Throwable) { + return null; + } +} + +function slugify(string $text): string +{ + $map = [ + 'à' => 'a', 'â' => 'a', 'ä' => 'a', + 'é' => 'e', 'è' => 'e', 'ê' => 'e', 'ë' => 'e', + 'î' => 'i', 'ï' => 'i', + 'ô' => 'o', 'ö' => 'o', + 'ù' => 'u', 'û' => 'u', 'ü' => 'u', + 'ç' => 'c', 'æ' => 'ae', 'œ' => 'oe', + ]; + $s = mb_strtolower($text, 'UTF-8'); + $s = strtr($s, $map); + $s = preg_replace('/[^a-z0-9]+/', '-', $s); + return trim((string)$s, '-'); +} + function dbPdo(): ?PDO { static $pdo = null; diff --git a/templates/author_profile.php b/templates/author_profile.php new file mode 100644 index 0000000..956ceac --- /dev/null +++ b/templates/author_profile.php @@ -0,0 +1,70 @@ + + +
+
+
+

+ + + ↗ + + +
+
+ + +

Aucun article publié.

+ +
+ text($post['content']); + $preview = mb_strimwidth(strip_tags($html), 0, 120, '…'); + $category = trim((string)($post['category'] ?? '')); + $gradient = coverGradient($category !== '' ? $category : $post['uuid'], $allCats ?? []); + $postUrl = '/post/' . rawurlencode($post['slug']); + $coverFile = $post['cover'] ?? ''; + $coverStyle = $coverFile !== '' + ? 'background-image: url(\'/file?uuid=' . rawurlencode($post['uuid']) . '&name=' . rawurlencode($coverFile) . '\')' + : 'background: ' . $gradient; + ?> +
+
+ + + +
+
+

+ +

+

+ +
+ +
+ +
+ + +

- + + + + + ·