Fixed ui design on admin pages

This commit is contained in:
ZacTohZY
2024-08-12 04:21:54 +08:00
parent 36ca8c08de
commit 22e778e907
9 changed files with 387 additions and 292 deletions

View File

@@ -0,0 +1,31 @@
import { Select, SelectItem } from "@nextui-org/react";
import { ChangeEvent } from "react";
interface SimpleSelectProps {
label: string;
placeholder: string;
options: Array<{ key: string; label: string }>;
onChange: (value: string) => void;
}
const NextUIFormikSelect2 = ({ label, placeholder, options, onChange }: SimpleSelectProps) => {
const handleChange = (e: ChangeEvent<HTMLSelectElement>) => {
onChange(e.target.value);
};
return (
<Select
aria-label={label}
placeholder={placeholder}
onChange={handleChange}
>
{options.map((option) => (
<SelectItem key={option.key} value={option.key}>
{option.label}
</SelectItem>
))}
</Select>
);
};
export default NextUIFormikSelect2;

View File

@@ -660,7 +660,7 @@ export const VoucherIcon = () => {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
fill="none" fill="none"
viewBox="0 0 24 24" viewBox="0 0 24 24"
stroke-width="1.5" strokeWidth="1.5"
stroke="currentColor" stroke="currentColor"
className="size-6" className="size-6"
> >
@@ -710,3 +710,23 @@ export const BookOpenIcon = () => {
</svg> </svg>
); );
}; };
export const ImageIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
strokeWidth="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m2.25 15.75 5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5 1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0 .375.375 0 0 1 .75 0Z"
/>
</svg>
);
};

View File

