Add calendar integrations and collaboration updates
Some checks failed
Backend CI/CD / build_and_deploy (push) Has been cancelled
Frontend CI/CD / build_and_deploy (push) Has been cancelled

This commit is contained in:
2026-05-05 15:25:53 -04:00
parent c49f03ec06
commit b66c10b681
82 changed files with 8420 additions and 2048 deletions

View File

@@ -4,6 +4,7 @@
import AppAvatar from '@/components/AppAvatar.vue';
import ImageCropperDialog from '@/components/ImageCropperDialog.vue';
import { useUserProfileStore } from '@/features/user-profile/stores/userProfileStore.js';
import config from '@/config.js';
const userProfileStore = useUserProfileStore();
const { t } = useI18n();
@@ -11,6 +12,8 @@
const isSavingPortrait = ref(false);
const settingsError = ref(null);
const settingsStatus = ref(null);
const calendarFeedStatus = ref(null);
const calendarFeedError = ref(null);
const form = reactive({
firstname: '',
lastname: '',
@@ -22,6 +25,17 @@
const alias = computed(() => userProfileStore.alias);
const fullname = computed(() => userProfileStore.fullname);
const canSave = computed(() => Boolean(form.email.trim()) && !userProfileStore.isUpdating);
const calendarFeedUrl = computed(() => {
const feedUrl = userProfileStore.calendarExportFeed?.feedUrl;
if (!feedUrl) {
return '';
}
return feedUrl.startsWith('http')
? feedUrl
: `${config.apiUrl.replace(/\/$/, '')}${feedUrl}`;
});
function syncFormFromUser(user) {
form.firstname = user?.firstname ?? '';
@@ -84,11 +98,54 @@
}
}
async function enableCalendarFeed() {
await updateCalendarFeed(() => userProfileStore.enableCalendarExportFeed(), t('userSettings.calendarFeed.enabled'));
}
async function regenerateCalendarFeed() {
await updateCalendarFeed(() => userProfileStore.regenerateCalendarExportFeed(), t('userSettings.calendarFeed.regenerated'));
}
async function revokeCalendarFeed() {
await updateCalendarFeed(() => userProfileStore.revokeCalendarExportFeed(), t('userSettings.calendarFeed.revoked'));
}
async function copyCalendarFeedUrl() {
if (!calendarFeedUrl.value) {
return;
}
try {
await navigator.clipboard.writeText(calendarFeedUrl.value);
calendarFeedStatus.value = t('userSettings.calendarFeed.copied');
calendarFeedError.value = null;
} catch (error) {
console.error('Failed to copy calendar feed URL:', error);
calendarFeedStatus.value = null;
calendarFeedError.value = t('userSettings.calendarFeed.errors.copyFailed');
}
}
async function updateCalendarFeed(action, successMessage) {
calendarFeedStatus.value = null;
calendarFeedError.value = null;
try {
await action();
calendarFeedStatus.value = successMessage;
} catch (error) {
console.error('Failed to update calendar feed:', error);
calendarFeedError.value = t('userSettings.calendarFeed.errors.updateFailed');
}
}
watch(
() => userProfileStore.user,
syncFormFromUser,
{ immediate: true, deep: true }
);
userProfileStore.fetchCalendarExportFeed();
</script>
<template>
@@ -201,6 +258,81 @@
</form>
</div>
<div class="panel">
<div class="panel-heading">
<strong>{{ t('userSettings.calendarFeed.title') }}</strong>
<span>{{ t('userSettings.calendarFeed.description') }}</span>
</div>
<div
v-if="calendarFeedError"
class="page-message error"
>
{{ calendarFeedError }}
</div>
<div
v-if="calendarFeedStatus"
class="page-message success"
>
{{ calendarFeedStatus }}
</div>
<div
v-if="userProfileStore.calendarExportFeed?.isEnabled && calendarFeedUrl"
class="calendar-feed-box"
>
<span>{{ t('userSettings.calendarFeed.feedUrl') }}</span>
<code>{{ calendarFeedUrl }}</code>
</div>
<div
v-else
class="calendar-feed-empty"
>
{{ t('userSettings.calendarFeed.empty') }}
</div>
<div class="calendar-feed-actions">
<button
v-if="!userProfileStore.calendarExportFeed?.isEnabled"
class="primary-button"
type="button"
:disabled="userProfileStore.isUpdatingCalendarFeed"
@click="enableCalendarFeed"
>
{{ t('userSettings.calendarFeed.enable') }}
</button>
<template v-else>
<button
class="secondary-button"
type="button"
:disabled="!calendarFeedUrl"
@click="copyCalendarFeedUrl"
>
{{ t('userSettings.calendarFeed.copy') }}
</button>
<button
class="secondary-button"
type="button"
:disabled="userProfileStore.isUpdatingCalendarFeed"
@click="regenerateCalendarFeed"
>
{{ t('userSettings.calendarFeed.regenerate') }}
</button>
<button
class="danger-button"
type="button"
:disabled="userProfileStore.isUpdatingCalendarFeed"
@click="revokeCalendarFeed"
>
{{ t('userSettings.calendarFeed.revoke') }}
</button>
</template>
</div>
</div>
<ImageCropperDialog
v-model="isPortraitDialogOpen"
:title="t('userSettings.cropperTitle')"
@@ -318,8 +450,47 @@
color: #fffaf2;
}
.primary-button:disabled {
.secondary-button,
.danger-button {
@apply inline-flex items-center justify-center gap-2 rounded-full px-5 py-3 text-sm font-bold transition;
}
.secondary-button {
background: rgba(23, 32, 51, 0.06);
color: #172033;
}
.danger-button {
background: rgba(185, 28, 28, 0.08);
color: #b91c1c;
}
.primary-button:disabled,
.secondary-button:disabled,
.danger-button:disabled {
cursor: not-allowed;
opacity: 0.55;
}
.calendar-feed-box {
@apply flex flex-col gap-2 rounded-[1rem] border p-4;
background: #fffaf2;
border-color: rgba(23, 32, 51, 0.08);
}
.calendar-feed-box span,
.calendar-feed-empty {
@apply text-sm leading-6;
color: #526178;
}
.calendar-feed-box code {
@apply overflow-x-auto rounded-[0.75rem] px-3 py-2 text-sm;
background: rgba(23, 32, 51, 0.06);
color: #172033;
}
.calendar-feed-actions {
@apply flex flex-wrap gap-3;
}
</style>