proposition frontend

This commit is contained in:
arussac
2026-05-29 12:06:40 +02:00
parent 12afb71a67
commit 97f6fdaeae
14 changed files with 793 additions and 193 deletions

View File

@@ -1,174 +1,96 @@
<template>
<ion-page>
<ion-header :translucent="true">
<ion-toolbar>
<ion-title>XIP 📡</ion-title>
</ion-toolbar>
</ion-header>
<div class="xip-root">
<!-- Bande pub gauche -->
<AdBand />
<ion-content :fullscreen="true">
<ion-header collapse="condense">
<ion-toolbar>
<ion-title size="large">XIP</ion-title>
</ion-toolbar>
</ion-header>
<!-- Zone chat centrale -->
<div class="xip-center">
<ChatHeader :connected-count="connectedCount" />
<MessageList :messages="messages" />
<!-- Composer -->
<div class="composer">
<ion-textarea
v-model="newContent"
<!-- Barre de saisie -->
<div class="input-bar">
<input
v-model="draft"
class="input-field"
type="text"
placeholder="Entrez un message..."
:maxlength="267"
:counter="true"
:auto-grow="true"
placeholder="Exprime-toi... (267 caractères max)"
fill="outline"
@keydown.enter.exact.prevent="submit"
/>
<ion-button
expand="block"
class="send-btn"
:disabled="!newContent.trim() || sending"
@click="postMessage"
>
{{ sending ? "Envoi..." : "Envoyer" }}
</ion-button>
<SendButton :disabled="!draft.trim() || sending" @send="submit" />
</div>
</div>
<!-- Feed -->
<ion-list v-if="messages.length > 0">
<template v-for="msg in messages" :key="msg.id">
<ion-item lines="full">
<ion-label class="ion-text-wrap">
<p class="meta">{{ msg.authorIp }} · {{ formatDate(msg.createdAt) }}</p>
<h2>{{ msg.content }}</h2>
<p v-if="msg.replies.length > 0" class="replies-count">
{{ msg.replies.length }} réponse(s)
</p>
</ion-label>
</ion-item>
<!-- Replies -->
<ion-item
v-for="reply in msg.replies"
:key="reply.id"
lines="none"
class="reply-item"
>
<ion-label class="ion-text-wrap">
<p class="meta"> {{ reply.authorIp }} · {{ formatDate(reply.createdAt) }}</p>
<p>{{ reply.content }}</p>
</ion-label>
</ion-item>
</template>
</ion-list>
<div v-else-if="!loading" class="empty-state">
<p>Aucun message pour l'instant.</p>
</div>
</ion-content>
</ion-page>
<!-- Bouton hamburger droit -->
<MenuToggle @toggle="menuOpen = !menuOpen" />
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from "vue";
import {
IonPage,
IonHeader,
IonToolbar,
IonTitle,
IonContent,
IonList,
IonItem,
IonLabel,
IonTextarea,
IonButton,
} from "@ionic/vue";
import { ref } from 'vue';
import AdBand from '@/components/AdBand.vue';
import ChatHeader from '@/components/ChatHeader.vue';
import MessageList from '@/components/MessageList.vue';
import SendButton from '@/components/SendButton.vue';
import MenuToggle from '@/components/MenuToggle.vue';
import { useMessages } from '@/composables/useMessages';
interface Message {
id: string;
content: string;
authorIp: string;
createdAt: string;
replies: Omit<Message, "replies">[];
const { messages, sending, postMessage } = useMessages();
const draft = ref('');
const menuOpen = ref(false);
// Compte simulé (connexion WebSocket à brancher plus tard)
const connectedCount = ref(312);
async function submit(): Promise<void> {
const ok = await postMessage(draft.value);
if (ok) draft.value = '';
}
const API_URL = import.meta.env.VITE_API_URL ?? "http://localhost:3000";
const messages = ref<Message[]>([]);
const newContent = ref("");
const loading = ref(false);
const sending = ref(false);
async function fetchMessages(): Promise<void> {
loading.value = true;
try {
const res = await fetch(`${API_URL}/api/messages`);
if (!res.ok) throw new Error("Failed to fetch messages");
messages.value = (await res.json()) as Message[];
} finally {
loading.value = false;
}
}
async function postMessage(): Promise<void> {
if (!newContent.value.trim()) return;
sending.value = true;
try {
const res = await fetch(`${API_URL}/api/messages`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ content: newContent.value.trim() }),
});
if (!res.ok) throw new Error("Failed to post message");
newContent.value = "";
await fetchMessages();
} finally {
sending.value = false;
}
}
function formatDate(date: string): string {
return new Date(date).toLocaleString("fr-FR", {
day: "2-digit",
month: "2-digit",
hour: "2-digit",
minute: "2-digit",
});
}
onMounted(fetchMessages);
</script>
<style scoped>
.composer {
padding: 16px;
border-bottom: 1px solid var(--ion-color-light-shade);
}
.send-btn {
margin-top: 8px;
}
.meta {
font-size: 0.75rem;
color: var(--ion-color-medium);
margin-bottom: 4px;
}
.replies-count {
font-size: 0.75rem;
color: var(--ion-color-primary);
margin-top: 4px;
}
.reply-item {
--padding-start: 32px;
background-color: var(--ion-color-light);
}
.empty-state {
.xip-root {
display: flex;
justify-content: center;
padding: 48px 16px;
color: var(--ion-color-medium);
width: 100vw;
height: 100dvh;
background: #080808;
overflow: hidden;
}
.xip-center {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
background: #090910;
overflow: hidden;
}
/* ── Barre de saisie ── */
.input-bar {
height: 70px;
flex-shrink: 0;
background: #0e0e16;
border-top: 1px solid #1a1a26;
display: flex;
align-items: center;
padding: 0 20px;
gap: 12px;
}
.input-field {
flex: 1;
background: #141420;
border: 1px solid #222234;
border-radius: 23px;
padding: 12px 22px;
color: #aaaacc;
font-family: Arial, sans-serif;
font-size: 13px;
outline: none;
transition: border-color 0.15s;
}
.input-field::placeholder { color: #2a2a44; }
.input-field:focus { border-color: #333355; }
</style>