diff --git a/README.md b/README.md index 9f1a8c98..66d6e944 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,55 @@ An interplanetary town square that maintains connections between cultures and id ## Authors -- Kamal Dolikay ([@kamalkdolikay](https://github.com/kamalkdolikay)) -- Ole Lammers ([@zyrakia](https://www.github.com/Zyrakia)) -- Tianyou Xie ([@Tianyou-Xie](https://github.com/Tianyou-Xie)) -- Samarjit Bhogal ([@SamarjitBhogal](https://github.com/SamarjitBhogal)) -- Marcus Lages ([@MarcusLages](https://github.com/MarcusLages)) +- Kamal Dolikay + - GitHub: [@kamalkdolikay](https://github.com/kamalkdolikay) + - Email: [kamaldolikay@gmail.com](mailto:kamaldolikay@gmail.com) +- Ole Lammers + - GitHub: [@zyrakia](https://www.github.com/Zyrakia) + - Email: [ole.lammers@pm.me](mailto:ole.lammers@pm.me) +- Tianyou Xie + - GitHub: [@Tianyou-Xie](https://github.com/Tianyou-Xie) + - Email: [tianyouxie001@gmail.com](mailto:tianyouxie001@gmail.com) +- Samarjit Bhogal + - GitHub: [@SamarjitBhogal](https://github.com/SamarjitBhogal) + - Email: [samarjit.v.bhogal@gmail.com](mailto:samarjit.v.bhogal@gmail.com) +- Marcus Lages + - GitHub: [@MarcusLages](https://github.com/MarcusLages) + - Email: [marcusvlages@gmail.com](mailto:marcusvlages@gmail.com) + +## Features + +- Create Account and Login (Email/Password or Google) +- Delete Account and Logout +- Edit Account +- Edit Profile +- View Available Planets + - Map Mode + - List Mode +- Create Posts on Various Planets +- View Posts + - Galactic Feed + - Planetary Feed + - User Feed +- Interact with Posts + - Comment + - Like / Unlike + - Save / Unsave + - Share +- Change Avatar +- Search for Posts +- Search for Users +- Interact with Users + - Follow / Unfollow + - Message (real-time) +- View Interactions + - Saved Posts + - Liked Posts + - Following + - Followers +- View Privacy Policy +- View Terms of Use +- View FAQs ## Technologies @@ -32,6 +76,13 @@ The project is built using [Typescript](https://www.typescriptlang.org/), and sp - [React If](https://https://www.npmjs.com/package/react-if/) - [React Toastify](https://fkhadra.github.io/react-toastify/introduction/) - [Socket IO](https://socket.io/) (Client) +- [Popper](https://popper.js.org/docs/v2/) +- [Axios](https://axios-http.com/docs/intro) +- [Axios Cache](https://axios-cache-interceptor.js.org/) +- [Is Mobile](https://www.npmjs.com/package/is-mobile) +- [Konva](https://konvajs.org/index.html) +- [React Helmet](https://www.npmjs.com/package/react-helmet) +- [Use Image](https://www.npmjs.com/package/use-image) **Server (built with [esno](https://www.npmjs.com/package/esno)):** @@ -41,15 +92,17 @@ The project is built using [Typescript](https://www.typescriptlang.org/), and sp - [Express File Routing](https://www.npmjs.com/package/express-file-routing) - [Joi](https://joi.dev/) - [bcrypt](https://github.com/kelektiv/node.bcrypt.js) -- [http-status-codes](https://www.npmjs.com/package/http-status-codes) -- [nodemailer](https://www.nodemailer.com/) +- [HTTP Status Codes](https://www.npmjs.com/package/http-status-codes) +- [Nodemailer](https://www.nodemailer.com/) - [JWT](https://jwt.io/) - [CORS](https://www.npmjs.com/package/cors) - [dotenv](https://www.dotenv.org/) - [Google Auth Library](https://cloud.google.com/nodejs/docs/reference/google-auth-library/latest) - [Google APIs](https://www.npmjs.com/package/googleapis) -- [Http Status Codes](https://www.npmjs.com/package/http-status-codes) - [Socket IO](https://socket.io/) (Server) +- [Cloudinary](https://cloudinary.com/) +- [OpenAI](https://openai.com/) +- [UUID](https://www.npmjs.com/package/uuid) **Development Utilities:** @@ -59,8 +112,17 @@ The project is built using [Typescript](https://www.typescriptlang.org/), and sp ## Code Attributions -- Regex escape utility: [(`./server/utils/regex.ts:10`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/server/src/utils/regex.ts#L10) +- Regex Escape Utility: [(`./server/src/utils/regex.ts:10`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/server/src/utils/regex.ts#L10) > https://github.com/component/escape-regexp/blob/master/index.js +- Helmet SEO Component Structure: [(`./client/src/components/seo/seo.tsx`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/client/src/components/seo/seo.tsx#L23-L37) + > https://www.freecodecamp.org/news/react-helmet-examples +- Hotbar Structure and Animation Inspiration: [(`./client/src/components/Hotbar/Hotbar.tsx`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/client/src/components/Hotbar/Hotbar.tsx) + > https://youtu.be/sgV111KxbwI?si=HnOS5OnfLswXk_8E +- Customized React Toast: [(`./client/src/index.css`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/client/src/index.css) + > - https://stackoverflow.com/questions/60849448/how-can-i-change-the-styles-of-the-react-toastify-popup-message + > - https://fkhadra.github.io/react-toastify/introduction/ +- Planet Map Orbit Calculations [(`./client/src/pages/planet-map/planet-visual.tsx`)](https://github.com/Tianyou-Xie/2800_202410_BBY07/blob/dev/client/src/pages/planet-map/planet-visual.tsx#L70-L78) + > https://jsfiddle.net/ColinCee/a2yu0af6/ ## Environment Variables @@ -76,30 +138,365 @@ Both the server and client utilize a `.env` file. **Client Variables:** -| Key | Usage | -| ---- | ------------------------------ | -| PORT | Port used for the frontend app | -| VITE_LOCALHOST | Host used for listening to Server Socket Events | +| Key | Usage | +| ---------------- | --------------------------------------------------------------------------------------- | +| VITE_PORT | The port that Vite serves the client on, when running locally | +| VITE_SERVER_PORT | The port that the server is running on, when running locally | +| VITE_NODE_ENV | The environment the client is running in (development_local / development / production) | **Server Variables:** -| Key | Usage | -| ---------- | --------------------------------------------- | -| PORT | Port used for the express server | -| MONGO_URL | The MongoDB connection string | -| JWT_TTL | The JWT token expiry time, in seconds | -| JWT_SECRET | The secret used to sign and verify JWT tokens | +| Key | Usage | +| ----------------------- | --------------------------------------------------------------------------------------- | +| PORT | The port that Express starts the server on, when running locally | +| CLIENT_PORT | The port that the client is being served on, when running locally | +| NODE_ENV | The environment the server is running in (development_local / development / production) | +| MONGO_URL | The MongoDB connection string used to connect to the database | +| JWT_SECRET | The secret used to sign and verify JWT tokens | +| JWT_TTL | The JWT token expiry time, in seconds | +| GOOGLE_OAUTH_ID | The Google OAuth API Client ID used to authenticate with Google | +| GOOGLE_OAUTH_SECRET | The Google OAuth API Client Secret used to authenticate with Google | +| EMAIL_HOST | The hostname of the email transporter used to send emails from the server | +| EMAIL_PORT | The port of the email transporter used to send emails from the server | +| EMAIL_USER | The username of the email transporter used to send emails from the server | +| EMAIL_PASS | The password of the email transporter used to send emails from the server | +| CLOUDINARY_CLOUD_NAME | The Cloudinary cloud name used to upload images | +| CLOUDINARY_CLOUD_KEY | The Cloudinary cloud API key used to upload images | +| CLOUDINARY_CLOUD_SECRET | The Cloudinary cloud API secret used to upload images | +| OPENAI_API_KEY | The OpenAI API key used to generate images | ## Running Locally & Deployment -During development, both the client and server use the command `npm run dev` to launch the development server with file watching. +**Running the Server:** + +1. Ensure you have all server environment variables set +2. Enter the server directory (`cd server`) +3. Ensure all dependencies are installed (`npm i`) +4. Launch the server in regular (`npm start`) or watch mode (`npm run dev`) + +The server does not compile, it runs as a Node app. In order to deploy it, you must deploy the `server` folder and run the `npm start` command inside the deployed folder. + +**Running the Client:** + +1. Ensure you have all client environment variables set +2. Enter the client directory (`cd client`) +3. Ensure all dependencies are installed (`npm i`) +4. Launch the client in preview (`npm start`) or watch mode (`npm run dev`) + +The client compiles to a `dist` folder (`npm run build`). This folder can be deployed as a static site. Because this project uses client side navigation, you must ensure the service you are using to deploy the client has a rewrite rule to rewrite all requests to the `/` route. If request URLs are not rewritten by the deployment service, the client side routing will not be able to display the correct page when loading the page from a URL that does not point to the `/` route. -The client compiles to a `dist` folder with the command `npm run build`. This folder can be deployed as is. The command `npm start` will first build and then preview the build with vite. +## AI Acknowledgement -The server does not compile, esno is used in watch mode during development (`npm run dev`), and in static mode during deployment. The whole project must be deployed and started with `npm start`. +There are a few instances where AI was used in this project: + +- Default Avatars - when a user signs up, an avatar is automatically generated for them using AI +- Policy Page - an inital draft was created with the help of ChatGPT +- Terms of Use Page - an inital draft was created with the help of ChatGPT +- FAQs Page - ChatGPT was use to get an idea of common questions that people have about social media platforms +- Planet Images - the images on the homepage for the different planets were generated with AI and then edited manually to enhance the look and feel ## Project Links +- [Hosted Site](https://skynetwork.app) +- [Hosted Development Site](https://dev.skynetwork.app) - [GitHub](https://github.com/1800-BBY8/1800_202410_BBY8) - [Trello](https://trello.com/b/ENhDCODq/2800202410bby07) - [FigJam](https://www.figma.com/file/lM0sT0hbMY3v0cW2zLn5hC/2800-202410-BBY07?type=whiteboard&node-id=0-1&t=fR12pb3gUrK1EDNj-0) + +## Files and Folders (as of commit [9720d71](https://github.com/Tianyou-Xie/2800_202410_BBY07/commit/9720d7117787446e357ea41a0d10af68b33e64a5)) + +``` +. +├── .gitignore +├── .prettierrc +├── .vscode +│ ├── extensions.json +│ └── settings.json +├── README.md +├── Skynet Logo.png +├── client +│ ├── index.html +│ ├── package-lock.json +│ ├── package.json +│ ├── public +│ │ ├── among-us.jpg +│ │ ├── among-us2.jpg +│ │ ├── favicon.ico +│ │ └── logo.webp +│ ├── src +│ │ ├── app.tsx +│ │ ├── assets +│ │ │ ├── fonts +│ │ │ │ ├── BITSUMIS.TTF +│ │ │ │ ├── BabaPro-Bold.ttf +│ │ │ │ ├── FjallaOne-Regular.ttf +│ │ │ │ └── TT-Octosquares-Trial-Regular.ttf +│ │ │ ├── images +│ │ │ │ ├── amongus-black.webp +│ │ │ │ ├── amongus-blue.webp +│ │ │ │ ├── amongus-green.webp +│ │ │ │ ├── amongus-pink.webp +│ │ │ │ ├── amongus-red.webp +│ │ │ │ ├── amongus-white.webp +│ │ │ │ ├── amongus-yellow.webp +│ │ │ │ ├── icons +│ │ │ │ │ ├── android-chrome-192x192.png +│ │ │ │ │ ├── android-chrome-512x512.png +│ │ │ │ │ ├── apple-touch-icon.png +│ │ │ │ │ ├── favicon-16x16.png +│ │ │ │ │ ├── favicon-32x32.png +│ │ │ │ │ ├── favicon.ico +│ │ │ │ │ └── site.webmanifest +│ │ │ │ └── skynet-logo.png +│ │ │ └── videos +│ │ │ ├── home.gif +│ │ │ ├── message.gif +│ │ │ └── post.gif +│ │ ├── components +│ │ │ ├── google-auth-btn +│ │ │ │ └── google-auth-btn.tsx +│ │ │ ├── header +│ │ │ │ ├── header.module.css +│ │ │ │ └── header.tsx +│ │ │ ├── hotbar +│ │ │ │ ├── hotbar-animation.css +│ │ │ │ ├── hotbar.module.css +│ │ │ │ └── hotbar.tsx +│ │ │ ├── loader +│ │ │ │ ├── loader.module.css +│ │ │ │ ├── loader.tsx +│ │ │ │ ├── small-loader.module.css +│ │ │ │ └── small-loader.tsx +│ │ │ ├── modal-confirmation +│ │ │ │ ├── modal-confirmation.module.css +│ │ │ │ └── modal-confirmation.tsx +│ │ │ ├── page +│ │ │ │ ├── page.module.css +│ │ │ │ └── page.tsx +│ │ │ ├── paginated-post-feed +│ │ │ │ └── paginated-post-feed.tsx +│ │ │ ├── paginated-user-list +│ │ │ │ ├── paginated-user-list.module.css +│ │ │ │ └── paginated-user-list.tsx +│ │ │ ├── post +│ │ │ │ ├── post-header +│ │ │ │ │ ├── post-header.module.css +│ │ │ │ │ └── post-header.tsx +│ │ │ │ ├── post.module.css +│ │ │ │ └── post.tsx +│ │ │ ├── profile +│ │ │ │ ├── profile.module.css +│ │ │ │ └── profile.tsx +│ │ │ ├── ques-accordion +│ │ │ │ ├── ques-accordion.module.css +│ │ │ │ └── ques-accordion.tsx +│ │ │ ├── scrambler +│ │ │ │ └── scrambler.tsx +│ │ │ ├── seo +│ │ │ │ └── seo.tsx +│ │ │ └── uibox +│ │ │ ├── uibox.module.css +│ │ │ └── uibox.tsx +│ │ ├── environment.ts +│ │ ├── index.css +│ │ ├── index.tsx +│ │ ├── lib +│ │ │ ├── auth.ts +│ │ │ ├── axios.ts +│ │ │ ├── callPosts.ts +│ │ │ ├── create-slug.ts +│ │ │ ├── is-user.ts +│ │ │ └── with-ref.ts +│ │ ├── pages +│ │ │ ├── about +│ │ │ │ ├── about-page.module.css +│ │ │ │ ├── about-page.tsx +│ │ │ │ └── options +│ │ │ │ ├── about-page.tsx +│ │ │ │ ├── policy-page.tsx +│ │ │ │ └── terms-page.tsx +│ │ │ ├── create-post +│ │ │ │ ├── create-post.module.css +│ │ │ │ └── create-post.tsx +│ │ │ ├── edit-profile-page +│ │ │ │ ├── edit-profile-page.module.css +│ │ │ │ └── edit-profile-page.tsx +│ │ │ ├── faqs +│ │ │ │ ├── faqs-page.tsx +│ │ │ │ └── faqs.module.css +│ │ │ ├── follower +│ │ │ │ ├── follower.module.css +│ │ │ │ └── follower.tsx +│ │ │ ├── following +│ │ │ │ ├── following.module.css +│ │ │ │ └── following.tsx +│ │ │ ├── forgot-password +│ │ │ │ ├── forgot-password.module.css +│ │ │ │ └── forgot-password.tsx +│ │ │ ├── general-feed +│ │ │ │ ├── general-feed.module.css +│ │ │ │ └── general-feed.tsx +│ │ │ ├── home +│ │ │ │ ├── planet-list-entry.tsx +│ │ │ │ ├── planet-list.module.css +│ │ │ │ └── planet-list.tsx +│ │ │ ├── landing-page +│ │ │ │ ├── landing-page.module.css +│ │ │ │ └── landing-page.tsx +│ │ │ ├── login +│ │ │ │ ├── login-component.tsx +│ │ │ │ ├── login-html.tsx +│ │ │ │ └── login.module.css +│ │ │ ├── messages-all +│ │ │ │ ├── messages.module.css +│ │ │ │ └── messages.tsx +│ │ │ ├── messages +│ │ │ │ ├── messages-component.tsx +│ │ │ │ ├── messages-html.tsx +│ │ │ │ └── messages.module.css +│ │ │ ├── page404 +│ │ │ │ ├── page404.module.css +│ │ │ │ └── page404.tsx +│ │ │ ├── planet-feed +│ │ │ │ ├── planet-feed.module.css +│ │ │ │ └── planet-feed.tsx +│ │ │ ├── planet-map +│ │ │ │ ├── center-visual.tsx +│ │ │ │ ├── decorative-star.tsx +│ │ │ │ ├── planet-info-card.tsx +│ │ │ │ ├── planet-map.tsx +│ │ │ │ ├── planet-visual.tsx +│ │ │ │ ├── space-traveller.tsx +│ │ │ │ └── star-background.tsx +│ │ │ ├── planets +│ │ │ │ ├── planets-component.tsx +│ │ │ │ ├── planets-html.tsx +│ │ │ │ └── planets.module.css +│ │ │ ├── post-details +│ │ │ │ ├── post-details.module.css +│ │ │ │ └── post-details.tsx +│ │ │ ├── profile-page +│ │ │ │ ├── profile-page.module.css +│ │ │ │ └── profile-page.tsx +│ │ │ ├── reset-password +│ │ │ │ ├── reset-password.module.css +│ │ │ │ └── reset-password.tsx +│ │ │ ├── search-page +│ │ │ │ ├── search-page.module.css +│ │ │ │ └── search-page.tsx +│ │ │ ├── signup +│ │ │ │ ├── signup-component.tsx +│ │ │ │ ├── signup-html.tsx +│ │ │ │ └── signup.module.css +│ │ │ ├── support-page +│ │ │ │ ├── support-page.module.css +│ │ │ │ └── support-page.tsx +│ │ │ ├── user-page +│ │ │ │ ├── user-page.module.css +│ │ │ │ └── user-page.tsx +│ │ │ └── user-settings +│ │ │ ├── options +│ │ │ │ ├── change-email-modal.tsx +│ │ │ │ ├── change-password-modal.tsx +│ │ │ │ ├── change-username-modal.tsx +│ │ │ │ ├── commented.tsx +│ │ │ │ ├── delete-account-modal.tsx +│ │ │ │ ├── liked.tsx +│ │ │ │ ├── manage-account-page.tsx +│ │ │ │ ├── saved.tsx +│ │ │ │ └── your-info-modal.tsx +│ │ │ ├── user-settings-page.module.css +│ │ │ └── user-settings-page.tsx +│ │ └── vite-env.d.ts +│ ├── tsconfig.json +│ └── vite.config.ts +└── server + ├── index.ts + ├── package-lock.json + ├── package.json + ├── src + │ ├── @types + │ │ ├── express.d.ts + │ │ └── model.d.ts + │ ├── environment.ts + │ ├── lib + │ │ └── auth + │ │ ├── adapters + │ │ │ ├── basic-jwt.ts + │ │ │ └── google-oauth.ts + │ │ ├── auth-adapter.ts + │ │ └── auth-worker.ts + │ ├── middlewares + │ │ ├── auth-protected.ts + │ │ └── log.ts + │ ├── models + │ │ ├── conversation.ts + │ │ ├── deletedUser.ts + │ │ ├── follow-relationship.ts + │ │ ├── like-interaction.ts + │ │ ├── location.ts + │ │ ├── media.ts + │ │ ├── message.ts + │ │ ├── planet.ts + │ │ ├── post.ts + │ │ ├── question.ts + │ │ ├── token.ts + │ │ └── user.ts + │ ├── routes + │ │ ├── faqs + │ │ │ └── index.ts + │ │ ├── feed + │ │ │ ├── [planetOrUserId].ts + │ │ │ └── index.ts + │ │ ├── index.ts + │ │ ├── planet + │ │ │ ├── [nameOrId].ts + │ │ │ └── index.ts + │ │ ├── post + │ │ │ ├── [id] + │ │ │ │ ├── comment.ts + │ │ │ │ ├── index.ts + │ │ │ │ ├── like.ts + │ │ │ │ └── save.ts + │ │ │ ├── index.ts + │ │ │ └── search + │ │ │ └── [search].ts + │ │ └── user + │ │ ├── [id].ts + │ │ ├── [id] + │ │ │ └── follow.ts + │ │ ├── changeBio.ts + │ │ ├── changeEmail.ts + │ │ ├── changeUsername.ts + │ │ ├── changeavatar.ts + │ │ ├── changepassword.ts + │ │ ├── chat.ts + │ │ ├── commented.ts + │ │ ├── conversations.ts + │ │ ├── deleteaccount + │ │ │ └── delete.ts + │ │ ├── follower.ts + │ │ ├── following.ts + │ │ ├── forgetpassword.ts + │ │ ├── getchats.ts + │ │ ├── index.ts + │ │ ├── liked.ts + │ │ ├── login.ts + │ │ ├── oauth + │ │ │ └── google.ts + │ │ ├── resetpassword + │ │ │ └── [token].ts + │ │ ├── saved.ts + │ │ ├── search + │ │ │ └── [search].ts + │ │ └── signup.ts + │ └── utils + │ ├── bcrypt.ts + │ ├── email.ts + │ ├── express.ts + │ ├── image.ts + │ ├── jwt.ts + │ └── regex.ts + └── tsconfig.json + +``` diff --git a/client/package-lock.json b/client/package-lock.json index 038cc1b5..e6527d16 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,6 +8,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "axios": "^1.6.8", + "axios-cache-interceptor": "^1.5.3", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", "is-mobile": "^4.0.0", @@ -15,6 +16,7 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.2", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-icons": "^5.2.1", "react-if": "^4.1.5", "react-konva": "^18.2.10", @@ -28,8 +30,8 @@ "@types/node": "^20.12.12", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.11", "@vitejs/plugin-react-swc": "^3.6.0", - "sass": "^1.77.1", "typescript": "^5.4.5" } }, @@ -906,6 +908,15 @@ "@types/react": "*" } }, + "node_modules/@types/react-helmet": { + "version": "6.1.11", + "resolved": "https://registry.npmjs.org/@types/react-helmet/-/react-helmet-6.1.11.tgz", + "integrity": "sha512-0QcdGLddTERotCXo3VFlUSWO3ztraw8nZ6e3zJSgG7apwV5xt+pJUS8ewPBqT4NYB1optGLprNQzFleIY84u/g==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/react-reconciler": { "version": "0.28.8", "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.8.tgz", @@ -943,7 +954,8 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" @@ -967,11 +979,31 @@ "proxy-from-env": "^1.1.0" } }, + "node_modules/axios-cache-interceptor": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.5.3.tgz", + "integrity": "sha512-kPgGId9XW7tR+VF7hgSkqF4f6FrV4ecCyKxjkD9v1hNJ4sXSAskocr7SMKaVHVvrbzVeruwB6yL6Y9/lY1ApKg==", + "dependencies": { + "cache-parser": "1.2.5", + "fast-defer": "1.1.8", + "object-code": "1.3.3" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/arthurfiorette/axios-cache-interceptor?sponsor=1" + }, + "peerDependencies": { + "axios": "^1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=8" }, @@ -1001,7 +1033,8 @@ "version": "3.0.2", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "fill-range": "^7.0.1" }, @@ -1009,11 +1042,17 @@ "node": ">=8" } }, + "node_modules/cache-parser": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.5.tgz", + "integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==" + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -1171,11 +1210,17 @@ "@esbuild/win32-x64": "0.20.2" } }, + "node_modules/fast-defer": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.8.tgz", + "integrity": "sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==" + }, "node_modules/fill-range": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "to-regex-range": "^5.0.1" }, @@ -1232,7 +1277,8 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "is-glob": "^4.0.1" }, @@ -1244,7 +1290,8 @@ "version": "4.3.6", "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.6.tgz", "integrity": "sha512-Ju0+lEMyzMVZarkTn/gqRpdqd5dOPaz1mCZ0SH3JV6iFw81PldE/PEB1hWVEA288HPt4WXW8O7AWxB10M+03QQ==", - "devOptional": true + "optional": true, + "peer": true }, "node_modules/invariant": { "version": "2.2.4", @@ -1258,7 +1305,8 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "binary-extensions": "^2.0.0" }, @@ -1270,7 +1318,8 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1279,7 +1328,8 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "is-extglob": "^2.1.1" }, @@ -1296,7 +1346,8 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.12.0" } @@ -1397,7 +1448,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -1410,6 +1462,11 @@ "node": ">=0.10.0" } }, + "node_modules/object-code": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.3.tgz", + "integrity": "sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==" + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -1419,7 +1476,8 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "devOptional": true, + "optional": true, + "peer": true, "engines": { "node": ">=8.6" }, @@ -1533,6 +1591,25 @@ "react": "^18.3.1" } }, + "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-helmet": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/react-helmet/-/react-helmet-6.1.0.tgz", + "integrity": "sha512-4uMzEY9nlDlgxr61NL3XbKRy1hEkXmKNXhjbAIOVw5vcFrsdYbH2FEwcNyWvWinl103nXgzYNlns9ca+8kFiWw==", + "dependencies": { + "object-assign": "^4.1.1", + "prop-types": "^15.7.2", + "react-fast-compare": "^3.1.1", + "react-side-effect": "^2.1.0" + }, + "peerDependencies": { + "react": ">=16.3.0" + } + }, "node_modules/react-icons": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-5.2.1.tgz", @@ -1610,6 +1687,14 @@ "react": "^18.3.1" } }, + "node_modules/react-side-effect": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.2.tgz", + "integrity": "sha512-PVjOcvVOyIILrYoyGEpDN3vmYNLdy1CajSFNt4TDsVQC5KpTijDvWVoR+/7Rz2xT978D8/ZtFceXxzsPwZEDvw==", + "peerDependencies": { + "react": "^16.3.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-toastify": { "version": "10.0.5", "resolved": "https://registry.npmjs.org/react-toastify/-/react-toastify-10.0.5.tgz", @@ -1641,7 +1726,8 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "picomatch": "^2.2.1" }, @@ -1700,7 +1786,8 @@ "version": "1.77.1", "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.1.tgz", "integrity": "sha512-OMEyfirt9XEfyvocduUIOlUSkWOXS/LAt6oblR/ISXCTukyavjex+zQNm51pPCOiFKY1QpWvEH1EeCkgyV3I6w==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "chokidar": ">=3.0.0 <4.0.0", "immutable": "^4.0.0", @@ -1759,7 +1846,8 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "devOptional": true, + "optional": true, + "peer": true, "dependencies": { "is-number": "^7.0.0" }, diff --git a/client/package.json b/client/package.json index eb6466fe..3c5a2615 100644 --- a/client/package.json +++ b/client/package.json @@ -11,6 +11,7 @@ "dependencies": { "@popperjs/core": "^2.11.8", "axios": "^1.6.8", + "axios-cache-interceptor": "^1.5.3", "bootstrap": "^5.3.3", "dotenv": "^16.4.5", "is-mobile": "^4.0.0", @@ -18,6 +19,7 @@ "react": "^18.3.1", "react-bootstrap": "^2.10.2", "react-dom": "^18.3.1", + "react-helmet": "^6.1.0", "react-icons": "^5.2.1", "react-if": "^4.1.5", "react-konva": "^18.2.10", @@ -31,8 +33,8 @@ "@types/node": "^20.12.12", "@types/react": "^18.3.1", "@types/react-dom": "^18.3.0", + "@types/react-helmet": "^6.1.11", "@vitejs/plugin-react-swc": "^3.6.0", - "sass": "^1.77.1", "typescript": "^5.4.5" } } diff --git a/client/public/among-us.jpg b/client/public/among-us.jpg new file mode 100644 index 00000000..46bf73f2 Binary files /dev/null and b/client/public/among-us.jpg differ diff --git a/client/public/among-us2.jpg b/client/public/among-us2.jpg new file mode 100644 index 00000000..9cf7d5bc Binary files /dev/null and b/client/public/among-us2.jpg differ diff --git a/client/public/logo.webp b/client/public/logo.webp new file mode 100644 index 00000000..b58a8538 Binary files /dev/null and b/client/public/logo.webp differ diff --git a/client/src/app.tsx b/client/src/app.tsx index 60f5a279..738c7e23 100644 --- a/client/src/app.tsx +++ b/client/src/app.tsx @@ -1,36 +1,63 @@ +/* Import for the main stylesheet */ +import './index.css'; + +/* Imports from React */ +import { useEffect, useState } from 'react'; +import { Else, If, Then } from 'react-if'; import { ToastContainer } from 'react-toastify'; + +/* Import from wouter */ import { Switch, Route, useLocation } from 'wouter'; -import About from './pages/about/about'; -import Changepassword from './pages/changepassword/changepassword'; -import Forgetpassword from './pages/forgetpassword/forgetpassword'; -import GeneralFeed from './pages/general-feed/general-feed'; + +/* Utility imports for client side authentication */ +import { Auth, UserAuthContext } from './lib/auth'; + +/* Import for client hostname */ +import { getClientHost } from './environment'; + +/* Imports for all pages of the website */ +import LandingPage from './pages/landing-page/landing-page'; +import { PlanetMap } from './pages/planet-map/planet-map'; +import Home from './pages/home/planet-list'; import Login from './pages/login/login-component'; -import MyFeed from './pages/my-feed/my-feed'; import Signup from './pages/signup/signup-component'; -import Test from './pages/test-page/test-page'; -import UserSettings from './pages/user-settings/user-settings'; -import Resetpassword from './pages/resetpassword/resetpassword'; -import ManageAccount from './pages/user-settings/options/manage-account'; +import About from './pages/about/about-page'; +import Forgetpassword from './pages/forgot-password/forgot-password'; +import GeneralFeed from './pages/general-feed/general-feed'; +import UserSettings from './pages/user-settings/user-settings-page'; +import Resetpassword from './pages/reset-password/reset-password'; +import ManageAccount from './pages/user-settings/options/manage-account-page'; import Messages from './pages/messages/messages-component'; -import Policy from './pages/about/options/policy'; -import Terms from './pages/about/options/terms'; -import { useEffect, useState } from 'react'; -import { Auth, UserAuthContext } from './lib/auth'; -import Cursors from './components/cursor/cursor'; - -import { Else, If, Then } from 'react-if'; -import { Loader } from './components/loader/loader'; -import PostPage from './pages/post-page/post-page'; +import Policy from './pages/about/options/policy-page'; +import Terms from './pages/about/options/terms-page'; +import FAQs from './pages/faqs/faqs-page'; +import LikedPage from './pages/user-settings/options/liked'; +import SavedPage from './pages/user-settings/options/saved'; +import CommentedPostPage from './pages/user-settings/options/commented'; +import FollowingPage from './pages/following/following'; +import FollowerPage from './pages/follower/follower'; +import PostPage from './pages/create-post/create-post'; import UserPage from './pages/user-page/user-page'; import ProfilePage from './pages/profile-page/profile-page'; -import { PlanetMap } from './pages/planet-map/planet-map'; - -import './index.css'; -import Home from './pages/home/home'; -import PostDetailPage from './pages/post/post'; +import PostDetailPage from './pages/post-details/post-details'; import Planets from './pages/planets/planets-component'; import PlanetFeed from './pages/planet-feed/planet-feed'; +import MessagesAll from './pages/messages-all/messages'; +import SearchPage from './pages/search-page/search-page'; +import Page404 from './pages/page404/page404'; +import EditProfilePage from './pages/edit-profile-page/edit-profile-page'; +import SupportPage from './pages/support-page/support-page'; +import AboutSkynetPage from './pages/about/options/about-page'; + +/* Imports for custom componets made */ +import SEO from './components/seo/seo'; +import { Loader } from './components/loader/loader'; +/** + * Contructs, manages, and returns the entire client side. + * + * @returns the client side application as a JSX.Element. + */ export const App = () => { const [loading, setLoading] = useState(true); const [authenticatedUser, setAuthenticatedUser] = useState(); @@ -46,6 +73,9 @@ export const App = () => { }); }, [loc]); + /** + * The common routes throughout this website. + */ const commonRoutes = ( <> @@ -53,19 +83,25 @@ export const App = () => { + + + {(params) => } - {(params) => } - 404 Not Found ); return ( <> + + - @@ -76,29 +112,37 @@ export const App = () => { - {commonRoutes} + - - + {() => } + {() => } - - + + + + + + + + {commonRoutes} + + 404 Not Found diff --git a/client/src/assets/images/icons8-cursor-38.png b/client/src/assets/images/icons8-cursor-38.png deleted file mode 100644 index 7715a690..00000000 Binary files a/client/src/assets/images/icons8-cursor-38.png and /dev/null differ diff --git a/client/src/assets/images/SkynetLogo.png b/client/src/assets/images/skynet-logo.png similarity index 100% rename from client/src/assets/images/SkynetLogo.png rename to client/src/assets/images/skynet-logo.png diff --git a/client/src/assets/videos/home.gif b/client/src/assets/videos/home.gif new file mode 100644 index 00000000..91c023a3 Binary files /dev/null and b/client/src/assets/videos/home.gif differ diff --git a/client/src/assets/videos/message.gif b/client/src/assets/videos/message.gif new file mode 100644 index 00000000..b280d72c Binary files /dev/null and b/client/src/assets/videos/message.gif differ diff --git a/client/src/assets/videos/post.gif b/client/src/assets/videos/post.gif new file mode 100644 index 00000000..6ef9fb12 Binary files /dev/null and b/client/src/assets/videos/post.gif differ diff --git a/client/src/components/Comment/Comment.module.css b/client/src/components/Comment/Comment.module.css deleted file mode 100644 index fe6a9945..00000000 --- a/client/src/components/Comment/Comment.module.css +++ /dev/null @@ -1,102 +0,0 @@ -.link { - color: inherit; - text-decoration: none; -} - -.commentContainer { - font-family: TTOctosquares; - max-width: 750px; - width: 100%; - margin-top: 20px; - display: flex; - flex-direction: column; -} - -.commentsList { - flex-grow: 1; -} - -.commentForm { - display: flex; - flex-direction: column; - margin-top: 20px; -} - -.commentTextArea { - width: 100%; - min-height: 50px; - padding: 10px; - font-size: 0.9rem; - /* Slightly smaller than the post text */ - resize: vertical; - border: 1px solid #ccc; - border-radius: 5px; - margin-bottom: 10px; - font-family: TTOctosquares; -} - -.commentButton { - width: fit-content; - align-self: flex-end; - padding: 5px 10px; - font-size: 1rem; - background-color: #007bff; - color: #fff; - border: none; - border-radius: 5px; - cursor: pointer; - font-family: TTOctosquares; -} - -.postContainer { - border-radius: 5px; - padding: 10px; - margin-bottom: 10px; - font-size: 0.8rem; - /* Slightly smaller than the post text */ - font-family: TTOctosquares; -} - -.userContainer { - padding: 2px 8px; - margin: 2px 0px; - width: max-content; - text-align: left; - font-size: 0.8rem; - /* Slightly smaller than the post text */ -} - -.paraContainer { - padding: 10px; - padding-bottom: 5px; - margin: 2px 0px; - width: 100%; - text-align: left; - word-break: break-word; - font-size: 0.9rem; - /* Slightly smaller than the post text */ -} - -.postDate { - font-size: 0.6em; - color: var(--soft-shadow); -} - -.iconsContainer { - display: flex; - font-size: 1.2rem; - /* Slightly smaller than the post icons */ - gap: 0.75rem; -} - -.iconsContainer button { - background-color: transparent; - padding: 5px; - width: auto; - box-shadow: none; - border: none; -} - -.share { - margin-right: auto; -} \ No newline at end of file diff --git a/client/src/components/Comment/Comment.tsx b/client/src/components/Comment/Comment.tsx deleted file mode 100644 index d6c224bb..00000000 --- a/client/src/components/Comment/Comment.tsx +++ /dev/null @@ -1,240 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { api } from '../../lib/axios'; -import styles from './Comment.module.css'; -import UIBox from '../UIBox/UIBox'; -import { FaRegHeart, FaHeart, FaRegBookmark, FaBookmark, FaRocketchat } from 'react-icons/fa'; -import { RiShareBoxLine } from 'react-icons/ri'; -import { Link } from 'wouter'; - -interface CommentProps { - postId: string; -} - -interface Comment { - _id: string; - authorId: string; - content: string; - createdAt: string; - userName: string; - repost: number; - like: number; - comment: number; - isSaved?: boolean; - isLiked?: boolean; -} - -interface UserProp { - username: string; - userURL: string; - imageURL?: string; -} - -const User = (props: UserProp): JSX.Element => { - return ( - - {props.username} - - } - /> - ); -}; - -const Comment: React.FC = ({ postId }) => { - const [comment, setComment] = useState(''); - const [comments, setComments] = useState([]); - const [page, setPage] = useState(1); - const [hasMore, setHasMore] = useState(true); - const [loading, setLoading] = useState(false); - const fetchComments = async (page: number) => { - setLoading(true); - try { - const response = await api.get(`/post/${postId}/comment`, { - params: { limit: 10, page }, - }); - if (response.data.success) { - const commentsWithStatus = await Promise.all( - response.data.value.map(async (comment: Comment) => { - const [savedRes, likedRes] = await Promise.all([ - api.get(`/post/${comment._id}/save`), - api.get(`/post/${comment._id}/like`), - ]); - return { - ...comment, - isSaved: savedRes.data.success ? savedRes.data.value : false, - isLiked: likedRes.data.value, - }; - }) - ); - setComments((prevComments) => [...prevComments, ...commentsWithStatus]); - setHasMore(commentsWithStatus.length === 10); // Assuming 10 is the limit - } else { - console.error(response.data.statusMessage); - } - } catch (error) { - console.error('Error fetching comments:', error); - } finally { - setLoading(false); - } - }; - - useEffect(() => { - fetchComments(page); - }, [page, postId]); - - const handleCommentChange = (event: React.ChangeEvent) => { - setComment(event.target.value); - }; - - const handleCommentSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - if (comment.trim()) { - try { - const response = await api.post(`/post/${postId}/comment`, { content: comment }); - if (response.data.success) { - const newComment = response.data.value; - const [savedRes, likedRes] = await Promise.all([ - api.get(`/post/${newComment._id}/save`), - api.get(`/post/${newComment._id}/like`), - ]); - newComment.isSaved = savedRes.data.success ? savedRes.data.value : false; - newComment.isLiked = likedRes.data.value; - const updatedComments = [newComment, ...comments].sort( - (a: Comment, b: Comment) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime() - ); - setComments(updatedComments); - setComment(''); - } else { - console.error(response.data.statusMessage); - } - } catch (error) { - console.error('Error posting comment:', error); - } - } - }; - - const handleBookmark = async (commentId: string, isSaved: boolean) => { - try { - if (isSaved) { - await api.delete(`/post/${commentId}/save`); - } else { - await api.post(`/post/${commentId}/save`); - } - setComments((prevComments) => - prevComments.map((comment) => - comment._id === commentId ? { ...comment, isSaved: !isSaved } : comment - ) - ); - } catch (error) { - console.error('Error updating save status:', error); - } - }; - - const handleLike = async (commentId: string, isLiked: boolean) => { - try { - if (isLiked) { - await api.delete(`/post/${commentId}/like`); - setComments((prevComments) => - prevComments.map((comment) => - comment._id === commentId ? { ...comment, like: comment.like - 1, isLiked: false } : comment - ) - ); - } else { - await api.post(`/post/${commentId}/like`); - setComments((prevComments) => - prevComments.map((comment) => - comment._id === commentId ? { ...comment, like: comment.like + 1, isLiked: true } : comment - ) - ); - } - } catch (error) { - console.error('Error updating like status:', error); - } - }; - - function formatDate(dateString: string): string { - const date = new Date(dateString); - const year = date.getFullYear(); - const month = date.getMonth() + 1; // Months are zero-indexed - const day = date.getDate(); - const hours = date.getHours(); - const minutes = date.getMinutes(); - const seconds = date.getSeconds(); - return `${year}/${month}/${day} ${hours}:${minutes}:${seconds}`; - } - - const loadMoreComments = () => { - if (hasMore && !loading) { - setPage((prevPage) => prevPage + 1); - } - }; - - return ( -
-
- - } - curved - /> -
- - -
-
- - } - /> - - ); -}; - -export default PostPage; diff --git a/client/src/pages/post/post.module.css b/client/src/pages/post/post.module.css deleted file mode 100644 index e69de29b..00000000 diff --git a/client/src/pages/post/post.tsx b/client/src/pages/post/post.tsx deleted file mode 100644 index c6f6f373..00000000 --- a/client/src/pages/post/post.tsx +++ /dev/null @@ -1,94 +0,0 @@ -import styles from './post.module.css'; - -import React, { useEffect, useState } from 'react'; -import { api } from '../../lib/axios'; -import Post from '../../components/Post/Post'; -import Page from '../../components/Page/Page'; -import Comment from '../../components/Comment/Comment'; - -interface Post { - _id: string; - authorId: string; - content: string; - likeCount: number; - commentCount: number; - repostCount: number; - location: { - planetId: string; - latitude: number; - longitude: number; - _id: string; - }; - media: any[]; - createdAt: Date; - deleted: boolean; - userName: string; -} - -interface PostResponse { - statusCode: number; - statusMessage: string; - value: Post; - success: boolean; -} - -interface Props { - id: string; -} - -const PostDetailPage: React.FC = ({ id }) => { - const [post, setPost] = useState(null); - const [error, setError] = useState(null); - - useEffect(() => { - const fetchPost = async () => { - try { - const response = await api.get(`/post/${id}`); - if (response.data.success) { - setPost(response.data.value); - } else { - setError(response.data.statusMessage); - } - } catch (err) { - setError('Error fetching the post.'); - } - }; - - fetchPost(); - }, [id]); - - if (error) { - return
Error: {error}
; - } - - if (!post) { - return
Loading...
; - } - - const postDetail = ( - - ); - - return ( - - {postDetail} - - - } - /> - ); -}; - -export default PostDetailPage; diff --git a/client/src/pages/profile-page/profile-page.tsx b/client/src/pages/profile-page/profile-page.tsx index 41e1b47f..e5aa84d3 100644 --- a/client/src/pages/profile-page/profile-page.tsx +++ b/client/src/pages/profile-page/profile-page.tsx @@ -1,87 +1,37 @@ -import styles from './profile-page.module.css'; -import Page from '../../components/Page/Page'; -import Post from '../../components/Post/Post'; -import Profile from '../../components/Profile/Profile'; +import Page from '../../components/page/page'; +import Profile from '../../components/profile/profile'; import { api } from '../../lib/axios'; -import { useContext, useEffect, useState } from 'react'; +import { useContext } from 'react'; import { UserAuthContext } from '../../lib/auth'; +import { PaginatedPostFeed } from '../../components/paginated-post-feed/paginated-post-feed'; -interface Post { - authorId: string; - commentCount: number; - content: string; - createdAt: Date; - deleted: false; - likeCount: number; - location: { - planetId: string; - latitude: number; - longitude: number; - _id: string; - }; - media: []; - repostCount: number; - __v: number; - _id: string; -} - +/** + * ProfilePage component representing the own user's profile page, containing profile + * information and personal feed (all posts made by the user) and being different by + * being and editable profile page. + * + * @return JSX.Element -Profile Page as a JSX.Element + */ const ProfilePage = () => { const user = useContext(UserAuthContext); - const [displayedPosts, setDisplayedPosts] = useState(Array()); - - useEffect(() => { - const displayPosts = async function () { - setDisplayedPosts(await getPosts()); - }; - displayPosts(); - }, [user]); - - async function getPosts() { - try { - if (!user) return; - const res = await api.get('/feed/' + user._id); - const postArray = res.data.value; - let postElements = postArray.map((post: Post) => { - return ( - - ); - }); - if (postArray.length == 0) { - postElements = [<>Nothing yet...]; - } - return postElements; - } catch (err) { - console.log(err); - } - } return ( - + api.get(`/feed/${user._id}?page=${page}`).then((res) => res.data.value)} /> - {displayedPosts} } /> ); }; +/** + * Exports the ProfilePage component for external use. + */ export default ProfilePage; diff --git a/client/src/pages/reset-password/reset-password.module.css b/client/src/pages/reset-password/reset-password.module.css new file mode 100644 index 00000000..d90ce29f --- /dev/null +++ b/client/src/pages/reset-password/reset-password.module.css @@ -0,0 +1,74 @@ +.resetpassword-container { + display: grid; + align-items: center; + justify-items: center; + height: 80vh; +} + +.resetpassword-form { + background: var(--signup-background); + padding: 48px 59px; + border: 2px solid black; + box-shadow: 4px 5px 0px #000; +} + +.resetpassword-upperdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 3px 0px #000; +} + +.resetpassword-bottomdiv { + background: var(--signup-background); + padding: 4px 0px; + border: 2px solid black; + box-shadow: 4px 4px 0px #000; +} + +.input { + display: block; + width: 100%; + line-height: 28pt; + margin-bottom: 20pt; + box-shadow: 4px 5px; +} + +.input:focus { + background: var(--signup-input-focus); +} + +.button { + padding: 2px; + width: 82.5%; + box-shadow: 4px 5px; +} + +.button:active { + box-shadow: 4px 4px var(--signup-button-active)5; + transform: translateY(5px); +} + +.select { + width: 100%; + margin-bottom: 20pt; + box-shadow: 4px 5px; + padding: 6px; +} + +.img { + max-width: 200px !important; +} + +.h1 { + font-family: Bitsumishi; +} + +.h5 { + font-family: BabaPro; +} + +.message { + font-family: BabaPro; + color: red; +} \ No newline at end of file diff --git a/client/src/pages/reset-password/reset-password.tsx b/client/src/pages/reset-password/reset-password.tsx new file mode 100644 index 00000000..69df8717 --- /dev/null +++ b/client/src/pages/reset-password/reset-password.tsx @@ -0,0 +1,105 @@ +/* Stylesheet imports */ +import styles from './reset-password.module.css'; + +/* Import from react and toastify */ +import React, { useState } from 'react'; +import { toast } from 'react-toastify'; + +/* Import from wouter */ +import { useLocation } from 'wouter'; + +/* Imports for frontend api call and authentication verification */ +import { api } from '../../lib/axios'; + +/* Imports from other components created */ +import Page from '../../components/page/page'; + +/* Import from local files */ +import logoUrl from '../../assets/images/skynet-logo.png'; + +/* Define the Props interface */ +interface Props { + token: string; +} + +/** + * Constructs, manages, and returns the Resetpassword component. + * + * @param token The token used to reset the password + * @return The Resetpassword component as a JSX.Element + */ +const Resetpassword: React.FC = ({ token }) => { + const [password, setPassword] = useState(''); + const [confirmpassword, setConfirmPassword] = useState(''); + + const [_, navigate] = useLocation(); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + + if (loading) return; + setLoading(true); + + try { + await api.patch(`/user/resetpassword/${token}`, { + password, + confirmpassword, + }); + + navigate('/login'); + toast.info('Your password has been reset!'); + } catch (error: any) { + toast.error(error.response.data.error); + } finally { + setLoading(false); + } + }; + + return ( + +
+ Skynet Logo +

RESET PASSWORD

+
+
+ + setPassword(event.target.value)} + required + /> +
+ setConfirmPassword(event.target.value)} + required + /> +
+
+ +
+ +
+
+
+ + } + /> + ); +}; + +export default Resetpassword; diff --git a/client/src/pages/resetpassword/resetpassword.module.css b/client/src/pages/resetpassword/resetpassword.module.css deleted file mode 100644 index 47f74e5f..00000000 --- a/client/src/pages/resetpassword/resetpassword.module.css +++ /dev/null @@ -1,74 +0,0 @@ -.resetpassword-container { - display: grid; - align-items: center; - justify-items: center; - height: 80vh; - } - - .resetpassword-form { - background: var(--signup-background); - padding: 48px 59px; - border: 2px solid black; - box-shadow: 4px 5px 0px #000; - } - - .resetpassword-upperdiv { - background: var(--signup-background); - padding: 4px 0px; - border: 2px solid black; - box-shadow: 4px 3px 0px #000; - } - - .resetpassword-bottomdiv { - background: var(--signup-background); - padding: 4px 0px; - border: 2px solid black; - box-shadow: 4px 4px 0px #000; - } - - .input { - display: block; - width: 100%; - line-height: 28pt; - margin-bottom: 20pt; - box-shadow: 4px 5px; - } - - .input:focus { - background: var(--signup-input-focus); - } - - .button { - padding: 2px; - width: 50%; - box-shadow: 4px 5px; - } - - .button:active { - box-shadow: 4px 4px var(--signup-button-active)5; - transform: translateY(5px); - } - - .select { - width: 100%; - margin-bottom: 20pt; - box-shadow: 4px 5px; - padding: 6px; - } - - .img { - max-width: 250px !important; - } - - .h1 { - font-family: Bitsumishi; - } - - .h5 { - font-family: BabaPro; - } - - .message { - font-family: BabaPro; - color: red; - } \ No newline at end of file diff --git a/client/src/pages/resetpassword/resetpassword.tsx b/client/src/pages/resetpassword/resetpassword.tsx deleted file mode 100644 index e100955a..00000000 --- a/client/src/pages/resetpassword/resetpassword.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import styles from './resetpassword.module.css'; -import logoUrl from '../../assets/images/SkynetLogo.png'; -import React, {useState} from 'react'; -import { api } from '../../lib/axios'; - -interface Props { - token: string; -} - -const Resetpassword: React.FC = ({ token }) => { - const [password, setPassword] = useState(''); - const [confirmpassword, setConfirmPassword] = useState(''); - const [message, setMessage] = useState(''); - const [resetSuccess, setResetSuccess] = useState(false); - - const handleSubmit = async (event: React.FormEvent) => { - event.preventDefault(); - try { - const response = await api.patch(`/user/resetpassword/${token}`, { - password, - confirmpassword - }); - setMessage(response.data.message); - if (response.data.message === 'Password has been reset successfully.') { - setResetSuccess(true); - } - } catch (error: any) { - setMessage(error.response.data.message); - } - }; - - return ( -
-
- Skynet Logo -

RESET YOUR PASSWORD

-
-
-
- setPassword(event.target.value)} - required - /> -
- setConfirmPassword(event.target.value)} - required - /> -
-
- -
-
-
-
-
-
- {message} - {resetSuccess && ( - - {' '} - Let's{' '} - setResetSuccess(false)}> - log in - - - )} -
-
-
- ); -}; - -export default Resetpassword; diff --git a/client/src/pages/search-page/search-page.module.css b/client/src/pages/search-page/search-page.module.css new file mode 100644 index 00000000..92ddc6dd --- /dev/null +++ b/client/src/pages/search-page/search-page.module.css @@ -0,0 +1,46 @@ +.general { + font-family: TTOctosquares; + padding: 0px; + margin: 0px; + width: 100%; + max-width: 1000px; + height: fit-content; + align-items: center; +} + +.search { + padding: 10px; + margin: 2px 0px; + text-align: left; + word-break: break-word; + font-size: 1.1rem; +} + +.search-box { + border: none; + font-family: TTOctosquares; + width: 100%; + background-color: inherit; + resize: none; +} + +.buttons { + padding-top: 6px; +} + +.submit { + border: none; + border-bottom: 2px solid gray; + padding: 5px; + background-color: transparent; + width: 50%; +} + +.selected { + color: white; + background-color: gray; +} + +.submit-box { + padding: 2px 7px; +} \ No newline at end of file diff --git a/client/src/pages/search-page/search-page.tsx b/client/src/pages/search-page/search-page.tsx new file mode 100644 index 00000000..ff4e7514 --- /dev/null +++ b/client/src/pages/search-page/search-page.tsx @@ -0,0 +1,101 @@ +import styles from './search-page.module.css'; +import { useEffect, useState } from 'react'; +import { api } from '../../lib/axios'; +import Page from '../../components/page/page'; +import UIBox from '../../components/uibox/uibox'; +import { Container } from 'react-bootstrap'; +import { PaginatedPostFeed } from '../../components/paginated-post-feed/paginated-post-feed'; +import { PaginatedUserList } from '../../components/paginated-user-list/paginated-user-list'; + +/** + * Creates a Search Page in which the user can search for posts or users by name/keywords. + * + * @return JSX.Element - SearchPage as a JSX.Element + */ +const SearchPage = function () { + const [search, setSearch] = useState(''); + const [feedComponent, setFeedComponent] = useState(); + // If true, searches for posts + const [searchPost, setSearchPost] = useState(true); + + /** + * Use effect used to search a post everytime the user presses a button between user or + * post or everytime the user types something they want to search for. + */ + useEffect(() => { + setFeedComponent(<>); + if (search != '') { + setTimeout(() => { + setFeedComponent( + searchPost ? ( + + api.get(`/post/search/${search}?page=${page}`).then((res) => res.data.value) + } + /> + ) : ( + + api.get(`/user/search/${search}?page=${page}`).then((res) => res.data.value) + } + /> + ), + ); + }, 0); + } + }, [search, searchPost]); + + /** + * Stores what is typed by the user in the search bar in a React state. + * + * @returns void + */ + function storeSearch() { + const search = document.getElementsByTagName('input'); + const content = search[0].value; + setSearch(content); + } + + return ( + + + + } + curved + /> +
+ + +
+
+
{feedComponent}
+ + } + /> + ); +}; + +export default SearchPage; diff --git a/client/src/pages/signup/signup-component.tsx b/client/src/pages/signup/signup-component.tsx index c6596ae3..d41b05b3 100644 --- a/client/src/pages/signup/signup-component.tsx +++ b/client/src/pages/signup/signup-component.tsx @@ -5,11 +5,18 @@ import { api } from '../../lib/axios'; import { Auth } from '../../lib/auth'; import SignupHtml from './signup-html'; +/** + * Signup component handles user registration. + * + * @component + */ const Signup = () => { interface Planet { - _id: string; - name: string; + _id: string; //The ID of the planet. + name: string; //The name of the planet. } + + const [loading, setLoading] = useState(false); const [planets, setPlanets] = useState>([]); const [username, setUsername] = useState(''); const [email, setEmail] = useState(''); @@ -17,11 +24,16 @@ const Signup = () => { const [location, setLocation] = useState(''); const [_, navigate] = useLocation(); + /** + * Fetches the list of planets from the API. + * + * @function fetchPlanets + * @returns {Promise} - A promise that resolves when the planets are fetched and state is updated. + */ useEffect(() => { const fetchPlanets = async () => { const { data: res } = await api.get('/planet'); try { - // console.log(res); setPlanets(res.value); setLocation(res.value[0]._id); } catch (error) { @@ -32,7 +44,15 @@ const Signup = () => { fetchPlanets(); }, []); + /** + * Handles form submission for user signup. + * + * @function submitForm + * @param {Object} e - The event object from the form submission. + * @returns {Promise} - A promise that resolves when the form is submitted and response is handled. + */ const submitForm = async (e: any) => { + setLoading(true); e.preventDefault(); const geoLoc = await new Promise((res) => { @@ -58,21 +78,12 @@ const Signup = () => { const token = res.value; Auth.saveToken(token); navigate('/'); - toast.success('User created successfully'); + toast.success('Account created successfully.'); } catch (err: any) { - toast.error(`🦄 ${err.response.data.error}`, { - position: 'top-right', - hideProgressBar: false, - closeOnClick: true, - pauseOnHover: true, - draggable: true, - progress: undefined, - theme: 'colored', - }); + toast.error(err.response.data.error); + } finally { + setLoading(false); } - - // if (!res.success) throw res.error; - // else alert('new user created'); }; return ( @@ -88,6 +99,7 @@ const Signup = () => { setPassword={setPassword} setLocation={setLocation} submitForm={submitForm} + loading={loading} /> ); }; diff --git a/client/src/pages/signup/signup-html.tsx b/client/src/pages/signup/signup-html.tsx index 15c9d516..80b8a0d5 100644 --- a/client/src/pages/signup/signup-html.tsx +++ b/client/src/pages/signup/signup-html.tsx @@ -1,34 +1,56 @@ -import { useLocation } from 'wouter'; +import { Link } from 'wouter'; import styles from './signup.module.css'; -import logoUrl from '../../assets/images/SkynetLogo.png'; +import logoUrl from '../../assets/images/skynet-logo.png'; import { GoogleAuthButton } from '../../components/google-auth-btn/google-auth-btn'; +import { Else, If, Then } from 'react-if'; +import { SmallLoader } from '../../components/loader/small-loader'; +import { useState } from 'react'; +import { FaEyeSlash, FaEye } from 'react-icons/fa'; + +/** + * SignupHtml component handles the presentation layer for user signup. + * + * @component + * @param {Object} props - The properties object. + * @param {Array} props.planets - The list of planets. + * @param {string} props.username - The username entered by the user. + * @param {string} props.email - The email entered by the user. + * @param {string} props.password - The password entered by the user. + * @param {Function} props.setUsername - The function to update the username. + * @param {Function} props.setEmail - The function to update the email. + * @param {Function} props.setPassword - The function to update the password. + * @param {Function} props.setLocation - The function to update the location. + * @param {Function} props.submitForm - The function to handle form submission. + * @param {boolean} props.loading - The loading state to indicate form submission. + * @returns {JSX.Element} The rendered signup HTML component. + */ const SignupHtml = ({ planets, username, email, password, - location, - setPlanets, setUsername, setEmail, setPassword, setLocation, submitForm, + loading, }: any) => { - const [_, navigate] = useLocation(); + const [showPassword, setShowPassword] = useState(false); + return (
-
+
Skynet Logo

SKY.NET

STAY CONNECTED ACROSS THE GALAXY
-
-
+
+ setEmail(e.target.value)} /> - setPassword(e.target.value)} - /> + +
+ setPassword(e.target.value)} + /> + + +
+
- +

