Skip to content

Instantly share code, notes, and snippets.

@rashgaroth
Last active February 26, 2023 09:59
Show Gist options
  • Save rashgaroth/769ede9f3a27f67eb63815bcb8129ee1 to your computer and use it in GitHub Desktop.
Save rashgaroth/769ede9f3a27f67eb63815bcb8129ee1 to your computer and use it in GitHub Desktop.
React (NextJS) Dynamic Table using MUI & Tailwind CSS
import React from 'react'
import { getSiteWorkspace, getWorkspacePaths } from '@/prisma/services/workspace'
import { useRouter } from 'next/router'
import AdminLayout from '@/layouts/AdminLayout'
import { Chip, IconButton, Stack } from '@mui/material'
import TableWrapper from '@/components/TableWrapper'
import { Delete, Edit } from '@mui/icons-material'
import { useDispatch } from 'react-redux'
import { setMessageModal } from '@/redux/reducers/global/actions'
import { Mahasiswa } from 'src/svg/Admin'
export const ADMIN_STUDENTS_LIST_HEAD = [
{
id: 'name',
title: 'Nama',
type: '',
sort_type: ''
},
{
id: 'nim',
title: 'NIM',
type: 'sort',
sort_type: 'up'
},
{
id: 'class',
title: 'Kelas',
type: '',
sort_type: ''
},
{
id: 'email',
title: 'Email',
type: '',
sort_type: ''
},
{
id: 'birth',
title: 'Tanggal Lahir',
type: '',
sort_type: ''
},
{
id: 'action',
title: '',
type: '',
sort_type: ''
}
]
const dummyStudentList = [
{
id: 1,
name: 'Dwiyan',
nim: '02984091203',
class: 'LAB302',
email: 'putra.dwiyan@gmail.com',
birth: '15 Jan 2000'
}
]
const navigation = [
{
name: 'Mahasiswa',
href: '#',
icon: Mahasiswa,
current: false,
child: [
{
id: 6,
name: 'Daftar Mahasiswa',
href: '/page/admin/civitas/students',
current: false
},
{
id: 1,
name: 'Registrasi',
href: '/page/admin/civitas/students/register',
current: false
},
{
id: 2,
name: 'Profil Mahasiswa',
href: '/page/admin/civitas/students/profile',
current: false
},
{
id: 3,
name: 'Jadwal',
href: '/page/admin/civitas/students/schedule',
current: false
},
{
id: 4,
name: 'Tugas Harian',
href: '/page/admin/civitas/students/tasks',
current: false
},
{
id: 5,
name: 'Penilaian',
href: '/page/admin/civitas/students/value',
current: false
}
]
}
]
const AdminStudentList = (props) => {
const router = useRouter()
const dispatch = useDispatch()
if (router.isFallback) {
return <h1>Loading ...</h1>
}
const path = router.asPath
const mSchoolNav = navigation.find((x) => x.name === 'Mahasiswa')
const handleFilter = () => {
console.log('todo')
}
const handleDelete = () => {
console.log('todo')
}
const handleEdit = () => {
console.log('todo')
}
const handlePagination = (state) => {
console.log('todo')
}
const handleImportCsv = () => {
console.log('todo')
}
const handleAdd = () => {
console.log('todo')
}
const handleSearch = () => {
console.log('todo')
}
const handleSort = () => {
console.log('todo')
}
const onClickLesson = (id) => {
console.log('todo')
}
return (
<AdminLayout>
<Stack direction="column" spacing={2} alignItems="start" mt={4}>
<TableWrapper
chipData={'6 Data'}
title="Mahasiswa"
tbHead={ADMIN_STUDENTS_LIST_HEAD}
tbData={dummyStudentList.map((data, i) => (
<tr key={data.id}>
<td
className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-link sm:pl-6 cursor-pointer"
onClick={() => onClickLesson(data.name)}>
{data.name}
</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6 cursor-pointer">{data.nim}</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6 cursor-pointer">{data.class}</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6 cursor-pointer">{data.birth}</td>
<td className="whitespace-nowrap py-4 pl-4 pr-3 text-sm font-medium text-black sm:pl-6 cursor-pointer">
<Stack direction={'row'} alignItems="end" justifyContent={'end'}>
<IconButton
onClick={() => {
dispatch(setMessageModal('error', 'Hapus data?', 'Jika data dihapus, maka data tidak akan dapat dikembalikan', true))
}}>
<Delete />
</IconButton>
<IconButton>
<Edit />
</IconButton>
</Stack>
</td>
</tr>
))}
descriptions="Data Seluruh Mahasiswa"
handleFilter={handleFilter}
handleImportCsv={handleImportCsv}
handleNextPagination={() => handlePagination('next')}
handlePrevPagination={() => handlePagination('prev')}
handleSearch={handleSearch}
handleSort={handleSort}
modalChild={null}
modalTitle={'Tambah Mahasiswa'}
/>
</Stack>
</AdminLayout>
)
}
AdminStudentList.propTypes = {}
export const getStaticPaths = async () => {
const paths = await getWorkspacePaths()
return {
paths,
fallback: true
}
}
export const getStaticProps = async ({ params }) => {
const { site } = params
const siteWorkspace = await getSiteWorkspace(site, site.includes('.'))
let workspace = null
if (siteWorkspace) {
const { host } = new URL(process.env.APP_URL)
workspace = {
domains: siteWorkspace.domains,
name: siteWorkspace.name,
hostname: `${siteWorkspace.slug}.${host}`,
slug: siteWorkspace.slug
}
}
return {
props: { workspace },
revalidate: 10
}
}
export default AdminStudentList
import * as React from 'react'
import Backdrop from '@mui/material/Backdrop'
import Box from '@mui/material/Box'
import Modal from '@mui/material/Modal'
import Fade from '@mui/material/Fade'
import Button from '@mui/material/Button'
import Typography from '@mui/material/Typography'
import { IconButton, Stack } from '@mui/material'
import { Close } from '@mui/icons-material'
const style = {
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
minWidth: 500,
bgcolor: 'background.paper',
boxShadow: 24,
p: 4,
borderRadius: 4
}
export default function ModalWrapper({ handleClose, open, title = '', children }) {
return (
<Modal
aria-labelledby="transition-modal-title"
aria-describedby="transition-modal-description"
open={open}
onClose={handleClose}
closeAfterTransition
BackdropComponent={Backdrop}
BackdropProps={{
timeout: 500
}}>
<Fade in={open}>
<Box sx={style}>
{title !== '' && (
<Stack direction="row" justifyContent={'space-between'} alignItems="center">
<p className="text-lg font-bold">{title}</p>
<IconButton onClick={handleClose}>
<Close />
</IconButton>
</Stack>
)}
<Box mt={2}>{children}</Box>
</Box>
</Fade>
</Modal>
)
}
/* eslint-disable jsx-a11y/no-static-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
/* eslint-disable jsx-a11y/anchor-is-valid */
import React from 'react'
import PropTypes from 'prop-types'
import { CircularProgress, Stack } from '@mui/material'
const ArrowIcon = ({ arrow = 'up' }) => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M8.39043 13.512C8.19027 13.7622 7.80973 13.7622 7.60957 13.512L4.64988 9.81235C4.38797 9.48497 4.62106 9 5.04031 9H10.9597C11.3789 9 11.612 9.48497 11.3501 9.81235L8.39043 13.512Z"
fill={arrow === 'up' ? '#464F60' : '#A1A9B8'}
/>
<path
d="M8.39043 2.48804C8.19027 2.23784 7.80973 2.23784 7.60957 2.48804L4.64988 6.18765C4.38797 6.51503 4.62106 7 5.04031 7H10.9597C11.3789 7 11.612 6.51503 11.3501 6.18765L8.39043 2.48804Z"
fill={arrow === 'down' ? '#464F60' : '#A1A9B8'}
/>
</svg>
)
const InfoIcon = () => (
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 8C14 11.3138 11.3138 14 8 14C4.68616 14 2 11.3138 2 8C2 4.68625 4.68616 2 8 2C11.3138 2 14 4.68625 14 8ZM6.70239 6.65747H8.82398V11.1769C8.82398 11.6144 8.46933 11.969 8.03186 11.969C7.59439 11.969 7.23975 11.6144 7.23975 11.1769V8.06226H6.70239C6.31447 8.06226 6 7.74778 6 7.35986C6 6.97194 6.31447 6.65747 6.70239 6.65747ZM7.00977 4.93247C7.00977 4.80778 7.03192 4.69149 7.07587 4.58359C7.12345 4.47236 7.18773 4.37629 7.26909 4.29539C7.35044 4.21449 7.44524 4.15048 7.55383 4.10326C7.60323 4.0824 7.65408 4.06615 7.70674 4.0545C7.77357 4.03987 7.84294 4.03247 7.91485 4.03247C8.03688 4.03247 8.15202 4.05604 8.26061 4.10326C8.29003 4.11599 8.31836 4.12998 8.3456 4.14515C8.41933 4.18623 8.4858 4.23634 8.54536 4.29539C8.62671 4.37629 8.691 4.47236 8.73858 4.58359C8.76328 4.63984 8.7818 4.69835 8.79342 4.75912C8.80432 4.815 8.80977 4.87279 8.80977 4.93247C8.80977 4.99965 8.80287 5.06492 8.78906 5.12822C8.77744 5.18239 8.76037 5.23512 8.73858 5.2864C8.691 5.3943 8.62671 5.48865 8.54536 5.56955C8.50359 5.61099 8.45856 5.64801 8.40989 5.6806C8.3634 5.71157 8.31364 5.73866 8.26061 5.76168C8.15202 5.80891 8.03688 5.83247 7.91485 5.83247C7.85129 5.83247 7.78991 5.82678 7.73071 5.81532C7.66933 5.80358 7.61049 5.7857 7.55383 5.76168C7.44524 5.71446 7.35044 5.65045 7.26909 5.56955C7.18773 5.48865 7.12345 5.3943 7.07587 5.2864C7.03192 5.17517 7.00977 5.05716 7.00977 4.93247Z"
fill="#868FA0"
/>
</svg>
)
export default function Table({ tHead, renderData, onSort, withNumber = true, loadingData = false }) {
return (
<div className="px-4 sm:px-6 lg:px-0">
<div className="mt-8 flex flex-col">
<div className="-my-2 -mx-4 overflow-x-auto sm:-mx-6 lg:-mx-8">
<div className="inline-block min-w-full py-2 align-middle md:px-6 lg:px-8">
<div className="overflow-hidden shadow md:rounded-lg">
<table className="min-w-full">
<thead className="bg-grey200">
<tr>
{withNumber && (
<th scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-grey900 sm:pl-6">
<a href="#" className="group inline-flex">
#
<span className="ml-2 flex-none rounded bg-gray-200 text-textTableHead group-hover:bg-gray-300">
<ArrowIcon arrow={'up'} />
</span>
</a>
</th>
)}
{!loadingData &&
tHead &&
tHead.map((x) => (
<th key={x.id} scope="col" className="py-3.5 pl-4 pr-3 text-left text-sm font-semibold text-grey900 sm:pl-6">
<a href="#" className="group inline-flex text-sm">
{x.title}
{x.type === 'sort' && (
<span
className="ml-2 flex-none rounded bg-gray-200 text-textTableHead group-hover:bg-gray-300"
onClick={() => onSort(x.id, x.sort_type)}>
<ArrowIcon arrow={x.sort_type} />
</span>
)}
{x.type === 'info' && (
<span className="ml-2 flex-none rounded bg-gray-200 text-textTableHead group-hover:bg-gray-300">
<InfoIcon />
</span>
)}
</a>
</th>
))}
</tr>
</thead>
{loadingData ? (
<Stack direction={'column'} alignItems="center" justifyContent={'center'} width="100%" className="m-4">
<CircularProgress />
<p className="text-l font-bold">Memuat data ...</p>
</Stack>
) : (
<tbody className="bg-white">{renderData}</tbody>
)}
</table>
</div>
</div>
</div>
</div>
</div>
)
}
Table.propTypes = {
tHead: PropTypes.array.isRequired,
renderData: PropTypes.node.isRequired,
onSort: PropTypes.func
}
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
/* eslint-disable jsx-a11y/click-events-have-key-events */
import React, { useState } from 'react'
import { Chip, Pagination, Stack } from '@mui/material'
import { SearchIcon } from '@heroicons/react/outline'
import { Add, ArrowRightAlt, ImportExport, Sort, West } from '@mui/icons-material'
import Table from './Table'
import { makeStyles } from '@mui/styles'
import ModalWrapper from './ModalWrapper'
const Form = ({ handleSearch }) => (
<div className="mt-1 relative rounded-md shadow-sm w-full">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<SearchIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
</div>
<input
type="text"
onChange={handleSearch}
name="search"
id="search"
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full pl-10 sm:text-sm border-gray-300 rounded-md"
placeholder="Cari"
/>
</div>
)
const useStyles = makeStyles((theme) => ({
root: {
'& .MuiPaginationItem-icon': {
display: 'none'
}
},
ul: {
'& .MuiPaginationItem-root': {
'&.Mui-selected': {
background: '#F9F5FF',
color: '#714588'
}
}
}
}))
const TableWrapper = ({
handleImportCsv,
handleNextPagination,
handlePrevPagination,
handleSort,
handleFilter,
handleSearch,
title,
descriptions,
modalChild,
modalTitle,
chipData,
tbData,
tbHead
}) => {
const [openModal, setOpenModal] = useState(false)
const classes = useStyles()
return (
<div className="bg-white px-4 py-5 border-b border-gray-200 sm:px-6 rounded-lg w-full">
{/* title */}
<div className="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-2">
<Stack direction={'column'} spacing={1} alignItems="start">
<Stack direction={'row'} spacing={2} alignItems="start">
<h2 className="text-l text-black">{title}</h2>
<Chip
label={<p className="text-primaryMain text-sm">{chipData}</p>}
size="small"
sx={{ backgroundColor: '#F9F5FF', borderRadius: 2 }}
/>
</Stack>
<p className="text-grey500">{descriptions}</p>
</Stack>
</div>
<div className="ml-4 mt-2 space-x-2">
<button className="btn gap-2 btn-sm h-10 bg-white hover:bg-purple-100 hover:border-none" onClick={handleImportCsv}>
<p className="text-grey700">Import</p>
<ImportExport className="text-grey700" />
</button>
<button
className="btn gap-2 btn-sm h-10 bg-primaryMain hover:bg-primaryLight hover:border-none"
onClick={() => {
setOpenModal(true)
}}>
<p className="text-white">Tambah Departemen</p>
<Add className="text-white" />
</button>
</div>
</div>
{/* divider */}
<div className="relative my-2">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="w-full border-t border-gray-300" />
</div>
</div>
{/* filter */}
<div className="-ml-4 -mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4 w-full">
<Stack direction={'row'} spacing={2} alignItems="center">
<Form handleSearch={handleSearch} />
<button className="btn gap-2 btn-sm h-10 hover:bg-purple-100" onClick={handleFilter}>
<Sort className="text-grey700" />
<p className="text-grey700">Filter</p>
</button>
</Stack>
</div>
</div>
{/* table */}
<Table renderData={tbData} tHead={tbHead} withNumber={false} onSort={handleSort} />
<div className="-ml-4 mt-2 flex items-center justify-between flex-wrap sm:flex-nowrap">
<div className="ml-4 mt-4">
<button className="btn gap-2 btn-sm h-10 hover:bg-purple-100 hover:border-none" onClick={handlePrevPagination}>
<West className="text-grey700" />
<p className="text-grey700">Sebelumnya</p>
</button>
</div>
<div className="ml-4 mt-4">
<div className={classes.root}>
<Pagination count={10} shape="rounded" classes={{ ul: classes.ul }} />
</div>
</div>
<div className="ml-4 mt-4">
<button className="btn gap-2 btn-sm h-10 hover:bg-purple-100 hover:border-none" onClick={handleNextPagination}>
<p className="text-grey700">Selanjutnya</p>
<ArrowRightAlt className="text-grey700" />
</button>
</div>
</div>
<ModalWrapper open={openModal} handleClose={() => setOpenModal(false)} title={modalTitle}>
{modalChild}
</ModalWrapper>
</div>
)
}
TableWrapper.propTypes = {}
export default TableWrapper
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment