diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 524d8bb..12a53b4 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -17,6 +17,8 @@ "react-bootstrap-icons": "^1.10.3", "react-bootstrap-typeahead": "^6.3.2", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.2.1", "react-router-dom": "^6.18.0", "web-vitals": "^2.1.4" }, @@ -2491,6 +2493,14 @@ "node": ">=4" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -3246,6 +3256,29 @@ "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "peerDependencies": { + "react": "*" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1f53551..27d1f40 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -12,6 +12,8 @@ "react-bootstrap-icons": "^1.10.3", "react-bootstrap-typeahead": "^6.3.2", "react-dom": "^18.2.0", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.2.1", "react-router-dom": "^6.18.0", "web-vitals": "^2.1.4" }, diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 8943852..d3850bc 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -12,6 +12,7 @@ import ProfileEdit from "./Components/Settings/Profile/ProfileEdit.jsx"; import SavedArticles from "./Components/Settings/SavedArticles/SavedArticles.jsx"; import CustomizationsEdit from "./Components/Settings/Customizations/CustomizationsEdit.jsx"; import SignWelcome from "./pages/SignPages/SignWelcome.jsx"; +import { Toaster } from "react-hot-toast"; function App() { return ( @@ -42,6 +43,7 @@ function App() { /> + ); } diff --git a/frontend/src/Components/Article/Article.jsx b/frontend/src/Components/Article/Article.jsx index f0d5ff2..0708df8 100644 --- a/frontend/src/Components/Article/Article.jsx +++ b/frontend/src/Components/Article/Article.jsx @@ -7,6 +7,10 @@ import { Container, Row, Col, Stack } from "react-bootstrap"; import "./ArticleComponents.scss"; import "./Article.scss"; import { useEffect, useState } from "react"; +import useBookmark from "../../hooks/useBookmark.jsx"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; +import { toast } from "react-hot-toast"; + //dummy data for table of contents const tableOfContents = [ "Introduction to Machine Learning", @@ -74,11 +78,15 @@ const relatedTopicsList = [ //this component accepts an article object and displays the corresponding article export default function Article({ article }) { + const { isLoading, error, updateBookmark } = useBookmark(); + + // keep track of a specific article's bookmark toggling process + const [isPendingArticle, setIsPendingArticle] = useState(false); + //extracts article data pieces from provided article //these properties that i'm defining aside from title don't exist in the database yet, //mostly made up so feel free to change later on - //will display default data from figma for now const [articleData, setArticleData] = useState({ @@ -90,7 +98,7 @@ export default function Article({ article }) { : "Embark on a journey through the basics; explore what machine learning entails and how one can apply it in the real world.", author: article.author ? article.author : "David Lam", date: article.date ? article.date : "October 29, 2023", - isBookmarked: false, + isBookmarked: article.isBookmarked, headers: article.headers && Array.isArray(article.headers) ? article.headers @@ -118,9 +126,35 @@ export default function Article({ article }) { }; } ); + //handler for toggling bookmark - const toggleBookmark = () => + const toggleBookmark = () => { setArticleData({ ...articleData, isBookmarked: !articleData.isBookmarked }); + + // set pending state of an article being bookmarked to true + setIsPendingArticle(true); + + // reset pending state to false + setTimeout(() => { + setIsPendingArticle(false); + }, 500); + }; + + // toggleBookmark() function will be called only if there is a successful response from the server + const handleToggleBookmark = async () => { + await updateBookmark( + articleData.id, + articleData.isBookmarked, + toggleBookmark + ); + }; + + useEffect(() => { + if (error) { + toast.error(error); + } + }, [error]); + return ( <> @@ -132,7 +166,8 @@ export default function Article({ article }) { author={articleData.author} date={articleData.date} isBookmarked={articleData.isBookmarked} - bookmarkToggler={toggleBookmark} + isPending={isPendingArticle} + bookmarkToggler={handleToggleBookmark} /> diff --git a/frontend/src/Components/Article/Article.scss b/frontend/src/Components/Article/Article.scss index a2377e0..2f89cfd 100644 --- a/frontend/src/Components/Article/Article.scss +++ b/frontend/src/Components/Article/Article.scss @@ -16,6 +16,20 @@ } } +@keyframes spin { + from { + transform: rotate(0deg); + } + to { + transform: rotate(360deg); + } +} + +.loading { + font-size: 1.2rem; + animation: spin 500ms linear infinite; +} + @include color-mode(light) { .rel-topics-container { border-color: $divider-light; diff --git a/frontend/src/Components/Article/ArticleHeader/ArticleHeader.jsx b/frontend/src/Components/Article/ArticleHeader/ArticleHeader.jsx index 5935b14..5eac95a 100644 --- a/frontend/src/Components/Article/ArticleHeader/ArticleHeader.jsx +++ b/frontend/src/Components/Article/ArticleHeader/ArticleHeader.jsx @@ -23,6 +23,7 @@ export default function ArticleHeader({ author, date, isBookmarked, + isPending, bookmarkToggler, }) { return ( @@ -30,15 +31,11 @@ export default function ArticleHeader({ - + ); } diff --git a/frontend/src/Components/Article/ArticleHeader/ArticleHeaderTitle.jsx b/frontend/src/Components/Article/ArticleHeader/ArticleHeaderTitle.jsx index ba24b8d..f905ed0 100644 --- a/frontend/src/Components/Article/ArticleHeader/ArticleHeaderTitle.jsx +++ b/frontend/src/Components/Article/ArticleHeader/ArticleHeaderTitle.jsx @@ -1,16 +1,20 @@ import { Stack } from "react-bootstrap"; import { Bookmark, BookmarkFill } from "react-bootstrap-icons"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; //component for article title export default function ArticleHeaderTitle({ title, isBookmarked, + isPending, bookmarkToggler, }) { return ( - {isBookmarked ? ( + {isPending ? ( + + ) : isBookmarked ? ( ) : ( diff --git a/frontend/src/Components/ArticleResults/ArticleResult/ArticleResult.jsx b/frontend/src/Components/ArticleResults/ArticleResult/ArticleResult.jsx index 6dea9a5..27e7ce6 100644 --- a/frontend/src/Components/ArticleResults/ArticleResult/ArticleResult.jsx +++ b/frontend/src/Components/ArticleResults/ArticleResult/ArticleResult.jsx @@ -2,7 +2,8 @@ import { Image } from "react-bootstrap"; import "./ArticleResult.scss"; import { Bookmark, BookmarkFill } from "react-bootstrap-icons"; import { useNavigate } from "react-router-dom"; -export default function ArticleResult({ article, bookmarkToggler }) { +import { AiOutlineLoading3Quarters } from "react-icons/ai"; +export default function ArticleResult({ article, isPending, bookmarkToggler }) { const navigate = useNavigate(); const articleNavigate = () => { @@ -10,11 +11,13 @@ export default function ArticleResult({ article, bookmarkToggler }) { }; return (
-
+
- {article.isBookmarked ? ( + {isPending ? ( + + ) : article.isBookmarked ? ( { + logout(); + }; return ( <> @@ -80,28 +86,12 @@ export default function Header() { as="h3" className="text-center bg-primary" > - Hello John! + {username + ? username.charAt(0).toUpperCase() + username.slice(1) + : "Guest"} diff --git a/frontend/src/Components/Header/Header.scss b/frontend/src/Components/Header/Header.scss index 1b1c3ac..21b31c5 100644 --- a/frontend/src/Components/Header/Header.scss +++ b/frontend/src/Components/Header/Header.scss @@ -28,7 +28,7 @@ #dropdown_items { color: $link-text; - padding-left: 10px; + padding: 8px 10px; font-size: 1.2em; &:hover { font-weight: 600 !important; diff --git a/frontend/src/Components/Home/PopularArticles/PopularArticles.jsx b/frontend/src/Components/Home/PopularArticles/PopularArticles.jsx index f49e0e1..1f7cce4 100644 --- a/frontend/src/Components/Home/PopularArticles/PopularArticles.jsx +++ b/frontend/src/Components/Home/PopularArticles/PopularArticles.jsx @@ -33,7 +33,7 @@ export default function PopularArticles() {
{popular_article_sample.map((article, id) => (
-
-
- -
+
navigate("/")} + > +
+
{toBeBookmarked ? ( @@ -38,13 +43,15 @@ export default function RecentArticleItem({ )}
- + {articleTitle.toUpperCase()} - + {/*

{articleDesc}

*/}

By {articleAuthor}

-

Published {articleDatePublished}

+

+ Published {articleDatePublished} +

diff --git a/frontend/src/Components/Home/RecentArticles/RecentArticleItem/RecentArticleItem.scss b/frontend/src/Components/Home/RecentArticles/RecentArticleItem/RecentArticleItem.scss index 58fa843..c1491b3 100644 --- a/frontend/src/Components/Home/RecentArticles/RecentArticleItem/RecentArticleItem.scss +++ b/frontend/src/Components/Home/RecentArticles/RecentArticleItem/RecentArticleItem.scss @@ -8,43 +8,12 @@ // recent article parent container .recent-article-container { display: flex; + gap: 10px; + width: 55vw; + cursor: pointer; - @media (min-width: 100px) { - flex-direction: column; - gap: 15px; - width: 300px; - } - - @media (min-width: 300px) { - flex-direction: column; - gap: 20px; - width: 200px; - } - - @media (min-width: 375px) { - flex-direction: column; - width: 350px; - } - - @media (min-width: 425px) and (max-width: 700px) { + @include media-breakpoint-down(md) { flex-direction: column; - width: 95vw; - } - - @media (min-width: 760px) { - flex-direction: row; - gap: 20px; - width: 70vw; - } - - @media (min-width: 992px) { - width: 30vw; - gap: 15px; - } - - @media (min-width: 1400px) { - width: 900px; - gap: 20px; } } @@ -61,7 +30,7 @@ position: relative; object-fit: contain; object-position: center top; - width: 93vw; + width: 90vw; height: 100%; @media (min-width: 760px) { @@ -94,7 +63,7 @@ } @media (min-width: 300px) { - width: 90vw; + width: 80vw; } @media (min-width: 760px) { diff --git a/frontend/src/Components/Home/RecentArticles/RecentArticles.jsx b/frontend/src/Components/Home/RecentArticles/RecentArticles.jsx index 0bba7ac..091c364 100644 --- a/frontend/src/Components/Home/RecentArticles/RecentArticles.jsx +++ b/frontend/src/Components/Home/RecentArticles/RecentArticles.jsx @@ -3,8 +3,13 @@ import "./RecentArticles.scss"; import { useEffect, useState } from "react"; import RecentArticleItem from "./RecentArticleItem/RecentArticleItem"; +import useBookmark from "../../../hooks/useBookmark"; +import toast from "react-hot-toast"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; export default function RecentArticles({ recent_articles }) { + const { isLoading, error, updateBookmark } = useBookmark(); + //array all the articles currently not deleted const [articles, setArticles] = useState([]); @@ -18,22 +23,42 @@ export default function RecentArticles({ recent_articles }) { return { ...prevArticles, [id]: !prevArticles[id] }; }); + const handleToggleBookmark = async (article_id) => { + const toggleBookmark = () => articleToggleHandler(article_id); + await updateBookmark( + article_id, + isBookmarkedArticles[article_id], + toggleBookmark + ); + }; + //use effect to get articles upon page load once, also init selected state of every article as false //just simulating retrieving articles useEffect(() => { - let initArticles = async () => { - let retrieved_articles = await recent_articles; - setArticles(retrieved_articles); + const initArticles = async () => { + try { + const retrieved_articles = await recent_articles; + setArticles(retrieved_articles); - let initIsBookmarkedArticles = {}; - retrieved_articles.forEach(({ id }) => { - initIsBookmarkedArticles[id] = false; - }); - setIsBookmarkedArticles(initIsBookmarkedArticles); + const initIsBookmarkedArticles = {}; + retrieved_articles.forEach(({ id }) => { + initIsBookmarkedArticles[id] = false; + }); + setIsBookmarkedArticles(initIsBookmarkedArticles); + } catch (error) { + toast.error(error.message); + } }; initArticles(); - }, []); + }, [recent_articles]); + + // Handle errors from the bookmark hook + useEffect(() => { + if (error) { + toast.error(error); + } + }, [error]); return ( <> @@ -41,19 +66,25 @@ export default function RecentArticles({ recent_articles }) {

Recent Articles

- {recent_articles.map((article) => ( - - ))} + {isLoading ? ( +
+ +
+ ) : ( + articles.map((article) => ( + handleToggleBookmark(article.id)} + /> + )) + )} {/** May add onClick function to fetch more articles */}
diff --git a/frontend/src/Components/Settings/SavedArticles/SavedArticles.jsx b/frontend/src/Components/Settings/SavedArticles/SavedArticles.jsx index 0d8788c..b427701 100644 --- a/frontend/src/Components/Settings/SavedArticles/SavedArticles.jsx +++ b/frontend/src/Components/Settings/SavedArticles/SavedArticles.jsx @@ -5,6 +5,12 @@ import ConfirmDeleteModal from "./ConfirmDeleteModal"; import ArrowMarker from "../../ArrowMarker/ArrowMarker"; import "../Settings.scss"; import SavedArticlesSearchBar from "./SearchBar/SavedArticlesSearchBar"; +import useBookmark from "../../../hooks/useBookmark"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; +import toast from "react-hot-toast"; +import { useDeleteMultipleBookmarks } from "../../../hooks/useDeleteMultipleBookmarks"; +import { useGetBookmarks } from "../../../hooks/useGetBookmarks"; +import { useAuthContext } from "../../../hooks/useAuthContext"; const test_articles = [ { id: 0, @@ -30,6 +36,19 @@ const test_articles = [ ]; export default function SavedArticles() { + const { user } = useAuthContext(); + + const { + getBookmarks, + bookmarks, + isLoading: getBookmarksIsLoading, + error: getBookmarksError, + } = useGetBookmarks(); + const { + deleteMultipleBookmarks, + isLoading: deleteMultIsLoading, + error: deleteMultError, + } = useDeleteMultipleBookmarks(); //array all the articles currently not deleted const [articles, setArticles] = useState([]); @@ -54,9 +73,22 @@ export default function SavedArticles() { //use effect to get articles upon page load once, also init selected state of every article as false //just simulating retrieving articles + // Show any error from the server if possible useEffect(() => { let initArticles = async () => { - let retrieved_articles = await test_articles; + // if (active) { + await getBookmarks(); + // } + if (getBookmarksError) { + toast.error(getBookmarksError); + } + const retrieved_articles = bookmarks; + console.log(bookmarks); + //fill in missing info for now + for (const article of retrieved_articles) { + article["image"] = + "https://builtin.com/cdn-cgi/image/f=auto,quality=80,width=752,height=435/https://builtin.com/sites/www.builtin.com/files/styles/byline_image/public/2021-12/machine-learning-examples-applications.png"; + } setArticles(retrieved_articles); let initIsDeletedArticles = {}; @@ -65,14 +97,12 @@ export default function SavedArticles() { }); setIsDeletedArticles(initIsDeletedArticles); }; - initArticles(); - }, []); + }, [bookmarks.length]); - //submit handler (the yes button in modal does not trigger submit event) + //handler for updating on clientside //simply removed the selected articles from the displayed articles state - //insert backend actions here - const submitHandler = () => { + const updateSavedArticles = () => { //filter out kept articles, replace articles state with them let keptArticles = articles.filter( (article) => !isDeletedArticles[article.id] @@ -87,6 +117,20 @@ export default function SavedArticles() { setIsDeletedArticles(initIsDeletedArticles); }; + // Submit handler (the yes button in modal does not trigger submit event) + // The function updateSavedArticles will be only executed if the server returns a response successfully + const submitHandler = async () => { + await deleteMultipleBookmarks( + Object.keys(isDeletedArticles).filter( + (articleId) => isDeletedArticles[articleId] + ), + updateSavedArticles + ); + if (deleteMultError) { + toast.error(deleteMultError); + } + }; + //state for whether delete confirmation modal is displayed or now const [deleteConfirmOpen, setDeleteConfirmOpen] = useState(false); @@ -118,7 +162,9 @@ export default function SavedArticles() {
-

My Bookmarks

+

+ My Bookmarks +

- {articles.map((article) => ( - - ))} + {getBookmarksIsLoading ? ( +
+ +
+ ) : ( + articles.map((article) => ( + + )) + )} + + {!getBookmarksIsLoading && articles.length === 0 && ( +

+ You do not have any saved articles. +

+ )}
{/*Remove/Cancel will only show if there are any articles selected to be deleted*/} {Object.values(isDeletedArticles).some((isDeleted) => isDeleted) && ( diff --git a/frontend/src/Components/Settings/Settings.scss b/frontend/src/Components/Settings/Settings.scss index cb07017..14745fa 100644 --- a/frontend/src/Components/Settings/Settings.scss +++ b/frontend/src/Components/Settings/Settings.scss @@ -139,7 +139,8 @@ .settings-header, .settings-section-header, .settings-section-field-header, - .password-checkbox-label { + .password-checkbox-label, + .no-saved-articles-text { color: $primary-text-dark; } @@ -164,7 +165,8 @@ .settings-header, .settings-section-header, .settings-section-field-header, - .password-checkbox-label { + .password-checkbox-label, + .no-saved-articles-text { border-color: $primary-text-light !important; } diff --git a/frontend/src/Components/Signin_Signup/Signin/SigninCard.jsx b/frontend/src/Components/Signin_Signup/Signin/SigninCard.jsx index 5bdbd87..361fc8c 100644 --- a/frontend/src/Components/Signin_Signup/Signin/SigninCard.jsx +++ b/frontend/src/Components/Signin_Signup/Signin/SigninCard.jsx @@ -1,55 +1,77 @@ -import { useState } from "react"; +import { useEffect, useState } from "react"; import { Form, Button, InputGroup, Container, Row, Col } from "react-bootstrap"; import * as auth from "../../auth/auth"; - +import { useLogin } from "../../../hooks/useLogin"; import { EyeFill, EyeSlashFill } from "react-bootstrap-icons"; import "./Signin.scss"; import "../SignForm.scss"; +import { useNavigate } from "react-router-dom"; + +const GoogleIcon = () => ( + + {/* SVG content */} + +); + export default function SigninCard() { + const { isLoading, error, login } = useLogin(); + const navigate = useNavigate(); + const [formVal, setFormVal] = useState({ username: "", password: "", }); const [showPassword, setShowPassword] = useState(false); - //whether form has run through validation yet const [isValidated, setValidated] = useState(false); - - // error messages const [errorMessages, setErrorMessages] = useState({}); - // handle input entered const handleInput = (e) => { const { name, value } = e.target; - - setFormVal({ - ...formVal, + setFormVal((prevState) => ({ + ...prevState, [name]: value, - }); + })); }; const handlePasswordToggle = () => { setShowPassword(!showPassword); }; - // handle submit - const handleSubmit = (e) => { + const handleSubmit = async (e) => { e.preventDefault(); - const errMessagesList = {}; const checkEmpty = auth.validationFunctions.checkEmpty; - // check empty for now (will add more authentication from backend soon) + // Validation for (const fieldName in formVal) { - let validateResult = checkEmpty(fieldName, formVal[fieldName]); - + const validateResult = checkEmpty(fieldName, formVal[fieldName]); if (typeof validateResult === "string") { errMessagesList[fieldName] = validateResult; } } - setValidated(true); - setErrorMessages(errMessagesList); + if (Object.keys(errMessagesList).length === 0) { + // If no errors, proceed with login + try { + await login(formVal.username, formVal.password); + // navigate("/"); + // Redirect or perform other actions on successful login + } catch (err) { + setErrorMessages({ form: "Invalid credentials" }); + } + } else { + setValidated(true); + setErrorMessages(errMessagesList); + } }; return ( @@ -59,21 +81,21 @@ export default function SigninCard() { onSubmit={handleSubmit} className="sign-form" > -

Login

+

+ Login +

- {errorMessages.hasOwnProperty("username") - ? errorMessages.username - : ""} + {errorMessages.username || ""} @@ -83,7 +105,7 @@ export default function SigninCard() { value={formVal.password} type={showPassword ? "text" : "password"} placeholder="Password" - isInvalid={errorMessages.hasOwnProperty("password")} + isInvalid={!!errorMessages.password} onChange={handleInput} required className="sign-text-input" @@ -92,21 +114,23 @@ export default function SigninCard() { title={showPassword ? "hide password" : "show password"} className="my-auto bg-white border-white sign-password-visbility-button" onClick={handlePasswordToggle} + type="button" > {showPassword ? : } - {errorMessages.hasOwnProperty("password") - ? errorMessages.password - : ""} + {errorMessages.password || ""} - + {errorMessages.form && ( +

{errorMessages.form}

+ )} + {error &&

{error}

}
- @@ -144,55 +168,3 @@ export default function SigninCard() { ); } - -const GoogleIcon = () => ( - - - - - - - - - - - -); diff --git a/frontend/src/Components/Signin_Signup/Signup/SignupCard.jsx b/frontend/src/Components/Signin_Signup/Signup/SignupCard.jsx index 95c4037..72a2112 100644 --- a/frontend/src/Components/Signin_Signup/Signup/SignupCard.jsx +++ b/frontend/src/Components/Signin_Signup/Signup/SignupCard.jsx @@ -1,13 +1,17 @@ import { useEffect, useState } from "react"; -import { Card, Form, Button, InputGroup } from "react-bootstrap"; +import { Form, Button, InputGroup } from "react-bootstrap"; import { Link, useNavigate } from "react-router-dom"; import * as auth from "../../auth/auth"; import "./Signup.scss"; import "../SignForm.scss"; - +import { useSignup } from "../../../hooks/useSignup"; import { EyeFill, EyeSlashFill } from "react-bootstrap-icons"; +import toast from "react-hot-toast"; export default function SignupCard() { + const { signup, isLoading, error } = useSignup(); + const navigate = useNavigate(); + const [formVal, setFormVal] = useState({ username: "", fname: "", @@ -18,15 +22,14 @@ export default function SignupCard() { }); const [showPassword, setShowPassword] = useState(false); const [showConfirmPassword, setShowConfirmPassword] = useState(false); - const navigate = useNavigate(); - //whether form has run through validation yet + // Whether form has run through validation yet const [isValidated, setValidated] = useState(false); - // error messages + // Error messages const [errorMessages, setErrorMessages] = useState({}); - // handle input entered + // Handle input entered const handleInput = (e) => { const { name, value } = e.target; @@ -44,26 +47,27 @@ export default function SignupCard() { setShowConfirmPassword(!showConfirmPassword); }; + //does not work because state is not manipulated instantly, so this can potentially be checking before errors are being pushed here const isValidationPassed = () => { - return Object.keys(errorMessages).length === 0 ? true : false; + return Object.keys(errorMessages).length === 0; }; - useEffect(() => { - // might add API endpoints to handle backend authentication here - if (isValidated && isValidationPassed()) { - navigate("/signin"); - } - }, [isValidationPassed]); - - // handle submit - const handleSubmit = (e) => { - e.preventDefault(); - + const validateInputField = () => { const newErrMessages = {}; - const formValidation = auth.formValidation; + const { username, password, confirmPassword, fname, lname, email } = + auth.formValidation; + + const fieldsToValidate = { + username, + password, + fname, + lname, + email, + confirmPassword, + }; - for (const fieldName in formValidation) { - const validationFuncs = formValidation[fieldName]; + for (const fieldName in fieldsToValidate) { + const validationFuncs = fieldsToValidate[fieldName]; validationFuncs.forEach((validationFunc) => { let validateResult = validationFunc( @@ -82,6 +86,35 @@ export default function SignupCard() { setErrorMessages(newErrMessages); }; + // handle submit - validate all input fields on the client side before directing + // the user to Home page + const handleSubmit = (e) => { + e.preventDefault(); + validateInputField(); + }; + + useEffect(() => { + const handleSignup = async () => { + if (isValidated && isValidationPassed()) { + try { + await signup(formVal.username, formVal.password); + navigate("/"); + } catch (error) { + toast.error(error.message); + } + } + }; + + handleSignup(); + }, [errorMessages]); + + // Handle errors from the bookmark hook + useEffect(() => { + if (error) { + toast.error(error); + } + }, [error]); + return (
-

Sign Up

+

+ Sign Up +

+ {error &&
{error}
}
-
diff --git a/frontend/src/Components/auth/auth.js b/frontend/src/Components/auth/auth.js index 49b039e..97428a8 100644 --- a/frontend/src/Components/auth/auth.js +++ b/frontend/src/Components/auth/auth.js @@ -23,7 +23,8 @@ const patterns = { export const validationFunctions = { checkEmpty: (name = "", value1 = "", value2 = "") => value1.length > 0 || `${errorLabels[name]} field is required`, - + noSpaces: (name = "", value1 = "", value2 = "") => + !value1.includes(" ") || `${errorLabels[name]} cannot have any spaces`, checkPasswordLength: (name = "", value1 = "", value2 = "") => value1.length >= 8 || value1.length === 0 || @@ -37,21 +38,22 @@ export const validationFunctions = { return regex.test(value1) || value1.length === 0 ? true : "Invalid email"; }, - checkboxRequired: (name="", value1=false, value2="") =>{ - return value1 || "This is required." - } + checkboxRequired: (name = "", value1 = false, value2 = "") => { + return value1 || "This is required."; + }, }; // an object containing input fields (keys) // and their associated validation funciton (values as an array) export const formValidation = { - username: [validationFunctions.checkEmpty], + username: [validationFunctions.checkEmpty, validationFunctions.noSpaces], fname: [validationFunctions.checkEmpty], lname: [validationFunctions.checkEmpty], email: [validationFunctions.checkEmpty, validationFunctions.checkValidEmail], password: [ validationFunctions.checkEmpty, validationFunctions.checkPasswordLength, + validationFunctions.noSpaces, ], oldPassword: [ validationFunctions.checkEmpty, @@ -60,16 +62,17 @@ export const formValidation = { newPassword: [ validationFunctions.checkEmpty, validationFunctions.checkPasswordLength, + validationFunctions.noSpaces, ], confirmPassword: [ validationFunctions.checkEmpty, validationFunctions.checkPasswordMatch, + validationFunctions.noSpaces, ], confirmNewPassword: [ validationFunctions.checkEmpty, validationFunctions.checkPasswordMatch, + validationFunctions.noSpaces, ], - confirmNewPasswordCheckbox: [ - validationFunctions.checkboxRequired - ] + confirmNewPasswordCheckbox: [validationFunctions.checkboxRequired], }; diff --git a/frontend/src/hooks/useBookmark.jsx b/frontend/src/hooks/useBookmark.jsx new file mode 100644 index 0000000..7d0dfe0 --- /dev/null +++ b/frontend/src/hooks/useBookmark.jsx @@ -0,0 +1,52 @@ +import { useState } from "react"; +import { useAuthContext } from "./useAuthContext"; +import toast from "react-hot-toast"; + +export default function useBookmark() { + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const apiUrl = import.meta.env.VITE_API_URL; + const { user } = useAuthContext(); + + const updateBookmark = async (article_id, isBookmarked, toggleBookmark) => { + setError(null); + setIsLoading(true); + + try { + if (!user) { + throw new Error("You need to be logged in first!"); + } + + const response = await fetch( + isBookmarked + ? `${apiUrl}/api/bookmarks/${article_id}` + : `${apiUrl}/api/bookmarks`, + { + method: isBookmarked ? "DELETE" : "POST", + headers: { + Authorization: `Bearer ${user.token}`, + "Content-Type": isBookmarked ? undefined : "application/json", + }, + body: isBookmarked + ? undefined + : JSON.stringify({ article_id: article_id }), + } + ); + + const json = await response.json(); + // console.log(json); + if (json.error) { + throw new Error(json.error || "Failed to toggle bookmark"); + } + + // Call the bookmark toggle function if the request is successful + toggleBookmark(); + } catch (error) { + setError(error.message); + } finally { + setIsLoading(false); + } + }; + + return { isLoading, error, updateBookmark }; +} diff --git a/frontend/src/hooks/useDeleteMultipleBookmarks.jsx b/frontend/src/hooks/useDeleteMultipleBookmarks.jsx new file mode 100644 index 0000000..a2a72aa --- /dev/null +++ b/frontend/src/hooks/useDeleteMultipleBookmarks.jsx @@ -0,0 +1,46 @@ +import { useState } from "react"; +import { useAuthContext } from "./useAuthContext"; + +export const useDeleteMultipleBookmarks = () => { + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const apiUrl = import.meta.env.VITE_API_URL; + const { user } = useAuthContext(); + //takes in list of bookmark ids to delete, method for deleting on client side + const deleteMultipleBookmarks = async ( + bookmarkIds, + deleteBookmarksHandler + ) => { + setIsLoading(true); + setError(null); + try { + if (!user) { + throw new Error("You need to be logged in first!"); + } + + const response = await fetch( + `${apiUrl}/api/bookmarks/${bookmarkIds.join(",")}`, + { + method: "DELETE", + headers: { + Authorization: `Bearer ${user.token}`, + }, + } + ); + + const json = await response.json(); + if (!response.ok) { + setError(json.error); + } + if (response.ok) { + deleteBookmarksHandler(); + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + return { deleteMultipleBookmarks, isLoading, error }; +}; diff --git a/frontend/src/hooks/useGetBookmarks.jsx b/frontend/src/hooks/useGetBookmarks.jsx new file mode 100644 index 0000000..6839871 --- /dev/null +++ b/frontend/src/hooks/useGetBookmarks.jsx @@ -0,0 +1,47 @@ +import { useState } from "react"; +import { useAuthContext } from "./useAuthContext"; + +export const useGetBookmarks = () => { + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const [bookmarks, setBookmarks] = useState([]); + const apiUrl = import.meta.env.VITE_API_URL; + const { user } = useAuthContext(); + + const getBookmarks = async () => { + setIsLoading(true); + setError(null); + try { + + let {token} = JSON.parse(localStorage.getItem('user')) + + if (!user && !token) { + throw new Error("You need to be logged in first!"); + } + + const response = await fetch( + `${apiUrl}/api/bookmarks`, + { + method: "GET", + headers: { + Authorization: `Bearer ${token}`, + }, + } + ); + + const json = await response.json(); + if (!response.ok) { + throw new Error("Failed to retrieve bookmarks"); + } + if (response.ok) { + setBookmarks(json) + } + } catch (err) { + setError(err.message); + } finally { + setIsLoading(false); + } + }; + + return { getBookmarks,bookmarks, isLoading, error }; +}; diff --git a/frontend/src/hooks/useLogin.jsx b/frontend/src/hooks/useLogin.jsx index b821e2c..aa99248 100644 --- a/frontend/src/hooks/useLogin.jsx +++ b/frontend/src/hooks/useLogin.jsx @@ -1,23 +1,32 @@ import { useState } from "react"; import { useAuthContext } from "./useAuthContext"; +import { Navigate, useNavigate } from "react-router-dom"; + export const useLogin = () => { const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(null) + const [isLoading, setIsLoading] = useState(false) const { dispatch } = useAuthContext() - - const login = async (username,user_password) => + const navigate = useNavigate() + const apiUrl = import.meta.env.VITE_API_URL; //change to .env + // const api = process.env.REACT_APP_API_URL + + + const login = async (username,user_password,error) => { setIsLoading(true); setError(null); - - const response = await fetch('http://localhost:3002/api/users/login', + try { + const response = await fetch(`http://localhost:3002/api/users/login`, { method : 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ username, user_password }) + }) + console.log(apiUrl) + const json = await response.json(); @@ -33,8 +42,16 @@ export const useLogin = () => dispatch({type: 'LOGIN', payload: json}) setIsLoading(false) + console.log(localStorage) + navigate("/") } + + } + catch (err) { + setError("Something went wrong. Please try again."); + setIsLoading(false); } +} return {login, isLoading, error} } \ No newline at end of file diff --git a/frontend/src/hooks/useSignup.jsx b/frontend/src/hooks/useSignup.jsx index e9d989c..c04fecc 100644 --- a/frontend/src/hooks/useSignup.jsx +++ b/frontend/src/hooks/useSignup.jsx @@ -1,40 +1,57 @@ import { useState } from "react"; import { useAuthContext } from "./useAuthContext"; +import { useNavigate } from "react-router-dom"; -export const useSignup = () => -{ - const [error, setError] = useState(null) - const [isLoading, setIsLoading] = useState(null) - const { dispatch } = useAuthContext() - const signup = async (username,user_password) => - { - setIsLoading(true); - setError(null); +export const useSignup = () => { + const [error, setError] = useState(null); + const [isLoading, setIsLoading] = useState(false); + const { dispatch } = useAuthContext(); + const navigate = useNavigate(); + const apiUrl = import.meta.env.VITE_API_URL; + // const api = process.env.REACT_APP_API_URL + - const response = await fetch('http://localhost:3002/api/users/', - { - method : 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({ username, user_password }) - }) + console.log("API URL from environment:", apiUrl); // Debug log - const json = await response.json(); - - if (!response.ok) { - setIsLoading(false) - setError(json.error) - } - if (response.ok) { - //save user to local storage - localStorage.setItem('user', JSON.stringify(json)) - - //update authcontext - dispatch({type: 'LOGIN', payload: json}) + const signup = async (username, user_password) => { + setIsLoading(true); + setError(null); - setIsLoading(false) + try { + const response = await fetch(`http://localhost:3002/api/users`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ username, user_password }) + }); + + let json; + try { + json = await response.json(); + } catch (err) { + setError("Invalid response from server"); + setIsLoading(false); + return; + } + + if (!response.ok) { + setError(json.error); + setIsLoading(false); + return; + } + + // Save user to local storage + localStorage.setItem('user', JSON.stringify(json)); + + // Update auth context + dispatch({ type: 'LOGIN', payload: json }); + + navigate("/signin"); + } catch (err) { + setError("Something went wrong. Please try again."); + setIsLoading(false); } - } + }; - return {signup, isLoading, error} -} \ No newline at end of file + return { signup, isLoading, error }; +}; diff --git a/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.jsx b/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.jsx index 2ee102a..8705975 100644 --- a/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.jsx +++ b/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.jsx @@ -1,9 +1,12 @@ import { useEffect, useState } from "react"; -import { useParams, useSearchParams } from "react-router-dom"; -import RelatedTags from "../../Components/ArticleResults/SideSections/RelatedTopicTags/RelatedTopicTags.jsx"; -import ArticleResultsList from "../../Components/ArticleResults/ArticleResultsList.jsx"; import { Col, Container, Row } from "react-bootstrap"; +import toast from "react-hot-toast"; +import { useSearchParams } from "react-router-dom"; +import ArticleResult from "../../Components/ArticleResults/ArticleResult/ArticleResult.jsx"; +import RelatedTags from "../../Components/ArticleResults/SideSections/RelatedTopicTags/RelatedTopicTags.jsx"; +import useBookmark from "../../hooks/useBookmark.jsx"; import "./ArticleResultsPage.scss"; +import { AiOutlineLoading3Quarters } from "react-icons/ai"; const dummy_topic_tags = [ { label: "Deep Learning" }, { label: "Artifical Intelligence" }, @@ -23,9 +26,14 @@ const dummmy_articles = [ ]; export default function ArticleResultsPage({}) { - const [articles, setArticles] = useState(); + const { error, updateBookmark } = useBookmark(); + + // keep track of a specific article's bookmark toggling process + const [pendingArticles, setPendingArticles] = useState({}); + + const [articles, setArticles] = useState([]); const [searchParams, setSearchParams] = useSearchParams(); - const [specificArticle, setSpecificArticle] = useState(); + // const [specificArticle, setSpecificArticle] = useState(); const titleQuery = searchParams.get("title"); //bookmark toggler creator function, returns function that toggles bookmark for certain id @@ -37,53 +45,131 @@ export default function ArticleResultsPage({}) { !newArticles[articleIndex].isBookmarked; setArticles(newArticles); } + + // set pending state of an article being bookmarked to true + setPendingArticles((prev) => ({ ...prev, [id]: true })); + + // reset pending state to false + setTimeout(() => { + setPendingArticles((prev) => ({ ...prev, [id]: false })); + }, 500); }; + //holds whether article search has been attempted + const [searchAttempted, setSearchAttempted] = useState(false); + useEffect(() => { - fetch(`http://localhost:3002/api/articles/?title=${titleQuery}`) - .then((res) => - res.json().then((data) => { - let dataCopy = [...data]; - - //we dont have author names, date, bookmarked, or image, so just inserting default in for now - dataCopy.forEach((articleObject) => { - articleObject.image = - "https://emeritus.org/in/wp-content/uploads/sites/3/2023/03/types-of-machine-learning.jpg.optimal.jpg"; - articleObject.author = "jeff"; - articleObject.date = "October 24, 2023"; - articleObject.isBookmarked = false; - }); - setArticles(dataCopy); - }) - ) - .catch((error) => { - console.error("error fetching data"); - }); + const authUser = localStorage.getItem("user"); + const user = authUser ? JSON.parse(authUser) : undefined; + + setSearchAttempted(false); + + const fetchArticles = async () => { + try { + const response = await fetch( + `http://localhost:3002/api/articles/?title=${titleQuery}`, + { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: user ? `Bearer ${user.token}` : undefined, + }, + } + ); + const data = await response.json(); + + const enrichedData = data.map((articleObject) => ({ + ...articleObject, + image: + "https://emeritus.org/in/wp-content/uploads/sites/3/2023/03/types-of-machine-learning.jpg.optimal.jpg", + author: "jeff", + date: "October 24, 2023", + isBookmarked: articleObject.isBookmarked + ? articleObject.isBookmarked + : false, + })); + setArticles(enrichedData); + + // initialize pending state of all article results to false + let initPendingArticles = {}; + enrichedData.forEach(({ id }) => { + initPendingArticles[id] = false; + }); + setPendingArticles(initPendingArticles); + } catch (error) { + toast.error(error.message); + } finally { + setSearchAttempted(true); + } + }; + + fetchArticles(); }, [titleQuery, setSearchParams]); + // handle bookmark toggling with an article with its specified ID + const handleToggleBookmark = async (article_id) => { + const article = articles.find((a) => a.id === article_id); + if (!article) return; + + const toggleBookmark = bookmarkTogglerCreator(article_id); + await updateBookmark(article_id, article.isBookmarked, toggleBookmark); + }; + + // Handle errors from the bookmark hook + useEffect(() => { + if (error) { + toast.error(error); + } + }, [error]); + return ( - - - -

- Displaying results for{" "} - "{titleQuery}" -

- - - - - -
-
+ <> + + + +

+ Displaying results for{" "} + + "{titleQuery}" + +

+
+ {articles && + articles.length > 0 && + articles.map((article) => ( + handleToggleBookmark(article.id)} + /> + ))} + + {(!articles || articles.length === 0) && searchAttempted && ( +

+ Your query did not match any results +

+ )} + + {!searchAttempted && ( + + )} +
+ + + + +
+
+ ); } diff --git a/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.scss b/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.scss index d5ed740..1f7774f 100644 --- a/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.scss +++ b/frontend/src/pages/ArticleResultsPage/ArticleResultsPage.scss @@ -13,6 +13,12 @@ } } +.no-article-results { + @include media-breakpoint-up(lg) { + padding-left: 20px; + } +} + .article-results-title-query { color: $primary; } @@ -31,7 +37,8 @@ } @include color-mode(light) { - .article-results-title { + .article-results-title, + .no-article-results { color: $primary-text-light; } .side-sections-container { @@ -39,7 +46,8 @@ } } @include color-mode(dark) { - .article-results-title { + .article-results-title, + .no-article-results { color: $primary-text-dark; } .side-sections-container { diff --git a/frontend/src/pages/ArticleView.jsx b/frontend/src/pages/ArticleView.jsx index 4bcc953..0b5e4ee 100644 --- a/frontend/src/pages/ArticleView.jsx +++ b/frontend/src/pages/ArticleView.jsx @@ -1,16 +1,28 @@ import { useEffect, useState } from "react"; import { useParams } from "react-router-dom"; import Article from "../Components/Article/Article.jsx"; +import { useAuthContext } from "../hooks/useAuthContext.jsx"; export default function ArticleView() { const [article, setArticle] = useState(); const { name = "" } = useParams(); useEffect(() => { - fetch(`http://localhost:3002/api/articles/${name}`) - .then((res) => res.json() - .then((data) => { + const authUser = localStorage.getItem("user"); + + const user = (authUser ? JSON.parse(authUser):undefined); + + fetch(`http://localhost:3002/api/articles/${name}`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Authorization: (user ? `Bearer ${user.token}` : undefined), + }, + }) + .then((res) => + res.json().then((data) => { let article = data[0]; + article.isBookmarked = (article.isBookmarked ? article.isBookmarked : false); console.log(data); setArticle(article); }) diff --git a/frontend/src/pages/HomePage/Home.jsx b/frontend/src/pages/HomePage/Home.jsx index 358d6da..ee2842d 100644 --- a/frontend/src/pages/HomePage/Home.jsx +++ b/frontend/src/pages/HomePage/Home.jsx @@ -14,7 +14,6 @@ import devin_ai from "../../assets/Article_Images/devin_ai.png"; import ai_in_business from "../../assets/Article_Images/ai_in_business.png"; import quantum from "../../assets/Article_Images/quantum.png"; import ai_brain from "../../assets/ml_brain.jpg"; - export default function Home() { // demo feature article const feature_article_example = { @@ -74,8 +73,11 @@ export default function Home() { }, ]; + + // useEffect to fetch the main feature article from the database (added later) - useEffect(() => {}, []); + useEffect(() => { + }, []); return ( <> diff --git a/frontend/vite.config.js b/frontend/vite.config.js index 081c8d9..a93e4a2 100644 --- a/frontend/vite.config.js +++ b/frontend/vite.config.js @@ -1,6 +1,17 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react"; +import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; -export default defineConfig({ - plugins: [react()], -}); +export default ({ mode }) => { + // Load environment variables based on the current mode + const env = loadEnv(mode, process.cwd()); + + return defineConfig({ + plugins: [react()], + define: { + // Make sure to stringify the environment variables + 'process.env': { + VITE_API_URL: JSON.stringify(env.VITE_API_URL), + }, + }, + }); +}; diff --git a/package-lock.json b/package-lock.json index a0dd4dd..8fb2d74 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,10 +10,17 @@ "license": "ISC", "dependencies": { "@vitejs/plugin-react": "^4.2.1", + "cloudinary": "^2.0.3", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "man": "^2.0.0", + "modules": "^0.4.0", + "multer": "^1.4.5-lts.1", + "react-bootstrap-typeahead": "^6.3.2", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.2.1", "vite": "^5.0.12" }, "devDependencies": { @@ -437,7 +444,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "dev": true, "dependencies": { "regenerator-runtime": "^0.14.0" }, @@ -879,6 +885,15 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@remix-run/router": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", @@ -888,6 +903,17 @@ "node": ">=14.0.0" } }, + "node_modules/@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "dependencies": { + "dequal": "^2.0.3" + }, + "peerDependencies": { + "react": ">=16.8.0" + } + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", @@ -1086,6 +1112,25 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, + "node_modules/@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, "node_modules/@vitejs/plugin-react": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", @@ -1159,6 +1204,11 @@ "node": ">= 8" } }, + "node_modules/append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "node_modules/array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -1273,6 +1323,22 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -1368,6 +1434,11 @@ "fsevents": "~2.3.2" } }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "node_modules/cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -1382,6 +1453,18 @@ "node": ">=12" } }, + "node_modules/cloudinary": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.0.3.tgz", + "integrity": "sha512-2JPxAUuV4iHwiW4ATSOZvii6+BhhKI9+9KscgUkxJPKa6V6wOnZJHlYyovBGrrIbIgEdmGSZgqEsLfD0wWBhBg==", + "dependencies": { + "lodash": "^4.17.21", + "q": "^1.5.1" + }, + "engines": { + "node": ">=9" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1400,12 +1483,31 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "engines": [ + "node >= 0.8" + ], + "dependencies": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "node_modules/concurrently": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", @@ -1470,6 +1572,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "node_modules/cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -1482,6 +1589,11 @@ "node": ">= 0.10" } }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "node_modules/date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -1540,6 +1652,14 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -1549,6 +1669,26 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -1701,6 +1841,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -1831,6 +1976,14 @@ "node": ">=4" } }, + "node_modules/goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "peerDependencies": { + "csstype": "^3.0.10" + } + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -1938,6 +2091,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -1997,6 +2158,11 @@ "node": ">=0.12.0" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -2067,8 +2233,12 @@ "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "node_modules/lodash.includes": { "version": "4.3.0", @@ -2109,8 +2279,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "peer": true, "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, @@ -2203,11 +2371,55 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/modules": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/modules/-/modules-0.4.0.tgz", + "integrity": "sha512-LX4JgwPHJr1FurPDKp1BlGgMXqZXtxO1O8ABGmj2g15CbLGlInTHcA9flqw6uN6oYKE2T0ngWdiHvcX97mdBsw==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "dependencies": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -2395,6 +2607,21 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -2413,6 +2640,15 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==", + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -2453,7 +2689,6 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0" @@ -2462,11 +2697,36 @@ "node": ">=0.10.0" } }, + "node_modules/react-bootstrap-typeahead": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.3.2.tgz", + "integrity": "sha512-N5Mb0WlSSMcD7Z0pcCypILgIuECybev0hl4lsnCa5lbXTnN4QdkuHLGuTLSlXBwm1ZMFpOc2SnsdSRgeFiF+Ow==", + "dependencies": { + "@babel/runtime": "^7.14.6", + "@popperjs/core": "^2.10.2", + "@restart/hooks": "^0.4.0", + "classnames": "^2.2.0", + "fast-deep-equal": "^3.1.1", + "invariant": "^2.2.1", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.8", + "react-overlays": "^5.2.0", + "react-popper": "^2.2.5", + "scroll-into-view-if-needed": "^3.1.0", + "warning": "^4.0.1" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, "node_modules/react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0", @@ -2476,6 +2736,77 @@ "react": "^18.2.0" } }, + "node_modules/react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "node_modules/react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "dependencies": { + "goober": "^2.1.10" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "react": ">=16", + "react-dom": ">=16" + } + }, + "node_modules/react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "peerDependencies": { + "react": "*" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "node_modules/react-overlays": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", + "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "dependencies": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.11.6", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + }, + "peerDependencies": { + "react": ">=16.3.0", + "react-dom": ">=16.3.0" + } + }, + "node_modules/react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "dependencies": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + }, + "peerDependencies": { + "@popperjs/core": "^2.0.0", + "react": "^16.8.0 || ^17 || ^18", + "react-dom": "^16.8.0 || ^17 || ^18" + } + }, "node_modules/react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -2516,6 +2847,25 @@ "react-dom": ">=16.8" } }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -2531,8 +2881,7 @@ "node_modules/regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "node_modules/require-directory": { "version": "2.1.1", @@ -2628,12 +2977,19 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, "peer": true, "dependencies": { "loose-envify": "^1.1.0" } }, + "node_modules/scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "dependencies": { + "compute-scroll-into-view": "^3.0.2" + } + }, "node_modules/semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -2773,6 +3129,27 @@ "node": ">= 0.8" } }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, "node_modules/string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -2881,6 +3258,25 @@ "node": ">= 0.6" } }, + "node_modules/typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "node_modules/uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "dependencies": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + }, + "peerDependencies": { + "react": ">=15.0.0" + } + }, "node_modules/undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -2924,6 +3320,11 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -2994,6 +3395,14 @@ } } }, + "node_modules/warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "dependencies": { + "loose-envify": "^1.0.0" + } + }, "node_modules/wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -3011,6 +3420,14 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -3364,7 +3781,6 @@ "version": "7.23.2", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.23.2.tgz", "integrity": "sha512-mM8eg4yl5D6i3lu2QKPuPH4FArvJ8KhTofbE7jwMUv9KX5mBvwPAqnV3MlyBNqdp9RyRKP6Yck8TrfYrPvX3bg==", - "dev": true, "requires": { "regenerator-runtime": "^0.14.0" } @@ -3578,12 +3994,25 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==" + }, "@remix-run/router": { "version": "1.11.0", "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.11.0.tgz", "integrity": "sha512-BHdhcWgeiudl91HvVa2wxqZjSHbheSgIiDvxrF1VjFzBzpTtuDPkOdOi3Iqvc08kXtFkLjhbS+ML9aM8mJS+wQ==", "dev": true }, + "@restart/hooks": { + "version": "0.4.16", + "resolved": "https://registry.npmjs.org/@restart/hooks/-/hooks-0.4.16.tgz", + "integrity": "sha512-f7aCv7c+nU/3mF7NWLtVVr0Ra80RqsO89hO72r+Y/nvQr5+q0UFGkocElTH6MJApvReVh6JHUFYn2cw1WdHF3w==", + "requires": { + "dequal": "^2.0.3" + } + }, "@rollup/rollup-android-arm-eabi": { "version": "4.9.6", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.9.6.tgz", @@ -3704,6 +4133,25 @@ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==" }, + "@types/prop-types": { + "version": "15.7.12", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", + "integrity": "sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==" + }, + "@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "requires": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "@types/warning": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", + "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==" + }, "@vitejs/plugin-react": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.2.1.tgz", @@ -3756,6 +4204,11 @@ "picomatch": "^2.0.4" } }, + "append-field": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz", + "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==" + }, "array-flatten": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", @@ -3842,6 +4295,19 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" + }, + "busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -3899,6 +4365,11 @@ "readdirp": "~3.6.0" } }, + "classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, "cliui": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", @@ -3910,6 +4381,15 @@ "wrap-ansi": "^7.0.0" } }, + "cloudinary": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/cloudinary/-/cloudinary-2.0.3.tgz", + "integrity": "sha512-2JPxAUuV4iHwiW4ATSOZvii6+BhhKI9+9KscgUkxJPKa6V6wOnZJHlYyovBGrrIbIgEdmGSZgqEsLfD0wWBhBg==", + "requires": { + "lodash": "^4.17.21", + "q": "^1.5.1" + } + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -3925,12 +4405,28 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "compute-scroll-into-view": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.0.tgz", + "integrity": "sha512-rj8l8pD4bJ1nx+dAkMhV1xB5RuZEyVysfxJqB1pRchh1KVvwOv9b7CGB8ZfjTImVv2oF+sYMUkMZq6Na5Ftmbg==" + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } + }, "concurrently": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/concurrently/-/concurrently-8.2.2.tgz", @@ -3976,6 +4472,11 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, "cors": { "version": "2.8.5", "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", @@ -3985,6 +4486,11 @@ "vary": "^1" } }, + "csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, "date-fns": { "version": "2.30.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz", @@ -4024,11 +4530,30 @@ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" }, + "dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==" + }, "destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" }, + "dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "requires": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "dotenv": { + "version": "16.4.5", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", + "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==" + }, "ecdsa-sig-formatter": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", @@ -4161,6 +4686,11 @@ } } }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, "fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", @@ -4256,6 +4786,12 @@ "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" }, + "goober": { + "version": "2.1.14", + "resolved": "https://registry.npmjs.org/goober/-/goober-2.1.14.tgz", + "integrity": "sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==", + "requires": {} + }, "gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -4333,6 +4869,14 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "invariant": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", + "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4374,6 +4918,11 @@ "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "devOptional": true }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -4428,8 +4977,12 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==" }, "lodash.includes": { "version": "4.3.0", @@ -4470,8 +5023,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "dev": true, - "peer": true, "requires": { "js-tokens": "^3.0.0 || ^4.0.0" } @@ -4531,11 +5082,43 @@ "brace-expansion": "^1.1.7" } }, + "minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==" + }, + "mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "requires": { + "minimist": "^1.2.6" + } + }, + "modules": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/modules/-/modules-0.4.0.tgz", + "integrity": "sha512-LX4JgwPHJr1FurPDKp1BlGgMXqZXtxO1O8ABGmj2g15CbLGlInTHcA9flqw6uN6oYKE2T0ngWdiHvcX97mdBsw==" + }, "ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "multer": { + "version": "1.4.5-lts.1", + "resolved": "https://registry.npmjs.org/multer/-/multer-1.4.5-lts.1.tgz", + "integrity": "sha512-ywPWvcDMeH+z9gQq5qYHCCy+ethsk4goepZ45GLD63fOu0YcNecQxi64nDs3qluZB+murG3/D4dJ7+dGctcCQQ==", + "requires": { + "append-field": "^1.0.0", + "busboy": "^1.0.0", + "concat-stream": "^1.5.2", + "mkdirp": "^0.5.4", + "object-assign": "^4.1.1", + "type-is": "^1.6.4", + "xtend": "^4.0.0" + } + }, "nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -4650,6 +5233,21 @@ "source-map-js": "^1.0.2" } }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "requires": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, "proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -4665,6 +5263,11 @@ "integrity": "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w==", "dev": true }, + "q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha512-kV/CThkXo6xyFEZUugw/+pIOywXcDbFYgSct5cT3gqlbkBE1SJdwy6UQoZvodiWF/ckQLZyDE/Bu1M6gVu5lVw==" + }, "qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -4693,23 +5296,93 @@ "version": "18.2.0", "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", - "dev": true, "peer": true, "requires": { "loose-envify": "^1.1.0" } }, + "react-bootstrap-typeahead": { + "version": "6.3.2", + "resolved": "https://registry.npmjs.org/react-bootstrap-typeahead/-/react-bootstrap-typeahead-6.3.2.tgz", + "integrity": "sha512-N5Mb0WlSSMcD7Z0pcCypILgIuECybev0hl4lsnCa5lbXTnN4QdkuHLGuTLSlXBwm1ZMFpOc2SnsdSRgeFiF+Ow==", + "requires": { + "@babel/runtime": "^7.14.6", + "@popperjs/core": "^2.10.2", + "@restart/hooks": "^0.4.0", + "classnames": "^2.2.0", + "fast-deep-equal": "^3.1.1", + "invariant": "^2.2.1", + "lodash.debounce": "^4.0.8", + "prop-types": "^15.5.8", + "react-overlays": "^5.2.0", + "react-popper": "^2.2.5", + "scroll-into-view-if-needed": "^3.1.0", + "warning": "^4.0.1" + } + }, "react-dom": { "version": "18.2.0", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", - "dev": true, "peer": true, "requires": { "loose-envify": "^1.1.0", "scheduler": "^0.23.0" } }, + "react-fast-compare": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", + "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==" + }, + "react-hot-toast": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz", + "integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==", + "requires": { + "goober": "^2.1.10" + } + }, + "react-icons": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", + "integrity": "sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==", + "requires": {} + }, + "react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "react-lifecycles-compat": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", + "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" + }, + "react-overlays": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/react-overlays/-/react-overlays-5.2.1.tgz", + "integrity": "sha512-GLLSOLWr21CqtJn8geSwQfoJufdt3mfdsnIiQswouuQ2MMPns+ihZklxvsTDKD3cR2tF8ELbi5xUsvqVhR6WvA==", + "requires": { + "@babel/runtime": "^7.13.8", + "@popperjs/core": "^2.11.6", + "@restart/hooks": "^0.4.7", + "@types/warning": "^3.0.0", + "dom-helpers": "^5.2.0", + "prop-types": "^15.7.2", + "uncontrollable": "^7.2.1", + "warning": "^4.0.3" + } + }, + "react-popper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz", + "integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==", + "requires": { + "react-fast-compare": "^3.0.1", + "warning": "^4.0.2" + } + }, "react-refresh": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", @@ -4734,6 +5407,27 @@ "react-router": "6.18.0" } }, + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -4746,8 +5440,7 @@ "regenerator-runtime": { "version": "0.14.0", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz", - "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==", - "dev": true + "integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==" }, "require-directory": { "version": "2.1.1", @@ -4811,12 +5504,19 @@ "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", - "dev": true, "peer": true, "requires": { "loose-envify": "^1.1.0" } }, + "scroll-into-view-if-needed": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz", + "integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==", + "requires": { + "compute-scroll-into-view": "^3.0.2" + } + }, "semver": { "version": "7.5.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", @@ -4930,6 +5630,26 @@ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "requires": { + "safe-buffer": "~5.1.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + } + } + }, "string-width": { "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", @@ -5008,6 +5728,22 @@ "mime-types": "~2.1.24" } }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" + }, + "uncontrollable": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/uncontrollable/-/uncontrollable-7.2.1.tgz", + "integrity": "sha512-svtcfoTADIB0nT9nltgjujTi7BzVmwjZClOmskKu/E8FW9BXzg9os8OLr4f8Dlnk0rYWJIWr4wv9eKUXiQvQwQ==", + "requires": { + "@babel/runtime": "^7.6.3", + "@types/react": ">=16.9.11", + "invariant": "^2.2.4", + "react-lifecycles-compat": "^3.0.4" + } + }, "undefsafe": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/undefsafe/-/undefsafe-2.0.5.tgz", @@ -5028,6 +5764,11 @@ "picocolors": "^1.0.0" } }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, "utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", @@ -5049,6 +5790,14 @@ "rollup": "^4.2.0" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "wrap-ansi": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", @@ -5060,6 +5809,11 @@ "strip-ansi": "^6.0.0" } }, + "xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" + }, "y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index 78768df..21e5f0f 100644 --- a/package.json +++ b/package.json @@ -28,10 +28,17 @@ }, "dependencies": { "@vitejs/plugin-react": "^4.2.1", + "cloudinary": "^2.0.3", "cors": "^2.8.5", + "dotenv": "^16.4.5", "express": "^4.18.2", "jsonwebtoken": "^9.0.2", "man": "^2.0.0", + "modules": "^0.4.0", + "multer": "^1.4.5-lts.1", + "react-bootstrap-typeahead": "^6.3.2", + "react-hot-toast": "^2.4.1", + "react-icons": "^5.2.1", "vite": "^5.0.12" } } diff --git a/server/db.js b/server/db.js index 7541cf7..6f76e93 100644 --- a/server/db.js +++ b/server/db.js @@ -9,7 +9,7 @@ const pool = new Pool( host: process.env.PGHOST, database: process.env.PGDATABASE, password: process.env.PGPASSWORD, - port: process.env.PGPORT, + port: process.env.PGPORT, } ) diff --git a/server/index.js b/server/index.js index 3fd3e4e..42ddbdb 100644 --- a/server/index.js +++ b/server/index.js @@ -6,13 +6,18 @@ const userRoutes = require("./src/users/routes.js"); const auth_userRoutes = require("./src/users/authroutes.js"); const bookmarkRoutes = require("./src/bookmarks/authroutes.js") +const imagesRoutes = require("./src/images/routes.js"); const cors = require("cors"); require("dotenv").config(); +console.log(process.env.PGUSER); + const app = express(); +// const number = 8080 const port = process.env.PORT; +// const port1= 8080 app.use(cors()); app.use(express.json()); @@ -31,5 +36,6 @@ app.use("/api/courses", courseRoutes); app.use('/api/users', userRoutes); app.use('/api/users', auth_userRoutes); +app.use("/api/images", imagesRoutes); // images routes -app.listen(port, () => console.log(`app listening on port ${port}`)); +app.listen(port, () => console.log(`app listening on port ${port}`)); \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 10864c4..0263623 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -16,6 +16,7 @@ "express": "^4.18.2", "firebase-admin": "^12.0.0", "jsonwebtoken": "^9.0.2", + "modules": "^0.4.0", "oauth": "^0.10.0", "pg": "^8.11.3" } @@ -1893,6 +1894,14 @@ "node": ">=10" } }, + "node_modules/modules": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/modules/-/modules-0.4.0.tgz", + "integrity": "sha512-LX4JgwPHJr1FurPDKp1BlGgMXqZXtxO1O8ABGmj2g15CbLGlInTHcA9flqw6uN6oYKE2T0ngWdiHvcX97mdBsw==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", @@ -4281,6 +4290,11 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" }, + "modules": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/modules/-/modules-0.4.0.tgz", + "integrity": "sha512-LX4JgwPHJr1FurPDKp1BlGgMXqZXtxO1O8ABGmj2g15CbLGlInTHcA9flqw6uN6oYKE2T0ngWdiHvcX97mdBsw==" + }, "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", diff --git a/server/package.json b/server/package.json index 10d1d7c..ef7459a 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "express": "^4.18.2", "firebase-admin": "^12.0.0", "jsonwebtoken": "^9.0.2", + "modules": "^0.4.0", "oauth": "^0.10.0", "pg": "^8.11.3" } diff --git a/server/src/articles/controller.js b/server/src/articles/controller.js index f9e3d3c..11a572a 100644 --- a/server/src/articles/controller.js +++ b/server/src/articles/controller.js @@ -4,6 +4,7 @@ const queries = require("./queries"); const getArticles = async (req, res) => { console.log("GET ARTICLES"); if (req.user) { + console.log("WITH USER") if (req.query.title) { const articles = await pool.query(queries.auth_getArticlesByTitle.replace('$1',req.query.title).replace('$2',req.user.id),(error, results) => { if (error) { @@ -22,6 +23,7 @@ const getArticles = async (req, res) => { }); } } else { + console.log("WITHOUT USER") if (req.query.title) { pool.query(queries.getArticlesByTitle.replace('$1',req.query.title),(error, results) => { if (error) { @@ -70,7 +72,6 @@ const addArticles = (req, res) => { const { title,headers,author_id } = req.body; let headers_json = JSON.stringify(headers); - pool.query(queries.addArticles, [title, author_id,headers_json], (error, results) => { if (error) { console.error(error); diff --git a/server/src/articles/routes.js b/server/src/articles/routes.js index 73c9d28..9861f9e 100644 --- a/server/src/articles/routes.js +++ b/server/src/articles/routes.js @@ -1,7 +1,7 @@ const { Router } = require("express"); const controller = require("./controller"); -//const authorizeArticle = require('../middleware/authorizeArticle') +const authorizeArticle = require('../middleware/authorizeArticle') const optionalAuth = require('../middleware/optionalAuth'); const router = Router(); @@ -12,4 +12,7 @@ router.use(optionalAuth); router.get("/", async (req, res) => controller.getArticles(req, res)); router.get("/:id", (req, res) => controller.getArticlesById(req, res)); +router.post("/", (req, res) => controller.addArticles(req, res)); +router.delete('/:id', authorizeArticle, controller.deleteArticle); + module.exports = router; diff --git a/server/src/images/config.js b/server/src/images/config.js new file mode 100644 index 0000000..3e7e7be --- /dev/null +++ b/server/src/images/config.js @@ -0,0 +1,18 @@ +const cloudinary = require('cloudinary').v2; +require("dotenv").config(); +// export const cloudinary1 = cloudinary +// transit the data into .env +cloudinary.config({ + cloud_name: pocess.env.CLOUD_NAME, + api_key: process.env.API_KEY, + api_secret: process.env.API_SECRET +}); + + +process.env.PGUSER, + +module.exports = { + cloudinary1:cloudinary +} + + diff --git a/server/src/images/controller.js b/server/src/images/controller.js new file mode 100644 index 0000000..9d13cd1 --- /dev/null +++ b/server/src/images/controller.js @@ -0,0 +1,103 @@ +const { createSearchParams } = require("react-router-dom") +const { cloudinary1 } = require("./config"); +const { upload } = require("./multer"); + +const getImages = async (req, res) => { + try { + const images = await cloudinary1.api.resources(); + res.json(images); + console.log(images); + } + catch (err) { + console.error(err); + res.status(500).message(err.message); + } +} + +const getImagesById = async (req, res) => { + const {id} = req.params; + try { + const image = await cloudinary1.api.resource(id); + res.json(image); + } + catch (err) { + console.log(err); + res.status(404).message(err.message); + } + +} + +const postImage = async (req, res) => { + try { + // Use multer's single() middleware to handle single file upload with field name 'image' + upload.single('image')(req, res, async (err) => { + if (err) { + console.error(err); + return res.status(400).json({ + success: false, + message: 'Error uploading image' + }); + } + + // If multer successfully parsed the file, you can proceed with Cloudinary upload + try { + const result = await cloudinary1.uploader.upload(req.file.path); + res.status(200).json({ + success: true, + message: 'Image uploaded successfully', + data: result // Optionally, you can send uploaded image data back to the client + }); + } catch (uploadError) { + console.error(uploadError); + res.status(500).json({ + success: false, + message: 'Error uploading image to Cloudinary' + }); + } + }); + } catch (multerError) { + console.error(multerError); + res.status(500).json({ + success: false, + message: 'Error processing file upload' + }); + } +}; +const deleteImageById = async (req, res) => { + const {id} = req.params; + try { + const image = await cloudinary1.api.resource(id); + if (!image) { + res.status(404).json({ + message: 'Image not found' + }) + } + else { + await cloudinary1.api.delete_resources(id) + .then(resp => { + console.log(resp); + res.status(200).json({ + message: "the image was successfully deleted", + }); + }) + .catch(err => { + console.log(err); + res.status(500).json({ message: "there has been an error deleting the image", }) + + }); + } + } + catch (err) { + console.error(err); + res.status(500).json({ message: err.message }); + + } +} + + +module.exports = { + getImages, + getImagesById, + postImage, + deleteImageById +} \ No newline at end of file diff --git a/server/src/images/multer.js b/server/src/images/multer.js new file mode 100644 index 0000000..2544c91 --- /dev/null +++ b/server/src/images/multer.js @@ -0,0 +1,12 @@ +const multer = require ("multer") +// multer middleware to handle files +const storage = multer.diskStorage({ + filename: function(req,file,cb){ + cb(null, file.originalname) + } +}); +// export const upload = multer({storage: storage}); + +module.exports = { + upload:multer({storage: storage}) +} \ No newline at end of file diff --git a/server/src/images/routes.js b/server/src/images/routes.js new file mode 100644 index 0000000..2dc4160 --- /dev/null +++ b/server/src/images/routes.js @@ -0,0 +1,11 @@ +const {Router} = require("express") +const controller = require("./controller"); +const upload = require("multer") +const router = new Router(); + +router.get("/" , controller.getImages); +router.get("/:id", controller.getImagesById); +router.post("/" , controller.postImage); +router.delete("/:id", controller.deleteImageById); + +module.exports = router; \ No newline at end of file diff --git a/server/src/users/controller.js b/server/src/users/controller.js index 06d7c84..18ff695 100644 --- a/server/src/users/controller.js +++ b/server/src/users/controller.js @@ -39,7 +39,15 @@ const createUser = async (req, res) => { const salt = await bcrypt.genSalt() const hashedPassword = await bcrypt.hash(req.body.user_password, salt) const { username } = req.body; - pool.query(queries.createUser, [username, hashedPassword]) + pool.query(queries.createUser, [username, hashedPassword], (err, results) => { + if(err){ + console.error(err); + return res.status(500).json({ + + }) + } + + }) const token = createToken(username);