├── api
├── .gitignore
├── utils
│ ├── catchAsync.js
│ ├── allowedOrigins.js
│ ├── corsOptions.js
│ ├── getRelationship.js
│ ├── cloudinary.js
│ ├── socket.js
│ └── notification.js
├── validator
│ ├── IsEmpty.js
│ ├── PostValidation.js
│ ├── SigninValidation.js
│ └── SignupValidation.js
├── models
│ ├── reaction.js
│ ├── friend.js
│ ├── comment.js
│ ├── notification.js
│ └── post.js
├── views
│ └── 404.html
├── public
│ └── css
│ │ └── style.css
├── middlewares
│ ├── postsLimiter.js
│ ├── multerMiddleware.js
│ ├── checkAuth.js
│ └── sharpMiddleware.js
├── config
│ └── db.js
├── routes
│ ├── notifications.js
│ ├── friends.js
│ ├── users.js
│ └── posts.js
├── package.json
├── controllers
│ ├── notification.js
│ └── auth.js
└── server.js
├── client
├── netlify.toml
├── src
│ ├── assets
│ │ ├── icons
│ │ │ ├── Close.png
│ │ │ ├── icons11.png
│ │ │ ├── icons15.png
│ │ │ ├── icons16.png
│ │ │ ├── icons17.png
│ │ │ ├── icons18.png
│ │ │ ├── icons2.png
│ │ │ ├── icons25.png
│ │ │ ├── icons32.png
│ │ │ ├── icons33.png
│ │ │ ├── icons40.png
│ │ │ ├── icons41.png
│ │ │ ├── icons5.png
│ │ │ ├── icons6.png
│ │ │ ├── icons7.png
│ │ │ └── publicpack.png
│ │ ├── sound
│ │ │ └── notification.wav
│ │ └── svg
│ │ │ ├── dots.jsx
│ │ │ ├── user.svg
│ │ │ ├── arrowDow1.jsx
│ │ │ ├── Signin.svg
│ │ │ ├── Error.svg
│ │ │ ├── watch.jsx
│ │ │ ├── trash.svg
│ │ │ ├── homeActive.jsx
│ │ │ ├── friendsActive.jsx
│ │ │ ├── notifications.jsx
│ │ │ ├── ReturnIcon.jsx
│ │ │ ├── newRoom.jsx
│ │ │ ├── index.jsx
│ │ │ ├── openEye.svg
│ │ │ ├── uploadFile.svg
│ │ │ ├── home.jsx
│ │ │ ├── closeEye.svg
│ │ │ ├── friends.jsx
│ │ │ ├── liveVideo.jsx
│ │ │ ├── feeling.jsx
│ │ │ ├── photo.jsx
│ │ │ ├── like.svg
│ │ │ ├── market.jsx
│ │ │ ├── search.jsx
│ │ │ ├── ZIWIBook.svg
│ │ │ └── 404Error.svg
│ ├── layouts
│ │ ├── Footer
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── index.jsx
│ │ ├── DeleteConfirm
│ │ │ ├── index.css
│ │ │ └── index.jsx
│ │ ├── Modal
│ │ │ ├── index.jsx
│ │ │ ├── ModalManager.jsx
│ │ │ └── index.css
│ │ └── Header
│ │ │ ├── NotificationMenu
│ │ │ └── Notification.module.css
│ │ │ ├── HeaderMenu
│ │ │ ├── DisplayAccessibility.jsx
│ │ │ ├── HeaderMenu.jsx
│ │ │ └── HeaderMenu.module.css
│ │ │ └── Header.module.css
│ ├── pages
│ │ ├── 404
│ │ │ ├── 404.module.css
│ │ │ └── index.jsx
│ │ ├── index.jsx
│ │ ├── Post
│ │ │ ├── style.module.css
│ │ │ └── index.jsx
│ │ ├── Home
│ │ │ ├── home.module.css
│ │ │ └── index.jsx
│ │ ├── login
│ │ │ └── index.jsx
│ │ ├── Profile
│ │ │ └── profile.module.css
│ │ └── friends
│ │ │ └── style.module.css
│ ├── components
│ │ ├── UI
│ │ │ ├── Card
│ │ │ │ ├── Card.module.css
│ │ │ │ └── index.jsx
│ │ │ ├── Loading
│ │ │ │ ├── index.jsx
│ │ │ │ └── loading.module.css
│ │ │ ├── FormLoader
│ │ │ │ ├── index.css
│ │ │ │ └── index.jsx
│ │ │ ├── skeleton
│ │ │ │ ├── notificationSkeleton.jsx
│ │ │ │ ├── skeleton.module.css
│ │ │ │ └── postSkeleton.jsx
│ │ │ ├── Notification
│ │ │ │ ├── Notification.module.css
│ │ │ │ └── index.jsx
│ │ │ └── Popper
│ │ │ │ └── index.jsx
│ │ ├── Posts
│ │ │ ├── Post
│ │ │ │ ├── Likes
│ │ │ │ │ ├── react.module.css
│ │ │ │ │ └── index.jsx
│ │ │ │ ├── PostList.jsx
│ │ │ │ ├── Comments
│ │ │ │ │ ├── index.jsx
│ │ │ │ │ ├── CommentForm.jsx
│ │ │ │ │ └── comment.module.css
│ │ │ │ ├── PostHead
│ │ │ │ │ ├── postHead.module.css
│ │ │ │ │ └── index.jsx
│ │ │ │ └── post.module.css
│ │ │ ├── CreatPost
│ │ │ │ ├── index.jsx
│ │ │ │ └── postbody.module.css
│ │ │ └── AddEditPost
│ │ │ │ └── Post.module.css
│ │ ├── input
│ │ │ ├── CustomInput
│ │ │ │ ├── index.css
│ │ │ │ └── index.jsx
│ │ │ └── AuthInput
│ │ │ │ ├── style.module.css
│ │ │ │ └── index.jsx
│ │ ├── Profile
│ │ │ ├── ProfileMenu
│ │ │ │ ├── profileMenu.module.css
│ │ │ │ └── index.jsx
│ │ │ ├── ProfileInfo.jsx
│ │ │ ├── ProfileCover
│ │ │ │ ├── OldCovers
│ │ │ │ │ ├── OldCovers.module.css
│ │ │ │ │ └── OldCovers.jsx
│ │ │ │ └── cover.module.css
│ │ │ ├── Friendship
│ │ │ │ └── Friendship.module.css
│ │ │ ├── Photos.jsx
│ │ │ ├── Friends.jsx
│ │ │ ├── index.module.css
│ │ │ └── ProfilePhoto
│ │ │ │ └── ProfilePhoto.module.css
│ │ ├── Search
│ │ │ ├── index.css
│ │ │ ├── index.jsx
│ │ │ └── SearchMenu
│ │ │ │ └── SearchMenu.module.css
│ │ ├── CustomButton
│ │ │ └── index.jsx
│ │ ├── index.jsx
│ │ ├── LoginForm
│ │ │ └── index.css
│ │ ├── friendCard
│ │ │ └── index.jsx
│ │ └── RegisterForm
│ │ │ ├── GenderSelector.jsx
│ │ │ ├── DateSelector.jsx
│ │ │ └── register.module.css
│ ├── utils
│ │ ├── Portal.jsx
│ │ ├── YupValidation.jsx
│ │ └── getCroppedImg.jsx
│ ├── app
│ │ ├── api
│ │ │ └── apiSlice.jsx
│ │ ├── features
│ │ │ ├── search
│ │ │ │ └── searchApi.jsx
│ │ │ ├── modal
│ │ │ │ └── modalSlice.jsx
│ │ │ ├── auth
│ │ │ │ ├── prefetch.jsx
│ │ │ │ └── authApi.jsx
│ │ │ ├── socket
│ │ │ │ └── socketSlice.jsx
│ │ │ ├── notification
│ │ │ │ └── notificationApi.jsx
│ │ │ ├── user
│ │ │ │ ├── photosApi.jsx
│ │ │ │ ├── userProfileApi.jsx
│ │ │ │ └── userSlice.jsx
│ │ │ ├── reaction
│ │ │ │ └── reactionApi.jsx
│ │ │ └── post
│ │ │ │ └── postApi.jsx
│ │ └── store.jsx
│ ├── styles
│ │ └── dark.css
│ ├── routes
│ │ ├── ForceRedirect.jsx
│ │ ├── PrivateRoute.jsx
│ │ └── Router.jsx
│ ├── main.jsx
│ ├── hooks
│ │ ├── useOnClickOutside.jsx
│ │ └── useHover.jsx
│ ├── App.jsx
│ └── index.css
├── vite.config.js
├── .gitignore
├── index.html
└── package.json
├── screenshots
├── ziwibook7.png
├── ziwibook11.png
├── ziwibook12.png
└── ziwibook13.png
└── LICENSE
/api/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .env
--------------------------------------------------------------------------------
/client/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "/*"
3 | to = "/index.html"
4 | status = 200
--------------------------------------------------------------------------------
/screenshots/ziwibook7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/screenshots/ziwibook7.png
--------------------------------------------------------------------------------
/screenshots/ziwibook11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/screenshots/ziwibook11.png
--------------------------------------------------------------------------------
/screenshots/ziwibook12.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/screenshots/ziwibook12.png
--------------------------------------------------------------------------------
/screenshots/ziwibook13.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/screenshots/ziwibook13.png
--------------------------------------------------------------------------------
/client/src/assets/icons/Close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/Close.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons11.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons11.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons15.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons15.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons16.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons17.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons17.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons18.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons18.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons2.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons25.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons25.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons32.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons33.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons33.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons40.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons40.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons41.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons41.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons5.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons6.png
--------------------------------------------------------------------------------
/client/src/assets/icons/icons7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/icons7.png
--------------------------------------------------------------------------------
/client/src/assets/icons/publicpack.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/icons/publicpack.png
--------------------------------------------------------------------------------
/client/src/assets/sound/notification.wav:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mohamedzhioua/ZiwiBook/HEAD/client/src/assets/sound/notification.wav
--------------------------------------------------------------------------------
/api/utils/catchAsync.js:
--------------------------------------------------------------------------------
1 | module.exports = (fn) => {
2 | return (req, res, next) => {
3 | fn(req, res, next).catch(next);
4 | };
5 | };
6 |
--------------------------------------------------------------------------------
/api/utils/allowedOrigins.js:
--------------------------------------------------------------------------------
1 | const allowedOrigins = ["http://127.0.0.1:5173", "http://localhost:3000" ,"https://ziwibook.netlify.app" ];
2 |
3 | module.exports = allowedOrigins
--------------------------------------------------------------------------------
/client/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import react from '@vitejs/plugin-react'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | })
8 |
--------------------------------------------------------------------------------
/client/src/layouts/Footer/index.css:
--------------------------------------------------------------------------------
1 | footer {
2 | position: fixed;
3 | background-color: var(--bg-third);
4 | bottom: 0;
5 | left: 0;
6 | right: 0;
7 | padding: 8px;
8 | text-align: center;
9 | }
10 |
--------------------------------------------------------------------------------
/client/src/pages/index.jsx:
--------------------------------------------------------------------------------
1 | export {default as NotFound} from './404'
2 | export {default as Home} from './Home'
3 | export {default as Profile} from './Profile'
4 | export {default as Login} from './login'
5 | export {default as PostPage} from './Post'
6 |
--------------------------------------------------------------------------------
/api/validator/IsEmpty.js:
--------------------------------------------------------------------------------
1 |
2 | const isEmpty = (value) =>
3 | value === null || value === undefined
4 | || typeof(value) === "object" && Object.keys(value).length === 0
5 | || typeof(value) === "string" && value.trim().length === 0
6 |
7 | module.exports = isEmpty ;
--------------------------------------------------------------------------------
/client/src/components/UI/Card/Card.module.css:
--------------------------------------------------------------------------------
1 | .card_container {
2 | background: var(--bg-primary);
3 | border-radius: 10px;
4 | box-shadow: 0 2px 4px var(--shadow-1);
5 | padding: 10px;
6 | /* width: 100%; */
7 | margin: 10px 0;
8 | }
9 |
10 |
--------------------------------------------------------------------------------
/client/src/layouts/Footer/index.jsx:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 |
3 | const Footer = () => {
4 | const year = new Date().getFullYear();
5 |
6 | return ;
7 | };
8 |
9 | export default Footer;
10 |
--------------------------------------------------------------------------------
/client/src/layouts/index.jsx:
--------------------------------------------------------------------------------
1 | export {default as Modal } from './Modal'
2 | export {default as Header } from './Header'
3 | export {default as DeleteConfirm } from './DeleteConfirm'
4 | export {default as Footer } from './Footer'
5 | export {default as ModalManager } from './Modal/ModalManager'
--------------------------------------------------------------------------------
/client/src/utils/Portal.jsx:
--------------------------------------------------------------------------------
1 | import { createPortal } from "react-dom";
2 |
3 | function Portal({ children, id }) {
4 | const portalDom = id ? document.getElementById(id) : document.body;
5 |
6 | return createPortal(children, portalDom);
7 | }
8 |
9 | export default Portal;
10 |
--------------------------------------------------------------------------------
/client/src/components/UI/Loading/index.jsx:
--------------------------------------------------------------------------------
1 | import style from "./loading.module.css"
2 | export const Loading = () => {
3 | return (
4 |
The resource you have requested does not exist.
15 |
16 |
17 |
--------------------------------------------------------------------------------
/api/public/css/style.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Share+Tech+Mono&display=swap');
2 |
3 | * {
4 | margin: 0;
5 | padding: 0;
6 | box-sizing: border-box;
7 | }
8 |
9 | html {
10 | font-family: 'Share Tech Mono', monospace;
11 | font-size: 2.5rem;
12 | }
13 |
14 | body {
15 | min-height: 100vh;
16 | background-color: #000;
17 | color: whitesmoke;
18 | display: grid;
19 | place-content: center;
20 | padding: 1rem;
21 | }
--------------------------------------------------------------------------------
/api/middlewares/postsLimiter.js:
--------------------------------------------------------------------------------
1 | const rateLimit = require('express-rate-limit');
2 |
3 | // limiter for posts
4 | const limiter = rateLimit({
5 | windowMs: 60 * 60 * 1000 * 24,
6 | max: 15,
7 |
8 | handler: (request, response, next, options) =>
9 | response.status(options.statusCode).json({
10 | message:
11 | 'You can only post 15 posts per day and you have reached the limit. You can post again tomorrow, have fun ',
12 | }),
13 | });
14 | module.exports = limiter;
15 |
--------------------------------------------------------------------------------
/client/src/assets/svg/arrowDow1.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function ArrowDown1({ color }) {
4 | return (
5 | ',
6 | process.env.DATABASE_PASSWORD
7 | );
8 |
9 | mongoose.set('strictQuery', false);
10 | mongoose
11 | .connect(DB, {
12 | useUnifiedTopology: true,
13 | useNewUrlParser: true,
14 | })
15 | .then(() => console.log("mongoose connected"))
16 | .catch((err) => console.log(err));
17 |
18 | const db = mongoose.connection;
19 |
20 | module.exports = db;
--------------------------------------------------------------------------------
/client/src/styles/dark.css:
--------------------------------------------------------------------------------
1 | .dark {
2 | --bg-primary: #242526;
3 | --bg-secondary: #18191a;
4 | --bg-third: #3a3b3c;
5 | --bg-forth: #3a3b3c;
6 | --bg-fifth: #888888;
7 | --color-secondary: #e4e6eb;
8 | --color-third: #b0b3b8;
9 | --divider: #4e4e4e;
10 | --light-blue-color: rgba(45, 136, 255, 0.1);
11 | }
12 | .dark .blur {
13 | background: rgba(1, 1, 1, 0.53);
14 | }
15 | .dark .small_circle i {
16 | filter: invert(100%);
17 | }
18 |
19 | .dark .create_icon {
20 | filter: invert(100%);
21 | }
22 |
--------------------------------------------------------------------------------
/client/src/assets/svg/Signin.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/routes/ForceRedirect.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Outlet } from "react-router-dom";
2 | import { useSelector } from "react-redux";
3 | import { Loading } from "../components";
4 | import React from "react";
5 |
6 | const ForceRedirect = () => {
7 | const { token } = useSelector((state) => state.user);
8 |
9 | return token ? (
10 |
11 | ) : (
12 | }>
13 |
14 |
15 | );
16 | };
17 |
18 | export default ForceRedirect;
19 |
--------------------------------------------------------------------------------
/client/src/pages/login/index.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { LoginForm, RegisterForm } from "../../components";
3 |
4 | function Login() {
5 | const [showRegister, setShowRegister] = useState(false);
6 |
7 | return (
8 |
9 |
10 | {showRegister && (
11 |
15 | )}
16 |
17 | );
18 | }
19 |
20 | export default Login;
21 |
--------------------------------------------------------------------------------
/client/src/assets/svg/Error.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import "./index.css";
4 | import "./styles/dark.css";
5 | import App from "./App";
6 | import { store } from "./app/store";
7 | import { Provider } from "react-redux";
8 | import "react-toastify/dist/ReactToastify.css";
9 | import "react-loading-skeleton/dist/skeleton.css";
10 |
11 |
12 | ReactDOM.createRoot(document.getElementById('root')).render(
13 |
14 |
15 |
16 |
17 | ,
18 | )
19 |
--------------------------------------------------------------------------------
/client/src/app/store.jsx:
--------------------------------------------------------------------------------
1 | import { configureStore } from "@reduxjs/toolkit";
2 | import { apiSlice } from "./api/apiSlice";
3 | import userReducer from "./features/user/userSlice";
4 | import modalReducer from "./features/modal/modalSlice";
5 | import { useFetchCommentsQuery } from "./features/comment/commentApi";
6 | export const store = configureStore({
7 | reducer: {
8 | user: userReducer,
9 | modal: modalReducer,
10 | [apiSlice.reducerPath]:apiSlice.reducer,
11 | },
12 | middleware:getDefaultMiddleware=>
13 | getDefaultMiddleware().concat(apiSlice.middleware)
14 | });
15 |
--------------------------------------------------------------------------------
/api/models/friend.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const Schema = mongoose.Schema;
3 |
4 | const FriendSchema = new Schema(
5 | {
6 | sender: { type: Schema.Types.ObjectId, ref: "user", required: true },
7 | recipient: {
8 | type: Schema.Types.ObjectId,
9 |
10 | ref: "user",
11 | required: true,
12 | },
13 | requestStatus: {
14 | type: String,
15 | enum: ["pending", "accepted", "cancelled"],
16 | required: true,
17 | },
18 | },
19 | { timestamps: true }
20 | );
21 |
22 | module.exports = mongoose.model("friend", FriendSchema);
23 |
--------------------------------------------------------------------------------
/client/src/assets/svg/watch.jsx:
--------------------------------------------------------------------------------
1 | function Watch({ color }) {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default Watch;
10 |
--------------------------------------------------------------------------------
/client/src/assets/svg/trash.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/routes/notifications.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const checkAuth = require("../middlewares/checkAuth");
4 | const NotifController = require("../controllers/notification");
5 |
6 | // PROTECT ALL ROUTES AFTER THIS MIDDLEWARE
7 | router.use(checkAuth);
8 |
9 | // GET request to fetch all Notif.
10 | router.get("/notifies", NotifController.getNotifcations);
11 |
12 | // PATCH request to change the notif seen to true .
13 | router.patch("/isNotifSeen/:id", NotifController.isNotifSeen);
14 |
15 | // DELETE a specific Notif.
16 | router.delete("/delete/:id", NotifController.deleteNotif);
17 |
18 | module.exports = router;
19 |
--------------------------------------------------------------------------------
/client/src/assets/svg/homeActive.jsx:
--------------------------------------------------------------------------------
1 | function HomeActive({ className }) {
2 | return (
3 |
12 | );
13 | }
14 |
15 | export default HomeActive;
16 |
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfileMenu/profileMenu.module.css:
--------------------------------------------------------------------------------
1 | /* profile menu */
2 |
3 | .profile_menu_container {
4 | display: flex;
5 | justify-content: space-between;
6 | align-items: center;
7 | }
8 | .profile_menu {
9 | display: flex;
10 | align-items: center;
11 | }
12 | .profile_menu a {
13 | padding: 10px;
14 | font-family: "Roboto", sans-serif;
15 | font-size: 300;
16 | }
17 | .profile_menu .active {
18 | color: var(--color-primary);
19 | border-bottom: 2px solid var(--color-primary);
20 | margin-bottom: -3px;
21 | }
22 | .link {
23 | text-decoration: none;
24 | }
25 |
26 | :global(.dark) .profile_menu_container svg {
27 | fill: var(--bg-primary);
28 | }
29 |
--------------------------------------------------------------------------------
/client/src/components/UI/Loading/loading.module.css:
--------------------------------------------------------------------------------
1 | .loadingSpinnerContainer {
2 | position: fixed;
3 | top: 0;
4 | right: 0;
5 | bottom: 0;
6 | left: 0;
7 | background-color: var(--shadow-5);
8 | z-index: 5000;
9 | display: flex;
10 | justify-content: center;
11 | align-items: center;
12 | }
13 | .loadingSpinner {
14 | width: 64px;
15 | height: 64px;
16 | border: 8px solid;
17 | border-color: var(--color-secondary) transparent var(--color-third) transparent;
18 | border-radius: 50%;
19 | animation: spin 1.2s linear infinite;
20 | }
21 |
22 | @keyframes spin {
23 | 0% {
24 | transform: rotate(0deg);
25 | }
26 | 100% {
27 | transform: rotate(360deg);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/src/components/Search/index.css:
--------------------------------------------------------------------------------
1 | .searchInputs {
2 | display: flex;
3 | align-items: center;
4 | gap: 6px;
5 | background:var(--bg-secondary);
6 | padding: 10px 20px 10px 13px;
7 | border-radius: 50px;
8 | cursor: text;
9 | min-width: 233px;
10 | }
11 |
12 | .search-input {
13 | outline: none;
14 | border: none;
15 | background: transparent;
16 | font-size: 15px;
17 | font-family: inherit;
18 | padding-left: 2px;
19 | }
20 |
21 | @media (max-width: 1100px) {
22 | .searchInputs {
23 | width: 40px;
24 | height: 40px;
25 | padding: 0;
26 | justify-content: center;
27 | min-width: auto;
28 | }
29 |
30 | .search-input {
31 | display: none;
32 | }
33 | }
34 |
35 |
--------------------------------------------------------------------------------
/client/src/pages/404/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import classes from "./404.module.css";
3 | import ErrorSVG from "../../assets/svg/404Error.svg";
4 | // 404 page
5 | const NotFound = () => {
6 | return (
7 |
8 |

9 |
10 |
11 | Looks like this page does not exist or Something Went Wrong.
12 |
13 |
14 | Go Back to the
15 |
16 | Home Page
17 |
18 |
19 |
20 |
21 | );
22 | };
23 |
24 | export default NotFound;
25 |
--------------------------------------------------------------------------------
/client/src/components/CustomButton/index.jsx:
--------------------------------------------------------------------------------
1 | import "./index.css";
2 |
3 | const CustomButton = ({
4 | onSubmit,
5 | type,
6 | className,
7 | value,
8 | disabled,
9 | onClick,
10 | Icon,
11 | children,
12 |
13 | }) => {
14 | return !Icon ? (
15 |
23 | ) : (
24 |
33 | );
34 | };
35 |
36 | export default CustomButton;
37 |
--------------------------------------------------------------------------------
/api/models/comment.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const Schema = mongoose.Schema;
3 |
4 | const CommentSchema = new Schema(
5 | {
6 | post: {
7 | type: Schema.Types.ObjectId,
8 | ref: "post",
9 | required: true,
10 | },
11 | parentId : {
12 | type: String,
13 | default: null
14 | },
15 | owner: {
16 | type: Schema.Types.ObjectId,
17 | ref: "user",
18 | required: true,
19 | },
20 | text: {
21 | type: String,
22 | required: true,
23 | },
24 | likes: {
25 | type: [String],
26 | default: [],
27 | },
28 | },
29 | { timestamps: true }
30 | );
31 |
32 |
33 | module.exports = mongoose.model("comment", CommentSchema);
34 |
--------------------------------------------------------------------------------
/client/src/assets/svg/friendsActive.jsx:
--------------------------------------------------------------------------------
1 | function FriendsActive() {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default FriendsActive;
10 |
--------------------------------------------------------------------------------
/client/src/assets/svg/notifications.jsx:
--------------------------------------------------------------------------------
1 | function Notifications() {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default Notifications;
10 |
--------------------------------------------------------------------------------
/client/src/components/input/AuthInput/style.module.css:
--------------------------------------------------------------------------------
1 | .authInput_container {
2 | width: 100%;
3 | position: relative;
4 | }
5 | .authInput_container input {
6 | background: var(--bg-secondary);
7 | border: 1px solid var(--bg-third);
8 | border-radius: 10px;
9 | color: var(--color-secondary);
10 | font-size: 17px;
11 | height: 40px;
12 | margin-bottom: 10px;
13 | outline: none;
14 | padding-left: 10px;
15 | width: 100%;
16 | }
17 | .ERROR {
18 | border-color: var(--color-error) !important;
19 | }
20 | .ER {
21 | position: absolute;
22 | right: 40px;
23 | top: 15px;
24 | }
25 |
26 | .showHidePassword {
27 | position: absolute;
28 | right: 9px;
29 | top: 8px;
30 | font-size: 17px;
31 | cursor: "pointer";
32 | }
33 |
--------------------------------------------------------------------------------
/client/src/hooks/useOnClickOutside.jsx:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | function useOnClickOutside(ref, state, handler) {
4 | useEffect(() => {
5 | const listener = (event) => {
6 | if (
7 | !ref.current ||
8 | ref.current.contains(event.target) ||
9 | state === false
10 | ) {
11 | return;
12 | }
13 | handler(event);
14 | };
15 | document.addEventListener("mousedown", listener);
16 | document.addEventListener("touchstart", listener);
17 | return () => {
18 | document.removeEventListener("mousedown", listener);
19 | document.removeEventListener("touchstart", listener);
20 | };
21 | }, [ref, handler, state]);
22 | }
23 | export default useOnClickOutside;
24 |
--------------------------------------------------------------------------------
/client/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
14 | ZIWIBook
15 |
16 |
17 |
18 |
19 |
20 |
21 |
--------------------------------------------------------------------------------
/client/src/assets/svg/ReturnIcon.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function ReturnIcon({ color }) {
4 | return (
5 |
17 | );
18 | }
19 |
20 | export default ReturnIcon;
21 |
--------------------------------------------------------------------------------
/client/src/assets/svg/newRoom.jsx:
--------------------------------------------------------------------------------
1 | function NewRoom({ color }) {
2 | return (
3 |
10 | );
11 | }
12 |
13 | export default NewRoom;
14 |
--------------------------------------------------------------------------------
/client/src/app/features/modal/modalSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | isOpen: false,
5 | componentName: null,
6 | childrenProps: {},
7 | };
8 |
9 | export const modalSlice = createSlice({
10 | name: "modal",
11 | initialState,
12 | reducers: {
13 | openModal: (state, action) => {
14 | state.isOpen = true;
15 | state.componentName = action.payload.name;
16 | state.childrenProps = action.payload.childrenProps;
17 | },
18 | closeModal: (state, action) => {
19 | state.isOpen = false;
20 | state.componentName = null;
21 | state.childrenProps = {};
22 | },
23 | },
24 | });
25 |
26 | export const { openModal, closeModal } = modalSlice.actions;
27 | export default modalSlice.reducer;
28 |
--------------------------------------------------------------------------------
/client/src/assets/svg/index.jsx:
--------------------------------------------------------------------------------
1 | export {default as HomeActive } from './homeActive'
2 | export {default as Market } from './market'
3 | export {default as Notifications } from './notifications'
4 | export {default as Home } from './home'
5 | export {default as Watch } from './watch'
6 | export {default as Friends } from './friends'
7 | export {default as FriendsActive } from './friendsActive'
8 | export {default as Dots } from './dots'
9 | export {default as Feeling } from './feeling'
10 | export {default as LiveVideo } from './liveVideo'
11 | export {default as Photo } from './photo'
12 | export {default as SearchIcon } from './search'
13 | export {default as ReturnIcon } from './ReturnIcon'
14 | export {default as NewRoom } from './newRoom'
15 | export {default as ArrowDown1} from './arrowDow1'
16 |
17 |
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfileMenu/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import classe from "./profileMenu.module.css";
3 |
4 | function ProfileMenu() {
5 | return (
6 |
7 |
8 |
9 | Posts
10 |
11 |
12 | About
13 |
14 |
15 | Friends
16 |
17 |
18 | Photos
19 |
20 |
21 |
22 | );
23 | }
24 |
25 | export default ProfileMenu;
26 |
--------------------------------------------------------------------------------
/api/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "start": "nodemon server.js"
8 | },
9 | "author": "Zhioua Mohamed",
10 | "license": "ISC",
11 | "dependencies": {
12 | "bcryptjs": "^2.4.3",
13 | "cloudinary": "^1.32.0",
14 | "cookie-parser": "^1.4.6",
15 | "cors": "^2.8.5",
16 | "dotenv": "^16.0.3",
17 | "express": "^4.18.2",
18 | "express-rate-limit": "^6.7.0",
19 | "jsonwebtoken": "^8.5.1",
20 | "mongoose": "^6.7.3",
21 | "multer": "^1.4.5-lts.1",
22 | "nodemon": "^2.0.20",
23 | "sharp": "^0.31.3",
24 | "socket.io": "^4.6.1",
25 | "unique-username-generator": "^1.1.3",
26 | "validator": "^13.7.0",
27 | "xss-clean": "^0.1.1"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/client/src/assets/svg/openEye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/app/features/auth/prefetch.jsx:
--------------------------------------------------------------------------------
1 | import { store } from "../../store"
2 | import { useEffect } from 'react';
3 | import { Outlet } from 'react-router-dom';
4 | import { extendedApiSlice } from "../post/postApi";
5 | import { reactionApiSlice } from "../reaction/reactionApi";
6 | import { CommentApiSlice } from "../comment/commentApi";
7 |
8 |
9 | const Prefetch = () => {
10 |
11 | useEffect(() => {
12 | store.dispatch(extendedApiSlice.util.prefetch('fetchPosts', 'fetchPosts', { force: true }));
13 | store.dispatch(reactionApiSlice.util.prefetch('fetchReactions', 'fetchReactions', { force: true }));
14 | store.dispatch(CommentApiSlice.util.prefetch('fetchComments', 'fetchComments', { force: true }));
15 | }, [])
16 |
17 | return
18 | }
19 | export default Prefetch
20 |
--------------------------------------------------------------------------------
/api/validator/SigninValidation.js:
--------------------------------------------------------------------------------
1 | const validator = require("validator");
2 | const isEmpty = require("./IsEmpty");
3 |
4 | module.exports = function SigninValidation(data) {
5 | let errors = {};
6 |
7 | // Convert empty fields to an empty string so we can use validator
8 | data.email = !isEmpty(data.email) ? data.email : "";
9 | data.password = !isEmpty(data.password) ? data.password : "";
10 |
11 | // Email checks
12 | if (validator.isEmpty(data.email)) {
13 | errors.email = "Email field is required";
14 | } else if (!validator.isEmail(data.email)) {
15 | errors.email = "Format Email required";
16 | }
17 |
18 | // Password checks
19 | if (validator.isEmpty(data.password)) {
20 | errors.password = "Password field is required";
21 | }
22 | return {
23 | errors,
24 | isValid: isEmpty(errors),
25 | };
26 | };
--------------------------------------------------------------------------------
/client/src/layouts/Modal/index.jsx:
--------------------------------------------------------------------------------
1 | import Close from "../../assets/icons/Close.png";
2 | import Portal from "../../utils/Portal";
3 | import "./index.css";
4 |
5 | const Modal = ({ isOpen, children, closeModalHandler }) => {
6 | return (
7 |
8 |
11 |
14 |
15 |

21 |
22 |
{children}
23 |
24 |
25 | );
26 | };
27 |
28 | export default Modal;
29 |
--------------------------------------------------------------------------------
/client/src/App.jsx:
--------------------------------------------------------------------------------
1 | import Router from "./routes/Router";
2 | import { BrowserRouter } from "react-router-dom";
3 | import { Footer, ModalManager } from "./layouts/index";
4 | import "./index.css";
5 | import { ToastContainer } from "react-toastify";
6 | import Portal from "./utils/Portal";
7 | import { useSelector } from "react-redux";
8 | import { useEffect } from "react";
9 | function App() {
10 | const theme = useSelector((state) => state.user.theme);
11 | useEffect(() => {
12 | if (theme === "dark") {
13 | document.body.classList.add("dark");
14 | } else {
15 | document.body.classList.remove("dark");
16 | }
17 | }, [theme]);
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 | )
28 | }
29 |
30 | export default App
31 |
--------------------------------------------------------------------------------
/client/src/app/features/socket/socketSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 |
4 | const initialState = {
5 | onlineUsers: [],
6 | };
7 | export const SocketSlice = createSlice({
8 | name: "socket",
9 | initialState,
10 |
11 | reducers: {
12 | setOnlineUsers: (state, action) => {
13 | const { type, info } = action.payload;
14 | if (
15 | type === "add" &&
16 | !state.onlineUsers.find((u) => u?.id === info?.id)
17 | ) {
18 | state.onlineUsers = [...state.onlineUsers, info];
19 | } else if (type === "remove") {
20 | state.onlineUsers = state.onlineUsers.filter((u) => u?.id !== info?.id);
21 | } else if (type === "connect") {
22 | state.onlineUsers = info;
23 | }
24 | },
25 | },
26 | });
27 | export const { setOnlineUsers } = SocketSlice.actions;
28 |
29 | export default SocketSlice.reducer;
30 |
--------------------------------------------------------------------------------
/api/middlewares/multerMiddleware.js:
--------------------------------------------------------------------------------
1 | const multer = require("multer");
2 |
3 | const multerStorage = multer.memoryStorage();
4 |
5 | // const multerFilter = (req, file, cb) => {
6 | // const fileRegex = new RegExp(".(jpg|jpeg|png)$");
7 | // const fileName = file.originalname;
8 | // if (!fileName.match(fileRegex)) {
9 | // //throw exception
10 | // return cb(new Error("Invalid file type"));
11 | // }
12 | // //pass the file
13 | // cb(null, true);
14 | // };
15 |
16 | const multerFilter = (req, file, cb) => {
17 | if (file.mimetype.startsWith('image')) {
18 | cb(null, true);
19 | } else {
20 | cb(new Error('Please upload only images') )
21 | }
22 | };
23 |
24 | module.exports = multer({
25 | storage: multerStorage ,
26 | limits: { fileSize: 5 * 1024 * 1024 },
27 | fileFilter: multerFilter,
28 | }).single("image"); //single for accepting only one file from 'image' form-data key
29 |
--------------------------------------------------------------------------------
/api/routes/friends.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const checkAuth = require("../middlewares/checkAuth");
4 | const FriendController = require("../controllers/friends");
5 |
6 | // PROTECT ALL ROUTES AFTER THIS MIDDLEWARE
7 | router.use(checkAuth);
8 |
9 | // PUT request to send a friend request .
10 | router.put("/add/:id", FriendController.addFriend);
11 |
12 | // PUT request to cancel a friend request .
13 | router.put("/cancel/:friendRequestId", FriendController.cancelRequest);
14 |
15 | // PUT request to accept a friend request .
16 | router.put("/accept/:friendRequestId", FriendController.acceptRequest);
17 |
18 | // PUT request to unfriend someone .
19 | router.put("/remove/:friendRequestId", FriendController.unfriend);
20 |
21 | // GET request to get user's friend list .
22 | router.get("/", FriendController.getFriends);
23 |
24 | module.exports = router;
25 |
--------------------------------------------------------------------------------
/api/utils/getRelationship.js:
--------------------------------------------------------------------------------
1 | const Friend = require('../models/friend');
2 |
3 | module.exports = {
4 | getRelationship: async (userId, profileId) => {
5 | const realationship = await Friend.findOne({
6 | $or: [
7 | { sender: userId, recipient: profileId },
8 | { sender: profileId, recipient: userId },
9 | ],
10 | });
11 |
12 | const friendship = {
13 | friends: realationship?.requestStatus === 'accepted' ? true : false,
14 | requestSent:
15 | realationship?.sender.toString() === userId &&
16 | realationship?.requestStatus === 'pending'
17 | ? true
18 | : false,
19 | requestReceived:
20 | realationship?.sender.toString() === profileId &&
21 | realationship?.requestStatus === 'pending'
22 | ? true
23 | : false,
24 | requestID: realationship?.id ? realationship?.id : null,
25 | };
26 |
27 | return friendship;
28 | }
29 | }
--------------------------------------------------------------------------------
/client/src/hooks/useHover.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | // const [ref, isHovered] = useHover(notif);
3 |
4 |
5 | function useHover(ref) {
6 | const [value, setValue] = React.useState(false);
7 | const handleMouseOver = () => setValue(true);
8 | const handleMouseOut = () => setValue(false);
9 | React.useEffect(
10 | () => {
11 | const node = ref.current;
12 | if (node) {
13 | node.addEventListener("mouseover", handleMouseOver);
14 | node.addEventListener("mouseout", handleMouseOut);
15 | return () => {
16 | node.removeEventListener("mouseover", handleMouseOver);
17 | node.removeEventListener("mouseout", handleMouseOut);
18 | };
19 | }
20 | },
21 | [ref.current]
22 | );
23 | return [ref, value];
24 | }
25 |
26 | export default useHover
--------------------------------------------------------------------------------
/client/src/app/features/notification/notificationApi.jsx:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../api/apiSlice";
2 |
3 | export const NotificationsApiSlice = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 | FetchNotif: builder.query({
6 | query: () => `/api/notifications/notifies`,
7 | providesTags: ["Notif"],
8 | }),
9 |
10 | isNotifSeen: builder.mutation({
11 | query: (id) => ({
12 | url: `/api/notifications/isNotifSeen/${id}`,
13 | method: "PATCH",
14 | }),
15 | invalidatesTags: ["Notif"],
16 | }),
17 | deleteNotif: builder.mutation({
18 | query: (id) => ({
19 | url: `/api/notifications/delete/${id}`,
20 | method: "DELETE",
21 | }),
22 | invalidatesTags: ["Notif"],
23 | }),
24 | }),
25 | });
26 |
27 | export const { useFetchNotifQuery, useIsNotifSeenMutation ,useDeleteNotifMutation} =
28 | NotificationsApiSlice;
29 |
--------------------------------------------------------------------------------
/client/src/assets/svg/uploadFile.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/components/Posts/Post/PostHead/postHead.module.css:
--------------------------------------------------------------------------------
1 | .post_row {
2 | display: flex;
3 | align-items: center;
4 | justify-content: space-between;
5 | }
6 | .user_profile {
7 | display: flex;
8 | align-items: center;
9 | gap: 10px;
10 | }
11 | .user_profile .left img {
12 | width: 40px;
13 | border-radius: 50%;
14 | }
15 | .username {
16 | display: flex;
17 | font-size: 14px;
18 | font-weight: 600;
19 | cursor: pointer;
20 | width: max-content;
21 | }
22 | .username:hover {
23 | text-decoration: underline;
24 | }
25 | .date {
26 | font-size: 12px;
27 | font-weight: 400;
28 | display: flex;
29 | gap: 5px;
30 | color:var(--color-third);
31 | }
32 | .date:hover {
33 | text-decoration: underline;
34 | }
35 | .dots {
36 | display: flex;
37 | align-items: center;
38 | justify-content: center;
39 | cursor: pointer;
40 | width: 30px;
41 | height: 30px;
42 | border-radius: 50%;
43 | }
44 |
--------------------------------------------------------------------------------
/client/src/assets/svg/home.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | function Home({ color, className }) {
4 | return (
5 |
14 | );
15 | }
16 |
17 | export default Home;
18 |
--------------------------------------------------------------------------------
/client/src/components/UI/skeleton/notificationSkeleton.jsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "react-loading-skeleton";
2 | import styles from "./skeleton.module.css"
3 |
4 | function NotificationSkeleton() {
5 | return (
6 |
7 |
8 |
14 |
15 |
16 |
24 |
25 |
26 | )
27 | }
28 |
29 | export default NotificationSkeleton
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfileInfo.jsx:
--------------------------------------------------------------------------------
1 | import { CustomButton } from "../index";
2 | import style from "./index.module.css";
3 |
4 | function ProfileInfo({ isVisitor }) {
5 | return (
6 |
7 |
8 | ziwi
9 |
10 |
11 | ziwi
12 | ziwi
13 |
14 |
15 |
16 | {isVisitor ? (
17 |
18 | ) : (
19 |
20 | )}
21 |
22 |
23 |
24 | );
25 | }
26 |
27 | export default ProfileInfo;
28 |
--------------------------------------------------------------------------------
/client/src/components/UI/Notification/Notification.module.css:
--------------------------------------------------------------------------------
1 | .main {
2 | display: flex;
3 | gap: 6px;
4 | flex-direction: column;
5 | background:var(--bg-primary);
6 | padding: 15px;
7 | border-radius: 10px;
8 | }
9 | .header {
10 | font-size: 14px;
11 | font-weight: 600;
12 | }
13 | .content {
14 | display: flex;
15 | align-items: center;
16 | justify-content: space-between;
17 | gap: 15px;
18 | font-size: 14px;
19 | font-weight: 400;
20 | cursor: pointer;
21 | text-decoration: none;
22 | }
23 |
24 | .content2 {
25 | display: flex;
26 | flex-direction: column;
27 | }
28 | .image {
29 | position: relative;
30 | }
31 | .image img:nth-child(1) {
32 | width: 56px;
33 | height: 56px;
34 | border-radius: 100%;
35 | }
36 | .type {
37 | width: 28px;
38 | height: 28px;
39 | position: absolute;
40 | right: -5px;
41 | bottom: 0;
42 | }
43 |
44 | .time {
45 | font-size: 12px;
46 | font-weight: 600;
47 | color:var(--color-primary);
48 | }
49 |
--------------------------------------------------------------------------------
/client/src/pages/Post/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import classes from "./style.module.css";
3 | import {Post , PostSkeleton} from "../../components/index";
4 | import { useNavigate ,useParams} from "react-router-dom";
5 | import { useFetchPostQuery } from "../../app/features/post/postApi";
6 |
7 | function PostPage() {
8 | const navigate = useNavigate();
9 |
10 | const { id } = useParams();
11 | const {data,isLoading ,isError ,isFetching,isSuccess} = useFetchPostQuery(id)
12 | const post = data?.entities[id]
13 |
14 |
15 | const postsSkelton = isLoading || isFetching;
16 |
17 | useEffect(() => {
18 | if (isError) {
19 | navigate("/404");
20 | }
21 | }, [isError]);
22 |
23 | return (
24 |
25 |
26 | {postsSkelton &&
}
27 | {isSuccess && !isLoading &&
}
28 |
29 |
30 | );
31 | }
32 |
33 | export default PostPage;
34 |
--------------------------------------------------------------------------------
/client/src/components/UI/FormLoader/index.jsx:
--------------------------------------------------------------------------------
1 | import PulseLoader from "react-spinners/PulseLoader";
2 | import BeatLoader from "react-spinners/BeatLoader";
3 | import "./index.css";
4 |
5 | const FormLoader = ({ loading, children, type }) => {
6 | return (
7 |
8 | {loading ?
: ""}
9 | {loading ? (
10 |
11 | {type === 2 ? (
12 |
17 | ) : (
18 |
23 | )}
24 |
25 | ) : (
26 | ""
27 | )}
28 |
29 | {children}
30 |
31 |
32 | );
33 | };
34 |
35 | export default FormLoader;
36 |
--------------------------------------------------------------------------------
/api/models/notification.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const Schema = mongoose.Schema;
3 | const User =require("../models/user")
4 |
5 | const NotificationSchema = new Schema(
6 | {
7 | sender: { type: Schema.Types.ObjectId, ref: "user" },
8 | recipient: { type: Schema.Types.ObjectId, ref: "user" },
9 | url: {
10 | type: String,
11 | },
12 | content: {
13 | type: String,
14 | trim: true,
15 | },
16 | type: {
17 | type: String,
18 | enum: ['react', 'comment', 'friend'],
19 | },
20 | seen: {type:Boolean, default: false}
21 | },
22 | { timestamps: true }
23 | );
24 |
25 | NotificationSchema.pre(/^find/, function (next) {
26 | this.populate({
27 | path: 'sender',
28 | select: 'photo',
29 | });
30 | next();
31 | });
32 |
33 | NotificationSchema.post('save', async function () {
34 | this.populate({
35 | path: 'sender',
36 | select: 'photo',
37 | });
38 | });
39 |
40 | module.exports = mongoose.model("notification", NotificationSchema);
41 |
--------------------------------------------------------------------------------
/client/src/app/features/user/photosApi.jsx:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../api/apiSlice";
2 |
3 | export const PhotosApiSlice = apiSlice.injectEndpoints({
4 | endpoints: (builder) => ({
5 |
6 | FetchPhotos: builder.query({
7 | query: (username) => `/api/users/${username}/photos`,
8 | providesTags: ["Photo"],
9 | }),
10 |
11 | updateCoverPhoto: builder.mutation({
12 | query: (credentials) => ({
13 | url: "/api/users/update/profile/cover",
14 | method: "POST",
15 | body: credentials,
16 | }),
17 | invalidatesTags: ["Photo"],
18 | }),
19 |
20 | updateProfilePhoto: builder.mutation({
21 | query: (credentials) => ({
22 | url: "/api/users/update/profile/Photo",
23 | method: "POST",
24 | body: credentials,
25 | }),
26 | invalidatesTags: ["Photo"],
27 | }),
28 | }),
29 | });
30 |
31 | export const {
32 | useFetchPhotosQuery,
33 | useUpdateCoverPhotoMutation,
34 | useUpdateProfilePhotoMutation,
35 | } = PhotosApiSlice;
36 |
--------------------------------------------------------------------------------
/client/src/components/Posts/Post/Likes/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import reactionStyle from "./react.module.css";
3 | import chekedlike from "../../../../assets/svg/like.svg";
4 |
5 | const Likes = ({ Reactions, userId }) => {
6 | if (Reactions?.length > 0) {
7 |
8 | return Reactions?.find((reaction) => reaction.owner === userId) ? (
9 |
10 | <>
11 |
12 | {/* */}
13 |
14 | {Reactions.length > 2
15 | ? `You and ${Reactions?.length - 1} others`
16 | : `${Reactions?.length} like${Reactions?.length > 1 ? "s" : ""}`}
17 |
18 | >
19 | ) : (
20 | <>
21 |
22 |
23 | {Reactions?.length} {Reactions?.length === 1 ? "Like" : "Likes"}
24 |
25 | >
26 | );
27 | }
28 | };
29 |
30 | export default Likes;
31 |
--------------------------------------------------------------------------------
/client/src/components/UI/skeleton/skeleton.module.css:
--------------------------------------------------------------------------------
1 | .post {
2 | padding: 0;
3 | }
4 | .header {
5 | display: flex;
6 | align-items: center;
7 | justify-content: space-between;
8 | gap: 8px;
9 | padding: 15px 10px 0;
10 | }
11 | .header .left img {
12 | width: 40px;
13 | border-radius: 50%;
14 | }
15 |
16 | .header .middle {
17 | display: flex;
18 | flex-direction: column;
19 | width: 100%;
20 | }
21 |
22 | @media (max-width: 1175px) {
23 | .post {
24 | width: 84%;
25 | }
26 |
27 | }
28 | @media (max-width: 1030px) {
29 | .post {
30 | width: 100%;
31 | }
32 | }
33 | @media (max-width: 960px) {
34 | .post {
35 | width: 93%;
36 | }
37 | }
38 |
39 | @media (max-width: 885px) {
40 | .post {
41 | width: 100%;
42 | }
43 | }
44 |
45 | @media (max-width: 620px) {
46 | .post {
47 | width: 100%;
48 | }
49 | }
50 | @media (max-width: 400px) {
51 | .post {
52 | width: 100%;
53 | }
54 |
55 | }
56 |
57 |
58 |
--------------------------------------------------------------------------------
/client/src/assets/svg/closeEye.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/layouts/Modal/ModalManager.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import { closeModal } from "../../app/features/modal/modalSlice";
4 | import { AddEditPost, RegisterForm } from "../../components/index";
5 | import { DeleteConfirm, Modal } from "../index";
6 |
7 | const ModalManager = () => {
8 | const dispatch = useDispatch();
9 | const { isOpen, componentName, childrenProps } = useSelector(
10 | (state) => state.modal
11 | );
12 |
13 | const closeModalHandler = () => dispatch(closeModal());
14 |
15 | const componentsLookUp = { AddEditPost, DeleteConfirm, RegisterForm };
16 | let renderComponent;
17 | if (componentName) {
18 | const SelectedComponent = componentsLookUp[componentName];
19 | if (SelectedComponent) {
20 | renderComponent = ;
21 | }
22 | }
23 | return (
24 |
25 | {renderComponent}
26 |
27 | );
28 | };
29 |
30 | export default ModalManager;
31 |
--------------------------------------------------------------------------------
/client/src/components/Search/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { SearchIcon } from "../../assets/svg";
3 | import { CustomInput } from "../index";
4 | import "./index.css";
5 | const Search = ({ onChange, setShowSearchMenu }) => {
6 | const [showIcon, setShowIcon] = React.useState(true);
7 | const input = React.useRef(null);
8 | return (
9 | <>
10 | {
13 | input.current.focus();
14 | setShowSearchMenu(true);
15 | }}
16 | >
17 | {showIcon && }
18 | {
25 | setShowIcon(false);
26 | setShowSearchMenu(true);
27 | }}
28 | onBlur={() => setShowIcon(true)}
29 | />
30 |
31 | >
32 | );
33 | };
34 |
35 | export default Search;
36 |
--------------------------------------------------------------------------------
/api/models/post.js:
--------------------------------------------------------------------------------
1 | const mongoose = require("mongoose");
2 | const Schema = mongoose.Schema;
3 | const Comment = require("./comment");
4 | const Reaction = require("./reaction");
5 |
6 | const PostSchema = new Schema(
7 | {
8 | owner: {
9 | type: Schema.Types.ObjectId,
10 | ref: "user",
11 | required: true,
12 | },
13 | text: {
14 | type: String,
15 | required: true,
16 | },
17 | image: {
18 | type: String,
19 | default: "no photo",
20 | },
21 | cloudinary_id: {
22 | type: String,
23 | },
24 | },
25 | { timestamps: true }
26 | );
27 |
28 | // delete post comments when the post is removed
29 | PostSchema.pre("remove", async function (next) {
30 | const post = this;
31 | await Comment.deleteMany({ post: post._id });
32 | next();
33 | });
34 |
35 | // delete post reactions when the post is removed
36 | PostSchema.pre("remove", async function (next) {
37 | const post = this;
38 | await Reaction.deleteMany({ post: post._id });
39 | next();
40 | });
41 |
42 | module.exports = mongoose.model("post", PostSchema);
43 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "preview": "vite preview"
10 | },
11 | "dependencies": {
12 | "@popperjs/core": "^2.11.6",
13 | "@reduxjs/toolkit": "^1.9.3",
14 | "formik": "^2.2.9",
15 | "js-cookie": "^3.0.1",
16 | "moment": "^2.29.4",
17 | "react": "^18.2.0",
18 | "react-dom": "^18.2.0",
19 | "react-easy-crop": "^4.7.4",
20 | "react-loading-skeleton": "^3.2.0",
21 | "react-popper": "^2.3.0",
22 | "react-redux": "^8.0.5",
23 | "react-responsive": "^9.0.2",
24 | "react-router-dom": "^6.9.0",
25 | "react-simple-image-viewer": "^1.2.2",
26 | "react-spinners": "^0.13.8",
27 | "react-toastify": "^9.1.1",
28 | "socket.io-client": "^4.6.1",
29 | "styled-components": "^5.3.9",
30 | "yup": "^1.0.2"
31 | },
32 | "devDependencies": {
33 | "@types/react": "^18.0.28",
34 | "@types/react-dom": "^18.0.11",
35 | "@vitejs/plugin-react": "^3.1.0",
36 | "vite": "^4.2.0"
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023 Zhioua Mohamed
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/api/middlewares/checkAuth.js:
--------------------------------------------------------------------------------
1 | const User = require("../models/user");
2 | const jwt = require("jsonwebtoken");
3 |
4 | async function checkAuth(req, res, next) {
5 | let token;
6 | try {
7 | // check if token exist
8 | if (
9 | req.headers.authorization &&
10 | req.headers.authorization.startsWith("Bearer")
11 | ) {
12 | token = req.headers.authorization.split(" ")[1];
13 | } else if (req.cookies.Authorization) {
14 | token = req.cookies.Authorization;
15 | }
16 | if (!token)
17 | return res
18 | .status(401)
19 | .json("You are not logged in, please log in to access");
20 | //decoding the token
21 | var decoded = jwt.verify(token, process.env.ACCESS_TOKEN_SECRET);
22 | //finding the user using decoded sub
23 | const user = await User.findById(decoded.sub);
24 | if (!user) {
25 | return res.status(401).json("user not found");
26 | } else {
27 | // attach user to req
28 | req.user = user;
29 | next();
30 | }
31 | } catch (error) {
32 | return res.status(404).json(error.message);
33 | }
34 | }
35 |
36 | module.exports = checkAuth;
37 |
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfileCover/OldCovers/OldCovers.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 | .card {
7 | width: 100%;
8 | max-width: 700px;
9 | min-height: 70vh;
10 | padding: 0;
11 | margin: 8px;
12 | }
13 | .header {
14 | display: flex;
15 | align-items: center;
16 | justify-content: center;
17 | padding: 16px 0;
18 | border-bottom: 1px solid var(--divider);
19 | font-size: 20px;
20 | font-weight: 700;
21 | position: relative;
22 | }
23 | .header div {
24 | position: absolute;
25 | right: 0;
26 | }
27 | .content {
28 | padding: 16px;
29 | display: flex;
30 | flex-direction: column;
31 | gap: 16px;
32 | }
33 |
34 | .old_photos {
35 | display: flex;
36 | flex-wrap: wrap;
37 | gap: 5px;
38 | justify-content: center;
39 | margin: 10px 0;
40 | max-height: 150px;
41 | overflow: auto;
42 | }
43 | .old_photos img {
44 | width: 100px;
45 | height: 100px;
46 | object-fit: cover;
47 | border-radius: 10px;
48 | cursor: pointer;
49 | }
50 |
--------------------------------------------------------------------------------
/client/src/app/features/user/userProfileApi.jsx:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../api/apiSlice";
2 | import { socket } from "../../../routes/PrivateRoute";
3 |
4 | export const UserProfileApiSlice = apiSlice.injectEndpoints({
5 | endpoints: (builder) => ({
6 | fetchUserProfile: builder.query({
7 | query: (username) => `/api/users/getUserProfile/${username}`,
8 | providesTags: ["Userprofile"],
9 | }),
10 |
11 | FetchFriends: builder.query({
12 | query: () => `/api/friends/`,
13 | providesTags: ["Userprofile"],
14 | }),
15 |
16 | FriendFunc: builder.mutation({
17 | query: ({ id, type }) => ({
18 | url: `/api/friends/${type}/${id}`,
19 | method: "PUT",
20 | }),
21 | invalidatesTags: ["Userprofile"],
22 | transformResponse: (responseData) => {
23 | const newNotification = responseData?.newNotif;
24 | if (newNotification) {
25 | socket.emit("notification", { notification: newNotification });
26 | }
27 | },
28 | }),
29 | }),
30 | });
31 |
32 | export const { useFetchUserProfileQuery, useFriendFuncMutation, useFetchFriendsQuery } =
33 | UserProfileApiSlice;
34 |
--------------------------------------------------------------------------------
/client/src/assets/svg/friends.jsx:
--------------------------------------------------------------------------------
1 | function Friends() {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default Friends;
10 |
--------------------------------------------------------------------------------
/client/src/assets/svg/liveVideo.jsx:
--------------------------------------------------------------------------------
1 | function LiveVideo({ color }) {
2 | return (
3 |
15 | );
16 | }
17 |
18 | export default LiveVideo;
19 |
--------------------------------------------------------------------------------
/client/src/components/UI/skeleton/postSkeleton.jsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "react-loading-skeleton";
2 | import {Card} from "../../index";
3 | import styles from"./skeleton.module.css"
4 |
5 | function PostSkeleton() {
6 | return (
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | }
38 |
39 | export default PostSkeleton;
40 |
--------------------------------------------------------------------------------
/client/src/components/Profile/Friendship/Friendship.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | gap: 10px;
4 | align-items: center;
5 | justify-content: center;
6 | width: 100%;
7 | padding: 16px;
8 | white-space: nowrap;
9 | position: relative;
10 | }
11 | .open_menu {
12 | position: absolute;
13 | top: 60%;
14 | right: auto;
15 | padding: 5px;
16 | width: 80%;
17 | border: solid 1px var(--bg-primary);
18 | }
19 | .open_menu:before {
20 | content: "";
21 | position: absolute;
22 | top: 0px;
23 | margin-top: -23px;
24 | right: 50%;
25 | border: solid 12px transparent;
26 | border-bottom-color: var(--bg-primary);
27 | z-index: 1;
28 | }
29 |
30 | .item {
31 | display: flex;
32 | align-items: center;
33 | gap: 10px;
34 | padding: 10px;
35 | cursor: pointer;
36 | font-weight: 600;
37 | font-size: 14px;
38 | border-radius: 10px;
39 | }
40 |
41 | @media (min-width: 495px) {
42 | .container {
43 | width: 50%;
44 | }
45 |
46 | .open_cover {
47 | right: 50%;
48 | }
49 | .open_cover:before {
50 | right: 20px;
51 | }
52 | }
53 | @media (min-width: 900px) {
54 | .container {
55 | width: 30%;
56 | margin-left: auto;
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/client/src/assets/svg/feeling.jsx:
--------------------------------------------------------------------------------
1 | function Feeling({ color }) {
2 | return (
3 |
20 | );
21 | }
22 |
23 | export default Feeling;
24 |
--------------------------------------------------------------------------------
/client/src/pages/Home/index.jsx:
--------------------------------------------------------------------------------
1 | import { useSelector } from "react-redux";
2 | import { useFetchPostsQuery } from "../../app/features/post/postApi";
3 | import * as component from "../../components";
4 | import style from "./home.module.css";
5 |
6 | function Home() {
7 | const { user } = useSelector((state) => state.user);
8 | const { isLoading, isFetching, isSuccess, isError, error } = useFetchPostsQuery("fetchPosts")
9 | const { sortedPosts } = useFetchPostsQuery("fetchPosts", {
10 | selectFromResult: ({ data }) => ({
11 | sortedPosts: data?.ids.map(id => data?.entities[id])
12 | }),
13 | })
14 |
15 | const postSkeleton = isLoading;
16 | const hidePostSkeleton = isSuccess && !isLoading && !error && sortedPosts
17 |
18 | return (
19 |
20 |
21 |
22 |
23 | {postSkeleton && (
24 | <>
25 |
26 |
27 | >
28 | )}
29 | {hidePostSkeleton && (
30 |
34 | )}
35 |
36 |
37 |
38 | );
39 | }
40 |
41 | export default Home;
42 |
--------------------------------------------------------------------------------
/client/src/app/features/auth/authApi.jsx:
--------------------------------------------------------------------------------
1 | import { apiSlice } from "../../api/apiSlice";
2 | import { logOut } from "../user/userSlice";
3 |
4 | export const authApiSlice = apiSlice.injectEndpoints({
5 | endpoints: (builder) => ({
6 | login: builder.mutation({
7 | query: (Credentials) => ({
8 | url: "/api/users/signin",
9 | method: "POST",
10 | body: { ...Credentials },
11 | }),
12 |
13 | }),
14 |
15 | register: builder.mutation({
16 | query: (credentials) => {
17 | return {
18 | url: "/api/users/signup",
19 | method: "POST",
20 | body: { ...credentials },
21 | };
22 | },
23 | }),
24 |
25 | Logout: builder.mutation({
26 | query: () => {
27 | return {
28 | url: "/api/users/logout",
29 | method: "GET",
30 | };
31 | },
32 | async onQueryStarted(arg, { dispatch, queryFulfilled }) {
33 | try {
34 | const { data } = await queryFulfilled
35 | dispatch(logOut());
36 | setTimeout(() => {
37 | dispatch(apiSlice.util.resetApiState())
38 | }, 1000)
39 | } catch (error) {
40 | console.log(error)
41 | }
42 | }
43 | }),
44 | }),
45 | });
46 |
47 | export const {
48 | useLoginMutation,
49 | useRegisterMutation,
50 | useLogoutMutation,
51 | } = authApiSlice;
52 |
--------------------------------------------------------------------------------
/client/src/assets/svg/photo.jsx:
--------------------------------------------------------------------------------
1 | function Photo({ color }) {
2 | return (
3 |
16 | );
17 | }
18 |
19 | export default Photo;
20 |
--------------------------------------------------------------------------------
/client/src/components/Profile/Photos.jsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "react-loading-skeleton";
2 | import { Link } from "react-router-dom";
3 | import { Card } from "../index";
4 | import style from "./index.module.css";
5 | import PuffLoader from "react-spinners/PuffLoader";
6 |
7 | function Photos({ photosData, photosSkelton }) {
8 | return (
9 |
10 |
11 | Photos
12 |
13 | See all photos
14 |
15 |
16 |
17 |
18 | {photosSkelton ? (
19 |
20 | ) : (
21 | `${photosData?.resources?.length} Photos`
22 | )}
23 |
24 | {photosSkelton ? (
25 |
26 | ) : (
27 |
28 | {photosData?.resources?.slice(0, 9).map((img) => (
29 |
34 | ))}
35 |
36 | )}
37 |
38 |
39 | );
40 | }
41 |
42 | export default Photos;
43 |
--------------------------------------------------------------------------------
/client/src/assets/svg/like.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/api/middlewares/sharpMiddleware.js:
--------------------------------------------------------------------------------
1 | const sharp = require("sharp");
2 | const catchAsync = require('../utils/catchAsync');
3 |
4 | // Load cloudinary methods
5 | const cloudinary = require("../utils/cloudinary");
6 |
7 | module.exports = {
8 | resizeProfileCover: catchAsync(async (req, file, next) => {
9 | const id = req.user.id;
10 | if (!file) {
11 | return res.status(404).json({ message: "Please provide a photo " });
12 | } else {
13 | const path = `${process.env.APP_NAME}/users/${id}/profile_covers/`;
14 | const data = await sharp(req.file.buffer)
15 | .resize(500, 500)
16 | .toFormat("webp")
17 | .webp({ quality: 90 })
18 | .toBuffer();
19 |
20 | const imageDetails = await cloudinary.uploadToCloudinary(data, path);
21 | req.body.cover = imageDetails.url;
22 | next();
23 | }
24 | }),
25 | resizeProfilePhoto: catchAsync(async (req, file, next) => {
26 | const id = req.user.id;
27 | if (!file) {
28 | return res.status(404).json({ message: "Please provide a photo " });
29 | } else {
30 | const path = `${process.env.APP_NAME}/users/${id}/profile_photos/`;
31 | const data = await sharp(req.file.buffer)
32 | .resize(500, 500)
33 | .toFormat("webp")
34 | .webp({ quality: 90 })
35 | .toBuffer();
36 |
37 | const imageDetails = await cloudinary.uploadToCloudinary(data, path);
38 | req.body.photo = imageDetails.url;
39 | next();
40 | }
41 | }),
42 | };
43 |
--------------------------------------------------------------------------------
/client/src/layouts/DeleteConfirm/index.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch } from "react-redux";
2 | import { useDeletePostMutation } from "../../app/features/post/postApi";
3 | import { closeModal } from "../../app/features/modal/modalSlice";
4 | import { CustomButton } from "../../components/index";
5 | import "./index.css";
6 | import { toast } from "react-toastify";
7 |
8 | const DeleteConfirm = ({ id }) => {
9 | const dispatch = useDispatch();
10 | const [deletePost, {isLoading, isSuccess, isError, error }] = useDeletePostMutation();
11 | if(isSuccess){
12 | toast.success("Post deleted successfully", {
13 | position: toast.POSITION.TOP_RIGHT,
14 | });
15 | }
16 | if(isError){
17 | toast.error(error, {
18 | position: toast.POSITION.TOP_RIGHT,
19 | });
20 | }
21 |
22 | return (
23 | <>
24 | Are you sure?
25 |
26 |
27 |
28 | Do you really want to delete these memorie? This process cannot be
29 | undone.
30 |
31 |
32 |
33 | dispatch(closeModal())}
37 | />
38 | {deletePost(id) ;dispatch(closeModal())}}
42 | />
43 |
44 | >
45 | );
46 | };
47 |
48 | export default DeleteConfirm;
49 |
--------------------------------------------------------------------------------
/client/src/utils/YupValidation.jsx:
--------------------------------------------------------------------------------
1 | import * as Yup from "yup";
2 |
3 | export const signupValidation = Yup.object({
4 | firstName: Yup.string()
5 | .required("What's your first name?")
6 | .max(100)
7 | .min(2)
8 | .matches(/^([a-zA-Z]+\s)*[a-zA-Z]+$/, "You can use english charcters only"),
9 | lastName: Yup.string()
10 | .required("What's your last name?")
11 | .max(100)
12 | .min(2)
13 | .matches(/^([a-zA-Z]+\s)*[a-zA-Z]+$/, "You can use english charcters only"),
14 | email: Yup.string()
15 | .required(
16 | "You'll need this when you log in and if you ever need to reset your password."
17 | )
18 | .email("Must be a valid email.")
19 | .max(100),
20 | password: Yup.string()
21 | .required("Password is required")
22 | .min(8)
23 | .matches(
24 | /(?=(.*[0-9]))((?=.*[A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z]))^.{8,}$/i,
25 | "Password should have at least 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long"
26 | ),
27 | passwordConfirm: Yup.string().test(
28 | "passwords-match",
29 | "Password confirm must match password !",
30 | function (value) {
31 | return this.parent.password === value;
32 | }
33 | ),
34 | gender: Yup.string().required("Gender is required"),
35 | });
36 |
37 | export const loginValidation = Yup.object({
38 | email: Yup.string()
39 | .required("Email address is required.")
40 | .email("Must be a valid email.")
41 | .max(100),
42 | password: Yup.string().required("Password is required").min(8),
43 | });
44 |
--------------------------------------------------------------------------------
/client/src/assets/svg/market.jsx:
--------------------------------------------------------------------------------
1 | function Market({ color }) {
2 | return (
3 |
6 | );
7 | }
8 |
9 | export default Market;
10 |
--------------------------------------------------------------------------------
/api/routes/users.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const multerUploads = require("../middlewares/multerMiddleware");
4 | const checkAuth = require("../middlewares/checkAuth");
5 | const AuthController = require("../controllers/auth");
6 | const UserProfileController = require("../controllers/usersProfile");
7 | const sharpMiddleware = require("../middlewares/sharpMiddleware");
8 |
9 | // POST request for creating a new User.
10 | router.post("/signup", AuthController.signup);
11 |
12 | // GET request for user login.
13 | router.post("/signin", AuthController.signin);
14 |
15 | // GET request to logout the User .
16 | router.get("/logout", AuthController.logout);
17 |
18 | // PROTECT ALL ROUTES AFTER THIS MIDDLEWARE
19 | router.use(checkAuth);
20 |
21 | // POST request to filter users by user term .
22 | router.post("/search", UserProfileController.searchUsers);
23 |
24 | /* User Profile */
25 |
26 | // Post request to update the User profile Cover.
27 | router.post(
28 | "/update/profile/cover",
29 | multerUploads,
30 | sharpMiddleware.resizeProfileCover,
31 | UserProfileController.updateProfileCover
32 | );
33 |
34 | // Post request to update the User profile image.
35 | router.post(
36 | "/update/profile/Photo",
37 | multerUploads,
38 | sharpMiddleware.resizeProfilePhoto,
39 | UserProfileController.updateProfilePhoto
40 | );
41 |
42 | // GET request to get all the User photos .
43 | router.get("/:username/photos", UserProfileController.getPhotos);
44 |
45 | // GET request to get a user profile.
46 | router.get("/getUserProfile/:username", UserProfileController.getUserProfile);
47 |
48 | module.exports = router;
49 |
--------------------------------------------------------------------------------
/client/src/components/UI/Notification/index.jsx:
--------------------------------------------------------------------------------
1 | import moment from "moment";
2 | import { Link } from "react-router-dom";
3 | import style from "./Notification.module.css";
4 | import chekedlike from "../.././../assets/svg/like.svg";
5 | import { useIsNotifSeenMutation } from "../../../app/features/notification/notificationApi";
6 |
7 | function Notification({ toast, t, notification }) {
8 | const [isNotifSeen] = useIsNotifSeenMutation();
9 |
10 | return (
11 |
12 |
New notification
13 |
{
17 | toast.dismiss(t.id);
18 | isNotifSeen(notification?._id);
19 | }}
20 | >
21 |
22 |

23 | {notification.type === "react" ? (
24 |

25 | ) : (
26 | ""
27 | )}
28 |
29 |
30 | {notification.content}
31 |
32 | {moment(notification?.createdAt).fromNow()}
33 |
34 |
35 |
45 |
46 |
47 | );
48 | }
49 |
50 | export default Notification;
51 |
--------------------------------------------------------------------------------
/client/src/components/Posts/CreatPost/index.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import { openModal } from "../../../app/features/modal/modalSlice";
3 | import classe from "./postbody.module.css";
4 | import { Feeling, LiveVideo, Photo } from "../../../assets/svg";
5 |
6 | function CreatPost() {
7 | const { user } = useSelector((state) => state.user);
8 |
9 | const dispatch = useDispatch();
10 | return (
11 |
12 |
13 |

14 |
17 | dispatch(
18 | openModal({
19 | name: "AddEditPost",
20 | childrenProps: { user: user },
21 | })
22 | )
23 | }
24 | >
25 | {` What's on your mind, ${user?.firstName}`}
26 |
27 |
28 |
29 |
30 |
31 |
32 | Live Video
33 |
34 |
35 |
39 |
40 | Photo/Video
41 |
42 |
43 |
44 | Feeling/Activity
45 |
46 |
47 |
48 | );
49 | }
50 |
51 | export default CreatPost;
52 |
--------------------------------------------------------------------------------
/client/src/layouts/Modal/index.css:
--------------------------------------------------------------------------------
1 | /* modal */
2 | .modal-backDrop {
3 | position: fixed;
4 | z-index: 1000;
5 | background: rgb(0 0 0 / 0.4);
6 | top: 0;
7 | left: 0;
8 | right: 0;
9 | bottom: 0;
10 | display: flex;
11 | align-items: center;
12 | justify-content: center;
13 | transition: all 0.4s;
14 | backdrop-filter: blur(2px);
15 | }
16 |
17 | .modal-container {
18 | position: fixed;
19 | z-index: 1500;
20 | background-color: var(--bg-primary);
21 | max-width: 100%;
22 | max-height: 100%;
23 | width: 450px;
24 | border-radius: 15px;
25 | box-shadow: 0px 0px 16px 1px var(--shadow-1);
26 | top: 50%;
27 | left: 50%;
28 | transform: translate(-50%, -50%);
29 | transition: all 0.3s;
30 | padding: 20px 40px;
31 | margin: 0 auto;
32 | }
33 |
34 | .modal-close {
35 | top: 6px;
36 | right: 6px;
37 | position: absolute;
38 | border-radius: 50%;
39 | display: flex;
40 | align-items: center;
41 | justify-content: center;
42 | cursor: pointer;
43 | }
44 | .modal-close-icon {
45 | top: 5px;
46 | right: 5px;
47 | width: 25px;
48 | height: 25px;
49 | position: absolute;
50 | -webkit-transition: -webkit-transform 0.25s, opacity 0.25s;
51 | -moz-transition: -moz-transform 0.25s, opacity 0.25s;
52 | transition: transform 0.25s, opacity 0.25s;
53 | opacity: 0.25;
54 | }
55 | .modal-close-icon:hover {
56 | -webkit-transform: rotate(270deg);
57 | -moz-transform: rotate(270deg);
58 | transform: rotate(270deg);
59 | opacity: 1;
60 | }
61 |
62 | .modal-content {
63 | width: 100%;
64 | height: 100%;
65 | }
66 |
67 | .modal-hide {
68 | opacity: 0;
69 | visibility: hidden;
70 | }
71 |
72 | .modal-show {
73 | opacity: 1;
74 | visibility: visible;
75 | }
76 |
--------------------------------------------------------------------------------
/client/src/components/index.jsx:
--------------------------------------------------------------------------------
1 | export { default as AddEditPost } from "./Posts/AddEditPost";
2 | export { default as Post } from "./Posts/Post/Post";
3 | export { default as Comments } from "./Posts/Post/Comments";
4 | export { default as CustomButton } from "./CustomButton";
5 | export { default as CustomInput } from "./input/CustomInput";
6 | export { default as Likes } from "./Posts/Post/Likes";
7 | export { default as PostHead } from "./Posts/Post/PostHead";
8 | export { default as SearchBar } from "./Search";
9 | export { default as Loading } from "./UI/Loading";
10 | export { default as FormLoader } from "./UI/FormLoader";
11 | export { default as Card } from "./UI/Card";
12 | export { default as PostList } from "./Posts/Post/PostList";
13 | export { default as LoginForm } from "./LoginForm";
14 | export { default as RegisterForm } from "./RegisterForm";
15 | export { default as ProfileMenu } from "./Profile/ProfileMenu";
16 | export {default as ProfileCover} from "./Profile/ProfileCover/ProfileCover"
17 | export {default as ProfileInfo} from "./Profile/ProfileInfo"
18 | export {default as Photos} from "./Profile/Photos"
19 | export {default as Friends} from "./Profile/Friends"
20 | export {default as CreatPost } from "./Posts/CreatPost"
21 | export {default as AuthInput } from "./input/AuthInput"
22 | export {default as Popper } from "./UI/Popper"
23 | export{default as PostSkeleton} from "./UI/skeleton/postSkeleton"
24 | export{default as NotificationSkeleton} from "./UI/skeleton/notificationSkeleton"
25 | export {default as ProfilePhoto} from "./Profile/ProfilePhoto"
26 | export {default as Friendship} from "./Profile/Friendship"
27 | export {default as Notification} from "./UI/Notification"
28 | export {default as FriendCard} from "./friendCard"
29 |
30 |
--------------------------------------------------------------------------------
/client/src/components/Profile/Friends.jsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "react-loading-skeleton";
2 | import { Link } from "react-router-dom";
3 | import { Card } from "../index";
4 | import style from "./index.module.css";
5 | import IconStyle from "../../styles/icons.module.css";
6 |
7 | function Friends({ userfriendsdata, photosSkelton }) {
8 | return (
9 |
10 |
11 | Friends
12 |
13 | See all Friends
14 |
15 |
16 |
17 |
18 | {photosSkelton ? (
19 |
20 | ) : (
21 | `${userfriendsdata?.length} Friends`
22 | )}
23 |
24 |
25 | {userfriendsdata &&
26 | userfriendsdata.slice(0, 9).map((user, index) => (
27 |
32 |
36 |
37 | {`${user?.firstName} ${user?.lastName}`}
38 |
41 |
42 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | }
49 |
50 | export default Friends;
51 |
--------------------------------------------------------------------------------
/api/routes/posts.js:
--------------------------------------------------------------------------------
1 | const express = require("express");
2 | const router = express.Router();
3 | const PostController = require("../controllers/posts");
4 | const multerUploads = require("../middlewares/multerMiddleware");
5 | const checkAuth = require("../middlewares/checkAuth");
6 | const limiter = require("../middlewares/postsLimiter")
7 | // PROTECT ALL ROUTES AFTER THIS MIDDLEWARE
8 | router.use(checkAuth);
9 | // POST request
10 | router.post("/addPost",limiter, multerUploads, PostController.addPost);
11 |
12 | // GET request
13 | router.get("/getOnePost/:id", PostController.getOnePost);
14 |
15 | // GET request
16 | router.get("/", PostController.getAllPost);
17 |
18 | // GET request
19 | router.get("/:username/posts", PostController.getAllPostbyUser);
20 |
21 | // PUT request
22 | router.patch("/updatePost/:id", multerUploads, PostController.updatePost);
23 |
24 | // DELETE request
25 | router.delete("/deletePost/:id", PostController.deletePost);
26 |
27 | // PUT request
28 | router.put("/like/:id", PostController.like);
29 |
30 | // GET request
31 | router.get("/getPostsReactions", PostController.getPostsReactions);
32 |
33 | //-------------------------------------Comments-------------------------------//
34 |
35 | // Post request
36 | router.post("/addComment/:id", PostController.addComment);
37 |
38 | // Post request
39 | router.post("/addCommentReply/:id", PostController.addCommentReply);
40 |
41 | // GET request
42 | router.get("/getComments", PostController.getComments);
43 |
44 | // Delete request
45 | router.delete("/deleteComment/:id", PostController.deleteComment);
46 |
47 | // PUT request
48 | router.put("/updateComment/:id", PostController.updateComment);
49 |
50 | // PATCH request
51 | router.patch("/Commentlike/:id", PostController.likeComment);
52 |
53 | module.exports = router;
54 |
--------------------------------------------------------------------------------
/client/src/components/Profile/index.module.css:
--------------------------------------------------------------------------------
1 | /* photos / friends */
2 |
3 | .card_header {
4 | font-size: 20px;
5 | font-weight: 700;
6 | display: flex;
7 | justify-content: space-between;
8 | }
9 | .photo_friends_link {
10 | color: var(--color-secondary);
11 | font-size: 16px;
12 | font-weight: 500;
13 | }
14 | .photo_friends_content {
15 | display: flex;
16 | flex-direction: column;
17 | align-items: flex-start;
18 | }
19 | .photo_friends_content_info {
20 | color: var(--color-third);
21 | font-size: 16px;
22 | font-weight: 400;
23 | }
24 | .photo_grid,
25 | .friends_grid {
26 | display: grid;
27 | grid-template-columns: repeat(3, 1fr);
28 | grid-template-rows: 1fr;
29 | grid-gap: 5px;
30 | width: 100%;
31 | border-radius: 10px;
32 | overflow: hidden;
33 | margin-top: 10px;
34 | }
35 | .friends_grid {
36 | grid-gap: 10px;
37 | border-radius: 0;
38 | }
39 | .photo_card {
40 | background-size: cover;
41 | /* background-position: center; */
42 | }
43 |
44 | .photo_card:before {
45 | content: "";
46 | display: block;
47 | height: 0;
48 | width: 0;
49 | padding-bottom: 100%;
50 | }
51 | .friend_card {
52 | background-size: cover;
53 | background-position: center;
54 | border-radius: 10px;
55 | cursor: pointer;
56 | }
57 | .friend_card:hover,
58 | .photo_card:hover,
59 | .photo img:hover {
60 | filter: brightness(0.9);
61 | }
62 | .friend_card:before {
63 | content: "";
64 | display: block;
65 | height: 0;
66 | width: 0;
67 | padding-bottom: 100%;
68 | }
69 | .friend_name {
70 | font-size: 13px;
71 | font-weight: 600;
72 | cursor: pointer;
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | }
77 | .friend_name:hover {
78 | text-decoration: underline;
79 | }
80 | .link {
81 | text-decoration: none;
82 | }
83 |
--------------------------------------------------------------------------------
/client/src/components/LoginForm/index.css:
--------------------------------------------------------------------------------
1 | .login-container {
2 | height: 78vh;
3 | padding-top: 2rem;
4 | }
5 | .login-head {
6 | margin: 0 auto;
7 | width: 300px;
8 | display: flex;
9 | flex-direction: column;
10 | align-items: center;
11 | }
12 | .SingninSvg {
13 | height: 35px;
14 | width: 45px;
15 | }
16 | .SingninTitle {
17 | display: flex;
18 | align-items: center;
19 | gap: 2px;
20 | }
21 | .title {
22 | font-size: 32px;
23 | font-weight: 600;
24 | color: var(--color-secondary);
25 | }
26 | .login-image {
27 | width: 80%;
28 | }
29 | .login-span {
30 | font-size: 20px;
31 | }
32 |
33 | .login-card {
34 | width: 350px;
35 | padding: 20px;
36 | margin: 1rem auto;
37 | display: flex;
38 | flex-direction: column;
39 | }
40 |
41 | .login-form {
42 | display: flex;
43 | flex-direction: column;
44 | }
45 |
46 | .login {
47 | justify-content: center;
48 | align-items: center;
49 | display: flex;
50 | margin-top: 10px;
51 | }
52 | .login_link:hover {
53 | text-decoration: underline;
54 | color: var(--color-primary);
55 | }
56 | .signup-link {
57 | cursor: pointer;
58 | font-size: 14px;
59 | font-weight: 700;
60 | color: var(--color-primary);
61 | text-decoration: none;
62 | }
63 | .signup-link:hover {
64 | text-decoration: underline;
65 | color: var(--color-primary);
66 | }
67 |
68 | @media (min-width: 900px) {
69 | .login-container {
70 | display: flex;
71 | margin: 0 auto;
72 | max-width: 1000px;
73 | align-items: center;
74 | }
75 |
76 | .login-head {
77 | width: 50%;
78 | margin: 0 auto 8rem;
79 | align-items: start;
80 | }
81 | .login-span {
82 | font-size: 27px;
83 | }
84 | .login-card {
85 | width: 400px;
86 | }
87 | .login-image {
88 | width: 300px;
89 | margin-left: -1.4rem;
90 | }
91 | }
92 |
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfileCover/cover.module.css:
--------------------------------------------------------------------------------
1 | /* ProfileCover */
2 | .coverContainer {
3 | width: 100%;
4 | padding: 18%;
5 | background: var(--bg-third);
6 | border-radius: 0 0 10px 10px;
7 | position: relative;
8 | min-height: 170px;
9 | /* background-size: cover ; */
10 | background-size: auto 100%;
11 | background-position: center;
12 | }
13 | .btns_container {
14 | position: fixed;
15 | left: 0;
16 | top: 56px;
17 | height: 56px;
18 | width: 100%;
19 | background: rgb(0 0 0 / 70%);
20 | z-index: 1;
21 | display: flex;
22 | align-items: center;
23 | justify-content: space-between;
24 | padding: 0 20px;
25 | }
26 | .left {
27 | display: flex;
28 | align-items: center;
29 | gap: 10px;
30 | color: var(--bg-primary);
31 | }
32 | .btns {
33 | display: flex;
34 | justify-content: flex-end;
35 | gap: 10px;
36 | }
37 | .cover {
38 | font-weight: 500;
39 | }
40 | .cover_cropper {
41 | width: 100%;
42 | height: 100%;
43 | }
44 | .mediaClassName {
45 | width: 100%;
46 | }
47 |
48 | .edit_container {
49 | position: absolute;
50 | right: 20px;
51 | bottom: 20px;
52 | }
53 | .edit {
54 | background: var(--bg-third);
55 | padding: 7px 15px;
56 | display: flex;
57 | align-items: center;
58 | gap: 10px;
59 | font-weight: 600;
60 | font-size: 14px;
61 | color: #111;
62 | border-radius: 10px;
63 | cursor: pointer;
64 | width: max-content;
65 | position: relative;
66 | }
67 | .cover_upload_menu {
68 | position: absolute;
69 | right: 50%;
70 | top: 41px;
71 | padding: 10px;
72 | z-index: 8;
73 | width: 250px;
74 | }
75 | .open_cover_menu_item {
76 | display: flex;
77 | align-items: center;
78 | gap: 10px;
79 | padding: 10px;
80 | cursor: pointer;
81 | font-weight: 600;
82 | font-size: 14px;
83 | border-radius: 10px;
84 | }
85 |
--------------------------------------------------------------------------------
/client/src/routes/PrivateRoute.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import { Navigate, Outlet } from "react-router-dom";
3 | import { useDispatch, useSelector } from "react-redux";
4 | import { Header } from "../layouts";
5 | import { io } from "socket.io-client";
6 | import { setOnlineUsers } from "../app/features/socket/socketSlice";
7 | import { Loading, Notification } from "../components/index";
8 | import { toast } from "react-toastify";
9 | import notification from "../assets/sound/notification.wav"
10 | export let socket;
11 | function playNotificationSound() {
12 | const audio = new Audio(notification);
13 | audio.play();
14 | }
15 | function PrivateRoute() {
16 | const dispatch = useDispatch();
17 | const { token, user } = useSelector((state) => state.user);
18 | useEffect(() => {
19 | if (user) {
20 | socket = io(import.meta.env.VITE_API_BASE_URL);
21 | socket.emit("setup", {
22 | userId: user._id,
23 | info: {
24 | id: user._id,
25 | username: user.username,
26 | firstName: user.firstName,
27 | lastName: user.lastName,
28 | photo: user.photo,
29 | },
30 | });
31 | socket.on("online_user", ({ type, info }) => {
32 | dispatch(setOnlineUsers({ type, info }));
33 | });
34 | socket.on("new_notification", ({ notification }) => {
35 | playNotificationSound();
36 | toast((t) => (
37 |
38 | ));
39 | });
40 | }
41 | }, []);
42 | return token ? (
43 | <>
44 |
45 | }>
46 |
47 |
48 | >
49 | ) : (
50 |
51 |
52 | );
53 | }
54 | export default PrivateRoute;
55 |
--------------------------------------------------------------------------------
/client/src/layouts/Header/NotificationMenu/Notification.module.css:
--------------------------------------------------------------------------------
1 | .notif_menu {
2 | padding: 0 0.3rem;
3 | position: absolute;
4 | top: 100%;
5 | right: 0;
6 | width: 360px;
7 | border-radius: 10px;
8 | background: var(--bg-primary);
9 | box-shadow: 2px 2px 2px var(--shadow-1);
10 | user-select: none;
11 | padding: 10px;
12 | border: 1px solid var(--color-primary);
13 | overflow: auto;
14 | max-height: 600px;
15 | }
16 | .trashIcon {
17 | position: absolute;
18 | left: 83%;
19 | }
20 | .trashIcon:hover {
21 | transform: scale(1.2);
22 | }
23 | .notif_menu h2 {
24 | padding: 3px;
25 | font-size: 17px;
26 | }
27 | .notification {
28 | padding: 10px;
29 | border-radius: 10px;
30 | display: flex;
31 | justify-content: center;
32 | align-items: center;
33 | }
34 |
35 | .content {
36 | display: flex;
37 | align-items: center;
38 | justify-content: flex-start;
39 | gap: 15px;
40 | font-size: 14px;
41 | font-weight: 400;
42 | cursor: pointer;
43 | text-decoration: none;
44 | }
45 | .image {
46 | position: relative;
47 | }
48 | .image img:nth-child(1) {
49 | width: 56px;
50 | height: 56px;
51 | border-radius: 100%;
52 | }
53 | .content2 {
54 | display: flex;
55 | flex-direction: column;
56 | flex: 1;
57 | }
58 | .type {
59 | width: 28px;
60 | height: 28px;
61 | position: absolute;
62 | right: -5px;
63 | bottom: 0;
64 | }
65 | .time {
66 | font-size: 12px;
67 | font-weight: 600;
68 | color: var(--color-primary);
69 | }
70 | .skeleton_header {
71 | display: flex;
72 | align-items: center;
73 | justify-content: space-between;
74 | gap: 8px;
75 | padding: 15px 10px 0;
76 | }
77 | .skeleton_header .skeleton_left img {
78 | width: 40px;
79 | border-radius: 50%;
80 | }
81 |
82 | .skeleton_header .skeleton_middle {
83 | display: flex;
84 | flex-direction: column;
85 | width: 100%;
86 | }
87 |
--------------------------------------------------------------------------------
/client/src/app/features/user/userSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 | import Cookies from "js-cookie";
3 |
4 | const initialState = {
5 | user: Cookies.get("user") ? JSON.parse(Cookies.get("user")) : null,
6 | token: Cookies.get("token") ? JSON.parse(Cookies.get("token")) : null,
7 | theme: Cookies.get("theme")
8 | ? JSON.parse(Cookies.get("theme"))
9 | : window.matchMedia &&
10 | window.matchMedia("(prefers-color-scheme: dark)").matches
11 | ? "dark"
12 | : "light",
13 | };
14 |
15 | export const userSlice = createSlice({
16 | name: "user",
17 | initialState,
18 | reducers: {
19 | setCredentials: (state, action) => {
20 | state.user = action.payload.user;
21 | state.token = action.payload.token;
22 | Cookies.set("user", JSON.stringify(action.payload.user), {
23 | expires: 90,
24 | });
25 | Cookies.set("token", JSON.stringify(action.payload.token), {
26 | expires: 90,
27 | });
28 | },
29 | UpdateCover: (state, action) => {
30 | state.user.cover = action.payload;
31 | Cookies.set("user", JSON.stringify(state.user), {
32 | expires: 90,
33 | });
34 | },
35 | Updatephoto: (state, action) => {
36 | state.user.photo = action.payload;
37 | Cookies.set("user", JSON.stringify(state.user), {
38 | expires: 90,
39 | });
40 | },
41 |
42 | logOut: (state, action) => {
43 | state.user = null;
44 | state.token = null;
45 | Cookies.set("user", null);
46 | Cookies.set("token", null);
47 | },
48 | changeTheme: (state, action) => {
49 | state.theme = action.payload;
50 | Cookies.set("theme", JSON.stringify(action.payload));
51 | },
52 | },
53 | });
54 |
55 | export const { logOut, setCredentials, UpdateCover, Updatephoto,changeTheme} = userSlice.actions;
56 | export default userSlice.reducer;
57 |
--------------------------------------------------------------------------------
/client/src/assets/svg/search.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | function SearchIcon({ color }) {
4 | return (
5 |
25 | );
26 | }
27 |
28 | export default SearchIcon;
29 |
--------------------------------------------------------------------------------
/client/src/routes/Router.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { Route, Routes } from "react-router-dom";
3 | import ForceRedirect from "./ForceRedirect";
4 | import PrivateRoute from "./PrivateRoute";
5 | import { NotFound } from "../pages/index";
6 | import Prefetch from "../app/features/auth/prefetch";
7 |
8 | const Home = React.lazy(() => import("../pages/Home"));
9 | const Login = React.lazy(() => import("../pages/login"));
10 | const Profile = React.lazy(() => import("../pages/Profile"));
11 | const PostPage = React.lazy(() => import("../pages/Post"));
12 | const FriendsPage = React.lazy(() => import("../pages/friends"));
13 |
14 | const Router = () => {
15 | return (
16 |
17 |
18 |
19 | }>
20 |
24 | }
25 | />
26 |
29 | }
30 | />
31 |
35 | }
36 | />
37 |
41 | }
42 | />
43 |
47 | }
48 | />
49 |
53 | }
54 | />
55 |
56 | }>
57 |
61 | }
62 | />
63 |
64 | } />
65 |
66 | );
67 | };
68 |
69 | export default Router;
70 |
--------------------------------------------------------------------------------
/api/controllers/notification.js:
--------------------------------------------------------------------------------
1 | const Notif = require("../models/notification");
2 |
3 | module.exports = {
4 | getNotifcations: async (req, res) => {
5 | try {
6 | const notifies = await Notif.find({ recipient: req.user.id })
7 | .sort("-createdAt")
8 | .populate("sender", ["firstName", "lastName", "photo"]);
9 | const notseenNotification = await Notif.find({
10 | recipient: req.user.id,
11 | seen: false,
12 | }).countDocuments();
13 | return res.status(201).json({notifies , notseenNotification});
14 | } catch (error) {
15 | return res.status(500).json({ message: error.message });
16 | }
17 | },
18 | // ----------------------//update notification method to make it seen//--------------------------- //
19 |
20 | isNotifSeen: async (req, res) => {
21 | try {
22 | const { id } = req.params;
23 | const notif = await Notif.findById(id);
24 | if (!notif) {
25 | return res.status(404).json({ message: "Notification not found" });
26 | } else {
27 | notif.seen = true;
28 | await notif.save();
29 | return res.status(201).json(notif);
30 | }
31 | } catch (error) {
32 | return res.status(404).json({ message: error.message });
33 | }
34 | },
35 | // ----------------------//delete a Notif by id//--------------------------- //
36 |
37 | deleteNotif: async (req, res) => {
38 | try {
39 | const { id } = req.params;
40 | const notif = await Notif.findById(id);
41 | if (!notif) {
42 | return res.status(404).json({ message: "Notification not found" });
43 | } else {
44 | await notif.remove();
45 | return res.status(201).json({ message: "Notification has been deleted successfuly" });
46 | }
47 | } catch (error) {
48 | return res.status(404).json({ message: error.message });
49 | }
50 | },
51 |
52 |
53 | };
54 |
--------------------------------------------------------------------------------
/client/src/components/friendCard/index.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import classes from "../../pages/friends/style.module.css";
3 | import { useFetchUserProfileQuery, useFriendFuncMutation } from "../../app/features/user/userProfileApi";
4 | import CustomButton from "../CustomButton";
5 |
6 | export default function FriendCard({ user, type, requestID }) {
7 | const { data } = useFetchUserProfileQuery(user?.usernameID);
8 | const [FriendFunc] = useFriendFuncMutation();
9 |
10 | const acceptRequestHanlder = async (requestID) => {
11 | FriendFunc({ id: requestID, type: "accept" });
12 | };
13 |
14 | const cancelRequestHandler = async (requestID) => {
15 | FriendFunc({ id: requestID, type: "cancel" });
16 | };
17 |
18 |
19 |
20 | return (
21 |
22 |
23 |

24 |
25 | {user.firstName} {user.lastName}
26 |
27 |
28 | {type === "sent" ? (
29 |
30 | cancelRequestHandler(requestID)}
33 | >
34 | Cancel Request
35 |
36 |
37 | ) : type === "request" ? (
38 |
39 | acceptRequestHanlder(requestID)}
42 | >
43 | Confirm
44 |
45 | cancelRequestHandler(requestID)}
48 | >
49 | Delete
50 |
51 |
52 | ) : (
53 | ""
54 | )}
55 |
56 | );
57 | }
58 |
--------------------------------------------------------------------------------
/client/src/components/Posts/Post/PostHead/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { useDispatch, useSelector } from "react-redux";
3 | import moment from "moment";
4 | import { Link } from "react-router-dom";
5 | import { openModal } from "../../../../app/features/modal/modalSlice";
6 | import style from "./postHead.module.css";
7 | import { Dots } from "../../../../assets/svg";
8 |
9 | const PostHead = ({ post, isVisitor }) => {
10 | const { user } = useSelector((state) => state.user);
11 |
12 | const dispatch = useDispatch();
13 | const canEdit = Boolean(
14 | user?._id === post?.owner?._id || user?._id === post?.owner
15 | );
16 | return (
17 |
18 |
19 |
20 |
24 |

25 |
26 |
27 |
28 |
32 | {post?.owner?.firstName}
33 |
34 |
35 | {moment(post?.createdAt).fromNow()}
36 |
37 |
38 |
39 | {canEdit && (
40 |
43 | dispatch(
44 | openModal({
45 | name: "AddEditPost",
46 | childrenProps: { post: post, user: user },
47 | })
48 | )
49 | }
50 | >
51 |
52 |
53 | )}
54 |
55 | );
56 | };
57 |
58 | export default PostHead;
59 |
--------------------------------------------------------------------------------
/client/src/components/RegisterForm/GenderSelector.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useMediaQuery } from "react-responsive";
3 | import classes from "./register.module.css";
4 | import { Field, ErrorMessage, useField } from "formik";
5 | import {Popper} from "../index";
6 | import ErrorSVG from "../../assets/svg/Error.svg";
7 |
8 | function GenderSelector() {
9 | const [trigger, setTrigger] = React.useState(null);
10 | const [show, setShow] = React.useState(false);
11 | const [field, meta] = useField({ name: "gender" });
12 |
13 | const genderError = meta.error;
14 |
15 | const desktopView = useMediaQuery({
16 | query: "(min-width: 850px)",
17 | });
18 |
19 |
20 | return (
21 | setShow(true)}
25 | onMouseLeave={() => setShow(false)}
26 | >
27 |
28 | Gender
29 | {genderError &&
30 |

}
31 |
32 |
33 |
42 |
51 | {genderError && show && (
52 |
57 |
58 |
59 | )}
60 |
61 |
62 | );
63 | }
64 |
65 | export default GenderSelector;
66 |
--------------------------------------------------------------------------------
/api/server.js:
--------------------------------------------------------------------------------
1 | const path = require('path')
2 | const express = require("express");
3 | const cors = require("cors");
4 | const cookieParser = require("cookie-parser");
5 | const { createServer } = require("http");
6 | const connectDB = require("./config/db");
7 | const corsOptions = require ("./utils/corsOptions")
8 | const allowedOrigins = require("./utils/allowedOrigins")
9 | const xss = require('xss-clean');
10 |
11 | const app = express();
12 |
13 | // connectDB()
14 | app.use(cors((corsOptions)));
15 |
16 | //load env variables
17 | require("dotenv").config();
18 | const PORT = process.env.PORT || 3070;
19 |
20 | // express app config
21 | // Middleware
22 | app.use(express.json({ limit: "5000kb" })); // LIMIT for JSON
23 | // parse requests of content-type - application/x-www-form-urlencoded
24 | app.use(express.urlencoded({ extended: true, limit: "5000kb" })); // LIMIT for URL ENCODE (image data)
25 | app.use(cookieParser());
26 | // Add the xss-clean middleware to all routes
27 | app.use(xss());
28 |
29 | // app.use('/', express.static(path.join(__dirname, 'public')))
30 |
31 | //Routes
32 | app.use("/api/users", require("./routes/users"));
33 | app.use("/api/posts", require("./routes/posts"));
34 | app.use("/api/friends", require("./routes/friends"));
35 | app.use("/api/notifications", require("./routes/notifications"));
36 |
37 | app.all('*', (req, res) => {
38 | res.status(404)
39 | if (req.accepts('html')) {
40 | res.sendFile(path.join(__dirname, 'views', '404.html'))
41 | } else if (req.accepts('json')) {
42 | res.json({ message: '404 Not Found' })
43 | } else {
44 | res.type('txt').send('404 Not Found')
45 | }
46 | })
47 |
48 |
49 | const httpServer = createServer(app);
50 | const sio = require("./utils/socket");
51 |
52 | sio.init(httpServer, {
53 | pingTimeout: 60000,
54 | pingInterval: 60000,
55 | cors: {
56 | origin: allowedOrigins,
57 | },
58 | });
59 |
60 | const server = httpServer.listen(PORT, function () {
61 | console.log(`Server Runs Perfectly at http://localhost:${PORT}`);
62 | });
63 |
--------------------------------------------------------------------------------
/client/src/components/Search/SearchMenu/SearchMenu.module.css:
--------------------------------------------------------------------------------
1 | .SearchMenu_container {
2 | position: absolute;
3 | align-items: flex-start;
4 | flex-direction: column;
5 | min-width: 250px;
6 | border-bottom-left-radius: 10px;
7 | border-bottom-right-radius: 10px;
8 | background: var(--bg-primary);
9 | box-shadow: 2px 2px 2px var(--shadow-1);
10 | border: 1px solid var(--color-primary);
11 | padding: 8px 6px 10px 6px;
12 | overflow: hidden;
13 | gap: 6px;
14 | min-height: 400px;
15 | max-height: 700px;
16 | z-index: 1;
17 | }
18 |
19 | .SearchMenu_wrap {
20 | display: flex;
21 | align-items: center;
22 | gap: 15px;
23 | }
24 | /* .SearchMenu_container .search {
25 | padding: 10px 44px 10px 12px;
26 | width: 252px;
27 | gap: 4px;
28 | } */
29 | .search {
30 | display: flex;
31 | align-items: center;
32 | gap: 6px;
33 | background: var(--bg-secondary);
34 | padding: 10px 20px 10px 13px;
35 | border-radius: 50px;
36 | cursor: text;
37 | min-width: 250px;
38 | }
39 | .search_input {
40 | outline: none;
41 | border: none;
42 | background: transparent;
43 | font-size: 15px;
44 | font-family: inherit;
45 | padding-left: 2px;
46 | }
47 | /*---Search menu-----*/
48 |
49 | .search_results {
50 | display: flex;
51 | flex-direction: column;
52 | gap: 5px;
53 | margin-top: 10px;
54 | }
55 | .search_result {
56 | display: flex;
57 | align-items: center;
58 | gap: 12px;
59 | font-size: 14px;
60 | padding: 5px;
61 | border-radius: 10px;
62 | cursor: pointer;
63 | color: var(--bg-primary);
64 | font-weight: 600;
65 | }
66 |
67 | .search_result_img {
68 | width: 36px;
69 | height: 36px;
70 | border-radius: 50%;
71 | object-fit: cover;
72 | }
73 |
74 | .search_result span {
75 | transform: translateY(-5px);
76 | }
77 |
78 | .search_item {
79 | border-radius: 10px;
80 | padding: 2px 5px;
81 | }
82 | .search_user_item {
83 | display: flex;
84 | align-items: center;
85 | width: 100%;
86 | justify-content: space-between;
87 | padding-right: 10px;
88 | }
89 |
--------------------------------------------------------------------------------
/api/utils/cloudinary.js:
--------------------------------------------------------------------------------
1 | const cloudinary = require("cloudinary");
2 |
3 | //here is cloudinary api credentials
4 | cloudinary.config({
5 | cloud_name: process.env.CLOUD_NAME,
6 | api_key: process.env.API_KEY,
7 | api_secret: process.env.API_SECRET,
8 | });
9 |
10 | module.exports = {
11 | // Upload image to cloudinary
12 |
13 | // uploadToCloudinary: async (fileString, format) => {
14 | // try {
15 | // const { uploader } = cloudinary;
16 |
17 | // const res = await uploader.upload(
18 | // `data:image/${format};base64,${fileString}`
19 | // );
20 | // return res;
21 | // } catch (error) {
22 | // console.log(error);
23 | // }
24 | // },
25 |
26 | uploadToCloudinary : async (file, path) => {
27 | return new Promise((resolve, reject) => {
28 | if (file) {
29 | cloudinary.v2.uploader
30 | .upload_stream({ folder: path }, (err, res) => {
31 | if (err) {
32 | console.log("🚀 ~ file: cloudinary.js:32 ~ .upload_stream ~ err:", err)
33 | reject(err);
34 | } else {
35 | resolve(res);
36 | console.log(`Upload succeed: ${res}`);
37 | }
38 | })
39 | .end(file);
40 | }
41 | });
42 | },
43 |
44 | // delete image from cloudinary
45 | removeFromCloudinary: async (public_id) => {
46 | await cloudinary.uploader.destroy(public_id, function (error, result) {
47 | console.log("🚀 ~ file: cloudinary.js:28 ~ result", result)
48 | console.log({ message: error.message })
49 | });
50 | },
51 | // get images from cloudinary
52 |
53 | getImages : async (path, max, sort) => {
54 | return new Promise((resolve, reject) => {
55 | cloudinary.v2.search
56 | .expression(`${path}`)
57 | .sort_by('created_at', `${sort}`)
58 | .max_results(max)
59 | .execute()
60 | .then((res) => {
61 | resolve(res);
62 | })
63 | .catch((error) => {
64 | reject(error);
65 | console.log({ message: error.message })
66 | });
67 |
68 | });
69 | },
70 |
71 | };
72 |
--------------------------------------------------------------------------------
/client/src/app/features/reaction/reactionApi.jsx:
--------------------------------------------------------------------------------
1 | import { createEntityAdapter, createSelector } from "@reduxjs/toolkit";
2 | import {socket} from "../../../routes/PrivateRoute"
3 | import { apiSlice } from "../../api/apiSlice";
4 |
5 | const reactionAdapter = createEntityAdapter({
6 | selectId: (reaction) => reaction._id,
7 | });
8 |
9 | const initialState = reactionAdapter.getInitialState();
10 |
11 | export const reactionApiSlice = apiSlice.injectEndpoints({
12 | endpoints: (builder) => ({
13 |
14 | fetchReactions: builder.query({
15 | query: () => "/api/posts/getPostsReactions/",
16 | transformResponse: (responseData) => {
17 | return reactionAdapter.setAll(initialState, responseData);
18 | },
19 | providesTags: (result, error, arg) => [
20 | { type: "Reaction", id: "LIST" },
21 | ...result.ids.map((id) => ({ type: "Reaction", id })),
22 | ],
23 | }),
24 |
25 | likePost: builder.mutation({
26 | query: (id) => ({
27 | url: `/api/posts/like/${id}`,
28 | method: "PUT",
29 | }),
30 | invalidatesTags: (result, error, arg) => [
31 | { type: "Reaction", id: arg.id },
32 | ],
33 | transformResponse: (responseData) => {
34 | const newNotification = responseData?.newNotif;
35 | if (newNotification) {
36 | socket.emit("notification", { notification: newNotification });
37 | }
38 | },
39 | }),
40 | }),
41 | });
42 |
43 |
44 | export const { useFetchReactionsQuery, useLikePostMutation } = reactionApiSlice;
45 |
46 | // returns the query result object
47 | export const selectReactionsResult =
48 | reactionApiSlice.endpoints.fetchReactions.select();
49 |
50 | // Creates memoized selector
51 | const selectReactionsData = createSelector(
52 | selectReactionsResult,
53 | (reactionsResult) => reactionsResult.data // normalized state object with ids & entities
54 | );
55 |
56 | export const {
57 | selectAll: selectAllReactions,
58 | selectById: selectReactionById,
59 | selectIds: selectReactionIds,
60 | } = reactionAdapter.getSelectors(
61 | (state) => selectReactionsData(state) ?? initialState
62 | );
63 |
--------------------------------------------------------------------------------
/client/src/components/UI/Popper/index.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import styled from "styled-components";
3 | import { usePopper } from "react-popper";
4 | import Portal from "../../../utils/Portal";
5 |
6 | export const TooltipArrow = styled.div`
7 | width: 0.6rem;
8 | height: 0.6rem;
9 | &::before {
10 | content: "";
11 | background: #b94a48;
12 | width: 0.6rem;
13 | height: 0.6rem;
14 | transform: translate(-50%, -50%) rotate(45deg);
15 | position: absolute;
16 | top: 0;
17 | left: 0;
18 | }
19 | `;
20 |
21 | const TipWrapper = styled.div`
22 | background: #b94a48;
23 | border-radius: 5px;
24 | color: white;
25 | padding: 0.8rem;
26 | font-weight: 300;
27 | font-size: 13px;
28 | z-index: 99;
29 | max-width: 318px;
30 |
31 | &[data-popper-placement^="right"] {
32 | ${TooltipArrow} {
33 | left: 0px;
34 | }
35 | }
36 |
37 | &[data-popper-placement^="left"] {
38 | ${TooltipArrow} {
39 | right: -0.6rem;
40 | }
41 | }
42 |
43 | &[data-popper-placement^="top"] {
44 | ${TooltipArrow} {
45 | bottom: -0.6rem;
46 | }
47 | }
48 |
49 | &[data-popper-placement^="bottom"] {
50 | ${TooltipArrow} {
51 | top: 0px;
52 | }
53 | }
54 | `;
55 |
56 | const Popper = ({ children, trigger, placement, offsetNum }) => {
57 | const [popperElement, setPopperElement] = useState(null);
58 | const [arrowElement, setArrowElement] = useState(null);
59 |
60 | const { styles, attributes } = usePopper(trigger, popperElement, {
61 | placement,
62 | modifiers: [
63 | { name: "arrow", options: { element: arrowElement } },
64 | {
65 | name: "offset",
66 | options: {
67 | offset: [0, parseInt(offsetNum || 8)],
68 | },
69 | },
70 | ],
71 | });
72 |
73 | return (
74 |
75 |
80 | {children}
81 |
82 |
83 |
84 | );
85 | };
86 |
87 | export default Popper;
88 |
--------------------------------------------------------------------------------
/client/src/components/Posts/Post/Comments/CommentForm.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { useSelector } from "react-redux";
3 | import { CustomInput, CustomButton } from "../../../index";
4 | import Styles from "./comment.module.css";
5 |
6 | const CommentForm = ({
7 | placholdertxt,
8 | submitLabel,
9 | onSubmit,
10 | InitialText = "",
11 | hasCancelButton = false,
12 | EditCancelHandler,
13 | autoFocus = false,
14 | }) => {
15 | const CurrentUserImage = useSelector((state) => state.user.user.photo);
16 | const [text, setText] = useState(InitialText);
17 | const isTextareaDisabled = text.length === 0 || text === InitialText;
18 |
19 | //onSubmit handler
20 | async function handleSubmit(e) {
21 | e.preventDefault();
22 | await onSubmit(text);
23 | setText("");
24 | }
25 | return (
26 |
27 | {!(submitLabel === "update") && (
28 |
29 |

30 |
31 | )}
32 |
62 |
63 | );
64 | };
65 |
66 | export default CommentForm;
67 |
--------------------------------------------------------------------------------
/api/validator/SignupValidation.js:
--------------------------------------------------------------------------------
1 | const validator = require("validator");
2 | const isEmpty = require("./IsEmpty");
3 |
4 | module.exports = function SignupValidation(data) {
5 | let regex = /(?=(.*[0-9]))((?=.*[A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z]))^.{8,}$/i;
6 | let errors = {};
7 | // Convert empty fields to an empty string so we can use validator
8 | data.firstName = !isEmpty(data.firstName) ? data.firstName : "";
9 | data.lastName = !isEmpty(data.lastName) ? data.lastName : "";
10 | data.email = !isEmpty(data.email) ? data.email : "";
11 | data.password = !isEmpty(data.password) ? data.password : "";
12 | data.passwordConfirm = !isEmpty(data.passwordConfirm) ? data.passwordConfirm : "";
13 | data.gender = !isEmpty(data.gender) ? data.gender : "";
14 | data.birthYear = !isEmpty(data.birthYear) ? data.birthYear : "";
15 | data.birthMonth = !isEmpty(data.birthMonth) ? data.birthMonth : "";
16 | data.birthDay = !isEmpty(data.birthDay) ? data.birthDay : "";
17 |
18 | // firstName checks
19 | if (validator.isEmpty(data.firstName)) {
20 | errors.firstName = "firstName field is required";
21 | }
22 | // lastName checks
23 | if (validator.isEmpty(data.lastName)) {
24 | errors.lastName = "lastName field is required";
25 | }
26 | // gender checks
27 | if (validator.isEmpty(data.gender)) {
28 | errors.gender = "gender field is required";
29 | }
30 |
31 | // Email checks
32 | if (validator.isEmpty(data.email)) {
33 | errors.email = "Email field is required";
34 | } else if (!validator.isEmail(data.email)) {
35 | errors.email = "Format Email required";
36 | }
37 |
38 | // Password checks
39 | if (validator.isEmpty(data.password)) {
40 | errors.password = "Password field is required";
41 | } else if (!regex.test(data.password)) {
42 | errors.password =
43 | "Password should have 1 lowercase letter, 1 uppercase letter, 1 number, and be at least 8 characters long";
44 | }
45 |
46 | if (!validator.equals(data.password, data.passwordConfirm)) {
47 | errors.passwordConfirm = "Passwords not matches";
48 | }
49 |
50 | if (validator.isEmpty(data.passwordConfirm)) {
51 | errors.passwordConfirm = "Required passwordConfirm";
52 | }
53 | return {
54 | errors,
55 | isValid: isEmpty(errors),
56 | };
57 | };
58 |
--------------------------------------------------------------------------------
/client/src/components/Posts/Post/post.module.css:
--------------------------------------------------------------------------------
1 | /* Post */
2 | .post {
3 | padding: 0;
4 | }
5 | .post_body {
6 | display: flex;
7 | flex-direction: column;
8 | gap: 10px;
9 | }
10 | .footer {
11 | display: flex;
12 | flex-direction: column;
13 | }
14 | .reaction {
15 | display: flex;
16 | justify-content: space-around;
17 | height: 40px;
18 | margin: 0 16px 10px;
19 | padding: 3px 0;
20 | position: relative;
21 | border-top: 1px solid var(--divider);
22 | border-bottom: 1px solid var(--divider);
23 | }
24 | .reaction_infos {
25 | display: flex;
26 | justify-content: space-between;
27 | padding: 10px 16px;
28 | }
29 | .reaction_infos_left {
30 | display: flex;
31 | gap: 8px;
32 | cursor: pointer;
33 | }
34 |
35 | .reaction_infos_right {
36 | display: flex;
37 | gap: 5px;
38 | color: var(--color-third);
39 | font-size: 15px;
40 | }
41 | .reaction_infos_right span {
42 | cursor: pointer;
43 | }
44 |
45 | .reaction_infos_right span:hover {
46 | text-decoration: underline;
47 | }
48 | .image_container {
49 | height: inherit;
50 | width: 100%;
51 | max-height: inherit;
52 | margin-top: 8px;
53 | }
54 | .post_image {
55 | height: inherit;
56 | width: inherit;
57 | max-height: inherit;
58 | object-fit: cover;
59 | }
60 | .edit_link {
61 | color: var(--color-secondary) !important;
62 | }
63 |
64 | .profile_img {
65 | width: 40px;
66 | height: 40px;
67 | border-radius: 50%;
68 | object-fit: cover;
69 | }
70 |
71 | .post_comments_section {
72 | width: 100%;
73 | height: 100%;
74 | }
75 | .comments_section {
76 | margin-top: 1rem;
77 | }
78 |
79 | /* PostList */
80 | .text-center {
81 | margin-top: 150px;
82 | }
83 | @media (max-width: 1175px) {
84 | .post {
85 | width: 84%;
86 | }
87 | }
88 | @media (max-width: 1030px) {
89 | .post {
90 | width: 100%;
91 | }
92 | }
93 | @media (max-width: 960px) {
94 | .post {
95 | width: 93%;
96 | }
97 | }
98 |
99 | @media (max-width: 885px) {
100 | .post {
101 | width: 100%;
102 | }
103 | }
104 |
105 | @media (max-width: 620px) {
106 | .post {
107 | width: 100%;
108 | }
109 | }
110 | @media (max-width: 400px) {
111 | .post {
112 | width: 100%;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/client/src/components/Posts/CreatPost/postbody.module.css:
--------------------------------------------------------------------------------
1 | .post_body {
2 | width: 100%;
3 | background:var(--bg-primary);
4 | border-radius: 10px;
5 | box-shadow: 0 1px 2px var(--shadow-1);
6 | cursor: pointer;
7 | }
8 | .post_body_header {
9 | display: flex;
10 | align-items: center;
11 | gap: 8px;
12 | padding: 10px 17px 5px 15px;
13 | }
14 |
15 | .post_body_image {
16 | width: 40px;
17 | height: 40px;
18 | border-radius: 50%;
19 | object-fit: cover;
20 | }
21 | .open_post {
22 | background: var(--bg-secondary);
23 | color: var(--color-third);
24 | height: 41px;
25 | flex: 1;
26 | border-radius: 50px;
27 | font-size: 17px;
28 | line-height: 21px;
29 | display: flex;
30 | align-items: center;
31 | padding-left: 10px;
32 | -webkit-line-clamp: 1;
33 | -webkit-box-orient: horizontal;
34 | overflow: hidden;
35 | line-height: 2rem;
36 | }
37 | .post_body_footer {
38 | padding: 0 10px 8px 10px;
39 | display: grid;
40 | grid-template-columns: repeat(3, 1fr);
41 | }
42 | .splitter {
43 | height: 1px;
44 | width: 95%;
45 | background: var(--divider);
46 | margin: 10px 10px;
47 | }
48 | .actions_icon {
49 | display: flex;
50 | align-items: center;
51 | justify-content: center;
52 | gap: 8px;
53 | font-weight: 600;
54 | padding: 7px;
55 | color: var(--color-third);
56 | border-radius: 10px;
57 | font-size: 14px;
58 | }
59 |
60 | @media (max-width: 1175px) {
61 | .create_post {
62 | width: 84%;
63 | }
64 | }
65 | @media (max-width: 1030px) {
66 | .create_post {
67 | width: 100%;
68 | }
69 | }
70 | @media (max-width: 960px) {
71 | .create_post {
72 | width: 93%;
73 | }
74 | }
75 |
76 | @media (max-width: 885px) {
77 | .create_post {
78 | width: 100%;
79 | }
80 | }
81 |
82 | @media (max-width: 620px) {
83 | .create_post {
84 | width: 100%;
85 | }
86 | .open_post {
87 | font-size: 15px;
88 | }
89 | }
90 | @media (max-width: 400px) {
91 | .create_post {
92 | width: 100%;
93 | }
94 | .open_post {
95 | font-size: 13px;
96 | }
97 | .actions_icon {
98 | gap: 4px;
99 | padding: 5px;
100 | font-size: 12px;
101 | }
102 | .actions_icon svg {
103 | width: 20px;
104 | height: 20px;
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/client/src/layouts/Header/HeaderMenu/DisplayAccessibility.jsx:
--------------------------------------------------------------------------------
1 | import { useDispatch, useSelector } from "react-redux";
2 | import styles from "./HeaderMenu.module.css";
3 | import IconStyle from "../../../styles/icons.module.css"
4 | import { changeTheme } from "../../../app/features/user/userSlice";
5 |
6 | export default function DisplayAccessibility({ setShow }) {
7 | const dispatch = useDispatch();
8 | const theme = useSelector((state) => state.user.theme);
9 | return (
10 |
11 |
12 |
{
15 | setShow(false);
16 | }}
17 | >
18 |
19 |
20 | Display & Accessibility
21 |
22 |
23 |
24 |
25 |
26 |
27 | Dark Mode
28 |
29 | Adjust the appearance of Ziwibook to reduce glare and give your eyes
30 | a break.
31 |
32 |
33 |
34 |
48 |
62 |
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/client/src/components/input/AuthInput/index.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useField, ErrorMessage } from "formik";
3 | import { Popper } from "../../index";
4 | import styles from "./style.module.css";
5 | import ErrorSVG from "../../../assets/svg/Error.svg";
6 | import CloseEye from "../../../assets/svg/closeEye.svg";
7 | import OpenEye from "../../../assets/svg/openEye.svg"
8 | import { useMediaQuery } from "react-responsive";
9 |
10 | function AuthInput({ placeholder, dir, type, disabled ,password,onClick, ...props }) {
11 | const [trigger, setTrigger] = React.useState(null);
12 | const [field, meta] = useField(props);
13 | const [show, setShow] = React.useState(false);
14 |
15 | const desktopView = useMediaQuery({
16 | query: "(min-width: 850px)",
17 | });
18 | return (
19 | <>
20 |
21 | {meta.touched && meta.error && show && (
22 |
27 |
28 |
29 | )}
30 |
setShow(true)}
39 | onBlurCapture={(e) => {
40 | setShow(false);
41 | }}
42 | />
43 |
45 | {(field.name === "password" )|| (field.name === "passwordConfirm") ? (
46 | <>
47 | {type === "password" ? (
48 |
49 |
50 | ) : (
51 |
52 |
53 | )}
54 | >
55 | ) : (
56 | ""
57 | )}
58 |
59 | {meta.touched && meta.error && (
60 |

61 | )}
62 |
63 |
64 | >
65 | );
66 | }
67 |
68 | export default AuthInput;
69 |
--------------------------------------------------------------------------------
/api/utils/socket.js:
--------------------------------------------------------------------------------
1 | const { Server } = require('socket.io');
2 |
3 | let io = new Server();
4 |
5 | let users = [];
6 | const addUser = (info, socketId) => {
7 | const checkUser = users.some((user) => user.info.id === info.id);
8 |
9 | if (!checkUser) {
10 | users.push({ info, socketId });
11 | }
12 | };
13 | const userRemove = (socketId) => {
14 | users = users.filter((user) => user.socketId !== socketId);
15 | };
16 |
17 | const findFriendBySoket = (socketId) => {
18 | return users.find((user) => user.socketId === socketId);
19 | };
20 |
21 | const findFrienddById = (userId) => {
22 | return users.find((user) => user.info.id === userId);
23 | };
24 |
25 | const userLogout = (userId) => {
26 | users = users.filter((user) => user.userId !== userId);
27 | };
28 |
29 | module.exports = {
30 | init: function (server, options) {
31 | io = new Server(server, options);
32 | io.on('connection', (socket) => {
33 | socket.emit('connected');
34 |
35 | socket.on('setup', ({ info }) => {
36 | const filterdUsers = users.map((user) => user.info);
37 |
38 | socket.emit('online_user', { type: 'connect', info: filterdUsers });
39 |
40 | addUser(info, socket.id);
41 | users.forEach((user) => {
42 | if (user.info.id == info.id) return;
43 | socket.to(user.socketId).emit('online_user', { type: 'add', info });
44 | });
45 | });
46 |
47 | socket.on('notification', ({ notification }) => {
48 | if (!notification?.recipient)
49 | return console.log('chat.users not defined');
50 | const onlineUser = findFrienddById(notification.recipient);
51 | if (onlineUser)
52 | socket
53 | .to(onlineUser.socketId)
54 | .emit('new_notification', { notification });
55 | });
56 |
57 | socket.on('disconnect', () => {
58 | const d_user = findFriendBySoket(socket.id);
59 |
60 | users.forEach((user) => {
61 | socket
62 | .to(user.socketId)
63 | .emit('online_user', { type: 'remove', info: d_user?.info });
64 | });
65 | userRemove(socket.id);
66 | });
67 | });
68 | return io;
69 | },
70 | getIO: function () {
71 | if (!io) {
72 | throw new Error("Can't get io instance before calling .init()");
73 | }
74 | return io;
75 | },
76 | };
77 |
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfileCover/OldCovers/OldCovers.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import useOnClickOutside from "../../../../hooks/useOnClickOutside";
3 | import classes from "./OldCovers.module.css";
4 | import IconStyle from "../../../../styles/icons.module.css";
5 | import {Card} from "../../../index";
6 |
7 | function OldCovers({ setShowOldCover, setImage, photosData, showOldCover }) {
8 | const oldCoversCardRef = React.useRef(null);
9 |
10 | useOnClickOutside(oldCoversCardRef, showOldCover, () => {
11 | setShowOldCover(false);
12 | });
13 |
14 | return (
15 |
16 |
17 |
18 | Update Cover Photo
19 |
setShowOldCover(false)}>
20 |
21 |
22 |
23 |
24 | {photosData?.profileCovers.length > 0 && (
25 | <>
26 |
Choose from old cover picture
27 |
28 | {photosData?.profileCovers.map((photo) => (
29 |

{
33 | setImage(photo.url);
34 | setShowOldCover(false);
35 | }}
36 | key={photo.id}
37 | />
38 | ))}
39 |
40 | >
41 | )}
42 | {photosData?.resources.length > 0 && (
43 | <>
44 |
Choose from your profile photos
45 |
46 | {photosData?.resources.map((photo) => (
47 |

{
51 | setImage(photo.url);
52 | setShowOldCover(false);
53 | }}
54 | key={photo.id}
55 | />
56 | ))}
57 |
58 | >
59 | )}
60 |
61 |
62 |
63 | );
64 | }
65 |
66 | export default OldCovers;
67 |
--------------------------------------------------------------------------------
/client/src/components/RegisterForm/DateSelector.jsx:
--------------------------------------------------------------------------------
1 | import * as React from "react";
2 | import { useMediaQuery } from "react-responsive";
3 | import classes from "./register.module.css";
4 | import { Field } from "formik";
5 | import {Popper} from "../index";
6 | import ErrorSVG from "../../assets/svg/Error.svg";
7 |
8 | function DateSelector({ birthDay, birthMonth, birthYear , dateError}) {
9 | const [trigger, setTrigger] = React.useState(null);
10 | const [show, setShow] = React.useState(false);
11 |
12 | const desktopView = useMediaQuery({
13 | query: "(min-width: 850px)",
14 | });
15 | const yearTemp = new Date().getFullYear();
16 | const years = Array.from(new Array(108), (val, index) => yearTemp - index);
17 | const months = Array.from(new Array(12), (val, index) => 1 + index);
18 | const getDays = () => {
19 | return new Date(birthYear, birthMonth, 0).getDate();
20 | };
21 |
22 | const days = Array.from(new Array(getDays()), (val, index) => 1 + index);
23 |
24 | return (
25 | setShow(true)}
29 | onMouseLeave={() => setShow(false)}
30 | >
31 |
Birthday
32 | {dateError &&
33 |

}
35 |
36 |
37 |
42 | {days.map((day, i) => (
43 |
46 | ))}
47 |
48 |
49 | {months.map((month, i) => (
50 |
53 | ))}
54 |
55 |
56 | {years.map((year, i) => (
57 |
60 | ))}
61 |
62 | {dateError && show && (
63 |
68 | {dateError}
69 |
70 | )}
71 |
72 |
73 | );
74 | }
75 |
76 | export default DateSelector;
77 |
--------------------------------------------------------------------------------
/client/src/components/RegisterForm/register.module.css:
--------------------------------------------------------------------------------
1 | .signup_card {
2 | position: absolute;
3 | left: 50%;
4 | top: 50%;
5 | transform: translate(-50%, -50%);
6 | width: 350px;
7 | padding: 20px;
8 | }
9 |
10 | .signup_header {
11 | border-bottom: 1px solid var(--bg-third);
12 | display: flex;
13 | flex-direction: column;
14 | gap: 2px;
15 | padding-bottom: 10px;
16 | position: relative;
17 | margin-bottom: 10px;
18 | }
19 | .signup_header i {
20 | cursor: pointer;
21 | position: absolute;
22 | right: 0;
23 | }
24 | .signup_header_title {
25 | font-size: 32px;
26 | font-weight: 600;
27 | color: var(--color-secondary);
28 | }
29 | .signup_header_title1 {
30 | color:var(--color-third);
31 | font-size: 15px;
32 | }
33 | .signup_form {
34 | display: flex;
35 | flex-direction: column;
36 | gap: 8px;
37 | }
38 | .LINE {
39 | display: flex;
40 | gap: 15px;
41 | }
42 |
43 | .login_link {
44 | cursor: pointer;
45 | font-size: 14px;
46 | font-weight: 700;
47 | color:var(--color-primary);
48 | text-decoration: none;
49 | }
50 | .login_link:hover {
51 | text-decoration: underline;
52 | color:var(--color-primary);
53 | }
54 | .register {
55 | justify-content: center;
56 | align-items: center;
57 | display: flex;
58 | margin-top: 10px;
59 | }
60 | .error_text {
61 | color: var(--color-error);
62 | font-weight: 700;
63 | text-align: center;
64 | margin: 0 auto 10px;
65 | }
66 | .col {
67 | margin-bottom: 10px;
68 | }
69 | .colHeader {
70 | align-items: center;
71 | color:var(--color-third);
72 | display: flex;
73 | font-size: 13px;
74 | gap: 4px;
75 | }
76 | .select_grid {
77 | grid-gap: 10px;
78 | display: grid;
79 | gap: 10px;
80 | grid-template-columns: repeat(3, 1fr);
81 | height: 35px;
82 | margin-top: 5px;
83 | width: 100%;
84 | }
85 | .gender {
86 | grid-template-columns: repeat(2, 1fr);
87 | }
88 | .select_grid select {
89 | border-radius: 5px;
90 | color:var(--color-secondary);
91 | cursor: pointer;
92 | font-size: 16px;
93 | width: 100%;
94 | padding: 0 10px;
95 | background:var(--bg-primary);
96 | }
97 | .select_grid label {
98 | border-radius: 5px;
99 | align-items: center;
100 | border: 1px solid var(--color-third);
101 | display: flex;
102 | justify-content: space-between;
103 | padding: 0 15px;
104 | accent-color:var(--color-primary);
105 | }
106 | .ERROR {
107 | border-color: var(--color-error) !important;
108 | }
109 |
110 | @media (min-width: 540px) {
111 | .signup_card {
112 | width: 400px;
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/client/src/components/input/CustomInput/index.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import "./index.css";
3 | import CloseEye from "../../../assets/svg/closeEye.svg";
4 | import OpenEye from "../../../assets/svg/openEye.svg"
5 | const CustomInput = ({
6 | onFocus,
7 | onBlur,
8 | name,
9 | label,
10 | type,
11 | onChange,
12 | error,
13 | placeholder,
14 | value,
15 | defaultValue,
16 | accept,
17 | hidden,
18 | float,
19 | onClick,
20 | className,
21 | autoFocus,
22 | innerRef,
23 | }) => {
24 | return (
25 |
26 |
27 | {!float &&
}
28 | {type === "textarea" ? (
29 |
45 | ) : (
46 |
68 | )}
69 | {name === "password" ? (
70 |
71 | {type === "password" ? (
72 |
73 | ) : (
74 |
75 | )}
76 |
77 | ) : (
78 | ""
79 | )}
80 |
81 | {float &&
}
82 | {error && (
83 |
84 | {error}
85 |
86 | )}
87 |
88 |
89 | );
90 | };
91 |
92 | export default CustomInput;
93 |
--------------------------------------------------------------------------------
/client/src/components/Profile/ProfilePhoto/ProfilePhoto.module.css:
--------------------------------------------------------------------------------
1 | .container {
2 | display: flex;
3 | align-items: center;
4 | justify-content: center;
5 | }
6 | .profile_photo_container {
7 | width: 100%;
8 | max-width: 500px;
9 | min-height: 400px;
10 | padding: 0;
11 | margin: 8px;
12 | }
13 | .header_exit {
14 | border-bottom: 1px solid var(--bg-third);
15 | display: flex;
16 | align-items: center;
17 | justify-content: center;
18 | font-weight: 700;
19 | position: relative;
20 | padding: 16px 0;
21 | }
22 | .exit {
23 | cursor: pointer;
24 | position: absolute;
25 | right: 0;
26 | }
27 | .profile_photo_content {
28 | display: flex;
29 | flex-direction: column;
30 | gap: 16px;
31 | }
32 | .crooper {
33 | position: relative;
34 | display: flex;
35 | justify-content: center;
36 | height: 300px;
37 | width: 100%;
38 | }
39 |
40 | .crooper img {
41 | width: 100%;
42 | object-fit: contain;
43 | }
44 |
45 | .slider {
46 | width: 100%;
47 | display: flex;
48 | align-items: center;
49 | justify-content: center;
50 | margin: 25px 0;
51 | gap: 10px;
52 | }
53 | .slider input[type="range"] {
54 | -webkit-appearance: none;
55 | -moz-appearance: none;
56 | appearance: none;
57 | width: 55%;
58 | height: 4px;
59 | background: var(--color-primary);
60 | cursor: pointer;
61 | }
62 |
63 | .slider input[type="range"]::-webkit-slider-thumb {
64 | -webkit-appearance: none;
65 | width: 20px;
66 | height: 20px;
67 | outline: none;
68 | border-radius: 50%;
69 | background: var(--color-primary);
70 | }
71 | .slider_circle {
72 | width: 36px;
73 | height: 36px;
74 | display: grid;
75 | place-items: center;
76 | border-radius: 50%;
77 | cursor: pointer;
78 | }
79 | .buttons {
80 | display: flex;
81 | gap: 10px;
82 | padding: 0 20px 20px 20px;
83 | }
84 | .upload_btn {
85 | border: none;
86 | outline: none;
87 | background: var(--bg-secondary);
88 | padding: 8.5px 12px;
89 | font-family: inherit;
90 | font-weight: 600;
91 | font-size: 14px;
92 | color: var(--color-secondary);
93 | display: flex;
94 | align-items: center;
95 | justify-content: center;
96 | gap: 6px;
97 | cursor: pointer;
98 | }
99 | .upload_btn:hover {
100 | background: var(--bg-third);
101 | }
102 | .old {
103 | padding: 0 8px;
104 | }
105 | .old_photos {
106 | display: flex;
107 | flex-wrap: wrap;
108 | gap: 5px;
109 | justify-content: center;
110 | margin: 10px 0;
111 | max-height: 150px;
112 | overflow: auto;
113 | }
114 | .old_photos img {
115 | width: 100px;
116 | height: 100px;
117 | object-fit: cover;
118 | border-radius: 10px;
119 | cursor: pointer;
120 | }
121 |
--------------------------------------------------------------------------------
/api/utils/notification.js:
--------------------------------------------------------------------------------
1 | const Notif = require("../models/notification");
2 |
3 | module.exports = class Notification {
4 | constructor({ recipient, sender, postId, postReact }) {
5 | this.sender = sender;
6 | this.recipient = recipient;
7 | this.postId = postId;
8 | this.postReact = postReact;
9 | }
10 |
11 | async createNotifcation({ content, type, path }) {
12 | let newNotif = null;
13 |
14 | if (this.recipient._id.toString().includes(this.sender?._id.toString())) return;
15 |
16 | newNotif = await Notif.create({
17 | sender: this.sender?._id,
18 | recipient: this.recipient?._id,
19 | type,
20 | url: path,
21 | content,
22 | });
23 | await newNotif.save();
24 | return newNotif;
25 | }
26 |
27 | async PostLike() {
28 | const path = `/${this.recipient?.username}/posts/${this.postId}`;
29 | const noti = await this.createNotifcation({
30 | content: `${this.sender?.firstName} reacted by like on your post`,
31 | type: "react",
32 | path: path,
33 | });
34 | return noti;
35 | }
36 |
37 | async PostComment() {
38 | const path = `/${this.recipient?.username}/posts/${this.postId}`;
39 | const noti = await this.createNotifcation({
40 | content: `${this.sender?.firstName} commented ${this.postReact} on your post`,
41 | type: "comment",
42 | path: path,
43 | });
44 | return noti;
45 | }
46 |
47 | async CommentLike() {
48 | const path = `/${this.recipient?.username}/posts/${this.postId}`;
49 | const noti = await this.createNotifcation({
50 | content: `${this.sender?.firstName} like your comment`,
51 | type: "react",
52 | path: path,
53 | });
54 | return noti;
55 | }
56 |
57 | async CommentReplie() {
58 | const postLink = `${process.env.FRONTEND_URL}/${this.recipient?.username}/posts/${this.postId}`;
59 | const path = `/${this.recipient?.username}/posts/${this.postId}`;
60 |
61 | const noti = await this.createNotifcation({
62 | content: `${this.sender?.firstName} replied ${this.postReact} on your comment`,
63 | click: postLink,
64 | type: "comment",
65 | path: path,
66 | });
67 | return noti;
68 | }
69 |
70 | async FriendRequest() {
71 | const path = `/profile/${this.sender?.username}`;
72 | const noti = await this.createNotifcation({
73 | content: `${this.sender?.firstName} Sent you a friend request`,
74 | type: "friend",
75 | path: path,
76 | });
77 | return noti;
78 | }
79 |
80 | async AcceptFriendRequest() {
81 | const path = `/profile/${this.sender?.username}`;
82 | const noti = await this.createNotifcation({
83 | content: `${this.sender?.firstName} Accept your friend request`,
84 | type: "friend",
85 | path: path,
86 | });
87 | return noti;
88 | }
89 | };
90 |
--------------------------------------------------------------------------------
/client/src/components/Posts/AddEditPost/Post.module.css:
--------------------------------------------------------------------------------
1 | .post_container {
2 | display: flex;
3 | flex-direction: column;
4 | max-width: 500px;
5 | }
6 | .post_head {
7 | display: flex;
8 | justify-content: center;
9 | align-items: center;
10 | }
11 | .post_head span {
12 | font-size: 18px;
13 | font-weight: 700;
14 | }
15 | .post_auther {
16 | display: flex;
17 | padding: 10px 0px;
18 | gap: 5px;
19 | }
20 | .auther_name {
21 | font-size: 14px;
22 | font-weight: 600;
23 | }
24 | .auther_photo {
25 | height: 40px;
26 | width: 40px;
27 | border-radius: 50%;
28 | }
29 | .post_content {
30 | max-height: 410px;
31 | overflow-y: scroll;
32 | }
33 | .textarea {
34 | width: 100%;
35 | font-size: 24px;
36 | background: transparent;
37 | resize: none;
38 | border: none;
39 | outline: none;
40 | font-family: inherit;
41 | /* color: var(--color-secondary); */
42 | }
43 |
44 | .post_footer {
45 | display: flex;
46 | flex-direction: column;
47 | }
48 | .post_action {
49 | display: flex;
50 | justify-content: space-between;
51 | align-items: center;
52 | padding: 8px;
53 | border: 1px solid var(--color-primary);
54 | border-radius: 10px;
55 | height: 57px;
56 | }
57 | .footer_text {
58 | font-size: 14px;
59 | font-weight: 600;
60 | padding: 8px;
61 | }
62 | .post_image_container {
63 | display: flex;
64 | flex-direction: column;
65 | gap: 10px;
66 | border: 1px solid var(--color-primary);
67 | border-radius: 10px;
68 | margin: 15px;
69 | padding: 10px;
70 | margin-right: 10px;
71 | }
72 | .add_image {
73 | background: var(--bg-secondary);
74 | border-radius: 10px;
75 | position: relative;
76 | height: 200px;
77 | display: flex;
78 | justify-content: center;
79 | align-items: center;
80 | }
81 | .exit {
82 | position: absolute;
83 | top: 5px;
84 | right: 0;
85 | }
86 | .small_white_circle {
87 | width: 30px;
88 | height: 30px;
89 | border-radius: 50%;
90 | border: 1px solid var(--bg-third);
91 | background: var(--bg-primary);
92 | margin-right: 8px;
93 | display: flex;
94 | align-items: center;
95 | justify-content: center;
96 | cursor: pointer;
97 | }
98 | .post_btn {
99 | display: flex;
100 | gap: 5px;
101 | align-items: center;
102 | padding: 8px 12px;
103 | background: var(--bg-primary);
104 | color: var(--color-primary);
105 | border: none;
106 | border-radius: 10px;
107 | cursor: pointer;
108 | font-size: 14px;
109 | font-weight: 500;
110 | }
111 | .imge {
112 | height: auto;
113 | width: 100%;
114 | }
115 | .img {
116 | height: inherit;
117 | width: inherit;
118 | max-height: inherit;
119 | border-radius: 10px;
120 | object-fit: cover;
121 | }
122 |
123 | .post_footer {
124 | margin: 0 16px;
125 | display: flex;
126 | flex-direction: column;
127 | gap: 16px;
128 | margin-bottom: 16px;
129 | padding-top: 10px;
130 | }
131 |
--------------------------------------------------------------------------------
/client/src/layouts/Header/Header.module.css:
--------------------------------------------------------------------------------
1 | .header {
2 | background-color: var(--bg-primary);
3 | box-shadow: 0 2px 4px var(--shadow-1);
4 | height: 56px;
5 | width: 100%;
6 | top: 0px;
7 | left: 0px;
8 | position: fixed !important;
9 | z-index: 100;
10 | display: grid;
11 | grid-template-columns: repeat(3, 1fr);
12 | }
13 |
14 | :global(.dark) .navbar_right svg,
15 | :global(.dark) .navbar_left svg {
16 | fill: rgb(174, 174, 174);
17 | }
18 |
19 | :global(.dark) .navbar_middle svg {
20 | fill: rgb(174, 174, 174);
21 | }
22 |
23 | .navbar_left {
24 | display: flex;
25 | align-items: center;
26 | transform: translateX(-39px);
27 | }
28 | .logo {
29 | width: 200px;
30 | cursor: pointer;
31 | }
32 |
33 | .navbar_search {
34 | margin-left: -10px;
35 | }
36 |
37 | .navbar_middle {
38 | display: flex;
39 | align-items: center;
40 | gap: 14px;
41 | }
42 | .navbar_middle_icon {
43 | position: relative;
44 | display: flex;
45 | align-items: center;
46 | justify-content: center;
47 | border-radius: 10px;
48 | cursor: pointer;
49 | width: 80px;
50 | height: 50px;
51 | }
52 |
53 | .active {
54 | border-bottom: 3px solid var(--color-primary);
55 | border-radius: 0;
56 | height: 56px;
57 | }
58 | .active_icon {
59 | display: none;
60 | }
61 | .active .active_icon {
62 | display: block;
63 | }
64 | .active .notActive_icon {
65 | display: none;
66 | }
67 | .navbar_right {
68 | display: flex;
69 | position: absolute;
70 | gap: 17px;
71 | right: 8px;
72 | top: 50%;
73 | transform: translateY(-50%);
74 | }
75 | .circle_icons {
76 | height: 40px;
77 | width: 40px;
78 | border-radius: 50%;
79 | display: flex;
80 | align-items: center;
81 | justify-content: center;
82 | background: var(--bg-forth);
83 | cursor: pointer;
84 | }
85 | .navbar_profile {
86 | cursor: pointer;
87 | width: 47px;
88 | height: 47px;
89 | border-radius: 50%;
90 | }
91 | .notification {
92 | position: absolute;
93 | top: 3px;
94 | right: 45px;
95 | background: var(--color-primary);
96 | border-radius: 50px;
97 | padding: 1px 7px;
98 | font-size: 15px;
99 | font-weight: 600;
100 | color:var(--bg-primary);
101 | }
102 |
103 | @media (max-width: 760px) {
104 | .navbar_middle_icon:nth-child(3) {
105 | display: none;
106 | }
107 | .navbar_middle_icon:nth-child(4) {
108 | display: none;
109 | }
110 | .navbar_middle_icon {
111 | width: 35px;
112 | }
113 | }
114 |
115 | @media (max-width: 550px) {
116 | .navbar_middle {
117 | transform: translateX(-20%);
118 | }
119 | .navbar_middle {
120 | gap: 12px;
121 | }
122 | .navbar_right {
123 | gap: 10px;
124 | }
125 | }
126 |
127 | @media (max-width: 400px) {
128 | .navbar_middle {
129 | transform: translateX(-48%);
130 | }
131 | .navbar_middle {
132 | gap: 5px;
133 | }
134 | .navbar_right {
135 | gap: 8px;
136 | }
137 | }
138 |
--------------------------------------------------------------------------------
/client/src/assets/svg/ZIWIBook.svg:
--------------------------------------------------------------------------------
1 |
2 |
4 |
48 |
--------------------------------------------------------------------------------
/client/src/utils/getCroppedImg.jsx:
--------------------------------------------------------------------------------
1 | export const createImage = (url) =>
2 | new Promise((resolve, reject) => {
3 | const image = new Image();
4 | image.addEventListener("load", () => resolve(image));
5 | image.addEventListener("error", (error) => reject(error));
6 | image.setAttribute("crossOrigin", "anonymous"); // needed to avoid cross-origin issues on CodeSandbox
7 | image.src = url;
8 | });
9 |
10 | export function getRadianAngle(degreeValue) {
11 | return (degreeValue * Math.PI) / 180;
12 | }
13 |
14 | /**
15 | * Returns the new bounding area of a rotated rectangle.
16 | */
17 | export function rotateSize(width, height, rotation) {
18 | const rotRad = getRadianAngle(rotation);
19 |
20 | return {
21 | width:
22 | Math.abs(Math.cos(rotRad) * width) + Math.abs(Math.sin(rotRad) * height),
23 | height:
24 | Math.abs(Math.sin(rotRad) * width) + Math.abs(Math.cos(rotRad) * height),
25 | };
26 | }
27 |
28 | /**
29 | * This function was adapted from the one in the ReadMe of https://github.com/DominicTobias/react-image-crop
30 | */
31 | export default async function getCroppedImg(
32 | imageSrc,
33 | pixelCrop,
34 | rotation = 0,
35 | flip = { horizontal: false, vertical: false }
36 | ) {
37 | const image = await createImage(imageSrc);
38 | const canvas = document.createElement("canvas");
39 | const ctx = canvas.getContext("2d");
40 |
41 | if (!ctx) {
42 | return null;
43 | }
44 |
45 | const rotRad = getRadianAngle(rotation);
46 |
47 | // calculate bounding box of the rotated image
48 | const { width: bBoxWidth, height: bBoxHeight } = rotateSize(
49 | image.width,
50 | image.height,
51 | rotation
52 | );
53 |
54 | // set canvas size to match the bounding box
55 | canvas.width = bBoxWidth;
56 | canvas.height = bBoxHeight;
57 |
58 | // translate canvas context to a central location to allow rotating and flipping around the center
59 | ctx.translate(bBoxWidth / 2, bBoxHeight / 2);
60 | ctx.rotate(rotRad);
61 | ctx.scale(flip.horizontal ? -1 : 1, flip.vertical ? -1 : 1);
62 | ctx.translate(-image.width / 2, -image.height / 2);
63 |
64 | // draw rotated image
65 | ctx.drawImage(image, 0, 0);
66 |
67 | // croppedAreaPixels values are bounding box relative
68 | // extract the cropped image using these values
69 | const data = ctx.getImageData(
70 | pixelCrop.x,
71 | pixelCrop.y,
72 | pixelCrop.width,
73 | pixelCrop.height
74 | );
75 |
76 | // set canvas width to final desired crop size - this will clear existing context
77 | canvas.width = pixelCrop.width;
78 | canvas.height = pixelCrop.height;
79 |
80 | // paste generated rotate image at the top left corner
81 | ctx.putImageData(data, 0, 0);
82 |
83 | // As Base64 string
84 | // return canvas.toDataURL('image/jpeg');
85 |
86 | // As a blob
87 | return new Promise((resolve, reject) => {
88 | canvas.toBlob((file) => {
89 | resolve(URL.createObjectURL(file));
90 | }, "image/jpeg");
91 | });
92 | }
93 |
--------------------------------------------------------------------------------
/client/src/assets/svg/404Error.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/client/src/layouts/Header/HeaderMenu/HeaderMenu.jsx:
--------------------------------------------------------------------------------
1 | import { Link} from "react-router-dom";
2 | import style from "./HeaderMenu.module.css";
3 | import styleIcons from "../../../styles/icons.module.css";
4 | import { useState } from "react";
5 | import { useLogoutMutation } from "../../../app/features/auth/authApi";
6 | import DisplayAccessibility from "./DisplayAccessibility";
7 |
8 | function HeaderMenu({ user, setShowHeaderMenu }) {
9 | const [Logout] = useLogoutMutation();
10 | const [show ,setShow]=useState(false)
11 | const LogoutHandler = async () => {
12 | Logout();
13 | };
14 | return (
15 |
16 |
17 |
setShowHeaderMenu(false)}
21 | >
22 |

23 |
24 | {user?.firstName} {user?.lastName}
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
Settings & privacy
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
Help & support
42 |
43 |
44 |
45 |
46 |
{
48 | setShow(true);
49 | }}>
50 |
51 |
52 |
53 |
Display & Accessibility
54 |
55 |
56 |
57 |
58 |
66 |
70 |
71 |
72 |
73 |
Logout
74 |
75 |
76 | {show && (
77 |
78 | )}
79 |
80 | );
81 | }
82 |
83 | export default HeaderMenu;
84 |
--------------------------------------------------------------------------------
/client/src/pages/Profile/profile.module.css:
--------------------------------------------------------------------------------
1 | .profile_container {
2 | /* padding: 0px 15%; */
3 | margin: 56px 0 30px;
4 | }
5 |
6 | .head {
7 | background: var(--bg-primary);
8 | width: 100%;
9 | display: flex;
10 | justify-content: center;
11 | }
12 | .head_container {
13 | max-width: 1100px;
14 | width: 100%;
15 | justify-items: center;
16 | }
17 | .top_head {
18 | display: flex;
19 | flex-direction: column;
20 | }
21 | .top_head_content {
22 | display: flex;
23 | flex-direction: column;
24 | align-items: flex-start;
25 | }
26 |
27 | .photo_container {
28 | height: 80px;
29 | }
30 | .photo {
31 | position: relative;
32 | top: -84px;
33 | }
34 |
35 | .user_photo {
36 | border-radius: 50%;
37 | width: 168px;
38 | height: 168px;
39 | border: 4px solid var(--bg-primary);
40 | }
41 | .add_photo {
42 | position: absolute;
43 | right: 0;
44 | bottom: 10px;
45 | }
46 | .profile_info {
47 | display: flex;
48 | flex-direction: column;
49 | align-items: center;
50 | justify-content: center;
51 | position: relative;
52 | gap: 10px;
53 | }
54 | .user_name {
55 | overflow-wrap: break-word;
56 | text-align: flex;
57 | line-height: 38px;
58 | width: max-content;
59 | }
60 | .friends {
61 | font-size: 16px;
62 | font-weight: 600;
63 | color: var(--color-third);
64 | /* align-self: flex-start; */
65 | }
66 |
67 | .profile_btns {
68 | display: flex;
69 | gap: 10px;
70 | align-items: center;
71 | justify-content: center;
72 | width: 100%;
73 | padding: 16px;
74 | white-space: nowrap;
75 | position: relative;
76 | }
77 |
78 | .footer {
79 | display: flex;
80 | align-items: center;
81 | justify-content: center;
82 | margin-bottom: 5px;
83 | }
84 | .footer_container {
85 | max-width: 1100px;
86 | width: 100%;
87 | justify-items: center;
88 | display: grid;
89 | grid-template-columns: 1fr;
90 | gap: 10px;
91 | padding: 6px;
92 | }
93 |
94 | .posts {
95 | display: flex;
96 | flex-direction: column;
97 | padding-top: 0.5rem;
98 | max-width: 680px;
99 | width: 100%;
100 | }
101 | .details {
102 | width: 100%;
103 | padding-top: 0.5rem;
104 | display: flex;
105 | flex-direction: column;
106 | gap: 10px;
107 | max-width: 680px;
108 | }
109 | .details_con {
110 | display: flex;
111 | flex-direction: column;
112 | position: static;
113 | }
114 | .line {
115 | width: 100%;
116 | height: 1px;
117 | background: var(--divider);
118 | margin: 5px 0;
119 | }
120 | @media (min-width: 495px) {
121 | .profile_btns {
122 | width: 50%;
123 | }
124 | }
125 | @media (min-width: 900px) {
126 | .details_con {
127 | position: sticky;
128 | }
129 | .footer_container {
130 | grid-template-columns: 1fr 1.6fr;
131 | }
132 |
133 | .top_head_content {
134 | flex-direction: row;
135 | padding: 10px 16px;
136 | justify-content: flex-start;
137 | column-gap: 25px;
138 | margin-bottom: 20px;
139 | }
140 | .profile_btns {
141 | width: 30%;
142 | margin-left: auto;
143 | }
144 | .profile_info {
145 | width: 37%;
146 | align-items: flex-start;
147 | }
148 | .photo {
149 | top: -60px;
150 | }
151 |
152 | .user_name {
153 | text-align: left;
154 | }
155 | }
156 |
--------------------------------------------------------------------------------
/client/src/app/features/post/postApi.jsx:
--------------------------------------------------------------------------------
1 | import { createEntityAdapter, createSelector } from "@reduxjs/toolkit";
2 | import { apiSlice } from "../../api/apiSlice";
3 |
4 |
5 | const postsAdapter = createEntityAdapter({
6 | selectId: (post) => post._id,
7 | sortComparer: (a, b) => b.createdAt.localeCompare(a.createdAt),
8 | });
9 |
10 | const initialState = postsAdapter.getInitialState();
11 |
12 | export const extendedApiSlice = apiSlice.injectEndpoints({
13 | endpoints: (builder) => ({
14 | fetchPosts: builder.query({
15 | query: () => "/api/posts/",
16 | transformResponse: (responseData) => {
17 | return postsAdapter.setAll(initialState, responseData);
18 | },
19 | providesTags: (result, error, arg) => [
20 | { type: "Post", id: "LIST" },
21 | ...result.ids.map((id) => ({ type: "Post", id })),
22 | ],
23 | }),
24 |
25 | fetchPostsByUser: builder.query({
26 | query: (usernameID) => `/api/posts/${usernameID}/posts`,
27 | transformResponse: (responseData) => {
28 | return postsAdapter.setAll(initialState, responseData);
29 | },
30 | providesTags: (result, error, arg) => {
31 | return [...result?.ids.map((id) => ({ type: "Post", id }))];
32 | },
33 | }),
34 |
35 | fetchPost: builder.query({
36 | query: (id) => `/api/posts/getOnePost/${id}`,
37 | transformResponse: (responseData) => {
38 | return postsAdapter.setOne(initialState, responseData);
39 | },
40 | providesTags: (result, error, id) => [{ type: "Post", id }],
41 | }
42 | ),
43 |
44 | addNewPost: builder.mutation({
45 | query: (form) => ({
46 | url: "/api/posts/addPost",
47 | method: "POST",
48 | body: form,
49 | }),
50 | invalidatesTags: (result, error, arg) => [{ type: "Post", id: arg.id }],
51 | }),
52 |
53 | updatePost: builder.mutation({
54 | query: ({ id, dataForm }) => ({
55 | url: `/api/posts/updatePost/${id}`,
56 | method: "PATCH",
57 | body: dataForm,
58 | }),
59 | invalidatesTags: (result, error, arg) => [{ type: "Post", id: arg.id }],
60 | }),
61 |
62 | deletePost: builder.mutation({
63 | query: (id) => ({
64 | url: `/api/posts/deletePost/${id}`,
65 | method: "DELETE",
66 | }),
67 | invalidatesTags: (result, error, arg) => [{ type: "Post", id: arg.id }],
68 | }),
69 | }),
70 | });
71 |
72 | export const {
73 | useFetchPostsQuery,
74 | useFetchPostsByUserQuery,
75 | useFetchPostQuery,
76 | useAddNewPostMutation,
77 | useUpdatePostMutation,
78 | useDeletePostMutation,
79 | } = extendedApiSlice;
80 |
81 | // returns the query result object
82 | export const selectPostsResult = extendedApiSlice.endpoints.fetchPosts.select();
83 |
84 | // Creates memoized selector
85 | const selectPostsData = createSelector(
86 | selectPostsResult,
87 | (postsResult) => postsResult.data // normalized state object with ids & entities
88 | );
89 |
90 | //getSelectors creates these selectors and we rename them with aliases using destructuring
91 | export const {
92 | selectAll: selectAllPosts,
93 | selectById: selectPostById,
94 | selectIds: selectPostIds,
95 | // Pass in a selector that returns the posts slice of state
96 | } = postsAdapter.getSelectors(
97 | (state) => selectPostsData(state) ?? initialState
98 | );
99 |
--------------------------------------------------------------------------------
/api/controllers/auth.js:
--------------------------------------------------------------------------------
1 | const bcrypt = require("bcryptjs");
2 | const jwt = require("jsonwebtoken");
3 | const User = require("../models/user");
4 | const SignupValidation = require("../validator/SignupValidation");
5 | const SigninValidation = require("../validator/SigninValidation");
6 |
7 | module.exports = {
8 | // --------------------------------------- //signup method to add a new user//--------------------------- //
9 | signup: async (req, res) => {
10 | const { email, password } = req.body;
11 |
12 | const { errors, isValid } = SignupValidation(req.body);
13 | try {
14 | if (!isValid) {
15 | res.status(404).json(errors);
16 | } else {
17 | await User.findOne({ email }).then(async (exist) => {
18 | if (exist) {
19 | errors.email = "Email already in use";
20 | res.status(404).json(errors);
21 | } else {
22 | const hashedpassword = bcrypt.hashSync(password, 8);
23 | req.body.password = hashedpassword;
24 | await User.create(req.body);
25 | res.status(201).json({ message: "user added with success" });
26 | }
27 | });
28 | }
29 | } catch (error) {
30 | res.status(404).json({ message: error.message });
31 | }
32 | },
33 | // --------------------------------------- //signin method to add a new user//--------------------------- //
34 |
35 | signin: async (req, res) => {
36 | const { email, password } = req.body;
37 | const { errors, isValid } = SigninValidation(req.body);
38 |
39 | try {
40 | if (!isValid) {
41 | return res.status(404).json(errors);
42 | } else {
43 | await User.findOne({ email }).then(async (user) => {
44 | if (!user) {
45 | errors.email =
46 | "Email does not exist ! please Enter the right Email or You can make account";
47 | return res.status(404).json(errors);
48 | }
49 | // Compare sent in password with found user hashed password
50 | const passwordMatch = bcrypt.compareSync(password, user.password);
51 | if (!passwordMatch) {
52 | errors.password = "Wrong Password";
53 | return res.status(404).json(errors);
54 | } else {
55 | // generate a token and send to client
56 | const exp = Date.now() + 1000 * 60 * 60 * 24 * 30;
57 | const token = jwt.sign(
58 | { "sub": user._id },
59 | process.env.ACCESS_TOKEN_SECRET,
60 | { expiresIn: "7d" }
61 |
62 | );
63 | // Authorization
64 | const options = {
65 | expires: new Date(exp),
66 | httpOnly: false, //accessible only by web server
67 | secure: true, //https
68 | sameSite: "None", //cross-site cookie
69 | };
70 | res.cookie("Authorization", token, options);
71 | res.status(201).json({
72 | token,
73 | user,
74 | });
75 | }
76 | });
77 | }
78 | } catch (error) {
79 | res.status(404).json({ message: error.message });
80 | }
81 | },
82 | // --------------------------------------- // logout method //--------------------------- //
83 |
84 | logout: async (req, res) => {
85 | try {
86 | res.clearCookie("Authorization");
87 | res.status(200).json(" You are logged out , to the next login !");
88 | } catch (error) {
89 | res.status(404).json({ message: error.message });
90 | }
91 | },
92 | };
93 |
--------------------------------------------------------------------------------
/client/src/layouts/Header/HeaderMenu/HeaderMenu.module.css:
--------------------------------------------------------------------------------
1 | /* HeaderMenu */
2 | .header_menu {
3 | padding: 0 0.3rem;
4 | position: absolute;
5 | top: 100%;
6 | right: 0;
7 | width: 360px;
8 | border-radius: 10px;
9 | background: var(--bg-primary);
10 | box-shadow: 2px 2px 2px var(--shadow-1);
11 | user-select: none;
12 | padding: 10px;
13 | border: 1px solid var(--color-primary);
14 | }
15 | :global(.dark) .header_menu i {
16 | filter: invert(100%);
17 | }
18 | .head {
19 | display: flex;
20 | align-items: center;
21 | border-radius: 10px;
22 | text-decoration: none;
23 | gap: 5px;
24 | }
25 | .menu_image {
26 | width: 40px;
27 | height: 40px;
28 | border-radius: 50%;
29 | }
30 |
31 | .menu_user {
32 | font-weight: 600;
33 | font-size: 15px;
34 | }
35 |
36 | .menu_splitter {
37 | width: 100%;
38 | height: 1px;
39 | background: var(--bg-third);
40 | margin-top: 5px;
41 | }
42 | .menu_action {
43 | position: relative;
44 | display: flex;
45 | align-items: center;
46 | margin-top: 5px;
47 | padding: 7px;
48 | font-weight: 600;
49 | font-size: 14px;
50 | border-radius: 10px;
51 | }
52 | .Arrow {
53 | right: 10px;
54 | transform: translateY(-50%);
55 | top: 30px;
56 | position: absolute;
57 | }
58 | .action_span {
59 | font-size: 14px;
60 | }
61 | .circle_icons {
62 | height: 40px;
63 | width: 40px;
64 | border-radius: 50%;
65 | display: flex;
66 | align-items: center;
67 | justify-content: center;
68 | background: var(--bg-forth);
69 | cursor: pointer;
70 | }
71 | :global(.dark) .menu i {
72 | filter: invert(100%);
73 | }
74 | /* displayAccessibility */
75 | .absolute_container {
76 | padding: 0 0.3rem;
77 | position: absolute;
78 | top: 0;
79 | bottom: 0;
80 | right: 0;
81 | left: 0;
82 | width: 360px;
83 | border-radius: 10px;
84 | background: var(--bg-primary);
85 | user-select: none;
86 | padding: 10px;
87 | }
88 | .absolute_container .mmenu_item {
89 | margin-top: 0;
90 | }
91 | .absolute_container_header {
92 | display: flex;
93 | align-items: center;
94 | gap: 10px;
95 | font-weight: 700;
96 | font-size: 24px;
97 | }
98 | .absolute_container label {
99 | display: flex;
100 | align-items: center;
101 | justify-content: space-between;
102 | margin-left: 50px;
103 | font-weight: 600;
104 | padding: 10px;
105 | cursor: pointer;
106 | border-radius: 10px;
107 | }
108 |
109 | .absolute_container label input {
110 | width: 20px;
111 | height: 20px;
112 | accent-color: var(--color-primary);
113 | }
114 | .mmenu_col {
115 | display: flex;
116 | flex-direction: column;
117 | justify-content: center;
118 | }
119 |
120 | .mmenu_col span:first-of-type {
121 | color: var(--color-secondary);
122 | font-weight: 600;
123 | font-size: 15px;
124 | letter-spacing: 1px;
125 | }
126 |
127 | .mmenu_col span:last-of-type {
128 | font-size: 14px;
129 | }
130 |
131 | .mmenu_main {
132 | padding: 10px;
133 | margin-top: 5px;
134 | display: flex;
135 | align-items: center;
136 | gap: 5px;
137 | }
138 |
139 | .mmenu_splitter {
140 | width: 100%;
141 | height: 1px;
142 | background: var(--bg-third);
143 | margin-top: 5px;
144 | }
145 |
146 | .mmenu_span1 {
147 | font-size: 14px !important;
148 | }
149 |
150 | .mmenu_span2 {
151 | font-size: 12px !important;
152 | color: var(--color-third);
153 | }
154 |
155 | .mmenu_item {
156 | position: relative;
157 | display: flex;
158 | align-items: center;
159 | margin-top: 5px;
160 | padding: 7px;
161 | font-weight: 600;
162 | font-size: 14px;
163 | border-radius: 10px;
164 | }
165 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | * {
2 | padding: 0;
3 | margin: 0;
4 | box-sizing: border-box;
5 | -webkit-touch-callout: none; /* iOS Safari */
6 | -webkit-user-select: none; /* Safari */
7 | -khtml-user-select: none; /* Konqueror HTML */
8 | -moz-user-select: none; /* Old versions of Firefox */
9 | -ms-user-select: none; /* Internet Explorer/Edge */
10 | user-select: none;
11 | color: var(--color-secondary);
12 | }
13 | body {
14 | font-family: "Segoe UI", Helvetica, Arial, sans-serif;
15 | background: var(--bg-secondary) !important ;
16 | }
17 |
18 | html {
19 | overflow-y: scroll;
20 | }
21 | :root {
22 |
23 | --bg-primary: #ffffff;
24 | --bg-secondary: #f0f2f5;
25 | --bg-third: #e4e6eb;
26 | --bg-forth: #f0f2f5;
27 | --color-primary:#5c6e58;
28 | --color-secondary: #050505;
29 | --color-third: #65676b;
30 | --divider: #d6d7d9;
31 | --dark-bg-primary: #18191a;
32 | --dark-bg-secondary: #242526;
33 | --dark-bg-third: #3a3b3c;
34 | --dark-bg-forth: #3a3b3c;
35 | --dark-color-primary: #242526;
36 | --dark-color-secondary: #b0b3b8;
37 | --red-color: #e40c2b;
38 | --orange-color: #e1523d;
39 | --blue-color: #0096ff;
40 | --light-blue-color: #0096ff21;
41 | --blue: #1876f2;
42 | --green-color: #42b72a;
43 | --light-blue-color: #e7f3ff;
44 | --border-color: #ccced2;
45 | --shadow-1: rgba(0, 0, 0, 0.1);
46 | --shadow-2: rgba(0, 0, 0, 0.2);
47 | --shadow-5: rgba(0, 0, 0, 0.5);
48 | --shadow-8: rgba(0, 0, 0, 0.8);
49 | --shadow-inset: rgba(255, 255, 255, 0.5);
50 | --color-error: #b94a48;
51 | }
52 |
53 |
54 | .line {
55 | width: 100%;
56 | height: 1px;
57 | background: var(--divider);
58 | margin: 5px 0;
59 | }
60 |
61 | /*----Scrollbar----*/
62 | .scrollbar {
63 | overflow: auto;
64 | }
65 |
66 | ::-webkit-scrollbar {
67 | width: 8px;
68 | }
69 |
70 | ::-webkit-scrollbar-thumb {
71 | background: #aeaeafa1;
72 | border-radius: 5px;
73 | }
74 |
75 | /*----Scrollbar----*/
76 |
77 | /* hover */
78 | .hover1:hover {
79 | background: var(--bg-secondary);
80 | }
81 | .hover2:hover {
82 | background: var(--bg-third);
83 | }
84 |
85 | /* hover */
86 |
87 | /*---Circles------*/
88 | .small_circle {
89 | position: relative;
90 | height: 36px;
91 | width: 36px;
92 | border-radius: 50%;
93 | display: flex;
94 | align-items: center;
95 | justify-content: center;
96 | margin-right: 11px;
97 | background: var(--bg-third);
98 | cursor: pointer;
99 | }
100 | .smaller_circle {
101 | position: relative;
102 | height: 30px;
103 | width: 30px;
104 | border-radius: 50%;
105 | border: 1px solid #5c6e58;
106 | display: flex;
107 | align-items: center;
108 | justify-content: center;
109 | margin-right: 11px;
110 | background: var(--bg-primary);
111 | cursor: pointer;
112 | }
113 |
114 | /*---Circles------*/
115 | .blur {
116 | position: fixed;
117 | top: 0;
118 | left: 0;
119 | right: 0;
120 | background: rgba(255, 255, 255, 0.768);
121 | bottom: 0;
122 | z-index: 99;
123 | backdrop-filter: blur(2px);
124 | }
125 | /*----filters---*/
126 | #ReactSimpleImageViewer {
127 | z-index: 19;
128 | background-color: #000000f0;
129 | }
130 | #ReactSimpleImageViewer img {
131 | max-height: 70%;
132 | max-width: 80%;
133 | border-radius: 10px;
134 | }
135 | .reactEasyCrop_CropArea {
136 | color: rgba(255, 255, 255, 0.5) !important;
137 | }
138 |
139 | .dark .react-loading-skeleton {
140 | --base-color: #2d2d2d !important;
141 | --highlight-color: #484848 !important;
142 | --animation-duration: 1s;
143 | --animation-direction: normal;
144 | --pseudo-element-display: block;
145 | }
146 |
--------------------------------------------------------------------------------
/client/src/pages/friends/style.module.css:
--------------------------------------------------------------------------------
1 | .friends_container {
2 | height: calc(100vh - 56px);
3 | width: 100%;
4 | margin-top: 56px;
5 | color:var(--color-secondary);
6 | display: grid;
7 | grid-template-columns: 22% auto;
8 | }
9 | .left {
10 | background:var(--bg-primary);
11 | padding: 10px;
12 | position: fixed;
13 | width: 22%;
14 | height: 100%;
15 | left: 0;
16 | }
17 |
18 | .right {
19 | background:var(--bg-secondary);
20 | padding: 1.2rem;
21 | top: 56px;
22 | left: 360px;
23 | right: 0;
24 | width: 100%;
25 | min-height: calc(100vh - 56px);
26 | }
27 | .left .header,
28 | .friends_left_header {
29 | display: flex;
30 | align-items: center;
31 | justify-content: space-between;
32 | }
33 |
34 | .left .header h3,
35 | .friends_left_header h3 {
36 | font-size: 21px;
37 | }
38 |
39 | .left .menu_item {
40 | align-items: center;
41 | border-radius: 10px;
42 | cursor: pointer;
43 | display: flex;
44 | font-size: 14px;
45 | font-weight: 600;
46 | margin-top: 5px;
47 | padding: 7px;
48 | position: relative;
49 | }
50 | .rArrow {
51 | position: absolute;
52 | right: 10px;
53 | top: 50%;
54 | -webkit-transform: translateY(-50%);
55 | transform: translateY(-50%);
56 | }
57 | .active_friends {
58 | background:var(--bg-secondary);
59 | }
60 | .active_friends .rArrow {
61 | display: none;
62 | }
63 | .active_friends > div {
64 | background:var(--color-primary);
65 | }
66 | .active_friends i {
67 | -webkit-filter: invert(100%);
68 | filter: invert(100%);
69 | }
70 |
71 | .req_card {
72 | width: 210px;
73 | height: fit-content;
74 | border-radius: 10px;
75 | font-weight: 600;
76 | display: flex;
77 | flex-direction: column;
78 | align-items: center;
79 | gap: 2px;
80 | padding: 6px;
81 | }
82 |
83 | .req_card img {
84 | width: 100%;
85 | height: 200px;
86 | object-fit: cover;
87 | border-radius: 10px;
88 | }
89 |
90 | .req_card button {
91 | width: 170px !important;
92 | }
93 | .friends_right_wrap {
94 | padding-bottom: 10px;
95 | border-bottom: 1px solid var(--bg-third);
96 | }
97 |
98 | .friends_right_wrap .flex_wrap {
99 | margin-top: 10px;
100 | display: flex;
101 | flex-wrap: wrap;
102 | align-items: center;
103 | gap: 15px;
104 | }
105 |
106 | .see_link {
107 | color:var(--color-primary);
108 | padding: 5px;
109 | border-radius: 10px;
110 | cursor: pointer;
111 | width: 60px;
112 | height: 40px;
113 | }
114 | .flex_wrap {
115 | margin-top: 10px;
116 | display: flex;
117 | flex-wrap: wrap;
118 | align-items: center;
119 | gap: 15px;
120 | }
121 | .photo {
122 | position: relative;
123 | border-radius: 10px;
124 | overflow: hidden;
125 | }
126 | .req_name {
127 | cursor: pointer;
128 | text-align: center;
129 | position: absolute;
130 | bottom: 0;
131 | background:var(--color-third);
132 | width: 100%;
133 | height: 12%;
134 | display: flex;
135 | justify-content: center;
136 | align-items: center;
137 | color:var(--bg-primary);
138 | font-size: 18px;
139 | font-family: sans-serif;
140 | }
141 |
142 | .btns {
143 | display: flex;
144 | flex-direction: column;
145 | gap: 4px;
146 | width: 100%;
147 | margin-top: 4px;
148 | }
149 | .btns button {
150 | width: 100% !important;
151 | font-weight: 500;
152 | font-size: 15px;
153 | }
154 |
155 | @media (max-width: 870px) {
156 | .left {
157 | width: auto;
158 | }
159 | .rArrow,
160 | .menu_item span,
161 | .header h3 {
162 | display: none;
163 | }
164 | .header {
165 | justify-content: center;
166 | }
167 | .friends_container {
168 | grid-template-columns: auto;
169 | padding-left: 70px;
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/client/src/components/Posts/Post/Comments/comment.module.css:
--------------------------------------------------------------------------------
1 | /* Comments */
2 | .comments {
3 | margin: 0.5rem 0;
4 | }
5 | .comments:last-child {
6 | margin-bottom: 0;
7 | }
8 | .comments .comments_img {
9 | width: 32px;
10 | height: 32px;
11 | border-radius: 50%;
12 | object-fit: cover;
13 | }
14 |
15 | /* Comment */
16 | .comment {
17 | width: 100%;
18 | margin: 20px 0;
19 | padding: 0.5rem;
20 | border: 1px solid var(--color-primary);
21 | border-radius: 0.5rem;
22 | }
23 | .comment_header {
24 | color: var(--color-primary);
25 | display: flex;
26 | justify-content: space-between;
27 | margin-bottom: 0.25rem;
28 | font-size: 0.75em;
29 | }
30 | .comment_header_left {
31 | display: flex;
32 | text-align: center;
33 | justify-content: center;
34 | }
35 | .comment_header_right {
36 | width: 100%;
37 | }
38 | .comment_info {
39 | padding: 8px;
40 | display: flex;
41 | flex-direction: column;
42 | border-radius: 10px;
43 | gap: 2px;
44 | min-width: 170px;
45 | max-width: max-content;
46 | position: relative;
47 | padding-right: 30px;
48 | background: var(--bg-forth);
49 | }
50 | .comment_info_auther {
51 | font-size: 12px;
52 | font-weight: 500;
53 | cursor: pointer;
54 | display: flex;
55 | align-items: center;
56 | }
57 |
58 | .comment_image {
59 | width: 40px;
60 | height: 40px;
61 | border-radius: 50%;
62 | object-fit: cover;
63 | }
64 |
65 | .author:hover {
66 | text-decoration: underline;
67 | }
68 | .date {
69 | font-size: 9px;
70 | color: var(--color-third);
71 | align-self: flex-end;
72 | }
73 | .date:hover {
74 | text-decoration: underline;
75 | }
76 |
77 | .comment_info_text {
78 | font-size: 14px;
79 | font-weight: 400;
80 | word-break: break-all;
81 | }
82 | .comment_info_likes {
83 | cursor: pointer;
84 | background: var(--bg-primary);
85 | width: 35px;
86 | height: 35px;
87 | border-radius: 50px;
88 | display: flex;
89 | justify-content: center;
90 | align-items: center;
91 | position: absolute;
92 | right: -15px;
93 | bottom: -10px;
94 | gap: 5px;
95 | box-shadow: 0 2px 4px var(--shadow-1);
96 | }
97 | .comment_actions {
98 | display: flex;
99 | margin-top: 8px;
100 | gap: 7px;
101 | }
102 | .actions {
103 | cursor: pointer;
104 | font-size: 12px;
105 | font-weight: 600;
106 | }
107 | .actions:hover {
108 | text-decoration: underline;
109 | }
110 | .comment_date {
111 | font-size: 12px;
112 | }
113 | .replying {
114 | margin-top: 10px;
115 | margin-left: 10px;
116 | }
117 | .nested_replies_stack {
118 | display: flex;
119 | }
120 |
121 | .nested_replies {
122 | padding-left: 0.2rem;
123 | flex-grow: 1;
124 | }
125 |
126 | .hide {
127 | display: none;
128 | }
129 |
130 | /* commentForm */
131 | .comment_form_container {
132 | padding: 5px 0px;
133 | display: flex;
134 | align-items: center;
135 | gap: 10px;
136 | margin-bottom: 5px;
137 | }
138 | .comment_form_left {
139 | display: flex;
140 | text-align: center;
141 | justify-content: center;
142 | }
143 | .comments_img {
144 | width: 32px;
145 | height: 32px;
146 | border-radius: 50%;
147 | }
148 | .comment_form_right {
149 | background: var(--bg-forth);
150 | color:var(--color-secondary);
151 | height: 38px;
152 | flex: 1 1;
153 | border-radius: 50px;
154 | font-size: 15px;
155 | line-height: 21px;
156 | display: flex;
157 | align-items: center;
158 | justify-content: space-between;
159 | padding: 0 10px;
160 | gap: 10px;
161 | }
162 | .comment_form_right:hover {
163 | background:var(--bg-third);
164 | }
165 | .comment_form_inputs {
166 | width: 100%;
167 | height: 100%;
168 | }
169 |
170 | /*
171 | .comment-form-row {
172 | display: flex;
173 | gap: 0.5rem;
174 | } */
175 |
--------------------------------------------------------------------------------