Introduction of town council

This commit is contained in:
2024-08-01 03:39:11 +08:00
parent 4a8b6a0b4c
commit 68a2062365
9 changed files with 3891 additions and 4578 deletions

6138
client/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,46 @@
import { Select, SelectItem } from "@nextui-org/react";
import { useField, useFormikContext } from "formik";
interface NextUIFormikSelectProps {
label: string;
name: string;
placeholder: string;
labelPlacement?: "inside" | "outside";
options: Array<{ key: string; label: string }>;
}
const NextUIFormikSelect = ({
label,
name,
placeholder,
labelPlacement = "outside",
options,
}: NextUIFormikSelectProps) => {
const [field, meta] = useField(name);
const { setFieldValue } = useFormikContext();
const handleChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setFieldValue(name, e.target.value);
};
return (
<Select
{...field}
label={label}
labelPlacement={labelPlacement}
placeholder={placeholder}
selectedKeys={[field.value]}
onChange={handleChange}
isInvalid={meta.touched && !!meta.error}
errorMessage={meta.touched && meta.error ? meta.error : ""}
>
{options.map((option) => (
<SelectItem key={option.key} value={option.key}>
{option.label}
</SelectItem>
))}
</Select>
);
};
export default NextUIFormikSelect;

View File

@@ -5,8 +5,10 @@ import config from "../config";
import NextUIFormikInput from "./NextUIFormikInput";
import { useNavigate } from "react-router-dom";
import { popErrorToast } from "../utilities";
import { useState } from "react";
import { useEffect, useState } from "react";
import instance from "../security/http";
import axios from "axios";
import NextUIFormikSelect from "./NextUIFormikSelect";
const validationSchema = Yup.object({
firstName: Yup.string()
@@ -31,6 +33,7 @@ const validationSchema = Yup.object({
.matches(/^[0-9]+$/, "Phone number must contain only numerical characters")
.length(8, "Phone number must be 8 digits")
.required("Phone number is required"),
townCouncil: Yup.string().trim().max(30).required(),
password: Yup.string()
.trim()
.min(8, "Password must be at least 8 characters")
@@ -49,12 +52,14 @@ const validationSchema = Yup.object({
export default function SignUpModule() {
const navigate = useNavigate();
const [step, setStep] = useState(1);
const [townCouncils, setTownCouncils] = useState<string[]>([]);
const initialValues = {
firstName: "",
lastName: "",
email: "",
phoneNumber: "",
townCouncil: "",
password: "",
terms: false,
};
@@ -75,6 +80,14 @@ export default function SignUpModule() {
const nextStep = () => setStep((prev) => prev + 1);
const prevStep = () => setStep((prev) => prev - 1);
useEffect(() => {
axios
.get(`${config.serverAddress}/users/town-councils-metadata`)
.then((values) => {
setTownCouncils(JSON.parse(values.data).townCouncils);
});
}, []);
return (
<div className="flex flex-col gap-16">
<Formik
@@ -132,6 +145,18 @@ export default function SignUpModule() {
</p>
}
/>
{townCouncils.length > 0 && (
<NextUIFormikSelect
label="Town council"
name="townCouncil"
placeholder="Choose the town council you belong to"
labelPlacement="outside"
options={townCouncils.map((townCouncil) => ({
key: townCouncil,
label: townCouncil,
}))}
/>
)}
<div className="flex justify-between">
<Button onClick={prevStep} variant="light">
Back

View File

@@ -20,10 +20,13 @@ import { useNavigate } from "react-router-dom";
import UserProfilePicture from "./UserProfilePicture";
import { popErrorToast, popToast } from "../utilities";
import instance from "../security/http";
import axios from "axios";
import NextUIFormikSelect from "./NextUIFormikSelect";
export default function UpdateAccountModule() {
const navigate = useNavigate();
const [userInformation, setUserInformation] = useState<any>();
const [townCouncils, setTownCouncils] = useState<string[]>([]);
const {
isOpen: isArchiveDialogOpen,
@@ -41,6 +44,11 @@ export default function UpdateAccountModule() {
retrieveUserInformation()
.then((response) => {
setUserInformation(response);
axios
.get(`${config.serverAddress}/users/town-councils-metadata`)
.then((values) => {
setTownCouncils(JSON.parse(values.data).townCouncils);
});
})
.catch(() => {
navigate("/signin");
@@ -73,6 +81,7 @@ export default function UpdateAccountModule() {
)
.length(8, "Phone number must be 8 digits")
.required("Phone number is required"),
townCouncil: Yup.string().trim().max(30).required(),
});
const handleSubmit = async (values: any) => {
@@ -95,6 +104,7 @@ export default function UpdateAccountModule() {
lastName: userInformation.lastName || "",
email: userInformation.email || "",
phoneNumber: userInformation.phoneNumber || "",
townCouncil: userInformation.townCouncil || "",
}
: {
id: "",
@@ -102,6 +112,7 @@ export default function UpdateAccountModule() {
lastName: "",
email: "",
phoneNumber: "",
townCouncil: "",
};
const archiveAccount = () => {
@@ -137,7 +148,7 @@ export default function UpdateAccountModule() {
return (
<div>
{userInformation && (
<div>
<div className="max-w-[800px] mx-auto">
<div className="flex flex-col gap-16">
<div>
<Formik
@@ -170,8 +181,8 @@ export default function UpdateAccountModule() {
</Button>
</div>
</div>
<div className="flex flex-row gap-8">
<div className="flex-grow flex sm:flex-row flex-col gap-4 *:w-full *:flex *:flex-col *:gap-4 *:my-auto">
<div className="flex flex-row *:my-auto">
<div className="w-full *:w-full *:flex *:flex-col *:gap-4 *:my-auto">
<div>
<NextUIFormikInput
label="First Name"
@@ -187,8 +198,6 @@ export default function UpdateAccountModule() {
placeholder="Doe"
labelPlacement="outside"
/>
</div>
<div>
<NextUIFormikInput
label="Email"
name="email"
@@ -208,13 +217,32 @@ export default function UpdateAccountModule() {
</p>
}
/>
{townCouncils.length > 0 && (
<NextUIFormikSelect
label="Town council"
name="townCouncil"
placeholder="Choose the town council you belong to"
labelPlacement="outside"
options={townCouncils.map((townCouncil) => ({
key: townCouncil,
label: townCouncil,
}))}
/>
)}
</div>
</div>
<div>
<div className="w-full flex flex-row justify-center">
<div className="flex flex-col gap-8 text-center">
<UserProfilePicture
userId={userInformation.id}
editable={true}
/>
<p className="font-bold opacity-50 text-sm">
Click to select a new
<br />
profile picture
</p>
</div>
</div>
</div>
</Form>

View File

@@ -51,7 +51,13 @@ export default function SpringboardPage() {
{greeting}, {userInformation.firstName}.
</p>
<p>
Resident of <Link>Bishan-Toa Payoh Town Council</Link>
Resident of{" "}
<Link>
{userInformation.townCouncil.length > 0
? userInformation.townCouncil
: "Unknown"}{" "}
Town Council
</Link>
</p>
<Button
className="w-max"

View File

@@ -0,0 +1,21 @@
{
"townCouncils": [
"Aljunied\u2014Hougang",
"Ang Mo Kio",
"Bishan\u2014Toa Payoh",
"Chua Chu Kang",
"East Coast",
"Holland\u2014Bukit Panjang",
"Jalan Besar",
"Jurong\u2014Clementi",
"Marine Parade",
"Marsiling\u2014Yew Tee",
"Nee Soon",
"Pasir Ris\u2014Punggol",
"Sembawang",
"Sengkang",
"Tampines",
"Tanjong Pagar",
"West Coast"
]
}

View File

@@ -40,6 +40,10 @@ module.exports = (sequelize) => {
type: DataTypes.BLOB("long"),
allowNull: true,
},
townCouncil: {
type: DataTypes.STRING(30),
allowNull: false,
},
accountType: {
type: DataTypes.TINYINT(2),
allowNull: false,

1931
server/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -9,6 +9,7 @@ const { sign } = require("jsonwebtoken");
const multer = require("multer");
const sharp = require("sharp");
const { sendPasswordResetEmail } = require("../connections/mailersend");
const fs = require("fs");
const {
generatePasswordResetToken,
} = require("../security/generatePasswordResetToken");
@@ -446,4 +447,25 @@ async function resetPassword(token, newPassword) {
await user.save();
}
function readTownCouncilsFile() {
try {
const data = fs.readFileSync("assets/town_councils.json", "utf8");
return data;
// const parsedData = JSON.parse(data);
// return parsedData;
} catch (err) {
console.error("Error reading JSON file:", err);
return null;
}
}
router.get("/town-councils-metadata", async (req, res) => {
try {
let result = readTownCouncilsFile();
res.json(result);
} catch {
res.status(401);
}
});
module.exports = router;