Api Admin Menu Desa Sub Menu Layanan Tabs 3 Pelayanan Done

This commit is contained in:
2025-06-16 15:01:15 +08:00
parent e03b071b00
commit 7bf5ee69d5
28 changed files with 1652 additions and 152 deletions

View File

@@ -0,0 +1,18 @@
export function convertYoutubeUrlToEmbed(url: string): string | null {
const watchRegex = /(?:https?:\/\/)?(?:www\.)?youtube\.com\/watch\?v=([^&]+)/;
const shortRegex = /(?:https?:\/\/)?youtu\.be\/([^?]+)/;
const matchWatch = url.match(watchRegex);
const matchShort = url.match(shortRegex);
if (matchWatch) {
return `https://www.youtube.com/embed/${matchWatch[1]}`;
}
if (matchShort) {
return `https://www.youtube.com/embed/${matchShort[1]}`;
}
return null;
}

View File

@@ -0,0 +1,33 @@
// components/YoutubeEmbed.tsx
"use client";
import { Box, Text } from "@mantine/core";
type YoutubeEmbedProps = {
url?: string;
showRawUrl?: boolean; // opsional, buat nampilin URL mentahnya
};
export default function YoutubeEmbed({ url, showRawUrl = false }: YoutubeEmbedProps) {
if (!url || !url.includes("embed")) {
return <Text c="red">Link embed Youtube tidak valid</Text>;
}
return (
<Box>
<Box
component="iframe"
src={url}
width="100%"
height={300}
allowFullScreen
style={{ borderRadius: 8 }}
/>
{showRawUrl && (
<Text fz="sm" c="dimmed" mt={5}>
{url}
</Text>
)}
</Box>
);
}

View File

