Merge branch 'join' into lukman/3-september-2024
This commit is contained in:
@@ -8,7 +8,8 @@ export async function GET(request: Request) {
|
||||
const { searchParams } = new URL(request.url);
|
||||
const kategori = searchParams.get('cat');
|
||||
const file = searchParams.get('file');
|
||||
fl = fs.readFileSync(`./public/image/${kategori}/${file}`)
|
||||
const jenis = searchParams.get('jenis');
|
||||
fl = fs.readFileSync(`./public/${jenis}/${kategori}/${file}`)
|
||||
} catch (err: any) {
|
||||
throw err;
|
||||
}
|
||||
|
||||
84
src/module/_global/components/pdf_viewer.tsx
Normal file
84
src/module/_global/components/pdf_viewer.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
import { getDocument, GlobalWorkerOptions } from 'pdfjs-dist';
|
||||
import { Image, Skeleton, Stack } from '@mantine/core';
|
||||
|
||||
GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/4.6.82/pdf.worker.min.mjs';
|
||||
|
||||
const PdfToImage = ({ md }: { md: string }) => {
|
||||
const [images, setImages] = useState<string[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof window !== 'undefined') {
|
||||
const renderPages = async () => {
|
||||
try {
|
||||
const loadingTask = getDocument(md); // Menggunakan md sebagai URL PDF
|
||||
const pdf = await loadingTask.promise;
|
||||
const numPages = pdf.numPages;
|
||||
const imagePromises: Promise<string>[] = [];
|
||||
|
||||
for (let pageNum = 1; pageNum <= numPages; pageNum++) {
|
||||
const renderPage = async (pageNum: number): Promise<string> => {
|
||||
const page = await pdf.getPage(pageNum);
|
||||
const viewport = page.getViewport({ scale: 2.0 });
|
||||
|
||||
// Buat elemen canvas
|
||||
const canvas = document.createElement('canvas');
|
||||
const context = canvas.getContext('2d');
|
||||
if (context) {
|
||||
canvas.width = viewport.width;
|
||||
canvas.height = viewport.height;
|
||||
|
||||
// Render halaman PDF ke dalam canvas
|
||||
const renderContext = {
|
||||
canvasContext: context,
|
||||
viewport: viewport,
|
||||
};
|
||||
await page.render(renderContext).promise;
|
||||
|
||||
// Konversi canvas ke gambar (data URL)
|
||||
return canvas.toDataURL('image/png');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
imagePromises.push(renderPage(pageNum));
|
||||
}
|
||||
|
||||
const imageSrcs = await Promise.all(imagePromises);
|
||||
setImages(imageSrcs);
|
||||
} catch (error) {
|
||||
console.error('Error rendering PDF to images:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
renderPages();
|
||||
}
|
||||
}, [md]);
|
||||
|
||||
return (
|
||||
<Stack>
|
||||
{loading
|
||||
? <CustomLoading />
|
||||
: images.map((src, index) => (
|
||||
<Image key={index} src={src} alt={`Page ${index + 1}`} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
function CustomLoading() {
|
||||
return (
|
||||
<Stack p="md">
|
||||
{[...Array(3)].map((_, index) => (
|
||||
<Skeleton key={index} height={200} />
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
export default PdfToImage
|
||||
@@ -10,6 +10,7 @@ import LayoutDrawer from "./layout/layout_drawer";
|
||||
import LayoutIconBack from "./layout/layout_icon_back";
|
||||
import LoadingPage from "./layout/layout_loading_page";
|
||||
import LayoutLogin from "./layout/layout_login";
|
||||
import LayoutModalViewFile from "./layout/layout_modal_view_file";
|
||||
import LayoutNavbarHome from "./layout/layout_navbar_home";
|
||||
import LayoutNavbarNew from "./layout/layout_navbar_new";
|
||||
import ViewFilter from "./view/view_filter";
|
||||
@@ -29,3 +30,4 @@ export { SkeletonDetailDiscussionComment }
|
||||
export { SkeletonDetailDiscussionMember }
|
||||
export { SkeletonDetailProfile }
|
||||
export { SkeletonDetailListTugasTask }
|
||||
export { LayoutModalViewFile }
|
||||
|
||||
34
src/module/_global/layout/layout_modal_view_file.tsx
Normal file
34
src/module/_global/layout/layout_modal_view_file.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
'use client'
|
||||
import { Image, Modal } from '@mantine/core';
|
||||
import dynamic from 'next/dynamic';
|
||||
import React, { useState } from 'react';
|
||||
const PdfToImage = dynamic(() => import('./../components/pdf_viewer').then((mod) => mod.default), { ssr: false });
|
||||
|
||||
export default function LayoutModal({ opened, onClose, extension, fitur, file }: { opened: boolean, onClose: () => void, extension: string, fitur: string, file: string }) {
|
||||
const [isValModal, setValModal] = useState(opened)
|
||||
const filePdf = '/file/' + fitur + '/' + file
|
||||
return (
|
||||
<Modal styles={{
|
||||
body: {
|
||||
margin: 10,
|
||||
},
|
||||
content: {
|
||||
border: `2px solid ${'#828AFC'}`,
|
||||
borderRadius: 10
|
||||
}
|
||||
}} opened={opened} onClose={onClose} withCloseButton={true} centered closeOnClickOutside={false}>
|
||||
|
||||
{
|
||||
extension === 'pdf' ? <PdfToImage md={filePdf} /> :
|
||||
<Image
|
||||
radius="md"
|
||||
h={200}
|
||||
w="auto"
|
||||
fit="contain"
|
||||
src={`/api/file/img?cat=${fitur}&file=${file}&jenis=file`}
|
||||
/>
|
||||
}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -111,7 +111,7 @@ export default function CreateUserCalender({ onClose }: { onClose: (val: any) =>
|
||||
<Box mb={15} key={i} onClick={() => handleFileClick(i)}>
|
||||
<Flex justify={"space-between"} align={"center"}>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Text style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
|
||||
@@ -172,7 +172,7 @@ export default function DetailEventDivision() {
|
||||
key={i}
|
||||
>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.name}
|
||||
|
||||
@@ -275,7 +275,7 @@ export default function NavbarCreateDivisionCalender() {
|
||||
key={i}
|
||||
>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.name}
|
||||
|
||||
@@ -339,7 +339,7 @@ export default function UpdateDivisionCalender() {
|
||||
key={i}
|
||||
>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.name}
|
||||
|
||||
@@ -145,7 +145,7 @@ export default function UpdateListUsers({ onClose }: { onClose: (val: any) => vo
|
||||
<Box mb={15} key={i} onClick={() => handleFileClick(i)}>
|
||||
<Flex justify={"space-between"} align={"center"}>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Text style={{
|
||||
cursor: 'pointer',
|
||||
display: 'flex',
|
||||
|
||||
@@ -103,7 +103,7 @@ export default function DetailDiscussion({ id, idDivision }: { id: string, idDiv
|
||||
>
|
||||
{isData?.username ?
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${isData?.user_img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${isData?.user_img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{isData?.username}
|
||||
@@ -142,7 +142,7 @@ export default function DetailDiscussion({ id, idDivision }: { id: string, idDiv
|
||||
>
|
||||
{isData?.username ?
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${isData?.user_img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${isData?.user_img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{isData?.username}
|
||||
@@ -216,7 +216,7 @@ export default function DetailDiscussion({ id, idDivision }: { id: string, idDiv
|
||||
align={"center"}
|
||||
>
|
||||
<Group>
|
||||
<Avatar alt="it's me" size="md" src={`/api/file/img?cat=user&file=${v.img}`} />
|
||||
<Avatar alt="it's me" size="md" src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"} fz={15}>
|
||||
{v.username}
|
||||
|
||||
@@ -110,7 +110,7 @@ export default function ListDiscussion({ id }: { id: string }) {
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<Avatar alt="it's me" src={`/api/file/img?cat=user&file=${v.img}`} size="lg" />
|
||||
<Avatar alt="it's me" src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.user_name}
|
||||
|
||||
@@ -123,7 +123,7 @@ export default function CreateAnggotaDivision() {
|
||||
xl: "xs"
|
||||
}}>
|
||||
<Grid.Col span={2}>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={10}>
|
||||
<Flex justify='space-between' align={"center"}>
|
||||
|
||||
@@ -189,7 +189,7 @@ export default function InformationDivision() {
|
||||
>
|
||||
<Grid.Col span={9}>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box w={{
|
||||
base: 140,
|
||||
xl: 270
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function NavbarCreateUsers({ grup, onClose }: { grup?: string, on
|
||||
onClick={() => handleFileClick(index)}
|
||||
>
|
||||
<Center>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="xl" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="xl" />
|
||||
</Center>
|
||||
<Text mt={20} ta="center">
|
||||
{v.name}
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function ViewSearch() {
|
||||
padding: 5,
|
||||
paddingLeft: 0,
|
||||
}} >
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} size="lg" />
|
||||
<Box>
|
||||
<Text fw={'bold'} c={WARNA.biruTua}>{v.name}</Text>
|
||||
<Text fw={'lighter'} fz={12}>{v.group + ' - ' + v.position}</Text>
|
||||
|
||||
@@ -133,7 +133,7 @@ export default function AddMemberDetailProject() {
|
||||
<Box mb={15} key={i} onClick={() => (!found) ? handleFileClick(i) : null}>
|
||||
<Flex justify={"space-between"} align={"center"}>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Stack align="flex-start" justify="flex-start">
|
||||
<Text style={{
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -297,7 +297,7 @@ export default function CreateProject() {
|
||||
key={i}
|
||||
>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.name}
|
||||
|
||||
@@ -114,7 +114,7 @@ export default function CreateUsersProject({ grup, onClose }: { grup?: string, o
|
||||
onClick={() => handleFileClick(index)}
|
||||
>
|
||||
<Center>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="xl" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="xl" />
|
||||
</Center>
|
||||
<Text mt={20} ta="center">
|
||||
{v.name}
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ListAnggotaDetailProject() {
|
||||
}}
|
||||
>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.name}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
'use client'
|
||||
import { LayoutDrawer, WARNA } from '@/module/_global';
|
||||
import { LayoutDrawer, LayoutModalViewFile, WARNA } from '@/module/_global';
|
||||
import { Box, Center, Flex, Grid, Group, SimpleGrid, Skeleton, Stack, Text } from '@mantine/core';
|
||||
import React, { useState } from 'react';
|
||||
import toast from 'react-hot-toast';
|
||||
@@ -19,6 +19,8 @@ export default function ListFileDetailProject() {
|
||||
const [nameData, setNameData] = useState('')
|
||||
const [openDrawer, setOpenDrawer] = useState(false)
|
||||
const [isOpenModal, setOpenModal] = useState(false)
|
||||
const [isOpenModalView, setOpenModalView] = useState(false)
|
||||
const [isExtension, setExtension] = useState('')
|
||||
|
||||
async function getOneData() {
|
||||
try {
|
||||
@@ -98,6 +100,7 @@ export default function ListFileDetailProject() {
|
||||
|
||||
onClick={() => {
|
||||
setNameData(item.name + '.' + item.extension)
|
||||
setExtension(item.extension)
|
||||
setIdData(item.id)
|
||||
setOpenDrawer(true)
|
||||
}}
|
||||
@@ -105,15 +108,15 @@ export default function ListFileDetailProject() {
|
||||
<Grid gutter={"sm"} justify='flex-start' align='flex-start'>
|
||||
<Grid.Col span={"auto"}>
|
||||
<Center >
|
||||
{item.extension == "pdf" && <BsFiletypePdf size={30} />}
|
||||
{item.extension == "csv" && <BsFiletypeCsv size={30} />}
|
||||
{item.extension == "png" && <BsFiletypePng size={30} />}
|
||||
{item.extension == "jpg" || item.extension == "jpeg" && <BsFiletypeJpg size={30} />}
|
||||
{item.extension == "heic" && <BsFiletypeHeic size={30} />}
|
||||
{item.extension == "pdf" && <BsFiletypePdf size={30} />}
|
||||
{item.extension == "csv" && <BsFiletypeCsv size={30} />}
|
||||
{item.extension == "png" && <BsFiletypePng size={30} />}
|
||||
{item.extension == "jpg" || item.extension == "jpeg" && <BsFiletypeJpg size={30} />}
|
||||
{item.extension == "heic" && <BsFiletypeHeic size={30} />}
|
||||
</Center>
|
||||
</Grid.Col>
|
||||
<Grid.Col span={10}>
|
||||
<Text>{item.name + '.' + item.extension}</Text>
|
||||
<Text>{item.name + '.' + item.extension}</Text>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Group>
|
||||
@@ -132,7 +135,7 @@ export default function ListFileDetailProject() {
|
||||
<SimpleGrid
|
||||
cols={{ base: 3, sm: 3, lg: 3 }}
|
||||
>
|
||||
<Flex onClick={() => { }} justify={'center'} align={'center'} direction={'column'} >
|
||||
<Flex onClick={() => { setOpenModalView(true) }} justify={'center'} align={'center'} direction={'column'} >
|
||||
<Box>
|
||||
<BsFileTextFill size={30} color={WARNA.biruTua} />
|
||||
</Box>
|
||||
@@ -163,6 +166,8 @@ export default function ListFileDetailProject() {
|
||||
}
|
||||
setOpenModal(false)
|
||||
}} />
|
||||
|
||||
<LayoutModalViewFile opened={isOpenModalView} onClose={() => setOpenModalView(false)} file={idData + '.' + isExtension} extension={isExtension} fitur='project' />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -154,7 +154,7 @@ export default function AddMemberDetailTask() {
|
||||
base: 3,
|
||||
xl: 2
|
||||
}}>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{
|
||||
base: 9,
|
||||
|
||||
@@ -237,7 +237,7 @@ export default function CreateTask() {
|
||||
key={i}
|
||||
>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box>
|
||||
<Text c={WARNA.biruTua} fw={"bold"}>
|
||||
{v.name}
|
||||
|
||||
@@ -134,7 +134,7 @@ export default function CreateUsersProject({ onClose }: { onClose: (val: any) =>
|
||||
base: 3,
|
||||
xl: 2
|
||||
}}>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={{
|
||||
base: 9,
|
||||
|
||||
@@ -100,7 +100,7 @@ export default function ListAnggotaDetailTask() {
|
||||
>
|
||||
<Grid.Col span={9}>
|
||||
<Group>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} alt="it's me" size="lg" />
|
||||
<Box w={{
|
||||
base: 140,
|
||||
xl: 270
|
||||
|
||||
@@ -68,7 +68,7 @@ export default function EditMember({ id }: { id: string }) {
|
||||
const res = await funGetOneMember(id)
|
||||
setData(res.data)
|
||||
getAllPosition(res.data?.idGroup)
|
||||
setIMG(`/api/file/img?cat=user&file=${res.data.img}`)
|
||||
setIMG(`/api/file/img?jenis=image&cat=user&file=${res.data.img}`)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
|
||||
@@ -66,7 +66,7 @@ export default function NavbarDetailMember({ id }: IMember) {
|
||||
gap="xs"
|
||||
>
|
||||
<Center>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${dataOne?.img}`} alt="it's me" size="xl" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${dataOne?.img}`} alt="it's me" size="xl" />
|
||||
</Center>
|
||||
{loading ?
|
||||
<>
|
||||
|
||||
@@ -109,7 +109,7 @@ export default function TabListMember() {
|
||||
xl: "xs"
|
||||
}} align="center">
|
||||
<Grid.Col span={2}>
|
||||
<Avatar src={`/api/file/img?cat=user&file=${v.img}`} size={50} alt="image" />
|
||||
<Avatar src={`/api/file/img?jenis=image&cat=user&file=${v.img}`} size={50} alt="image" />
|
||||
</Grid.Col>
|
||||
<Grid.Col span={9}>
|
||||
<Text fw={'bold'} c={WARNA.biruTua} lineClamp={1}>{_.startCase(v.name)}</Text>
|
||||
|
||||
@@ -45,7 +45,7 @@ export default function EditProfile() {
|
||||
setLoading(true)
|
||||
const res = await funGetProfileByCookies()
|
||||
setData(res.data)
|
||||
setIMG(`/api/file/img?cat=user&file=${res.data.img}`)
|
||||
setIMG(`/api/file/img?jenis=image&cat=user&file=${res.data.img}`)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -27,7 +27,7 @@ export default function Profile() {
|
||||
setLoading(true)
|
||||
const res = await funGetProfileByCookies()
|
||||
setData(res.data)
|
||||
setIMG(`/api/file/img?cat=user&file=${res.data.img}`)
|
||||
setIMG(`/api/file/img?jenis=image&cat=user&file=${res.data.img}`)
|
||||
setLoading(false)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
Reference in New Issue
Block a user