diff --git a/bun.lockb b/bun.lockb index fbe4156d..e571b0b1 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/package.json b/package.json index 4ec51ca6..472efe96 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.5", "private": true, "scripts": { - "dev": "bun --bun next dev --hostname 0.0.0.0", + "dev": "bun --bun next dev", "build": "bun --bun next build", "start": "bun --bun next start" }, @@ -43,6 +43,7 @@ "@types/bun": "^1.2.2", "@types/leaflet": "^1.9.20", "@types/lodash": "^4.17.16", + "@types/nodemailer": "^7.0.2", "add": "^2.0.6", "adm-zip": "^0.5.16", "animate.css": "^4.1.1", @@ -52,6 +53,7 @@ "classnames": "^2.5.1", "colors": "^1.4.0", "dayjs": "^1.11.13", + "dotenv": "^17.2.3", "elysia": "^1.3.5", "embla-carousel-autoplay": "^8.5.2", "embla-carousel-react": "^7.1.0", @@ -71,6 +73,7 @@ "next": "^15.5.2", "next-view-transitions": "^0.3.4", "node-fetch": "^3.3.2", + "nodemailer": "^7.0.10", "p-limit": "^6.2.0", "primeicons": "^7.0.0", "primereact": "^10.9.6", diff --git a/src/app/api/subscribe/route.ts b/src/app/api/subscribe/route.ts new file mode 100644 index 00000000..ce360546 --- /dev/null +++ b/src/app/api/subscribe/route.ts @@ -0,0 +1,54 @@ +import { NextResponse } from 'next/server'; +import nodemailer from 'nodemailer'; + +export async function POST(request: Request) { + try { + const { email } = await request.json(); + + // Input validation + if (!email) { + return NextResponse.json( + { success: false, message: 'Email is required' }, + { status: 400 } + ); + } + + // Email regex validation + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) { + return NextResponse.json( + { success: false, message: 'Invalid email format' }, + { status: 400 } + ); + } + + // Configure nodemailer + const transporter = nodemailer.createTransport({ + service: 'gmail', + auth: { + user: process.env.EMAIL_USER, + pass: process.env.EMAIL_PASS, + }, + }); + + // Send email + await transporter.sendMail({ + from: `"Tim Info" <${process.env.EMAIL_USER}>`, + to: email, + subject: '✅ Berhasil Berlangganan!', + html: `

Terima kasih telah berlangganan info terbaru dari kami!

