Skip to content

Commit 65d2057

Browse files
committed
feat: incorporate mentions in posts (vue-tribute)
1 parent 8362925 commit 65d2057

File tree

9 files changed

+302
-35
lines changed

9 files changed

+302
-35
lines changed

app/Domains/Vault/ManageJournals/Web/ViewHelpers/JournalShowViewHelper.php

+13
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
namespace App\Domains\Vault\ManageJournals\Web\ViewHelpers;
44

55
use App\Helpers\DateHelper;
6+
use App\Helpers\NameHelper;
67
use App\Helpers\SliceOfLifeHelper;
78
use App\Helpers\SQLHelper;
89
use App\Models\Journal;
@@ -95,6 +96,18 @@ public static function postsInYear(Journal $journal, int $year, User $user): Col
9596
'post' => $post,
9697
]),
9798
],
99+
'contacts' => $post->contacts->map(function ($contact) use ($user) {
100+
// Format the contact name using the NameHelper
101+
$contact->name = NameHelper::formatContactName($user, $contact);
102+
103+
// Generate the URL for the contact
104+
$contact->url = route('contact.show', [
105+
'vault' => $contact->vault_id,
106+
'contact' => $contact->id,
107+
]);
108+
109+
return $contact;
110+
}),
98111
]);
99112

