feat: update create form content and public landing

This commit is contained in:
hanif salafi 2025-09-17 09:47:48 +07:00
parent d07e92aa2a
commit 5fdcfdfdb9
12 changed files with 936 additions and 292 deletions

View File

@ -41,11 +41,18 @@ import { Item } from "@radix-ui/react-dropdown-menu";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getCsrfToken } from "@/service/auth"; import { getCsrfToken } from "@/service/auth";
import { import {
listEnableCategory,
getTagsBySubCategoryId,
createMedia, createMedia,
createArticle,
getTagsBySubCategoryId,
listEnableCategory,
listArticleCategories,
uploadThumbnail, uploadThumbnail,
} from "@/service/content"; uploadArticleFiles,
uploadArticleThumbnail,
CreateArticleData,
} from "@/service/content/content";
import { request } from "http";
import { toast } from "sonner";
import { htmlToString } from "@/utils/globals"; import { htmlToString } from "@/utils/globals";
import { import {
generateDataArticle, generateDataArticle,
@ -54,6 +61,7 @@ import {
getGenerateKeywords, getGenerateKeywords,
getGenerateTitle, getGenerateTitle,
} from "@/service/content/ai"; } from "@/service/content/ai";
import Link from "next/link";
const CustomEditor = dynamic( const CustomEditor = dynamic(
() => { () => {
@ -188,27 +196,30 @@ export default function FormVideo() {
}); });
const videoSchema = z.object({ const videoSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "titleRequired" }),
description: z.string().optional(), description: z.string().optional(),
descriptionOri: z.string().optional(), descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(), rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }), creatorName: z.string().min(1, { message: "creatorRequired" }),
category: z.string().min(1, { message: "Kategori harus dipilih" }),
tags: z
.array(z.string().min(1))
.min(1, { message: "Minimal 1 tag diperlukan" }),
files: z files: z
.array(z.any()) .array(z.any())
.min(1, { message: "File video harus dipilih" }) .min(1, { message: "Minimal 1 file harus diunggah." })
.refine( .refine(
(files) => (files) =>
files.every((file: File) => ACCEPTED_FILE_TYPES.includes(file.type)), files.every(
{ message: "File harus berformat mp4 atau mov" } (file: File) =>
) ["video/mp4", "video/mov", "video/avi"].includes(file.type) &&
.refine( file.size <= 100 * 1024 * 1024
(files) => files.every((file: File) => file.size <= MAX_FILE_SIZE), ),
{ message: "Ukuran file maksimal 100 MB" } {
message:
"Hanya file .mp4, .mov, .avi, maksimal 100MB yang diperbolehkan.",
}
), ),
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
tags: z
.array(z.string())
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
publishedFor: z publishedFor: z
.array(z.string()) .array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }), .min(1, { message: "Minimal 1 target publish harus dipilih." }),
@ -227,8 +238,9 @@ export default function FormVideo() {
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
category: "", creatorName: "",
files: [], files: [],
categoryId: "",
tags: [], tags: [],
publishedFor: [], publishedFor: [],
}, },
@ -463,11 +475,30 @@ export default function FormVideo() {
const getCategories = async () => { const getCategories = async () => {
try { try {
const category = await listEnableCategory(fileTypeId); // Use new Article Categories API
const resCategory: Category[] = category?.data?.data?.content; const category = await listArticleCategories(1, 100);
console.log("Article categories response:", category);
if (category?.error) {
console.error("Failed to fetch article categories:", category.message);
// Fallback to old API if new one fails
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
setCategories(resCategory);
return;
}
// Handle new API response structure
const resCategory: Category[] = category?.data?.data?.map((item: any) => ({
id: item.id,
name: item.title, // map title to name for backward compatibility
title: item.title,
description: item.description,
...item
})) || [];
setCategories(resCategory); setCategories(resCategory);
console.log("data category", resCategory); console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") { if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) => const findCategory = resCategory.find((o) =>
@ -475,7 +506,6 @@ export default function FormVideo() {
); );
if (findCategory) { if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id); const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data); setTags(response?.data?.data);
@ -483,6 +513,14 @@ export default function FormVideo() {
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch categories:", error); console.error("Failed to fetch categories:", error);
// Fallback to old API if error occurs
try {
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
setCategories(resCategory);
} catch (fallbackError) {
console.error("Fallback category fetch also failed:", fallbackError);
}
} }
}; };
@ -578,39 +616,98 @@ export default function FormVideo() {
} }
if (id == undefined) { if (id == undefined) {
const response = await createMedia(requestData); // New Articles API request data structure
console.log("Form Data Submitted:", requestData); const articleData: CreateArticleData = {
title: finalTitle,
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
categoryIds: selectedCategory.toString(),
typeId: 4, // Video content type
tags: finalTags,
isDraft: true,
isPublish: false,
oldId: 0,
slug: finalTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
};
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
if (response?.error) { if (response?.error) {
MySwal.fire("Error", response?.message, "error"); MySwal.fire("Error", response.message || "Failed to create article", "error");
return; return false;
} }
Cookies.set("idCreate", response?.data?.data, { expires: 1 });
id = response?.data?.data;
if (thumbnail) { // Get the article ID from the new API response
const formMedia = new FormData(); const articleId = response?.data?.data?.id;
formMedia.append("file", thumbnail); Cookies.set("idCreate", articleId, { expires: 1 });
const responseThumbnail = await uploadThumbnail(id, formMedia); id = articleId;
if (responseThumbnail?.error) {
error(responseThumbnail.message); // Upload files using new article-files API
const formData = new FormData();
// Add all files to FormData
files.forEach((file, index) => {
formData.append('files', file);
});
console.log("Uploading files to article:", articleId);
console.log("Files to upload:", files.length);
try {
const uploadResponse = await uploadArticleFiles(articleId, formData);
if (uploadResponse?.error) {
MySwal.fire("Error", uploadResponse.message || "Failed to upload files", "error");
return false; return false;
} }
}
}
const progressInfoArr = [];
for (const item of files) {
progressInfoArr.push({ percentage: 0, fileName: item.name });
}
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close(); console.log("Files uploaded successfully:", uploadResponse);
// showProgress();
files.map(async (item: any, index: number) => { // Upload thumbnail using first file as thumbnail
await uploadResumableFile(index, String(id), item, "0"); if (files.length > 0) {
}); const thumbnailFormData = new FormData();
thumbnailFormData.append('files', files[0]); // Use first file as thumbnail
console.log("Uploading thumbnail for article:", articleId);
try {
const thumbnailResponse = await uploadArticleThumbnail(articleId, thumbnailFormData);
if (thumbnailResponse?.error) {
console.warn("Thumbnail upload failed:", thumbnailResponse.message);
// Don't fail the whole process if thumbnail upload fails
} else {
console.log("Thumbnail uploaded successfully:", thumbnailResponse);
}
} catch (thumbnailError) {
console.warn("Thumbnail upload error:", thumbnailError);
// Don't fail the whole process if thumbnail upload fails
}
}
} catch (uploadError) {
console.error("Upload error:", uploadError);
MySwal.fire("Error", "Failed to upload files. Please try again.", "error");
return false;
}
// Show success message
MySwal.fire({
title: "Sukses",
text: "Article dan files berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/video");
});
Cookies.remove("idCreate");
return;
}
Cookies.remove("idCreate"); Cookies.remove("idCreate");
}; };
@ -885,37 +982,40 @@ export default function FormVideo() {
<Label>Category</Label> <Label>Category</Label>
<Controller <Controller
control={control} control={control}
name="category" name="categoryId"
render={({ field }) => ( render={({ field }) => (
<Select <div className="w-full">
value={field.value} <Label>Category</Label>
onValueChange={(id) => { <Select
field.onChange(id); value={field.value}
console.log("Selected Category ID:", id); onValueChange={(value) => {
setSelectedCategory(id); // tetap set ini kalau mau field.onChange(value);
}} setSelectedCategory(value);
> }}
<SelectTrigger> >
<SelectValue placeholder="Pilih" /> <SelectTrigger>
</SelectTrigger> <SelectValue placeholder="Pilih" />
<SelectContent> </SelectTrigger>
{categories.map((category) => ( <SelectContent>
<SelectItem {categories.map((category) => (
key={category.id} <SelectItem
value={category.id.toString()} key={category.id}
> value={category.id.toString()}
{category.name} >
</SelectItem> {category.name}
))} </SelectItem>
</SelectContent> ))}
</Select> </SelectContent>
</Select>
{errors.categoryId && (
<p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p>
)}
</div>
)} )}
/> />
{errors.category && (
<p className="text-red-500 text-sm">
{errors.category.message}
</p>
)}
</div> </div>
</div> </div>
<div className="flex flex-row items-center gap-3 py-2"> <div className="flex flex-row items-center gap-3 py-2">

View File

@ -32,9 +32,14 @@ import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { import {
createMedia, createMedia,
createArticle,
getTagsBySubCategoryId, getTagsBySubCategoryId,
listEnableCategory, listEnableCategory,
listArticleCategories,
uploadThumbnail, uploadThumbnail,
uploadArticleFiles,
uploadArticleThumbnail,
CreateArticleData,
} from "@/service/content/content"; } from "@/service/content/content";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
@ -54,6 +59,8 @@ import { Item } from "@radix-ui/react-dropdown-menu";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getCsrfToken } from "@/service/auth"; import { getCsrfToken } from "@/service/auth";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
import { request } from "http";
import { toast } from "sonner";
import { htmlToString } from "@/utils/globals"; import { htmlToString } from "@/utils/globals";
import Link from "next/link"; import Link from "next/link";
@ -127,7 +134,7 @@ export default function FormAudio() {
polres: false, polres: false,
}); });
let fileTypeId = "4"; let fileTypeId = "3";
let progressInfo: any = []; let progressInfo: any = [];
let counterUpdateProgress = 0; let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]); const [progressList, setProgressList] = useState<any>([]);
@ -197,18 +204,30 @@ export default function FormAudio() {
}; };
const audioSchema = z.object({ const audioSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "titleRequired" }),
description: z.string().optional(), description: z.string().optional(),
descriptionOri: z.string().optional(), descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(), rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }), creatorName: z.string().min(1, { message: "creatorRequired" }),
category: z.string().min(1, { message: "Category diperlukan" }),
files: z files: z
.array(z.any()) .array(z.any())
.min(1, { message: "Wajib upload minimal 1 file mp3 atau wav" }), .min(1, { message: "Minimal 1 file harus diunggah." })
.refine(
(files) =>
files.every(
(file: File) =>
["audio/mpeg", "audio/wav", "audio/mp3"].includes(file.type) &&
file.size <= 100 * 1024 * 1024
),
{
message:
"Hanya file .mp3, .wav, maksimal 100MB yang diperbolehkan.",
}
),
categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
tags: z tags: z
.array(z.string().min(1)) .array(z.string())
.min(1, { message: "Wajib isi minimal 1 tag" }), .min(1, { message: "Minimal 1 tag harus ditambahkan." }),
publishedFor: z publishedFor: z
.array(z.string()) .array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }), .min(1, { message: "Minimal 1 target publish harus dipilih." }),
@ -227,7 +246,9 @@ export default function FormAudio() {
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
category: "", creatorName: "",
files: [],
categoryId: "",
tags: [], tags: [],
publishedFor: [], publishedFor: [],
}, },
@ -456,11 +477,30 @@ export default function FormAudio() {
const getCategories = async () => { const getCategories = async () => {
try { try {
const category = await listEnableCategory(fileTypeId); // Use new Article Categories API
const resCategory: Category[] = category?.data?.data?.content; const category = await listArticleCategories(1, 100);
console.log("Article categories response:", category);
if (category?.error) {
console.error("Failed to fetch article categories:", category.message);
// Fallback to old API if new one fails
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
setCategories(resCategory);
return;
}
// Handle new API response structure
const resCategory: Category[] = category?.data?.data?.map((item: any) => ({
id: item.id,
name: item.title, // map title to name for backward compatibility
title: item.title,
description: item.description,
...item
})) || [];
setCategories(resCategory); setCategories(resCategory);
console.log("data category", resCategory); console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") { if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) => const findCategory = resCategory.find((o) =>
@ -468,7 +508,6 @@ export default function FormAudio() {
); );
if (findCategory) { if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id); const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data); setTags(response?.data?.data);
@ -476,6 +515,14 @@ export default function FormAudio() {
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch categories:", error); console.error("Failed to fetch categories:", error);
// Fallback to old API if error occurs
try {
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
setCategories(resCategory);
} catch (fallbackError) {
console.error("Fallback category fetch also failed:", fallbackError);
}
} }
}; };
@ -567,43 +614,100 @@ export default function FormAudio() {
} }
if (id == undefined) { if (id == undefined) {
const response = await createMedia(requestData); // New Articles API request data structure
console.log("Form Data Submitted:", requestData); const articleData: CreateArticleData = {
title: finalTitle,
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
categoryIds: selectedCategory.toString(),
typeId: 3, // Audio content type
tags: finalTags,
isDraft: true,
isPublish: false,
oldId: 0,
slug: finalTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
};
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
if (response?.error) { if (response?.error) {
MySwal.fire("Error", response?.message, "error"); MySwal.fire("Error", response.message || "Failed to create article", "error");
return;
}
Cookies.set("idCreate", response?.data?.data, { expires: 1 });
id = response?.data?.data;
const formMedia = new FormData();
console.log("Thumbnail : ", files[0]);
formMedia.append("file", files[0]);
const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error == true) {
error(responseThumbnail?.message);
return false; return false;
} }
}
const progressInfoArr = []; // Get the article ID from the new API response
for (const item of files) { const articleId = response?.data?.data?.id;
progressInfoArr.push({ percentage: 0, fileName: item.name }); Cookies.set("idCreate", articleId, { expires: 1 });
} id = articleId;
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close(); // Upload files using new article-files API
// showProgress(); const formData = new FormData();
files.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "0"); // Add all files to FormData
}); files.forEach((file, index) => {
formData.append('files', file);
});
console.log("Uploading files to article:", articleId);
console.log("Files to upload:", files.length);
try {
const uploadResponse = await uploadArticleFiles(articleId, formData);
if (uploadResponse?.error) {
MySwal.fire("Error", uploadResponse.message || "Failed to upload files", "error");
return false;
}
console.log("Files uploaded successfully:", uploadResponse);
// Upload thumbnail using first file as thumbnail
if (files.length > 0) {
const thumbnailFormData = new FormData();
thumbnailFormData.append('files', files[0]); // Use first file as thumbnail
console.log("Uploading thumbnail for article:", articleId);
try {
const thumbnailResponse = await uploadArticleThumbnail(articleId, thumbnailFormData);
if (thumbnailResponse?.error) {
console.warn("Thumbnail upload failed:", thumbnailResponse.message);
// Don't fail the whole process if thumbnail upload fails
} else {
console.log("Thumbnail uploaded successfully:", thumbnailResponse);
}
} catch (thumbnailError) {
console.warn("Thumbnail upload error:", thumbnailError);
// Don't fail the whole process if thumbnail upload fails
}
}
} catch (uploadError) {
console.error("Upload error:", uploadError);
MySwal.fire("Error", "Failed to upload files. Please try again.", "error");
return false;
}
// Show success message
MySwal.fire({
title: "Sukses",
text: "Article dan files berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/audio");
});
Cookies.remove("idCreate");
return;
}
Cookies.remove("idCreate"); Cookies.remove("idCreate");
// MySwal.fire("Sukses", "Data berhasil disimpan.", "success");
}; };
const onSubmit = (data: AudioSchema) => { const onSubmit = (data: AudioSchema) => {
@ -867,17 +971,16 @@ export default function FormAudio() {
<div className="py-3 w-full space-y-2"> <div className="py-3 w-full space-y-2">
<Label>Category</Label> <Label>Category</Label>
<Controller <Controller
name="category"
control={control} control={control}
rules={{ required: "Category is required" }} name="categoryId"
render={({ field, fieldState }) => ( render={({ field }) => (
<> <div className="w-full">
<Label>Category</Label>
<Select <Select
value={field.value} value={field.value}
onValueChange={(id) => { onValueChange={(value) => {
field.onChange(id); field.onChange(value);
console.log("Selected Category ID:", id); setSelectedCategory(value);
setSelectedCategory(id);
}} }}
> >
<SelectTrigger> <SelectTrigger>
@ -894,12 +997,13 @@ export default function FormAudio() {
))} ))}
</SelectContent> </SelectContent>
</Select> </Select>
{fieldState.error && (
<p className="text-sm text-red-500"> {errors.categoryId && (
{fieldState.error.message} <p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p> </p>
)} )}
</> </div>
)} )}
/> />
</div> </div>

View File

@ -30,9 +30,14 @@ import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie"; import Cookies from "js-cookie";
import { import {
createMedia, createMedia,
createArticle,
getTagsBySubCategoryId, getTagsBySubCategoryId,
listEnableCategory, listEnableCategory,
listArticleCategories,
uploadThumbnail, uploadThumbnail,
uploadArticleFiles,
uploadArticleThumbnail,
CreateArticleData,
} from "@/service/content/content"; } from "@/service/content/content";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import { import {
@ -50,6 +55,8 @@ import Image from "next/image";
import { error, loading } from "@/config/swal"; import { error, loading } from "@/config/swal";
import dynamic from "next/dynamic"; import dynamic from "next/dynamic";
import { getCsrfToken } from "@/service/auth"; import { getCsrfToken } from "@/service/auth";
import { request } from "http";
import { toast } from "sonner";
import { htmlToString } from "@/utils/globals"; import { htmlToString } from "@/utils/globals";
import Link from "next/link"; import Link from "next/link";
@ -125,7 +132,7 @@ export default function FormTeks() {
polres: false, polres: false,
}); });
let fileTypeId = "3"; let fileTypeId = "2";
let progressInfo: any = []; let progressInfo: any = [];
let counterUpdateProgress = 0; let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]); const [progressList, setProgressList] = useState<any>([]);
@ -178,38 +185,34 @@ export default function FormTeks() {
}); });
const teksSchema = z.object({ const teksSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }), title: z.string().min(1, { message: "titleRequired" }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }), description: z.string().optional(),
category: z.string().min(1, { message: "Kategori harus dipilih" }), descriptionOri: z.string().optional(),
tags: z rewriteDescription: z.string().optional(),
.array(z.string()) creatorName: z.string().min(1, { message: "creatorRequired" }),
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
files: z files: z
.array( .array(z.any())
z .min(1, { message: "Minimal 1 file harus diunggah." })
.object({ .refine(
name: z.string(), (files) =>
type: z.string(), files.every(
size: z (file: File) =>
.number()
.max(20 * 1024 * 1024, { message: "Max file size 20 MB" }),
})
.refine(
(file) =>
[ [
"application/pdf", "application/pdf",
"application/msword", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint", "text/plain",
"application/vnd.openxmlformats-officedocument.presentationml.presentation", ].includes(file.type) && file.size <= 100 * 1024 * 1024
].includes(file.type), ),
{ message: "Format file tidak didukung" } {
) message:
) "Hanya file .pdf, .doc, .docx, .txt, maksimal 100MB yang diperbolehkan.",
.min(1, { message: "File wajib diunggah" }), }
description: z.string().optional(), ),
descriptionOri: z.string().optional(), categoryId: z.string().min(1, { message: "Kategori wajib dipilih." }),
rewriteDescription: z.string().optional(), tags: z
.array(z.string())
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
publishedFor: z publishedFor: z
.array(z.string()) .array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }), .min(1, { message: "Minimal 1 target publish harus dipilih." }),
@ -226,13 +229,13 @@ export default function FormTeks() {
resolver: zodResolver(teksSchema), resolver: zodResolver(teksSchema),
defaultValues: { defaultValues: {
title: "", title: "",
creatorName: "",
category: "",
tags: [],
files: [],
description: "", description: "",
descriptionOri: "", descriptionOri: "",
rewriteDescription: "", rewriteDescription: "",
creatorName: "",
files: [],
categoryId: "",
tags: [],
publishedFor: [], publishedFor: [],
}, },
}); });
@ -470,11 +473,30 @@ export default function FormTeks() {
const getCategories = async () => { const getCategories = async () => {
try { try {
const category = await listEnableCategory(fileTypeId); // Use new Article Categories API
const resCategory: Category[] = category?.data?.data?.content; const category = await listArticleCategories(1, 100);
console.log("Article categories response:", category);
if (category?.error) {
console.error("Failed to fetch article categories:", category.message);
// Fallback to old API if new one fails
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
setCategories(resCategory);
return;
}
// Handle new API response structure
const resCategory: Category[] = category?.data?.data?.map((item: any) => ({
id: item.id,
name: item.title, // map title to name for backward compatibility
title: item.title,
description: item.description,
...item
})) || [];
setCategories(resCategory); setCategories(resCategory);
console.log("data category", resCategory); console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") { if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) => const findCategory = resCategory.find((o) =>
@ -482,14 +504,21 @@ export default function FormTeks() {
); );
if (findCategory) { if (findCategory) {
// setValue("categoryId", findCategory.id); setSelectedCategory(findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
const response = await getTagsBySubCategoryId(findCategory.id); const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data); setTags(response?.data?.data);
} }
} }
} catch (error) { } catch (error) {
console.error("Failed to fetch categories:", error); console.error("Failed to fetch categories:", error);
// Fallback to old API if error occurs
try {
const fallbackCategory = await listEnableCategory(fileTypeId);
const resCategory: Category[] = fallbackCategory?.data.data.content || [];
setCategories(resCategory);
} catch (fallbackError) {
console.error("Fallback category fetch also failed:", fallbackError);
}
} }
}; };
@ -589,37 +618,98 @@ export default function FormTeks() {
} }
if (id == undefined) { if (id == undefined) {
const response = await createMedia(requestData); // New Articles API request data structure
console.log("Form Data Submitted:", requestData); const articleData: CreateArticleData = {
title: finalTitle,
description: htmlToString(finalDescription),
htmlDescription: finalDescription,
categoryIds: selectedCategory.toString(),
typeId: 2, // Document content type
tags: finalTags,
isDraft: true,
isPublish: false,
oldId: 0,
slug: finalTitle.toLowerCase().replace(/\s+/g, '-').replace(/[^a-z0-9-]/g, ''),
};
Cookies.set("idCreate", response?.data?.data, { expires: 1 }); // Use new Articles API
id = response?.data?.data; const response = await createArticle(articleData);
const formMedia = new FormData(); console.log("Article Data Submitted:", articleData);
const thumbnail = files[0]; console.log("Article API Response:", response);
formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia); if (response?.error) {
if (responseThumbnail?.error == true) { MySwal.fire("Error", response.message || "Failed to create article", "error");
error(responseThumbnail?.message);
return false; return false;
} }
}
const progressInfoArr = files.map((item) => ({
percentage: 0,
fileName: item.name,
}));
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close(); // Get the article ID from the new API response
files.map(async (item: any, index: number) => { const articleId = response?.data?.data?.id;
await uploadResumableFile( Cookies.set("idCreate", articleId, { expires: 1 });
index, id = articleId;
String(id),
item, // Upload files using new article-files API
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0" const formData = new FormData();
);
}); // Add all files to FormData
files.forEach((file, index) => {
formData.append('files', file);
});
console.log("Uploading files to article:", articleId);
console.log("Files to upload:", files.length);
try {
const uploadResponse = await uploadArticleFiles(articleId, formData);
if (uploadResponse?.error) {
MySwal.fire("Error", uploadResponse.message || "Failed to upload files", "error");
return false;
}
console.log("Files uploaded successfully:", uploadResponse);
// Upload thumbnail using first file as thumbnail
if (files.length > 0) {
const thumbnailFormData = new FormData();
thumbnailFormData.append('files', files[0]); // Use first file as thumbnail
console.log("Uploading thumbnail for article:", articleId);
try {
const thumbnailResponse = await uploadArticleThumbnail(articleId, thumbnailFormData);
if (thumbnailResponse?.error) {
console.warn("Thumbnail upload failed:", thumbnailResponse.message);
// Don't fail the whole process if thumbnail upload fails
} else {
console.log("Thumbnail uploaded successfully:", thumbnailResponse);
}
} catch (thumbnailError) {
console.warn("Thumbnail upload error:", thumbnailError);
// Don't fail the whole process if thumbnail upload fails
}
}
} catch (uploadError) {
console.error("Upload error:", uploadError);
MySwal.fire("Error", "Failed to upload files. Please try again.", "error");
return false;
}
// Show success message
MySwal.fire({
title: "Sukses",
text: "Article dan files berhasil disimpan.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then(() => {
router.push("/admin/content/document");
});
Cookies.remove("idCreate");
return;
}
Cookies.remove("idCreate"); Cookies.remove("idCreate");
}; };
@ -848,7 +938,7 @@ export default function FormTeks() {
<div className="flex flex-col lg:flex-row gap-10"> <div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12"> <Card className="w-full lg:w-8/12">
<div className="px-6 py-6"> <div className="px-6 py-6">
<p className="text-lg font-semibold mb-3">Form Text</p> <p className="text-lg font-semibold mb-3">Form Document</p>
<div className="gap-5 mb-5"> <div className="gap-5 mb-5">
{/* Input Title */} {/* Input Title */}
<div className="space-y-2 py-3"> <div className="space-y-2 py-3">
@ -873,38 +963,42 @@ export default function FormTeks() {
<div className="flex items-center"> <div className="flex items-center">
<div className="py-3 w-full space-y-2"> <div className="py-3 w-full space-y-2">
<Label>Category</Label> <Label>Category</Label>
<Controller <Controller
control={control} control={control}
name="category" name="categoryId"
render={({ field }) => ( render={({ field }) => (
<Select <div className="w-full">
value={field.value} <Label>Category</Label>
onValueChange={(val) => { <Select
field.onChange(val); value={field.value}
setSelectedCategory(val); onValueChange={(value) => {
}} field.onChange(value);
> setSelectedCategory(value);
<SelectTrigger> }}
<SelectValue placeholder="Pilih" /> >
</SelectTrigger> <SelectTrigger>
<SelectContent> <SelectValue placeholder="Pilih" />
{categories.map((category) => ( </SelectTrigger>
<SelectItem <SelectContent>
key={category.id} {categories.map((category) => (
value={category.id.toString()} <SelectItem
> key={category.id}
{category.name} value={category.id.toString()}
</SelectItem> >
))} {category.name}
</SelectContent> </SelectItem>
</Select> ))}
</SelectContent>
</Select>
{errors.categoryId && (
<p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p>
)} )}
/> </div>
{errors.category?.message && ( )}
<p className="text-red-400 text-sm"> />
{errors.category.message}
</p>
)}
</div> </div>
</div> </div>
<div className="flex flex-row items-center gap-3 py-2"> <div className="flex flex-row items-center gap-3 py-2">

View File

@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import { import {
getListContent, getListContent,
listData, listData,
listArticles,
listStaticBanner, listStaticBanner,
} from "@/service/landing/landing"; } from "@/service/landing/landing";
import { data } from "framer-motion/client"; import { data } from "framer-motion/client";
@ -17,25 +18,66 @@ export default function Header() {
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
try { try {
// const request = { // Use new Articles API
// group: "mabes", const response = await listArticles(1, 5, undefined, undefined, undefined, "createdAt");
// }; console.log("Articles API response:", response);
const response = await listData(
"", if (response?.error) {
"", console.error("Articles API failed, falling back to old API");
"", // Fallback to old API
5, const fallbackResponse = await listData(
0, "",
"createdAt", "",
"", "",
"", 5,
"" 0,
); "createdAt",
const content = response?.data?.data?.content || []; "",
console.log("data", content); "",
setData(content); ""
);
const content = fallbackResponse?.data?.data?.content || [];
setData(content);
return;
}
// Handle new API response structure
const articlesData = response?.data?.data || [];
console.log("Articles data:", articlesData);
// Transform articles data to match old structure for backward compatibility
const transformedData = articlesData.map((article: any) => ({
id: article.id,
title: article.title,
categoryName: article.categoryName || (article.categories && article.categories[0]?.title) || "",
createdAt: article.createdAt,
smallThumbnailLink: article.thumbnailUrl,
fileTypeId: article.typeId,
label: article.typeId === 1 ? "Image" : article.typeId === 2 ? "Video" : article.typeId === 3 ? "Text" : article.typeId === 4 ? "Audio" : "",
...article
}));
setData(transformedData);
} catch (error) { } catch (error) {
console.error("Gagal memuat data:", error); console.error("Gagal memuat data:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await listData(
"",
"",
"",
5,
0,
"createdAt",
"",
"",
""
);
const content = fallbackResponse?.data?.data?.content || [];
setData(content);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} }
}; };
@ -70,13 +112,13 @@ function Card({ item, isBig = false }: { item: any; isBig?: boolean }) {
const getLink = () => { const getLink = () => {
switch (item?.fileTypeId) { switch (item?.fileTypeId) {
case 1: case 1:
return `/public/content/image/detail/${item?.id}`; return `/content/image/detail/${item?.id}`;
case 2: case 2:
return `/public/content/video/detail/${item?.id}`; return `/content/video/detail/${item?.id}`;
case 3: case 3:
return `/public/content/text/detail/${item?.id}`; return `/content/text/detail/${item?.id}`;
case 4: case 4:
return `/public/content/audio/detail/${item?.id}`; return `/content/audio/detail/${item?.id}`;
default: default:
return "#"; // fallback kalau type tidak dikenali return "#"; // fallback kalau type tidak dikenali
} }

View File

@ -6,7 +6,7 @@ import { Button } from "@/components/ui/button";
import { ThumbsUp, ThumbsDown } from "lucide-react"; import { ThumbsUp, ThumbsDown } from "lucide-react";
import { Card } from "../ui/card"; import { Card } from "../ui/card";
import Link from "next/link"; import Link from "next/link";
import { listData } from "@/service/landing/landing"; import { listData, listArticles } from "@/service/landing/landing";
import { Swiper, SwiperSlide } from "swiper/react"; import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css"; import "swiper/css";
import "swiper/css/navigation"; import "swiper/css/navigation";
@ -42,20 +42,71 @@ export default function MediaUpdate() {
async function fetchData(section: "latest" | "popular") { async function fetchData(section: "latest" | "popular") {
try { try {
setLoading(true); setLoading(true);
const res = await listData(
"1", // Use new Articles API
"", const response = await listArticles(
"", 1,
20, 20,
0, 1, // typeId for images
section === "latest" ? "createdAt" : "clickCount", undefined,
"", undefined,
"", section === "latest" ? "createdAt" : "viewCount"
""
); );
setDataToRender(res?.data?.data?.content || []);
console.log("Media Update Articles API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackRes = await listData(
"1",
"",
"",
20,
0,
section === "latest" ? "createdAt" : "clickCount",
"",
"",
""
);
setDataToRender(fallbackRes?.data?.data?.content || []);
return;
}
// Handle new API response structure
const articlesData = response?.data?.data || [];
// Transform articles data to match old structure for backward compatibility
const transformedData = articlesData.map((article: any) => ({
id: article.id,
title: article.title,
category: article.categoryName || (article.categories && article.categories[0]?.title) || "Tanpa Kategori",
createdAt: article.createdAt,
smallThumbnailLink: article.thumbnailUrl,
label: article.typeId === 1 ? "Image" : article.typeId === 2 ? "Video" : article.typeId === 3 ? "Text" : article.typeId === 4 ? "Audio" : "",
...article
}));
setDataToRender(transformedData);
} catch (err) { } catch (err) {
console.error("Gagal memuat data:", err); console.error("Gagal memuat data:", err);
// Try fallback to old API if new API fails
try {
const fallbackRes = await listData(
"1",
"",
"",
20,
0,
section === "latest" ? "createdAt" : "clickCount",
"",
"",
""
);
setDataToRender(fallbackRes?.data?.data?.content || []);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }

View File

@ -15,7 +15,7 @@ import {
FaLink, FaLink,
FaShareAlt, FaShareAlt,
} from "react-icons/fa"; } from "react-icons/fa";
import { getDetail } from "@/service/landing/landing"; import { getDetail, getArticleDetail } from "@/service/landing/landing";
export default function AudioDetail({ id }: { id: string }) { export default function AudioDetail({ id }: { id: string }) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@ -60,14 +60,75 @@ export default function AudioDetail({ id }: { id: string }) {
const fetchDetail = async () => { const fetchDetail = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getDetail(id);
setData(response?.data?.data); // Try new Articles API first
console.log( const response = await getArticleDetail(id);
"doc", console.log("Article Detail API response:", response);
response?.data?.data.files[selectedDoc]?.secondaryUrl
); if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
return;
}
// Handle new API response structure
const articleData = response?.data?.data;
if (articleData) {
// Transform article data to match old structure for backward compatibility
const transformedData = {
id: articleData.id,
title: articleData.title,
description: articleData.description,
createdAt: articleData.createdAt,
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
secondaryUrl: file.file_url, // For audio files, use same URL
...file
})) || [],
...articleData
};
setData(transformedData);
console.log(
"doc",
transformedData.files[selectedDoc]?.secondaryUrl
);
}
} catch (error) { } catch (error) {
console.error("Error fetching detail:", error); console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -185,7 +246,7 @@ export default function AudioDetail({ id }: { id: string }) {
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"
@ -238,7 +299,7 @@ export default function AudioDetail({ id }: { id: string }) {
<span>SHARE</span> <span>SHARE</span>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"

View File

@ -15,7 +15,7 @@ import {
FaLink, FaLink,
FaShareAlt, FaShareAlt,
} from "react-icons/fa"; } from "react-icons/fa";
import { getDetail } from "@/service/landing/landing"; import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player"; import VideoPlayer from "@/utils/video-player";
import { toBase64, shimmer } from "@/utils/globals"; import { toBase64, shimmer } from "@/utils/globals";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@ -63,14 +63,75 @@ export default function DocumentDetail({ id }: { id: string }) {
const fetchDetail = async () => { const fetchDetail = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getDetail(id);
setData(response?.data?.data); // Try new Articles API first
console.log( const response = await getArticleDetail(id);
"doc", console.log("Article Detail API response:", response);
response?.data?.data.files[selectedDoc]?.secondaryUrl
); if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
return;
}
// Handle new API response structure
const articleData = response?.data?.data;
if (articleData) {
// Transform article data to match old structure for backward compatibility
const transformedData = {
id: articleData.id,
title: articleData.title,
description: articleData.description,
createdAt: articleData.createdAt,
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
secondaryUrl: file.file_url, // For document files, use same URL
...file
})) || [],
...articleData
};
setData(transformedData);
console.log(
"doc",
transformedData.files[selectedDoc]?.secondaryUrl
);
}
} catch (error) { } catch (error) {
console.error("Error fetching detail:", error); console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
console.log(
"doc",
fallbackResponse?.data?.data.files[selectedDoc]?.secondaryUrl
);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -189,7 +250,7 @@ export default function DocumentDetail({ id }: { id: string }) {
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"
@ -242,7 +303,7 @@ export default function DocumentDetail({ id }: { id: string }) {
<span>SHARE</span> <span>SHARE</span>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"

View File

@ -15,7 +15,7 @@ import {
FaLink, FaLink,
FaShareAlt, FaShareAlt,
} from "react-icons/fa"; } from "react-icons/fa";
import { getDetail } from "@/service/landing/landing"; import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player"; import VideoPlayer from "@/utils/video-player";
import { toBase64, shimmer } from "@/utils/globals"; import { toBase64, shimmer } from "@/utils/globals";
import { Skeleton } from "@/components/ui/skeleton"; import { Skeleton } from "@/components/ui/skeleton";
@ -63,10 +63,63 @@ export default function ImageDetail({ id }: { id: string }) {
const fetchDetail = async () => { const fetchDetail = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getDetail(id);
setData(response?.data?.data); // Try new Articles API first
const response = await getArticleDetail(id);
console.log("Article Detail API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
return;
}
// Handle new API response structure
const articleData = response?.data?.data;
if (articleData) {
// Transform article data to match old structure for backward compatibility
const transformedData = {
id: articleData.id,
title: articleData.title,
description: articleData.description,
createdAt: articleData.createdAt,
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
...file
})) || [],
};
console.log("transformedData : ", transformedData.files);
setData(transformedData);
}
} catch (error) { } catch (error) {
console.error("Error fetching detail:", error); console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -194,7 +247,7 @@ export default function ImageDetail({ id }: { id: string }) {
<div className="flex flex-col md:flex-row gap-6 mt-6"> <div className="flex flex-col md:flex-row gap-6 mt-6">
{/* Sidebar actions */} {/* Sidebar actions */}
<div className="hidden md:flex flex-col gap-4 relative z-10"> {/* <div className="hidden md:flex flex-col gap-4 relative z-10">
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Button <Button
onClick={handleCopyLink} onClick={handleCopyLink}
@ -229,7 +282,7 @@ export default function ImageDetail({ id }: { id: string }) {
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"
@ -250,7 +303,7 @@ export default function ImageDetail({ id }: { id: string }) {
COMMENT COMMENT
</Link> </Link>
</div> </div>
</div> </div> */}
{/* Content */} {/* Content */}
<div className="flex-1 space-y-4"> <div className="flex-1 space-y-4">
@ -282,7 +335,7 @@ export default function ImageDetail({ id }: { id: string }) {
<span>SHARE</span> <span>SHARE</span>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"

View File

@ -15,7 +15,7 @@ import {
FaLink, FaLink,
FaShareAlt, FaShareAlt,
} from "react-icons/fa"; } from "react-icons/fa";
import { getDetail } from "@/service/landing/landing"; import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player"; import VideoPlayer from "@/utils/video-player";
export default function VideoDetail({ id }: { id: string }) { export default function VideoDetail({ id }: { id: string }) {
@ -52,10 +52,63 @@ export default function VideoDetail({ id }: { id: string }) {
const fetchDetail = async () => { const fetchDetail = async () => {
try { try {
setLoading(true); setLoading(true);
const response = await getDetail(id);
setData(response?.data?.data); // Try new Articles API first
const response = await getArticleDetail(id);
console.log("Article Detail API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
return;
}
// Handle new API response structure
const articleData = response?.data?.data;
if (articleData) {
// Transform article data to match old structure for backward compatibility
const transformedData = {
id: articleData.id,
title: articleData.title,
description: articleData.description,
createdAt: articleData.createdAt,
clickCount: articleData.viewCount,
creatorGroupLevelName: articleData.createdByName || "Unknown",
uploadedBy: {
publisher: articleData.createdByName || "MABES POLRI"
},
files: articleData.files?.map((file: any) => ({
id: file.id,
url: file.file_url,
fileName: file.file_name,
filePath: file.file_path,
fileThumbnail: file.file_thumbnail,
fileAlt: file.file_alt,
widthPixel: file.width_pixel,
heightPixel: file.height_pixel,
size: file.size,
downloadCount: file.download_count,
createdAt: file.created_at,
updatedAt: file.updated_at,
thumbnailFileUrl: file.file_thumbnail || articleData.thumbnailUrl,
...file
})) || [],
...articleData
};
setData(transformedData);
}
} catch (error) { } catch (error) {
console.error("Error fetching detail:", error); console.error("Error fetching detail:", error);
// Try fallback to old API if new API fails
try {
const fallbackResponse = await getDetail(id);
setData(fallbackResponse?.data?.data);
} catch (fallbackError) {
console.error("Fallback API also failed:", fallbackError);
}
} finally { } finally {
setLoading(false); setLoading(false);
} }
@ -179,7 +232,7 @@ export default function VideoDetail({ id }: { id: string }) {
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"
@ -232,7 +285,7 @@ export default function VideoDetail({ id }: { id: string }) {
<span>SHARE</span> <span>SHARE</span>
</div> </div>
<div className="flex gap-2 items-center"> <div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}> <Link href={`/content/video/comment/${id}`}>
<Button <Button
variant="default" variant="default"
size="lg" size="lg"

View File

@ -26,7 +26,7 @@ export default function PublicationKlFilter({
return ( return (
<Card className="overflow-hidden shadow-md w-[315px] h-[417px] p-0 flex flex-col"> <Card className="overflow-hidden shadow-md w-[315px] h-[417px] p-0 flex flex-col">
{/* Gambar atas */} {/* Gambar atas */}
<Link href={`/public/content/video/detail/${id}`}> <Link href={`/content/video/detail/${id}`}>
<div className="relative w-full h-[250px]"> <div className="relative w-full h-[250px]">
<Image src={image} alt={title} fill className="object-cover" /> <Image src={image} alt={title} fill className="object-cover" />
</div> </div>

View File

@ -2,7 +2,7 @@ import type { NextConfig } from "next";
const nextConfig = { const nextConfig = {
images: { images: {
domains: ["kontenhumas.com"], domains: ["kontenhumas.com", "dev.mikulnews.com"],
}, },
}; };

View File

@ -167,6 +167,31 @@ export async function listData(
); );
} }
// New Articles API for public/landing usage
export async function listArticles(
page = 1,
totalPage = 10,
typeId?: number,
search?: string,
categoryId?: string,
sortBy = "createdAt"
) {
let url = `articles?page=${page}&totalPage=${totalPage}`;
if (typeId !== undefined) url += `&typeId=${typeId}`;
if (search) url += `&title=${encodeURIComponent(search)}`;
if (categoryId) url += `&categoryId=${categoryId}`;
// if (sortBy) url += `&sortBy=${sortBy}`;
return await httpGetInterceptor(url);
}
// New Article Detail API for public/landing usage
export async function getArticleDetail(id: string | number) {
const url = `articles/${id}`;
return await httpGetInterceptor(url);
}
export async function listDataRegional( export async function listDataRegional(
type: string, type: string,
search: string, search: string,