260 lines
11 KiB
Vue
260 lines
11 KiB
Vue
<script setup lang="ts">
|
|
import { storeToRefs } from 'pinia'
|
|
import { useProjectsStore, type ProjectStatus } from '~/stores/projects'
|
|
import { usePagesStore } from '~/stores/pages'
|
|
import { ref } from 'vue'
|
|
import CreateProjectModal from '~/components/admin/CreateProjectModal.vue'
|
|
|
|
const projectsStore = useProjectsStore()
|
|
const pagesStore = usePagesStore()
|
|
const { filteredProjects, searchQuery, statusFilter, sortBy } = storeToRefs(projectsStore)
|
|
const projects = filteredProjects
|
|
const showCreateProject = ref(false)
|
|
|
|
const statusOptions = [
|
|
{ value: 'all', label: '전체' },
|
|
{ value: 'active', label: '진행중' },
|
|
{ value: 'archived', label: '종료' },
|
|
{ value: 'draft', label: '임시저장' }
|
|
] as const
|
|
|
|
const sortOptions = [
|
|
{ value: 'newest', label: '최신순' },
|
|
{ value: 'oldest', label: '오래된순' },
|
|
{ value: 'most-leads', label: '리드 많은순' },
|
|
{ value: 'most-variants', label: '변형 많은순' }
|
|
] as const
|
|
|
|
const statusLabel = (status: ProjectStatus) => {
|
|
if (status === 'active') {
|
|
return '진행중'
|
|
}
|
|
|
|
if (status === 'archived') {
|
|
return '종료'
|
|
}
|
|
|
|
return '임시저장'
|
|
}
|
|
|
|
const statusClass = (status: ProjectStatus) => {
|
|
if (status === 'active') {
|
|
return 'text-emerald-200 bg-emerald-400/10'
|
|
}
|
|
|
|
if (status === 'archived') {
|
|
return 'text-slate-300 bg-slate-500/10'
|
|
}
|
|
|
|
return 'text-amber-200 bg-amber-400/10'
|
|
}
|
|
|
|
const statusDotClass = (status: ProjectStatus) => {
|
|
if (status === 'active') {
|
|
return 'bg-emerald-300'
|
|
}
|
|
|
|
if (status === 'archived') {
|
|
return 'bg-slate-400'
|
|
}
|
|
|
|
return 'bg-amber-300'
|
|
}
|
|
|
|
const cardAccent = (status: ProjectStatus) => {
|
|
if (status === 'active') {
|
|
return 'before:bg-emerald-400'
|
|
}
|
|
|
|
if (status === 'archived') {
|
|
return 'before:bg-slate-400'
|
|
}
|
|
|
|
return 'before:bg-amber-300'
|
|
}
|
|
|
|
const getVariantPageId = (projectId: string) => {
|
|
return pagesStore.pages.find((page) => page.campaignId === projectId)?.id ?? ''
|
|
}
|
|
|
|
const openCreateModal = () => {
|
|
showCreateProject.value = true
|
|
}
|
|
|
|
const closeCreateModal = () => {
|
|
showCreateProject.value = false
|
|
}
|
|
|
|
const submitCreateProject = (payload: { name: string; campaignName: string }) => {
|
|
projectsStore.createProject(payload)
|
|
showCreateProject.value = false
|
|
}
|
|
</script>
|
|
|
|
<template>
|
|
<div class="space-y-5">
|
|
<header class="rounded-xl border border-slate-700/80 bg-gradient-to-r from-slate-900 via-slate-900 to-indigo-950/70 px-5 py-4 shadow-[0_16px_40px_rgba(0,0,0,0.35)]">
|
|
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
<div>
|
|
<p class="text-sm text-slate-400">홈 <span class="px-2">></span> 프로젝트</p>
|
|
<h1 class="mt-1 text-3xl font-black tracking-tight text-slate-100">프로젝트 그룹</h1>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
@click="openCreateModal"
|
|
class="rounded-xl bg-gradient-to-r from-indigo-500 to-cyan-500 px-5 py-2.5 text-base font-semibold text-white shadow-lg shadow-cyan-950/40 transition hover:from-indigo-400 hover:to-cyan-400"
|
|
>
|
|
+ 새 프로젝트 만들기
|
|
</button>
|
|
</div>
|
|
</header>
|
|
|
|
<section class="rounded-2xl border border-indigo-400/20 bg-gradient-to-br from-slate-900/80 via-slate-950 to-slate-900/90 p-5 shadow-[0_12px_30px_rgba(0,0,0,0.3)]">
|
|
<div class="flex flex-nowrap items-end justify-between gap-4">
|
|
<label class="min-w-0 space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">검색</span>
|
|
<div class="relative">
|
|
<span class="pointer-events-none absolute inset-y-0 left-3 flex items-center text-cyan-300">⌕</span>
|
|
<input
|
|
type="text"
|
|
placeholder="이름 또는 태그로 검색"
|
|
v-model="searchQuery"
|
|
class="w-full min-w-[520px] rounded-xl border border-indigo-500/20 bg-slate-950/80 px-9 py-3 text-sm text-slate-100 outline-none ring-0 transition focus:border-cyan-400 focus:shadow-[0_0_0_1px_rgba(103,232,249,0.35)]"
|
|
/>
|
|
</div>
|
|
</label>
|
|
<div class="ml-auto flex shrink-0 items-end gap-3">
|
|
<label class="flex flex-col space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">상태</span>
|
|
<select
|
|
v-model="statusFilter"
|
|
class="shrink-0 rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400"
|
|
>
|
|
<option
|
|
v-for="status in statusOptions"
|
|
:key="status.value"
|
|
:value="status.value"
|
|
>
|
|
{{ status.label }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<label class="flex flex-col space-y-2">
|
|
<span class="px-1 text-xs font-semibold tracking-[0.12em] text-cyan-200">정렬</span>
|
|
<select
|
|
v-model="sortBy"
|
|
class="shrink-0 rounded-xl border border-indigo-500/25 bg-slate-950/80 px-5 py-3 text-sm font-medium text-slate-200 outline-none transition hover:border-cyan-400"
|
|
>
|
|
<option v-for="option in sortOptions" :key="option.value" :value="option.value">
|
|
{{ option.label }}
|
|
</option>
|
|
</select>
|
|
</label>
|
|
<button
|
|
type="button"
|
|
class="shrink-0 rounded-xl border border-cyan-300/30 bg-gradient-to-r from-cyan-500/20 via-indigo-500/20 to-purple-500/20 px-5 py-3 text-sm font-semibold whitespace-nowrap text-cyan-50 transition hover:bg-slate-800 hover:border-cyan-200/50"
|
|
>
|
|
필터
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</section>
|
|
|
|
<section class="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
|
|
<article
|
|
v-for="project in projects"
|
|
:key="project.id"
|
|
class="relative overflow-hidden rounded-2xl border border-slate-800 bg-gradient-to-b from-slate-900/95 to-slate-950/95 p-5 shadow-lg shadow-black/20 transition hover:-translate-y-0.5 hover:border-cyan-300/40 before:absolute before:bottom-0 before:left-0 before:top-0 before:w-[3px] before:rounded-l-2xl"
|
|
:class="cardAccent(project.status)"
|
|
>
|
|
<div class="flex items-start justify-between gap-2">
|
|
<div>
|
|
<h2 class="text-xl font-bold text-slate-100">{{ project.title }}</h2>
|
|
<p class="mt-1 text-sm text-slate-400">{{ project.subtitle }}</p>
|
|
</div>
|
|
<span class="inline-flex items-center gap-1 rounded-full px-2 py-0.5 font-semibold tracking-[0.08em]" :class="statusClass(project.status)" style="font-size: 10px; line-height: 1.1;">
|
|
<span class="h-1.5 w-1.5 rounded-full" :class="statusDotClass(project.status)" />
|
|
{{ statusLabel(project.status) }}
|
|
</span>
|
|
</div>
|
|
|
|
<div class="mt-4 gap-3" style="display:grid;grid-template-columns:1fr 1fr;">
|
|
<div class="min-w-0 rounded-xl border border-slate-800 bg-slate-950/80 p-3">
|
|
<p class="text-xs tracking-wider text-slate-400">총 리드</p>
|
|
<p class="mt-1 text-3xl font-black text-slate-100">{{ project.leads }}</p>
|
|
</div>
|
|
<div class="min-w-0 rounded-xl border border-slate-800 bg-slate-950/80 p-3">
|
|
<p class="text-xs tracking-wider text-slate-400">{{ project.metricLabel }}</p>
|
|
<p class="mt-1 text-3xl font-black text-slate-100">{{ project.metricValue }}</p>
|
|
<p class="mt-1 text-sm" :class="project.trendUp ? 'text-emerald-300' : 'text-slate-400'">{{ project.trend }}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between border-t border-slate-700/60 pt-4"
|
|
style="margin-top: 28px;"
|
|
>
|
|
<p class="text-sm text-slate-400">{{ project.variants }}개 변형 테스트</p>
|
|
<div class="flex items-center gap-2">
|
|
<NuxtLink
|
|
:to="`/admin/pages?campaignId=${project.id}`"
|
|
class="rounded-md border border-slate-700/80 bg-slate-900 px-3 py-1.5 text-sm text-slate-200 transition hover:bg-slate-800/70"
|
|
>
|
|
페이지 관리
|
|
</NuxtLink>
|
|
<NuxtLink
|
|
v-if="getVariantPageId(project.id)"
|
|
:to="`/admin/pages/${getVariantPageId(project.id)}/variants`"
|
|
class="rounded-md bg-gradient-to-r from-indigo-600 to-cyan-500 px-3 py-1.5 text-sm font-medium text-white hover:from-indigo-500 hover:to-cyan-400"
|
|
>
|
|
변형 관리
|
|
</NuxtLink>
|
|
<button
|
|
v-else
|
|
type="button"
|
|
class="rounded-md bg-slate-700 px-3 py-1.5 text-sm text-slate-300"
|
|
disabled
|
|
>
|
|
변형 없음
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</article>
|
|
|
|
<article
|
|
class="flex min-h-[320px] items-center justify-center rounded-2xl border border-dashed border-cyan-300/30 bg-gradient-to-b from-slate-900 to-indigo-950/60 p-5"
|
|
>
|
|
<div class="text-center">
|
|
<div class="mx-auto mb-4 flex h-12 w-12 items-center justify-center rounded-full bg-gradient-to-r from-indigo-400/20 to-cyan-400/20 text-2xl text-cyan-200">+</div>
|
|
<p class="text-2xl font-bold text-slate-100">새 그룹 만들기</p>
|
|
<p class="mt-2 text-slate-400">새 A/B 테스트나 캠페인을 시작하세요</p>
|
|
<button
|
|
type="button"
|
|
class="mx-auto mt-4 rounded-md bg-gradient-to-r from-indigo-600 to-cyan-500 px-4 py-2 text-sm font-semibold text-white"
|
|
@click="openCreateModal"
|
|
>
|
|
바로 생성하기
|
|
</button>
|
|
</div>
|
|
</article>
|
|
</section>
|
|
|
|
<footer class="mt-4 flex flex-wrap items-center justify-between gap-3 border-t border-slate-800 pt-4">
|
|
<p class="text-slate-400">총 12개 중 1~4개 표시</p>
|
|
<div class="flex items-center gap-2">
|
|
<button class="rounded-lg border border-slate-700 px-3 py-1.5 text-slate-300 hover:bg-slate-900">이전</button>
|
|
<button class="rounded-lg border border-indigo-300/30 bg-indigo-900/50 px-3 py-1.5 text-cyan-100">1</button>
|
|
<button class="rounded-lg border border-slate-700 px-3 py-1.5 text-slate-300 hover:bg-slate-900">2</button>
|
|
<button class="rounded-lg border border-slate-700 px-3 py-1.5 text-slate-300 hover:bg-slate-900">3</button>
|
|
<button class="rounded-lg border border-slate-700 px-3 py-1.5 text-slate-300 hover:bg-slate-900">다음</button>
|
|
</div>
|
|
</footer>
|
|
|
|
<CreateProjectModal
|
|
v-if="showCreateProject"
|
|
@close="closeCreateModal"
|
|
@submit="submitCreateProject"
|
|
/>
|
|
</div>
|
|
</template>
|