Merge branch 'main' into PRWLR-4393-Setup-NextAuth-client-session

This commit is contained in:
Pablo Lara
2024-08-26 15:46:03 +02:00
19 changed files with 643 additions and 17 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ export const getErrorMessage = (error: unknown): string => {
} else if (typeof error === "string") {
message = error;
} else {
message = "Wops! Something when wrong.";
message = "Oops! Something went wrong.";
}
return message;
};
+1 -1
View File
@@ -119,7 +119,7 @@ export const getErrorMessage = (error: unknown): string => {
} else if (typeof error === "string") {
message = error;
} else {
message = "Wops! Something when wrong.";
message = "Oops! Something went wrong.";
}
return message;
};
+1 -1
View File
@@ -33,7 +33,7 @@ export const getErrorMessage = (error: unknown): string => {
} else if (typeof error === "string") {
message = error;
} else {
message = "Wops! Something when wrong.";
message = "Oops! Something went wrong.";
}
return message;
};
+39
View File
@@ -0,0 +1,39 @@
"use server";
import { revalidatePath } from "next/cache";
import { redirect } from "next/navigation";
import { parseStringify } from "@/lib";
export const getUsers = async ({ page = 1 }) => {
if (isNaN(Number(page)) || page < 1) redirect("/users");
const keyServer = process.env.LOCAL_SITE_URL;
try {
const users = await fetch(
`${keyServer}/api/users?page%5Bnumber%5D=${page}`,
);
const data = await users.json();
const parsedData = parseStringify(data);
revalidatePath("/users");
return parsedData;
} catch (error) {
console.error("Error fetching Users:", error);
return undefined;
}
};
export const getErrorMessage = (error: unknown): string => {
let message: string;
if (error instanceof Error) {
message = error.message;
} else if (error && typeof error === "object" && "message" in error) {
message = String(error.message);
} else if (typeof error === "string") {
message = error;
} else {
message = "Oops! Something went wrong.";
}
return message;
};
+12
View File
@@ -0,0 +1,12 @@
import { Spacer } from "@nextui-org/react";
import { Header } from "@/components/ui";
export default async function Categories() {
return (
<>
<Header title="Categories" icon="material-symbols:folder-open-outline" />
<Spacer y={4} />
</>
);
}
@@ -2,10 +2,10 @@ import React from "react";
import { Header } from "@/components/ui";
export default function Integration() {
export default function Integrations() {
return (
<>
<Header title="Integration" icon="tabler:puzzle" />
<Header title="Integrations" icon="tabler:puzzle" />
<p>Hi hi from Integration page</p>
</>
+43
View File
@@ -0,0 +1,43 @@
import { Spacer } from "@nextui-org/react";
import { redirect } from "next/navigation";
import { Suspense } from "react";
import { getUsers } from "@/actions/users";
import { Header } from "@/components/ui";
import {
ColumnsUser,
DataTableUser,
SkeletonTableUser,
} from "@/components/users";
import { searchParamsProps } from "@/types";
export default async function Users({ searchParams }: searchParamsProps) {
return (
<>
<Header title="User Management" icon="ci:users" />
<Spacer y={4} />
<div className="flex flex-col items-end w-full">
<Spacer y={6} />
<Suspense key={searchParams.page} fallback={<SkeletonTableUser />}>
<SSRDataTable searchParams={searchParams} />
</Suspense>
</div>
</>
);
}
const SSRDataTable = async ({ searchParams }: searchParamsProps) => {
const page = searchParams.page ? parseInt(searchParams.page) : 1;
const usersData = await getUsers({ page });
const [users] = await Promise.all([usersData]);
if (users?.errors) redirect("/users");
return (
<DataTableUser
columns={ColumnsUser}
data={users?.users?.data ?? []}
metadata={users?.meta}
/>
);
};
+12
View File
@@ -0,0 +1,12 @@
import { Spacer } from "@nextui-org/react";
import { Header } from "@/components/ui";
export default async function Workloads() {
return (
<>
<Header title="Workloads" icon="lucide:tags" />
<Spacer y={4} />
</>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { NextResponse } from "next/server";
import data from "../../../dataUsers.json";
export async function GET() {
// Simulate fetching data with a delay
await new Promise((resolve) => setTimeout(resolve, 2000));
return NextResponse.json({ users: data });
}
+23 -11
View File
@@ -126,12 +126,6 @@ export const sectionItems: SidebarItem[] = [
// />
// ),
// },
{
key: "services",
href: "/services",
icon: "material-symbols:linked-services-outline",
title: "Services",
},
{
key: "compliance",
href: "/compliance",
@@ -143,6 +137,24 @@ export const sectionItems: SidebarItem[] = [
</Chip>
),
},
{
key: "services",
href: "/services",
icon: "material-symbols:linked-services-outline",
title: "Services",
},
{
key: "categories",
href: "/categories",
icon: "material-symbols:folder-open-outline",
title: "Categories",
},
{
key: "workloads",
href: "/workloads",
icon: "lucide:tags",
title: "Workloads",
},
],
},
{
@@ -190,10 +202,10 @@ export const sectionItems: SidebarItem[] = [
),
},
{
key: "integration",
href: "/integration",
key: "integrations",
href: "/integrations",
icon: "tabler:puzzle",
title: "Integration",
title: "Integrations",
},
{
key: "settings",
@@ -213,9 +225,9 @@ export const sectionItemsWithTeams: SidebarItem[] = [
items: [
{
key: "users",
href: "#",
href: "/users",
title: "Users",
startContent: <TeamAvatar name="Users page" />,
icon: "ci:users",
},
{
key: "roles",
+5 -1
View File
@@ -7,7 +7,9 @@ type Status =
| "cancelled"
| "fail"
| "success"
| "muted";
| "muted"
| "active"
| "inactive";
const statusColorMap: Record<
Status,
@@ -19,6 +21,8 @@ const statusColorMap: Record<
fail: "danger",
success: "success",
muted: "default",
active: "success",
inactive: "default",
};
export const StatusBadge = ({ status }: { status: Status }) => {
+5
View File
@@ -0,0 +1,5 @@
export * from "./table/ColumnsUser";
export * from "./table/DataTableColumnHeader";
export * from "./table/DataTablePagination";
export * from "./table/DataTableUser";
export * from "./table/SkeletonTableUser";
+83
View File
@@ -0,0 +1,83 @@
"use client";
import {
Button,
Dropdown,
DropdownItem,
DropdownMenu,
DropdownTrigger,
} from "@nextui-org/react";
import { ColumnDef } from "@tanstack/react-table";
import { VerticalDotsIcon } from "@/components/icons";
import { DateWithTime } from "@/components/providers";
import { StatusBadge } from "@/components/ui";
import { UserProps } from "@/types";
const getUserData = (row: { original: UserProps }) => {
return row.original;
};
export const ColumnsUser: ColumnDef<UserProps>[] = [
{
accessorKey: "email",
header: "Email",
cell: ({ row }) => {
const { email } = getUserData(row);
return <p className="font-semibold">{email}</p>;
},
},
{
accessorKey: "name",
header: "Name",
cell: ({ row }) => {
const { name } = getUserData(row);
return <p className="font-semibold">{name}</p>;
},
},
{
accessorKey: "role",
header: "Role",
cell: ({ row }) => {
const { role } = getUserData(row);
return <p className="font-semibold">{role}</p>;
},
},
{
accessorKey: "added",
header: "Added",
cell: ({ row }) => {
const { dateAdded } = getUserData(row);
return <DateWithTime dateTime={dateAdded} showTime={false} />;
},
},
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const { status } = getUserData(row);
return <StatusBadge status={status} />;
},
},
{
accessorKey: "actions",
header: () => <div className="text-right">Actions</div>,
id: "actions",
cell: () => {
return (
<div className="relative flex justify-end items-center gap-2">
<Dropdown className="bg-background border-1 border-default-200">
<DropdownTrigger>
<Button isIconOnly radius="full" size="sm" variant="light">
<VerticalDotsIcon className="text-default-400" />
</Button>
</DropdownTrigger>
<DropdownMenu>
<DropdownItem>Edit</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
);
},
},
];
@@ -0,0 +1,49 @@
import { Button } from "@nextui-org/react";
import { Column } from "@tanstack/react-table";
import {
ArrowDownIcon,
ArrowUpIcon,
ChevronsLeftRightIcon,
} from "lucide-react";
import { HTMLAttributes } from "react";
interface DataTableColumnHeaderProps<TData, TValue>
extends HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>;
title: string;
}
export const DataTableColumnHeader = <TData, TValue>({
column,
title,
className,
}: DataTableColumnHeaderProps<TData, TValue>) => {
const renderSortIcon = () => {
const sort = column.getIsSorted();
if (!sort) {
return <ChevronsLeftRightIcon className="ml-2 h-4 w-4 rotate-90" />;
}
return sort === "desc" ? (
<ArrowDownIcon className="ml-2 h-4 w-4" />
) : (
<ArrowUpIcon className="ml-2 h-4 w-4" />
);
};
if (!column.getCanSort()) {
return <div className={className}>{title}</div>;
}
return (
<div className={className}>
<Button
variant="ghost"
size="sm"
className="h-8"
onClick={column.getToggleSortingHandler()}
>
<span>{title}</span>
{renderSortIcon()}
</Button>
</div>
);
};
@@ -0,0 +1,85 @@
"use client";
import {
ChevronLeftIcon,
ChevronRightIcon,
DoubleArrowLeftIcon,
DoubleArrowRightIcon,
} from "@radix-ui/react-icons";
import Link from "next/link";
import { usePathname, useSearchParams } from "next/navigation";
import { extractPaginationInfo } from "@/lib";
import { MetaDataProps } from "@/types";
interface DataTablePaginationProps {
pageSizeOptions?: number[];
metadata?: MetaDataProps;
}
export function DataTablePagination({ metadata }: DataTablePaginationProps) {
if (!metadata) return null;
const pathname = usePathname();
const searchParams = useSearchParams();
const { currentPage, totalPages, totalEntries } =
extractPaginationInfo(metadata);
const createPageUrl = (pageNumber: number | string) => {
const params = new URLSearchParams(searchParams);
if (pageNumber === "...") return `${pathname}?${params.toString()}`;
if (+pageNumber > totalPages) {
return `${pathname}?${params.toString()}`;
}
params.set("page", pageNumber.toString());
return `${pathname}?${params.toString()}`;
};
return (
<div className="flex w-full flex-col-reverse items-center justify-between gap-4 overflow-auto p-1 sm:flex-row sm:gap-8">
<div className="whitespace-nowrap text-sm font-medium">
{totalEntries} entries in Total.
</div>
<div className="flex flex-col-reverse items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
<div className="flex items-center justify-center text-sm font-medium">
Page {currentPage} of {totalPages}
</div>
<div className="flex items-center space-x-2">
<Link
aria-label="Go to first page"
className="page-link relative block py-1.5 px-3 border-0 bg-transparent outline-none transition-all duration-300 rounded text-gray-800 hover:text-gray-800 hover:bg-gray-200 focus:shadow-none"
href={createPageUrl(1)}
aria-disabled="true"
>
<DoubleArrowLeftIcon className="size-4" aria-hidden="true" />
</Link>
<Link
aria-label="Go to previous page"
className="page-link relative block py-1.5 px-3 border-0 bg-transparent outline-none transition-all duration-300 rounded text-gray-800 hover:text-gray-800 hover:bg-gray-200 focus:shadow-none"
href={createPageUrl(currentPage - 1)}
aria-disabled="true"
>
<ChevronLeftIcon className="size-4" aria-hidden="true" />
</Link>
<Link
aria-label="Go to next page"
className="page-link relative block py-1.5 px-3 border-0 bg-transparent outline-none transition-all duration-300 rounded text-gray-800 hover:text-gray-800 hover:bg-gray-200 focus:shadow-none"
href={createPageUrl(currentPage + 1)}
>
<ChevronRightIcon className="size-4" aria-hidden="true" />
</Link>
<Link
aria-label="Go to last page"
className="page-link relative block py-1.5 px-3 border-0 bg-transparent outline-none transition-all duration-300 rounded text-gray-800 hover:text-gray-800 hover:bg-gray-200 focus:shadow-none"
href={createPageUrl(totalPages)}
>
<DoubleArrowRightIcon className="size-4" aria-hidden="true" />
</Link>
</div>
</div>
</div>
);
}
+106
View File
@@ -0,0 +1,106 @@
"use client";
import {
ColumnDef,
flexRender,
getCoreRowModel,
getPaginationRowModel,
getSortedRowModel,
SortingState,
useReactTable,
} from "@tanstack/react-table";
import { useState } from "react";
import { DataTablePagination } from "@/components/providers";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui";
import { MetaDataProps } from "@/types";
interface DataTableUserProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
metadata?: MetaDataProps;
}
export function DataTableUser<TData, TValue>({
columns,
data,
metadata,
}: DataTableUserProps<TData, TValue>) {
const [sorting, setSorting] = useState<SortingState>([]);
const table = useReactTable({
data,
columns,
enableSorting: true,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
state: {
sorting,
},
});
return (
<>
<div className="rounded-md border w-full">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead key={header.id}>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef.header,
header.getContext(),
)}
</TableHead>
);
})}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
table.getRowModel().rows.map((row) => (
<TableRow
key={row.id}
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}>
{flexRender(
cell.column.columnDef.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
) : (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 text-center"
>
No results.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="flex items-center w-full space-x-2 py-4">
<DataTablePagination metadata={metadata} />
</div>
</>
);
}
@@ -0,0 +1,59 @@
import { Card, Skeleton } from "@nextui-org/react";
import React from "react";
export const SkeletonTableUser = () => {
return (
<Card className="w-full h-full space-y-5 p-4" radius="sm">
{/* Table headers */}
<div className="hidden md:flex justify-between">
<Skeleton className="w-2/12 rounded-lg">
<div className="h-8 bg-default-200"></div>
</Skeleton>
<Skeleton className="w-2/12 rounded-lg">
<div className="h-8 bg-default-200"></div>
</Skeleton>
<Skeleton className="w-2/12 rounded-lg">
<div className="h-8 bg-default-200"></div>
</Skeleton>
<Skeleton className="w-2/12 rounded-lg">
<div className="h-8 bg-default-200"></div>
</Skeleton>
<Skeleton className="w-2/12 rounded-lg">
<div className="h-8 bg-default-200"></div>
</Skeleton>
<Skeleton className="w-1/12 rounded-lg">
<div className="h-8 bg-default-200"></div>
</Skeleton>
</div>
{/* Table body */}
<div className="space-y-3">
{[...Array(10)].map((_, index) => (
<div
key={index}
className="flex flex-col md:flex-row justify-between items-center space-x-0 md:space-x-4"
>
<Skeleton className="w-full md:w-2/12 rounded-lg mb-2 md:mb-0">
<div className="h-12 bg-default-300"></div>
</Skeleton>
<Skeleton className="w-full md:w-2/12 rounded-lg mb-2 md:mb-0">
<div className="h-12 bg-default-300"></div>
</Skeleton>
<Skeleton className="hidden sm:flex md:w-2/12 rounded-lg mb-2 md:mb-0">
<div className="h-12 bg-default-300"></div>
</Skeleton>
<Skeleton className="hidden sm:flex md:w-2/12 rounded-lg mb-2 md:mb-0">
<div className="h-12 bg-default-300"></div>
</Skeleton>
<Skeleton className="hidden sm:flex md:w-2/12 rounded-lg mb-2 md:mb-0">
<div className="h-12 bg-default-300"></div>
</Skeleton>
<Skeleton className="hidden sm:flex md:w-1/12 rounded-lg mb-2 md:mb-0">
<div className="h-12 bg-default-300"></div>
</Skeleton>
</div>
))}
</div>
</Card>
);
};
+98
View File
@@ -0,0 +1,98 @@
{
"links": {
"first": "http://localhost:8080/api/v1/users?page%5Bnumber%5D=1",
"last": "http://localhost:8080/api/v1/users?page%5Bnumber%5D=1",
"next": null,
"prev": null
},
"data": [
{
"id": "001",
"email": "john.doe@example.com",
"name": "John Doe",
"role": "Admin",
"dateAdded": "2024-08-14T10:00:00.000000Z",
"status": "active"
},
{
"id": "002",
"email": "jane.smith@example.com",
"name": "Jane Smith",
"role": "User",
"dateAdded": "2024-08-15T11:30:00.000000Z",
"status": "inactive"
},
{
"id": "003",
"email": "will.johnson@example.com",
"name": "Will Johnson",
"role": "Admin",
"dateAdded": "2024-08-16T09:15:00.000000Z",
"status": "active"
},
{
"id": "004",
"email": "emily.davis@example.com",
"name": "Emily Davis",
"role": "User",
"dateAdded": "2024-08-17T14:00:00.000000Z",
"status": "active"
},
{
"id": "005",
"email": "michael.brown@example.com",
"name": "Michael Brown",
"role": "User",
"dateAdded": "2024-08-18T08:45:00.000000Z",
"status": "inactive"
},
{
"id": "006",
"email": "sarah.miller@example.com",
"name": "Sarah Miller",
"role": "Admin",
"dateAdded": "2024-08-19T13:25:00.000000Z",
"status": "active"
},
{
"id": "007",
"email": "david.wilson@example.com",
"name": "David Wilson",
"role": "User",
"dateAdded": "2024-08-20T10:50:00.000000Z",
"status": "active"
},
{
"id": "008",
"email": "lisa.moore@example.com",
"name": "Lisa Moore",
"role": "Admin",
"dateAdded": "2024-08-21T07:30:00.000000Z",
"status": "inactive"
},
{
"id": "009",
"email": "james.taylor@example.com",
"name": "James Taylor",
"role": "User",
"dateAdded": "2024-08-22T12:10:00.000000Z",
"status": "active"
},
{
"id": "010",
"email": "anna.anderson@example.com",
"name": "Anna Anderson",
"role": "User",
"dateAdded": "2024-08-23T11:00:00.000000Z",
"status": "inactive"
}
],
"meta": {
"pagination": {
"page": 1,
"pages": 1,
"count": 10
},
"version": "v1"
}
}
+9
View File
@@ -80,3 +80,12 @@ export interface MetaDataProps {
};
version: string;
}
export interface UserProps {
id: string;
email: string;
name: string;
role: string;
dateAdded: string;
status: "active" | "inactive";
}