macbook 에서 리눅스로 이동
This commit is contained in:
69
frontend/app/stores/dashboard.ts
Normal file
69
frontend/app/stores/dashboard.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface KpiCard {
|
||||
label: string
|
||||
value: string
|
||||
note: string
|
||||
color: string
|
||||
band: string
|
||||
border: string
|
||||
}
|
||||
|
||||
export interface RunningProject {
|
||||
name: string
|
||||
domain: string
|
||||
status: string
|
||||
leads: number
|
||||
}
|
||||
|
||||
export const useDashboardStore = defineStore('admin-dashboard', {
|
||||
state: () => ({
|
||||
kpiCards: [
|
||||
{
|
||||
label: '진행중 프로젝트',
|
||||
value: '5',
|
||||
note: '최근 7일 내 변경 3건',
|
||||
color: 'text-cyan-300',
|
||||
band: 'from-cyan-300/80 to-cyan-200/20',
|
||||
border: 'border-cyan-300/25'
|
||||
},
|
||||
{
|
||||
label: '전체 프로젝트',
|
||||
value: '18',
|
||||
note: '캠페인 기준 총 18개',
|
||||
color: 'text-fuchsia-200',
|
||||
band: 'from-fuchsia-300/80 to-fuchsia-200/20',
|
||||
border: 'border-fuchsia-300/25'
|
||||
},
|
||||
{
|
||||
label: '새로운 리드',
|
||||
value: '24',
|
||||
note: '오늘 신규 유입',
|
||||
color: 'text-emerald-300',
|
||||
band: 'from-emerald-300/80 to-emerald-200/20',
|
||||
border: 'border-emerald-300/25'
|
||||
}
|
||||
],
|
||||
trendBars: [12, 20, 16, 26, 18, 24],
|
||||
runningProjects: [
|
||||
{
|
||||
name: '여름 프로모션',
|
||||
domain: 'summer.ad-camp.kr',
|
||||
status: '운영중',
|
||||
leads: 42
|
||||
},
|
||||
{
|
||||
name: '주말 특가 랜딩',
|
||||
domain: 'weekend.offer.kr',
|
||||
status: '테스트',
|
||||
leads: 13
|
||||
},
|
||||
{
|
||||
name: '리타겟팅 A',
|
||||
domain: 'retarget.example.com',
|
||||
status: '준비중',
|
||||
leads: 0
|
||||
}
|
||||
] as RunningProject[]
|
||||
})
|
||||
})
|
||||
242
frontend/app/stores/pages.ts
Normal file
242
frontend/app/stores/pages.ts
Normal file
@@ -0,0 +1,242 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type PageStatus = 'live' | 'draft' | 'paused'
|
||||
|
||||
export type PageSortBy =
|
||||
| 'updated'
|
||||
| 'name'
|
||||
| 'leads'
|
||||
| 'visitors'
|
||||
| 'variants'
|
||||
|
||||
export interface CampaignLabel {
|
||||
id: string
|
||||
name: string
|
||||
}
|
||||
|
||||
export interface AdminPage {
|
||||
id: string
|
||||
campaignId: string
|
||||
name: string
|
||||
domain: string
|
||||
routePath: string
|
||||
status: PageStatus
|
||||
leadCount: number
|
||||
visitorCount: number
|
||||
variantCount: number
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
const campaigns: CampaignLabel[] = [
|
||||
{ id: 'p1', name: 'Q3 마케팅 캠페인' },
|
||||
{ id: 'p2', name: '블랙프라이데이 2023' },
|
||||
{ id: 'p3', name: 'SaaS 웨비나 시리즈' },
|
||||
{ id: 'p4', name: 'Product Hunt 론칭' }
|
||||
]
|
||||
|
||||
const pageSource: AdminPage[] = [
|
||||
{
|
||||
id: 'lp01',
|
||||
campaignId: 'p1',
|
||||
name: '메인 랜딩',
|
||||
domain: 'summer.ad-camp.kr',
|
||||
routePath: '/',
|
||||
status: 'live',
|
||||
leadCount: 124,
|
||||
visitorCount: 2400,
|
||||
variantCount: 2,
|
||||
updatedAt: '2026-03-04T09:12:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'lp02',
|
||||
campaignId: 'p1',
|
||||
name: '오퍼 페이지 B',
|
||||
domain: 'summer.ad-camp.kr',
|
||||
routePath: '/offer',
|
||||
status: 'draft',
|
||||
leadCount: 16,
|
||||
visitorCount: 420,
|
||||
variantCount: 1,
|
||||
updatedAt: '2026-03-03T18:05:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'lp03',
|
||||
campaignId: 'p2',
|
||||
name: '구글 전용 랜딩',
|
||||
domain: 'weekend.offer.kr',
|
||||
routePath: '/google',
|
||||
status: 'live',
|
||||
leadCount: 88,
|
||||
visitorCount: 1210,
|
||||
variantCount: 3,
|
||||
updatedAt: '2026-03-01T13:40:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'lp04',
|
||||
campaignId: 'p2',
|
||||
name: '일반 버전',
|
||||
domain: 'weekend.offer.kr',
|
||||
routePath: '/',
|
||||
status: 'paused',
|
||||
leadCount: 22,
|
||||
visitorCount: 540,
|
||||
variantCount: 1,
|
||||
updatedAt: '2026-02-28T06:25:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'lp05',
|
||||
campaignId: 'p4',
|
||||
name: '리타겟 랜딩',
|
||||
domain: 'retarget.example.com',
|
||||
routePath: '/',
|
||||
status: 'draft',
|
||||
leadCount: 0,
|
||||
visitorCount: 40,
|
||||
variantCount: 1,
|
||||
updatedAt: '2026-03-02T22:17:00.000Z'
|
||||
}
|
||||
]
|
||||
|
||||
const statusRank: Record<PageStatus, number> = {
|
||||
live: 0,
|
||||
draft: 1,
|
||||
paused: 2
|
||||
}
|
||||
|
||||
const statusLabel = (status: PageStatus) => {
|
||||
if (status === 'live') {
|
||||
return '라이브'
|
||||
}
|
||||
|
||||
if (status === 'paused') {
|
||||
return '일시정지'
|
||||
}
|
||||
|
||||
return '초안'
|
||||
}
|
||||
|
||||
export const usePagesStore = defineStore('admin-pages', {
|
||||
state: () => ({
|
||||
pages: pageSource,
|
||||
campaigns,
|
||||
searchQuery: '',
|
||||
campaignFilter: 'all' as 'all' | string,
|
||||
statusFilter: 'all' as 'all' | PageStatus,
|
||||
sortBy: 'updated' as PageSortBy
|
||||
}),
|
||||
getters: {
|
||||
filteredPages: (state): AdminPage[] => {
|
||||
const query = state.searchQuery.trim().toLowerCase()
|
||||
|
||||
const result = state.pages
|
||||
.filter((page) => {
|
||||
const text = `${page.name} ${page.domain} ${page.routePath}`.toLowerCase()
|
||||
const searchOk = query === '' || text.includes(query)
|
||||
const campaignOk = state.campaignFilter === 'all' || page.campaignId === state.campaignFilter
|
||||
const statusOk = state.statusFilter === 'all' || page.status === state.statusFilter
|
||||
|
||||
return searchOk && campaignOk && statusOk
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (state.sortBy === 'name') {
|
||||
return a.name.localeCompare(b.name)
|
||||
}
|
||||
if (state.sortBy === 'leads') {
|
||||
return b.leadCount - a.leadCount
|
||||
}
|
||||
if (state.sortBy === 'visitors') {
|
||||
return b.visitorCount - a.visitorCount
|
||||
}
|
||||
if (state.sortBy === 'variants') {
|
||||
return b.variantCount - a.variantCount
|
||||
}
|
||||
|
||||
return new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||
})
|
||||
|
||||
return result
|
||||
},
|
||||
campaignOptions: (state): CampaignLabel[] => state.campaigns
|
||||
},
|
||||
actions: {
|
||||
setSearchQuery(value: string) {
|
||||
this.searchQuery = value
|
||||
},
|
||||
setCampaignFilter(value: 'all' | string) {
|
||||
this.campaignFilter = value
|
||||
},
|
||||
setStatusFilter(value: 'all' | PageStatus) {
|
||||
this.statusFilter = value
|
||||
},
|
||||
setSortBy(value: PageSortBy) {
|
||||
this.sortBy = value
|
||||
},
|
||||
removePage(pageId: string) {
|
||||
this.pages = this.pages.filter((page) => page.id !== pageId)
|
||||
},
|
||||
createQuickPage() {
|
||||
const campaign = this.campaigns[this.pages.length % this.campaigns.length]
|
||||
this.createPage({
|
||||
name: `새 페이지 ${this.pages.length + 1}`,
|
||||
campaignId: campaign.id,
|
||||
domain: 'ad-camp-temp.test',
|
||||
routePath: `/lp-${this.pages.length + 1}`
|
||||
})
|
||||
},
|
||||
createPage(payload: {
|
||||
name: string
|
||||
campaignId: string
|
||||
domain: string
|
||||
routePath: string
|
||||
}) {
|
||||
const normalizedRoute = payload.routePath.trim().startsWith('/') ? payload.routePath.trim() : `/${payload.routePath.trim()}`
|
||||
const normalizedDomain = payload.domain.trim()
|
||||
|
||||
const newPage: AdminPage = {
|
||||
id: `lp-${Date.now()}`,
|
||||
campaignId: payload.campaignId,
|
||||
name: payload.name.trim(),
|
||||
domain: normalizedDomain,
|
||||
routePath: normalizedRoute,
|
||||
status: 'draft',
|
||||
leadCount: 0,
|
||||
visitorCount: 0,
|
||||
variantCount: 1,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
this.pages.unshift(newPage)
|
||||
},
|
||||
statusClass(status: PageStatus) {
|
||||
if (status === 'live') {
|
||||
return {
|
||||
label: statusLabel(status),
|
||||
chipClass: 'text-emerald-200 bg-emerald-400/10',
|
||||
dotClass: 'bg-emerald-300'
|
||||
}
|
||||
}
|
||||
|
||||
if (status === 'paused') {
|
||||
return {
|
||||
label: statusLabel(status),
|
||||
chipClass: 'text-slate-300 bg-slate-500/10',
|
||||
dotClass: 'bg-slate-400'
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
label: statusLabel(status),
|
||||
chipClass: 'text-amber-200 bg-amber-400/10',
|
||||
dotClass: 'bg-amber-300'
|
||||
}
|
||||
},
|
||||
campaignName(campaignId: string) {
|
||||
return (
|
||||
this.campaigns.find((campaign) => campaign.id === campaignId)?.name || campaignId || '미지정'
|
||||
)
|
||||
},
|
||||
previewUrl(page: AdminPage) {
|
||||
return `https://${page.domain}${page.routePath}`
|
||||
}
|
||||
}
|
||||
})
|
||||
156
frontend/app/stores/projects.ts
Normal file
156
frontend/app/stores/projects.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export type ProjectStatus = 'active' | 'archived' | 'draft'
|
||||
|
||||
export type ProjectSortBy = 'newest' | 'oldest' | 'most-leads' | 'most-variants'
|
||||
|
||||
export type ProjectStatusFilter = 'all' | ProjectStatus
|
||||
|
||||
export interface ProjectCard {
|
||||
id: string
|
||||
title: string
|
||||
subtitle: string
|
||||
status: ProjectStatus
|
||||
leads: string
|
||||
metricLabel: string
|
||||
metricValue: string
|
||||
trend: string
|
||||
trendUp: boolean
|
||||
variants: number
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
const projectSource: ProjectCard[] = [
|
||||
{
|
||||
id: 'p1',
|
||||
title: 'Q3 마케팅 캠페인',
|
||||
subtitle: '2일 전 생성',
|
||||
status: 'active',
|
||||
leads: '1,240',
|
||||
metricLabel: '전환율',
|
||||
metricValue: '4.8%',
|
||||
trend: '+12%',
|
||||
trendUp: true,
|
||||
variants: 3,
|
||||
createdAt: '2026-03-01T09:00:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'p2',
|
||||
title: '블랙프라이데이 2023',
|
||||
subtitle: '2023년 12월 1일 종료',
|
||||
status: 'archived',
|
||||
leads: '5,100',
|
||||
metricLabel: '방문자',
|
||||
metricValue: '42.5k',
|
||||
trend: '종료',
|
||||
trendUp: false,
|
||||
variants: 3,
|
||||
createdAt: '2023-12-01T03:00:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'p3',
|
||||
title: 'SaaS 웨비나 시리즈',
|
||||
subtitle: '4시간 전 수정됨',
|
||||
status: 'draft',
|
||||
leads: '0',
|
||||
metricLabel: '노출 수',
|
||||
metricValue: '12k',
|
||||
trend: '+18%',
|
||||
trendUp: true,
|
||||
variants: 2,
|
||||
createdAt: '2026-03-04T01:20:00.000Z'
|
||||
},
|
||||
{
|
||||
id: 'p4',
|
||||
title: 'Product Hunt 런칭',
|
||||
subtitle: '2주간 운영중',
|
||||
status: 'active',
|
||||
leads: '892',
|
||||
metricLabel: '클릭률',
|
||||
metricValue: '2.1%',
|
||||
trend: '-2%',
|
||||
trendUp: false,
|
||||
variants: 1,
|
||||
createdAt: '2026-02-12T16:30:00.000Z'
|
||||
}
|
||||
]
|
||||
|
||||
const toLeadNumber = (value: string) => {
|
||||
const trimmed = value.trim().toLowerCase().replace(/,/g, '')
|
||||
const numeric = Number(trimmed.replace('k', ''))
|
||||
|
||||
if (Number.isNaN(numeric)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return trimmed.endsWith('k') ? numeric * 1000 : numeric
|
||||
}
|
||||
|
||||
export const useProjectsStore = defineStore('admin-projects', {
|
||||
state: () => ({
|
||||
projects: projectSource,
|
||||
searchQuery: '',
|
||||
statusFilter: 'all' as ProjectStatusFilter,
|
||||
sortBy: 'newest' as ProjectSortBy
|
||||
}),
|
||||
getters: {
|
||||
filteredProjects: (state): ProjectCard[] => {
|
||||
const query = state.searchQuery.trim().toLowerCase()
|
||||
const list = state.projects
|
||||
.filter((project) => {
|
||||
const target = `${project.title} ${project.subtitle}`.toLowerCase()
|
||||
const matchQuery = query === '' || target.includes(query)
|
||||
const matchStatus =
|
||||
state.statusFilter === 'all' || project.status === state.statusFilter
|
||||
|
||||
return matchQuery && matchStatus
|
||||
})
|
||||
.sort((a, b) => {
|
||||
if (state.sortBy === 'oldest') {
|
||||
return new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
|
||||
}
|
||||
|
||||
if (state.sortBy === 'most-leads') {
|
||||
return toLeadNumber(b.leads) - toLeadNumber(a.leads)
|
||||
}
|
||||
|
||||
if (state.sortBy === 'most-variants') {
|
||||
return b.variants - a.variants
|
||||
}
|
||||
|
||||
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
|
||||
})
|
||||
|
||||
return list
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
createProject(input: { name: string; campaignName: string }) {
|
||||
const now = new Date().toISOString()
|
||||
const id = `p-${Date.now().toString(36)}`
|
||||
|
||||
this.projects.unshift({
|
||||
id,
|
||||
title: input.name.trim(),
|
||||
subtitle: `${input.campaignName.trim()}에서 생성`,
|
||||
status: 'draft',
|
||||
leads: '0',
|
||||
metricLabel: '전환율',
|
||||
metricValue: '-',
|
||||
trend: '신규',
|
||||
trendUp: true,
|
||||
variants: 0,
|
||||
createdAt: now
|
||||
})
|
||||
},
|
||||
setSearchQuery(value: string) {
|
||||
this.searchQuery = value
|
||||
},
|
||||
setStatusFilter(value: ProjectStatusFilter) {
|
||||
this.statusFilter = value
|
||||
},
|
||||
setSortBy(value: ProjectSortBy) {
|
||||
this.sortBy = value
|
||||
}
|
||||
}
|
||||
})
|
||||
238
frontend/app/stores/variants.ts
Normal file
238
frontend/app/stores/variants.ts
Normal file
@@ -0,0 +1,238 @@
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
export interface PageVariant {
|
||||
id: string
|
||||
pageId: string
|
||||
name: string
|
||||
description: string
|
||||
trafficWeight: number
|
||||
isActive: boolean
|
||||
leadCount: number
|
||||
updatedAt: string
|
||||
}
|
||||
|
||||
export interface CreateVariantInput {
|
||||
pageId: string
|
||||
name: string
|
||||
description: string
|
||||
trafficWeight: number
|
||||
}
|
||||
|
||||
export interface UpdateVariantInput {
|
||||
id: string
|
||||
pageId: string
|
||||
name?: string
|
||||
description?: string
|
||||
trafficWeight?: number
|
||||
isActive?: boolean
|
||||
leadCount?: number
|
||||
}
|
||||
|
||||
type VariantMap = Record<string, PageVariant[]>
|
||||
|
||||
const toVariantIso = (value = 0) => {
|
||||
const date = new Date()
|
||||
date.setSeconds(date.getSeconds() + value)
|
||||
|
||||
return date.toISOString()
|
||||
}
|
||||
|
||||
const variantSeed: VariantMap = {
|
||||
lp01: [
|
||||
{
|
||||
id: 'v-lp01-a',
|
||||
pageId: 'lp01',
|
||||
name: '일반형',
|
||||
description: '현재 운영 중인 기본 랜딩 버전',
|
||||
trafficWeight: 70,
|
||||
isActive: true,
|
||||
leadCount: 76,
|
||||
updatedAt: toVariantIso(-1200)
|
||||
},
|
||||
{
|
||||
id: 'v-lp01-b',
|
||||
pageId: 'lp01',
|
||||
name: '강한 메시지형',
|
||||
description: 'CTA 문구를 자극적으로 강화한 변형',
|
||||
trafficWeight: 30,
|
||||
isActive: true,
|
||||
leadCount: 48,
|
||||
updatedAt: toVariantIso(-900)
|
||||
}
|
||||
],
|
||||
lp02: [
|
||||
{
|
||||
id: 'v-lp02-a',
|
||||
pageId: 'lp02',
|
||||
name: '기본 오퍼',
|
||||
description: '초안 상태 테스트용 템플릿',
|
||||
trafficWeight: 100,
|
||||
isActive: true,
|
||||
leadCount: 16,
|
||||
updatedAt: toVariantIso(-2000)
|
||||
}
|
||||
],
|
||||
lp03: [
|
||||
{
|
||||
id: 'v-lp03-a',
|
||||
pageId: 'lp03',
|
||||
name: '구글 버전 A',
|
||||
description: '광고 채널별로 분기되는 기본 버전',
|
||||
trafficWeight: 50,
|
||||
isActive: true,
|
||||
leadCount: 58,
|
||||
updatedAt: toVariantIso(-1300)
|
||||
},
|
||||
{
|
||||
id: 'v-lp03-b',
|
||||
pageId: 'lp03',
|
||||
name: '구글 버전 B',
|
||||
description: '강조 문구/이미지를 변경한 실험군',
|
||||
trafficWeight: 30,
|
||||
isActive: true,
|
||||
leadCount: 22,
|
||||
updatedAt: toVariantIso(-800)
|
||||
},
|
||||
{
|
||||
id: 'v-lp03-c',
|
||||
pageId: 'lp03',
|
||||
name: '구글 버전 C',
|
||||
description: '폼 위치를 상단으로 이동한 변형',
|
||||
trafficWeight: 20,
|
||||
isActive: true,
|
||||
leadCount: 8,
|
||||
updatedAt: toVariantIso(-500)
|
||||
}
|
||||
],
|
||||
lp04: [
|
||||
{
|
||||
id: 'v-lp04-a',
|
||||
pageId: 'lp04',
|
||||
name: '저녁 타겟형',
|
||||
description: '기본 버전',
|
||||
trafficWeight: 100,
|
||||
isActive: false,
|
||||
leadCount: 22,
|
||||
updatedAt: toVariantIso(-700)
|
||||
}
|
||||
],
|
||||
lp05: [
|
||||
{
|
||||
id: 'v-lp05-a',
|
||||
pageId: 'lp05',
|
||||
name: '리타겟 기본형',
|
||||
description: '리마케팅 유입용 기본 템플릿',
|
||||
trafficWeight: 100,
|
||||
isActive: true,
|
||||
leadCount: 0,
|
||||
updatedAt: toVariantIso(-1000)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
const clampTrafficWeight = (value: number) => {
|
||||
if (!Number.isFinite(value)) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (value < 1) {
|
||||
return 1
|
||||
}
|
||||
|
||||
if (value > 100) {
|
||||
return 100
|
||||
}
|
||||
|
||||
return Math.round(value)
|
||||
}
|
||||
|
||||
export const useVariantsStore = defineStore('admin-variants', {
|
||||
state: () => ({
|
||||
variantsByPage: variantSeed as VariantMap
|
||||
}),
|
||||
getters: {
|
||||
listByPage: (state) => (pageId: string) => {
|
||||
const list = state.variantsByPage[pageId] ?? []
|
||||
return [...list].sort((a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime())
|
||||
},
|
||||
countByPage: (state) => (pageId: string) => {
|
||||
return state.variantsByPage[pageId]?.length ?? 0
|
||||
},
|
||||
totalLeadsByPage: (state) => (pageId: string) => {
|
||||
return (state.variantsByPage[pageId] ?? []).reduce((sum, item) => sum + item.leadCount, 0)
|
||||
}
|
||||
},
|
||||
actions: {
|
||||
createVariant(payload: CreateVariantInput) {
|
||||
const normalizedName = payload.name.trim()
|
||||
const pageId = payload.pageId.trim()
|
||||
const description = payload.description.trim()
|
||||
|
||||
if (pageId.length === 0 || normalizedName.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
const pageVariants = this.variantsByPage[pageId] ?? []
|
||||
const newVariant: PageVariant = {
|
||||
id: `v-${Date.now().toString(36)}`,
|
||||
pageId,
|
||||
name: normalizedName,
|
||||
description,
|
||||
trafficWeight: clampTrafficWeight(payload.trafficWeight),
|
||||
isActive: true,
|
||||
leadCount: 0,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
|
||||
this.variantsByPage[pageId] = [newVariant, ...pageVariants]
|
||||
},
|
||||
updateVariant(payload: UpdateVariantInput) {
|
||||
const pageId = payload.pageId.trim()
|
||||
const variants = this.variantsByPage[pageId]
|
||||
|
||||
if (!variants) {
|
||||
return
|
||||
}
|
||||
|
||||
const idx = variants.findIndex((item) => item.id === payload.id)
|
||||
if (idx === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
const target = variants[idx]
|
||||
variants[idx] = {
|
||||
...target,
|
||||
...payload,
|
||||
updatedAt: new Date().toISOString(),
|
||||
pageId
|
||||
}
|
||||
},
|
||||
toggleActive(pageId: string, variantId: string) {
|
||||
const variants = this.variantsByPage[pageId]
|
||||
|
||||
if (!variants) {
|
||||
return
|
||||
}
|
||||
|
||||
const idx = variants.findIndex((item) => item.id === variantId)
|
||||
if (idx === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
variants[idx] = {
|
||||
...variants[idx],
|
||||
isActive: !variants[idx].isActive,
|
||||
updatedAt: new Date().toISOString()
|
||||
}
|
||||
},
|
||||
removeVariant(pageId: string, variantId: string) {
|
||||
const variants = this.variantsByPage[pageId]
|
||||
|
||||
if (!variants) {
|
||||
return
|
||||
}
|
||||
|
||||
this.variantsByPage[pageId] = variants.filter((variant) => variant.id !== variantId)
|
||||
}
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user