diff --git a/database/migration_006_profile_links.sql b/database/migration_006_profile_links.sql new file mode 100644 index 0000000..219837d --- /dev/null +++ b/database/migration_006_profile_links.sql @@ -0,0 +1,9 @@ +CREATE TABLE IF NOT EXISTS profile_links ( + id SERIAL PRIMARY KEY, + user_email TEXT NOT NULL, + url TEXT NOT NULL, + title TEXT NOT NULL DEFAULT '', + description TEXT NOT NULL DEFAULT '', + position INT NOT NULL DEFAULT 0, + created_at TIMESTAMP DEFAULT now() +); diff --git a/public/.htaccess b/public/.htaccess index de04719..9575fa6 100644 --- a/public/.htaccess +++ b/public/.htaccess @@ -44,8 +44,12 @@ RewriteRule ^flux/?$ /index.php?action=flux [L,QSA] RewriteRule ^feed/add/?$ /index.php?action=add_feed [L,QSA] RewriteRule ^feed/delete/?$ /index.php?action=delete_feed [L,QSA] -# Profil public auteur +# Profil public auteur + page liens RewriteRule ^profil/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=author&slug=$1 [L,QSA] +RewriteRule ^liens/([a-z0-9][a-z0-9-]*)/?$ /index.php?action=liens&slug=$1 [L,QSA] +RewriteRule ^link/add/?$ /index.php?action=add_link [L,QSA] +RewriteRule ^link/delete/?$ /index.php?action=delete_link [L,QSA] +RewriteRule ^link/reorder/?$ /index.php?action=reorder_links [L,QSA] # Pages statiques RewriteRule ^about/?$ /index.php?action=about [L,QSA] diff --git a/public/assets/css/style.css b/public/assets/css/style.css index fe5479d..e1eb707 100644 --- a/public/assets/css/style.css +++ b/public/assets/css/style.css @@ -1286,6 +1286,101 @@ footer.mt-5 { margin-top: 0 !important; } font-size: .9375rem; } +/* ─── Page "Mes liens" ───────────────────── */ + +.liens-page { + max-width: 480px; + margin: 0 auto; + padding: 2.5rem 1rem 4rem; + display: flex; + flex-direction: column; + align-items: center; + gap: 2rem; +} + +.liens-header { + display: flex; + flex-direction: column; + align-items: center; + gap: .75rem; + text-align: center; +} + +.liens-avatar { + width: 5rem; + height: 5rem; + border-radius: 50%; + background: var(--vl-accent); + color: #fff; + font-size: 2rem; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; +} + +.liens-name { + font-size: 1.25rem; + font-weight: 700; + margin: 0; +} + +.liens-bio { + font-size: .9rem; + color: var(--vl-muted); + line-height: 1.6; + margin: 0; +} + +.liens-list { + width: 100%; + display: flex; + flex-direction: column; + gap: .75rem; +} + +.liens-item { + display: flex; + flex-direction: column; + align-items: center; + gap: .2rem; + width: 100%; + padding: .875rem 1.25rem; + border-radius: var(--vl-radius); + border: 1.5px solid var(--vl-border); + background: var(--vl-surface); + text-align: center; + text-decoration: none; + color: var(--vl-text); + font-weight: 600; + transition: border-color .15s, background .15s, transform .1s; + box-shadow: var(--vl-shadow-sm); +} + +.liens-item:hover { + border-color: var(--vl-accent); + background: var(--vl-accent-soft); + color: var(--vl-accent); + transform: translateY(-1px); + box-shadow: var(--vl-shadow-md); +} + +.liens-item-title { font-size: 1rem; } + +.liens-item-desc { + font-size: .8rem; + font-weight: 400; + color: var(--vl-muted); +} +.liens-item:hover .liens-item-desc { color: var(--vl-accent); opacity: .8; } + +.liens-footer { + font-size: .8rem; + color: var(--vl-muted); +} +.liens-footer a { color: inherit; } +.liens-footer a:hover { color: var(--vl-accent); } + /* ─── Agrégateur de flux ─────────────────── */ .flux-list { diff --git a/public/index.php b/public/index.php index 439d92b..9120399 100644 --- a/public/index.php +++ b/public/index.php @@ -22,7 +22,7 @@ $action = $_GET['action'] ?? 'list'; $uuid = $_GET['uuid'] ?? ''; $slug = $_GET['slug'] ?? ''; -$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed']; +$_noindexActions = ['create', 'edit', 'admin', 'categories', 'diff', 'add_files', 'import_image', 'import_image_step2', 'sources', 'profile', 'delete_file', 'delete_external_link', 'rename_category', 'delete_category', 'toggle_private_category', 'admin_save_site', 'not_found', 'add_feed', 'delete_feed', 'add_link', 'delete_link', 'reorder_links']; $metaRobots = in_array($action, $_noindexActions, true) ? 'noindex, nofollow' : null; unset($_noindexActions); @@ -881,6 +881,92 @@ switch ($action) { include BASE_PATH . '/templates/author_profile.php'; break; + case 'liens': + $liensSlug = trim($_GET['slug'] ?? ''); + $liensRow = profileBySlug($liensSlug); + if (!$liensRow) { + http_response_code(404); + $content = '

