Modified HBForm & HBContest Page

This commit is contained in:
ZacTohZY
2024-07-29 21:26:34 +08:00
parent 34a96a8445
commit a76ec634ca
6 changed files with 203 additions and 129 deletions

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
interface InsertImageProps { interface InsertImageProps {
onImageSelected: (file: File) => void; onImageSelected: (file: File | null) => void;
} }
const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => { const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
@@ -10,19 +10,28 @@ const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => { const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
const selectedFiles = event.target.files as FileList; const selectedFiles = event.target.files as FileList;
const file = selectedFiles?.[0]; const file = selectedFiles?.[0] || null;
if (file) { setSelectedFile(file);
setSelectedFile(file); setPreviewImage(file ? URL.createObjectURL(file) : '');
setPreviewImage(URL.createObjectURL(file)); onImageSelected(file);
onImageSelected(file);
}
}; };
return ( return (
<div> <div
<input type="file" onChange={handleImageSelect} /> className="flex flex-col items-center p-5 bg-white dark:bg-zinc-800 rounded-md"
style={{ width: 350, height: 500 }}
>
<input
type="file"
onChange={handleImageSelect}
className="mb-4"
/>
{selectedFile && ( {selectedFile && (
<img src={previewImage} alt="Selected Image" /> <img
src={previewImage}
alt="Selected Image"
className="w-full h-full object-cover rounded-md"
/>
)} )}
</div> </div>
); );

View File

@@ -10,16 +10,28 @@ interface NextUIFormikInputProps {
placeholder: string; placeholder: string;
labelPlacement?: "inside" | "outside"; labelPlacement?: "inside" | "outside";
startContent?: JSX.Element; startContent?: JSX.Element;
readOnly?: boolean;
setFieldValue?: (field: string, value: any, shouldValidate?: boolean) => void;
} }
const NextUIFormikInput = ({ const NextUIFormikInput = ({
label, label,
startContent, startContent,
readOnly = false,
setFieldValue,
...props ...props
}: NextUIFormikInputProps) => { }: NextUIFormikInputProps) => {
const [field, meta] = useField(props.name); const [field, meta] = useField(props.name);
const [inputType, setInputType] = useState(props.type); const [inputType, setInputType] = useState(props.type);
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const { value } = e.target;
field.onChange(e);
if (setFieldValue) {
setFieldValue(props.name, value);
}
};
return ( return (
<Input <Input
{...field} {...field}
@@ -43,6 +55,8 @@ const NextUIFormikInput = ({
</> </>
) : null ) : null
} }
readOnly={readOnly}
onChange={handleChange}
/> />
); );
}; };

View File

