diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ef686fc..b1a035e 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -100,6 +100,7 @@ model User { phone String @unique email String? @unique gender String @default("M") //M= Male, F= Female + img String? isFirstLogin Boolean @default(true) isActive Boolean @default(true) createdAt DateTime @default(now()) diff --git a/public/image/user/.gitkeep b/public/image/user/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/src/app/api/file/img/route.ts b/src/app/api/file/img/route.ts new file mode 100644 index 0000000..9142ccc --- /dev/null +++ b/src/app/api/file/img/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server" +import fs from 'fs' + +export async function GET(request: Request) { + let fl; + + try { + const { searchParams } = new URL(request.url); + const kategori = searchParams.get('cat'); + const file = searchParams.get('file'); + fl = fs.readFileSync(`./public/image/${kategori}/${file}`) + } catch (err: any) { + throw err; + } + + return new NextResponse(fl, { + headers: { + "Content-Type": "image/png" + } + }) + +} \ No newline at end of file diff --git a/src/app/api/project/route.ts b/src/app/api/project/route.ts index 0a0ef8f..f7d1d94 100644 --- a/src/app/api/project/route.ts +++ b/src/app/api/project/route.ts @@ -122,7 +122,6 @@ export async function POST(request: Request) { const f: any = file[index].get('file') const fName = f.name const fExt = fName.split(".").pop() - // funUploadFile(fName, f) const dataFile = { name: fName, diff --git a/src/app/api/task/route.ts b/src/app/api/task/route.ts index 281be45..00c7f4a 100644 --- a/src/app/api/task/route.ts +++ b/src/app/api/task/route.ts @@ -1,4 +1,4 @@ -import { funUploadFile, prisma } from "@/module/_global"; +import { prisma } from "@/module/_global"; import { funGetUserByCookies } from "@/module/auth"; import _, { ceil } from "lodash"; import moment from "moment"; @@ -148,7 +148,6 @@ export async function POST(request: Request) { const f: any = file[index].get('file') const fName = f.name const fExt = fName.split(".").pop() - // funUploadFile(fName, f) const dataFile = { name: fName, diff --git a/src/app/api/user/profile/[id]/route.ts b/src/app/api/user/profile/[id]/route.ts deleted file mode 100644 index 1dc7be6..0000000 --- a/src/app/api/user/profile/[id]/route.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { prisma } from "@/module/_global"; -import { funGetUserByCookies } from "@/module/auth"; -import { NextResponse } from "next/server"; - - -// UPDATE PROFILE BY COOKIES -export async function PUT(request: Request, context: { params: { id: string } }) { - try { - const user = await funGetUserByCookies() - if (user.id == undefined) { - return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); - } - const { id } = context.params; - const data = await request.json(); - const cek = await prisma.user.count({ - where: { - id: id, - nik: data.nik, - email: data.email, - phone: data.phone - } - }) - if (cek == 0) { - return NextResponse.json( - { - success: false, - message: "Gagal mendapatkan profile, data tidak ditemukan", - }, - { status: 404 } - ); - } - - const result = await prisma.user.update({ - where: { - id: id, - }, - data: { - nik: data.nik, - name: data.name, - email: data.email, - phone: data.phone, - gender: data.gender, - }, - }); - - return NextResponse.json( - { - success: true, - message: "Berhasil mendapatkan profile", - result, - }, - { status: 200 } - ); - - } catch (error) { - console.error(error); - return NextResponse.json({ success: false, message: "Gagal mendapatkan anggota, coba lagi nanti", reason: (error as Error).message, }, { status: 500 }); - } -} \ No newline at end of file diff --git a/src/app/api/user/profile/route.ts b/src/app/api/user/profile/route.ts index 63f8172..45a33f0 100644 --- a/src/app/api/user/profile/route.ts +++ b/src/app/api/user/profile/route.ts @@ -2,6 +2,8 @@ import { prisma } from "@/module/_global"; import { funGetUserByCookies } from "@/module/auth"; import _ from "lodash"; import { NextResponse } from "next/server"; +import path from "path"; +import fs from "fs"; // GET PROFILE BY COOKIES @@ -24,6 +26,7 @@ export async function GET(request: Request) { gender: true, idGroup: true, idPosition: true, + img: true, Group: { select: { name: true @@ -39,16 +42,17 @@ export async function GET(request: Request) { const { ...userData } = data; const group = data?.Group.name const position = data?.Position.name + const phone = data?.phone.substr(2) - const result = { ...userData, group, position }; + const omitData = _.omit(data, ["Group", "Position", "phone"]) - const omitData = _.omit(result, ["Group", "Position",]) + const result = { ...userData, group, position, phone }; - return NextResponse.json({ success: true, data: omitData }); + return NextResponse.json({ success: true, data: result }); } catch (error) { return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); } - + } // UPDATE PROFILE BY COOKIES @@ -59,24 +63,85 @@ export async function PUT(request: Request) { return NextResponse.json({ success: false, message: "Anda harus login untuk mengakses ini" }, { status: 401 }); } - const body = await request.json(); - const { name, email, phone, nik, gender } = body; - const data = await prisma.user.update({ + const body = await request.formData() + const file = body.get("file") as File; + const data = body.get("data"); + + const { name, email, phone, nik, gender } = JSON.parse(data as string) + + const cekNIK = await prisma.user.count({ + where: { + nik: nik, + NOT: { + id: user.id + } + }, + }); + + const cekEmail = await prisma.user.count({ + where: { + email: email, + NOT: { + id: user.id + } + }, + }); + + const cekPhone = await prisma.user.count({ + where: { + phone: "62" + phone, + NOT: { + id: user.id + } + }, + }); + + if (cekNIK > 0 || cekEmail > 0 || cekPhone > 0) { + return NextResponse.json({ success: false, message: "Gagal ubah profile, NIK/email/phone sudah terdaftar" }, { status: 401 }); + } + + const update = await prisma.user.update({ where: { id: user.id }, data: { name: name, email: email, - phone: phone, + phone: "62" + phone, nik: nik, gender: gender + }, + select: { + img: true } }) - return NextResponse.json({ success: true, message: "Berhasil ubah profile", data: data }); + if (String(file) != "undefined" && String(file) != "null") { + fs.unlink(`./public/image/user/${update.img}`, (err) => { }) + const root = path.join(process.cwd(), "./public/image/user/"); + const fExt = file.name.split(".").pop() + const fileName = user.id + '.' + fExt; + const filePath = path.join(root, fileName); + + // Konversi ArrayBuffer ke Buffer + const buffer = Buffer.from(await file.arrayBuffer()); + + // Tulis file ke sistem + fs.writeFileSync(filePath, buffer); + + await prisma.user.update({ + where: { + id: user.id + }, + data: { + img: fileName + } + }) + } + + return NextResponse.json({ success: true, message: "Berhasil ubah profile" }); } catch (error) { - return NextResponse.json({ success: false, message: "Gagal ubah profile" }, { status: 401 }); + return NextResponse.json({ success: false, message: "Gagal ubah profile" }, { status: 500 }); } } diff --git a/src/module/_global/fun/upload-file.ts b/src/module/_global/fun/upload-file.ts deleted file mode 100644 index 96035fb..0000000 --- a/src/module/_global/fun/upload-file.ts +++ /dev/null @@ -1,6 +0,0 @@ -'use server' -import fs from 'fs' -export async function funUploadFile(fName: any, f: any) { - const filenya = Buffer.from(await f.arrayBuffer()) - fs.writeFileSync(`./public/assets/file/${fName}`, filenya) -} \ No newline at end of file diff --git a/src/module/_global/index.ts b/src/module/_global/index.ts index b1ffe41..7e2b534 100644 --- a/src/module/_global/index.ts +++ b/src/module/_global/index.ts @@ -5,7 +5,6 @@ import SkeletonDetailDiscussionMember from "./components/skeleton_detail_discuss import SkeletonDetailListTugasTask from "./components/skeleton_detail_list_tugas_task"; import SkeletonDetailProfile from "./components/skeleton_detail_profile"; import SkeletonSingle from "./components/skeleton_single"; -import { funUploadFile } from "./fun/upload-file"; import { WARNA } from "./fun/WARNA"; import LayoutDrawer from "./layout/layout_drawer"; import LayoutIconBack from "./layout/layout_icon_back"; @@ -28,6 +27,5 @@ export { pwd_key_config }; export { SkeletonSingle } export { SkeletonDetailDiscussionComment } export { SkeletonDetailDiscussionMember } -export { funUploadFile } export { SkeletonDetailProfile } -export {SkeletonDetailListTugasTask} \ No newline at end of file +export { SkeletonDetailListTugasTask } diff --git a/src/module/user/member/ui/create_member.tsx b/src/module/user/member/ui/create_member.tsx index 9d2fc3d..6c77941 100644 --- a/src/module/user/member/ui/create_member.tsx +++ b/src/module/user/member/ui/create_member.tsx @@ -343,7 +343,7 @@ export default function CreateMember() { size="md" type="number" radius={30} - placeholder="xxx xxxx xxxx" + placeholder="8xx xxxx xxxx" leftSection={+62} withAsterisk label="Nomor Telepon" diff --git a/src/module/user/profile/lib/api_profile.ts b/src/module/user/profile/lib/api_profile.ts index bf05d41..da2fa14 100644 --- a/src/module/user/profile/lib/api_profile.ts +++ b/src/module/user/profile/lib/api_profile.ts @@ -5,13 +5,10 @@ export const funGetProfileByCookies = async (path?: string) => { return await response.json().catch(() => null); } -export const funEditProfileByCookies = async ( data: IEditDataProfile) => { +export const funEditProfileByCookies = async (data: FormData) => { const response = await fetch(`/api/user/profile/`, { method: "PUT", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(data), + body: data, }); return await response.json().catch(() => null); } diff --git a/src/module/user/profile/lib/type_profile.ts b/src/module/user/profile/lib/type_profile.ts index db31c15..e1b8acc 100644 --- a/src/module/user/profile/lib/type_profile.ts +++ b/src/module/user/profile/lib/type_profile.ts @@ -1,21 +1,22 @@ export interface IProfileById { - id: string - name: string - email: string - phone: string - nik: string - gender: string - idGroup: string - idPosition: string - group: string - position: string - } - - export interface IEditDataProfile { - id: string; - nik: string; - name: string; - phone: string; - email: string; - gender: string; - } \ No newline at end of file + id: string + name: string + email: string + phone: string + nik: string + gender: string + idGroup: string + idPosition: string + group: string + position: string +} + +export interface IEditDataProfile { + id: string; + nik: string; + name: string; + phone: string; + email: string; + gender: string; + img: string; +} \ No newline at end of file diff --git a/src/module/user/profile/ui/edit_profile.tsx b/src/module/user/profile/ui/edit_profile.tsx index 596f6ef..75521a8 100644 --- a/src/module/user/profile/ui/edit_profile.tsx +++ b/src/module/user/profile/ui/edit_profile.tsx @@ -1,18 +1,25 @@ "use client" import { LayoutNavbarNew, WARNA } from "@/module/_global"; -import { Box, Button, Flex, Modal, Select, Stack, Text, TextInput } from "@mantine/core"; -import { HiUser } from "react-icons/hi2"; +import { Avatar, Box, Button, Flex, Indicator, Modal, Select, Stack, Text, TextInput } from "@mantine/core"; import toast from "react-hot-toast"; import LayoutModal from "@/module/_global/layout/layout_modal"; -import { useState } from "react"; +import { useRef, useState } from "react"; import { IEditDataProfile, IProfileById } from "../lib/type_profile"; import { funEditProfileByCookies, funGetProfileByCookies } from "../lib/api_profile"; import { useShallowEffect } from "@mantine/hooks"; -import { funGetUserByCookies } from "@/module/auth"; +import { FaCamera, FaShare } from "react-icons/fa6"; +import { Dropzone } from "@mantine/dropzone"; +import _ from "lodash"; +import { useRouter } from "next/navigation"; export default function EditProfile() { const [isValModal, setValModal] = useState(false) const [isDataEdit, setDataEdit] = useState([]) + const openRef = useRef<() => void>(null) + const [img, setIMG] = useState() + const [imgForm, setImgForm] = useState() + const router = useRouter() + const [touched, setTouched] = useState({ nik: false, name: false, @@ -20,6 +27,7 @@ export default function EditProfile() { email: false, gender: false, }); + const [data, setData] = useState({ id: "", nik: "", @@ -27,12 +35,15 @@ export default function EditProfile() { phone: "", email: "", gender: "", + img: "", }) + async function getAllProfile() { try { const res = await funGetProfileByCookies() setData(res.data) + setIMG(`/api/file/img?cat=user&file=${res.data.img}`) } catch (error) { console.error(error); } @@ -45,17 +56,15 @@ export default function EditProfile() { async function onEditProfile(val: boolean) { try { if (val) { - const res = await funEditProfileByCookies({ - id: data.id, - nik: data.nik, - name: data.name, - phone: data.phone, - email: data.email, - gender: data.gender, - }) + const fd = new FormData(); + fd.append("file", imgForm) + fd.append("data", JSON.stringify(data)) + + const res = await funEditProfileByCookies(fd) if (res.success) { setValModal(false) toast.success(res.message) + router.push('/profile') } else { toast.error(res.message) } @@ -77,12 +86,31 @@ export default function EditProfile() { pt={30} px={20} > - - - + { + if (!files || _.isEmpty(files)) + return toast.error('Tidak ada gambar yang dipilih') + setImgForm(files[0]) + const buffer = URL.createObjectURL(new Blob([new Uint8Array(await files[0].arrayBuffer())])) + setIMG(buffer) + }} + activateOnClick={false} + maxSize={1 * 1024 ** 2} + accept={['image/png', 'image/jpeg', 'image/heic']} + onReject={(files) => { + return toast.error('File yang diizinkan: .png, .jpg, dan .heic dengan ukuran maksimal 1 MB') + }} + > + + + } size={40} onClick={() => openRef.current?.()}> + + +62} onChange={(e) => { setData({ ...data, phone: e.target.value }) setTouched({ ...touched, phone: false }) @@ -198,7 +227,7 @@ export default function EditProfile() { onBlur={() => setTouched({ ...touched, gender: true })} error={ touched.gender && ( - data.gender == "" ? "Gender Tidak Boleh Kosong" : null + data.gender == "" ? "Jenis Kelamin Tidak Boleh Kosong" : null ) } /> diff --git a/src/module/user/profile/ui/profile.tsx b/src/module/user/profile/ui/profile.tsx index dc435c1..8154c70 100644 --- a/src/module/user/profile/ui/profile.tsx +++ b/src/module/user/profile/ui/profile.tsx @@ -1,12 +1,10 @@ "use client" import { LayoutIconBack, LayoutNavbarHome, SkeletonDetailProfile, WARNA } from "@/module/_global"; -import { ActionIcon, Anchor, Box, Button, Flex, Group, Skeleton, Stack, Text } from "@mantine/core"; -import { BsInfo } from "react-icons/bs"; +import { ActionIcon, Anchor, Avatar, Box, Button, Flex, Group, Skeleton, Stack, Text } from "@mantine/core"; import { HiUser } from "react-icons/hi2"; import { RiIdCardFill } from "react-icons/ri"; import { FaSquarePhone } from "react-icons/fa6"; import { MdEmail } from "react-icons/md"; -import { InfoTitleProfile } from "../component/ui/ui_profile"; import { IoMaleFemale } from "react-icons/io5"; import toast from "react-hot-toast"; import { LuLogOut } from "react-icons/lu"; @@ -22,12 +20,14 @@ export default function Profile() { const [isData, setData] = useState() const router = useRouter() const [loading, setLoading] = useState(true) + const [img, setIMG] = useState() async function getData() { try { setLoading(true) const res = await funGetProfileByCookies() setData(res.data) + setIMG(`/api/file/img?cat=user&file=${res.data.img}`) setLoading(false) } catch (error) { console.error(error); @@ -62,7 +62,6 @@ export default function Profile() { - { setOpenModal(true) }} variant="light" bg={WARNA.bgIcon} size="lg" radius="lg" aria-label="Info"> @@ -72,7 +71,11 @@ export default function Profile() { justify="center" gap="xs" > - + {loading ? :