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 NextUIFormikInput from "./NextUIFormikInput";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { popErrorToast } from "../utilities";
|
import { popErrorToast } from "../utilities";
|
||||||
import { useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import instance from "../security/http";
|
import instance from "../security/http";
|
||||||
|
import axios from "axios";
|
||||||
|
import NextUIFormikSelect from "./NextUIFormikSelect";
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
firstName: Yup.string()
|
firstName: Yup.string()
|
||||||
@@ -31,6 +33,7 @@ const validationSchema = Yup.object({
|
|||||||
.matches(/^[0-9]+$/, "Phone number must contain only numerical characters")
|
.matches(/^[0-9]+$/, "Phone number must contain only numerical characters")
|
||||||
.length(8, "Phone number must be 8 digits")
|
.length(8, "Phone number must be 8 digits")
|
||||||
.required("Phone number is required"),
|
.required("Phone number is required"),
|
||||||
|
townCouncil: Yup.string().trim().max(30).required(),
|
||||||
password: Yup.string()
|
password: Yup.string()
|
||||||
.trim()
|
.trim()
|
||||||
.min(8, "Password must be at least 8 characters")
|
.min(8, "Password must be at least 8 characters")
|
||||||
@@ -49,12 +52,14 @@ const validationSchema = Yup.object({
|
|||||||
export default function SignUpModule() {
|
export default function SignUpModule() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [step, setStep] = useState(1);
|
const [step, setStep] = useState(1);
|
||||||
|
const [townCouncils, setTownCouncils] = useState<string[]>([]);
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
firstName: "",
|
firstName: "",
|
||||||
lastName: "",
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
townCouncil: "",
|
||||||
password: "",
|
password: "",
|
||||||
terms: false,
|
terms: false,
|
||||||
};
|
};
|
||||||
@@ -75,6 +80,14 @@ export default function SignUpModule() {
|
|||||||
const nextStep = () => setStep((prev) => prev + 1);
|
const nextStep = () => setStep((prev) => prev + 1);
|
||||||
const prevStep = () => 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 (
|
return (
|
||||||
<div className="flex flex-col gap-16">
|
<div className="flex flex-col gap-16">
|
||||||
<Formik
|
<Formik
|
||||||
@@ -132,6 +145,18 @@ export default function SignUpModule() {
|
|||||||
</p>
|
</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">
|
<div className="flex justify-between">
|
||||||
<Button onClick={prevStep} variant="light">
|
<Button onClick={prevStep} variant="light">
|
||||||
Back
|
Back
|
||||||
|
|||||||
@@ -20,10 +20,13 @@ import { useNavigate } from "react-router-dom";
|
|||||||
import UserProfilePicture from "./UserProfilePicture";
|
import UserProfilePicture from "./UserProfilePicture";
|
||||||
import { popErrorToast, popToast } from "../utilities";
|
import { popErrorToast, popToast } from "../utilities";
|
||||||
import instance from "../security/http";
|
import instance from "../security/http";
|
||||||
|
import axios from "axios";
|
||||||
|
import NextUIFormikSelect from "./NextUIFormikSelect";
|
||||||
|
|
||||||
export default function UpdateAccountModule() {
|
export default function UpdateAccountModule() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [userInformation, setUserInformation] = useState<any>();
|
const [userInformation, setUserInformation] = useState<any>();
|
||||||
|
const [townCouncils, setTownCouncils] = useState<string[]>([]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen: isArchiveDialogOpen,
|
isOpen: isArchiveDialogOpen,
|
||||||
@@ -41,6 +44,11 @@ export default function UpdateAccountModule() {
|
|||||||
retrieveUserInformation()
|
retrieveUserInformation()
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
setUserInformation(response);
|
setUserInformation(response);
|
||||||
|
axios
|
||||||
|
.get(`${config.serverAddress}/users/town-councils-metadata`)
|
||||||
|
.then((values) => {
|
||||||
|
setTownCouncils(JSON.parse(values.data).townCouncils);
|
||||||
|
});
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
navigate("/signin");
|
navigate("/signin");
|
||||||
@@ -73,6 +81,7 @@ export default function UpdateAccountModule() {
|
|||||||
)
|
)
|
||||||
.length(8, "Phone number must be 8 digits")
|
.length(8, "Phone number must be 8 digits")
|
||||||
.required("Phone number is required"),
|
.required("Phone number is required"),
|
||||||
|
townCouncil: Yup.string().trim().max(30).required(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
const handleSubmit = async (values: any) => {
|
||||||
@@ -95,6 +104,7 @@ export default function UpdateAccountModule() {
|
|||||||
lastName: userInformation.lastName || "",
|
lastName: userInformation.lastName || "",
|
||||||
email: userInformation.email || "",
|
email: userInformation.email || "",
|
||||||
phoneNumber: userInformation.phoneNumber || "",
|
phoneNumber: userInformation.phoneNumber || "",
|
||||||
|
townCouncil: userInformation.townCouncil || "",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
id: "",
|
id: "",
|
||||||
@@ -102,6 +112,7 @@ export default function UpdateAccountModule() {
|
|||||||
lastName: "",
|
lastName: "",
|
||||||
email: "",
|
email: "",
|
||||||
phoneNumber: "",
|
phoneNumber: "",
|
||||||
|
townCouncil: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
const archiveAccount = () => {
|
const archiveAccount = () => {
|
||||||
@@ -137,7 +148,7 @@ export default function UpdateAccountModule() {
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{userInformation && (
|
{userInformation && (
|
||||||
<div>
|
<div className="max-w-[800px] mx-auto">
|
||||||
<div className="flex flex-col gap-16">
|
<div className="flex flex-col gap-16">
|
||||||
<div>
|
<div>
|
||||||
<Formik
|
<Formik
|
||||||
@@ -170,8 +181,8 @@ export default function UpdateAccountModule() {
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-8">
|
<div className="flex flex-row *:my-auto">
|
||||||
<div className="flex-grow flex sm:flex-row flex-col gap-4 *:w-full *:flex *:flex-col *:gap-4 *:my-auto">
|
<div className="w-full *:w-full *:flex *:flex-col *:gap-4 *:my-auto">
|
||||||
<div>
|
<div>
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="First Name"
|
label="First Name"
|
||||||
@@ -187,8 +198,6 @@ export default function UpdateAccountModule() {
|
|||||||
placeholder="Doe"
|
placeholder="Doe"
|
||||||
labelPlacement="outside"
|
labelPlacement="outside"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Email"
|
label="Email"
|
||||||
name="email"
|
name="email"
|
||||||
@@ -208,13 +217,32 @@ export default function UpdateAccountModule() {
|
|||||||
</p>
|
</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>
|
||||||
<div>
|
<div className="w-full flex flex-row justify-center">
|
||||||
<UserProfilePicture
|
<div className="flex flex-col gap-8 text-center">
|
||||||
userId={userInformation.id}
|
<UserProfilePicture
|
||||||
editable={true}
|
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>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -51,7 +51,13 @@ export default function SpringboardPage() {
|
|||||||
{greeting}, {userInformation.firstName}.
|
{greeting}, {userInformation.firstName}.
|
||||||
</p>
|
</p>
|
||||||
<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>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
className="w-max"
|
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"),
|
type: DataTypes.BLOB("long"),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
},
|
},
|
||||||
|
townCouncil: {
|
||||||
|
type: DataTypes.STRING(30),
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
accountType: {
|
accountType: {
|
||||||
type: DataTypes.TINYINT(2),
|
type: DataTypes.TINYINT(2),
|
||||||
allowNull: false,
|
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 multer = require("multer");
|
||||||
const sharp = require("sharp");
|
const sharp = require("sharp");
|
||||||
const { sendPasswordResetEmail } = require("../connections/mailersend");
|
const { sendPasswordResetEmail } = require("../connections/mailersend");
|
||||||
|
const fs = require("fs");
|
||||||
const {
|
const {
|
||||||
generatePasswordResetToken,
|
generatePasswordResetToken,
|
||||||
} = require("../security/generatePasswordResetToken");
|
} = require("../security/generatePasswordResetToken");
|
||||||
@@ -446,4 +447,25 @@ async function resetPassword(token, newPassword) {
|
|||||||
await user.save();
|
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;
|
module.exports = router;
|
||||||
|
|||||||
Reference in New Issue
Block a user