Implement portfolio admin management
This commit is contained in:
@@ -0,0 +1,112 @@
|
||||
CREATE TYPE "PortfolioSectionType" AS ENUM (
|
||||
'RICH_TEXT',
|
||||
'GALLERY',
|
||||
'STATS',
|
||||
'DELIVERABLES',
|
||||
'LINK'
|
||||
);
|
||||
|
||||
CREATE TYPE "PortfolioAssetKind" AS ENUM (
|
||||
'IMAGE',
|
||||
'DOCUMENT'
|
||||
);
|
||||
|
||||
CREATE TABLE "Category" (
|
||||
"id" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"nameAr" TEXT NOT NULL,
|
||||
"nameEn" TEXT NOT NULL,
|
||||
"nameDe" TEXT NOT NULL,
|
||||
"descriptionAr" TEXT NOT NULL,
|
||||
"descriptionEn" TEXT NOT NULL,
|
||||
"descriptionDe" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"isActive" BOOLEAN NOT NULL DEFAULT true,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "Category_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "PortfolioProject" (
|
||||
"id" TEXT NOT NULL,
|
||||
"categoryId" TEXT NOT NULL,
|
||||
"slug" TEXT NOT NULL,
|
||||
"titleAr" TEXT NOT NULL,
|
||||
"titleEn" TEXT NOT NULL,
|
||||
"titleDe" TEXT NOT NULL,
|
||||
"summaryAr" TEXT NOT NULL,
|
||||
"summaryEn" TEXT NOT NULL,
|
||||
"summaryDe" TEXT NOT NULL,
|
||||
"clientName" TEXT NOT NULL,
|
||||
"projectYear" INTEGER NOT NULL,
|
||||
"serviceLabelAr" TEXT NOT NULL,
|
||||
"serviceLabelEn" TEXT NOT NULL,
|
||||
"serviceLabelDe" TEXT NOT NULL,
|
||||
"previewUrl" TEXT,
|
||||
"coverImagePath" TEXT,
|
||||
"isFeatured" BOOLEAN NOT NULL DEFAULT false,
|
||||
"isPublished" BOOLEAN NOT NULL DEFAULT false,
|
||||
"publishedAt" TIMESTAMP(3),
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PortfolioProject_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "PortfolioSection" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"type" "PortfolioSectionType" NOT NULL,
|
||||
"titleAr" TEXT NOT NULL,
|
||||
"titleEn" TEXT NOT NULL,
|
||||
"titleDe" TEXT NOT NULL,
|
||||
"bodyAr" TEXT NOT NULL,
|
||||
"bodyEn" TEXT NOT NULL,
|
||||
"bodyDe" TEXT NOT NULL,
|
||||
"imagePath" TEXT,
|
||||
"linkUrl" TEXT,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PortfolioSection_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE TABLE "PortfolioAsset" (
|
||||
"id" TEXT NOT NULL,
|
||||
"projectId" TEXT NOT NULL,
|
||||
"kind" "PortfolioAssetKind" NOT NULL,
|
||||
"filePath" TEXT NOT NULL,
|
||||
"altAr" TEXT NOT NULL,
|
||||
"altEn" TEXT NOT NULL,
|
||||
"altDe" TEXT NOT NULL,
|
||||
"sortOrder" INTEGER NOT NULL DEFAULT 0,
|
||||
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updatedAt" TIMESTAMP(3) NOT NULL,
|
||||
|
||||
CONSTRAINT "PortfolioAsset_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX "Category_slug_key" ON "Category"("slug");
|
||||
CREATE UNIQUE INDEX "PortfolioProject_slug_key" ON "PortfolioProject"("slug");
|
||||
CREATE INDEX "PortfolioProject_categoryId_isPublished_sortOrder_idx" ON "PortfolioProject"("categoryId", "isPublished", "sortOrder");
|
||||
CREATE INDEX "PortfolioProject_isPublished_sortOrder_idx" ON "PortfolioProject"("isPublished", "sortOrder");
|
||||
CREATE INDEX "PortfolioSection_projectId_sortOrder_idx" ON "PortfolioSection"("projectId", "sortOrder");
|
||||
CREATE INDEX "PortfolioAsset_projectId_sortOrder_idx" ON "PortfolioAsset"("projectId", "sortOrder");
|
||||
|
||||
ALTER TABLE "PortfolioProject"
|
||||
ADD CONSTRAINT "PortfolioProject_categoryId_fkey"
|
||||
FOREIGN KEY ("categoryId") REFERENCES "Category"("id")
|
||||
ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "PortfolioSection"
|
||||
ADD CONSTRAINT "PortfolioSection_projectId_fkey"
|
||||
FOREIGN KEY ("projectId") REFERENCES "PortfolioProject"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
ALTER TABLE "PortfolioAsset"
|
||||
ADD CONSTRAINT "PortfolioAsset_projectId_fkey"
|
||||
FOREIGN KEY ("projectId") REFERENCES "PortfolioProject"("id")
|
||||
ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
@@ -13,3 +13,149 @@ model AppConfig {
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
enum PortfolioSectionType {
|
||||
RICH_TEXT
|
||||
GALLERY
|
||||
STATS
|
||||
DELIVERABLES
|
||||
LINK
|
||||
}
|
||||
|
||||
enum PortfolioAssetKind {
|
||||
IMAGE
|
||||
DOCUMENT
|
||||
}
|
||||
|
||||
enum MediaSource {
|
||||
UPLOAD
|
||||
EXTERNAL
|
||||
}
|
||||
|
||||
enum MediaKind {
|
||||
IMAGE
|
||||
DOCUMENT
|
||||
}
|
||||
|
||||
enum MediaUsageType {
|
||||
PORTFOLIO_COVER
|
||||
PORTFOLIO_SECTION
|
||||
PORTFOLIO_ASSET
|
||||
GENERIC
|
||||
}
|
||||
|
||||
model Category {
|
||||
id String @id @default(cuid())
|
||||
slug String @unique
|
||||
nameAr String
|
||||
nameEn String
|
||||
nameDe String
|
||||
descriptionAr String
|
||||
descriptionEn String
|
||||
descriptionDe String
|
||||
sortOrder Int @default(0)
|
||||
isActive Boolean @default(true)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
projects PortfolioProject[]
|
||||
}
|
||||
|
||||
model PortfolioProject {
|
||||
id String @id @default(cuid())
|
||||
categoryId String
|
||||
slug String @unique
|
||||
titleAr String
|
||||
titleEn String
|
||||
titleDe String
|
||||
summaryAr String
|
||||
summaryEn String
|
||||
summaryDe String
|
||||
clientName String
|
||||
projectYear Int
|
||||
serviceLabelAr String
|
||||
serviceLabelEn String
|
||||
serviceLabelDe String
|
||||
previewUrl String?
|
||||
coverImagePath String?
|
||||
isFeatured Boolean @default(false)
|
||||
isPublished Boolean @default(false)
|
||||
publishedAt DateTime?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
category Category @relation(fields: [categoryId], references: [id], onDelete: Restrict)
|
||||
sections PortfolioSection[]
|
||||
assets PortfolioAsset[]
|
||||
|
||||
@@index([categoryId, isPublished, sortOrder])
|
||||
@@index([isPublished, sortOrder])
|
||||
}
|
||||
|
||||
model PortfolioSection {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
type PortfolioSectionType
|
||||
titleAr String
|
||||
titleEn String
|
||||
titleDe String
|
||||
bodyAr String
|
||||
bodyEn String
|
||||
bodyDe String
|
||||
imagePath String?
|
||||
linkUrl String?
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
project PortfolioProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([projectId, sortOrder])
|
||||
}
|
||||
|
||||
model PortfolioAsset {
|
||||
id String @id @default(cuid())
|
||||
projectId String
|
||||
kind PortfolioAssetKind
|
||||
filePath String
|
||||
altAr String
|
||||
altEn String
|
||||
altDe String
|
||||
sortOrder Int @default(0)
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
project PortfolioProject @relation(fields: [projectId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([projectId, sortOrder])
|
||||
}
|
||||
|
||||
model MediaAsset {
|
||||
id String @id @default(cuid())
|
||||
source MediaSource
|
||||
kind MediaKind
|
||||
url String
|
||||
fileName String
|
||||
label String
|
||||
altText String?
|
||||
mimeType String?
|
||||
size Int?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
usages MediaUsage[]
|
||||
|
||||
@@index([kind, createdAt])
|
||||
}
|
||||
|
||||
model MediaUsage {
|
||||
id String @id @default(cuid())
|
||||
assetId String
|
||||
usageType MediaUsageType
|
||||
entityType String
|
||||
entityId String
|
||||
fieldKey String
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
asset MediaAsset @relation(fields: [assetId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([usageType, entityType, entityId, fieldKey])
|
||||
@@index([assetId])
|
||||
@@index([entityType, entityId])
|
||||
}
|
||||
|
||||
+201
@@ -15,6 +15,207 @@ async function main() {
|
||||
update: { value: "moh-sass" },
|
||||
create: { key: "siteName", value: "moh-sass" },
|
||||
});
|
||||
|
||||
const brandCategory = await prisma.category.upsert({
|
||||
where: { slug: "branding" },
|
||||
update: {
|
||||
nameAr: "الهوية البصرية",
|
||||
nameEn: "Branding",
|
||||
nameDe: "Branding",
|
||||
descriptionAr: "مشاريع هوية بصرية وشعارات وأنظمة علامة.",
|
||||
descriptionEn: "Brand identity, logo, and design system work.",
|
||||
descriptionDe: "Branding, Logos und visuelle Systeme.",
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
slug: "branding",
|
||||
nameAr: "الهوية البصرية",
|
||||
nameEn: "Branding",
|
||||
nameDe: "Branding",
|
||||
descriptionAr: "مشاريع هوية بصرية وشعارات وأنظمة علامة.",
|
||||
descriptionEn: "Brand identity, logo, and design system work.",
|
||||
descriptionDe: "Branding, Logos und visuelle Systeme.",
|
||||
sortOrder: 1,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const webCategory = await prisma.category.upsert({
|
||||
where: { slug: "web-experiences" },
|
||||
update: {
|
||||
nameAr: "تجارب الويب",
|
||||
nameEn: "Web Experiences",
|
||||
nameDe: "Web Experiences",
|
||||
descriptionAr: "مواقع وصفحات هبوط وتجارب رقمية سريعة.",
|
||||
descriptionEn: "Websites, landing pages, and digital experiences.",
|
||||
descriptionDe: "Webseiten, Landingpages und digitale Erlebnisse.",
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
},
|
||||
create: {
|
||||
slug: "web-experiences",
|
||||
nameAr: "تجارب الويب",
|
||||
nameEn: "Web Experiences",
|
||||
nameDe: "Web Experiences",
|
||||
descriptionAr: "مواقع وصفحات هبوط وتجارب رقمية سريعة.",
|
||||
descriptionEn: "Websites, landing pages, and digital experiences.",
|
||||
descriptionDe: "Webseiten, Landingpages und digitale Erlebnisse.",
|
||||
sortOrder: 2,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
const project = await prisma.portfolioProject.upsert({
|
||||
where: { slug: "brand-redesign" },
|
||||
update: {
|
||||
categoryId: brandCategory.id,
|
||||
titleAr: "إعادة تصميم الهوية",
|
||||
titleEn: "Brand Redesign",
|
||||
titleDe: "Brand Redesign",
|
||||
summaryAr: "إعادة بناء لهوية رقمية مع نظام مرئي أوضح ومسارات استخدام أسرع.",
|
||||
summaryEn: "A digital brand refresh with a clearer visual system and faster journeys.",
|
||||
summaryDe: "Ein digitales Redesign mit klarerem visuellen System und schnelleren Journeys.",
|
||||
clientName: "Studio Client",
|
||||
projectYear: 2025,
|
||||
serviceLabelAr: "هوية بصرية",
|
||||
serviceLabelEn: "Brand Identity",
|
||||
serviceLabelDe: "Brand Identity",
|
||||
previewUrl: "https://example.com/preview/brand-redesign",
|
||||
coverImagePath: "/uploads/portfolio/demo-cover.svg",
|
||||
isFeatured: true,
|
||||
isPublished: true,
|
||||
publishedAt: new Date("2025-01-10T09:00:00.000Z"),
|
||||
sortOrder: 1,
|
||||
},
|
||||
create: {
|
||||
categoryId: brandCategory.id,
|
||||
slug: "brand-redesign",
|
||||
titleAr: "إعادة تصميم الهوية",
|
||||
titleEn: "Brand Redesign",
|
||||
titleDe: "Brand Redesign",
|
||||
summaryAr: "إعادة بناء لهوية رقمية مع نظام مرئي أوضح ومسارات استخدام أسرع.",
|
||||
summaryEn: "A digital brand refresh with a clearer visual system and faster journeys.",
|
||||
summaryDe: "Ein digitales Redesign mit klarerem visuellen System und schnelleren Journeys.",
|
||||
clientName: "Studio Client",
|
||||
projectYear: 2025,
|
||||
serviceLabelAr: "هوية بصرية",
|
||||
serviceLabelEn: "Brand Identity",
|
||||
serviceLabelDe: "Brand Identity",
|
||||
previewUrl: "https://example.com/preview/brand-redesign",
|
||||
coverImagePath: "/uploads/portfolio/demo-cover.svg",
|
||||
isFeatured: true,
|
||||
isPublished: true,
|
||||
publishedAt: new Date("2025-01-10T09:00:00.000Z"),
|
||||
sortOrder: 1,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.portfolioSection.deleteMany({
|
||||
where: { projectId: project.id },
|
||||
});
|
||||
|
||||
await prisma.portfolioAsset.deleteMany({
|
||||
where: { projectId: project.id },
|
||||
});
|
||||
|
||||
await prisma.portfolioSection.createMany({
|
||||
data: [
|
||||
{
|
||||
projectId: project.id,
|
||||
type: "RICH_TEXT",
|
||||
titleAr: "التحدي",
|
||||
titleEn: "Challenge",
|
||||
titleDe: "Herausforderung",
|
||||
bodyAr: "كان المطلوب تحديث الهوية بدون خسارة التعرف البصري الحالي.",
|
||||
bodyEn: "The brief required a refreshed identity without losing recognition.",
|
||||
bodyDe: "Die Marke sollte modernisiert werden, ohne die Wiedererkennbarkeit zu verlieren.",
|
||||
sortOrder: 1,
|
||||
},
|
||||
{
|
||||
projectId: project.id,
|
||||
type: "RICH_TEXT",
|
||||
titleAr: "الحل",
|
||||
titleEn: "Solution",
|
||||
titleDe: "Loesung",
|
||||
bodyAr: "تم بناء نظام مرئي أوضح مع قواعد استخدام قابلة للتوسع.",
|
||||
bodyEn: "A clearer visual system with scalable usage rules was created.",
|
||||
bodyDe: "Es wurde ein klareres visuelles System mit skalierbaren Regeln aufgebaut.",
|
||||
sortOrder: 2,
|
||||
},
|
||||
{
|
||||
projectId: project.id,
|
||||
type: "LINK",
|
||||
titleAr: "المعاينة",
|
||||
titleEn: "Preview",
|
||||
titleDe: "Vorschau",
|
||||
bodyAr: "رابط مباشر لعرض المشروع.",
|
||||
bodyEn: "Direct link for reviewing the work.",
|
||||
bodyDe: "Direkter Link zur Projektansicht.",
|
||||
linkUrl: "https://example.com/preview/brand-redesign",
|
||||
sortOrder: 3,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.portfolioAsset.createMany({
|
||||
data: [
|
||||
{
|
||||
projectId: project.id,
|
||||
kind: "IMAGE",
|
||||
filePath: "/uploads/portfolio/demo-cover.svg",
|
||||
altAr: "غلاف مشروع إعادة تصميم الهوية",
|
||||
altEn: "Brand redesign cover artwork",
|
||||
altDe: "Titelgrafik fuer Brand Redesign",
|
||||
sortOrder: 1,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await prisma.portfolioProject.upsert({
|
||||
where: { slug: "campaign-site" },
|
||||
update: {
|
||||
categoryId: webCategory.id,
|
||||
titleAr: "موقع حملة",
|
||||
titleEn: "Campaign Site",
|
||||
titleDe: "Campaign Site",
|
||||
summaryAr: "صفحة إطلاق مرنة لحملة رقمية مع تركيز على السرعة والتحويل.",
|
||||
summaryEn: "A launch site built for speed, iteration, and conversion.",
|
||||
summaryDe: "Eine Kampagnenseite mit Fokus auf Tempo, Iteration und Conversion.",
|
||||
clientName: "Launch Client",
|
||||
projectYear: 2024,
|
||||
serviceLabelAr: "موقع تسويقي",
|
||||
serviceLabelEn: "Marketing Website",
|
||||
serviceLabelDe: "Marketing Website",
|
||||
previewUrl: "https://example.com/preview/campaign-site",
|
||||
coverImagePath: "/uploads/portfolio/demo-cover.svg",
|
||||
isFeatured: false,
|
||||
isPublished: true,
|
||||
publishedAt: new Date("2024-09-05T09:00:00.000Z"),
|
||||
sortOrder: 2,
|
||||
},
|
||||
create: {
|
||||
categoryId: webCategory.id,
|
||||
slug: "campaign-site",
|
||||
titleAr: "موقع حملة",
|
||||
titleEn: "Campaign Site",
|
||||
titleDe: "Campaign Site",
|
||||
summaryAr: "صفحة إطلاق مرنة لحملة رقمية مع تركيز على السرعة والتحويل.",
|
||||
summaryEn: "A launch site built for speed, iteration, and conversion.",
|
||||
summaryDe: "Eine Kampagnenseite mit Fokus auf Tempo, Iteration und Conversion.",
|
||||
clientName: "Launch Client",
|
||||
projectYear: 2024,
|
||||
serviceLabelAr: "موقع تسويقي",
|
||||
serviceLabelEn: "Marketing Website",
|
||||
serviceLabelDe: "Marketing Website",
|
||||
previewUrl: "https://example.com/preview/campaign-site",
|
||||
coverImagePath: "/uploads/portfolio/demo-cover.svg",
|
||||
isFeatured: false,
|
||||
isPublished: true,
|
||||
publishedAt: new Date("2024-09-05T09:00:00.000Z"),
|
||||
sortOrder: 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
main()
|
||||
|
||||
Reference in New Issue
Block a user