From 71c65f9a6057b7fe027176e0a91e196c820b79ed Mon Sep 17 00:00:00 2001 From: hanif salafi Date: Tue, 14 Apr 2026 10:27:00 +0700 Subject: [PATCH] feat: fixing role id usage, fixing content website, etc --- .env | 4 +- app/(admin)/admin/content-website/page.tsx | 11 +- app/layout.tsx | 32 ++- app/page.tsx | 17 ++ components/form/article/edit-article-form.tsx | 11 +- .../landing-page/retracting-sidedar.tsx | 73 +---- components/main/content-website-approver.tsx | 269 ++++++++++++------ components/main/content-website.tsx | 129 +++++++-- .../main/dashboard/dashboard-container.tsx | 4 +- components/main/my-content.tsx | 10 +- components/main/news-article-list.tsx | 8 +- components/main/news-image.tsx | 6 +- constants/user-roles.ts | 16 ++ 13 files changed, 384 insertions(+), 206 deletions(-) create mode 100644 constants/user-roles.ts diff --git a/.env b/.env index 41dfb31..5e5e04b 100644 --- a/.env +++ b/.env @@ -1,3 +1,3 @@ MEDOLS_CLIENT_KEY=bb65b1ad-e954-4a1a-b4d0-74df5bb0b640 -NEXT_PUBLIC_API_URL=http://localhost:8800 -# NEXT_PUBLIC_API_URL=https://qudo.id/api \ No newline at end of file +# NEXT_PUBLIC_API_URL=http://localhost:8800 +NEXT_PUBLIC_API_URL=https://qudo.id/api \ No newline at end of file diff --git a/app/(admin)/admin/content-website/page.tsx b/app/(admin)/admin/content-website/page.tsx index 1be1930..92d64eb 100644 --- a/app/(admin)/admin/content-website/page.tsx +++ b/app/(admin)/admin/content-website/page.tsx @@ -2,6 +2,10 @@ import Cookies from "js-cookie"; import ContentWebsite from "@/components/main/content-website"; +import { + isApproverOrAdmin, + isContributorRole, +} from "@/constants/user-roles"; import { motion } from "framer-motion"; import { useEffect, useState } from "react"; @@ -13,8 +17,7 @@ export default function ContentWebsitePage() { useEffect(() => { setMounted(true); - const ulne = Cookies.get("ulne"); - setLevelId(ulne); + setLevelId(Cookies.get("urie")); }, []); if (!mounted) { @@ -33,10 +36,10 @@ export default function ContentWebsitePage() { transition={{ duration: 0.3 }} >
- {levelId === "3" ? ( + {isApproverOrAdmin(levelId) ? ( ) : ( - + )}
diff --git a/app/layout.tsx b/app/layout.tsx index f7fa87e..e5a1204 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -12,9 +12,35 @@ const geistMono = Geist_Mono({ 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 = { - title: "Create Next App", - description: "Generated by create next app", + metadataBase: new URL("https://qudo.id"), + 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({ @@ -23,7 +49,7 @@ export default function RootLayout({ children: React.ReactNode; }>) { return ( - + diff --git a/app/page.tsx b/app/page.tsx index 8ef069c..6d4f3c1 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -1,3 +1,4 @@ +import type { Metadata } from "next"; import Header from "@/components/landing-page/headers"; import AboutSection from "@/components/landing-page/about"; import ProductSection from "@/components/landing-page/product"; @@ -15,6 +16,22 @@ import type { CmsServiceContent, } 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() { const [hero, aboutList, productList, serviceList, partners, popupList] = await Promise.all([ diff --git a/components/form/article/edit-article-form.tsx b/components/form/article/edit-article-form.tsx index 5982a48..6ff6933 100644 --- a/components/form/article/edit-article-form.tsx +++ b/components/form/article/edit-article-form.tsx @@ -18,6 +18,10 @@ import { useParams, useRouter } from "next/navigation"; import GetSeoScore from "./get-seo-score-form"; import Link from "next/link"; import Cookies from "js-cookie"; +import { + isApproverOrAdmin, + isContributorRole, +} from "@/constants/user-roles"; import { createArticleSchedule, deleteArticleFiles, @@ -162,8 +166,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { const [levelId, setLevelId] = useState(); useEffect(() => { - const ulne = Cookies.get("ulne"); - setLevelId(ulne); + setLevelId(Cookies.get("urie")); }, []); const { getRootProps, getInputProps } = useDropzone({ @@ -812,7 +815,7 @@ export default function EditArticleForm(props: { isDetail: boolean }) { {/* ================= ACTION BUTTON ================= */}
- {levelId === "2" && !detailData?.isPublish && ( + {isApproverOrAdmin(levelId) && !detailData?.isPublish && ( <> -
+
+
+

+ Perubahan menunggu persetujuan +

+ + {filtered.length} pending + +
+

+ Setujui atau tolak di sini. Gunakan{" "} + Lihat konten live{" "} + untuk membuka tab yang sama dengan bagian yang diajukan. +

- - - {loading ? ( -
- -
- ) : ( - - - - Judul - Bagian - Pengaju - Tanggal - Aksi - - - - {filtered.length === 0 ? ( - - - Tidak ada pengajuan tertunda. - +
+ setSearch(e.target.value)} + className="max-w-md" + /> + +
+ + + + {loading ? ( +
+ +
+ ) : ( +
+ + + Judul + Bagian + Pengaju + Tanggal + Aksi - ) : ( - filtered.map((item) => ( - - - {item.title} - - - - {DOMAIN_LABEL[item.domain] ?? item.domain} - - - - {item.submitter_name || `User #${item.submitted_by_id}`} - - - {formatDate(item.created_at)} - - - - + + + {filtered.length === 0 ? ( + + + Tidak ada pengajuan tertunda. - )) - )} - -
- )} -
-
+ ) : ( + filtered.map((item) => ( + + + {item.title} + + + + {DOMAIN_LABEL[item.domain] ?? item.domain} + + + + {item.submitter_name || + `User #${item.submitted_by_id}`} + + + {formatDate(item.created_at)} + + +
+ + + +
+
+
+ )) + )} + + + )} + + +
+ +
+
+

+ Konten live (semua tab, hanya lihat) +

+

+ Data yang sedang ditampilkan di website. Field tidak dapat diubah di + sini — bandingkan dengan baris pengajuan di atas sebelum + menyetujui. +

+
+ +
); } diff --git a/components/main/content-website.tsx b/components/main/content-website.tsx index 929a2a4..90dc169 100644 --- a/components/main/content-website.tsx +++ b/components/main/content-website.tsx @@ -91,16 +91,33 @@ function setPickedFile( type ContentWebsiteProps = { /** User level 2: changes go through approval instead of live CMS APIs. */ 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({ contributorMode = false, + viewOnly = false, + hideHeader = false, + tabFocusSignal = 0, + tabFocusTarget = "", + liveDataReloadSignal = 0, }: ContentWebsiteProps) { const [activeTab, setActiveTab] = useState("hero"); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [editMode, setEditMode] = useState(!contributorMode); - const canInteract = !contributorMode || editMode; + const canInteract = (!contributorMode || editMode) && !viewOnly; + const dimContributorPreview = + contributorMode && !editMode && !viewOnly; const [heroId, setHeroId] = useState(null); const [heroImageId, setHeroImageId] = useState(null); @@ -262,6 +279,24 @@ export default function ContentWebsite({ 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() { if (!heroPrimary.trim()) { await Swal.fire({ icon: "warning", title: "Main title is required" }); @@ -1239,14 +1274,42 @@ export default function ContentWebsite({ return (
-
-
-

Content Website

-

- Update homepage content, products, services, and partners. -

+ {!hideHeader ? ( +
+
+

+ Content Website +

+

+ {viewOnly + ? "Konten live di semua tab: hanya lihat. Pengajuan perubahan ditangani di bagian atas halaman." + : "Update homepage content, products, services, and partners."} +

+
+
+ + {contributorMode ? ( + + ) : null} +
-
+ ) : viewOnly ? ( +
- {contributorMode ? ( - - ) : null}
-
+ ) : null} - {contributorMode && !editMode ? ( + {contributorMode && !editMode && !viewOnly ? (

Aktifkan Edit Mode untuk mengusulkan perubahan. Perubahan akan masuk ke{" "} My Content menunggu persetujuan approver. Unggah file gambar tidak tersedia sebagai kontributor; @@ -1279,7 +1332,9 @@ export default function ContentWebsite({

@@ -1305,6 +1360,10 @@ export default function ContentWebsite({ +
@@ -1415,9 +1474,14 @@ export default function ContentWebsite({ +
+
@@ -1562,9 +1626,14 @@ export default function ContentWebsite({ +
+
@@ -1752,9 +1821,14 @@ export default function ContentWebsite({ +
+
@@ -1942,9 +2016,14 @@ export default function ContentWebsite({ +
+
@@ -2102,9 +2181,14 @@ export default function ContentWebsite({ +
+
@@ -2295,6 +2379,7 @@ export default function ContentWebsite({ +
diff --git a/components/main/dashboard/dashboard-container.tsx b/components/main/dashboard/dashboard-container.tsx index cad2d38..a509fe6 100644 --- a/components/main/dashboard/dashboard-container.tsx +++ b/components/main/dashboard/dashboard-container.tsx @@ -54,8 +54,8 @@ interface PostCount { export default function DashboardContainer() { const [levelName, setLevelName] = useState(); useEffect(() => { - const levelId = Cookies.get("ulne"); - setLevelName(levelId); + const roleId = Cookies.get("urie"); + setLevelName(roleId); }, []); const username = Cookies.get("username"); diff --git a/components/main/my-content.tsx b/components/main/my-content.tsx index 81bb57b..0debb77 100644 --- a/components/main/my-content.tsx +++ b/components/main/my-content.tsx @@ -13,6 +13,10 @@ import { listCmsContentSubmissions } from "@/service/cms-content-submissions"; import { getArticlesForMyContent } from "@/service/article"; import { apiPayload } from "@/service/cms-landing"; import { Loader2 } from "lucide-react"; +import { + isApproverOrAdmin, + isContributorRole, +} from "@/constants/user-roles"; const PLACEHOLDER_IMG = "https://placehold.co/400x240/f1f5f9/64748b?text=Content"; @@ -149,11 +153,11 @@ export default function MyContent() { const [articleRows, setArticleRows] = useState([]); const [page, setPage] = useState(1); - const isApprover = levelId === "3"; - const isContributor = levelId === "2"; + const isContributor = isContributorRole(levelId); + const isApprover = isApproverOrAdmin(levelId); useEffect(() => { - setLevelId(Cookies.get("ulne")); + setLevelId(Cookies.get("urie")); }, []); useEffect(() => { diff --git a/components/main/news-article-list.tsx b/components/main/news-article-list.tsx index 145aebe..0a4cd61 100644 --- a/components/main/news-article-list.tsx +++ b/components/main/news-article-list.tsx @@ -19,6 +19,7 @@ import { getArticlePagination, deleteArticle } from "@/service/article"; import { formatDate } from "@/utils/global"; import { close, loading } from "@/config/swal"; import Cookies from "js-cookie"; +import { isContributorRole } from "@/constants/user-roles"; import Swal from "sweetalert2"; import type { ArticleContentKind } 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(); useEffect(() => { - const ulne = Cookies.get("ulne"); - setLevelId(ulne); + setLevelId(Cookies.get("urie")); }, []); useEffect(() => { @@ -128,7 +128,7 @@ export default function NewsArticleList({ kind, typeId }: Props) { Create and manage {label.toLowerCase()} articles. Organize with tags only.

- {levelId === "3" && ( + {isContributorRole(levelId) && ( - {levelId === "3" && ( + {isContributorRole(levelId) && (
- {levelId === "3" && ( + {isContributorRole(levelId) && (