diff --git a/client/src/App.js b/client/src/App.js index d5b5aaf..9e6a0ec 100644 --- a/client/src/App.js +++ b/client/src/App.js @@ -6,8 +6,13 @@ import { useSelector } from 'react-redux'; import { CssBaseline, ThemeProvider } from '@mui/material'; import { createTheme } from '@mui/material/styles'; import { themeSettings } from 'theme'; -import HomePage from 'scenes/homePage'; + import LandingPage from 'scenes/landingPage'; +import ProfilePage from 'scenes/profilePage'; +import EventsPage from 'scenes/eventsPage'; +import HomePage from 'scenes/homePage'; +import EventDetailsPage from 'scenes/eventDetailsPage'; +import EventForm from 'scenes/eventForm'; const App = () => { const mode = useSelector((state) => state.mode); @@ -20,9 +25,31 @@ const App = () => { - } /> - } /> - : } /> + } /> + : } + /> + : } + /> + : } + /> + : } + /> + : } + /> + : } + /> diff --git a/client/src/assets/about.jpg b/client/src/assets/about.jpg new file mode 100644 index 0000000..35ed493 Binary files /dev/null and b/client/src/assets/about.jpg differ diff --git a/client/src/components/Follower.jsx b/client/src/components/Follower.jsx new file mode 100644 index 0000000..c94875a --- /dev/null +++ b/client/src/components/Follower.jsx @@ -0,0 +1,84 @@ +import { PersonAddOutlined, PersonRemoveOutlined } from "@mui/icons-material"; +import { Box, Typography, IconButton, useTheme } from "@mui/material"; +import { useDispatch, useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import { setFollowers } from "state"; +import FlexBetween from "./FlexBetween"; +import UserImage from "./UserImage"; + +const Follower = ({ followerId, name, subtitle, userPicturePath }) => { + const dispatch = useDispatch(); + const navigate = useNavigate(); + const { _id } = useSelector((state) => state.user); + const token = useSelector((state) => state.token); + const followers = useSelector((state) => state.user.followers); + + const { palette } = useTheme(); + const primaryLight = palette.primary.light; + const primaryDark = palette.primary.dark; + const main = palette.neutral.main; + const medium = palette.neutral.medium; + + const isFollower = followers.find((follower) => follower._id === followerId); + + const patchFollower = async () => { + const response = await fetch( + `http://localhost:3001/users/${_id}/${followerId}`, + { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + } + ); + const data = await response.json(); + dispatch(setFollowers({ followers: data })); + }; + + return ( + + + + { + navigate(`/profile/${followerId}`); + navigate(0); + }} + > + + {name} + + + {subtitle} + + + + patchFollower()} + sx={{ backgroundColor: primaryLight, p: "0.6rem" }} + > + {isFollower ? ( + + ) : ( + + )} + + + ); + }; + + export default Follower; + + + diff --git a/client/src/components/SearchBar.jsx b/client/src/components/SearchBar.jsx new file mode 100644 index 0000000..28738b3 --- /dev/null +++ b/client/src/components/SearchBar.jsx @@ -0,0 +1,78 @@ +import React, { useState } from 'react'; +import { InputBase, IconButton, Paper, Popper, Typography, Box } from '@mui/material'; +import { Search as SearchIcon } from '@mui/icons-material'; +import { useNavigate } from 'react-router-dom'; +import { useSelector } from 'react-redux'; + +const SearchBar = () => { + const [searchTerm, setSearchTerm] = useState(''); + const [searchResults, setSearchResults] = useState(null); + const [anchorEl, setAnchorEl] = useState(null); + const navigate = useNavigate(); + const token = useSelector((state) => state.token); + + const handleSearch = async (event) => { + event.preventDefault(); + setAnchorEl(event.currentTarget); + + try { + const response = await fetch(`http://localhost:3001/search?query=${searchTerm}`, { + method: 'GET', + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + setSearchResults(data); + } catch (error) { + console.error('Error searching:', error); + } + }; + + const handleResultClick = (type, id) => { + setSearchResults(null); + setSearchTerm(''); + if (type === 'user') navigate(`/profile/${id}`); + else if (type === 'event') navigate(`/event/${id}`); + }; + + return ( + <> + + setSearchTerm(e.target.value)} + /> + + + + + + + {searchResults && ( + <> + {/* Users */} + {searchResults.users.map((user) => ( + handleResultClick('user', user._id)}> + {`${user.firstName} ${user.lastName}`} + + ))} + {/* Events + {searchResults.events.map((event) => ( + handleResultClick('event', event._id)}> + {event.title} + + ))} */} + + )} + + + + ); +}; + +export default SearchBar; \ No newline at end of file diff --git a/client/src/scenes/eventDetailsPage/index.jsx b/client/src/scenes/eventDetailsPage/index.jsx new file mode 100644 index 0000000..ee9ae92 --- /dev/null +++ b/client/src/scenes/eventDetailsPage/index.jsx @@ -0,0 +1,141 @@ +import React, { useEffect, useState } from "react"; +import { Box, Typography, Button, useTheme } from "@mui/material"; +import { useParams, useNavigate } from "react-router-dom"; +import { useSelector } from "react-redux"; +import Navbar from "scenes/navbar"; +import WidgetWrapper from "components/WidgetWrapper"; +import FlexBetween from "components/FlexBetween"; +import UserImage from "components/UserImage"; + +const EventDetailsPage = () => { + const [event, setEvent] = useState(null); + const [isAttending, setIsAttending] = useState(false); + const { eventId } = useParams(); + const token = useSelector((state) => state.token); + const user = useSelector((state) => state.user); + const navigate = useNavigate(); + const { palette } = useTheme(); + + const getEvent = async () => { + const response = await fetch(`http://localhost:3001/events/${eventId}`, { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + setEvent(data); + setIsAttending(data.attendees?.includes(user._id)); + }; + + const handleAttendance = async () => { + const endpoint = isAttending ? 'unattend' : 'attend'; + const response = await fetch(`http://localhost:3001/events/${eventId}/${endpoint}`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json" + }, + body: JSON.stringify({ userId: user._id }) + }); + if (response.ok) { + setIsAttending(!isAttending); + getEvent(); // Refresh event data + } else { + console.error("Failed to update attendance:", await response.text()); + } + }; + + useEffect(() => { + getEvent(); + }, []); + + if (!event) return null; + + const isCreator = user._id === event.creatorId?._id; + + return ( + + + + + + + + + + {event.title} + + + Created by: {event.creatorId ? `${event.creatorId.firstName} ${event.creatorId.lastName}` : "Unknown"} + + + + + + {event.description} + + + Date: {new Date(event.date).toLocaleString()} + + + Location: {event.location} + + + Attendees: {event.attendees?.length || 0} + + + {!isCreator && ( + + )} + + {isCreator && ( + + + + + )} + + + + ); +}; + +export default EventDetailsPage; \ No newline at end of file diff --git a/client/src/scenes/eventForm/index.jsx b/client/src/scenes/eventForm/index.jsx new file mode 100644 index 0000000..4529213 --- /dev/null +++ b/client/src/scenes/eventForm/index.jsx @@ -0,0 +1,170 @@ +import React, { useState, useEffect } from "react"; +import { Box, Button, TextField, useMediaQuery, Typography, useTheme } from "@mui/material"; +import { Formik } from "formik"; +import * as yup from "yup"; +import { useNavigate, useParams } from "react-router-dom"; +import { useSelector } from "react-redux"; +import Navbar from "scenes/navbar"; + +const eventSchema = yup.object().shape({ + title: yup.string().required("required"), + description: yup.string().required("required"), + location: yup.string().required("required"), + date: yup.date().required("required"), +}); + +const initialValuesEvent = { + title: "", + description: "", + location: "", + date: "", +}; + +const EventForm = () => { + const { palette } = useTheme(); + const navigate = useNavigate(); + const isNonMobile = useMediaQuery("(min-width:600px)"); + const token = useSelector((state) => state.token); + const { eventId } = useParams(); + const [event, setEvent] = useState(null); + + const getEvent = async () => { + const response = await fetch(`http://localhost:3001/events/${eventId}`, { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + setEvent(data); + }; + + useEffect(() => { + if (eventId) { + getEvent(); + } + }, [eventId]); + + const handleFormSubmit = async (values, onSubmitProps) => { + const url = eventId + ? `http://localhost:3001/events/${eventId}` + : "http://localhost:3001/events"; + const method = eventId ? "PUT" : "POST"; + + const response = await fetch(url, { + method: method, + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(values), + }); + const savedEvent = await response.json(); + onSubmitProps.resetForm(); + + if (savedEvent) { + navigate("/events"); + } + }; + + if (eventId && !event) return null; + + return ( + + + + + {({ + values, + errors, + touched, + handleBlur, + handleChange, + handleSubmit, + setFieldValue, + resetForm, + }) => ( +
+ div": { gridColumn: isNonMobile ? undefined : "span 4" }, + }} + > + + + + + + + + + +
+ )} +
+
+
+ ); +}; + +export default EventForm; \ No newline at end of file diff --git a/client/src/scenes/eventsPage/index.jsx b/client/src/scenes/eventsPage/index.jsx new file mode 100644 index 0000000..6a149fd --- /dev/null +++ b/client/src/scenes/eventsPage/index.jsx @@ -0,0 +1,84 @@ +import React, { useEffect, useState } from "react"; +import { Box, Button, useMediaQuery, useTheme } from "@mui/material"; +import { useSelector } from "react-redux"; +import { useNavigate } from "react-router-dom"; +import Navbar from "scenes/navbar"; +import EventWidget from "widgets/EventWidget"; +import UserWidget from "widgets/UserWidget"; +import FlexBetween from "components/FlexBetween"; + +const EventsPage = () => { + const [events, setEvents] = useState([]); + const isNonMobileScreens = useMediaQuery("(min-width:1000px)"); + const { _id, picturePath } = useSelector((state) => state.user); + const token = useSelector((state) => state.token); + const navigate = useNavigate(); + const theme = useTheme(); + + const getEvents = async () => { + const response = await fetch("http://localhost:3001/events", { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + setEvents(data); + }; + + useEffect(() => { + getEvents(); + }, []); + + return ( + + + + + + + + + + + {events.map((event) => ( + + ))} + + {isNonMobileScreens && } + + + ); +}; + +export default EventsPage; \ No newline at end of file diff --git a/client/src/scenes/homePage/index.jsx b/client/src/scenes/homePage/index.jsx index 5bf2f04..212caff 100644 --- a/client/src/scenes/homePage/index.jsx +++ b/client/src/scenes/homePage/index.jsx @@ -5,6 +5,8 @@ import { useSelector } from "react-redux"; import UserWidget from "widgets/UserWidget"; import MyPostWidget from "widgets/MyPostWidget"; import PostsWidget from "widgets/PostsWidget"; +import AdvertWidget from "widgets/AdvertWidget"; +import FollowersListWidget from "widgets/FollowersListWidget"; const HomePage = () => { const isNonMobileScreens = useMediaQuery("(min-width:1000px)"); @@ -31,7 +33,13 @@ const HomePage = () => { - {isNonMobileScreens && } + {isNonMobileScreens && ( + + + + + + )} ) diff --git a/client/src/scenes/landingPage/About.jsx b/client/src/scenes/landingPage/About.jsx index 11ab551..1c4cab8 100644 --- a/client/src/scenes/landingPage/About.jsx +++ b/client/src/scenes/landingPage/About.jsx @@ -1,11 +1,153 @@ import React from "react"; +import { Box, Typography, useTheme, useMediaQuery, Grid, Avatar } from "@mui/material"; +import { Search, Group, Brush, Chat, Favorite, Explore, EmojiObjects, Stars } from "@mui/icons-material"; +import FlexBetween from "components/FlexBetween"; +import about from 'assets/about.jpg'; // Replace with your actual image path + +const AboutUs = () => { + const theme = useTheme(); + const isNonMobileScreens = useMediaQuery("(min-width: 1000px)"); + + const values = [ + { icon: , text: 'Curiosity' }, + { icon: , text: 'Community' }, + { icon: , text: 'Creativity' }, + { icon: , text: 'Conversation' }, + { icon: , text: 'Connection' }, + { icon: , text: 'Discovery' }, + { icon: , text: 'Inspiration' }, + { icon: , text: 'Wonder' }, + ]; + + const teamMembers = [ + 'Member1', 'Member2', 'Member3', 'Member4', 'Member5' + ]; -const About = () => { return ( -
-

About

-
- ) -} + + {/* Hero Section */} + + + + Welcome to Astrogram + + + Astrogram is a platform for astronomy enthusiasts to share and explore the universe. Our mission is to inspire curiosity, connect people, and foster a community of stargazers. + + + + + {/* Values Section */} + + + Our values + + + {values.map((value, i) => ( + + + {value.icon} + + {value.text} + + + + ))} + + + + {/* Team Section */} + + + Who we are + + + Meet the team + + + {teamMembers.map((member, i) => ( + + + + + + + {member} + + + + ))} + + + + ); +}; -export default About; \ No newline at end of file +export default AboutUs; \ No newline at end of file diff --git a/client/src/scenes/navbar/index.jsx b/client/src/scenes/navbar/index.jsx index 44d7b34..c7edc20 100644 --- a/client/src/scenes/navbar/index.jsx +++ b/client/src/scenes/navbar/index.jsx @@ -2,10 +2,12 @@ import React, { useState } from "react"; import { Box, IconButton, InputBase, Typography, Select, MenuItem, FormControl, useTheme, useMediaQuery } from "@mui/material"; import { Search, Message, DarkMode, LightMode, Help, Menu, Close } from "@mui/icons-material"; import { Notifications } from "@mui/icons-material"; +import { Event as EventIcon } from "@mui/icons-material"; import { useDispatch, useSelector } from "react-redux"; import { setMode, setLogout } from "state"; import { useNavigate } from "react-router-dom"; import FlexBetween from "components/FlexBetween"; +import SearchBar from "components/SearchBar"; const Navbar = () => { const [isMobileMenuToggled, setIsMobileMenuToggled] = useState(false); @@ -42,12 +44,13 @@ const Navbar = () => { Astrogram {isNonMobileScreens && ( - - - - - - + // + // + // + // + // + // + )} @@ -60,6 +63,9 @@ const Navbar = () => { + navigate("/events")}> + + { - return ( -
Hi ProfilePage
- ) -} + const [user, setUser] = useState(null); + const { userId } = useParams(); + const token = useSelector((state) => state.token); + const isNonMobileScreens = useMediaQuery("(min-width:1000px)"); + + const getUser = async () => { + const response = await fetch(`http://localhost:3001/users/${userId}`, { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + }); + const data = await response.json(); + setUser(data); + }; + + useEffect(() => { + getUser(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + if (!user) return null; + + return ( + + + + + + + + + + + + + + + + ); +}; export default ProfilePage; \ No newline at end of file diff --git a/client/src/state/index.js b/client/src/state/index.js index ce069e9..f5db4f7 100644 --- a/client/src/state/index.js +++ b/client/src/state/index.js @@ -34,8 +34,15 @@ export const authSlice = createSlice({ }); state.posts = updatedPosts; }, + setFollowers: (state, action) => { + if (state.user) { + state.user.followers = action.payload.followers; + } else { + console.error('User is not logged in'); + } + } } }); -export const { setMode, setLogin, setLogout, setPost, setPosts } = authSlice.actions; +export const { setMode, setLogin, setLogout, setPost, setPosts, setFollowers} = authSlice.actions; export default authSlice.reducer; \ No newline at end of file diff --git a/client/src/widgets/AdvertWidget.jsx b/client/src/widgets/AdvertWidget.jsx new file mode 100644 index 0000000..db0050a --- /dev/null +++ b/client/src/widgets/AdvertWidget.jsx @@ -0,0 +1,38 @@ +import { Typography, useTheme } from "@mui/material"; +import FlexBetween from "components/FlexBetween"; +import WidgetWrapper from "components/WidgetWrapper"; + +const AdvertWidget = () => { + const { palette } = useTheme(); + const dark = palette.neutral.dark; + const main = palette.neutral.main; + const medium = palette.neutral.medium; + + return ( + + + + Sponsored + + Create Ad + + advert + + SpaceEnthusiast + astrogram.com + + + The best place to find all your space related content. Follow us for more! 🚀 + #space #nasa #astronomy + + + ); +}; + +export default AdvertWidget; \ No newline at end of file diff --git a/client/src/widgets/EventWidget.jsx b/client/src/widgets/EventWidget.jsx new file mode 100644 index 0000000..0cfebf0 --- /dev/null +++ b/client/src/widgets/EventWidget.jsx @@ -0,0 +1,72 @@ +import React from "react"; +import { Box, Typography, Button, useTheme } from "@mui/material"; +import { useNavigate } from "react-router-dom"; +import FlexBetween from "components/FlexBetween"; +import UserImage from "components/UserImage"; +import WidgetWrapper from "components/WidgetWrapper"; + +const EventWidget = ({ eventId, creatorId, creatorName, creatorPicturePath, title, description, date, location, attendees }) => { + const { palette } = useTheme(); + const navigate = useNavigate(); + const main = palette.neutral.main; + const medium = palette.neutral.medium; + + return ( + + + + + + navigate(`/profile/${creatorId}`)} + > + {creatorName || "Unknown Creator"} + + + Event Creator + + + + + + {title} + + + {description} + + + Date: {new Date(date).toLocaleDateString()} + + + Location: {location} + + + Attendees: {attendees?.length || 0} + + + + ); +}; + +export default EventWidget; \ No newline at end of file diff --git a/client/src/widgets/FollowersListWidget.jsx b/client/src/widgets/FollowersListWidget.jsx new file mode 100644 index 0000000..67a86a2 --- /dev/null +++ b/client/src/widgets/FollowersListWidget.jsx @@ -0,0 +1,55 @@ +import { Box, Typography, useTheme } from "@mui/material"; +import Follower from "components/Follower"; +import WidgetWrapper from "components/WidgetWrapper"; +import { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { setFollowers } from "state"; + +const FollowersListWidget = ({ userId }) => { + const dispatch = useDispatch(); + const { palette } = useTheme(); + const token = useSelector((state) => state.token); + const followers = useSelector((state) => state.user.followers); + + const getFollowers = async () => { + const response = await fetch( + `http://localhost:3001/users/${userId}/followers`, + { + method: "GET", + headers: { Authorization: `Bearer ${token}` }, + } + ); + const data = await response.json(); + dispatch(setFollowers({ followers: data })); + }; + + useEffect(() => { + getFollowers(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + + + Followers List + + + {followers.map((follower) => ( + + ))} + + + ); +}; + +export default FollowersListWidget; \ No newline at end of file diff --git a/client/src/widgets/PostWidget.jsx b/client/src/widgets/PostWidget.jsx index df48f98..91f70b6 100644 --- a/client/src/widgets/PostWidget.jsx +++ b/client/src/widgets/PostWidget.jsx @@ -1,106 +1,107 @@ import { - ChatBubbleOutlineOutlined, - FavoriteBorderOutlined, - FavoriteOutlined, - ShareOutlined, - } from "@mui/icons-material"; - import { Box, Divider, IconButton, Typography, useTheme } from "@mui/material"; - import FlexBetween from "components/FlexBetween"; - import WidgetWrapper from "components/WidgetWrapper"; - import { useState } from "react"; - import { useDispatch, useSelector } from "react-redux"; - import { setPost } from "state"; + ChatBubbleOutlineOutlined, + FavoriteBorderOutlined, + FavoriteOutlined, + ShareOutlined, +} from "@mui/icons-material"; +import { Box, Divider, IconButton, Typography, useTheme } from "@mui/material"; +import FlexBetween from "components/FlexBetween"; +import WidgetWrapper from "components/WidgetWrapper"; +import { useState } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { setPost } from "state"; + +const PostWidget = ({ + postId, + postUserId, + name, + description, + location, + picturePath, + userPicturePath, + likes, + comments, +}) => { + const [isComments, setIsComments] = useState(false); + const dispatch = useDispatch(); + const token = useSelector((state) => state.token); + const loggedInUserId = useSelector((state) => state.user._id); - const PostWidget = ({ - postId, - postUserId, - name, - description, - location, - picturePath, - userPicturePath, - likes, - comments, - }) => { - const [isComments, setIsComments] = useState(false); - const dispatch = useDispatch(); - const token = useSelector((state) => state.token); - const loggedInUserId = useSelector((state) => state.user._id); - const isLiked = Boolean(likes[loggedInUserId]); - const likeCount = Object.keys(likes).length; - - const { palette } = useTheme(); - const main = palette.neutral.main; - const primary = palette.primary.main; - - const patchLike = async () => { - const response = await fetch(`http://localhost:3001/posts/${postId}/like`, { - method: "PATCH", - headers: { - Authorization: `Bearer ${token}`, - "Content-Type": "application/json", - }, - body: JSON.stringify({ userId: loggedInUserId }), - }); - const updatedPost = await response.json(); - dispatch(setPost({ post: updatedPost })); - }; - - return ( - - - {description} - - {picturePath && ( - post - )} - - - - - {isLiked ? ( - - ) : ( - - )} - - {likeCount} - - - - setIsComments(!isComments)}> - - - {comments.length} - + // Add a check to ensure likes is an object + const isLiked = Boolean(likes && typeof likes === 'object' && likes[loggedInUserId]); + const likeCount = likes && typeof likes === 'object' ? Object.keys(likes).length : 0; + + const { palette } = useTheme(); + const main = palette.neutral.main; + const primary = palette.primary.main; + + const patchLike = async () => { + const response = await fetch(`http://localhost:3001/posts/${postId}/like`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ userId: loggedInUserId }), + }); + const updatedPost = await response.json(); + dispatch(setPost({ post: updatedPost })); + }; + + return ( + + + {description} + + {picturePath && ( + post + )} + + + + + {isLiked ? ( + + ) : ( + + )} + + {likeCount} + + + + setIsComments(!isComments)}> + + + {comments.length} - - - - - {isComments && ( - - {comments.map((comment, i) => ( - - - - {comment} - - - ))} - - - )} - - ); - }; - - export default PostWidget; - \ No newline at end of file + + + + + + {isComments && ( + + {comments.map((comment, i) => ( + + + + {comment} + + + ))} + + + )} + + ); +}; + +export default PostWidget; \ No newline at end of file diff --git a/server/.env b/server/.env index b8ab685..283355a 100644 --- a/server/.env +++ b/server/.env @@ -1,3 +1,3 @@ -MONGO_URL='mongodb+srv://spacegram10:4sHqTrKxd2HYveAx@cluster0.lqsxy.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0' +MONGO_URL='mongodb://localhost:27017/astrogram-develop' JWT_SECRET='somesupersecretkeywhichcannotbeguessed' PORT=3001 \ No newline at end of file diff --git a/server/controllers/events.js b/server/controllers/events.js new file mode 100644 index 0000000..d47000c --- /dev/null +++ b/server/controllers/events.js @@ -0,0 +1,130 @@ +import Event from "../models/Event.js"; +import User from "../models/User.js"; + +export const createEvent = async (req, res) => { + try { + const { title, description, date, location, imageUrl } = req.body; + const newEvent = new Event({ + creatorId: req.user.id, + title, + description, + date, + location, + imageUrl, + attendees: [req.user.id], + }); + const savedEvent = await newEvent.save(); + res.status(201).json(savedEvent); + } catch (err) { + res.status(409).json({ message: err.message }); + } +}; + +export const getEvent = async (req, res) => { + try { + const { id } = req.params; + const event = await Event.findById(id).populate("creatorId", "firstName lastName picturePath"); + if (!event) { + return res.status(404).json({ message: "Event not found" }); + } + res.status(200).json(event); + } catch (err) { + res.status(404).json({ message: err.message }); + } +}; + +export const updateEvent = async (req, res) => { + try { + const { id } = req.params; + const { title, description, date, location, imageUrl } = req.body; + const updatedEvent = await Event.findByIdAndUpdate( + id, + { title, description, date, location, imageUrl }, + { new: true } + ); + if (!updatedEvent) { + return res.status(404).json({ message: "Event not found" }); + } + res.status(200).json(updatedEvent); + } catch (err) { + res.status(400).json({ message: err.message }); + } +}; + +export const deleteEvent = async (req, res) => { + try { + const { id } = req.params; + const deletedEvent = await Event.findByIdAndDelete(id); + if (!deletedEvent) { + return res.status(404).json({ message: "Event not found" }); + } + res.status(200).json({ message: "Event deleted successfully" }); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const attendEvent = async (req, res) => { + try { + const { id } = req.params; + const { userId } = req.body; + + const event = await Event.findById(id); + if (!event) { + return res.status(404).json({ message: "Event not found" }); + } + + if (event.attendees.includes(userId)) { + return res.status(400).json({ message: "User is already attending this event" }); + } + + event.attendees.push(userId); + await event.save(); + + res.status(200).json(event); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const unattendEvent = async (req, res) => { + try { + const { id } = req.params; + const { userId } = req.body; + + const event = await Event.findById(id); + if (!event) { + return res.status(404).json({ message: "Event not found" }); + } + + if (!event.attendees.includes(userId)) { + return res.status(400).json({ message: "User is not attending this event" }); + } + + event.attendees = event.attendees.filter(attendeeId => attendeeId.toString() !== userId); + await event.save(); + + res.status(200).json(event); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; + +export const getUserEvents = async (req, res) => { + try { + const { userId } = req.params; + const events = await Event.find({ creatorId: userId }).sort({ date: 1 }); + res.status(200).json(events); + } catch (err) { + res.status(404).json({ message: err.message }); + } +}; + +export const getAllEvents = async (req, res) => { + try { + const events = await Event.find().sort({ date: 1 }).populate("creatorId", "firstName lastName"); + res.status(200).json(events); + } catch (err) { + res.status(500).json({ message: err.message }); + } +}; \ No newline at end of file diff --git a/server/controllers/search.js b/server/controllers/search.js new file mode 100644 index 0000000..ccba3f4 --- /dev/null +++ b/server/controllers/search.js @@ -0,0 +1,33 @@ +import User from "../models/User.js"; +//import Event from "../models/Event.js"; + +export const searchAll = async (req, res) => { + try { + const { query } = req.query; + const regex = new RegExp(query, 'i'); + + const users = await User.find({ + $or: [ + { firstName: regex }, + { lastName: regex }, + { email: regex } + ] + }).limit(5); + + // const events = await Event.find({ + // $or: [ + // { title: regex }, + // { description: regex } + // ] + // }).limit(5); + + res.json({ + users, + //events + }); + } catch (error) { + res.status(500).json({ message: error.message }); + } +}; + +export default { searchAll }; \ No newline at end of file diff --git a/server/controllers/users.js b/server/controllers/users.js index 0e63122..ff757c2 100644 --- a/server/controllers/users.js +++ b/server/controllers/users.js @@ -1,5 +1,6 @@ import User from '../models/User.js'; +/* READ */ export const getUser = async (req, res) => { try { const { id } = req.params; @@ -8,4 +9,53 @@ export const getUser = async (req, res) => { } catch(error) { res.status(404).json({ message: error.message}); } -} \ No newline at end of file +} + +export const getUserFollowers = async (req, res) => { + try { + const { id } = req.params; + const user = await User.findById(id); + + const followers = await Promise.all( + user.followers.map((id) => User.findById(id)) + ); + res.status(200).json(followers); + } + catch(error) { + res.status(404).json({ message: error.message}); + } +} + +/* UPDATE */ + export const followUnfollowUser = async (req, res) => { + try { + const { id, followerId } = req.params; + const user = await User.findById(id); + const follower = await User.findById(followerId); + + if (user.following.includes(followerId)) { + user.following = user.following.filter((id) => id !== followerId); + follower.followers = follower.followers.filter((id) => id !== id); + } else { + user.following.push(followerId); + follower.followers.push(id); + } + await user.save(); + await follower.save(); + + const following = await Promise.all( + user.following.map((id) => User.findById(id)) + ); + + const formattedFollowing = following.map( + ({ _id, firstName, lastName, occupation, location, picturePath }) => { + return { _id, firstName, lastName, occupation, location, picturePath }; + } + ); + + res.status(200).json(formattedFollowing); + + } catch (err) { + res.status(404).json({ message: err.message }); + } + } \ No newline at end of file diff --git a/server/data/index.js b/server/data/index.js index 83798cb..f954ef9 100644 --- a/server/data/index.js +++ b/server/data/index.js @@ -274,4 +274,57 @@ export const posts = [ "Michael, stop it.", ], }, +]; + +export const events = [ + { + _id: new mongoose.Types.ObjectId(), + creatorId: userIds[0], + title: "Mars Colony Simulation Workshop", + description: "Join us for an immersive workshop where we simulate life in a Mars colony. Learn about the challenges of space habitation and brainstorm innovative solutions.", + date: new Date("2024-10-15T14:00:00Z"), + location: "Virtual Event", + attendees: [userIds[1], userIds[2], userIds[3]], + imageUrl: "event1.jpg", + }, + { + _id: new mongoose.Types.ObjectId(), + creatorId: userIds[1], + title: "Astrophotography Nightout", + description: "Grab your cameras and join fellow space enthusiasts for a night of astrophotography. We'll be capturing celestial objects and learning techniques to improve our space photography skills.", + date: new Date("2024-09-22T20:00:00Z"), + location: "Dark Sky Park, Colorado", + attendees: [userIds[0], userIds[4], userIds[5]], + imageUrl: "event2.jpg", + }, + { + _id: new mongoose.Types.ObjectId(), + creatorId: userIds[2], + title: "SpaceX Starship Launch Viewing Party", + description: "Come watch the historic SpaceX Starship launch with fellow space enthusiasts. We'll have expert commentary and a Q&A session after the launch.", + date: new Date("2024-11-30T10:00:00Z"), + location: "Cape Canaveral, Florida", + attendees: [userIds[1], userIds[3], userIds[5], userIds[6]], + imageUrl: "sevent3.jpg", + }, + { + _id: new mongoose.Types.ObjectId(), + creatorId: userIds[3], + title: "Exoplanet Discovery Symposium", + description: "A day-long symposium featuring talks from leading astronomers about recent exoplanet discoveries and the search for habitable worlds beyond our solar system.", + date: new Date("2025-02-18T09:00:00Z"), + location: "Griffith Observatory, Los Angeles", + attendees: [userIds[0], userIds[2], userIds[4], userIds[7]], + imageUrl: "event4.jpg", + }, + { + _id: new mongoose.Types.ObjectId(), + creatorId: userIds[4], + title: "Space Debris Cleanup Hackathon", + description: "Participate in our 48-hour hackathon to develop innovative solutions for cleaning up space debris. Cash prizes for the top three teams!", + date: new Date("2024-12-05T18:00:00Z"), + location: "MIT Campus, Cambridge, MA", + attendees: [userIds[1], userIds[5], userIds[6], userIds[7]], + imageUrl: "event5.jpg", + }, ]; \ No newline at end of file diff --git a/server/index.js b/server/index.js index 4952961..8b7a3dd 100644 --- a/server/index.js +++ b/server/index.js @@ -11,10 +11,14 @@ import { fileURLToPath } from 'url'; import authRoutes from './routes/auth.js'; import userRoutes from './routes/users.js'; import postRoutes from './routes/posts.js'; +import searchRoutes from './routes/search.js'; +import eventRoutes from './routes/events.js'; import { register } from './controllers/auth.js'; import User from './models/User.js'; import Post from './models/Post.js'; -import { users, posts } from './data/index.js'; +import Event from "./models/Event.js"; +import { users, posts, events } from './data/index.js'; + /* CONFIGURATIONS */ const __filename = fileURLToPath(import.meta.url); @@ -48,6 +52,8 @@ app.post('/auth/register', upload.single('picture'), register); app.use('/auth', authRoutes); app.use('/users', userRoutes); app.use("/posts", postRoutes); +app.use("/search", searchRoutes); +app.use("/events", eventRoutes); /* MONGOOSE SETUP */ const PORT = process.env.PORT || 6001; @@ -59,7 +65,8 @@ mongoose .then(() => { app.listen(PORT, () => console.log(`Server port ${PORT}`)); - // User.insertMany(users); - // Post.insertMany(posts); + //User.insertMany(users); + //Post.insertMany(posts); + //Event.insertMany(events); }) .catch((err) => console.log(`${err} did not connect !`)); \ No newline at end of file diff --git a/server/models/Event.js b/server/models/Event.js new file mode 100644 index 0000000..a47a161 --- /dev/null +++ b/server/models/Event.js @@ -0,0 +1,40 @@ +import mongoose from "mongoose"; + +const eventSchema = new mongoose.Schema( + { + creatorId: { + type: mongoose.Schema.Types.ObjectId, + ref: "User", + required: true, + }, + title: { + type: String, + required: true, + trim: true, + }, + description: { + type: String, + required: true, + }, + date: { + type: Date, + required: true, + }, + location: { + type: String, + required: true, + }, + attendees: [{ + type: mongoose.Schema.Types.ObjectId, + ref: "User" + }], + imageUrl: { + type: String, + }, + }, + { timestamps: true } +); + +const Event = mongoose.model("Event", eventSchema); + +export default Event; \ No newline at end of file diff --git a/server/public/assets/Avatar.jpeg b/server/public/assets/Avatar.jpeg new file mode 100644 index 0000000..38b3a3d Binary files /dev/null and b/server/public/assets/Avatar.jpeg differ diff --git a/server/public/assets/event1.jpg b/server/public/assets/event1.jpg new file mode 100644 index 0000000..d2c449f Binary files /dev/null and b/server/public/assets/event1.jpg differ diff --git a/server/public/assets/event2.jpg b/server/public/assets/event2.jpg new file mode 100644 index 0000000..527c371 Binary files /dev/null and b/server/public/assets/event2.jpg differ diff --git a/server/public/assets/event3.jpg b/server/public/assets/event3.jpg new file mode 100644 index 0000000..41bf3e5 Binary files /dev/null and b/server/public/assets/event3.jpg differ diff --git a/server/public/assets/event4.jpg b/server/public/assets/event4.jpg new file mode 100644 index 0000000..f81a9c0 Binary files /dev/null and b/server/public/assets/event4.jpg differ diff --git a/server/public/assets/event5.jpg b/server/public/assets/event5.jpg new file mode 100644 index 0000000..0929214 Binary files /dev/null and b/server/public/assets/event5.jpg differ diff --git a/server/public/assets/info.jpg b/server/public/assets/info.jpg new file mode 100644 index 0000000..57158d1 Binary files /dev/null and b/server/public/assets/info.jpg differ diff --git a/server/public/assets/logo192.png b/server/public/assets/logo192.png new file mode 100644 index 0000000..0725510 Binary files /dev/null and b/server/public/assets/logo192.png differ diff --git a/server/routes/events.js b/server/routes/events.js new file mode 100644 index 0000000..d92494c --- /dev/null +++ b/server/routes/events.js @@ -0,0 +1,25 @@ +import express from "express"; +import { verifyToken } from "../middleware/auth.js"; +import { + createEvent, + getEvent, + updateEvent, + deleteEvent, + attendEvent, + unattendEvent, + getUserEvents, + getAllEvents, +} from "../controllers/events.js"; + +const router = express.Router(); + +router.post("/", verifyToken, createEvent); +router.get("/:id", verifyToken, getEvent); +router.put("/:id", verifyToken, updateEvent); +router.delete("/:id", verifyToken, deleteEvent); +router.post("/:id/attend", verifyToken, attendEvent); +router.post("/:id/unattend", verifyToken, unattendEvent); +router.get("/user/:userId", verifyToken, getUserEvents); +router.get("/", verifyToken, getAllEvents); + +export default router; \ No newline at end of file diff --git a/server/routes/search.js b/server/routes/search.js new file mode 100644 index 0000000..112cf51 --- /dev/null +++ b/server/routes/search.js @@ -0,0 +1,9 @@ +import express from "express"; +import { searchAll } from "../controllers/search.js"; +import { verifyToken } from "../middleware/auth.js"; + +const router = express.Router(); + +router.get("/", verifyToken, searchAll); + +export default router; \ No newline at end of file diff --git a/server/routes/users.js b/server/routes/users.js index 0ceecb9..3564921 100644 --- a/server/routes/users.js +++ b/server/routes/users.js @@ -1,10 +1,13 @@ import express from 'express'; -import { getUser } from '../controllers/users.js'; +import { followUnfollowUser, getUser, getUserFollowers } from '../controllers/users.js'; import { verifyToken } from '../middleware/auth.js' const router = express.Router(); /* READ */ router.get('/:id', verifyToken, getUser); +router.get('/:id/followers', verifyToken, getUserFollowers); +/* UPDATE */ +router.patch('/:id/:followerId', verifyToken, followUnfollowUser); export default router; \ No newline at end of file