diff --git a/frontend/src/components/ChatHeader.vue b/frontend/src/components/ChatHeader.vue index 99f2fa1..c80d0ff 100644 --- a/frontend/src/components/ChatHeader.vue +++ b/frontend/src/components/ChatHeader.vue @@ -9,6 +9,7 @@
+ {{ ip }} @@ -23,10 +24,13 @@ diff --git a/frontend/src/components/MessageList.vue b/frontend/src/components/MessageList.vue index 8e89da6..16c95da 100644 --- a/frontend/src/components/MessageList.vue +++ b/frontend/src/components/MessageList.vue @@ -1,15 +1,17 @@ @@ -69,14 +77,15 @@ watch( font-size: 13px; } -/* Positionné en absolu sur la droite du wrapper */ .casino-overlay { position: absolute; right: 30px; top: 20px; pointer-events: none; } -.casino-overlay :deep(.casino-cta) { - pointer-events: all; -} +.casino-overlay :deep(.casino-cta) { pointer-events: all; } + +/* Transition d'entrée des nouveaux messages */ +.msg-enter-active { transition: opacity 0.2s ease, transform 0.2s ease; } +.msg-enter-from { opacity: 0; transform: translateY(6px); } diff --git a/frontend/src/components/ThemePicker.vue b/frontend/src/components/ThemePicker.vue new file mode 100644 index 0000000..559b934 --- /dev/null +++ b/frontend/src/components/ThemePicker.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/frontend/src/composables/useMessageItem.ts b/frontend/src/composables/useMessageItem.ts new file mode 100644 index 0000000..32606a3 --- /dev/null +++ b/frontend/src/composables/useMessageItem.ts @@ -0,0 +1,62 @@ +import { type GeoInfo } from '@/composables/useMessages'; +import { getIpColorWithPerks, getIpGlowWithPerks, getIpGlow } from '@/composables/ipColor'; +import { usePerks } from '@/composables/usePerks'; +import { useCustomStyles } from '@/composables/useCustomStyles'; +import { useMyPerks } from '@/composables/useMessages'; + +export function useMessageItem() { + const { perksFor } = usePerks(); + const { myPerks } = useMyPerks(); + const { prefs } = useCustomStyles(); + + function perksOf(m: { authorIp: string; authorPerks?: any }) { + return m.authorPerks ?? perksFor(m.authorIp); + } + + function ipStyle(m: { authorIp: string; authorPerks?: any }) { + const ip = m.authorIp; + const override = prefs.ipColors[ip]; + if (override && override !== 'auto') { + return { color: override, textShadow: getIpGlow(override) }; + } + const p = perksOf(m); + return { color: getIpColorWithPerks(ip, p), textShadow: getIpGlowWithPerks(ip, p) }; + } + + function petsLeft(m: { authorIp: string; authorPerks?: any }) { + const ip = m.authorIp; + if (ip in prefs.ipPets) return prefs.ipPets[ip]; + return (perksOf(m)?.pets ?? []) + .filter((x: any) => x.position === 'left' || x.position === 'both') + .map((x: any) => x.char).join(''); + } + + function petsRight(m: { authorIp: string; authorPerks?: any }) { + const ip = m.authorIp; + if (ip in prefs.ipPets) return ''; + return (perksOf(m)?.pets ?? []) + .filter((x: any) => x.position === 'right' || x.position === 'both') + .map((x: any) => x.char).join(''); + } + + function fmt(date: string) { + return new Date(date).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }); + } + + function geoLabel(geo?: GeoInfo | null): string { + if (!geo) return ''; + if (!geo.countryCode) return 'Local'; + const place = geo.city || geo.country; + if (geo.lat != null && geo.lon != null) { + return `${place} · ${geo.lat.toFixed(4)}, ${geo.lon.toFixed(4)}`; + } + return place; + } + + function geoLink(geo?: GeoInfo | null): string { + if (!geo || geo.lat == null || geo.lon == null) return 'https://maps.google.com'; + return `https://www.google.com/maps/search/?api=1&query=${geo.lat},${geo.lon}`; + } + + return { perksOf, ipStyle, petsLeft, petsRight, fmt, geoLabel, geoLink, myPerks, prefs }; +} diff --git a/frontend/src/composables/useTheme.ts b/frontend/src/composables/useTheme.ts new file mode 100644 index 0000000..7732f5d --- /dev/null +++ b/frontend/src/composables/useTheme.ts @@ -0,0 +1,39 @@ +import { ref, provide, inject, type InjectionKey, type Ref } from 'vue'; + +export type Theme = 'default' | 'bubble' | 'compact'; + +export interface ThemeContext { + theme: Ref; + setTheme: (t: Theme) => void; +} + +export const THEME_KEY: InjectionKey = Symbol('xip-theme'); + +const THEMES: Record = { + default: { label: 'Classique', emoji: '📋' }, + bubble: { label: 'Bulles', emoji: '💬' }, + compact: { label: 'Compact', emoji: '📐' }, +}; + +export function provideTheme() { + const saved = (localStorage.getItem('xip-theme') ?? 'default') as Theme; + const theme = ref(THEMES[saved] ? saved : 'default'); + + function setTheme(t: Theme) { + theme.value = t; + localStorage.setItem('xip-theme', t); + } + + const ctx: ThemeContext = { theme, setTheme }; + provide(THEME_KEY, ctx); + return ctx; +} + +export function useTheme(): ThemeContext { + return inject(THEME_KEY, { + theme: ref('default'), + setTheme: () => {}, + }); +} + +export { THEMES }; diff --git a/frontend/src/views/HomePage.vue b/frontend/src/views/HomePage.vue index 5bfe7a7..6c5b29d 100644 --- a/frontend/src/views/HomePage.vue +++ b/frontend/src/views/HomePage.vue @@ -57,7 +57,7 @@ type="button" >🔊 -
+