refactor: organize frontend by feature
This commit is contained in:
49
frontend/src/features/reviews/stores/reviewQueueStore.js
Normal file
49
frontend/src/features/reviews/stores/reviewQueueStore.js
Normal file
@@ -0,0 +1,49 @@
|
||||
import { computed } from 'vue';
|
||||
import { defineStore } from 'pinia';
|
||||
import { useContentItemsStore } from '@/features/content/stores/contentItemsStore.js';
|
||||
import { useProjectsStore } from '@/features/projects/stores/projectsStore.js';
|
||||
|
||||
const stageByStatus = {
|
||||
Draft: 'Draft',
|
||||
'In internal review': 'Internal review',
|
||||
'Changes requested internally': 'Internal changes requested',
|
||||
'Internal changes in progress': 'Internal revision',
|
||||
'Ready for client review': 'Ready for client review',
|
||||
'In client review': 'Client review',
|
||||
'Changes requested by client': 'Client changes requested',
|
||||
'Client changes in progress': 'Client revision',
|
||||
Approved: 'Approved',
|
||||
Rejected: 'Rejected',
|
||||
'Ready to publish': 'Ready to publish',
|
||||
Published: 'Published',
|
||||
Archived: 'Archived',
|
||||
};
|
||||
|
||||
export const useReviewQueueStore = defineStore('review-queue', () => {
|
||||
const contentItemsStore = useContentItemsStore();
|
||||
const projectsStore = useProjectsStore();
|
||||
|
||||
const items = computed(() =>
|
||||
contentItemsStore.items
|
||||
.filter(item => item.status !== 'Draft' && item.status !== 'Published' && item.status !== 'Archived')
|
||||
.map(item => {
|
||||
const project = projectsStore.projects.find(candidate => candidate.id === item.projectId);
|
||||
|
||||
return {
|
||||
id: item.id,
|
||||
title: item.title,
|
||||
projectName: project?.name ?? 'Unknown campaign',
|
||||
stage: stageByStatus[item.status] ?? item.status,
|
||||
status: item.status,
|
||||
dueLabel: item.dueDate ? `Due ${new Date(item.dueDate).toLocaleDateString()}` : 'No due date',
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
const urgentItems = computed(() => items.value.slice(0, 5));
|
||||
|
||||
return {
|
||||
items,
|
||||
urgentItems,
|
||||
};
|
||||
});
|
||||
102
frontend/src/features/reviews/views/ReviewQueueView.vue
Normal file
102
frontend/src/features/reviews/views/ReviewQueueView.vue
Normal file
@@ -0,0 +1,102 @@
|
||||
<script setup>
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { useReviewQueueStore } from '@/features/reviews/stores/reviewQueueStore.js';
|
||||
|
||||
const { t } = useI18n();
|
||||
const reviewQueueStore = useReviewQueueStore();
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-shell">
|
||||
<div class="header">
|
||||
<div>
|
||||
<div class="eyebrow">{{ t('reviewQueue.eyebrow') }}</div>
|
||||
<h1>{{ t('reviewQueue.title') }}</h1>
|
||||
<p>{{ t('reviewQueue.description') }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="queue-list">
|
||||
<article
|
||||
v-for="item in reviewQueueStore.items"
|
||||
:key="item.id"
|
||||
class="queue-row"
|
||||
>
|
||||
<div>
|
||||
<strong>{{ item.title }}</strong>
|
||||
<span>{{ item.projectName }} · {{ item.stage }}</span>
|
||||
</div>
|
||||
<div class="queue-meta">
|
||||
<em>{{ item.status }}</em>
|
||||
<small>{{ item.dueLabel }}</small>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!reviewQueueStore.items.length"
|
||||
class="page-message"
|
||||
>
|
||||
{{ t('reviewQueue.empty') }}
|
||||
</div>
|
||||
</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;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
@apply text-xs font-bold uppercase tracking-[0.24em];
|
||||
color: #0f766e;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
@apply mt-2 text-4xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.header p {
|
||||
@apply mt-3 max-w-2xl text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.queue-list {
|
||||
@apply flex flex-col gap-4;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.queue-row {
|
||||
@apply flex flex-col justify-between gap-4 rounded-[1.5rem] border p-5 lg:flex-row lg:items-center;
|
||||
background: rgba(255, 255, 255, 0.84);
|
||||
border-color: rgba(23, 32, 51, 0.08);
|
||||
}
|
||||
|
||||
.queue-row strong {
|
||||
@apply block text-xl font-black;
|
||||
color: #172033;
|
||||
}
|
||||
|
||||
.queue-row span,
|
||||
.queue-meta span,
|
||||
.queue-meta small {
|
||||
@apply text-sm leading-6;
|
||||
color: #526178;
|
||||
}
|
||||
|
||||
.queue-meta {
|
||||
@apply flex flex-col items-start gap-1 lg:items-end;
|
||||
}
|
||||
|
||||
.queue-meta em {
|
||||
@apply text-sm font-semibold uppercase tracking-[0.16em] not-italic;
|
||||
color: #ef4444;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user