From 553493052bccb42a071bab4605083b0601bf2944 Mon Sep 17 00:00:00 2001 From: ewen Date: Sat, 16 May 2026 21:22:08 +0200 Subject: [PATCH] =?UTF-8?q?fonctionnalit=C3=A9s=20d=C3=A9p=C3=AAches?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- assets/main.js | 253 ++++++++++++++++++++++++++++++++++++++-------- assets/styles.css | 132 +++++++++++++++++++++++- 2 files changed, 343 insertions(+), 42 deletions(-) diff --git a/assets/main.js b/assets/main.js index 045004f..4b13010 100644 --- a/assets/main.js +++ b/assets/main.js @@ -89,46 +89,179 @@ function chargerAlbums() { }); } -/* === FEED DEPECHES (depeches.html) === */ +/* ═══════════════════════════════════════════════════ + DEPECHES — VERSION ENRICHIE + ═══════════════════════════════════════════════════ */ + +/* ── helpers texte ───────────────────────────────── */ +function escapeHtml(str) { + return str + .replace(/&/g, '&') + .replace(//g, '>'); +} + +function parseMarkdown(text) { + // gras + text = text.replace(/\*\*([^*]+)\*\*/g, '$1'); + // italique + text = text.replace(/\*([^*]+)\*/g, '$1'); + // barré + text = text.replace(/~~([^~]+)~~/g, '$1'); + // code inline + text = text.replace(/`([^`]+)`/g, '$1'); + return text; +} + +function parseHashtags(text) { + return text.replace(/(^|\s)(#[\wÀ-ÿ]+)/g, (m, before, tag) => { + const q = encodeURIComponent(tag); + return `${before}${tag}`; + }); +} + +function parseMentions(text) { + return text.replace(/(^|\s)(@[\w.]+)/g, (m, before, mention) => { + return `${before}${mention}`; + }); +} + +function timeAgo(dateStr) { + const m = dateStr.match(/(\d{2})\/(\d{2})\/(\d{4}) (\d{2}):(\d{2})/); + if (!m) return dateStr; + const [, d, mo, y, H, Mi] = m; + const date = new Date(`${y}-${mo}-${d}T${H}:${Mi}:00`); + const sec = Math.floor((Date.now() - date) / 1000); + if (sec < 60) return "à l'instant"; + if (sec < 3600) return `il y a ${Math.floor(sec / 60)} min`; + if (sec < 86400)return `il y a ${Math.floor(sec / 3600)} h`; + if (sec < 604800)return `il y a ${Math.floor(sec / 86400)} j`; + if (sec < 2592000)return `il y a ${Math.floor(sec / 604800)} sem`; + return dateStr; +} + +/* ── split texte / urls ──────────────────────────── */ +function splitTextAndUrls(text) { + const re = /(https?:\/\/[^\s]+)/g; + const parts = []; + let last = 0, m; + while ((m = re.exec(text)) !== null) { + if (m.index > last) parts.push({ type: 'text', value: text.slice(last, m.index) }); + parts.push({ type: 'url', value: m[1] }); + last = m.index + m[0].length; + } + if (last < text.length) parts.push({ type: 'text', value: text.slice(last) }); + return parts; +} + +/* ── embeds ──────────────────────────────────────── */ +function styledLink(url) { + try { + const domain = new URL(url).hostname.replace(/^www\./, ''); + return `${escapeHtml(domain)}${escapeHtml(url)}`; + } catch { + return `${escapeHtml(url)}`; + } +} + +async function resolveEmbed(url) { + /* vidéo 16:9 */ + const yt = url.match(/(?:youtube\.com\/watch\?v=|youtu\.be\/)([a-zA-Z0-9_-]{11})/); + if (yt) return `
`; + + const vm = url.match(/vimeo\.com\/(\d+)/); + if (vm) return `
`; + + const dm = url.match(/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/); + if (dm) return `
`; + + const twClip = url.match(/(?:clips\.twitch\.tv\/|twitch\.tv\/[^/]+\/clip\/)([a-zA-Z0-9_-]+)/); + if (twClip) return `
`; + + const twVid = url.match(/twitch\.tv\/videos\/(\d+)/); + if (twVid) return `
`; + + const coub = url.match(/coub\.com\/view\/([a-zA-Z0-9]+)/); + if (coub) return `
`; + + /* musique */ + const sp = url.match(/open\.spotify\.com\/(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/); + if (sp) return `
`; + + const dz = url.match(/deezer\.com\/[a-z]{2}\/(track|album|playlist)\/(\d+)/); + if (dz) return `
`; + + const am = url.match(/music\.apple\.com\/[a-z]{2}\/(album|song|playlist)\/[^/]+\/(\d+)/); + if (am) return `
`; + + if (url.includes('mixcloud.com')) { + return `
`; + } + + const aud = url.match(/audiomack\.com\/([^/]+)\/(song|album|playlist)\/([^/]+)/); + if (aud) return `
`; + + /* oembed */ + if (url.includes('soundcloud.com')) { + try { + const r = await fetch(`https://soundcloud.com/oembed?format=json&url=${encodeURIComponent(url)}&maxwidth=500`); + const d = await r.json(); + return `
${d.html}
`; + } catch { /* fallthrough */ } + } + + if (url.includes('bandcamp.com')) { + try { + const r = await fetch(`https://bandcamp.com/api/embed/1/oembed?url=${encodeURIComponent(url)}&format=json`); + if (!r.ok) throw new Error('ko'); + const d = await r.json(); + return `
${d.html}
`; + } catch { /* fallthrough */ } + } + + if (url.includes('tiktok.com')) { + try { + const r = await fetch(`https://www.tiktok.com/oembed?url=${encodeURIComponent(url)}`); + const d = await r.json(); + return `
${d.html}
`; + } catch { /* fallthrough */ } + } + + if (url.includes('reddit.com')) { + try { + const r = await fetch(`https://www.reddit.com/oembed?url=${encodeURIComponent(url)}`); + const d = await r.json(); + return `
${d.html}
`; + } catch { /* fallthrough */ } + } + + return null; +} + +/* ── parser principal ────────────────────────────── */ +async function parseTexte(texte) { + const parts = splitTextAndUrls(texte); + const resolved = await Promise.all(parts.map(async part => { + if (part.type === 'text') { + let html = escapeHtml(part.value); + html = parseMarkdown(html); + html = parseHashtags(html); + html = parseMentions(html); + html = html.replace(/\n/g, '
'); + return html; + } + const embed = await resolveEmbed(part.value); + return embed || styledLink(part.value); + })); + return resolved.join(''); +} + +/* ── rendu du feed ───────────────────────────────── */ async function chargerDepeches() { const feed = document.getElementById('feed'); - function escapeHtml(str) { - return str - .replace(/&/g, '&') - .replace(//g, '>'); - } - - async function parseTexte(texte) { - const urlRegex = /(https?:\/\/[^\s]+)/g; - const urls = texte.match(urlRegex) || []; - let html = texte.replace(urlRegex, '___URL___'); - - // échappe le HTML et convertit les retours à la ligne - html = escapeHtml(html).replace(/\n/g, '
'); - - const embeds = await Promise.all(urls.map(async url => { - if (url.includes('soundcloud.com')) { - try { - const r = await fetch(`https://soundcloud.com/oembed?format=json&url=${encodeURIComponent(url)}&maxwidth=500`); - const data = await r.json(); - return data.html; - } catch { return `${url}`; } - } - if (url.includes('youtube.com') || url.includes('youtu.be')) { - const id = url.match(/(?:v=|youtu\.be\/)([^&\s]+)/)?.[1]; - if (id) return ``; - } - return `${url}`; - })); - - embeds.forEach(e => { html = html.replace('___URL___', e); }); - return html; - } - try { - const r = await fetch('/api/depeches'); + const r = await fetch('/depeches'); const depeches = await r.json(); feed.innerHTML = ''; @@ -140,33 +273,69 @@ async function chargerDepeches() { for (const d of depeches) { const el = document.createElement('div'); el.className = 'depeche'; + el.id = d.id; const texteHtml = await parseTexte(d.texte); const img = d.image - ? `` + ? `` : ''; + const permalink = `${location.origin}${location.pathname}#${d.id}`; + el.innerHTML = ` -
ewen - ${d.date}
+
+ ewen — ${timeAgo(d.date)} +
+ + +
+
${texteHtml}
${img} `; + /* actions */ + el.querySelector('[data-action="copy"]').addEventListener('click', function () { + navigator.clipboard.writeText(permalink).then(() => { + this.textContent = '✓'; + setTimeout(() => this.textContent = '#', 1200); + }); + }); + + el.querySelector('[data-action="share"]').addEventListener('click', function () { + if (navigator.share) { + navigator.share({ title: 'dépêche', url: permalink }); + } else { + navigator.clipboard.writeText(permalink).then(() => { + this.textContent = '✓'; + setTimeout(() => this.textContent = '↗', 1200); + }); + } + }); + feed.appendChild(el); } + + /* scroll vers l'ancre si présente */ + if (location.hash) { + const target = document.querySelector(location.hash); + if (target) target.scrollIntoView({ behavior: 'smooth', block: 'start' }); + } } catch { feed.innerHTML = '

impossible de charger les dépêches.

'; } } -/* === LIGHTBOX (images dans les entrées) === */ +/* ═══════════════════════════════════════════════════ + LIGHTBOX + ═══════════════════════════════════════════════════ */ function initLightbox() { const contenu = document.querySelector('.fenetre-contenu'); if (!contenu) return; contenu.addEventListener('click', e => { const img = e.target.closest('img:not(.thanks)'); - if (!img) return; + if (!img || img.closest('.depeche-link') || img.closest('.embed-container')) return; const overlay = document.createElement('div'); overlay.className = 'lightbox'; @@ -203,7 +372,9 @@ function initLightbox() { }); } -/* === INIT === */ +/* ═══════════════════════════════════════════════════ + INIT + ═══════════════════════════════════════════════════ */ document.addEventListener('DOMContentLoaded', () => { animerTitreNav(); diff --git a/assets/styles.css b/assets/styles.css index 6ff1205..ae1572e 100644 --- a/assets/styles.css +++ b/assets/styles.css @@ -256,20 +256,54 @@ body { gap: 0; } +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(12px); } + to { opacity: 1; transform: translateY(0); } +} + .depeche { border-top: 1px solid #ddd; padding: 1rem 0; + scroll-margin-top: 1rem; + animation: fadeInUp 0.45s ease-out both; } .depeche:last-child { border-bottom: 1px solid #ddd; } +.depeche-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.4rem; +} + .depeche-date { font-size: 0.75rem; font-style: italic; color: #999; - margin-bottom: 0.4rem; +} + +.depeche-actions { + display: flex; + gap: 0.3rem; +} + +.depeche-action { + font-family: 'IM Fell English', serif; + font-size: 0.8rem; + background: none; + border: none; + color: #bbb; + cursor: pointer; + padding: 0 0.2rem; + line-height: 1; + transition: color 0.2s; +} + +.depeche-action:hover { + color: #111; } .depeche-texte { @@ -277,6 +311,102 @@ body { color: #111; } +.depeche-texte strong { font-weight: bold; } +.depeche-texte em { font-style: italic; } +.depeche-texte del { text-decoration: line-through; opacity: 0.6; } + +.inline-code { + font-family: 'Courier New', monospace; + font-size: 0.8em; + background: #f4f4f4; + padding: 0.1em 0.35em; + border: 1px solid #e0e0e0; + border-radius: 2px; + color: #333; +} + +.hashtag { + color: hsl(180, 50%, 40%); + text-decoration: none; + border-bottom: 1px dotted hsl(180, 50%, 60%); +} + +.hashtag:hover { + color: hsl(180, 60%, 25%); + border-bottom-style: solid; +} + +.mention { + color: hsl(270, 40%, 50%); + font-weight: bold; +} + +.depeche-link { + display: inline-flex; + align-items: baseline; + gap: 0.35rem; + text-decoration: none; + color: inherit; + border-bottom: 1px dotted #bbb; + max-width: 100%; + overflow-wrap: anywhere; +} + +.depeche-link:hover { + border-bottom-style: solid; + border-bottom-color: #111; +} + +.link-domain { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.03em; + background: #111; + color: #fff; + padding: 0.05em 0.4em; + border-radius: 2px; + white-space: nowrap; + flex-shrink: 0; +} + +.link-url { + font-size: 0.85rem; + font-style: italic; + color: #555; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 300px; + display: inline-block; + vertical-align: bottom; +} + +.embed-container { + margin-top: 0.8rem; + width: 100%; + overflow: hidden; + border: 1px solid #ddd; + background: #fafafa; +} + +.embed-container iframe { + width: 100%; + display: block; + border: none; +} + +.embed-container.video { + aspect-ratio: 16 / 9; +} + +.embed-container.video iframe { + height: 100%; +} + +.embed-container.music iframe { + height: 152px; +} + .depeche-image { margin-top: 0.8rem; max-height: 400px;