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
7 changes: 7 additions & 0 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{ protocol: "http", hostname: "localhost" },
{ protocol: "http", hostname: "127.0.0.1" },
{ protocol: "https", hostname: "**.supabase.co" },
],
},
experimental: {
serverActions: {
bodySizeLimit: "10mb",
Expand Down
21 changes: 19 additions & 2 deletions src/app/api/career-dev-articles/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { NextResponse } from "next/server";
import { prisma } from "@/lib/prisma";
import { createClient } from "@/utils/supabase/server";
import { createClient, createServiceRoleClient } from "@/utils/supabase/server";

const db = prisma.careerDevArticles;

Expand Down Expand Up @@ -77,7 +77,24 @@ async function requireAdmin(): Promise<{ userId: string } | NextResponse> {
export async function GET() {
try {
const rows = await db.findMany({ orderBy: { created_at: "desc" } });
return NextResponse.json(rows.map(toArticleResponse));
const supabase = createServiceRoleClient();

const articles = await Promise.all(
rows.map(async (r) => {
const response = toArticleResponse(r);
// If imageurl is a storage path (not a full URL), generate a signed URL at read-time
if (r.imageurl && !r.imageurl.startsWith("http")) {
const { data } = await supabase.storage.from("article").createSignedUrl(r.imageurl, 3600);
if (data?.signedUrl) {
response.imageUrl = data.signedUrl;
response.image = { src: data.signedUrl, alt: `${r.title} image` };
}
}
return response;
}),
);

return NextResponse.json(articles);
} catch (err) {
console.error("GET /api/career-dev-articles failed:", err);
return NextResponse.json({ error: "GET failed", message: String(err) }, { status: 500 });
Expand Down
103 changes: 103 additions & 0 deletions src/components/hrservices/career-dev/ArticleDetailsModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"use client";

import { useEffect } from "react";
import { type CareerDevArticleUI } from "./CareerDev";

const ArticleDetailsModal = ({
isOpen,
onClose,
article,
onEdit,
onDelete,
isAdmin,
deleting,
}: {
isOpen: boolean;
onClose: () => void;
article: CareerDevArticleUI | null;
onEdit: () => void;
onDelete: () => void;
isAdmin: boolean;
deleting: boolean;
}) => {
useEffect(() => {
if (!isOpen) return;
const handleKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", handleKey);
return () => window.removeEventListener("keydown", handleKey);
}, [isOpen, onClose]);

if (!isOpen || !article) return null;

return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4"
role="dialog"
aria-modal="true"
onClick={onClose}
>
<div
className="max-h-[90vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-base-100 shadow-lg"
onClick={(e) => e.stopPropagation()}
>
<div className="flex items-start justify-between border-b border-base-300 p-4">
<h4 className="text-lg font-semibold text-base-content">{article.title}</h4>

<div className="flex items-center gap-2">
{isAdmin && (
<>
<button
type="button"
className="rounded-md border border-base-300 bg-base-100 px-3 py-1.5 text-sm font-medium text-base-content hover:bg-base-200"
onClick={onEdit}
>
Edit
</button>
<button
type="button"
disabled={deleting}
className="rounded-md border border-error/30 bg-base-100 px-3 py-1.5 text-sm font-medium text-error hover:bg-error/10 disabled:opacity-50"
onClick={onDelete}
>
{deleting ? "Deleting..." : "Delete"}
</button>
</>
)}

<button
className="rounded-md p-1 text-base-content/60 hover:bg-base-200"
onClick={onClose}
aria-label="Close"
>
</button>
</div>
</div>
<div className="px-4 pb-4 text-base-content/80">
<p className="mb-2 text-sm text-base-content/60">{article.blurb}</p>

<div className="mb-4 space-y-1 text-sm">
<p>
<span className="font-medium">Author:</span> {article.author}
</p>
<p>
<span className="font-medium">Date:</span> {article.date}
</p>
<p>
<span className="font-medium">Time:</span> {article.startTime} to {article.endTime}
</p>
<p>
<span className="font-medium">Location:</span> {article.location}
</p>
</div>

<p className="leading-relaxed whitespace-pre-line">{article.body ?? "Coming soon..."}</p>
</div>
</div>
</div>
);
};

export default ArticleDetailsModal;
Loading
Loading