diff --git a/prisma/migrations/20260430000000_restructure_career_description/migration.sql b/prisma/migrations/20260430000000_restructure_career_description/migration.sql new file mode 100644 index 0000000..1a247e7 --- /dev/null +++ b/prisma/migrations/20260430000000_restructure_career_description/migration.sql @@ -0,0 +1,51 @@ +-- Migration: restructure_career_description +-- Replaces the single `description` TEXT column with proper structured columns. +-- Includes a data migration that parses existing JSON descriptions into the new columns. + +-- Step 1: Add new columns (nullable first so existing rows don't fail) +ALTER TABLE "careers" + ADD COLUMN "about_role" TEXT NOT NULL DEFAULT '', + ADD COLUMN "responsibilities" TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN "nice_to_have" TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN "what_we_offer" TEXT[] NOT NULL DEFAULT '{}', + ADD COLUMN "how_to_apply" TEXT NOT NULL DEFAULT '', + ADD COLUMN "duration" TEXT; + +-- Step 2: Data migration — parse existing JSON descriptions into the new columns. +-- Rows that contain valid JSON with an "aboutRole" key are migrated field by field. +-- Rows with legacy plain text are moved into about_role as-is. +UPDATE "careers" +SET + "about_role" = CASE + WHEN description ~ '^\s*\{' AND description::jsonb ? 'aboutRole' + THEN (description::jsonb ->> 'aboutRole') + ELSE description + END, + "responsibilities" = CASE + WHEN description ~ '^\s*\{' AND description::jsonb ? 'responsibilities' + THEN ARRAY(SELECT jsonb_array_elements_text(description::jsonb -> 'responsibilities')) + ELSE '{}' + END, + "nice_to_have" = CASE + WHEN description ~ '^\s*\{' AND description::jsonb ? 'niceToHave' + THEN ARRAY(SELECT jsonb_array_elements_text(description::jsonb -> 'niceToHave')) + ELSE '{}' + END, + "what_we_offer" = CASE + WHEN description ~ '^\s*\{' AND description::jsonb ? 'whatWeOffer' + THEN ARRAY(SELECT jsonb_array_elements_text(description::jsonb -> 'whatWeOffer')) + ELSE '{}' + END, + "how_to_apply" = CASE + WHEN description ~ '^\s*\{' AND description::jsonb ? 'howToApply' + THEN (description::jsonb ->> 'howToApply') + ELSE '' + END, + "duration" = CASE + WHEN description ~ '^\s*\{' AND description::jsonb ? 'duration' + THEN NULLIF(description::jsonb ->> 'duration', '') + ELSE NULL + END; + +-- Step 3: Drop the old description column +ALTER TABLE "careers" DROP COLUMN "description"; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4ce75e2..2ac8056 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -163,19 +163,24 @@ model NewsletterDeliveryLog { } model Career { - id Int @id @default(autoincrement()) - title String - company String @default("Codetopia") - type String - location String - description String @db.Text - requirements String[] - link String? - expiryDate DateTime @map("expiry_date") - isFeatured Boolean @default(false) @map("is_featured") - status String @default("open") - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime @updatedAt @map("updated_at") + id Int @id @default(autoincrement()) + title String + company String @default("Codetopia") + type String + location String + aboutRole String @default("") @map("about_role") @db.Text + responsibilities String[] @map("responsibilities") + niceToHave String[] @map("nice_to_have") + whatWeOffer String[] @map("what_we_offer") + howToApply String @default("") @map("how_to_apply") @db.Text + duration String? + requirements String[] + link String? + expiryDate DateTime @map("expiry_date") + isFeatured Boolean @default(false) @map("is_featured") + status String @default("open") + createdAt DateTime @default(now()) @map("created_at") + updatedAt DateTime @updatedAt @map("updated_at") @@map("careers") } diff --git a/src/app/(site)/careers/[id]/page.tsx b/src/app/(site)/careers/[id]/page.tsx index baa5fd2..05bb8eb 100644 --- a/src/app/(site)/careers/[id]/page.tsx +++ b/src/app/(site)/careers/[id]/page.tsx @@ -5,11 +5,13 @@ import { Briefcase, Building2, Calendar, + CheckCircle2, Clock, MapPin, + Sparkles, Star, + Timer, } from "lucide-react"; -import { marked } from "marked"; import Link from "next/link"; import { notFound } from "next/navigation"; import { prisma } from "@/../prisma/prisma"; @@ -38,10 +40,7 @@ export default async function CareerDetailPage({ const daysLeft = differenceInDays(expiry, now); const isUrgent = daysLeft <= 7; - const descriptionHtml = await marked.parse(career.description, { - gfm: true, - breaks: true, - }); + const desc = career; return (
@@ -74,6 +73,12 @@ export default async function CareerDetailPage({ {career.type} + {desc.duration && ( + + + {desc.duration} + + )} {career.location} + {career.duration && ( + + + {career.duration} + + )} Posted {format(new Date(career.createdAt), "MMM d, yyyy")} @@ -152,18 +163,40 @@ export default async function CareerDetailPage({ 01 / About the Role -
+

+ {desc.aboutRole} +

+ {/* Responsibilities */} + {desc.responsibilities.length > 0 && ( +
+ + 02 / Responsibilities + +
    + {desc.responsibilities.map((item, i) => ( +
  • + + {String(i + 1).padStart(2, "0")} + + + {item} + +
  • + ))} +
+
+ )} + {/* Requirements */} {career.requirements.length > 0 && (
- 02 / Requirements + 03 / Requirements & Skills
    @@ -183,11 +216,61 @@ export default async function CareerDetailPage({
)} + + {/* Nice to Have */} + {desc.niceToHave.length > 0 && ( +
+ + 04 / Nice to Have + +
    + {desc.niceToHave.map((item) => ( +
  • + + + {item} + +
  • + ))} +
+
+ )} + + {/* What We Offer */} + {desc.whatWeOffer.length > 0 && ( +
+ + 05 / What We Offer + +
    + {desc.whatWeOffer.map((item) => ( +
  • + + + {item} + +
  • + ))} +
+
+ )} + + {/* How to Apply */} + {desc.howToApply && ( +
+ + 06 / How to Apply + +

+ {desc.howToApply} +

+
+ )}
{/* Right: sticky sidebar */}