@@ -3,18 +3,19 @@
import EditEditor from '@/app/admin/(dashboard)/_com/editEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { ActionIcon, Box, Button, Flex, Group, Image, Modal, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useParams, useRouter } from 'next/navigation';
import { useEffect, useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../../lib/youtube-utils';
function EditVideo() {
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [embedLink, setEmbedLink] = useState("");
const videoState = useProxy(stateGallery.video)
const params = useParams()
const [formData, setFormData] = useState({
@@ -31,10 +32,12 @@ function EditVideo() {
const data = await videoState.update.load(id);
if (data) {
setFormData({
name: data.name || '',
name: data.name || '',
deskripsi: data.deskripsi || '',
linkVideo: data.linkVideo || '',
});
const embed = convertYoutubeUrlToEmbed(data.linkVideo);
setEmbedLink(embed || "");
}
} catch (error) {
console.error('Error loading video:', error);
@@ -52,6 +55,11 @@ function EditVideo() {
deskripsi: formData.deskripsi,
linkVideo: formData.linkVideo,
};
const converted = convertYoutubeUrlToEmbed(formData.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
await videoState.update.update();
toast.success('Video berhasil diperbarui!');
router.push('/admin/desa/gallery/video');
@@ -85,22 +93,30 @@ function EditVideo() {
/>
<Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Link Video Youtube <Text span c="red" inherit>*</Text></Text>}
placeholder='Masukkan link video youtube'
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={formData.linkVideo}
onChange={(val) => {
onChange={(e) => {
setFormData({
...formData,
linkVideo: val.target.value,
linkVideo: e.currentTarget.value,
})
const embed = convertYoutubeUrlToEmbed(e.currentTarget.value);
setEmbedLink(embed || "");
}}
required
/>
<Flex pt={5} align={"center"} gap={"xs"}>
<Text c={"dimmed"} fw={"bold"} fz={"xs"}>Cara mendapatkan link video youtube</Text>
<ActionIcon size={"xs"} radius={"xl"} color='black' variant='filled' onClick={() => setModalHapus(true)}>
<Text fw={"bold"} fz={"xs"} c="white">?</Text>
</ActionIcon>
</Flex>
{embedLink && (
<iframe
className="rounded"
width="100%"
height="200"
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
)}
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
@@ -119,36 +135,6 @@ function EditVideo() {
</Group>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<Modal
opened={modalHapus}
onClose={() => setModalHapus(false)}
title={<Text fw={"bold"} fz={"xl"}>Cara mendapatkan link video youtube</Text>}
>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 1</Text>
<Text fz={"sm"}>Buka video youtube yang ingin Anda bagikan lalu klik icon titik tiga</Text>
<Image src="/video.png" w={300} alt="" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 2</Text>
<Text fz={"sm"}>Klik bagikan</Text>
<Image src="/Share.png" w={300} alt="" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 3</Text>
<Text fz={"sm"}>Klik dibagian sematkan</Text>
<Image src="/bagikanPostingan.png" w={300} alt="" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 4</Text>
<Text fz={"sm"}>Lalu copy pada bagaian srcnya aja</Text>
<Image src="/sematkan.png" w={300} alt="" />
</Box>
</Stack>
</Modal>
</Box>
);
}

View File

@@ -9,6 +9,7 @@ import { useParams, useRouter } from 'next/navigation';
import { useState } from 'react';
import { useProxy } from 'valtio/utils';
function DetailVideo() {
const videoState = useProxy(stateGallery.video)
const [modalHapus, setModalHapus] = useState(false);
@@ -55,9 +56,17 @@ function DetailVideo() {
<Text fz={"lg"}>{videoState.findUnique.data?.name}</Text>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Link Video</Text>
<Text fz={"lg"}>{videoState.findUnique.data?.linkVideo}</Text>
<Text fw={"bold"} fz={"lg"}>Video</Text>
<Box component="iframe"
src={convertToEmbedUrl(videoState.findUnique.data?.linkVideo)}
width="100%"
height={300}
allowFullScreen
style={{ borderRadius: 8 }}
/>
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Tanggal Video</Text>
<Text fz={"lg"}>{new Date(videoState.findUnique.data?.createdAt).toDateString()}</Text>
@@ -105,7 +114,20 @@ function DetailVideo() {
text='Apakah anda yakin ingin menghapus berita ini?'
/>
</Box>
);
function convertToEmbedUrl(youtubeUrl: string): string {
try {
const url = new URL(youtubeUrl);
const videoId = url.searchParams.get("v");
if (!videoId) return youtubeUrl;
return `https://www.youtube.com/embed/${videoId}`;
} catch (err) {
console.error("Error converting YouTube URL to embed:", err);
return youtubeUrl;
}
}
}
export default DetailVideo;

View File

@@ -2,18 +2,21 @@
import CreateEditor from '@/app/admin/(dashboard)/_com/createEditor';
import stateGallery from '@/app/admin/(dashboard)/_state/desa/gallery';
import colors from '@/con/colors';
import { ActionIcon, Box, Button, Flex, Group, Image, Modal, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { Box, Button, Group, Paper, Stack, Text, TextInput, Title } from '@mantine/core';
import { IconArrowBack } from '@tabler/icons-react';
import { useRouter } from 'next/navigation';
import { useState } from 'react';
import { toast } from 'react-toastify';
import { useProxy } from 'valtio/utils';
import { convertYoutubeUrlToEmbed } from '../../lib/youtube-utils';
function CreateVideo() {
const videoState = useProxy(stateGallery.video)
const router = useRouter();
const [modalHapus, setModalHapus] = useState(false);
const [link, setLink] = useState("");
const [embedLink, setEmbedLink] = useState("");
const resetForm = () => {
videoState.create.form = {
@@ -22,8 +25,12 @@ function CreateVideo() {
linkVideo: "",
};
};
const handleSubmit = async () => {
const converted = convertYoutubeUrlToEmbed(videoState.create.form.linkVideo);
if (!converted) {
toast.error("Link YouTube tidak valid. Pastikan formatnya benar.");
return;
}
await videoState.create.create();
resetForm();
router.push("/admin/desa/gallery/video")
@@ -49,20 +56,28 @@ function CreateVideo() {
}}
/>
<Box>
<TextInput
label={<Text fw={"bold"} fz={"sm"}>Link Video Youtube <Text span c="red" inherit>*</Text></Text>}
placeholder='Masukkan link video youtube'
value={videoState.create.form.linkVideo}
onChange={(val) => {
videoState.create.form.linkVideo = val.target.value;
}}
/>
<Flex pt={5} align={"center"} gap={"xs"}>
<Text c={"dimmed"} fw={"bold"} fz={"xs"}>Cara mendapatkan link video youtube</Text>
<ActionIcon size={"xs"} radius={"xl"} color='black' variant='filled' onClick={() => setModalHapus(true)}>
<Text fw={"bold"} fz={"xs"} c="white">?</Text>
</ActionIcon>
</Flex>
<Stack gap={"xs"}>
<TextInput
label="Link Video YouTube"
placeholder="https://www.youtube.com/watch?v=abc123"
value={link}
onChange={(e) => {
setLink(e.currentTarget.value);
const embed = convertYoutubeUrlToEmbed(e.currentTarget.value);
setEmbedLink(embed || "");
}}
required
/>
{embedLink && (
<iframe
style={{ borderRadius: 10, width: "100%", height: 400 }}
src={embedLink}
title="Preview Video"
allowFullScreen
></iframe>
)}
</Stack>
</Box>
<Box>
<Text fw={"bold"} fz={"sm"}>Deskripsi Video</Text>
@@ -78,36 +93,6 @@ function CreateVideo() {
</Group>
</Stack>
</Paper>
{/* Modal Konfirmasi Hapus */}
<Modal
opened={modalHapus}
onClose={() => setModalHapus(false)}
title={<Text fw={"bold"} fz={"xl"}>Cara mendapatkan link video youtube</Text>}
>
<Stack gap={"xs"}>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 1</Text>
<Text fz={"sm"}>Buka video youtube yang ingin Anda bagikan lalu klik icon titik tiga</Text>
<Image src="/video.png" w={300} alt="" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 2</Text>
<Text fz={"sm"}>Klik bagikan</Text>
<Image src="/Share.png" w={300} alt="" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 3</Text>
<Text fz={"sm"}>Klik dibagian sematkan</Text>
<Image src="/bagikanPostingan.png" w={300} alt="" />
</Box>
<Box>
<Text fw={"bold"} fz={"lg"}>Langkah 4</Text>
<Text fz={"sm"}>Lalu copy pada bagaian srcnya aja</Text>
<Image src="/sematkan.png" w={300} alt="" />
</Box>
</Stack>
</Modal>
</Box>
);
}