`, + }); + + return NextResponse.json({ + success: true, + message: 'Subscription successful! Please check your email.', + }); + + } catch (error) { + console.error('Error in subscribe API:', error); + return NextResponse.json( + { success: false, message: 'Internal server error' }, + { status: 500 } + ); + } +} \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx index b24c8fc4..08f63752 100644 --- a/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx +++ b/src/app/darmasaba/(pages)/desa/layanan/_com/pelayananPerizinanBerusaha.tsx @@ -7,34 +7,52 @@ import { useEffect, useState } from 'react'; import { useProxy } from 'valtio/utils'; function PelayananPerizinanBerusaha() { - const state = useProxy(stateLayananDesa) - const [loading, setLoading] = useState(false) - const [active, setActive] = useState(1); - const nextStep = () => setActive((current) => (current < 6 ? current + 1 : current)); - const prevStep = () => setActive((current) => (current > 0 ? current - 1 : current)); + const state = useProxy(stateLayananDesa); + const [loading, setLoading] = useState(false); + const [active, setActive] = useState(0); + + const totalSteps = 6; + + const nextStep = () => { + if (active < totalSteps - 1) { + setActive(active + 1); + } else if (active === totalSteps - 1) { + setActive(totalSteps); // Mark as completed + } + }; + + const prevStep = () => { + if (active > 0) { + setActive(active - 1); + } + }; useEffect(() => { const loadData = async () => { try { setLoading(true); - await state.pelayananPerizinanBerusaha.findById.load('edit') + await state.pelayananPerizinanBerusaha.findById.load('edit'); } catch (error) { console.error('Gagal memuat data:', error); } finally { setLoading(false); } - } - loadData() - }, []) + }; + loadData(); + }, []); const data = state.pelayananPerizinanBerusaha.findById.data; - + if (!data && !loading) { return (
- Belum ada informasi layanan yang tersedia - + + Belum ada informasi layanan yang tersedia + +
); @@ -47,72 +65,111 @@ function PelayananPerizinanBerusaha() { ) : ( - - - - Perizinan Berusaha Berbasis Risiko melalui OSS - - - Sistem Online Single Submission (OSS) untuk pendaftaran NIB - - + + + + Perizinan Berusaha Berbasis Risiko melalui OSS + + + Sistem Online Single Submission (OSS) untuk pendaftaran NIB + + - + - - Alur pendaftaran NIB: - - - Membuat akun di portal OSS - - - Lengkapi informasi perusahaan, data pemegang saham, dan alamat - - - Menentukan kode KBLI sesuai jenis usaha - - - Unggah akta pendirian, surat izin, dan dokumen wajib lainnya - - - Menunggu verifikasi dan persetujuan dari pihak berwenang - - - Menerima NIB sebagai identitas resmi usaha - - -
- - - Proses pendaftaran selesai - -
-
-
+ + + Alur pendaftaran NIB: + + { + if (step <= active) { // Only allow clicking on previous or current steps + setActive(step); + } + }} + orientation="vertical" + color="blue" + radius="md" + styles={{ + step: { padding: '14px 0' }, + stepBody: { marginLeft: 8 } + }} + > + + Membuat akun di portal OSS + + + Lengkapi informasi perusahaan, data pemegang saham, dan alamat + + + Menentukan kode KBLI sesuai jenis usaha + + + Unggah akta pendirian, surat izin, dan dokumen wajib lainnya + + + Menunggu verifikasi dan persetujuan dari pihak berwenang + + + Menerima NIB sebagai identitas resmi usaha + + +
+ + + Proses pendaftaran selesai + +
+
+
+ {active < totalSteps && ( - - - -
- - Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{" "} - oss.go.id atau hubungi instansi pemerintah terkait. - -
+ {active < totalSteps ? ( + + ) : ( + + )} + + )} + + + + Catatan: Persyaratan dan prosedur dapat berubah sewaktu-waktu. Untuk informasi resmi terbaru, silakan kunjungi situs{' '} + + oss.go.id + {' '} + atau hubungi instansi pemerintah terkait. + +
)} ); } -export default PelayananPerizinanBerusaha; +export default PelayananPerizinanBerusaha; \ No newline at end of file diff --git a/src/app/darmasaba/(pages)/keamanan/laporan-publik/page.tsx b/src/app/darmasaba/(pages)/keamanan/laporan-publik/page.tsx index fc566fc0..57b83ab5 100644 --- a/src/app/darmasaba/(pages)/keamanan/laporan-publik/page.tsx +++ b/src/app/darmasaba/(pages)/keamanan/laporan-publik/page.tsx @@ -1,15 +1,15 @@ 'use client' +import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; import laporanPublikState from '@/app/admin/(dashboard)/_state/keamanan/laporan-publik'; import colors from '@/con/colors'; -import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput, Title } from '@mantine/core'; +import { Box, Button, Center, ColorSwatch, Flex, Group, Modal, Pagination, Paper, SimpleGrid, Skeleton, Stack, Text, TextInput } from '@mantine/core'; import { DateTimePicker } from '@mantine/dates'; import { useDebouncedValue, useDisclosure, useShallowEffect } from '@mantine/hooks'; import { IconArrowRight, IconPlus, IconSearch } from '@tabler/icons-react'; +import { useTransitionRouter } from 'next-view-transitions'; import { useState } from 'react'; import { useProxy } from 'valtio/utils'; import BackButton from '../../desa/layanan/_com/BackButto'; -import { useTransitionRouter } from 'next-view-transitions'; -import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor'; function Page() { const [search, setSearch] = useState(""); @@ -53,7 +53,7 @@ function Page() { return ( - + setSearch(e.target.value)} leftSection={} - w={{ base: "50%", md: "100%" }} + w={{ base: "100%", md: "30%" }} /> - + @@ -118,7 +118,7 @@ function Page() { return ( - {v.judul} + {v.judul} {v.tanggalWaktu ? new Date(v.tanggalWaktu).toLocaleString('id-ID') diff --git a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx index dc1301f2..e2fa96a7 100644 --- a/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/data-kesehatan-warga/artikel-kesehatan-page/[id]/page.tsx @@ -84,9 +84,8 @@ function Page() { - Kenali Gejala DBD + {state.findUnique.data.symptom?.title} - {state.findUnique.data.symptom?.title} diff --git a/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx b/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx index 458a9fa3..822ee8d6 100644 --- a/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx +++ b/src/app/darmasaba/(pages)/kesehatan/program-kesehatan/page.tsx @@ -126,17 +126,36 @@ export default function Page() { className="hover-scale" > - - {v.name} - +
+ + {v.name} (e.currentTarget.style.transform = 'scale(1.05)')} + onMouseLeave={(e) => (e.currentTarget.style.transform = 'scale(1)')} + /> + + + +
Dasar Hukum - + Informasi regulasi dan kebijakan resmi yang menjadi dasar hukum
diff --git a/src/app/darmasaba/(pages)/ppid/profile-ppid/page.tsx b/src/app/darmasaba/(pages)/ppid/profile-ppid/page.tsx index 7e7b99d1..f63c6d59 100644 --- a/src/app/darmasaba/(pages)/ppid/profile-ppid/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/profile-ppid/page.tsx @@ -54,28 +54,34 @@ function Page() {
Logo Desa
- + Pejabat Pengelola Informasi dan Dokumentasi - + - - -
- Foto Pimpinan -
- - + + + Foto Pimpinan e.currentTarget.src = "/perbekel.png"} + loading="lazy" + /> + + {item.name} diff --git a/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx b/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx index 84afbbc3..f6f8d745 100644 --- a/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx +++ b/src/app/darmasaba/(pages)/ppid/visi-misi-ppid/page.tsx @@ -54,12 +54,11 @@ function Page() { ta="center" fz={{ base: 28, md: 36 }} fw={800} - variant="gradient" - gradient={{ from: colors['blue-button'], to: 'cyan', deg: 45 }} + c={colors['blue-button']} > Moto PPID Desa Darmasaba - + Memberikan informasi yang cepat, mudah, tepat, dan transparan
diff --git a/src/app/darmasaba/_com/Footer.tsx b/src/app/darmasaba/_com/Footer.tsx index 7c84a976..4a7c63f7 100644 --- a/src/app/darmasaba/_com/Footer.tsx +++ b/src/app/darmasaba/_com/Footer.tsx @@ -1,6 +1,8 @@ 'use client' import { ActionIcon, Anchor, Box, Button, Center, Container, Divider, Flex, Group, Image, Paper, SimpleGrid, Stack, Text, TextInput, Title } from '@mantine/core'; import { IconAt, IconBrandFacebook, IconBrandInstagram, IconBrandTiktok, IconBrandYoutube } from '@tabler/icons-react'; +import { useRef } from 'react'; +import { toast } from 'react-toastify'; const sosialMedia = [ { @@ -60,6 +62,39 @@ const tautanPenting = [ ] function Footer() { + + const emailRef = useRef(null) + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + const email = emailRef.current?.value.trim(); + if (!email) return toast.error('Email wajib diisi!'); + + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + if (!emailRegex.test(email)) return toast.error('Format email tidak valid!'); + + try { + const res = await fetch('/api/subscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + + const data = await res.json(); + + if (res.ok && data.success) { + toast.success('Berhasil! Cek email Anda untuk konfirmasi.'); + emailRef.current!.value = ''; + } else { + toast.error(data.message || 'Gagal berlangganan.'); + } + } catch (err) { + console.error(err); + toast.error('Gagal menghubungi server. Coba lagi nanti.'); + } + }; + + return ( @@ -166,8 +201,9 @@ function Footer() { w="70%" placeholder="Masukkan email Anda" rightSection={} + ref={emailRef} // ini aja cukup /> - +
diff --git a/src/app/darmasaba/_com/main-page/apbdes/index.tsx b/src/app/darmasaba/_com/main-page/apbdes/index.tsx index c958f5fa..7dc89bb0 100644 --- a/src/app/darmasaba/_com/main-page/apbdes/index.tsx +++ b/src/app/darmasaba/_com/main-page/apbdes/index.tsx @@ -34,10 +34,10 @@ function Apbdes() { const data = (state.findMany.data || []).slice(0, 3) return ( - + - + {textHeading.title} @@ -117,7 +117,7 @@ function Apbdes() { )} - + - + @@ -416,16 +416,16 @@ function Kepuasan() { } return ( - +
- Indeks Kepuasan Masyarakat + Indeks Kepuasan Masyarakat
Ukur kebahagiaan warga, tingkatkan layanan desa! Dengan partisipasi aktif masyarakat, kami berkomitmen untuk terus memperbaiki layanan agar lebih transparan, efektif, dan sesuai dengan kebutuhan warga. Kepuasan Anda adalah prioritas utama kami dalam membangun desa yang lebih baik!
- + diff --git a/src/app/darmasaba/_com/main-page/landing-page/index.tsx b/src/app/darmasaba/_com/main-page/landing-page/index.tsx index 6910571f..470ecfe4 100644 --- a/src/app/darmasaba/_com/main-page/landing-page/index.tsx +++ b/src/app/darmasaba/_com/main-page/landing-page/index.tsx @@ -121,7 +121,7 @@ function LandingPage() { }, []); return ( - + diff --git a/src/app/darmasaba/_com/main-page/layanan/index.tsx b/src/app/darmasaba/_com/main-page/layanan/index.tsx index ef172252..128bc2e3 100644 --- a/src/app/darmasaba/_com/main-page/layanan/index.tsx +++ b/src/app/darmasaba/_com/main-page/layanan/index.tsx @@ -30,10 +30,10 @@ const textHeading = { function Layanan() { return ( - - + + - + {textHeading.title} diff --git a/src/app/darmasaba/_com/main-page/potensi/index.tsx b/src/app/darmasaba/_com/main-page/potensi/index.tsx index 66394213..ff2bf284 100644 --- a/src/app/darmasaba/_com/main-page/potensi/index.tsx +++ b/src/app/darmasaba/_com/main-page/potensi/index.tsx @@ -49,9 +49,9 @@ function Potensi() { const data = (state.findMany.data || []).slice(0, 4); return ( - - - + + + {textHeading.title} diff --git a/src/app/darmasaba/_com/main-page/prestasi/index.tsx b/src/app/darmasaba/_com/main-page/prestasi/index.tsx index 77c47cfe..13ff8133 100644 --- a/src/app/darmasaba/_com/main-page/prestasi/index.tsx +++ b/src/app/darmasaba/_com/main-page/prestasi/index.tsx @@ -35,7 +35,7 @@ function Prestasi() { - + Prestasi Desa @@ -55,7 +55,7 @@ function Prestasi() { - + {loading ? (
diff --git a/src/app/darmasaba/_com/main-page/sdgs/index.tsx b/src/app/darmasaba/_com/main-page/sdgs/index.tsx index 7897fb35..ad79d5df 100644 --- a/src/app/darmasaba/_com/main-page/sdgs/index.tsx +++ b/src/app/darmasaba/_com/main-page/sdgs/index.tsx @@ -5,6 +5,7 @@ import { useMediaQuery } from "@mantine/hooks" import { Prisma } from "@prisma/client" import Link from "next/link" import { IconMoodSad } from "@tabler/icons-react" +import colors from "@/con/colors" export default function SDGS() { const theme = useMantineTheme() @@ -41,11 +42,7 @@ export default function SDGS() { order={1} fz={{ base: "2.4rem", md: "3.6rem" }} fw={900} - style={{ - background: "linear-gradient(90deg, #1A5F7A, #159895)", - WebkitBackgroundClip: "text", - WebkitTextFillColor: "transparent", - }} + c={colors["blue-button"]} > SDGs Desa @@ -54,7 +51,7 @@ export default function SDGS() { SDGs Desa merupakan langkah nyata untuk mewujudkan desa yang maju, inklusif, dan berkelanjutan melalui 17 tujuan pembangunan dari pengentasan kemiskinan, pendidikan, kesehatan, kesetaraan gender, hingga pelestarian lingkungan. - + - + diff --git a/src/con/colors.ts b/src/con/colors.ts index 16cbba8e..250d01aa 100644 --- a/src/con/colors.ts +++ b/src/con/colors.ts @@ -1,6 +1,14 @@ const colors = { "orange": "#FCAE00", "blue-button": "#0A4E78", + "blue-button-1": "#E5F2FA", + "blue-button-2": "#B8DAEF", + "blue-button-3": "#8AC1E3", + "blue-button-4": "#5DA9D8", + "blue-button-5": "#2F91CC", + "blue-button-6": "#083F61", + "blue-button-7": "#062F49", + "blue-button-8": "#041F32", "blue-button-trans": "#628EC6", "white-1": "#FBFBFC", "white-trans-1": "rgba(255, 255, 255, 0.5)",