Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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";
31 changes: 18 additions & 13 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
116 changes: 104 additions & 12 deletions src/app/(site)/careers/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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 (
<div className="flex-1 bg-black text-white min-h-screen">
Expand Down Expand Up @@ -74,6 +73,12 @@ export default async function CareerDetailPage({
<span className="font-mono text-[9px] uppercase tracking-[0.2em] border border-zinc-800 text-zinc-400 px-2 py-1">
{career.type}
</span>
{desc.duration && (
<span className="font-mono text-[9px] uppercase tracking-[0.2em] border border-zinc-800 text-zinc-400 px-2 py-1 flex items-center gap-1">
<Timer className="h-2.5 w-2.5" />
{desc.duration}
</span>
)}
<span
className={`font-mono text-[9px] uppercase tracking-[0.2em] px-2 py-1 font-black ${
isUrgent
Expand Down Expand Up @@ -104,6 +109,12 @@ export default async function CareerDetailPage({
<MapPin className="h-4 w-4 text-zinc-600" />
{career.location}
</span>
{career.duration && (
<span className="flex items-center gap-2">
<Timer className="h-4 w-4 text-zinc-600" />
{career.duration}
</span>
)}
<span className="flex items-center gap-2">
<Calendar className="h-4 w-4 text-zinc-600" />
Posted {format(new Date(career.createdAt), "MMM d, yyyy")}
Expand Down Expand Up @@ -152,18 +163,40 @@ export default async function CareerDetailPage({
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600 flex items-center gap-3">
<span className="text-white/10">01 /</span> About the Role
</span>
<div
className="career-description text-zinc-300 font-mono text-sm leading-relaxed"
// biome-ignore lint/security/noDangerouslySetInnerHtml: content is admin-authored markdown
dangerouslySetInnerHTML={{ __html: descriptionHtml }}
/>
<p className="text-zinc-300 font-mono text-sm leading-relaxed">
{desc.aboutRole}
</p>
</div>

{/* Responsibilities */}
{desc.responsibilities.length > 0 && (
<div className="flex flex-col gap-6">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600 flex items-center gap-3">
<span className="text-white/10">02 /</span> Responsibilities
</span>
<ul className="flex flex-col divide-y divide-zinc-900">
{desc.responsibilities.map((item, i) => (
<li
key={item}
className="flex items-start gap-4 py-4 group"
>
<span className="font-mono text-[9px] text-zinc-700 w-6 shrink-0 mt-0.5">
{String(i + 1).padStart(2, "0")}
</span>
<span className="text-zinc-300 font-mono text-sm group-hover:text-white transition-colors">
{item}
</span>
</li>
))}
</ul>
</div>
)}

{/* Requirements */}
{career.requirements.length > 0 && (
<div className="flex flex-col gap-6">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600 flex items-center gap-3">
<span className="text-white/10">02 /</span> Requirements
<span className="text-white/10">03 /</span> Requirements
&amp; Skills
</span>
<ul className="flex flex-col divide-y divide-zinc-900">
Expand All @@ -183,11 +216,61 @@ export default async function CareerDetailPage({
</ul>
</div>
)}

{/* Nice to Have */}
{desc.niceToHave.length > 0 && (
<div className="flex flex-col gap-6">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600 flex items-center gap-3">
<span className="text-white/10">04 /</span> Nice to Have
</span>
<ul className="flex flex-col gap-3">
{desc.niceToHave.map((item) => (
<li key={item} className="flex items-start gap-3">
<CheckCircle2 className="h-4 w-4 text-zinc-700 mt-0.5 shrink-0" />
<span className="text-zinc-400 font-mono text-sm">
{item}
</span>
</li>
))}
</ul>
</div>
)}

{/* What We Offer */}
{desc.whatWeOffer.length > 0 && (
<div className="flex flex-col gap-6">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600 flex items-center gap-3">
<span className="text-white/10">05 /</span> What We Offer
</span>
<ul className="flex flex-col gap-3">
{desc.whatWeOffer.map((item) => (
<li key={item} className="flex items-start gap-3">
<Sparkles className="h-4 w-4 text-zinc-700 mt-0.5 shrink-0" />
<span className="text-zinc-400 font-mono text-sm">
{item}
</span>
</li>
))}
</ul>
</div>
)}

{/* How to Apply */}
{desc.howToApply && (
<div className="flex flex-col gap-6">
<span className="font-mono text-[10px] uppercase tracking-[0.3em] text-zinc-600 flex items-center gap-3">
<span className="text-white/10">06 /</span> How to Apply
</span>
<p className="text-zinc-300 font-mono text-sm leading-relaxed">
{desc.howToApply}
</p>
</div>
)}
</div>

{/* Right: sticky sidebar */}
<aside className="flex flex-col gap-6">
<div className="sticky top-28 flex flex-col gap-4">
<div className="sticky top-36 flex flex-col gap-4">
{/* Summary card */}
<div className="border border-zinc-800 bg-zinc-950 p-6 flex flex-col gap-5">
<span className="font-mono text-[9px] uppercase tracking-[0.3em] text-zinc-600">
Expand All @@ -207,6 +290,15 @@ export default async function CareerDetailPage({
icon: MapPin,
},
{ label: "Type", value: career.type, icon: Briefcase },
...(desc.duration
? [
{
label: "Duration",
value: desc.duration,
icon: Timer,
},
]
: []),
{
label: "Closes",
value: format(expiry, "MMM d, yyyy"),
Expand Down
11 changes: 9 additions & 2 deletions src/app/api/admin/careers/[id]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export async function PUT(
"title",
"type",
"location",
"description",
"aboutRole",
"requirements",
"expiryDate",
]);
Expand All @@ -77,7 +77,14 @@ export async function PUT(
company: data.company?.trim() || "Codetopia",
type: data.type.trim(),
location: data.location.trim(),
description: data.description.trim(),
aboutRole: data.aboutRole?.trim() ?? "",
responsibilities: Array.isArray(data.responsibilities)
? data.responsibilities
: [],
niceToHave: Array.isArray(data.niceToHave) ? data.niceToHave : [],
whatWeOffer: Array.isArray(data.whatWeOffer) ? data.whatWeOffer : [],
howToApply: data.howToApply?.trim() ?? "",
duration: data.duration?.trim() || null,
requirements: Array.isArray(data.requirements)
? data.requirements
: data.requirements.split("\n").filter((r: string) => r.trim()),
Expand Down
11 changes: 9 additions & 2 deletions src/app/api/admin/careers/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ export async function POST(request: Request) {
"title",
"type",
"location",
"description",
"aboutRole",
"requirements",
"expiryDate",
]);
Expand All @@ -52,7 +52,14 @@ export async function POST(request: Request) {
company: data.company?.trim() || "Codetopia",
type: data.type.trim(),
location: data.location.trim(),
description: data.description.trim(),
aboutRole: data.aboutRole?.trim() ?? "",
responsibilities: Array.isArray(data.responsibilities)
? data.responsibilities
: [],
niceToHave: Array.isArray(data.niceToHave) ? data.niceToHave : [],
whatWeOffer: Array.isArray(data.whatWeOffer) ? data.whatWeOffer : [],
howToApply: data.howToApply?.trim() ?? "",
duration: data.duration?.trim() || null,
requirements: Array.isArray(data.requirements)
? data.requirements
: data.requirements.split("\n").filter((r: string) => r.trim()),
Expand Down
Loading
Loading