100113
$monthsCollection->push([

app/Domains/Vault/Search/Web/ViewHelpers/VaultContactSearchViewHelper.php

+8-2
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@
22

33
namespace App\Domains\Vault\Search\Web\ViewHelpers;
44

5+
use App\Helpers\NameHelper;
56
use App\Models\Contact;
67
use App\Models\Vault;
78
use Illuminate\Support\Collection;
9+
use Illuminate\Support\Facades\Auth;
810

911
class VaultContactSearchViewHelper
1012
{
@@ -16,10 +18,14 @@ public static function data(Vault $vault, string $term): Collection
1618
->take(5)
1719
->get();
1820

19-
return $contacts->map(function (Contact $contact): array {
21+
// Get the current user
22+
$user = Auth::user();
23+
24+
return $contacts->map(function (Contact $contact) use ($user): array {
2025
return [
2126
'id' => $contact->id,
22-
'name' => $contact->first_name.' '.$contact->last_name.' '.$contact->nickname.' '.$contact->maiden_name.' '.$contact->middle_name,
27+
// Format the name using the NameHelper to ensure consistency
28+
'name' => NameHelper::formatContactName($user, $contact),
2329
'url' => route('contact.show', [
2430
'vault' => $contact->vault_id,
2531
'contact' => $contact->id,

package.json

+2
Original file line numberDiff line numberDiff line change
@@ -40,11 +40,13 @@
4040
"sass": "^1.85.1",
4141
"tailwindcss": "^4.0.9",
4242
"tiny-emitter": "^2.1.0",
43+
"tributejs": "^5.1.3",
4344
"uploadcare-vue": "^1.0.0",
4445
"v-calendar": "^3.1.2",
4546
"vite": "^6.2.0",
4647
"vue": "^3.5.13",
4748
"vue-clipboard3": "^2.0.0",
49+
"vue-tribute": "^2.0.0",
4850
"vuedraggable": "^4.1.0",
4951
"ziggy-js": "2.5.1"
5052
},

resources/js/Pages/Vault/Journal/Post/Edit.vue

+93-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<script setup>
22
import { Link, useForm } from '@inertiajs/vue3';
3-
import { watch, ref } from 'vue';
3+
import { watch, ref, defineProps, computed } from 'vue';
44
import { debounce } from 'lodash';
55
import { trans } from 'laravel-vue-i18n';
66
import { DatePicker } from 'v-calendar';
@@ -30,7 +30,20 @@ const form = useForm({
3030
sections: props.data.sections.map((section) => ({
3131
id: section.id,
3232
label: section.label,
33-
content: section.content,
33+
content: (section.content || '').replace(
34+
// not defined when dealing with a new post
35+
/\{\{\{CONTACT-ID:([a-f0-9-]+)\|(.*?)\}\}\}/g,
36+
(match, contactId, fallbackName) => {
37+
let contact = props.data.contacts.find((c) => c.id === contactId);
38+
39+
if (contact) {
40+
return `@"${contact.name.trim()}"`;
41+
}
42+
43+
// If contact is missing, use fallback name
44+
return `@${fallbackName} (contact no longer linked to post)`;
45+
},
46+
),
3447
})),
3548
uuid: null,
3649
name: null,
@@ -40,6 +53,22 @@ const form = useForm({
4053
size: null,
4154
});
4255
56+
const hasInvalidMentions = ref(false); // Track if there are invalid mentions
57+
const mentionErrorMessage = ref(''); // Error message for invalid mentions
58+
59+
const tributeOptions = computed(() => ({
60+
trigger: '@',
61+
allowSpaces: true,
62+
values: form.contacts.map((contact) => ({
63+
key: contact.name,
64+
value: contact.name,
65+
id: contact.id,
66+
original: contact,
67+
})),
68+
selectTemplate: function (item) {
69+
return `@"${item.original.key.trim()}"`;
70+
},
71+
}));
4372
const saveInProgress = ref(false);
4473
const statistics = ref(props.data.statistics);
4574
const deletePhotoModalShown = ref(false);
@@ -131,8 +160,54 @@ const destroyPhoto = () => {
131160
const update = () => {
132161
saveInProgress.value = true;
133162
163+
// Clone form to avoid modifying the UI content
164+
let processedForm = JSON.parse(JSON.stringify(form));
165+
166+
let invalidMentionsFound = false;
167+
let invalidMentionText = '';
168+
169+
processedForm.sections.forEach((section) => {
170+
if (section.content) {
171+
section.content = section.content.replace(/@"([A-Za-z.'’]+(?:\s+[A-Za-z.'’]+)*)"/g, (match, name) => {
172+
name = name.trim(); // Trim spaces from the mention name
173+
console.log('Matched Name:', name);
174+
175+
let contact = processedForm.contacts.find((c) => c.name.trim() === name); // Trim contact names before matching
176+
177+
if (contact) {
178+
// Check if there are duplicate contacts with the same name and different IDs
179+
const duplicateContacts = processedForm.contacts.filter((c) => c.name.trim() === name && c.id !== contact.id);
180+
181+
if (duplicateContacts.length > 0) {
182+
// If there are duplicate contacts with the same name and different IDs, mark as invalid
183+
invalidMentionsFound = true;
184+
invalidMentionText = `${trans('Cannot mention a contact when there are 2 identical contacts linked: @name', { name })}`;
185+
return `@${name} (duplicate contacts exist)`;
186+
}
187+
188+
return `{{{CONTACT-ID:${contact.id}|${name}}}}`;
189+
}
190+
191+
// If no contact is found, mark as invalid
192+
invalidMentionsFound = true;
193+
invalidMentionText = trans('Invalid mention') + `: @${name}`;
194+
195+
// If no contact is found, remove apostrophes and add fallback text
196+
return `@${name} (contact not linked to post)`;
197+
});
198+
}
199+
});
200+
201+
// If invalid mentions were found, set the error state and stop the upload
202+
if (invalidMentionsFound) {
203+
hasInvalidMentions.value = true;
204+
mentionErrorMessage.value = invalidMentionText;
205+
saveInProgress.value = false;
206+
return; // Prevent the form from being uploaded
207+
}
208+
134209
axios
135-
.put(props.data.url.update, form)
210+
.put(props.data.url.update, processedForm)
136211
.then((response) => {
137212
setTimeout(() => (saveInProgress.value = false), 350);
138213
statistics.value = response.data.data;
@@ -326,7 +401,15 @@ const destroy = () => {
326401
:required="true"
327402
:maxlength="65535"
328403
:markdown="true"
329-
:textarea-class="'block w-full'" />
404+
:tribute-options="tributeOptions"
405+
:textarea-class="{
406+
'block w-full': true,
407+
'border-red-500': hasInvalidMentions, // Add the red border if invalid mentions
408+
}" />
409+
</div>
410+
<!-- Show error message if invalid mentions exist -->
411+
<div v-if="hasInvalidMentions" class="text-red-500 text-sm mt-2">
412+
{{ mentionErrorMessage }}
330413
</div>
331414
</div>
332415
</div>
@@ -343,7 +426,8 @@ const destroy = () => {
343426

344427
<!-- auto save -->
345428
<div class="mb-6 text-sm">
346-
<div v-if="!saveInProgress" class="flex items-center justify-center">
429+
<!-- Show the auto-saved message unless there's an error with mentions -->
430+
<div v-if="!hasInvalidMentions && !saveInProgress" class="flex items-center justify-center">
347431
<svg
348432
class="me-2 h-4 w-4 text-green-700"
349433
xmlns="http://www.w3.org/2000/svg"
@@ -371,6 +455,10 @@ const destroy = () => {
371455
</div>
372456
</div>
373457

458+
<div v-if="hasInvalidMentions" class="text-red-500 text-sm mt-2 flex items-center justify-center">
459+
<span>{{ $t('Not saving until errors are fixed') }}</span>
460+
</div>
461+
374462
<!-- written at -->
375463
<p class="mb-2 flex items-center font-bold">
376464
<span>{{ $t('Written on') }}</span>

resources/js/Pages/Vault/Journal/Post/Show.vue

+4-3
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22
import { Link } from '@inertiajs/vue3';
33
import Layout from '@/Shared/Layout.vue';
44
import ContactCard from '@/Shared/ContactCard.vue';
5+
import { convertMentions } from '@/utils/mentionUtils.js';
56
6-
defineProps({
7+
const props = defineProps({
78
layoutData: Object,
8-
data: Object,
9+
data: Object, // Ensure data is correctly passed as a prop
910
});
1011
</script>
1112

@@ -138,7 +139,7 @@ defineProps({
138139
{{ section.label }}
139140
</div>
140141

141-
<div class="mb-6" v-html="section.content"></div>
142+
<div class="mb-6" v-html="convertMentions(section.content, props.data.contacts)"></div>
142143
</div>
143144
</div>
144145

resources/js/Pages/Vault/Journal/Show.vue

+34-22
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ import { Link, useForm } from '@inertiajs/vue3';
33
import Layout from '@/Shared/Layout.vue';
44
import PrettyLink from '@/Shared/Form/PrettyLink.vue';
55
import { trans } from 'laravel-vue-i18n';
6-
import { ChevronRight } from 'lucide-vue-next';
6+
import { convertMentions } from '@/utils/mentionUtils.js';
7+
78
const props = defineProps({
89
layoutData: Object,
910
data: Object,
@@ -25,28 +26,37 @@ const destroy = () => {
2526
<template>
2627
<layout :layout-data="layoutData" :inside-vault="true">
2728
<!-- breadcrumb -->
28-
<nav class="bg-white dark:bg-gray-900 sm:mt-20 sm:border-b sm:border-gray-300 dark:border-gray-700">
29+
<nav class="bg-white dark:bg-gray-900 sm:mt-20 sm:border-b">
2930
<div class="max-w-8xl mx-auto hidden px-4 py-2 sm:px-6 md:block">
30-
<div class="flex items-center gap-1 text-sm">
31-
<div class="text-gray-600 dark:text-gray-400">
32-
{{ $t('You are here:') }}
33-
</div>
34-
<div class="inline">
35-
<Link :href="layoutData.vault.url.journals" class="text-blue-500 hover:underline">
36-
{{ $t('Journals') }}
37-
</Link>
38-
</div>
39-
<div class="relative inline">
40-
<ChevronRight class="h-3 w-3" />
41-
</div>
42-
<div class="inline">
43-
{{ data.name }}
44-
</div>
31+
<div class="flex items-baseline justify-between space-x-6">
32+
<ul class="text-sm">
33+
<li class="me-2 inline text-gray-600 dark:text-gray-400">
34+
{{ $t('You are here:') }}
35+
</li>
36+
<li class="me-2 inline">
37+
<Link :href="layoutData.vault.url.journals" class="text-blue-500 hover:underline">
38+
{{ $t('Journals') }}
39+
</Link>
40+
</li>
41+
<li class="relative me-2 inline">
42+
<svg
43+
xmlns="http://www.w3.org/2000/svg"
44+
class="icon-breadcrumb relative inline h-3 w-3"
45+
fill="none"
46+
viewBox="0 0 24 24"
47+
stroke="currentColor">
48+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
49+
</svg>
50+
</li>
51+
<li class="inline">
52+
{{ data.name }}
53+
</li>
54+
</ul>
4555
</div>
4656
</div>
4757
</nav>
4858

49-
<main class="sm:mt-10 relative">
59+
<main class="sm:mt-18 relative">
5060
<div class="mx-auto max-w-6xl px-2 py-2 sm:px-6 sm:py-6 lg:px-8">
5161
<h1 class="text-2xl" :class="data.description ? 'mb-4' : 'mb-8'">{{ data.name }}</h1>
5262

@@ -170,10 +180,12 @@ const destroy = () => {
170180
<div class="flex w-full items-center justify-between">
171181
<!-- title and excerpt -->
172182
<div>
173-
<span>
174-
<Link :href="post.url.show" class="text-blue-500 hover:underline">{{ post.title }}</Link>
175-
</span>
176-
<p v-if="post.excerpt">{{ post.excerpt }}</p>
183+
<span
184+
><Link :href="post.url.show" class="text-blue-500 hover:underline">{{
185+
post.title
186+
}}</Link></span
187+
>
188+
<p v-if="post.excerpt" v-html="convertMentions(post.excerpt, post.contacts)"></p>
177189
</div>
178190

179191
<!-- photo -->

0 commit comments

Comments
 (0)