Introduction of town council
This commit is contained in:
6272
client/pnpm-lock.yaml
generated
6272
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
46
client/src/components/NextUIFormikSelect.tsx
Normal file
46
client/src/components/NextUIFormikSelect.tsx
Normal 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;
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
<UserProfilePicture
|
||||
userId={userInformation.id}
|
||||
editable={true}
|
||||
/>
|
||||
<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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
21
server/assets/town_councils.json
Normal file
21
server/assets/town_councils.json
Normal 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"
|
||||
]
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
2021
server/pnpm-lock.yaml
generated
2021
server/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user