Leaderboard Added to HBContestPage

This commit is contained in:
ZacTohZY
2024-07-31 03:10:31 +08:00
parent 93d2cd8a05
commit b6fb95a5fe
4 changed files with 276 additions and 52 deletions

View File

@@ -539,3 +539,78 @@ export const ArrowTopRightOnSquare = () => {
</svg>
);
};
export const TrashDeleteIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m14.74 9-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 0 1-2.244 2.077H8.084a2.25 2.25 0 0 1-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 0 0-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 0 1 3.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 0 0-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 0 0-7.5 0"
/>
</svg>
);
}
export const EmailIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M21.75 6.75v10.5a2.25 2.25 0 0 1-2.25 2.25h-15a2.25 2.25 0 0 1-2.25-2.25V6.75m19.5 0A2.25 2.25 0 0 0 19.5 4.5h-15a2.25 2.25 0 0 0-2.25 2.25m19.5 0v.243a2.25 2.25 0 0 1-1.07 1.916l-7.5 4.615a2.25 2.25 0 0 1-2.36 0L3.32 8.91a2.25 2.25 0 0 1-1.07-1.916V6.75"
/>
</svg>
)
}
export const InfoIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="m11.25 11.25.041-.02a.75.75 0 0 1 1.063.852l-.708 2.836a.75.75 0 0 0 1.063.853l.041-.021M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Zm-9-3.75h.008v.008H12V8.25Z"
/>
</svg>
)
}
export const TrophyIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none" viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
className="size-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M16.5 18.75h-9m9 0a3 3 0 0 1 3 3h-15a3 3 0 0 1 3-3m9 0v-3.375c0-.621-.503-1.125-1.125-1.125h-.871M7.5 18.75v-3.375c0-.621.504-1.125 1.125-1.125h.872m5.007 0H9.497m5.007 0a7.454 7.454 0 0 1-.982-3.172M9.497 14.25a7.454 7.454 0 0 0 .981-3.172M5.25 4.236c-.982.143-1.954.317-2.916.52A6.003 6.003 0 0 0 7.73 9.728M5.25 4.236V4.5c0 2.108.966 3.99 2.48 5.228M5.25 4.236V2.721C7.456 2.41 9.71 2.25 12 2.25c2.291 0 4.545.16 6.75.47v1.516M7.73 9.728a6.726 6.726 0 0 0 2.748 1.35m8.272-6.842V4.5c0 2.108-.966 3.99-2.48 5.228m2.48-5.492a46.32 46.32 0 0 1 2.916.52 6.003 6.003 0 0 1-5.395 4.972m0 0a6.726 6.726 0 0 1-2.749 1.35m0 0a6.772 6.772 0 0 1-3.044 0"
/>
</svg>
)
}

View File

