233 lines
7.0 KiB
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>
|