feat: all section in notes meet

This commit is contained in:
Sabda Yagra 2025-10-31 23:21:05 +07:00
parent 28c1b79812
commit a892343f3e
46 changed files with 4294 additions and 750 deletions

View File

@ -0,0 +1,242 @@
"use client";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { useRouter } from "next/navigation";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { createSchedule } from "@/service/landing/landing";
const MySwal = withReactContent(Swal);
export default function CreateSchedulePage() {
const router = useRouter();
const [formData, setFormData] = useState({
createdById: 1, // bisa diganti sesuai ID user login
description: "",
endDate: "",
endTime: "",
isLiveStreaming: false,
location: "",
speakers: "",
startDate: "",
startTime: "",
title: "",
typeId: 1,
liveStreamingUrl: "",
posterImagePath: "",
});
const [loading, setLoading] = useState(false);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleToggle = (checked: boolean) => {
setFormData((prev) => ({ ...prev, isLiveStreaming: checked }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
try {
// Format tanggal sesuai ISO backend (2025-11-01T00:00:00Z)
const payload = {
...formData,
startDate: `${formData.startDate}T${formData.startTime}:00Z`,
endDate: `${formData.endDate}T${formData.endTime}:00Z`,
};
const res = await createSchedule(payload);
if (!res?.error) {
MySwal.fire({
icon: "success",
title: "Berhasil!",
text: "Schedule berhasil dibuat.",
timer: 2000,
showConfirmButton: false,
});
setTimeout(() => router.push("/admin/schedule"), 2000);
} else {
MySwal.fire({
icon: "error",
title: "Gagal!",
text: res?.message || "Gagal membuat schedule.",
});
}
} catch (error) {
MySwal.fire({
icon: "error",
title: "Error",
text: "Terjadi kesalahan pada sistem.",
});
console.error(error);
} finally {
setLoading(false);
}
};
return (
<div className="max-w-4xl mx-auto mt-10">
<Card className="shadow-lg border border-gray-200 dark:border-gray-800">
<CardHeader>
<CardTitle className="text-xl font-semibold">
Buat Jadwal Baru
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<Label htmlFor="title">Judul Acara</Label>
<Input
id="title"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Masukkan judul acara"
required
/>
</div>
<div>
<Label htmlFor="description">Deskripsi</Label>
<Textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Masukkan deskripsi acara"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="startDate">Tanggal Mulai</Label>
<Input
type="date"
id="startDate"
name="startDate"
value={formData.startDate}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="endDate">Tanggal Selesai</Label>
<Input
type="date"
id="endDate"
name="endDate"
value={formData.endDate}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="startTime">Jam Mulai</Label>
<Input
type="time"
id="startTime"
name="startTime"
value={formData.startTime}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="endTime">Jam Selesai</Label>
<Input
type="time"
id="endTime"
name="endTime"
value={formData.endTime}
onChange={handleChange}
required
/>
</div>
</div>
<div>
<Label htmlFor="location">Lokasi</Label>
<Input
id="location"
name="location"
value={formData.location}
onChange={handleChange}
placeholder="Masukkan lokasi acara"
required
/>
</div>
<div>
<Label htmlFor="speakers">Pembicara</Label>
<Input
id="speakers"
name="speakers"
value={formData.speakers}
onChange={handleChange}
placeholder="Nama pembicara"
required
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="isLiveStreaming">Live Streaming?</Label>
<Switch
id="isLiveStreaming"
checked={formData.isLiveStreaming}
onCheckedChange={handleToggle}
/>
</div>
{formData.isLiveStreaming && (
<div>
<Label htmlFor="liveStreamingUrl">Link Live Streaming</Label>
<Input
id="liveStreamingUrl"
name="liveStreamingUrl"
value={formData.liveStreamingUrl}
onChange={handleChange}
placeholder="https://youtube.com/..."
/>
</div>
)}
<div>
<Label htmlFor="posterImagePath">Poster (opsional)</Label>
<Input
id="posterImagePath"
name="posterImagePath"
value={formData.posterImagePath}
onChange={handleChange}
placeholder="/uploads/poster/namafile.png"
/>
</div>
<Button
type="submit"
className="w-full h-11 font-semibold"
disabled={loading}
>
{loading ? "Menyimpan..." : "Simpan Jadwal"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,303 @@
"use client";
import { useEffect, useState } from "react";
import { useParams, useRouter } from "next/navigation";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { getScheduleById, updateSchedule } from "@/service/landing/landing";
const MySwal = withReactContent(Swal);
export default function EditSchedulePage() {
const router = useRouter();
const { id } = useParams();
const [loading, setLoading] = useState(true);
const [saving, setSaving] = useState(false);
const [formData, setFormData] = useState({
description: "",
endDate: "",
endTime: "",
isLiveStreaming: false,
liveStreamingUrl: "",
location: "",
posterImagePath: "",
speakers: "",
startDate: "",
startTime: "",
statusId: 1,
title: "",
typeId: 1,
});
// Fetch existing schedule data
useEffect(() => {
const fetchData = async () => {
setLoading(true);
try {
const res = await getScheduleById(id as string);
if (!res.error && res.data) {
const data = res.data.data || res.data;
setFormData({
description: data.description || "",
endDate: data.endDate
? new Date(data.endDate).toISOString().slice(0, 10)
: "",
endTime: data.endDate
? new Date(data.endDate).toISOString().slice(11, 16)
: "",
isLiveStreaming: data.isLiveStreaming || false,
liveStreamingUrl: data.liveStreamingUrl || "",
location: data.location || "",
posterImagePath: data.posterImagePath || "",
speakers: data.speakers || "",
startDate: data.startDate
? new Date(data.startDate).toISOString().slice(0, 10)
: "",
startTime: data.startDate
? new Date(data.startDate).toISOString().slice(11, 16)
: "",
statusId: data.statusId || 1,
title: data.title || "",
typeId: data.typeId || 1,
});
} else {
MySwal.fire({
icon: "error",
title: "Gagal",
text: res.message || "Data tidak ditemukan.",
});
}
} catch (error) {
MySwal.fire({
icon: "error",
title: "Error",
text: "Terjadi kesalahan saat memuat data.",
});
console.error(error);
} finally {
setLoading(false);
}
};
fetchData();
}, [id]);
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
const { name, value } = e.target;
setFormData((prev) => ({ ...prev, [name]: value }));
};
const handleToggle = (checked: boolean) => {
setFormData((prev) => ({ ...prev, isLiveStreaming: checked }));
};
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setSaving(true);
try {
const payload = {
...formData,
startDate: `${formData.startDate}T${formData.startTime}:00Z`,
endDate: `${formData.endDate}T${formData.endTime}:00Z`,
};
const res = await updateSchedule(id as string, payload);
if (!res?.error) {
MySwal.fire({
icon: "success",
title: "Berhasil!",
text: "Jadwal berhasil diperbarui.",
timer: 2000,
showConfirmButton: false,
});
setTimeout(() => router.push("/admin/schedules"), 2000);
} else {
MySwal.fire({
icon: "error",
title: "Gagal!",
text: res.message || "Gagal memperbarui jadwal.",
});
}
} catch (error) {
MySwal.fire({
icon: "error",
title: "Error",
text: "Terjadi kesalahan pada sistem.",
});
console.error(error);
} finally {
setSaving(false);
}
};
if (loading) {
return (
<div className="flex justify-center items-center min-h-[70vh] text-muted-foreground">
Memuat data jadwal...
</div>
);
}
return (
<div className="max-w-4xl mx-auto mt-10">
<Card className="shadow-lg border border-gray-200 dark:border-gray-800">
<CardHeader>
<CardTitle className="text-xl font-semibold">
Edit Jadwal
</CardTitle>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<Label htmlFor="title">Judul Acara</Label>
<Input
id="title"
name="title"
value={formData.title}
onChange={handleChange}
placeholder="Masukkan judul acara"
required
/>
</div>
<div>
<Label htmlFor="description">Deskripsi</Label>
<Textarea
id="description"
name="description"
value={formData.description}
onChange={handleChange}
placeholder="Masukkan deskripsi acara"
required
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<Label htmlFor="startDate">Tanggal Mulai</Label>
<Input
type="date"
id="startDate"
name="startDate"
value={formData.startDate}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="endDate">Tanggal Selesai</Label>
<Input
type="date"
id="endDate"
name="endDate"
value={formData.endDate}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="startTime">Jam Mulai</Label>
<Input
type="time"
id="startTime"
name="startTime"
value={formData.startTime}
onChange={handleChange}
required
/>
</div>
<div>
<Label htmlFor="endTime">Jam Selesai</Label>
<Input
type="time"
id="endTime"
name="endTime"
value={formData.endTime}
onChange={handleChange}
required
/>
</div>
</div>
<div>
<Label htmlFor="location">Lokasi</Label>
<Input
id="location"
name="location"
value={formData.location}
onChange={handleChange}
placeholder="Masukkan lokasi acara"
required
/>
</div>
<div>
<Label htmlFor="speakers">Pembicara</Label>
<Input
id="speakers"
name="speakers"
value={formData.speakers}
onChange={handleChange}
placeholder="Nama pembicara"
required
/>
</div>
<div className="flex items-center justify-between">
<Label htmlFor="isLiveStreaming">Live Streaming?</Label>
<Switch
id="isLiveStreaming"
checked={formData.isLiveStreaming}
onCheckedChange={handleToggle}
/>
</div>
{formData.isLiveStreaming && (
<div>
<Label htmlFor="liveStreamingUrl">Link Live Streaming</Label>
<Input
id="liveStreamingUrl"
name="liveStreamingUrl"
value={formData.liveStreamingUrl}
onChange={handleChange}
placeholder="https://youtube.com/..."
/>
</div>
)}
<div>
<Label htmlFor="posterImagePath">Poster (opsional)</Label>
<Input
id="posterImagePath"
name="posterImagePath"
value={formData.posterImagePath}
onChange={handleChange}
placeholder="/uploads/poster/namafile.png"
/>
</div>
<Button
type="submit"
className="w-full h-11 font-semibold"
disabled={saving}
>
{saving ? "Menyimpan..." : "Simpan Perubahan"}
</Button>
</form>
</CardContent>
</Card>
</div>
);
}

View File

@ -0,0 +1,329 @@
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { format } from "date-fns";
import { id as localeId } from "date-fns/locale";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import {
Loader2,
Plus,
Trash2,
Pencil,
CalendarDays,
Search,
ChevronLeft,
ChevronRight,
} from "lucide-react";
import { Calendar } from "@/components/ui/calendar";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { CalendarIcon } from "lucide-react";
import { cn } from "@/lib/utils";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { deleteSchedule, getAllSchedules } from "@/service/landing/landing";
const MySwal = withReactContent(Swal);
function safeFormatDate(dateString?: string, formatString = "dd MMM yyyy HH:mm") {
if (!dateString) return "-";
const date = new Date(dateString);
if (isNaN(date.getTime())) return "-";
return format(date, formatString, { locale: localeId });
}
export default function ScheduleListPage() {
const router = useRouter();
const [schedules, setSchedules] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [search, setSearch] = useState("");
const [isLive, setIsLive] = useState("semua");
const [page, setPage] = useState(1);
const [limit] = useState(6);
const [startDate, setStartDate] = useState<Date | undefined>();
const [endDate, setEndDate] = useState<Date | undefined>();
const [totalPage, setTotalPage] = useState(1);
const fetchSchedules = async () => {
setLoading(true);
try {
const params: any = {
page,
limit,
sortBy: "startDate",
sort: "asc",
title: search,
};
if (isLive !== "semua") {
params.isLiveStreaming = isLive === "true";
}
if (startDate) params.startDate = startDate.toISOString();
if (endDate) params.endDate = endDate.toISOString();
const res = await getAllSchedules(params);
if (!res.error) {
setSchedules(res.data?.data || res.data?.items || []);
setTotalPage(res.data?.meta?.totalPage || 1);
}
} catch (error) {
console.error("Error fetching schedules:", error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSchedules();
}, [page, isLive]);
const handleSearch = () => {
setPage(1);
fetchSchedules();
};
const handleDelete = async (id: number) => {
MySwal.fire({
title: "Hapus Jadwal?",
text: "Tindakan ini tidak dapat dibatalkan.",
icon: "warning",
showCancelButton: true,
confirmButtonText: "Ya, hapus",
cancelButtonText: "Batal",
confirmButtonColor: "#d33",
}).then(async (result) => {
if (result.isConfirmed) {
const res = await deleteSchedule(id);
if (!res?.error) {
MySwal.fire({
icon: "success",
title: "Dihapus!",
text: "Jadwal berhasil dihapus.",
timer: 1500,
showConfirmButton: false,
});
fetchSchedules();
} else {
MySwal.fire({
icon: "error",
title: "Gagal!",
text: res.message || "Gagal menghapus jadwal.",
});
}
}
});
};
return (
<div className="max-w-7xl mx-auto p-6">
{/* Header */}
<div className="flex flex-wrap items-center justify-between mb-8 gap-4">
<h1 className="text-2xl font-semibold flex items-center gap-2">
<CalendarDays className="w-6 h-6 text-blue-600" />
Daftar Jadwal
</h1>
<Button onClick={() => router.push("/admin/schedules/create")}>
<Plus className="mr-2 h-4 w-4" /> Tambah Jadwal
</Button>
</div>
{/* Filter Bar */}
<div className="flex flex-wrap items-end gap-4 mb-6">
<div className="flex items-center gap-2">
<Input
placeholder="Cari judul..."
className="w-64"
value={search}
onChange={(e) => setSearch(e.target.value)}
/>
<Button variant="outline" onClick={handleSearch}>
<Search className="w-4 h-4 mr-1" /> Cari
</Button>
</div>
<div className="flex flex-col">
<label className="text-sm font-medium mb-1">Tanggal Mulai</label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[180px] justify-start text-left">
<CalendarIcon className="mr-2 h-4 w-4" />
{startDate ? format(startDate, "dd MMM yyyy") : "Pilih"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={startDate}
onSelect={setStartDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col">
<label className="text-sm font-medium mb-1">Tanggal Selesai</label>
<Popover>
<PopoverTrigger asChild>
<Button variant="outline" className="w-[180px] justify-start text-left">
<CalendarIcon className="mr-2 h-4 w-4" />
{endDate ? format(endDate, "dd MMM yyyy") : "Pilih"}
</Button>
</PopoverTrigger>
<PopoverContent className="w-auto p-0">
<Calendar
mode="single"
selected={endDate}
onSelect={setEndDate}
initialFocus
/>
</PopoverContent>
</Popover>
</div>
<div className="flex flex-col">
<label className="text-sm font-medium mb-1">Tipe</label>
<Select value={isLive} onValueChange={setIsLive}>
<SelectTrigger className="w-[160px]">
<SelectValue placeholder="Semua" />
</SelectTrigger>
<SelectContent>
<SelectItem value="semua">Semua</SelectItem>
<SelectItem value="true">Live</SelectItem>
<SelectItem value="false">Offline</SelectItem>
</SelectContent>
</Select>
</div>
<Button
variant="default"
onClick={() => {
setSearch("");
setStartDate(undefined);
setEndDate(undefined);
setIsLive("semua");
setPage(1);
fetchSchedules();
}}
>
Reset
</Button>
</div>
{/* Content */}
{loading ? (
<div className="flex justify-center items-center py-16 text-muted-foreground">
<Loader2 className="animate-spin mr-2 h-5 w-5" />
Memuat data...
</div>
) : schedules.length === 0 ? (
<div className="text-center py-16 text-muted-foreground">
<p>Tidak ada jadwal ditemukan.</p>
</div>
) : (
<>
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{schedules.map((item) => (
<Card
key={item.id}
className="shadow-md border border-gray-200 dark:border-gray-800 hover:shadow-lg transition-all"
>
<CardHeader className="pb-3">
<CardTitle className="text-lg font-semibold flex justify-between items-center">
<span>{item.title}</span>
<Badge
className={
item.isLiveStreaming
? "bg-green-600 text-white"
: "bg-gray-400 text-white"
}
>
{item.isLiveStreaming ? "Live" : "Offline"}
</Badge>
</CardTitle>
</CardHeader>
<CardContent className="text-sm space-y-3">
<div className="flex justify-between text-gray-600">
<span>{safeFormatDate(item.startDate, "dd MMM yyyy")}</span>
<span>
{safeFormatDate(item.startDate, "HH:mm")} -{" "}
{safeFormatDate(item.endDate, "HH:mm")}
</span>
</div>
<p className="text-gray-800 font-medium line-clamp-2">
{item.description || "-"}
</p>
<p className="text-gray-500 text-sm italic">
Lokasi: {item.location || "-"}
</p>
<div className="flex justify-between pt-3 border-t border-gray-200 mt-3">
<Button
variant="outline"
size="sm"
onClick={() => router.push(`/admin/schedule/edit/${item.id}`)}
>
<Pencil className="mr-2 h-4 w-4" /> Edit
</Button>
<Button
variant="default"
size="sm"
onClick={() => handleDelete(item.id)}
>
<Trash2 className="mr-2 h-4 w-4" /> Hapus
</Button>
</div>
</CardContent>
</Card>
))}
</div>
{/* Pagination */}
<div className="flex justify-center items-center mt-8 gap-3">
<Button
variant="outline"
size="sm"
disabled={page <= 1}
onClick={() => setPage((p) => Math.max(p - 1, 1))}
>
<ChevronLeft className="h-4 w-4" /> Prev
</Button>
<span className="text-sm font-medium">
Halaman {page} dari {totalPage}
</span>
<Button
variant="outline"
size="sm"
disabled={page >= totalPage}
onClick={() => setPage((p) => p + 1)}
>
Next <ChevronRight className="h-4 w-4" />
</Button>
</div>
</>
)}
</div>
);
}

View File

@ -0,0 +1,14 @@
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
const layout = async ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default layout;

View File

@ -0,0 +1,104 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Reveal } from "@/components/landing-page/Reveal";
import { Separator } from "@/components/ui/separator";
import Image from "next/image";
import { useTranslations } from "next-intl";
export default function AboutPage() {
const t = useTranslations("AboutPage");
return (
<div className="min-h-screen bg-[#f9f9f9] dark:bg-black text-gray-800 dark:text-white px-6 lg:px-20 py-12">
<Reveal>
<div className="max-w-4xl mx-auto space-y-8">
{/* Header Section */}
<div className="text-center space-y-4">
<h1 className="text-3xl md:text-4xl font-bold text-[#bb3523]">
{t("title")}
</h1>
<p className="text-base md:text-lg text-gray-600 dark:text-gray-300">
{t("subtitle")}
</p>
</div>
<Separator className="my-8" />
{/* Deskripsi Umum */}
<Card className="shadow-md border-none bg-white dark:bg-gray-900">
<CardContent className="p-6 space-y-4">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("whatIs.title")}
</h2>
<p className="leading-relaxed text-justify">
{t.rich("whatIs.p1", {
b: (chunks) => <b>{chunks}</b>,
})}
</p>
<p className="leading-relaxed text-justify">{t("whatIs.p2")}</p>
</CardContent>
</Card>
{/* Fungsi Utama */}
<Card className="shadow-md border-none bg-white dark:bg-gray-900">
<CardContent className="p-6 space-y-4">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("functions.title")}
</h2>
<ul className="list-disc list-inside space-y-2 leading-relaxed">
<li>{t("functions.f1")}</li>
<li>{t("functions.f2")}</li>
<li>{t("functions.f3")}</li>
<li>{t("functions.f4")}</li>
</ul>
</CardContent>
</Card>
{/* Cara Kerja */}
<Card className="shadow-md border-none bg-white dark:bg-gray-900">
<CardContent className="p-6 space-y-4">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("howItWorks.title")}
</h2>
<ol className="list-decimal list-inside space-y-2 leading-relaxed">
<li>{t("howItWorks.s1")}</li>
<li>{t("howItWorks.s2")}</li>
<li>{t("howItWorks.s3")}</li>
<li>{t("howItWorks.s4")}</li>
</ol>
</CardContent>
</Card>
{/* Ilustrasi */}
<div className="flex justify-center py-6">
<Image
src="/assets/about-illustration.png"
alt={t("title")}
width={600}
height={400}
className="rounded-lg shadow-lg object-cover"
/>
</div>
{/* Visi & Misi */}
<Card className="shadow-md border-none bg-white dark:bg-gray-900">
<CardContent className="p-6 space-y-4">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("vision.title")}
</h2>
<p>{t("vision.text")}</p>
<p className="font-semibold">{t("mission.title")}</p>
<ul className="list-disc list-inside space-y-2">
<li>{t("mission.m1")}</li>
<li>{t("mission.m2")}</li>
<li>{t("mission.m3")}</li>
</ul>
</CardContent>
</Card>
</div>
</Reveal>
</div>
);
}

View File

@ -0,0 +1,14 @@
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
const layout = async ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default layout;

View File

@ -0,0 +1,149 @@
"use client";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { useState } from "react";
import { Reveal } from "@/components/landing-page/Reveal";
import { useTranslations } from "next-intl";
export default function AdvertisingPage() {
const t = useTranslations("AdvertisingPage");
const [form, setForm] = useState({
name: "",
company: "",
email: "",
phone: "",
message: "",
});
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
alert("Terima kasih! Permintaan kerja sama Anda telah dikirim.");
setForm({
name: "",
company: "",
email: "",
phone: "",
message: "",
});
};
return (
<div className="min-h-screen bg-[#f9f9f9] dark:bg-black text-gray-800 dark:text-white px-6 lg:px-20 py-12">
<Reveal>
<div className="max-w-5xl mx-auto space-y-10">
{/* Header */}
<div className="text-center space-y-3">
<h1 className="text-3xl md:text-4xl font-bold text-[#bb3523]">
{t("title")}
</h1>
<p className="text-base md:text-lg text-gray-600 dark:text-gray-300">
{t("subtitle")}
</p>
</div>
{/* Benefit Section */}
<Card className="bg-white dark:bg-gray-900 border-none shadow-md">
<CardContent className="p-6 space-y-4">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("whyAdvertise.title")}
</h2>
<ul className="list-disc list-inside space-y-2 leading-relaxed">
<li>{t("whyAdvertise.point1")}</li>
<li>{t("whyAdvertise.point2")}</li>
<li>{t("whyAdvertise.point3")}</li>
<li>{t("whyAdvertise.point4")}</li>
</ul>
</CardContent>
</Card>
{/* Placement Section */}
<Card className="bg-white dark:bg-gray-900 border-none shadow-md">
<CardContent className="p-6 space-y-4">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("placement.title")}
</h2>
<p className="text-justify">{t("placement.text")}</p>
<ul className="list-disc list-inside space-y-2">
<li>{t("placement.option1")}</li>
<li>{t("placement.option2")}</li>
<li>{t("placement.option3")}</li>
<li>{t("placement.option4")}</li>
</ul>
</CardContent>
</Card>
{/* Form Section */}
<Card className="bg-white dark:bg-gray-900 border-none shadow-md">
<CardContent className="p-6 space-y-6">
<h2 className="text-xl font-semibold text-[#bb3523]">
{t("form.title")}
</h2>
<p>{t("form.subtitle")}</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div className="grid md:grid-cols-2 gap-4">
<Input
name="name"
placeholder={t("form.name")}
value={form.name}
onChange={handleChange}
required
/>
<Input
name="company"
placeholder={t("form.company")}
value={form.company}
onChange={handleChange}
required
/>
</div>
<div className="grid md:grid-cols-2 gap-4">
<Input
name="email"
type="email"
placeholder={t("form.email")}
value={form.email}
onChange={handleChange}
required
/>
<Input
name="phone"
placeholder={t("form.phone")}
value={form.phone}
onChange={handleChange}
/>
</div>
<Textarea
name="message"
placeholder={t("form.message")}
value={form.message}
onChange={handleChange}
required
rows={5}
/>
<Button
type="submit"
className="bg-[#bb3523] hover:bg-[#9d2b1c] text-white"
>
{t("form.submit")}
</Button>
</form>
</CardContent>
</Card>
</div>
</Reveal>
</div>
);
}

View File

@ -0,0 +1,14 @@
import Footer from "@/components/landing-page/footer";
import Navbar from "@/components/landing-page/navbar";
const layout = async ({ children }: { children: React.ReactNode }) => {
return (
<>
<Navbar />
{children}
<Footer />
</>
);
};
export default layout;

View File

@ -0,0 +1,290 @@
"use client";
import { getCookiesDecrypt } from "@/lib/utils";
import React, { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { getInfoProfile, getSubjects } from "@/service/auth";
import { close, error, loading, successCallback } from "@/config/swal";
import { sendMessage } from "@/service/landing/landing";
import { useTranslations } from "next-intl";
import { z } from "zod";
import { zodResolver } from "@hookform/resolvers/zod";
import { useRouter } from "@/i18n/routing";
import { Reveal } from "@/components/landing-page/Reveal";
interface IFormInput {
name: string;
email: string;
phone?: string | undefined;
subjects: string;
othersubject?: string | undefined;
message: string;
}
const ContactForm = () => {
const router = useRouter();
const userId = getCookiesDecrypt("uie");
const [subjects, setSubjects] = useState<any[]>([]);
const [isOtherActive, setIsOtherActive] = useState(false);
const t = useTranslations("LandingPage");
const validationSchema = z.object({
name: z.string().min(1, "Nama tidak boleh kosong"),
email: z.string().email("Email tidak valid"),
phone: z.string().optional(),
subjects: z.string().min(1, "Subjek tidak boleh kosong"),
othersubject: z.string().optional(),
message: z.string().min(1, "Pesan tidak boleh kosong"),
});
type IFormInput = z.infer<typeof validationSchema>;
const {
register,
handleSubmit,
formState: { errors },
setValue,
reset,
} = useForm<IFormInput>({
resolver: zodResolver(validationSchema),
});
// Init state
useEffect(() => {
async function initState() {
const response = await getInfoProfile();
const responseSubject = await getSubjects();
const profile = response?.data?.data;
setSubjects(responseSubject?.data?.data || []);
if (profile) {
setValue("name", profile?.fullname || "");
setValue("email", profile?.email || "");
}
}
initState();
}, [setValue]);
async function save(data: IFormInput) {
loading();
const finalData = {
name: data.name,
email: data.email,
phone: data.phone,
title: isOtherActive ? data.othersubject : data.subjects,
message: data.message,
};
const response = await sendMessage(finalData);
if (response?.error) {
error(response?.message);
return;
}
close();
successCallback("Terima kasih, pesan Anda telah terkirim");
reset();
}
async function onSubmit(data: IFormInput) {
if (userId == undefined) {
router.push("/auth");
} else {
save(data);
}
}
const handleSubjects = (e: React.ChangeEvent<HTMLSelectElement>) => {
if (e.target.value === "Lainnya") {
setIsOtherActive(true);
} else {
setIsOtherActive(false);
}
};
return (
<form
method="POST"
onSubmit={handleSubmit(onSubmit)}
className="max-w-2xl mx-auto bg-white dark:bg-black p-6"
>
<Reveal>
{/* Header */}
<div className="flex items-center justify-center mb-6">
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
>
<g fill="none" stroke="currentColor" strokeWidth="1.5">
<path d="M4 10c0-3.771 0-5.657 1.172-6.828S8.229 2 12 2h1.5c3.771 0 5.657 0 6.828 1.172S21.5 6.229 21.5 10v4c0 3.771 0 5.657-1.172 6.828S17.271 22 13.5 22H12c-3.771 0-5.657 0-6.828-1.172S4 17.771 4 14z" />
<path
strokeLinejoin="round"
d="M9.8 11.974c-.427-.744-.633-1.351-.757-1.967c-.184-.91.237-1.8.933-2.368c.295-.24.632-.158.806.155l.393.705c.311.558.467.838.436 1.134c-.03.296-.24.537-.66 1.02zm0 0a10.36 10.36 0 0 0 3.726 3.726m0 0c.744.427 1.351.633 1.967.757c.91.184 1.8-.237 2.368-.933c.24-.295.158-.632-.155-.806l-.704-.393c-.56-.311-.839-.467-1.135-.436c-.296.03-.537.24-1.02.66z"
/>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M5 6H2.5M5 12H2.5M5 18H2.5"
/>
</g>
</svg>
</div>
<h2 className="ml-4 text-2xl font-bold">
{t("contactUs", { defaultValue: "Contact Us" })}
</h2>
</div>
{/* <h3 className="text-lg font-semibold text-gray-800 dark:text-white mb-1">
{t("writeMessage", { defaultValue: "Write Message" })}
</h3>
<p className="text-sm text-gray-600 dark:text-white mb-6">
{t("leaveMessage", { defaultValue: "Leave Message" })}
</p> */}
{/* Form */}
<div>
{/* Name */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
{t("name", { defaultValue: "Name" })}
</label>
<input
type="text"
placeholder={t("enterName", { defaultValue: "Enter Name" })}
className={`w-full p-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.name
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
{...register("name")}
/>
{errors.name && (
<p className="text-red-500 text-sm">{errors.name.message}</p>
)}
</div>
{/* Email */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
Email
</label>
<input
type="email"
placeholder="name@mail.com"
className={`w-full p-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.email
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
{...register("email")}
/>
{errors.email && (
<p className="text-red-500 text-sm">{errors.email.message}</p>
)}
</div>
{/* Phone */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
{t("number", { defaultValue: "Number" })} (Optional)
</label>
<input
type="text"
placeholder={t("enterNumber", { defaultValue: "Enter Number" })}
className="w-full p-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
{...register("phone")}
/>
</div>
{/* Subjects */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
{t("subject", { defaultValue: "Subject" })}
</label>
<select
className={`w-full p-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.subjects
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
{...register("subjects", { onChange: (e) => handleSubjects(e) })}
defaultValue=""
>
<option value="" disabled>
{t("selectSubject", { defaultValue: "Select Subject" })}
</option>
{subjects?.map((list: any) => (
<option key={list.id} value={list.title}>
{list.title}
</option>
))}
<option value="Lainnya">Lainnya</option>
</select>
{errors.subjects && (
<p className="text-red-500 text-sm">{errors.subjects.message}</p>
)}
</div>
{/* Other Subject */}
{isOtherActive && (
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
Subjek Lainnya
</label>
<input
type="text"
placeholder="Masukkan subjek lainnya"
className={`w-full p-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.othersubject
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
{...register("othersubject")}
/>
{errors.othersubject && (
<p className="text-red-500 text-sm">
{errors.othersubject.message}
</p>
)}
</div>
)}
{/* Message */}
<div className="mb-4">
<label className="block text-sm font-medium text-gray-700 dark:text-white mb-1">
{t("messages", { defaultValue: "Messages" })}
</label>
<textarea
placeholder={t("writeYourMessage", {
defaultValue: "Write Your Message",
})}
rows={4}
className={`w-full p-2 border rounded-md focus:outline-none focus:ring-2 ${
errors.message
? "border-red-500 focus:ring-red-500"
: "border-gray-300 focus:ring-blue-500"
}`}
{...register("message")}
></textarea>
{errors.message && (
<p className="text-red-500 text-sm">{errors.message.message}</p>
)}
</div>
<button
type="submit"
className="w-fit bg-blue-500 flex justify-self-end text-white p-2 px-8 rounded-md hover:bg-blue-600 transition"
>
{t("send", { defaultValue: "Send" })}
</button>
</div>
</Reveal>
</form>
);
};
export default ContactForm;

View File

@ -0,0 +1,5 @@
import DetailCommentVideo from "@/components/main/comment-detail-video";
export default async function DetailCommentInfo() {
return <DetailCommentVideo />;
}

View File

@ -0,0 +1,5 @@
import DetailCommentImage from "@/components/main/comment-detail-image";
export default async function DetailCommentInfo() {
return <DetailCommentImage />;
}

View File

@ -0,0 +1,5 @@
import DetailCommentVideo from "@/components/main/comment-detail-video";
export default async function DetailCommentInfo() {
return <DetailCommentVideo />;
}

View File

@ -0,0 +1,59 @@
import { Link } from "@/i18n/routing";
import ForgotPass from "@/components/partials/auth/forgot-pass";
import Image from "next/image";
const ForgotPassPage = () => {
return (
<div className="flex w-full items-center overflow-hidden min-h-dvh h-dvh basis-full">
<div className="overflow-y-auto flex flex-wrap w-full h-dvh">
{/* === KIRI: IMAGE / LOGO AREA === */}
<div className="lg:block hidden flex-1 overflow-hidden text-[40px] leading-[48px] text-default-600 relative z-[1] bg-default-50">
<div className="max-w-[520px] pt-16 ps-20">
<Link href="/" className="mb-6 inline-block">
<img
src="/Group.png"
alt="Mikul News Logo"
className="max-w-2xl h-auto drop-shadow-lg"
/>
</Link>
</div>
{/* bisa tambahkan background atau ilustrasi tambahan di sini */}
</div>
{/* === KANAN: FORM AREA === */}
<div className="flex-1 relative dark:bg-default-100 bg-white">
<div className="h-full flex flex-col">
<div className="max-w-[524px] mx-auto w-full md:px-[42px] md:py-[44px] p-7 text-2xl text-default-900 mb-3 flex flex-col justify-center h-full">
{/* TITLE */}
<div className="text-center mb-5">
<h4 className="font-medium mb-4">Forgot Your Password?</h4>
</div>
{/* INSTRUCTION */}
<div className="font-normal text-base text-default-500 text-center px-2 bg-gray-100 rounded py-3 mb-6 mt-6">
Enter your Username and instructions will be sent to you!
</div>
{/* FORM */}
<ForgotPass />
{/* LINK BACK */}
<div className="md:max-w-[345px] mx-auto font-normal text-default-500 2xl:mt-12 mt-8 uppercase text-sm text-center">
Forget it?{" "}
<Link
href="/auth"
className="text-default-900 font-medium hover:underline"
>
Send me back
</Link>{" "}
to the Sign In
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default ForgotPassPage;

View File

@ -8,6 +8,7 @@ import { OTPForm } from "@/components/auth/otp-form";
import { LoginFormData } from "@/types/auth";
import { useAuth, useEmailValidation } from "@/hooks/use-auth";
import { toast } from "sonner";
import { useTranslations } from "next-intl";
type AuthStep = "login" | "email-setup" | "otp";

View File

@ -18,6 +18,7 @@ import { useAuth } from "@/hooks/use-auth";
import { listRole } from "@/service/landing/landing";
import { Role } from "@/types/auth";
import Link from "next/link";
import { useTranslations } from "next-intl";
export const LoginForm: React.FC<LoginFormProps> = ({
onSuccess,
@ -25,7 +26,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
className,
}) => {
const { login } = useAuth();
const t = useTranslations("MediaUpdate");
const [showPassword, setShowPassword] = useState(false);
const [rememberMe, setRememberMe] = useState(true);
const [roles, setRoles] = useState<Role[]>([]);
@ -92,7 +93,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
/>
</div>
<h2 className="text-lg font-bold text-gray-900 mb-2 mt-5">
MENYATUKAN INDONESIA
{t("unite")}
</h2>
</div>
@ -147,7 +148,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
label="Username"
name="username"
type="text"
placeholder="Enter your username"
placeholder={t("username")}
error={errors.username?.message}
disabled={isSubmitting}
required
@ -158,10 +159,10 @@ export const LoginForm: React.FC<LoginFormProps> = ({
{/* Password Field */}
<FormField
label="Password"
label={t("password")}
name="password"
type="password"
placeholder="Enter your password"
placeholder={t("password2")}
error={errors.password?.message}
disabled={isSubmitting}
required
@ -183,14 +184,14 @@ export const LoginForm: React.FC<LoginFormProps> = ({
disabled={isSubmitting}
/>
<Label htmlFor="rememberMe" className="text-sm">
Remember Me
{t("rememberMe")}
</Label>
</div>
<Link
href="/auth/forgot-password"
className="text-sm text-default-800 dark:text-default-400 leading-6 font-medium hover:underline"
>
Lupa kata sandi?
{t("forgotPass")}
</Link>
</div>
@ -199,7 +200,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
type="submit"
fullWidth
disabled={isSubmitting}
className="mt-6 bg-red-700"
className="mt-6 bg-red-700 cursor-pointer"
color="primary"
>
{isSubmitting ? (
@ -208,7 +209,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
Processing...
</div>
) : (
"Selanjutnya"
t("enterOTP5")
)}
</Button>
</form>

View File

@ -2,7 +2,6 @@
import React, { useState } from "react";
import { Button } from "@/components/ui/button";
import { OTPFormProps } from "@/types/auth";
import { useOTPVerification } from "@/hooks/use-auth";
import {
@ -11,6 +10,7 @@ import {
InputOTPSeparator,
InputOTPSlot,
} from "../ui/input-otp";
import { useTranslations } from "next-intl";
export const OTPForm: React.FC<OTPFormProps> = ({
loginCredentials,
@ -21,6 +21,7 @@ export const OTPForm: React.FC<OTPFormProps> = ({
}) => {
const { verifyOTP, loading } = useOTPVerification();
const [otpValue, setOtpValue] = useState("");
const t = useTranslations("MediaUpdate");
const handleTypeOTP = (event: React.KeyboardEvent<HTMLInputElement>) => {
const { key } = event;
@ -75,10 +76,8 @@ export const OTPForm: React.FC<OTPFormProps> = ({
<div className="space-y-6">
{/* Header */}
<div className="text-left space-y-2">
<h1 className="font-semibold text-3xl text-left">Please Enter OTP</h1>
<p className="text-default-500 text-base">
Enter the 6-digit code sent to your email address.
</p>
<h1 className="font-semibold text-3xl text-left">{t("enterOTP")}</h1>
<p className="text-default-500 text-base">{t("enterOTP2")}</p>
</div>
{/* OTP Input */}
@ -139,7 +138,7 @@ export const OTPForm: React.FC<OTPFormProps> = ({
disabled={loading}
className="text-sm text-blue-600 hover:text-blue-800 underline disabled:opacity-50 disabled:cursor-not-allowed"
>
Didn't receive the code? Resend
{t("enterOTP3")}
</button>
</div>
@ -155,10 +154,10 @@ export const OTPForm: React.FC<OTPFormProps> = ({
{loading ? (
<div className="flex items-center gap-2">
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
Verifying...
</div>
) : (
"Sign In"
t("enterOTP4")
)}
</Button>
</div>

View File

@ -689,7 +689,7 @@ export default function SignUp() {
)}
</div>
<div>
{/* <div>
<Select
value={namaTenant}
onValueChange={(value) => setNamaTenant(value)}
@ -715,9 +715,9 @@ export default function SignUp() {
{formErrors.namaTenant}
</p>
)}
</div>
</div> */}
{/* <div>
<div>
<Input
type="text"
required
@ -733,7 +733,7 @@ export default function SignUp() {
{formErrors.namaTenant}
</p>
)}
</div> */}
</div>
<div className="relative">
<Input

View File

@ -0,0 +1,58 @@
import React, { useRef, useEffect } from "react";
import { motion, useInView, useAnimation } from "framer-motion";
interface Props {
children: React.ReactNode;
}
export const Reveal = ({ children }: Props) => {
const ref = useRef(null);
const isInView = useInView(ref, { once: false });
const mainControls = useAnimation();
const slideControls = useAnimation();
useEffect(() => {
if (isInView) {
mainControls.start("visible");
slideControls.start("visible");
} else mainControls.start("hidden");
}, [isInView]);
return (
<div ref={ref}>
<motion.div
variants={{
hidden: { opacity: 0, y: 75 },
visible: { opacity: 1, y: 0 },
}}
initial="hidden"
animate={mainControls}
transition={{
duration: 1,
delay: 0.1,
}}
>
{children}
</motion.div>
{/* TODO green slide thingy */}
{/* <motion.div
variants={{
hidden: { left: 0 },
visible: { left: "100%" },
}}
initial="hidden"
animate={slideControls}
transition={{ duration: 0.5, ease: "easeIn" }}
style={{
position: "absolute",
top: 4,
bottom: 4,
left: 0,
right: 0,
background: "#5e84ff",
zIndex: 20,
}}
/> */}
</div>
);
};

View File

@ -1,9 +1,14 @@
"use client";
import { useState, useEffect } from "react";
import { getArticleCategories, ArticleCategory } from "@/service/categories/article-categories";
import {
getArticleCategories,
ArticleCategory,
} from "@/service/categories/article-categories";
import { useTranslations } from "next-intl";
export default function Category() {
const t = useTranslations("MediaUpdate");
const [categories, setCategories] = useState<ArticleCategory[]>([]);
const [loading, setLoading] = useState(true);
@ -15,7 +20,8 @@ export default function Category() {
if (response?.data?.success && response.data.data) {
// Filter hanya kategori yang aktif dan published
const activeCategories = response.data.data.filter(
(category: ArticleCategory) => category.isActive && category.isPublish
(category: ArticleCategory) =>
category.isActive && category.isPublish
);
setCategories(activeCategories);
}
@ -45,13 +51,16 @@ export default function Category() {
"SEPUTAR PRESTASI",
];
const displayCategories = categories.length > 0 ? categories : fallbackCategories;
const displayCategories =
categories.length > 0 ? categories : fallbackCategories;
return (
<section className="px-4 py-10">
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
<h2 className="text-xl font-semibold mb-5">
{loading ? "Memuat Kategori..." : `${displayCategories.length} Kategori Paling Populer`}
{loading
? t("loadCategory")
: `${displayCategories.length} ${t("category")}`}
</h2>
{loading ? (
@ -70,8 +79,12 @@ export default function Category() {
<div className="flex flex-wrap gap-3">
{displayCategories.map((category, index) => {
// Handle both API data and fallback data
const categoryTitle = typeof category === 'string' ? category : category.title;
const categorySlug = typeof category === 'string' ? category.toLowerCase().replace(/\s+/g, '-') : category.slug;
const categoryTitle =
typeof category === "string" ? category : category.title;
const categorySlug =
typeof category === "string"
? category.toLowerCase().replace(/\s+/g, "-")
: category.slug;
return (
<button
@ -79,7 +92,9 @@ export default function Category() {
className="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200"
onClick={() => {
// Navigate to category page or search by category
console.log(`Category clicked: ${categoryTitle} (${categorySlug})`);
console.log(
`Category clicked: ${categoryTitle} (${categorySlug})`
);
// TODO: Implement navigation to category page
}}
>

View File

@ -3,11 +3,16 @@
import { Instagram } from "lucide-react";
import Image from "next/image";
import { useState, useEffect } from "react";
import { getPublicClients, PublicClient } from "@/service/client/public-clients";
import {
getPublicClients,
PublicClient,
} from "@/service/client/public-clients";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Autoplay } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import LocalSwitcher from "../partials/header/locale-switcher";
import { useTranslations } from "next-intl";
// Custom styles for Swiper
const swiperStyles = `
@ -71,6 +76,7 @@ const logos = [
];
export default function Footer() {
const t = useTranslations("MediaUpdate");
const [clients, setClients] = useState<PublicClient[]>([]);
const [loading, setLoading] = useState(true);
@ -84,7 +90,6 @@ export default function Footer() {
}
} catch (error) {
console.error("Error fetching public clients:", error);
// Fallback to static logos if API fails
setClients([]);
} finally {
setLoading(false);
@ -100,7 +105,7 @@ export default function Footer() {
<div className="max-w-[1350px] mx-auto">
<div className="py-6">
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
Publikasi
{t("publication")}
</h2>
<div className="px-4 md:px-12">
<Swiper
@ -117,17 +122,19 @@ export default function Footer() {
disableOnInteraction: false,
}}
loop={clients.length > 4}
className={`client-swiper ${clients.length <= 4 ? 'swiper-centered' : ''}`}
className={`client-swiper ${
clients.length <= 4 ? "swiper-centered" : ""
}`}
>
{loading ? (
// Loading skeleton
{loading
? // Loading skeleton
Array.from({ length: 8 }).map((_, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
</SwiperSlide>
))
) : clients.length > 0 ? (
// Dynamic clients from API
: clients.length > 0
? // Dynamic clients from API
clients.map((client, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<a
@ -159,8 +166,7 @@ export default function Footer() {
</a>
</SwiperSlide>
))
) : (
// Fallback to static logos if API fails or no data
: // Fallback to static logos if API fails or no data
logos.map((logo, idx) => (
<SwiperSlide key={idx} className="!w-auto">
<a
@ -178,13 +184,12 @@ export default function Footer() {
/>
</a>
</SwiperSlide>
))
)}
))}
</Swiper>
{/* Navigation Buttons */}
<div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div>
<div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div>
{/* <div className="swiper-button-prev !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !left-0 !-translate-y-1/2"></div> */}
{/* <div className="swiper-button-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div> */}
</div>
</div>
@ -192,7 +197,7 @@ export default function Footer() {
<div className="flex flex-col md:flex-row items-center justify-between gap-4 px-4 pb-6 max-w-6xl mx-auto text-sm text-gray-600">
<div className="flex items-center gap-2">
<span>ver 1.0.0 @2025 - Layanan NetIDHUB disediakan</span>
<span>ver 1.0.0 @2025 - {t("netidhub")}</span>
<Image
src="/qudo.png"
alt="qudoco"
@ -260,6 +265,11 @@ export default function Footer() {
</g>
</svg>
</div>
{/* button language */}
<div className={`relative text-left border rounded-lg`}>
<LocalSwitcher />
</div>
</div>
</div>
</footer>

View File

@ -11,22 +11,33 @@ import { listData, listArticles } from "@/service/landing/landing";
import { getCookiesDecrypt } from "@/lib/utils";
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
import { Swiper, SwiperSlide } from "swiper/react";
import { Navigation, Pagination } from "swiper/modules";
import "swiper/css";
import "swiper/css/navigation";
import "swiper/css/pagination";
import ImageBlurry from "../ui/image-blurry";
import { useTranslations } from "next-intl";
const images = ["/PPS.png", "/PPS2.jpeg", "/PPS3.jpg", "/PPS4.png"];
export default function Header() {
const t = useTranslations("MediaUpdate");
const [data, setData] = useState<any[]>([]);
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
const router = useRouter();
const params = useParams();
const MySwal = withReactContent(Swal);
const slug = params?.slug as string;
// ✅ Ambil data artikel (khusus typeId = 1 -> Image)
useEffect(() => {
const fetchData = async () => {
try {
const response = await listArticles(
1,
5,
1, // hanya typeId = 1 (image)
1,
undefined,
undefined,
"createdAt",
@ -36,7 +47,7 @@ export default function Header() {
let articlesData: any[] = [];
if (response?.error) {
const fallback = await listData(
const fallbackResponse = await listData(
"",
"",
"",
@ -45,40 +56,46 @@ export default function Header() {
"createdAt",
"",
"",
"1"
""
);
articlesData = (fallbackResponse?.data?.data?.content || []).filter(
(item: any) => item.typeId === 1
);
articlesData = fallback?.data?.data?.content || [];
} else {
articlesData = response?.data?.data || [];
articlesData = (response?.data?.data || []).filter(
(item: any) => item.typeId === 1
);
}
const transformed = articlesData.map((article: any) =>
itemTransform(article)
);
const transformed = 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,
clientName: article.clientName,
categories: article.categories,
label: "Image",
}));
setData(transformed);
} catch (error) {
console.error("Gagal memuat data:", error);
}
};
fetchData();
}, [slug]);
// ✅ Sinkronisasi bookmark: dari localStorage + backend user login
useEffect(() => {
const syncBookmarks = async () => {
try {
const roleId = Number(getCookiesDecrypt("urie"));
let localSet = new Set<number>();
if (roleId && !isNaN(roleId)) {
const userId = getCookiesDecrypt("uie");
const localKey = `bookmarkedIds_${userId || "guest"}`;
const saved = localStorage.getItem(localKey);
const simpananLocal = localStorage.getItem("bookmarkedIds");
if (simpananLocal) {
localSet = new Set(JSON.parse(simpananLocal));
let localSet = new Set<number>();
if (saved) {
localSet = new Set(JSON.parse(saved));
setBookmarkedIds(localSet);
}
// Jika user login, gabungkan dengan data dari backend
if (roleId && !isNaN(roleId)) {
const res = await getBookmarkSummaryForUser();
const bookmarks =
res?.data?.data?.recentBookmarks ||
@ -92,34 +109,41 @@ export default function Header() {
.filter((x) => !isNaN(x))
);
const gabungan = new Set([...localSet, ...ids]);
setBookmarkedIds(gabungan);
const merged = new Set([...localSet, ...ids]);
setBookmarkedIds(merged);
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(gabungan))
JSON.stringify(Array.from(merged))
);
} else {
// Jika belum login, pakai local saja
setBookmarkedIds(localSet);
}
} catch (err) {
console.error("Gagal sinkronisasi bookmark:", err);
} catch (error) {
console.error("Gagal memuat data:", error);
}
};
syncBookmarks();
fetchData();
}, []);
useEffect(() => {
if (bookmarkedIds.size > 0) {
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(bookmarkedIds))
);
}
}, [bookmarkedIds]);
return (
<section className="max-w-[1350px] mx-auto px-4">
<div className="flex flex-col lg:flex-row gap-6 py-6">
{data.length > 0 && (
<Card
key={data[0].id}
item={data[0]}
isBig
bookmarkedIds={bookmarkedIds}
setBookmarkedIds={setBookmarkedIds}
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
onSaved={(id) =>
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
/>
)}
@ -128,74 +152,80 @@ export default function Header() {
<Card
key={item.id}
item={item}
bookmarkedIds={bookmarkedIds}
setBookmarkedIds={setBookmarkedIds}
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
onSaved={(id) =>
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
}
/>
))}
</div>
</div>
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
<Image
src={"/PPS.png"}
alt={"pps"}
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden">
<Swiper
modules={[Navigation, Pagination]}
navigation
pagination={{ clickable: true }}
spaceBetween={10}
slidesPerView={1}
loop={true}
className="w-full h-full"
>
{images.map((img, index) => (
<SwiperSlide key={index}>
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px]">
{/* <Image
src={img}
alt={`slide-${index}`}
fill
className="object-cover rounded-xl"
priority={index === 0}
/> */}
<ImageBlurry
priority
src={img}
alt="gambar"
style={{
objectFit: "contain",
width: "100%",
height: "100%",
}}
/>
</div>
</SwiperSlide>
))}
</Swiper>
</div>
</section>
);
}
// 🔹 Helper function
function itemTransform(article: any) {
return {
id: article.id,
title: article.title,
categoryName:
article.categoryName ||
(article.categories && article.categories[0]?.title) ||
"",
createdAt: article.createdAt,
smallThumbnailLink: article.thumbnailUrl,
fileTypeId: article.typeId,
clientName: article.clientName,
categories: article.categories,
label:
article.typeId === 1
? "Image"
: article.typeId === 2
? "Video"
: article.typeId === 3
? "Text"
: article.typeId === 4
? "Audio"
: "",
};
}
// 🔹 Komponen Card
function Card({
item,
isBig = false,
bookmarkedIds,
setBookmarkedIds,
isInitiallyBookmarked = false,
onSaved,
}: {
item: any;
isBig?: boolean;
bookmarkedIds: Set<number>;
setBookmarkedIds: React.Dispatch<React.SetStateAction<Set<number>>>;
isInitiallyBookmarked?: boolean;
onSaved?: (id: number) => void;
}) {
const router = useRouter();
const t = useTranslations("MediaUpdate");
const MySwal = withReactContent(Swal);
const [isSaving, setIsSaving] = useState(false);
const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
const isBookmarked = bookmarkedIds.has(Number(item.id));
useEffect(() => {
setIsBookmarked(isInitiallyBookmarked);
}, [isInitiallyBookmarked]);
const getLink = () => `/content/image/detail/${item?.id}`;
const handleToggleBookmark = async () => {
const handleSave = async () => {
const roleId = Number(getCookiesDecrypt("urie"));
if (!roleId || isNaN(roleId)) {
MySwal.fire({
icon: "warning",
@ -215,31 +245,26 @@ function Card({
MySwal.fire({
icon: "error",
title: "Gagal",
text: "Gagal memperbarui bookmark.",
text: "Gagal menyimpan artikel.",
confirmButtonColor: "#d33",
});
} else {
const updated = new Set(bookmarkedIds);
let pesan = "";
setIsBookmarked(true);
onSaved?.(item.id);
if (isBookmarked) {
updated.delete(Number(item.id));
pesan = "Dihapus dari bookmark.";
} else {
updated.add(Number(item.id));
pesan = "Artikel disimpan ke bookmark.";
}
setBookmarkedIds(updated);
const saved = localStorage.getItem("bookmarkedIds");
const newSet = new Set<number>(saved ? JSON.parse(saved) : []);
newSet.add(Number(item.id));
localStorage.setItem(
"bookmarkedIds",
JSON.stringify(Array.from(updated))
JSON.stringify(Array.from(newSet))
);
MySwal.fire({
icon: "success",
title: isBookmarked ? "Dihapus!" : "Disimpan!",
text: pesan,
title: "Berhasil",
text: "Artikel berhasil disimpan ke bookmark.",
confirmButtonColor: "#3085d6",
timer: 1500,
showConfirmButton: false,
});
@ -258,14 +283,19 @@ function Card({
};
return (
<div>
<div
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
isBig
? "w-full lg:max-w-[670px] h-[680px]"
: "w-full h-[360px] md:h-[340px]"
? "w-full lg:max-w-[670px] lg:min-h-[680px]"
: "w-full h-[350px] md:h-[330px]"
}`}
>
<div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
<div
className={`relative ${
isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
} w-full`}
>
<Link href={getLink()}>
<Image
src={item.smallThumbnailLink || "/contributor.png"}
@ -276,8 +306,7 @@ function Card({
</Link>
</div>
<div className="p-4 flex flex-col justify-between flex-1">
<div className="space-y-2">
<div className="py-[26px] px-4 space-y-2">
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
<span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
{item.clientName}
@ -307,9 +336,8 @@ function Card({
{item.title}
</h3>
</Link>
</div>
<div className="flex justify-between items-center pt-4">
<div className="flex justify-between items-center pt-2">
<div className="flex gap-2 text-gray-500">
<ThumbsUp
size={18}
@ -322,7 +350,7 @@ function Card({
</div>
<button
onClick={handleToggleBookmark}
onClick={handleSave}
disabled={isSaving || isBookmarked}
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
isBookmarked
@ -330,11 +358,12 @@ function Card({
: "bg-[#F60100] text-white hover:bg-[#c90000]"
}`}
>
{isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
{isSaving ? t("saving") : isBookmarked ? t("saved") : t("save")}
</button>
</div>
</div>
</div>
</div>
);
}
@ -357,18 +386,16 @@ function Card({
// const router = useRouter();
// const params = useParams();
// const MySwal = withReactContent(Swal);
// // Get slug from URL params
// const slug = params?.slug as string;
// // ✅ Ambil data artikel (khusus typeId = 1 -> Image)
// useEffect(() => {
// const fetchData = async () => {
// try {
// // 🔹 Ambil artikel
// const response = await listArticles(
// 1,
// 5,
// undefined,
// 1, // hanya typeId = 1 (image)
// undefined,
// undefined,
// "createdAt",
@ -378,8 +405,7 @@ function Card({
// let articlesData: any[] = [];
// if (response?.error) {
// // fallback ke API lama
// const fallbackResponse = await listData(
// const fallback = await listData(
// "",
// "",
// "",
@ -388,15 +414,111 @@ function Card({
// "createdAt",
// "",
// "",
// ""
// "1"
// );
// articlesData = fallbackResponse?.data?.data?.content || [];
// articlesData = fallback?.data?.data?.content || [];
// } else {
// articlesData = response?.data?.data || [];
// }
// // 🔹 Transform agar seragam
// const transformed = articlesData.map((article: any) => ({
// const transformed = articlesData.map((article: any) =>
// itemTransform(article)
// );
// setData(transformed);
// } catch (error) {
// console.error("Gagal memuat data:", error);
// }
// };
// fetchData();
// }, [slug]);
// // ✅ Sinkronisasi bookmark: dari localStorage + backend user login
// useEffect(() => {
// const syncBookmarks = async () => {
// try {
// const roleId = Number(getCookiesDecrypt("urie"));
// let localSet = new Set<number>();
// const simpananLocal = localStorage.getItem("bookmarkedIds");
// if (simpananLocal) {
// localSet = new Set(JSON.parse(simpananLocal));
// }
// // Jika user login, gabungkan dengan data dari backend
// if (roleId && !isNaN(roleId)) {
// const res = await getBookmarkSummaryForUser();
// const bookmarks =
// res?.data?.data?.recentBookmarks ||
// res?.data?.data?.bookmarks ||
// res?.data?.data ||
// [];
// const ids = new Set<number>(
// (Array.isArray(bookmarks) ? bookmarks : [])
// .map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
// .filter((x) => !isNaN(x))
// );
// const gabungan = new Set([...localSet, ...ids]);
// setBookmarkedIds(gabungan);
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(gabungan))
// );
// } else {
// // Jika belum login, pakai local saja
// setBookmarkedIds(localSet);
// }
// } catch (err) {
// console.error("Gagal sinkronisasi bookmark:", err);
// }
// };
// syncBookmarks();
// }, []);
// return (
// <section className="max-w-[1350px] mx-auto px-4">
// <div className="flex flex-col lg:flex-row gap-6 py-6">
// {data.length > 0 && (
// <Card
// key={data[0].id}
// item={data[0]}
// isBig
// bookmarkedIds={bookmarkedIds}
// setBookmarkedIds={setBookmarkedIds}
// />
// )}
// <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
// {data.slice(1, 5).map((item) => (
// <Card
// key={item.id}
// item={item}
// bookmarkedIds={bookmarkedIds}
// setBookmarkedIds={setBookmarkedIds}
// />
// ))}
// </div>
// </div>
// <div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
// <Image
// src={"/PPS.png"}
// alt={"pps"}
// fill
// className="object-cover rounded-xl"
// />
// </div>
// </section>
// );
// }
// // 🔹 Helper function
// function itemTransform(article: any) {
// return {
// id: article.id,
// title: article.title,
// categoryName:
@ -418,139 +540,31 @@ function Card({
// : article.typeId === 4
// ? "Audio"
// : "",
// }));
// setData(transformed);
// const roleId = Number(getCookiesDecrypt("urie"));
// if (roleId && !isNaN(roleId)) {
// // const saved = localStorage.getItem("bookmarkedIds");
// const userId = getCookiesDecrypt("uie");
// const localKey = `bookmarkedIds_${userId || "guest"}`;
// const saved = localStorage.getItem(localKey);
// let localSet = new Set<number>();
// if (saved) {
// localSet = new Set(JSON.parse(saved));
// setBookmarkedIds(localSet);
// }
// const res = await getBookmarkSummaryForUser();
// const bookmarks =
// res?.data?.data?.recentBookmarks ||
// res?.data?.data?.bookmarks ||
// res?.data?.data ||
// [];
// const ids = new Set<number>(
// (Array.isArray(bookmarks) ? bookmarks : [])
// .map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
// .filter((x) => !isNaN(x))
// );
// const merged = new Set([...localSet, ...ids]);
// setBookmarkedIds(merged);
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(merged))
// );
// }
// } catch (error) {
// console.error("Gagal memuat data:", error);
// }
// };
// fetchData();
// }, []);
// // Simpan setiap kali state berubah
// useEffect(() => {
// if (bookmarkedIds.size > 0) {
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(bookmarkedIds))
// );
// }
// }, [bookmarkedIds]);
// return (
// <section className="max-w-[1350px] mx-auto px-4">
// <div className="flex flex-col lg:flex-row gap-6 py-6">
// {data.length > 0 && (
// <Card
// item={data[0]}
// isBig
// isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
// onSaved={(id) =>
// setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
// }
// />
// )}
// <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
// {data.slice(1, 5).map((item) => (
// <Card
// key={item.id}
// item={item}
// isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
// onSaved={(id) =>
// setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
// }
// />
// ))}
// </div>
// </div>
// <div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
// <Image
// src={"/PPS.png"}
// alt={"pps"}
// fill
// className="object-cover rounded-xl"
// />
// </div>
// </section>
// );
// }
// // 🔹 Komponen Card
// function Card({
// item,
// isBig = false,
// isInitiallyBookmarked = false,
// onSaved,
// bookmarkedIds,
// setBookmarkedIds,
// }: {
// item: any;
// isBig?: boolean;
// isInitiallyBookmarked?: boolean;
// onSaved?: (id: number) => void;
// bookmarkedIds: Set<number>;
// setBookmarkedIds: React.Dispatch<React.SetStateAction<Set<number>>>;
// }) {
// const router = useRouter();
// const MySwal = withReactContent(Swal);
// const [isSaving, setIsSaving] = useState(false);
// const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
// useEffect(() => {
// setIsBookmarked(isInitiallyBookmarked);
// }, [isInitiallyBookmarked]);
// const isBookmarked = bookmarkedIds.has(Number(item.id));
// const getLink = () => {
// switch (item?.fileTypeId) {
// case 1:
// return `/content/image/detail/${item?.id}`;
// case 2:
// return `/content/video/detail/${item?.id}`;
// case 3:
// return `/content/text/detail/${item?.id}`;
// case 4:
// return `/content/audio/detail/${item?.id}`;
// default:
// return "#";
// }
// };
// const getLink = () => `/content/image/detail/${item?.id}`;
// const handleSave = async () => {
// const handleToggleBookmark = async () => {
// const roleId = Number(getCookiesDecrypt("urie"));
// if (!roleId || isNaN(roleId)) {
// MySwal.fire({
// icon: "warning",
@ -570,27 +584,31 @@ function Card({
// MySwal.fire({
// icon: "error",
// title: "Gagal",
// text: "Gagal menyimpan artikel.",
// text: "Gagal memperbarui bookmark.",
// confirmButtonColor: "#d33",
// });
// } else {
// setIsBookmarked(true);
// onSaved?.(item.id);
// const updated = new Set(bookmarkedIds);
// let pesan = "";
// // 🔹 Simpan ke localStorage
// const saved = localStorage.getItem("bookmarkedIds");
// const newSet = new Set<number>(saved ? JSON.parse(saved) : []);
// newSet.add(Number(item.id));
// if (isBookmarked) {
// updated.delete(Number(item.id));
// pesan = "Dihapus dari bookmark.";
// } else {
// updated.add(Number(item.id));
// pesan = "Artikel disimpan ke bookmark.";
// }
// setBookmarkedIds(updated);
// localStorage.setItem(
// "bookmarkedIds",
// JSON.stringify(Array.from(newSet))
// JSON.stringify(Array.from(updated))
// );
// MySwal.fire({
// icon: "success",
// title: "Berhasil",
// text: "Artikel berhasil disimpan ke bookmark.",
// confirmButtonColor: "#3085d6",
// title: isBookmarked ? "Dihapus!" : "Disimpan!",
// text: pesan,
// timer: 1500,
// showConfirmButton: false,
// });
@ -609,19 +627,14 @@ function Card({
// };
// return (
// <div>
// <div
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
// isBig
// ? "w-full lg:max-w-[670px] lg:min-h-[680px]"
// : "w-full h-[350px] md:h-[330px]"
// ? "w-full lg:max-w-[670px] h-[680px]"
// : "w-full h-[360px] md:h-[340px]"
// }`}
// >
// <div
// className={`relative ${
// isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
// } w-full`}
// >
// <div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
// <Link href={getLink()}>
// <Image
// src={item.smallThumbnailLink || "/contributor.png"}
@ -632,7 +645,8 @@ function Card({
// </Link>
// </div>
// <div className="p-4 space-y-2">
// <div className="p-4 flex flex-col justify-between flex-1">
// <div className="space-y-2">
// <div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
// <span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
// {item.clientName}
@ -662,8 +676,9 @@ function Card({
// {item.title}
// </h3>
// </Link>
// </div>
// <div className="flex justify-between items-center pt-2">
// <div className="flex justify-between items-center pt-4">
// <div className="flex gap-2 text-gray-500">
// <ThumbsUp
// size={18}
@ -676,7 +691,7 @@ function Card({
// </div>
// <button
// onClick={handleSave}
// onClick={handleToggleBookmark}
// disabled={isSaving || isBookmarked}
// className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
// isBookmarked
@ -684,11 +699,10 @@ function Card({
// : "bg-[#F60100] text-white hover:bg-[#c90000]"
// }`}
// >
// {isSaving ? "Saving..." : isBookmarked ? "Saved" : "Save"}
// {isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
// </button>
// </div>
// </div>
// </div>
// </div>
// );
// }

View File

@ -16,6 +16,7 @@ import { Navigation } from "swiper/modules";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { ThumbsUp, ThumbsDown } from "lucide-react";
import { useTranslations } from "next-intl";
function formatTanggal(dateString: string) {
if (!dateString) return "";
@ -35,6 +36,7 @@ function formatTanggal(dateString: string) {
}
export default function MediaUpdate() {
const t = useTranslations("MediaUpdate");
const [tab, setTab] = useState<"latest" | "popular">("latest");
const [contentType, setContentType] = useState<
"audiovisual" | "audio" | "foto" | "text" | "all"
@ -329,7 +331,7 @@ export default function MediaUpdate() {
<section className="bg-white px-4 py-10 border max-w-[1350px] mx-auto rounded-md border-[#CDD5DF] my-10">
<div className="max-w-screen-xl mx-auto">
<h2 className="text-2xl font-semibold text-center mb-6">
Media Update
{t("title")}
</h2>
{/* Main Tab */}
@ -343,7 +345,7 @@ export default function MediaUpdate() {
: "text-[#C6A455] hover:bg-[#C6A455]/10"
}`}
>
Terbaru
{t("latest")}
</button>
<button
onClick={() => setTab("popular")}
@ -353,7 +355,7 @@ export default function MediaUpdate() {
: "text-[#C6A455] hover:bg-[#C6A455]/10"
}`}
>
Terpopuler
{t("popular")}{" "}
</button>
</Card>
</div>
@ -381,7 +383,7 @@ export default function MediaUpdate() {
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
}`}
>
📸 Foto
📸 {t("image")}
</button>
<button
onClick={() => setContentType("audiovisual")}
@ -391,7 +393,7 @@ export default function MediaUpdate() {
: "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md"
}`}
>
🎬 Audio Visual
🎬 {t("video")}
</button>
<button
onClick={() => setContentType("audio")}
@ -411,7 +413,7 @@ export default function MediaUpdate() {
: "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md"
}`}
>
📝 Text
📝 {t("text")}
</button>
</div>
</div>
@ -419,7 +421,7 @@ export default function MediaUpdate() {
{/* Slider */}
{loading ? (
<p className="text-center">Memuat konten...</p>
<p className="text-center">{t("loadContent")}</p>
) : (
<Swiper
modules={[Navigation]}
@ -517,8 +519,8 @@ export default function MediaUpdate() {
}`}
>
{bookmarkedIds.has(Number(item.id))
? "Tersimpan"
: "Simpan"}
? t("saved")
: t("save")}
</Button>
</div>
</div>
@ -536,7 +538,7 @@ export default function MediaUpdate() {
size={"lg"}
className="text-[#b3882e] bg-transparent border border-[#b3882e] px-6 py-2 rounded-s-sm text-sm font-medium hover:bg-[#b3882e]/10 transition"
>
Lihat Lebih Banyak
{t("seeMore")}
</Button>
</Link>
</div>

View File

@ -11,22 +11,11 @@ import { Input } from "../ui/input";
import { usePathname, useRouter } from "next/navigation";
import { Link } from "@/i18n/routing";
import { DynamicLogoTenant } from "./dynamic-logo-tenant";
const NAV_ITEMS = [
{ label: "Beranda", href: "/" },
{ label: "Untuk Anda", href: "/for-you" },
{ label: "Mengikuti", href: "/auth" },
{ label: "Publikasi", href: "/publikasi" },
{ label: "Jadwal", href: "/schedule" },
];
const PUBLIKASI_SUBMENU = [
{ label: "K/L", href: "/in/publication/kl" },
{ label: "BUMN", href: "/in/publication/bumn" },
{ label: "Pemerintah Daerah", href: "/in/publication/pemerintah-daerah" },
];
import { useTranslations } from "next-intl";
import LocalSwitcher from "../partials/header/locale-switcher";
export default function Navbar() {
const t = useTranslations("Navbar");
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [isDropdownOpen, setDropdownOpen] = useState(false);
const [showProfileMenu, setShowProfileMenu] = useState(false);
@ -34,6 +23,20 @@ export default function Navbar() {
const pathname = usePathname();
const router = useRouter();
const NAV_ITEMS = [
{ label: t("home"), href: "/" },
{ label: t("forYou"), href: "/for-you" },
{ label: t("following"), href: "/auth" },
{ label: t("publication"), href: "/publikasi" },
{ label: t("schedule"), href: "/schedule" },
];
const PUBLIKASI_SUBMENU = [
{ label: "K/L", href: "/in/publication/kl" },
{ label: "BUMN", href: "/in/publication/bumn" },
{ label: t("localGov"), href: "/in/publication/pemerintah-daerah" },
];
// 🔍 Fungsi cek login
const checkLoginStatus = () => {
const roleId = getCookiesDecrypt("urie");
@ -46,7 +49,7 @@ export default function Navbar() {
}, []);
const filteredNavItems = isLoggedIn
? NAV_ITEMS.filter((item) => item.label !== "Mengikuti")
? NAV_ITEMS.filter((item) => item.label !== t("following"))
: NAV_ITEMS;
// 🚪 Fungsi logout
@ -73,18 +76,20 @@ export default function Navbar() {
return (
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white z-50">
<div className="flex flex-row items-center justify-between space-x-4 z-10">
<div className="relative w-32 h-20">
<Menu
className="w-6 h-6 cursor-pointer"
onClick={() => setIsSidebarOpen(true)}
/>
<Link href="/" className="relative w-32 h-20">
<Image
src="/assets/logo1.png"
alt="Logo"
fill
className="object-contain"
/>
</div>
<Menu
className="w-6 h-6 cursor-pointer"
onClick={() => setIsSidebarOpen(true)}
/>
</Link>
<DynamicLogoTenant />
</div>
@ -95,7 +100,7 @@ export default function Navbar() {
// 🔹 Pengecekan khusus untuk "Untuk Anda"
const handleClick = (e: React.MouseEvent) => {
if (item.label === "Untuk Anda") {
if (item.label === t("forYou")) {
e.preventDefault();
if (!checkLoginStatus()) {
router.push("/auth");
@ -107,7 +112,7 @@ export default function Navbar() {
return (
<div key={item.label} className="relative">
{item.label === "Publikasi" ? (
{item.label === t("publication") ? (
<>
<button
onClick={() => setDropdownOpen(!isDropdownOpen)}
@ -171,11 +176,11 @@ export default function Navbar() {
<>
<Link href="/auth/register">
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white">
Daftar
{t("register")}{" "}
</Button>
</Link>
<Link href="/auth">
<Button className="bg-red-700 text-white">Masuk</Button>
<Button className="bg-red-700 text-white">{t("login")}</Button>
</Link>
</>
) : (
@ -205,7 +210,7 @@ export default function Navbar() {
onClick={handleLogout}
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-gray-700"
>
Logout
{t("logout")}
</button>
</div>
)}
@ -226,9 +231,13 @@ export default function Navbar() {
<div className="mt-10">
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
Bahasa
{t("language")}
</h3>
<div className="space-y-5 ml-3">
{/* button language */}
<div className={`relative text-left border w-fit rounded-lg`}>
<LocalSwitcher />
</div>
{/* <div className="space-y-5 ml-3">
<button className="flex items-center gap-2 text-sm text-gray-800">
<Image src={"/Flag.svg"} width={24} height={24} alt="flag" />
English
@ -251,19 +260,19 @@ export default function Navbar() {
</svg>{" "}
Bahasa Indonesia
</button>
</div>
</div> */}
</div>
<div>
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
Fitur
{t("features")}
</h3>
<div className="space-y-5 ml-3">
{NAV_ITEMS.map((item) => (
<button
key={item.label}
onClick={() => {
if (item.label === "Untuk Anda") {
if (item.label === t("forYou")) {
if (!checkLoginStatus()) {
router.push("/auth");
} else {
@ -284,25 +293,25 @@ export default function Navbar() {
<div className="space-y-5 text-[16px] font-bold">
<Link href="/about" className="block text-black">
Tentang Kami
{t("about")}
</Link>
<Link href="/advertising" className="block text-black">
Advertising
{t("advertising")}
</Link>
<Link href="/contact" className="block text-black">
Kontak Kami
{t("contact")}
</Link>
{!isLoggedIn ? (
<>
<Link href="/auth" className="block text-lg text-gray-800">
Login
{t("login")}
</Link>
<Link
href="/auth/register"
className="block text-lg text-gray-800"
>
Daftar
{t("register")}
</Link>
</>
) : (
@ -310,17 +319,17 @@ export default function Navbar() {
onClick={handleLogout}
className="block text-left w-full text-lg text-red-600 hover:underline"
>
Logout
{t("logout")}
</button>
)}
</div>
<Card className="rounded-none p-4">
<h2 className="text-[#C6A455] text-center text-lg font-semibold">
Subscribe to Our Newsletter
<h2 className="text-[#C6A455] text-center text-lg font-semibold mb-2">
{t("subscribeTitle")}
</h2>
<Input type="email" placeholder="Your email address" />
<Button className="bg-[#C6A455]">Subscribe</Button>
<Input type="email" placeholder={t("subscribePlaceholder")} />
<Button className="bg-[#C6A455] mt-2">{t("subscribeButton")}</Button>
</Card>
</div>

View File

@ -1,6 +1,6 @@
"use client";
import { useState } from "react";
import { useState, useEffect } from "react";
import { format } from "date-fns";
import { Calendar } from "@/components/ui/calendar";
import {
@ -20,131 +20,17 @@ import {
SelectItem,
} from "@/components/ui/select";
import { Badge } from "@/components/ui/badge";
import { getAllSchedules } from "@/service/landing/landing";
const scheduleData = [
{
date: "Jul 1 2025",
type: "POLRI",
title: "HUT Bhayangkara RI - 79",
location: "Mabes Polri, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "POLRI",
title: "Hari Lahir Pancasila",
location: "Mabes Polri, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "POLRI",
title: "Pers Rilis Kasus",
location: "Mabes Polri, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "POLRI",
title: "Rapat Koordinasi HUT Bhayangkara RI - 79",
location: "Mabes Polri, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "MPR",
title: "Rapat PIMPINAN MPR RI",
location: "Gedung MPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "DPR",
title: "Rapat Anggota Komisi I",
location: "Gedung DPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "MPR",
title: "Sidang Paripurna",
location: "Gedung MPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "KEJAKSAAN AGUNG",
title: "Hari Lahir Pancasila",
location: "Kejaksaan Agung, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "KPU",
title: "Hari Lahir Pancasila",
location: "Kantor KPU, Jakarta Indonesia",
},
{
date: "Jul 1 2025",
type: "DPR",
title: "Rapat Anggota Komisi II",
location: "Gedung DPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "MPR",
title: "Rapat DPR dan Basarnas",
location: "Gedung MPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "BUMN",
title: "Hari Lahir Pancasila",
location: "Kantor BUMN, Jakarta Indonesia",
},
{
date: "Jul 1 2025",
type: "BUMN",
title: "Focus Group Discussion",
location: "Kantor BUMN, Jakarta Indonesia",
},
{
date: "Jul 1 2025",
type: "MPR",
title: "Rapat Anggota MPR RI",
location: "Gedung MPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "BUMN",
title: "Seremoni Sinergi BUMN",
location: "Kantor BUMN, Jakarta Indonesia",
},
{
date: "Jul 1 2025",
type: "MPR",
title: "Sumpah Janji Anggota MPR RI",
location: "Gedung MPR, Jakarta, Indonesia",
},
{
date: "Jul 1 2025",
type: "KPK",
title: "Hari Lahir Pancasila",
location: "Kantor KPK, Jakarta Indonesia",
},
{
date: "Jul 1 2025",
type: "BUMN",
title: "Monitoring dan Evaluasi Keterbukaan Informasi Publik Tahun 2025",
location: "Kantor BUMN, Jakarta Indonesia",
},
{
date: "Jul 1 2025",
type: "KEJAKSAAN AGUNG",
title: "Hari Lahir Pancasila",
location: "Kejaksaan Agung, Jakarta, Indonesia",
},
{
date: "Jul 2 2025",
type: "MPR",
title: "Monitoring dan Evaluasi Informasi MPR Tahun 2025",
location: "Gedung MPR, Jakarta, Indonesia",
},
];
export default function Schedule() {
const [search, setSearch] = useState("");
const [startDate, setStartDate] = useState<Date | undefined>(new Date());
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
const [selectedCategory, setSelectedCategory] = useState("SEMUA");
const [scheduleData, setScheduleData] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
const categories = [
const categories = [
"SEMUA",
"POLRI",
"MAHKAMAH AGUNG",
@ -156,26 +42,61 @@ const categories = [
"BSKDN",
"BUMN",
"KPU",
];
];
export default function Schedule() {
const [search, setSearch] = useState("");
const [startDate, setStartDate] = useState<Date | undefined>(
new Date("2025-07-01")
);
const [endDate, setEndDate] = useState<Date | undefined>(
new Date("2025-07-30")
);
const [selectedCategory, setSelectedCategory] = useState("SEMUA");
const fetchSchedules = async () => {
try {
setLoading(true);
const filteredData = scheduleData.filter((item) => {
const params = {
title: search || undefined,
startDate: startDate ? format(startDate, "yyyy-MM-dd") : undefined,
endDate: endDate ? format(endDate, "yyyy-MM-dd") : undefined,
page: 1,
limit: 50,
sortBy: "startDate",
sort: "asc",
};
const res = await getAllSchedules(params);
if (!res.error) {
const apiData = Array.isArray(res.data)
? res.data
: Array.isArray(res.data?.data)
? res.data.data
: Array.isArray(res.data?.records)
? res.data.records
: [];
setScheduleData(apiData);
} else {
console.error("Gagal memuat jadwal:", res.message);
setScheduleData([]);
}
} catch (error) {
console.error("Error fetching schedules:", error);
setScheduleData([]);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchSchedules();
}, [startDate, endDate, search]);
const filteredData = Array.isArray(scheduleData)
? scheduleData.filter((item) => {
const matchesCategory =
selectedCategory === "SEMUA" || item.type === selectedCategory;
selectedCategory === "SEMUA" ||
item.type?.toUpperCase() === selectedCategory;
const matchesSearch = item.title
.toLowerCase()
?.toLowerCase()
.includes(search.toLowerCase());
return matchesCategory && matchesSearch;
});
})
: [];
return (
<div className="p-6 max-w-[1350px] mx-auto">
@ -276,6 +197,16 @@ export default function Schedule() {
<h2 className="text-md font-semibold text-muted-foreground mb-4">
Semua Jadwal
</h2>
{loading ? (
<div className="text-center text-muted-foreground py-10">
Memuat data jadwal...
</div>
) : filteredData.length === 0 ? (
<div className="text-center text-muted-foreground py-10">
Tidak ada jadwal ditemukan.
</div>
) : (
<div className="flex flex-col gap-3">
{filteredData.map((item, index) => (
<div
@ -283,7 +214,9 @@ export default function Schedule() {
className="flex justify-between items-start border-b pb-2"
>
<div className="w-1/6 text-sm text-muted-foreground">
{item.date}
{item.startDate
? format(new Date(item.startDate), "dd MMM yyyy")
: "-"}
</div>
<div className="w-1/5">
<Badge
@ -297,7 +230,7 @@ export default function Schedule() {
"bg-red-700": item.type === "KPK",
})}
>
{item.type}
{item.type || "-"}
</Badge>
</div>
<div className="w-2/5 text-sm">{item.title}</div>
@ -307,6 +240,8 @@ export default function Schedule() {
</div>
))}
</div>
)}
<div className="flex justify-between mt-6 text-sm text-blue-600">
<button>Preview</button>
<button>Next</button>
@ -314,3 +249,320 @@ export default function Schedule() {
</div>
);
}
// "use client";
// import { useState } from "react";
// import { format } from "date-fns";
// import { Calendar } from "@/components/ui/calendar";
// import {
// Popover,
// PopoverContent,
// PopoverTrigger,
// } from "@/components/ui/popover";
// import { Button } from "@/components/ui/button";
// import { Input } from "@/components/ui/input";
// import { CalendarIcon, ChevronRight } from "lucide-react";
// import { cn } from "@/lib/utils";
// import {
// Select,
// SelectTrigger,
// SelectValue,
// SelectContent,
// SelectItem,
// } from "@/components/ui/select";
// import { Badge } from "@/components/ui/badge";
// const scheduleData = [
// {
// date: "Jul 1 2025",
// type: "POLRI",
// title: "HUT Bhayangkara RI - 79",
// location: "Mabes Polri, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "POLRI",
// title: "Hari Lahir Pancasila",
// location: "Mabes Polri, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "POLRI",
// title: "Pers Rilis Kasus",
// location: "Mabes Polri, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "POLRI",
// title: "Rapat Koordinasi HUT Bhayangkara RI - 79",
// location: "Mabes Polri, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "MPR",
// title: "Rapat PIMPINAN MPR RI",
// location: "Gedung MPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "DPR",
// title: "Rapat Anggota Komisi I",
// location: "Gedung DPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "MPR",
// title: "Sidang Paripurna",
// location: "Gedung MPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "KEJAKSAAN AGUNG",
// title: "Hari Lahir Pancasila",
// location: "Kejaksaan Agung, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "KPU",
// title: "Hari Lahir Pancasila",
// location: "Kantor KPU, Jakarta Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "DPR",
// title: "Rapat Anggota Komisi II",
// location: "Gedung DPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "MPR",
// title: "Rapat DPR dan Basarnas",
// location: "Gedung MPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "BUMN",
// title: "Hari Lahir Pancasila",
// location: "Kantor BUMN, Jakarta Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "BUMN",
// title: "Focus Group Discussion",
// location: "Kantor BUMN, Jakarta Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "MPR",
// title: "Rapat Anggota MPR RI",
// location: "Gedung MPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "BUMN",
// title: "Seremoni Sinergi BUMN",
// location: "Kantor BUMN, Jakarta Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "MPR",
// title: "Sumpah Janji Anggota MPR RI",
// location: "Gedung MPR, Jakarta, Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "KPK",
// title: "Hari Lahir Pancasila",
// location: "Kantor KPK, Jakarta Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "BUMN",
// title: "Monitoring dan Evaluasi Keterbukaan Informasi Publik Tahun 2025",
// location: "Kantor BUMN, Jakarta Indonesia",
// },
// {
// date: "Jul 1 2025",
// type: "KEJAKSAAN AGUNG",
// title: "Hari Lahir Pancasila",
// location: "Kejaksaan Agung, Jakarta, Indonesia",
// },
// {
// date: "Jul 2 2025",
// type: "MPR",
// title: "Monitoring dan Evaluasi Informasi MPR Tahun 2025",
// location: "Gedung MPR, Jakarta, Indonesia",
// },
// ];
// const categories = [
// "SEMUA",
// "POLRI",
// "MAHKAMAH AGUNG",
// "DPR",
// "MPR",
// "KEJAKSAAN AGUNG",
// "KPK",
// "PUPR",
// "BSKDN",
// "BUMN",
// "KPU",
// ];
// export default function Schedule() {
// const [search, setSearch] = useState("");
// const [startDate, setStartDate] = useState<Date | undefined>(
// new Date("2025-07-01")
// );
// const [endDate, setEndDate] = useState<Date | undefined>(
// new Date("2025-07-30")
// );
// const [selectedCategory, setSelectedCategory] = useState("SEMUA");
// const filteredData = scheduleData.filter((item) => {
// const matchesCategory =
// selectedCategory === "SEMUA" || item.type === selectedCategory;
// const matchesSearch = item.title
// .toLowerCase()
// .includes(search.toLowerCase());
// return matchesCategory && matchesSearch;
// });
// return (
// <div className="p-6 max-w-[1350px] mx-auto">
// {/* Filter Bar */}
// <div className="flex flex-wrap gap-4 items-center mb-6">
// <Input
// placeholder="Pencarian"
// className="w-70 mt-7"
// value={search}
// onChange={(e) => setSearch(e.target.value)}
// />
// {/* Tanggal Mulai */}
// <div className="flex flex-col gap-2">
// <label className="text-sm font-medium">Tanggal Mulai</label>
// <Popover>
// <PopoverTrigger asChild>
// <Button
// variant="outline"
// className="w-[160px] justify-start text-left"
// >
// <CalendarIcon className="mr-2 h-4 w-4" />
// {startDate ? format(startDate, "dd MMM yyyy") : "Pilih"}
// </Button>
// </PopoverTrigger>
// <PopoverContent className="w-auto p-0">
// <Calendar
// mode="single"
// selected={startDate}
// onSelect={setStartDate}
// initialFocus
// />
// </PopoverContent>
// </Popover>
// </div>
// {/* Tanggal Selesai */}
// <div className="flex flex-col gap-2">
// <label className="text-sm font-medium">Tanggal Selesai</label>
// <Popover>
// <PopoverTrigger asChild>
// <Button
// variant="outline"
// className="w-[160px] justify-start text-left"
// >
// <CalendarIcon className="mr-2 h-4 w-4" />
// {endDate ? format(endDate, "dd MMM yyyy") : "Pilih"}
// </Button>
// </PopoverTrigger>
// <PopoverContent className="w-auto p-0">
// <Calendar
// mode="single"
// selected={endDate}
// onSelect={setEndDate}
// initialFocus
// />
// </PopoverContent>
// </Popover>
// </div>
// {/* Publikasi */}
// <div className="flex flex-col gap-2">
// <label className="text-sm font-medium">Publikasi</label>
// <Select>
// <SelectTrigger className="w-[150px]">
// <SelectValue placeholder="Semua" />
// </SelectTrigger>
// <SelectContent>
// <SelectItem value="semua">K/L</SelectItem>
// <SelectItem value="dipublikasikan">BUMN</SelectItem>
// <SelectItem value="belum">PEMERINTAH DAERAH</SelectItem>
// </SelectContent>
// </Select>
// </div>
// </div>
// {/* Filter Chips */}
// <div className="flex w-full gap-5 overflow-x-auto pb-2 border-b border-[#C6A455] mb-6">
// {categories.map((cat) => (
// <Button
// key={cat}
// variant={selectedCategory === cat ? "default" : "outline"}
// className={cn(
// "rounded-sm whitespace-nowrap",
// selectedCategory === cat && "bg-[#C6A455] text-white"
// )}
// onClick={() => setSelectedCategory(cat)}
// >
// {cat}
// </Button>
// ))}
// <Button variant="ghost">
// <ChevronRight className="w-5 h-5" />
// </Button>
// </div>
// {/* Schedule Table */}
// <h2 className="text-md font-semibold text-muted-foreground mb-4">
// Semua Jadwal
// </h2>
// <div className="flex flex-col gap-3">
// {filteredData.map((item, index) => (
// <div
// key={index}
// className="flex justify-between items-start border-b pb-2"
// >
// <div className="w-1/6 text-sm text-muted-foreground">
// {item.date}
// </div>
// <div className="w-1/5">
// <Badge
// className={cn("text-white", {
// "bg-red-600": item.type === "POLRI",
// "bg-yellow-500": item.type === "DPR",
// "bg-green-600": item.type === "KEJAKSAAN AGUNG",
// "bg-blue-700": item.type === "BUMN",
// "bg-amber-500": item.type === "MPR",
// "bg-pink-500": item.type === "KPU",
// "bg-red-700": item.type === "KPK",
// })}
// >
// {item.type}
// </Badge>
// </div>
// <div className="w-2/5 text-sm">{item.title}</div>
// <div className="w-1/3 text-sm text-muted-foreground">
// {item.location}
// </div>
// </div>
// ))}
// </div>
// <div className="flex justify-between mt-6 text-sm text-blue-600">
// <button>Preview</button>
// <button>Next</button>
// </div>
// </div>
// );
// }

View File

@ -0,0 +1,730 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { MessageCircle, Share2, Trash2 } from "lucide-react";
import { useRouter, useParams } from "next/navigation";
import {
getArticleDetail,
createArticleComment,
getArticleComments,
deleteArticleComment,
} from "@/service/content/content";
import { getCookiesDecrypt } from "@/lib/utils";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
function getAvatarColor(name: string) {
const colors = [
"#F87171",
"#FB923C",
"#FACC15",
"#4ADE80",
"#60A5FA",
"#A78BFA",
"#F472B6",
];
const index = name.charCodeAt(0) % colors.length;
return colors[index];
}
export default function DetailCommentImage() {
const [isLoggedIn, setIsLoggedIn] = useState(false);
const [currentUserId, setCurrentUserId] = useState<number | null>(null);
const [article, setArticle] = useState<any>(null);
const [comments, setComments] = useState<any[]>([]);
const [newComment, setNewComment] = useState("");
const [replyParentId, setReplyParentId] = useState<number | null>(null);
const [replyMessage, setReplyMessage] = useState("");
const router = useRouter();
const params = useParams();
const MySwal = withReactContent(Swal);
const id = Number(params?.id);
useEffect(() => {
checkLoginStatus();
if (id) {
fetchArticleDetail(id);
fetchComments(id);
}
}, [id]);
const checkLoginStatus = () => {
const userId = getCookiesDecrypt("urie");
if (userId) {
setIsLoggedIn(true);
setCurrentUserId(Number(userId));
} else {
setIsLoggedIn(false);
setCurrentUserId(null);
}
};
const fetchArticleDetail = async (articleId: number) => {
try {
const res = await getArticleDetail(articleId);
if (res?.data?.data) setArticle(res.data.data);
} catch (error) {
console.error("Gagal memuat artikel:", error);
}
};
const fetchComments = async (articleId: number) => {
try {
const res = await getArticleComments(articleId);
if (res?.data?.data) {
const all = res.data.data.map((c: any) => ({
...c,
parentId: c.parentId ?? 0,
}));
const structured = buildCommentTree(all);
setComments(structured);
}
} catch (error) {
console.error("Gagal memuat komentar:", error);
}
};
const buildCommentTree: any = (comments: any[], parentId = 0) =>
comments
.filter((c) => c.parentId === parentId)
.map((c) => ({
...c,
replies: buildCommentTree(comments, c.id),
}));
const handlePostComment = async () => {
if (!newComment.trim()) {
MySwal.fire("Oops!", "Komentar tidak boleh kosong.", "warning");
return;
}
await sendComment({
articleId: id,
message: newComment,
isPublic: true,
parentId: 0,
});
setNewComment("");
};
const handleReplySubmit = async (parentId: number) => {
if (!replyMessage.trim()) {
MySwal.fire("Oops!", "Balasan tidak boleh kosong.", "warning");
return;
}
await sendComment({
articleId: id,
message: replyMessage,
isPublic: true,
parentId,
});
setReplyMessage("");
setReplyParentId(null);
};
const sendComment = async (payload: any) => {
MySwal.fire({
title: "Mengirim komentar...",
didOpen: () => MySwal.showLoading(),
allowOutsideClick: false,
showConfirmButton: false,
});
try {
const res = await createArticleComment(payload);
if (res?.data?.success || !res?.error) {
MySwal.fire({
icon: "success",
title: "Komentar terkirim!",
timer: 1000,
showConfirmButton: false,
});
fetchComments(id);
} else {
MySwal.fire(
"Gagal",
res.message || "Tidak dapat mengirim komentar.",
"error"
);
}
} catch (error) {
console.error(error);
MySwal.fire(
"Error",
"Terjadi kesalahan saat mengirim komentar.",
"error"
);
}
};
const handleDeleteComment = async (commentId: number) => {
const confirm = await MySwal.fire({
title: "Hapus komentar ini?",
text: "Tindakan ini tidak dapat dibatalkan!",
icon: "warning",
showCancelButton: true,
confirmButtonColor: "#d33",
cancelButtonColor: "#6b7280",
confirmButtonText: "Ya, hapus",
cancelButtonText: "Batal",
});
if (!confirm.isConfirmed) return;
MySwal.fire({
title: "Menghapus komentar...",
didOpen: () => MySwal.showLoading(),
allowOutsideClick: false,
showConfirmButton: false,
});
try {
const res = await deleteArticleComment(commentId);
if (res?.data?.success || !res?.error) {
MySwal.fire({
icon: "success",
title: "Komentar dihapus!",
timer: 1000,
showConfirmButton: false,
});
fetchComments(id);
} else {
MySwal.fire(
"Gagal",
res.message || "Tidak dapat menghapus komentar.",
"error"
);
}
} catch (error) {
console.error("Gagal menghapus komentar:", error);
MySwal.fire(
"Error",
"Terjadi kesalahan saat menghapus komentar.",
"error"
);
}
};
return (
<div className="max-w-5xl mx-auto p-4 space-y-6">
<button
onClick={() => router.back()}
className="text-sm text-gray-500 hover:underline cursor-pointer"
>
Kembali ke Artikel
</button>
<div>
<p className="font-semibold text-sm uppercase text-gray-600 mb-1">
Comments on:
</p>
<h1 className="text-lg font-bold">
{article?.title || "Memuat judul..."}
</h1>
</div>
<div className="rounded-md p-3 space-y-3 bg-gray-50 border border-gray-200 shadow-sm">
{isLoggedIn ? (
<>
<Textarea
placeholder="Tulis komentar kamu di sini..."
className="min-h-[80px] border border-[#C6A455]"
value={newComment}
onChange={(e) => setNewComment(e.target.value)}
/>
<div className="flex justify-end">
<Button size="sm" onClick={handlePostComment}>
Kirim Komentar
</Button>
</div>
</>
) : (
<>
<Textarea
disabled
placeholder="Tulis komentar kamu di sini..."
className="min-h-[80px] opacity-70"
/>
<Button
onClick={() => router.push("/auth")}
className="w-full bg-blue-600 hover:bg-blue-700 text-white"
>
Sign in and Join the Conversation
</Button>
</>
)}
</div>
{/* Daftar komentar */}
<div className="space-y-6">
{comments.length > 0 ? (
comments.map((comment) => (
<CommentTree
key={comment.id}
comment={comment}
level={0}
replyParentId={replyParentId}
setReplyParentId={setReplyParentId}
replyMessage={replyMessage}
setReplyMessage={setReplyMessage}
onReplySubmit={handleReplySubmit}
onDelete={handleDeleteComment}
currentUserId={currentUserId}
/>
))
) : (
<p className="text-sm text-gray-500 text-center py-4">
Belum ada komentar.
</p>
)}
</div>
</div>
);
}
function CommentTree({
comment,
level,
replyParentId,
setReplyParentId,
replyMessage,
setReplyMessage,
onReplySubmit,
onDelete,
currentUserId,
}: any) {
const color = getAvatarColor(comment.commentFromName || "Anonim");
console.log("🧩 comment.id:", comment.id);
console.log("🧩 commentFromId:", comment.commentFromId);
console.log("🧩 currentUserId:", currentUserId);
const canDelete =
currentUserId &&
(comment.commentFromId == currentUserId ||
comment.userId == currentUserId ||
comment.createdBy == currentUserId);
return (
<div
className={`space-y-3 ${
level > 0 ? "ml-6 border-l-2 border-gray-200 pl-4" : ""
}`}
>
<div className="p-2 rounded-lg transition hover:bg-gray-50 bg-white">
<div className="flex items-start gap-2">
<div
className="w-8 h-8 rounded-full flex items-center justify-center text-white font-bold"
style={{ backgroundColor: color }}
>
{comment.commentFromName?.[0]?.toUpperCase() || "U"}
</div>
<div className="flex-1">
<p className="font-semibold text-sm">
{comment.commentFromName}{" "}
<span className="text-gray-500 text-xs font-normal">
{new Date(comment.createdAt).toLocaleString("id-ID")}
</span>
</p>
<p className="text-gray-800 text-sm leading-snug mt-1">
{comment.message}
</p>
<div className="flex items-center gap-3 mt-1 text-xs text-gray-600">
<button
onClick={() =>
setReplyParentId(
replyParentId === comment.id ? null : comment.id
)
}
className="hover:underline flex items-center gap-1"
>
<MessageCircle className="w-3 h-3" /> Reply
</button>
<button className="hover:underline flex items-center gap-1">
<Share2 className="w-3 h-3" /> Share
</button>
{/* ✅ Tombol Delete muncul hanya untuk komentar user login */}
{canDelete && (
<button
onClick={() => onDelete(comment.id)}
className="flex items-center gap-1 px-2 py-1 text-xs bg-red-100 text-red-600 rounded-md hover:bg-red-200"
>
<Trash2 className="w-3 h-3" /> Delete
</button>
)}
</div>
</div>
</div>
{replyParentId === comment.id && (
<div className="mt-3 ml-10 space-y-2">
<Textarea
placeholder={`Balas komentar ${comment.commentFromName}`}
className="min-h-[60px] border border-[#C6A455]"
value={replyMessage}
onChange={(e) => setReplyMessage(e.target.value)}
/>
<div className="flex justify-end gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setReplyParentId(null)}
>
Batal
</Button>
<Button
variant="outline"
size="sm"
onClick={() => onReplySubmit(comment.id)}
>
Kirim Balasan
</Button>
</div>
</div>
)}
</div>
{comment.replies && comment.replies.length > 0 && (
<div className="space-y-3">
{comment.replies.map((reply: any) => (
<CommentTree
key={reply.id}
comment={reply}
level={level + 1}
replyParentId={replyParentId}
setReplyParentId={setReplyParentId}
replyMessage={replyMessage}
setReplyMessage={setReplyMessage}
onReplySubmit={onReplySubmit}
onDelete={onDelete}
currentUserId={currentUserId}
/>
))}
</div>
)}
</div>
);
}
// "use client";
// import { useState, useEffect } from "react";
// import { Button } from "@/components/ui/button";
// import { Textarea } from "@/components/ui/textarea";
// import {
// ChevronDown,
// Flag,
// MessageCircle,
// Share2,
// ThumbsUp,
// } from "lucide-react";
// import { useRouter, useParams } from "next/navigation";
// import {
// getArticleDetail,
// createArticleComment,
// getArticleComments,
// } from "@/service/content/content";
// import { getCookiesDecrypt } from "@/lib/utils";
// import Swal from "sweetalert2";
// import withReactContent from "sweetalert2-react-content";
// export default function DetailCommentImage() {
// const [isLoggedIn, setIsLoggedIn] = useState(false);
// const [article, setArticle] = useState<any>(null);
// const [comments, setComments] = useState<any[]>([]);
// const [newComment, setNewComment] = useState("");
// const router = useRouter();
// const params = useParams();
// const MySwal = withReactContent(Swal);
// const id = Number(params?.id);
// useEffect(() => {
// checkLoginStatus();
// if (id) {
// fetchArticleDetail(id);
// fetchComments(id);
// }
// }, [id]);
// // ✅ Cek login dari cookies
// const checkLoginStatus = () => {
// const userId = getCookiesDecrypt("urie");
// setIsLoggedIn(!!userId);
// };
// // ✅ Ambil detail artikel
// const fetchArticleDetail = async (articleId: number) => {
// try {
// const res = await getArticleDetail(articleId);
// if (res?.data?.data) setArticle(res.data.data);
// } catch (error) {
// console.error("Gagal memuat artikel:", error);
// }
// };
// // ✅ Ambil komentar dari API
// const fetchComments = async (articleId: number) => {
// try {
// const res = await getArticleComments(articleId);
// if (res?.data?.data) {
// const allComments = res.data.data;
// // Normalisasi parentId (karena ada yg null)
// const normalized = allComments.map((c: any) => ({
// ...c,
// parentId: c.parentId ?? 0,
// }));
// // Susun komentar utama dan balasan
// const rootComments = normalized.filter((c: any) => c.parentId === 0);
// const replies = normalized.filter((c: any) => c.parentId !== 0);
// const structured = rootComments.map((comment: any) => ({
// ...comment,
// replies: replies.filter((r: any) => r.parentId === comment.id),
// }));
// setComments(structured);
// } else {
// setComments([]);
// }
// } catch (error) {
// console.error("Gagal memuat komentar:", error);
// }
// };
// // ✅ Kirim komentar ke API /article-comments
// const handlePostComment = async () => {
// if (!newComment.trim()) {
// MySwal.fire("Oops!", "Komentar tidak boleh kosong.", "warning");
// return;
// }
// MySwal.fire({
// title: "Mengirim komentar...",
// didOpen: () => {
// MySwal.showLoading();
// },
// allowOutsideClick: false,
// allowEscapeKey: false,
// showConfirmButton: false,
// });
// try {
// const payload = {
// articleId: id,
// message: newComment,
// isPublic: true,
// parentId: 0,
// };
// const res = await createArticleComment(payload);
// if (res?.data?.success || !res?.error) {
// MySwal.fire({
// icon: "success",
// title: "Komentar terkirim!",
// text: "Komentar kamu telah ditambahkan.",
// timer: 1200,
// showConfirmButton: false,
// });
// setNewComment("");
// fetchComments(id); // Refresh komentar
// } else {
// MySwal.fire(
// "Gagal",
// res.message || "Tidak dapat mengirim komentar.",
// "error"
// );
// }
// } catch (error) {
// console.error("Error posting comment:", error);
// MySwal.fire(
// "Error",
// "Terjadi kesalahan saat mengirim komentar.",
// "error"
// );
// }
// };
// return (
// <div className="max-w-5xl mx-auto p-4 space-y-6">
// {/* Tombol kembali */}
// <button
// onClick={() => router.back()}
// className="text-sm text-gray-500 hover:underline cursor-pointer"
// >
// ← Kembali ke Artikel
// </button>
// {/* Judul artikel */}
// <div>
// <p className="font-semibold text-sm uppercase text-gray-600 mb-1">
// Comments on:
// </p>
// <h1 className="text-lg font-bold">
// {article?.title || "Memuat judul..."}
// </h1>
// </div>
// {/* Form komentar */}
// <div className="rounded-md p-3 space-y-3">
// {isLoggedIn ? (
// <>
// <Textarea
// placeholder="Tulis komentar kamu di sini..."
// className="min-h-[80px] border border-[#C6A455]"
// value={newComment}
// onChange={(e) => setNewComment(e.target.value)}
// />
// <div className="flex justify-end">
// <Button size="sm" onClick={handlePostComment}>
// Kirim Komentar
// </Button>
// </div>
// </>
// ) : (
// <>
// <Textarea
// disabled
// placeholder="Tulis komentar kamu di sini..."
// className="min-h-[80px] opacity-70"
// />
// <Button
// onClick={() => router.push("/auth")}
// className="w-full bg-blue-600 hover:bg-blue-700 text-white"
// >
// Sign in and Join the Conversation
// </Button>
// </>
// )}
// </div>
// {/* Jumlah & Sort komentar */}
// <div className="flex items-center justify-between border-b pb-2">
// <p className="font-semibold">
// All Comments{" "}
// <span className="text-sm font-medium text-gray-500">
// {comments.length}
// </span>
// </p>
// <div className="flex items-center gap-1 text-sm text-gray-600">
// Sort by
// <button className="flex items-center gap-1 border border-[#C6A455] px-2 py-1 rounded text-gray-700 hover:bg-gray-100">
// Newest <ChevronDown className="w-4 h-4" />
// </button>
// </div>
// </div>
// {/* Daftar komentar */}
// <div className="space-y-6">
// {comments.length > 0 ? (
// comments.map((comment) => (
// <CommentItem
// key={comment.id}
// username={comment.commentFromName || "Anonymous"}
// time={new Date(comment.createdAt).toLocaleString("id-ID", {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// })}
// content={comment.message}
// replies={comment.replies?.map((r: any) => ({
// username: r.commentFromName || "Anonymous",
// time: new Date(r.createdAt).toLocaleString("id-ID", {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// }),
// content: r.message,
// inReplyTo: comment.commentFromName || "Anonymous",
// }))}
// />
// ))
// ) : (
// <p className="text-sm text-gray-500 text-center py-4">
// Belum ada komentar.
// </p>
// )}
// </div>
// </div>
// );
// }
// type Comment = {
// username: string;
// time: string;
// content: string;
// inReplyTo?: string;
// };
// function CommentItem({
// username,
// time,
// content,
// replies,
// }: Comment & { replies?: Comment[] }) {
// return (
// <div className="space-y-3">
// <div>
// <p className="font-semibold">
// {username}{" "}
// <span className="text-gray-500 text-sm font-normal">{time}</span>
// </p>
// <p className="text-gray-800 text-sm">{content}</p>
// <div className="flex items-center gap-4 mt-2 text-sm text-gray-600">
// <ThumbsUp className="w-4 h-4 cursor-pointer" />
// <button className="hover:underline flex items-center gap-1">
// <MessageCircle className="w-4 h-4" /> Reply
// </button>
// <button className="hover:underline flex items-center gap-1">
// <Share2 className="w-4 h-4" /> Share
// </button>
// <button className="ml-auto hover:underline text-gray-400 text-xs flex items-center gap-1">
// <Flag className="w-3 h-3" /> Report
// </button>
// </div>
// </div>
// {replies && replies.length > 0 && (
// <div className="ml-6 border-l pl-4 space-y-3">
// {replies.map((reply, idx) => (
// <div key={idx}>
// <p className="font-semibold">
// {reply.username}{" "}
// <span className="text-gray-500 text-sm font-normal">
// {reply.time}
// </span>
// </p>
// <p className="text-xs text-gray-500">
// In Reply To {reply.inReplyTo}
// </p>
// <p className="text-gray-800 text-sm">{reply.content}</p>
// <div className="flex items-center gap-4 mt-2 text-sm text-gray-600">
// <ThumbsUp className="w-4 h-4 cursor-pointer" />
// <button className="hover:underline flex items-center gap-1">
// <MessageCircle className="w-4 h-4" /> Reply
// </button>
// <button className="hover:underline flex items-center gap-1">
// <Share2 className="w-4 h-4" /> Share
// </button>
// <button className="ml-auto hover:underline text-gray-400 text-xs flex items-center gap-1">
// <Flag className="w-3 h-3" /> Report
// </button>
// </div>
// </div>
// ))}
// </div>
// )}
// </div>
// );
// }

View File

@ -262,7 +262,7 @@ export default function ImageDetail({ id }: { id: number }) {
<span>SHARE</span>
</div>
<div className="flex gap-2 items-center">
<Link href={`/content/video/comment/${id}`}>
<Link href={`/content/image/comment/${id}`}>
<Button
variant="default"
size="lg"

View File

@ -37,6 +37,7 @@ export default function PublicationKlLayout() {
{/* Grid Konten */}
<div className="flex-1">
<ForYouCardGrid
key={`${activeTab}-${selectedCategory}`}
selectedCategory={selectedCategory}
filterType={activeTab}
/>

View File

@ -4,17 +4,20 @@ import { useState, useEffect } from "react";
import Image from "next/image";
import Link from "next/link";
import { Button } from "@/components/ui/button";
import { Archive, Star, Trash2, Box, Undo2, HeartOff } from "lucide-react";
import { Card } from "@/components/ui/card";
import { Archive, Star, Trash2, Undo2, HeartOff } from "lucide-react";
import Swal from "sweetalert2";
import withReactContent from "sweetalert2-react-content";
import { getBookmarks } from "@/service/content";
import { BookmarkItem } from "@/service/content";
import { Swiper, SwiperSlide } from "swiper/react";
import "swiper/css";
import "swiper/css/navigation";
import { Navigation } from "swiper/modules";
import { getBookmarks, BookmarkItem } from "@/service/content";
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination";
// 🔹 Format tanggal WIB
function formatTanggal(dateString: string) {
if (!dateString) return "";
return (
@ -32,7 +35,6 @@ function formatTanggal(dateString: string) {
);
}
// 🔹 Link detail konten
function getLink(item: BookmarkItem) {
switch (item.article?.typeId) {
case 1:
@ -50,7 +52,7 @@ function getLink(item: BookmarkItem) {
type ForYouCardGridProps = {
filterType: "My Collections" | "Archives" | "Favorites";
selectedCategory?: string;
selectedCategory?: string; // ✅ tambahkan ini
};
export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
@ -59,18 +61,38 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
const [contentType, setContentType] = useState<
"image" | "video" | "text" | "audio"
>("image");
const [currentPage, setCurrentPage] = useState(1);
const [totalPages, setTotalPages] = useState(1);
const [totalData, setTotalData] = useState(0);
const itemsPerPage = 6;
const MySwal = withReactContent(Swal);
useEffect(() => {
fetchBookmarks();
}, [filterType]);
}, [filterType, currentPage]);
const fetchBookmarks = async () => {
try {
setLoading(true);
const response = await getBookmarks(1, 50, filterType);
const response = await getBookmarks(
currentPage,
itemsPerPage,
filterType,
);
if (!response?.error) {
setBookmarks(response.data?.data || []);
const data = response.data?.data || [];
const meta = response.data?.meta;
const totalCount = meta?.count ?? data.length ?? 0;
const totalPageCount = Math.max(
1,
Math.ceil(totalCount / itemsPerPage)
);
setBookmarks(data);
setTotalPages(totalPageCount);
setTotalData(totalCount);
if (currentPage > totalPageCount) setCurrentPage(1);
} else {
MySwal.fire("Error", "Gagal memuat bookmark.", "error");
}
@ -82,24 +104,6 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
}
};
// 🔹 Filter konten berdasarkan tab aktif
const filtered = bookmarks.filter((b) => {
const t = b.article?.typeId;
switch (contentType) {
case "image":
return t === 1;
case "video":
return t === 2;
case "text":
return t === 3;
case "audio":
return t === 4;
default:
return true;
}
});
// 🔹 Aksi tombol (dummy sementara)
const handleAction = (action: string, title: string) => {
MySwal.fire({
icon: "info",
@ -114,77 +118,74 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
const title = bookmark.article?.title || "Konten";
if (filterType === "Archives") {
// 🗃️ Unarchive + Delete
return (
<div className="flex gap-2 mt-3 justify-center">
<Button
size="sm"
variant="outline"
onClick={() => handleAction("Unarchive", title)}
className="flex items-center text-xs gap-1 text-blue-600 border-blue-200 hover:bg-blue-50 cursor-pointer"
className="text-blue-600 border-blue-200 hover:bg-blue-50"
>
<Undo2 className="w-[10px] h-[10px]" />
<p className="text-[10px]">Unarchive</p>
Unarchive
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleAction("Delete", title)}
className="flex items-center text-xs gap-1 text-red-600 border-red-200 hover:bg-red-50 cursor-pointer"
className="text-red-600 border-red-200 hover:bg-red-50"
>
<Trash2 className="w-[10px] h-[10px]" />
<p className="text-[10px]">Delete</p>
Delete
</Button>
</div>
);
}
if (filterType === "Favorites") {
// 📦 Archive + 💔 Unfavorite
return (
<div className="flex gap-2 mt-3 justify-center">
<Button
size="sm"
variant="outline"
onClick={() => handleAction("Archive", title)}
className="flex items-center text-xs gap-1 text-gray-700 border-gray-300 hover:bg-gray-100 cursor-pointer"
className="text-gray-700 border-gray-300 hover:bg-gray-100"
>
<Archive className="w-[10px] h-[10px]" />
<p className="text-[10px]">Archive</p>
Archive
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleAction("Unfavorite", title)}
className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50 cursor-pointer"
className="text-yellow-600 border-yellow-200 hover:bg-yellow-50"
>
<HeartOff className="w-[10px] h-[10px]" />
<p className="text-[10px]">Unfavorite</p>
Unfavorite
</Button>
</div>
);
}
// Default: My Collections → Archive + Favorite
return (
<div className="flex gap-2 mt-3 justify-center">
<Button
size="sm"
variant="outline"
onClick={() => handleAction("Archive", title)}
className="flex items-center text-xs gap-1 text-gray-700 border-gray-300 hover:bg-gray-100 cursor-pointer"
className="text-gray-700 border-gray-300 hover:bg-gray-100"
>
<Archive className="w-[10px] h-[10px]" />
<p className="text-[10px]">Archive</p>
Archive
</Button>
<Button
size="sm"
variant="outline"
onClick={() => handleAction("Favorite", title)}
className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50 cursor-pointer"
className="text-yellow-600 border-yellow-200 hover:bg-yellow-50"
>
<Star className="w-[10px] h-[10px]" />
<p className="text-[10px]">Favorite</p>
Favorite
</Button>
</div>
);
@ -252,31 +253,36 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
</div>
);
if (loading)
return (
<div className="text-center py-12 text-gray-500">Memuat konten...</div>
);
if (filtered.length === 0)
return (
<div className="text-center py-12">
<div className="text-gray-400 text-6xl mb-4">📂</div>
<h3 className="text-xl font-semibold text-gray-600 mb-2">
Tidak Ada Konten di {filterType}
</h3>
</div>
);
// Filter sesuai tab
const filtered = bookmarks.filter((b) => {
const t = b.article?.typeId;
switch (contentType) {
case "image":
return t === 1;
case "video":
return t === 2;
case "text":
return t === 3;
case "audio":
return t === 4;
default:
return true;
}
});
return (
<section className="bg-white px-4 py-6 border rounded-md border-[#CDD5DF]">
{/* Tab Filter Konten */}
{/* Tabs */}
<div className="flex justify-center mb-8">
<div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg">
<div className="flex flex-wrap justify-center gap-2">
{["image", "video", "audio", "text"].map((type) => (
<button
key={type}
onClick={() => setContentType(type as any)}
onClick={() => {
setContentType(type as any);
setCurrentPage(1);
}}
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
contentType === type
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
@ -296,28 +302,418 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
</div>
</div>
{/* Slider Konten */}
<Swiper
modules={[Navigation]}
navigation
spaceBetween={20}
slidesPerView={1.2}
breakpoints={{
640: { slidesPerView: 2.2 },
1024: { slidesPerView: 3.2 },
1280: { slidesPerView: 4.2 },
}}
>
{filtered.map((bookmark) => (
<SwiperSlide key={bookmark.id}>{renderCard(bookmark)}</SwiperSlide>
{/* Grid + Pagination */}
{loading ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{[...Array(6)].map((_, i) => (
<Card key={i} className="p-4 animate-pulse">
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
<div className="space-y-2">
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
</div>
</Card>
))}
</Swiper>
</div>
) : filtered.length > 0 ? (
<>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
{filtered.map((b) => renderCard(b))}
</div>
{totalPages > 1 && (
<div className="flex flex-col items-center space-y-2">
<Pagination>
<PaginationContent>
<PaginationItem>
<PaginationPrevious
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
className={
currentPage === 1
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
(n) => (
<PaginationItem key={n}>
<PaginationLink
onClick={() => setCurrentPage(n)}
isActive={currentPage === n}
>
{n}
</PaginationLink>
</PaginationItem>
)
)}
<PaginationItem>
<PaginationNext
onClick={() =>
setCurrentPage((p) => Math.min(totalPages, p + 1))
}
className={
currentPage === totalPages
? "pointer-events-none opacity-50"
: "cursor-pointer"
}
/>
</PaginationItem>
</PaginationContent>
</Pagination>
<p className="text-xs text-gray-500">
Menampilkan halaman {currentPage} dari {totalPages} Total{" "}
{totalData} konten
</p>
</div>
)}
</>
) : (
<Card className="p-8 text-center">
<p className="text-gray-600 text-sm">Tidak ada bookmark ditemukan.</p>
</Card>
)}
</section>
);
}
// "use client";
// import { useState, useEffect } from "react";
// import Image from "next/image";
// import Link from "next/link";
// import { Button } from "@/components/ui/button";
// import { Archive, Star, Trash2, Box, Undo2, HeartOff } from "lucide-react";
// import Swal from "sweetalert2";
// import withReactContent from "sweetalert2-react-content";
// import { getBookmarks } from "@/service/content";
// import { BookmarkItem } from "@/service/content";
// import { Swiper, SwiperSlide } from "swiper/react";
// import "swiper/css";
// import "swiper/css/navigation";
// import { Navigation } from "swiper/modules";
// function formatTanggal(dateString: string) {
// if (!dateString) return "";
// return (
// new Date(dateString)
// .toLocaleString("id-ID", {
// day: "2-digit",
// month: "short",
// year: "numeric",
// hour: "2-digit",
// minute: "2-digit",
// hour12: false,
// timeZone: "Asia/Jakarta",
// })
// .replace(/\./g, ":") + " WIB"
// );
// }
// function getLink(item: BookmarkItem) {
// switch (item.article?.typeId) {
// case 1:
// return `/content/image/detail/${item.article?.id}`;
// case 2:
// return `/content/video/detail/${item.article?.id}`;
// case 3:
// return `/content/text/detail/${item.article?.id}`;
// case 4:
// return `/content/audio/detail/${item.article?.id}`;
// default:
// return "#";
// }
// }
// type ForYouCardGridProps = {
// filterType: "My Collections" | "Archives" | "Favorites";
// selectedCategory?: string;
// };
// export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
// const [bookmarks, setBookmarks] = useState<BookmarkItem[]>([]);
// const [loading, setLoading] = useState(true);
// const [contentType, setContentType] = useState<
// "image" | "video" | "text" | "audio"
// >("image");
// const MySwal = withReactContent(Swal);
// useEffect(() => {
// fetchBookmarks();
// }, [filterType]);
// const fetchBookmarks = async () => {
// try {
// setLoading(true);
// const response = await getBookmarks(1, 50, filterType);
// if (!response?.error) {
// setBookmarks(response.data?.data || []);
// } else {
// MySwal.fire("Error", "Gagal memuat bookmark.", "error");
// }
// } catch (err) {
// console.error(err);
// MySwal.fire("Error", "Terjadi kesalahan saat memuat bookmark.", "error");
// } finally {
// setLoading(false);
// }
// };
// const filtered = bookmarks.filter((b) => {
// const t = b.article?.typeId;
// switch (contentType) {
// case "image":
// return t === 1;
// case "video":
// return t === 2;
// case "text":
// return t === 3;
// case "audio":
// return t === 4;
// default:
// return true;
// }
// });
// const handleAction = (action: string, title: string) => {
// MySwal.fire({
// icon: "info",
// title: action,
// text: `${title} berhasil di${action.toLowerCase()}.`,
// timer: 1200,
// showConfirmButton: false,
// });
// };
// const renderButtons = (bookmark: BookmarkItem) => {
// const title = bookmark.article?.title || "Konten";
// if (filterType === "Archives") {
// return (
// <div className="flex gap-2 mt-3 justify-center">
// <Button
// size="sm"
// variant="outline"
// onClick={() => handleAction("Unarchive", title)}
// className="flex items-center text-xs gap-1 text-blue-600 border-blue-200 hover:bg-blue-50 cursor-pointer"
// >
// <Undo2 className="w-[10px] h-[10px]" />
// <p className="text-[10px]">Unarchive</p>
// </Button>
// <Button
// size="sm"
// variant="outline"
// onClick={() => handleAction("Delete", title)}
// className="flex items-center text-xs gap-1 text-red-600 border-red-200 hover:bg-red-50 cursor-pointer"
// >
// <Trash2 className="w-[10px] h-[10px]" />
// <p className="text-[10px]">Delete</p>
// </Button>
// </div>
// );
// }
// if (filterType === "Favorites") {
// return (
// <div className="flex gap-2 mt-3 justify-center">
// <Button
// size="sm"
// variant="outline"
// onClick={() => handleAction("Archive", title)}
// className="flex items-center text-xs gap-1 text-gray-700 border-gray-300 hover:bg-gray-100 cursor-pointer"
// >
// <Archive className="w-[10px] h-[10px]" />
// <p className="text-[10px]">Archive</p>
// </Button>
// <Button
// size="sm"
// variant="outline"
// onClick={() => handleAction("Unfavorite", title)}
// className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50 cursor-pointer"
// >
// <HeartOff className="w-[10px] h-[10px]" />
// <p className="text-[10px]">Unfavorite</p>
// </Button>
// </div>
// );
// }
// return (
// <div className="flex gap-2 mt-3 justify-center">
// <Button
// size="sm"
// variant="outline"
// onClick={() => handleAction("Archive", title)}
// className="flex items-center text-xs gap-1 text-gray-700 border-gray-300 hover:bg-gray-100 cursor-pointer"
// >
// <Archive className="w-[10px] h-[10px]" />
// <p className="text-[10px]">Archive</p>
// </Button>
// <Button
// size="sm"
// variant="outline"
// onClick={() => handleAction("Favorite", title)}
// className="flex items-center gap-1 text-yellow-600 border-yellow-200 hover:bg-yellow-50 cursor-pointer"
// >
// <Star className="w-[10px] h-[10px]" />
// <p className="text-[10px]">Favorite</p>
// </Button>
// </div>
// );
// };
// const renderCard = (bookmark: BookmarkItem) => (
// <div className="rounded-xl shadow-md bg-white hover:shadow-lg transition-shadow overflow-hidden">
// {/* Gambar / ikon */}
// {bookmark.article?.typeId === 3 ? (
// // 📝 TEXT
// <div className="bg-[#e0c350] flex items-center justify-center h-[200px] text-white">
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="80"
// height="80"
// viewBox="0 0 16 16"
// >
// <path
// fill="currentColor"
// d="M5 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2V5.414a1.5 1.5 0 0 0-.44-1.06L9.647 1.439A1.5 1.5 0 0 0 8.586 1zM4 3a1 1 0 0 1 1-1h3v2.5A1.5 1.5 0 0 0 9.5 6H12v7a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1zm7.793 2H9.5a.5.5 0 0 1-.5-.5V2.207z"
// />
// </svg>
// </div>
// ) : bookmark.article?.typeId === 4 ? (
// // 🎵 AUDIO
// <div className="flex items-center justify-center bg-[#bb3523] w-full h-[200px] text-white">
// <svg
// xmlns="http://www.w3.org/2000/svg"
// width="100"
// height="100"
// viewBox="0 0 20 20"
// >
// <path
// fill="currentColor"
// d="M14.702 2.226A1 1 0 0 1 16 3.18v6.027a5.5 5.5 0 0 0-1-.184V6.18L8 8.368V15.5a2.5 2.5 0 1 1-1-2V5.368a1 1 0 0 1 .702-.955zM8 7.32l7-2.187V3.18L8 5.368zM5.5 14a1.5 1.5 0 1 0 0 3a1.5 1.5 0 0 0 0-3m13.5.5a4.5 4.5 0 1 1-9 0a4.5 4.5 0 0 1 9 0m-2.265-.436l-2.994-1.65a.5.5 0 0 0-.741.438v3.3a.5.5 0 0 0 .741.438l2.994-1.65a.5.5 0 0 0 0-.876"
// />
// </svg>
// </div>
// ) : (
// // 📸 / 🎬
// <div className="relative w-full h-[200px]">
// <Link href={getLink(bookmark)}>
// <Image
// src={bookmark.article?.thumbnailUrl || "/placeholder.png"}
// alt={bookmark.article?.title || "No Title"}
// fill
// className="object-cover cursor-pointer hover:opacity-90 transition"
// />
// </Link>
// </div>
// )}
// {/* Caption */}
// <div className="p-3">
// <p className="text-xs text-gray-500 mb-1">
// Disimpan: {formatTanggal(bookmark.createdAt)}
// </p>
// <Link href={getLink(bookmark)}>
// <p className="text-sm font-semibold line-clamp-2 hover:text-blue-600 transition-colors">
// {bookmark.article?.title}
// </p>
// </Link>
// {renderButtons(bookmark)}
// </div>
// </div>
// );
// if (loading)
// return (
// <div className="text-center py-12 text-gray-500">Memuat konten...</div>
// );
// // if (filtered.length === 0)
// // return (
// // <div className="text-center py-12">
// // <div className="text-gray-400 text-6xl mb-4">📂</div>
// // <h3 className="text-xl font-semibold text-gray-600 mb-2">
// // Tidak Ada Konten di {filterType}
// // </h3>
// // </div>
// // );
// return (
// <section className="bg-white px-4 py-6 border rounded-md border-[#CDD5DF]">
// {/* Tab Filter Konten */}
// <div className="flex justify-center mb-8">
// <div className="bg-gradient-to-r from-blue-50 to-indigo-50 rounded-2xl p-4 border-2 border-blue-100 shadow-lg">
// <div className="flex flex-wrap justify-center gap-2">
// {["image", "video", "audio", "text"].map((type) => (
// <button
// key={type}
// onClick={() => setContentType(type as any)}
// className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
// contentType === type
// ? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
// : "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
// }`}
// >
// {type === "image"
// ? "📸 Foto"
// : type === "video"
// ? "🎬 Audio Visual"
// : type === "audio"
// ? "🎵 Audio"
// : "📝 Text"}
// </button>
// ))}
// </div>
// </div>
// </div>
// {/* 🔹 Jika tidak ada data */}
// {filtered.length === 0 ? (
// <div className="text-center py-12 text-gray-500">
// Tidak ada konten di {filterType}.
// </div>
// ) : (
// /* 🔹 Swiper hanya muncul kalau ada data */
// <div className="w-full overflow-hidden">
// <Swiper
// modules={[Navigation]}
// navigation
// spaceBetween={16}
// slidesPerView="auto"
// centeredSlides={false}
// className="!overflow-hidden px-2"
// breakpoints={{
// 320: { slidesPerView: 1.2, spaceBetween: 12 },
// 640: { slidesPerView: 2, spaceBetween: 16 },
// 1024: { slidesPerView: 3, spaceBetween: 20 },
// 1280: { slidesPerView: 4, spaceBetween: 24 },
// }}
// style={{
// maxWidth: "100%",
// overflow: "hidden",
// }}
// >
// {filtered.map((bookmark) => (
// <SwiperSlide
// key={bookmark.id}
// className="!w-[280px] sm:!w-[320px] md:!w-[190px] !h-auto"
// >
// {renderCard(bookmark)}
// </SwiperSlide>
// ))}
// </Swiper>
// </div>
// )}
// </section>
// );
// }
// "use client";
// import { useEffect, useState } from "react";
// import PublicationKlFilter from "./publication-filter";
// import { Button } from "@/components/ui/button";

View File

@ -1,74 +1,229 @@
"use client";
import React, { useState } from "react";
import { Label } from "@/components/ui/label";
import { useState } from "react";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
type Inputs = {
example: string;
exampleRequired: string;
};
import { useForm, SubmitHandler } from "react-hook-form";
import { error, loading } from "@/config/swal";
import withReactContent from "sweetalert2-react-content";
import Swal from "sweetalert2";
import { useRouter } from "@/i18n/routing";
import { forgotPassword } from "@/service/landing/landing";
import withReactContent from "sweetalert2-react-content";
import { useRouter } from "next/navigation";
import { forgotPassword } from "@/service/auth";
const ForgotPass = () => {
const [username, setUsername] = useState<any>();
const MySwal = withReactContent(Swal);
const MySwal = withReactContent(Swal);
export default function ForgotPass() {
const [username, setUsername] = useState("");
const [loading, setLoading] = useState(false);
const router = useRouter();
const {
register,
handleSubmit,
watch,
formState: { errors },
} = useForm<Inputs>();
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
const handleForgot = async (e: React.FormEvent) => {
e.preventDefault();
async function handleCheckUsername() {
loading();
const response = await forgotPassword(username);
if (response.error) {
error(response.message);
return false;
}
successSubmit();
return false;
}
function successSubmit() {
if (!username.trim()) {
MySwal.fire({
title: "Email berhasil dikirim. Silahkan cek email Anda.",
icon: "success",
confirmButtonColor: "#3085d6",
confirmButtonText: "OK",
}).then((result: any) => {
if (result.isConfirmed) {
router.push("/admin");
icon: "warning",
title: "Oops...",
text: "Username tidak boleh kosong!",
});
return;
}
setLoading(true);
try {
const payload = { username };
const res = await forgotPassword(payload);
if (!res.error) {
MySwal.fire({
icon: "success",
title: "Berhasil!",
text: "Instruksi reset password telah dikirim ke EMAIL Anda.",
timer: 2500,
timerProgressBar: true,
showConfirmButton: false,
});
setTimeout(() => {
router.push("/auth");
}, 3000);
setUsername("");
} else {
MySwal.fire({
icon: "error",
title: "Gagal",
text: res.message || "Gagal mengirim instruksi reset password.",
});
}
} catch (err) {
console.error(err);
MySwal.fire({
icon: "error",
title: "Error",
text: "Terjadi kesalahan pada sistem.",
});
} finally {
setLoading(false);
}
};
return (
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4 ">
<form
onSubmit={handleForgot}
className="w-full mt-8 space-y-6 bg-white dark:bg-default-100 rounded-xl shadow-lg p-6 border border-gray-200 dark:border-gray-700 transition-all duration-300"
>
<div className="space-y-2">
<Label htmlFor="user-name">Username</Label>
<Input id="user-name" defaultValue="akun1234" className="h-[48px] text-sm text-default-900 " onChange={(e) => setUsername(e.target.value)} />
<label
htmlFor="username"
className="block text-sm font-semibold text-default-900"
>
Username
</label>
<Input
id="username"
type="text"
placeholder="Masukkan username Anda"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full h-11 px-4 text-base border-gray-300 focus:border-blue-500 focus:ring-2 focus:ring-blue-400 focus:outline-none transition-all duration-200"
/>
<p className="text-xs text-default-500 mt-1">
Masukkan username yang terdaftar untuk menerima tautan reset.
</p>
</div>
<Button type="submit" fullWidth onClick={handleCheckUsername}>
Check Username
</Button>
<Button type="submit" fullWidth onClick={handleCheckUsername}>
Kirim Ulang?{" "}
<Button
type="submit"
disabled={loading}
className={`w-full h-11 font-semibold text-black bg-gradient-to-r from-blue-600 to-blue-500 hover:from-blue-700 hover:to-blue-600 transition-all duration-300 shadow-md ${
loading ? "opacity-70 cursor-not-allowed" : ""
}`}
>
{loading ? (
<span className="flex items-center justify-center gap-2">
<svg
className="animate-spin h-4 w-4 text-black"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
></circle>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8v4a4 4 0 00-4 4H4z"
></path>
</svg>
Mengirim...
</span>
) : (
"Kirim Instruksi"
)}
</Button>
</form>
);
};
}
export default ForgotPass;
// "use client";
// import { useState } from "react";
// import { Input } from "@/components/ui/input";
// import { Button } from "@/components/ui/button";
// import Swal from "sweetalert2";
// import withReactContent from "sweetalert2-react-content";
// import { useRouter } from "next/navigation";
// import { forgotPassword } from "@/service/auth"; // pastikan path sesuai
// const MySwal = withReactContent(Swal);
// export default function ForgotPass() {
// const [username, setUsername] = useState("");
// const [loading, setLoading] = useState(false);
// const router = useRouter();
// const handleForgot = async (e: React.FormEvent) => {
// e.preventDefault();
// if (!username.trim()) {
// MySwal.fire({
// icon: "warning",
// title: "Oops...",
// text: "Username tidak boleh kosong!",
// });
// return;
// }
// setLoading(true);
// try {
// const payload = { username };
// const res = await forgotPassword(payload);
// if (!res.error) {
// MySwal.fire({
// icon: "success",
// title: "Berhasil!",
// text: "Instruksi reset password telah dikirim ke EMAIL Anda.",
// timer: 2500,
// timerProgressBar: true,
// showConfirmButton: false,
// });
// setTimeout(() => {
// router.push("/auth");
// }, 3000);
// setUsername("");
// } else {
// MySwal.fire({
// icon: "error",
// title: "Gagal",
// text: res.message || "Gagal mengirim instruksi reset password.",
// });
// }
// } catch (err) {
// console.error(err);
// MySwal.fire({
// icon: "error",
// title: "Error",
// text: "Terjadi kesalahan pada sistem.",
// });
// } finally {
// setLoading(false);
// }
// };
// return (
// <form onSubmit={handleForgot} className="w-full mt-8 space-y-6">
// <div>
// <label
// htmlFor="username"
// className="block text-sm font-medium text-default-900 mb-2"
// >
// Username
// </label>
// <Input
// id="username"
// type="text"
// placeholder="Masukkan username Anda"
// value={username}
// onChange={(e) => setUsername(e.target.value)}
// className="w-full h-11"
// />
// </div>
// <Button
// type="submit"
// className="w-full h-11 font-semibold border"
// disabled={loading}
// >
// {loading ? "Mengirim..." : "Kirim Instruksi"}
// </Button>
// </form>
// );
// }

View File

@ -101,6 +101,56 @@ export function getMenuList(pathname: string, t: any): Group[] {
},
],
},
{
groupLabel: "",
id: "schedule",
menus: [
{
id: "schedule",
href: "/admin/schedule",
label: t("schedule"),
active: pathname.includes("/schedule"),
icon: "uil:schedule",
submenus: [
{
href: "/contributor/schedule",
label: t("schedule"),
active: pathname.includes("/schedule"),
icon: "heroicons:arrow-trending-up",
children: [],
},
// {
// href: "/contributor/schedule/press-conference",
// label: t("press-conference"),
// active: pathname.includes("/schedule/press-conference"),
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
// {
// href: "/contributor/schedule/event",
// label: "event",
// active: pathname.includes("/schedule/event"),
// icon: "heroicons:shopping-cart",
// children: [],
// },
// {
// href: "/contributor/schedule/press-release",
// label: t("press-release"),
// active: pathname.includes("/schedule/press-release"),
// icon: "heroicons:shopping-cart",
// children: [],
// },
// {
// href: "/contributor/schedule/calendar-polri",
// label: t("calendar-polri"),
// active: pathname.includes("/schedule/calendar-polri"),
// icon: "heroicons:arrow-trending-up",
// children: [],
// },
],
},
],
},
{
groupLabel: "",
id: "settings",

View File

@ -865,5 +865,123 @@
"searchTitle": "Find Title...",
"selectFilter": "Select a Filter",
"noResult": "No results."
},
"AboutPage": {
"title": "About Us",
"subtitle": "An integrated digital news portal managed by registered tenants to distribute credible and reliable information.",
"whatIs": {
"title": "What is MediaHub?",
"p1": "<b>MediaHub</b> is a digital news platform designed to simplify content publication for <b>registered tenants and official organizations</b>. Each tenant has a dedicated space to manage and publish articles, photos, videos, and other information related to their field.",
"p2": "With a multi-tenant system, the platform allows every institution to have its own news channel without building from scratch. All processes — from writing, media uploading, to publishing — are managed through a secure and centralized dashboard."
},
"functions": {
"title": "Website Functions & Objectives",
"f1": "Provide an official news portal for each registered tenant.",
"f2": "Serve as a trusted information source for the public with verified content.",
"f3": "Enable tenants to easily upload, edit, and distribute news in various formats (text, photo, video, document, audio).",
"f4": "Support inter-agency collaboration through a centralized and structured publication system."
},
"howItWorks": {
"title": "How the Platform Works",
"s1": "Tenants register officially and receive verified accounts.",
"s2": "Each tenant manages their articles and media through a personal dashboard.",
"s3": "Content undergoes validation and approval before being published.",
"s4": "Published articles can be shared across digital channels, including social media."
},
"vision": {
"title": "Vision",
"text": "To become a trusted digital news portal that promotes transparency and collaboration among institutions."
},
"mission": {
"title": "Mission",
"m1": "Enhance public access to official and positive information.",
"m2": "Empower tenants to share informative and inspiring content.",
"m3": "Maintain news integrity through a multi-layered verification system."
}
},
"AdvertisingPage": {
"title": "Advertising & Partnership",
"subtitle": "Boost your brand visibility by advertising on our trusted news platform.",
"whyAdvertise": {
"title": "Why Advertise With Us?",
"point1": "Reach thousands of active readers every day across regions and demographics.",
"point2": "Your ads will appear alongside credible, professional news content.",
"point3": "Flexible ad placements to suit your brands marketing goals.",
"point4": "Professional support team ready to help with your campaign strategy."
},
"placement": {
"title": "Advertising Placement Options",
"text": "We offer a variety of ad formats and placements to reach your target audience:",
"option1": "Main banner on the homepage (top banner).",
"option2": "Sidebar ads on article pages.",
"option3": "Sponsored articles with custom labeling.",
"option4": "Video ads within multimedia content."
},
"form": {
"title": "Contact Our Advertising Team",
"subtitle": "Fill out the form below to request an advertising proposal.",
"name": "Full Name",
"company": "Company / Brand Name",
"email": "Email Address",
"phone": "Phone Number",
"message": "Message or details about your advertising needs...",
"submit": "Send Request"
}
},
"Navbar": {
"home": "Home",
"forYou": "For You",
"following": "Following",
"publication": "Publication",
"localGov": "Local Government",
"schedule": "Schedule",
"login": "Login",
"register": "Register",
"logout": "Logout",
"language": "Language",
"features": "Features",
"about": "About Us",
"advertising": "Advertising",
"contact": "Contact Us",
"subscribeTitle": "Subscribe to Our Newsletter",
"subscribePlaceholder": "Your email address",
"subscribeButton": "Subscribe"
},
"MediaUpdate": {
"title": "Media Update",
"latest": "Latest",
"popular": "Popular",
"image": "Image",
"video": "Video",
"text": "Text",
"loadContent": "Load Content...",
"seeMore": "See More",
"loadCategory": "Load Category...",
"category": "Most Popular Categories",
"publication": "Publication",
"netidhub": "NetIDHUB services are provided",
"save": "Save",
"saved": "Saved",
"saving": "Saving...",
"unite": "UNITE INDONESIA",
"username": "Enter your username",
"password": "Password",
"password2": "Enter Your Password",
"rememberMe": "Remember Me",
"forgotPass": "Forgot password?",
"next": "Next",
"enterOTP": "Please Enter OTP",
"enterOTP2": "Enter the 6-digit code sent to your email address.",
"enterOTP3": "Didn't receive the code? Resend",
"enterOTP4": "Verifying...",
"enterOTP5": "Sign In"
}
}

View File

@ -866,5 +866,123 @@
"searchTitle": "Cari Judul...",
"selectFilter": "Pilih Filter",
"noResult": "Tidak ada hasil"
},
"AboutPage": {
"title": "Tentang Kami",
"subtitle": "Portal berita digital terintegrasi yang dikelola oleh berbagai tenant resmi untuk menyebarkan informasi terpercaya.",
"whatIs": {
"title": "Apa itu MediaHub?",
"p1": "<b>MediaHub</b> adalah platform berita digital yang dirancang untuk mempermudah publikasi konten oleh para <b>tenant resmi dan organisasi terdaftar</b>. Setiap tenant memiliki ruang khusus untuk mengelola dan mempublikasikan artikel, foto, video, maupun informasi lainnya sesuai bidangnya masing-masing.",
"p2": "Dengan sistem multi-tenant, platform ini memungkinkan setiap lembaga untuk memiliki kanal berita sendiri tanpa harus membangun sistem dari awal. Semua proses — mulai dari penulisan, pengunggahan media, hingga publikasi — dilakukan melalui satu dashboard yang aman dan terpusat."
},
"functions": {
"title": "Fungsi & Tujuan Website",
"f1": "Menyediakan portal berita resmi untuk setiap tenant yang terdaftar.",
"f2": "Menjadi sumber informasi terpercaya bagi masyarakat dengan verifikasi konten sebelum tayang.",
"f3": "Mempermudah tenant dalam mengunggah, menyunting, dan menyebarluaskan berita dalam berbagai format (teks, foto, video, dokumen, audio).",
"f4": "Mendukung kolaborasi antar instansi melalui sistem publikasi terpusat dan terstruktur."
},
"howItWorks": {
"title": "Cara Kerja Platform",
"s1": "Tenant melakukan registrasi resmi dan mendapatkan akun terverifikasi.",
"s2": "Setiap tenant dapat mengelola artikel dan media melalui dashboard masing-masing.",
"s3": "Konten akan melalui proses validasi dan approval sebelum dipublikasikan.",
"s4": "Artikel yang sudah terbit dapat dibagikan ke berbagai kanal digital, termasuk media sosial."
},
"vision": {
"title": "Visi",
"text": "Menjadi portal berita digital terpercaya yang mendukung transparansi dan kolaborasi antar lembaga."
},
"mission": {
"title": "Misi",
"m1": "Meningkatkan akses publik terhadap informasi resmi dan positif.",
"m2": "Memberdayakan tenant untuk menyebarkan konten informatif dan inspiratif.",
"m3": "Menjaga integritas berita dengan sistem verifikasi berlapis."
}
},
"AdvertisingPage": {
"title": "Iklan & Kerja Sama",
"subtitle": "Tingkatkan jangkauan dan visibilitas merek Anda dengan beriklan di platform berita kami.",
"whyAdvertise": {
"title": "Mengapa Beriklan di Website Kami?",
"point1": "Menjangkau ribuan pembaca aktif setiap hari dari berbagai kalangan dan wilayah.",
"point2": "Iklan Anda akan muncul berdampingan dengan konten berita kredibel dan profesional.",
"point3": "Fleksibilitas penempatan iklan sesuai kebutuhan brand Anda.",
"point4": "Dukungan tim profesional untuk membantu strategi promosi digital Anda."
},
"placement": {
"title": "Pilihan Penempatan Iklan",
"text": "Kami menyediakan berbagai format dan posisi iklan yang dapat disesuaikan dengan target audiens Anda:",
"option1": "Banner utama di halaman depan (homepage top banner).",
"option2": "Iklan sidebar pada halaman berita.",
"option3": "Artikel bersponsor (sponsored article) dengan label khusus.",
"option4": "Iklan video pada konten multimedia."
},
"form": {
"title": "Hubungi Tim Iklan Kami",
"subtitle": "Isi formulir di bawah ini untuk mendapatkan penawaran kerja sama iklan.",
"name": "Nama Lengkap",
"company": "Nama Perusahaan / Brand",
"email": "Alamat Email",
"phone": "Nomor Telepon",
"message": "Pesan atau detail kebutuhan iklan Anda...",
"submit": "Kirim Permintaan"
}
},
"Navbar": {
"home": "Beranda",
"forYou": "Untuk Anda",
"following": "Mengikuti",
"publication": "Publikasi",
"localGov": "Pemerintah Daerah",
"schedule": "Jadwal",
"login": "Masuk",
"register": "Daftar",
"logout": "Keluar",
"language": "Bahasa",
"features": "Fitur",
"about": "Tentang Kami",
"advertising": "Advertising",
"contact": "Kontak Kami",
"subscribeTitle": "Berlangganan Newsletter Kami",
"subscribePlaceholder": "Masukkan alamat email Anda",
"subscribeButton": "Berlangganan"
},
"MediaUpdate": {
"title": "Media Update",
"latest": "Terbaru",
"popular": "Terpopuler",
"image": "Foto",
"video": "Audio Visual",
"text": "Teks",
"loadContent": "Memuat Konten...",
"seeMore": "Lihat Lebih Banyak",
"loadCategory": "Memuat Kategori...",
"category": "Kategori Paling Populer",
"publication": "Publikasi",
"netidhub": "Layanan NetIDHUB disediakan",
"save": "Simpan",
"saved": "Tersimpan",
"saving": "Menyimpan...",
"unite": "MENYATUKAN INDONESIA",
"username": "Masukkan Username Anda",
"password": "Kata Sandi",
"password2": "Masukkan Kata Sandi Anda",
"rememberMe": "Ingat Saya",
"forgotPass": "Lupa Kata Sandi?",
"next": "Selanjutnya",
"enterOTP": "Masukkan kode OTP",
"enterOTP2": "Masukkan kode 6 digit yang dikirim ke alamat email Anda.",
"enterOTP3": "Tidak menerima kode? Kirim ulang",
"enterOTP4": "Kirim",
"enterOTP5": "Masuk"
}
}

BIN
public/PPS2.jpeg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

BIN
public/PPS3.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

BIN
public/PPS4.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 478 KiB

View File

@ -215,4 +215,9 @@ export async function registerTenant(data: any) {
return httpPost(url, data);
}
export async function forgotPassword(data: any) {
const pathUrl = "users/forgot-password";
return httpPost(pathUrl, data);
}

View File

@ -59,3 +59,4 @@ export async function uploadClientLogo(logoFile: File) {
return httpPostInterceptor(url, formData, headers);
}

View File

@ -582,3 +582,21 @@ export async function getArticleDetail(id: number) {
const url = `articles/${id}`;
return await httpGetInterceptor(url);
}
export async function createArticleComment(payload: {
articleId: number;
message: string;
}) {
const url = `article-comments`;
return await httpPostInterceptor(url, payload);
}
export async function getArticleComments(articleId: number) {
const url = `article-comments?articleId=${articleId}`;
return await httpGetInterceptor(url);
}
export async function deleteArticleComment(id: number) {
const url = `article-comments/${id}`;
return await httpDeleteInterceptor(url);
}

View File

@ -4,6 +4,7 @@ import {
httpGetInterceptor,
httpGetInterceptorForMetadata,
httpPostInterceptor,
httpPutInterceptor,
} from "../http-config/http-interceptor-service";
export async function getCsrfToken() {
@ -116,7 +117,6 @@ export async function getDetail(slug: string) {
return await httpGetInterceptor(`media/public?slug=${slug}&state=mabes`);
}
export async function getDetailMetaData(slug: string) {
return await httpGetInterceptorForMetadata(
`media/public?slug=${slug}&state=mabes&isSummary=true`
@ -261,6 +261,43 @@ export async function getBookmarksByUserId(
);
}
export async function getAllSchedules(params?: {
createdById?: number;
description?: string;
endDate?: string;
isLiveStreaming?: boolean;
location?: string;
speakers?: string;
startDate?: string;
statusId?: number;
title?: string;
typeId?: number;
count?: number;
limit?: number;
nextPage?: number;
page?: number;
previousPage?: number;
sort?: string;
sortBy?: string;
totalPage?: number;
}) {
// base endpoint dari swagger
let url = "schedules?";
// tambahkan semua query parameter yang memiliki nilai
Object.entries(params || {}).forEach(([key, value]) => {
if (value !== undefined && value !== null && value !== "") {
url += `${key}=${encodeURIComponent(String(value))}&`;
}
});
// hapus '&' terakhir jika ada
url = url.endsWith("&") ? url.slice(0, -1) : url;
// gunakan interceptor kamu untuk GET
return httpGetInterceptor(url);
}
export async function getBookmarksForUser(
page = 1,
limit = 10,
@ -382,3 +419,24 @@ export async function enableListCategory() {
const url = `media/categories/list/publish?enablePage=1`;
return httpPostInterceptor(url);
}
export async function createSchedule(data: any) {
const pathUrl = "/schedules";
return httpPostInterceptor(pathUrl, data);
}
export async function updateSchedule(id: string | number, data: any) {
const pathUrl = `/schedules/${id}`;
return httpPutInterceptor(pathUrl, data);
}
export async function deleteSchedule(id: string | number) {
const pathUrl = `/schedules/${id}`;
return httpDeleteInterceptor(pathUrl);
}
export async function getScheduleById(id: string | number) {
const pathUrl = `/schedules/${id}`;
return httpGetInterceptor(pathUrl);
}