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 { LoginFormData } from "@/types/auth";
|
||||||
import { useAuth, useEmailValidation } from "@/hooks/use-auth";
|
import { useAuth, useEmailValidation } from "@/hooks/use-auth";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
type AuthStep = "login" | "email-setup" | "otp";
|
type AuthStep = "login" | "email-setup" | "otp";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import { useAuth } from "@/hooks/use-auth";
|
||||||
import { listRole } from "@/service/landing/landing";
|
import { listRole } from "@/service/landing/landing";
|
||||||
import { Role } from "@/types/auth";
|
import { Role } from "@/types/auth";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export const LoginForm: React.FC<LoginFormProps> = ({
|
export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
onSuccess,
|
onSuccess,
|
||||||
|
|
@ -25,7 +26,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
className,
|
className,
|
||||||
}) => {
|
}) => {
|
||||||
const { login } = useAuth();
|
const { login } = useAuth();
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
const [showPassword, setShowPassword] = useState(false);
|
const [showPassword, setShowPassword] = useState(false);
|
||||||
const [rememberMe, setRememberMe] = useState(true);
|
const [rememberMe, setRememberMe] = useState(true);
|
||||||
const [roles, setRoles] = useState<Role[]>([]);
|
const [roles, setRoles] = useState<Role[]>([]);
|
||||||
|
|
@ -92,7 +93,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-lg font-bold text-gray-900 mb-2 mt-5">
|
<h2 className="text-lg font-bold text-gray-900 mb-2 mt-5">
|
||||||
MENYATUKAN INDONESIA
|
{t("unite")}
|
||||||
</h2>
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -147,7 +148,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
label="Username"
|
label="Username"
|
||||||
name="username"
|
name="username"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Enter your username"
|
placeholder={t("username")}
|
||||||
error={errors.username?.message}
|
error={errors.username?.message}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
required
|
required
|
||||||
|
|
@ -158,10 +159,10 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
|
|
||||||
{/* Password Field */}
|
{/* Password Field */}
|
||||||
<FormField
|
<FormField
|
||||||
label="Password"
|
label={t("password")}
|
||||||
name="password"
|
name="password"
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="Enter your password"
|
placeholder={t("password2")}
|
||||||
error={errors.password?.message}
|
error={errors.password?.message}
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
required
|
required
|
||||||
|
|
@ -183,14 +184,14 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
/>
|
/>
|
||||||
<Label htmlFor="rememberMe" className="text-sm">
|
<Label htmlFor="rememberMe" className="text-sm">
|
||||||
Remember Me
|
{t("rememberMe")}
|
||||||
</Label>
|
</Label>
|
||||||
</div>
|
</div>
|
||||||
<Link
|
<Link
|
||||||
href="/auth/forgot-password"
|
href="/auth/forgot-password"
|
||||||
className="text-sm text-default-800 dark:text-default-400 leading-6 font-medium hover:underline"
|
className="text-sm text-default-800 dark:text-default-400 leading-6 font-medium hover:underline"
|
||||||
>
|
>
|
||||||
Lupa kata sandi?
|
{t("forgotPass")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -199,7 +200,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
type="submit"
|
type="submit"
|
||||||
fullWidth
|
fullWidth
|
||||||
disabled={isSubmitting}
|
disabled={isSubmitting}
|
||||||
className="mt-6 bg-red-700"
|
className="mt-6 bg-red-700 cursor-pointer"
|
||||||
color="primary"
|
color="primary"
|
||||||
>
|
>
|
||||||
{isSubmitting ? (
|
{isSubmitting ? (
|
||||||
|
|
@ -208,7 +209,7 @@ export const LoginForm: React.FC<LoginFormProps> = ({
|
||||||
Processing...
|
Processing...
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"Selanjutnya"
|
t("enterOTP5")
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@
|
||||||
|
|
||||||
import React, { useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
||||||
import { OTPFormProps } from "@/types/auth";
|
import { OTPFormProps } from "@/types/auth";
|
||||||
import { useOTPVerification } from "@/hooks/use-auth";
|
import { useOTPVerification } from "@/hooks/use-auth";
|
||||||
import {
|
import {
|
||||||
|
|
@ -11,6 +10,7 @@ import {
|
||||||
InputOTPSeparator,
|
InputOTPSeparator,
|
||||||
InputOTPSlot,
|
InputOTPSlot,
|
||||||
} from "../ui/input-otp";
|
} from "../ui/input-otp";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
export const OTPForm: React.FC<OTPFormProps> = ({
|
export const OTPForm: React.FC<OTPFormProps> = ({
|
||||||
loginCredentials,
|
loginCredentials,
|
||||||
|
|
@ -21,6 +21,7 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
||||||
}) => {
|
}) => {
|
||||||
const { verifyOTP, loading } = useOTPVerification();
|
const { verifyOTP, loading } = useOTPVerification();
|
||||||
const [otpValue, setOtpValue] = useState("");
|
const [otpValue, setOtpValue] = useState("");
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
|
|
||||||
const handleTypeOTP = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleTypeOTP = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
const { key } = event;
|
const { key } = event;
|
||||||
|
|
@ -75,10 +76,8 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="text-left space-y-2">
|
<div className="text-left space-y-2">
|
||||||
<h1 className="font-semibold text-3xl text-left">Please Enter OTP</h1>
|
<h1 className="font-semibold text-3xl text-left">{t("enterOTP")}</h1>
|
||||||
<p className="text-default-500 text-base">
|
<p className="text-default-500 text-base">{t("enterOTP2")}</p>
|
||||||
Enter the 6-digit code sent to your email address.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* OTP Input */}
|
{/* OTP Input */}
|
||||||
|
|
@ -139,7 +138,7 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className="text-sm text-blue-600 hover:text-blue-800 underline disabled:opacity-50 disabled:cursor-not-allowed"
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -155,10 +154,10 @@ export const OTPForm: React.FC<OTPFormProps> = ({
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
<div className="w-4 h-4 border-2 border-white border-t-transparent rounded-full animate-spin" />
|
||||||
Verifying...
|
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
"Sign In"
|
t("enterOTP4")
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -689,7 +689,7 @@ export default function SignUp() {
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
{/* <div>
|
||||||
<Select
|
<Select
|
||||||
value={namaTenant}
|
value={namaTenant}
|
||||||
onValueChange={(value) => setNamaTenant(value)}
|
onValueChange={(value) => setNamaTenant(value)}
|
||||||
|
|
@ -715,9 +715,9 @@ export default function SignUp() {
|
||||||
{formErrors.namaTenant}
|
{formErrors.namaTenant}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div> */}
|
||||||
|
|
||||||
{/* <div>
|
<div>
|
||||||
<Input
|
<Input
|
||||||
type="text"
|
type="text"
|
||||||
required
|
required
|
||||||
|
|
@ -733,7 +733,7 @@ export default function SignUp() {
|
||||||
{formErrors.namaTenant}
|
{formErrors.namaTenant}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div> */}
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Input
|
<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";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
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() {
|
export default function Category() {
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
const [categories, setCategories] = useState<ArticleCategory[]>([]);
|
const [categories, setCategories] = useState<ArticleCategory[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
|
@ -15,7 +20,8 @@ export default function Category() {
|
||||||
if (response?.data?.success && response.data.data) {
|
if (response?.data?.success && response.data.data) {
|
||||||
// Filter hanya kategori yang aktif dan published
|
// Filter hanya kategori yang aktif dan published
|
||||||
const activeCategories = response.data.data.filter(
|
const activeCategories = response.data.data.filter(
|
||||||
(category: ArticleCategory) => category.isActive && category.isPublish
|
(category: ArticleCategory) =>
|
||||||
|
category.isActive && category.isPublish
|
||||||
);
|
);
|
||||||
setCategories(activeCategories);
|
setCategories(activeCategories);
|
||||||
}
|
}
|
||||||
|
|
@ -45,13 +51,16 @@ export default function Category() {
|
||||||
"SEPUTAR PRESTASI",
|
"SEPUTAR PRESTASI",
|
||||||
];
|
];
|
||||||
|
|
||||||
const displayCategories = categories.length > 0 ? categories : fallbackCategories;
|
const displayCategories =
|
||||||
|
categories.length > 0 ? categories : fallbackCategories;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="px-4 py-10">
|
<section className="px-4 py-10">
|
||||||
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
|
<div className="max-w-[1350px] mx-auto bg-white rounded-xl shadow-md p-6">
|
||||||
<h2 className="text-xl font-semibold mb-5">
|
<h2 className="text-xl font-semibold mb-5">
|
||||||
{loading ? "Memuat Kategori..." : `${displayCategories.length} Kategori Paling Populer`}
|
{loading
|
||||||
|
? t("loadCategory")
|
||||||
|
: `${displayCategories.length} ${t("category")}`}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{loading ? (
|
{loading ? (
|
||||||
|
|
@ -70,8 +79,12 @@ export default function Category() {
|
||||||
<div className="flex flex-wrap gap-3">
|
<div className="flex flex-wrap gap-3">
|
||||||
{displayCategories.map((category, index) => {
|
{displayCategories.map((category, index) => {
|
||||||
// Handle both API data and fallback data
|
// Handle both API data and fallback data
|
||||||
const categoryTitle = typeof category === 'string' ? category : category.title;
|
const categoryTitle =
|
||||||
const categorySlug = typeof category === 'string' ? category.toLowerCase().replace(/\s+/g, '-') : category.slug;
|
typeof category === "string" ? category : category.title;
|
||||||
|
const categorySlug =
|
||||||
|
typeof category === "string"
|
||||||
|
? category.toLowerCase().replace(/\s+/g, "-")
|
||||||
|
: category.slug;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
|
@ -79,7 +92,9 @@ export default function Category() {
|
||||||
className="px-4 py-2 rounded border border-gray-300 text-gray-700 text-sm font-medium hover:bg-gray-100 hover:border-gray-400 transition-all duration-200"
|
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={() => {
|
onClick={() => {
|
||||||
// Navigate to category page or search by category
|
// 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
|
// TODO: Implement navigation to category page
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -3,11 +3,16 @@
|
||||||
import { Instagram } from "lucide-react";
|
import { Instagram } from "lucide-react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import { useState, useEffect } from "react";
|
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 { Swiper, SwiperSlide } from "swiper/react";
|
||||||
import { Navigation, Autoplay } from "swiper/modules";
|
import { Navigation, Autoplay } from "swiper/modules";
|
||||||
import "swiper/css";
|
import "swiper/css";
|
||||||
import "swiper/css/navigation";
|
import "swiper/css/navigation";
|
||||||
|
import LocalSwitcher from "../partials/header/locale-switcher";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
// Custom styles for Swiper
|
// Custom styles for Swiper
|
||||||
const swiperStyles = `
|
const swiperStyles = `
|
||||||
|
|
@ -71,6 +76,7 @@ const logos = [
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Footer() {
|
export default function Footer() {
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
const [clients, setClients] = useState<PublicClient[]>([]);
|
const [clients, setClients] = useState<PublicClient[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
|
@ -84,7 +90,6 @@ export default function Footer() {
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error fetching public clients:", error);
|
console.error("Error fetching public clients:", error);
|
||||||
// Fallback to static logos if API fails
|
|
||||||
setClients([]);
|
setClients([]);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -100,7 +105,7 @@ export default function Footer() {
|
||||||
<div className="max-w-[1350px] mx-auto">
|
<div className="max-w-[1350px] mx-auto">
|
||||||
<div className="py-6">
|
<div className="py-6">
|
||||||
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
|
<h2 className="text-2xl font-semibold mb-4 px-4 md:px-0">
|
||||||
Publikasi
|
{t("publication")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="px-4 md:px-12">
|
<div className="px-4 md:px-12">
|
||||||
<Swiper
|
<Swiper
|
||||||
|
|
@ -117,17 +122,19 @@ export default function Footer() {
|
||||||
disableOnInteraction: false,
|
disableOnInteraction: false,
|
||||||
}}
|
}}
|
||||||
loop={clients.length > 4}
|
loop={clients.length > 4}
|
||||||
className={`client-swiper ${clients.length <= 4 ? 'swiper-centered' : ''}`}
|
className={`client-swiper ${
|
||||||
|
clients.length <= 4 ? "swiper-centered" : ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
{loading ? (
|
{loading
|
||||||
// Loading skeleton
|
? // Loading skeleton
|
||||||
Array.from({ length: 8 }).map((_, idx) => (
|
Array.from({ length: 8 }).map((_, idx) => (
|
||||||
<SwiperSlide key={idx} className="!w-auto">
|
<SwiperSlide key={idx} className="!w-auto">
|
||||||
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
|
<div className="w-[80px] h-[80px] md:w-[100px] md:h-[100px] bg-gray-200 rounded animate-pulse" />
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))
|
))
|
||||||
) : clients.length > 0 ? (
|
: clients.length > 0
|
||||||
// Dynamic clients from API
|
? // Dynamic clients from API
|
||||||
clients.map((client, idx) => (
|
clients.map((client, idx) => (
|
||||||
<SwiperSlide key={idx} className="!w-auto">
|
<SwiperSlide key={idx} className="!w-auto">
|
||||||
<a
|
<a
|
||||||
|
|
@ -159,8 +166,7 @@ export default function Footer() {
|
||||||
</a>
|
</a>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))
|
))
|
||||||
) : (
|
: // Fallback to static logos if API fails or no data
|
||||||
// Fallback to static logos if API fails or no data
|
|
||||||
logos.map((logo, idx) => (
|
logos.map((logo, idx) => (
|
||||||
<SwiperSlide key={idx} className="!w-auto">
|
<SwiperSlide key={idx} className="!w-auto">
|
||||||
<a
|
<a
|
||||||
|
|
@ -178,13 +184,12 @@ export default function Footer() {
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</SwiperSlide>
|
</SwiperSlide>
|
||||||
))
|
))}
|
||||||
)}
|
|
||||||
</Swiper>
|
</Swiper>
|
||||||
|
|
||||||
{/* Navigation Buttons */}
|
{/* 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-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-next !text-gray-600 !w-8 !h-8 !mt-0 !top-1/2 !right-0 !-translate-y-1/2"></div> */}
|
||||||
</div>
|
</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 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">
|
<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
|
<Image
|
||||||
src="/qudo.png"
|
src="/qudo.png"
|
||||||
alt="qudoco"
|
alt="qudoco"
|
||||||
|
|
@ -260,6 +265,11 @@ export default function Footer() {
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* button language */}
|
||||||
|
<div className={`relative text-left border rounded-lg`}>
|
||||||
|
<LocalSwitcher />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,33 @@ import { listData, listArticles } from "@/service/landing/landing";
|
||||||
import { getCookiesDecrypt } from "@/lib/utils";
|
import { getCookiesDecrypt } from "@/lib/utils";
|
||||||
import { toggleBookmark, getBookmarkSummaryForUser } from "@/service/content";
|
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() {
|
export default function Header() {
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
const [data, setData] = useState<any[]>([]);
|
const [data, setData] = useState<any[]>([]);
|
||||||
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
const [bookmarkedIds, setBookmarkedIds] = useState<Set<number>>(new Set());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
const slug = params?.slug as string;
|
const slug = params?.slug as string;
|
||||||
|
|
||||||
// ✅ Ambil data artikel (khusus typeId = 1 -> Image)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
try {
|
try {
|
||||||
const response = await listArticles(
|
const response = await listArticles(
|
||||||
1,
|
1,
|
||||||
5,
|
5,
|
||||||
1, // hanya typeId = 1 (image)
|
1,
|
||||||
undefined,
|
undefined,
|
||||||
undefined,
|
undefined,
|
||||||
"createdAt",
|
"createdAt",
|
||||||
|
|
@ -36,7 +47,7 @@ export default function Header() {
|
||||||
let articlesData: any[] = [];
|
let articlesData: any[] = [];
|
||||||
|
|
||||||
if (response?.error) {
|
if (response?.error) {
|
||||||
const fallback = await listData(
|
const fallbackResponse = await listData(
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
|
|
@ -45,40 +56,46 @@ export default function Header() {
|
||||||
"createdAt",
|
"createdAt",
|
||||||
"",
|
"",
|
||||||
"",
|
"",
|
||||||
"1"
|
""
|
||||||
|
);
|
||||||
|
articlesData = (fallbackResponse?.data?.data?.content || []).filter(
|
||||||
|
(item: any) => item.typeId === 1
|
||||||
);
|
);
|
||||||
articlesData = fallback?.data?.data?.content || [];
|
|
||||||
} else {
|
} else {
|
||||||
articlesData = response?.data?.data || [];
|
articlesData = (response?.data?.data || []).filter(
|
||||||
|
(item: any) => item.typeId === 1
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const transformed = articlesData.map((article: any) =>
|
const transformed = articlesData.map((article: any) => ({
|
||||||
itemTransform(article)
|
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);
|
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"));
|
const roleId = Number(getCookiesDecrypt("urie"));
|
||||||
let localSet = new Set<number>();
|
if (roleId && !isNaN(roleId)) {
|
||||||
|
const userId = getCookiesDecrypt("uie");
|
||||||
|
const localKey = `bookmarkedIds_${userId || "guest"}`;
|
||||||
|
const saved = localStorage.getItem(localKey);
|
||||||
|
|
||||||
const simpananLocal = localStorage.getItem("bookmarkedIds");
|
let localSet = new Set<number>();
|
||||||
if (simpananLocal) {
|
if (saved) {
|
||||||
localSet = new Set(JSON.parse(simpananLocal));
|
localSet = new Set(JSON.parse(saved));
|
||||||
|
setBookmarkedIds(localSet);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Jika user login, gabungkan dengan data dari backend
|
|
||||||
if (roleId && !isNaN(roleId)) {
|
|
||||||
const res = await getBookmarkSummaryForUser();
|
const res = await getBookmarkSummaryForUser();
|
||||||
const bookmarks =
|
const bookmarks =
|
||||||
res?.data?.data?.recentBookmarks ||
|
res?.data?.data?.recentBookmarks ||
|
||||||
|
|
@ -92,34 +109,41 @@ export default function Header() {
|
||||||
.filter((x) => !isNaN(x))
|
.filter((x) => !isNaN(x))
|
||||||
);
|
);
|
||||||
|
|
||||||
const gabungan = new Set([...localSet, ...ids]);
|
const merged = new Set([...localSet, ...ids]);
|
||||||
setBookmarkedIds(gabungan);
|
setBookmarkedIds(merged);
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"bookmarkedIds",
|
"bookmarkedIds",
|
||||||
JSON.stringify(Array.from(gabungan))
|
JSON.stringify(Array.from(merged))
|
||||||
);
|
);
|
||||||
} else {
|
|
||||||
// Jika belum login, pakai local saja
|
|
||||||
setBookmarkedIds(localSet);
|
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (error) {
|
||||||
console.error("Gagal sinkronisasi bookmark:", err);
|
console.error("Gagal memuat data:", error);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
syncBookmarks();
|
fetchData();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (bookmarkedIds.size > 0) {
|
||||||
|
localStorage.setItem(
|
||||||
|
"bookmarkedIds",
|
||||||
|
JSON.stringify(Array.from(bookmarkedIds))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}, [bookmarkedIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="max-w-[1350px] mx-auto px-4">
|
<section className="max-w-[1350px] mx-auto px-4">
|
||||||
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
<div className="flex flex-col lg:flex-row gap-6 py-6">
|
||||||
{data.length > 0 && (
|
{data.length > 0 && (
|
||||||
<Card
|
<Card
|
||||||
key={data[0].id}
|
|
||||||
item={data[0]}
|
item={data[0]}
|
||||||
isBig
|
isBig
|
||||||
bookmarkedIds={bookmarkedIds}
|
isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
|
||||||
setBookmarkedIds={setBookmarkedIds}
|
onSaved={(id) =>
|
||||||
|
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
@ -128,74 +152,80 @@ export default function Header() {
|
||||||
<Card
|
<Card
|
||||||
key={item.id}
|
key={item.id}
|
||||||
item={item}
|
item={item}
|
||||||
bookmarkedIds={bookmarkedIds}
|
isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
|
||||||
setBookmarkedIds={setBookmarkedIds}
|
onSaved={(id) =>
|
||||||
|
setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
|
<div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl overflow-hidden">
|
||||||
<Image
|
<Swiper
|
||||||
src={"/PPS.png"}
|
modules={[Navigation, Pagination]}
|
||||||
alt={"pps"}
|
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
|
fill
|
||||||
className="object-cover rounded-xl"
|
className="object-cover rounded-xl"
|
||||||
|
priority={index === 0}
|
||||||
|
/> */}
|
||||||
|
<ImageBlurry
|
||||||
|
priority
|
||||||
|
src={img}
|
||||||
|
alt="gambar"
|
||||||
|
style={{
|
||||||
|
objectFit: "contain",
|
||||||
|
width: "100%",
|
||||||
|
height: "100%",
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</SwiperSlide>
|
||||||
|
))}
|
||||||
|
</Swiper>
|
||||||
|
</div>
|
||||||
</section>
|
</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({
|
function Card({
|
||||||
item,
|
item,
|
||||||
isBig = false,
|
isBig = false,
|
||||||
bookmarkedIds,
|
isInitiallyBookmarked = false,
|
||||||
setBookmarkedIds,
|
onSaved,
|
||||||
}: {
|
}: {
|
||||||
item: any;
|
item: any;
|
||||||
isBig?: boolean;
|
isBig?: boolean;
|
||||||
bookmarkedIds: Set<number>;
|
isInitiallyBookmarked?: boolean;
|
||||||
setBookmarkedIds: React.Dispatch<React.SetStateAction<Set<number>>>;
|
onSaved?: (id: number) => void;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
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 getLink = () => `/content/image/detail/${item?.id}`;
|
||||||
|
|
||||||
const handleToggleBookmark = async () => {
|
const handleSave = async () => {
|
||||||
const roleId = Number(getCookiesDecrypt("urie"));
|
const roleId = Number(getCookiesDecrypt("urie"));
|
||||||
|
|
||||||
if (!roleId || isNaN(roleId)) {
|
if (!roleId || isNaN(roleId)) {
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
icon: "warning",
|
icon: "warning",
|
||||||
|
|
@ -215,31 +245,26 @@ function Card({
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
icon: "error",
|
icon: "error",
|
||||||
title: "Gagal",
|
title: "Gagal",
|
||||||
text: "Gagal memperbarui bookmark.",
|
text: "Gagal menyimpan artikel.",
|
||||||
confirmButtonColor: "#d33",
|
confirmButtonColor: "#d33",
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
const updated = new Set(bookmarkedIds);
|
setIsBookmarked(true);
|
||||||
let pesan = "";
|
onSaved?.(item.id);
|
||||||
|
|
||||||
if (isBookmarked) {
|
const saved = localStorage.getItem("bookmarkedIds");
|
||||||
updated.delete(Number(item.id));
|
const newSet = new Set<number>(saved ? JSON.parse(saved) : []);
|
||||||
pesan = "Dihapus dari bookmark.";
|
newSet.add(Number(item.id));
|
||||||
} else {
|
|
||||||
updated.add(Number(item.id));
|
|
||||||
pesan = "Artikel disimpan ke bookmark.";
|
|
||||||
}
|
|
||||||
|
|
||||||
setBookmarkedIds(updated);
|
|
||||||
localStorage.setItem(
|
localStorage.setItem(
|
||||||
"bookmarkedIds",
|
"bookmarkedIds",
|
||||||
JSON.stringify(Array.from(updated))
|
JSON.stringify(Array.from(newSet))
|
||||||
);
|
);
|
||||||
|
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
icon: "success",
|
icon: "success",
|
||||||
title: isBookmarked ? "Dihapus!" : "Disimpan!",
|
title: "Berhasil",
|
||||||
text: pesan,
|
text: "Artikel berhasil disimpan ke bookmark.",
|
||||||
|
confirmButtonColor: "#3085d6",
|
||||||
timer: 1500,
|
timer: 1500,
|
||||||
showConfirmButton: false,
|
showConfirmButton: false,
|
||||||
});
|
});
|
||||||
|
|
@ -258,14 +283,19 @@ function Card({
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<div>
|
||||||
<div
|
<div
|
||||||
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
|
className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
|
||||||
isBig
|
isBig
|
||||||
? "w-full lg:max-w-[670px] h-[680px]"
|
? "w-full lg:max-w-[670px] lg:min-h-[680px]"
|
||||||
: "w-full h-[360px] md:h-[340px]"
|
: "w-full h-[350px] md:h-[330px]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
|
<div
|
||||||
|
className={`relative ${
|
||||||
|
isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
|
||||||
|
} w-full`}
|
||||||
|
>
|
||||||
<Link href={getLink()}>
|
<Link href={getLink()}>
|
||||||
<Image
|
<Image
|
||||||
src={item.smallThumbnailLink || "/contributor.png"}
|
src={item.smallThumbnailLink || "/contributor.png"}
|
||||||
|
|
@ -276,8 +306,7 @@ function Card({
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 flex flex-col justify-between flex-1">
|
<div className="py-[26px] px-4 space-y-2">
|
||||||
<div className="space-y-2">
|
|
||||||
<div className="flex items-center gap-2 text-xs font-semibold flex-wrap">
|
<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">
|
<span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
|
||||||
{item.clientName}
|
{item.clientName}
|
||||||
|
|
@ -307,9 +336,8 @@ function Card({
|
||||||
{item.title}
|
{item.title}
|
||||||
</h3>
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex justify-between items-center pt-4">
|
<div className="flex justify-between items-center pt-2">
|
||||||
<div className="flex gap-2 text-gray-500">
|
<div className="flex gap-2 text-gray-500">
|
||||||
<ThumbsUp
|
<ThumbsUp
|
||||||
size={18}
|
size={18}
|
||||||
|
|
@ -322,7 +350,7 @@ function Card({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
onClick={handleToggleBookmark}
|
onClick={handleSave}
|
||||||
disabled={isSaving || isBookmarked}
|
disabled={isSaving || isBookmarked}
|
||||||
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
||||||
isBookmarked
|
isBookmarked
|
||||||
|
|
@ -330,11 +358,12 @@ function Card({
|
||||||
: "bg-[#F60100] text-white hover:bg-[#c90000]"
|
: "bg-[#F60100] text-white hover:bg-[#c90000]"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
|
{isSaving ? t("saving") : isBookmarked ? t("saved") : t("save")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -357,18 +386,16 @@ function Card({
|
||||||
// const router = useRouter();
|
// const router = useRouter();
|
||||||
// const params = useParams();
|
// const params = useParams();
|
||||||
// const MySwal = withReactContent(Swal);
|
// const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
// // Get slug from URL params
|
|
||||||
// const slug = params?.slug as string;
|
// const slug = params?.slug as string;
|
||||||
|
|
||||||
|
// // ✅ Ambil data artikel (khusus typeId = 1 -> Image)
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
// const fetchData = async () => {
|
// const fetchData = async () => {
|
||||||
// try {
|
// try {
|
||||||
// // 🔹 Ambil artikel
|
|
||||||
// const response = await listArticles(
|
// const response = await listArticles(
|
||||||
// 1,
|
// 1,
|
||||||
// 5,
|
// 5,
|
||||||
// undefined,
|
// 1, // hanya typeId = 1 (image)
|
||||||
// undefined,
|
// undefined,
|
||||||
// undefined,
|
// undefined,
|
||||||
// "createdAt",
|
// "createdAt",
|
||||||
|
|
@ -378,8 +405,7 @@ function Card({
|
||||||
// let articlesData: any[] = [];
|
// let articlesData: any[] = [];
|
||||||
|
|
||||||
// if (response?.error) {
|
// if (response?.error) {
|
||||||
// // fallback ke API lama
|
// const fallback = await listData(
|
||||||
// const fallbackResponse = await listData(
|
|
||||||
// "",
|
// "",
|
||||||
// "",
|
// "",
|
||||||
// "",
|
// "",
|
||||||
|
|
@ -388,15 +414,111 @@ function Card({
|
||||||
// "createdAt",
|
// "createdAt",
|
||||||
// "",
|
// "",
|
||||||
// "",
|
// "",
|
||||||
// ""
|
// "1"
|
||||||
// );
|
// );
|
||||||
// articlesData = fallbackResponse?.data?.data?.content || [];
|
// articlesData = fallback?.data?.data?.content || [];
|
||||||
// } else {
|
// } else {
|
||||||
// articlesData = response?.data?.data || [];
|
// articlesData = response?.data?.data || [];
|
||||||
// }
|
// }
|
||||||
|
|
||||||
// // 🔹 Transform agar seragam
|
// const transformed = articlesData.map((article: any) =>
|
||||||
// const transformed = articlesData.map((article: any) => ({
|
// itemTransform(article)
|
||||||
|
// );
|
||||||
|
|
||||||
|
// setData(transformed);
|
||||||
|
// } catch (error) {
|
||||||
|
// console.error("Gagal memuat data:", error);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// fetchData();
|
||||||
|
// }, [slug]);
|
||||||
|
|
||||||
|
// // ✅ Sinkronisasi bookmark: dari localStorage + backend user login
|
||||||
|
// useEffect(() => {
|
||||||
|
// const syncBookmarks = async () => {
|
||||||
|
// try {
|
||||||
|
// const roleId = Number(getCookiesDecrypt("urie"));
|
||||||
|
// let localSet = new Set<number>();
|
||||||
|
|
||||||
|
// const simpananLocal = localStorage.getItem("bookmarkedIds");
|
||||||
|
// if (simpananLocal) {
|
||||||
|
// localSet = new Set(JSON.parse(simpananLocal));
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // Jika user login, gabungkan dengan data dari backend
|
||||||
|
// if (roleId && !isNaN(roleId)) {
|
||||||
|
// const res = await getBookmarkSummaryForUser();
|
||||||
|
// const bookmarks =
|
||||||
|
// res?.data?.data?.recentBookmarks ||
|
||||||
|
// res?.data?.data?.bookmarks ||
|
||||||
|
// res?.data?.data ||
|
||||||
|
// [];
|
||||||
|
|
||||||
|
// const ids = new Set<number>(
|
||||||
|
// (Array.isArray(bookmarks) ? bookmarks : [])
|
||||||
|
// .map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
|
||||||
|
// .filter((x) => !isNaN(x))
|
||||||
|
// );
|
||||||
|
|
||||||
|
// const gabungan = new Set([...localSet, ...ids]);
|
||||||
|
// setBookmarkedIds(gabungan);
|
||||||
|
// localStorage.setItem(
|
||||||
|
// "bookmarkedIds",
|
||||||
|
// JSON.stringify(Array.from(gabungan))
|
||||||
|
// );
|
||||||
|
// } else {
|
||||||
|
// // Jika belum login, pakai local saja
|
||||||
|
// setBookmarkedIds(localSet);
|
||||||
|
// }
|
||||||
|
// } catch (err) {
|
||||||
|
// console.error("Gagal sinkronisasi bookmark:", err);
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// syncBookmarks();
|
||||||
|
// }, []);
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <section className="max-w-[1350px] mx-auto px-4">
|
||||||
|
// <div className="flex flex-col lg:flex-row gap-6 py-6">
|
||||||
|
// {data.length > 0 && (
|
||||||
|
// <Card
|
||||||
|
// key={data[0].id}
|
||||||
|
// item={data[0]}
|
||||||
|
// isBig
|
||||||
|
// bookmarkedIds={bookmarkedIds}
|
||||||
|
// setBookmarkedIds={setBookmarkedIds}
|
||||||
|
// />
|
||||||
|
// )}
|
||||||
|
|
||||||
|
// <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
|
||||||
|
// {data.slice(1, 5).map((item) => (
|
||||||
|
// <Card
|
||||||
|
// key={item.id}
|
||||||
|
// item={item}
|
||||||
|
// bookmarkedIds={bookmarkedIds}
|
||||||
|
// setBookmarkedIds={setBookmarkedIds}
|
||||||
|
// />
|
||||||
|
// ))}
|
||||||
|
// </div>
|
||||||
|
// </div>
|
||||||
|
|
||||||
|
// <div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
|
||||||
|
// <Image
|
||||||
|
// src={"/PPS.png"}
|
||||||
|
// alt={"pps"}
|
||||||
|
// fill
|
||||||
|
// className="object-cover rounded-xl"
|
||||||
|
// />
|
||||||
|
// </div>
|
||||||
|
// </section>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// // 🔹 Helper function
|
||||||
|
// function itemTransform(article: any) {
|
||||||
|
// return {
|
||||||
// id: article.id,
|
// id: article.id,
|
||||||
// title: article.title,
|
// title: article.title,
|
||||||
// categoryName:
|
// categoryName:
|
||||||
|
|
@ -418,139 +540,31 @@ function Card({
|
||||||
// : article.typeId === 4
|
// : article.typeId === 4
|
||||||
// ? "Audio"
|
// ? "Audio"
|
||||||
// : "",
|
// : "",
|
||||||
// }));
|
|
||||||
|
|
||||||
// setData(transformed);
|
|
||||||
|
|
||||||
// const roleId = Number(getCookiesDecrypt("urie"));
|
|
||||||
// if (roleId && !isNaN(roleId)) {
|
|
||||||
// // const saved = localStorage.getItem("bookmarkedIds");
|
|
||||||
// const userId = getCookiesDecrypt("uie");
|
|
||||||
// const localKey = `bookmarkedIds_${userId || "guest"}`;
|
|
||||||
// const saved = localStorage.getItem(localKey);
|
|
||||||
|
|
||||||
// let localSet = new Set<number>();
|
|
||||||
// if (saved) {
|
|
||||||
// localSet = new Set(JSON.parse(saved));
|
|
||||||
// setBookmarkedIds(localSet);
|
|
||||||
// }
|
|
||||||
|
|
||||||
// const res = await getBookmarkSummaryForUser();
|
|
||||||
// const bookmarks =
|
|
||||||
// res?.data?.data?.recentBookmarks ||
|
|
||||||
// res?.data?.data?.bookmarks ||
|
|
||||||
// res?.data?.data ||
|
|
||||||
// [];
|
|
||||||
|
|
||||||
// const ids = new Set<number>(
|
|
||||||
// (Array.isArray(bookmarks) ? bookmarks : [])
|
|
||||||
// .map((b: any) => Number(b.articleId ?? b.id ?? b.article?.id))
|
|
||||||
// .filter((x) => !isNaN(x))
|
|
||||||
// );
|
|
||||||
|
|
||||||
// const merged = new Set([...localSet, ...ids]);
|
|
||||||
// setBookmarkedIds(merged);
|
|
||||||
// localStorage.setItem(
|
|
||||||
// "bookmarkedIds",
|
|
||||||
// JSON.stringify(Array.from(merged))
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error("Gagal memuat data:", error);
|
|
||||||
// }
|
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// fetchData();
|
|
||||||
// }, []);
|
|
||||||
|
|
||||||
// // Simpan setiap kali state berubah
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (bookmarkedIds.size > 0) {
|
|
||||||
// localStorage.setItem(
|
|
||||||
// "bookmarkedIds",
|
|
||||||
// JSON.stringify(Array.from(bookmarkedIds))
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
// }, [bookmarkedIds]);
|
|
||||||
|
|
||||||
// return (
|
|
||||||
// <section className="max-w-[1350px] mx-auto px-4">
|
|
||||||
// <div className="flex flex-col lg:flex-row gap-6 py-6">
|
|
||||||
// {data.length > 0 && (
|
|
||||||
// <Card
|
|
||||||
// item={data[0]}
|
|
||||||
// isBig
|
|
||||||
// isInitiallyBookmarked={bookmarkedIds.has(Number(data[0].id))}
|
|
||||||
// onSaved={(id) =>
|
|
||||||
// setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
|
||||||
// }
|
|
||||||
// />
|
|
||||||
// )}
|
|
||||||
|
|
||||||
// <div className="grid grid-cols-1 sm:grid-cols-2 gap-4 w-full">
|
|
||||||
// {data.slice(1, 5).map((item) => (
|
|
||||||
// <Card
|
|
||||||
// key={item.id}
|
|
||||||
// item={item}
|
|
||||||
// isInitiallyBookmarked={bookmarkedIds.has(Number(item.id))}
|
|
||||||
// onSaved={(id) =>
|
|
||||||
// setBookmarkedIds((prev) => new Set([...prev, Number(id)]))
|
|
||||||
// }
|
|
||||||
// />
|
|
||||||
// ))}
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
|
|
||||||
// <div className="relative w-full h-48 sm:h-64 md:h-80 lg:h-[460px] mt-4 rounded-xl">
|
|
||||||
// <Image
|
|
||||||
// src={"/PPS.png"}
|
|
||||||
// alt={"pps"}
|
|
||||||
// fill
|
|
||||||
// className="object-cover rounded-xl"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// </section>
|
|
||||||
// );
|
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
// // 🔹 Komponen Card
|
||||||
// function Card({
|
// function Card({
|
||||||
// item,
|
// item,
|
||||||
// isBig = false,
|
// isBig = false,
|
||||||
// isInitiallyBookmarked = false,
|
// bookmarkedIds,
|
||||||
// onSaved,
|
// setBookmarkedIds,
|
||||||
// }: {
|
// }: {
|
||||||
// item: any;
|
// item: any;
|
||||||
// isBig?: boolean;
|
// isBig?: boolean;
|
||||||
// isInitiallyBookmarked?: boolean;
|
// bookmarkedIds: Set<number>;
|
||||||
// onSaved?: (id: number) => void;
|
// setBookmarkedIds: React.Dispatch<React.SetStateAction<Set<number>>>;
|
||||||
// }) {
|
// }) {
|
||||||
// const router = useRouter();
|
// const router = useRouter();
|
||||||
// const MySwal = withReactContent(Swal);
|
// const MySwal = withReactContent(Swal);
|
||||||
// const [isSaving, setIsSaving] = useState(false);
|
// const [isSaving, setIsSaving] = useState(false);
|
||||||
// const [isBookmarked, setIsBookmarked] = useState(isInitiallyBookmarked);
|
|
||||||
|
|
||||||
// useEffect(() => {
|
// const isBookmarked = bookmarkedIds.has(Number(item.id));
|
||||||
// setIsBookmarked(isInitiallyBookmarked);
|
|
||||||
// }, [isInitiallyBookmarked]);
|
|
||||||
|
|
||||||
// const getLink = () => {
|
// const getLink = () => `/content/image/detail/${item?.id}`;
|
||||||
// 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 handleSave = async () => {
|
// const handleToggleBookmark = async () => {
|
||||||
// const roleId = Number(getCookiesDecrypt("urie"));
|
// const roleId = Number(getCookiesDecrypt("urie"));
|
||||||
|
|
||||||
// if (!roleId || isNaN(roleId)) {
|
// if (!roleId || isNaN(roleId)) {
|
||||||
// MySwal.fire({
|
// MySwal.fire({
|
||||||
// icon: "warning",
|
// icon: "warning",
|
||||||
|
|
@ -570,27 +584,31 @@ function Card({
|
||||||
// MySwal.fire({
|
// MySwal.fire({
|
||||||
// icon: "error",
|
// icon: "error",
|
||||||
// title: "Gagal",
|
// title: "Gagal",
|
||||||
// text: "Gagal menyimpan artikel.",
|
// text: "Gagal memperbarui bookmark.",
|
||||||
// confirmButtonColor: "#d33",
|
// confirmButtonColor: "#d33",
|
||||||
// });
|
// });
|
||||||
// } else {
|
// } else {
|
||||||
// setIsBookmarked(true);
|
// const updated = new Set(bookmarkedIds);
|
||||||
// onSaved?.(item.id);
|
// let pesan = "";
|
||||||
|
|
||||||
// // 🔹 Simpan ke localStorage
|
// if (isBookmarked) {
|
||||||
// const saved = localStorage.getItem("bookmarkedIds");
|
// updated.delete(Number(item.id));
|
||||||
// const newSet = new Set<number>(saved ? JSON.parse(saved) : []);
|
// pesan = "Dihapus dari bookmark.";
|
||||||
// newSet.add(Number(item.id));
|
// } else {
|
||||||
|
// updated.add(Number(item.id));
|
||||||
|
// pesan = "Artikel disimpan ke bookmark.";
|
||||||
|
// }
|
||||||
|
|
||||||
|
// setBookmarkedIds(updated);
|
||||||
// localStorage.setItem(
|
// localStorage.setItem(
|
||||||
// "bookmarkedIds",
|
// "bookmarkedIds",
|
||||||
// JSON.stringify(Array.from(newSet))
|
// JSON.stringify(Array.from(updated))
|
||||||
// );
|
// );
|
||||||
|
|
||||||
// MySwal.fire({
|
// MySwal.fire({
|
||||||
// icon: "success",
|
// icon: "success",
|
||||||
// title: "Berhasil",
|
// title: isBookmarked ? "Dihapus!" : "Disimpan!",
|
||||||
// text: "Artikel berhasil disimpan ke bookmark.",
|
// text: pesan,
|
||||||
// confirmButtonColor: "#3085d6",
|
|
||||||
// timer: 1500,
|
// timer: 1500,
|
||||||
// showConfirmButton: false,
|
// showConfirmButton: false,
|
||||||
// });
|
// });
|
||||||
|
|
@ -609,19 +627,14 @@ function Card({
|
||||||
// };
|
// };
|
||||||
|
|
||||||
// return (
|
// return (
|
||||||
// <div>
|
|
||||||
// <div
|
// <div
|
||||||
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white ${
|
// className={`rounded-xl overflow-hidden shadow hover:shadow-lg transition-all bg-white flex flex-col ${
|
||||||
// isBig
|
// isBig
|
||||||
// ? "w-full lg:max-w-[670px] lg:min-h-[680px]"
|
// ? "w-full lg:max-w-[670px] h-[680px]"
|
||||||
// : "w-full h-[350px] md:h-[330px]"
|
// : "w-full h-[360px] md:h-[340px]"
|
||||||
// }`}
|
// }`}
|
||||||
// >
|
// >
|
||||||
// <div
|
// <div className={`relative ${isBig ? "h-[420px]" : "h-[180px]"} w-full`}>
|
||||||
// className={`relative ${
|
|
||||||
// isBig ? "aspect-[3/2] lg:h-[525px]" : "aspect-video"
|
|
||||||
// } w-full`}
|
|
||||||
// >
|
|
||||||
// <Link href={getLink()}>
|
// <Link href={getLink()}>
|
||||||
// <Image
|
// <Image
|
||||||
// src={item.smallThumbnailLink || "/contributor.png"}
|
// src={item.smallThumbnailLink || "/contributor.png"}
|
||||||
|
|
@ -632,7 +645,8 @@ function Card({
|
||||||
// </Link>
|
// </Link>
|
||||||
// </div>
|
// </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">
|
// <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">
|
// <span className="bg-emerald-600 text-white px-2 py-0.5 rounded">
|
||||||
// {item.clientName}
|
// {item.clientName}
|
||||||
|
|
@ -662,8 +676,9 @@ function Card({
|
||||||
// {item.title}
|
// {item.title}
|
||||||
// </h3>
|
// </h3>
|
||||||
// </Link>
|
// </Link>
|
||||||
|
// </div>
|
||||||
|
|
||||||
// <div className="flex justify-between items-center pt-2">
|
// <div className="flex justify-between items-center pt-4">
|
||||||
// <div className="flex gap-2 text-gray-500">
|
// <div className="flex gap-2 text-gray-500">
|
||||||
// <ThumbsUp
|
// <ThumbsUp
|
||||||
// size={18}
|
// size={18}
|
||||||
|
|
@ -676,7 +691,7 @@ function Card({
|
||||||
// </div>
|
// </div>
|
||||||
|
|
||||||
// <button
|
// <button
|
||||||
// onClick={handleSave}
|
// onClick={handleToggleBookmark}
|
||||||
// disabled={isSaving || isBookmarked}
|
// disabled={isSaving || isBookmarked}
|
||||||
// className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
// className={`text-sm px-3 py-1 rounded-md transition-all duration-200 ${
|
||||||
// isBookmarked
|
// isBookmarked
|
||||||
|
|
@ -684,11 +699,10 @@ function Card({
|
||||||
// : "bg-[#F60100] text-white hover:bg-[#c90000]"
|
// : "bg-[#F60100] text-white hover:bg-[#c90000]"
|
||||||
// }`}
|
// }`}
|
||||||
// >
|
// >
|
||||||
// {isSaving ? "Saving..." : isBookmarked ? "Saved" : "Save"}
|
// {isSaving ? "Menyimpan" : isBookmarked ? "Tersimpan" : "Simpan"}
|
||||||
// </button>
|
// </button>
|
||||||
// </div>
|
// </div>
|
||||||
// </div>
|
// </div>
|
||||||
// </div>
|
// </div>
|
||||||
// </div>
|
|
||||||
// );
|
// );
|
||||||
// }
|
// }
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { Navigation } from "swiper/modules";
|
||||||
import Swal from "sweetalert2";
|
import Swal from "sweetalert2";
|
||||||
import withReactContent from "sweetalert2-react-content";
|
import withReactContent from "sweetalert2-react-content";
|
||||||
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
import { ThumbsUp, ThumbsDown } from "lucide-react";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
|
|
||||||
function formatTanggal(dateString: string) {
|
function formatTanggal(dateString: string) {
|
||||||
if (!dateString) return "";
|
if (!dateString) return "";
|
||||||
|
|
@ -35,6 +36,7 @@ function formatTanggal(dateString: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MediaUpdate() {
|
export default function MediaUpdate() {
|
||||||
|
const t = useTranslations("MediaUpdate");
|
||||||
const [tab, setTab] = useState<"latest" | "popular">("latest");
|
const [tab, setTab] = useState<"latest" | "popular">("latest");
|
||||||
const [contentType, setContentType] = useState<
|
const [contentType, setContentType] = useState<
|
||||||
"audiovisual" | "audio" | "foto" | "text" | "all"
|
"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">
|
<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">
|
<div className="max-w-screen-xl mx-auto">
|
||||||
<h2 className="text-2xl font-semibold text-center mb-6">
|
<h2 className="text-2xl font-semibold text-center mb-6">
|
||||||
Media Update
|
{t("title")}
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
{/* Main Tab */}
|
{/* Main Tab */}
|
||||||
|
|
@ -343,7 +345,7 @@ export default function MediaUpdate() {
|
||||||
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Terbaru
|
{t("latest")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setTab("popular")}
|
onClick={() => setTab("popular")}
|
||||||
|
|
@ -353,7 +355,7 @@ export default function MediaUpdate() {
|
||||||
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
: "text-[#C6A455] hover:bg-[#C6A455]/10"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
Terpopuler
|
{t("popular")}{" "}
|
||||||
</button>
|
</button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</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"
|
: "bg-white text-orange-600 border-2 border-orange-200 hover:border-orange-400 hover:shadow-md"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
📸 Foto
|
📸 {t("image")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setContentType("audiovisual")}
|
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"
|
: "bg-white text-purple-600 border-2 border-purple-200 hover:border-purple-400 hover:shadow-md"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
🎬 Audio Visual
|
🎬 {t("video")}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setContentType("audio")}
|
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"
|
: "bg-white text-gray-600 border-2 border-gray-200 hover:border-gray-400 hover:shadow-md"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
📝 Text
|
📝 {t("text")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -419,7 +421,7 @@ export default function MediaUpdate() {
|
||||||
|
|
||||||
{/* Slider */}
|
{/* Slider */}
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<p className="text-center">Memuat konten...</p>
|
<p className="text-center">{t("loadContent")}</p>
|
||||||
) : (
|
) : (
|
||||||
<Swiper
|
<Swiper
|
||||||
modules={[Navigation]}
|
modules={[Navigation]}
|
||||||
|
|
@ -517,8 +519,8 @@ export default function MediaUpdate() {
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{bookmarkedIds.has(Number(item.id))
|
{bookmarkedIds.has(Number(item.id))
|
||||||
? "Tersimpan"
|
? t("saved")
|
||||||
: "Simpan"}
|
: t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -536,7 +538,7 @@ export default function MediaUpdate() {
|
||||||
size={"lg"}
|
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"
|
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>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -11,22 +11,11 @@ import { Input } from "../ui/input";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { Link } from "@/i18n/routing";
|
import { Link } from "@/i18n/routing";
|
||||||
import { DynamicLogoTenant } from "./dynamic-logo-tenant";
|
import { DynamicLogoTenant } from "./dynamic-logo-tenant";
|
||||||
|
import { useTranslations } from "next-intl";
|
||||||
const NAV_ITEMS = [
|
import LocalSwitcher from "../partials/header/locale-switcher";
|
||||||
{ 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" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function Navbar() {
|
export default function Navbar() {
|
||||||
|
const t = useTranslations("Navbar");
|
||||||
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
|
||||||
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
const [isDropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
const [showProfileMenu, setShowProfileMenu] = useState(false);
|
||||||
|
|
@ -34,6 +23,20 @@ export default function Navbar() {
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
const router = useRouter();
|
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
|
// 🔍 Fungsi cek login
|
||||||
const checkLoginStatus = () => {
|
const checkLoginStatus = () => {
|
||||||
const roleId = getCookiesDecrypt("urie");
|
const roleId = getCookiesDecrypt("urie");
|
||||||
|
|
@ -46,7 +49,7 @@ export default function Navbar() {
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const filteredNavItems = isLoggedIn
|
const filteredNavItems = isLoggedIn
|
||||||
? NAV_ITEMS.filter((item) => item.label !== "Mengikuti")
|
? NAV_ITEMS.filter((item) => item.label !== t("following"))
|
||||||
: NAV_ITEMS;
|
: NAV_ITEMS;
|
||||||
|
|
||||||
// 🚪 Fungsi logout
|
// 🚪 Fungsi logout
|
||||||
|
|
@ -73,18 +76,20 @@ export default function Navbar() {
|
||||||
return (
|
return (
|
||||||
<header className="relative max-w-[1400px] mx-auto flex items-center justify-between px-4 py-3 border-b bg-white z-50">
|
<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="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
|
<Image
|
||||||
src="/assets/logo1.png"
|
src="/assets/logo1.png"
|
||||||
alt="Logo"
|
alt="Logo"
|
||||||
fill
|
fill
|
||||||
className="object-contain"
|
className="object-contain"
|
||||||
/>
|
/>
|
||||||
</div>
|
</Link>
|
||||||
<Menu
|
|
||||||
className="w-6 h-6 cursor-pointer"
|
|
||||||
onClick={() => setIsSidebarOpen(true)}
|
|
||||||
/>
|
|
||||||
<DynamicLogoTenant />
|
<DynamicLogoTenant />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -95,7 +100,7 @@ export default function Navbar() {
|
||||||
|
|
||||||
// 🔹 Pengecekan khusus untuk "Untuk Anda"
|
// 🔹 Pengecekan khusus untuk "Untuk Anda"
|
||||||
const handleClick = (e: React.MouseEvent) => {
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
if (item.label === "Untuk Anda") {
|
if (item.label === t("forYou")) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
if (!checkLoginStatus()) {
|
if (!checkLoginStatus()) {
|
||||||
router.push("/auth");
|
router.push("/auth");
|
||||||
|
|
@ -107,7 +112,7 @@ export default function Navbar() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div key={item.label} className="relative">
|
<div key={item.label} className="relative">
|
||||||
{item.label === "Publikasi" ? (
|
{item.label === t("publication") ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<button
|
||||||
onClick={() => setDropdownOpen(!isDropdownOpen)}
|
onClick={() => setDropdownOpen(!isDropdownOpen)}
|
||||||
|
|
@ -171,11 +176,11 @@ export default function Navbar() {
|
||||||
<>
|
<>
|
||||||
<Link href="/auth/register">
|
<Link href="/auth/register">
|
||||||
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white">
|
<Button className="bg-transparent border text-black hover:bg-red-600 hover:text-white">
|
||||||
Daftar
|
{t("register")}{" "}
|
||||||
</Button>
|
</Button>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/auth">
|
<Link href="/auth">
|
||||||
<Button className="bg-red-700 text-white">Masuk</Button>
|
<Button className="bg-red-700 text-white">{t("login")}</Button>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -205,7 +210,7 @@ export default function Navbar() {
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-gray-700"
|
className="w-full text-left px-4 py-2 text-sm hover:bg-gray-100 text-gray-700"
|
||||||
>
|
>
|
||||||
Logout
|
{t("logout")}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
@ -226,9 +231,13 @@ export default function Navbar() {
|
||||||
|
|
||||||
<div className="mt-10">
|
<div className="mt-10">
|
||||||
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
||||||
Bahasa
|
{t("language")}
|
||||||
</h3>
|
</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">
|
<button className="flex items-center gap-2 text-sm text-gray-800">
|
||||||
<Image src={"/Flag.svg"} width={24} height={24} alt="flag" />
|
<Image src={"/Flag.svg"} width={24} height={24} alt="flag" />
|
||||||
English
|
English
|
||||||
|
|
@ -251,19 +260,19 @@ export default function Navbar() {
|
||||||
</svg>{" "}
|
</svg>{" "}
|
||||||
Bahasa Indonesia
|
Bahasa Indonesia
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
<h3 className="text-[16px] font-bold text-gray-700 mb-2">
|
||||||
Fitur
|
{t("features")}
|
||||||
</h3>
|
</h3>
|
||||||
<div className="space-y-5 ml-3">
|
<div className="space-y-5 ml-3">
|
||||||
{NAV_ITEMS.map((item) => (
|
{NAV_ITEMS.map((item) => (
|
||||||
<button
|
<button
|
||||||
key={item.label}
|
key={item.label}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (item.label === "Untuk Anda") {
|
if (item.label === t("forYou")) {
|
||||||
if (!checkLoginStatus()) {
|
if (!checkLoginStatus()) {
|
||||||
router.push("/auth");
|
router.push("/auth");
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -284,25 +293,25 @@ export default function Navbar() {
|
||||||
|
|
||||||
<div className="space-y-5 text-[16px] font-bold">
|
<div className="space-y-5 text-[16px] font-bold">
|
||||||
<Link href="/about" className="block text-black">
|
<Link href="/about" className="block text-black">
|
||||||
Tentang Kami
|
{t("about")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/advertising" className="block text-black">
|
<Link href="/advertising" className="block text-black">
|
||||||
Advertising
|
{t("advertising")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link href="/contact" className="block text-black">
|
<Link href="/contact" className="block text-black">
|
||||||
Kontak Kami
|
{t("contact")}
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{!isLoggedIn ? (
|
{!isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
<Link href="/auth" className="block text-lg text-gray-800">
|
<Link href="/auth" className="block text-lg text-gray-800">
|
||||||
Login
|
{t("login")}
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
href="/auth/register"
|
href="/auth/register"
|
||||||
className="block text-lg text-gray-800"
|
className="block text-lg text-gray-800"
|
||||||
>
|
>
|
||||||
Daftar
|
{t("register")}
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
|
|
@ -310,17 +319,17 @@ export default function Navbar() {
|
||||||
onClick={handleLogout}
|
onClick={handleLogout}
|
||||||
className="block text-left w-full text-lg text-red-600 hover:underline"
|
className="block text-left w-full text-lg text-red-600 hover:underline"
|
||||||
>
|
>
|
||||||
Logout
|
{t("logout")}
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Card className="rounded-none p-4">
|
<Card className="rounded-none p-4">
|
||||||
<h2 className="text-[#C6A455] text-center text-lg font-semibold">
|
<h2 className="text-[#C6A455] text-center text-lg font-semibold mb-2">
|
||||||
Subscribe to Our Newsletter
|
{t("subscribeTitle")}
|
||||||
</h2>
|
</h2>
|
||||||
<Input type="email" placeholder="Your email address" />
|
<Input type="email" placeholder={t("subscribePlaceholder")} />
|
||||||
<Button className="bg-[#C6A455]">Subscribe</Button>
|
<Button className="bg-[#C6A455] mt-2">{t("subscribeButton")}</Button>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Calendar } from "@/components/ui/calendar";
|
import { Calendar } from "@/components/ui/calendar";
|
||||||
import {
|
import {
|
||||||
|
|
@ -20,131 +20,17 @@ import {
|
||||||
SelectItem,
|
SelectItem,
|
||||||
} from "@/components/ui/select";
|
} from "@/components/ui/select";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
|
import { getAllSchedules } from "@/service/landing/landing";
|
||||||
|
|
||||||
const scheduleData = [
|
export default function Schedule() {
|
||||||
{
|
const [search, setSearch] = useState("");
|
||||||
date: "Jul 1 2025",
|
const [startDate, setStartDate] = useState<Date | undefined>(new Date());
|
||||||
type: "POLRI",
|
const [endDate, setEndDate] = useState<Date | undefined>(new Date());
|
||||||
title: "HUT Bhayangkara RI - 79",
|
const [selectedCategory, setSelectedCategory] = useState("SEMUA");
|
||||||
location: "Mabes Polri, Jakarta, Indonesia",
|
const [scheduleData, setScheduleData] = useState<any[]>([]);
|
||||||
},
|
const [loading, setLoading] = useState(false);
|
||||||
{
|
|
||||||
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 = [
|
const categories = [
|
||||||
"SEMUA",
|
"SEMUA",
|
||||||
"POLRI",
|
"POLRI",
|
||||||
"MAHKAMAH AGUNG",
|
"MAHKAMAH AGUNG",
|
||||||
|
|
@ -156,26 +42,61 @@ const categories = [
|
||||||
"BSKDN",
|
"BSKDN",
|
||||||
"BUMN",
|
"BUMN",
|
||||||
"KPU",
|
"KPU",
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function Schedule() {
|
const fetchSchedules = async () => {
|
||||||
const [search, setSearch] = useState("");
|
try {
|
||||||
const [startDate, setStartDate] = useState<Date | undefined>(
|
setLoading(true);
|
||||||
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 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 =
|
const matchesCategory =
|
||||||
selectedCategory === "SEMUA" || item.type === selectedCategory;
|
selectedCategory === "SEMUA" ||
|
||||||
|
item.type?.toUpperCase() === selectedCategory;
|
||||||
const matchesSearch = item.title
|
const matchesSearch = item.title
|
||||||
.toLowerCase()
|
?.toLowerCase()
|
||||||
.includes(search.toLowerCase());
|
.includes(search.toLowerCase());
|
||||||
return matchesCategory && matchesSearch;
|
return matchesCategory && matchesSearch;
|
||||||
});
|
})
|
||||||
|
: [];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 max-w-[1350px] mx-auto">
|
<div className="p-6 max-w-[1350px] mx-auto">
|
||||||
|
|
@ -276,6 +197,16 @@ export default function Schedule() {
|
||||||
<h2 className="text-md font-semibold text-muted-foreground mb-4">
|
<h2 className="text-md font-semibold text-muted-foreground mb-4">
|
||||||
Semua Jadwal
|
Semua Jadwal
|
||||||
</h2>
|
</h2>
|
||||||
|
|
||||||
|
{loading ? (
|
||||||
|
<div className="text-center text-muted-foreground py-10">
|
||||||
|
Memuat data jadwal...
|
||||||
|
</div>
|
||||||
|
) : filteredData.length === 0 ? (
|
||||||
|
<div className="text-center text-muted-foreground py-10">
|
||||||
|
Tidak ada jadwal ditemukan.
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<div className="flex flex-col gap-3">
|
<div className="flex flex-col gap-3">
|
||||||
{filteredData.map((item, index) => (
|
{filteredData.map((item, index) => (
|
||||||
<div
|
<div
|
||||||
|
|
@ -283,7 +214,9 @@ export default function Schedule() {
|
||||||
className="flex justify-between items-start border-b pb-2"
|
className="flex justify-between items-start border-b pb-2"
|
||||||
>
|
>
|
||||||
<div className="w-1/6 text-sm text-muted-foreground">
|
<div className="w-1/6 text-sm text-muted-foreground">
|
||||||
{item.date}
|
{item.startDate
|
||||||
|
? format(new Date(item.startDate), "dd MMM yyyy")
|
||||||
|
: "-"}
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/5">
|
<div className="w-1/5">
|
||||||
<Badge
|
<Badge
|
||||||
|
|
@ -297,7 +230,7 @@ export default function Schedule() {
|
||||||
"bg-red-700": item.type === "KPK",
|
"bg-red-700": item.type === "KPK",
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{item.type}
|
{item.type || "-"}
|
||||||
</Badge>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-2/5 text-sm">{item.title}</div>
|
<div className="w-2/5 text-sm">{item.title}</div>
|
||||||
|
|
@ -307,6 +240,8 @@ export default function Schedule() {
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<div className="flex justify-between mt-6 text-sm text-blue-600">
|
<div className="flex justify-between mt-6 text-sm text-blue-600">
|
||||||
<button>Preview</button>
|
<button>Preview</button>
|
||||||
<button>Next</button>
|
<button>Next</button>
|
||||||
|
|
@ -314,3 +249,320 @@ export default function Schedule() {
|
||||||
</div>
|
</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>
|
<span>SHARE</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<Link href={`/content/video/comment/${id}`}>
|
<Link href={`/content/image/comment/${id}`}>
|
||||||
<Button
|
<Button
|
||||||
variant="default"
|
variant="default"
|
||||||
size="lg"
|
size="lg"
|
||||||
|
|
|
||||||
|
|
@ -37,6 +37,7 @@ export default function PublicationKlLayout() {
|
||||||
{/* Grid Konten */}
|
{/* Grid Konten */}
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<ForYouCardGrid
|
<ForYouCardGrid
|
||||||
|
key={`${activeTab}-${selectedCategory}`}
|
||||||
selectedCategory={selectedCategory}
|
selectedCategory={selectedCategory}
|
||||||
filterType={activeTab}
|
filterType={activeTab}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,20 @@ import { useState, useEffect } from "react";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Button } from "@/components/ui/button";
|
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 Swal from "sweetalert2";
|
||||||
import withReactContent from "sweetalert2-react-content";
|
import withReactContent from "sweetalert2-react-content";
|
||||||
import { getBookmarks } from "@/service/content";
|
import { getBookmarks, BookmarkItem } from "@/service/content";
|
||||||
import { BookmarkItem } from "@/service/content";
|
import {
|
||||||
import { Swiper, SwiperSlide } from "swiper/react";
|
Pagination,
|
||||||
import "swiper/css";
|
PaginationContent,
|
||||||
import "swiper/css/navigation";
|
PaginationItem,
|
||||||
import { Navigation } from "swiper/modules";
|
PaginationLink,
|
||||||
|
PaginationNext,
|
||||||
|
PaginationPrevious,
|
||||||
|
} from "@/components/ui/pagination";
|
||||||
|
|
||||||
// 🔹 Format tanggal WIB
|
|
||||||
function formatTanggal(dateString: string) {
|
function formatTanggal(dateString: string) {
|
||||||
if (!dateString) return "";
|
if (!dateString) return "";
|
||||||
return (
|
return (
|
||||||
|
|
@ -32,7 +35,6 @@ function formatTanggal(dateString: string) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Link detail konten
|
|
||||||
function getLink(item: BookmarkItem) {
|
function getLink(item: BookmarkItem) {
|
||||||
switch (item.article?.typeId) {
|
switch (item.article?.typeId) {
|
||||||
case 1:
|
case 1:
|
||||||
|
|
@ -50,7 +52,7 @@ function getLink(item: BookmarkItem) {
|
||||||
|
|
||||||
type ForYouCardGridProps = {
|
type ForYouCardGridProps = {
|
||||||
filterType: "My Collections" | "Archives" | "Favorites";
|
filterType: "My Collections" | "Archives" | "Favorites";
|
||||||
selectedCategory?: string;
|
selectedCategory?: string; // ✅ tambahkan ini
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
|
export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
|
||||||
|
|
@ -59,18 +61,38 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
|
||||||
const [contentType, setContentType] = useState<
|
const [contentType, setContentType] = useState<
|
||||||
"image" | "video" | "text" | "audio"
|
"image" | "video" | "text" | "audio"
|
||||||
>("image");
|
>("image");
|
||||||
|
const [currentPage, setCurrentPage] = useState(1);
|
||||||
|
const [totalPages, setTotalPages] = useState(1);
|
||||||
|
const [totalData, setTotalData] = useState(0);
|
||||||
|
const itemsPerPage = 6;
|
||||||
const MySwal = withReactContent(Swal);
|
const MySwal = withReactContent(Swal);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchBookmarks();
|
fetchBookmarks();
|
||||||
}, [filterType]);
|
}, [filterType, currentPage]);
|
||||||
|
|
||||||
const fetchBookmarks = async () => {
|
const fetchBookmarks = async () => {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const response = await getBookmarks(1, 50, filterType);
|
const response = await getBookmarks(
|
||||||
|
currentPage,
|
||||||
|
itemsPerPage,
|
||||||
|
filterType,
|
||||||
|
);
|
||||||
if (!response?.error) {
|
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 {
|
} else {
|
||||||
MySwal.fire("Error", "Gagal memuat bookmark.", "error");
|
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) => {
|
const handleAction = (action: string, title: string) => {
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
icon: "info",
|
icon: "info",
|
||||||
|
|
@ -114,77 +118,74 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
|
||||||
const title = bookmark.article?.title || "Konten";
|
const title = bookmark.article?.title || "Konten";
|
||||||
|
|
||||||
if (filterType === "Archives") {
|
if (filterType === "Archives") {
|
||||||
// 🗃️ Unarchive + Delete
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 mt-3 justify-center">
|
<div className="flex gap-2 mt-3 justify-center">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAction("Unarchive", title)}
|
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]" />
|
<Undo2 className="w-[10px] h-[10px]" />
|
||||||
<p className="text-[10px]">Unarchive</p>
|
Unarchive
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAction("Delete", title)}
|
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]" />
|
<Trash2 className="w-[10px] h-[10px]" />
|
||||||
<p className="text-[10px]">Delete</p>
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filterType === "Favorites") {
|
if (filterType === "Favorites") {
|
||||||
// 📦 Archive + 💔 Unfavorite
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 mt-3 justify-center">
|
<div className="flex gap-2 mt-3 justify-center">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAction("Archive", title)}
|
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]" />
|
<Archive className="w-[10px] h-[10px]" />
|
||||||
<p className="text-[10px]">Archive</p>
|
Archive
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAction("Unfavorite", title)}
|
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]" />
|
<HeartOff className="w-[10px] h-[10px]" />
|
||||||
<p className="text-[10px]">Unfavorite</p>
|
Unfavorite
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default: My Collections → Archive + Favorite
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 mt-3 justify-center">
|
<div className="flex gap-2 mt-3 justify-center">
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAction("Archive", title)}
|
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]" />
|
<Archive className="w-[10px] h-[10px]" />
|
||||||
<p className="text-[10px]">Archive</p>
|
Archive
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => handleAction("Favorite", title)}
|
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]" />
|
<Star className="w-[10px] h-[10px]" />
|
||||||
<p className="text-[10px]">Favorite</p>
|
Favorite
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -252,31 +253,36 @@ export default function ForYouCardGrid({ filterType }: ForYouCardGridProps) {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading)
|
// Filter sesuai tab
|
||||||
return (
|
const filtered = bookmarks.filter((b) => {
|
||||||
<div className="text-center py-12 text-gray-500">Memuat konten...</div>
|
const t = b.article?.typeId;
|
||||||
);
|
switch (contentType) {
|
||||||
|
case "image":
|
||||||
if (filtered.length === 0)
|
return t === 1;
|
||||||
return (
|
case "video":
|
||||||
<div className="text-center py-12">
|
return t === 2;
|
||||||
<div className="text-gray-400 text-6xl mb-4">📂</div>
|
case "text":
|
||||||
<h3 className="text-xl font-semibold text-gray-600 mb-2">
|
return t === 3;
|
||||||
Tidak Ada Konten di {filterType}
|
case "audio":
|
||||||
</h3>
|
return t === 4;
|
||||||
</div>
|
default:
|
||||||
);
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="bg-white px-4 py-6 border rounded-md border-[#CDD5DF]">
|
<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="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="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">
|
<div className="flex flex-wrap justify-center gap-2">
|
||||||
{["image", "video", "audio", "text"].map((type) => (
|
{["image", "video", "audio", "text"].map((type) => (
|
||||||
<button
|
<button
|
||||||
key={type}
|
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 ${
|
className={`px-4 py-2 rounded-full text-xs font-semibold transition-all duration-300 transform hover:scale-105 ${
|
||||||
contentType === type
|
contentType === type
|
||||||
? "bg-gradient-to-r from-orange-500 to-red-600 text-white shadow-lg ring-2 ring-orange-300"
|
? "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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Slider Konten */}
|
{/* Grid + Pagination */}
|
||||||
<Swiper
|
{loading ? (
|
||||||
modules={[Navigation]}
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
|
||||||
navigation
|
{[...Array(6)].map((_, i) => (
|
||||||
spaceBetween={20}
|
<Card key={i} className="p-4 animate-pulse">
|
||||||
slidesPerView={1.2}
|
<div className="w-full h-48 bg-gray-200 rounded-lg mb-4"></div>
|
||||||
breakpoints={{
|
<div className="space-y-2">
|
||||||
640: { slidesPerView: 2.2 },
|
<div className="h-4 bg-gray-200 rounded w-3/4"></div>
|
||||||
1024: { slidesPerView: 3.2 },
|
<div className="h-4 bg-gray-200 rounded w-1/2"></div>
|
||||||
1280: { slidesPerView: 4.2 },
|
</div>
|
||||||
}}
|
</Card>
|
||||||
>
|
|
||||||
{filtered.map((bookmark) => (
|
|
||||||
<SwiperSlide key={bookmark.id}>{renderCard(bookmark)}</SwiperSlide>
|
|
||||||
))}
|
))}
|
||||||
</Swiper>
|
</div>
|
||||||
|
) : filtered.length > 0 ? (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6 mb-6">
|
||||||
|
{filtered.map((b) => renderCard(b))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{totalPages > 1 && (
|
||||||
|
<div className="flex flex-col items-center space-y-2">
|
||||||
|
<Pagination>
|
||||||
|
<PaginationContent>
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationPrevious
|
||||||
|
onClick={() => setCurrentPage((p) => Math.max(1, p - 1))}
|
||||||
|
className={
|
||||||
|
currentPage === 1
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
|
||||||
|
{Array.from({ length: totalPages }, (_, i) => i + 1).map(
|
||||||
|
(n) => (
|
||||||
|
<PaginationItem key={n}>
|
||||||
|
<PaginationLink
|
||||||
|
onClick={() => setCurrentPage(n)}
|
||||||
|
isActive={currentPage === n}
|
||||||
|
>
|
||||||
|
{n}
|
||||||
|
</PaginationLink>
|
||||||
|
</PaginationItem>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
|
||||||
|
<PaginationItem>
|
||||||
|
<PaginationNext
|
||||||
|
onClick={() =>
|
||||||
|
setCurrentPage((p) => Math.min(totalPages, p + 1))
|
||||||
|
}
|
||||||
|
className={
|
||||||
|
currentPage === totalPages
|
||||||
|
? "pointer-events-none opacity-50"
|
||||||
|
: "cursor-pointer"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</PaginationItem>
|
||||||
|
</PaginationContent>
|
||||||
|
</Pagination>
|
||||||
|
|
||||||
|
<p className="text-xs text-gray-500">
|
||||||
|
Menampilkan halaman {currentPage} dari {totalPages} • Total{" "}
|
||||||
|
{totalData} konten
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Card className="p-8 text-center">
|
||||||
|
<p className="text-gray-600 text-sm">Tidak ada bookmark ditemukan.</p>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// "use client";
|
// "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 { useEffect, useState } from "react";
|
||||||
// import PublicationKlFilter from "./publication-filter";
|
// import PublicationKlFilter from "./publication-filter";
|
||||||
// import { Button } from "@/components/ui/button";
|
// import { Button } from "@/components/ui/button";
|
||||||
|
|
|
||||||
|
|
@ -1,74 +1,229 @@
|
||||||
"use client";
|
"use client";
|
||||||
import React, { useState } from "react";
|
|
||||||
|
|
||||||
import { Label } from "@/components/ui/label";
|
import { useState } from "react";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Button } from "@/components/ui/button";
|
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 Swal from "sweetalert2";
|
||||||
import { useRouter } from "@/i18n/routing";
|
import withReactContent from "sweetalert2-react-content";
|
||||||
import { forgotPassword } from "@/service/landing/landing";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { forgotPassword } from "@/service/auth";
|
||||||
|
|
||||||
const ForgotPass = () => {
|
const MySwal = withReactContent(Swal);
|
||||||
const [username, setUsername] = useState<any>();
|
|
||||||
const MySwal = withReactContent(Swal);
|
export default function ForgotPass() {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const {
|
const handleForgot = async (e: React.FormEvent) => {
|
||||||
register,
|
e.preventDefault();
|
||||||
handleSubmit,
|
|
||||||
watch,
|
|
||||||
formState: { errors },
|
|
||||||
} = useForm<Inputs>();
|
|
||||||
const onSubmit: SubmitHandler<Inputs> = (data) => console.log(data);
|
|
||||||
|
|
||||||
async function handleCheckUsername() {
|
if (!username.trim()) {
|
||||||
loading();
|
|
||||||
const response = await forgotPassword(username);
|
|
||||||
|
|
||||||
if (response.error) {
|
|
||||||
error(response.message);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
successSubmit();
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function successSubmit() {
|
|
||||||
MySwal.fire({
|
MySwal.fire({
|
||||||
title: "Email berhasil dikirim. Silahkan cek email Anda.",
|
icon: "warning",
|
||||||
icon: "success",
|
title: "Oops...",
|
||||||
confirmButtonColor: "#3085d6",
|
text: "Username tidak boleh kosong!",
|
||||||
confirmButtonText: "OK",
|
});
|
||||||
}).then((result: any) => {
|
return;
|
||||||
if (result.isConfirmed) {
|
|
||||||
router.push("/admin");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 (
|
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">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="user-name">Username</Label>
|
<label
|
||||||
<Input id="user-name" defaultValue="akun1234" className="h-[48px] text-sm text-default-900 " onChange={(e) => setUsername(e.target.value)} />
|
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>
|
</div>
|
||||||
|
|
||||||
<Button type="submit" fullWidth onClick={handleCheckUsername}>
|
<Button
|
||||||
Check Username
|
type="submit"
|
||||||
</Button>
|
disabled={loading}
|
||||||
<Button type="submit" fullWidth onClick={handleCheckUsername}>
|
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 ${
|
||||||
Kirim Ulang?{" "}
|
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>
|
</Button>
|
||||||
</form>
|
</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>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
|
||||||
50
lib/menus.ts
50
lib/menus.ts
|
|
@ -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: "",
|
groupLabel: "",
|
||||||
id: "settings",
|
id: "settings",
|
||||||
|
|
|
||||||
118
messages/en.json
118
messages/en.json
|
|
@ -865,5 +865,123 @@
|
||||||
"searchTitle": "Find Title...",
|
"searchTitle": "Find Title...",
|
||||||
"selectFilter": "Select a Filter",
|
"selectFilter": "Select a Filter",
|
||||||
"noResult": "No results."
|
"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...",
|
"searchTitle": "Cari Judul...",
|
||||||
"selectFilter": "Pilih Filter",
|
"selectFilter": "Pilih Filter",
|
||||||
"noResult": "Tidak ada hasil"
|
"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);
|
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);
|
return httpPostInterceptor(url, formData, headers);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -582,3 +582,21 @@ export async function getArticleDetail(id: number) {
|
||||||
const url = `articles/${id}`;
|
const url = `articles/${id}`;
|
||||||
return await httpGetInterceptor(url);
|
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,
|
httpGetInterceptor,
|
||||||
httpGetInterceptorForMetadata,
|
httpGetInterceptorForMetadata,
|
||||||
httpPostInterceptor,
|
httpPostInterceptor,
|
||||||
|
httpPutInterceptor,
|
||||||
} from "../http-config/http-interceptor-service";
|
} from "../http-config/http-interceptor-service";
|
||||||
|
|
||||||
export async function getCsrfToken() {
|
export async function getCsrfToken() {
|
||||||
|
|
@ -116,7 +117,6 @@ export async function getDetail(slug: string) {
|
||||||
return await httpGetInterceptor(`media/public?slug=${slug}&state=mabes`);
|
return await httpGetInterceptor(`media/public?slug=${slug}&state=mabes`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function getDetailMetaData(slug: string) {
|
export async function getDetailMetaData(slug: string) {
|
||||||
return await httpGetInterceptorForMetadata(
|
return await httpGetInterceptorForMetadata(
|
||||||
`media/public?slug=${slug}&state=mabes&isSummary=true`
|
`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(
|
export async function getBookmarksForUser(
|
||||||
page = 1,
|
page = 1,
|
||||||
limit = 10,
|
limit = 10,
|
||||||
|
|
@ -382,3 +419,24 @@ export async function enableListCategory() {
|
||||||
const url = `media/categories/list/publish?enablePage=1`;
|
const url = `media/categories/list/publish?enablePage=1`;
|
||||||
return httpPostInterceptor(url);
|
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