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 { getCsrfToken } from "@/service/auth";
import {
listEnableCategory,
getTagsBySubCategoryId,
createMedia,
createArticle,
getTagsBySubCategoryId,
listEnableCategory,
listArticleCategories,
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 {
generateDataArticle,
@ -54,6 +61,7 @@ import {
getGenerateKeywords,
getGenerateTitle,
} from "@/service/content/ai";
import Link from "next/link";
const CustomEditor = dynamic(
() => {
@ -188,27 +196,30 @@ export default function FormVideo() {
});
const videoSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
title: z.string().min(1, { message: "titleRequired" }),
description: z.string().optional(),
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
category: z.string().min(1, { message: "Kategori harus dipilih" }),
tags: z
.array(z.string().min(1))
.min(1, { message: "Minimal 1 tag diperlukan" }),
creatorName: z.string().min(1, { message: "creatorRequired" }),
files: z
.array(z.any())
.min(1, { message: "File video harus dipilih" })
.min(1, { message: "Minimal 1 file harus diunggah." })
.refine(
(files) =>
files.every((file: File) => ACCEPTED_FILE_TYPES.includes(file.type)),
{ message: "File harus berformat mp4 atau mov" }
)
.refine(
(files) => files.every((file: File) => file.size <= MAX_FILE_SIZE),
{ message: "Ukuran file maksimal 100 MB" }
files.every(
(file: File) =>
["video/mp4", "video/mov", "video/avi"].includes(file.type) &&
file.size <= 100 * 1024 * 1024
),
{
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
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
@ -227,8 +238,9 @@ export default function FormVideo() {
description: "",
descriptionOri: "",
rewriteDescription: "",
category: "",
creatorName: "",
files: [],
categoryId: "",
tags: [],
publishedFor: [],
},
@ -463,11 +475,30 @@ export default function FormVideo() {
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
// Use new Article Categories API
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);
console.log("data category", resCategory);
console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
@ -475,7 +506,6 @@ export default function FormVideo() {
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
@ -483,6 +513,14 @@ export default function FormVideo() {
}
} catch (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,40 +616,99 @@ export default function FormVideo() {
}
if (id == undefined) {
const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData);
// New Articles API request data structure
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) {
MySwal.fire("Error", response?.message, "error");
return;
MySwal.fire("Error", response.message || "Failed to create article", "error");
return false;
}
Cookies.set("idCreate", response?.data?.data, { expires: 1 });
id = response?.data?.data;
if (thumbnail) {
const formMedia = new FormData();
formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error) {
error(responseThumbnail.message);
// Get the article ID from the new API response
const articleId = response?.data?.data?.id;
Cookies.set("idCreate", articleId, { expires: 1 });
id = articleId;
// 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;
}
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/video");
});
Cookies.remove("idCreate");
return;
}
const progressInfoArr = [];
for (const item of files) {
progressInfoArr.push({ percentage: 0, fileName: item.name });
}
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close();
// showProgress();
files.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "0");
});
Cookies.remove("idCreate");
};
@ -885,37 +982,40 @@ export default function FormVideo() {
<Label>Category</Label>
<Controller
control={control}
name="category"
name="categoryId"
render={({ field }) => (
<Select
value={field.value}
onValueChange={(id) => {
field.onChange(id);
console.log("Selected Category ID:", id);
setSelectedCategory(id); // tetap set ini kalau mau
}}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<div className="w-full">
<Label>Category</Label>
<Select
value={field.value}
onValueChange={(value) => {
field.onChange(value);
setSelectedCategory(value);
}}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</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 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 {
createMedia,
createArticle,
getTagsBySubCategoryId,
listEnableCategory,
listArticleCategories,
uploadThumbnail,
uploadArticleFiles,
uploadArticleThumbnail,
CreateArticleData,
} from "@/service/content/content";
import { Textarea } from "@/components/ui/textarea";
import {
@ -54,6 +59,8 @@ import { Item } from "@radix-ui/react-dropdown-menu";
import dynamic from "next/dynamic";
import { getCsrfToken } from "@/service/auth";
import { useParams } from "next/navigation";
import { request } from "http";
import { toast } from "sonner";
import { htmlToString } from "@/utils/globals";
import Link from "next/link";
@ -127,7 +134,7 @@ export default function FormAudio() {
polres: false,
});
let fileTypeId = "4";
let fileTypeId = "3";
let progressInfo: any = [];
let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]);
@ -197,18 +204,30 @@ export default function FormAudio() {
};
const audioSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
title: z.string().min(1, { message: "titleRequired" }),
description: z.string().optional(),
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
category: z.string().min(1, { message: "Category diperlukan" }),
creatorName: z.string().min(1, { message: "creatorRequired" }),
files: z
.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
.array(z.string().min(1))
.min(1, { message: "Wajib isi minimal 1 tag" }),
.array(z.string())
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
publishedFor: z
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
@ -227,7 +246,9 @@ export default function FormAudio() {
description: "",
descriptionOri: "",
rewriteDescription: "",
category: "",
creatorName: "",
files: [],
categoryId: "",
tags: [],
publishedFor: [],
},
@ -456,11 +477,30 @@ export default function FormAudio() {
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
// Use new Article Categories API
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);
console.log("data category", resCategory);
console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
@ -468,7 +508,6 @@ export default function FormAudio() {
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
@ -476,6 +515,14 @@ export default function FormAudio() {
}
} catch (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) {
const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData);
// New Articles API request data structure
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) {
MySwal.fire("Error", response?.message, "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);
MySwal.fire("Error", response.message || "Failed to create article", "error");
return false;
}
// Get the article ID from the new API response
const articleId = response?.data?.data?.id;
Cookies.set("idCreate", articleId, { expires: 1 });
id = articleId;
// 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;
}
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;
}
const progressInfoArr = [];
for (const item of files) {
progressInfoArr.push({ percentage: 0, fileName: item.name });
}
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close();
// showProgress();
files.map(async (item: any, index: number) => {
await uploadResumableFile(index, String(id), item, "0");
});
Cookies.remove("idCreate");
// MySwal.fire("Sukses", "Data berhasil disimpan.", "success");
};
const onSubmit = (data: AudioSchema) => {
@ -867,17 +971,16 @@ export default function FormAudio() {
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Controller
name="category"
control={control}
rules={{ required: "Category is required" }}
render={({ field, fieldState }) => (
<>
name="categoryId"
render={({ field }) => (
<div className="w-full">
<Label>Category</Label>
<Select
value={field.value}
onValueChange={(id) => {
field.onChange(id);
console.log("Selected Category ID:", id);
setSelectedCategory(id);
onValueChange={(value) => {
field.onChange(value);
setSelectedCategory(value);
}}
>
<SelectTrigger>
@ -894,12 +997,13 @@ export default function FormAudio() {
))}
</SelectContent>
</Select>
{fieldState.error && (
<p className="text-sm text-red-500">
{fieldState.error.message}
{errors.categoryId && (
<p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p>
)}
</>
</div>
)}
/>
</div>

View File

@ -30,9 +30,14 @@ import { Switch } from "@/components/ui/switch";
import Cookies from "js-cookie";
import {
createMedia,
createArticle,
getTagsBySubCategoryId,
listEnableCategory,
listArticleCategories,
uploadThumbnail,
uploadArticleFiles,
uploadArticleThumbnail,
CreateArticleData,
} from "@/service/content/content";
import { Textarea } from "@/components/ui/textarea";
import {
@ -50,6 +55,8 @@ import Image from "next/image";
import { error, loading } from "@/config/swal";
import dynamic from "next/dynamic";
import { getCsrfToken } from "@/service/auth";
import { request } from "http";
import { toast } from "sonner";
import { htmlToString } from "@/utils/globals";
import Link from "next/link";
@ -125,7 +132,7 @@ export default function FormTeks() {
polres: false,
});
let fileTypeId = "3";
let fileTypeId = "2";
let progressInfo: any = [];
let counterUpdateProgress = 0;
const [progressList, setProgressList] = useState<any>([]);
@ -178,38 +185,34 @@ export default function FormTeks() {
});
const teksSchema = z.object({
title: z.string().min(1, { message: "Judul diperlukan" }),
creatorName: z.string().min(1, { message: "Creator diperlukan" }),
category: z.string().min(1, { message: "Kategori harus dipilih" }),
tags: z
.array(z.string())
.min(1, { message: "Minimal 1 tag harus ditambahkan." }),
title: z.string().min(1, { message: "titleRequired" }),
description: z.string().optional(),
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
creatorName: z.string().min(1, { message: "creatorRequired" }),
files: z
.array(
z
.object({
name: z.string(),
type: z.string(),
size: z
.number()
.max(20 * 1024 * 1024, { message: "Max file size 20 MB" }),
})
.refine(
(file) =>
.array(z.any())
.min(1, { message: "Minimal 1 file harus diunggah." })
.refine(
(files) =>
files.every(
(file: File) =>
[
"application/pdf",
"application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"application/vnd.ms-powerpoint",
"application/vnd.openxmlformats-officedocument.presentationml.presentation",
].includes(file.type),
{ message: "Format file tidak didukung" }
)
)
.min(1, { message: "File wajib diunggah" }),
description: z.string().optional(),
descriptionOri: z.string().optional(),
rewriteDescription: z.string().optional(),
"text/plain",
].includes(file.type) && file.size <= 100 * 1024 * 1024
),
{
message:
"Hanya file .pdf, .doc, .docx, .txt, 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
.array(z.string())
.min(1, { message: "Minimal 1 target publish harus dipilih." }),
@ -226,13 +229,13 @@ export default function FormTeks() {
resolver: zodResolver(teksSchema),
defaultValues: {
title: "",
creatorName: "",
category: "",
tags: [],
files: [],
description: "",
descriptionOri: "",
rewriteDescription: "",
creatorName: "",
files: [],
categoryId: "",
tags: [],
publishedFor: [],
},
});
@ -470,11 +473,30 @@ export default function FormTeks() {
const getCategories = async () => {
try {
const category = await listEnableCategory(fileTypeId);
const resCategory: Category[] = category?.data?.data?.content;
// Use new Article Categories API
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);
console.log("data category", resCategory);
console.log("Article categories loaded:", resCategory);
if (scheduleId && scheduleType === "3") {
const findCategory = resCategory.find((o) =>
@ -482,14 +504,21 @@ export default function FormTeks() {
);
if (findCategory) {
// setValue("categoryId", findCategory.id);
setSelectedCategory(findCategory.id); // Set the selected category
setSelectedCategory(findCategory.id);
const response = await getTagsBySubCategoryId(findCategory.id);
setTags(response?.data?.data);
}
}
} catch (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,38 +618,99 @@ export default function FormTeks() {
}
if (id == undefined) {
const response = await createMedia(requestData);
console.log("Form Data Submitted:", requestData);
// New Articles API request data structure
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 });
id = response?.data?.data;
const formMedia = new FormData();
const thumbnail = files[0];
formMedia.append("file", thumbnail);
const responseThumbnail = await uploadThumbnail(id, formMedia);
if (responseThumbnail?.error == true) {
error(responseThumbnail?.message);
// Use new Articles API
const response = await createArticle(articleData);
console.log("Article Data Submitted:", articleData);
console.log("Article API Response:", response);
if (response?.error) {
MySwal.fire("Error", response.message || "Failed to create article", "error");
return false;
}
// Get the article ID from the new API response
const articleId = response?.data?.data?.id;
Cookies.set("idCreate", articleId, { expires: 1 });
id = articleId;
// 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;
}
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;
}
const progressInfoArr = files.map((item) => ({
percentage: 0,
fileName: item.name,
}));
progressInfo = progressInfoArr;
setIsStartUpload(true);
setProgressList(progressInfoArr);
close();
files.map(async (item: any, index: number) => {
await uploadResumableFile(
index,
String(id),
item,
fileTypeId == "2" || fileTypeId == "4" ? item.duration : "0"
);
});
Cookies.remove("idCreate");
};
@ -848,7 +938,7 @@ export default function FormTeks() {
<div className="flex flex-col lg:flex-row gap-10">
<Card className="w-full lg:w-8/12">
<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">
{/* Input Title */}
<div className="space-y-2 py-3">
@ -873,38 +963,42 @@ export default function FormTeks() {
<div className="flex items-center">
<div className="py-3 w-full space-y-2">
<Label>Category</Label>
<Controller
control={control}
name="category"
render={({ field }) => (
<Select
value={field.value}
onValueChange={(val) => {
field.onChange(val);
setSelectedCategory(val);
}}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
<Controller
control={control}
name="categoryId"
render={({ field }) => (
<div className="w-full">
<Label>Category</Label>
<Select
value={field.value}
onValueChange={(value) => {
field.onChange(value);
setSelectedCategory(value);
}}
>
<SelectTrigger>
<SelectValue placeholder="Pilih" />
</SelectTrigger>
<SelectContent>
{categories.map((category) => (
<SelectItem
key={category.id}
value={category.id.toString()}
>
{category.name}
</SelectItem>
))}
</SelectContent>
</Select>
{errors.categoryId && (
<p className="text-sm text-red-500 mt-1">
{errors.categoryId.message}
</p>
)}
/>
{errors.category?.message && (
<p className="text-red-400 text-sm">
{errors.category.message}
</p>
)}
</div>
)}
/>
</div>
</div>
<div className="flex flex-row items-center gap-3 py-2">

View File

@ -6,6 +6,7 @@ import { useEffect, useState } from "react";
import {
getListContent,
listData,
listArticles,
listStaticBanner,
} from "@/service/landing/landing";
import { data } from "framer-motion/client";
@ -17,25 +18,66 @@ export default function Header() {
useEffect(() => {
const fetchData = async () => {
try {
// const request = {
// group: "mabes",
// };
const response = await listData(
"",
"",
"",
5,
0,
"createdAt",
"",
"",
""
);
const content = response?.data?.data?.content || [];
console.log("data", content);
setData(content);
// Use new Articles API
const response = await listArticles(1, 5, undefined, undefined, undefined, "createdAt");
console.log("Articles API response:", response);
if (response?.error) {
console.error("Articles API failed, falling back to old API");
// Fallback to old API
const fallbackResponse = await listData(
"",
"",
"",
5,
0,
"createdAt",
"",
"",
""
);
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) {
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 = () => {
switch (item?.fileTypeId) {
case 1:
return `/public/content/image/detail/${item?.id}`;
return `/content/image/detail/${item?.id}`;
case 2:
return `/public/content/video/detail/${item?.id}`;
return `/content/video/detail/${item?.id}`;
case 3:
return `/public/content/text/detail/${item?.id}`;
return `/content/text/detail/${item?.id}`;
case 4:
return `/public/content/audio/detail/${item?.id}`;
return `/content/audio/detail/${item?.id}`;
default:
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 { Card } from "../ui/card";
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/css";
import "swiper/css/navigation";
@ -42,20 +42,71 @@ export default function MediaUpdate() {
async function fetchData(section: "latest" | "popular") {
try {
setLoading(true);
const res = await listData(
"1",
"",
"",
20,
0,
section === "latest" ? "createdAt" : "clickCount",
"",
"",
""
// Use new Articles API
const response = await listArticles(
1,
20,
1, // typeId for images
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) {
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 {
setLoading(false);
}

View File

@ -15,7 +15,7 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail } from "@/service/landing/landing";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
export default function AudioDetail({ id }: { id: string }) {
const [copied, setCopied] = useState(false);
@ -60,14 +60,75 @@ export default function AudioDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
setLoading(true);
const response = await getDetail(id);
setData(response?.data?.data);
console.log(
"doc",
response?.data?.data.files[selectedDoc]?.secondaryUrl
);
// 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);
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) {
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 {
setLoading(false);
}
@ -185,7 +246,7 @@ export default function AudioDetail({ id }: { id: string }) {
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"
@ -238,7 +299,7 @@ export default function AudioDetail({ id }: { id: string }) {
<span>SHARE</span>
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"

View File

@ -15,7 +15,7 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail } from "@/service/landing/landing";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player";
import { toBase64, shimmer } from "@/utils/globals";
import { Skeleton } from "@/components/ui/skeleton";
@ -63,14 +63,75 @@ export default function DocumentDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
setLoading(true);
const response = await getDetail(id);
setData(response?.data?.data);
console.log(
"doc",
response?.data?.data.files[selectedDoc]?.secondaryUrl
);
// 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);
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) {
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 {
setLoading(false);
}
@ -189,7 +250,7 @@ export default function DocumentDetail({ id }: { id: string }) {
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"
@ -242,7 +303,7 @@ export default function DocumentDetail({ id }: { id: string }) {
<span>SHARE</span>
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"

View File

@ -15,7 +15,7 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail } from "@/service/landing/landing";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player";
import { toBase64, shimmer } from "@/utils/globals";
import { Skeleton } from "@/components/ui/skeleton";
@ -63,10 +63,63 @@ export default function ImageDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
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) {
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 {
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">
{/* 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">
<Button
onClick={handleCopyLink}
@ -229,7 +282,7 @@ export default function ImageDetail({ id }: { id: string }) {
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"
@ -250,7 +303,7 @@ export default function ImageDetail({ id }: { id: string }) {
COMMENT
</Link>
</div>
</div>
</div> */}
{/* Content */}
<div className="flex-1 space-y-4">
@ -282,7 +335,7 @@ export default function ImageDetail({ id }: { id: string }) {
<span>SHARE</span>
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"

View File

@ -15,7 +15,7 @@ import {
FaLink,
FaShareAlt,
} from "react-icons/fa";
import { getDetail } from "@/service/landing/landing";
import { getDetail, getArticleDetail } from "@/service/landing/landing";
import VideoPlayer from "@/utils/video-player";
export default function VideoDetail({ id }: { id: string }) {
@ -52,10 +52,63 @@ export default function VideoDetail({ id }: { id: string }) {
const fetchDetail = async () => {
try {
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) {
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 {
setLoading(false);
}
@ -179,7 +232,7 @@ export default function VideoDetail({ id }: { id: string }) {
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"
@ -232,7 +285,7 @@ export default function VideoDetail({ id }: { id: string }) {
<span>SHARE</span>
</div>
<div className="flex gap-2 items-center">
<Link href={`/public/content/video/comment/${id}`}>
<Link href={`/content/video/comment/${id}`}>
<Button
variant="default"
size="lg"

View File

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

View File

@ -2,7 +2,7 @@ import type { NextConfig } from "next";
const nextConfig = {
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(
type: string,
search: string,