@@ -1,5 +1,22 @@
import { Button } from "@nextui-org/react";
import { Button, Tooltip, Modal, ModalContent, ModalHeader, ModalBody } from "@nextui-org/react";
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import instance from "../security/http";
import config from "../config";
import { InfoIcon, TrophyIcon } from "../icons";
interface User {
id: string;
firstName: string;
lastName: string;
}
interface CombinedData {
userId: string;
formId: string;
name: string;
avgBill: number;
}
export default function HBContestPage() {
const navigate = useNavigate();
@@ -15,51 +32,181 @@ export default function HBContestPage() {
}
};
const [combinedData, setCombinedData] = useState<CombinedData[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const handleOpenModal = () => {
setIsModalOpen(true);
};
const [isInfoModalOpen, setIsInfoModalOpen] = useState(false);
const handleOpenInfoModal = () => setIsInfoModalOpen(true);
useEffect(() => {
const fetchData = async () => {
try {
// Fetch form data
const formDataResponse = await instance.get<CombinedData[]>(`${config.serverAddress}/hbcform`);
const processedFormData = formDataResponse.data;
// Fetch user information
const userInfoResponse = await instance.get<User[]>(`${config.serverAddress}/users/all`);
// Combine form data with user information
const combined = processedFormData.map((form) => {
const user = userInfoResponse.data.find((user) => user.id === form.userId);
return {
userId: user ? user.id : "Unknown User",
formId: form.userId,
name: user ? `${user.firstName} ${user.lastName}` : "Unknown Name",
avgBill: form.avgBill,
};
});
// Sort combined data by avgBill in ascending order
combined.sort((a, b) => a.avgBill - b.avgBill);
setCombinedData(combined);
} catch (error) {
console.log("Failed to fetch data");
}
};
fetchData();
}, []);
const topUser = combinedData.length > 0 ? combinedData[0] : null;
const top5Users = combinedData.slice(1, 10);
return (
<div className="h-screen flex items-center justify-center">
<section className="bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl shadow-md w-full max-w-2xl">
<div className="space-y-4">
<div>
<p className="text-2xl font-bold text-red-900 dark:text-white">
Home Bill Contest
</p>
<div className="flex flex-col items-center justify-center">
<div className="h-[700px] flex flex-col items-center justify-center overflow-y-auto p-4">
<section className="bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl w-full max-w-2xl flex flex-col items-center">
<div className="w-full flex justify-end">
<Tooltip content="Information">
<Button isIconOnly variant="light" onPress={handleOpenInfoModal}>
<InfoIcon />
</Button>
</Tooltip>
<Tooltip content="Leaderboard">
<Button isIconOnly variant="light" onPress={handleOpenModal}>
<TrophyIcon />
</Button>
</Tooltip>
</div>
<div>
<p className="text-gray-700 dark:text-gray-300">
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.{" "}
<span className="text-red-600">
Participants would be required to input and upload their bills into the form to ensure integrity and honesty.{" "}
</span>
</p>
<div className="space-y-4">
<div>
<p className="text-3xl font-bold text-red-900 dark:text-white">
Home Bill Contest
</p>
</div>
<div>
<p className="text-gray-700 dark:text-gray-300">
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.{" "}
</p>
</div>
<div>
<p className="text-3xl font-bold text-red-900 dark:text-white">
Winners
</p>
</div>
<div>
<p className="text-gray-700 dark:text-gray-300">
There will be 3 winners for each month. Each winner will receive
random vouchers.
</p>
<br></br>
<p className="text-gray-700 dark:text-gray-300 font-bold">1st Place &rarr; 3 vouchers</p>
<p className="text-gray-700 dark:text-gray-300 font-bold">2nd Place &rarr; 2 vouchers</p>
<p className="text-gray-700 dark:text-gray-300 font-bold">3rd Place &rarr; 1 voucher</p>
</div>
<div className="flex justify-end">
<Button
className="bg-red-500 text-white hover:bg-red-900 focus:ring-red-400 dark:bg-red-600 dark:hover:bg-red-900 dark:focus:ring-red-700 w-100"
size="lg"
onPress={handleJoinClick}
>
<p>Join</p>
</Button>
</div>
</div>
<div>
<p className="text-xl font-bold text-red-900 dark:text-white">
Winners
</p>
</div>
<div>
<p className="text-gray-700 dark:text-gray-300">
There will 3 winners for each month. Each winner will receive
random food vouchers.
</p>
<p className="text-gray-700 dark:text-gray-300">1st &rarr; 3 vouchers</p>
<p className="text-gray-700 dark:text-gray-300">2nd &rarr; 2 vouchers</p>
<p className="text-gray-700 dark:text-gray-300">3rd &rarr; 1 voucher</p>
</div>
<div>
<Button
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"
size="lg"
onPress={handleJoinClick}
>
<p>Join</p>
</Button>
</div>
</div>
</section>
</div>
</section>
</div>
<Modal isOpen={isModalOpen} onOpenChange={setIsModalOpen} isDismissable={false} isKeyboardDismissDisabled={true}>
<ModalContent className="w-full max-w-[400px] relative">
<ModalHeader className="flex justify-center items-center font-bold text-2xl text-red-900">
<span>Leaderboard</span>
</ModalHeader>
<ModalBody className="pb-8"> {/* Add padding-bottom for white space */}
<div className="flex flex-col gap-4">
{topUser && (
<div className="p-4 border rounded-md bg-red-100 flex items-center">
<TrophyIcon />
<p className="text-lg flex-1 text-center font-bold">{topUser.name}</p> {/* Center text */}
</div>
)}
</div>
<div className="grid grid-rows-1 md:grid-rows-2 gap-4">
{top5Users.map((user, index) => (
<div key={user.userId} className="p-4 border rounded-md bg-red-100 flex items-center">
<span className="text-xl font-bold w-8">{index + 2}</span>
<span className="flex-1 text-center">{user.name}</span>
</div>
))}
</div>
</ModalBody>
</ModalContent>
</Modal>
{/* Info Modal */}
<Modal
isOpen={isInfoModalOpen}
onOpenChange={setIsInfoModalOpen}
isDismissable={true}
isKeyboardDismissDisabled={true}
>
<ModalContent className="w-full max-w-[400px]">
<ModalHeader className="flex justify-between items-center font-bold text-2xl text-red-900">
Information
</ModalHeader>
<ModalBody className="pb-8">
<div className="space-y-4 text-gray-700 dark:text-gray-300">
<p className="font-semibold">Form Completion:</p>
<ul className="list-disc list-inside pl-4">
Participants must complete the online form, providing the following details
<li>Water bill amount</li>
<li>Electricity bill amount</li>
<li>Number of dependents</li>
</ul>
<p className="font-semibold">Document Upload</p>
<ul className="list-disc list-inside pl-4">
<li>Participants must upload clear, legible images of their water and electricity bills.</li>
<li>Both documents should clearly show the bill amount and date.</li>
</ul>
<p className="font-semibold">Data Accuracy</p>
<ul className="list-disc list-inside pl-4">
<li>All provided information must be accurate and truthful. Inaccurate or misleading information will result in disqualification.</li>
<li>All submitted bills and details will be subject to verification.</li>
</ul>
<p className="font-semibold">Privacy and Data Handling</p>
<ul className="list-disc list-inside pl-4">
<li>All personal data and bill images submitted will be handled in accordance with privacy regulations and will not be shared with third parties without consent.</li>
<li>Data will be used solely for contest purposes and to verify the validity of entries.</li>
</ul>
</div>
</ModalBody>
</ModalContent>
</Modal>
</div >
);
}
}