Page introuvable.

'; + $title = 'Page introuvable'; + include BASE_PATH . '/templates/layout.php'; + break; + } + $_lName = $liensRow['display_name'] ?? ''; + $_lBio = $liensRow['bio'] ?? ''; + $_lSlug = $liensRow['profile_slug'] ?? ''; + $_lInitials = mb_strtoupper(mb_substr($_lName, 0, 1, 'UTF-8'), 'UTF-8'); + $profileLinks = []; + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare( + 'SELECT id, url, title, description FROM profile_links + WHERE user_email = :e ORDER BY position, id' + ); + $st->execute([':e' => $liensRow['email']]); + $profileLinks = $st->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable) { + } + } + include BASE_PATH . '/templates/liens.php'; + break; + + case 'add_link': + requireAuth(); + $linkUrl = filter_var(trim($_POST['link_url'] ?? ''), FILTER_VALIDATE_URL) ?: ''; + $linkTitle = trim($_POST['link_title'] ?? ''); + $linkDesc = trim($_POST['link_desc'] ?? ''); + if ($linkUrl !== '') { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare( + 'INSERT INTO profile_links (user_email, url, title, description, position) + VALUES (:e, :u, :t, :d, + COALESCE((SELECT MAX(position)+1 FROM profile_links WHERE user_email = :e), 0))' + ); + $st->execute([':e' => currentUserEmail(), ':u' => $linkUrl, ':t' => $linkTitle, ':d' => $linkDesc]); + } catch (\Throwable) { + } + } + } + header('Location: /profile#links'); + exit; + + case 'delete_link': + requireAuth(); + $linkId = (int)($_POST['link_id'] ?? 0); + if ($linkId > 0) { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('DELETE FROM profile_links WHERE id = :id AND user_email = :e'); + $st->execute([':id' => $linkId, ':e' => currentUserEmail()]); + } catch (\Throwable) { + } + } + } + header('Location: /profile#links'); + exit; + + case 'reorder_links': + requireAuth(); + $order = $_POST['order'] ?? []; + if (is_array($order)) { + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('UPDATE profile_links SET position = :p WHERE id = :id AND user_email = :e'); + foreach (array_values($order) as $pos => $id) { + $st->execute([':p' => $pos, ':id' => (int)$id, ':e' => currentUserEmail()]); + } + } catch (\Throwable) { + } + } + } + header('Location: /profile#links'); + exit; + case 'flux': require_once BASE_PATH . '/src/FeedFetcher.php'; $fetcher = new FeedFetcher(BASE_PATH . '/data/_cache/feeds'); @@ -1878,6 +1964,17 @@ switch ($action) { if ($profileCurrentUrl === '' && $profileCurrentSlug !== '') { $profileCurrentUrl = rtrim(APP_URL, '/') . '/profil/' . rawurlencode($profileCurrentSlug); } + // Liens de la page "Mes liens" + $profileLinks = []; + $pdo = dbPdo(); + if ($pdo) { + try { + $st = $pdo->prepare('SELECT id, url, title, description FROM profile_links WHERE user_email = :e ORDER BY position, id'); + $st->execute([':e' => currentUserEmail()]); + $profileLinks = $st->fetchAll(PDO::FETCH_ASSOC); + } catch (\Throwable) { + } + } // Feeds RSS de l'utilisateur $profileFeeds = []; $pdo = dbPdo(); diff --git a/templates/liens.php b/templates/liens.php new file mode 100644 index 0000000..1911b47 --- /dev/null +++ b/templates/liens.php @@ -0,0 +1,39 @@ + + +
+ +
+
+

+ +

+ +
+ +
+ + + + + + + + +
+ + + + + +
+ + + + +

Flux RSS