feat: Implement chat pagination, participant single page with mutations, and fix user link substitutions
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue';
|
||||
import { useForm, usePage } from '@inertiajs/vue3';
|
||||
import { useForm, usePage, router } from '@inertiajs/vue3';
|
||||
import { useEcho, echoIsConfigured, configureEcho } from '@laravel/echo-vue';
|
||||
import { route } from 'ziggy-js';
|
||||
import { Paperclip, Info } from '@lucide/vue';
|
||||
import { ref, computed } from 'vue';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
chat: {
|
||||
id: number;
|
||||
messages: Array<{
|
||||
messages?: Array<{
|
||||
id: number;
|
||||
user: { id: number; name: string } | null;
|
||||
content: string;
|
||||
@@ -33,12 +33,36 @@ const props = withDefaults(
|
||||
} | null;
|
||||
}>;
|
||||
dynamicId: number;
|
||||
initialMessages: {
|
||||
data: Array<any>;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
}>(),
|
||||
{
|
||||
participants: () => [],
|
||||
}
|
||||
);
|
||||
|
||||
const messages = ref(props.initialMessages.data.reverse());
|
||||
const nextPageUrl = ref(props.initialMessages.next_page_url);
|
||||
|
||||
function loadMoreMessages() {
|
||||
if (!nextPageUrl.value) {
|
||||
return;
|
||||
}
|
||||
|
||||
router.get(nextPageUrl.value, {}, {
|
||||
preserveState: true,
|
||||
preserveScroll: true,
|
||||
only: ['messages'],
|
||||
onSuccess: (page) => {
|
||||
const newMessages = page.props.messages as { data: Array<any>; next_page_url: string | null };
|
||||
messages.value = [...newMessages.data.reverse(), ...messages.value];
|
||||
nextPageUrl.value = newMessages.next_page_url;
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (!echoIsConfigured()) {
|
||||
configureEcho({
|
||||
broadcaster: 'reverb',
|
||||
@@ -63,11 +87,12 @@ const form = useForm({
|
||||
});
|
||||
|
||||
useEcho(`chats.${props.chat.id}`, 'MessageSent', (e: any) => {
|
||||
props.chat.messages.push(e.message);
|
||||
messages.value.push(e.message);
|
||||
});
|
||||
|
||||
const participantsById = computed(() => {
|
||||
return props.participants.reduce(
|
||||
const list = props.participants || [];
|
||||
return list.reduce(
|
||||
(acc, p) => {
|
||||
acc[p.id] = p;
|
||||
return acc;
|
||||
@@ -130,7 +155,7 @@ function parseMessageContent(message: {
|
||||
/[-\/\\^$*+?.()|[\]{}]/g,
|
||||
'\\$&',
|
||||
);
|
||||
|
||||
|
||||
const nameRegex = new RegExp(`"${escapedName}"`, 'g');
|
||||
content = content.replace(
|
||||
nameRegex,
|
||||
@@ -197,17 +222,20 @@ function closeLightbox() {
|
||||
<div class="c-chat">
|
||||
<h4 class="c-chat__title">Chat</h4>
|
||||
<div class="c-chat__list">
|
||||
<div v-if="nextPageUrl" class="c-chat__load-more">
|
||||
<button @click="loadMoreMessages" class="c-chat__load-more-btn">
|
||||
Load More
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
v-for="message in chat.messages"
|
||||
v-for="message in messages"
|
||||
:key="message.id"
|
||||
:class="[
|
||||
'c-chat__message',
|
||||
{
|
||||
'c-chat__message--system': message.user === null,
|
||||
'c-chat__message--own': isOwnMessage(message.user?.id),
|
||||
'c-chat__message--other': message.user !== null && !isOwnMessage(
|
||||
message.user?.id,
|
||||
),
|
||||
'c-chat__message--other': message.user !== null && !isOwnMessage(message.user?.id)
|
||||
},
|
||||
]"
|
||||
>
|
||||
@@ -276,7 +304,7 @@ function closeLightbox() {
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-if="chat.messages.length === 0" class="c-chat__empty">
|
||||
<div v-if="messages.length === 0" class="c-chat__empty">
|
||||
No messages yet.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<script setup lang="ts">
|
||||
import { computed } from 'vue';
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
const props = defineProps<{
|
||||
message: {
|
||||
id: number;
|
||||
user: { id: number; name: string } | null;
|
||||
content: string;
|
||||
created_at: string;
|
||||
media?: Array<{
|
||||
id: number;
|
||||
url: string;
|
||||
file_name: string;
|
||||
mime_type: string;
|
||||
}>;
|
||||
};
|
||||
}>();
|
||||
|
||||
const processedContent = computed(() => {
|
||||
return props.message.content.replace(/<user:(\d+)>/g, (match, userId) => {
|
||||
// This is a placeholder for a more robust user lookup
|
||||
return `<a href="${route('users.show', userId)}" class="text-blue-500 hover:underline">@user${userId}</a>`;
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-html="processedContent"></div>
|
||||
</template>
|
||||
@@ -1,5 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
|
||||
defineProps<{
|
||||
dynamicId: number;
|
||||
participants: Array<{
|
||||
id: number;
|
||||
name: string;
|
||||
@@ -19,7 +23,12 @@ defineProps<{
|
||||
:key="participant.id"
|
||||
class="c-participants-list__item"
|
||||
>
|
||||
{{ participant.pivot.display_name ?? participant.name }}
|
||||
<Link
|
||||
:href="route('dynamics.users.show', [dynamicId, participant.id])"
|
||||
class="block"
|
||||
>
|
||||
{{ participant.pivot.display_name ?? participant.name }}
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
<script setup lang="ts">
|
||||
import { Head, Link } from '@inertiajs/vue3';
|
||||
import { route } from 'ziggy-js';
|
||||
import AppLayout from '@/layouts/AppLayout.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
dynamic: {
|
||||
id: number;
|
||||
name: string;
|
||||
};
|
||||
participant: {
|
||||
id: number;
|
||||
name: string;
|
||||
display_name: string | null;
|
||||
role: string;
|
||||
};
|
||||
mutations: Array<{
|
||||
id: number;
|
||||
amount: number;
|
||||
description: string;
|
||||
status: string;
|
||||
created_at: string;
|
||||
ledger: { id: number; name: string };
|
||||
}>;
|
||||
}>();
|
||||
|
||||
const breadcrumbs = [
|
||||
{
|
||||
name: 'Dynamics',
|
||||
href: route('dynamics.index'),
|
||||
},
|
||||
{
|
||||
name: props.dynamic.name,
|
||||
href: route('dynamics.show', props.dynamic.id),
|
||||
},
|
||||
{
|
||||
name: props.participant.display_name ?? props.participant.name,
|
||||
href: route('dynamics.users.show', [props.dynamic.id, props.participant.id]),
|
||||
},
|
||||
];
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Head :title="participant.display_name ?? participant.name" />
|
||||
|
||||
<AppLayout :breadcrumbs="breadcrumbs">
|
||||
<div class="c-participant-show">
|
||||
<div class="c-participant-show__container">
|
||||
<h2 class="c-participant-show__title">
|
||||
Activity for {{ participant.display_name ?? participant.name }} ({{ participant.role.toUpperCase() }}) in {{ dynamic.name }}
|
||||
</h2>
|
||||
|
||||
<div class="c-participant-show__activity-list">
|
||||
<div
|
||||
v-for="mutation in mutations"
|
||||
:key="mutation.id"
|
||||
class="c-participant-show__activity-item"
|
||||
>
|
||||
<Link :href="route('dynamics.ledgers.show', [dynamic.id, mutation.ledger.id])" class="block">
|
||||
<div class="c-participant-show__activity-meta">
|
||||
<span class="c-participant-show__activity-time">
|
||||
{{ new Date(mutation.created_at).toLocaleString() }}
|
||||
</span>
|
||||
<span class="font-semibold ml-2" :class="mutation.amount > 0 ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400'">
|
||||
{{ mutation.amount > 0 ? '+' : '' }}{{ mutation.amount }}
|
||||
</span>
|
||||
<span class="text-neutral-400 ml-2">on {{ mutation.ledger.name }}</span>
|
||||
<span class="uppercase text-xs px-1.5 py-0.5 rounded ml-auto" :class="mutation.status === 'approved' ? 'bg-green-100 text-green-700 dark:bg-green-950/30 dark:text-green-400' : 'bg-yellow-100 text-yellow-700 dark:bg-yellow-950/30 dark:text-yellow-400'">
|
||||
{{ mutation.status }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="c-participant-show__activity-desc">
|
||||
{{ mutation.description }}
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
<div v-if="mutations.length === 0" class="text-neutral-500 text-sm">
|
||||
No mutations recorded for this participant in this Dynamic yet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AppLayout>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
@reference "../../../../css/app.css";
|
||||
|
||||
.c-participant-show {
|
||||
@apply py-12;
|
||||
}
|
||||
|
||||
.c-participant-show__container {
|
||||
@apply mx-auto max-w-7xl sm:px-6 lg:px-8;
|
||||
}
|
||||
|
||||
.c-participant-show__title {
|
||||
@apply mb-6 text-xl font-bold tracking-tight text-neutral-900 dark:text-neutral-100;
|
||||
}
|
||||
|
||||
.c-participant-show__activity-list {
|
||||
@apply space-y-4;
|
||||
}
|
||||
|
||||
.c-participant-show__activity-item {
|
||||
@apply rounded-lg border p-4 transition-colors hover:bg-neutral-50 dark:border-neutral-800 dark:hover:bg-neutral-900;
|
||||
background-color: var(--card);
|
||||
}
|
||||
|
||||
.c-participant-show__activity-meta {
|
||||
@apply mb-1.5 flex items-center text-xs;
|
||||
}
|
||||
|
||||
.c-participant-show__activity-time {
|
||||
@apply text-neutral-400 dark:text-neutral-500;
|
||||
}
|
||||
|
||||
.c-participant-show__activity-desc {
|
||||
@apply text-sm text-neutral-600 dark:text-neutral-400 mt-1;
|
||||
}
|
||||
</style>
|
||||
@@ -21,6 +21,10 @@ const props = defineProps<{
|
||||
}>;
|
||||
};
|
||||
isOwner: boolean;
|
||||
messages: {
|
||||
data: Array<any>;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
}>();
|
||||
|
||||
|
||||
@@ -58,7 +62,7 @@ const breadcrumbs = [
|
||||
</div>
|
||||
|
||||
<!-- Dynamic Chat -->
|
||||
<Chat :chat="dynamic.chat" :participants="dynamic.participants" :dynamic-id="dynamic.id" />
|
||||
<Chat :chat="dynamic.chat" :initial-messages="messages" :participants="dynamic.participants" :dynamic-id="dynamic.id" />
|
||||
|
||||
<!-- Participants Component -->
|
||||
<ParticipantsList :participants="dynamic.participants" />
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useEcho } from '@laravel/echo-vue';
|
||||
import { ref } from 'vue';
|
||||
import { route } from 'ziggy-js';
|
||||
import AddMutationForm from '@/components/AddMutationForm.vue';
|
||||
import Chat from '@/components/Chat.vue';
|
||||
import MutationList from '@/components/MutationList.vue';
|
||||
|
||||
const props = defineProps<{
|
||||
@@ -37,6 +38,10 @@ const props = defineProps<{
|
||||
}>;
|
||||
};
|
||||
isOwner: boolean;
|
||||
messages: {
|
||||
data: Array<any>;
|
||||
next_page_url: string | null;
|
||||
};
|
||||
}>();
|
||||
|
||||
const breadcrumbs = [
|
||||
@@ -258,6 +263,8 @@ function isOwnerUser(userId: number): boolean {
|
||||
:ledger-alignment="ledger.alignment"
|
||||
@open-lightbox="openLightbox"
|
||||
/>
|
||||
|
||||
<Chat :chat="dynamic.chat" :initial-messages="messages" :participants="dynamic.participants" :dynamic-id="dynamic.id" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user