event details for event page

This commit is contained in:
Harini312821
2024-07-31 01:15:09 +08:00
33 changed files with 3819 additions and 3585 deletions

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

View File

@@ -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

File diff suppressed because it is too large Load Diff

View File

@@ -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>

View 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>
);
}

View File

@@ -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;

View File

@@ -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 && (

View File

@@ -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}
/>
);
};

View File

@@ -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"

View 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>
);
}

View File

@@ -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>
);
};

View File

@@ -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 />

View 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>
);
}

View File

@@ -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)}

View File

@@ -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

View 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>
);
}

View File

@@ -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 &rarr; 3 vouchers</p>
<p className="text-gray-700 dark:text-gray-300">2nd &rarr; 2 vouchers</p>
<p className="text-gray-700 dark:text-gray-300">3rd &rarr; 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>
);
}
}

View File

@@ -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>

View File

@@ -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)}

View File

@@ -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>
);
}

View 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;
}
}

View 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 };

View File

@@ -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",
{

View 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 };

View File

@@ -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(() => {

View File

@@ -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,

View File

@@ -17,9 +17,14 @@ module.exports = (sequelize, DataTypes) => {
content: {
type: DataTypes.TEXT,
allowNull: false
}
},
userId: {
type: DataTypes.UUID,
allowNull: false,
},
}, {
tableName: 'posts'
});
return Post;
}

View File

@@ -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
View File

@@ -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

View 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;

View File

@@ -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({

View File

@@ -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);
});