- +
-
- Already a User. Login Below - +
+ + Already Registered? Login Instead +
+
diff --git a/client/src/pages/signup/signup.module.css b/client/src/pages/signup/signup.module.css index 45e5ec86..52e77845 100644 --- a/client/src/pages/signup/signup.module.css +++ b/client/src/pages/signup/signup.module.css @@ -1,16 +1,19 @@ +/* Styles for the container of the signup component */ .signup-container { + padding-bottom: 10px; display: grid; align-items: center; justify-items: center; } +/* Styles for the signup form */ .signup-form { background: var(--signup-background); - padding: 29px 59px; border: 2px solid black; box-shadow: 4px 5px 0px #000; } +/* Styles for the upper div of the signup form */ .signup-upperdiv { background: var(--signup-background); padding: 4px 0px; @@ -18,6 +21,7 @@ box-shadow: 4px 3px 0px #000; } +/* Styles for the bottom div of the signup form */ .signup-bottomdiv { background: var(--signup-background); padding: 4px 0px; @@ -25,47 +29,61 @@ box-shadow: 4px 4px 0px #000; } +/* Styles for the login button within the signup form */ .signup-button-login { box-shadow: 4px 5px gray !important; background: black !important; color: white !important; } +/* Styles for the active state of the login button within the signup form */ .signup-button-login:active { - box-shadow: 4px 4px var(--signup-button-active) 5 !important; + box-shadow: 4px 4px var(--signup-button-active) 5 !important; transform: translateY(5px) !important; } -input, -.button, -.select { +/* Styles for input fields within the signup form */ +.signup-form input { + font-size: 20px; + font-family: Fjalla One; + padding-left: 5px; + border: 2px solid black; + outline: none; +} + +/* Styles for buttons and select dropdowns within the signup form */ +.signup-form .button, +.signup-form .select { font-size: 14pt; - font-family: 'BabaPro'; + font-family: BabaPro; background: var(--signup-background); border: 2px solid black; outline: none; padding-left: 5px; } +/* Common styles for input elements */ .input { display: block; width: 100%; line-height: 28pt; - margin-bottom: 20pt; box-shadow: 4px 5px; } +/* Common styles for buttons */ .button { padding: 2px; width: 50%; box-shadow: 4px 5px; } +/* Styles for the active state of buttons */ .button:active { box-shadow: 4px 4px var(--signup-button-active) 5; transform: translateY(5px); } +/* Styles for select dropdowns */ .select { width: 100%; margin-bottom: 20pt; @@ -73,22 +91,26 @@ input, padding: 6px; } +/* Styles for images */ .img { max-width: 250px !important; } +/* Styles for focused input fields */ .input:focus { background: var(--signup-input-focus); } +/* Styles for heading elements */ .h1 { font-family: Bitsumishi; } .h5 { - font-family: BabaPro; + font-family: TTOctosquares; } +/* Styles for span elements */ .span { - font-family: TTOctosquares; + font-family: TTOctosquares; } diff --git a/client/src/pages/support-page/support-page.module.css b/client/src/pages/support-page/support-page.module.css new file mode 100644 index 00000000..0664a5d2 --- /dev/null +++ b/client/src/pages/support-page/support-page.module.css @@ -0,0 +1,8 @@ +.panel { + font-family: Fjalla One; + width: 75%; +} + +.panel h1 { + font-family: Babapro; +} \ No newline at end of file diff --git a/client/src/pages/support-page/support-page.tsx b/client/src/pages/support-page/support-page.tsx new file mode 100644 index 00000000..b50fe5e1 --- /dev/null +++ b/client/src/pages/support-page/support-page.tsx @@ -0,0 +1,57 @@ +/* Stylesheet imports */ +import styles from './support-page.module.css'; + +/* Imports from other components created */ +import Page from '../../components/page/page'; +import { Container } from 'react-bootstrap'; + +/** + * Constructs and returns the Support page for this website in which users + * can contact someone in case of any errors while using the website. + * + * @returns the Support page as a JSX.Element. + */ +const SupportPage = () => { + return ( + <> + +

Sky.net support

+

We are sad to see you had a problem with Sky.net. 😥

+
+

+ Please check our FAQ (Frequently Asked Questions) page first as your + request or problem may have already been answered by one of our professionals before. +

+

+ If you could not find your problem on FAQ or you have any other problems, technical issues + or questions, feel free to contact us using our support contact: +

+ +
+
+
+
+

+ *Phone call support only in Canada. +

+ + } + /> + + ); +}; + +/** + * Exports the Support page for external use. + */ +export default SupportPage; diff --git a/client/src/pages/test-page/test-page.module.css b/client/src/pages/test-page/test-page.module.css deleted file mode 100644 index d5d291b2..00000000 --- a/client/src/pages/test-page/test-page.module.css +++ /dev/null @@ -1,4 +0,0 @@ -.page-container { - max-width: 100vw; - padding: 0%; -} diff --git a/client/src/pages/test-page/test-page.tsx b/client/src/pages/test-page/test-page.tsx deleted file mode 100644 index 982a691a..00000000 --- a/client/src/pages/test-page/test-page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import styles from './test-page.module.css'; -import Container from 'react-bootstrap/Container'; - -import Header from '../../components/Header/Header'; -import Hotbar from '../../components/Hotbar/Hotbar'; -import Post from '../../components/Post/Post'; - -// PAGE FOR TESTING COMPONENTS -// Feel free to edit it -const Test = () => { - return ( - - - -
- - - - - - ); -}; - -export default Test; diff --git a/client/src/pages/user-page/user-page.tsx b/client/src/pages/user-page/user-page.tsx index 1c1bb809..6e78949b 100644 --- a/client/src/pages/user-page/user-page.tsx +++ b/client/src/pages/user-page/user-page.tsx @@ -1,57 +1,45 @@ -import styles from './user-page.module.css'; -import Page from '../../components/Page/Page'; -import Post from '../../components/Post/Post'; -import Profile from '../../components/Profile/Profile'; -import { useLocation, useParams } from 'wouter'; +/* Imports from React */ import { useEffect, useState } from 'react'; -import { api } from '../../lib/axios'; -import { isUser } from '../../lib/isUser'; -interface Post { - authorId: string; - commentCount: number; - content: string; - createdAt: Date; - deleted: false; - likeCount: number; - location: { - planetId: string; - latitude: number; - longitude: number; - _id: string; - }; - media: []; - repostCount: number; - __v: number; - _id: string; -} +/* Import from wouter */ +import { useLocation, useParams } from 'wouter'; + +/* Imports from other components created */ +import Page from '../../components/page/page'; +import Profile from '../../components/profile/profile'; +import { PaginatedPostFeed } from '../../components/paginated-post-feed/paginated-post-feed'; +import SEO from '../../components/seo/seo'; +/* Imports for frontend api call and authentication verification */ +import { isUser } from '../../lib/is-user'; +import { api } from '../../lib/axios'; + +/** + * Constructs, manages, and returns the UserPage component, which + * represents the profile page of another specific user. + * + * @return The UserPage component as a JSX.Element + */ const UserPage = () => { - const [username, setUsername] = useState(''); - const [follower, setFollower] = useState(0); - const [following, setFollowing] = useState(0); - const [postCount, setPostCount] = useState(0); - const [displayedPosts, setDisplayedPosts] = useState(Array()); - const [userID, setUserID] = useState(''); + const [userData, setUserData] = useState({ userName: '' }); const [_, navigate] = useLocation(); - let { id } = useParams() ?? ''; + let { id = '' } = useParams(); + /** + * Use effect used to check if it's the own user's profile page, + * and if so, redirects him to editable version of the profile page. + * If not, retrieves the data from a user and store it in a + * React state. + */ useEffect(() => { const getUserData = async function () { if (id !== undefined && (await isUser(id))) { - navigate('/profile'); - return; + return navigate('/profile', { replace: true }); } else { try { if (id == '') return; const res = await api.get('/user/' + id); - const data = res.data.value; - setUserID(data._id); - setUsername(data.userName); - setFollower(data.followerCount); - setFollowing(data.followingCount); - setPostCount(data.postCount); - // setDisplayedPosts(await getPosts()); + setUserData(res.data.value); } catch (err) { console.log(err); } @@ -61,57 +49,23 @@ const UserPage = () => { getUserData(); }, []); - useEffect(() => { - const displayPosts = async function () { - setDisplayedPosts(await getPosts()); - }; - displayPosts(); - }, [userID]); - - async function getPosts() { - try { - if (userID == '') return; - const res = await api.get('/feed/' + userID); - const postArray = res.data.value; - console.log(postArray); - let postElements = postArray.map((post: Post) => { - return ( - - ); - }); - if (postArray.length == 0) { - postElements = [<>Nothing yet...]; - } - return postElements; - } catch (err) { - console.log(err); - } - } - return ( - + + + + api.get(`/feed/${id}?page=${page}`).then((res) => res.data.value)} /> - {displayedPosts} } /> diff --git a/client/src/pages/user-settings/options/change-email.tsx b/client/src/pages/user-settings/options/change-email-modal.tsx similarity index 51% rename from client/src/pages/user-settings/options/change-email.tsx rename to client/src/pages/user-settings/options/change-email-modal.tsx index 3b406aff..f9f2474f 100644 --- a/client/src/pages/user-settings/options/change-email.tsx +++ b/client/src/pages/user-settings/options/change-email-modal.tsx @@ -1,8 +1,16 @@ -import styles from '../user-settings.module.css'; +/* Stylesheet imports */ +import styles from '../user-settings-page.module.css'; -import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +/* Imports from react-bootstrap */ import Button from 'react-bootstrap/Button'; +/* Imports from other components created */ +import ModalConfirmation from '../../../components/modal-confirmation/modal-confirmation'; +import UIBox from '../../../components/uibox/uibox'; + +/** + * The properties and types for the ChangeEmailModal. + */ interface Props { emailBody1: { showEmailBody1: boolean; @@ -21,9 +29,18 @@ interface Props { }; } +/** + * Contructs, manages, and returns the ChangeEmailModal component. + * + * @param props the props for this ChangeEmailModal, as seen outlined in the interface + * @returns The ChangeEmailModal component as a JSX.Element + */ const ChangeEmailModal = (props: Props) => { + /** + * Clears the current input feilds. + */ const clearFields = () => { - props.emailBody2.setCurrEmail(''); + props.emailBody2.setCurrEmail(''); props.emailBody2.setEmailInput(''); props.emailBody2.setConfEmailInput(''); }; @@ -66,33 +83,51 @@ const ChangeEmailModal = (props: Props) => { <>

What would you like your new email to be?

- props.emailBody2.setCurrEmail(event.target.value)} - required + props.emailBody2.setCurrEmail(event.target.value)} + required + /> + } /> - props.emailBody2.setEmailInput(event.target.value)} - required + + props.emailBody2.setEmailInput(event.target.value)} + required + /> + } /> - props.emailBody2.setConfEmailInput(event.target.value)} - required + + props.emailBody2.setConfEmailInput(event.target.value)} + required + /> + } /> +
+
+ +
+ props.passBody2.setNewPassword(event.target.value)} + required + /> + } + /> + + +
+ +
+ + props.passBody2.setConfPassword(event.target.value) + } + required + /> + } + /> + + +
+ +
@@ -142,4 +197,7 @@ const ChangePasswordModal = (props: Props) => { ); }; +/** + * Exports the ChangePasswordModal for external use. + */ export default ChangePasswordModal; diff --git a/client/src/pages/user-settings/options/change-username.tsx b/client/src/pages/user-settings/options/change-username-modal.tsx similarity index 64% rename from client/src/pages/user-settings/options/change-username.tsx rename to client/src/pages/user-settings/options/change-username-modal.tsx index 87672f5b..fcf9832f 100644 --- a/client/src/pages/user-settings/options/change-username.tsx +++ b/client/src/pages/user-settings/options/change-username-modal.tsx @@ -1,8 +1,16 @@ -import styles from '../user-settings.module.css'; +/* Stylesheet imports */ +import styles from '../user-settings-page.module.css'; -import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +/* Imports from other components created */ +import ModalConfirmation from '../../../components/modal-confirmation/modal-confirmation'; +import UIBox from '../../../components/uibox/uibox'; + +/* Imports from react-bootstrap */ import Button from 'react-bootstrap/Button'; +/** + * The properties and types for the ChangeNameModal. + */ interface Props { usernameBody1: { showNameBody1: boolean; @@ -17,7 +25,16 @@ interface Props { }; } +/** + * Contructs, manages, and returns the ChangeNameModal component. + * + * @param props the props for this ChangeNameModal, as seen outlined in the interface + * @returns The ChangeNameModal component as a JSX.Element + */ const ChangeNameModal = (props: Props) => { + /** + * Clears the current input feilds. + */ const clearFields = () => { props.usernameBody2.setNameInput(''); }; @@ -60,14 +77,19 @@ const ChangeNameModal = (props: Props) => { <>

What would you like your new username to be?

- props.usernameBody2.setNameInput(event.target.value)} - required + props.usernameBody2.setNameInput(event.target.value)} + required + /> + } />
- + -
- + props.deleteBody3.setShowDelete3(true); + } else { + toast.error('The phrase given does not match.'); + } + }}> + DELETE ACCOUNT + +
+ + } + disableFooter={true} + /> + + props.deleteBody3.setShowDelete3(false)} + body={ + <> +

+ By clicking "DELETE ACCOUNT" below you account will be{' '} + permenatly deleted after 30 days. +
+
+ If you wish to undo this contact: support@skynetwork.app +
+
+ If you do not wish to proceed click "CANCEL". +

+
+ + +
} disableFooter={true} @@ -104,4 +180,7 @@ const DeleteAccountModal = (props: Props) => { ); }; +/** + * Exports the DeleteAccountModal for external use. + */ export default DeleteAccountModal; diff --git a/client/src/pages/user-settings/options/liked.tsx b/client/src/pages/user-settings/options/liked.tsx new file mode 100644 index 00000000..1d2245be --- /dev/null +++ b/client/src/pages/user-settings/options/liked.tsx @@ -0,0 +1,61 @@ +/* Imported from React */ +import { useContext } from 'react'; + +/* Import for the authorized user context */ +import { UserAuthContext } from '../../../lib/auth'; + +/* Imports from other components created */ +import Page from '../../../components/page/page'; +import { PaginatedPostFeed } from '../../../components/paginated-post-feed/paginated-post-feed'; + +/* Imports for frontend api call and authentication verification */ +import { api } from '../../../lib/axios'; + +// Define the Post interface +interface PostType { + _id: string; + authorId: string; + content: string; + likeCount: number; + commentCount: number; + repostCount: number; + location: { + planetId: string; + latitude: number; + longitude: number; + _id: string; + }; + media: any[]; + createdAt: Date; + deleted: boolean; + parentPost?: string; + userName: string; + avatar: string; +} + +/** + * Constructs, manages, and returns the LikedPage component. + * + * @return The LikedPage component as a JSX.Element + */ +const LikedPage = () => { + const user = useContext(UserAuthContext); + + return ( + + + api.get(`/user/liked?page=${page}`).then((res) => res.data.value.likedPosts) + } + /> + + } + /> + ); +}; + +export default LikedPage; diff --git a/client/src/pages/user-settings/options/manage-account.tsx b/client/src/pages/user-settings/options/manage-account-page.tsx similarity index 74% rename from client/src/pages/user-settings/options/manage-account.tsx rename to client/src/pages/user-settings/options/manage-account-page.tsx index 82c8b86a..b97cdc00 100644 --- a/client/src/pages/user-settings/options/manage-account.tsx +++ b/client/src/pages/user-settings/options/manage-account-page.tsx @@ -1,17 +1,29 @@ -import { useState } from 'react'; -import { toast } from 'react-toastify'; -import { api } from '../../../lib/axios'; +/* Stylesheet imports */ +import styles from '../user-settings-page.module.css'; + +/* Import from wouter */ import { useLocation } from 'wouter'; + +/* Imports for frontend api call and authentication verification */ +import { api } from '../../../lib/axios'; import { Auth } from '../../../lib/auth'; -import styles from '../user-settings.module.css'; +/* Imports from React */ +import { useState } from 'react'; +import { toast } from 'react-toastify'; +/* Imports from react-bootstrap */ import ListGroup from 'react-bootstrap/ListGroup'; -import ChangePasswordModal from '../options/change-password-modal'; -import DeleteAccountModal from '../options/delete-account-modal'; -import ChangeNameModal from './change-username'; -import ChangeEmailModal from './change-email'; -import Page from '../../../components/Page/Page'; + +/* Icon imports from react-icons */ +import { MdOutlineArrowForwardIos } from 'react-icons/md'; + +/* Imports from other components created */ +import ChangePasswordModal from './change-password-modal'; +import DeleteAccountModal from './delete-account-modal'; +import ChangeNameModal from './change-username-modal'; +import ChangeEmailModal from './change-email-modal'; +import Page from '../../../components/page/page'; const ManageAccount = () => { const [_, setLocation] = useLocation(); @@ -23,6 +35,7 @@ const ManageAccount = () => { // variables responsible for showing delete account modals const [showDeleteBody1, setShowDelete1] = useState(false); const [showDeleteBody2, setShowDelete2] = useState(false); + const [showDeleteBody3, setShowDelete3] = useState(false); // variables responsible for showing change username modals const [showNameBody1, setNameBody1] = useState(false); @@ -48,6 +61,11 @@ const ManageAccount = () => { const [emailInput, setEmailInput] = useState(''); const [confEmailInput, setConfEmailInput] = useState(''); + /** + * Handles the patch request to change the user's password with the help of axios. + * + * @param event the form event from onSubmit. + */ const changePassword = async (event: React.FormEvent) => { event.preventDefault(); try { @@ -63,8 +81,12 @@ const ManageAccount = () => { } }; - const deleteAccount = async (event: React.FormEvent) => { - event.preventDefault(); + /** + * Handles the delete request to delete the user's account with the help of axios. + * + * @param event the form event from onSubmit. + */ + const deleteAccount = async () => { try { const response = await api.post('/user/deleteaccount/delete', { confirmationInput: confInput, @@ -74,9 +96,16 @@ const ManageAccount = () => { logout(); } catch (error: any) { toast.error(error.response.data.error); + setShowDelete3(false); + setShowDelete2(true); } }; + /** + * Handles the patch request to change the user's username with the help of axios. + * + * @param event the form event from onSubmit. + */ const changeUsername = async (event: React.FormEvent) => { event.preventDefault(); try { @@ -90,6 +119,11 @@ const ManageAccount = () => { } }; + /** + * Handles the patch request to change the user's email with the help of axios. + * + * @param event the form event from onSubmit. + */ const changeEmail = async (event: React.FormEvent) => { event.preventDefault(); try { @@ -105,6 +139,9 @@ const ManageAccount = () => { } }; + /** + * Reseta all inputs and visibility statuses of the modals. + */ const wrapUpModal = () => { setPassword(''); setNewPassword(''); @@ -120,6 +157,9 @@ const ManageAccount = () => { setEmailBody2(false); }; + /** + * Logs the user out. + */ const logout = () => [Auth.loseToken(), setLocation('/login')]; // defining values for the Change Password Modal @@ -151,6 +191,10 @@ const ManageAccount = () => { confInput: confInput, setConfInput: setConfInput, }; + const deleteBody3 = { + showDeleteBody3: showDeleteBody3, + setShowDelete3: setShowDelete3, + }; // defining values for the change username Modal const usernameBody1 = { @@ -188,29 +232,42 @@ const ManageAccount = () => { logoHeader={false} pageName='Manage Account' content={ - +

