event details for event page
This commit is contained in:
1
client/assets/google-passkey.svg
Normal file
1
client/assets/google-passkey.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 8.4 KiB |
BIN
client/assets/gov-footer.png
Normal file
BIN
client/assets/gov-footer.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
@@ -14,6 +14,7 @@
|
||||
"axios": "^1.7.2",
|
||||
"formik": "^2.4.6",
|
||||
"framer-motion": "^11.2.10",
|
||||
"openai": "^4.53.2",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-hot-toast": "^2.4.1",
|
||||
|
||||
5810
client/pnpm-lock.yaml
generated
5810
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -22,61 +22,72 @@ import DefaultLayout from "./layouts/default";
|
||||
import AdministratorLayout from "./layouts/administrator";
|
||||
import UsersManagement from "./pages/UsersManagement";
|
||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
||||
import RestrictedLayout from "./layouts/restricted";
|
||||
|
||||
function App() {
|
||||
return (
|
||||
<Routes>
|
||||
{/* User Routes */}
|
||||
<Route path="/">
|
||||
<Route element={<DefaultLayout />}>
|
||||
{/* General Routes */}
|
||||
<Route index element={<HomePage />} />
|
||||
<Route element={<SignUpPage />} path="signup" />
|
||||
<Route element={<SignInPage />} path="signin" />
|
||||
<Route element={<SpringboardPage />} path="springboard" />
|
||||
<Route element={<ManageUserAccountPage />} path="manage-account" />
|
||||
<Route>
|
||||
{/* User Routes */}
|
||||
<Route path="/">
|
||||
<Route element={<DefaultLayout />}>
|
||||
{/* General Routes */}
|
||||
<Route index element={<HomePage />} />
|
||||
<Route element={<SignUpPage />} path="signup" />
|
||||
<Route element={<SignInPage />} path="signin" />
|
||||
<Route element={<SpringboardPage />} path="springboard" />
|
||||
<Route element={<ManageUserAccountPage />} path="manage-account" />
|
||||
|
||||
{/* Events Route */}
|
||||
{/* Events Route */}
|
||||
<Route path="events">
|
||||
<Route index element={<EventsPage />} />
|
||||
</Route>
|
||||
|
||||
{/* Karang Guni Schedules Route */}
|
||||
<Route path="karang-guni-schedules">
|
||||
<Route index element={<SchedulePage />} />
|
||||
</Route>
|
||||
|
||||
{/* Home Bill Contest Route */}
|
||||
<Route path="home-bill-contest">
|
||||
<Route index element={<HBContestPage />} />
|
||||
<Route element={<HBFormPage />} path="new-submission" />
|
||||
</Route>
|
||||
|
||||
{/* Community Posts Route */}
|
||||
<Route path="community-posts">
|
||||
<Route index element={<CommunityPage />} />
|
||||
<Route element={<CreatePostPage />} path="create" />
|
||||
<Route element={<PostPage />} path="post/:id" />
|
||||
<Route element={<EditPostPage />} path="edit/:id" />
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Special (Restricted) Routes */}
|
||||
<Route element={<RestrictedLayout />}>
|
||||
<Route element={<ForgotPasswordPage />} path="forgot-password" />
|
||||
<Route
|
||||
element={<ResetPasswordPage />}
|
||||
path="reset-password/:token"
|
||||
/>
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Admin Routes */}
|
||||
<Route path="/admin" element={<AdministratorLayout />}>
|
||||
<Route index element={<AdministratorSpringboard />} />
|
||||
<Route path="manage-account" element={<ManageUserAccountPage />} />
|
||||
<Route path="users-management">
|
||||
<Route index element={<UsersManagement />} />
|
||||
</Route>
|
||||
|
||||
{/* Events */}
|
||||
<Route path="events">
|
||||
<Route index element={<EventsPage />} />
|
||||
<Route index element={<ManageEventsPage />} />
|
||||
<Route element={<CreateEventsPage />} path="createEvent" />
|
||||
<Route element={<EditEventsPage />} path="editEvent/:id" />
|
||||
</Route>
|
||||
<Route element={<EventDetailsPage />} path="event/:id"/>
|
||||
|
||||
{/* Karang Guni Schedules Route */}
|
||||
<Route path="karang-guni-schedules">
|
||||
<Route index element={<SchedulePage />} />
|
||||
</Route>
|
||||
|
||||
{/* Home Bill Contest Route */}
|
||||
<Route path="home-bill-contest">
|
||||
<Route index element={<HBContestPage />} />
|
||||
<Route element={<HBFormPage />} path="new-submission" />
|
||||
</Route>
|
||||
|
||||
{/* Community Posts Route */}
|
||||
<Route path="community-posts">
|
||||
<Route index element={<CommunityPage />} />
|
||||
<Route element={<CreatePostPage />} path="create" />
|
||||
<Route element={<PostPage />} path="post/:id" />
|
||||
<Route element={<EditPostPage />} path="edit/:id" />
|
||||
</Route>
|
||||
</Route>
|
||||
<Route element={<ResetPasswordPage />} path="reset-password/:token" />
|
||||
</Route>
|
||||
|
||||
{/* Admin Routes */}
|
||||
<Route path="/admin" element={<AdministratorLayout />}>
|
||||
<Route index element={<AdministratorSpringboard />} />
|
||||
<Route path="manage-account" element={<ManageUserAccountPage />} />
|
||||
<Route path="users-management">
|
||||
<Route index element={<UsersManagement />} />
|
||||
</Route>
|
||||
|
||||
{/* Events */}
|
||||
<Route path="events">
|
||||
<Route index element={<ManageEventsPage />} />
|
||||
<Route element={<CreateEventsPage />} path="createEvent" />
|
||||
<Route element={<EditEventsPage />} path="editEvent/:id" />
|
||||
</Route>
|
||||
</Route>
|
||||
</Routes>
|
||||
|
||||
189
client/src/components/EcoconnectSearch.tsx
Normal file
189
client/src/components/EcoconnectSearch.tsx
Normal file
@@ -0,0 +1,189 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Input,
|
||||
Kbd,
|
||||
Modal,
|
||||
ModalBody,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
useDisclosure,
|
||||
} from "@nextui-org/react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { ArrowTopRightOnSquare, MagnifyingGlassIcon } from "../icons";
|
||||
import config from "../config";
|
||||
import instance from "../security/http";
|
||||
import EcoconnectFullLogo from "./EcoconnectFullLogo";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function EcoconnectSearch() {
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [aiResponseRoute, setAiResponseRoute] = useState("");
|
||||
const [aiResponseRouteDescription, setAiResponseRouteDescription] =
|
||||
useState("");
|
||||
const [isQueryLoading, setIsQueryLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const {
|
||||
isOpen: isAiDialogOpen,
|
||||
onOpen: onAiDialogOpen,
|
||||
onOpenChange: onAiDialogOpenChange,
|
||||
} = useDisclosure();
|
||||
|
||||
const dialogOpenChange = () => {
|
||||
onAiDialogOpenChange();
|
||||
setSearchQuery("");
|
||||
setAiResponseRoute("");
|
||||
};
|
||||
|
||||
const handleKeyPress = (event: KeyboardEvent) => {
|
||||
if ((event.ctrlKey || event.metaKey) && event.key === "s") {
|
||||
event.preventDefault();
|
||||
onAiDialogOpen();
|
||||
}
|
||||
};
|
||||
|
||||
const executeSearch = async () => {
|
||||
if (searchQuery.length <= 0) return;
|
||||
setIsQueryLoading(true);
|
||||
instance
|
||||
.get(`${config.serverAddress}/connections/nls/${searchQuery}`)
|
||||
.then((response) => {
|
||||
const rawResponse = response.data.response;
|
||||
const parsedResponse = JSON.parse(rawResponse);
|
||||
const resolvedRoute = parsedResponse.route;
|
||||
setAiResponseRoute(resolvedRoute);
|
||||
setAiResponseRouteDescription(getRouteDescription(resolvedRoute));
|
||||
})
|
||||
.finally(() => {
|
||||
setIsQueryLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
const routeDescriptions: { [key: string]: string } = {
|
||||
"/": "Go home",
|
||||
"/springboard": "Go to the Dashboard",
|
||||
"/manage-account": "Manage your account",
|
||||
"/events": "Browse events",
|
||||
"/karang-guni-schedules": "Browse available Karang Guni",
|
||||
"/home-bill-contest": "Take part in the home bill contest",
|
||||
"/home-bill-contest/new-submission": "Submit your bill",
|
||||
"/community-posts": "Browse community posts",
|
||||
"/community-posts/create": "Create a post",
|
||||
};
|
||||
|
||||
const getRouteDescription = (route: string) => {
|
||||
return routeDescriptions[route] || "Unknown";
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
window.addEventListener("keydown", handleKeyPress);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("keydown", handleKeyPress);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="-mb-1">
|
||||
<Button
|
||||
size="sm"
|
||||
className="p-0"
|
||||
variant="light"
|
||||
isDisabled={isAiDialogOpen}
|
||||
onPress={() => {
|
||||
onAiDialogOpen();
|
||||
}}
|
||||
>
|
||||
<Input
|
||||
size="sm"
|
||||
disabled
|
||||
className="w-44 h-min"
|
||||
startContent={<MagnifyingGlassIcon />}
|
||||
endContent={
|
||||
<div className="-mr-1">
|
||||
<Kbd keys={["command"]}>S</Kbd>
|
||||
</div>
|
||||
}
|
||||
placeholder="Search..."
|
||||
/>
|
||||
</Button>
|
||||
<Modal
|
||||
isOpen={isAiDialogOpen}
|
||||
onOpenChange={dialogOpenChange}
|
||||
closeButton={<></>}
|
||||
placement="top"
|
||||
>
|
||||
<ModalContent>
|
||||
{(onClose) => {
|
||||
return (
|
||||
<>
|
||||
<ModalBody>
|
||||
<div className="py-4 flex flex-col gap-4">
|
||||
<Input
|
||||
placeholder="What would you like to do?"
|
||||
size="lg"
|
||||
value={searchQuery}
|
||||
onValueChange={setSearchQuery}
|
||||
isDisabled={isQueryLoading}
|
||||
onKeyDown={(keyEvent) => {
|
||||
if (keyEvent.key == "Enter") {
|
||||
executeSearch();
|
||||
}
|
||||
}}
|
||||
endContent={
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="flat"
|
||||
onPress={executeSearch}
|
||||
isLoading={isQueryLoading}
|
||||
isDisabled={searchQuery.length <= 0}
|
||||
className="-mr-2"
|
||||
>
|
||||
<MagnifyingGlassIcon />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
{aiResponseRoute.length > 0 && (
|
||||
<Card className="p-4">
|
||||
<div className="flex flex-row justify-between *:my-auto">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xl font-bold">
|
||||
{aiResponseRouteDescription}
|
||||
</p>
|
||||
<p className="text-sm opacity-50">
|
||||
https://ecoconnect.gov{aiResponseRoute}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
isIconOnly
|
||||
variant="light"
|
||||
className="text-red-500"
|
||||
onPress={() => {
|
||||
onClose();
|
||||
navigate(aiResponseRoute);
|
||||
}}
|
||||
>
|
||||
<ArrowTopRightOnSquare />
|
||||
</Button>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</ModalBody>
|
||||
<ModalFooter className="bg-red-50 dark:bg-red-950 w-full h-full">
|
||||
<div className="w-full h-full flex flex-row justify-between *:my-auto">
|
||||
<EcoconnectFullLogo />
|
||||
<p className="text-lg text-red-900 dark:text-red-100">
|
||||
Natural Language Search™
|
||||
</p>
|
||||
</div>
|
||||
</ModalFooter>
|
||||
</>
|
||||
);
|
||||
}}
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
interface InsertImageProps {
|
||||
onImageSelected: (file: File) => void;
|
||||
onImageSelected: (file: File | null) => void;
|
||||
}
|
||||
|
||||
const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
|
||||
@@ -10,22 +10,31 @@ const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
|
||||
|
||||
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const selectedFiles = event.target.files as FileList;
|
||||
const file = selectedFiles?.[0];
|
||||
if (file) {
|
||||
setSelectedFile(file);
|
||||
setPreviewImage(URL.createObjectURL(file));
|
||||
onImageSelected(file);
|
||||
}
|
||||
const file = selectedFiles?.[0] || null;
|
||||
setSelectedFile(file);
|
||||
setPreviewImage(file ? URL.createObjectURL(file) : '');
|
||||
onImageSelected(file);
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input type="file" onChange={handleImageSelect} />
|
||||
<div
|
||||
className="flex flex-col items-center p-5 bg-white dark:bg-zinc-800 rounded-md"
|
||||
style={{ width: 350, height: 500 }}
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
onChange={handleImageSelect}
|
||||
className="mb-4"
|
||||
/>
|
||||
{selectedFile && (
|
||||
<img src={previewImage} alt="Selected Image" />
|
||||
<img
|
||||
src={previewImage}
|
||||
alt="Selected Image"
|
||||
className="w-full h-full object-cover rounded-md"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default InsertImage;
|
||||
export default InsertImage;
|
||||
|
||||
@@ -19,6 +19,7 @@ import { retrieveUserInformation } from "../security/users";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import EcoconnectFullLogo from "./EcoconnectFullLogo";
|
||||
import EcoconnectSearch from "./EcoconnectSearch";
|
||||
|
||||
export default function NavigationBar() {
|
||||
const [userProfileImageURL, setUserProfileImageURL] = useState("");
|
||||
@@ -58,7 +59,7 @@ export default function NavigationBar() {
|
||||
}
|
||||
>
|
||||
<div className="relative bg-primary-50 dark:bg-primary-800 border-2 border-primary-100 dark:border-primary-950 shadow-lg w-full h-full rounded-xl flex flex-col justify-center p-1">
|
||||
<div className=" w-full flex flex-row justify-between gap-4">
|
||||
<div className="w-full flex flex-row justify-between gap-4">
|
||||
<div className="flex flex-row gap-0 my-auto *:my-auto">
|
||||
<Button
|
||||
variant="light"
|
||||
@@ -109,60 +110,65 @@ export default function NavigationBar() {
|
||||
</div>
|
||||
</div>
|
||||
{userInformation && (
|
||||
<div className="my-auto pr-1">
|
||||
<Dropdown placement="bottom" backdrop="blur">
|
||||
<DropdownTrigger>
|
||||
<Avatar src={userProfileImageURL} as="button" size="sm" />
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions">
|
||||
<DropdownSection showDivider>
|
||||
<DropdownItem key="account-overview" isReadOnly>
|
||||
<div className="flex flex-col gap-2 text-center *:mx-auto p-2">
|
||||
<Avatar
|
||||
src={userProfileImageURL}
|
||||
as="button"
|
||||
size="lg"
|
||||
isBordered
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p>Signed in as</p>
|
||||
<p className="text-lg font-bold">
|
||||
<span>{userInformation.firstName}</span>{" "}
|
||||
<span>{userInformation.lastName}</span>
|
||||
</p>
|
||||
<p className="opacity-50">{userInformation.email}</p>
|
||||
<div className="my-auto pr-1 flex flex-row justify-end">
|
||||
<div className="flex flex-row gap-2 w-min">
|
||||
<EcoconnectSearch />
|
||||
<Dropdown placement="bottom" backdrop="blur">
|
||||
<DropdownTrigger>
|
||||
<Avatar src={userProfileImageURL} as="button" size="sm" />
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu aria-label="Profile Actions">
|
||||
<DropdownSection showDivider>
|
||||
<DropdownItem key="account-overview" isReadOnly>
|
||||
<div className="flex flex-col gap-2 text-center *:mx-auto p-2 w-full">
|
||||
<Avatar
|
||||
src={userProfileImageURL}
|
||||
as="button"
|
||||
size="lg"
|
||||
isBordered
|
||||
/>
|
||||
<div className="flex flex-col">
|
||||
<p>Signed in as</p>
|
||||
<p className="text-lg font-bold">
|
||||
<span>{userInformation.firstName}</span>{" "}
|
||||
<span>{userInformation.lastName}</span>
|
||||
</p>
|
||||
<p className="opacity-50">
|
||||
{userInformation.email}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DropdownItem>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="dashboard"
|
||||
title="Dashboard"
|
||||
startContent={<RocketLaunchIcon />}
|
||||
onPress={() => {
|
||||
navigate("/springboard");
|
||||
}}
|
||||
/>
|
||||
<DropdownItem
|
||||
key="manage-account"
|
||||
title="Manage your account"
|
||||
startContent={<PencilSquareIcon />}
|
||||
onPress={() => {
|
||||
navigate("/manage-account");
|
||||
}}
|
||||
/>
|
||||
</DropdownSection>
|
||||
<DropdownItem
|
||||
key="dashboard"
|
||||
title="Dashboard"
|
||||
startContent={<RocketLaunchIcon />}
|
||||
key="signout"
|
||||
startContent={<ArrowRightStartOnRectangleIcon />}
|
||||
color="danger"
|
||||
title="Sign out"
|
||||
onPress={() => {
|
||||
navigate("/springboard");
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
<DropdownItem
|
||||
key="manage-account"
|
||||
title="Manage your account"
|
||||
startContent={<PencilSquareIcon />}
|
||||
onPress={() => {
|
||||
navigate("/manage-account");
|
||||
}}
|
||||
/>
|
||||
</DropdownSection>
|
||||
<DropdownItem
|
||||
key="signout"
|
||||
startContent={<ArrowRightStartOnRectangleIcon />}
|
||||
color="danger"
|
||||
title="Sign out"
|
||||
onPress={() => {
|
||||
localStorage.clear();
|
||||
window.location.reload();
|
||||
}}
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!userInformation && doneLoading && (
|
||||
|
||||
@@ -10,16 +10,28 @@ interface NextUIFormikInputProps {
|
||||
placeholder: string;
|
||||
labelPlacement?: "inside" | "outside";
|
||||
startContent?: JSX.Element;
|
||||
readOnly?: boolean;
|
||||
setFieldValue?: (field: string, value: any, shouldValidate?: boolean) => void;
|
||||
}
|
||||
|
||||
const NextUIFormikInput = ({
|
||||
label,
|
||||
startContent,
|
||||
readOnly = false,
|
||||
setFieldValue,
|
||||
...props
|
||||
}: NextUIFormikInputProps) => {
|
||||
const [field, meta] = useField(props.name);
|
||||
const [inputType, setInputType] = useState(props.type);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = e.target;
|
||||
field.onChange(e);
|
||||
if (setFieldValue) {
|
||||
setFieldValue(props.name, value);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Input
|
||||
{...field}
|
||||
@@ -43,6 +55,8 @@ const NextUIFormikInput = ({
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
readOnly={readOnly}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Button, Link } from "@nextui-org/react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import axios from "axios";
|
||||
import config from "../config";
|
||||
import NextUIFormikInput from "./NextUIFormikInput";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -74,13 +73,24 @@ export default function SignInModule() {
|
||||
placeholder="johndoe@email.com"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder=" "
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<div className="flex flex-col gap-2 w-full">
|
||||
<NextUIFormikInput
|
||||
label="Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder=" "
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<Link
|
||||
className="hover:cursor-pointer w-max"
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
navigate("/forgot-password");
|
||||
}}
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
|
||||
38
client/src/components/SiteFooter.tsx
Normal file
38
client/src/components/SiteFooter.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Button, Link } from "@nextui-org/react";
|
||||
|
||||
export default function SiteFooter() {
|
||||
return (
|
||||
<div className="bg-black text-white p-8">
|
||||
<div className="flex flex-col text-center *:mx-auto gap-16">
|
||||
<div className="flex flex-col gap-4 *:mx-auto">
|
||||
<p className="text-2xl font-bold">Have a question?</p>
|
||||
<Button color="primary" variant="solid" className="px-24" size="lg">
|
||||
Get in touch with us
|
||||
</Button>
|
||||
</div>
|
||||
<div className="w-full flex flex-col gap-8">
|
||||
<div className="flex flex-row justify-between w-full">
|
||||
<p className="font-bold">Powered by DIT2303</p>
|
||||
<div className="flex flex-row gap-2 *:underline *:text-white">
|
||||
<Link>Terms of use</Link>
|
||||
<Link>Privacy statement</Link>
|
||||
<Link>Report vulnerability</Link>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8 *:mx-auto">
|
||||
<p className="text-xl">Connecting the neighbourhood together</p>
|
||||
<div className="flex flex-col gap-6 *:mx-auto">
|
||||
<img
|
||||
src="../assets/gov-footer.png"
|
||||
alt="Gov Tech footer logo"
|
||||
className=" w-80"
|
||||
/>
|
||||
<p className="font-bold">©2024 STUDENTS OF NANYANG POLYTECHNIC</p>
|
||||
<p>Last updated on 23 July 1921</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -520,3 +520,22 @@ export const LifebuoyIcon = () => {
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ArrowTopRightOnSquare = () => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
strokeWidth={1.5}
|
||||
stroke="currentColor"
|
||||
className="size-6"
|
||||
>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
d="M13.5 6H5.25A2.25 2.25 0 0 0 3 8.25v10.5A2.25 2.25 0 0 0 5.25 21h10.5A2.25 2.25 0 0 0 18 18.75V10.5m-10.5 6L21 3m0 0h-5.25M21 3v5.25"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,14 +2,18 @@ import { Toaster } from "react-hot-toast";
|
||||
import SingaporeAgencyStrip from "../components/SingaporeAgencyStrip";
|
||||
import NavigationBar from "../components/NavigationBar";
|
||||
import { Outlet } from "react-router-dom";
|
||||
import SiteFooter from "../components/SiteFooter";
|
||||
|
||||
export default function DefaultLayout() {
|
||||
return (
|
||||
<div className="relative flex flex-col h-screen">
|
||||
<SingaporeAgencyStrip />
|
||||
<main className="pt-16 flex-grow">
|
||||
<Outlet />
|
||||
</main>
|
||||
<div className="relative flex flex-col justify-between h-screen">
|
||||
<div className="flex flex-col h-screen">
|
||||
<SingaporeAgencyStrip />
|
||||
<main className="pt-16 flex-grow">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
<SiteFooter />
|
||||
<Toaster />
|
||||
<NavigationBar />
|
||||
|
||||
|
||||
34
client/src/layouts/restricted.tsx
Normal file
34
client/src/layouts/restricted.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Outlet, useNavigate } from "react-router-dom";
|
||||
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
|
||||
import { Button, Card } from "@nextui-org/react";
|
||||
|
||||
export default function RestrictedLayout() {
|
||||
const navigate = useNavigate();
|
||||
return (
|
||||
<div className="absolute inset-0 p-8 w-full h-full flex flex-col justify-center">
|
||||
<div className="flex flex-row justify-center">
|
||||
<div className="flex flex-col gap-8 *:mx-auto">
|
||||
<Card className="max-w-[800px] w-full mx-auto p-8">
|
||||
<Outlet />
|
||||
</Card>
|
||||
<div className="flex flex-row gap-2 *:my-auto">
|
||||
<Button
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
navigate("/");
|
||||
}}
|
||||
>
|
||||
<EcoconnectFullLogo />
|
||||
</Button>
|
||||
<div className="flex flex-row gap-6">
|
||||
<p>·</p>
|
||||
<p className="opacity-50">
|
||||
© Copyright {new Date().getFullYear()}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
ModalFooter,
|
||||
useDisclosure,
|
||||
Spinner,
|
||||
User,
|
||||
} from "@nextui-org/react";
|
||||
import config from "../config";
|
||||
import instance from "../security/http";
|
||||
@@ -28,7 +29,8 @@ import {
|
||||
XMarkIcon,
|
||||
} from "../icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
// import { retrieveUserInformation } from "../security/users";
|
||||
import { retrieveUserInformationById } from "../security/usersbyid";
|
||||
import { number } from "yup";
|
||||
// import UserPostImage from "../components/UserPostImage";
|
||||
|
||||
interface Post {
|
||||
@@ -37,17 +39,29 @@ interface Post {
|
||||
content: string;
|
||||
tags: string;
|
||||
id: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
export default function CommunityPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
|
||||
const [communityList, setCommunityList] = useState<Post[]>([]);
|
||||
const [search, setSearch] = useState(""); // Search Function
|
||||
const [userInformation, setUserInformation] = useState<Record<number, User>>({});
|
||||
|
||||
let accessToken = localStorage.getItem("accessToken");
|
||||
if (!accessToken) {
|
||||
return (
|
||||
setTimeout(() => {
|
||||
navigate("/signin");
|
||||
}, 1000)
|
||||
}, 1000)
|
||||
&&
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="text-center">
|
||||
@@ -59,29 +73,40 @@ export default function CommunityPage() {
|
||||
);
|
||||
}
|
||||
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
|
||||
|
||||
// const [userInformation, setUserInformation] = useState(null);
|
||||
|
||||
// communityList is a state variable
|
||||
// function setCommunityList is the setter function for the state variable
|
||||
// e initial value of the state variable is an empty array []
|
||||
// After getting the api response, call setCommunityList() to set the value of CommunityList
|
||||
const [communityList, setCommunityList] = useState<Post[]>([]);
|
||||
|
||||
// Search Function
|
||||
const [search, setSearch] = useState("");
|
||||
const onSearchChange = (e: { target: { value: SetStateAction<string> } }) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
const getPosts = () => {
|
||||
instance.get(config.serverAddress + "/post").then((res) => {
|
||||
setCommunityList(res.data);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPosts();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUserInformation = async (userId: number) => {
|
||||
try {
|
||||
const user = await retrieveUserInformationById(userId);
|
||||
setUserInformation((prevMap) => ({
|
||||
...prevMap,
|
||||
[userId]: user,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
communityList.forEach((post) => {
|
||||
if (!userInformation[post.userId]) {
|
||||
fetchUserInformation(post.userId);
|
||||
}
|
||||
});
|
||||
}, [communityList]);
|
||||
|
||||
const onSearchChange = (e: { target: { value: SetStateAction<string> } }) => {
|
||||
setSearch(e.target.value);
|
||||
};
|
||||
|
||||
const searchPosts = () => {
|
||||
instance
|
||||
.get(config.serverAddress + `/post?search=${search}`)
|
||||
@@ -90,10 +115,6 @@ export default function CommunityPage() {
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
getPosts();
|
||||
}, []);
|
||||
|
||||
const onSearchKeyDown = (e: { key: string }) => {
|
||||
if (e.key === "Enter") {
|
||||
searchPosts();
|
||||
@@ -108,13 +129,6 @@ export default function CommunityPage() {
|
||||
getPosts();
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
instance.get(config.serverAddress + "/post").then((res) => {
|
||||
console.log(res.data);
|
||||
setCommunityList(res.data);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleDeleteClick = (post: Post) => {
|
||||
setSelectedPost(post);
|
||||
onOpen();
|
||||
@@ -168,7 +182,7 @@ export default function CommunityPage() {
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xl font-bold">{post.title}</p>
|
||||
<p className="text-md text-neutral-500">Adam</p>
|
||||
<p className="text-md text-neutral-500">{userInformation[post.userId]?.firstName} {userInformation[post.userId]?.lastName}</p>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse justify-center items-center">
|
||||
<Dropdown>
|
||||
@@ -185,6 +199,7 @@ export default function CommunityPage() {
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="edit"
|
||||
textValue="Edit"
|
||||
onClick={() => {
|
||||
navigate(`edit/${post.id}`);
|
||||
}}
|
||||
@@ -193,6 +208,7 @@ export default function CommunityPage() {
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
textValue="Delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onClick={() => handleDeleteClick(post)}
|
||||
|
||||
@@ -7,6 +7,8 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||
import NextUIFormikTextarea from "../components/NextUIFormikTextarea";
|
||||
import config from "../config";
|
||||
import { ArrowUTurnLeftIcon } from "../icons";
|
||||
import { useEffect, useState } from "react";
|
||||
import { retrieveUserInformation } from "../security/users";
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
title: Yup.string()
|
||||
@@ -31,6 +33,7 @@ const validationSchema = Yup.object({
|
||||
|
||||
function CreatePostPage() {
|
||||
const navigate = useNavigate();
|
||||
const [userId, setUserId] = useState(null);
|
||||
|
||||
const initialValues = {
|
||||
title: "",
|
||||
@@ -38,12 +41,28 @@ function CreatePostPage() {
|
||||
tags: "",
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const getUserInformation = async () => {
|
||||
try {
|
||||
const user = await retrieveUserInformation(); // Get the user ID
|
||||
setUserId(user.id); // Set the user ID in the state
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
getUserInformation();
|
||||
}, []);
|
||||
|
||||
const handleSubmit = async (
|
||||
values: any,
|
||||
{ setSubmitting, resetForm, setFieldError }: any
|
||||
) => {
|
||||
try {
|
||||
const response = await axios.post(config.serverAddress + "/post", values); // Assuming an API route
|
||||
const postData = {
|
||||
...values,
|
||||
userId: userId
|
||||
}
|
||||
const response = await axios.post(config.serverAddress + "/post", postData); // Assuming an API route
|
||||
if (response.status === 200) {
|
||||
console.log("Post created successfully:", response.data);
|
||||
resetForm(); // Clear form after successful submit
|
||||
|
||||
109
client/src/pages/ForgotPasswordPage.tsx
Normal file
109
client/src/pages/ForgotPasswordPage.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import axios from "axios";
|
||||
import config from "../config";
|
||||
import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||
import { popErrorToast, popToast } from "../utilities";
|
||||
import { ArrowUTurnLeftIcon } from "../icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import instance from "../security/http";
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
email: Yup.string()
|
||||
.trim()
|
||||
.lowercase()
|
||||
.min(5)
|
||||
.max(69)
|
||||
.email("Invalid email format")
|
||||
.required("Email is required"),
|
||||
});
|
||||
|
||||
export default function ForgotPasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
const initialValues = {
|
||||
email: "",
|
||||
};
|
||||
|
||||
const handleSubmit = (values: any): void => {
|
||||
instance
|
||||
.put(
|
||||
`${
|
||||
config.serverAddress
|
||||
}/users/request-reset-password/${encodeURIComponent(values.email)}`
|
||||
)
|
||||
.then(() => {
|
||||
console.log("Email sent successfully");
|
||||
popToast("Email sent to your mailbox!", 1);
|
||||
setTimeout(() => {
|
||||
navigate("/signin");
|
||||
}, 1500);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Failed to send email:", error);
|
||||
popErrorToast("Failed to send email: " + error);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full flex flex-row gap-10">
|
||||
<div className="flex flex-col justify-center relative">
|
||||
<Button
|
||||
variant="light"
|
||||
isIconOnly
|
||||
radius="full"
|
||||
className="-mt-2 -ml-2 mr-auto absolute top-0 left-0"
|
||||
onPress={() => {
|
||||
navigate(-1);
|
||||
}}
|
||||
>
|
||||
<ArrowUTurnLeftIcon />
|
||||
</Button>
|
||||
<div className="relative">
|
||||
<img
|
||||
src="../assets/google-passkey.svg"
|
||||
alt="Google Passkey SVG"
|
||||
className="saturate-0"
|
||||
/>
|
||||
<div className="absolute inset-0 bg-primary-500 mix-blend-overlay w-full h-full"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-8">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-3xl font-bold">Password Recovery</p>
|
||||
<p>
|
||||
Enter your email address below, and we will send you a recovery
|
||||
mail.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={initialValues}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty }) => (
|
||||
<Form className="flex flex-col gap-8">
|
||||
<NextUIFormikInput
|
||||
label="Email"
|
||||
name="email"
|
||||
type="email"
|
||||
placeholder="johndoe@email.com"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="lg"
|
||||
isDisabled={!isValid || !dirty}
|
||||
>
|
||||
Send Recovery Email
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,64 +1,65 @@
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardBody,
|
||||
CardFooter,
|
||||
Divider,
|
||||
Button,
|
||||
} from "@nextui-org/react";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export default function HBContestPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleJoinClick = () => {
|
||||
let accessToken = localStorage.getItem("accessToken");
|
||||
if (!accessToken) {
|
||||
setTimeout(() => {
|
||||
navigate("/signin");
|
||||
}, 1000);
|
||||
} else {
|
||||
navigate("new-submission");
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<section>
|
||||
<Card className="max-w-[800px] bg-red-50 mx-auto">
|
||||
<CardHeader className="flex gap-3">
|
||||
<div className="flex flex-col">
|
||||
<h2 className="text-md">Home Bill Contest</h2>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<Divider />
|
||||
<CardBody>
|
||||
<p>
|
||||
<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>
|
||||
<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.
|
||||
Participants would be required to input and upload their bills
|
||||
into the form to ensure integrity and honesty.{" "}
|
||||
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>
|
||||
</CardBody>
|
||||
<Divider />
|
||||
<CardFooter>
|
||||
<div className="flex-col">
|
||||
<div>
|
||||
<h4>Winners</h4>
|
||||
<p>
|
||||
There will 3 winners for each month. Each winner will receive
|
||||
random food vouchers.
|
||||
</p>
|
||||
<p>1st: 3 vouchers</p>
|
||||
<p>2nd: 2 vouchers</p>
|
||||
<p>3rd: 1 voucher</p>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
className=" bg-red-500 dark:bg-red-700 text-white"
|
||||
size="lg"
|
||||
onPress={() => {
|
||||
navigate("new-submission");
|
||||
}}
|
||||
>
|
||||
<p className="font-bold">Join</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import { Button } from "@nextui-org/react";
|
||||
import { ArrowUTurnLeftIcon } from "../icons";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
@@ -8,29 +9,25 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||
import axios from "axios";
|
||||
import InsertImage from "../components/InsertImage";
|
||||
import { retrieveUserInformation } from "../security/users";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
const validationSchema = Yup.object({
|
||||
electricalBill: Yup.number()
|
||||
.typeError("Must be a number")
|
||||
.positive("Must be a positive value")
|
||||
.max(99999.99, "Value is too large")
|
||||
.required(),
|
||||
.required("Electrical bill is a required field"),
|
||||
waterBill: Yup.number()
|
||||
.typeError("Must be a number")
|
||||
.positive("Must be a positive value")
|
||||
.max(99999.99, "Value is too large")
|
||||
.required(),
|
||||
totalBill: Yup.number()
|
||||
.typeError("Must be a number")
|
||||
.positive("Must be a positive value")
|
||||
.max(99999.99, "Value is too large")
|
||||
.required(),
|
||||
.required("Water bill is a required field"),
|
||||
noOfDependents: Yup.number()
|
||||
.typeError("Must be a number")
|
||||
.integer("Must be a whole number")
|
||||
.positive("Must be a positive value")
|
||||
.required(),
|
||||
.required("No. of dependents is a required field"),
|
||||
ebPicture: Yup.mixed().required("Electrical bill picture is required"),
|
||||
wbPicture: Yup.mixed().required("Water bill picture is required"),
|
||||
});
|
||||
|
||||
export default function HBFormPage() {
|
||||
@@ -41,11 +38,18 @@ export default function HBFormPage() {
|
||||
waterBill: "",
|
||||
totalBill: "",
|
||||
noOfDependents: "",
|
||||
avgBill: "",
|
||||
ebPicture: null,
|
||||
wbPicture: null,
|
||||
userId: "",
|
||||
});
|
||||
|
||||
// Add state for image selection
|
||||
const [imagesSelected, setImagesSelected] = useState({
|
||||
ebPicture: false,
|
||||
wbPicture: false,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const getUserInformation = async () => {
|
||||
try {
|
||||
@@ -78,6 +82,7 @@ export default function HBFormPage() {
|
||||
formData.append("waterBill", values.waterBill);
|
||||
formData.append("totalBill", values.totalBill);
|
||||
formData.append("noOfDependents", values.noOfDependents);
|
||||
formData.append("avgBill", values.avgBill);
|
||||
|
||||
if (values.ebPicture) {
|
||||
formData.append("ebPicture", values.ebPicture);
|
||||
@@ -124,78 +129,117 @@ export default function HBFormPage() {
|
||||
}
|
||||
};
|
||||
|
||||
// Handler for image selection
|
||||
const handleImageSelection = (name: string, file: File | null) => {
|
||||
setImagesSelected(prevState => ({
|
||||
...prevState,
|
||||
[name]: !!file,
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="w-full h-full">
|
||||
<section className="w-7/12 mx-auto">
|
||||
<section className="w-8/12 mx-auto">
|
||||
<Button variant="light" onPress={() => navigate(-1)}>
|
||||
<ArrowUTurnLeftIcon />
|
||||
</Button>
|
||||
</section>
|
||||
<section className="w-7/12 mx-auto p-5 bg-red-100 border border-none rounded-2xl h-600px">
|
||||
<section className="w-8/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}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty, isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-row gap-10">
|
||||
<div className="flex flex-col gap-5">
|
||||
<NextUIFormikInput
|
||||
label="Electrical Bill"
|
||||
name="electricalBill"
|
||||
type="text"
|
||||
placeholder="$"
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Water Bill"
|
||||
name="waterBill"
|
||||
type="text"
|
||||
placeholder="$"
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Total Bill"
|
||||
name="totalBill"
|
||||
type="text"
|
||||
placeholder="$"
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Number of dependents"
|
||||
name="noOfDependents"
|
||||
type="text"
|
||||
placeholder="0"
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
{({ isValid, dirty, isSubmitting, setFieldValue, values }) => {
|
||||
// Calculate the total bill
|
||||
useEffect(() => {
|
||||
const totalBill = Number(values.electricalBill) + Number(values.waterBill);
|
||||
setFieldValue("totalBill", totalBill.toFixed(2));
|
||||
|
||||
const avgBill = Number(values.noOfDependents) > 0
|
||||
? totalBill / Number(values.noOfDependents)
|
||||
: 0;
|
||||
setFieldValue("avgBill", avgBill.toFixed(2));
|
||||
|
||||
}, [values.electricalBill, values.waterBill, values.noOfDependents, setFieldValue]);
|
||||
|
||||
// Disabled the submit button because the images field are not selected
|
||||
const isSubmitDisabled = !imagesSelected.ebPicture || !imagesSelected.wbPicture;
|
||||
|
||||
return (
|
||||
<Form>
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex flex-row gap-10">
|
||||
<div className="flex flex-col gap-10">
|
||||
<NextUIFormikInput
|
||||
label="Electrical Bill"
|
||||
name="electricalBill"
|
||||
type="text"
|
||||
placeholder="0.00"
|
||||
labelPlacement="inside"
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Water Bill"
|
||||
name="waterBill"
|
||||
type="text"
|
||||
placeholder="0.00"
|
||||
labelPlacement="inside"
|
||||
setFieldValue={setFieldValue}
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Total Bill"
|
||||
name="totalBill"
|
||||
type="text"
|
||||
placeholder="0.00"
|
||||
labelPlacement="inside"
|
||||
readOnly={true}
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Number of dependents"
|
||||
name="noOfDependents"
|
||||
type="text"
|
||||
placeholder="0"
|
||||
labelPlacement="inside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Average Bill"
|
||||
name="avgBill"
|
||||
type="text"
|
||||
placeholder="0"
|
||||
labelPlacement="inside"
|
||||
readOnly={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row max-w-xs h-[500px] gap-10">
|
||||
<InsertImage
|
||||
onImageSelected={(file) => {
|
||||
setFieldValue("ebPicture", file);
|
||||
handleImageSelection("ebPicture", file);
|
||||
}}
|
||||
/>
|
||||
<InsertImage
|
||||
onImageSelected={(file) => {
|
||||
setFieldValue("wbPicture", file);
|
||||
handleImageSelection("wbPicture", file);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-row gap-8 max-w-xs h-[500px]">
|
||||
<InsertImage
|
||||
onImageSelected={(file) => {
|
||||
setFieldValue("ebPicture", file);
|
||||
}}
|
||||
/>
|
||||
<InsertImage
|
||||
onImageSelected={(file) => {
|
||||
setFieldValue("wbPicture", file);
|
||||
}}
|
||||
/>
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-red-400 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-900 text-white"
|
||||
size="lg"
|
||||
isDisabled={!isValid || !dirty || isSubmitting || isSubmitDisabled}
|
||||
>
|
||||
<p>Submit</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
className="bg-red-500 dark:bg-red-700 text-white"
|
||||
isDisabled={!isValid || !dirty || isSubmitting}
|
||||
>
|
||||
<p>Submit</p>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Form>
|
||||
);
|
||||
}}
|
||||
</Formik>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -24,6 +24,7 @@ import {
|
||||
HandThumbsUpIcon,
|
||||
ArrowUTurnLeftIcon,
|
||||
} from "../icons";
|
||||
import { retrieveUserInformationById } from "../security/usersbyid";
|
||||
|
||||
interface Post {
|
||||
title: string;
|
||||
@@ -31,23 +32,49 @@ interface Post {
|
||||
content: string;
|
||||
tags: string;
|
||||
id: number;
|
||||
userId: number;
|
||||
}
|
||||
|
||||
type User = {
|
||||
id: number;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
};
|
||||
|
||||
const PostPage: React.FC = () => {
|
||||
const navigate = useNavigate();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [post, setPost] = useState<Post | null>(null);
|
||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
|
||||
const [userInformation, setUserInformation] = useState<Record<number, User>>({});
|
||||
|
||||
useEffect(() => {
|
||||
if (id) {
|
||||
instance.get(`${config.serverAddress}/post/${id}`).then((res) => {
|
||||
setPost(res.data);
|
||||
});
|
||||
instance.get(`${config.serverAddress}/post/${id}`)
|
||||
.then((res) => setPost(res.data))
|
||||
.catch((error) => console.error("Error fetching post:", error));
|
||||
}
|
||||
}, [id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (post) {
|
||||
const fetchUserInformation = async () => {
|
||||
try {
|
||||
const user = await retrieveUserInformationById(post.userId);
|
||||
setUserInformation((prevMap) => ({
|
||||
...prevMap,
|
||||
[post.userId]: user,
|
||||
}));
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
};
|
||||
|
||||
fetchUserInformation();
|
||||
}
|
||||
}, [post]);
|
||||
|
||||
if (!post) {
|
||||
return (
|
||||
<div className="flex justify-center min-h-screen">
|
||||
@@ -104,7 +131,7 @@ const PostPage: React.FC = () => {
|
||||
<div className="flex flex-row justify-between">
|
||||
<div className="flex flex-col">
|
||||
<p className="text-xl font-bold">{post.title}</p>
|
||||
<p className="text-md text-neutral-500">Adam</p>
|
||||
<p className="text-md text-neutral-500">{userInformation[post.userId]?.firstName} {userInformation[post.userId]?.lastName}</p>
|
||||
</div>
|
||||
<div className="flex flex-row-reverse justify-center items-center">
|
||||
<Dropdown>
|
||||
@@ -116,6 +143,7 @@ const PostPage: React.FC = () => {
|
||||
<DropdownMenu aria-label="Static Actions">
|
||||
<DropdownItem
|
||||
key="edit"
|
||||
textValue="Edit"
|
||||
onClick={() => {
|
||||
navigate(`edit/${post.id}`);
|
||||
}}
|
||||
@@ -124,6 +152,7 @@ const PostPage: React.FC = () => {
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
key="delete"
|
||||
textValue="Delete"
|
||||
className="text-danger"
|
||||
color="danger"
|
||||
onClick={() => handleDeleteClick(post)}
|
||||
|
||||
@@ -2,8 +2,7 @@ import { useParams, useNavigate } from "react-router-dom";
|
||||
import instance from "../security/http";
|
||||
import config from "../config";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Button, Card, CircularProgress } from "@nextui-org/react";
|
||||
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
|
||||
import { Button } from "@nextui-org/react";
|
||||
import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
@@ -29,7 +28,6 @@ export default function ResetPasswordPage() {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const [validationResult, setValidationResult] = useState<boolean>(false);
|
||||
const [pageLoading, setPageLoading] = useState<boolean>(true);
|
||||
|
||||
const validateToken = () => {
|
||||
instance
|
||||
@@ -39,9 +37,6 @@ export default function ResetPasswordPage() {
|
||||
})
|
||||
.catch(() => {
|
||||
setValidationResult(false);
|
||||
})
|
||||
.finally(() => {
|
||||
setPageLoading(false);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -66,77 +61,55 @@ export default function ResetPasswordPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="absolute inset-0 p-8 w-full h-full flex flex-col justify-center">
|
||||
<div className="flex flex-row justify-center">
|
||||
{pageLoading && (
|
||||
<div>
|
||||
<CircularProgress label="Loading..." />
|
||||
<div>
|
||||
{validationResult && (
|
||||
<div className="flex flex-col gap-8 p-12 text-left">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-3xl font-bold">Password Reset</p>
|
||||
<p>Enter a new password below.</p>
|
||||
</div>
|
||||
)}
|
||||
{!pageLoading && (
|
||||
<div className="flex flex-col gap-8 *:mx-auto">
|
||||
<Card className="max-w-[600px] w-full mx-auto">
|
||||
{validationResult && (
|
||||
<div className="flex flex-col gap-8 p-12 text-left">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-3xl font-bold">Password Reset</p>
|
||||
<p>Enter a new password below.</p>
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={{ password: "", confirmPassword: "" }}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty }) => (
|
||||
<Form className="flex flex-col gap-8">
|
||||
<NextUIFormikInput
|
||||
label="New Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="lg"
|
||||
isDisabled={!isValid || !dirty}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)}
|
||||
{!validationResult && (
|
||||
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-3xl font-bold">
|
||||
Reset portal has been closed.
|
||||
</p>
|
||||
<p>Please request for a password reset again.</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
<div className="flex flex-row gap-4">
|
||||
<EcoconnectFullLogo />
|
||||
<p>·</p>
|
||||
<p className="opacity-50">
|
||||
© Copyright {new Date().getFullYear()}. All rights reserved.
|
||||
</p>
|
||||
</div>
|
||||
<Formik
|
||||
initialValues={{ password: "", confirmPassword: "" }}
|
||||
validationSchema={validationSchema}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{({ isValid, dirty }) => (
|
||||
<Form className="flex flex-col gap-8">
|
||||
<NextUIFormikInput
|
||||
label="New Password"
|
||||
name="password"
|
||||
type="password"
|
||||
placeholder="Enter new password"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<NextUIFormikInput
|
||||
label="Confirm Password"
|
||||
name="confirmPassword"
|
||||
type="password"
|
||||
placeholder="Confirm new password"
|
||||
labelPlacement="outside"
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
color="primary"
|
||||
size="lg"
|
||||
isDisabled={!isValid || !dirty}
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
)}
|
||||
{!validationResult && (
|
||||
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
|
||||
<div className="flex flex-col gap-2">
|
||||
<p className="text-3xl font-bold">Reset portal has been closed.</p>
|
||||
<p>Please request for a password reset again.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
17
client/src/security/usersbyid.ts
Normal file
17
client/src/security/usersbyid.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { AxiosError } from "axios";
|
||||
import config from "../config";
|
||||
import instance from "./http";
|
||||
|
||||
export async function retrieveUserInformationById(userId: number) {
|
||||
if (!localStorage.getItem("accessToken")) {
|
||||
throw "No access token";
|
||||
}
|
||||
try {
|
||||
let userInformation = await instance.get(
|
||||
`${config.serverAddress}/users/individual/${userId}`
|
||||
);
|
||||
return userInformation.data;
|
||||
} catch (error) {
|
||||
throw ((error as AxiosError).response?.data as any).message;
|
||||
}
|
||||
}
|
||||
18
server/connections/apiKey.js
Normal file
18
server/connections/apiKey.js
Normal file
@@ -0,0 +1,18 @@
|
||||
const axios = require("axios");
|
||||
|
||||
// Adam's personal API key server access
|
||||
// Requires connection to private tailscale subnet.
|
||||
// no abusing of my api keys or i abuse you 🔫
|
||||
async function getApiKey(serviceUrl) {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
"http://mommy.rya-orfe.ts.net:8069/" + serviceUrl
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error retrieving API key:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = { getApiKey };
|
||||
@@ -1,21 +1,10 @@
|
||||
const axios = require("axios");
|
||||
const senderEmail = "ecoconnect@trial-ynrw7gy0qxol2k8e.mlsender.net";
|
||||
|
||||
async function getApiKey() {
|
||||
try {
|
||||
const response = await axios.get(
|
||||
"http://mommy.rya-orfe.ts.net:8069/mailersend_api_key"
|
||||
);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
console.error("Error retrieving API key:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
const { getApiKey } = require("./apiKey");
|
||||
|
||||
async function sendEmail(recipientEmail, title, content) {
|
||||
try {
|
||||
const apiKey = await getApiKey();
|
||||
const apiKey = await getApiKey("mailersend_api_key");
|
||||
const response = await axios.post(
|
||||
"https://api.mailersend.com/v1/email",
|
||||
{
|
||||
|
||||
20
server/connections/openai.js
Normal file
20
server/connections/openai.js
Normal file
@@ -0,0 +1,20 @@
|
||||
const OpenAI = require("openai");
|
||||
const { getApiKey } = require("./apiKey");
|
||||
|
||||
async function openAiChatCompletion(query, systemPrompt) {
|
||||
const openai = new OpenAI({ apiKey: await getApiKey("openai_api_key") });
|
||||
const completion = await openai.chat.completions.create({
|
||||
messages: [
|
||||
{ role: "system", content: systemPrompt },
|
||||
{ role: "user", content: query },
|
||||
],
|
||||
model: "gpt-4o-mini",
|
||||
});
|
||||
|
||||
let response = completion.choices[0].message.content;
|
||||
console.log(response);
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
module.exports = { openAiChatCompletion };
|
||||
@@ -37,6 +37,9 @@ app.use("/schedule", schedulesRoute);
|
||||
const HBCformRoute = require("./routes/hbcform");
|
||||
app.use("/hbcform", HBCformRoute);
|
||||
|
||||
const connections = require("./routes/connections");
|
||||
app.use("/connections", connections);
|
||||
|
||||
db.sequelize
|
||||
.sync({ alter: true })
|
||||
.then(() => {
|
||||
|
||||
@@ -26,6 +26,10 @@ module.exports = (sequelize, DataTypes) => {
|
||||
type: DataTypes.INTEGER,
|
||||
allowNull: false
|
||||
},
|
||||
avgBill: {
|
||||
type: DataTypes.DECIMAL(7, 2),
|
||||
allowNull: false
|
||||
},
|
||||
ebPicture: {
|
||||
type: DataTypes.BLOB("long"),
|
||||
allowNull: true,
|
||||
|
||||
@@ -17,9 +17,14 @@ module.exports = (sequelize, DataTypes) => {
|
||||
content: {
|
||||
type: DataTypes.TEXT,
|
||||
allowNull: false
|
||||
}
|
||||
},
|
||||
userId: {
|
||||
type: DataTypes.UUID,
|
||||
allowNull: false,
|
||||
},
|
||||
}, {
|
||||
tableName: 'posts'
|
||||
});
|
||||
|
||||
return Post;
|
||||
}
|
||||
@@ -20,6 +20,7 @@
|
||||
"multer": "1.4.5-lts.1",
|
||||
"mysql2": "^3.10.1",
|
||||
"nodemon": "^3.1.3",
|
||||
"openai": "^4.53.2",
|
||||
"sequelize": "^6.37.3",
|
||||
"sharp": "^0.33.4",
|
||||
"uuid": "^10.0.0",
|
||||
|
||||
220
server/pnpm-lock.yaml
generated
220
server/pnpm-lock.yaml
generated
@@ -4,52 +4,52 @@ settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
argon2:
|
||||
specifier: ^0.40.3
|
||||
version: 0.40.3
|
||||
axios:
|
||||
specifier: ^1.7.2
|
||||
version: 1.7.2
|
||||
bad-words:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
express:
|
||||
specifier: ^4.19.2
|
||||
version: 4.19.2
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
multer:
|
||||
specifier: 1.4.5-lts.1
|
||||
version: 1.4.5-lts.1
|
||||
mysql2:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
nodemon:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
sequelize:
|
||||
specifier: ^6.37.3
|
||||
version: 6.37.3(mysql2@3.10.1)
|
||||
sharp:
|
||||
specifier: ^0.33.4
|
||||
version: 0.33.4
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
yup:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
dependencies:
|
||||
argon2:
|
||||
specifier: ^0.40.3
|
||||
version: 0.40.3
|
||||
axios:
|
||||
specifier: ^1.7.2
|
||||
version: 1.7.2
|
||||
bad-words:
|
||||
specifier: ^3.0.4
|
||||
version: 3.0.4
|
||||
cors:
|
||||
specifier: ^2.8.5
|
||||
version: 2.8.5
|
||||
dotenv:
|
||||
specifier: ^16.4.5
|
||||
version: 16.4.5
|
||||
express:
|
||||
specifier: ^4.19.2
|
||||
version: 4.19.2
|
||||
jsonwebtoken:
|
||||
specifier: ^9.0.2
|
||||
version: 9.0.2
|
||||
multer:
|
||||
specifier: 1.4.5-lts.1
|
||||
version: 1.4.5-lts.1
|
||||
mysql2:
|
||||
specifier: ^3.10.1
|
||||
version: 3.10.1
|
||||
nodemon:
|
||||
specifier: ^3.1.3
|
||||
version: 3.1.3
|
||||
openai:
|
||||
specifier: ^4.53.2
|
||||
version: 4.53.2
|
||||
sequelize:
|
||||
specifier: ^6.37.3
|
||||
version: 6.37.3(mysql2@3.10.1)
|
||||
sharp:
|
||||
specifier: ^0.33.4
|
||||
version: 0.33.4
|
||||
uuid:
|
||||
specifier: ^10.0.0
|
||||
version: 10.0.0
|
||||
yup:
|
||||
specifier: ^1.4.0
|
||||
version: 1.4.0
|
||||
|
||||
packages:
|
||||
|
||||
@@ -179,17 +179,44 @@ packages:
|
||||
'@types/ms@0.7.34':
|
||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
||||
|
||||
'@types/node@20.14.6':
|
||||
/@types/node-fetch@2.6.11:
|
||||
resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==}
|
||||
dependencies:
|
||||
'@types/node': 20.14.6
|
||||
form-data: 4.0.0
|
||||
dev: false
|
||||
|
||||
/@types/node@18.19.42:
|
||||
resolution: {integrity: sha512-d2ZFc/3lnK2YCYhos8iaNIYu9Vfhr92nHiyJHRltXWjXUBjEE+A4I58Tdbnw4VhggSW+2j5y5gTrLs4biNnubg==}
|
||||
dependencies:
|
||||
undici-types: 5.26.5
|
||||
dev: false
|
||||
|
||||
/@types/node@20.14.6:
|
||||
resolution: {integrity: sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==}
|
||||
|
||||
'@types/validator@13.12.0':
|
||||
resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==}
|
||||
|
||||
accepts@1.3.8:
|
||||
/abort-controller@3.0.0:
|
||||
resolution: {integrity: sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==}
|
||||
engines: {node: '>=6.5'}
|
||||
dependencies:
|
||||
event-target-shim: 5.0.1
|
||||
dev: false
|
||||
|
||||
/accepts@1.3.8:
|
||||
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
anymatch@3.1.3:
|
||||
/agentkeepalive@4.5.0:
|
||||
resolution: {integrity: sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==}
|
||||
engines: {node: '>= 8.0.0'}
|
||||
dependencies:
|
||||
humanize-ms: 1.2.1
|
||||
dev: false
|
||||
|
||||
/anymatch@3.1.3:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
@@ -376,7 +403,12 @@ packages:
|
||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
express@4.19.2:
|
||||
/event-target-shim@5.0.1:
|
||||
resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==}
|
||||
engines: {node: '>=6'}
|
||||
dev: false
|
||||
|
||||
/express@4.19.2:
|
||||
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
|
||||
engines: {node: '>= 0.10.0'}
|
||||
|
||||
@@ -397,11 +429,23 @@ packages:
|
||||
debug:
|
||||
optional: true
|
||||
|
||||
form-data@4.0.0:
|
||||
/form-data-encoder@1.7.2:
|
||||
resolution: {integrity: sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==}
|
||||
dev: false
|
||||
|
||||
/form-data@4.0.0:
|
||||
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
forwarded@0.2.0:
|
||||
/formdata-node@4.4.1:
|
||||
resolution: {integrity: sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==}
|
||||
engines: {node: '>= 12.20'}
|
||||
dependencies:
|
||||
node-domexception: 1.0.0
|
||||
web-streams-polyfill: 4.0.0-beta.3
|
||||
dev: false
|
||||
|
||||
/forwarded@0.2.0:
|
||||
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
@@ -454,7 +498,13 @@ packages:
|
||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
iconv-lite@0.4.24:
|
||||
/humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
dev: false
|
||||
|
||||
/iconv-lite@0.4.24:
|
||||
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
@@ -615,7 +665,24 @@ packages:
|
||||
resolution: {integrity: sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==}
|
||||
engines: {node: ^18 || ^20 || >= 21}
|
||||
|
||||
node-gyp-build@4.8.1:
|
||||
/node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
dev: false
|
||||
|
||||
/node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
dev: false
|
||||
|
||||
/node-gyp-build@4.8.1:
|
||||
resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
|
||||
hasBin: true
|
||||
|
||||
@@ -639,7 +706,22 @@ packages:
|
||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
parseurl@1.3.3:
|
||||
/openai@4.53.2:
|
||||
resolution: {integrity: sha512-ohYEv6OV3jsFGqNrgolDDWN6Ssx1nFg6JDJQuaBFo4SL2i+MBoOQ16n2Pq1iBF5lH1PKnfCIOfqAGkmzPvdB9g==}
|
||||
hasBin: true
|
||||
dependencies:
|
||||
'@types/node': 18.19.42
|
||||
'@types/node-fetch': 2.6.11
|
||||
abort-controller: 3.0.0
|
||||
agentkeepalive: 4.5.0
|
||||
form-data-encoder: 1.7.2
|
||||
formdata-node: 4.4.1
|
||||
node-fetch: 2.7.0
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
dev: false
|
||||
|
||||
/parseurl@1.3.3:
|
||||
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||
engines: {node: '>= 0.8'}
|
||||
|
||||
@@ -1629,7 +1711,14 @@ snapshots:
|
||||
|
||||
touch@3.1.1: {}
|
||||
|
||||
tslib@2.6.3:
|
||||
/tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
dev: false
|
||||
|
||||
/tslib@2.6.3:
|
||||
resolution: {integrity: sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==}
|
||||
requiresBuild: true
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
type-fest@2.19.0: {}
|
||||
@@ -1659,7 +1748,24 @@ snapshots:
|
||||
|
||||
vary@1.1.2: {}
|
||||
|
||||
wkx@0.5.0:
|
||||
/web-streams-polyfill@4.0.0-beta.3:
|
||||
resolution: {integrity: sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==}
|
||||
engines: {node: '>= 14'}
|
||||
dev: false
|
||||
|
||||
/webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
dev: false
|
||||
|
||||
/whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
dev: false
|
||||
|
||||
/wkx@0.5.0:
|
||||
resolution: {integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==}
|
||||
dependencies:
|
||||
'@types/node': 20.14.6
|
||||
|
||||
|
||||
40
server/routes/connections.js
Normal file
40
server/routes/connections.js
Normal file
@@ -0,0 +1,40 @@
|
||||
const express = require("express");
|
||||
const { openAiChatCompletion } = require("../connections/openai");
|
||||
const { validateToken } = require("../middlewares/auth");
|
||||
const router = express.Router();
|
||||
|
||||
const nlsPrompt = `
|
||||
"/": home
|
||||
"/springboard": user dashboard
|
||||
"/manage-account": user account management
|
||||
"/events": events
|
||||
"/karang-guni-schedules": browse slots
|
||||
"/home-bill-contest": participate in contest & earn vouchers
|
||||
"/home-bill-contest/new-submission": submit bill
|
||||
"/community-posts": show posts
|
||||
"/community-posts/create": create post
|
||||
|
||||
based on input, provide appropriate route closest to fulfilling user's needs
|
||||
If none matches user query, return empty route.
|
||||
in following format:
|
||||
|
||||
{"route": "<appropriate route>"}
|
||||
`;
|
||||
|
||||
async function naturalLanguageSearch(userQuery) {
|
||||
return await openAiChatCompletion(userQuery, nlsPrompt);
|
||||
}
|
||||
|
||||
router.get("/nls/:query", validateToken, async (req, res) => {
|
||||
let data = req.params.query;
|
||||
console.log(data);
|
||||
try {
|
||||
let chatResponse = await naturalLanguageSearch(data);
|
||||
res.json({ response: chatResponse });
|
||||
} catch (error) {
|
||||
console.error("Error with AI:", error);
|
||||
res.status(500).json({ message: "Internal Server Error" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -23,6 +23,7 @@ router.post("/", upload.fields([
|
||||
waterBill: yup.number().positive().required(),
|
||||
totalBill: yup.number().positive().required(),
|
||||
noOfDependents: yup.number().integer().positive().required(),
|
||||
avgBill: yup.number().positive().required(),
|
||||
});
|
||||
try {
|
||||
data = await validationSchema.validate(data, { abortEarly: false });
|
||||
@@ -48,6 +49,7 @@ router.get("/", async (req, res) => {
|
||||
{ waterBill: { [Op.like]: `%${search}%` } },
|
||||
{ totalBill: { [Op.like]: `%${search}%` } },
|
||||
{ noOfDependents: { [Op.like]: `%${search}%` } },
|
||||
{ avgBill: { [Op.like]: `%${search}%` } },
|
||||
];
|
||||
}
|
||||
let list = await HBCform.findAll({
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { Post } = require('../models');
|
||||
const { Post, User } = require('../models');
|
||||
const { Op, where } = require("sequelize");
|
||||
const yup = require("yup");
|
||||
const multer = require("multer");
|
||||
@@ -56,10 +56,14 @@ router.post("/", async (req, res) => {
|
||||
// });
|
||||
|
||||
router.get("/", async (req, res) => {
|
||||
let condition = {};
|
||||
let condition = {
|
||||
where: {},
|
||||
order: [['createdAt', 'DESC']]
|
||||
};
|
||||
|
||||
let search = req.query.search;
|
||||
if (search) {
|
||||
condition[Op.or] = [
|
||||
condition.where[Op.or] = [
|
||||
{ title: { [Op.like]: `%${search}%` } },
|
||||
{ content: { [Op.like]: `%${search}%` } }
|
||||
];
|
||||
@@ -67,11 +71,7 @@ router.get("/", async (req, res) => {
|
||||
// You can add condition for other columns here
|
||||
// e.g. condition.columnName = value;
|
||||
|
||||
let list = await Post.findAll({
|
||||
where: condition,
|
||||
// order option takes an array of items. These items are themselves in the form of [column, direction]
|
||||
order: [['createdAt', 'DESC']]
|
||||
});
|
||||
let list = await Post.findAll(condition);
|
||||
res.json(list);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user