@@ -7,6 +7,7 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import instance from "../security/http"; import instance from "../security/http";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ArrowUTurnLeftIcon } from "../icons";
// Validation schema // Validation schema
const validationSchema = yup.object().shape({ const validationSchema = yup.object().shape({
@@ -73,15 +74,17 @@ export default function CreateSchedulePage() {
}; };
return ( return (
<section className="flex flex-col items-center justify-center gap-20 py-8 md:py-10"> <div className="w-full h-full pb-12 pt-10">
<div className="w-full flex items-start"> <div className="w-[400px] mx-auto p-6 bg-red-50 dark:bg-primary-950 border border-primary-100 rounded-2xl">
<div>
<Button <Button
isIconOnly
variant="light" variant="light"
onPress={() => navigate(-1)} onPress={() => navigate(-1)}
> >
Back <ArrowUTurnLeftIcon />
</Button> </Button>
<div className="flex-grow text-center"> <div className="pt-2">
<p className="text-3xl font-bold">Add New Schedule</p> <p className="text-3xl font-bold">Add New Schedule</p>
</div> </div>
</div> </div>
@@ -91,12 +94,12 @@ export default function CreateSchedulePage() {
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{({ isValid, dirty }) => ( {({ isValid, dirty }) => (
<Form className="flex flex-col gap-4"> <Form className="flex flex-col gap-4 pt-5 items-center justify-center">
<div className="flex gap-8"> <div className="flex flex-col gap-5 w-[360px]">
<NextUIFormikDatePicker <NextUIFormikDatePicker
label="Date" label="Date"
name="date" name="date"
className="max-w-[280px]" className="max-w-[360px]"
/> />
<NextUIFormikInput <NextUIFormikInput
type='time' type='time'
@@ -104,8 +107,6 @@ export default function CreateSchedulePage() {
name="time" name="time"
placeholder="" placeholder=""
/> />
</div>
<div className="flex gap-8">
<NextUIFormikInput <NextUIFormikInput
type="text" type="text"
label="Location" label="Location"
@@ -128,13 +129,18 @@ export default function CreateSchedulePage() {
<Radio value="On going">On going</Radio> <Radio value="On going">On going</Radio>
<Radio value="Ended">Ended</Radio> <Radio value="Ended">Ended</Radio>
</NextUIFormikRadioGroup> </NextUIFormikRadioGroup>
<Button type="submit" color="primary" className="w-[100px]">Create</Button> <Button
{/* Example of using isValid and dirty */} type="submit"
<p>Form is {isValid ? 'valid' : 'invalid'}</p> color="primary"
<p>Form has been {dirty ? 'touched' : 'not touched'}</p> className="w-[100px]"
isDisabled={!isValid || !dirty}
>
Create
</Button>
</Form> </Form>
)} )}
</Formik> </Formik>
</section > </div >
</div>
) )
} }

View File

@@ -6,6 +6,7 @@ import InsertPostImage from "../components/InsertPostImage";
import NextUIFormikInput from "../components/NextUIFormikInput"; import NextUIFormikInput from "../components/NextUIFormikInput";
import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker"; import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker";
import instance from "../security/http"; import instance from "../security/http";
import { ArrowUTurnLeftIcon } from "../icons";
// Validation schema // Validation schema
const validationSchema = Yup.object({ const validationSchema = Yup.object({
@@ -75,11 +76,11 @@ export default function CreateVoucherPage() {
}; };
return ( return (
<div className="w-full h-full pb-12"> <div className="w-full h-full pb-12 pt-10">
<div className="w-[680px] mx-auto p-6 bg-gray-100 dark:bg-gray-950 border border-primary-100 rounded-2xl"> <div className="w-[350px] mx-auto p-6 bg-red-50 dark:bg-primary-950 border border-primary-100 rounded-2xl ">
<div className="py-2"> <div className="py-2">
<Button variant="light" onPress={() => navigate(-1)}> <Button variant="light" isIconOnly onPress={() => navigate(-1)}>
Back <ArrowUTurnLeftIcon />
</Button> </Button>
</div> </div>
<div className="flex-grow overflow-y-auto"> <div className="flex-grow overflow-y-auto">
@@ -90,7 +91,7 @@ export default function CreateVoucherPage() {
> >
{({ isValid, dirty, setFieldValue }) => ( {({ isValid, dirty, setFieldValue }) => (
<Form className="flex flex-col gap-4"> <Form className="flex flex-col gap-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-5 w-[280px]">
<InsertPostImage <InsertPostImage
onImageSelected={(file) => { onImageSelected={(file) => {
setFieldValue("brandLogo", file); setFieldValue("brandLogo", file);
@@ -130,7 +131,7 @@ export default function CreateVoucherPage() {
labelPlacement="inside" labelPlacement="inside"
/> />
</div> </div>
<Button type="submit" color="primary" className="w-[100px]" <Button type="submit" color="primary" className="w-[90px]"
isDisabled={!isValid || !dirty}> isDisabled={!isValid || !dirty}>
Create Create
</Button> </Button>

View File

@@ -8,6 +8,7 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
import { useNavigate, useParams } from "react-router-dom"; import { useNavigate, useParams } from "react-router-dom";
import instance from "../security/http"; import instance from "../security/http";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { ArrowUTurnLeftIcon } from "../icons";
// Validation schema // Validation schema
@@ -92,18 +93,20 @@ export default function EditSchedulePage() {
}; };
return ( return (
<section className="flex flex-col items-center justify-center gap-20 py-8 md:py-10"> <div className="w-full h-full pb-12 pt-10">
<div className="w-full flex items-start"> <div className="w-[400px] mx-auto p-6 bg-red-50 dark:bg-primary-950 border border-primary-100 rounded-2xl">
<div>
<Button <Button
isIconOnly
variant="light" variant="light"
onPress={() => navigate(-1)} onPress={() => navigate(-1)}
> >
Back <ArrowUTurnLeftIcon />
</Button> </Button>
<div className="flex-grow text-center">
<p className="text-3xl font-bold">Add New Schedule</p> <p className="text-3xl font-bold pt-2">Update Schedule</p>
</div>
</div> </div>
<Formik <Formik
enableReinitialize enableReinitialize
initialValues={schedule} initialValues={schedule}
@@ -111,12 +114,12 @@ export default function EditSchedulePage() {
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{({ isValid, dirty }) => ( {({ isValid, dirty }) => (
<Form className="flex flex-col gap-4"> <Form className="flex flex-col gap-4 pt-5 items-center justify-center">
<div className="flex gap-8"> <div className="flex flex-col gap-5 w-[360px]">
<NextUIFormikDatePicker <NextUIFormikDatePicker
label="Date" label="Date"
name="date" name="date"
className="max-w-[284px]" className="max-w-[360px]"
/> />
<NextUIFormikInput <NextUIFormikInput
type='time' type='time'
@@ -124,8 +127,6 @@ export default function EditSchedulePage() {
name="time" name="time"
placeholder="" placeholder=""
/> />
</div>
<div className="flex gap-8">
<NextUIFormikInput <NextUIFormikInput
type="text" type="text"
label="Location" label="Location"
@@ -150,15 +151,18 @@ export default function EditSchedulePage() {
<Radio value="Ended">Ended</Radio> <Radio value="Ended">Ended</Radio>
</NextUIFormikRadioGroup> </NextUIFormikRadioGroup>
</div> </div>
<div className="flex gap-5"> <Button
<Button type="submit" color="secondary" className="max-w-[100px]">Update</Button> type="submit"
</div> color="primary"
{/* Example of using isValid and dirty */} className="w-[100px]"
<p>Form is {isValid ? 'valid' : 'invalid'}</p> isDisabled={!isValid || !dirty}
<p>Form has been {dirty ? 'touched' : 'not touched'}</p> >
Update
</Button>
</Form> </Form>
)} )}
</Formik> </Formik>
</section> </div>
</div>
) )
} }

View File

@@ -6,14 +6,22 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker"; import { NextUIFormikDatePicker } from "../components/NextUIFormikDatePicker";
import instance from "../security/http"; import instance from "../security/http";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { ArrowUTurnLeftIcon } from "../icons";
// Validation schema // Validation schema
const validationSchema = Yup.object({ const validationSchema = Yup.object({
brand: Yup.string().trim().required("Brand name is required"), brand: Yup.string().trim()
description: Yup.string().trim().required("Description is required"), .required("Brand name is required"),
expirationDate: Yup.date().required("Expiry date is required"), description: Yup.string().trim()
quantityAvailable: Yup.number().typeError("Must be a number").positive("Must be a positive value").required("Quantity is required"), .required("Description is required"),
code: Yup.string().trim().required("Code is required"), expirationDate: Yup.date()
.required("Expiry date is required"),
quantityAvailable: Yup.number()
.typeError("Must be a number")
.positive("Must be a positive value")
.required("Quantity is required"),
code: Yup.string().trim()
.required("Code is required"),
}); });
export default function EditVoucherPage() { export default function EditVoucherPage() {
@@ -74,11 +82,11 @@ export default function EditVoucherPage() {
}; };
return ( return (
<div className="w-full h-full pb-12"> <div className="w-full h-full pb-12 pt-20">
<div className="w-[680px] mx-auto p-6 bg-gray-100 dark:bg-gray-950 border border-primary-100 rounded-2xl"> <div className="w-[350px] mx-auto p-6 bg-red-50 dark:bg-primary-950 border border-primary-100 rounded-2xl">
<div className="py-2"> <div className="py-2">
<Button variant="light" onPress={() => navigate(-1)}> <Button variant="light" isIconOnly onPress={() => navigate(-1)}>
Back <ArrowUTurnLeftIcon />
</Button> </Button>
</div> </div>
<div className="flex-grow overflow-y-auto"> <div className="flex-grow overflow-y-auto">
@@ -90,7 +98,7 @@ export default function EditVoucherPage() {
> >
{({ isValid }) => ( {({ isValid }) => (
<Form className="flex flex-col gap-4"> <Form className="flex flex-col gap-4">
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4 w-[280px]">
<NextUIFormikInput <NextUIFormikInput
label="Brand name" label="Brand name"
name="brand" name="brand"

View File

@@ -97,9 +97,9 @@ export default function ManageSchedulePage() {
const sortedScheduleList = sortScheduleList(scheduleList, sortDescriptor); const sortedScheduleList = sortScheduleList(scheduleList, sortDescriptor);
return ( return (
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10"> <div className="flex flex-col gap-8 p-8">
<div className="inline-block text-center justify-center flex flex-row gap-10"> <div className="inline-block text-center justify-between flex flex-row gap-10">
<p className="text-3xl font-bold">Admin Karang Guni Schedule</p> <p className="text-4xl font-bold">Karang Guni Schedule</p>
<Button <Button
isIconOnly isIconOnly
color="primary" color="primary"
@@ -108,7 +108,7 @@ export default function ManageSchedulePage() {
<PlusIcon /> <PlusIcon />
</Button> </Button>
</div> </div>
<div className="w-full overflow-auto max-w-screen-lg"> <div>
<Table aria-label="Schedule Table"> <Table aria-label="Schedule Table">
<TableHeader> <TableHeader>
<TableColumn> <TableColumn>
@@ -170,7 +170,7 @@ export default function ManageSchedulePage() {
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
</section> </div>
) )
} }

View File

@@ -88,14 +88,15 @@ export default function ManageVoucherPage() {
}; };
return ( return (
<div className="w-full h-full"> <div className="flex flex-col gap-8 p-8">
<section className="flex flex-col items-center justify-center gap-4 py-8 md:py-10"> <div className="flex flex-row justify-between gap-10">
<div className="flex flex-row gap-10"> <p className="text-4xl font-bold">Manage Vouchers</p>
<p className="text-2xl font-bold">Manage Vouchers</p> <div className="flex justify-end">
<div>
<Button <Button
isIconOnly isIconOnly
onPress={() => navigate("create-voucher")} onPress={() => navigate("create-voucher")}
color="primary"
variant="solid"
> >
<PlusIcon /> <PlusIcon />
</Button> </Button>
@@ -120,7 +121,7 @@ export default function ManageVoucherPage() {
<img <img
src={brandLogoUrls[voucher.id]} src={brandLogoUrls[voucher.id]}
alt={voucher.brand} alt={voucher.brand}
style={{ width: "50px", height: "50px" }} style={{ width: "40px", height: "40px" }}
/> />
)} )}
</TableCell> </TableCell>
@@ -187,7 +188,6 @@ export default function ManageVoucherPage() {
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
</section>
</div> </div>
); );
} }

View File

@@ -2,7 +2,8 @@ import { useEffect, useState } from 'react';
import config from '../config'; import config from '../config';
import instance from '../security/http'; import instance from '../security/http';
import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor, Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react'; import { Table, TableHeader, TableColumn, TableBody, TableRow, TableCell, SortDescriptor, Button, Modal, ModalContent, ModalHeader, ModalBody, ModalFooter } from '@nextui-org/react';
import { EmailIcon, TrashDeleteIcon } from '../icons'; import { EmailIcon, ImageIcon, TrashDeleteIcon } from '../icons';
import NextUIFormikSelect2 from '../components/NextUIFormikSelect2';
interface User { interface User {
id: string; id: string;
@@ -125,31 +126,15 @@ export default function Ranking() {
// Sort form data based on descriptor // Sort form data based on descriptor
const sortFormData = (list: FormData[], descriptor: SortDescriptor) => { const sortFormData = (list: FormData[], descriptor: SortDescriptor) => {
const { column, direction } = descriptor; const { column } = descriptor;
if (column === "avgBill") { if (column === "avgBill") {
return [...list].sort((a, b) => return [...list].sort((a, b) => a.avgBill - b.avgBill);
direction === "ascending" ? a.avgBill - b.avgBill : b.avgBill - a.avgBill
);
} }
return list; return list;
}; };
const handleSort = () => {
const { direction } = sortDescriptor;
const newDirection = direction === "ascending" ? "descending" : "ascending";
setSortDescriptor({ column: "avgBill", direction: newDirection });
};
const renderSortIndicator = () => {
if (sortDescriptor.column === "avgBill") {
return sortDescriptor.direction === "ascending" ? <span>&uarr;</span> : <span>&darr;</span>;
}
return null;
};
const sortedFormData = sortFormData(filteredFormData, sortDescriptor); const sortedFormData = sortFormData(filteredFormData, sortDescriptor);
const combinedData: FormDataWithUser[] = sortedFormData.map((data) => { const combinedData: FormDataWithUser[] = sortedFormData.map((data) => {
@@ -162,6 +147,14 @@ export default function Ranking() {
}; };
}); });
const [isImageModalOpen, setIsImageModalOpen] = useState(false);
const [modalImageUrl, setModalImageUrl] = useState<string | null>(null);
const handleImageClick = (imageUrl: string) => {
setModalImageUrl(imageUrl);
setIsImageModalOpen(true);
};
const handleEmailClick = (email: string, name: string) => { const handleEmailClick = (email: string, name: string) => {
setSelectedUser({ email, name }); setSelectedUser({ email, name });
setIsEmailModalOpen(true); setIsEmailModalOpen(true);
@@ -241,58 +234,76 @@ export default function Ranking() {
} }
}; };
const handleTownCouncilChange = (value: string) => {
setSelectedTownCouncil(value);
};
return ( return (
<section className="flex flex-col items-center justify-center py-5"> <div className="flex flex-col gap-8 p-8">
<div className="flex justify-between w-full"> <div className="flex justify-between items-center gap-5">
<p className="text-xl font-bold">Form Data</p> <div className="flex w-[500px]">
<p className="text-4xl font-bold">Home Bill Contest Form Data</p>
</div>
<div className="flex flex-row gap-4 ">
<div className="w-[200px]">
{townCouncils.length > 0 && (
<NextUIFormikSelect2
label="Town council"
placeholder="Choose towncouncil"
options={townCouncils.map((townCouncil) => ({
key: townCouncil,
label: townCouncil,
}))}
onChange={handleTownCouncilChange}
/>
)}
</div>
<div className="w-[130px]">
{top3Users.length > 0 && ( {top3Users.length > 0 && (
<Button color="primary" onPress={handleGiveVouchers}> <Button color="primary" onPress={handleGiveVouchers} className="w-full">
Give Vouchers Give Vouchers
</Button> </Button>
)} )}
</div> </div>
<div className="gap-8 p-8"> </div>
{townCouncils.length > 0 && ( </div>
<select
value={selectedTownCouncil} <div>
onChange={(e) => setSelectedTownCouncil(e.target.value)}
>
<option value="">All locations</option>
{townCouncils.map((townCouncil) => (
<option key={townCouncil} value={townCouncil}>
{townCouncil}
</option>
))}
</select>
)}
<Table aria-label="Form Data Table"> <Table aria-label="Form Data Table">
<TableHeader> <TableHeader>
<TableColumn>User Name</TableColumn> <TableColumn>User Name</TableColumn>
<TableColumn>User Email</TableColumn>
<TableColumn>Electrical Bill</TableColumn> <TableColumn>Electrical Bill</TableColumn>
<TableColumn>Water Bill</TableColumn> <TableColumn>Water Bill</TableColumn>
<TableColumn>Total Bill</TableColumn> <TableColumn>Total Bill</TableColumn>
<TableColumn>Dependents</TableColumn> <TableColumn>Dependents</TableColumn>
<TableColumn onClick={handleSort}> <TableColumn>
Average Bill {renderSortIndicator()} Average Bill
</TableColumn> </TableColumn>
<TableColumn>Bill Picture</TableColumn> <TableColumn>Bill Picture</TableColumn>
<TableColumn>Actions</TableColumn> <TableColumn>Actions</TableColumn>
</TableHeader> </TableHeader>
<TableBody> <TableBody>
{combinedData.map((data) => ( {combinedData.map((data) => (
<TableRow key={data.id}> <TableRow key={data.id}>
<TableCell>{data.userName}</TableCell> <TableCell>{data.userName}</TableCell>
<TableCell>{data.userEmail}</TableCell>
<TableCell>${data.electricalBill.toFixed(2)}</TableCell> <TableCell>${data.electricalBill.toFixed(2)}</TableCell>
<TableCell>${data.waterBill.toFixed(2)}</TableCell> <TableCell>${data.waterBill.toFixed(2)}</TableCell>
<TableCell>${data.totalBill.toFixed(2)}</TableCell> <TableCell>${data.totalBill.toFixed(2)}</TableCell>
<TableCell>{data.noOfDependents}</TableCell> <TableCell>{data.noOfDependents}</TableCell>
<TableCell>${data.avgBill.toFixed(2)}</TableCell> <TableCell>${data.avgBill.toFixed(2)}</TableCell>
<TableCell> <TableCell>
{data.billPicture && <img src={`${config.serverAddress}/hbcform/billPicture/${data.id}`} alt="bill picture" className="w-full" />} {data.billPicture ? (
<Button isIconOnly variant="light" onPress={() => handleImageClick(`${config.serverAddress}/hbcform/billPicture/${data.id}`)}>
<ImageIcon />
</Button>
) : (
<Button isIconOnly variant="light">
<ImageIcon />
</Button>
)}
</TableCell> </TableCell>
<TableCell className="flex flex-row"> <TableCell className="flex flex-row">
<Button isIconOnly variant="light" className="text-blue-500" onClick={() => handleEmailClick(data.userEmail, data.userName)}><EmailIcon /></Button> <Button isIconOnly variant="light" className="text-blue-500" onClick={() => handleEmailClick(data.userEmail, data.userName)}><EmailIcon /></Button>
<Button isIconOnly variant="light" color="danger" onClick={() => handleDeleteClick(data.id)}><TrashDeleteIcon /></Button> <Button isIconOnly variant="light" color="danger" onClick={() => handleDeleteClick(data.id)}><TrashDeleteIcon /></Button>
@@ -344,6 +355,20 @@ export default function Ranking() {
)} )}
</ModalContent> </ModalContent>
</Modal> </Modal>
</section> {/* Open Image Modal */}
<Modal
isOpen={isImageModalOpen}
onOpenChange={setIsImageModalOpen}
isDismissable={true}
>
<ModalContent>
<ModalBody>
{modalImageUrl && (
<img src={modalImageUrl} alt="Bill Picture" style={{ width: '100%', height: 'auto' }} />
)}
</ModalBody>
</ModalContent>
</Modal>
</div >
); );
} }