fonctionnalités dépêches
This commit is contained in:
parent
624b21554c
commit
553493052b
227
assets/main.js
227
assets/main.js
|
|
@ -89,46 +89,179 @@ function chargerAlbums() {
|
|||
});
|
||||
}
|
||||
|
||||
/* === FEED DEPECHES (depeches.html) === */
|
||||
async function chargerDepeches() {
|
||||
const feed = document.getElementById('feed');
|
||||
/* ═══════════════════════════════════════════════════
|
||||
DEPECHES — VERSION ENRICHIE
|
||||
═══════════════════════════════════════════════════ */
|
||||
|
||||
function escapeHtml(str) {
|
||||
/* ── helpers texte ───────────────────────────────── */
|
||||
function escapeHtml(str) {
|
||||
return str
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>');
|
||||
}
|
||||
|
||||
function parseMarkdown(text) {
|
||||
// gras
|
||||
text = text.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
||||
// italique
|
||||
text = text.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
||||
// barré
|
||||
text = text.replace(/~~([^~]+)~~/g, '<del>$1</del>');
|
||||
// code inline
|
||||
text = text.replace(/`([^`]+)`/g, '<code class="inline-code">$1</code>');
|
||||
return text;
|
||||
}
|
||||
|
||||
function parseHashtags(text) {
|
||||
return text.replace(/(^|\s)(#[\wÀ-ÿ]+)/g, (m, before, tag) => {
|
||||
const q = encodeURIComponent(tag);
|
||||
return `${before}<a href="https://www.google.com/search?q=${q}" target="_blank" class="hashtag">${tag}</a>`;
|
||||
});
|
||||
}
|
||||
|
||||
function parseMentions(text) {
|
||||
return text.replace(/(^|\s)(@[\w.]+)/g, (m, before, mention) => {
|
||||
return `${before}<span class="mention">${mention}</span>`;
|
||||
});
|
||||
}
|
||||
|
||||
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 `<a href="${escapeHtml(url)}" target="_blank" class="depeche-link"><span class="link-domain">${escapeHtml(domain)}</span><span class="link-url">${escapeHtml(url)}</span></a>`;
|
||||
} catch {
|
||||
return `<a href="${escapeHtml(url)}" target="_blank" class="depeche-link">${escapeHtml(url)}</a>`;
|
||||
}
|
||||
}
|
||||
|
||||
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 `<div class="embed-container video"><iframe src="https://www.youtube.com/embed/${yt[1]}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
|
||||
|
||||
const vm = url.match(/vimeo\.com\/(\d+)/);
|
||||
if (vm) return `<div class="embed-container video"><iframe src="https://player.vimeo.com/video/${vm[1]}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
|
||||
|
||||
const dm = url.match(/(?:dailymotion\.com\/video\/|dai\.ly\/)([a-zA-Z0-9]+)/);
|
||||
if (dm) return `<div class="embed-container video"><iframe src="https://www.dailymotion.com/embed/video/${dm[1]}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
|
||||
|
||||
const twClip = url.match(/(?:clips\.twitch\.tv\/|twitch\.tv\/[^/]+\/clip\/)([a-zA-Z0-9_-]+)/);
|
||||
if (twClip) return `<div class="embed-container video"><iframe src="https://clips.twitch.tv/embed?clip=${twClip[1]}&parent=${location.hostname}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
|
||||
|
||||
const twVid = url.match(/twitch\.tv\/videos\/(\d+)/);
|
||||
if (twVid) return `<div class="embed-container video"><iframe src="https://player.twitch.tv/?video=${twVid[1]}&parent=${location.hostname}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
|
||||
|
||||
const coub = url.match(/coub\.com\/view\/([a-zA-Z0-9]+)/);
|
||||
if (coub) return `<div class="embed-container video"><iframe src="https://coub.com/embed/${coub[1]}" frameborder="0" allowfullscreen loading="lazy"></iframe></div>`;
|
||||
|
||||
/* musique */
|
||||
const sp = url.match(/open\.spotify\.com\/(track|album|playlist|episode|show)\/([a-zA-Z0-9]+)/);
|
||||
if (sp) return `<div class="embed-container music"><iframe src="https://open.spotify.com/embed/${sp[1]}/${sp[2]}" frameborder="0" allowtransparency="true" allow="encrypted-media" loading="lazy"></iframe></div>`;
|
||||
|
||||
const dz = url.match(/deezer\.com\/[a-z]{2}\/(track|album|playlist)\/(\d+)/);
|
||||
if (dz) return `<div class="embed-container music"><iframe src="https://widget.deezer.com/widget/dark/${dz[1]}/${dz[2]}" frameborder="0" allowtransparency="true" allow="encrypted-media; clipboard-write" loading="lazy"></iframe></div>`;
|
||||
|
||||
const am = url.match(/music\.apple\.com\/[a-z]{2}\/(album|song|playlist)\/[^/]+\/(\d+)/);
|
||||
if (am) return `<div class="embed-container music"><iframe src="https://embed.music.apple.com/us/${am[1]}/${am[2]}" frameborder="0" allow="encrypted-media" loading="lazy"></iframe></div>`;
|
||||
|
||||
if (url.includes('mixcloud.com')) {
|
||||
return `<div class="embed-container music"><iframe src="https://www.mixcloud.com/widget/iframe/?feed=${encodeURIComponent(url)}&hide_cover=1&light=1" frameborder="0" loading="lazy"></iframe></div>`;
|
||||
}
|
||||
|
||||
async function parseTexte(texte) {
|
||||
const urlRegex = /(https?:\/\/[^\s]+)/g;
|
||||
const urls = texte.match(urlRegex) || [];
|
||||
let html = texte.replace(urlRegex, '___URL___');
|
||||
const aud = url.match(/audiomack\.com\/([^/]+)\/(song|album|playlist)\/([^/]+)/);
|
||||
if (aud) return `<div class="embed-container music"><iframe src="https://audiomack.com/embed/${aud[1]}/${aud[2]}/${aud[3]}" scrolling="no" frameborder="0" loading="lazy"></iframe></div>`;
|
||||
|
||||
// échappe le HTML et convertit les retours à la ligne
|
||||
html = escapeHtml(html).replace(/\n/g, '<br>');
|
||||
|
||||
const embeds = await Promise.all(urls.map(async url => {
|
||||
/* oembed */
|
||||
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 `<a href="${url}" target="_blank">${url}</a>`; }
|
||||
const d = await r.json();
|
||||
return `<div class="embed-container">${d.html}</div>`;
|
||||
} catch { /* fallthrough */ }
|
||||
}
|
||||
if (url.includes('youtube.com') || url.includes('youtu.be')) {
|
||||
const id = url.match(/(?:v=|youtu\.be\/)([^&\s]+)/)?.[1];
|
||||
if (id) return `<iframe width="500" height="281" src="https://www.youtube.com/embed/${id}" frameborder="0" allowfullscreen style="max-width:100%;display:block;margin-top:0.8rem;"></iframe>`;
|
||||
}
|
||||
return `<a href="${url}" target="_blank">${url}</a>`;
|
||||
}));
|
||||
|
||||
embeds.forEach(e => { html = html.replace('___URL___', e); });
|
||||
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 `<div class="embed-container">${d.html}</div>`;
|
||||
} 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 `<div class="embed-container">${d.html}</div>`;
|
||||
} 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 `<div class="embed-container">${d.html}</div>`;
|
||||
} 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, '<br>');
|
||||
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');
|
||||
|
||||
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
|
||||
? `<img src="${d.image}" class="depeche-image" alt="">`
|
||||
? `<img src="${d.image}" class="depeche-image" alt="" loading="lazy">`
|
||||
: '';
|
||||
|
||||
const permalink = `${location.origin}${location.pathname}#${d.id}`;
|
||||
|
||||
el.innerHTML = `
|
||||
<div class="depeche-date">ewen - ${d.date}</div>
|
||||
<div class="depeche-header">
|
||||
<span class="depeche-date" title="${d.date}">ewen — ${timeAgo(d.date)}</span>
|
||||
<div class="depeche-actions">
|
||||
<button class="depeche-action" data-action="copy" title="copier le lien">#</button>
|
||||
<button class="depeche-action" data-action="share" title="partager">↗</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="depeche-texte">${texteHtml}</div>
|
||||
${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 = '<p class="chargement">impossible de charger les dépêches.</p>';
|
||||
}
|
||||
}
|
||||
|
||||
/* === 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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue