Fix Menu Lingkungan Darmasaba User

This commit is contained in:
2025-08-26 17:49:33 +08:00
parent b21e1f0c2e
commit 3a726a3334
36 changed files with 2509 additions and 1977 deletions

View File

@@ -1,64 +1,81 @@
import profileLandingPageState from "@/app/admin/(dashboard)/_state/landing-page/profile";
import { Center, Image, Paper, SimpleGrid, Text } from "@mantine/core";
import { Box, Center, Image, Paper, SimpleGrid, Stack, Text, Tooltip } from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { motion } from 'framer-motion';
import { useTransitionRouter } from 'next-view-transitions';
import { motion } from "framer-motion";
import { useTransitionRouter } from "next-view-transitions";
import { useProxy } from "valtio/utils";
import { Prisma } from "@prisma/client";
import { IconPhotoOff } from "@tabler/icons-react";
type ProgramInovasiItem = Prisma.ProgramInovasiGetPayload<{ include: { image: true } }>;
function ModuleItem({ data }: { data: ProgramInovasiItem }) {
const router = useTransitionRouter();
return (
<Paper
onClick={() => {
router.push(`/${data.name}`);
}}
p={"md"}
bg={"white"}
radius={"32"}
pos={"relative"}
>
<Center h={"100%"}>
<motion.div
whileHover={{ scale: 1.05 }}
<motion.div whileHover={{ scale: 1.04 }}>
<Tooltip label={`Lihat ${data.name}`} withArrow>
<Paper
onClick={() => router.push(`/${data.name}`)}
p="xl"
radius="2xl"
bg="white"
className="cursor-pointer transition-all shadow-md hover:shadow-xl"
>
{data.image?.link ? (
<Image src={data.image.link} alt="icon"
fit="contain"
sizes="100%"
loading="lazy"
style={{
objectFit: "contain",
objectPosition: "center"
}}
/>
) : (
<Text>
-
</Text>
)}
</motion.div>
</Center>
</Paper>
<Center h={180}>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name}
fit="contain"
radius="lg"
loading="lazy"
style={{ objectFit: "contain", objectPosition: "center" }}
/>
) : (
<Stack align="center" gap="xs">
<IconPhotoOff size={40} stroke={1.5} />
<Text size="sm" c="dimmed">
Belum ada gambar
</Text>
</Stack>
)}
</Center>
<Box mt="md">
<Text fw={600} ta="center" size="lg" c="black">
{data.name}
</Text>
</Box>
</Paper>
</Tooltip>
</motion.div>
);
}
function ModuleView() {
const listImageState = useProxy(profileLandingPageState.programInovasi)
const listImageState = useProxy(profileLandingPageState.programInovasi);
useShallowEffect(() => {
listImageState.findMany.load()
}, [])
listImageState.findMany.load();
}, []);
if (!listImageState.findMany.loading && !listImageState.findMany.data?.length) {
return (
<Center h={320}>
<Stack align="center" gap="sm">
<IconPhotoOff size={54} stroke={1.5} />
<Text size="lg" fw={600} c="white">
Belum ada program inovasi
</Text>
<Text size="sm" c="dimmed">
Tambahkan program inovasi untuk ditampilkan di sini
</Text>
</Stack>
</Center>
);
}
return (
<SimpleGrid
cols={{
base: 2,
md: 3,
}}
>
<SimpleGrid cols={{ base: 1, sm: 2, md: 3 }} spacing="xl" mt="lg">
{listImageState.findMany.data?.map((item) => (
<ModuleItem key={item.id} data={item} />
))}

View File

@@ -1,15 +1,28 @@
import colors from '@/con/colors';
import { Box, Card, Image, Stack, Text } from '@mantine/core';
import { Box, Card, Image, Stack, Text, Tooltip } from '@mantine/core';
import { IconUserCircle } from '@tabler/icons-react';
import React from 'react';
import { Prisma } from '@prisma/client';
import colors from '@/con/colors';
interface ProfileViewProps {
data: Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null;
}
function ProfileView({ data }: ProfileViewProps) {
export default function ProfileView({ data }: ProfileViewProps) {
if (!data) {
return <div>No profile data available</div>;
return (
<Card radius="2xl" className="glass3" py="xl" px="lg" withBorder>
<Stack align="center" gap="sm">
<IconUserCircle size={72} stroke={1.4} />
<Text fw={500} c="dimmed">
Profil belum tersedia
</Text>
<Text fz="sm" c="dimmed">
Data pejabat desa akan muncul di sini
</Text>
</Stack>
</Card>
);
}
return (
@@ -17,42 +30,38 @@ function ProfileView({ data }: ProfileViewProps) {
justify="end"
align="end"
pos="relative"
w={{
base: "100%",
md: "40%",
}}
w={{ base: '100%', md: '40%' }}
px="xl"
>
{data.image?.link ? (
<Image
src={data.image.link}
alt={data.name || "Profile image"}
sizes="100%"
fit="contain"
alt={data.name || 'Foto profil'}
fit="cover"
radius="lg"
/>
): (
<Text>
-
</Text>
) : (
<Stack align="center" gap="xs" w="100%" py="xl">
<IconUserCircle size={96} stroke={1.5} />
<Text c="dimmed" fz="sm">
Belum ada foto
</Text>
</Stack>
)}
<Box
pos="absolute"
bottom={0}
p={{
base: "xs",
md: "md",
}}
>
<Box pos="absolute" bottom={0} w="100%" p={{ base: 'xs', md: 'md' }}>
<Card
px="lg"
radius="32"
radius="2xl"
withBorder
className="glass3"
style={{
border: `1px solid white`,
}}
style={{ border: '1px solid rgba(255,255,255,0.15)' }}
>
<Text>{data.position}</Text>
<Text c={colors["blue-button"]} fw="bolder" fz="1rem">
<Tooltip label="Jabatan Resmi" withArrow>
<Text fz="sm" c="dimmed">
{data.position || 'Tidak ada jabatan'}
</Text>
</Tooltip>
<Text c={colors['blue-button']} fw={700} fz="xl" mt={4}>
{data.name}
</Text>
</Card>
@@ -60,5 +69,3 @@ function ProfileView({ data }: ProfileViewProps) {
</Stack>
);
}
export default ProfileView;

View File

@@ -1,35 +1,79 @@
import { ActionIcon, Flex, Image, Text } from "@mantine/core";
import { ActionIcon, Card, Flex, Image, Text, Tooltip } from "@mantine/core";
import { Prisma } from "@prisma/client";
import { useTransitionRouter } from "next-view-transitions";
import { IconBrandInstagram, IconBrandFacebook, IconBrandTwitter, IconWorld } from "@tabler/icons-react";
function SosmedView({data} : {data : Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]}) {
function SosmedView({
data,
}: {
data: Prisma.MediaSosialGetPayload<{ include: { image: true } }>[];
}) {
const router = useTransitionRouter();
const fallbackIcon = (platform?: string) => {
switch (platform?.toLowerCase()) {
case "instagram":
return <IconBrandInstagram size={22} />;
case "facebook":
return <IconBrandFacebook size={22} />;
case "twitter":
return <IconBrandTwitter size={22} />;
default:
return <IconWorld size={22} />;
}
};
return (
<Flex gap={"md"} justify={"center"} align={"center"}>
{data?.map((item, k) => {
return (
<Flex gap="lg" justify="center" align="center" wrap="wrap">
{data && data.length > 0 ? (
data.map((item, k) => (
<Tooltip
key={k}
label={item.name || "Tautan Sosial"}
withArrow
position="top"
transitionProps={{ transition: "pop", duration: 150 }}
>
<ActionIcon
variant="transparent"
key={k}
w={32}
h={32}
pos={"relative"}
onClick={() => {
router.push(item.iconUrl || "");
variant="light"
radius="xl"
size="xl"
onClick={() => item.iconUrl && router.push(item.iconUrl)}
style={{
transition: "all 0.3s ease",
boxShadow: "0 0 12px rgba(28, 110, 164, 0.6)",
}}
>
{item.image?.link ? (
<Image src={item.image.link} alt="icon" loading="lazy" />
<Image
src={item.image.link}
alt={item.name || "ikon"}
w={24}
h={24}
fit="contain"
loading="lazy"
/>
) : (
<Text>
none
</Text>
fallbackIcon(item.name)
)}
</ActionIcon>
);
})}
</Tooltip>
))
) : (
<Card
shadow="md"
radius="xl"
p="lg"
withBorder
style={{
background: "linear-gradient(135deg, #1C6EA4 0%, #000 100%)",
}}
>
<Text ta="center" c="dimmed" size="sm">
Belum ada media sosial yang terhubung
</Text>
</Card>
)}
</Flex>
);
}

View File

@@ -11,79 +11,77 @@ import {
Image,
Paper,
Stack,
Text
Text,
Center,
Tooltip,
Badge,
} from "@mantine/core";
import { IconCalendarTime, IconInfoCircle } from "@tabler/icons-react";
import { useEffect, useState } from "react";
import ModuleView from "./ModuleView";
import SosmedView from "./SosmedView";
import ProfileView from "./ProfileView";
const getDayOfWeek = () => {
const days = ['Minggu', 'Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu'];
const days = ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"];
const today = new Date();
return days[today.getDay()];
}
};
const getCurrentTime = () => {
const now = new Date();
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, "0");
const minutes = String(now.getMinutes()).padStart(2, "0");
return `${hours}:${minutes}`;
}
};
const isWorkingHours = (currentTime: string): boolean => {
const [openTime, closeTime] = ['08:00', '16:00'];
const [openTime, closeTime] = ["08:00", "16:00"];
const compareTimes = (time1: string, time2: string) => {
const [hour1, minute1] = time1.split(':').map(Number);
const [hour2, minute2] = time2.split(':').map(Number);
const [hour1, minute1] = time1.split(":").map(Number);
const [hour2, minute2] = time2.split(":").map(Number);
if (hour1 < hour2) return true;
if (hour1 > hour2) return false;
return minute1 <= minute2;
};
return compareTimes(currentTime, closeTime) && !compareTimes(currentTime, openTime);
}
};
const getWorkStatus = (day: string, currentTime: string): { status: string; message: string } => {
const workingDays = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat'];
const workingDays = ["Senin", "Selasa", "Rabu", "Kamis", "Jumat"];
if (!workingDays.includes(day)) {
return {
status: 'Tutup',
message: 'Sabtu - Minggu'
}
return { status: "Tutup", message: "Libur Akhir Pekan" };
}
const isOpen = isWorkingHours(currentTime)
return isOpen ? { status: 'Buka', message: '08:00 - 16:00' } : { status: 'Tutup', message: '08:00 - 16:00' };
}
const isOpen = isWorkingHours(currentTime);
return isOpen
? { status: "Buka", message: "08:00 - 16:00" }
: { status: "Tutup", message: "08:00 - 16:00" };
};
function LandingPage() {
const [socialMedia, setSocialMedia] = useState<Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]>([]);
const [profile, setProfile] = useState<Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null>(null);
const [socialMedia, setSocialMedia] = useState<
Prisma.MediaSosialGetPayload<{ include: { image: true } }>[]
>([]);
const [profile, setProfile] = useState<
Prisma.PejabatDesaGetPayload<{ include: { image: true } }> | null
>(null);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchSocialMedia = async () => {
try {
const response = await fetch('/api/landingpage/mediasosial/findMany');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const response = await fetch("/api/landingpage/mediasosial/findMany");
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
// Ensure the data is an array before setting it
if (Array.isArray(result.data)) {
setSocialMedia(result.data);
} else if (Array.isArray(result)) {
// In case the API returns the array directly
setSocialMedia(result);
} else {
console.error('Unexpected API response format:', result);
setSocialMedia([]);
}
} catch (error) {
console.error('Error fetching social media:', error);
setSocialMedia([]); // Ensure we always have an array
} catch {
setSocialMedia([]);
} finally {
setIsLoading(false);
}
@@ -92,22 +90,22 @@ function LandingPage() {
const fetchProfile = async () => {
try {
const response = await fetch(`/api/landingpage/pejabatdesa/edit`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const result = await response.json();
setProfile(result.data || null); // Handle single object response
} catch (error) {
console.error('Error fetching profile:', error);
setProfile(result.data || null);
} catch {
setProfile(null);
}
};
fetchSocialMedia();
fetchProfile();
}, []);
const [workStatus, setWorkStatus] = useState<{ status: string; message: string }>
({ status: '', message: '' });
const [workStatus, setWorkStatus] = useState<{ status: string; message: string }>({
status: "",
message: "",
});
useEffect(() => {
const updateWorkStatus = () => {
@@ -115,212 +113,110 @@ function LandingPage() {
const time = getCurrentTime();
const status = getWorkStatus(day, time);
setWorkStatus(status);
}
};
updateWorkStatus();
const intervalId = setInterval(updateWorkStatus, 60 * 1000);
return () => clearInterval(intervalId);
}, []);
return (
<Stack bg={colors["Bg"]}>
<Flex
gap={"md"}
wrap={{
base: "wrap",
md: "nowrap",
}}
>
<Stack
gap={"xl"}
w={{
base: "100%",
md: "60%",
}}
py={{
base: "xs",
md: "40",
}}
px={{
base: "xs",
md: "100",
}}
>
<Card
radius={"32"}
bg={colors.grey[1]}
p={{
base: "xs",
md: "32",
}}
>
<Stack gap={42}>
<Flex
gap={"md"}
wrap={{
base: "wrap",
md: "nowrap",
}}
>
<Grid
>
<Grid.Col span={{
base: 3,
lg: 2,
md: 3,
}}>
<Box
pos={"relative"}
bg={"white"}
w={{
base: 64,
md: 72,
}}
h={{
base: 64,
md: 72,
}}
style={{
borderRadius: 24,
}}
p={"sm"}
>
<Image
src={"/darmasaba-icon.png"}
alt="icon"
sizes="100%"
/>
<Stack bg={colors.Bg} p="md" gap="xl">
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}>
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
<Stack gap="xl">
<Flex gap="md" wrap="wrap">
<Grid w="100%">
<Grid.Col span={{ base: 3, sm: 2 }}>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image src="/darmasaba-icon.png" alt="Logo Darmasaba" fit="contain" />
</Box>
</Grid.Col>
<Grid.Col span={{
base: 3,
lg: 2,
md: 3,
}}>
<Box
pos={"relative"}
w={{
base: 64,
md: 72,
}}
h={{
base: 64,
md: 72,
}}
style={{
borderRadius: 24,
}}
p={"sm"}
bg={"white"}
>
<Image
src={"/pudak-icon.png"}
alt="icon"
sizes={"100%"}
fit="contain"
/>
<Grid.Col span={{ base: 9, sm: 10 }}>
<Box bg="white" w={72} h={72} p="sm" style={{ borderRadius: 24 }}>
<Image src="/pudak-icon.png" alt="Logo Pudak" fit="contain" />
</Box>
</Grid.Col>
<Grid.Col span={{
base: 12,
lg: 12,
md: 12,
}}>
<Grid.Col span={12}>
<Paper
pos={"relative"}
bg={colors["blue-button"]}
p={10}
w={{ base: "100%", sm: "auto", md: "auto" }}
flex={{ base: "1", sm: "1", md: "1" }}
p="md"
radius="lg"
shadow="md"
style={{ position: "relative", overflow: "hidden" }}
>
<Grid
>
<GridCol span={{
base: 12,
lg: 6,
md: 6,
}}>
<Box>
<Text c={colors["white-1"]} fz={"sm"}>
Jadwal Kerja
</Text>
<Paper
w={{ base: "100%", sm: "100%", md: "auto" }}
p={5}
bg={colors["white-1"]}
>
<Flex justify={"space-between"} align={"center"}>
<Paper
w={{ base: "100%", sm: "100%", md: "auto" }}
p={5}
bg={colors["white-1"]}
<Grid gutter="md">
<GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs">
<Flex align="center" gap="xs">
<IconCalendarTime size={16} color="white" />
<Text c="white" fz="sm">Jam Operasional</Text>
</Flex>
<Paper p="sm" radius="md" bg="white">
<Tooltip label="Status saat ini berdasarkan jam operasional kantor">
<Badge
color={workStatus.status === "Buka" ? "green" : "red"}
radius="sm"
variant="filled"
>
<Box>
<Text fw="bold" fz="sm" c={workStatus.status === 'Buka' ? "black" : "red"}>
{workStatus.status}
</Text>
<Text fw="bold" fz="lg" >
{workStatus.message}
</Text>
</Box>
</Paper>
</Flex>
{workStatus.status}
</Badge>
</Tooltip>
<Text fw="bold" fz="lg">{workStatus.message}</Text>
</Paper>
</Box>
</Stack>
</GridCol>
{/* Edit yang ini */}
<GridCol span={{ base: 12, lg: 6, md: 6 }}>
<Box>
<Text c={colors["white-1"]} fz={"sm"}>
{new Intl.DateTimeFormat('id-ID', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
}).format(new Date())}
</Text>
<Paper bg={colors["white-1"]} p={10}>
<Text fz="sm" >
Status
</Text>
<Text fw="bold" fz="lg" >
{workStatus.status === 'Buka' ? 'Operasional' : 'Tutup'}
<GridCol span={{ base: 12, md: 6 }}>
<Stack gap="xs">
<Flex align="center" gap="xs">
<IconInfoCircle size={16} color="white" />
<Text c="white" fz="sm">Hari Ini</Text>
</Flex>
<Paper p="sm" radius="md" bg="white">
<Text fz="sm">Status Kantor</Text>
<Text fw="bold" fz="lg">
{workStatus.status === "Buka" ? "Sedang Beroperasi" : "Tidak Beroperasi"}
</Text>
</Paper>
</Box>
</Stack>
</GridCol>
</Grid>
</Paper>
</Grid.Col>
</Grid>
</Flex>
<ModuleView />
{isLoading ? (
<Skeleton height={32} width="100%" />
) : socialMedia.length > 0 ? (
<SosmedView data={socialMedia} />
) : (
<div>No social media links available</div>
<Center>
<Text c="dimmed">Belum ada tautan media sosial yang tersedia</Text>
</Center>
)}
<Text c={colors.trans.dark[2]} style={{
textAlign: "center"
}} >Sampaikan saran dan masukan guna kemajuan dalam pembangunan. Semua lebih mudah melalui fitur interaktif</Text>
<Text ta="center" c={colors.trans.dark[2]}>
Bagikan ide, kritik, atau saran Anda untuk mendukung pembangunan desa.
Semua lebih mudah dengan fitur interaktif yang kami sediakan.
</Text>
</Stack>
</Card>
</Stack>
{isLoading ? (
<Skeleton height={32} width="100%" />
<Skeleton height={300} width="100%" radius="lg" />
) : profile ? (
<ProfileView data={profile} />
) : (
<div>No profile available</div>
<Center w="100%">
<Text c="dimmed">Informasi profil belum tersedia</Text>
</Center>
)}
</Flex>
</Stack >
</Stack>
);
}