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",
|
"axios": "^1.7.2",
|
||||||
"formik": "^2.4.6",
|
"formik": "^2.4.6",
|
||||||
"framer-motion": "^11.2.10",
|
"framer-motion": "^11.2.10",
|
||||||
|
"openai": "^4.53.2",
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-hot-toast": "^2.4.1",
|
"react-hot-toast": "^2.4.1",
|
||||||
|
|||||||
5680
client/pnpm-lock.yaml
generated
5680
client/pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -22,10 +22,13 @@ import DefaultLayout from "./layouts/default";
|
|||||||
import AdministratorLayout from "./layouts/administrator";
|
import AdministratorLayout from "./layouts/administrator";
|
||||||
import UsersManagement from "./pages/UsersManagement";
|
import UsersManagement from "./pages/UsersManagement";
|
||||||
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
import ResetPasswordPage from "./pages/ResetPasswordPage";
|
||||||
|
import ForgotPasswordPage from "./pages/ForgotPasswordPage";
|
||||||
|
import RestrictedLayout from "./layouts/restricted";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
return (
|
return (
|
||||||
<Routes>
|
<Routes>
|
||||||
|
<Route>
|
||||||
{/* User Routes */}
|
{/* User Routes */}
|
||||||
<Route path="/">
|
<Route path="/">
|
||||||
<Route element={<DefaultLayout />}>
|
<Route element={<DefaultLayout />}>
|
||||||
@@ -40,7 +43,6 @@ function App() {
|
|||||||
<Route path="events">
|
<Route path="events">
|
||||||
<Route index element={<EventsPage />} />
|
<Route index element={<EventsPage />} />
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<EventDetailsPage />} path="event/:id"/>
|
|
||||||
|
|
||||||
{/* Karang Guni Schedules Route */}
|
{/* Karang Guni Schedules Route */}
|
||||||
<Route path="karang-guni-schedules">
|
<Route path="karang-guni-schedules">
|
||||||
@@ -61,7 +63,15 @@ function App() {
|
|||||||
<Route element={<EditPostPage />} path="edit/:id" />
|
<Route element={<EditPostPage />} path="edit/:id" />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
<Route element={<ResetPasswordPage />} path="reset-password/:token" />
|
|
||||||
|
{/* Special (Restricted) Routes */}
|
||||||
|
<Route element={<RestrictedLayout />}>
|
||||||
|
<Route element={<ForgotPasswordPage />} path="forgot-password" />
|
||||||
|
<Route
|
||||||
|
element={<ResetPasswordPage />}
|
||||||
|
path="reset-password/:token"
|
||||||
|
/>
|
||||||
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
{/* Admin Routes */}
|
{/* Admin Routes */}
|
||||||
@@ -79,6 +89,7 @@ function App() {
|
|||||||
<Route element={<EditEventsPage />} path="editEvent/:id" />
|
<Route element={<EditEventsPage />} path="editEvent/:id" />
|
||||||
</Route>
|
</Route>
|
||||||
</Route>
|
</Route>
|
||||||
|
</Route>
|
||||||
</Routes>
|
</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';
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
interface InsertImageProps {
|
interface InsertImageProps {
|
||||||
onImageSelected: (file: File) => void;
|
onImageSelected: (file: File | null) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
|
const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
|
||||||
@@ -10,19 +10,28 @@ const InsertImage: React.FC<InsertImageProps> = ({ onImageSelected }) => {
|
|||||||
|
|
||||||
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleImageSelect = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const selectedFiles = event.target.files as FileList;
|
const selectedFiles = event.target.files as FileList;
|
||||||
const file = selectedFiles?.[0];
|
const file = selectedFiles?.[0] || null;
|
||||||
if (file) {
|
|
||||||
setSelectedFile(file);
|
setSelectedFile(file);
|
||||||
setPreviewImage(URL.createObjectURL(file));
|
setPreviewImage(file ? URL.createObjectURL(file) : '');
|
||||||
onImageSelected(file);
|
onImageSelected(file);
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div
|
||||||
<input type="file" onChange={handleImageSelect} />
|
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 && (
|
{selectedFile && (
|
||||||
<img src={previewImage} alt="Selected Image" />
|
<img
|
||||||
|
src={previewImage}
|
||||||
|
alt="Selected Image"
|
||||||
|
className="w-full h-full object-cover rounded-md"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { retrieveUserInformation } from "../security/users";
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import EcoconnectFullLogo from "./EcoconnectFullLogo";
|
import EcoconnectFullLogo from "./EcoconnectFullLogo";
|
||||||
|
import EcoconnectSearch from "./EcoconnectSearch";
|
||||||
|
|
||||||
export default function NavigationBar() {
|
export default function NavigationBar() {
|
||||||
const [userProfileImageURL, setUserProfileImageURL] = useState("");
|
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="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">
|
<div className="flex flex-row gap-0 my-auto *:my-auto">
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
@@ -109,7 +110,9 @@ export default function NavigationBar() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{userInformation && (
|
{userInformation && (
|
||||||
<div className="my-auto pr-1">
|
<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">
|
<Dropdown placement="bottom" backdrop="blur">
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Avatar src={userProfileImageURL} as="button" size="sm" />
|
<Avatar src={userProfileImageURL} as="button" size="sm" />
|
||||||
@@ -117,7 +120,7 @@ export default function NavigationBar() {
|
|||||||
<DropdownMenu aria-label="Profile Actions">
|
<DropdownMenu aria-label="Profile Actions">
|
||||||
<DropdownSection showDivider>
|
<DropdownSection showDivider>
|
||||||
<DropdownItem key="account-overview" isReadOnly>
|
<DropdownItem key="account-overview" isReadOnly>
|
||||||
<div className="flex flex-col gap-2 text-center *:mx-auto p-2">
|
<div className="flex flex-col gap-2 text-center *:mx-auto p-2 w-full">
|
||||||
<Avatar
|
<Avatar
|
||||||
src={userProfileImageURL}
|
src={userProfileImageURL}
|
||||||
as="button"
|
as="button"
|
||||||
@@ -130,7 +133,9 @@ export default function NavigationBar() {
|
|||||||
<span>{userInformation.firstName}</span>{" "}
|
<span>{userInformation.firstName}</span>{" "}
|
||||||
<span>{userInformation.lastName}</span>
|
<span>{userInformation.lastName}</span>
|
||||||
</p>
|
</p>
|
||||||
<p className="opacity-50">{userInformation.email}</p>
|
<p className="opacity-50">
|
||||||
|
{userInformation.email}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -164,6 +169,7 @@ export default function NavigationBar() {
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{!userInformation && doneLoading && (
|
{!userInformation && doneLoading && (
|
||||||
<div className="flex flex-row gap-1 *:my-auto">
|
<div className="flex flex-row gap-1 *:my-auto">
|
||||||
|
|||||||
@@ -10,16 +10,28 @@ interface NextUIFormikInputProps {
|
|||||||
placeholder: string;
|
placeholder: string;
|
||||||
labelPlacement?: "inside" | "outside";
|
labelPlacement?: "inside" | "outside";
|
||||||
startContent?: JSX.Element;
|
startContent?: JSX.Element;
|
||||||
|
readOnly?: boolean;
|
||||||
|
setFieldValue?: (field: string, value: any, shouldValidate?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const NextUIFormikInput = ({
|
const NextUIFormikInput = ({
|
||||||
label,
|
label,
|
||||||
startContent,
|
startContent,
|
||||||
|
readOnly = false,
|
||||||
|
setFieldValue,
|
||||||
...props
|
...props
|
||||||
}: NextUIFormikInputProps) => {
|
}: NextUIFormikInputProps) => {
|
||||||
const [field, meta] = useField(props.name);
|
const [field, meta] = useField(props.name);
|
||||||
const [inputType, setInputType] = useState(props.type);
|
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 (
|
return (
|
||||||
<Input
|
<Input
|
||||||
{...field}
|
{...field}
|
||||||
@@ -43,6 +55,8 @@ const NextUIFormikInput = ({
|
|||||||
</>
|
</>
|
||||||
) : null
|
) : null
|
||||||
}
|
}
|
||||||
|
readOnly={readOnly}
|
||||||
|
onChange={handleChange}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { Button, Link } from "@nextui-org/react";
|
import { Button, Link } from "@nextui-org/react";
|
||||||
import { Formik, Form } from "formik";
|
import { Formik, Form } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
import axios from "axios";
|
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import NextUIFormikInput from "./NextUIFormikInput";
|
import NextUIFormikInput from "./NextUIFormikInput";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -74,6 +73,7 @@ export default function SignInModule() {
|
|||||||
placeholder="johndoe@email.com"
|
placeholder="johndoe@email.com"
|
||||||
labelPlacement="outside"
|
labelPlacement="outside"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex flex-col gap-2 w-full">
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Password"
|
label="Password"
|
||||||
name="password"
|
name="password"
|
||||||
@@ -81,6 +81,16 @@ export default function SignInModule() {
|
|||||||
placeholder=" "
|
placeholder=" "
|
||||||
labelPlacement="outside"
|
labelPlacement="outside"
|
||||||
/>
|
/>
|
||||||
|
<Link
|
||||||
|
className="hover:cursor-pointer w-max"
|
||||||
|
size="sm"
|
||||||
|
onPress={() => {
|
||||||
|
navigate("/forgot-password");
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Forgot password?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary"
|
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>
|
</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 SingaporeAgencyStrip from "../components/SingaporeAgencyStrip";
|
||||||
import NavigationBar from "../components/NavigationBar";
|
import NavigationBar from "../components/NavigationBar";
|
||||||
import { Outlet } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
|
import SiteFooter from "../components/SiteFooter";
|
||||||
|
|
||||||
export default function DefaultLayout() {
|
export default function DefaultLayout() {
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col h-screen">
|
<div className="relative flex flex-col justify-between h-screen">
|
||||||
|
<div className="flex flex-col h-screen">
|
||||||
<SingaporeAgencyStrip />
|
<SingaporeAgencyStrip />
|
||||||
<main className="pt-16 flex-grow">
|
<main className="pt-16 flex-grow">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</main>
|
</main>
|
||||||
|
</div>
|
||||||
|
<SiteFooter />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<NavigationBar />
|
<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,
|
ModalFooter,
|
||||||
useDisclosure,
|
useDisclosure,
|
||||||
Spinner,
|
Spinner,
|
||||||
|
User,
|
||||||
} from "@nextui-org/react";
|
} from "@nextui-org/react";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import instance from "../security/http";
|
import instance from "../security/http";
|
||||||
@@ -28,7 +29,8 @@ import {
|
|||||||
XMarkIcon,
|
XMarkIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
import { useNavigate } from "react-router-dom";
|
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";
|
// import UserPostImage from "../components/UserPostImage";
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
@@ -37,10 +39,22 @@ interface Post {
|
|||||||
content: string;
|
content: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
id: number;
|
id: number;
|
||||||
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
|
||||||
export default function CommunityPage() {
|
export default function CommunityPage() {
|
||||||
const navigate = useNavigate();
|
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");
|
let accessToken = localStorage.getItem("accessToken");
|
||||||
if (!accessToken) {
|
if (!accessToken) {
|
||||||
@@ -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 = () => {
|
const getPosts = () => {
|
||||||
instance.get(config.serverAddress + "/post").then((res) => {
|
instance.get(config.serverAddress + "/post").then((res) => {
|
||||||
setCommunityList(res.data);
|
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 = () => {
|
const searchPosts = () => {
|
||||||
instance
|
instance
|
||||||
.get(config.serverAddress + `/post?search=${search}`)
|
.get(config.serverAddress + `/post?search=${search}`)
|
||||||
@@ -90,10 +115,6 @@ export default function CommunityPage() {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getPosts();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const onSearchKeyDown = (e: { key: string }) => {
|
const onSearchKeyDown = (e: { key: string }) => {
|
||||||
if (e.key === "Enter") {
|
if (e.key === "Enter") {
|
||||||
searchPosts();
|
searchPosts();
|
||||||
@@ -108,13 +129,6 @@ export default function CommunityPage() {
|
|||||||
getPosts();
|
getPosts();
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
instance.get(config.serverAddress + "/post").then((res) => {
|
|
||||||
console.log(res.data);
|
|
||||||
setCommunityList(res.data);
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleDeleteClick = (post: Post) => {
|
const handleDeleteClick = (post: Post) => {
|
||||||
setSelectedPost(post);
|
setSelectedPost(post);
|
||||||
onOpen();
|
onOpen();
|
||||||
@@ -168,7 +182,7 @@ export default function CommunityPage() {
|
|||||||
<div className="flex flex-row justify-between">
|
<div className="flex flex-row justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-xl font-bold">{post.title}</p>
|
<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>
|
||||||
<div className="flex flex-row-reverse justify-center items-center">
|
<div className="flex flex-row-reverse justify-center items-center">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
@@ -185,6 +199,7 @@ export default function CommunityPage() {
|
|||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="edit"
|
key="edit"
|
||||||
|
textValue="Edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`edit/${post.id}`);
|
navigate(`edit/${post.id}`);
|
||||||
}}
|
}}
|
||||||
@@ -193,6 +208,7 @@ export default function CommunityPage() {
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="delete"
|
key="delete"
|
||||||
|
textValue="Delete"
|
||||||
className="text-danger"
|
className="text-danger"
|
||||||
color="danger"
|
color="danger"
|
||||||
onClick={() => handleDeleteClick(post)}
|
onClick={() => handleDeleteClick(post)}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
|
|||||||
import NextUIFormikTextarea from "../components/NextUIFormikTextarea";
|
import NextUIFormikTextarea from "../components/NextUIFormikTextarea";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import { ArrowUTurnLeftIcon } from "../icons";
|
import { ArrowUTurnLeftIcon } from "../icons";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { retrieveUserInformation } from "../security/users";
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
title: Yup.string()
|
title: Yup.string()
|
||||||
@@ -31,6 +33,7 @@ const validationSchema = Yup.object({
|
|||||||
|
|
||||||
function CreatePostPage() {
|
function CreatePostPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const [userId, setUserId] = useState(null);
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
title: "",
|
title: "",
|
||||||
@@ -38,12 +41,28 @@ function CreatePostPage() {
|
|||||||
tags: "",
|
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 (
|
const handleSubmit = async (
|
||||||
values: any,
|
values: any,
|
||||||
{ setSubmitting, resetForm, setFieldError }: any
|
{ setSubmitting, resetForm, setFieldError }: any
|
||||||
) => {
|
) => {
|
||||||
try {
|
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) {
|
if (response.status === 200) {
|
||||||
console.log("Post created successfully:", response.data);
|
console.log("Post created successfully:", response.data);
|
||||||
resetForm(); // Clear form after successful submit
|
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,63 +1,64 @@
|
|||||||
import {
|
import { Button } from "@nextui-org/react";
|
||||||
Card,
|
|
||||||
CardHeader,
|
|
||||||
CardBody,
|
|
||||||
CardFooter,
|
|
||||||
Divider,
|
|
||||||
Button,
|
|
||||||
} from "@nextui-org/react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
export default function HBContestPage() {
|
export default function HBContestPage() {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
const handleJoinClick = () => {
|
||||||
|
let accessToken = localStorage.getItem("accessToken");
|
||||||
|
if (!accessToken) {
|
||||||
|
setTimeout(() => {
|
||||||
|
navigate("/signin");
|
||||||
|
}, 1000);
|
||||||
|
} else {
|
||||||
|
navigate("new-submission");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<div className="h-screen flex items-center justify-center">
|
||||||
<section>
|
<section className="bg-red-50 dark:bg-primary-950 border border-primary-100 p-10 rounded-xl shadow-md w-full max-w-2xl">
|
||||||
<Card className="max-w-[800px] bg-red-50 mx-auto">
|
<div className="space-y-4">
|
||||||
<CardHeader className="flex gap-3">
|
<div>
|
||||||
<div className="flex flex-col">
|
<p className="text-2xl font-bold text-red-900 dark:text-white">
|
||||||
<h2 className="text-md">Home Bill Contest</h2>
|
Home Bill Contest
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
<div>
|
||||||
<Divider />
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
<CardBody>
|
|
||||||
<p>
|
|
||||||
This contest is to encourage residents to reduce the use of
|
This contest is to encourage residents to reduce the use of
|
||||||
electricity and water usage. This contest would be won by the
|
electricity and water usage. This contest would be won by the
|
||||||
person with the lowest overall bill average. Join us in this
|
person with the lowest overall bill average. Join us in this
|
||||||
important effort to create a more sustainable future for everyone.
|
important effort to create a more sustainable future for everyone.{" "}
|
||||||
Participants would be required to input and upload their bills
|
<span className="text-red-600">
|
||||||
into the form to ensure integrity and honesty.{" "}
|
Participants would be required to input and upload their bills into the form to ensure integrity and honesty.{" "}
|
||||||
|
</span>
|
||||||
</p>
|
</p>
|
||||||
</CardBody>
|
</div>
|
||||||
<Divider />
|
|
||||||
<CardFooter>
|
|
||||||
<div className="flex-col">
|
|
||||||
<div>
|
<div>
|
||||||
<h4>Winners</h4>
|
<p className="text-xl font-bold text-red-900 dark:text-white">
|
||||||
<p>
|
Winners
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-gray-700 dark:text-gray-300">
|
||||||
There will 3 winners for each month. Each winner will receive
|
There will 3 winners for each month. Each winner will receive
|
||||||
random food vouchers.
|
random food vouchers.
|
||||||
</p>
|
</p>
|
||||||
<p>1st: 3 vouchers</p>
|
<p className="text-gray-700 dark:text-gray-300">1st → 3 vouchers</p>
|
||||||
<p>2nd: 2 vouchers</p>
|
<p className="text-gray-700 dark:text-gray-300">2nd → 2 vouchers</p>
|
||||||
<p>3rd: 1 voucher</p>
|
<p className="text-gray-700 dark:text-gray-300">3rd → 1 voucher</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className=" bg-red-500 dark:bg-red-700 text-white"
|
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"
|
size="lg"
|
||||||
onPress={() => {
|
onPress={handleJoinClick}
|
||||||
navigate("new-submission");
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<p className="font-bold">Join</p>
|
<p>Join</p>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardFooter>
|
|
||||||
</Card>
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
import { Button } from "@nextui-org/react";
|
import { Button } from "@nextui-org/react";
|
||||||
import { ArrowUTurnLeftIcon } from "../icons";
|
import { ArrowUTurnLeftIcon } from "../icons";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -8,29 +9,25 @@ import NextUIFormikInput from "../components/NextUIFormikInput";
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import InsertImage from "../components/InsertImage";
|
import InsertImage from "../components/InsertImage";
|
||||||
import { retrieveUserInformation } from "../security/users";
|
import { retrieveUserInformation } from "../security/users";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
|
|
||||||
const validationSchema = Yup.object({
|
const validationSchema = Yup.object({
|
||||||
electricalBill: Yup.number()
|
electricalBill: Yup.number()
|
||||||
.typeError("Must be a number")
|
.typeError("Must be a number")
|
||||||
.positive("Must be a positive value")
|
.positive("Must be a positive value")
|
||||||
.max(99999.99, "Value is too large")
|
.max(99999.99, "Value is too large")
|
||||||
.required(),
|
.required("Electrical bill is a required field"),
|
||||||
waterBill: Yup.number()
|
waterBill: Yup.number()
|
||||||
.typeError("Must be a number")
|
.typeError("Must be a number")
|
||||||
.positive("Must be a positive value")
|
.positive("Must be a positive value")
|
||||||
.max(99999.99, "Value is too large")
|
.max(99999.99, "Value is too large")
|
||||||
.required(),
|
.required("Water bill is a required field"),
|
||||||
totalBill: Yup.number()
|
|
||||||
.typeError("Must be a number")
|
|
||||||
.positive("Must be a positive value")
|
|
||||||
.max(99999.99, "Value is too large")
|
|
||||||
.required(),
|
|
||||||
noOfDependents: Yup.number()
|
noOfDependents: Yup.number()
|
||||||
.typeError("Must be a number")
|
.typeError("Must be a number")
|
||||||
.integer("Must be a whole number")
|
.integer("Must be a whole number")
|
||||||
.positive("Must be a positive value")
|
.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() {
|
export default function HBFormPage() {
|
||||||
@@ -41,11 +38,18 @@ export default function HBFormPage() {
|
|||||||
waterBill: "",
|
waterBill: "",
|
||||||
totalBill: "",
|
totalBill: "",
|
||||||
noOfDependents: "",
|
noOfDependents: "",
|
||||||
|
avgBill: "",
|
||||||
ebPicture: null,
|
ebPicture: null,
|
||||||
wbPicture: null,
|
wbPicture: null,
|
||||||
userId: "",
|
userId: "",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Add state for image selection
|
||||||
|
const [imagesSelected, setImagesSelected] = useState({
|
||||||
|
ebPicture: false,
|
||||||
|
wbPicture: false,
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const getUserInformation = async () => {
|
const getUserInformation = async () => {
|
||||||
try {
|
try {
|
||||||
@@ -78,6 +82,7 @@ export default function HBFormPage() {
|
|||||||
formData.append("waterBill", values.waterBill);
|
formData.append("waterBill", values.waterBill);
|
||||||
formData.append("totalBill", values.totalBill);
|
formData.append("totalBill", values.totalBill);
|
||||||
formData.append("noOfDependents", values.noOfDependents);
|
formData.append("noOfDependents", values.noOfDependents);
|
||||||
|
formData.append("avgBill", values.avgBill);
|
||||||
|
|
||||||
if (values.ebPicture) {
|
if (values.ebPicture) {
|
||||||
formData.append("ebPicture", values.ebPicture);
|
formData.append("ebPicture", values.ebPicture);
|
||||||
@@ -124,44 +129,71 @@ export default function HBFormPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handler for image selection
|
||||||
|
const handleImageSelection = (name: string, file: File | null) => {
|
||||||
|
setImagesSelected(prevState => ({
|
||||||
|
...prevState,
|
||||||
|
[name]: !!file,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full">
|
<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)}>
|
<Button variant="light" onPress={() => navigate(-1)}>
|
||||||
<ArrowUTurnLeftIcon />
|
<ArrowUTurnLeftIcon />
|
||||||
</Button>
|
</Button>
|
||||||
</section>
|
</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
|
<Formik
|
||||||
initialValues={initialValues}
|
initialValues={initialValues}
|
||||||
validationSchema={validationSchema}
|
validationSchema={validationSchema}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
>
|
>
|
||||||
{({ isValid, dirty, isSubmitting, setFieldValue }) => (
|
{({ 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>
|
<Form>
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-5">
|
||||||
<div className="flex flex-row gap-10">
|
<div className="flex flex-row gap-10">
|
||||||
<div className="flex flex-col gap-5">
|
<div className="flex flex-col gap-10">
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Electrical Bill"
|
label="Electrical Bill"
|
||||||
name="electricalBill"
|
name="electricalBill"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="$"
|
placeholder="0.00"
|
||||||
labelPlacement="inside"
|
labelPlacement="inside"
|
||||||
|
setFieldValue={setFieldValue}
|
||||||
/>
|
/>
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Water Bill"
|
label="Water Bill"
|
||||||
name="waterBill"
|
name="waterBill"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="$"
|
placeholder="0.00"
|
||||||
labelPlacement="inside"
|
labelPlacement="inside"
|
||||||
|
setFieldValue={setFieldValue}
|
||||||
/>
|
/>
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Total Bill"
|
label="Total Bill"
|
||||||
name="totalBill"
|
name="totalBill"
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="$"
|
placeholder="0.00"
|
||||||
labelPlacement="inside"
|
labelPlacement="inside"
|
||||||
|
readOnly={true}
|
||||||
/>
|
/>
|
||||||
<NextUIFormikInput
|
<NextUIFormikInput
|
||||||
label="Number of dependents"
|
label="Number of dependents"
|
||||||
@@ -170,16 +202,26 @@ export default function HBFormPage() {
|
|||||||
placeholder="0"
|
placeholder="0"
|
||||||
labelPlacement="inside"
|
labelPlacement="inside"
|
||||||
/>
|
/>
|
||||||
|
<NextUIFormikInput
|
||||||
|
label="Average Bill"
|
||||||
|
name="avgBill"
|
||||||
|
type="text"
|
||||||
|
placeholder="0"
|
||||||
|
labelPlacement="inside"
|
||||||
|
readOnly={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-8 max-w-xs h-[500px]">
|
<div className="flex flex-row max-w-xs h-[500px] gap-10">
|
||||||
<InsertImage
|
<InsertImage
|
||||||
onImageSelected={(file) => {
|
onImageSelected={(file) => {
|
||||||
setFieldValue("ebPicture", file);
|
setFieldValue("ebPicture", file);
|
||||||
|
handleImageSelection("ebPicture", file);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<InsertImage
|
<InsertImage
|
||||||
onImageSelected={(file) => {
|
onImageSelected={(file) => {
|
||||||
setFieldValue("wbPicture", file);
|
setFieldValue("wbPicture", file);
|
||||||
|
handleImageSelection("wbPicture", file);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,15 +229,17 @@ export default function HBFormPage() {
|
|||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="bg-red-500 dark:bg-red-700 text-white"
|
className="bg-red-400 hover:bg-red-700 dark:bg-red-700 dark:hover:bg-red-900 text-white"
|
||||||
isDisabled={!isValid || !dirty || isSubmitting}
|
size="lg"
|
||||||
|
isDisabled={!isValid || !dirty || isSubmitting || isSubmitDisabled}
|
||||||
>
|
>
|
||||||
<p>Submit</p>
|
<p>Submit</p>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
)}
|
);
|
||||||
|
}}
|
||||||
</Formik>
|
</Formik>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import {
|
|||||||
HandThumbsUpIcon,
|
HandThumbsUpIcon,
|
||||||
ArrowUTurnLeftIcon,
|
ArrowUTurnLeftIcon,
|
||||||
} from "../icons";
|
} from "../icons";
|
||||||
|
import { retrieveUserInformationById } from "../security/usersbyid";
|
||||||
|
|
||||||
interface Post {
|
interface Post {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -31,23 +32,49 @@ interface Post {
|
|||||||
content: string;
|
content: string;
|
||||||
tags: string;
|
tags: string;
|
||||||
id: number;
|
id: number;
|
||||||
|
userId: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type User = {
|
||||||
|
id: number;
|
||||||
|
firstName: string;
|
||||||
|
lastName: string;
|
||||||
|
};
|
||||||
|
|
||||||
const PostPage: React.FC = () => {
|
const PostPage: React.FC = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const [post, setPost] = useState<Post | null>(null);
|
const [post, setPost] = useState<Post | null>(null);
|
||||||
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
const { isOpen, onOpen, onOpenChange } = useDisclosure();
|
||||||
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
|
const [selectedPost, setSelectedPost] = useState<Post | null>(null);
|
||||||
|
const [userInformation, setUserInformation] = useState<Record<number, User>>({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (id) {
|
if (id) {
|
||||||
instance.get(`${config.serverAddress}/post/${id}`).then((res) => {
|
instance.get(`${config.serverAddress}/post/${id}`)
|
||||||
setPost(res.data);
|
.then((res) => setPost(res.data))
|
||||||
});
|
.catch((error) => console.error("Error fetching post:", error));
|
||||||
}
|
}
|
||||||
}, [id]);
|
}, [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) {
|
if (!post) {
|
||||||
return (
|
return (
|
||||||
<div className="flex justify-center min-h-screen">
|
<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-row justify-between">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<p className="text-xl font-bold">{post.title}</p>
|
<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>
|
||||||
<div className="flex flex-row-reverse justify-center items-center">
|
<div className="flex flex-row-reverse justify-center items-center">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
@@ -116,6 +143,7 @@ const PostPage: React.FC = () => {
|
|||||||
<DropdownMenu aria-label="Static Actions">
|
<DropdownMenu aria-label="Static Actions">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="edit"
|
key="edit"
|
||||||
|
textValue="Edit"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigate(`edit/${post.id}`);
|
navigate(`edit/${post.id}`);
|
||||||
}}
|
}}
|
||||||
@@ -124,6 +152,7 @@ const PostPage: React.FC = () => {
|
|||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key="delete"
|
key="delete"
|
||||||
|
textValue="Delete"
|
||||||
className="text-danger"
|
className="text-danger"
|
||||||
color="danger"
|
color="danger"
|
||||||
onClick={() => handleDeleteClick(post)}
|
onClick={() => handleDeleteClick(post)}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import { useParams, useNavigate } from "react-router-dom";
|
|||||||
import instance from "../security/http";
|
import instance from "../security/http";
|
||||||
import config from "../config";
|
import config from "../config";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Button, Card, CircularProgress } from "@nextui-org/react";
|
import { Button } from "@nextui-org/react";
|
||||||
import EcoconnectFullLogo from "../components/EcoconnectFullLogo";
|
|
||||||
import NextUIFormikInput from "../components/NextUIFormikInput";
|
import NextUIFormikInput from "../components/NextUIFormikInput";
|
||||||
import { Formik, Form } from "formik";
|
import { Formik, Form } from "formik";
|
||||||
import * as Yup from "yup";
|
import * as Yup from "yup";
|
||||||
@@ -29,7 +28,6 @@ export default function ResetPasswordPage() {
|
|||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [validationResult, setValidationResult] = useState<boolean>(false);
|
const [validationResult, setValidationResult] = useState<boolean>(false);
|
||||||
const [pageLoading, setPageLoading] = useState<boolean>(true);
|
|
||||||
|
|
||||||
const validateToken = () => {
|
const validateToken = () => {
|
||||||
instance
|
instance
|
||||||
@@ -39,9 +37,6 @@ export default function ResetPasswordPage() {
|
|||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
setValidationResult(false);
|
setValidationResult(false);
|
||||||
})
|
|
||||||
.finally(() => {
|
|
||||||
setPageLoading(false);
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -66,16 +61,7 @@ export default function ResetPasswordPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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>
|
<div>
|
||||||
<CircularProgress label="Loading..." />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!pageLoading && (
|
|
||||||
<div className="flex flex-col gap-8 *:mx-auto">
|
|
||||||
<Card className="max-w-[600px] w-full mx-auto">
|
|
||||||
{validationResult && (
|
{validationResult && (
|
||||||
<div className="flex flex-col gap-8 p-12 text-left">
|
<div className="flex flex-col gap-8 p-12 text-left">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
@@ -119,24 +105,11 @@ export default function ResetPasswordPage() {
|
|||||||
{!validationResult && (
|
{!validationResult && (
|
||||||
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
|
<div className="flex flex-col gap-8 p-12 *:mr-auto text-left">
|
||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<p className="text-3xl font-bold">
|
<p className="text-3xl font-bold">Reset portal has been closed.</p>
|
||||||
Reset portal has been closed.
|
|
||||||
</p>
|
|
||||||
<p>Please request for a password reset again.</p>
|
<p>Please request for a password reset again.</p>
|
||||||
</div>
|
</div>
|
||||||
</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>
|
|
||||||
</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 axios = require("axios");
|
||||||
const senderEmail = "ecoconnect@trial-ynrw7gy0qxol2k8e.mlsender.net";
|
const senderEmail = "ecoconnect@trial-ynrw7gy0qxol2k8e.mlsender.net";
|
||||||
|
const { getApiKey } = require("./apiKey");
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendEmail(recipientEmail, title, content) {
|
async function sendEmail(recipientEmail, title, content) {
|
||||||
try {
|
try {
|
||||||
const apiKey = await getApiKey();
|
const apiKey = await getApiKey("mailersend_api_key");
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
"https://api.mailersend.com/v1/email",
|
"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");
|
const HBCformRoute = require("./routes/hbcform");
|
||||||
app.use("/hbcform", HBCformRoute);
|
app.use("/hbcform", HBCformRoute);
|
||||||
|
|
||||||
|
const connections = require("./routes/connections");
|
||||||
|
app.use("/connections", connections);
|
||||||
|
|
||||||
db.sequelize
|
db.sequelize
|
||||||
.sync({ alter: true })
|
.sync({ alter: true })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
type: DataTypes.INTEGER,
|
type: DataTypes.INTEGER,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
},
|
},
|
||||||
|
avgBill: {
|
||||||
|
type: DataTypes.DECIMAL(7, 2),
|
||||||
|
allowNull: false
|
||||||
|
},
|
||||||
ebPicture: {
|
ebPicture: {
|
||||||
type: DataTypes.BLOB("long"),
|
type: DataTypes.BLOB("long"),
|
||||||
allowNull: true,
|
allowNull: true,
|
||||||
|
|||||||
@@ -17,9 +17,14 @@ module.exports = (sequelize, DataTypes) => {
|
|||||||
content: {
|
content: {
|
||||||
type: DataTypes.TEXT,
|
type: DataTypes.TEXT,
|
||||||
allowNull: false
|
allowNull: false
|
||||||
}
|
},
|
||||||
|
userId: {
|
||||||
|
type: DataTypes.UUID,
|
||||||
|
allowNull: false,
|
||||||
|
},
|
||||||
}, {
|
}, {
|
||||||
tableName: 'posts'
|
tableName: 'posts'
|
||||||
});
|
});
|
||||||
|
|
||||||
return Post;
|
return Post;
|
||||||
}
|
}
|
||||||
@@ -20,6 +20,7 @@
|
|||||||
"multer": "1.4.5-lts.1",
|
"multer": "1.4.5-lts.1",
|
||||||
"mysql2": "^3.10.1",
|
"mysql2": "^3.10.1",
|
||||||
"nodemon": "^3.1.3",
|
"nodemon": "^3.1.3",
|
||||||
|
"openai": "^4.53.2",
|
||||||
"sequelize": "^6.37.3",
|
"sequelize": "^6.37.3",
|
||||||
"sharp": "^0.33.4",
|
"sharp": "^0.33.4",
|
||||||
"uuid": "^10.0.0",
|
"uuid": "^10.0.0",
|
||||||
|
|||||||
136
server/pnpm-lock.yaml
generated
136
server/pnpm-lock.yaml
generated
@@ -4,10 +4,7 @@ settings:
|
|||||||
autoInstallPeers: true
|
autoInstallPeers: true
|
||||||
excludeLinksFromLockfile: false
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
importers:
|
dependencies:
|
||||||
|
|
||||||
.:
|
|
||||||
dependencies:
|
|
||||||
argon2:
|
argon2:
|
||||||
specifier: ^0.40.3
|
specifier: ^0.40.3
|
||||||
version: 0.40.3
|
version: 0.40.3
|
||||||
@@ -38,6 +35,9 @@ importers:
|
|||||||
nodemon:
|
nodemon:
|
||||||
specifier: ^3.1.3
|
specifier: ^3.1.3
|
||||||
version: 3.1.3
|
version: 3.1.3
|
||||||
|
openai:
|
||||||
|
specifier: ^4.53.2
|
||||||
|
version: 4.53.2
|
||||||
sequelize:
|
sequelize:
|
||||||
specifier: ^6.37.3
|
specifier: ^6.37.3
|
||||||
version: 6.37.3(mysql2@3.10.1)
|
version: 6.37.3(mysql2@3.10.1)
|
||||||
@@ -179,17 +179,44 @@ packages:
|
|||||||
'@types/ms@0.7.34':
|
'@types/ms@0.7.34':
|
||||||
resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==}
|
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==}
|
resolution: {integrity: sha512-JbA0XIJPL1IiNnU7PFxDXyfAwcwVVrOoqyzzyQTyMeVhBzkJVMSkC1LlVsRQ2lpqiY4n6Bb9oCS6lzDKVQxbZw==}
|
||||||
|
|
||||||
'@types/validator@13.12.0':
|
'@types/validator@13.12.0':
|
||||||
resolution: {integrity: sha512-nH45Lk7oPIJ1RVOF6JgFI6Dy0QpHEzq4QecZhvguxYPDwT8c93prCMqAtiIttm39voZ+DDR+qkNnMpJmMBRqag==}
|
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==}
|
resolution: {integrity: sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==}
|
||||||
engines: {node: '>= 0.6'}
|
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==}
|
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||||
engines: {node: '>= 8'}
|
engines: {node: '>= 8'}
|
||||||
|
|
||||||
@@ -376,7 +403,12 @@ packages:
|
|||||||
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==}
|
||||||
engines: {node: '>= 0.6'}
|
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==}
|
resolution: {integrity: sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==}
|
||||||
engines: {node: '>= 0.10.0'}
|
engines: {node: '>= 0.10.0'}
|
||||||
|
|
||||||
@@ -397,11 +429,23 @@ packages:
|
|||||||
debug:
|
debug:
|
||||||
optional: true
|
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==}
|
resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==}
|
||||||
engines: {node: '>= 6'}
|
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==}
|
resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
@@ -454,7 +498,13 @@ packages:
|
|||||||
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
resolution: {integrity: sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==}
|
||||||
engines: {node: '>= 0.8'}
|
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==}
|
resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==}
|
||||||
engines: {node: '>=0.10.0'}
|
engines: {node: '>=0.10.0'}
|
||||||
|
|
||||||
@@ -615,7 +665,24 @@ packages:
|
|||||||
resolution: {integrity: sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==}
|
resolution: {integrity: sha512-ipO7rsHEBqa9STO5C5T10fj732ml+5kLN1cAG8/jdHd56ldQeGj3Q7+scUS+VHK/qy1zLEwC4wMK5+yM0btPvw==}
|
||||||
engines: {node: ^18 || ^20 || >= 21}
|
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==}
|
resolution: {integrity: sha512-OSs33Z9yWr148JZcbZd5WiAXhh/n9z8TxQcdMhIOlpN9AhWpLfvVFO73+m77bBABQMaY9XSvIa+qk0jlI7Gcaw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
@@ -639,7 +706,22 @@ packages:
|
|||||||
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==}
|
||||||
engines: {node: '>= 0.8'}
|
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==}
|
resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==}
|
||||||
engines: {node: '>= 0.8'}
|
engines: {node: '>= 0.8'}
|
||||||
|
|
||||||
@@ -1629,7 +1711,14 @@ snapshots:
|
|||||||
|
|
||||||
touch@3.1.1: {}
|
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
|
optional: true
|
||||||
|
|
||||||
type-fest@2.19.0: {}
|
type-fest@2.19.0: {}
|
||||||
@@ -1659,7 +1748,24 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
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:
|
dependencies:
|
||||||
'@types/node': 20.14.6
|
'@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(),
|
waterBill: yup.number().positive().required(),
|
||||||
totalBill: yup.number().positive().required(),
|
totalBill: yup.number().positive().required(),
|
||||||
noOfDependents: yup.number().integer().positive().required(),
|
noOfDependents: yup.number().integer().positive().required(),
|
||||||
|
avgBill: yup.number().positive().required(),
|
||||||
});
|
});
|
||||||
try {
|
try {
|
||||||
data = await validationSchema.validate(data, { abortEarly: false });
|
data = await validationSchema.validate(data, { abortEarly: false });
|
||||||
@@ -48,6 +49,7 @@ router.get("/", async (req, res) => {
|
|||||||
{ waterBill: { [Op.like]: `%${search}%` } },
|
{ waterBill: { [Op.like]: `%${search}%` } },
|
||||||
{ totalBill: { [Op.like]: `%${search}%` } },
|
{ totalBill: { [Op.like]: `%${search}%` } },
|
||||||
{ noOfDependents: { [Op.like]: `%${search}%` } },
|
{ noOfDependents: { [Op.like]: `%${search}%` } },
|
||||||
|
{ avgBill: { [Op.like]: `%${search}%` } },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
let list = await HBCform.findAll({
|
let list = await HBCform.findAll({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const { Post } = require('../models');
|
const { Post, User } = require('../models');
|
||||||
const { Op, where } = require("sequelize");
|
const { Op, where } = require("sequelize");
|
||||||
const yup = require("yup");
|
const yup = require("yup");
|
||||||
const multer = require("multer");
|
const multer = require("multer");
|
||||||
@@ -56,10 +56,14 @@ router.post("/", async (req, res) => {
|
|||||||
// });
|
// });
|
||||||
|
|
||||||
router.get("/", async (req, res) => {
|
router.get("/", async (req, res) => {
|
||||||
let condition = {};
|
let condition = {
|
||||||
|
where: {},
|
||||||
|
order: [['createdAt', 'DESC']]
|
||||||
|
};
|
||||||
|
|
||||||
let search = req.query.search;
|
let search = req.query.search;
|
||||||
if (search) {
|
if (search) {
|
||||||
condition[Op.or] = [
|
condition.where[Op.or] = [
|
||||||
{ title: { [Op.like]: `%${search}%` } },
|
{ title: { [Op.like]: `%${search}%` } },
|
||||||
{ content: { [Op.like]: `%${search}%` } }
|
{ content: { [Op.like]: `%${search}%` } }
|
||||||
];
|
];
|
||||||
@@ -67,11 +71,7 @@ router.get("/", async (req, res) => {
|
|||||||
// You can add condition for other columns here
|
// You can add condition for other columns here
|
||||||
// e.g. condition.columnName = value;
|
// e.g. condition.columnName = value;
|
||||||
|
|
||||||
let list = await Post.findAll({
|
let list = await Post.findAll(condition);
|
||||||
where: condition,
|
|
||||||
// order option takes an array of items. These items are themselves in the form of [column, direction]
|
|
||||||
order: [['createdAt', 'DESC']]
|
|
||||||
});
|
|
||||||
res.json(list);
|
res.json(list);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user