feat: L'Ami Fiduciaire V1.0.0 — full codebase with Story 0.1 complete
Initial commit of the L'Ami Fiduciaire SaaS platform built on Laravel 12, Vue 3, Inertia.js 2, and Tailwind CSS 4. Story 0.1 (rename folders to declarations in database) is implemented and code-reviewed: migration, rollback, and 6 Pest tests all passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
136
resources/js/components/folders/MessageBubble.vue
Normal file
136
resources/js/components/folders/MessageBubble.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { CheckCircle2, Download, FileText } from 'lucide-vue-next';
|
||||
|
||||
type Props = {
|
||||
message: {
|
||||
id: number;
|
||||
type: string;
|
||||
body: string;
|
||||
sent_by_type: string;
|
||||
sender_name: string;
|
||||
created_at: string;
|
||||
attachments?: Array<{
|
||||
id: number;
|
||||
file_name: string;
|
||||
mime_type?: string;
|
||||
size: string;
|
||||
downloadUrl: string;
|
||||
is_downloaded?: boolean;
|
||||
}>;
|
||||
confirmation_status?: 'pending' | 'confirmed' | 'refused' | null;
|
||||
};
|
||||
messageTypeLabels: Record<string, string>;
|
||||
};
|
||||
|
||||
const props = defineProps<Props>();
|
||||
|
||||
const typeColors: Record<string, string> = {
|
||||
invite: 'bg-blue-100 text-blue-800 dark:bg-blue-900/30 dark:text-blue-300',
|
||||
situation: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
file_request: 'bg-orange-100 text-orange-800 dark:bg-orange-900/30 dark:text-orange-300',
|
||||
confirmation: 'bg-violet-100 text-violet-800 dark:bg-violet-900/30 dark:text-violet-300',
|
||||
text: 'bg-slate-100 text-slate-700 dark:bg-slate-800/50 dark:text-slate-300',
|
||||
};
|
||||
|
||||
const confirmationStatusLabels: Record<string, { label: string; class: string }> = {
|
||||
pending: {
|
||||
label: 'En attente',
|
||||
class: 'bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-300',
|
||||
},
|
||||
confirmed: {
|
||||
label: 'Validé',
|
||||
class: 'bg-green-100 text-green-800 dark:bg-green-900/30 dark:text-green-300',
|
||||
},
|
||||
refused: {
|
||||
label: 'Refusé',
|
||||
class: 'bg-red-100 text-red-800 dark:bg-red-900/30 dark:text-red-300',
|
||||
},
|
||||
};
|
||||
|
||||
function isImageMime(mime?: string | null): boolean {
|
||||
return mime?.startsWith('image/') ?? false;
|
||||
}
|
||||
|
||||
function getTypeColor(type: string): string {
|
||||
return typeColors[type] ?? 'bg-muted text-muted-foreground';
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="rounded-2xl px-4 py-3"
|
||||
:class="
|
||||
message.sent_by_type === 'user'
|
||||
? 'ml-auto max-w-[85%] bg-muted'
|
||||
: 'mr-auto max-w-[85%] bg-muted/70'
|
||||
"
|
||||
>
|
||||
<div class="flex flex-wrap items-center justify-between gap-2 text-sm">
|
||||
<span class="font-medium">{{ message.sender_name }}</span>
|
||||
<span class="text-muted-foreground">{{ message.created_at }}</span>
|
||||
</div>
|
||||
<div class="mt-1 flex flex-wrap items-center gap-2">
|
||||
<span
|
||||
class="inline-flex rounded px-2 py-0.5 text-xs font-medium"
|
||||
:class="getTypeColor(message.type)"
|
||||
>
|
||||
{{ messageTypeLabels[message.type] ?? message.type }}
|
||||
</span>
|
||||
<span
|
||||
v-if="message.confirmation_status"
|
||||
class="inline-flex rounded px-2 py-0.5 text-xs font-medium"
|
||||
:class="confirmationStatusLabels[message.confirmation_status]?.class ?? ''"
|
||||
>
|
||||
{{ confirmationStatusLabels[message.confirmation_status]?.label ?? message.confirmation_status }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="mt-2 whitespace-pre-wrap text-sm">{{ message.body }}</p>
|
||||
<div
|
||||
v-if="message.attachments?.length"
|
||||
class="mt-3 flex flex-wrap gap-2"
|
||||
>
|
||||
<div
|
||||
v-for="att in message.attachments"
|
||||
:key="att.id"
|
||||
class="flex items-center gap-2 rounded-lg border border-sidebar-border/70 bg-background/50 p-2"
|
||||
>
|
||||
<div class="flex size-10 shrink-0 items-center justify-center overflow-hidden rounded bg-muted">
|
||||
<img
|
||||
v-if="isImageMime(att.mime_type)"
|
||||
:src="att.downloadUrl"
|
||||
:alt="att.file_name"
|
||||
class="size-10 object-cover"
|
||||
/>
|
||||
<FileText
|
||||
v-else
|
||||
class="size-5 text-muted-foreground"
|
||||
/>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="inline-flex items-center gap-1.5 truncate text-xs font-medium">
|
||||
{{ att.file_name }}
|
||||
<CheckCircle2 v-if="att.is_downloaded" class="size-3.5 text-green-500" />
|
||||
</p>
|
||||
<p class="text-xs text-muted-foreground">{{ att.size }}</p>
|
||||
</div>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
as-child
|
||||
class="shrink-0"
|
||||
>
|
||||
<a
|
||||
:href="att.downloadUrl"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
download
|
||||
@click="att.is_downloaded = true"
|
||||
>
|
||||
<Download class="size-4" />
|
||||
</a>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user