merge: PC6 journal n8n + JournalList (round 2)

This commit is contained in:
Jules Neny
2026-05-09 01:15:10 +02:00
6 changed files with 521 additions and 8 deletions

View File

@@ -1,6 +1,8 @@
---
// ColJournal - colonne gauche : CTA Manifeste + Hashtags accordeon + Journal skeleton
// ColJournal - colonne gauche : CTA Manifeste + Hashtags accordeon + Journal (PC6)
// 7 hashtags = 7 plateformes (cf delta 15 du PILOTE-PC.md)
import JournalList from '../vue/JournalList.vue';
const hashtags = [
{ tag: '#manifeste', plateforme: 'Blog trans-former.fr', canal: 'ecriture longue' },
{ tag: '#building-public', plateforme: 'LinkedIn', canal: 'journal pro' },
@@ -55,10 +57,7 @@ const hashtags = [
<span class="text-xs text-neutral-400 font-normal">chrono</span>
</h2>
<div id="journal-list" class="space-y-3 text-sm">
<!-- PC6 remplit ce slot via <JournalList client:visible /> -->
<p class="text-neutral-400 italic text-xs">
Chargement du journal...
</p>
<JournalList client:visible />
</div>
</section>
</div>

View File

@@ -1,9 +1,168 @@
<script setup lang="ts">
// Placeholder journal list — PC6 lit public/data/journal.json
import { ref, computed, onMounted, onUnmounted } from 'vue'
interface JournalItem {
id: string
platform: 'substack' | 'gitea' | 'github' | 'instagram' | 'castopod' | 'blog' | 'linkedin'
hashtag: string
date: string
titre: string
extrait: string
url: string
thumbnail: string | null
}
interface JournalPayload {
generatedAt: string
items: JournalItem[]
counts?: Record<string, number>
}
const items = ref<JournalItem[]>([])
const filters = ref<Record<string, boolean>>({})
const loading = ref(true)
const errored = ref(false)
const empty = ref(false)
const JOURNAL_URL =
(import.meta as unknown as { env: Record<string, string | undefined> }).env
.PUBLIC_JOURNAL_URL || 'https://data.trans-former.fr/journal.json'
const STORAGE_KEY = 'tf-hashtag-filters'
const loadFilters = () => {
try {
filters.value = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}')
} catch {
filters.value = {}
}
}
const onFilterChange = (e: Event) => {
const ce = e as CustomEvent
if (ce.detail && typeof ce.detail === 'object') {
filters.value = { ...ce.detail }
}
}
onMounted(async () => {
loadFilters()
window.addEventListener('hashtag-filter-change', onFilterChange as EventListener)
try {
const res = await fetch(JOURNAL_URL, { cache: 'no-store' })
if (!res.ok) {
errored.value = true
return
}
const data = (await res.json()) as JournalPayload
items.value = Array.isArray(data.items) ? data.items : []
if (items.value.length === 0) empty.value = true
} catch (e) {
errored.value = true
console.error('JournalList fetch failed', e)
} finally {
loading.value = false
}
})
onUnmounted(() => {
window.removeEventListener('hashtag-filter-change', onFilterChange as EventListener)
})
const visibleItems = computed(() => {
// Tous les filtres faux = aucun affiche ; tous true ou aucune cle = tout afficher
const keys = Object.keys(filters.value)
if (keys.length === 0) return items.value
const activeKeys = keys.filter((k) => filters.value[k])
// Si l'utilisateur a explicitement décoché tous les hashtags, on n'affiche rien
if (activeKeys.length === 0 && keys.some((k) => filters.value[k] === false)) return []
if (activeKeys.length === 0) return items.value
return items.value.filter((it) => activeKeys.includes(it.hashtag))
})
const formatDate = (iso: string) => {
try {
const d = new Date(iso)
if (isNaN(d.getTime())) return ''
return `${String(d.getDate()).padStart(2, '0')}/${String(d.getMonth() + 1).padStart(2, '0')}`
} catch {
return ''
}
}
const platformLabel = (p: string) => {
switch (p) {
case 'gitea':
case 'github':
return 'code'
case 'substack':
return 'substack'
case 'instagram':
return 'insta'
case 'castopod':
return 'pod'
case 'blog':
return 'blog'
case 'linkedin':
return 'linkedin'
default:
return p
}
}
</script>
<template>
<div class="h-full w-full flex items-center justify-center text-sm text-neutral-400">
Journal list placeholder (PC6)
<div class="space-y-3">
<p v-if="loading" class="text-neutral-400 italic text-xs">
Chargement du journal...
</p>
<p v-else-if="errored" class="text-neutral-400 italic text-xs">
Journal en cours d'agregation. Reviens dans quelques heures.
</p>
<p v-else-if="empty" class="text-neutral-400 italic text-xs">
Aucun item agrege. Le cron nocturne tourne a 3h.
</p>
<p
v-else-if="!visibleItems.length"
class="text-neutral-400 italic text-xs"
>
Aucun item ; ajuste les filtres.
</p>
<article
v-for="item in visibleItems"
:key="item.id"
class="border-b border-neutral-100 pb-3 last:border-b-0"
>
<a
:href="item.url"
target="_blank"
rel="noopener noreferrer"
class="block group"
>
<div class="flex items-baseline gap-2 text-[11px] text-neutral-500 mb-1">
<time>{{ formatDate(item.date) }}</time>
<span class="font-mono text-neutral-700">{{ item.hashtag }}</span>
<span class="text-neutral-400">{{ platformLabel(item.platform) }}</span>
</div>
<h4
class="text-sm text-neutral-900 group-hover:text-neutral-600 leading-snug"
>
{{ item.titre }}
</h4>
<p
v-if="item.extrait"
class="text-xs text-neutral-500 mt-1 line-clamp-2"
>
{{ item.extrait }}
</p>
<img
v-if="item.thumbnail"
:src="item.thumbnail"
:alt="item.titre"
loading="lazy"
class="mt-2 w-full max-h-32 object-cover rounded"
/>
</a>
</article>
</div>
</template>