feat: fixing role id usage, fixing content website, etc
continuous-integration/drone/push Build is passing
Details
continuous-integration/drone/push Build is passing
Details
This commit is contained in:
parent
7515f55e88
commit
71c65f9a60
4
.env
4
.env
|
|
@ -1,3 +1,3 @@
|
||||||
MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640
|
MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640
|
||||||
NEXT_PUBLIC_API_URL=http://localhost:8800
|
# NEXT_PUBLIC_API_URL=http://localhost:8800
|
||||||
# NEXT_PUBLIC_API_URL=https://qudo.id/api
|
NEXT_PUBLIC_API_URL=https://qudo.id/api
|
||||||
|
|
@ -2,6 +2,10 @@
|
||||||
|
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
import ContentWebsite from "@/components/main/content-website";
|
import ContentWebsite from "@/components/main/content-website";
|
||||||
|
import {
|
||||||
|
isApproverOrAdmin,
|
||||||
|
isContributorRole,
|
||||||
|
} from "@/constants/user-roles";
|
||||||
|
|
||||||
import { motion } from "framer-motion";
|
import { motion } from "framer-motion";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
|
@ -13,8 +17,7 @@ export default function ContentWebsitePage() {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setMounted(true);
|
setMounted(true);
|
||||||
const ulne = Cookies.get("ulne");
|
setLevelId(Cookies.get("urie"));
|
||||||
setLevelId(ulne);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
|
|
@ -33,10 +36,10 @@ export default function ContentWebsitePage() {
|
||||||
transition={{ duration: 0.3 }}
|
transition={{ duration: 0.3 }}
|
||||||
>
|
>
|
||||||
<div className="p-6">
|
<div className="p-6">
|
||||||
{levelId === "3" ? (
|
{isApproverOrAdmin(levelId) ? (
|
||||||
<ApproverContentWebsite />
|
<ApproverContentWebsite />
|
||||||
) : (
|
) : (
|
||||||
<ContentWebsite contributorMode={levelId === "2"} />
|
<ContentWebsite contributorMode={isContributorRole(levelId)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,35 @@ const geistMono = Geist_Mono({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const siteDescription =
|
||||||
|
"Qudoco — portal konten dan layanan untuk mengelola website, berita, serta aset media dengan alur kerja yang terstruktur.";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
metadataBase: new URL("https://qudo.id"),
|
||||||
description: "Generated by create next app",
|
title: {
|
||||||
|
default: "Qudoco",
|
||||||
|
template: "%s | Qudoco",
|
||||||
|
},
|
||||||
|
description: siteDescription,
|
||||||
|
applicationName: "Qudoco",
|
||||||
|
keywords: ["Qudoco", "portal konten", "CMS", "berita", "layanan"],
|
||||||
|
authors: [{ name: "Qudoco" }],
|
||||||
|
openGraph: {
|
||||||
|
type: "website",
|
||||||
|
locale: "id_ID",
|
||||||
|
siteName: "Qudoco",
|
||||||
|
title: "Qudoco",
|
||||||
|
description: siteDescription,
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Qudoco",
|
||||||
|
description: siteDescription,
|
||||||
|
},
|
||||||
|
robots: {
|
||||||
|
index: true,
|
||||||
|
follow: true,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
|
|
@ -23,7 +49,7 @@ export default function RootLayout({
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="id">
|
||||||
<body
|
<body
|
||||||
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
17
app/page.tsx
17
app/page.tsx
|
|
@ -1,3 +1,4 @@
|
||||||
|
import type { Metadata } from "next";
|
||||||
import Header from "@/components/landing-page/headers";
|
import Header from "@/components/landing-page/headers";
|
||||||
import AboutSection from "@/components/landing-page/about";
|
import AboutSection from "@/components/landing-page/about";
|
||||||
import ProductSection from "@/components/landing-page/product";
|
import ProductSection from "@/components/landing-page/product";
|
||||||
|
|
@ -15,6 +16,22 @@ import type {
|
||||||
CmsServiceContent,
|
CmsServiceContent,
|
||||||
} from "@/types/cms-landing";
|
} from "@/types/cms-landing";
|
||||||
|
|
||||||
|
const landingDescription =
|
||||||
|
"Jelajahi layanan, produk, dan informasi terbaru dari Qudoco — satu portal untuk konten profesional dan komunikasi yang jelas.";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Beranda",
|
||||||
|
description: landingDescription,
|
||||||
|
openGraph: {
|
||||||
|
title: "Beranda | Qudoco",
|
||||||
|
description: landingDescription,
|
||||||
|
url: "/",
|
||||||
|
},
|
||||||
|
alternates: {
|
||||||
|
canonical: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const [hero, aboutList, productList, serviceList, partners, popupList] =
|
const [hero, aboutList, productList, serviceList, partners, popupList] =
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,10 @@ import { useParams, useRouter } from "next/navigation";
|
||||||
import GetSeoScore from "./get-seo-score-form";
|
import GetSeoScore from "./get-seo-score-form";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import {
|
||||||
|
isApproverOrAdmin,
|
||||||
|
isContributorRole,
|
||||||
|
} from "@/constants/user-roles";
|
||||||
import {
|
import {
|
||||||
createArticleSchedule,
|
createArticleSchedule,
|
||||||
deleteArticleFiles,
|
deleteArticleFiles,
|
||||||
|
|
@ -162,8 +166,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
const [levelId, setLevelId] = useState<string | undefined>();
|
const [levelId, setLevelId] = useState<string | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ulne = Cookies.get("ulne");
|
setLevelId(Cookies.get("urie"));
|
||||||
setLevelId(ulne);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { getRootProps, getInputProps } = useDropzone({
|
const { getRootProps, getInputProps } = useDropzone({
|
||||||
|
|
@ -812,7 +815,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
|
|
||||||
{/* ================= ACTION BUTTON ================= */}
|
{/* ================= ACTION BUTTON ================= */}
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{levelId === "2" && !detailData?.isPublish && (
|
{isApproverOrAdmin(levelId) && !detailData?.isPublish && (
|
||||||
<>
|
<>
|
||||||
<Button
|
<Button
|
||||||
onClick={doPublish}
|
onClick={doPublish}
|
||||||
|
|
@ -844,7 +847,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* 🔥 Jika levelId 3 → hanya tampilkan Cancel */}
|
{/* 🔥 Jika levelId 3 → hanya tampilkan Cancel */}
|
||||||
{levelId === "3" && (
|
{isContributorRole(levelId) && (
|
||||||
<Link href="/admin/news-article/image">
|
<Link href="/admin/news-article/image">
|
||||||
<Button variant="outline" className="w-full py-3 rounded-xl">
|
<Button variant="outline" className="w-full py-3 rounded-xl">
|
||||||
Cancel
|
Cancel
|
||||||
|
|
|
||||||
|
|
@ -29,8 +29,8 @@ interface SidebarSection {
|
||||||
children?: SidebarItem[];
|
children?: SidebarItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const getSidebarByLevel = (levelId: string | null) => {
|
const getSidebarByRole = (roleId: string | null) => {
|
||||||
if (levelId === "1") {
|
if (roleId === "1") {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
|
|
@ -64,7 +64,7 @@ const getSidebarByLevel = (levelId: string | null) => {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (levelId === "3") {
|
if (roleId === "2" || roleId === "3") {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
title: "Dashboard",
|
title: "Dashboard",
|
||||||
|
|
@ -141,66 +141,7 @@ const getSidebarByLevel = (levelId: string | null) => {
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (levelId === "2") {
|
// fallback jika role tidak dikenal
|
||||||
return [
|
|
||||||
{
|
|
||||||
title: "Dashboard",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Dashboard",
|
|
||||||
icon: () => (
|
|
||||||
<Icon icon="material-symbols:dashboard" className="text-lg" />
|
|
||||||
),
|
|
||||||
link: "/admin/dashboard",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "Content Website",
|
|
||||||
icon: () => <Icon icon="ri:article-line" className="text-lg" />,
|
|
||||||
link: "/admin/content-website",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "News & Article",
|
|
||||||
items: [
|
|
||||||
{
|
|
||||||
title: "News & Article",
|
|
||||||
icon: () => (
|
|
||||||
<Icon icon="grommet-icons:article" className="text-lg" />
|
|
||||||
),
|
|
||||||
children: [
|
|
||||||
{
|
|
||||||
title: "Text",
|
|
||||||
icon: () => <Icon icon="mdi:file-document-outline" />,
|
|
||||||
link: "/admin/news-article/text",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Image",
|
|
||||||
icon: () => <Icon icon="mdi:image-outline" />,
|
|
||||||
link: "/admin/news-article/image",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Video",
|
|
||||||
icon: () => <Icon icon="mdi:video-outline" />,
|
|
||||||
link: "/admin/news-article/video",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: "Audio",
|
|
||||||
icon: () => <Icon icon="mdi:music-note-outline" />,
|
|
||||||
link: "/admin/news-article/audio",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
// fallback kalau Level tidak dikenal
|
|
||||||
return [];
|
return [];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -334,10 +275,10 @@ const SidebarContent = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const cookieUsername = getCookie("username");
|
const cookieUsername = getCookie("username");
|
||||||
const cookieLevelId = getCookie("ulne");
|
const cookieRoleId = getCookie("urie");
|
||||||
|
|
||||||
if (cookieUsername) setUsername(cookieUsername);
|
if (cookieUsername) setUsername(cookieUsername);
|
||||||
if (cookieLevelId) setLevelId(cookieLevelId);
|
if (cookieRoleId) setLevelId(cookieRoleId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// ===============================
|
// ===============================
|
||||||
|
|
@ -357,7 +298,7 @@ const SidebarContent = ({
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const sidebarSections = getSidebarByLevel(LevelId);
|
const sidebarSections = getSidebarByRole(LevelId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col h-full">
|
<div className="flex flex-col h-full">
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ import {
|
||||||
TableRow,
|
TableRow,
|
||||||
} from "@/components/ui/table";
|
} from "@/components/ui/table";
|
||||||
import { Card, CardContent } from "@/components/ui/card";
|
import { Card, CardContent } from "@/components/ui/card";
|
||||||
import { Filter, Loader2 } from "lucide-react";
|
import { ExternalLink, Filter, Loader2 } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
approveCmsContentSubmission,
|
approveCmsContentSubmission,
|
||||||
listCmsContentSubmissions,
|
listCmsContentSubmissions,
|
||||||
|
|
@ -22,6 +22,7 @@ import {
|
||||||
import { apiPayload } from "@/service/cms-landing";
|
import { apiPayload } from "@/service/cms-landing";
|
||||||
import { formatDate } from "@/utils/global";
|
import { formatDate } from "@/utils/global";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
|
import ContentWebsite from "@/components/main/content-website";
|
||||||
|
|
||||||
const DOMAIN_LABEL: Record<string, string> = {
|
const DOMAIN_LABEL: Record<string, string> = {
|
||||||
hero: "Hero",
|
hero: "Hero",
|
||||||
|
|
@ -32,6 +33,16 @@ const DOMAIN_LABEL: Record<string, string> = {
|
||||||
popup: "Pop Up",
|
popup: "Pop Up",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Maps submission `domain` to ContentWebsite tab `value`. */
|
||||||
|
const DOMAIN_TO_TAB: Record<string, string> = {
|
||||||
|
hero: "hero",
|
||||||
|
about: "about",
|
||||||
|
product: "products",
|
||||||
|
service: "services",
|
||||||
|
partner: "partners",
|
||||||
|
popup: "popup",
|
||||||
|
};
|
||||||
|
|
||||||
type Row = {
|
type Row = {
|
||||||
id: string;
|
id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
|
|
@ -48,6 +59,9 @@ export default function ApproverContentWebsite() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [actingId, setActingId] = useState<string | null>(null);
|
const [actingId, setActingId] = useState<string | null>(null);
|
||||||
|
const [tabFocusSignal, setTabFocusSignal] = useState(0);
|
||||||
|
const [tabFocusTarget, setTabFocusTarget] = useState("hero");
|
||||||
|
const [liveDataReloadSignal, setLiveDataReloadSignal] = useState(0);
|
||||||
|
|
||||||
const load = useCallback(async () => {
|
const load = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -78,6 +92,18 @@ export default function ApproverContentWebsite() {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function jumpToLiveTab(domain: string) {
|
||||||
|
const tab = DOMAIN_TO_TAB[domain];
|
||||||
|
if (!tab) return;
|
||||||
|
setTabFocusTarget(tab);
|
||||||
|
setTabFocusSignal((n) => n + 1);
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
document
|
||||||
|
.getElementById("approver-live-cms")
|
||||||
|
?.scrollIntoView({ behavior: "smooth", block: "start" });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function onApprove(id: string) {
|
async function onApprove(id: string) {
|
||||||
const ok = await Swal.fire({
|
const ok = await Swal.fire({
|
||||||
icon: "question",
|
icon: "question",
|
||||||
|
|
@ -102,6 +128,7 @@ export default function ApproverContentWebsite() {
|
||||||
timer: 1600,
|
timer: 1600,
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
});
|
});
|
||||||
|
setLiveDataReloadSignal((n) => n + 1);
|
||||||
await load();
|
await load();
|
||||||
} finally {
|
} finally {
|
||||||
setActingId(null);
|
setActingId(null);
|
||||||
|
|
@ -144,107 +171,163 @@ export default function ApproverContentWebsite() {
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-10">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-slate-800">Content Website</h1>
|
<h1 className="text-2xl font-bold text-slate-800">Content Website</h1>
|
||||||
<p className="text-slate-500">
|
<p className="mt-1 text-slate-500">
|
||||||
Tinjau pengajuan perubahan dari kontributor dan terapkan ke konten live.
|
Di bagian atas: pengajuan baru yang perlu disetujui. Di bawah: konten
|
||||||
|
live di semua tab (hanya lihat) untuk membandingkan dengan pengajuan.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
<section className="space-y-4">
|
||||||
<Input
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
placeholder="Cari judul, domain, atau nama pengaju…"
|
<h2 className="text-lg font-semibold text-slate-900">
|
||||||
value={search}
|
Perubahan menunggu persetujuan
|
||||||
onChange={(e) => setSearch(e.target.value)}
|
</h2>
|
||||||
className="max-w-md"
|
<Badge variant="secondary" className="font-normal">
|
||||||
/>
|
{filtered.length} pending
|
||||||
<Button
|
</Badge>
|
||||||
type="button"
|
</div>
|
||||||
variant="outline"
|
<p className="text-sm text-slate-500">
|
||||||
className="gap-2 shrink-0"
|
Setujui atau tolak di sini. Gunakan{" "}
|
||||||
onClick={() => load()}
|
<span className="font-medium text-slate-700">Lihat konten live</span>{" "}
|
||||||
disabled={loading}
|
untuk membuka tab yang sama dengan bagian yang diajukan.
|
||||||
>
|
</p>
|
||||||
{loading ? (
|
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
|
||||||
) : (
|
|
||||||
<Filter className="h-4 w-4" />
|
|
||||||
)}
|
|
||||||
Refresh
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Card className="rounded-2xl border shadow-sm overflow-hidden">
|
<div className="flex flex-col gap-3 sm:flex-row sm:items-center">
|
||||||
<CardContent className="p-0">
|
<Input
|
||||||
{loading ? (
|
placeholder="Cari judul, bagian, atau nama pengaju…"
|
||||||
<div className="flex justify-center py-16">
|
value={search}
|
||||||
<Loader2 className="h-10 w-10 animate-spin text-[#966314]" />
|
onChange={(e) => setSearch(e.target.value)}
|
||||||
</div>
|
className="max-w-md"
|
||||||
) : (
|
/>
|
||||||
<Table>
|
<Button
|
||||||
<TableHeader>
|
type="button"
|
||||||
<TableRow className="bg-slate-50">
|
variant="outline"
|
||||||
<TableHead>Judul</TableHead>
|
className="gap-2 shrink-0"
|
||||||
<TableHead>Bagian</TableHead>
|
onClick={() => load()}
|
||||||
<TableHead>Pengaju</TableHead>
|
disabled={loading}
|
||||||
<TableHead>Tanggal</TableHead>
|
>
|
||||||
<TableHead className="text-right">Aksi</TableHead>
|
{loading ? (
|
||||||
</TableRow>
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
</TableHeader>
|
) : (
|
||||||
<TableBody>
|
<Filter className="h-4 w-4" />
|
||||||
{filtered.length === 0 ? (
|
)}
|
||||||
<TableRow>
|
Refresh
|
||||||
<TableCell colSpan={5} className="text-center text-slate-500 py-10">
|
</Button>
|
||||||
Tidak ada pengajuan tertunda.
|
</div>
|
||||||
</TableCell>
|
|
||||||
|
<Card className="rounded-2xl border shadow-sm overflow-hidden">
|
||||||
|
<CardContent className="p-0">
|
||||||
|
{loading ? (
|
||||||
|
<div className="flex justify-center py-16">
|
||||||
|
<Loader2 className="h-10 w-10 animate-spin text-[#966314]" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow className="bg-slate-50">
|
||||||
|
<TableHead>Judul</TableHead>
|
||||||
|
<TableHead>Bagian</TableHead>
|
||||||
|
<TableHead>Pengaju</TableHead>
|
||||||
|
<TableHead>Tanggal</TableHead>
|
||||||
|
<TableHead className="text-right w-[280px]">Aksi</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
) : (
|
</TableHeader>
|
||||||
filtered.map((item) => (
|
<TableBody>
|
||||||
<TableRow key={item.id}>
|
{filtered.length === 0 ? (
|
||||||
<TableCell className="font-medium max-w-xs">
|
<TableRow>
|
||||||
{item.title}
|
<TableCell
|
||||||
</TableCell>
|
colSpan={5}
|
||||||
<TableCell>
|
className="text-center text-slate-500 py-10"
|
||||||
<Badge variant="outline">
|
>
|
||||||
{DOMAIN_LABEL[item.domain] ?? item.domain}
|
Tidak ada pengajuan tertunda.
|
||||||
</Badge>
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-slate-600">
|
|
||||||
{item.submitter_name || `User #${item.submitted_by_id}`}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-slate-600">
|
|
||||||
{formatDate(item.created_at)}
|
|
||||||
</TableCell>
|
|
||||||
<TableCell className="text-right space-x-2">
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
className="bg-green-600 hover:bg-green-700"
|
|
||||||
disabled={actingId === item.id}
|
|
||||||
onClick={() => onApprove(item.id)}
|
|
||||||
>
|
|
||||||
Setujui
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
size="sm"
|
|
||||||
variant="outline"
|
|
||||||
className="text-red-600 border-red-200"
|
|
||||||
disabled={actingId === item.id}
|
|
||||||
onClick={() => onReject(item.id)}
|
|
||||||
>
|
|
||||||
Tolak
|
|
||||||
</Button>
|
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))
|
) : (
|
||||||
)}
|
filtered.map((item) => (
|
||||||
</TableBody>
|
<TableRow key={item.id}>
|
||||||
</Table>
|
<TableCell className="font-medium max-w-xs">
|
||||||
)}
|
{item.title}
|
||||||
</CardContent>
|
</TableCell>
|
||||||
</Card>
|
<TableCell>
|
||||||
|
<Badge variant="outline">
|
||||||
|
{DOMAIN_LABEL[item.domain] ?? item.domain}
|
||||||
|
</Badge>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-slate-600">
|
||||||
|
{item.submitter_name ||
|
||||||
|
`User #${item.submitted_by_id}`}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-slate-600">
|
||||||
|
{formatDate(item.created_at)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<div className="flex flex-wrap justify-end gap-2">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="gap-1"
|
||||||
|
onClick={() => jumpToLiveTab(item.domain)}
|
||||||
|
>
|
||||||
|
<ExternalLink className="h-3.5 w-3.5" />
|
||||||
|
Lihat konten live
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
className="bg-green-600 hover:bg-green-700"
|
||||||
|
disabled={actingId === item.id}
|
||||||
|
onClick={() => onApprove(item.id)}
|
||||||
|
>
|
||||||
|
Setujui
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
size="sm"
|
||||||
|
variant="outline"
|
||||||
|
className="text-red-600 border-red-200"
|
||||||
|
disabled={actingId === item.id}
|
||||||
|
onClick={() => onReject(item.id)}
|
||||||
|
>
|
||||||
|
Tolak
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section
|
||||||
|
id="approver-live-cms"
|
||||||
|
className="space-y-4 border-t border-slate-200 pt-10 scroll-mt-6"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h2 className="text-lg font-semibold text-slate-900">
|
||||||
|
Konten live (semua tab, hanya lihat)
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
Data yang sedang ditampilkan di website. Field tidak dapat diubah di
|
||||||
|
sini — bandingkan dengan baris pengajuan di atas sebelum
|
||||||
|
menyetujui.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ContentWebsite
|
||||||
|
viewOnly
|
||||||
|
hideHeader
|
||||||
|
tabFocusSignal={tabFocusSignal}
|
||||||
|
tabFocusTarget={tabFocusTarget}
|
||||||
|
liveDataReloadSignal={liveDataReloadSignal}
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -91,16 +91,33 @@ function setPickedFile(
|
||||||
type ContentWebsiteProps = {
|
type ContentWebsiteProps = {
|
||||||
/** User level 2: changes go through approval instead of live CMS APIs. */
|
/** User level 2: changes go through approval instead of live CMS APIs. */
|
||||||
contributorMode?: boolean;
|
contributorMode?: boolean;
|
||||||
|
/** Approver (or admin): load live CMS data but disable all edits (all tabs). */
|
||||||
|
viewOnly?: boolean;
|
||||||
|
/** Omit page title/actions row — parent supplies section headings (e.g. approver layout). */
|
||||||
|
hideHeader?: boolean;
|
||||||
|
/** Parent increments this (e.g. 1,2,3…) to switch the visible tab. */
|
||||||
|
tabFocusSignal?: number;
|
||||||
|
/** Tab id matching `TabsTrigger` values: hero | about | products | services | partners | popup */
|
||||||
|
tabFocusTarget?: string;
|
||||||
|
/** Increment (e.g. after approver applies CMS) to reload live data from API without remounting. */
|
||||||
|
liveDataReloadSignal?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ContentWebsite({
|
export default function ContentWebsite({
|
||||||
contributorMode = false,
|
contributorMode = false,
|
||||||
|
viewOnly = false,
|
||||||
|
hideHeader = false,
|
||||||
|
tabFocusSignal = 0,
|
||||||
|
tabFocusTarget = "",
|
||||||
|
liveDataReloadSignal = 0,
|
||||||
}: ContentWebsiteProps) {
|
}: ContentWebsiteProps) {
|
||||||
const [activeTab, setActiveTab] = useState("hero");
|
const [activeTab, setActiveTab] = useState("hero");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [editMode, setEditMode] = useState(!contributorMode);
|
const [editMode, setEditMode] = useState(!contributorMode);
|
||||||
const canInteract = !contributorMode || editMode;
|
const canInteract = (!contributorMode || editMode) && !viewOnly;
|
||||||
|
const dimContributorPreview =
|
||||||
|
contributorMode && !editMode && !viewOnly;
|
||||||
|
|
||||||
const [heroId, setHeroId] = useState<string | null>(null);
|
const [heroId, setHeroId] = useState<string | null>(null);
|
||||||
const [heroImageId, setHeroImageId] = useState<string | null>(null);
|
const [heroImageId, setHeroImageId] = useState<string | null>(null);
|
||||||
|
|
@ -262,6 +279,24 @@ export default function ContentWebsite({
|
||||||
loadAll();
|
loadAll();
|
||||||
}, [loadAll]);
|
}, [loadAll]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!viewOnly) return;
|
||||||
|
setProductModalOpen(false);
|
||||||
|
setServiceModalOpen(false);
|
||||||
|
setPartnerModalOpen(false);
|
||||||
|
setPopupModalOpen(false);
|
||||||
|
}, [viewOnly]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (tabFocusSignal < 1 || !tabFocusTarget) return;
|
||||||
|
setActiveTab(tabFocusTarget);
|
||||||
|
}, [tabFocusSignal, tabFocusTarget]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (liveDataReloadSignal < 1) return;
|
||||||
|
void loadAll();
|
||||||
|
}, [liveDataReloadSignal, loadAll]);
|
||||||
|
|
||||||
async function saveHeroTab() {
|
async function saveHeroTab() {
|
||||||
if (!heroPrimary.trim()) {
|
if (!heroPrimary.trim()) {
|
||||||
await Swal.fire({ icon: "warning", title: "Main title is required" });
|
await Swal.fire({ icon: "warning", title: "Main title is required" });
|
||||||
|
|
@ -1239,14 +1274,42 @@ export default function ContentWebsite({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
{!hideHeader ? (
|
||||||
<div>
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
<h1 className="text-2xl font-semibold text-slate-800">Content Website</h1>
|
<div>
|
||||||
<p className="mt-1 text-sm text-slate-500">
|
<h1 className="text-2xl font-semibold text-slate-800">
|
||||||
Update homepage content, products, services, and partners.
|
Content Website
|
||||||
</p>
|
</h1>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
{viewOnly
|
||||||
|
? "Konten live di semua tab: hanya lihat. Pengajuan perubahan ditangani di bagian atas halaman."
|
||||||
|
: "Update homepage content, products, services, and partners."}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-3">
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant="outline"
|
||||||
|
className="rounded-lg"
|
||||||
|
onClick={() => window.open("/", "_blank")}
|
||||||
|
>
|
||||||
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
|
Preview
|
||||||
|
</Button>
|
||||||
|
{contributorMode ? (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
className={`rounded-lg ${editMode ? "bg-amber-600 hover:bg-amber-700" : "bg-blue-600 hover:bg-blue-700"}`}
|
||||||
|
onClick={() => setEditMode((v) => !v)}
|
||||||
|
>
|
||||||
|
<Pencil className="mr-2 h-4 w-4" />
|
||||||
|
{editMode ? "Exit Edit Mode" : "Edit Mode"}
|
||||||
|
</Button>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-wrap gap-3">
|
) : viewOnly ? (
|
||||||
|
<div className="flex flex-wrap justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
|
|
@ -1254,22 +1317,12 @@ export default function ContentWebsite({
|
||||||
onClick={() => window.open("/", "_blank")}
|
onClick={() => window.open("/", "_blank")}
|
||||||
>
|
>
|
||||||
<Eye className="mr-2 h-4 w-4" />
|
<Eye className="mr-2 h-4 w-4" />
|
||||||
Preview
|
Preview website
|
||||||
</Button>
|
</Button>
|
||||||
{contributorMode ? (
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
className={`rounded-lg ${editMode ? "bg-amber-600 hover:bg-amber-700" : "bg-blue-600 hover:bg-blue-700"}`}
|
|
||||||
onClick={() => setEditMode((v) => !v)}
|
|
||||||
>
|
|
||||||
<Pencil className="mr-2 h-4 w-4" />
|
|
||||||
{editMode ? "Exit Edit Mode" : "Edit Mode"}
|
|
||||||
</Button>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
) : null}
|
||||||
|
|
||||||
{contributorMode && !editMode ? (
|
{contributorMode && !editMode && !viewOnly ? (
|
||||||
<p className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
<p className="rounded-lg border border-amber-200 bg-amber-50 px-4 py-3 text-sm text-amber-900">
|
||||||
Aktifkan <strong>Edit Mode</strong> untuk mengusulkan perubahan. Perubahan akan masuk ke{" "}
|
Aktifkan <strong>Edit Mode</strong> untuk mengusulkan perubahan. Perubahan akan masuk ke{" "}
|
||||||
<strong>My Content</strong> menunggu persetujuan approver. Unggah file gambar tidak tersedia sebagai kontributor;
|
<strong>My Content</strong> menunggu persetujuan approver. Unggah file gambar tidak tersedia sebagai kontributor;
|
||||||
|
|
@ -1279,7 +1332,9 @@ export default function ContentWebsite({
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className={
|
className={
|
||||||
canInteract ? "" : "pointer-events-none select-none opacity-50"
|
dimContributorPreview
|
||||||
|
? "pointer-events-none select-none opacity-50"
|
||||||
|
: ""
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
<Tabs value={activeTab} onValueChange={setActiveTab}>
|
||||||
|
|
@ -1305,6 +1360,10 @@ export default function ContentWebsite({
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="hero" className="mt-4">
|
<TabsContent value="hero" className="mt-4">
|
||||||
|
<fieldset
|
||||||
|
disabled={viewOnly}
|
||||||
|
className="min-w-0 border-0 p-0 m-0"
|
||||||
|
>
|
||||||
<Card className="rounded-2xl border shadow-sm">
|
<Card className="rounded-2xl border shadow-sm">
|
||||||
<CardContent className="space-y-6 p-6">
|
<CardContent className="space-y-6 p-6">
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
|
@ -1415,9 +1474,14 @@ export default function ContentWebsite({
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="about" className="mt-4">
|
<TabsContent value="about" className="mt-4">
|
||||||
|
<fieldset
|
||||||
|
disabled={viewOnly}
|
||||||
|
className="min-w-0 border-0 p-0 m-0"
|
||||||
|
>
|
||||||
<Card className="rounded-2xl border shadow-sm">
|
<Card className="rounded-2xl border shadow-sm">
|
||||||
<CardContent className="space-y-6 p-6">
|
<CardContent className="space-y-6 p-6">
|
||||||
<div className="grid gap-6 md:grid-cols-2">
|
<div className="grid gap-6 md:grid-cols-2">
|
||||||
|
|
@ -1562,9 +1626,14 @@ export default function ContentWebsite({
|
||||||
</Button>
|
</Button>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="products" className="mt-4">
|
<TabsContent value="products" className="mt-4">
|
||||||
|
<fieldset
|
||||||
|
disabled={viewOnly}
|
||||||
|
className="min-w-0 border-0 p-0 m-0"
|
||||||
|
>
|
||||||
<Card className="rounded-2xl border shadow-sm">
|
<Card className="rounded-2xl border shadow-sm">
|
||||||
<CardContent className="space-y-6 p-6">
|
<CardContent className="space-y-6 p-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
|
@ -1752,9 +1821,14 @@ export default function ContentWebsite({
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="services" className="mt-4">
|
<TabsContent value="services" className="mt-4">
|
||||||
|
<fieldset
|
||||||
|
disabled={viewOnly}
|
||||||
|
className="min-w-0 border-0 p-0 m-0"
|
||||||
|
>
|
||||||
<Card className="rounded-2xl border shadow-sm">
|
<Card className="rounded-2xl border shadow-sm">
|
||||||
<CardContent className="space-y-6 p-6">
|
<CardContent className="space-y-6 p-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
|
@ -1942,9 +2016,14 @@ export default function ContentWebsite({
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="partners" className="mt-4">
|
<TabsContent value="partners" className="mt-4">
|
||||||
|
<fieldset
|
||||||
|
disabled={viewOnly}
|
||||||
|
className="min-w-0 border-0 p-0 m-0"
|
||||||
|
>
|
||||||
<Card className="rounded-2xl border shadow-sm">
|
<Card className="rounded-2xl border shadow-sm">
|
||||||
<CardContent className="space-y-6 p-6">
|
<CardContent className="space-y-6 p-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
|
@ -2102,9 +2181,14 @@ export default function ContentWebsite({
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="popup" className="mt-4">
|
<TabsContent value="popup" className="mt-4">
|
||||||
|
<fieldset
|
||||||
|
disabled={viewOnly}
|
||||||
|
className="min-w-0 border-0 p-0 m-0"
|
||||||
|
>
|
||||||
<Card className="rounded-2xl border shadow-sm">
|
<Card className="rounded-2xl border shadow-sm">
|
||||||
<CardContent className="space-y-6 p-6">
|
<CardContent className="space-y-6 p-6">
|
||||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
<div className="flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between">
|
||||||
|
|
@ -2295,6 +2379,7 @@ export default function ContentWebsite({
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</fieldset>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -54,8 +54,8 @@ interface PostCount {
|
||||||
export default function DashboardContainer() {
|
export default function DashboardContainer() {
|
||||||
const [levelName, setLevelName] = useState<string | undefined>();
|
const [levelName, setLevelName] = useState<string | undefined>();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const levelId = Cookies.get("ulne");
|
const roleId = Cookies.get("urie");
|
||||||
setLevelName(levelId);
|
setLevelName(roleId);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const username = Cookies.get("username");
|
const username = Cookies.get("username");
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,10 @@ import { listCmsContentSubmissions } from "@/service/cms-content-submissions";
|
||||||
import { getArticlesForMyContent } from "@/service/article";
|
import { getArticlesForMyContent } from "@/service/article";
|
||||||
import { apiPayload } from "@/service/cms-landing";
|
import { apiPayload } from "@/service/cms-landing";
|
||||||
import { Loader2 } from "lucide-react";
|
import { Loader2 } from "lucide-react";
|
||||||
|
import {
|
||||||
|
isApproverOrAdmin,
|
||||||
|
isContributorRole,
|
||||||
|
} from "@/constants/user-roles";
|
||||||
|
|
||||||
const PLACEHOLDER_IMG =
|
const PLACEHOLDER_IMG =
|
||||||
"https://placehold.co/400x240/f1f5f9/64748b?text=Content";
|
"https://placehold.co/400x240/f1f5f9/64748b?text=Content";
|
||||||
|
|
@ -149,11 +153,11 @@ export default function MyContent() {
|
||||||
const [articleRows, setArticleRows] = useState<ArticleRow[]>([]);
|
const [articleRows, setArticleRows] = useState<ArticleRow[]>([]);
|
||||||
const [page, setPage] = useState(1);
|
const [page, setPage] = useState(1);
|
||||||
|
|
||||||
const isApprover = levelId === "3";
|
const isContributor = isContributorRole(levelId);
|
||||||
const isContributor = levelId === "2";
|
const isApprover = isApproverOrAdmin(levelId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLevelId(Cookies.get("ulne"));
|
setLevelId(Cookies.get("urie"));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { getArticlePagination, deleteArticle } from "@/service/article";
|
||||||
import { formatDate } from "@/utils/global";
|
import { formatDate } from "@/utils/global";
|
||||||
import { close, loading } from "@/config/swal";
|
import { close, loading } from "@/config/swal";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { isContributorRole } from "@/constants/user-roles";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import type { ArticleContentKind } from "@/constants/article-content-types";
|
import type { ArticleContentKind } from "@/constants/article-content-types";
|
||||||
import { ARTICLE_KIND_LABEL, articleListPath } from "@/constants/article-content-types";
|
import { ARTICLE_KIND_LABEL, articleListPath } from "@/constants/article-content-types";
|
||||||
|
|
@ -37,8 +38,7 @@ export default function NewsArticleList({ kind, typeId }: Props) {
|
||||||
const [levelId, setLevelId] = useState<string | undefined>();
|
const [levelId, setLevelId] = useState<string | undefined>();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ulne = Cookies.get("ulne");
|
setLevelId(Cookies.get("urie"));
|
||||||
setLevelId(ulne);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -128,7 +128,7 @@ export default function NewsArticleList({ kind, typeId }: Props) {
|
||||||
Create and manage {label.toLowerCase()} articles. Organize with tags only.
|
Create and manage {label.toLowerCase()} articles. Organize with tags only.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{levelId === "3" && (
|
{isContributorRole(levelId) && (
|
||||||
<Link href={`${basePath}/create`}>
|
<Link href={`${basePath}/create`}>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
|
@ -203,7 +203,7 @@ export default function NewsArticleList({ kind, typeId }: Props) {
|
||||||
<Pencil className="w-4 h-4" />
|
<Pencil className="w-4 h-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
{levelId === "3" && (
|
{isContributorRole(levelId) && (
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@ import { getArticleByCategory, getArticlePagination } from "@/service/article";
|
||||||
import { formatDate } from "@/utils/global";
|
import { formatDate } from "@/utils/global";
|
||||||
import { close, loading } from "@/config/swal";
|
import { close, loading } from "@/config/swal";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import { isContributorRole } from "@/constants/user-roles";
|
||||||
|
|
||||||
export default function NewsImage() {
|
export default function NewsImage() {
|
||||||
const [articles, setArticles] = useState<any[]>([]);
|
const [articles, setArticles] = useState<any[]>([]);
|
||||||
|
|
@ -30,8 +31,7 @@ export default function NewsImage() {
|
||||||
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
const [selectedCategory, setSelectedCategory] = useState<number | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const ulne = Cookies.get("ulne");
|
setLevelId(Cookies.get("urie"));
|
||||||
setLevelId(ulne);
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -127,7 +127,7 @@ export default function NewsImage() {
|
||||||
Create and manage news articles and blog posts
|
Create and manage news articles and blog posts
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{levelId === "3" && (
|
{isContributorRole(levelId) && (
|
||||||
<Link href={"/admin/news-article/image/create"}>
|
<Link href={"/admin/news-article/image/create"}>
|
||||||
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
<Button className="bg-blue-600 hover:bg-blue-700 rounded-lg">
|
||||||
<Plus className="w-4 h-4 mr-2" />
|
<Plus className="w-4 h-4 mr-2" />
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,16 @@
|
||||||
|
/** Matches cookie `urie` / profile `userRoleId`. */
|
||||||
|
export const USER_ROLE_ADMIN = "1";
|
||||||
|
export const USER_ROLE_APPROVER = "2";
|
||||||
|
export const USER_ROLE_CONTRIBUTOR = "3";
|
||||||
|
|
||||||
|
export function isApproverOrAdmin(
|
||||||
|
roleId: string | undefined | null,
|
||||||
|
): boolean {
|
||||||
|
return roleId === USER_ROLE_ADMIN || roleId === USER_ROLE_APPROVER;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isContributorRole(
|
||||||
|
roleId: string | undefined | null,
|
||||||
|
): boolean {
|
||||||
|
return roleId === USER_ROLE_CONTRIBUTOR;
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue