feat: all section in notes meet
This commit is contained in:
parent
28c1b79812
commit
a892343f3e
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import DetailCommentVideo from "@/components/main/comment-detail-video";
|
||||
|
||||
export default async function DetailCommentInfo() {
|
||||
return <DetailCommentVideo />;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import DetailCommentImage from "@/components/main/comment-detail-image";
|
||||
|
||||
export default async function DetailCommentInfo() {
|
||||
return <DetailCommentImage />;
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
import DetailCommentVideo from "@/components/main/comment-detail-video";
|
||||
|
||||
export default async function DetailCommentInfo() {
|
||||
return <DetailCommentVideo />;
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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,15 +51,18 @@ 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 ? (
|
||||
// Loading skeleton
|
||||
<div className="flex flex-wrap gap-3">
|
||||
|
|
@ -70,16 +79,22 @@ 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
|
||||
key={index}
|
||||
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
|
||||
}}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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,74 +122,74 @@ 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
|
||||
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.map((client, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${client.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block"
|
||||
>
|
||||
{client.logoUrl ? (
|
||||
<Image
|
||||
src={client.logoUrl}
|
||||
alt={client.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
) : (
|
||||
// Fallback when no logo - menggunakan placeholder image
|
||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
|
||||
{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.map((client, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${client.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="group block"
|
||||
>
|
||||
{client.logoUrl ? (
|
||||
<Image
|
||||
src="/logo-netidhub.png"
|
||||
alt={`${client.name} placeholder`}
|
||||
src={client.logoUrl}
|
||||
alt={client.name}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))
|
||||
) : (
|
||||
// Fallback to static logos if API fails or no data
|
||||
logos.map((logo, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${logo.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={`logo-${idx}`}
|
||||
width={80}
|
||||
height={80}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))
|
||||
)}
|
||||
) : (
|
||||
// Fallback when no logo - menggunakan placeholder image
|
||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] rounded flex items-center justify-center hover:from-blue-200 hover:to-blue-300 transition-all duration-200">
|
||||
<Image
|
||||
src="/logo-netidhub.png"
|
||||
alt={`${client.name} placeholder`}
|
||||
width={100}
|
||||
height={100}
|
||||
className="md:w-[100px] md:h-[100px] object-contain opacity-70"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
</SwiperSlide>
|
||||
))
|
||||
: // Fallback to static logos if API fails or no data
|
||||
logos.map((logo, idx) => (
|
||||
<SwiperSlide key={idx} className="!w-auto">
|
||||
<a
|
||||
href={`/in/tenant/${logo.slug}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="block"
|
||||
>
|
||||
<Image
|
||||
src={logo.src}
|
||||
alt={`logo-${idx}`}
|
||||
width={80}
|
||||
height={80}
|
||||
className="md:w-[100px] md:h-[100px] object-contain hover:opacity-80 transition"
|
||||
/>
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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>();
|
||||
|
||||
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 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 ||
|
||||
|
|
@ -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"}
|
||||
fill
|
||||
className="object-cover rounded-xl"
|
||||
/>
|
||||
<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,26 +283,30 @@ function Card({
|
|||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
|
||||
isBig
|
||||
? "w-full lg:max-w-[670px] h-[680px]"
|
||||
: "w-full h-[360px] md:h-[340px]"
|
||||
}`}
|
||||
>
|
||||
<div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
|
||||
<Link href={getLink()}>
|
||||
<Image
|
||||
src={item.smallThumbnailLink || "/contributor.png"}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
<div>
|
||||
<div
|
||||
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
|
||||
isBig
|
||||
? "w-full lg:max-w-[670px] lg:min-h-[680px]"
|
||||
: "w-full h-[350px] md:h-[330px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`relative ${
|
||||
isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
|
||||
} w-full`}
|
||||
>
|
||||
<Link href={getLink()}>
|
||||
<Image
|
||||
src={item.smallThumbnailLink || "/contributor.png"}
|
||||
alt={item.title}
|
||||
fill
|
||||
className="object-cover"
|
||||
/>
|
||||
</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,31 +336,31 @@ function Card({
|
|||
{item.title}
|
||||
</h3>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center pt-4">
|
||||
<div className="flex gap-2 text-gray-500">
|
||||
<ThumbsUp
|
||||
size={18}
|
||||
className="cursor-pointer hover:text-[#F60100]"
|
||||
/>
|
||||
<ThumbsDown
|
||||
size={18}
|
||||
className="cursor-pointer hover:text-red-600"
|
||||
/>
|
||||
<div className="flex justify-between items-center pt-2">
|
||||
<div className="flex gap-2 text-gray-500">
|
||||
<ThumbsUp
|
||||
size={18}
|
||||
className="cursor-pointer hover:text-[#F60100]"
|
||||
/>
|
||||
<ThumbsDown
|
||||
size={18}
|
||||
className="cursor-pointer hover:text-red-600"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={isSaving || isBookmarked}
|
||||
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
||||
isBookmarked
|
||||
? "bg-gray-400 text-white cursor-not-allowed"
|
||||
: "bg-[#F60100] text-white hover:bg-[#c90000]"
|
||||
}`}
|
||||
>
|
||||
{isSaving ? t("saving") : isBookmarked ? t("saved") : t("save")}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={handleToggleBookmark}
|
||||
disabled={isSaving || isBookmarked}
|
||||
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
||||
isBookmarked
|
||||
? "bg-gray-400 text-white cursor-not-allowed"
|
||||
: "bg-[#F60100] text-white hover:bg-[#c90000]"
|
||||
}`}
|
||||
>
|
||||
{isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
|
||||
</button>
|
||||
</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,53 +414,40 @@ 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) => ({
|
||||
// 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"
|
||||
// : "",
|
||||
// }));
|
||||
// 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 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 ||
|
||||
|
|
@ -448,42 +461,34 @@ function Card({
|
|||
// .filter((x) => !isNaN(x))
|
||||
// );
|
||||
|
||||
// const merged = new Set([...localSet, ...ids]);
|
||||
// setBookmarkedIds(merged);
|
||||
// const gabungan = new Set([...localSet, ...ids]);
|
||||
// setBookmarkedIds(gabungan);
|
||||
// localStorage.setItem(
|
||||
// "bookmarkedIds",
|
||||
// JSON.stringify(Array.from(merged))
|
||||
// JSON.stringify(Array.from(gabungan))
|
||||
// );
|
||||
// } else {
|
||||
// // Jika belum login, pakai local saja
|
||||
// setBookmarkedIds(localSet);
|
||||
// }
|
||||
// } catch (error) {
|
||||
// console.error("Gagal memuat data:", error);
|
||||
// } catch (err) {
|
||||
// console.error("Gagal sinkronisasi bookmark:", err);
|
||||
// }
|
||||
// };
|
||||
|
||||
// fetchData();
|
||||
// syncBookmarks();
|
||||
// }, []);
|
||||
|
||||
// // 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
|
||||
// key={data[0].id}
|
||||
// item={data[0]}
|
||||
// isBig
|
||||
// isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
|
||||
// onSaved={(id) =>
|
||||
// setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||
// }
|
||||
// bookmarkedIds={bookmarkedIds}
|
||||
// setBookmarkedIds={setBookmarkedIds}
|
||||
// />
|
||||
// )}
|
||||
|
||||
|
|
@ -492,10 +497,8 @@ function Card({
|
|||
// <Card
|
||||
// key={item.id}
|
||||
// item={item}
|
||||
// isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
|
||||
// onSaved={(id) =>
|
||||
// setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||
// }
|
||||
// bookmarkedIds={bookmarkedIds}
|
||||
// setBookmarkedIds={setBookmarkedIds}
|
||||
// />
|
||||
// ))}
|
||||
// </div>
|
||||
|
|
@ -513,44 +516,55 @@ function Card({
|
|||
// );
|
||||
// }
|
||||
|
||||
// // 🔹 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,
|
||||
// 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,30 +627,26 @@ function Card({
|
|||
// };
|
||||
|
||||
// return (
|
||||
// <div>
|
||||
// <div
|
||||
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
|
||||
// isBig
|
||||
// ? "w-full lg:max-w-[670px] lg:min-h-[680px]"
|
||||
// : "w-full h-[350px] md:h-[330px]"
|
||||
// }`}
|
||||
// >
|
||||
// <div
|
||||
// className={`relative ${
|
||||
// isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
|
||||
// } w-full`}
|
||||
// >
|
||||
// <Link href={getLink()}>
|
||||
// <Image
|
||||
// src={item.smallThumbnailLink || "/contributor.png"}
|
||||
// alt={item.title}
|
||||
// fill
|
||||
// className="object-cover"
|
||||
// />
|
||||
// </Link>
|
||||
// </div>
|
||||
// <div
|
||||
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
|
||||
// isBig
|
||||
// ? "w-full lg:max-w-[670px] h-[680px]"
|
||||
// : "w-full h-[360px] md:h-[340px]"
|
||||
// }`}
|
||||
// >
|
||||
// <div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
|
||||
// <Link href={getLink()}>
|
||||
// <Image
|
||||
// src={item.smallThumbnailLink || "/contributor.png"}
|
||||
// alt={item.title}
|
||||
// fill
|
||||
// className="object-cover"
|
||||
// />
|
||||
// </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,31 +676,31 @@ function Card({
|
|||
// {item.title}
|
||||
// </h3>
|
||||
// </Link>
|
||||
// </div>
|
||||
|
||||
// <div className="flex justify-between items-center pt-2">
|
||||
// <div className="flex gap-2 text-gray-500">
|
||||
// <ThumbsUp
|
||||
// size={18}
|
||||
// className="cursor-pointer hover:text-[#F60100]"
|
||||
// />
|
||||
// <ThumbsDown
|
||||
// size={18}
|
||||
// className="cursor-pointer hover:text-red-600"
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <button
|
||||
// onClick={handleSave}
|
||||
// disabled={isSaving || isBookmarked}
|
||||
// className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
||||
// isBookmarked
|
||||
// ? "bg-gray-400 text-white cursor-not-allowed"
|
||||
// : "bg-[#F60100] text-white hover:bg-[#c90000]"
|
||||
// }`}
|
||||
// >
|
||||
// {isSaving ? "Saving..." : isBookmarked ? "Saved" : "Save"}
|
||||
// </button>
|
||||
// <div className="flex justify-between items-center pt-4">
|
||||
// <div className="flex gap-2 text-gray-500">
|
||||
// <ThumbsUp
|
||||
// size={18}
|
||||
// className="cursor-pointer hover:text-[#F60100]"
|
||||
// />
|
||||
// <ThumbsDown
|
||||
// size={18}
|
||||
// className="cursor-pointer hover:text-red-600"
|
||||
// />
|
||||
// </div>
|
||||
|
||||
// <button
|
||||
// onClick={handleToggleBookmark}
|
||||
// disabled={isSaving || isBookmarked}
|
||||
// className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
||||
// isBookmarked
|
||||
// ? "bg-gray-400 text-white cursor-not-allowed"
|
||||
// : "bg-[#F60100] text-white hover:bg-[#c90000]"
|
||||
// }`}
|
||||
// >
|
||||
// {isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
|
||||
// </button>
|
||||
// </div>
|
||||
// </div>
|
||||
// </div>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
||||
|
|
|
|||
|
|
@ -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,162 +20,83 @@ import {
|
|||
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",
|
||||
];
|
||||
import { getAllSchedules } from "@/service/landing/landing";
|
||||
|
||||
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 [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 filteredData = scheduleData.filter((item) => {
|
||||
const matchesCategory =
|
||||
selectedCategory === "SEMUA" || item.type === selectedCategory;
|
||||
const matchesSearch = item.title
|
||||
.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
});
|
||||
const categories = [
|
||||
"SEMUA",
|
||||
"POLRI",
|
||||
"MAHKAMAH AGUNG",
|
||||
"DPR",
|
||||
"MPR",
|
||||
"KEJAKSAAN AGUNG",
|
||||
"KPK",
|
||||
"PUPR",
|
||||
"BSKDN",
|
||||
"BUMN",
|
||||
"KPU",
|
||||
];
|
||||
|
||||
const fetchSchedules = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
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?.toUpperCase() === selectedCategory;
|
||||
const matchesSearch = item.title
|
||||
?.toLowerCase()
|
||||
.includes(search.toLowerCase());
|
||||
return matchesCategory && matchesSearch;
|
||||
})
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-[1350px] mx-auto">
|
||||
|
|
@ -276,37 +197,51 @@ export default function Schedule() {
|
|||
<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}
|
||||
|
||||
{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
|
||||
key={index}
|
||||
className="flex justify-between items-start border-b pb-2"
|
||||
>
|
||||
<div className="w-1/6 text-sm text-muted-foreground">
|
||||
{item.startDate
|
||||
? format(new Date(item.startDate), "dd MMM yyyy")
|
||||
: "-"}
|
||||
</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 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>
|
||||
)}
|
||||
|
||||
<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>
|
||||
// );
|
||||
// }
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
// );
|
||||
// }
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ export default function PublicationKlLayout() {
|
|||
{/* Grid Konten */}
|
||||
<div className="flex-1">
|
||||
<ForYouCardGrid
|
||||
key={`${activeTab}-${selectedCategory}`}
|
||||
selectedCategory={selectedCategory}
|
||||
filterType={activeTab}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
))}
|
||||
</Swiper>
|
||||
{/* 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>
|
||||
))}
|
||||
</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";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
if (!username.trim()) {
|
||||
MySwal.fire({
|
||||
icon: "warning",
|
||||
title: "Oops...",
|
||||
text: "Username tidak boleh kosong!",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
successSubmit();
|
||||
return false;
|
||||
}
|
||||
setLoading(true);
|
||||
|
||||
function successSubmit() {
|
||||
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");
|
||||
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>
|
||||
// );
|
||||
// }
|
||||
|
|
|
|||
56
lib/menus.ts
56
lib/menus.ts
|
|
@ -56,8 +56,8 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
],
|
||||
},
|
||||
...(Number(roleId) === 3
|
||||
? [
|
||||
...(Number(roleId) === 3
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
id: "content",
|
||||
|
|
@ -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",
|
||||
|
|
@ -139,7 +189,7 @@ export function getMenuList(pathname: string, t: any): Group[] {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(Number(roleId) === 2
|
||||
...(Number(roleId) === 2
|
||||
? [
|
||||
{
|
||||
groupLabel: "",
|
||||
|
|
|
|||
118
messages/en.json
118
messages/en.json
|
|
@ -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 brand’s 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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
118
messages/in.json
118
messages/in.json
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Binary file not shown.
|
After Width: | Height: | Size: 24 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 730 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 478 KiB |
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -59,3 +59,4 @@ export async function uploadClientLogo(logoFile: File) {
|
|||
|
||||
return httpPostInterceptor(url, formData, headers);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue