feat: admin app information

deskripsi:
- feature tambah stiker
This commit is contained in:
2025-05-15 15:04:30 +08:00
parent fbea35eef9
commit bc10b80139
16 changed files with 831 additions and 113 deletions

View File

@@ -28,6 +28,7 @@ import { master_nama_bank } from "@/bin/seeder/master";
import { master_status_transaksi } from "@/bin/seeder/master";
import pLimit from "p-limit";
import { master_new_bidang_bisnis } from "@/bin/seeder/master";
import { master_emotions } from "@/bin/seeder/master";
async function masterUserRole() {
for (let i of userRole) {
@@ -614,6 +615,20 @@ async function masterStatusTransaksi() {
console.log("masterStatusTransaksi success");
}
async function masterEmotions() {
await Promise.all(
master_emotions.map((a) =>
prisma.masterEmotions.upsert({
where: { value: a.value },
create: { value: a.value, label: a.label },
update: { value: a.value, label: a.label },
})
)
);
console.log("masterEmotions success");
}
const listSeederQueue = [
masterUserRole,
seederUser,
@@ -643,6 +658,7 @@ const listSeederQueue = [
masterKategoriApp,
masterInvestasiNewTransaksiStatus,
masterStatusTransaksi,
masterEmotions,
];
const limit = pLimit(1);

View File

@@ -3,6 +3,7 @@ export {
apiGetMasterBidangBisnis,
apiGetMasterStatusTransaksi,
apiGetAdminContact,
apiGetMasterEmotions,
};
const apiGetMasterBank = async () => {
@@ -90,3 +91,20 @@ const apiGetAdminContact = async () => {
throw error; // Re-throw the error to handle it in the calling function
}
};
const apiGetMasterEmotions = async () => {
const { token } = await fetch("/api/get-cookie").then((res) => res.json());
if (!token) return await token.json().catch(() => null);
const response = await fetch(`/api/master/emotions`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
"Access-Control-Allow-Origin": "*",
Authorization: `Bearer ${token}`,
},
});
return await response.json().catch(() => null);
};

View File

@@ -0,0 +1,17 @@
import { Prisma } from "@prisma/client";
export type ISticker = Prisma.StickerGetPayload<{
select: {
id: true;
name: true;
fileId: true;
emotions: true;
};
include: {
MasterEmotions: {
select: {
value: true;
};
};
};
}>;

View File

@@ -0,0 +1,67 @@
export const apiAdminCreateSticker = async ({ data }: { data: any }) => {
try {
// Fetch token from cookie
const { token } = await fetch("/api/get-cookie").then((res) => res.json());
if (!token) {
console.error("No token found");
return null;
}
const response = await fetch(`/api/sticker`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
body: JSON.stringify(data),
});
// Check if the response is OK
if (!response.ok) {
const errorData = await response.json().catch(() => null);
console.error("Failed to create sticker", response.statusText, errorData);
throw new Error(errorData?.message || "Failed to create sticker");
}
// Return the JSON response
return await response.json();
} catch (error) {
console.error("Error create sticker", error);
throw error; // Re-throw the error to handle it in the calling function
}
};
export const apiAdminGetSticker = async () => {
try {
// Fetch token from cookie
const { token } = await fetch("/api/get-cookie").then((res) => res.json());
if (!token) {
console.error("No token found");
return null;
}
const response = await fetch(`/api/sticker`, {
method: "GET",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
Authorization: `Bearer ${token}`,
},
});
// Check if the response is OK
if (!response.ok) {
const errorData = await response.json().catch(() => null);
console.error("Failed to get sticker", response.statusText, errorData);
throw new Error(errorData?.message || "Failed to get sticker");
}
// Return the JSON response
return await response.json();
} catch (error) {
console.error("Error get sticker", error);
throw error; // Re-throw the error to handle it in the calling function
}
};

View File

@@ -1,21 +1,24 @@
"use client";
import {
AspectRatio,
Box,
Button,
Center,
Chip,
Group,
Image,
Paper,
Select,
Stack,
TextInput,
} from "@mantine/core";
import { ComponentAdminGlobal_TitlePage } from "../../_admin_global/_component";
import { Admin_ComponentBoxStyle } from "../../_admin_global/_component/comp_admin_boxstyle";
import { Admin_V3_ComponentBreakpoint } from "../../_components_v3/comp_simple_grid_breakpoint";
import { pathAssetImage } from "@/lib";
import { DIRECTORY_ID, pathAssetImage } from "@/lib";
import Admin_ComponentBackButton from "../../_admin_global/back_button";
import { IconCheck, IconUpload } from "@tabler/icons-react";
import { IconCheck, IconPhoto, IconUpload } from "@tabler/icons-react";
import {
AdminColor,
MainColor,
@@ -23,27 +26,129 @@ import {
import { baseStylesTextInput } from "@/app_modules/_global/lib/base_style_text_input";
import { useState } from "react";
import Component_V3_Label_TextInput from "@/app_modules/_global/component/new/comp_V3_label_text_input";
import {
ComponentGlobal_BoxInformation,
ComponentGlobal_BoxUploadImage,
ComponentGlobal_ButtonUploadFileImage,
} from "@/app_modules/_global/component";
import { useRouter } from "next/navigation";
import { apiGetMasterEmotions } from "@/app_modules/_global/lib/api_fetch_master";
import { useShallowEffect } from "@mantine/hooks";
import CustomSkeleton from "@/app_modules/components/CustomSkeleton";
import { funGlobal_UploadToStorage } from "@/app_modules/_global/fun";
import {
ComponentGlobal_NotifikasiBerhasil,
ComponentGlobal_NotifikasiGagal,
ComponentGlobal_NotifikasiPeringatan,
} from "@/app_modules/_global/notif_global";
import { ComponentAdminGlobal_NotifikasiPeringatan } from "../../_admin_global/admin_notifikasi/notifikasi_peringatan";
import { apiAdminCreateSticker } from "../lib/api_fetch_stiker";
interface IData {
name: string;
// jenis_kelamin: "Laki-laki" | "Perempuan" | null;
}
export default function AdminAppInformation_ViewCreateSticker() {
const [value, setValue] = useState(["senang"]);
const router = useRouter();
const [file, setFile] = useState<File | null>(null);
const [img, setImg] = useState<any | null>(null);
const [valueEmotion, setValueEmotion] = useState(["senang"]);
const [data, setData] = useState<IData>({
name: "",
// jenis_kelamin: null,
});
const listEmotion = [
{ value: "senang", label: "Senang" },
{ value: "sedih", label: "Sedih" },
{ value: "marah", label: "Marah" },
{ value: "takut", label: "Takut" },
{ value: "terkejut", label: "Terkejut" },
{ value: "cinta", label: "Cinta" },
{ value: "malas", label: "Malas" },
{ value: "bangga", label: "Bangga" },
{ value: "penasaran", label: "Penasaran" },
{ value: "malu", label: "Malu" },
{ value: "iri", label: "Iri" },
{ value: "kesal", label: "Kesal" },
{ value: "kaget", label: "Kaget" },
{ value: "bingung", label: "Bingung" },
{ value: "lega", label: "Lega" },
];
const [listEmotion, setListEmotion] = useState<any[]>([]);
const [loading, setLoading] = useState(false);
useShallowEffect(() => {
onLoadMasterEmotions();
}, []);
async function onLoadMasterEmotions() {
try {
const response = await apiGetMasterEmotions();
if (response.success) {
setListEmotion(response.data);
}
} catch (error) {
console.error("Error on load master emotions:", error);
}
}
const validateData = () => {
if (!file) {
ComponentAdminGlobal_NotifikasiPeringatan("File tidak ada");
return false;
}
if (valueEmotion.length === 0) {
ComponentAdminGlobal_NotifikasiPeringatan("Pilih emosi");
return false;
}
return true;
};
async function onUploadFile() {
try {
const response = await funGlobal_UploadToStorage({
file: file as File,
dirId: DIRECTORY_ID.sticker,
});
if (!response.success) {
ComponentGlobal_NotifikasiPeringatan("Gagal upload gambar");
return;
}
return response.data.id;
} catch (error) {
console.error("Error on upload file", error);
}
}
async function handleCreateSticker({ fileId }: { fileId: string }) {
try {
const response = await apiAdminCreateSticker({
data: {
emotions: valueEmotion,
fileId: fileId,
},
});
if (response.success) {
ComponentGlobal_NotifikasiBerhasil("Berhasil disimpan");
router.back();
} else {
setLoading(false);
throw new Error("Failed to create sticker");
}
} catch (error) {
setLoading(false);
ComponentGlobal_NotifikasiGagal("Gagal disimpan");
console.error("Error create sticker", error);
}
}
async function onSubmit() {
if (!validateData()) return;
try {
setLoading(true);
const uploadFile = await onUploadFile();
if (!uploadFile) {
setLoading(false);
return;
}
await handleCreateSticker({ fileId: uploadFile });
} catch (error) {
console.error("Error on create sticker", error);
}
}
return (
<>
@@ -52,61 +157,124 @@ export default function AdminAppInformation_ViewCreateSticker() {
<Admin_ComponentBackButton />
<Admin_V3_ComponentBreakpoint lg={2} md={2} sm={1}>
<Admin_ComponentBoxStyle>
<Stack>
<Stack align="center">
<Paper bg={MainColor.white} p="sm" radius="lg">
<Image
alt="Preview Stiker"
src={pathAssetImage.dummy_image}
w="100%"
style={{ maxWidth: 300, objectFit: "contain" }}
radius="md"
/>
</Paper>
<Button radius="xl" leftIcon={<IconUpload size={20} />}>
Upload Stiker
</Button>
</Stack>
{!listEmotion.length ? (
<CustomSkeleton height={265} />
) : (
<Admin_ComponentBoxStyle>
<Stack>
<TextInput
<Stack spacing={"xs"}>
<ComponentGlobal_BoxUploadImage>
{img ? (
<AspectRatio ratio={1 / 1} mah={265} mx={"auto"}>
<Image
style={{
maxHeight: 250,
margin: "auto",
padding: "5px",
}}
alt="Foto"
height={250}
src={img}
/>
</AspectRatio>
) : (
<Stack
spacing={5}
justify="center"
align="center"
h={"100%"}
>
<IconPhoto size={100} />
</Stack>
)}
</ComponentGlobal_BoxUploadImage>
<Center>
<ComponentGlobal_ButtonUploadFileImage
accept="image/webp, image/jpeg, image/png"
onSetFile={setFile}
onSetImage={setImg}
/>
</Center>
</Stack>
<Stack>
{/* <TextInput
required
placeholder="Masukkan nama stiker"
label="Nama stiker"
styles={{
...baseStylesTextInput,
required: { color: MainColor.red },
}}
onChange={(val) => {
setData({
...data,
name: val.target.value,
});
}}
/> */}
{/* <Select
required
placeholder="Masukkan nama stiker"
label="Nama Stiker"
placeholder="Pilih jenis kelamin"
label="Jenis kelamin"
styles={{
...baseStylesTextInput,
required: { color: MainColor.red },
}}
/>
data={[
{ value: "Laki-laki", label: "Laki-laki" },
{ value: "Perempuan", label: "Perempuan" },
]}
onChange={(val: any) => {
setData({
...data,
jenis_kelamin: val,
});
}}
/> */}
<Stack>
<Component_V3_Label_TextInput text="Pilih emosi stiker" />
<Group style={{ display: "flex", flexWrap: "wrap" }}>
<Chip.Group multiple value={value} onChange={setValue}>
{listEmotion.map((e, i) => {
return (
<Chip key={i} value={e.value}>
{e.label}
</Chip>
);
})}
</Chip.Group>
</Group>
<Stack>
<Component_V3_Label_TextInput text="Pilih emosi stiker" />
<Group style={{ display: "flex", flexWrap: "wrap" }}>
<Chip.Group
multiple
value={valueEmotion}
onChange={setValueEmotion}
>
{listEmotion.map((e, i) => {
return (
<Chip key={i} value={e.value}>
{e.label}
</Chip>
);
})}
</Chip.Group>
</Group>
</Stack>
<Box
mt={"xl"}
style={{ display: "flex", justifyContent: "flex-end" }}
>
<Button
color="green"
bg={MainColor.green}
disabled={loading}
loading={loading}
loaderPosition="center"
radius="xl"
leftIcon={<IconCheck size={20} />}
onClick={() => onSubmit()}
>
Simpan
</Button>
</Box>
</Stack>
<Box
mt={"xl"}
style={{ display: "flex", justifyContent: "flex-end" }}
>
<Button radius="xl" leftIcon={<IconCheck size={20} />}>
Simpan
</Button>
</Box>
</Stack>
</Stack>
</Admin_ComponentBoxStyle>
</Admin_ComponentBoxStyle>
)}
</Admin_V3_ComponentBreakpoint>
</Stack>
</>

View File

@@ -1,72 +1,163 @@
"use client";
import { AdminColor } from "@/app_modules/_global/color/color_pallet";
import { Button, Center, ScrollArea, Stack, Table, Text } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import CustomSkeleton from "@/app_modules/components/CustomSkeleton";
import { APIs } from "@/lib";
import { RouterAdminAppInformation } from "@/lib/router_admin/router_app_information";
import {
Badge,
Box,
Button,
Center,
Group,
Image,
ScrollArea,
Spoiler,
Stack,
Table,
Text,
} from "@mantine/core";
import { useShallowEffect } from "@mantine/hooks";
import { Prisma } from "@prisma/client";
import { IconPencil, IconPlus } from "@tabler/icons-react";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { ComponentAdminGlobal_TitlePage } from "../../_admin_global/_component";
import { Admin_ComponentBoxStyle } from "../../_admin_global/_component/comp_admin_boxstyle";
import { RouterAdminAppInformation } from "@/lib/router_admin/router_app_information";
import { apiAdminGetSticker } from "../lib/api_fetch_stiker";
import { ISticker } from "@/app_modules/_global/lib/interface/stiker";
export default function AdminAppInformation_ViewSticker() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [dataSticker, setDataSticker] = useState<ISticker[] | null>(null);
useShallowEffect(() => {
const fetchData = async () => {
try {
const response = await apiAdminGetSticker();
if (response.success) {
setDataSticker(response.data);
}
} catch (error) {
console.error("Error fetching data", error);
}
};
fetchData();
}, []);
const rowTable = () => {
if (!Array.isArray(dataSticker) || dataSticker.length === 0) {
return (
<tr>
<td colSpan={12}>
<Center>
<Text color={"gray"}>Tidak ada data</Text>
</Center>
</td>
</tr>
);
}
return dataSticker.map((e, i) => (
<tr key={i}>
<td>
<Center>
<Button
radius={"xl"}
leftIcon={<IconPencil size={20} />}
onClick={() => {}}
>
Detail
</Button>
</Center>
</td>
<td>
<Center>
<Box bg="gray" p={"xs"}>
<Image
src={APIs.GET({ fileId: e.fileId, size: "200" })}
alt="Sticker"
width={100}
height={100}
/>
</Box>
</Center>
</td>
<td>
<Center>
<Box maw={300}>
<Spoiler
maxHeight={50}
hideLabel="Sembunyikan"
showLabel="Tampilkan"
>
<Group>
{e.MasterEmotions.map((e) => (
<Badge key={e.value}>{e.value}</Badge>
))}
</Group>
</Spoiler>
</Box>
</Center>
</td>
</tr>
));
};
return (
<>
<Stack>
<ComponentAdminGlobal_TitlePage name="Stiker " />
<Button
loading={isLoading}
loaderPosition="center"
w={120}
radius={"xl"}
leftIcon={<IconPlus size={20} />}
onClick={() => {
router.push(RouterAdminAppInformation.createSticker);
setIsLoading(true);
}}
>
Tambah
</Button>
<Admin_ComponentBoxStyle
style={{ height: "65dvh", overflow: "hidden" }}
>
<ScrollArea w={"100%"} h={"100%"} scrollbarSize={"md"}>
<Table
verticalSpacing={"md"}
horizontalSpacing={"md"}
p={"md"}
w={"100%"}
>
<thead>
<tr>
<th>
<Center c={AdminColor.white}>Aksi</Center>
</th>
<th>
<Center c={AdminColor.white}>Status</Center>
</th>
<th>
<Text c={AdminColor.white}>Kategori</Text>
</th>
</tr>
</thead>
<tbody>
<tr>
<td>1</td>
<td>Stiker 1</td>
<td>
<Button
radius={"xl"}
leftIcon={<IconPlus size={20} />}
onClick={() => {}}
>
Tambah
</Button>
</td>
</tr>
</tbody>
</Table>
</ScrollArea>
</Admin_ComponentBoxStyle>
{!dataSticker ? (
<CustomSkeleton height={"65dvh"} />
) : (
<Admin_ComponentBoxStyle
style={{ height: "65dvh", overflow: "hidden" }}
>
<ScrollArea w={"100%"} h={"100%"} scrollbarSize={"md"}>
<Table
verticalSpacing={"md"}
horizontalSpacing={"md"}
p={"md"}
w={"100%"}
>
<thead>
<tr>
<th>
<Center c={AdminColor.white}>Aksi</Center>
</th>
<th>
<Center c={AdminColor.white}>Stiker</Center>
</th>
<th>
<Center c={AdminColor.white}>Kategori</Center>
</th>
</tr>
</thead>
<tbody>{rowTable()}</tbody>
</Table>
</ScrollArea>
</Admin_ComponentBoxStyle>
)}
</Stack>
</>
);