- InstaFeed.vue (Vue island) : consomme feeds.behold.so/{feedId}, skeleton loading, grid 2x3 thumbnails, fallback profil si placeholder ou erreur
- ColInsta.astro enrichi : 2 sections @aep.politique + @julesneny, hydratation client:visible (lazy)
- .env.example committe (PUBLIC_BEHOLD_AEP/JULESNENY vides) ; .env.local deja gitignore
- docs/BEHOLD-SETUP.md : procedure inscription + recup feed IDs + alternatives + note CSP PC8
Action Jules requise (async) : inscription Behold + connexion 2 comptes Insta + remplir .env.local.
108 lines
2.8 KiB
Vue
108 lines
2.8 KiB
Vue
<script setup lang="ts">
|
|
import { ref, onMounted } from 'vue';
|
|
|
|
interface BeholdPost {
|
|
id: string;
|
|
permalink: string;
|
|
mediaUrl: string;
|
|
thumbnailUrl?: string;
|
|
caption?: string;
|
|
mediaType: 'IMAGE' | 'VIDEO' | 'CAROUSEL_ALBUM';
|
|
timestamp: string;
|
|
}
|
|
|
|
const props = defineProps<{
|
|
feedId: string;
|
|
account: string;
|
|
accountUrl: string;
|
|
fallbackBio?: string;
|
|
}>();
|
|
|
|
const posts = ref<BeholdPost[]>([]);
|
|
const loading = ref(true);
|
|
const error = ref<string | null>(null);
|
|
|
|
const isPlaceholder = (id: string) => !id || id.startsWith('PLACEHOLDER_');
|
|
|
|
onMounted(async () => {
|
|
if (isPlaceholder(props.feedId)) {
|
|
loading.value = false;
|
|
error.value = 'no-feed-id';
|
|
return;
|
|
}
|
|
try {
|
|
const controller = new AbortController();
|
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
const res = await fetch(`https://feeds.behold.so/${props.feedId}`, {
|
|
signal: controller.signal,
|
|
});
|
|
clearTimeout(timeoutId);
|
|
if (!res.ok) throw new Error(`Behold returned ${res.status}`);
|
|
const data = await res.json();
|
|
const items: BeholdPost[] = Array.isArray(data) ? data : (data.posts ?? []);
|
|
posts.value = items.slice(0, 6);
|
|
} catch (e) {
|
|
error.value = (e as Error).message || 'fetch-error';
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<template>
|
|
<section class="border-b border-neutral-200 last:border-b-0">
|
|
<header class="px-4 py-3 flex items-center justify-between">
|
|
<a
|
|
:href="accountUrl"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="font-medium text-sm hover:underline"
|
|
>
|
|
{{ account }}
|
|
</a>
|
|
<span v-if="loading" class="text-xs text-neutral-400">...</span>
|
|
</header>
|
|
|
|
<div v-if="loading" class="grid grid-cols-2 gap-1 p-1">
|
|
<div
|
|
v-for="i in 4"
|
|
:key="i"
|
|
class="aspect-square bg-neutral-100 animate-pulse"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
v-else-if="posts.length"
|
|
class="grid grid-cols-2 gap-1 p-1"
|
|
>
|
|
<a
|
|
v-for="post in posts"
|
|
:key="post.id"
|
|
:href="post.permalink"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="block aspect-square overflow-hidden group"
|
|
>
|
|
<img
|
|
:src="post.thumbnailUrl || post.mediaUrl"
|
|
:alt="post.caption?.slice(0, 80) || account"
|
|
loading="lazy"
|
|
class="w-full h-full object-cover group-hover:scale-105 transition-transform"
|
|
/>
|
|
</a>
|
|
</div>
|
|
|
|
<div v-else class="p-4 text-sm text-neutral-600">
|
|
<p v-if="fallbackBio" class="mb-3">{{ fallbackBio }}</p>
|
|
<a
|
|
:href="accountUrl"
|
|
target="_blank"
|
|
rel="noopener"
|
|
class="inline-block px-3 py-2 bg-neutral-900 text-white rounded-lg text-sm"
|
|
>
|
|
Voir sur Instagram →
|
|
</a>
|
|
</div>
|
|
</section>
|
|
</template>
|