Danger Zone

setNameBody1(true)}> - Change Username +
+

Change Username

+ +
+ setShowPass1(true)}> - Change Password + onClick={() => setEmailBody1(true)}> +
+

Change Email

+ +
setEmailBody1(true)}> - Change Email + onClick={() => setShowPass1(true)}> +
+

Change Password

+ +
setShowDelete1(true)}> - DELETE ACCOUNT +
+

DELETE ACCOUNT

+ +
@@ -219,11 +276,14 @@ const ManageAccount = () => { /> - + ); }; +/** + * Exports the user manage account page for external use. + */ export default ManageAccount; diff --git a/client/src/pages/user-settings/options/saved.tsx b/client/src/pages/user-settings/options/saved.tsx new file mode 100644 index 00000000..19ed949c --- /dev/null +++ b/client/src/pages/user-settings/options/saved.tsx @@ -0,0 +1,60 @@ +/* Imported from React */ +import { useContext, useEffect, useState } from 'react'; + +/* Import for the authorized user context */ +import { UserAuthContext } from '../../../lib/auth'; + +/* Imports from other components created */ +import Page from '../../../components/page/page'; +import { PaginatedPostFeed } from '../../../components/paginated-post-feed/paginated-post-feed'; + +/* Imports for frontend api call and authentication verification */ +import { api } from '../../../lib/axios'; + +// Define the Post interface +interface PostType { + _id: string; + authorId: string; + content: string; + likeCount: number; + commentCount: number; + repostCount: number; + location: { + planetId: string; + latitude: number; + longitude: number; + _id: string; + }; + media: any[]; + createdAt: Date; + deleted: boolean; + parentPost: string; + userName: string; + avatar: string; +} +/** + * Constructs, manages, and returns the SavedPage component. + * + * @return The SavedPage component as a JSX.Element + */ +const SavedPage = () => { + const user = useContext(UserAuthContext); + + return ( + + + api.get(`/user/saved?page=${page}`).then((res) => res.data.value.savedPosts) + } + /> + + } + /> + ); +}; + +export default SavedPage; diff --git a/client/src/pages/user-settings/options/your-info.tsx b/client/src/pages/user-settings/options/your-info-modal.tsx similarity index 52% rename from client/src/pages/user-settings/options/your-info.tsx rename to client/src/pages/user-settings/options/your-info-modal.tsx index 8deb016c..3f9ebe47 100644 --- a/client/src/pages/user-settings/options/your-info.tsx +++ b/client/src/pages/user-settings/options/your-info-modal.tsx @@ -1,10 +1,19 @@ +/* Imports from React */ import { useContext } from 'react'; +import { Else, If, Then } from 'react-if'; -import ModalConfirmation from '../../../components/ModalConfirmation/ModalConfirmation'; +/* Imports from react-bootstrap */ import Button from 'react-bootstrap/Button'; + +/* Import for the authorized user context */ import { UserAuthContext } from '../../../lib/auth'; -import { Else, If, Then } from 'react-if'; +/* Imports from other components created */ +import ModalConfirmation from '../../../components/modal-confirmation/modal-confirmation'; + +/** + * The properties and types for the YourInfoModal. + */ interface Props { infoBody: { showInfoBody: boolean; @@ -12,8 +21,19 @@ interface Props { }; } +/** + * Contructs, manages, and returns the YourInfoModal component. + * + * @param props the props for this YourInfoModal, as seen outlined in the interface + * @returns The YourInfoModal component as a JSX.Element + */ const YourInfoModal = (props: Props) => { const user = useContext(UserAuthContext); + const joinedDateFmt = new Intl.DateTimeFormat(navigator.language, { + month: 'long', + day: 'numeric', + year: 'numeric', + }); return ( <> @@ -26,7 +46,11 @@ const YourInfoModal = (props: Props) => {

Username: {user.userName}

Email: {user.email ?? 'Authenticated with Provider'}

-

Bio: {user.bio ?? 'No Bio Provided'}

+

Bio: {user.bio ? user.bio : 'No Bio Provided'}

+

Followers: {user.followerCount}

+

Following: {user.followingCount}

+

User Type: {!user.admin ? 'Basic user' : 'Administrator'}

+

Account Created: {joinedDateFmt.format(new Date(user.createdAt))}

An error occured.

@@ -37,7 +61,6 @@ const YourInfoModal = (props: Props) => { footer={