@@ -1,63 +1,64 @@
import { import { Button } from "@nextui-org/react";
Card,
CardHeader,
CardBody,
CardFooter,
Divider,
Button,
} from "@nextui-org/react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
export default function HBContestPage() { export default function HBContestPage() {
const navigate = useNavigate(); const navigate = useNavigate();
const handleJoinClick = () => {
let accessToken = localStorage.getItem("accessToken");
if (!accessToken) {
setTimeout(() => {
navigate("/signin");
}, 1000);
} else {
navigate("new-submission");
}
};
return ( return (
<div className="w-full h-full"> <div className="h-screen flex items-center justify-center">
<section> <section className="bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl shadow-md w-full max-w-2xl">
<Card className="max-w-[800px] bg-red-50 mx-auto"> <div className="space-y-4">
<CardHeader className="flex gap-3"> <div>
<div className="flex flex-col"> <p className="text-2xl font-bold text-red-900 dark:text-white">
<h2 className="text-md">Home Bill Contest</h2> Home Bill Contest
</div> </p>
</CardHeader> </div>
<Divider /> <div>
<CardBody> <p className="text-gray-700 dark:text-gray-300">
<p>
This contest is to encourage residents to reduce the use of This contest is to encourage residents to reduce the use of
electricity and water usage. This contest would be won by the electricity and water usage. This contest would be won by the
person with the lowest overall bill average. Join us in this person with the lowest overall bill average. Join us in this
important effort to create a more sustainable future for everyone. important effort to create a more sustainable future for everyone.{" "}
Participants would be required to input and upload their bills <span className="text-red-600">
into the form to ensure integrity and honesty.{" "} Participants would be required to input and upload their bills into the form to ensure integrity and honesty.{" "}
</span>
</p> </p>
</CardBody> </div>
<Divider /> <div>
<CardFooter> <p className="text-xl font-bold text-red-900 dark:text-white">
<div className="flex-col"> Winners
<div> </p>
<h4>Winners</h4> </div>
<p> <div>
There will 3 winners for each month. Each winner will receive <p className="text-gray-700 dark:text-gray-300">
random food vouchers. There will 3 winners for each month. Each winner will receive
</p> random food vouchers.
<p>1st: 3 vouchers</p> </p>
<p>2nd: 2 vouchers</p> <p className="text-gray-700 dark:text-gray-300">1st &rarr; 3 vouchers</p>
<p>3rd: 1 voucher</p> <p className="text-gray-700 dark:text-gray-300">2nd &rarr; 2 vouchers</p>
</div> <p className="text-gray-700 dark:text-gray-300">3rd &rarr; 1 voucher</p>
<div> </div>
<Button <div>
className=" bg-red-500 dark:bg-red-700 text-white" <Button
size="lg" className="bg-red-300 text-white hover:bg-red-600 focus:ring-red-400 dark:bg-red-600 dark:hover:bg-red-900 dark:focus:ring-red-700 w-100"
onPress={() => { size="lg"
navigate("new-submission"); onPress={handleJoinClick}
}} >
> <p>Join</p>
<p className="font-bold">Join</p> </Button>
</Button> </div>
</div> </div>
</div>
</CardFooter>
</Card>
</section> </section>
</div> </div>
); );

View File

@@ -1,3 +1,4 @@
import { useEffect, useState } from 'react';
import { Button } from "@nextui-org/react"; import { Button } from "@nextui-org/react";
import { ArrowUTurnLeftIcon } from "../icons"; import { ArrowUTurnLeftIcon } from "../icons";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
@@ -8,29 +9,25 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
import axios from "axios"; import axios from "axios";
import InsertImage from "../components/InsertImage"; import InsertImage from "../components/InsertImage";
import { retrieveUserInformation } from "../security/users"; import { retrieveUserInformation } from "../security/users";
import { useEffect, useState } from "react";
const validationSchema = Yup.object({ const validationSchema = Yup.object({
electricalBill: Yup.number() electricalBill: Yup.number()
.typeError("Must be a number") .typeError("Must be a number")
.positive("Must be a positive value") .positive("Must be a positive value")
.max(99999.99, "Value is too large") .max(99999.99, "Value is too large")
.required(), .required("Electrical bill is a required field"),
waterBill: Yup.number() waterBill: Yup.number()
.typeError("Must be a number") .typeError("Must be a number")
.positive("Must be a positive value") .positive("Must be a positive value")
.max(99999.99, "Value is too large") .max(99999.99, "Value is too large")
.required(), .required("Water bill is a required field"),
totalBill: Yup.number()
.typeError("Must be a number")
.positive("Must be a positive value")
.max(99999.99, "Value is too large")
.required(),
noOfDependents: Yup.number() noOfDependents: Yup.number()
.typeError("Must be a number") .typeError("Must be a number")
.integer("Must be a whole number") .integer("Must be a whole number")
.positive("Must be a positive value") .positive("Must be a positive value")
.required(), .required("No. of dependents is a required field"),
ebPicture: Yup.mixed().required("Electrical bill picture is required"),
wbPicture: Yup.mixed().required("Water bill picture is required"),
}); });
export default function HBFormPage() { export default function HBFormPage() {
@@ -41,11 +38,18 @@ export default function HBFormPage() {
waterBill: "", waterBill: "",
totalBill: "", totalBill: "",
noOfDependents: "", noOfDependents: "",
avgBill: "",
ebPicture: null, ebPicture: null,
wbPicture: null, wbPicture: null,
userId: "", userId: "",
}); });
// Add state for image selection
const [imagesSelected, setImagesSelected] = useState({
ebPicture: false,
wbPicture: false,
});
useEffect(() => { useEffect(() => {
const getUserInformation = async () => { const getUserInformation = async () => {
try { try {
@@ -78,6 +82,7 @@ export default function HBFormPage() {
formData.append("waterBill", values.waterBill); formData.append("waterBill", values.waterBill);
formData.append("totalBill", values.totalBill); formData.append("totalBill", values.totalBill);
formData.append("noOfDependents", values.noOfDependents); formData.append("noOfDependents", values.noOfDependents);
formData.append("avgBill", values.avgBill);
if (values.ebPicture) { if (values.ebPicture) {
formData.append("ebPicture", values.ebPicture); formData.append("ebPicture", values.ebPicture);
@@ -124,78 +129,117 @@ export default function HBFormPage() {
} }
}; };
// Handler for image selection
const handleImageSelection = (name: string, file: File | null) => {
setImagesSelected(prevState => ({
...prevState,
[name]: !!file,
}));
};
return ( return (
<div className="w-full h-full"> <div className="w-full h-full">
<section className="w-7/12 mx-auto"> <section className="w-8/12 mx-auto">
<Button variant="light" onPress={() => navigate(-1)}> <Button variant="light" onPress={() => navigate(-1)}>
<ArrowUTurnLeftIcon /> <ArrowUTurnLeftIcon />
</Button> </Button>
</section> </section>
<section className="w-7/12 mx-auto p-5 bg-red-100 border border-none rounded-2xl h-600px"> <section className="w-8/12 mx-auto p-5 bg-red-100 dark:bg-red-950 border border-primary-100 rounded-2xl h-600px">
<Formik <Formik
initialValues={initialValues} initialValues={initialValues}
validationSchema={validationSchema} validationSchema={validationSchema}
onSubmit={handleSubmit} onSubmit={handleSubmit}
> >
{({ isValid, dirty, isSubmitting, setFieldValue }) => ( {({ isValid, dirty, isSubmitting, setFieldValue, values }) => {
<Form> // Calculate the total bill
<div className="flex flex-col gap-5"> useEffect(() => {
<div className="flex flex-row gap-10"> const totalBill = Number(values.electricalBill) + Number(values.waterBill);
<div className="flex flex-col gap-5"> setFieldValue("totalBill", totalBill.toFixed(2));
<NextUIFormikInput
label="Electrical Bill" const avgBill = Number(values.noOfDependents) > 0
name="electricalBill" ? totalBill / Number(values.noOfDependents)
type="text" : 0;
placeholder="$" setFieldValue("avgBill", avgBill.toFixed(2));
labelPlacement="inside"
/> }, [values.electricalBill, values.waterBill, values.noOfDependents, setFieldValue]);
<NextUIFormikInput
label="Water Bill" // Disabled the submit button because the images field are not selected
name="waterBill" const isSubmitDisabled = !imagesSelected.ebPicture || !imagesSelected.wbPicture;
type="text"
placeholder="$" return (
labelPlacement="inside" <Form>
/> <div className="flex flex-col gap-5">
<NextUIFormikInput <div className="flex flex-row gap-10">
label="Total Bill" <div className="flex flex-col gap-10">
name="totalBill" <NextUIFormikInput
type="text" label="Electrical Bill"
placeholder="$" name="electricalBill"
labelPlacement="inside" type="text"
/> placeholder="0.00"
<NextUIFormikInput labelPlacement="inside"
label="Number of dependents" setFieldValue={setFieldValue}
name="noOfDependents" />
type="text" <NextUIFormikInput
placeholder="0" label="Water Bill"
labelPlacement="inside" name="waterBill"
/> type="text"
placeholder="0.00"
labelPlacement="inside"
setFieldValue={setFieldValue}
/>
<NextUIFormikInput
label="Total Bill"
name="totalBill"
type="text"
placeholder="0.00"
labelPlacement="inside"
readOnly={true}
/>
<NextUIFormikInput
label="Number of dependents"
name="noOfDependents"
type="text"
placeholder="0"
labelPlacement="inside"
/>
<NextUIFormikInput
label="Average Bill"
name="avgBill"
type="text"
placeholder="0"
labelPlacement="inside"
readOnly={true}
/>
</div>
<div className="flex flex-row max-w-xs h-[500px] gap-10">
<InsertImage
onImageSelected={(file) => {
setFieldValue("ebPicture", file);
handleImageSelection("ebPicture", file);
}}
/>
<InsertImage
onImageSelected={(file) => {
setFieldValue("wbPicture", file);
handleImageSelection("wbPicture", file);
}}
/>
</div>
</div> </div>
<div className="flex flex-row gap-8 max-w-xs h-[500px]"> <div>
<InsertImage <Button
onImageSelected={(file) => { type="submit"
setFieldValue("ebPicture", file); className="bg-red-400 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-900 text-white"
}} size="lg"
/> isDisabled={!isValid || !dirty || isSubmitting || isSubmitDisabled}
<InsertImage >
onImageSelected={(file) => { <p>Submit</p>
setFieldValue("wbPicture", file); </Button>
}}
/>
</div> </div>
</div> </div>
<div> </Form>
<Button );
type="submit" }}
className="bg-red-500 dark:bg-red-700 text-white"
isDisabled={!isValid || !dirty || isSubmitting}
>
<p>Submit</p>
</Button>
</div>
</div>
</Form>
)}
</Formik> </Formik>
</section> </section>
</div> </div>

View File

@@ -26,6 +26,10 @@ module.exports = (sequelize, DataTypes) => {
type: DataTypes.INTEGER, type: DataTypes.INTEGER,
allowNull: false allowNull: false
}, },
avgBill: {
type: DataTypes.DECIMAL(7, 2),
allowNull: false
},
ebPicture: { ebPicture: {
type: DataTypes.BLOB("long"), type: DataTypes.BLOB("long"),
allowNull: true, allowNull: true,

View File

@@ -23,6 +23,7 @@ router.post("/", upload.fields([
waterBill: yup.number().positive().required(), waterBill: yup.number().positive().required(),
totalBill: yup.number().positive().required(), totalBill: yup.number().positive().required(),
noOfDependents: yup.number().integer().positive().required(), noOfDependents: yup.number().integer().positive().required(),
avgBill: yup.number().positive().required(),
}); });
try { try {
data = await validationSchema.validate(data, { abortEarly: false }); data = await validationSchema.validate(data, { abortEarly: false });
@@ -48,6 +49,7 @@ router.get("/", async (req, res) => {
{ waterBill: { [Op.like]: `%${search}%` } }, { waterBill: { [Op.like]: `%${search}%` } },
{ totalBill: { [Op.like]: `%${search}%` } }, { totalBill: { [Op.like]: `%${search}%` } },
{ noOfDependents: { [Op.like]: `%${search}%` } }, { noOfDependents: { [Op.like]: `%${search}%` } },
{ avgBill: { [Op.like]: `%${search}%` } },
]; ];
} }
let list = await HBCform.findAll({ let list = await HBCform.findAll({