Add Menu Musik
Add News Reader for Difable Add Running text news / announcement
This commit is contained in:
@@ -19,6 +19,7 @@
|
|||||||
"@elysiajs/static": "^1.3.0",
|
"@elysiajs/static": "^1.3.0",
|
||||||
"@elysiajs/stream": "^1.1.0",
|
"@elysiajs/stream": "^1.1.0",
|
||||||
"@elysiajs/swagger": "^1.2.0",
|
"@elysiajs/swagger": "^1.2.0",
|
||||||
|
"@emotion/react": "^11.14.0",
|
||||||
"@mantine/carousel": "^7.16.2",
|
"@mantine/carousel": "^7.16.2",
|
||||||
"@mantine/charts": "^7.17.1",
|
"@mantine/charts": "^7.17.1",
|
||||||
"@mantine/core": "^7.17.4",
|
"@mantine/core": "^7.17.4",
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} 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';
|
||||||
@@ -96,11 +95,9 @@ function EditMediaSosial() {
|
|||||||
py="md"
|
py="md"
|
||||||
>
|
>
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<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} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Media Sosial
|
Edit Media Sosial
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -11,8 +11,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} 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';
|
||||||
@@ -69,11 +68,9 @@ export default function CreateMediaSosial() {
|
|||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<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} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Media Sosial
|
Tambah Media Sosial
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'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, Tooltip } from '@mantine/core';
|
import { Box, Button, Center, Group, Image, Pagination, Paper, Skeleton, Stack, Table, TableTbody, TableTd, TableTh, TableThead, TableTr, Text, Title } from '@mantine/core';
|
||||||
import { useShallowEffect } from '@mantine/hooks';
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
import { IconDeviceImacCog, IconPlus, IconSearch } from '@tabler/icons-react';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
@@ -56,11 +56,9 @@ function ListMediaSosial({ search }: { search: string }) {
|
|||||||
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
<Paper withBorder bg={colors['white-1']} p={'lg'} shadow="md" radius="md">
|
||||||
<Group justify="space-between" mb="md">
|
<Group justify="space-between" mb="md">
|
||||||
<Title order={4}>Daftar Media Sosial</Title>
|
<Title order={4}>Daftar Media Sosial</Title>
|
||||||
<Tooltip label="Tambah Media Sosial" withArrow>
|
|
||||||
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
|
<Button leftSection={<IconPlus size={18} />} color="blue" variant="light" onClick={() => router.push('/admin/landing-page/profile/media-sosial/create')}>
|
||||||
Tambah Baru
|
Tambah Baru
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: "auto" }}>
|
<Box style={{ overflowX: "auto" }}>
|
||||||
<Table highlightOnHover>
|
<Table highlightOnHover>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import {
|
import {
|
||||||
Alert, Box, Button, Center, Group, Image,
|
Alert, Box, Button, Center, Group, Image,
|
||||||
Paper, Stack, Text, TextInput, Title, Tooltip
|
Paper, Stack, Text, TextInput, Title
|
||||||
} from '@mantine/core';
|
} from '@mantine/core';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -177,11 +177,9 @@ function EditPejabatDesa() {
|
|||||||
<Box>
|
<Box>
|
||||||
<Stack gap="xs">
|
<Stack gap="xs">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
||||||
<Button variant="subtle" onClick={handleBack} p="xs" radius="md">
|
<IconArrowBack color={colors['blue-button']} size={24} />
|
||||||
<IconArrowBack color={colors['blue-button']} size={24} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Pejabat Desa
|
Edit Pejabat Desa
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -13,8 +13,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip,
|
|
||||||
} 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';
|
||||||
@@ -104,11 +103,9 @@ function EditProgramInovasi() {
|
|||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<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} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Edit Program Inovasi
|
Edit Program Inovasi
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import {
|
|||||||
Stack,
|
Stack,
|
||||||
Text,
|
Text,
|
||||||
TextInput,
|
TextInput,
|
||||||
Title,
|
Title
|
||||||
Tooltip
|
|
||||||
} 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';
|
||||||
@@ -70,11 +69,9 @@ function CreateProgramInovasi() {
|
|||||||
return (
|
return (
|
||||||
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
<Box px={{ base: 'sm', md: 'lg' }} py="md">
|
||||||
<Group mb="md">
|
<Group mb="md">
|
||||||
<Tooltip label="Kembali ke halaman sebelumnya" withArrow>
|
<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} />
|
</Button>
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
<Title order={4} ml="sm" c="dark">
|
<Title order={4} ml="sm" c="dark">
|
||||||
Tambah Program Inovasi
|
Tambah Program Inovasi
|
||||||
</Title>
|
</Title>
|
||||||
|
|||||||
@@ -51,8 +51,7 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
<Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
|
<Paper bg={colors['white-1']} withBorder p="lg" radius="md" shadow="sm">
|
||||||
<Group justify='space-between'>
|
<Group justify='space-between'>
|
||||||
<Title order={4}>Daftar Program Inovasi</Title>
|
<Title order={4}>Daftar Program Inovasi</Title>
|
||||||
<Tooltip label="Tambah Program Inovasi" withArrow>
|
<Button
|
||||||
<Button
|
|
||||||
color="blue"
|
color="blue"
|
||||||
leftSection={<IconPlus size={18} />}
|
leftSection={<IconPlus size={18} />}
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -61,7 +60,6 @@ function ListProgramInovasi({ search }: { search: string }) {
|
|||||||
>
|
>
|
||||||
Tambah Program
|
Tambah Program
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
|
||||||
</Group>
|
</Group>
|
||||||
<Box style={{ overflowX: 'auto' }}>
|
<Box style={{ overflowX: 'auto' }}>
|
||||||
<Table highlightOnHover striped verticalSpacing="sm">
|
<Table highlightOnHover striped verticalSpacing="sm">
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
/* eslint-disable react-hooks/exhaustive-deps */
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
'use client'
|
'use client'
|
||||||
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
import stateDashboardBerita from '@/app/admin/(dashboard)/_state/desa/berita';
|
||||||
|
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||||
import colors from '@/con/colors';
|
import colors from '@/con/colors';
|
||||||
import { Box, Center, Container, Image, Skeleton, Stack, Text } from '@mantine/core';
|
import { Box, Center, Container, Group, Image, Skeleton, Stack, Text } from '@mantine/core';
|
||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
@@ -49,6 +50,9 @@ function Page() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
|
<Stack pos={"relative"} bg={colors.Bg} py={"xl"} gap={"22"} px={{ base: "md", md: 0 }}>
|
||||||
|
<Group px={{ base: "md", md: 100 }}>
|
||||||
|
<NewsReader />
|
||||||
|
</Group>
|
||||||
<Container w={{ base: "100%", md: "50%" }} >
|
<Container w={{ base: "100%", md: "50%" }} >
|
||||||
<Box pb={20}>
|
<Box pb={20}>
|
||||||
<Text ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
|
<Text ta={"center"} fz={"2.4rem"} c={colors["blue-button"]} fw={"bold"}>
|
||||||
@@ -67,6 +71,7 @@ function Page() {
|
|||||||
<Box px={{ base: "md", md: 100 }}>
|
<Box px={{ base: "md", md: 100 }}>
|
||||||
<Stack gap={"xs"}>
|
<Stack gap={"xs"}>
|
||||||
<Text
|
<Text
|
||||||
|
id='news-content'
|
||||||
py={20}
|
py={20}
|
||||||
fz={{ base: "sm", md: "lg" }}
|
fz={{ base: "sm", md: "lg" }}
|
||||||
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
|
lh={{ base: 1.6, md: 1.8 }} // ✅ line-height lebih rapat dan responsif
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { useShallowEffect } from '@mantine/hooks';
|
|||||||
import { useParams } from 'next/navigation';
|
import { useParams } from 'next/navigation';
|
||||||
import { useProxy } from 'valtio/utils';
|
import { useProxy } from 'valtio/utils';
|
||||||
import BackButton from '../../../layanan/_com/BackButto';
|
import BackButton from '../../../layanan/_com/BackButto';
|
||||||
|
import NewsReader from '@/app/darmasaba/_com/NewsReader';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
|
const detail = useProxy(stateDesaPengumuman.pengumuman.findUnique)
|
||||||
@@ -30,6 +31,9 @@ function Page() {
|
|||||||
<BackButton />
|
<BackButton />
|
||||||
</Box>
|
</Box>
|
||||||
<Container size="lg" px="md">
|
<Container size="lg" px="md">
|
||||||
|
<Group>
|
||||||
|
<NewsReader />
|
||||||
|
</Group>
|
||||||
<Stack gap="xs" >
|
<Stack gap="xs" >
|
||||||
<Group justify={"space-between"} align={"center"}>
|
<Group justify={"space-between"} align={"center"}>
|
||||||
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
|
<Text fz={{ base: "2rem", md: "2rem" }} c={colors["blue-button"]} fw="bold" >
|
||||||
@@ -42,7 +46,7 @@ function Page() {
|
|||||||
</Group>
|
</Group>
|
||||||
</Group>
|
</Group>
|
||||||
<Paper bg={colors["white-1"]} p="md">
|
<Paper bg={colors["white-1"]} p="md">
|
||||||
<Text fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
|
<Text id='news-content' fz={"md"} style={{wordBreak: "break-word", whiteSpace: "normal"}} dangerouslySetInnerHTML={{ __html: detail.data?.content }} />
|
||||||
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >
|
<Text fz={"md"} c={colors["blue-button"]} fw="bold" >
|
||||||
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
|
{new Date(detail.data?.createdAt).toLocaleDateString('id-ID', {
|
||||||
weekday: 'long',
|
weekday: 'long',
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import ProfilPerbekel from './ui/profilPerbekel';
|
|||||||
import MotoDesa from './ui/motoDesa';
|
import MotoDesa from './ui/motoDesa';
|
||||||
import SemuaPerbekel from './ui/semuaPerbekel';
|
import SemuaPerbekel from './ui/semuaPerbekel';
|
||||||
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton';
|
||||||
|
import StrukturPerangkatDesa from './struktur-perangkat-desa/page';
|
||||||
|
|
||||||
function Page() {
|
function Page() {
|
||||||
return (
|
return (
|
||||||
@@ -33,6 +34,7 @@ function Page() {
|
|||||||
<LambangDesa />
|
<LambangDesa />
|
||||||
<MaskotDesa />
|
<MaskotDesa />
|
||||||
<ProfilPerbekel />
|
<ProfilPerbekel />
|
||||||
|
<StrukturPerangkatDesa/>
|
||||||
<MotoDesa />
|
<MotoDesa />
|
||||||
<SemuaPerbekel />
|
<SemuaPerbekel />
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|||||||
@@ -0,0 +1,143 @@
|
|||||||
|
'use client';
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID';
|
||||||
|
import colors from '@/con/colors';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Divider,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Paper,
|
||||||
|
Skeleton,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
Title,
|
||||||
|
} from '@mantine/core';
|
||||||
|
import { useShallowEffect } from '@mantine/hooks';
|
||||||
|
import { useParams } from 'next/navigation';
|
||||||
|
import { useProxy } from 'valtio/utils';
|
||||||
|
import BackButton from '../../_com/BackButto';
|
||||||
|
|
||||||
|
function DetailPegawaiUser() {
|
||||||
|
const statePegawai = useProxy(stateStrukturPPID.pegawai);
|
||||||
|
const params = useParams();
|
||||||
|
|
||||||
|
useShallowEffect(() => {
|
||||||
|
stateStrukturPPID.posisiOrganisasi.findMany.load();
|
||||||
|
statePegawai.findUnique.load(params?.id as string);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
if (!statePegawai.findUnique.data) {
|
||||||
|
return (
|
||||||
|
<Stack py="lg">
|
||||||
|
<Skeleton height={500} radius="md" />
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = statePegawai.findUnique.data;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||||
|
{/* Back button */}
|
||||||
|
<Group mb="lg" px={{ base: 'md', md: 100 }}>
|
||||||
|
<BackButton/>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
w={{ base: '100%', md: '70%' }}
|
||||||
|
mx="auto"
|
||||||
|
p="xl"
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
bg="white"
|
||||||
|
style={{
|
||||||
|
border: '1px solid #eaeaea',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
{/* Foto Profil */}
|
||||||
|
<Image
|
||||||
|
src={data.image?.link || '/placeholder-profile.png'}
|
||||||
|
alt={data.namaLengkap || 'Foto Profil'}
|
||||||
|
w={160}
|
||||||
|
h={160}
|
||||||
|
radius={100}
|
||||||
|
fit="cover"
|
||||||
|
style={{ border: `2px solid ${colors['blue-button']}` }}
|
||||||
|
loading="lazy"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Nama & Jabatan */}
|
||||||
|
<Stack align="center" gap={2}>
|
||||||
|
<Title order={3} fw={700} c={colors['blue-button']}>
|
||||||
|
{data.namaLengkap || '-'} {data.gelarAkademik || ''}
|
||||||
|
</Title>
|
||||||
|
<Text fz="sm" c="dimmed">
|
||||||
|
{data.posisi?.nama || 'Posisi tidak tersedia'}
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Divider my="lg" />
|
||||||
|
|
||||||
|
{/* Informasi Detail */}
|
||||||
|
<Stack gap="md">
|
||||||
|
<InfoRow label="Email" value={data.email} />
|
||||||
|
<InfoRow label="Telepon" value={data.telepon} />
|
||||||
|
<InfoRow label="Alamat" value={data.alamat} multiline />
|
||||||
|
<InfoRow
|
||||||
|
label="Tanggal Masuk"
|
||||||
|
value={
|
||||||
|
data.tanggalMasuk
|
||||||
|
? new Date(data.tanggalMasuk).toLocaleDateString('id-ID', {
|
||||||
|
day: '2-digit',
|
||||||
|
month: 'long',
|
||||||
|
year: 'numeric',
|
||||||
|
})
|
||||||
|
: '-'
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<InfoRow
|
||||||
|
label="Status"
|
||||||
|
value={data.isActive ? 'Aktif' : 'Tidak Aktif'}
|
||||||
|
valueColor={data.isActive ? 'green' : 'red'}
|
||||||
|
/>
|
||||||
|
</Stack>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Komponen kecil untuk menampilkan baris informasi */
|
||||||
|
function InfoRow({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
valueColor,
|
||||||
|
multiline = false,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value?: string | null;
|
||||||
|
valueColor?: string;
|
||||||
|
multiline?: boolean;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Text fz="sm" fw={600} c="dark">
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
|
<Text
|
||||||
|
fz="sm"
|
||||||
|
c={valueColor || 'dimmed'}
|
||||||
|
style={{
|
||||||
|
whiteSpace: multiline ? 'normal' : 'nowrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{value || '-'}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DetailPegawaiUser;
|
||||||
@@ -0,0 +1,469 @@
|
|||||||
|
/* eslint-disable react-hooks/exhaustive-deps */
|
||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client'
|
||||||
|
import stateStrukturPPID from '@/app/admin/(dashboard)/_state/ppid/struktur_ppid/struktur_PPID'
|
||||||
|
import ScrollToTopButton from '@/app/darmasaba/_com/scrollToTopButton'
|
||||||
|
import colors from '@/con/colors'
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
Center,
|
||||||
|
Group,
|
||||||
|
Image,
|
||||||
|
Loader,
|
||||||
|
Paper,
|
||||||
|
Stack,
|
||||||
|
Text,
|
||||||
|
TextInput,
|
||||||
|
Title,
|
||||||
|
Transition
|
||||||
|
} from '@mantine/core'
|
||||||
|
import {
|
||||||
|
IconArrowsMaximize,
|
||||||
|
IconArrowsMinimize,
|
||||||
|
IconRefresh,
|
||||||
|
IconSearch,
|
||||||
|
IconUsers,
|
||||||
|
IconZoomIn,
|
||||||
|
IconZoomOut,
|
||||||
|
} from '@tabler/icons-react'
|
||||||
|
import { debounce } from 'lodash'
|
||||||
|
import { useTransitionRouter } from 'next-view-transitions'
|
||||||
|
import { OrganizationChart } from 'primereact/organizationchart'
|
||||||
|
import { useEffect, useRef, useState } from 'react'
|
||||||
|
import { useProxy } from 'valtio/utils'
|
||||||
|
import './struktur.css'
|
||||||
|
import BackButton from '../_com/BackButto'
|
||||||
|
|
||||||
|
export default function StrukturPerangkatDesa() {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
minHeight: '100vh',
|
||||||
|
background: colors['Bg'],
|
||||||
|
color: '#E6F0FF',
|
||||||
|
paddingBottom: 48,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box px={{ base: 'md', md: 100 }} py={"xl"}>
|
||||||
|
<BackButton />
|
||||||
|
|
||||||
|
<Stack align="center" gap="xl" mt="xl">
|
||||||
|
<Title
|
||||||
|
order={1}
|
||||||
|
ta="center"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
fz={{ base: 28, md: 36, lg: 44 }}
|
||||||
|
>
|
||||||
|
Struktur Perangkat Desa
|
||||||
|
</Title>
|
||||||
|
<Text ta="center" c="black" maw={800}>
|
||||||
|
Gambaran visual peran dan pegawai yang ditugaskan. Arahkan kursor
|
||||||
|
untuk melihat detail atau klik node untuk fokus tampilan.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Box mt="lg">
|
||||||
|
<StrukturPerangkatDesaNode />
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<ScrollToTopButton />
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function StrukturPerangkatDesaNode() {
|
||||||
|
const stateOrganisasi: any = useProxy(stateStrukturPPID.pegawai)
|
||||||
|
const router = useTransitionRouter()
|
||||||
|
const chartContainerRef = useRef<HTMLDivElement>(null)
|
||||||
|
const [scale, setScale] = useState(1)
|
||||||
|
const [isFullscreen, setFullscreen] = useState(false)
|
||||||
|
const [searchQuery, setSearchQuery] = useState('')
|
||||||
|
|
||||||
|
// debounce pencarian
|
||||||
|
const debouncedSearch = useRef(
|
||||||
|
debounce((value: string) => {
|
||||||
|
setSearchQuery(value)
|
||||||
|
}, 400)
|
||||||
|
).current
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void stateOrganisasi.findMany.load()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const isLoading =
|
||||||
|
!stateOrganisasi.findMany.data && stateOrganisasi.findMany.loading !== false
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<Center py={48}>
|
||||||
|
<Stack align="center" gap="sm">
|
||||||
|
<Loader size="lg" />
|
||||||
|
<Text fw={600}>Memuat struktur organisasi…</Text>
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
Mengambil data pegawai dan posisi. Mohon tunggu sebentar.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = stateOrganisasi.findMany.data || []
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<Center py={40}>
|
||||||
|
<Stack align="center" gap="md">
|
||||||
|
<Paper
|
||||||
|
radius="md"
|
||||||
|
p="xl"
|
||||||
|
style={{
|
||||||
|
width: 560,
|
||||||
|
background: 'rgba(28,110,164,0.2)',
|
||||||
|
border: `1px solid rgba(255,255,255,0.1)`,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Center>
|
||||||
|
<IconUsers size={56} />
|
||||||
|
</Center>
|
||||||
|
<Title order={3} mt="md">
|
||||||
|
Data pegawai belum tersedia
|
||||||
|
</Title>
|
||||||
|
<Text c="dimmed" mt="xs">
|
||||||
|
Belum ada data pegawai yang tercatat untuk PPID.
|
||||||
|
</Text>
|
||||||
|
<Group justify="center" mt="lg">
|
||||||
|
<Button
|
||||||
|
leftSection={<IconRefresh size={16} />}
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: 'indigo', to: 'cyan' }}
|
||||||
|
onClick={() => stateOrganisasi.findMany.load()}
|
||||||
|
>
|
||||||
|
Muat Ulang
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
</Stack>
|
||||||
|
</Center>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🧩 buat struktur organisasi
|
||||||
|
const posisiMap = new Map<string, any>()
|
||||||
|
const aktifPegawai = data.filter((p: any) => p.isActive)
|
||||||
|
|
||||||
|
for (const pegawai of aktifPegawai) {
|
||||||
|
const posisiId = pegawai.posisi.id
|
||||||
|
if (!posisiMap.has(posisiId)) {
|
||||||
|
posisiMap.set(posisiId, {
|
||||||
|
...pegawai.posisi,
|
||||||
|
pegawaiList: [],
|
||||||
|
children: [],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
posisiMap.get(posisiId)!.pegawaiList.push(pegawai)
|
||||||
|
}
|
||||||
|
|
||||||
|
const root: any[] = []
|
||||||
|
posisiMap.forEach((posisi) => {
|
||||||
|
if (posisi.parentId) {
|
||||||
|
const parent = posisiMap.get(posisi.parentId)
|
||||||
|
if (parent) parent.children.push(posisi)
|
||||||
|
else root.push(posisi)
|
||||||
|
} else root.push(posisi)
|
||||||
|
})
|
||||||
|
|
||||||
|
const toOrgChartFormat = (node: any): any => {
|
||||||
|
const pegawai = node.pegawaiList?.[0]
|
||||||
|
return {
|
||||||
|
expanded: true,
|
||||||
|
data: {
|
||||||
|
id: pegawai?.id,
|
||||||
|
name: pegawai?.namaLengkap || 'Belum Ditugaskan',
|
||||||
|
title: node.nama || 'Tanpa Jabatan',
|
||||||
|
image: pegawai?.image?.link || '/img/default.png',
|
||||||
|
},
|
||||||
|
children: node.children?.map(toOrgChartFormat) || [],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let chartData = root.map(toOrgChartFormat)
|
||||||
|
|
||||||
|
// 🔍 filter by search
|
||||||
|
if (searchQuery) {
|
||||||
|
const filterNodes = (nodes: any[]): any[] =>
|
||||||
|
nodes
|
||||||
|
.map((n) => ({
|
||||||
|
...n,
|
||||||
|
children: filterNodes(n.children || []),
|
||||||
|
}))
|
||||||
|
.filter(
|
||||||
|
(n) =>
|
||||||
|
n.data.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
n.data.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
|
||||||
|
n.children.length > 0
|
||||||
|
)
|
||||||
|
chartData = filterNodes(chartData)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 🎬 fullscreen & zoom control
|
||||||
|
const toggleFullscreen = () => {
|
||||||
|
if (!document.fullscreenElement) {
|
||||||
|
chartContainerRef.current?.requestFullscreen()
|
||||||
|
setFullscreen(true)
|
||||||
|
} else {
|
||||||
|
document.exitFullscreen()
|
||||||
|
setFullscreen(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleZoomIn = () => setScale((s) => Math.min(s + 0.1, 2))
|
||||||
|
const handleZoomOut = () => setScale((s) => Math.max(s - 0.1, 0.5))
|
||||||
|
const resetZoom = () => setScale(1)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack align="center" mt="xl">
|
||||||
|
{/* 🔍 Controls */}
|
||||||
|
<Paper
|
||||||
|
shadow="xs"
|
||||||
|
p="md"
|
||||||
|
radius="md"
|
||||||
|
style={{
|
||||||
|
background: colors['blue-button']
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Group gap="sm" wrap="wrap" justify="center">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari nama atau jabatan..."
|
||||||
|
leftSection={<IconSearch size={16} />}
|
||||||
|
onChange={(e) => debouncedSearch(e.target.value)}
|
||||||
|
styles={{
|
||||||
|
input: {
|
||||||
|
minWidth: 250,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Group gap="xs">
|
||||||
|
<Button
|
||||||
|
variant="light"
|
||||||
|
bg={colors['blue-button-2']}
|
||||||
|
size="sm"
|
||||||
|
onClick={handleZoomOut}
|
||||||
|
leftSection={<IconZoomOut size={16} />}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
>
|
||||||
|
Zoom Out
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
bg={colors['blue-button-2']}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
px={16}
|
||||||
|
py={8}
|
||||||
|
style={{
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: 700,
|
||||||
|
borderRadius: '8px',
|
||||||
|
minWidth: 70,
|
||||||
|
textAlign: 'center',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{Math.round(scale * 100)}%
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
bg={colors['blue-button-2']}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onClick={handleZoomIn}
|
||||||
|
leftSection={<IconZoomIn size={16} />}
|
||||||
|
>
|
||||||
|
Zoom In
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
bg={colors['blue-button-2']}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
variant="light"
|
||||||
|
size="sm"
|
||||||
|
onClick={resetZoom}
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
bg={colors['blue-button-2']}
|
||||||
|
c={colors['blue-button']}
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleFullscreen}
|
||||||
|
leftSection={
|
||||||
|
isFullscreen ? (
|
||||||
|
<IconArrowsMinimize size={16} />
|
||||||
|
) : (
|
||||||
|
<IconArrowsMaximize size={16} />
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Fullscreen
|
||||||
|
</Button>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
{/* 🧩 Chart Container */}
|
||||||
|
<Center style={{ width: '100%' }}>
|
||||||
|
<Box
|
||||||
|
ref={chartContainerRef}
|
||||||
|
style={{
|
||||||
|
overflowX: 'auto',
|
||||||
|
overflowY: 'auto',
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: '100%',
|
||||||
|
padding: '32px 16px',
|
||||||
|
transition: 'transform 0.2s ease',
|
||||||
|
transform: `scale(${scale})`,
|
||||||
|
transformOrigin: 'center top',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<OrganizationChart
|
||||||
|
value={chartData}
|
||||||
|
nodeTemplate={(node) => <NodeCard node={node} router={router} />}
|
||||||
|
className="p-organizationchart p-organizationchart-horizontal"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
</Center>
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function NodeCard({ node, router }: any) {
|
||||||
|
const imageSrc = node?.data?.image || '/img/default.png'
|
||||||
|
const name = node?.data?.name || 'Tanpa Nama'
|
||||||
|
const title = node?.data?.title || 'Tanpa Jabatan'
|
||||||
|
const hasId = Boolean(node?.data?.id)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Transition mounted transition="pop" duration={300}>
|
||||||
|
{(styles) => (
|
||||||
|
<Card
|
||||||
|
shadow="md"
|
||||||
|
radius="xl"
|
||||||
|
withBorder
|
||||||
|
style={{
|
||||||
|
...styles,
|
||||||
|
width: 240,
|
||||||
|
minHeight: 280,
|
||||||
|
padding: 20,
|
||||||
|
background: 'linear-gradient(135deg, rgba(28,110,164,0.15) 0%, rgba(255,255,255,0.95) 100%)',
|
||||||
|
borderColor: 'rgba(28, 110, 164, 0.3)',
|
||||||
|
borderWidth: 2,
|
||||||
|
transition: 'all 0.3s ease',
|
||||||
|
cursor: hasId ? 'pointer' : 'default',
|
||||||
|
}}
|
||||||
|
onMouseEnter={(e) => {
|
||||||
|
if (hasId) {
|
||||||
|
e.currentTarget.style.transform = 'translateY(-4px)'
|
||||||
|
e.currentTarget.style.boxShadow = '0 8px 24px rgba(28, 110, 164, 0.25)'
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
onMouseLeave={(e) => {
|
||||||
|
if (hasId) {
|
||||||
|
e.currentTarget.style.transform = 'translateY(0)'
|
||||||
|
e.currentTarget.style.boxShadow = ''
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack align="center" gap={12}>
|
||||||
|
{/* Photo */}
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
width: 96,
|
||||||
|
height: 96,
|
||||||
|
borderRadius: '50%',
|
||||||
|
overflow: 'hidden',
|
||||||
|
border: '3px solid rgba(28, 110, 164, 0.4)',
|
||||||
|
boxShadow: '0 4px 12px rgba(0, 0, 0, 0.1)',
|
||||||
|
background: 'white',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={imageSrc}
|
||||||
|
alt={name}
|
||||||
|
width={96}
|
||||||
|
height={96}
|
||||||
|
fit="cover"
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
objectFit: 'cover',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Name */}
|
||||||
|
<Text
|
||||||
|
fw={700}
|
||||||
|
size="sm"
|
||||||
|
ta="center"
|
||||||
|
c={colors['blue-button']}
|
||||||
|
lineClamp={2}
|
||||||
|
style={{
|
||||||
|
minHeight: 40,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
lineHeight: 1.3,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Title/Position */}
|
||||||
|
<Text
|
||||||
|
size="xs"
|
||||||
|
c="dimmed"
|
||||||
|
ta="center"
|
||||||
|
fw={500}
|
||||||
|
lineClamp={2}
|
||||||
|
style={{
|
||||||
|
minHeight: 32,
|
||||||
|
display: 'flex',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
lineHeight: 1.2,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{title}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* Detail Button */}
|
||||||
|
{hasId && (
|
||||||
|
<Button
|
||||||
|
variant="gradient"
|
||||||
|
gradient={{ from: 'blue', to: 'cyan' }}
|
||||||
|
size="xs"
|
||||||
|
fullWidth
|
||||||
|
mt={8}
|
||||||
|
radius="md"
|
||||||
|
onClick={() =>
|
||||||
|
router.push(`/darmasaba/desa/profile/struktur-perangkat-desa/${node.data.id}`)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
height: 32,
|
||||||
|
fontWeight: 600,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Lihat Detail
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
/* ============================================
|
||||||
|
STRUKTUR ORGANISASI PPID - STYLING
|
||||||
|
============================================ */
|
||||||
|
|
||||||
|
/* Tabel chart selalu center */
|
||||||
|
.p-organizationchart-table {
|
||||||
|
margin: 0 auto !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jarak vertikal antar level - lebih lega */
|
||||||
|
.p-organizationchart-line-down {
|
||||||
|
height: 32px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Padding di dalam node - lebih rapi */
|
||||||
|
.p-organizationchart-node-content {
|
||||||
|
padding: 0 !important;
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garis connector antar node - lebih tebal dan jelas */
|
||||||
|
.p-organizationchart-line-down,
|
||||||
|
.p-organizationchart-line-left,
|
||||||
|
.p-organizationchart-line-right,
|
||||||
|
.p-organizationchart-line-top {
|
||||||
|
border-color: rgba(28, 110, 164, 0.4) !important;
|
||||||
|
border-width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Garis horizontal */
|
||||||
|
.p-organizationchart-line-left,
|
||||||
|
.p-organizationchart-line-right {
|
||||||
|
border-top-width: 2px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Jarak horizontal antar node - lebih proporsional */
|
||||||
|
.p-organizationchart-table > tbody > tr > td {
|
||||||
|
padding: 0 24px !important;
|
||||||
|
vertical-align: top !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Node container spacing */
|
||||||
|
.p-organizationchart-node {
|
||||||
|
padding: 8px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive adjustments */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.p-organizationchart-table > tbody > tr > td {
|
||||||
|
padding: 0 12px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.p-organizationchart-line-down {
|
||||||
|
height: 24px !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Smooth transitions untuk zoom */
|
||||||
|
.p-organizationchart {
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fullscreen mode adjustments */
|
||||||
|
.p-organizationchart-table:fullscreen {
|
||||||
|
background: rgba(230, 240, 255, 0.98);
|
||||||
|
padding: 40px;
|
||||||
|
}
|
||||||
248
src/app/darmasaba/(pages)/musik/musik-desa/page.tsx
Normal file
248
src/app/darmasaba/(pages)/musik/musik-desa/page.tsx
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||||
|
'use client'
|
||||||
|
import { ActionIcon, Avatar, Badge, Box, Card, Flex, Grid, Group, Paper, Slider, Stack, Text, TextInput } from '@mantine/core';
|
||||||
|
import { IconArrowsShuffle, IconPlayerPauseFilled, IconPlayerPlayFilled, IconPlayerSkipBackFilled, IconPlayerSkipForwardFilled, IconRepeat, IconRepeatOff, IconSearch, IconVolume, IconVolumeOff, IconX } from '@tabler/icons-react';
|
||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import BackButton from '../../desa/layanan/_com/BackButto';
|
||||||
|
|
||||||
|
const MusicPlayer = () => {
|
||||||
|
const [isPlaying, setIsPlaying] = useState(false);
|
||||||
|
const [currentTime, setCurrentTime] = useState(0);
|
||||||
|
const [duration, setDuration] = useState(245);
|
||||||
|
const [volume, setVolume] = useState(70);
|
||||||
|
const [isMuted, setIsMuted] = useState(false);
|
||||||
|
const [isRepeat, setIsRepeat] = useState(false);
|
||||||
|
const [isShuffle, setIsShuffle] = useState(false);
|
||||||
|
|
||||||
|
const songs = [
|
||||||
|
{ id: 1, title: 'Midnight Dreams', artist: 'The Wanderers', duration: '4:05', cover: 'https://images.unsplash.com/photo-1470225620780-dba8ba36b745?w=400&h=400&fit=crop' },
|
||||||
|
{ id: 2, title: 'Summer Breeze', artist: 'Coastal Vibes', duration: '3:42', cover: 'https://images.unsplash.com/photo-1493225457124-a3eb161ffa5f?w=400&h=400&fit=crop' },
|
||||||
|
{ id: 3, title: 'City Lights', artist: 'Urban Echo', duration: '4:18', cover: 'https://images.unsplash.com/photo-1514320291840-2e0a9bf2a9ae?w=400&h=400&fit=crop' },
|
||||||
|
{ id: 4, title: 'Ocean Waves', artist: 'Serenity Sound', duration: '5:20', cover: 'https://images.unsplash.com/photo-1459749411175-04bf5292ceea?w=400&h=400&fit=crop' },
|
||||||
|
{ id: 5, title: 'Neon Nights', artist: 'Electric Dreams', duration: '3:55', cover: 'https://images.unsplash.com/photo-1487180144351-b8472da7d491?w=400&h=400&fit=crop' },
|
||||||
|
{ id: 6, title: 'Mountain High', artist: 'Peak Performers', duration: '4:32', cover: 'https://images.unsplash.com/photo-1511671782779-c97d3d27a1d4?w=400&h=400&fit=crop' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const [currentSong, setCurrentSong] = useState(songs[0]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: any;
|
||||||
|
if (isPlaying) {
|
||||||
|
interval = setInterval(() => {
|
||||||
|
setCurrentTime(prev => {
|
||||||
|
if (prev >= duration) {
|
||||||
|
setIsPlaying(false);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return prev + 1;
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
}
|
||||||
|
return () => clearInterval(interval);
|
||||||
|
}, [isPlaying, duration]);
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const mins = Math.floor(seconds / 60);
|
||||||
|
const secs = Math.floor(seconds % 60);
|
||||||
|
return `${mins}:${secs.toString().padStart(2, '0')}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const playSong = (song: any) => {
|
||||||
|
setCurrentSong(song);
|
||||||
|
setCurrentTime(0);
|
||||||
|
setIsPlaying(true);
|
||||||
|
const durationInSeconds = parseInt(song.duration.split(':')[0]) * 60 + parseInt(song.duration.split(':')[1]);
|
||||||
|
setDuration(durationInSeconds);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMute = () => {
|
||||||
|
setIsMuted(!isMuted);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box px={{ base: 'md', md: 100 }} py="xl">
|
||||||
|
<Paper
|
||||||
|
mx="auto"
|
||||||
|
p="xl"
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
bg="white"
|
||||||
|
style={{
|
||||||
|
border: '1px solid #eaeaea',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Stack gap="md">
|
||||||
|
<BackButton />
|
||||||
|
<Group justify="space-between" mb="xl" mt={"md"}>
|
||||||
|
<div>
|
||||||
|
<Text size="32px" fw={700} c="#0B4F78">Selamat Datang Kembali</Text>
|
||||||
|
<Text size="md" c="#5A6C7D">Temukan musik favorit Anda hari ini</Text>
|
||||||
|
</div>
|
||||||
|
<Group gap="md">
|
||||||
|
<TextInput
|
||||||
|
placeholder="Cari lagu..."
|
||||||
|
leftSection={<IconSearch size={18} />}
|
||||||
|
radius="xl"
|
||||||
|
w={280}
|
||||||
|
styles={{ input: { backgroundColor: '#fff' } }}
|
||||||
|
/>
|
||||||
|
</Group>
|
||||||
|
</Group>
|
||||||
|
<Stack gap="xl">
|
||||||
|
<div>
|
||||||
|
<Text size="xl" fw={700} c="#0B4F78" mb="md">Sedang Diputar</Text>
|
||||||
|
<Card radius="md" p="xl" shadow="md">
|
||||||
|
<Group align="center" gap="xl">
|
||||||
|
<Avatar src={currentSong.cover} size={180} radius="md" />
|
||||||
|
<Stack gap="md" style={{ flex: 1 }}>
|
||||||
|
<div>
|
||||||
|
<Text size="28px" fw={700} c="#0B4F78">{currentSong.title}</Text>
|
||||||
|
<Text size="lg" c="#5A6C7D">{currentSong.artist}</Text>
|
||||||
|
</div>
|
||||||
|
<Group gap="xs" align="center">
|
||||||
|
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(currentTime)}</Text>
|
||||||
|
<Slider
|
||||||
|
value={currentTime}
|
||||||
|
max={duration}
|
||||||
|
onChange={setCurrentTime}
|
||||||
|
color="#0B4F78"
|
||||||
|
size="sm"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
styles={{ thumb: { borderWidth: 2 } }}
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="#5A6C7D" w={42}>{formatTime(duration)}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Text size="xl" fw={700} c="#0B4F78" mb="md">Daftar Putar</Text>
|
||||||
|
<Grid gutter="md">
|
||||||
|
{songs.map(song => (
|
||||||
|
<Grid.Col span={{ base: 12, sm: 6, lg: 4 }} key={song.id}>
|
||||||
|
<Card
|
||||||
|
radius="md"
|
||||||
|
p="md"
|
||||||
|
shadow="sm"
|
||||||
|
style={{
|
||||||
|
cursor: 'pointer',
|
||||||
|
border: currentSong.id === song.id ? '2px solid #0B4F78' : '2px solid transparent',
|
||||||
|
transition: 'all 0.2s'
|
||||||
|
}}
|
||||||
|
onClick={() => playSong(song)}
|
||||||
|
>
|
||||||
|
<Group gap="md" align="center">
|
||||||
|
<Avatar src={song.cover} size={64} radius="md" />
|
||||||
|
<Stack gap={4} style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Text size="sm" fw={600} c="#0B4F78" truncate>{song.title}</Text>
|
||||||
|
<Text size="xs" c="#5A6C7D">{song.artist}</Text>
|
||||||
|
<Text size="xs" c="#8A9BA8">{song.duration}</Text>
|
||||||
|
</Stack>
|
||||||
|
{currentSong.id === song.id && isPlaying && (
|
||||||
|
<Badge color="#0B4F78" variant="filled">Memutar</Badge>
|
||||||
|
)}
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
</Grid.Col>
|
||||||
|
))}
|
||||||
|
</Grid>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
</Paper>
|
||||||
|
|
||||||
|
<Paper
|
||||||
|
mt="xl"
|
||||||
|
mx="auto"
|
||||||
|
p="xl"
|
||||||
|
radius="lg"
|
||||||
|
shadow="sm"
|
||||||
|
bg="white"
|
||||||
|
style={{
|
||||||
|
border: '1px solid #eaeaea',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Flex align="center" justify="space-between" gap="xl" h="100%">
|
||||||
|
<Group gap="md" style={{ flex: 1 }}>
|
||||||
|
<Avatar src={currentSong.cover} size={56} radius="md" />
|
||||||
|
<div style={{ flex: 1, minWidth: 0 }}>
|
||||||
|
<Text size="sm" fw={600} c="#0B4F78" truncate>{currentSong.title}</Text>
|
||||||
|
<Text size="xs" c="#5A6C7D">{currentSong.artist}</Text>
|
||||||
|
</div>
|
||||||
|
</Group>
|
||||||
|
|
||||||
|
<Stack gap="xs" style={{ flex: 1 }} align="center">
|
||||||
|
<Group gap="md">
|
||||||
|
<ActionIcon
|
||||||
|
variant={isShuffle ? 'filled' : 'subtle'}
|
||||||
|
color="#0B4F78"
|
||||||
|
onClick={() => setIsShuffle(!isShuffle)}
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
{isShuffle ? <IconArrowsShuffle size={18} /> : <IconX size={18} />}
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl">
|
||||||
|
<IconPlayerSkipBackFilled size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant="filled"
|
||||||
|
color="#0B4F78"
|
||||||
|
size={56}
|
||||||
|
radius="xl"
|
||||||
|
onClick={() => setIsPlaying(!isPlaying)}
|
||||||
|
>
|
||||||
|
{isPlaying ? <IconPlayerPauseFilled size={26} /> : <IconPlayerPlayFilled size={26} />}
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon variant="light" color="#0B4F78" size={40} radius="xl">
|
||||||
|
<IconPlayerSkipForwardFilled size={20} />
|
||||||
|
</ActionIcon>
|
||||||
|
<ActionIcon
|
||||||
|
variant={isRepeat ? 'filled' : 'subtle'}
|
||||||
|
color="#0B4F78"
|
||||||
|
onClick={() => setIsRepeat(!isRepeat)}
|
||||||
|
radius="xl"
|
||||||
|
>
|
||||||
|
{isRepeat ? <IconRepeat size={18} /> : <IconRepeatOff size={18} />}
|
||||||
|
</ActionIcon>
|
||||||
|
</Group>
|
||||||
|
<Group gap="xs" style={{ width: '100%', maxWidth: 500 }}>
|
||||||
|
<Text size="xs" c="#5A6C7D" w={40} ta="right">{formatTime(currentTime)}</Text>
|
||||||
|
<Slider
|
||||||
|
value={currentTime}
|
||||||
|
max={duration}
|
||||||
|
onChange={setCurrentTime}
|
||||||
|
color="#0B4F78"
|
||||||
|
size="xs"
|
||||||
|
style={{ flex: 1 }}
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="#5A6C7D" w={40}>{formatTime(duration)}</Text>
|
||||||
|
</Group>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<Group gap="xs" style={{ flex: 1 }} justify="flex-end">
|
||||||
|
<ActionIcon variant="subtle" color="gray" onClick={toggleMute}>
|
||||||
|
{isMuted || volume === 0 ? <IconVolumeOff size={20} /> : <IconVolume size={20} />}
|
||||||
|
</ActionIcon>
|
||||||
|
<Slider
|
||||||
|
value={isMuted ? 0 : volume}
|
||||||
|
onChange={(val) => {
|
||||||
|
setVolume(val);
|
||||||
|
if (val > 0) setIsMuted(false);
|
||||||
|
}}
|
||||||
|
color="#0B4F78"
|
||||||
|
size="xs"
|
||||||
|
w={100}
|
||||||
|
/>
|
||||||
|
<Text size="xs" c="#5A6C7D" w={32}>{isMuted ? 0 : volume}%</Text>
|
||||||
|
</Group>
|
||||||
|
</Flex>
|
||||||
|
</Paper>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MusicPlayer;
|
||||||
96
src/app/darmasaba/_com/NewsReader.tsx
Normal file
96
src/app/darmasaba/_com/NewsReader.tsx
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
'use client';
|
||||||
|
import { Button } from '@mantine/core';
|
||||||
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
const NewsReader = () => {
|
||||||
|
const [isSpeaking, setIsSpeaking] = useState(false);
|
||||||
|
const [isAllowed, setIsAllowed] = useState(false);
|
||||||
|
const utteranceRef = useRef<SpeechSynthesisUtterance | null>(null);
|
||||||
|
|
||||||
|
// Fungsi untuk membaca teks
|
||||||
|
const speakText = () => {
|
||||||
|
if (typeof window === 'undefined' || !window.speechSynthesis) {
|
||||||
|
console.warn('Browser tidak mendukung SpeechSynthesis.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const contentElement = document.getElementById('news-content');
|
||||||
|
const rawText = contentElement?.innerText || '';
|
||||||
|
if (!rawText.trim()) return;
|
||||||
|
|
||||||
|
// Hentikan semua suara sebelumnya
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
|
||||||
|
const utterance = new SpeechSynthesisUtterance(rawText);
|
||||||
|
utterance.lang = 'id-ID';
|
||||||
|
utterance.rate = 1;
|
||||||
|
utterance.pitch = 1;
|
||||||
|
|
||||||
|
utterance.onstart = () => setIsSpeaking(true);
|
||||||
|
utterance.onend = () => setIsSpeaking(false);
|
||||||
|
|
||||||
|
utteranceRef.current = utterance;
|
||||||
|
|
||||||
|
try {
|
||||||
|
window.speechSynthesis.speak(utterance);
|
||||||
|
} catch (err) {
|
||||||
|
console.warn('Autoplay gagal karena kebijakan browser:', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto play jika sudah pernah diizinkan
|
||||||
|
useEffect(() => {
|
||||||
|
const hasPermission = localStorage.getItem('ttsAllowed') === 'true';
|
||||||
|
setIsAllowed(hasPermission);
|
||||||
|
|
||||||
|
if (hasPermission) {
|
||||||
|
const trySpeak = setInterval(() => {
|
||||||
|
const contentElement = document.getElementById('news-content');
|
||||||
|
if (contentElement && contentElement.innerText.trim()) {
|
||||||
|
speakText();
|
||||||
|
clearInterval(trySpeak);
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(trySpeak);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Hentikan suara saat user keluar halaman / komponen unmount
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (typeof window !== 'undefined' && window.speechSynthesis) {
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
setIsSpeaking(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Handle tombol manual
|
||||||
|
const handleToggle = () => {
|
||||||
|
if (isSpeaking) {
|
||||||
|
window.speechSynthesis.cancel();
|
||||||
|
setIsSpeaking(false);
|
||||||
|
} else {
|
||||||
|
if (!isAllowed) {
|
||||||
|
localStorage.setItem('ttsAllowed', 'true');
|
||||||
|
setIsAllowed(true);
|
||||||
|
}
|
||||||
|
speakText();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
onClick={handleToggle}
|
||||||
|
color="#0B4F78"
|
||||||
|
variant="filled"
|
||||||
|
radius="xl"
|
||||||
|
size="md"
|
||||||
|
mt="md"
|
||||||
|
>
|
||||||
|
{isSpeaking ? '🔇 Hentikan Suara' : '🔊 Dengarkan Berita'}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default NewsReader;
|
||||||
185
src/app/darmasaba/_com/RunningText.tsx
Normal file
185
src/app/darmasaba/_com/RunningText.tsx
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { Box } from "@mantine/core";
|
||||||
|
import { IconBell } from "@tabler/icons-react";
|
||||||
|
import { useMemo, useState, useEffect } from "react";
|
||||||
|
|
||||||
|
interface RunningTextProps {
|
||||||
|
news?: string[];
|
||||||
|
speed?: number; // dalam detik (jika mau manual)
|
||||||
|
autoSpeed?: boolean; // otomatis sesuaikan speed dengan panjang text
|
||||||
|
bgColor?: string;
|
||||||
|
textColor?: string;
|
||||||
|
maxLength?: number; // max karakter per item
|
||||||
|
}
|
||||||
|
|
||||||
|
// Utility function untuk strip HTML (works on both server and client)
|
||||||
|
function stripHtmlTags(html: string): string {
|
||||||
|
const text = html
|
||||||
|
.replace(/<style[^>]*>.*?<\/style>/gi, '')
|
||||||
|
.replace(/<script[^>]*>.*?<\/script>/gi, '')
|
||||||
|
.replace(/<[^>]+>/g, '')
|
||||||
|
.replace(/ /gi, ' ')
|
||||||
|
.replace(/&/gi, '&')
|
||||||
|
.replace(/</gi, '<')
|
||||||
|
.replace(/>/gi, '>')
|
||||||
|
.replace(/"/gi, '"')
|
||||||
|
.replace(/'/gi, "'")
|
||||||
|
.replace(/’/gi, "'")
|
||||||
|
.replace(/—/gi, '—')
|
||||||
|
.replace(/–/gi, '–')
|
||||||
|
.replace(/\s+/g, ' ')
|
||||||
|
.trim();
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function RunningText({
|
||||||
|
news = [
|
||||||
|
"Selamat datang di Portal Desa Darmasaba",
|
||||||
|
"Jam operasional kantor: Senin - Jumat 08:00 - 17:00",
|
||||||
|
],
|
||||||
|
speed = 20,
|
||||||
|
autoSpeed = true,
|
||||||
|
bgColor = "#1e5a7e",
|
||||||
|
textColor = "white",
|
||||||
|
maxLength = 200 // default max 200 karakter per item
|
||||||
|
}: RunningTextProps) {
|
||||||
|
|
||||||
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Process news data
|
||||||
|
const processedNews = useMemo(() => {
|
||||||
|
return news
|
||||||
|
.filter(item => item && item.trim() !== "")
|
||||||
|
.map(item => {
|
||||||
|
let text = stripHtmlTags(item);
|
||||||
|
// Limit panjang per item
|
||||||
|
if (text.length > maxLength) {
|
||||||
|
text = text.substring(0, maxLength) + "...";
|
||||||
|
}
|
||||||
|
return text;
|
||||||
|
})
|
||||||
|
.filter(item => item.length > 0);
|
||||||
|
}, [news, maxLength]);
|
||||||
|
|
||||||
|
const allNews = processedNews.length > 0
|
||||||
|
? processedNews.join(" • ")
|
||||||
|
: "Tidak ada pengumuman";
|
||||||
|
|
||||||
|
// Hitung speed berdasarkan mode
|
||||||
|
const calculatedSpeed = useMemo(() => {
|
||||||
|
if (!autoSpeed) {
|
||||||
|
return speed; // Gunakan speed manual
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto speed: berdasarkan panjang text
|
||||||
|
const textLength = allNews.length;
|
||||||
|
|
||||||
|
// Formula yang lebih natural:
|
||||||
|
// - Text pendek (< 100 char): 15 detik
|
||||||
|
// - Text sedang (100-300 char): 20-30 detik
|
||||||
|
// - Text panjang (> 300 char): 30-45 detik
|
||||||
|
let calculatedTime;
|
||||||
|
|
||||||
|
if (textLength < 100) {
|
||||||
|
calculatedTime = 15;
|
||||||
|
} else if (textLength < 300) {
|
||||||
|
calculatedTime = 15 + ((textLength - 100) / 200) * 15; // 15-30 detik
|
||||||
|
} else {
|
||||||
|
calculatedTime = 30 + Math.min(((textLength - 300) / 500) * 15, 15); // 30-45 detik max
|
||||||
|
}
|
||||||
|
|
||||||
|
return Math.round(calculatedTime);
|
||||||
|
}, [allNews, speed, autoSpeed]);
|
||||||
|
|
||||||
|
// Prevent hydration mismatch
|
||||||
|
if (!isMounted) {
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px 0",
|
||||||
|
borderBottom: "2px solid rgba(255, 255, 255, 0.1)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{
|
||||||
|
display: "inline-flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "8px",
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}>
|
||||||
|
<IconBell size={20} color={textColor} style={{ flexShrink: 0 }} />
|
||||||
|
<span style={{
|
||||||
|
color: textColor,
|
||||||
|
fontSize: "15px",
|
||||||
|
fontWeight: 500,
|
||||||
|
whiteSpace: "nowrap"
|
||||||
|
}}>
|
||||||
|
Memuat pengumuman...
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
style={{
|
||||||
|
backgroundColor: bgColor,
|
||||||
|
overflow: "hidden",
|
||||||
|
position: "relative",
|
||||||
|
width: "100%",
|
||||||
|
padding: "12px 0",
|
||||||
|
borderBottom: "2px solid rgba(255, 255, 255, 0.1)"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<style dangerouslySetInnerHTML={{
|
||||||
|
__html: `
|
||||||
|
@keyframes scrollText {
|
||||||
|
0% {
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
100% {
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.running-text-wrapper {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
animation: scrollText ${calculatedSpeed}s linear infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
.running-text-wrapper:hover {
|
||||||
|
animation-play-state: paused;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.running-text-content {
|
||||||
|
color: ${textColor};
|
||||||
|
font-size: 18px;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
}} />
|
||||||
|
|
||||||
|
<div className="running-text-wrapper">
|
||||||
|
<IconBell size={20} color={textColor} style={{ flexShrink: 0 }} />
|
||||||
|
<span className="running-text-content">
|
||||||
|
{allNews}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -124,7 +124,7 @@ function LandingPage() {
|
|||||||
<Stack bg={colors.Bg} p="md" gap="lg">
|
<Stack bg={colors.Bg} p="md" gap="lg">
|
||||||
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}>
|
<Flex gap="lg" wrap={{ base: "wrap", md: "nowrap" }}>
|
||||||
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
|
<Stack w={{ base: "100%", md: "65%" }} gap="lg">
|
||||||
<Card radius="xl" bg={colors.grey[1]} p="lg" shadow="xl">
|
<Card radius="xl" bg={colors.grey[1]} p="lg" mt={10} shadow="xl">
|
||||||
<Stack gap="xl">
|
<Stack gap="xl">
|
||||||
<Flex gap="md" wrap="wrap">
|
<Flex gap="md" wrap="wrap">
|
||||||
<Group>
|
<Group>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
'use client'
|
||||||
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
|
import DesaAntiKorupsi from "@/app/darmasaba/_com/main-page/desaantikorupsi";
|
||||||
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
|
import Kepuasan from "@/app/darmasaba/_com/main-page/kepuasan";
|
||||||
import LandingPage from "@/app/darmasaba/_com/main-page/landing-page";
|
import LandingPage from "@/app/darmasaba/_com/main-page/landing-page";
|
||||||
@@ -6,21 +7,85 @@ import Penghargaan from "@/app/darmasaba/_com/main-page/penghargaan";
|
|||||||
import Potensi from "@/app/darmasaba/_com/main-page/potensi";
|
import Potensi from "@/app/darmasaba/_com/main-page/potensi";
|
||||||
import colors from "@/con/colors";
|
import colors from "@/con/colors";
|
||||||
import SDGS from "./_com/main-page/sdgs";
|
import SDGS from "./_com/main-page/sdgs";
|
||||||
// import ApiFetch from "@/lib/api-fetch";
|
|
||||||
|
|
||||||
import { Box, Stack } from "@mantine/core";
|
import { Box, Stack } from "@mantine/core";
|
||||||
import Apbdes from "./_com/main-page/apbdes";
|
import Apbdes from "./_com/main-page/apbdes";
|
||||||
import Prestasi from "./_com/main-page/prestasi";
|
import Prestasi from "./_com/main-page/prestasi";
|
||||||
import ScrollToTopButton from "./_com/scrollToTopButton";
|
import ScrollToTopButton from "./_com/scrollToTopButton";
|
||||||
|
import RunningText from "./_com/RunningText";
|
||||||
|
import { useProxy } from "valtio/utils";
|
||||||
|
import stateDashboardBerita from "../admin/(dashboard)/_state/desa/berita";
|
||||||
|
import { useEffect, useMemo } from "react";
|
||||||
|
import stateDesaPengumuman from "../admin/(dashboard)/_state/desa/pengumuman";
|
||||||
|
|
||||||
|
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
|
const featured = useProxy(stateDashboardBerita.berita.findFirst);
|
||||||
|
const loadingFeatured = featured.loading;
|
||||||
|
const pengumuman = useProxy(stateDesaPengumuman.pengumuman.findFirst);
|
||||||
|
const loadingPengumuman = pengumuman.loading;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!featured.data && !loadingFeatured) {
|
||||||
|
stateDashboardBerita.berita.findFirst.load();
|
||||||
|
}
|
||||||
|
}, [featured.data, loadingFeatured]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!pengumuman.data && !loadingPengumuman) {
|
||||||
|
stateDesaPengumuman.pengumuman.findFirst.load();
|
||||||
|
}
|
||||||
|
}, [pengumuman.data, loadingPengumuman]);
|
||||||
|
|
||||||
|
// Memoize news data untuk performa lebih baik
|
||||||
|
const newsData = useMemo(() => {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
// Tambahkan judul berita jika ada
|
||||||
|
if (featured.data?.judul) {
|
||||||
|
items.push(`📰 BERITA: ${featured.data.judul}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan content berita (akan di-strip HTML di component)
|
||||||
|
if (featured.data?.content) {
|
||||||
|
items.push(featured.data.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan judul pengumuman jika ada
|
||||||
|
if (pengumuman.data?.judul) {
|
||||||
|
items.push(`📢 PENGUMUMAN: ${pengumuman.data.judul}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tambahkan content pengumuman
|
||||||
|
if (pengumuman.data?.content) {
|
||||||
|
items.push(pengumuman.data.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jika tidak ada data, return default message
|
||||||
|
if (items.length === 0) {
|
||||||
|
return [
|
||||||
|
"Selamat datang di Portal Desa Darmasaba",
|
||||||
|
"Jam operasional kantor: Senin - Jumat 08:00 - 17:00"
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}, [featured.data, pengumuman.data]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Stack
|
<Stack
|
||||||
bg={colors.grey[1]}
|
bg={colors.grey[1]}
|
||||||
gap={0}
|
gap={0}
|
||||||
>
|
>
|
||||||
|
<RunningText
|
||||||
|
news={newsData}
|
||||||
|
autoSpeed={false}
|
||||||
|
maxLength={150} // Potong text panjang
|
||||||
|
speed={100} // Base speed (tidak dipakai jika autoSpeed=true)
|
||||||
|
bgColor="#1e5a7e"
|
||||||
|
textColor="white"
|
||||||
|
/>
|
||||||
<LandingPage />
|
<LandingPage />
|
||||||
<Penghargaan />
|
<Penghargaan />
|
||||||
<Layanan />
|
<Layanan />
|
||||||
|
|||||||
@@ -292,7 +292,8 @@ const navbarListMenu = [
|
|||||||
href: "/darmasaba/lingkungan/konservasi-adat-bali"
|
href: "/darmasaba/lingkungan/konservasi-adat-bali"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}, {
|
},
|
||||||
|
{
|
||||||
id: "8",
|
id: "8",
|
||||||
name: "Pendidikan",
|
name: "Pendidikan",
|
||||||
children: [
|
children: [
|
||||||
@@ -332,6 +333,17 @@ const navbarListMenu = [
|
|||||||
href: "/darmasaba/pendidikan/data-pendidikan"
|
href: "/darmasaba/pendidikan/data-pendidikan"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "9",
|
||||||
|
name: "Musik",
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
id: "9.1",
|
||||||
|
name: "Musik Desa",
|
||||||
|
href: "/darmasaba/musik/musik-desa"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user