Leaderboard Added to HBContestPage
This commit is contained in:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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 → 3 vouchers</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">2nd Place → 2 vouchers</p>
|
||||
<p className="text-gray-700 dark:text-gray-300 font-bold">3rd Place → 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 → 3 vouchers</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">2nd → 2 vouchers</p>
|
||||
<p className="text-gray-700 dark:text-gray-300">3rd → 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 >
|
||||
);
|
||||
}
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user