Files
monica/resources/js/Shared/Modules/LifeEvent.vue

312 lines
12 KiB
Vue

<script setup>
import { onMounted, ref } from 'vue';
import { trans } from 'laravel-vue-i18n';
import Loading from '@/Shared/Loading.vue';
import PrettyButton from '@/Shared/Form/PrettyButton.vue';
import ContactCard from '@/Shared/ContactCard.vue';
import HoverMenu from '@/Shared/HoverMenu.vue';
import CreateLifeEvent from '@/Shared/Modules/CreateLifeEvent.vue';
import ChevronIcon from '@/Shared/Icons/ChevronIcon.vue';
import ClockIcon from '@/Shared/Icons/ClockIcon.vue';
import TwoPinMapIcon from '@/Shared/Icons/TwoPinMapIcon.vue';
import { Flame } from 'lucide-vue-next';
const props = defineProps({
layoutData: Object,
data: Object,
});
const createLifeEventModalShown = ref(false);
const loadingData = ref(true);
const paginator = ref([]);
const localTimelines = ref([]);
const showAddLifeEventModalForTimelineEventId = ref(0);
onMounted(() => {
axios
.get(props.data.url.load)
.then((response) => {
loadingData.value = false;
response.data.data.timeline_events.forEach((entry) => {
localTimelines.value.push(entry);
});
paginator.value = response.data.paginator;
})
.catch(() => {});
});
const loadMore = () => {
axios
.get(paginator.value.nextPageUrl)
.then((response) => {
loadingData.value = false;
response.data.data.timeline_events.forEach((entry) => {
localTimelines.value.push(entry);
});
paginator.value = response.data.paginator;
})
.catch(() => {});
};
const destroy = (timelineEvent) => {
if (confirm(trans('Are you sure? This action cannot be undone.'))) {
axios
.delete(timelineEvent.url.destroy)
.then(() => {
var id = localTimelines.value.findIndex((x) => x.id === timelineEvent.id);
localTimelines.value.splice(id, 1);
})
.catch(() => {});
}
};
const destroyLifeEvent = (timelineEvent, lifeEvent) => {
if (confirm(trans('Are you sure? This action cannot be undone.'))) {
axios
.delete(lifeEvent.url.destroy)
.then(() => {
var id = localTimelines.value.findIndex((x) => x.id === timelineEvent.id);
var lifeEventId = localTimelines.value[id].life_events.findIndex((x) => x.id === lifeEvent.id);
localTimelines.value[id].life_events.splice(lifeEventId, 1);
})
.catch(() => {});
}
};
const refreshLifeEvent = (timelineEvent, lifeEvent) => {
let id = localTimelines.value.findIndex((x) => x.id === timelineEvent.id);
let lifeEventId = localTimelines.value[id].life_events.findIndex((x) => x.id === lifeEvent.id);
localTimelines.value[id].life_events[lifeEventId] = lifeEvent;
};
const refreshTimelineEvents = (timelineEvent) => {
localTimelines.value.unshift(timelineEvent);
};
const refreshLifeEvents = (lifeEvent) => {
var id = localTimelines.value.findIndex((x) => x.id === lifeEvent.timeline_event.id);
localTimelines.value[id].life_events.unshift(lifeEvent);
};
const showCreateLifeEventModal = () => {
createLifeEventModalShown.value = true;
};
const toggleTimelineEventVisibility = (timelineEvent) => {
timelineEvent.collapsed = !timelineEvent.collapsed;
axios.post(timelineEvent.url.toggle);
};
const toggleLifeEventVisibility = (lifeEvent) => {
lifeEvent.collapsed = !lifeEvent.collapsed;
axios.post(lifeEvent.url.toggle);
};
</script>
<template>
<div class="mb-10">
<!-- title + cta -->
<div class="mb-3 items-center justify-between border-b border-gray-200 pb-2 dark:border-gray-700 sm:flex">
<div class="mb-2 sm:mb-0 flex items-center gap-2">
<Flame class="h-4 w-4" />
<span class="font-semibold"> {{ $t('Life events') }} </span>
</div>
<pretty-button
:text="$t('Add a life event')"
:icon="'plus'"
:class="'w-full sm:w-fit'"
@click="showCreateLifeEventModal" />
</div>
<div>
<!-- add a timeline event -->
<create-life-event
:data="data"
:layout-data="layoutData"
:open-modal="createLifeEventModalShown"
:create-timeline-event="true"
@close-modal="createLifeEventModalShown = false"
@timeline-event-created="refreshTimelineEvents" />
<!-- list of timeline events -->
<div v-if="localTimelines">
<div v-for="timelineEvent in localTimelines" :key="timelineEvent.id" class="mb-4">
<!-- timeline event name -->
<div
class="mb-2 flex cursor-pointer items-center justify-between rounded-lg border border-gray-200 px-3 py-2 hover:bg-slate-50 dark:border-gray-700 dark:bg-gray-900 dark:hover:bg-slate-900"
@click="toggleTimelineEventVisibility(timelineEvent)">
<!-- timeline date / label / number of events -->
<div>
<span class="me-2 text-gray-500">{{ timelineEvent.happened_at }}</span>
<span
class="ms-3 whitespace-nowrap rounded-lg bg-slate-100 px-2 py-0.5 text-sm text-slate-400 dark:bg-slate-900">
{{ timelineEvent.life_events.length }}
</span>
</div>
<!-- chevrons and menu -->
<div class="flex">
<!-- chevrons -->
<ChevronIcon v-if="timelineEvent.collapsed" :type="'down'" />
<ChevronIcon v-if="!timelineEvent.collapsed" :type="'up'" />
<!-- menu -->
<hover-menu :show-edit="false" :show-delete="true" @delete="destroy(timelineEvent)" />
</div>
</div>
<!-- life events -->
<div v-if="!timelineEvent.collapsed">
<div
v-for="lifeEvent in timelineEvent.life_events"
:key="lifeEvent.id"
:class="!lifeEvent.collapsed ? 'border' : ''"
class="mb-2 ms-6 rounded-lg border-gray-200 dark:border-gray-700 dark:bg-gray-900">
<template v-if="lifeEvent.edit">
<create-life-event
:data="data"
:layout-data="layoutData"
:open-modal="lifeEvent.edit"
:life-event="lifeEvent"
@close-modal="lifeEvent.edit = false"
@life-event-created="(event) => refreshLifeEvent(timelineEvent, event)" />
</template>
<template v-else>
<!-- name of life event -->
<div
:class="lifeEvent.collapsed ? 'rounded-lg border' : ''"
class="flex cursor-pointer items-center justify-between rounded-t-lg border-b border-gray-200 px-3 py-2 hover:bg-slate-50 dark:border-gray-700 dark:hover:bg-slate-900">
<!-- title -->
<div @click="toggleLifeEventVisibility(lifeEvent)" class="flex items-center">
<p v-if="lifeEvent.summary" class="me-4 text-sm font-bold">{{ lifeEvent.summary }}</p>
<div>
<span
class="rounded-xs border bg-white px-2 py-1 font-mono text-sm dark:border-gray-700 dark:bg-gray-800">
{{ lifeEvent.life_event_type.category.label }}
</span>
>
<span
class="rounded-xs border bg-white px-2 py-1 font-mono text-sm dark:border-gray-700 dark:bg-gray-800">
{{ lifeEvent.life_event_type.label }}
</span>
</div>
</div>
<!-- chevrons and menu -->
<div class="flex">
<!-- chevrons -->
<ChevronIcon
v-if="timelineEvent.collapsed"
:type="'down'"
@click="toggleLifeEventVisibility(lifeEvent)" />
<ChevronIcon
v-if="!timelineEvent.collapsed"
:type="'up'"
@click="toggleLifeEventVisibility(lifeEvent)" />
<!-- menu -->
<hover-menu
:show-edit="true"
:show-delete="true"
@edit="lifeEvent.edit = true"
@delete="destroyLifeEvent(timelineEvent, lifeEvent)" />
</div>
</div>
<!-- description -->
<div
v-if="!lifeEvent.collapsed && lifeEvent.description"
class="flex items-center border-b border-gray-200 px-3 py-2 dark:border-gray-700">
{{ lifeEvent.description }}
</div>
<!-- date of life event | distance -->
<div
v-if="!lifeEvent.collapsed"
class="flex items-center border-b border-gray-200 px-3 py-2 text-sm dark:border-gray-700">
<!-- date -->
<div class="me-4 flex items-center">
<ClockIcon />
{{ lifeEvent.happened_at }}
</div>
<!-- distance -->
<div v-if="lifeEvent.distance" class="flex items-center">
<TwoPinMapIcon />
<span>{{ lifeEvent.distance }}</span>
</div>
</div>
<!-- participants -->
<div v-if="!lifeEvent.collapsed" class="flex flex-wrap p-3 pb-1">
<div v-for="contact in lifeEvent.participants" :key="contact.id" class="me-4">
<contact-card
:contact="contact"
:avatar-classes="'h-5 w-5 rounded-full me-2'"
:display-name="true" />
</div>
</div>
</template>
</div>
<!-- add a new life event to the timeline -->
<div class="mb-2 ms-6">
<span
@click="showAddLifeEventModalForTimelineEventId = timelineEvent.id"
v-if="showAddLifeEventModalForTimelineEventId !== timelineEvent.id"
class="cursor-pointer text-sm text-blue-500 hover:underline">
{{ $t('Add another life event') }}
</span>
<create-life-event
:data="data"
:layout-data="layoutData"
:open-modal="showAddLifeEventModalForTimelineEventId === timelineEvent.id"
:create-timeline-event="false"
:timeline-event="timelineEvent"
@close-modal="showAddLifeEventModalForTimelineEventId = 0"
@life-event-created="refreshLifeEvents" />
</div>
</div>
</div>
<!-- pagination -->
<div class="text-center" v-if="paginator.hasMorePages">
<span
@click="loadMore()"
class="cursor-pointer rounded-xs border border-gray-200 px-3 py-1 text-sm text-blue-500 hover:border-gray-500 dark:border-gray-700">
{{ $t('Load previous entries') }}
</span>
</div>
</div>
<!-- loading mode -->
<div v-if="loadingData" class="mb-5 rounded-lg border border-gray-200 p-20 text-center dark:border-gray-700">
<loading />
</div>
<!-- blank state -->
<div
v-if="localTimelines.length === 0"
class="mb-6 rounded-lg border border-gray-200 bg-white dark:border-gray-700 dark:bg-gray-900">
<img src="/img/contact_blank_life_event.svg" :alt="$t('Life events')" class="mx-auto mt-4 h-20 w-20" />
<p class="px-5 pb-5 pt-2 text-center">{{ $t('Life events let you document what happened in your life.') }}</p>
</div>
</div>
</div>
</template>
<style lang="scss" scoped>
.icon-sidebar {
top: -2px;
}
</style>