diff --git a/client/src/App.tsx b/client/src/App.tsx index f26e763..9558f5d 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -14,6 +14,8 @@ import EventsPage from "./pages/EventsPage"; import CreateEventsPage from "./pages/CreateEventsPage"; import ManageEventsPage from "./pages/ManageEventsPage"; import AdministratorSpringboard from "./pages/AdministratorSpringboard"; +import HBContestPage from "./pages/HBContestPage"; +import HBFormPage from "./pages/HBFormPage"; import EditEventsPage from "./pages/EditEventsPage"; function App() { @@ -32,6 +34,8 @@ function App() { } path="/post/:id" /> } path="/schedule" /> } path="/events" /> + } path="/contest" /> + } path="/hbcform" /> } path="/createEvent" /> } path="/manageEvent" /> } path="/editEvent/:id" /> diff --git a/client/src/components/InsertImage.tsx b/client/src/components/InsertImage.tsx new file mode 100644 index 0000000..4fe7a9c --- /dev/null +++ b/client/src/components/InsertImage.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; + +interface InsertImageProps { + onImageSelected: (file: File) => void; +} + +const InsertImage: React.FC = ({ onImageSelected }) => { + const [selectedFile, setSelectedFile] = useState(null); + const [previewImage, setPreviewImage] = useState(''); + + const handleImageSelect = (event: React.ChangeEvent) => { + const selectedFiles = event.target.files as FileList; + const file = selectedFiles?.[0]; + if (file) { + setSelectedFile(file); + setPreviewImage(URL.createObjectURL(file)); + onImageSelected(file); + } + }; + + return ( + + + {selectedFile && ( + + )} + + ); +}; + +export default InsertImage; \ No newline at end of file diff --git a/client/src/components/NavigationBar.tsx b/client/src/components/NavigationBar.tsx index 8121eef..c23bb75 100644 --- a/client/src/components/NavigationBar.tsx +++ b/client/src/components/NavigationBar.tsx @@ -91,6 +91,15 @@ export default function NavigationBar() { > Schedules + { + navigate("/contest"); + }} + > + HB Contest + + + + + + Home Bill Contest + + + + + This contest is to encourage residents to reduce the use of electricity and water usage. + This contest would be won by the person with the lowest overall bill average. + Join us in this important effort to create a more sustainable future for everyone. + Participants would be required to input and upload their bills into the form to ensure integrity and honesty. + + + + + + Winners + There will 3 winners for each month. Each winner will receive random food vouchers. + 1st: 3 vouchers + 2nd: 2 vouchers + 3rd: 1 voucher + + + { + navigate("/hbcform"); + }} + > + Join + + + + + + + + ) +} + diff --git a/client/src/pages/HBFormPage.tsx b/client/src/pages/HBFormPage.tsx new file mode 100644 index 0000000..6f08138 --- /dev/null +++ b/client/src/pages/HBFormPage.tsx @@ -0,0 +1,168 @@ +import DefaultLayout from '../layouts/default'; +import { Button } from '@nextui-org/react'; +import { ArrowUTurnLeftIcon } from '../icons'; +import { useNavigate } from 'react-router-dom'; +import { Formik, Form } from 'formik'; +import * as Yup from 'yup'; +import config from '../config'; +import NextUIFormikInput from '../components/NextUIFormikInput'; +import axios from "axios"; +import InsertImage from '../components/InsertImage'; + +const validationSchema = Yup.object({ + electricalBill: Yup.number().positive().required(), + waterBill: Yup.number().positive().required(), + totalBill: Yup.number().positive().required(), + noOfDependents: Yup.number().integer().positive().required(), +}); + +export default function HBFormPage() { + let navigate = useNavigate(); + + const initialValues: { + id: string; + electricalBill: string; + waterBill: string; + totalBill: string; + noOfDependents: string; + ebPicture: File | null; + wbPicture: File | null; + } = { + id: '', + electricalBill: '', + waterBill: '', + totalBill: '', + noOfDependents: '', + ebPicture: null, + wbPicture: null, + }; + + + const handleSubmit = async ( + values: any, + { setSubmitting, resetForm, setFieldError }: any + ) => { + const formData = new FormData(); + formData.append('electricalBill', values.electricalBill); + formData.append('waterBill', values.waterBill); + formData.append('totalBill', values.totalBill); + formData.append('noOfDependents', values.noOfDependents); + + // Convert image files to blobs + const ebPictureFile = values.ebPicture; + const wbPictureFile = values.wbPicture; + + if (ebPictureFile) { + const ebPictureBlob = ebPictureFile.slice(0, ebPictureFile.size, ebPictureFile.type); + formData.append('ebPicture', ebPictureBlob); + } + + if (wbPictureFile) { + const wbPictureBlob = wbPictureFile.slice(0, wbPictureFile.size, wbPictureFile.type); + formData.append('wbPicture', wbPictureBlob); + } + + try { + const response = await axios.post(config.serverAddress + "/hbcform", formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + if (response.status === 200) { + console.log("Form created successfully:", response.data); + resetForm(); // Clear form after successful submit + navigate("/contest"); + } else { + console.error("Error creating form:", response.statusText); + } + } catch (error: any) { + if (error.response && error.response.data && error.response.data.field) { + setFieldError(error.response.data.field, error.response.data.error); + } else { + console.error("Unexpected error:", error); + } + } finally { + setSubmitting(false); + } + }; + + return ( + + + navigate(-1)} + > + + + + + + {({ isValid, dirty, isSubmitting, setFieldValue }) => ( + + + + + + + + + + + { + setFieldValue('ebPicture', file); + }} + /> + { + setFieldValue('wbPicture', file); + }} + /> + + + + + Submit + + + + + )} + + + + ); +} diff --git a/server/index.js b/server/index.js index 42cf0d5..657cb4b 100644 --- a/server/index.js +++ b/server/index.js @@ -34,6 +34,9 @@ app.use("/post", postRoute); const schedulesRoute = require("./routes/schedule"); app.use("/schedule", schedulesRoute) +const HBCformRoute = require("./routes/hbcform"); +app.use("/hbcform", HBCformRoute) + db.sequelize .sync({ alter: true }) .then(() => { diff --git a/server/models/HBCform.js b/server/models/HBCform.js new file mode 100644 index 0000000..5273b2f --- /dev/null +++ b/server/models/HBCform.js @@ -0,0 +1,42 @@ +const { DataTypes } = require("sequelize"); + +module.exports = (sequelize, DataTypes) => { + const HBCform = sequelize.define( + "HBCform", + { + id: { + type: DataTypes.UUID, + defaultValue: DataTypes.UUIDV4, + allowNull: false, + primaryKey: true, + }, + electricalBill: { + type: DataTypes.DECIMAL(7, 2), + allowNull: false + }, + waterBill: { + type: DataTypes.DECIMAL(7, 2), + allowNull: false + }, + totalBill: { + type: DataTypes.DECIMAL(7, 2), + allowNull: false + }, + noOfDependents: { + type: DataTypes.INTEGER, + allowNull: false + }, + ebPicture: { + type: DataTypes.BLOB("long"), + allowNull: true, + }, + wbPicture: { + type: DataTypes.BLOB("long"), + allowNull: true, + }, + }, + { + tableName: 'hbcform' + }); + return HBCform; +} \ No newline at end of file diff --git a/server/routes/hbcform.js b/server/routes/hbcform.js new file mode 100644 index 0000000..da91a4a --- /dev/null +++ b/server/routes/hbcform.js @@ -0,0 +1,111 @@ +const express = require('express'); +const router = express.Router(); +const { HBCform } = require('../models'); +const { Op } = require("sequelize"); +const yup = require("yup"); +const multer = require("multer"); +const sharp = require("sharp"); + + +const upload = multer({ storage: multer.memoryStorage() }); + +router.post("/", upload.fields([ + { name: 'ebPicture', maxCount: 1 }, + { name: 'wbPicture', maxCount: 1 } +]), async (req, res) => { + console.log("Request Body:", req.body); + console.log("Request Files:", req.files); + + let data = req.body; + let files = req.files; + + // Validate request body + let validationSchema = yup.object({ + electricalBill: yup.number().positive().required(), + waterBill: yup.number().positive().required(), + totalBill: yup.number().positive().required(), + noOfDependents: yup.number().integer().positive().required(), + }); + try { + data = await validationSchema.validate(data, { abortEarly: false }); + console.log("Validated data:", data); + + // Process valid data + let ebPicture = files.ebPicture[0].buffer; + let wbPicture = files.wbPicture[0].buffer; + + let result = await HBCform.create({ ...data, ebPicture, wbPicture }); + res.json(result); + } catch (err) { + console.error("Error:", err); + res.status(400).json({ errors: err.errors }); + } +}); + +router.get("/", async (req, res) => { + let condition = {}; + let search = req.query.search; + if (search) { + condition[Op.or] = [ + { electricalBill: { [Op.like]: `%${search}%` } }, + { waterBill: { [Op.like]: `%${search}%` } }, + { totalBill: { [Op.like]: `%${search}%` } }, + { noOfDependents: { [Op.like]: `%${search}%` } }, + ]; + } + let list = await HBCform.findAll({ + where: condition, + order: [['createdAt', 'ASC']] + }); + res.json(list); +}); + +router.get("/:id", async (req, res) => { + let id = req.params.id; + let hbcform = await HBCform.findByPk(id); + if (!hbcform) { + res.sendStatus(404); + return; + } + res.json(hbcform); +}); + +router.get("/ebPicture/:id", async (req, res) => { + let id = req.params.id; + let hbcform = await HBCform.findByPk(id); + + if (!hbcform || !hbcform.ebPicture) { + res.sendStatus(404); + return; + } + + try { + res.set("Content-Type", "image/jpeg"); // Adjust the content type as necessary + res.send(hbcform.ebPicture); + } catch (err) { + res + .status(500) + .json({ message: "Error retrieving electrical bill", error: err }); + } +}); + +router.get("/wbPicture/:id", async (req, res) => { + let id = req.params.id; + let hbcform = await HBCform.findByPk(id); + + if (!hbcform || !hbcform.wbPicture) { + res.sendStatus(404); + return; + } + + try { + res.set("Content-Type", "image/jpeg"); // Adjust the content type as necessary + res.send(hbcform.wbPicture); + } catch (err) { + res + .status(500) + .json({ message: "Error retrieving electrical bill", error: err }); + } +}); + +module.exports = router; \ No newline at end of file
Schedules
HB Contest
This contest is to encourage residents to reduce the use of electricity and water usage. + This contest would be won by the person with the lowest overall bill average. + Join us in this important effort to create a more sustainable future for everyone. + Participants would be required to input and upload their bills into the form to ensure integrity and honesty.
There will 3 winners for each month. Each winner will receive random food vouchers.
1st: 3 vouchers
2nd: 2 vouchers
3rd: 1 voucher
Join
Submit