mirror of
https://github.com/Sonny93/my-links.git
synced 2025-12-10 07:25:35 +00:00
refactor: admin dashboard page and improve style
This commit is contained in:
@@ -4,7 +4,7 @@ import PATHS from 'constants/paths';
|
|||||||
import useUser from 'hooks/useUser';
|
import useUser from 'hooks/useUser';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { MdOutlineAdminPanelSettings } from 'react-icons/md';
|
import { MdAdminPanelSettings } from 'react-icons/md';
|
||||||
import styles from './user-card.module.scss';
|
import styles from './user-card.module.scss';
|
||||||
|
|
||||||
export default function UserCard() {
|
export default function UserCard() {
|
||||||
@@ -26,12 +26,20 @@ export default function UserCard() {
|
|||||||
/>
|
/>
|
||||||
{user.name}
|
{user.name}
|
||||||
</div>
|
</div>
|
||||||
{user.is_admin && (
|
<span className={styles['user-controls']}>
|
||||||
<Link href={PATHS.ADMIN}>
|
{user.is_admin && (
|
||||||
<MdOutlineAdminPanelSettings />
|
<Link
|
||||||
</Link>
|
href={PATHS.ADMIN}
|
||||||
)}
|
className='reset'
|
||||||
<SettingsModal />
|
>
|
||||||
|
<MdAdminPanelSettings
|
||||||
|
color='red'
|
||||||
|
size={24}
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
<SettingsModal />
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,17 +12,8 @@
|
|||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
& .user-card {
|
& button,
|
||||||
display: flex;
|
& a {
|
||||||
gap: 0.5em;
|
|
||||||
align-items: center;
|
|
||||||
|
|
||||||
& img {
|
|
||||||
border-radius: 50%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& button {
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
color: $blue;
|
color: $blue;
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -33,3 +24,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.user-card {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
& img {
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35em;
|
||||||
|
}
|
||||||
|
|||||||
1
src/constants/date.ts
Normal file
1
src/constants/date.ts
Normal file
@@ -0,0 +1 @@
|
|||||||
|
export const DATE_FORMAT = 'DD MMM YYYY (HH:mm)';
|
||||||
@@ -3,13 +3,13 @@ import clsx from 'clsx';
|
|||||||
import Navbar from 'components/Navbar/Navbar';
|
import Navbar from 'components/Navbar/Navbar';
|
||||||
import PageTransition from 'components/PageTransition';
|
import PageTransition from 'components/PageTransition';
|
||||||
import RoundedImage from 'components/RoundedImage/RoundedImage';
|
import RoundedImage from 'components/RoundedImage/RoundedImage';
|
||||||
|
import { DATE_FORMAT } from 'constants/date';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import relativeTime from 'dayjs/plugin/relativeTime';
|
import relativeTime from 'dayjs/plugin/relativeTime';
|
||||||
import { getServerSideTranslation } from 'i18n';
|
import { getServerSideTranslation } from 'i18n';
|
||||||
import getUsers from 'lib/user/getUsers';
|
import getUsers from 'lib/user/getUsers';
|
||||||
import { useTranslation } from 'next-i18next';
|
import { useTranslation } from 'next-i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
|
|
||||||
import 'react-tabs/style/react-tabs.css';
|
import 'react-tabs/style/react-tabs.css';
|
||||||
import styles from 'styles/admin.module.scss';
|
import styles from 'styles/admin.module.scss';
|
||||||
import { withAuthentication } from 'utils/session';
|
import { withAuthentication } from 'utils/session';
|
||||||
@@ -19,103 +19,114 @@ dayjs.extend(relativeTime);
|
|||||||
type UserExtended = User & { _count: { categories: number; links: number } };
|
type UserExtended = User & { _count: { categories: number; links: number } };
|
||||||
|
|
||||||
export default function AdminDashboard({ users }: { users: UserExtended[] }) {
|
export default function AdminDashboard({ users }: { users: UserExtended[] }) {
|
||||||
const { t } = useTranslation('common');
|
const { t } = useTranslation();
|
||||||
|
const totalCategories = users.reduce(
|
||||||
|
(acc, user) => (acc = acc + user._count.categories),
|
||||||
|
0,
|
||||||
|
);
|
||||||
|
const totalLinks = users.reduce(
|
||||||
|
(acc, user) => (acc = acc + user._count.links),
|
||||||
|
0,
|
||||||
|
);
|
||||||
return (
|
return (
|
||||||
<PageTransition className={styles['admin']}>
|
<PageTransition className={styles['admin']}>
|
||||||
<Navbar />
|
<Navbar />
|
||||||
<Tabs>
|
<div style={{ overflow: 'auto', marginTop: '1em' }}>
|
||||||
<TabList>
|
<table>
|
||||||
<Tab>{t('common:users')}</Tab>
|
<thead>
|
||||||
<Tab>{t('common:stats')}</Tab>
|
<tr>
|
||||||
</TabList>
|
<TableCell type='th'>#</TableCell>
|
||||||
|
<TableCell type='th'>{t('common:name')}</TableCell>
|
||||||
<TabPanel>
|
<TableCell type='th'>{t('common:email')}</TableCell>
|
||||||
<p>
|
<TableCell type='th'>
|
||||||
{users.length} {t('admin:users')}
|
{t('common:category.categories')} <b>({totalCategories})</b>
|
||||||
</p>
|
</TableCell>
|
||||||
<table>
|
<TableCell type='th'>
|
||||||
<thead>
|
{t('common:link.links')} <b>({totalLinks})</b>
|
||||||
<tr>
|
</TableCell>
|
||||||
<TH>#</TH>
|
<TableCell type='th'>{t('admin:role')}</TableCell>
|
||||||
<TH>{t('common:name')}</TH>
|
<TableCell type='th'>{t('admin:created_at')}</TableCell>
|
||||||
<TH>{t('common:email')}</TH>
|
<TableCell type='th'>{t('admin:updated_at')}</TableCell>
|
||||||
<TH>{t('common:category.categories')}</TH>
|
</tr>
|
||||||
<TH>{t('common:link.links')}</TH>
|
</thead>
|
||||||
<TH>{t('admin:role')}</TH>
|
<tbody>
|
||||||
<TH>{t('admin:created_at')}</TH>
|
{users.length !== 0 &&
|
||||||
<TH>{t('admin:updated_at')}</TH>
|
users.map((user) => (
|
||||||
</tr>
|
<TableUserRow
|
||||||
</thead>
|
user={user}
|
||||||
<tbody>
|
key={user.id}
|
||||||
{users.length !== 0 &&
|
/>
|
||||||
users.map(
|
))}
|
||||||
({
|
</tbody>
|
||||||
id,
|
</table>
|
||||||
image,
|
</div>
|
||||||
name,
|
|
||||||
email,
|
|
||||||
_count,
|
|
||||||
is_admin,
|
|
||||||
createdAt,
|
|
||||||
updatedAt,
|
|
||||||
}) => (
|
|
||||||
<tr key={id}>
|
|
||||||
<TD>{id}</TD>
|
|
||||||
<TD fixed>
|
|
||||||
{image && (
|
|
||||||
<RoundedImage
|
|
||||||
src={image}
|
|
||||||
width={24}
|
|
||||||
height={24}
|
|
||||||
alt={name}
|
|
||||||
title={name}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{name ?? '-'}
|
|
||||||
</TD>
|
|
||||||
<TD>{email}</TD>
|
|
||||||
<TD>{_count.categories}</TD>
|
|
||||||
<TD>{_count.links}</TD>
|
|
||||||
<TD>
|
|
||||||
{is_admin ? (
|
|
||||||
<span style={{ color: 'red' }}>
|
|
||||||
{t('admin:admin')}
|
|
||||||
</span>
|
|
||||||
) : (
|
|
||||||
<span style={{ color: 'green' }}>
|
|
||||||
{t('admin:user')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</TD>
|
|
||||||
<TD>{dayjs(createdAt).fromNow()}</TD>
|
|
||||||
<TD>{dayjs(updatedAt).fromNow()}</TD>
|
|
||||||
</tr>
|
|
||||||
),
|
|
||||||
)}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</TabPanel>
|
|
||||||
<TabPanel>{/* // stats */}</TabPanel>
|
|
||||||
</Tabs>
|
|
||||||
</PageTransition>
|
</PageTransition>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
type TableItem = { children: ReactNode; fixed?: boolean };
|
function TableUserRow({ user }: { user: UserExtended }) {
|
||||||
function TH({ children, fixed = false }: TableItem) {
|
const { t } = useTranslation();
|
||||||
|
const { id, image, name, email, _count, is_admin, createdAt, updatedAt } =
|
||||||
|
user;
|
||||||
return (
|
return (
|
||||||
<th>
|
<tr>
|
||||||
<div className={clsx('cell', fixed && 'fixed')}>{children}</div>
|
<TableCell type='td'>{id}</TableCell>
|
||||||
</th>
|
<TableCell
|
||||||
|
type='td'
|
||||||
|
fixed
|
||||||
|
>
|
||||||
|
{image && (
|
||||||
|
<RoundedImage
|
||||||
|
src={image}
|
||||||
|
width={32}
|
||||||
|
height={32}
|
||||||
|
alt={name}
|
||||||
|
title={name}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{name ?? '-'}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell type='td'>{email}</TableCell>
|
||||||
|
<TableCell type='td'>{_count.categories}</TableCell>
|
||||||
|
<TableCell type='td'>{_count.links}</TableCell>
|
||||||
|
<TableCell type='td'>
|
||||||
|
{is_admin ? (
|
||||||
|
<span style={{ color: 'red' }}>{t('admin:admin')}</span>
|
||||||
|
) : (
|
||||||
|
<span style={{ color: 'green' }}>{t('admin:user')}</span>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell
|
||||||
|
type='td'
|
||||||
|
column
|
||||||
|
>
|
||||||
|
<span>{dayjs(createdAt).fromNow()}</span>
|
||||||
|
<span>{dayjs(createdAt).format(DATE_FORMAT)}</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell type='td'>{dayjs(updatedAt).fromNow()}</TableCell>
|
||||||
|
</tr>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function TD({ children, fixed = false }: TableItem) {
|
type TableItem = {
|
||||||
return (
|
children: ReactNode;
|
||||||
<td>
|
fixed?: boolean;
|
||||||
<div className={clsx('cell', fixed && 'fixed')}>{children}</div>
|
column?: boolean;
|
||||||
</td>
|
type: 'td' | 'th';
|
||||||
|
};
|
||||||
|
|
||||||
|
function TableCell({
|
||||||
|
children,
|
||||||
|
fixed = false,
|
||||||
|
column = false,
|
||||||
|
type,
|
||||||
|
}: TableItem) {
|
||||||
|
const child = (
|
||||||
|
<div className={clsx('cell', fixed && 'fixed', column && 'column')}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
|
return type === 'td' ? <td>{child}</td> : <th>{child}</th>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = withAuthentication(
|
export const getServerSideProps = withAuthentication(
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ body {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
a {
|
a:not(.reset) {
|
||||||
width: fit-content;
|
width: fit-content;
|
||||||
color: $blue;
|
color: $blue;
|
||||||
border-bottom: 1px solid transparent;
|
border-bottom: 1px solid transparent;
|
||||||
@@ -51,6 +51,12 @@ a {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.reset {
|
||||||
|
color: $blue;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: 0;
|
||||||
|
}
|
||||||
|
|
||||||
h1,
|
h1,
|
||||||
h2,
|
h2,
|
||||||
h3,
|
h3,
|
||||||
|
|||||||
@@ -4,16 +4,21 @@ table {
|
|||||||
height: auto;
|
height: auto;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
border-collapse: collapse;
|
border-collapse: collapse;
|
||||||
width: 100%;
|
}
|
||||||
|
|
||||||
|
th {
|
||||||
|
font-weight: 400;
|
||||||
|
background-color: $lightest-blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
td,
|
td,
|
||||||
th {
|
th {
|
||||||
padding: 1em;
|
padding: 0.45em;
|
||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th,
|
||||||
background-color: $lightest-blue;
|
td {
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr:nth-child(even) {
|
tr:nth-child(even) {
|
||||||
@@ -24,9 +29,14 @@ table .cell {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.25em;
|
gap: 0.35em;
|
||||||
|
|
||||||
&:not(.fixed) {
|
&:not(.fixed) {
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.column {
|
||||||
|
gap: 0;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user