Compare commits
7 Commits
nico/2-des
...
nico/28-no
| Author | SHA1 | Date | |
|---|---|---|---|
| b2066caa13 | |||
| 9bf3ec72cf | |||
| 1c1e8fb190 | |||
| 54f83da3b8 | |||
| f8985c550f | |||
| e3d909e760 | |||
| 16a8df50c1 |
@@ -136,7 +136,6 @@ model MediaSosial {
|
|||||||
name String
|
name String
|
||||||
image FileStorage? @relation(fields: [imageId], references: [id])
|
image FileStorage? @relation(fields: [imageId], references: [id])
|
||||||
imageId String?
|
imageId String?
|
||||||
icon String?
|
|
||||||
iconUrl String? @db.VarChar(255)
|
iconUrl String? @db.VarChar(255)
|
||||||
createdAt DateTime @default(now())
|
createdAt DateTime @default(now())
|
||||||
updatedAt DateTime @updatedAt
|
updatedAt DateTime @updatedAt
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Box, Image, Select, rem } from '@mantine/core';
|
|
||||||
|
|
||||||
const sosmedMap = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
type SosmedKey = keyof typeof sosmedMap;
|
|
||||||
|
|
||||||
const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({
|
|
||||||
value,
|
|
||||||
label: item.label,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SelectSosialMedia({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: SosmedKey;
|
|
||||||
onChange: (value: SosmedKey) => void;
|
|
||||||
}) {
|
|
||||||
const selected = value;
|
|
||||||
const selectedImage = sosmedMap[selected]?.src;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box maw={300}>
|
|
||||||
<Select
|
|
||||||
placeholder="Pilih sosial media"
|
|
||||||
value={selected}
|
|
||||||
data={sosmedList}
|
|
||||||
searchable={false}
|
|
||||||
withCheckIcon={false}
|
|
||||||
onChange={(val) => val && onChange(val as SosmedKey)}
|
|
||||||
styles={{
|
|
||||||
input: {
|
|
||||||
textAlign: 'left',
|
|
||||||
fontSize: rem(16),
|
|
||||||
paddingLeft: 36,
|
|
||||||
},
|
|
||||||
section: {
|
|
||||||
left: 10,
|
|
||||||
right: 'auto',
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 🔥 PREVIEW DIPISAH DI LUAR SELECT */}
|
|
||||||
{selectedImage && (
|
|
||||||
<Box mt="md">
|
|
||||||
<Image
|
|
||||||
alt=""
|
|
||||||
src={selectedImage}
|
|
||||||
radius="md"
|
|
||||||
style={{
|
|
||||||
width: 120,
|
|
||||||
height: 120,
|
|
||||||
objectFit: 'contain',
|
|
||||||
border: '1px solid #eee',
|
|
||||||
padding: 8,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { Box, Select } from '@mantine/core';
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
export const sosmedMap = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
type SosmedKey = keyof typeof sosmedMap;
|
|
||||||
|
|
||||||
const sosmedList = Object.entries(sosmedMap).map(([value, item]) => ({
|
|
||||||
value,
|
|
||||||
label: item.label,
|
|
||||||
}));
|
|
||||||
|
|
||||||
export default function SelectSocialMediaEdit({
|
|
||||||
value,
|
|
||||||
onChange,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
onChange: (val: SosmedKey) => void;
|
|
||||||
}) {
|
|
||||||
const [selected, setSelected] = useState<SosmedKey>('facebook');
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (value && sosmedMap[value as SosmedKey]) {
|
|
||||||
setSelected(value as SosmedKey);
|
|
||||||
}
|
|
||||||
}, [value]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box>
|
|
||||||
<Select
|
|
||||||
label="Jenis Media Sosial"
|
|
||||||
value={selected}
|
|
||||||
data={sosmedList}
|
|
||||||
searchable={false}
|
|
||||||
onChange={(val) => {
|
|
||||||
if (!val) return;
|
|
||||||
setSelected(val as SosmedKey);
|
|
||||||
onChange(val as SosmedKey);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -27,7 +27,7 @@ const programInovasi = proxy({
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
imageId: "",
|
imageId: "",
|
||||||
link: "",
|
link: ""
|
||||||
} as ProgramInovasiForm,
|
} as ProgramInovasiForm,
|
||||||
loading: false,
|
loading: false,
|
||||||
async create() {
|
async create() {
|
||||||
@@ -71,8 +71,7 @@ const programInovasi = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => {
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
// Change to arrow function
|
|
||||||
programInovasi.findMany.loading = true; // Use the full path to access the property
|
programInovasi.findMany.loading = true; // Use the full path to access the property
|
||||||
programInovasi.findMany.page = page;
|
programInovasi.findMany.page = page;
|
||||||
programInovasi.findMany.search = search;
|
programInovasi.findMany.search = search;
|
||||||
@@ -83,7 +82,7 @@ const programInovasi = proxy({
|
|||||||
const res = await ApiFetch.api.landingpage.programinovasi[
|
const res = await ApiFetch.api.landingpage.programinovasi[
|
||||||
"findMany"
|
"findMany"
|
||||||
].get({
|
].get({
|
||||||
query,
|
query
|
||||||
});
|
});
|
||||||
|
|
||||||
if (res.status === 200 && res.data?.success) {
|
if (res.status === 200 && res.data?.success) {
|
||||||
@@ -390,10 +389,7 @@ const pejabatDesa = proxy({
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Ensure ID is properly encoded in the URL
|
// Ensure ID is properly encoded in the URL
|
||||||
const url = new URL(
|
const url = new URL(`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`, window.location.origin);
|
||||||
`/api/landingpage/pejabatdesa/${encodeURIComponent(this.id)}`,
|
|
||||||
window.location.origin
|
|
||||||
);
|
|
||||||
const response = await fetch(url.toString(), {
|
const response = await fetch(url.toString(), {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
@@ -442,19 +438,16 @@ const pejabatDesa = proxy({
|
|||||||
|
|
||||||
const templateMediaSosial = z.object({
|
const templateMediaSosial = z.object({
|
||||||
name: z.string().min(3, "Nama minimal 3 karakter"),
|
name: z.string().min(3, "Nama minimal 3 karakter"),
|
||||||
imageId: z.string().nullable().optional(),
|
imageId: z.string().min(1, "Gambar wajib dipilih"),
|
||||||
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
iconUrl: z.string().min(3, "Icon URL minimal 3 karakter"),
|
||||||
icon: z.string().nullable().optional(),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type MediaSosialForm = {
|
type MediaSosialForm = {
|
||||||
name: string;
|
name: string;
|
||||||
imageId: string | null; // boleh null
|
imageId: string;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
icon: string | null; // boleh null
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
const mediaSosial = proxy({
|
const mediaSosial = proxy({
|
||||||
create: {
|
create: {
|
||||||
form: {} as MediaSosialForm,
|
form: {} as MediaSosialForm,
|
||||||
@@ -462,10 +455,9 @@ const mediaSosial = proxy({
|
|||||||
async create() {
|
async create() {
|
||||||
// Ensure all required fields are non-null
|
// Ensure all required fields are non-null
|
||||||
const formData = {
|
const formData = {
|
||||||
name: mediaSosial.create.form.name ?? "",
|
name: mediaSosial.create.form.name || "",
|
||||||
imageId: mediaSosial.create.form.imageId ?? null, // FIXED
|
imageId: mediaSosial.create.form.imageId || "",
|
||||||
iconUrl: mediaSosial.create.form.iconUrl ?? "",
|
iconUrl: mediaSosial.create.form.iconUrl || "",
|
||||||
icon: mediaSosial.create.form.icon ?? null, // FIXED
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const cek = templateMediaSosial.safeParse(formData);
|
const cek = templateMediaSosial.safeParse(formData);
|
||||||
@@ -500,8 +492,7 @@ const mediaSosial = proxy({
|
|||||||
total: 0,
|
total: 0,
|
||||||
loading: false,
|
loading: false,
|
||||||
search: "",
|
search: "",
|
||||||
load: async (page = 1, limit = 10, search = "") => {
|
load: async (page = 1, limit = 10, search = "") => { // Change to arrow function
|
||||||
// Change to arrow function
|
|
||||||
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
mediaSosial.findMany.loading = true; // Use the full path to access the property
|
||||||
mediaSosial.findMany.page = page;
|
mediaSosial.findMany.page = page;
|
||||||
mediaSosial.findMany.search = search;
|
mediaSosial.findMany.search = search;
|
||||||
@@ -509,7 +500,9 @@ const mediaSosial = proxy({
|
|||||||
const query: any = { page, limit };
|
const query: any = { page, limit };
|
||||||
if (search) query.search = search;
|
if (search) query.search = search;
|
||||||
|
|
||||||
const res = await ApiFetch.api.landingpage.mediasosial["findMany"].get({
|
const res = await ApiFetch.api.landingpage.mediasosial[
|
||||||
|
"findMany"
|
||||||
|
].get({
|
||||||
query,
|
query,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -624,16 +617,12 @@ const mediaSosial = proxy({
|
|||||||
this.id = data.id;
|
this.id = data.id;
|
||||||
this.form = {
|
this.form = {
|
||||||
name: data.name || "",
|
name: data.name || "",
|
||||||
imageId: data.imageId || null,
|
imageId: data.imageId || "",
|
||||||
iconUrl: data.iconUrl || "",
|
iconUrl: data.iconUrl || "",
|
||||||
icon: data.icon || null,
|
|
||||||
|
|
||||||
};
|
};
|
||||||
return data;
|
return data;
|
||||||
} else {
|
} else {
|
||||||
throw new Error(
|
throw new Error(result?.message || "Gagal mengambil data media sosial");
|
||||||
result?.message || "Gagal mengambil data media sosial"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error((error as Error).message);
|
console.error((error as Error).message);
|
||||||
@@ -656,9 +645,7 @@ const mediaSosial = proxy({
|
|||||||
try {
|
try {
|
||||||
mediaSosial.update.loading = true;
|
mediaSosial.update.loading = true;
|
||||||
|
|
||||||
const response = await fetch(
|
const response = await fetch(`/api/landingpage/mediasosial/${this.id}`, {
|
||||||
`/api/landingpage/mediasosial/${this.id}`,
|
|
||||||
{
|
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
@@ -667,10 +654,8 @@ const mediaSosial = proxy({
|
|||||||
name: this.form.name,
|
name: this.form.name,
|
||||||
imageId: this.form.imageId,
|
imageId: this.form.imageId,
|
||||||
iconUrl: this.form.iconUrl,
|
iconUrl: this.form.iconUrl,
|
||||||
icon: this.form.icon,
|
|
||||||
}),
|
}),
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorData = await response.json().catch(() => ({}));
|
const errorData = await response.json().catch(() => ({}));
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ export default function Registrasi() {
|
|||||||
const [username, setUsername] = useState('');
|
const [username, setUsername] = useState('');
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
const [phone, setPhone] = useState(''); // ✅ tambahkan state untuk phone
|
||||||
const [agree, setAgree] = useState(false)
|
|
||||||
|
|
||||||
// Ambil data dari localStorage (dari login)
|
// Ambil data dari localStorage (dari login)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -47,11 +46,6 @@ export default function Registrasi() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!agree) {
|
|
||||||
toast.error("Anda harus menyetujui syarat dan ketentuan!");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// ✅ Hanya kirim username & nomor → dapat kodeId
|
// ✅ Hanya kirim username & nomor → dapat kodeId
|
||||||
@@ -114,29 +108,9 @@ export default function Registrasi() {
|
|||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Box pt="md">
|
<Box pt="md">
|
||||||
<Checkbox
|
<Checkbox label="Saya menyetujui syarat dan ketentuan" defaultChecked />
|
||||||
checked={agree}
|
|
||||||
onChange={(e) => setAgree(e.currentTarget.checked)}
|
|
||||||
label={
|
|
||||||
<Text fz="sm">
|
|
||||||
Saya menyetujui{" "}
|
|
||||||
<a
|
|
||||||
href="/terms-of-service"
|
|
||||||
target="_blank"
|
|
||||||
style={{
|
|
||||||
color: colors["blue-button"],
|
|
||||||
textDecoration: "underline",
|
|
||||||
fontWeight: 500,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
syarat dan ketentuan
|
|
||||||
</a>
|
|
||||||
</Text>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
<Box pt="xl">
|
<Box pt="xl">
|
||||||
<Button
|
<Button
|
||||||
fullWidth
|
fullWidth
|
||||||
|
|||||||
@@ -361,7 +361,6 @@ function CreateAPBDes() {
|
|||||||
data={[
|
data={[
|
||||||
{ value: 'pendapatan', label: 'Pendapatan' },
|
{ value: 'pendapatan', label: 'Pendapatan' },
|
||||||
{ value: 'belanja', label: 'Belanja' },
|
{ value: 'belanja', label: 'Belanja' },
|
||||||
{ value: 'pembiayaan', label: 'Pembiayaan' },
|
|
||||||
]}
|
]}
|
||||||
value={newItem.level === 1 ? null : newItem.tipe}
|
value={newItem.level === 1 ? null : newItem.tipe}
|
||||||
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
onChange={(val) => setNewItem({ ...newItem, tipe: val as any })}
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
export const sosmedMap = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import SelectSocialMediaEdit from '@/app/admin/(dashboard)/_com/selectSocialMediaEdit';
|
|
||||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
@@ -16,7 +14,7 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title,
|
||||||
Loader,
|
Loader
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { Dropzone } from '@mantine/dropzone';
|
import { Dropzone } from '@mantine/dropzone';
|
||||||
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
import { IconArrowBack, IconPhoto, IconUpload, IconX } from '@tabler/icons-react';
|
||||||
@@ -25,45 +23,15 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
|
||||||
type SosmedKey =
|
|
||||||
| 'none'
|
|
||||||
| 'facebook'
|
|
||||||
| 'instagram'
|
|
||||||
| 'tiktok'
|
|
||||||
| 'youtube'
|
|
||||||
| 'whatsapp'
|
|
||||||
| 'gmail'
|
|
||||||
| 'telegram'
|
|
||||||
| 'x'
|
|
||||||
| 'telephone'
|
|
||||||
| 'custom';
|
|
||||||
|
|
||||||
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
|
||||||
none: { label: "None", src: '/no-image.jpg' },
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
function EditMediaSosial() {
|
function EditMediaSosial() {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
||||||
const [selectedSosmed, setSelectedSosmed] = useState<SosmedKey>('facebook');
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
|
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
name: '',
|
name: '',
|
||||||
icon: '',
|
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
imageId: '',
|
imageId: '',
|
||||||
});
|
});
|
||||||
@@ -71,14 +39,13 @@ function EditMediaSosial() {
|
|||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
|
|
||||||
const [originalData, setOriginalData] = useState({
|
const [originalData, setOriginalData] = useState({
|
||||||
name: '',
|
name: "",
|
||||||
icon: '',
|
iconUrl: "",
|
||||||
iconUrl: '',
|
imageId: "",
|
||||||
imageId: '',
|
imageUrl: "",
|
||||||
imageUrl: '',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Load Data by ID
|
// Load data by ID
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const id = params?.id as string;
|
const id = params?.id as string;
|
||||||
if (!id) return;
|
if (!id) return;
|
||||||
@@ -87,97 +54,81 @@ function EditMediaSosial() {
|
|||||||
try {
|
try {
|
||||||
const data = await stateMediaSosial.update.load(id);
|
const data = await stateMediaSosial.update.load(id);
|
||||||
|
|
||||||
if (!data) return;
|
if (data) {
|
||||||
|
// isi form awal
|
||||||
// Tentukan default/custom icon
|
|
||||||
// Tentukan default/custom icon
|
|
||||||
if (data.imageId) {
|
|
||||||
setSelectedSosmed('custom');
|
|
||||||
} else {
|
|
||||||
// ✅ Gunakan langsung data.icon jika ada dan valid
|
|
||||||
if (data.icon && sosmedMap[data.icon as SosmedKey]) {
|
|
||||||
setSelectedSosmed(data.icon as SosmedKey);
|
|
||||||
} else {
|
|
||||||
setSelectedSosmed('none'); // fallback
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const newForm = {
|
const newForm = {
|
||||||
name: data.name || '',
|
name: data.name || "",
|
||||||
icon: data.icon || '',
|
iconUrl: data.iconUrl || "",
|
||||||
iconUrl: data.iconUrl || '',
|
imageId: data.imageId || "",
|
||||||
imageId: data.imageId || '',
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setFormData(newForm);
|
setFormData(newForm);
|
||||||
|
|
||||||
|
// simpan juga versi original
|
||||||
setOriginalData({
|
setOriginalData({
|
||||||
...newForm,
|
...newForm,
|
||||||
imageUrl: data.image?.link || '',
|
imageUrl: data.image?.link || "",
|
||||||
});
|
});
|
||||||
|
|
||||||
setPreviewImage(data.image?.link || null);
|
setPreviewImage(data.image?.link || null);
|
||||||
} catch {
|
}
|
||||||
toast.error('Gagal mengambil data media sosial');
|
} catch (error) {
|
||||||
|
console.error('Error loading media sosial:', error);
|
||||||
|
toast.error(
|
||||||
|
error instanceof Error ? error.message : 'Gagal mengambil data media sosial'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadData();
|
loadData();
|
||||||
}, [params?.id]);
|
}, [params?.id]);
|
||||||
|
|
||||||
const handleChange = (field: keyof typeof formData, value: string) => {
|
const handleChange = (field: string, value: string) => {
|
||||||
setFormData((prev) => ({ ...prev, [field]: value }));
|
setFormData((prev) => ({ ...prev, [field]: value }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// update global state hanya saat submit
|
||||||
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
stateMediaSosial.update.form = { ...stateMediaSosial.update.form, ...formData };
|
||||||
|
|
||||||
if (file) {
|
if (file) {
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({ file, name: file.name });
|
||||||
file,
|
|
||||||
name: file.name,
|
|
||||||
});
|
|
||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) return toast.error('Gagal upload gambar');
|
||||||
toast.error('Gagal upload gambar');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
stateMediaSosial.update.form.imageId = uploaded.id;
|
stateMediaSosial.update.form.imageId = uploaded.id;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚨 Tambahkan ini untuk debugging
|
|
||||||
console.log("Data yang akan dikirim ke backend:", stateMediaSosial.update.form);
|
|
||||||
|
|
||||||
await stateMediaSosial.update.update();
|
await stateMediaSosial.update.update();
|
||||||
toast.success('Media sosial berhasil diperbarui!');
|
toast.success('Media sosial berhasil diperbarui!');
|
||||||
router.push('/admin/landing-page/profil/media-sosial');
|
router.push('/admin/landing-page/profil/media-sosial');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error di handleSubmit:", error); // 🚨 Tambahkan ini juga
|
console.error('Error updating media sosial:', error);
|
||||||
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
toast.error('Terjadi kesalahan saat memperbarui media sosial');
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// ✅ Tombol Batal → balikin ke data original
|
||||||
const handleResetForm = () => {
|
const handleResetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
name: originalData.name,
|
name: originalData.name,
|
||||||
icon: originalData.icon,
|
|
||||||
iconUrl: originalData.iconUrl,
|
iconUrl: originalData.iconUrl,
|
||||||
imageId: originalData.imageId,
|
imageId: originalData.imageId,
|
||||||
});
|
});
|
||||||
setPreviewImage(originalData.imageUrl || null);
|
setPreviewImage(originalData.imageUrl || null);
|
||||||
setFile(null);
|
setFile(null);
|
||||||
toast.info('Form dikembalikan ke data awal');
|
toast.info("Form dikembalikan ke data awal");
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box
|
||||||
|
px={{ base: 'sm', md: 'lg' }}
|
||||||
|
py="md"
|
||||||
|
>
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -196,50 +147,20 @@ function EditMediaSosial() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Upload / Icon */}
|
{/* Upload Gambar */}
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
Icon / Gambar Media Sosial
|
Gambar Program Inovasi
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
{/* Custom Upload */}
|
|
||||||
{/* PILIH ICON */}
|
|
||||||
<SelectSocialMediaEdit
|
|
||||||
value={selectedSosmed}
|
|
||||||
onChange={(key) => {
|
|
||||||
setSelectedSosmed(key);
|
|
||||||
|
|
||||||
if (key === 'custom') {
|
|
||||||
// custom → gunakan Dropzone
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
icon: '',
|
|
||||||
imageId: '',
|
|
||||||
}));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// default → pakai icon bawaan
|
|
||||||
setFormData((prev) => ({
|
|
||||||
...prev,
|
|
||||||
icon: key, // <-- simpan 'facebook', bukan path
|
|
||||||
imageId: '',
|
|
||||||
}));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{selectedSosmed === 'custom' ? (
|
|
||||||
<>
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0];
|
const selectedFile = files[0];
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
setFile(selectedFile);
|
setFile(selectedFile);
|
||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
handleChange('imageId', '');
|
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -255,29 +176,33 @@ function EditMediaSosial() {
|
|||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<Stack align="center" gap="xs">
|
<Text size="md" fw={500}>
|
||||||
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB, format: .png, .jpg, .jpeg, .webp
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* ✅ Preview gambar + tombol X */}
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
}}
|
}}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -287,30 +212,17 @@ function EditMediaSosial() {
|
|||||||
top={5}
|
top={5}
|
||||||
right={5}
|
right={5}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFile(null);
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
handleChange('imageId', '');
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
|
||||||
>
|
>
|
||||||
<IconX size={14} />
|
<IconX size={14} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
// Default icon
|
|
||||||
<Box mt="xs">
|
|
||||||
<Image
|
|
||||||
src={sosmedMap[selectedSosmed].src || ''}
|
|
||||||
alt="Icon bawaan"
|
|
||||||
width={40}
|
|
||||||
height={40}
|
|
||||||
radius="md"
|
|
||||||
style={{ border: '1px solid #ddd', padding: 4, background: '#fff' }}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{/* Nama Media Sosial */}
|
{/* Nama Media Sosial */}
|
||||||
@@ -325,17 +237,25 @@ function EditMediaSosial() {
|
|||||||
{/* Link Media Sosial */}
|
{/* Link Media Sosial */}
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link Media Sosial / Nomor Telepon"
|
label="Link Media Sosial / Nomor Telepon"
|
||||||
placeholder="Masukkan link atau nomor telepon"
|
placeholder="Masukkan link media sosial atau nomor telepon"
|
||||||
value={formData.iconUrl}
|
value={formData.iconUrl}
|
||||||
onChange={(e) => handleChange('iconUrl', e.target.value)}
|
onChange={(e) => handleChange('iconUrl', e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button variant="outline" color="gray" radius="md" size="md" onClick={handleResetForm}>
|
{/* Tombol Batal */}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={handleResetForm}
|
||||||
|
>
|
||||||
Batal
|
Batal
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* Tombol Simpan */}
|
||||||
<Button
|
<Button
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
radius="md"
|
radius="md"
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
'use client'
|
'use client'
|
||||||
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
import { ModalKonfirmasiHapus } from '@/app/admin/(dashboard)/_com/modalKonfirmasiHapus';
|
||||||
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
import profileLandingPageState from '@/app/admin/(dashboard)/_state/landing-page/profile';
|
||||||
@@ -9,7 +8,6 @@ import { IconArrowBack, IconEdit, IconTrash } from '@tabler/icons-react';
|
|||||||
import { useParams, useRouter } from 'next/navigation';
|
import { useParams, useRouter } from 'next/navigation';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import { sosmedMap } from '../../_lib/sosmed';
|
|
||||||
|
|
||||||
function DetailMediaSosial() {
|
function DetailMediaSosial() {
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
@@ -18,14 +16,6 @@ function DetailMediaSosial() {
|
|||||||
const params = useParams();
|
const params = useParams();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
|
||||||
if (item.image?.link) return item.image.link;
|
|
||||||
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
|
||||||
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
useShallowEffect(() => {
|
useShallowEffect(() => {
|
||||||
stateMediaSosial.findUnique.load(params?.id as string);
|
stateMediaSosial.findUnique.load(params?.id as string);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -87,22 +77,21 @@ function DetailMediaSosial() {
|
|||||||
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fz="lg" fw="bold">Gambar</Text>
|
<Text fz="lg" fw="bold">Gambar</Text>
|
||||||
{(() => {
|
{data.image?.link ? (
|
||||||
const src = getIconSource(data);
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
return (
|
|
||||||
<Image
|
<Image
|
||||||
|
src={data.image.link}
|
||||||
|
alt={data.name || 'Gambar Media Sosial'}
|
||||||
|
w="100%"
|
||||||
|
maw={120} // max width biar tidak keluar layar
|
||||||
|
h="auto"
|
||||||
|
radius="md"
|
||||||
|
fit="cover"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src={src}
|
|
||||||
alt={data.name}
|
|
||||||
fit={data.image?.link ? "cover" : "contain"}
|
|
||||||
/>
|
/>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
) : (
|
||||||
})()}
|
<Text fz="sm" c="dimmed">Tidak ada gambar</Text>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<Group gap="sm">
|
<Group gap="sm">
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import ApiFetch from '@/lib/api-fetch';
|
import ApiFetch from '@/lib/api-fetch';
|
||||||
import {
|
import {
|
||||||
@@ -23,41 +22,10 @@ import { useEffect, useState } from 'react';
|
|||||||
import { toast } from 'react-toastify';
|
import { toast } from 'react-toastify';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../../_state/landing-page/profile';
|
||||||
import SelectSosialMedia from '@/app/admin/(dashboard)/_com/selectSocialMedia';
|
|
||||||
|
|
||||||
|
|
||||||
// ⭐ Tambah type SosmedKey
|
|
||||||
type SosmedKey =
|
|
||||||
| 'facebook'
|
|
||||||
| 'instagram'
|
|
||||||
| 'tiktok'
|
|
||||||
| 'youtube'
|
|
||||||
| 'whatsapp'
|
|
||||||
| 'gmail'
|
|
||||||
| 'telegram'
|
|
||||||
| 'x'
|
|
||||||
| 'telephone'
|
|
||||||
| 'custom';
|
|
||||||
|
|
||||||
// ⭐ mapping icon sosmed bawaan
|
|
||||||
const sosmedMap: Record<SosmedKey, { label: string; src: string | null }> = {
|
|
||||||
facebook: { label: 'Facebook', src: '/assets/images/sosmed/facebook.png' },
|
|
||||||
instagram: { label: 'Instagram', src: '/assets/images/sosmed/instagram.png' },
|
|
||||||
tiktok: { label: 'Tiktok', src: '/assets/images/sosmed/tiktok.png' },
|
|
||||||
youtube: { label: 'YouTube', src: '/assets/images/sosmed/youtube.png' },
|
|
||||||
whatsapp: { label: 'WhatsApp', src: '/assets/images/sosmed/whatsapp.png' },
|
|
||||||
gmail: { label: 'Gmail', src: '/assets/images/sosmed/gmail.png' },
|
|
||||||
telegram: { label: 'Telegram', src: '/assets/images/sosmed/telegram.png' },
|
|
||||||
x: { label: 'X (Twitter)', src: '/assets/images/sosmed/x-twitter.png' },
|
|
||||||
telephone: { label: 'Telephone', src: '/assets/images/sosmed/telephone-call.png' },
|
|
||||||
custom: { label: 'Custom Icon', src: null },
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function CreateMediaSosial() {
|
export default function CreateMediaSosial() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial);
|
||||||
|
|
||||||
const [selectedSosmed, setSelectedSosmed] = useState<SosmedKey>('facebook');
|
|
||||||
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
const [previewImage, setPreviewImage] = useState<string | null>(null);
|
||||||
const [file, setFile] = useState<File | null>(null);
|
const [file, setFile] = useState<File | null>(null);
|
||||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||||
@@ -71,34 +39,16 @@ export default function CreateMediaSosial() {
|
|||||||
name: '',
|
name: '',
|
||||||
imageId: '',
|
imageId: '',
|
||||||
iconUrl: '',
|
iconUrl: '',
|
||||||
icon: ''
|
|
||||||
};
|
};
|
||||||
|
|
||||||
setFile(null);
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
setSelectedSosmed('facebook');
|
setFile(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ──────────────── ⭐ CASE 1: PAKAI ICON DEFAULT ────────────────
|
|
||||||
if (selectedSosmed !== 'custom') {
|
|
||||||
stateMediaSosial.create.form.imageId = null;
|
|
||||||
stateMediaSosial.create.form.icon = sosmedMap[selectedSosmed].src!;
|
|
||||||
|
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
|
||||||
resetForm();
|
|
||||||
router.push('/admin/landing-page/profil/media-sosial');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ──────────────── ⭐ CASE 2: CUSTOM ICON → WAJIB UPLOAD ────────────────
|
|
||||||
if (!file) {
|
if (!file) {
|
||||||
toast.warn('Silakan upload icon custom terlebih dahulu');
|
return toast.warn('Silakan pilih file gambar terlebih dahulu');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await ApiFetch.api.fileStorage.create.post({
|
const res = await ApiFetch.api.fileStorage.create.post({
|
||||||
@@ -109,12 +59,10 @@ export default function CreateMediaSosial() {
|
|||||||
const uploaded = res.data?.data;
|
const uploaded = res.data?.data;
|
||||||
|
|
||||||
if (!uploaded?.id) {
|
if (!uploaded?.id) {
|
||||||
toast.error('Gagal mengunggah icon custom');
|
return toast.error('Gagal mengunggah gambar, silakan coba lagi');
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
stateMediaSosial.create.form.imageId = uploaded.id;
|
stateMediaSosial.create.form.imageId = uploaded.id;
|
||||||
stateMediaSosial.create.form.icon = null;
|
|
||||||
|
|
||||||
await stateMediaSosial.create.create();
|
await stateMediaSosial.create.create();
|
||||||
|
|
||||||
@@ -130,7 +78,6 @@ export default function CreateMediaSosial() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
{/* Header */}
|
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
<Button variant="subtle" onClick={() => router.back()} p="xs" radius="md">
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
@@ -149,16 +96,10 @@ export default function CreateMediaSosial() {
|
|||||||
style={{ border: '1px solid #e0e0e0' }}
|
style={{ border: '1px solid #e0e0e0' }}
|
||||||
>
|
>
|
||||||
<Stack gap="md">
|
<Stack gap="md">
|
||||||
{/* Select Sosmed */}
|
|
||||||
<SelectSosialMedia value={selectedSosmed} onChange={setSelectedSosmed} />
|
|
||||||
|
|
||||||
{/* Custom icon uploader */}
|
|
||||||
{selectedSosmed === 'custom' && (
|
|
||||||
<Box>
|
<Box>
|
||||||
<Text fw="bold" fz="sm" mb={6}>
|
<Text fw="bold" fz="sm" mb={6}>
|
||||||
Upload Custom Icon
|
Gambar Program Inovasi
|
||||||
</Text>
|
</Text>
|
||||||
|
|
||||||
<Dropzone
|
<Dropzone
|
||||||
onDrop={(files) => {
|
onDrop={(files) => {
|
||||||
const selectedFile = files[0];
|
const selectedFile = files[0];
|
||||||
@@ -167,7 +108,7 @@ export default function CreateMediaSosial() {
|
|||||||
setPreviewImage(URL.createObjectURL(selectedFile));
|
setPreviewImage(URL.createObjectURL(selectedFile));
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onReject={() => toast.error('File tidak valid')}
|
onReject={() => toast.error('File tidak valid, gunakan format gambar')}
|
||||||
maxSize={5 * 1024 ** 2}
|
maxSize={5 * 1024 ** 2}
|
||||||
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
accept={{ 'image/*': ['.jpeg', '.jpg', '.png', '.webp'] }}
|
||||||
radius="md"
|
radius="md"
|
||||||
@@ -183,29 +124,33 @@ export default function CreateMediaSosial() {
|
|||||||
<Dropzone.Idle>
|
<Dropzone.Idle>
|
||||||
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
<IconPhoto size={48} color="#868e96" stroke={1.5} />
|
||||||
</Dropzone.Idle>
|
</Dropzone.Idle>
|
||||||
|
<Stack gap="xs" align="center">
|
||||||
<Stack align="center" gap="xs">
|
<Text size="md" fw={500}>
|
||||||
<Text fw={500}>Seret gambar atau klik untuk pilih</Text>
|
Seret gambar atau klik untuk memilih file
|
||||||
|
</Text>
|
||||||
<Text size="sm" c="dimmed">
|
<Text size="sm" c="dimmed">
|
||||||
Maksimal 5MB, format .png, .jpg, .jpeg, webp
|
Maksimal 5MB, format gambar .png, .jpg, .jpeg, webp
|
||||||
</Text>
|
</Text>
|
||||||
</Stack>
|
</Stack>
|
||||||
</Group>
|
</Group>
|
||||||
</Dropzone>
|
</Dropzone>
|
||||||
|
|
||||||
|
{/* ✅ Preview gambar + tombol X */}
|
||||||
{previewImage && (
|
{previewImage && (
|
||||||
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
<Box mt="sm" pos="relative" style={{ textAlign: 'center' }}>
|
||||||
<Image
|
<Image
|
||||||
src={previewImage}
|
src={previewImage}
|
||||||
alt="Preview"
|
alt="Preview Gambar"
|
||||||
radius="md"
|
radius="md"
|
||||||
style={{
|
style={{
|
||||||
maxHeight: 200,
|
maxHeight: 200,
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
border: '1px solid #ddd',
|
border: '1px solid #ddd',
|
||||||
}}
|
}}
|
||||||
|
loading="lazy"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Tombol hapus (pojok kanan atas) */}
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
variant="filled"
|
variant="filled"
|
||||||
color="red"
|
color="red"
|
||||||
@@ -215,44 +160,48 @@ export default function CreateMediaSosial() {
|
|||||||
top={5}
|
top={5}
|
||||||
right={5}
|
right={5}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setFile(null);
|
|
||||||
setPreviewImage(null);
|
setPreviewImage(null);
|
||||||
|
setFile(null);
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
boxShadow: '0 2px 6px rgba(0,0,0,0.15)',
|
||||||
}}
|
}}
|
||||||
style={{ boxShadow: '0 2px 6px rgba(0,0,0,0.15)' }}
|
|
||||||
>
|
>
|
||||||
<IconX size={14} />
|
<IconX size={14} />
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Input name */}
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Nama Media Sosial"
|
label="Nama Media Sosial / Kontak"
|
||||||
placeholder="Masukkan nama media sosial"
|
placeholder="Masukkan nama media sosial atau kontak"
|
||||||
value={stateMediaSosial.create.form.name ?? ''}
|
value={stateMediaSosial.create.form.name || ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.name = e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Input link */}
|
|
||||||
<TextInput
|
<TextInput
|
||||||
label="Link / Kontak"
|
label="Link Media Sosial / Nomor Telepon"
|
||||||
placeholder="Masukkan link atau nomor"
|
placeholder="Masukkan link media sosial atau nomor telepon"
|
||||||
value={stateMediaSosial.create.form.iconUrl ?? ''}
|
value={stateMediaSosial.create.form.iconUrl || ''}
|
||||||
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
onChange={(e) => (stateMediaSosial.create.form.iconUrl = e.target.value)}
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Actions */}
|
|
||||||
<Group justify="right">
|
<Group justify="right">
|
||||||
<Button variant="outline" color="gray" radius="md" onClick={resetForm}>
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
color="gray"
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
|
onClick={resetForm}
|
||||||
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
radius="md"
|
|
||||||
onClick={handleSubmit}
|
onClick={handleSubmit}
|
||||||
|
radius="md"
|
||||||
|
size="md"
|
||||||
style={{
|
style={{
|
||||||
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
background: `linear-gradient(135deg, ${colors['blue-button']}, #4facfe)`,
|
||||||
color: '#fff',
|
color: '#fff',
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
'use client'
|
'use client'
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
@@ -9,7 +8,6 @@ import { useState } from 'react';
|
|||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import HeaderSearch from '../../../_com/header';
|
import HeaderSearch from '../../../_com/header';
|
||||||
import profileLandingPageState from '../../../_state/landing-page/profile';
|
import profileLandingPageState from '../../../_state/landing-page/profile';
|
||||||
import { sosmedMap } from '../_lib/sosmed';
|
|
||||||
|
|
||||||
function MediaSosial() {
|
function MediaSosial() {
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
@@ -31,14 +29,6 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
const stateMediaSosial = useProxy(profileLandingPageState.mediaSosial)
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
|
||||||
if (item.image?.link) return item.image.link;
|
|
||||||
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
|
||||||
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
data,
|
data,
|
||||||
page,
|
page,
|
||||||
@@ -87,26 +77,13 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
<TableTd style={{ width: '25%', }}>
|
<TableTd style={{ width: '25%', }}>
|
||||||
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
<Text fw={500} truncate="end" lineClamp={1}>{item.name}</Text>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%' }}>
|
<TableTd style={{ width: '20%', }}>
|
||||||
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden' }}>
|
<Box w={50} h={50} style={{ borderRadius: 8, overflow: 'hidden', }}>
|
||||||
|
{item.image?.link ? (
|
||||||
{(() => {
|
<Image loading='lazy' src={item.image.link} alt={item.name} fit="cover" />
|
||||||
const src = getIconSource(item);
|
) : (
|
||||||
|
<Box bg={colors['blue-button']} w="100%" h="100%" />
|
||||||
if (src) {
|
)}
|
||||||
return (
|
|
||||||
<Image
|
|
||||||
loading="lazy"
|
|
||||||
src={src}
|
|
||||||
alt={item.name}
|
|
||||||
fit={item.image?.link ? "cover" : "contain"}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
|
||||||
})()}
|
|
||||||
|
|
||||||
</Box>
|
</Box>
|
||||||
</TableTd>
|
</TableTd>
|
||||||
<TableTd style={{ width: '20%', }}>
|
<TableTd style={{ width: '20%', }}>
|
||||||
|
|||||||
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
25
src/app/admin/(dashboard)/user&role/_com/getMenuIdByRole.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
// src/app/admin/_com/getMenuIdsByRoleId.ts
|
||||||
|
import { navBar, role1, role2, role3 } from '@/app/admin/_com/list_PageAdmin';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mengembalikan daftar ID menu (string[]) berdasarkan roleId
|
||||||
|
*/
|
||||||
|
export function getMenuIdsByRoleId(roleId: string | number): string[] {
|
||||||
|
const id = typeof roleId === 'string' ? parseInt(roleId, 10) : roleId;
|
||||||
|
|
||||||
|
switch (id) {
|
||||||
|
case 0:
|
||||||
|
// Asumsikan devBar ada dan punya struktur sama
|
||||||
|
return []; // atau sesuaikan jika ada devBar
|
||||||
|
case 1:
|
||||||
|
return navBar.map(section => section.id);
|
||||||
|
case 2:
|
||||||
|
return role1.map(section => section.id);
|
||||||
|
case 3:
|
||||||
|
return role2.map(section => section.id);
|
||||||
|
case 4:
|
||||||
|
return role3.map(section => section.id);
|
||||||
|
default:
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,6 @@ type FormCreate = {
|
|||||||
name: string;
|
name: string;
|
||||||
imageId: string;
|
imageId: string;
|
||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
icon: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function mediaSosialCreate(context: Context) {
|
export default async function mediaSosialCreate(context: Context) {
|
||||||
@@ -15,9 +14,8 @@ export default async function mediaSosialCreate(context: Context) {
|
|||||||
const result = await prisma.mediaSosial.create({
|
const result = await prisma.mediaSosial.create({
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
imageId: body.imageId || null,
|
imageId: body.imageId,
|
||||||
iconUrl: body.iconUrl,
|
iconUrl: body.iconUrl,
|
||||||
icon: body.icon || null,
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
@@ -31,6 +29,8 @@ export default async function mediaSosialCreate(context: Context) {
|
|||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error creating media sosial:", error);
|
console.error("Error creating media sosial:", error);
|
||||||
throw new Error("Gagal membuat media sosial: " + (error as Error).message);
|
throw new Error(
|
||||||
|
"Gagal membuat media sosial: " + (error as Error).message
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,8 @@ const MediaSosial = new Elysia({
|
|||||||
.post("/create", MediaSosialCreate, {
|
.post("/create", MediaSosialCreate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String(),
|
name: t.String(),
|
||||||
imageId: t.Union([t.String(), t.Null()]),
|
imageId: t.String(),
|
||||||
iconUrl: t.Union([t.String(), t.Null()]),
|
iconUrl: t.String(),
|
||||||
icon: t.Union([t.String(), t.Null()]),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -30,9 +29,8 @@ const MediaSosial = new Elysia({
|
|||||||
.put("/:id", MediaSosialUpdate, {
|
.put("/:id", MediaSosialUpdate, {
|
||||||
body: t.Object({
|
body: t.Object({
|
||||||
name: t.String(),
|
name: t.String(),
|
||||||
imageId: t.Optional(t.Union([t.String(), t.Null()])),
|
imageId: t.Optional(t.String()),
|
||||||
iconUrl: t.Optional(t.Union([t.String(), t.Null()])),
|
iconUrl: t.Optional(t.String()),
|
||||||
icon: t.Optional(t.Union([t.String(), t.Null()])),
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
// ✅ Delete
|
// ✅ Delete
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ type FormUpdateMediaSosial = {
|
|||||||
name?: string;
|
name?: string;
|
||||||
imageId?: string;
|
imageId?: string;
|
||||||
iconUrl?: string;
|
iconUrl?: string;
|
||||||
icon?: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function mediaSosialUpdate(context: Context) {
|
export default async function mediaSosialUpdate(context: Context) {
|
||||||
@@ -21,29 +20,13 @@ export default async function mediaSosialUpdate(context: Context) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🚨 Tambahkan validasi di sini
|
|
||||||
if (!body.name || body.name.trim().length < 3) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Nama media sosial minimal 3 karakter",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!body.iconUrl || body.iconUrl.trim().length < 3) {
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
message: "Icon URL minimal 3 karakter",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const updated = await prisma.mediaSosial.update({
|
const updated = await prisma.mediaSosial.update({
|
||||||
where: { id },
|
where: { id },
|
||||||
data: {
|
data: {
|
||||||
name: body.name,
|
name: body.name,
|
||||||
imageId: body.imageId || null, // pastikan null jika kosong
|
imageId: body.imageId,
|
||||||
iconUrl: body.iconUrl,
|
iconUrl: body.iconUrl,
|
||||||
icon: body.icon || null, // pastikan null jika kosong
|
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
image: true,
|
image: true,
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
import { getMenuIdsByRoleId } from "@/app/admin/(dashboard)/user&role/_com/getMenuIdByRole";
|
||||||
import prisma from "@/lib/prisma";
|
import prisma from "@/lib/prisma";
|
||||||
import { Context } from "elysia";
|
import { Context } from "elysia";
|
||||||
|
|
||||||
@@ -34,11 +35,25 @@ export default async function userUpdate(context: Context) {
|
|||||||
const isActiveChanged =
|
const isActiveChanged =
|
||||||
isActive !== undefined && currentUser.isActive !== isActive;
|
isActive !== undefined && currentUser.isActive !== isActive;
|
||||||
|
|
||||||
// ✅ Jika role berubah, hapus semua akses menu yang ada
|
// ✅ Jika role berubah, reset dan set ulang akses menu
|
||||||
if (isRoleChanged) {
|
if (isRoleChanged && roleId) {
|
||||||
|
// Hapus akses lama
|
||||||
await prisma.userMenuAccess.deleteMany({
|
await prisma.userMenuAccess.deleteMany({
|
||||||
where: { userId: id }
|
where: { userId: id }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Ambil menu default untuk role baru
|
||||||
|
const menuIds = getMenuIdsByRoleId(roleId);
|
||||||
|
|
||||||
|
if (menuIds.length > 0) {
|
||||||
|
// Buat akses baru
|
||||||
|
await prisma.userMenuAccess.createMany({
|
||||||
|
data: menuIds.map(menuId => ({
|
||||||
|
userId: id,
|
||||||
|
menuId
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update user
|
// Update user
|
||||||
|
|||||||
@@ -151,7 +151,7 @@ function Page() {
|
|||||||
variant="light"
|
variant="light"
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconDeviceImacCog size={16} />}
|
leftSection={<IconDeviceImacCog size={16} />}
|
||||||
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik/${item.id}`)}
|
onClick={() => router.push(`/darmasaba/ppid/daftar-informasi-publik-desa-darmasaba/${item.id}`)}
|
||||||
>
|
>
|
||||||
Detail
|
Detail
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,8 +1,13 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// src/app/admin/(dashboard)/landing-page/APBDes/APBDesProgress.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import colors from '@/con/colors';
|
|
||||||
import { Box, Paper, Progress, Stack, Text, Title } from '@mantine/core';
|
import { Box, Paper, Progress, Stack, Text, Title } from '@mantine/core';
|
||||||
import { APBDesData } from './types';
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function formatRupiah(value: number) {
|
function formatRupiah(value: number) {
|
||||||
return new Intl.NumberFormat('id-ID', {
|
return new Intl.NumberFormat('id-ID', {
|
||||||
@@ -12,33 +17,31 @@ function formatRupiah(value: number) {
|
|||||||
}).format(value);
|
}).format(value);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APBDesProgressProps {
|
function APBDesProgress() {
|
||||||
apbdesData: APBDesData;
|
const state = useProxy(apbdes);
|
||||||
|
const data = state.findMany.data || [];
|
||||||
|
|
||||||
|
// Ambil APBDes pertama (misalnya, jika hanya satu tahun ditampilkan)
|
||||||
|
const apbdesItem = data[0]; // 👈 sesuaikan logika jika ada banyak APBDes
|
||||||
|
|
||||||
|
if (!apbdesItem) {
|
||||||
|
return (
|
||||||
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
|
<Text c="dimmed">Belum ada data APBDes untuk ditampilkan.</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
const items = apbdesItem.items || [];
|
||||||
// Return null if apbdesData is not available yet
|
|
||||||
if (!apbdesData) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
const items = apbdesData.items || [];
|
|
||||||
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
||||||
|
|
||||||
// Kelompokkan berdasarkan tipe
|
// Kelompokkan berdasarkan tipe
|
||||||
const pendapatanItems = sortedItems.filter(item => item.tipe === 'pendapatan');
|
const pendapatanItems = sortedItems.filter(item => item.tipe === 'pendapatan');
|
||||||
const belanjaItems = sortedItems.filter(item => item.tipe === 'belanja');
|
const belanjaItems = sortedItems.filter(item => item.tipe === 'belanja');
|
||||||
const pembiayaanItems = sortedItems.filter(item => item.tipe === 'pembiayaan');
|
const pembiayaanItems = sortedItems.filter(item => item.tipe === 'pembiayaan'); // jika ada
|
||||||
|
|
||||||
// Items without a type (should be filtered out from calculations)
|
|
||||||
const untypedItems = sortedItems.filter(item => !item.tipe);
|
|
||||||
|
|
||||||
if (untypedItems.length > 0) {
|
|
||||||
console.warn(`Found ${untypedItems.length} items without a type. These will be excluded from calculations.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hitung total per kategori
|
// Hitung total per kategori
|
||||||
const calcTotal = (items: { anggaran: number; realisasi: number }[]) => {
|
const calcTotal = (items: any[]) => {
|
||||||
const anggaran = items.reduce((sum, item) => sum + item.anggaran, 0);
|
const anggaran = items.reduce((sum, item) => sum + item.anggaran, 0);
|
||||||
const realisasi = items.reduce((sum, item) => sum + item.realisasi, 0);
|
const realisasi = items.reduce((sum, item) => sum + item.realisasi, 0);
|
||||||
const persen = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
|
const persen = anggaran > 0 ? (realisasi / anggaran) * 100 : 0;
|
||||||
@@ -47,10 +50,10 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
|
|
||||||
const pendapatan = calcTotal(pendapatanItems);
|
const pendapatan = calcTotal(pendapatanItems);
|
||||||
const belanja = calcTotal(belanjaItems);
|
const belanja = calcTotal(belanjaItems);
|
||||||
const pembiayaan = calcTotal(pembiayaanItems);
|
const pembiayaan = calcTotal(pembiayaanItems); // bisa kosong
|
||||||
|
|
||||||
// Render satu progress bar
|
// Render satu progress bar
|
||||||
const renderProgress = (label: string, dataset: { realisasi: number; anggaran: number; persen: number }) => {
|
const renderProgress = (label: string, dataset: any) => {
|
||||||
const isPembiayaan = label.includes('Pembiayaan');
|
const isPembiayaan = label.includes('Pembiayaan');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -68,8 +71,8 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
root: { backgroundColor: '#d7e3f1' },
|
root: { backgroundColor: '#d7e3f1' },
|
||||||
section: {
|
section: {
|
||||||
backgroundColor: isPembiayaan
|
backgroundColor: isPembiayaan
|
||||||
? 'green'
|
? 'green' // warna hijau untuk pembiayaan
|
||||||
: colors['blue-button'],
|
: colors['blue-button'], // biru untuk pendapatan/belanja
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
'&::after': {
|
'&::after': {
|
||||||
content: `'${dataset.persen.toFixed(2)}%'`,
|
content: `'${dataset.persen.toFixed(2)}%'`,
|
||||||
@@ -99,7 +102,7 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
>
|
>
|
||||||
<Stack gap="lg">
|
<Stack gap="lg">
|
||||||
<Title order={4} c={colors['blue-button']} ta="center">
|
<Title order={4} c={colors['blue-button']} ta="center">
|
||||||
Grafik Pelaksanaan APBDes Tahun {apbdesData.tahun}
|
Grafik Pelaksanaan APBDes Tahun {apbdesItem.tahun}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Text ta="center" fw="bold" fz="sm" c="dimmed">
|
<Text ta="center" fw="bold" fz="sm" c="dimmed">
|
||||||
@@ -109,9 +112,97 @@ function APBDesProgress({ apbdesData }: APBDesProgressProps) {
|
|||||||
{renderProgress('Pendapatan Desa', pendapatan)}
|
{renderProgress('Pendapatan Desa', pendapatan)}
|
||||||
{renderProgress('Belanja Desa', belanja)}
|
{renderProgress('Belanja Desa', belanja)}
|
||||||
{renderProgress('Pembiayaan Desa', pembiayaan)}
|
{renderProgress('Pembiayaan Desa', pembiayaan)}
|
||||||
|
{pembiayaanItems.length > 0 && renderProgress('Pembiayaan Desa', pembiayaan)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Paper>
|
</Paper>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default APBDesProgress;
|
export default APBDesProgress;
|
||||||
|
|
||||||
|
// /* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
// 'use client';
|
||||||
|
|
||||||
|
// import { Box, Paper, Stack, Text, Title } from '@mantine/core';
|
||||||
|
// import { BarChart } from '@mantine/charts';
|
||||||
|
// import { useProxy } from 'valtio/utils';
|
||||||
|
// import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
|
// import colors from '@/con/colors';
|
||||||
|
|
||||||
|
// function APBDesProgress() {
|
||||||
|
// const state = useProxy(apbdes);
|
||||||
|
// const data = state.findMany.data || [];
|
||||||
|
|
||||||
|
// const apbdesItem = data[0];
|
||||||
|
// if (!apbdesItem) {
|
||||||
|
// return (
|
||||||
|
// <Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
|
// <Text c="dimmed">Belum ada data APBDes untuk ditampilkan.</Text>
|
||||||
|
// </Box>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const items = apbdesItem.items || [];
|
||||||
|
// const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
||||||
|
|
||||||
|
// const pendapatanItems = sortedItems.filter(i => i.tipe === 'pendapatan');
|
||||||
|
// const belanjaItems = sortedItems.filter(i => i.tipe === 'belanja');
|
||||||
|
// const pembiayaanItems = sortedItems.filter(i => i.tipe === 'pembiayaan');
|
||||||
|
|
||||||
|
// const total = (rows: any[]) => {
|
||||||
|
// const anggaran = rows.reduce((s, i) => s + i.anggaran, 0);
|
||||||
|
// const realisasi = rows.reduce((s, i) => s + i.realisasi, 0);
|
||||||
|
// return anggaran === 0 ? 0 : (realisasi / anggaran) * 100;
|
||||||
|
// };
|
||||||
|
|
||||||
|
// const chartData = [
|
||||||
|
// { name: 'Pendapatan', persen: total(pendapatanItems) },
|
||||||
|
// { name: 'Belanja', persen: total(belanjaItems) },
|
||||||
|
// ];
|
||||||
|
|
||||||
|
// if (pembiayaanItems.length > 0) {
|
||||||
|
// chartData.push({ name: 'Pembiayaan', persen: total(pembiayaanItems) });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// return (
|
||||||
|
// <Paper
|
||||||
|
// mx={{ base: 'md', md: 100 }}
|
||||||
|
// p="xl"
|
||||||
|
// radius="md"
|
||||||
|
// shadow="sm"
|
||||||
|
// withBorder
|
||||||
|
// bg={colors['white-1']}
|
||||||
|
// >
|
||||||
|
// <Stack gap="lg">
|
||||||
|
// <Title order={4} c={colors['blue-button']} ta="center">
|
||||||
|
// Grafik Pelaksanaan APBDes Tahun {apbdesItem.tahun}
|
||||||
|
// </Title>
|
||||||
|
|
||||||
|
// <Text ta="center" fw="bold" fz="sm" c="dimmed">
|
||||||
|
// Persentase Realisasi (%) dari Anggaran
|
||||||
|
// </Text>
|
||||||
|
|
||||||
|
// <BarChart
|
||||||
|
// h={200}
|
||||||
|
// data={chartData}
|
||||||
|
// orientation="vertical"
|
||||||
|
// dataKey="name"
|
||||||
|
// barProps={{ radius: 6 }}
|
||||||
|
// series={[
|
||||||
|
// {
|
||||||
|
// name: 'persen',
|
||||||
|
// label: 'Persentase',
|
||||||
|
// color: colors['blue-button'],
|
||||||
|
// },
|
||||||
|
// ]}
|
||||||
|
// yAxisProps={{
|
||||||
|
// domain: [0, 100],
|
||||||
|
// }}
|
||||||
|
// valueFormatter={(v) => `${v.toFixed(1)}%`}
|
||||||
|
// />
|
||||||
|
// </Stack>
|
||||||
|
// </Paper>
|
||||||
|
// );
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export default APBDesProgress;
|
||||||
|
|||||||
@@ -1,8 +1,30 @@
|
|||||||
|
// src/app/admin/(dashboard)/landing-page/APBDes/APBDesTable.tsx
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { Box, Paper, Table, Text, Title, Badge, Group } from '@mantine/core';
|
import { Box, Paper, Table, Text, Title, Badge, Group } from '@mantine/core';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { APBDesData } from './types';
|
|
||||||
|
interface APBDesItem {
|
||||||
|
id: string;
|
||||||
|
kode: string;
|
||||||
|
uraian: string;
|
||||||
|
anggaran: number;
|
||||||
|
realisasi: number;
|
||||||
|
selisih: number;
|
||||||
|
persentase: number;
|
||||||
|
level: number;
|
||||||
|
tipe: 'pendapatan' | 'belanja' | 'pembiayaan';
|
||||||
|
}
|
||||||
|
|
||||||
|
interface APBDesData {
|
||||||
|
id: string;
|
||||||
|
tahun: number;
|
||||||
|
items: APBDesItem[];
|
||||||
|
image?: { id: string; url: string } | null;
|
||||||
|
file?: { id: string; url: string } | null;
|
||||||
|
}
|
||||||
|
|
||||||
// Helper: Format Rupiah, tapi jika 0 → tampilkan '-'
|
// Helper: Format Rupiah, tapi jika 0 → tampilkan '-'
|
||||||
function formatRupiahOrEmpty(value: number): string {
|
function formatRupiahOrEmpty(value: number): string {
|
||||||
@@ -29,12 +51,22 @@ function getIndent(level: number) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
interface APBDesTableProps {
|
function APBDesTable() {
|
||||||
apbdesData: APBDesData;
|
const state = useProxy(apbdes);
|
||||||
|
const data = state.findMany.data || [];
|
||||||
|
|
||||||
|
// Get the first APBDes item
|
||||||
|
const apbdesItem = data[0] as unknown as APBDesData | undefined;
|
||||||
|
|
||||||
|
if (!apbdesItem) {
|
||||||
|
return (
|
||||||
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
|
<Text c="dimmed">Belum ada data APBDes untuk ditampilkan.</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function APBDesTable({ apbdesData }: APBDesTableProps) {
|
const items = Array.isArray(apbdesItem.items) ? apbdesItem.items : [];
|
||||||
const items = Array.isArray(apbdesData.items) ? apbdesData.items : [];
|
|
||||||
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
const sortedItems = [...items].sort((a, b) => a.kode.localeCompare(b.kode));
|
||||||
|
|
||||||
// Calculate totals
|
// Calculate totals
|
||||||
@@ -44,9 +76,9 @@ function APBDesTable({ apbdesData }: APBDesTableProps) {
|
|||||||
const totalPersentase = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
|
const totalPersentase = totalAnggaran > 0 ? (totalRealisasi / totalAnggaran) * 100 : 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box pt={"xs"} pb="md" px={{ base: 'md', md: 100 }}>
|
<Box py="md" px={{ base: 'md', md: 100 }}>
|
||||||
<Title order={4} c={colors['blue-button']} mb="sm">
|
<Title order={4} c={colors['blue-button']} mb="sm">
|
||||||
Rincian APBDes Tahun {apbdesData.tahun}
|
Rincian APBDes Tahun {apbdesItem.tahun}
|
||||||
</Title>
|
</Title>
|
||||||
|
|
||||||
<Paper withBorder radius="md" shadow="xs" p="md">
|
<Paper withBorder radius="md" shadow="xs" p="md">
|
||||||
@@ -77,7 +109,9 @@ function APBDesTable({ apbdesData }: APBDesTableProps) {
|
|||||||
<Table.Td style={getIndent(item.level)}>
|
<Table.Td style={getIndent(item.level)}>
|
||||||
<Group gap="xs" align="flex-start">
|
<Group gap="xs" align="flex-start">
|
||||||
<Text fw={item.level === 1 ? 'bold' : 'normal'}>{item.kode}</Text>
|
<Text fw={item.level === 1 ? 'bold' : 'normal'}>{item.kode}</Text>
|
||||||
<Text fz="sm">{item.uraian}</Text>
|
<Text fz="sm" >
|
||||||
|
{item.uraian}
|
||||||
|
</Text>
|
||||||
</Group>
|
</Group>
|
||||||
</Table.Td>
|
</Table.Td>
|
||||||
<Table.Td ta="right">{formatRupiahOrEmpty(item.anggaran)}</Table.Td>
|
<Table.Td ta="right">{formatRupiahOrEmpty(item.anggaran)}</Table.Td>
|
||||||
|
|||||||
@@ -1,42 +0,0 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
export type APBDesTipe = 'pendapatan' | 'belanja' | 'pembiayaan';
|
|
||||||
|
|
||||||
export function isAPBDesTipe(tipe: string | null | undefined): tipe is APBDesTipe {
|
|
||||||
return tipe === 'pendapatan' || tipe === 'belanja' || tipe === 'pembiayaan';
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APBDesItem {
|
|
||||||
id: string;
|
|
||||||
kode: string;
|
|
||||||
uraian: string;
|
|
||||||
anggaran: number;
|
|
||||||
realisasi: number;
|
|
||||||
selisih: number;
|
|
||||||
persentase: number;
|
|
||||||
level: number;
|
|
||||||
tipe?: APBDesTipe | null;
|
|
||||||
// Additional fields from API
|
|
||||||
createdAt?: Date;
|
|
||||||
updatedAt?: Date;
|
|
||||||
deletedAt?: Date | null;
|
|
||||||
isActive?: boolean;
|
|
||||||
apbdesId?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface APBDesData {
|
|
||||||
id: string;
|
|
||||||
tahun: number | null;
|
|
||||||
items: APBDesItem[];
|
|
||||||
image?: { id: string; url: string } | null;
|
|
||||||
file?: { id: string; url: string } | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function transformAPBDesData(data: any): APBDesData {
|
|
||||||
return {
|
|
||||||
...data,
|
|
||||||
items: data.items.map((item: any) => ({
|
|
||||||
...item,
|
|
||||||
tipe: isAPBDesTipe(item.tipe) ? item.tipe : null
|
|
||||||
}))
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -4,21 +4,19 @@
|
|||||||
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'
|
import PendapatanAsliDesa from '@/app/admin/(dashboard)/_state/ekonomi/PADesa'
|
||||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import { ActionIcon, BackgroundImage, Box, Center, Container, Group, Loader, Select, SimpleGrid, Stack, Text, Title } from '@mantine/core'
|
import { ActionIcon, BackgroundImage, Box, Center, Container, Group, Loader, SimpleGrid, Stack, Text, Title } from '@mantine/core'
|
||||||
import { IconDownload } from '@tabler/icons-react'
|
import { IconDownload } from '@tabler/icons-react'
|
||||||
import { Link } from 'next-view-transitions'
|
import { Link } from 'next-view-transitions'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useProxy } from 'valtio/utils'
|
import { useProxy } from 'valtio/utils'
|
||||||
import BackButton from '../../(pages)/desa/layanan/_com/BackButto'
|
import BackButton from '../../(pages)/desa/layanan/_com/BackButto'
|
||||||
import APBDesTable from './lib/apbDesaTable'
|
|
||||||
import APBDesProgress from './lib/apbDesaProgress'
|
import APBDesProgress from './lib/apbDesaProgress'
|
||||||
import { transformAPBDesData } from './lib/types'
|
import APBDesTable from './lib/apbDesaTable'
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const state = useProxy(apbdes)
|
const state = useProxy(apbdes)
|
||||||
const paDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
|
const paDesaState = useProxy(PendapatanAsliDesa.ApbDesa)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const loadData = async () => {
|
const loadData = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -36,23 +34,6 @@ function Page() {
|
|||||||
|
|
||||||
const dataAPBDes = state.findMany.data || []
|
const dataAPBDes = state.findMany.data || []
|
||||||
|
|
||||||
// Buat daftar tahun unik dari data
|
|
||||||
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
|
|
||||||
.sort((a, b) => b - a) // urutkan descending
|
|
||||||
.map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
|
|
||||||
|
|
||||||
// Pilih tahun pertama sebagai default jika belum ada yang dipilih
|
|
||||||
useEffect(() => {
|
|
||||||
if (years.length > 0 && !selectedYear) {
|
|
||||||
setSelectedYear(years[0].value)
|
|
||||||
}
|
|
||||||
}, [years, selectedYear])
|
|
||||||
|
|
||||||
// Transform and filter data based on selected year
|
|
||||||
const currentApbdes = dataAPBDes.length > 0
|
|
||||||
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
|
|
||||||
: null
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos="relative" bg={colors.Bg} py="xl" gap={32}>
|
<Stack pos="relative" bg={colors.Bg} py="xl" gap={32}>
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<Box px={{ base: 'md', md: 100 }}>
|
||||||
@@ -113,31 +94,8 @@ function Page() {
|
|||||||
))}
|
))}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
)}
|
)}
|
||||||
{/* 🔥 COMBOBOX UNTUK PILIH TAHUN */}
|
<APBDesTable />
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<APBDesProgress />
|
||||||
<Select
|
|
||||||
label="Pilih Tahun APBDes"
|
|
||||||
placeholder="Pilih tahun"
|
|
||||||
value={selectedYear}
|
|
||||||
onChange={setSelectedYear}
|
|
||||||
data={years}
|
|
||||||
w={{ base: '100%', sm: 200 }}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
nothingFoundMessage="Tidak ada tahun tersedia"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
{/* ❗ Pass currentApbdes ke komponen anak */}
|
|
||||||
{currentApbdes ? (
|
|
||||||
<>
|
|
||||||
<APBDesTable apbdesData={currentApbdes} />
|
|
||||||
<APBDesProgress apbdesData={currentApbdes} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Box px={{ base: 'md', md: 100 }} py="md">
|
|
||||||
<Text c="dimmed">Tidak ada data APBDes untuk tahun yang dipilih.</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
</Stack>
|
</Stack>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
import apbdes from '@/app/admin/(dashboard)/_state/landing-page/apbdes'
|
||||||
import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress'
|
import APBDesProgress from '@/app/darmasaba/(tambahan)/apbdes/lib/apbDesaProgress'
|
||||||
import { transformAPBDesData } from '@/app/darmasaba/(tambahan)/apbdes/lib/types'
|
|
||||||
import colors from '@/con/colors'
|
import colors from '@/con/colors'
|
||||||
import { ActionIcon, BackgroundImage, Box, Button, Center, Group, Loader, Select, SimpleGrid, Stack, Text } from '@mantine/core'
|
import { ActionIcon, BackgroundImage, Box, Button, Center, Flex, Group, Loader, SimpleGrid, Stack, Text } from '@mantine/core'
|
||||||
import { IconDownload } from '@tabler/icons-react'
|
import { IconDownload } from '@tabler/icons-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
@@ -14,7 +12,6 @@ import { useProxy } from 'valtio/utils'
|
|||||||
function Apbdes() {
|
function Apbdes() {
|
||||||
const state = useProxy(apbdes)
|
const state = useProxy(apbdes)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
const [selectedYear, setSelectedYear] = useState<string | null>(null)
|
|
||||||
|
|
||||||
const textHeading = {
|
const textHeading = {
|
||||||
title: 'APBDes',
|
title: 'APBDes',
|
||||||
@@ -35,24 +32,6 @@ function Apbdes() {
|
|||||||
loadData()
|
loadData()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const dataAPBDes = state.findMany.data || []
|
|
||||||
|
|
||||||
const years = Array.from(new Set(dataAPBDes.map((item: any) => item.tahun)))
|
|
||||||
.sort((a, b) => b - a) // urutkan descending
|
|
||||||
.map(year => ({ value: year.toString(), label: `Tahun ${year}` }))
|
|
||||||
|
|
||||||
// Pilih tahun pertama sebagai default jika belum ada yang dipilih
|
|
||||||
useEffect(() => {
|
|
||||||
if (years.length > 0 && !selectedYear) {
|
|
||||||
setSelectedYear(years[0].value)
|
|
||||||
}
|
|
||||||
}, [years, selectedYear])
|
|
||||||
|
|
||||||
// Transform and filter data based on selected year
|
|
||||||
const currentApbdes = dataAPBDes.length > 0
|
|
||||||
? transformAPBDesData(dataAPBDes.find(item => item?.tahun?.toString() === selectedYear) || dataAPBDes[0])
|
|
||||||
: null
|
|
||||||
|
|
||||||
const data = (state.findMany.data || []).slice(0, 3)
|
const data = (state.findMany.data || []).slice(0, 3)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -81,30 +60,8 @@ function Apbdes() {
|
|||||||
</Button>
|
</Button>
|
||||||
</Group>
|
</Group>
|
||||||
|
|
||||||
{/* 🔥 COMBOBOX UNTUK PILIH TAHUN */}
|
{/* Chart */}
|
||||||
<Box px={{ base: 'md', md: 100 }}>
|
<APBDesProgress />
|
||||||
<Select
|
|
||||||
label="Pilih Tahun APBDes"
|
|
||||||
placeholder="Pilih tahun"
|
|
||||||
value={selectedYear}
|
|
||||||
onChange={setSelectedYear}
|
|
||||||
data={years}
|
|
||||||
w={{ base: '100%', sm: 200 }}
|
|
||||||
searchable
|
|
||||||
clearable
|
|
||||||
nothingFoundMessage="Tidak ada tahun tersedia"
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{currentApbdes ? (
|
|
||||||
<>
|
|
||||||
<APBDesProgress apbdesData={currentApbdes} />
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Box px={{ base: 'md', md: 100 }} py="md">
|
|
||||||
<Text c="dimmed">Tidak ada data APBDes untuk tahun yang dipilih.</Text>
|
|
||||||
</Box>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<SimpleGrid mx={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 3 }} spacing="lg" pb={"xl"}>
|
<SimpleGrid mx={{ base: 'md', md: 100 }} cols={{ base: 1, sm: 3 }} spacing="lg" pb={"xl"}>
|
||||||
{loading ? (
|
{loading ? (
|
||||||
@@ -133,7 +90,7 @@ function Apbdes() {
|
|||||||
style={{ overflow: 'hidden' }}
|
style={{ overflow: 'hidden' }}
|
||||||
>
|
>
|
||||||
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
|
<Box pos="absolute" inset={0} bg="rgba(0,0,0,0.45)" style={{ borderRadius: 16 }} />
|
||||||
<Stack gap={"xs"} justify="space-between" h="100%" p="xl" pos="relative">
|
<Stack justify="space-between" h="100%" p="xl" pos="relative">
|
||||||
<Text
|
<Text
|
||||||
c="white"
|
c="white"
|
||||||
fw={600}
|
fw={600}
|
||||||
@@ -152,20 +109,7 @@ function Apbdes() {
|
|||||||
>
|
>
|
||||||
{v.jumlah}
|
{v.jumlah}
|
||||||
</Text>
|
</Text>
|
||||||
<Center>
|
<Group justify="center">
|
||||||
<ActionIcon
|
|
||||||
component={Link}
|
|
||||||
href={v.file?.link || ''}
|
|
||||||
radius="xl"
|
|
||||||
size="xl"
|
|
||||||
variant="gradient"
|
|
||||||
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
|
|
||||||
>
|
|
||||||
<IconDownload size={20} color="white" />
|
|
||||||
</ActionIcon>
|
|
||||||
|
|
||||||
</Center>
|
|
||||||
{/* <Group justify="center">
|
|
||||||
<ActionIcon
|
<ActionIcon
|
||||||
component={Link}
|
component={Link}
|
||||||
href={v.file?.link || ''}
|
href={v.file?.link || ''}
|
||||||
@@ -174,11 +118,11 @@ function Apbdes() {
|
|||||||
variant="gradient"
|
variant="gradient"
|
||||||
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
|
gradient={{ from: '#1C6EA4', to: '#1C6EA4' }}
|
||||||
>
|
>
|
||||||
<Group align="center" gap="xs" px="md" py={6}>
|
<Flex align="center" gap="xs" px="md" py={6}>
|
||||||
<IconDownload size={25} color="white" />
|
<IconDownload size={18} color="white" />
|
||||||
</Group>
|
</Flex>
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Group> */}
|
</Group>
|
||||||
</Stack>
|
</Stack>
|
||||||
</BackgroundImage>
|
</BackgroundImage>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
import { ActionIcon, Card, Flex, Image, Text, Tooltip } from "@mantine/core";
|
||||||
import { sosmedMap } from "@/app/admin/(dashboard)/landing-page/profil/_lib/sosmed";
|
|
||||||
import colors from "@/con/colors";
|
|
||||||
import { ActionIcon, Box, Card, Flex, Image, Text, Tooltip } from "@mantine/core";
|
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { useTransitionRouter } from "next-view-transitions";
|
import { useTransitionRouter } from "next-view-transitions";
|
||||||
|
import { IconBrandInstagram, IconBrandFacebook, IconBrandTwitter, IconWorld } from "@tabler/icons-react";
|
||||||
|
|
||||||
function SosmedView({
|
function SosmedView({
|
||||||
data,
|
data,
|
||||||
@@ -12,12 +10,17 @@ function SosmedView({
|
|||||||
}) {
|
}) {
|
||||||
const router = useTransitionRouter();
|
const router = useTransitionRouter();
|
||||||
|
|
||||||
const getIconSource = (item: any) => {
|
const fallbackIcon = (platform?: string) => {
|
||||||
if (item.image?.link) return item.image.link;
|
switch (platform?.toLowerCase()) {
|
||||||
if (item.icon && sosmedMap[item.icon as keyof typeof sosmedMap]?.src) {
|
case "instagram":
|
||||||
return sosmedMap[item.icon as keyof typeof sosmedMap].src;
|
return <IconBrandInstagram size={22} />;
|
||||||
|
case "facebook":
|
||||||
|
return <IconBrandFacebook size={22} />;
|
||||||
|
case "twitter":
|
||||||
|
return <IconBrandTwitter size={22} />;
|
||||||
|
default:
|
||||||
|
return <IconWorld size={22} />;
|
||||||
}
|
}
|
||||||
return null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -41,22 +44,18 @@ function SosmedView({
|
|||||||
boxShadow: "0 0 12px rgba(28, 110, 164, 0.6)",
|
boxShadow: "0 0 12px rgba(28, 110, 164, 0.6)",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(() => {
|
{item.image?.link ? (
|
||||||
const src = getIconSource(item);
|
|
||||||
|
|
||||||
if (src) {
|
|
||||||
return (
|
|
||||||
<Image
|
<Image
|
||||||
|
src={item.image.link}
|
||||||
|
alt={item.name || "ikon"}
|
||||||
|
w={24}
|
||||||
|
h={24}
|
||||||
|
fit="contain"
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
src={src}
|
|
||||||
alt={item.name}
|
|
||||||
fit={item.image?.link ? "cover" : "contain"}
|
|
||||||
/>
|
/>
|
||||||
);
|
) : (
|
||||||
}
|
fallbackIcon(item.name)
|
||||||
|
)}
|
||||||
return <Box bg={colors['blue-button']} w="100%" h="100%" />;
|
|
||||||
})()}
|
|
||||||
</ActionIcon>
|
</ActionIcon>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
))
|
))
|
||||||
|
|||||||
@@ -1,173 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="id">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Syarat & Ketentuan Penggunaan HIPMI Badung Connect</title>
|
|
||||||
<style>
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
|
||||||
line-height: 1.6;
|
|
||||||
color: #1e293b;
|
|
||||||
background-color: #f8fafc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.container {
|
|
||||||
max-width: 800px;
|
|
||||||
margin: 0 auto;
|
|
||||||
padding: 40px 20px;
|
|
||||||
background-color: white;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e3a5f;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #1e3a5f;
|
|
||||||
margin-top: 2.5rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
strong {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #1e293b;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-left: 1.5rem;
|
|
||||||
margin-bottom: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
color: #334155;
|
|
||||||
}
|
|
||||||
|
|
||||||
.intro {
|
|
||||||
margin-bottom: 2rem;
|
|
||||||
padding: 1.25rem;
|
|
||||||
background-color: #f1f5f9;
|
|
||||||
border-radius: 8px;
|
|
||||||
border-left: 4px solid #1e3a5f;
|
|
||||||
}
|
|
||||||
|
|
||||||
.footer {
|
|
||||||
margin-top: 3rem;
|
|
||||||
padding-top: 2rem;
|
|
||||||
border-top: 1px solid #e2e8f0;
|
|
||||||
text-align: center;
|
|
||||||
color: #64748b;
|
|
||||||
font-size: 0.875rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
|
||||||
.container {
|
|
||||||
padding: 24px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
margin-top: 2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
|
||||||
h1 {
|
|
||||||
font-size: 1.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-size: 1.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
margin-left: 1.25rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div class="container">
|
|
||||||
<h1>Syarat & Ketentuan Penggunaan HIPMI Badung Connect</h1>
|
|
||||||
|
|
||||||
<div class="intro">
|
|
||||||
<p>Dengan menggunakan aplikasi <strong>HIPMI Badung Connect</strong> ("Aplikasi"), Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan berikut. Jika Anda tidak setuju dengan ketentuan ini, harap jangan gunakan Aplikasi.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<h2>1. Definisi</h2>
|
|
||||||
<p><strong>HIPMI Badung Connect</strong> adalah platform digital resmi untuk anggota Himpunan Pengusaha Muda Indonesia (HIPMI) Kabupaten Badung, yang bertujuan memfasilitasi jaringan, kolaborasi, dan pertumbuhan bisnis para pengusaha muda.</p>
|
|
||||||
|
|
||||||
<h2>2. Larangan Konten Tidak Pantas</h2>
|
|
||||||
<p>Anda <strong>dilarang keras</strong> memposting, mengirim, membagikan, atau mengunggah konten apa pun yang mengandung:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, Ras, Antar-golongan)</li>
|
|
||||||
<li>Pornografi, konten seksual eksplisit, atau gambar tidak senonoh</li>
|
|
||||||
<li>Ancaman, pelecehan, bullying, atau perilaku melecehkan</li>
|
|
||||||
<li>Informasi palsu, hoaks, spam, atau konten menyesatkan</li>
|
|
||||||
<li>Konten ilegal, melanggar hukum, atau melanggar hak kekayaan intelektual pihak lain</li>
|
|
||||||
<li>Promosi narkoba, perjudian, atau aktivitas ilegal lainnya</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<h2>3. Tanggung Jawab Pengguna</h2>
|
|
||||||
<p>Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau bagikan melalui fitur-fitur berikut:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Profil (bio, foto, portofolio)</li>
|
|
||||||
<li>Forum diskusi</li>
|
|
||||||
<li>Chat pribadi atau grup</li>
|
|
||||||
<li>Lowongan kerja, investasi, dan donasi</li>
|
|
||||||
</ul>
|
|
||||||
<p>Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa pemberitahuan.</p>
|
|
||||||
|
|
||||||
<h2>4. Tindakan terhadap Pelanggaran</h2>
|
|
||||||
<p>Jika kami menerima laporan atau menemukan konten yang melanggar ketentuan ini, kami akan:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Segera menghapus konten tersebut</li>
|
|
||||||
<li>Memberikan peringatan atau memblokir akun pengguna</li>
|
|
||||||
<li>Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang berlaku</li>
|
|
||||||
</ul>
|
|
||||||
<p>Tim kami berkomitmen untuk menanggapi laporan konten tidak pantas <strong>dalam waktu 24 jam</strong>.</p>
|
|
||||||
|
|
||||||
<h2>5. Mekanisme Pelaporan</h2>
|
|
||||||
<p>Anda dapat melaporkan konten atau pengguna yang mencurigakan melalui:</p>
|
|
||||||
<ul>
|
|
||||||
<li>Tombol <strong>"Laporkan"</strong> di setiap posting forum atau pesan chat</li>
|
|
||||||
<li>Tombol <strong>"Blokir Pengguna"</strong> di profil pengguna</li>
|
|
||||||
</ul>
|
|
||||||
<p>Setiap laporan akan ditangani secara rahasia dan segera.</p>
|
|
||||||
|
|
||||||
<h2>6. Perubahan Ketentuan</h2>
|
|
||||||
<p>Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi terbaru akan dipublikasikan di halaman ini dengan tanggal revisi yang diperbarui.</p>
|
|
||||||
|
|
||||||
<h2>7. Kontak</h2>
|
|
||||||
<p>Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi kami di:<br>
|
|
||||||
<strong>bip.baliinteraktifperkasa@gmail.com</strong></p>
|
|
||||||
|
|
||||||
<div class="footer">
|
|
||||||
© 2025 Bali Interaktif Perkasa. All rights reserved.
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import "@mantine/core/styles.css";
|
import "@mantine/core/styles.css";
|
||||||
import "./globals.css";
|
import "./globals.css"; // Sisanya import di globals.css
|
||||||
|
|
||||||
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
import LoadDataFirstClient from "@/app/darmasaba/_com/LoadDataFirstClient";
|
||||||
import {
|
import {
|
||||||
@@ -8,28 +8,16 @@ import {
|
|||||||
createTheme,
|
createTheme,
|
||||||
mantineHtmlProps,
|
mantineHtmlProps,
|
||||||
} from "@mantine/core";
|
} from "@mantine/core";
|
||||||
import { Metadata, Viewport } from "next";
|
import { Metadata } from "next";
|
||||||
import { ViewTransitions } from "next-view-transitions";
|
import { ViewTransitions } from "next-view-transitions";
|
||||||
import { ToastContainer } from "react-toastify";
|
import { ToastContainer } from "react-toastify";
|
||||||
|
|
||||||
// ✅ Pisahkan viewport ke export tersendiri
|
|
||||||
export const viewport: Viewport = {
|
|
||||||
width: "device-width",
|
|
||||||
initialScale: 1,
|
|
||||||
maximumScale: 5,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
// ✅ Tambahkan metadataBase
|
|
||||||
metadataBase: new URL("https://cld-dkr-staging-desa-darmasaba.wibudev.com"),
|
|
||||||
|
|
||||||
title: {
|
title: {
|
||||||
default: "Desa Darmasaba",
|
default: "Desa Darmasaba",
|
||||||
template: "%s | Desa Darmasaba",
|
template: "%s | Desa Darmasaba",
|
||||||
},
|
},
|
||||||
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali. Informasi layanan publik, berita, dan profil desa.",
|
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali. Informasi layanan publik, berita, dan profil desa.",
|
||||||
// ❌ HAPUS viewport dari sini
|
|
||||||
keywords: [
|
keywords: [
|
||||||
"desa darmasaba",
|
"desa darmasaba",
|
||||||
"darmasaba",
|
"darmasaba",
|
||||||
@@ -75,6 +63,17 @@ export const metadata: Metadata = {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Desa Darmasaba - Kabupaten Badung, Bali",
|
||||||
|
description: "Website resmi Desa Darmasaba, Kabupaten Badung, Bali",
|
||||||
|
images: ["/assets/images/darmasaba-icon.png"],
|
||||||
|
},
|
||||||
|
verification: {
|
||||||
|
// google: "your-google-verification-code", // Tambahkan jika sudah punya
|
||||||
|
// yandex: "your-yandex-verification-code",
|
||||||
|
// yahoo: "your-yahoo-verification-code",
|
||||||
|
},
|
||||||
category: "government",
|
category: "government",
|
||||||
other: {
|
other: {
|
||||||
"msapplication-TileColor": "#ffffff",
|
"msapplication-TileColor": "#ffffff",
|
||||||
@@ -97,19 +96,18 @@ export default function RootLayout({
|
|||||||
<ViewTransitions>
|
<ViewTransitions>
|
||||||
<html lang="id" {...mantineHtmlProps}>
|
<html lang="id" {...mantineHtmlProps}>
|
||||||
<head>
|
<head>
|
||||||
<meta charSet="utf-8" />
|
|
||||||
<ColorSchemeScript />
|
<ColorSchemeScript />
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<MantineProvider theme={theme}>
|
<MantineProvider theme={theme}>
|
||||||
{children}
|
{children}
|
||||||
<LoadDataFirstClient />
|
<LoadDataFirstClient />
|
||||||
|
</MantineProvider>
|
||||||
<ToastContainer
|
<ToastContainer
|
||||||
position="bottom-center"
|
position="bottom-center"
|
||||||
hideProgressBar
|
hideProgressBar
|
||||||
style={{ zIndex: 9999 }}
|
style={{ zIndex: 9999 }}
|
||||||
/>
|
/>
|
||||||
</MantineProvider>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
</ViewTransitions>
|
</ViewTransitions>
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
import { Box, Container, Divider, List, ListItem, Paper, Stack, Text, Title } from '@mantine/core';
|
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
function Page() {
|
|
||||||
return (
|
|
||||||
<Container size="md" py={40}>
|
|
||||||
<Stack gap="xl">
|
|
||||||
<Title order={1} size="h1" fw={700} c="blue.9">
|
|
||||||
Syarat & Ketentuan Penggunaan Admin Desa Darmasaba
|
|
||||||
</Title>
|
|
||||||
|
|
||||||
<Paper p="lg" radius="md" withBorder bg="gray.0" style={{ borderLeft: '4px solid #1e3a5f' }}>
|
|
||||||
<Text c="gray.8">
|
|
||||||
Dengan menggunakan website <Text component="span" fw={600}>Admin Desa Darmasaba</Text> ("Website"),
|
|
||||||
Anda setuju untuk mematuhi dan terikat oleh syarat dan ketentuan berikut. Jika Anda tidak setuju
|
|
||||||
dengan ketentuan ini, harap jangan gunakan Website.
|
|
||||||
</Text>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
1. Definisi
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7">
|
|
||||||
<Text component="span" fw={600}>Admin Desa Darmasaba</Text> adalah website resmi untuk Admin Desa Darmasaba, yang bertujuan
|
|
||||||
menambahkan, menghapus, dan mengedit konten desa ke dalam website.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
2. Larangan Konten Tidak Pantas
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7" mb="md">
|
|
||||||
Anda <Text component="span" fw={600}>dilarang keras</Text> menambahkan, menghapus, dan mengedit konten desa apa pun yang mengandung:
|
|
||||||
</Text>
|
|
||||||
<List spacing="xs" c="gray.7">
|
|
||||||
<ListItem>Ujaran kebencian, diskriminasi, atau konten SARA (Suku, Agama, Ras, Antar-golongan)</ListItem>
|
|
||||||
<ListItem>Pornografi, konten seksual eksplisit, atau gambar tidak senonoh</ListItem>
|
|
||||||
<ListItem>Ancaman, pelecehan, bullying, atau perilaku melecehkan</ListItem>
|
|
||||||
<ListItem>Informasi palsu, hoaks, spam, atau konten menyesatkan</ListItem>
|
|
||||||
<ListItem>Konten ilegal, melanggar hukum, atau melanggar hak kekayaan intelektual pihak lain</ListItem>
|
|
||||||
<ListItem>Promosi narkoba, perjudian, atau aktivitas ilegal lainnya</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
3. Tanggung Jawab Pengguna
|
|
||||||
</Title>
|
|
||||||
<List spacing="xs" c="gray.7">
|
|
||||||
<ListItem>Anda bertanggung jawab penuh atas setiap konten yang Anda unggah atau bagikan.</ListItem>
|
|
||||||
<ListItem>Konten yang melanggar ketentuan ini dapat dihapus kapan saja tanpa pemberitahuan.</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
4. Tindakan terhadap Pelanggaran
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7" mb="md">
|
|
||||||
Jika kami menerima laporan atau menemukan konten yang melanggar ketentuan ini, kami akan:
|
|
||||||
</Text>
|
|
||||||
<List spacing="xs" c="gray.7">
|
|
||||||
<ListItem>Segera menghapus konten tersebut</ListItem>
|
|
||||||
<ListItem>Menghapus akun pengguna</ListItem>
|
|
||||||
<ListItem>Dalam kasus berat, melaporkan ke pihak berwajib sesuai hukum yang berlaku</ListItem>
|
|
||||||
</List>
|
|
||||||
</Box>
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
5. Perubahan Ketentuan
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7">
|
|
||||||
Kami berhak memperbarui Syarat & Ketentuan ini sewaktu-waktu. Versi terbaru akan dipublikasikan di
|
|
||||||
halaman ini dengan tanggal revisi yang diperbarui.
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Box>
|
|
||||||
<Title order={2} size="h2" fw={700} c="blue.9" mb="md">
|
|
||||||
6. Kontak
|
|
||||||
</Title>
|
|
||||||
<Text c="gray.7">
|
|
||||||
Jika Anda memiliki pertanyaan tentang ketentuan ini, silakan hubungi kami di:
|
|
||||||
</Text>
|
|
||||||
<Text c="gray.7" fw={600} mt="xs">
|
|
||||||
bip.baliinteraktifperkasa@gmail.com
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<Divider my="xl" />
|
|
||||||
|
|
||||||
<Text ta="center" c="gray.6" size="sm">
|
|
||||||
© 2025 Bali Interaktif Perkasa. All rights reserved.
|
|
||||||
</Text>
|
|
||||||
</Stack>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Page;
|
|
||||||
Reference in New Issue
Block a user