Some checks failed
Deploy XIP / deploy (push) Failing after 37s
Fonctionnel
- Backend messages : GET /api/messages/:id (detail) + recherche (q),
pagination par curseur (before/limit) avec enveloppe { items, nextCursor,
hasMore } ; le flux temps reel garde l'ancien format quand aucun parametre.
- Explorer (/explorer) : catalogue distant, recherche debouncee + annulable
(AbortController), filtre, defilement infini, etat garde (keep-alive).
- Details par id : /message/:id et /shop/p/:id (consomment route.params).
- Favoris (/favoris) : liste perso persistee en localStorage, notation
(note/rating/statut) via modale, refletee partout (bouton favori).
- Mes stats (/mes-stats) : agregats derives des favoris (note moyenne, top
pays/auteurs, statuts), auto-mis a jour, route gardee si liste vide.
- Routeur : pages secondaires en lazy-load + repli, garde beforeEnter.
Technique
- Slots : PrefSection (slot defaut + slot nomme) enveloppe les 5 sections
"Mes Persos" ; Modal (Teleport + slots).
- v-model custom : SearchBox (defineModel + debounce).
- Directive custom : v-click-outside.
- Tests Vitest : 25 tests (etat, fonctions, composants), ~86% du code metier.
- Retrait d'Ionic (inutilise). Script typecheck backend ; tsconfig @types/bun.
- Correctif type : garde stockLimit nullable dans l'achat (catalog.ts).
- README complet (URL, stack, run, tests, secrets, deploiement, mention IA).
104 lines
3.1 KiB
Vue
104 lines
3.1 KiB
Vue
<template>
|
|
<div class="xip-app" :data-theme="theme">
|
|
<!-- Bandeau de stats temps réel, toujours visible -->
|
|
<StatsTicker :stats="stats" :connected="connected" />
|
|
|
|
<div class="xip-root">
|
|
<!-- Zone chat centrale -->
|
|
<div class="xip-center" :style="chatBgStyle">
|
|
<ChatHeader :connected-count="stats?.connectedTabs ?? 0" />
|
|
<MessageList :messages="messages" :hide-ads="!!myPerks.noads" :my-ip="myIp" @reply="startReply" />
|
|
|
|
<!-- Bannière de réponse -->
|
|
<div v-if="replyingTo" class="reply-banner">
|
|
<span class="reply-banner-txt">
|
|
En réponse à <span class="reply-ip">{{ replyingTo.authorIp }}</span>
|
|
</span>
|
|
<button class="reply-cancel" @click="cancelReply" type="button">✕</button>
|
|
</div>
|
|
|
|
<!-- Composer (texte / riche / pièces jointes / envoi) -->
|
|
<ChatComposer :replying-to="replyingTo" @clear-reply="cancelReply" />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, computed } from 'vue';
|
|
import ChatHeader from '@/components/ChatHeader.vue';
|
|
import MessageList from '@/components/MessageList.vue';
|
|
import ChatComposer from '@/components/ChatComposer.vue';
|
|
import StatsTicker from '@/components/StatsTicker.vue';
|
|
import { useMessages, useMyPerks } from '@/composables/useMessages';
|
|
import { provideTheme } from '@/composables/useTheme';
|
|
import { useCustomStyles } from '@/composables/useCustomStyles';
|
|
|
|
const { theme } = provideTheme();
|
|
|
|
const { messages, stats, connected, myIp } = useMessages();
|
|
const { myPerks } = useMyPerks();
|
|
const { prefs: stylePrefs } = useCustomStyles();
|
|
|
|
const chatBgStyle = computed(() => {
|
|
if (!stylePrefs.chatBgUrl) return {};
|
|
return {
|
|
backgroundImage: `url(${stylePrefs.chatBgUrl})`,
|
|
backgroundSize: 'cover',
|
|
backgroundPosition: 'center',
|
|
backgroundRepeat: 'no-repeat',
|
|
};
|
|
});
|
|
|
|
// ── Réponse (la bannière vit ici ; le composer envoie avec parentId) ──
|
|
const replyingTo = ref<{ id: string; authorIp: string } | null>(null);
|
|
function startReply(payload: { id: string; authorIp: string }): void {
|
|
replyingTo.value = payload;
|
|
}
|
|
function cancelReply(): void {
|
|
replyingTo.value = null;
|
|
}
|
|
</script>
|
|
|
|
<style scoped>
|
|
.xip-app {
|
|
display: flex;
|
|
flex-direction: column;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: var(--xip-app-bg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
.xip-root {
|
|
flex: 1;
|
|
min-height: 0;
|
|
display: flex;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.xip-center {
|
|
flex: 1;
|
|
min-width: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
background: var(--xip-bg);
|
|
overflow: hidden;
|
|
}
|
|
|
|
/* ── Bannière de réponse ── */
|
|
.reply-banner {
|
|
flex-shrink: 0;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
background: #0c1622;
|
|
border-top: 1px solid #16324a;
|
|
padding: 6px 20px;
|
|
}
|
|
.reply-banner-txt { font-family: Arial, sans-serif; font-size: 11px; color: #6688aa; }
|
|
.reply-ip { font-family: 'Courier New', monospace; color: #00ccff; font-weight: bold; }
|
|
.reply-cancel { background: none; border: none; color: #557; cursor: pointer; font-size: 13px; }
|
|
.reply-cancel:hover { color: #aac; }
|
|
</style>
|