feat: just getting better and better
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-04 21:34:38 -04:00
parent 664eb07201
commit b7379cf823
45 changed files with 1411 additions and 11114 deletions

View File

@@ -1,5 +1,5 @@
<script setup>
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
@@ -13,8 +13,6 @@
mdiBellOutline,
mdiCalendarMonthOutline,
mdiChevronDown,
mdiCogOutline,
mdiFileDocumentOutline,
mdiFolderOutline,
mdiHomeOutline,
mdiImageMultipleOutline,
@@ -45,14 +43,13 @@
const notificationsRef = ref(null);
const searchRef = ref(null);
const collapsedSearchInputRef = ref(null);
const collapsedSearchPanelStyle = ref({});
const primaryLinks = [
{ to: '/app/dashboard', labelKey: 'nav.overview', icon: mdiHomeOutline },
{ to: '/app/workspace', labelKey: 'nav.workspacePlan', icon: mdiCalendarMonthOutline },
{ to: '/app/media-library', labelKey: 'nav.mediaLibrary', icon: mdiImageMultipleOutline },
{ to: '/app/my-feedback', labelKey: 'nav.myFeedback', icon: mdiBugOutline },
{ to: '/app/feedback', labelKey: 'nav.feedbackReview', icon: mdiBugOutline, roles: ['developer'] },
{ to: '/app/workspace-settings', labelKey: 'nav.settings', icon: mdiCogOutline },
];
const visiblePrimaryLinks = computed(() =>
primaryLinks.filter(link => !link.roles || authStore.hasAnyRole(link.roles))
@@ -102,6 +99,9 @@
campaignResults.value.length > 0 || contentResults.value.length > 0
);
const isSearchOpen = computed(() => isSearchFocused.value && normalizedSearchQuery.value.length > 0);
const isSearchPanelOpen = computed(() =>
isSearchFocused.value && (!props.isExpanded || normalizedSearchQuery.value.length > 0)
);
const notificationTitleMap = computed(() => ({
'approval.requested': t('notifications.events.approvalRequested'),
@@ -127,6 +127,34 @@
isNotificationsOpen.value = !isNotificationsOpen.value;
}
function updateCollapsedSearchPanelPosition() {
if (props.isExpanded || !searchRef.value) {
collapsedSearchPanelStyle.value = {};
return;
}
const rect = searchRef.value.getBoundingClientRect();
const left = rect.right + 12;
const availableWidth = Math.max(240, window.innerWidth - left - 12);
collapsedSearchPanelStyle.value = {
left: `${left}px`,
top: `${Math.max(12, rect.top)}px`,
width: `${Math.min(352, availableWidth)}px`,
};
}
async function openCollapsedSearch() {
if (props.isExpanded) {
return;
}
isSearchFocused.value = true;
updateCollapsedSearchPanelPosition();
await nextTick();
collapsedSearchInputRef.value?.focus();
}
function formatNotificationTitle(notification) {
return notificationTitleMap.value[notification.eventType] ?? notification.message;
}
@@ -181,12 +209,35 @@
{ immediate: true }
);
watch(isSearchPanelOpen, isOpen => {
if (isOpen) {
updateCollapsedSearchPanelPosition();
}
});
watch(
() => props.isExpanded,
isExpanded => {
if (isExpanded) {
collapsedSearchPanelStyle.value = {};
return;
}
isNotificationsOpen.value = false;
updateCollapsedSearchPanelPosition();
}
);
onMounted(() => {
document.addEventListener('click', handleDocumentClick);
window.addEventListener('resize', updateCollapsedSearchPanelPosition);
window.addEventListener('scroll', updateCollapsedSearchPanelPosition, true);
});
onBeforeUnmount(() => {
document.removeEventListener('click', handleDocumentClick);
window.removeEventListener('resize', updateCollapsedSearchPanelPosition);
window.removeEventListener('scroll', updateCollapsedSearchPanelPosition, true);
});
</script>
@@ -222,6 +273,8 @@
<label
class="sidebar-search"
:class="{ 'sidebar-search-open': isSearchOpen }"
:title="!isExpanded ? 'Search' : null"
@click="openCollapsedSearch"
>
<v-icon
:icon="mdiMagnify"
@@ -238,9 +291,28 @@
</label>
<div
v-if="isSearchOpen"
v-if="isSearchPanelOpen"
class="sidebar-floating-panel"
:class="{ 'sidebar-search-panel-collapsed': !isExpanded }"
:style="!isExpanded ? collapsedSearchPanelStyle : null"
>
<label
v-if="!isExpanded"
class="sidebar-search sidebar-search-panel-input"
>
<v-icon
:icon="mdiMagnify"
class="sidebar-search-icon"
/>
<input
ref="collapsedSearchInputRef"
v-model="searchQuery"
type="search"
class="sidebar-search-input"
placeholder="Search"
/>
</label>
<div
v-if="campaignResults.length"
class="sidebar-search-group"
@@ -274,7 +346,7 @@
</div>
<div
v-if="!hasSearchResults"
v-if="normalizedSearchQuery.length > 0 && !hasSearchResults"
class="sidebar-search-empty"
>
No results found.
@@ -383,7 +455,7 @@
:title="!isExpanded ? t('nav.content') : null"
>
<span class="sidebar-link-main">
<v-icon :icon="mdiFileDocumentOutline" />
<v-icon :icon="mdiCalendarMonthOutline" />
<span
v-if="isExpanded"
class="sidebar-link-label"
@@ -411,6 +483,7 @@
class="sidebar-link sidebar-link-section"
active-class="sidebar-link-active"
:title="!isExpanded ? t('nav.campaigns') : null"
@click="toggleSection('campaigns')"
>
<span class="sidebar-link-main">
<v-icon :icon="mdiFolderOutline" />
@@ -420,6 +493,12 @@
>
{{ t('nav.campaigns') }}
</span>
<v-icon
v-if="isExpanded"
:icon="mdiChevronDown"
class="sidebar-chevron"
:class="{ 'sidebar-chevron-open': openSections.campaigns }"
/>
</span>
</router-link>
@@ -431,19 +510,6 @@
>
<v-icon :icon="mdiPlus" />
</router-link>
<button
v-if="isExpanded"
class="sidebar-section-toggle"
type="button"
@click="toggleSection('campaigns')"
>
<v-icon
:icon="mdiChevronDown"
class="sidebar-chevron"
:class="{ 'sidebar-chevron-open': openSections.campaigns }"
/>
</button>
</div>
<div
@@ -484,6 +550,7 @@
class="sidebar-link sidebar-link-section"
active-class="sidebar-link-active"
:title="!isExpanded ? t('nav.channels') : null"
@click="toggleSection('channels')"
>
<span class="sidebar-link-main">
<v-icon :icon="mdiLan" />
@@ -493,6 +560,12 @@
>
{{ t('nav.channels') }}
</span>
<v-icon
v-if="isExpanded"
:icon="mdiChevronDown"
class="sidebar-chevron"
:class="{ 'sidebar-chevron-open': openSections.channels }"
/>
</span>
</router-link>
@@ -504,19 +577,6 @@
>
<v-icon :icon="mdiPlus" />
</router-link>
<button
v-if="isExpanded"
class="sidebar-section-toggle"
type="button"
@click="toggleSection('channels')"
>
<v-icon
:icon="mdiChevronDown"
class="sidebar-chevron"
:class="{ 'sidebar-chevron-open': openSections.channels }"
/>
</button>
</div>
<div
@@ -566,20 +626,25 @@
}
.app-sidebar-scroll {
@apply flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pb-4 pt-3 pr-3;
@apply flex min-h-0 flex-1 flex-col gap-4 overflow-y-auto pb-4 pt-4;
}
.brand-block {
@apply flex items-center gap-3;
@apply flex items-center gap-3 pb-4;
border-bottom: 1px solid rgba(23, 32, 51, 0.08);
}
.brand-link {
@apply flex items-center gap-3 no-underline;
@apply flex min-w-0 items-center gap-3 no-underline;
color: inherit;
}
.brand-link-collapsed {
@apply w-full justify-center;
}
.brand-mark {
@apply flex h-11 w-11 items-center justify-center rounded-2xl text-lg font-black;
@apply flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1.1rem] text-xl font-black;
background: linear-gradient(135deg, #ff8a3d 0%, #ef4444 100%);
color: #fffaf2;
}
@@ -621,7 +686,7 @@
}
.sidebar-search-icon {
@apply text-xl;
@apply h-5 w-5 flex-shrink-0 text-xl;
}
.sidebar-search-input {
@@ -641,6 +706,14 @@
box-shadow: 0 18px 40px rgba(23, 32, 51, 0.12);
}
.sidebar-search-panel-collapsed {
@apply fixed right-auto top-auto;
}
.sidebar-search-panel-input {
@apply bg-white;
}
.sidebar-search-group {
@apply flex flex-col gap-1;
}
@@ -761,18 +834,12 @@
@apply truncate;
}
.sidebar-section-toggle {
@apply flex h-11 w-11 items-center justify-center rounded-[1rem] transition-colors;
color: #526178;
}
.sidebar-section-action {
@apply flex h-11 w-11 items-center justify-center rounded-[1rem] transition-colors no-underline;
@apply ml-auto flex h-11 w-11 flex-shrink-0 items-center justify-center rounded-[1rem] transition-colors no-underline;
color: #526178;
}
.sidebar-section-action:hover,
.sidebar-section-toggle:hover {
.sidebar-section-action:hover {
background: rgba(23, 32, 51, 0.06);
color: #172033;
}
@@ -785,8 +852,9 @@
transform: rotate(180deg);
}
.sidebar-link :deep(.v-icon) {
@apply text-xl;
.sidebar-link :deep(.v-icon),
.sidebar-section-action :deep(.v-icon) {
@apply h-5 w-5 flex-shrink-0 text-xl;
}
.sidebar-sublist {
@@ -818,10 +886,30 @@
@apply w-[5.5rem] px-3;
}
.app-sidebar-collapsed .brand-block {
@apply justify-center;
}
.app-sidebar-collapsed .sidebar-search {
@apply justify-center px-0;
}
.app-sidebar-collapsed .sidebar-link {
@apply justify-center px-0;
}
.app-sidebar-collapsed .sidebar-link-main {
@apply justify-center;
}
.app-sidebar-collapsed .sidebar-search:hover {
background: rgba(23, 32, 51, 0.07);
}
.app-sidebar-collapsed .sidebar-search-panel-input {
@apply justify-start px-4;
}
.app-sidebar-collapsed .sidebar-floating-panel {
left: calc(100% + 0.75rem);
right: auto;