From 97d0e410eb74649e6c2ba132da996cdb9febeaaf Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Mon, 19 Feb 2024 21:21:14 -0500 Subject: [PATCH 1/7] setup frontend api and state for geese features --- src/frontend/api/geese-features.js | 17 +++++++++++++++++ src/frontend/api/index.js | 4 +++- src/frontend/hooks/geese-features.js | 1 + src/frontend/state/action-types.js | 4 +++- src/frontend/state/geese-features/actions.js | 6 ++++++ src/frontend/state/geese-features/reducer.js | 19 +++++++++++++++++++ .../state/geese-features/selectors.js | 1 + src/frontend/state/reducer.js | 4 +++- 8 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 src/frontend/api/geese-features.js create mode 100644 src/frontend/hooks/geese-features.js create mode 100644 src/frontend/state/geese-features/actions.js create mode 100644 src/frontend/state/geese-features/reducer.js create mode 100644 src/frontend/state/geese-features/selectors.js diff --git a/src/frontend/api/geese-features.js b/src/frontend/api/geese-features.js new file mode 100644 index 0000000..6ceddd7 --- /dev/null +++ b/src/frontend/api/geese-features.js @@ -0,0 +1,17 @@ +const getGeeseFeatures = (server) => () => server.get('/api/geese-features'); +const getGeeseFeatureById = (server) => (id) => + server.get(`/api/geese-features/${id}`); +const addGeeseFeature = (server) => (featureInfo) => + server.post(`/api/geese-features`, featureInfo); +const updateGeeseFeature = (server) => (id, updatedFeatureInfo) => + server.patch(`/api/geese-features/${id}`, updatedFeatureInfo); +const deleteGeeseFeature = (server) => (id) => + server.delete(`/api/geese-features/${id}`); + +export default (server) => ({ + getGeeseFeatures: getGeeseFeatures(server), + getGeeseFeatureById: getGeeseFeatureById(server), + addGeeseFeature: addGeeseFeature(server), + updateGeeseFeature: updateGeeseFeature(server), + deleteGeeseFeature: deleteGeeseFeature(server), +}); diff --git a/src/frontend/api/index.js b/src/frontend/api/index.js index 85e794e..d30a881 100644 --- a/src/frontend/api/index.js +++ b/src/frontend/api/index.js @@ -7,6 +7,7 @@ import openingsDescription from './openings-description'; import sponsors from './sponsors'; import geeseInfo from './geese-info'; import blogs from './blogs'; +import geeseFeatures from './geese-features' export default { google: google(server), @@ -16,5 +17,6 @@ export default { formUpload: formUpload(server), openingsDescription: openingsDescription(server), geeseInfo: geeseInfo(server), - blogs: blogs(server) + blogs: blogs(server), + geeseFeatures: geeseFeatures(server) }; diff --git a/src/frontend/hooks/geese-features.js b/src/frontend/hooks/geese-features.js new file mode 100644 index 0000000..a6bc370 --- /dev/null +++ b/src/frontend/hooks/geese-features.js @@ -0,0 +1 @@ +import {useEffect} from 'react' \ No newline at end of file diff --git a/src/frontend/state/action-types.js b/src/frontend/state/action-types.js index aa242d3..ec513e1 100644 --- a/src/frontend/state/action-types.js +++ b/src/frontend/state/action-types.js @@ -14,4 +14,6 @@ export const SPONSORS_SET_SPONSOR_TIERS = 'SPONSORS_SET_SPONSOR_TIERS'; export const GEESE_INFO_SET_GEESE_INFO = 'GEESE_INFO_SET_GEESE_INFO'; -export const UPDATE_BLOG_INFO = 'UPDATE_BLOG_INFO'; \ No newline at end of file +export const UPDATE_BLOG_INFO = 'UPDATE_BLOG_INFO'; + +export const GEESE_FEATURES_SET_GEESE_FEATURE = 'GEESE_FEATURES_SET_GEESE_FEATURE' \ No newline at end of file diff --git a/src/frontend/state/geese-features/actions.js b/src/frontend/state/geese-features/actions.js new file mode 100644 index 0000000..cc6f529 --- /dev/null +++ b/src/frontend/state/geese-features/actions.js @@ -0,0 +1,6 @@ +import * as actionTypes from '../action-types' + +export const updateGeeseFeatures = (geeseFeatures) => ({ + type: actionTypes.GEESE_FEATURES_SET_GEESE_FEATURE, + payload: geeseFeatures +}) \ No newline at end of file diff --git a/src/frontend/state/geese-features/reducer.js b/src/frontend/state/geese-features/reducer.js new file mode 100644 index 0000000..c578b6c --- /dev/null +++ b/src/frontend/state/geese-features/reducer.js @@ -0,0 +1,19 @@ +import * as actionTypes from '../action-types'; + +const initialState = { + allGeeseFeatures: [], +}; + +const reducer = (state = initialState, action) => { + switch (action.type) { + case actionTypes.GEESE_FEATURES_SET_GEESE_FEATURE: + return { + ...state, + allGeeseFeatures: action.payload, + }; + default: + return state; + } +}; + +export default reducer; diff --git a/src/frontend/state/geese-features/selectors.js b/src/frontend/state/geese-features/selectors.js new file mode 100644 index 0000000..6c3f019 --- /dev/null +++ b/src/frontend/state/geese-features/selectors.js @@ -0,0 +1 @@ +export const allGeeseFeatures = (state) => state.geeseFeatures.allGeeseFeatures \ No newline at end of file diff --git a/src/frontend/state/reducer.js b/src/frontend/state/reducer.js index e3e9d81..d91aa49 100644 --- a/src/frontend/state/reducer.js +++ b/src/frontend/state/reducer.js @@ -5,6 +5,7 @@ import teamsReducer from './teams/reducer'; import sponsorsReducer from './sponsors/reducer'; import geeseInfoReducer from './geese-info/reducer'; import blogInfoReducer from './blogs/reducer'; +import geeseFeaturesReducer from './geese-features/reducer' export default combineReducers({ user: userReducer, @@ -12,5 +13,6 @@ export default combineReducers({ teams: teamsReducer, sponsors: sponsorsReducer, geeseInfo: geeseInfoReducer, - blogInfo: blogInfoReducer + blogInfo: blogInfoReducer, + geeseFeatures: geeseFeaturesReducer }); From 22983a530276909ad4dd0a7f5b0fab26da88c9a8 Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Fri, 23 Feb 2024 21:49:21 -0500 Subject: [PATCH 2/7] add geese-features hook --- src/frontend/hooks/geese-features.js | 32 +++++++++++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/src/frontend/hooks/geese-features.js b/src/frontend/hooks/geese-features.js index a6bc370..6cbe0db 100644 --- a/src/frontend/hooks/geese-features.js +++ b/src/frontend/hooks/geese-features.js @@ -1 +1,31 @@ -import {useEffect} from 'react' \ No newline at end of file +import { useEffect } from 'react'; +import api from '../api'; +import { useDispatch, useSelector } from 'react-redux'; +import * as geeseFeaturesActions from '../state/geese-features/actions'; +import * as geeseFeaturesSelectors from '../state/geese-features/selectors'; + +const useGeeseFeatures = () => { + const dispatch = useDispatch(); + const geeseFeatures = useSelector(geeseFeaturesSelectors.allGeeseFeatures).features; + + useEffect(() => { + (async () => { + try { + const geeseFeaturesResponse = + await api.geeseFeatures.getGeeseFeatures(); + + const newGeeseFeatures = geeseFeaturesResponse.data; + + dispatch(geeseFeaturesActions.updateGeeseFeatures(newGeeseFeatures)); + } catch (error) { + console.log(error); + } + })(); + }, [dispatch]); + + return { + geeseFeatures, + }; +}; + +export default useGeeseFeatures; From f83f748f64f2f74aabcddc4b5887c268087564b3 Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Sun, 25 Feb 2024 22:09:41 -0500 Subject: [PATCH 3/7] add saveForm for features --- src/frontend/hooks/geese-features-form.js | 131 ++++++++++++++++++++ src/frontend/pages/features/FeaturesPage.js | 2 + 2 files changed, 133 insertions(+) create mode 100644 src/frontend/hooks/geese-features-form.js diff --git a/src/frontend/hooks/geese-features-form.js b/src/frontend/hooks/geese-features-form.js new file mode 100644 index 0000000..c7262a6 --- /dev/null +++ b/src/frontend/hooks/geese-features-form.js @@ -0,0 +1,131 @@ +import { useState, useEffect, useCallback } from 'react'; +import { useHistory, useParams } from 'react-router-dom'; +import useGeeseFeatures from '../hooks/geese-features'; +import api from '../api'; +import moment from 'moment'; + +const useGeeseFeaturesForm = () => { + const [featureName, setFeatureName] = useState(''); + const [picture, setPicture] = useState(''); + const [pictureUrl, setPictureUrl] = useState(''); + const [description, setDescription] = useState(''); + const [showModal, setShowModal] = useState(false); + + const history = useHistory(); + const params = useParams(); + + const { geeseFeatures } = useGeeseFeatures(); + + useEffect(() => { + if (geeseFeatures.length > 0) { + (async () => { + try { + if (params.featureId) { + const featureId = parseInt(params.featureId, 10); + + const feature = geeseFeatures.find( + (feature) => feature.id === featureId, + ); + + if (feature) { + setFeatureName(feature.featureName); + setPictureUrl(feature.picture); + setDescription(feature.description); + } + } + } catch (error) { + console.log(error); + } + })(); + } + }, [ + setFeatureName, + setPicture, + setPictureUrl, + setDescription, + params, + geeseFeatures, + ]); + + const imageUpload = useCallback( + (image) => { + setPicture(image); + }, + [setPicture], + ); + + const imageDelete = useCallback(() => { + setPicture(''); + }, [setPicture]); + + const imageUrlDelete = useCallback(() => { + setPictureUrl(''); + }, [setPictureUrl]); + + const openModal = () => { + setShowModal(true); + }; + + const closeModal = () => { + setShowModal(false); + }; + + const closeForm = useCallback(() => { + history.push('/features'); + }, [history]); + + const saveForm = useCallback(async () => { + try { + let newPictureUrl = pictureUrl; + + if (picture instanceof File) { + const formData = new FormData(); + formData.append('image', picture, picture.name); + const response = await api.formUpload(formData); + + if (!response.data.data) { + throw new Error('Error uploading image'); + } + + newPictureUrl = response.data.data[0]; + } + + const feature = { + name: featureName, + picture: newPictureUrl, + description, + }; + + if (featureName && picture && description) { + if (params.featureId) { + await api.geeseFeatures.addGeeseFeature({ ...feature }); + } else { + await api.geeseFeatures.updateGeeseFeature(params.featureId, { + ...feature, + }); + } + } else { + throw new Error('Fields must be non empty'); + } + } catch (error) { + throw new Error(error); + } + }, [featureName, picture, pictureUrl, description, params]); + + const deleteForm = useCallback(async (featureId) => { + await api.geeseFeatures.deleteGeeseFeature(featureId); + }, [params.featureId, closeForm]); + + return { + featureName, + setFeatureName, + picture, + setPicture, + pictureUrl, + setPictureUrl, + description, + setDescription, + }; +}; + +export default useGeeseFeaturesForm; diff --git a/src/frontend/pages/features/FeaturesPage.js b/src/frontend/pages/features/FeaturesPage.js index 05ea14c..e72f401 100644 --- a/src/frontend/pages/features/FeaturesPage.js +++ b/src/frontend/pages/features/FeaturesPage.js @@ -1,6 +1,8 @@ import React from 'react' import FeaturePreview from './components/FeaturePreview' import useFeatures from './hooks/features'; +import useGeeseFeatures from '../../hooks/geese-features'; +import useGeeseFeaturesForm from '../../hooks/geese-features-form'; const FeaturesPage = () => { const { From 0e3da636d62cbccddabcec8ee993caa63a1f433c Mon Sep 17 00:00:00 2001 From: Kevin Lau Date: Wed, 13 Mar 2024 21:41:27 -0400 Subject: [PATCH 4/7] finish save and add feature functionality --- src/frontend/App.js | 6 +- src/frontend/hooks/geese-features-form.js | 48 +++--- src/frontend/hooks/geese-features.js | 14 +- src/frontend/pages/features/EditFeature.js | 151 ++++++++++++++++++ src/frontend/pages/features/Feature.router.js | 23 +++ src/frontend/pages/features/FeaturesPage.js | 67 ++++++-- .../features/components/FeaturePreview.js | 17 +- .../pages/sponsors/forms/EditSponsors.js | 1 + 8 files changed, 277 insertions(+), 50 deletions(-) create mode 100644 src/frontend/pages/features/EditFeature.js create mode 100644 src/frontend/pages/features/Feature.router.js diff --git a/src/frontend/App.js b/src/frontend/App.js index 272194d..a07e42b 100644 --- a/src/frontend/App.js +++ b/src/frontend/App.js @@ -6,7 +6,6 @@ import Cookies from 'js-cookie'; import * as userSelectors from './state/user/selectors'; import TopBar from './components/TopBar'; import LandingPage from './pages/landing/LandingPage'; -import FeaturesPage from './pages/features/FeaturesPage'; import SignInPage from './pages/sign-in/SignInPage'; import NotFoundPage from './pages/NotFound'; import PostingsRouter from './pages/postings/Postings.router'; @@ -15,6 +14,7 @@ import GeeseRouter from './pages/geese/Geese.router'; import TeamDescriptionsRouter from './pages/team-descriptions/TeamDescriptions.router'; import { addAuthTokenToRequests } from './api/server'; import BlogsRouter from './pages/blogs/Blogs.Router'; +import FeaturesRouter from './pages/features/Feature.router'; const App = () => { let token = useSelector(userSelectors.token) || Cookies.get('tokenId'); @@ -39,11 +39,11 @@ const App = () => { - +
{!token && } - +
diff --git a/src/frontend/hooks/geese-features-form.js b/src/frontend/hooks/geese-features-form.js index c7262a6..6914ade 100644 --- a/src/frontend/hooks/geese-features-form.js +++ b/src/frontend/hooks/geese-features-form.js @@ -17,7 +17,7 @@ const useGeeseFeaturesForm = () => { const { geeseFeatures } = useGeeseFeatures(); useEffect(() => { - if (geeseFeatures.length > 0) { + if (geeseFeatures?.length > 0) { (async () => { try { if (params.featureId) { @@ -28,9 +28,10 @@ const useGeeseFeaturesForm = () => { ); if (feature) { - setFeatureName(feature.featureName); + setFeatureName(feature.name); setPictureUrl(feature.picture); setDescription(feature.description); + console.log(feature) } } } catch (error) { @@ -47,21 +48,16 @@ const useGeeseFeaturesForm = () => { geeseFeatures, ]); - const imageUpload = useCallback( - (image) => { - setPicture(image); - }, - [setPicture], - ); + const imageUpload = (image) => { + setPicture(image); + setPictureUrl(URL.createObjectURL(image)); + }; const imageDelete = useCallback(() => { setPicture(''); + setPictureUrl('') }, [setPicture]); - const imageUrlDelete = useCallback(() => { - setPictureUrl(''); - }, [setPictureUrl]); - const openModal = () => { setShowModal(true); }; @@ -80,7 +76,7 @@ const useGeeseFeaturesForm = () => { if (picture instanceof File) { const formData = new FormData(); - formData.append('image', picture, picture.name); + formData.append('files', picture, picture.name); const response = await api.formUpload(formData); if (!response.data.data) { @@ -96,26 +92,23 @@ const useGeeseFeaturesForm = () => { description, }; - if (featureName && picture && description) { - if (params.featureId) { - await api.geeseFeatures.addGeeseFeature({ ...feature }); + if (featureName && newPictureUrl && description) { + if (!params.featureId) { + await api.geeseFeatures.addGeeseFeature(feature); } else { - await api.geeseFeatures.updateGeeseFeature(params.featureId, { - ...feature, - }); + await api.geeseFeatures.updateGeeseFeature(params.featureId, feature); } } else { throw new Error('Fields must be non empty'); } } catch (error) { - throw new Error(error); + console.log(error) + } finally{ + closeForm() + } }, [featureName, picture, pictureUrl, description, params]); - const deleteForm = useCallback(async (featureId) => { - await api.geeseFeatures.deleteGeeseFeature(featureId); - }, [params.featureId, closeForm]); - return { featureName, setFeatureName, @@ -125,6 +118,13 @@ const useGeeseFeaturesForm = () => { setPictureUrl, description, setDescription, + imageUpload, + imageDelete, + saveForm, + closeForm, + showModal, + openModal, + closeModal, }; }; diff --git a/src/frontend/hooks/geese-features.js b/src/frontend/hooks/geese-features.js index 6cbe0db..4c8eb4a 100644 --- a/src/frontend/hooks/geese-features.js +++ b/src/frontend/hooks/geese-features.js @@ -1,4 +1,4 @@ -import { useEffect } from 'react'; +import { useCallback, useEffect } from 'react'; import api from '../api'; import { useDispatch, useSelector } from 'react-redux'; import * as geeseFeaturesActions from '../state/geese-features/actions'; @@ -6,7 +6,9 @@ import * as geeseFeaturesSelectors from '../state/geese-features/selectors'; const useGeeseFeatures = () => { const dispatch = useDispatch(); - const geeseFeatures = useSelector(geeseFeaturesSelectors.allGeeseFeatures).features; + const geeseFeatures = useSelector( + geeseFeaturesSelectors.allGeeseFeatures, + ).features; useEffect(() => { (async () => { @@ -23,8 +25,16 @@ const useGeeseFeatures = () => { })(); }, [dispatch]); + const deleteFeature = useCallback(async (featureId) => { + await api.geeseFeatures.deleteGeeseFeature(featureId); + const geeseFeaturesResponse = await api.geeseFeatures.getGeeseFeatures(); + const newGeeseFeatures = geeseFeaturesResponse.data; + dispatch(geeseFeaturesActions.updateGeeseFeatures(newGeeseFeatures)); + }, []); + return { geeseFeatures, + deleteFeature, }; }; diff --git a/src/frontend/pages/features/EditFeature.js b/src/frontend/pages/features/EditFeature.js new file mode 100644 index 0000000..b0268f4 --- /dev/null +++ b/src/frontend/pages/features/EditFeature.js @@ -0,0 +1,151 @@ +import React, { useMemo } from 'react'; +import ImagePreview from '../../components/ImagePreview/index'; +import FormContainer from '../../components/FormContainer/index'; +import Button from '../../components/Button/index'; +import TextInput from '../../components/TextInput/index'; +import useFeaturesForm from '../../hooks/geese-features-form'; +import styled from 'styled-components'; + +const EditFeaturePage = styled.div` + padding: 64px 88px 65px 58px; +`; + +const TopRow = styled.div` + display: flex; +`; + +const LastUpdated = styled.div` + font-family: IBM Plex Sans; + font-size: 18px; + font-weight: 500; + text-align: right; + flex: auto; +`; + +const EditFeatureBody = styled.div` + padding-left: 30px; +`; + +const FeatureInfo = styled.div` + padding-left: 10px; +`; + +const FeatureImages = styled.div` + margin-bottom: 60px; +`; + +const ImagesText = styled.div` + font-family: IBM Plex Sans; + font-size: 18px; + color: #232535; + margin: 0; +`; + +const ImageCard = styled.div` + margin-right: 18px; +`; + +const ImageCards = styled.div` + padding-top: 20px; + display: flex; +`; + +const FeatureButtons = styled.div` + padding-left: 10px; +`; + +const EditFeature = ({ add }) => { + const { + featureName, + setFeatureName, + picture, + setPicture, + pictureUrl, + setPictureUrl, + description, + setDescription, + imageUpload, + imageDelete, + saveForm, + deleteForm, + showModal, + openModal, + closeModal, + closeForm + } = useFeaturesForm(); + + + const displayImages = useMemo(() => { + if (pictureUrl){ + return [ + + { + imageDelete(); + }} + /> + + ]}; + + }, [picture, pictureUrl]); + + return ( + + + + {/* {`Last updated: ${getLastUpdated()}`} */} + + + + + + + + + + + + + + + + + + If this Feature is not selected as the "current Feature", only + the leftmost image will be displayed. + + + The images can be rearranged by dragging them into the desired + order. + + + {displayImages} + + + + + + + +