Files
social-media/frontend/src/features/campaigns/views/CampaignDetailView.vue

233 lines
7.0 KiB
Vue

<script setup>
import { computed } from 'vue';
import { useRoute } from 'vue-router';
import { useAuthStore } from '@/features/auth/stores/authStore.js';
import { useWorkspaceStore } from '@/features/workspaces/stores/workspaceStore.js';
import { useCampaignsStore } from '@/features/campaigns/stores/campaignsStore.js';
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
const authStore = useAuthStore();
const route = useRoute();
const workspaceStore = useWorkspaceStore();
const campaignsStore = useCampaignsStore();
const contentItemsStore = useContentItemsStore();
const campaign = computed(() =>
campaignsStore.campaigns.find(candidate => candidate.id === route.params.campaignId) ?? null
);
const scopedItems = computed(() =>
contentItemsStore.items
.filter(item => item.campaignId === route.params.campaignId)
.sort((left, right) => {
const leftDue = left.dueDate ? new Date(left.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
const rightDue = right.dueDate ? new Date(right.dueDate).getTime() : Number.MAX_SAFE_INTEGER;
return leftDue - rightDue;
})
);
function formatCampaignDateRange(campaignValue) {
if (!campaignValue?.startDate || !campaignValue?.endDate) {
return 'No date range';
}
return new Intl.DateTimeFormat(undefined, {
month: 'short',
day: 'numeric',
year: 'numeric',
}).formatRange(new Date(campaignValue.startDate), new Date(campaignValue.endDate));
}
</script>
<template>
<section class="page-shell">
<div
v-if="!campaign"
class="page-message error"
>
The selected campaign could not be found in the active workspace.
</div>
<template v-else>
<div class="hero">
<div>
<div class="breadcrumb-row">
<router-link
class="breadcrumb"
:to="{ name: 'workspace-dashboard' }"
>
Workspace
</router-link>
<span>/</span>
<router-link
class="breadcrumb"
:to="{ name: 'campaigns' }"
>
Campaigns
</router-link>
</div>
<h1>{{ campaign.name }}</h1>
<p>{{ campaign.description || `${workspaceStore.activeWorkspace?.name} delivery stream with only the content scheduled in this campaign.` }}</p>
</div>
<div class="hero-meta">
<div class="meta-chip">{{ campaign.status }}</div>
<div class="meta-copy">{{ formatCampaignDateRange(campaign) }}</div>
</div>
</div>
<div
v-if="campaign.notes"
class="page-message"
>
{{ campaign.notes }}
</div>
<div class="section-header">
<strong>Content items</strong>
<span>{{ scopedItems.length }} scheduled in this campaign</span>
</div>
<div class="scope-actions">
<router-link
v-if="authStore.isManager || authStore.isProvider"
:to="{ name: 'content-item-create', query: { campaignId: campaign.id } }"
class="scope-button"
>
New content in {{ campaign.name }}
</router-link>
</div>
<div
v-if="scopedItems.length"
class="content-grid"
>
<router-link
v-for="item in scopedItems"
:key="item.id"
:to="{ name: 'content-item-detail', params: { id: item.id } }"
class="content-card"
>
<div class="version-chip">{{ item.currentRevisionLabel }}</div>
<strong>{{ item.title }}</strong>
<span>{{ item.publicationTargets }}</span>
<div class="status-row">
<em>{{ item.status }}</em>
<small>{{ item.dueDate ? new Date(item.dueDate).toLocaleDateString() : 'No due date' }}</small>
</div>
</router-link>
</div>
<div
v-else
class="page-message"
>
No content items are attached to this campaign yet.
</div>
</template>
</section>
</template>
<style scoped>
.page-shell {
@apply mx-auto flex w-full max-w-7xl flex-col gap-6 px-5 py-8 md:px-8;
}
.hero,
.content-card {
@apply rounded-[1.5rem] border;
background: rgba(255, 255, 255, 0.9);
border-color: rgba(23, 32, 51, 0.08);
}
.hero {
@apply flex flex-col gap-5 p-6 lg:flex-row lg:items-start lg:justify-between;
}
.breadcrumb-row {
@apply flex items-center gap-2 text-sm;
color: #0f766e;
}
.breadcrumb,
.hero p,
.meta-copy,
.section-header span,
.content-card span,
.status-row small,
.status-row em {
@apply text-sm leading-6 not-italic;
color: #526178;
}
.breadcrumb {
@apply font-bold uppercase tracking-[0.16em];
color: #0f766e;
}
.hero h1,
.section-header strong,
.content-card strong {
color: #172033;
}
.hero h1 {
@apply mt-2 text-4xl font-black;
}
.hero-meta {
@apply flex flex-wrap items-start gap-3;
}
.meta-chip,
.version-chip {
@apply rounded-full px-4 py-2 text-xs font-bold uppercase tracking-[0.16em];
background: rgba(23, 32, 51, 0.08);
color: #172033;
}
.section-header {
@apply flex items-center justify-between gap-4;
}
.scope-actions {
@apply flex justify-start;
}
.scope-button {
@apply inline-flex items-center justify-center rounded-full px-5 py-3 text-sm font-bold no-underline transition;
background: #172033;
color: #fffaf2;
}
.scope-button:hover {
background: #0f172a;
}
.section-header strong {
@apply text-lg font-black;
}
.content-grid {
@apply grid gap-4 md:grid-cols-2 xl:grid-cols-3;
}
.content-card {
@apply flex flex-col gap-4 p-5 no-underline;
}
.status-row {
@apply flex items-center justify-between gap-3;
}
.page-message {
@apply rounded-[1.25rem] border p-4 text-sm font-medium;
background: rgba(255, 255, 255, 0.84);
border-color: rgba(23, 32, 51, 0.08);
color: #526178;
}
.page-message.error {
color: #b91c1c;
}
</style>