🔧 chore(frontend): add @radix-ui/react-select package to package.json dependencies
🔧 chore(frontend): add PaginatorComponent and Select component to the project 🔧 fix(api.tsx): comment out error retry logic to temporarily disable it ✨ feat(LoginPage): add login functionality for admin page 🆕 feat(AdminPage): add AdminPage component to display a list of users and provide filtering and pagination functionality 🐛 fix(AdminPage): fix handleInputChange function to update the password value correctly 🔨 refactor(AdminPage): refactor handleFilterUsers function to filter users based on user and email fields 🔥 chore(AdminPage): remove unused imports and console.log statements ✨ feat(routes.tsx): add routes for admin login and admin page to enable access to admin features 🔧 chore(components/index.ts): add PaginatorComponentType to define the type for a paginator component
This commit is contained in:
parent
c1d1684c73
commit
dd6d1e64f7
10 changed files with 3995 additions and 1904 deletions
5196
src/frontend/package-lock.json
generated
5196
src/frontend/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -17,6 +17,7 @@
|
||||||
"@radix-ui/react-menubar": "^1.0.3",
|
"@radix-ui/react-menubar": "^1.0.3",
|
||||||
"@radix-ui/react-popover": "^1.0.6",
|
"@radix-ui/react-popover": "^1.0.6",
|
||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
|
"@radix-ui/react-select": "^1.2.2",
|
||||||
"@radix-ui/react-separator": "^1.0.3",
|
"@radix-ui/react-separator": "^1.0.3",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
"@radix-ui/react-switch": "^1.0.3",
|
"@radix-ui/react-switch": "^1.0.3",
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,9 @@ export default function App() {
|
||||||
}>
|
}>
|
||||||
>([]);
|
>([]);
|
||||||
|
|
||||||
|
const isLoginPage = location.pathname.includes('login');
|
||||||
|
const isAdminPage = location.pathname.includes('admin');
|
||||||
|
|
||||||
// Use effect hook to update alertsList when a new alert is added
|
// Use effect hook to update alertsList when a new alert is added
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// If there is an error alert open with data, add it to the alertsList
|
// If there is an error alert open with data, add it to the alertsList
|
||||||
|
|
@ -120,6 +123,7 @@ export default function App() {
|
||||||
prevAlertsList.filter((alert) => alert.id !== id)
|
prevAlertsList.filter((alert) => alert.id !== id)
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
//need parent component with width and height
|
//need parent component with width and height
|
||||||
|
|
@ -133,7 +137,10 @@ export default function App() {
|
||||||
}}
|
}}
|
||||||
FallbackComponent={CrashErrorComponent}
|
FallbackComponent={CrashErrorComponent}
|
||||||
>
|
>
|
||||||
<Header />
|
{
|
||||||
|
!isLoginPage
|
||||||
|
&&
|
||||||
|
<Header />}
|
||||||
<Router />
|
<Router />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
<div></div>
|
<div></div>
|
||||||
|
|
|
||||||
123
src/frontend/src/components/PaginatorComponent/index.tsx
Normal file
123
src/frontend/src/components/PaginatorComponent/index.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import {
|
||||||
|
ChevronLeft,
|
||||||
|
ChevronRight,
|
||||||
|
ChevronsLeft,
|
||||||
|
ChevronsRight,
|
||||||
|
} from "lucide-react";
|
||||||
|
import { useState } from "react";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "../../components/ui/select";
|
||||||
|
import { PaginatorComponentType } from "../../types/components";
|
||||||
|
import { Button } from "../ui/button";
|
||||||
|
|
||||||
|
export default function PaginatorComponent({
|
||||||
|
pageSize = 10,
|
||||||
|
pageIndex = 1,
|
||||||
|
rowsCount = [10, 20, 30],
|
||||||
|
totalRowsCount = 0,
|
||||||
|
paginate,
|
||||||
|
}: PaginatorComponentType) {
|
||||||
|
const [size, setPageSize] = useState(pageSize);
|
||||||
|
const [index, setPageIndex] = useState(pageIndex);
|
||||||
|
|
||||||
|
const [maxIndex, setMaxPageIndex] = useState(
|
||||||
|
Math.ceil(totalRowsCount / pageSize)
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between px-2">
|
||||||
|
<div className="flex-1 text-sm text-muted-foreground"></div>
|
||||||
|
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<p className="text-sm font-medium">Rows per page</p>
|
||||||
|
<Select
|
||||||
|
onValueChange={(pageSize: string) => {
|
||||||
|
setPageSize(Number(pageSize));
|
||||||
|
setMaxPageIndex(Math.ceil(totalRowsCount / Number(pageSize)));
|
||||||
|
paginate(Number(pageSize), index);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-[100px]">
|
||||||
|
<SelectValue placeholder="10" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{rowsCount.map((item, i) => (
|
||||||
|
<SelectItem key={i} value={item.toString()}>
|
||||||
|
{item}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||||
|
Page {index} of {maxIndex}
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => {
|
||||||
|
setPageIndex(1);
|
||||||
|
paginate(size, 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to first page</span>
|
||||||
|
<ChevronsLeft className="h-4 w-4" strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (index <= 1) {
|
||||||
|
setPageIndex(1);
|
||||||
|
paginate(size, 1);
|
||||||
|
} else {
|
||||||
|
{
|
||||||
|
setPageIndex(index - 1);
|
||||||
|
paginate(size, index - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to previous page</span>
|
||||||
|
<ChevronLeft className="h-4 w-4" strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
if (index >= maxIndex) {
|
||||||
|
setPageIndex(maxIndex);
|
||||||
|
paginate(size, maxIndex);
|
||||||
|
} else {
|
||||||
|
setPageIndex(index + 1);
|
||||||
|
paginate(size, index + 1);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
variant="outline"
|
||||||
|
className="h-8 w-8 p-0"
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to next page</span>
|
||||||
|
<ChevronRight className="h-4 w-4" strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
className="hidden h-8 w-8 p-0 lg:flex"
|
||||||
|
onClick={() => {
|
||||||
|
setPageIndex(maxIndex);
|
||||||
|
paginate(size, maxIndex);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="sr-only">Go to last page</span>
|
||||||
|
<ChevronsRight className="h-4 w-4" strokeWidth={1.5} />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
120
src/frontend/src/components/ui/select.tsx
Normal file
120
src/frontend/src/components/ui/select.tsx
Normal file
|
|
@ -0,0 +1,120 @@
|
||||||
|
"use client"
|
||||||
|
|
||||||
|
import * as React from "react"
|
||||||
|
import * as SelectPrimitive from "@radix-ui/react-select"
|
||||||
|
import { Check, ChevronDown } from "lucide-react"
|
||||||
|
import { cn } from "../../utils/utils"
|
||||||
|
|
||||||
|
const Select = SelectPrimitive.Root
|
||||||
|
|
||||||
|
const SelectGroup = SelectPrimitive.Group
|
||||||
|
|
||||||
|
const SelectValue = SelectPrimitive.Value
|
||||||
|
|
||||||
|
const SelectTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
<SelectPrimitive.Icon asChild>
|
||||||
|
<ChevronDown className="h-4 w-4 opacity-50" />
|
||||||
|
</SelectPrimitive.Icon>
|
||||||
|
</SelectPrimitive.Trigger>
|
||||||
|
))
|
||||||
|
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const SelectContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
|
||||||
|
>(({ className, children, position = "popper", ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Portal>
|
||||||
|
<SelectPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
|
position === "popper" &&
|
||||||
|
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
position={position}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<SelectPrimitive.Viewport
|
||||||
|
className={cn(
|
||||||
|
"p-1",
|
||||||
|
position === "popper" &&
|
||||||
|
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</SelectPrimitive.Viewport>
|
||||||
|
</SelectPrimitive.Content>
|
||||||
|
</SelectPrimitive.Portal>
|
||||||
|
))
|
||||||
|
SelectContent.displayName = SelectPrimitive.Content.displayName
|
||||||
|
|
||||||
|
const SelectLabel = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Label>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Label
|
||||||
|
ref={ref}
|
||||||
|
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectLabel.displayName = SelectPrimitive.Label.displayName
|
||||||
|
|
||||||
|
const SelectItem = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
|
||||||
|
>(({ className, children, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Item
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
|
<SelectPrimitive.ItemIndicator>
|
||||||
|
<Check className="h-4 w-4" />
|
||||||
|
</SelectPrimitive.ItemIndicator>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
|
||||||
|
</SelectPrimitive.Item>
|
||||||
|
))
|
||||||
|
SelectItem.displayName = SelectPrimitive.Item.displayName
|
||||||
|
|
||||||
|
const SelectSeparator = React.forwardRef<
|
||||||
|
React.ElementRef<typeof SelectPrimitive.Separator>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<SelectPrimitive.Separator
|
||||||
|
ref={ref}
|
||||||
|
className={cn("-mx-1 my-1 h-px bg-muted", className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
|
||||||
|
|
||||||
|
export {
|
||||||
|
Select,
|
||||||
|
SelectGroup,
|
||||||
|
SelectValue,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectContent,
|
||||||
|
SelectLabel,
|
||||||
|
SelectItem,
|
||||||
|
SelectSeparator,
|
||||||
|
}
|
||||||
|
|
@ -16,32 +16,32 @@ function ApiInterceptor() {
|
||||||
const interceptor = api.interceptors.response.use(
|
const interceptor = api.interceptors.response.use(
|
||||||
(response) => response,
|
(response) => response,
|
||||||
async (error: AxiosError) => {
|
async (error: AxiosError) => {
|
||||||
if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
// if (URL_EXCLUDED_FROM_ERROR_RETRIES.includes(error.config?.url)) {
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
let retryCount = 0;
|
// let retryCount = 0;
|
||||||
|
|
||||||
while (retryCount < 4) {
|
// while (retryCount < 4) {
|
||||||
await sleep(5000); // Sleep for 5 seconds
|
// await sleep(5000); // Sleep for 5 seconds
|
||||||
retryCount++;
|
// retryCount++;
|
||||||
try {
|
// try {
|
||||||
const response = await axios.request(error.config);
|
// const response = await axios.request(error.config);
|
||||||
return response;
|
// return response;
|
||||||
} catch (error) {
|
// } catch (error) {
|
||||||
if (retryCount === 3) {
|
// if (retryCount === 3) {
|
||||||
setErrorData({
|
// setErrorData({
|
||||||
title: "There was an error on web connection, please: ",
|
// title: "There was an error on web connection, please: ",
|
||||||
list: [
|
// list: [
|
||||||
"Refresh the page",
|
// "Refresh the page",
|
||||||
"Use a new flow tab",
|
// "Use a new flow tab",
|
||||||
"Check if the backend is up",
|
// "Check if the backend is up",
|
||||||
"Endpoint: " + error.config?.url,
|
// "Endpoint: " + error.config?.url,
|
||||||
],
|
// ],
|
||||||
});
|
// });
|
||||||
return Promise.reject(error);
|
// return Promise.reject(error);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
||||||
32
src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Normal file
32
src/frontend/src/pages/AdminPage/LoginPage/index.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { FaApple, FaGithub } from "react-icons/fa";
|
||||||
|
import { Button } from "../../../components/ui/button";
|
||||||
|
import { Input } from "../../../components/ui/input";
|
||||||
|
import { GoogleIcon } from "../../../icons/Google";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function LoginAdminPage() {
|
||||||
|
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function loginAdmin() {
|
||||||
|
navigate("/admin/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex h-full w-full flex-col items-center justify-center bg-muted">
|
||||||
|
<div className="flex w-72 flex-col items-center justify-center gap-2">
|
||||||
|
<span className="mb-4 text-5xl">⛓️</span>
|
||||||
|
<span className="mb-6 text-2xl font-semibold text-primary">
|
||||||
|
Admin
|
||||||
|
</span>
|
||||||
|
<Input className="bg-background" placeholder="Email address" />
|
||||||
|
<Input className="bg-background" placeholder="Password" />
|
||||||
|
<Button
|
||||||
|
onClick={() => {loginAdmin()}}
|
||||||
|
variant="default" className="w-full">
|
||||||
|
Login
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
356
src/frontend/src/pages/AdminPage/index.tsx
Normal file
356
src/frontend/src/pages/AdminPage/index.tsx
Normal file
|
|
@ -0,0 +1,356 @@
|
||||||
|
import _ from "lodash";
|
||||||
|
import { Trash2, X } from "lucide-react";
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import PaginatorComponent from "../../components/PaginatorComponent";
|
||||||
|
import { Button } from "../../components/ui/button";
|
||||||
|
import { Input } from "../../components/ui/input";
|
||||||
|
import {
|
||||||
|
Table,
|
||||||
|
TableBody,
|
||||||
|
TableCell,
|
||||||
|
TableHead,
|
||||||
|
TableHeader,
|
||||||
|
TableRow,
|
||||||
|
} from "../../components/ui/table";
|
||||||
|
|
||||||
|
export default function AdminPage() {
|
||||||
|
const [inputValue, setInputValue] = useState("");
|
||||||
|
|
||||||
|
const [size, setPageSize] = useState(10);
|
||||||
|
const [index, setPageIndex] = useState(1);
|
||||||
|
|
||||||
|
const userList = useRef([
|
||||||
|
{
|
||||||
|
user: generateRandomString(50),
|
||||||
|
email: generateRandomString(50) + "@example.com",
|
||||||
|
password: generateRandomString(50),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
user: generateRandomString(8),
|
||||||
|
email: generateRandomString(10) + "@example.com",
|
||||||
|
password: generateRandomString(12),
|
||||||
|
register_date: generateRandomDate(),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
const [filterUserList, setFilterUserList] = useState(userList.current);
|
||||||
|
|
||||||
|
function generateRandomString(length) {
|
||||||
|
const characters =
|
||||||
|
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < length; i++) {
|
||||||
|
const randomIndex = Math.floor(Math.random() * characters.length);
|
||||||
|
result += characters.charAt(randomIndex);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateRandomDate() {
|
||||||
|
const start = new Date(2010, 0, 1);
|
||||||
|
const end = new Date();
|
||||||
|
const randomTimestamp =
|
||||||
|
start.getTime() + Math.random() * (end.getTime() - start.getTime());
|
||||||
|
const randomDate = new Date(randomTimestamp);
|
||||||
|
|
||||||
|
const options = { year: "numeric", month: "short", day: "numeric" };
|
||||||
|
return randomDate.toLocaleDateString("en-US");
|
||||||
|
}
|
||||||
|
|
||||||
|
const [editUser, setEditUser] = useState(-1);
|
||||||
|
const [editedUser, setEditedUser] = useState("");
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
resetFilter();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleEditClick = (index, userEdit) => {
|
||||||
|
setEditUser(index);
|
||||||
|
setEditedUser(userEdit);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputChange = (event, index) => {
|
||||||
|
const user = _.cloneDeepWith(userList.current);
|
||||||
|
user[index].password = event.target.value;
|
||||||
|
userList.current = user;
|
||||||
|
|
||||||
|
const userFilter = _.cloneDeepWith(filterUserList);
|
||||||
|
userFilter[index].password = event.target.value;
|
||||||
|
setFilterUserList(userFilter);
|
||||||
|
|
||||||
|
setEditedUser(event.target.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSaveClick = (index) => {
|
||||||
|
setEditUser(-1);
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleChangePagination(pageIndex: number, pageSize: number) {
|
||||||
|
setPageIndex(pageIndex);
|
||||||
|
setPageSize(pageSize);
|
||||||
|
|
||||||
|
const startIndex = (pageIndex - 1) * pageSize;
|
||||||
|
const endIndex = startIndex + pageSize;
|
||||||
|
const newList = userList.current.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
setFilterUserList(newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetFilter() {
|
||||||
|
setFilterUserList(userList.current);
|
||||||
|
setPageIndex(1);
|
||||||
|
setPageSize(10);
|
||||||
|
|
||||||
|
const startIndex = (index - 1) * size;
|
||||||
|
const endIndex = index + size - 1;
|
||||||
|
const newList = userList.current.slice(startIndex, endIndex);
|
||||||
|
|
||||||
|
console.log(userList.current);
|
||||||
|
|
||||||
|
setFilterUserList(newList);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleFilterUsers(input: string) {
|
||||||
|
setInputValue(input);
|
||||||
|
|
||||||
|
if (input === "") {
|
||||||
|
resetFilter();
|
||||||
|
} else {
|
||||||
|
const filteredList = userList.current.filter(
|
||||||
|
(user) =>
|
||||||
|
user.user.toLowerCase().includes(input.toLowerCase()) ||
|
||||||
|
user.email.toLowerCase().includes(input.toLowerCase())
|
||||||
|
);
|
||||||
|
setFilterUserList(filteredList);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteUser(index) {
|
||||||
|
const user = _.cloneDeepWith(userList.current);
|
||||||
|
user.splice(index, 1);
|
||||||
|
userList.current = user;
|
||||||
|
|
||||||
|
const userFilter = _.cloneDeepWith(filterUserList);
|
||||||
|
userFilter.splice(index, 1);
|
||||||
|
setFilterUserList(userFilter);
|
||||||
|
|
||||||
|
resetFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="grid grid-cols-6 gap-4">
|
||||||
|
<div className="col-span-4 col-start-2">
|
||||||
|
<div className="m-auto h-full flex-1 flex-col space-y-8 p-8 md:flex">
|
||||||
|
<div className="flex items-center justify-between space-y-2">
|
||||||
|
<div>
|
||||||
|
<h2 className="text-2xl font-bold tracking-tight">
|
||||||
|
Welcome back!
|
||||||
|
</h2>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Here's a list of all users!
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center space-x-2"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{userList.current.length === 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<h2>There's no users left :)</h2>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{userList.current.length > 0 && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex flex-1 items-center space-x-2">
|
||||||
|
<Input
|
||||||
|
value={inputValue}
|
||||||
|
placeholder="Filter users..."
|
||||||
|
className="h-8 w-[150px] lg:w-[250px]"
|
||||||
|
onChange={(e) => handleFilterUsers(e.target.value)}
|
||||||
|
/>
|
||||||
|
{inputValue.length > 0 && (
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
resetFilter();
|
||||||
|
}}
|
||||||
|
variant="ghost"
|
||||||
|
className="h-8 px-2 lg:px-3"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
|
<X className="ml-2 h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className="overflow-scroll overflow-x-hidden rounded-md border-2 bg-muted
|
||||||
|
custom-scroll min-[320px]:h-[20rem] md:h-[25rem] xl:h-[25rem] 2xl:h-[30rem]"
|
||||||
|
>
|
||||||
|
<Table className="table-fixed bg-muted outline-1 ">
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead className="h-10">User</TableHead>
|
||||||
|
<TableHead className="h-10">E-mail</TableHead>
|
||||||
|
<TableHead className="h-10">Password</TableHead>
|
||||||
|
<TableHead className="h-10">Register Date</TableHead>
|
||||||
|
<TableHead className="h-10 w-[100px] text-right"></TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{filterUserList.map((user, index) => (
|
||||||
|
<TableRow key={user.user}>
|
||||||
|
<TableCell className="truncate py-2 font-medium">
|
||||||
|
{user.user}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate py-2">
|
||||||
|
{user.email}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="truncate py-2">
|
||||||
|
{editUser === index ? (
|
||||||
|
<Input
|
||||||
|
className="h-6 truncate"
|
||||||
|
onBlur={() => {
|
||||||
|
setEditUser(-1);
|
||||||
|
}}
|
||||||
|
value={editedUser}
|
||||||
|
onChange={(e) => handleInputChange(e, index)}
|
||||||
|
autoFocus
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="h-6 truncate"
|
||||||
|
onClick={() =>
|
||||||
|
handleEditClick(index, user.password)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{user.password}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="py-2">
|
||||||
|
{user.register_date.toString()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="flex w-[100px] py-2 text-right">
|
||||||
|
<Trash2
|
||||||
|
className="h-4 w-4 cursor-pointer"
|
||||||
|
strokeWidth={1.5}
|
||||||
|
onClick={() => {
|
||||||
|
handleDeleteUser(index);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
<PaginatorComponent
|
||||||
|
pageIndex={index}
|
||||||
|
pageSize={size}
|
||||||
|
totalRowsCount={filterUserList.length}
|
||||||
|
paginate={(pageIndex, pageSize) => {
|
||||||
|
handleChangePagination(pageSize, pageIndex);
|
||||||
|
}}
|
||||||
|
></PaginatorComponent>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -3,6 +3,8 @@ import CommunityPage from "./pages/CommunityPage";
|
||||||
import FlowPage from "./pages/FlowPage";
|
import FlowPage from "./pages/FlowPage";
|
||||||
import HomePage from "./pages/MainPage";
|
import HomePage from "./pages/MainPage";
|
||||||
import LoginPage from "./pages/loginPage";
|
import LoginPage from "./pages/loginPage";
|
||||||
|
import LoginAdminPage from "./pages/AdminPage/LoginPage";
|
||||||
|
import AdminPage from "./pages/AdminPage";
|
||||||
|
|
||||||
const Router = () => {
|
const Router = () => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -14,6 +16,8 @@ const Router = () => {
|
||||||
</Route>
|
</Route>
|
||||||
<Route path="*" element={<HomePage />} />
|
<Route path="*" element={<HomePage />} />
|
||||||
<Route path="/login" element={<LoginPage />} />
|
<Route path="/login" element={<LoginPage />} />
|
||||||
|
<Route path="/login/admin" element={<LoginAdminPage />} />
|
||||||
|
<Route path="/admin" element={<AdminPage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -171,3 +171,11 @@ export type IconComponentProps = {
|
||||||
export interface languageMap {
|
export interface languageMap {
|
||||||
[key: string]: string | undefined;
|
[key: string]: string | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type PaginatorComponentType = {
|
||||||
|
pageSize: number;
|
||||||
|
pageIndex: number;
|
||||||
|
rowsCount?: number[];
|
||||||
|
totalRowsCount: number;
|
||||||
|
paginate: (pageIndex: number, pageSize: number) => void;
|
||||||
|
};
|
||||||
Loading…
Add table
Add a link
Reference in a new issue