View File

@@ -139,12 +139,12 @@ export default function HBFormPage() {
return (
<div className="w-full h-full">
<section className="w-8/12 mx-auto">
<section className="w-7/12 mx-auto">
<Button variant="light" onPress={() => navigate(-1)}>
<ArrowUTurnLeftIcon />
</Button>
</section>
<section className="w-8/12 mx-auto p-5 bg-red-100 dark:bg-red-950 border border-primary-100 rounded-2xl h-600px">
<section className="w-7/12 mx-auto p-5 bg-red-100 dark:bg-red-950 border border-primary-100 rounded-2xl h-600px">
<Formik
initialValues={initialValues}
validationSchema={validationSchema}
@@ -201,6 +201,7 @@ export default function HBFormPage() {
type="text"
placeholder="0"
labelPlacement="inside"
setFieldValue={setFieldValue}
/>
<NextUIFormikInput
label="Average Bill"

View File

@@ -12,23 +12,23 @@ module.exports = (sequelize, DataTypes) => {
},
electricalBill: {
type: DataTypes.DECIMAL(7, 2),
allowNull: false
allowNull: false,
},
waterBill: {
type: DataTypes.DECIMAL(7, 2),
allowNull: false
allowNull: false,
},
totalBill: {
type: DataTypes.DECIMAL(7, 2),
allowNull: false
allowNull: false,
},
noOfDependents: {
type: DataTypes.INTEGER,
allowNull: false
allowNull: false,
},
avgBill: {
type: DataTypes.DECIMAL(7, 2),
allowNull: false
allowNull: false,
},
ebPicture: {
type: DataTypes.BLOB("long"),
@@ -40,6 +40,7 @@ module.exports = (sequelize, DataTypes) => {
},
userId: {
type: DataTypes.UUID,
allowNull: false,
},
},
{