Darkmift user table (#553)

* [user-table] added deps for feature

* [user-table] added first component draft

* [user-table] use of table in dashboard

* [user-table] zindex removed disrupts click events

* [user-table] WIP fetch data from backend

* [user-table] TODO:fix generic type declaration

* [user-table] fetches data,TODO refetch on reload

* [user-table] fixed page reload not fetching data

* [user-table] added filter input

* [user-table] issue scope/serialize on table object

* [user-table] applied my own table

* [user-table] added types

* [user-table] excluding local mock

* [user-table] added server paginated table

* [user-table] rewrite to use paginated table

* [user-table] local /remote pagination hybrid

* [user-table] applied some style

* [user-table] removed mock ignore

* [user-table] removed tanstack table

* [user-table] deprecated file

* [user-table] applied enum

* [user-table] deprecated file

* [user-table] removed unused style

* [user-table] removed unused style

* [user-table] commenting out logs

* [user-table] removed file

* [user-table] moved types under file

* [user-table] minor css tweaks

* [user-table] renamed files

* [user-table] api call,file import,total changes

* [user-table] added option to hide columns

* [user-table] type change,total page fix value

* [user-table] code refactoring

* [user-table] refactor for optional attributes

* [user-table] set sort to null

* [user-table] deprecated unused file

* [user-table] added util

* [user-table] fixed maxpage ,disabled page input

* [user-table] migrated pagination to reusable hook

* [user-table] refactor code

* [user-table] migrated fn back into file

* [user-table] removeed comment

* [user-table] cleanup of comments

* [user-table] progress

* [user-table] work towards abstrated cmp

* wip: cleaning some stuff

* fix: drawer hight

* fix: use visibleTask instead of resource

* fix: remove old code

* feat: add document head for the users route

* fix: columns orders

* feat: add sortable option

---------

Co-authored-by: darkmift <darkmift@gmail.com>
Co-authored-by: orig <oriorigranot@gamil.com>
This commit is contained in:
orig
2023-10-29 23:07:11 +02:00
committed by GitHub
parent d84bc7dd73
commit 86bd583aae
8 changed files with 325 additions and 61 deletions

View File

@ -0,0 +1,19 @@
import { component$, Signal } from '@builder.io/qwik';
export interface FilterInputProps {
filter: Signal<string>;
onInput: (ev: InputEvent) => void;
}
export const FilterInput = component$<FilterInputProps>(({ filter, onInput }) => {
return (
<input
type="text"
value={filter.value || ''}
onInput$={(ev: InputEvent) => onInput(ev)}
placeholder={`Search...`}
style={{ width: '100%' }}
class="input input-bordered w-full max-w-xs mb-5"
/>
);
});

View File

@ -0,0 +1,38 @@
import { Signal, component$ } from '@builder.io/qwik';
import { ResponseData } from './table-server-pagination';
export interface PaginationActionsProps {
tableData: Signal<ResponseData>;
page: Signal<number>;
limit: Signal<number>;
maxPages: Signal<number>;
isOnFirstPage: Signal<boolean>;
isOnLastPage: Signal<boolean>;
}
export const PaginationActions = component$((props: PaginationActionsProps) => {
const { page, limit, tableData, maxPages, isOnFirstPage, isOnLastPage } = props;
return (
<div class="flex gap-2">
<div class="join">
<button class="join-item btn btn-xs sm:btn-md" onClick$={() => (page.value = 1)} disabled={isOnFirstPage.value}>
{'<<'}
</button>
<button class="join-item btn btn-xs sm:btn-md" onClick$={() => (page.value = page.value - 1)} disabled={isOnFirstPage.value}>
{'<'}
</button>
<button class="join-item btn btn-xs sm:btn-md" onClick$={() => (page.value = page.value + 1)} disabled={isOnLastPage.value}>
{'>'}
</button>
<button class="join-item btn btn-xs sm:btn-md" onClick$={() => (page.value = maxPages.value)} disabled={isOnLastPage.value}>
{'>>'}
</button>
</div>
<span class="flex items-center gap-1">
Page {page.value} of {maxPages.value}
</span>
</div>
);
});

View File

@ -0,0 +1,190 @@
import { component$, useSignal, $, PropFunction, useVisibleTask$ } from '@builder.io/qwik';
import { FilterInput } from './default-filter';
import { authorizedFetch } from '../../shared/auth.service';
import { PaginationActions } from './pagination-actions';
import { HiChevronDownOutline, HiChevronUpDownOutline, HiChevronUpOutline } from '@qwikest/icons/heroicons';
export enum SortOrder {
DESC = 'desc',
ASC = 'asc',
}
export interface PaginationParams {
limit: number;
page: number;
filter: string;
sort: Record<string, SortOrder>;
}
export type PaginationFetcher = ({ limit, page, filter, sort }: PaginationParams) => Promise<ResponseData>;
export type OptionalHeader = {
displayName?: string;
classNames?: string;
hide?: boolean;
sortable?: boolean;
};
export type Columns = Record<string, OptionalHeader>;
export interface TableServerPaginationParams {
endpoint: string;
columns: Columns;
pageSize?: number;
}
export interface ResponseData {
total: number;
data: Record<string, unknown>[];
}
export const serializeQueryUserPaginationParams = (paginationParams: PaginationParams) => {
const paramsForQuery = new URLSearchParams();
paramsForQuery.set('limit', paginationParams.limit.toString());
paramsForQuery.set('page', paginationParams.page.toString());
if (paginationParams.filter) {
paramsForQuery.set('filter', paginationParams.filter);
}
if (paginationParams.sort) {
Object.entries(paginationParams.sort).forEach(([key, value]) => {
paramsForQuery.set(`sort[${key}]`, value);
});
}
return paramsForQuery.toString();
};
export const TableServerPagination = component$((props: TableServerPaginationParams) => {
const filter = useSignal('');
const currentPage = useSignal(1);
const limit = useSignal(10);
const sortSignal = useSignal<Record<string, SortOrder>>({});
const tableData = useSignal<ResponseData>({ total: 0, data: [] });
const maxPages = useSignal(0);
const isOnFirstPage = useSignal(true);
const isOnLastPage = useSignal(true);
const isLoading = useSignal(true);
const fetchTableData: PropFunction<PaginationFetcher> = $(async (paginationParams: PaginationParams) => {
const queryParams = serializeQueryUserPaginationParams(paginationParams);
const data = await authorizedFetch(`${props.endpoint}?${queryParams}`);
const response = (await data.json()) as ResponseData;
if (!response || !response.data) {
console.warn('Server response is not valid', response);
response.total = 0;
response.data = [];
}
return response;
});
const onFilterInputChange = $(async (ev: InputEvent) => {
filter.value = (ev.target as HTMLInputElement).value;
currentPage.value = 1;
});
useVisibleTask$(async ({ track }) => {
track(() => currentPage.value);
track(() => limit.value);
track(() => filter.value);
track(() => sortSignal.value);
// Fetch data
const result = await fetchTableData({
page: currentPage.value,
limit: limit.value,
filter: filter.value,
sort: sortSignal.value,
});
tableData.value = result;
maxPages.value = Math.ceil(result.total / limit.value || 1);
// Update isOnFirstPage and isOnLastPage
isOnFirstPage.value = currentPage.value === 1;
isOnLastPage.value = currentPage.value >= maxPages.value;
isLoading.value = false;
});
const sortColumn = (columnName: string) =>
$(() => {
if (!props.columns[columnName].sortable) return;
const currentSortOrder = sortSignal.value[columnName];
if (!currentSortOrder || currentSortOrder === SortOrder.DESC) {
sortSignal.value = { [columnName]: SortOrder.ASC };
} else {
sortSignal.value = { [columnName]: SortOrder.DESC };
}
// Reset the page to the first page when sorting
currentPage.value = 1;
});
return (
<div class="flex flex-col justify-start">
<FilterInput filter={filter} onInput={onFilterInputChange} />
{isLoading.value ? ( // Show loader covering the entire table
<div class="animate-pulse">
<div class="h-4 bg-base-200 mb-6 mt-2 rounded"></div>
{Array.from({ length: 12 }).map(() => (
<div class="h-4 bg-base-200 mb-6 rounded"></div>
))}
</div>
) : (
<>
<div class="overflow-x-auto">
<table id="table" class="table table-zebra table-fixed whitespace-nowrap">
<thead>
<tr>
{Object.keys(props.columns).map((columnName, idx) => {
if (props.columns[columnName].hide) return;
return (
<th class={props.columns[columnName].classNames ?? ''} onClick$={sortColumn(columnName)} key={idx}>
{props.columns[columnName].displayName ?? columnName}{' '}
{props.columns[columnName].sortable &&
(!sortSignal.value[columnName] ? (
<HiChevronUpDownOutline class="inline align-text-bottom text-base" />
) : sortSignal.value[columnName] === SortOrder.ASC ? (
<HiChevronUpOutline class="inline align-text-bottom" />
) : (
<HiChevronDownOutline class="inline align-text-bottom" />
))}
</th>
);
})}
</tr>
</thead>
<tbody>
{tableData.value.data.map((row, rowIndex) => (
<tr key={rowIndex}>
{Object.keys(props.columns).map((columnName, idx) => {
if (props.columns[columnName].hide) return;
return <td key={idx}>{row[columnName]?.toString()}</td>;
})}
</tr>
))}
</tbody>
</table>
</div>
<div class="flex pt-5 text-sm font-bold">
<PaginationActions
tableData={tableData}
limit={limit}
page={currentPage}
maxPages={maxPages}
isOnFirstPage={isOnFirstPage}
isOnLastPage={isOnLastPage}
/>
</div>
</>
)}
</div>
);
});

View File

@ -1,53 +0,0 @@
import { component$ } from '@builder.io/qwik';
import type { DocumentHead } from '@builder.io/qwik-city';
export default component$(() => {
return (
<>
<h1>Admin panel</h1>
<p class="text-red-700">//TODO: Implement a table of users</p>
</>
);
});
export const head: DocumentHead = {
title: 'Reduced.to | Admin panel',
meta: [
{
name: 'title',
content: 'Reduced.to | Admin panel',
},
{
name: 'description',
content: 'Reduced.to | Admin panel, see other users, and more!',
},
{
property: 'og:type',
content: 'website',
},
{
property: 'og:url',
content: 'https://reduced.to/dashboard/admin',
},
{
property: 'og:title',
content: 'Reduced.to | Admin panel',
},
{
property: 'og:description',
content: 'Reduced.to | Admin panel, see other users, and more!',
},
{
property: 'twitter:card',
content: 'summary',
},
{
property: 'twitter:title',
content: 'Reduced.to | Admin panel',
},
{
property: 'twitter:description',
content: 'Reduced.to | Admin panel, see other users, and more!',
},
],
};

View File

@ -0,0 +1,60 @@
import { component$ } from '@builder.io/qwik';
import { Columns, TableServerPagination } from '../../../../components/table/table-server-pagination';
import { DocumentHead } from '@builder.io/qwik-city';
export default component$(() => {
const columns: Columns = {
name: { displayName: 'Name', classNames: 'w-1/4', sortable: true },
email: { displayName: 'Email', classNames: 'w-1/4', sortable: true },
role: { displayName: 'Role', classNames: 'w-1/4', sortable: true },
verified: { displayName: 'Verified', classNames: 'w-1/4' },
};
return (
<>
<TableServerPagination endpoint={`${process.env.API_DOMAIN}/api/v1/users`} columns={columns} />
</>
);
});
export const head: DocumentHead = {
title: 'Reduced.to | Admin Dashboard - Users',
meta: [
{
name: 'title',
content: 'Reduced.to | Admin Dashboard - Users',
},
{
name: 'description',
content: 'Reduced.to | Admin Dashboard - See all users',
},
{
property: 'og:type',
content: 'website',
},
{
property: 'og:url',
content: 'https://reduced.to/dashboard',
},
{
property: 'og:title',
content: 'Reduced.to | Admin Dashboard - Users',
},
{
property: 'og:description',
content: 'Reduced.to | Admin Dashboard - See all users',
},
{
property: 'twitter:card',
content: 'summary',
},
{
property: 'twitter:title',
content: 'Reduced.to | Admin Dashboard - Users',
},
{
property: 'twitter:description',
content: 'Reduced.to | Admin Dashboard - See all users',
},
],
};

View File

@ -18,7 +18,7 @@ export default component$(() => {
const toggleDrawer = $(() => (isDrawerOpen.value = !isDrawerOpen.value));
return (
<div class="drawer lg:drawer-open h-[calc(100vh-64px)]">
<div class="drawer lg:drawer-open min-h-[calc(100vh-64px)]">
<input id="drawer" type="checkbox" class="drawer-toggle" checked={isDrawerOpen.value} onChange$={toggleDrawer} />
<div class="drawer-content w-100vh m-5">
<Slot />
@ -114,7 +114,8 @@ export default component$(() => {
</li>
<li>
<Link
class={`${location.url.pathname.slice(0, -1) === '/dashboard/admin/users' ? 'active' : ''} !cursor-not-allowed`}
href="/dashboard/admin/users"
class={`${location.url.pathname.slice(0, -1) === '/dashboard/admin/users' ? 'active' : ''}`}
onClick$={toggleDrawer}
>
<svg
@ -132,7 +133,6 @@ export default component$(() => {
/>
</svg>
<span class="font-medium">Users</span>
<span class="badge badge-neutral">Soon</span>
</Link>
</li>
</ul>

15
package-lock.json generated
View File

@ -27,6 +27,7 @@
"@novu/node": "^0.19.0",
"@origranot/ts-logger": "^1.12.0",
"@prisma/client": "^5.2.0",
"@qwikest/icons": "^0.0.13",
"axios": "^1.5.0",
"bcryptjs": "^2.4.3",
"cache-manager": "^5.2.3",
@ -2323,7 +2324,6 @@
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@builder.io/qwik/-/qwik-1.2.14.tgz",
"integrity": "sha512-z+NFSNrAKjCVV/9d2f50Y/AthcHdlH4Hbltyjjl/0wOv4FUR3F/Ke9wz9eIhDYQ6rzFqzdav2/yTiip05El1zA==",
"dev": true,
"dependencies": {
"csstype": "^3.1.2"
},
@ -3903,7 +3903,6 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz",
"integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==",
"dev": true,
"engines": {
"node": ">=14"
}
@ -7391,6 +7390,17 @@
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz",
"integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="
},
"node_modules/@qwikest/icons": {
"version": "0.0.13",
"resolved": "https://registry.npmjs.org/@qwikest/icons/-/icons-0.0.13.tgz",
"integrity": "sha512-e0wY8vmx0nDSUiuCATlk+ojTvdBV4txIGHHWjZW5SRkv4XB8H9+3WSDcLPz0ItUdRyzcrohE9k2jtQI/98aRPA==",
"engines": {
"node": ">=15.0.0"
},
"peerDependencies": {
"@builder.io/qwik": ">=1.0.0"
}
},
"node_modules/@redis/bloom": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz",
@ -24136,7 +24146,6 @@
"version": "5.26.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-5.26.2.tgz",
"integrity": "sha512-a4PDLQgLTPHVzOK+x3F79/M4GtyYPl+aX9AAK7aQxpwxDwCqkeZCScy7Gk5kWT3JtdFq1uhO3uZJdLtHI4dK9A==",
"dev": true,
"dependencies": {
"@fastify/busboy": "^2.0.0"
},

View File

@ -14,6 +14,7 @@
"@docusaurus/module-type-aliases": "^2.4.1",
"@nestjs/schematics": "^10.0.1",
"@nestjs/testing": "^10.0.2",
"@nx/eslint": "17.0.1",
"@nx/eslint-plugin": "17.0.1",
"@nx/jest": "17.0.1",
"@nx/js": "17.0.1",
@ -58,8 +59,7 @@
"undici": "^5.26.2",
"vite": "^4.4.9",
"vite-tsconfig-paths": "~4.2.0",
"vitest": "~0.32.0",
"@nx/eslint": "17.0.1"
"vitest": "~0.32.0"
},
"dependencies": {
"@docusaurus/core": "^2.4.1",
@ -76,6 +76,7 @@
"@novu/node": "^0.19.0",
"@origranot/ts-logger": "^1.12.0",
"@prisma/client": "^5.2.0",
"@qwikest/icons": "^0.0.13",
"axios": "^1.5.0",
"bcryptjs": "^2.4.3",
"cache-manager": "^5.2.3",