├── client
├── netlify.toml
├── public
│ ├── Logo.png
│ ├── favicon.ico
│ └── index.html
├── src
│ ├── Images
│ │ ├── Logo.png
│ │ └── google.webp
│ ├── utils
│ │ ├── Themes.js
│ │ └── Data.js
│ ├── redux
│ │ ├── setSigninSlice.jsx
│ │ ├── snackbarSlice.jsx
│ │ ├── audioplayerSlice.jsx
│ │ ├── store.js
│ │ └── userSlice.jsx
│ ├── index.css
│ ├── firebase.js
│ ├── components
│ │ ├── ToastMessage.jsx
│ │ ├── SearchCard.jsx
│ │ ├── DefaultCard.jsx
│ │ ├── MoreResult.jsx
│ │ ├── ImageSelector.jsx
│ │ ├── TopResult.jsx
│ │ ├── Navbar.jsx
│ │ ├── Episodecard.jsx
│ │ ├── VideoPlayer.jsx
│ │ ├── Menu.jsx
│ │ ├── PodcastCard.jsx
│ │ ├── AudioPlayer.jsx
│ │ ├── OTP.jsx
│ │ ├── Signup.jsx
│ │ ├── Upload.jsx
│ │ └── Signin.jsx
│ ├── index.js
│ ├── api
│ │ └── index.js
│ ├── pages
│ │ ├── Favourites.jsx
│ │ ├── DisplayPodcasts.jsx
│ │ ├── Profile.jsx
│ │ ├── Search.jsx
│ │ ├── Dashboard.jsx
│ │ └── PodcastDetails.jsx
│ └── App.js
├── package.json
└── README.md
├── server
├── error.js
├── middleware
│ ├── auth.js
│ └── verifyToken.js
├── routes
│ ├── user.js
│ ├── auth.js
│ └── podcast.js
├── package.json
├── models
│ ├── Episodes.js
│ ├── User.js
│ └── Podcasts.js
├── controllers
│ ├── user.js
│ ├── podcasts.js
│ └── auth.js
└── index.js
├── .gitignore
└── README.md
/client/netlify.toml:
--------------------------------------------------------------------------------
1 | [[redirects]]
2 | from = "/*"
3 | to = "/index.html"
4 | status = 200
--------------------------------------------------------------------------------
/client/public/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terabyte-sourcer/Podstream/HEAD/client/public/Logo.png
--------------------------------------------------------------------------------
/client/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terabyte-sourcer/Podstream/HEAD/client/public/favicon.ico
--------------------------------------------------------------------------------
/client/src/Images/Logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terabyte-sourcer/Podstream/HEAD/client/src/Images/Logo.png
--------------------------------------------------------------------------------
/client/src/Images/google.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/terabyte-sourcer/Podstream/HEAD/client/src/Images/google.webp
--------------------------------------------------------------------------------
/server/error.js:
--------------------------------------------------------------------------------
1 | export const createError = (status, message)=>{
2 | const err = new Error()
3 | err.status= status
4 | err.message= message
5 | return err
6 | }
--------------------------------------------------------------------------------
/server/middleware/auth.js:
--------------------------------------------------------------------------------
1 |
2 | export function localVariables(req, res, next) {
3 | res.app.locals = {
4 | OTP: null,
5 | resetSession: false,
6 | };
7 | next();
8 | }
--------------------------------------------------------------------------------
/server/routes/user.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { verifyToken } from "../middleware/verifyToken.js";
3 | import { getUser } from "../controllers/user.js";
4 |
5 |
6 | const router = express.Router();
7 |
8 | //get user
9 | router.get("/",verifyToken, getUser);
10 |
11 |
12 |
13 |
14 |
15 | export default router;
--------------------------------------------------------------------------------
/client/src/utils/Themes.js:
--------------------------------------------------------------------------------
1 | export const darkTheme = {
2 | bg:"#15171E",
3 | bgLight: "#1C1E27",
4 | primary:"#be1adb",
5 | text_primary:"#F2F3F4",
6 | text_secondary:"#b1b2b3",
7 | card:"#121212",
8 | button:"#5c5b5b",
9 | }
10 |
11 | export const lightTheme = {
12 | bg:"#FFFFFF",
13 | bgLight: "#f0f0f0",
14 | primary:"#be1adb",
15 | text_primary:"#111111",
16 | text_secondary:"#48494a",
17 | card:"#FFFFFF",
18 | button:"#5c5b5b",
19 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | server/node_modules
5 | client/node_modules
6 | client/.env
7 | server/.env
8 | /.pnp
9 | .pnp.js
10 |
11 | # testing
12 | /coverage
13 |
14 | # production
15 | /build
16 |
17 | # misc
18 | .env
19 | .DS_Store
20 | .env.local
21 | .env.development.local
22 | .env.test.local
23 | .env.production.local
24 |
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
--------------------------------------------------------------------------------
/client/src/redux/setSigninSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | opensi: false,
5 | };
6 |
7 | const signin = createSlice({
8 | name: 'signin',
9 | initialState,
10 | reducers: {
11 | openSignin: (state, action) => {
12 | state.opensi = true;
13 | },
14 | closeSignin: (state) => {
15 | state.opensi = false;
16 | }
17 | }
18 | });
19 |
20 | export const { openSignin, closeSignin } = signin.actions;
21 |
22 | export default signin.reducer;
--------------------------------------------------------------------------------
/server/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "server",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "index.js",
6 | "type": "module",
7 | "scripts": {
8 | "start": "node index.js",
9 | "dev": "nodemon index.js"
10 | },
11 | "author": "rishav",
12 | "license": "ISC",
13 | "dependencies": {
14 | "bcrypt": "^5.1.0",
15 | "cors": "^2.8.5",
16 | "dotenv": "^16.0.3",
17 | "express": "^4.18.2",
18 | "jsonwebtoken": "^9.0.0",
19 | "mongoose": "^7.0.4",
20 | "morgan": "^1.10.0",
21 | "nodemailer": "^6.9.1",
22 | "nodemon": "^2.0.22",
23 | "otp-generator": "^4.0.1"
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/client/src/index.css:
--------------------------------------------------------------------------------
1 | @import url('https://fonts.googleapis.com/css2?family=Poppins:wght@100;300;400;500;600&display=swap');
2 | html{
3 | scroll-behavior: smooth;
4 | }
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | font-family: 'Poppins', sans-serif;
9 | -webkit-font-smoothing: antialiased;
10 | -moz-osx-font-smoothing: grayscale;
11 |
12 | }
13 |
14 | /* width */
15 | ::-webkit-scrollbar {
16 | width: 2px;
17 | }
18 | /* Track */
19 | ::-webkit-scrollbar-track {
20 |
21 | }
22 |
23 | /* Handle */
24 | ::-webkit-scrollbar-thumb {
25 | background: #888;
26 | border-radius: 6px;
27 | height: 50px;
28 | }
--------------------------------------------------------------------------------
/client/src/redux/snackbarSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | open: false,
5 | message: "",
6 | severity: "success",
7 | };
8 |
9 | const snackbar = createSlice({
10 | name: 'snackbar',
11 | initialState,
12 | reducers: {
13 | openSnackbar: (state, action) => {
14 | state.open = true;
15 | state.message = action.payload.message;
16 | state.severity = action.payload.severity;
17 | },
18 | closeSnackbar: (state) => {
19 | state.open = false;
20 | }
21 | }
22 | });
23 |
24 | export const { openSnackbar, closeSnackbar } = snackbar.actions;
25 |
26 | export default snackbar.reducer;
--------------------------------------------------------------------------------
/server/middleware/verifyToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import { createError } from "../error.js";
3 |
4 | export const verifyToken = async (req, res, next) => {
5 | try {
6 | if (!req.headers.authorization) return next(createError(401, "You are not authenticated!"));
7 | // Get the token from the header
8 | const token = req.headers.authorization.split(" ")[1];
9 | // Check if token exists
10 | if (!token) return next(createError(401, "You are not authenticated!"));
11 |
12 | const decode = await jwt.verify(token, process.env.JWT);
13 | req.user = decode;
14 | next();
15 | } catch (error) {
16 | console.log(error)
17 | res.status(402).json({ error: error.message })
18 | }
19 | };
--------------------------------------------------------------------------------
/server/models/Episodes.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import jwt from "jsonwebtoken";
3 |
4 | const EpisodesSchema = new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | desc: {
10 | type: String,
11 | required: true,
12 | },
13 | thumbnail: {
14 | type: String,
15 | default: "",
16 | },
17 | creator:{
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: "User",
20 | required: true,
21 | },
22 | type: {
23 | type: String,
24 | default: "audio",
25 | },
26 | duration: {
27 | type: String,
28 | default: "",
29 | },
30 | file: {
31 | type: String,
32 | default: "",
33 | },
34 | },
35 | { timestamps: true,
36 | }
37 | );
38 |
39 | export default mongoose.model("Episodes", EpisodesSchema);
--------------------------------------------------------------------------------
/client/src/firebase.js:
--------------------------------------------------------------------------------
1 | // Import the functions you need from the SDKs you need
2 | import { initializeApp } from "firebase/app";
3 | import { getAnalytics } from "firebase/analytics";
4 | // TODO: Add SDKs for Firebase products that you want to use
5 | // https://firebase.google.com/docs/web/setup#available-libraries
6 |
7 | // Your web app's Firebase configuration
8 | // For Firebase JS SDK v7.20.0 and later, measurementId is optional
9 | const firebaseConfig = {
10 | apiKey: "AIzaSyCd2X9WXZiBIAAI8TzrR7MUa3-EvZp2V0c",
11 | authDomain: "test-app-rishav.firebaseapp.com",
12 | projectId: "test-app-rishav",
13 | storageBucket: "test-app-rishav.appspot.com",
14 | messagingSenderId: "129567474175",
15 | appId: "1:129567474175:web:8473430c58c34cac8f27ca",
16 | measurementId: "G-B8JJ57Y5T6"
17 | };
18 |
19 | // Initialize Firebase
20 | const app = initializeApp(firebaseConfig);
21 | export default app;
--------------------------------------------------------------------------------
/client/src/components/ToastMessage.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 | import Alert from "@mui/material/Alert";
3 | import Snackbar from "@mui/material/Snackbar";
4 | import { useState } from "react";
5 | import { closeSnackbar } from "../redux/snackbarSlice";
6 | import { useSelector } from "react-redux";
7 | import { useDispatch } from "react-redux";
8 |
9 | const ToastMessage = ({
10 | message,
11 | severity,
12 | open,
13 | }) => {
14 | const dispatch = useDispatch();
15 | return (
16 | dispatch(closeSnackbar())}
20 | >
21 | dispatch(closeSnackbar())}
23 | severity={severity}
24 | sx={{ width: "100%" }}
25 | >
26 | {message}
27 |
28 |
29 | );
30 | };
31 |
32 | export default ToastMessage;
--------------------------------------------------------------------------------
/server/routes/auth.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { signup,signin, logout, googleAuthSignIn, generateOTP, verifyOTP, createResetSession,findUserByEmail, resetPassword } from "../controllers/auth.js";
3 | import { localVariables } from "../middleware/auth.js";
4 |
5 | const router = express.Router();
6 |
7 | //create a user
8 | router.post("/signup", signup);
9 | //signin
10 | router.post("/signin", signin);
11 | //logout
12 | router.post("/logout", logout);
13 | //google signin
14 | router.post("/google", googleAuthSignIn);
15 | //find user by email
16 | router.get("/findbyemail", findUserByEmail);
17 | //generate opt
18 | router.get("/generateotp",localVariables, generateOTP);
19 | //verify opt
20 | router.get("/verifyotp", verifyOTP);
21 | //create reset session
22 | router.get("/createResetSession", createResetSession);
23 | //forget password
24 | router.put("/forgetpassword", resetPassword);
25 |
26 |
27 |
28 |
29 | export default router;
--------------------------------------------------------------------------------
/server/models/User.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import jwt from "jsonwebtoken";
3 |
4 | const UserSchema = new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | unique: false,
9 | },
10 | email: {
11 | type: String,
12 | required: true,
13 | unique: true,
14 | },
15 | password: {
16 | type: String,
17 | default: "",
18 | },
19 | img: {
20 | type: String,
21 | default: "",
22 | },
23 | googleSignIn:{
24 | type: Boolean,
25 | required: true,
26 | default: false,
27 | },
28 | podcasts: {
29 | type: [mongoose.Schema.Types.ObjectId],
30 | ref: "Podcasts",
31 | default: [],
32 | },
33 | favorits: {
34 | type: [mongoose.Schema.Types.ObjectId],
35 | ref: "Podcasts",
36 | default: [],
37 | }
38 | },
39 | { timestamps: true }
40 | );
41 |
42 | export default mongoose.model("User", UserSchema);
--------------------------------------------------------------------------------
/server/routes/podcast.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { verifyToken } from "../middleware/verifyToken.js";
3 | import { addView, addepisodes, createPodcast, favoritPodcast, getByCategory, getByTag, getPodcastById, getPodcasts, random, search, mostpopular } from "../controllers/podcasts.js";
4 |
5 |
6 | const router = express.Router();
7 |
8 | //create a podcast
9 | router.post("/",verifyToken, createPodcast);
10 | //get all podcasts
11 | router.get("/", getPodcasts);
12 | //get podcast by id
13 | router.get("/get/:id",getPodcastById)
14 |
15 | //add episode to a
16 | router.post("/episode",verifyToken, addepisodes);
17 |
18 | //favorit/unfavorit podcast
19 | router.post("/favorit",verifyToken,favoritPodcast);
20 |
21 | //add view
22 | router.post("/addview/:id",addView);
23 |
24 |
25 | //searches
26 | router.get("/mostpopular", mostpopular)
27 | router.get("/random", random)
28 | router.get("/tags", getByTag)
29 | router.get("/category", getByCategory)
30 | router.get("/search", search)
31 |
32 |
33 |
34 |
35 |
36 | export default router;
--------------------------------------------------------------------------------
/client/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import { store, persistor } from './redux/store';
6 | import { Provider } from 'react-redux';
7 | import { PersistGate } from 'redux-persist/integration/react';
8 | import { GoogleOAuthProvider } from '@react-oauth/google';
9 |
10 | const root = ReactDOM.createRoot(document.getElementById('root'));
11 | root.render(
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | );
22 |
23 | // If you want to start measuring performance in your app, pass a function
24 | // to log results (for example: reportWebVitals(console.log))
25 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
26 | // reportWebVitals();
27 |
--------------------------------------------------------------------------------
/client/src/redux/audioplayerSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit";
2 |
3 | const initialState = {
4 | openplayer: false,
5 | type: "audio",
6 | episode: null,
7 | podid: null,
8 | currenttime: 0,
9 | index: 0
10 | };
11 |
12 | const audioplayer = createSlice({
13 | name: 'audioplayer',
14 | initialState,
15 | reducers: {
16 | openPlayer: (state, action) => {
17 | state.openplayer = true;
18 | state.type = action.payload.type;
19 | state.episode = action.payload.episode;
20 | state.podid = action.payload.podid;
21 | state.currenttime = action.payload.currenttime;
22 | state.index = action.payload.index;
23 | },
24 | closePlayer: (state) => {
25 | state.openplayer = false;
26 | },
27 | setCurrentTime: (state, action) => {
28 | state.currenttime = action.payload.currenttime;
29 | }
30 | }
31 | });
32 |
33 | export const { openPlayer, closePlayer,setCurrentTime } = audioplayer.actions;
34 |
35 | export default audioplayer.reducer;
--------------------------------------------------------------------------------
/server/models/Podcasts.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import jwt from "jsonwebtoken";
3 |
4 | const PodcastsSchema = new mongoose.Schema({
5 | name: {
6 | type: String,
7 | required: true,
8 | },
9 | desc: {
10 | type: String,
11 | required: true,
12 | },
13 | thumbnail: {
14 | type: String,
15 | default: "",
16 | },
17 | creator: {
18 | type: mongoose.Schema.Types.ObjectId,
19 | ref: "User",
20 | required: true,
21 | },
22 | tags: {
23 | type: [String],
24 | default: [],
25 | },
26 | type: {
27 | type: String,
28 | default: "audio",
29 | },
30 | category: {
31 | type: String,
32 | default: "podcast",
33 | },
34 | views: {
35 | type: Number,
36 | default: 0,
37 | },
38 | episodes: {
39 | type: [mongoose.Schema.Types.ObjectId],
40 | ref: "Episodes",
41 | default: [],
42 | }
43 | },
44 | {
45 | timestamps: true,
46 | }
47 | );
48 |
49 | export default mongoose.model("Podcasts", PodcastsSchema);
--------------------------------------------------------------------------------
/client/src/components/SearchCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import Avatar from '@mui/material/Avatar';
3 | import styled from 'styled-components';
4 | const Card=styled.div`
5 | height:250px;
6 | width:250px;
7 | background-color:#305506;
8 | padding:1rem;
9 | border-radius:0.6rem;
10 | `
11 | const PodcastName=styled.div`
12 | color: ${({ theme }) => theme.text_primary};
13 | margin:1.6rem;
14 | font-weight:600;
15 | font-size:1.5rem;
16 | `
17 | const PodcastDescription=styled.div`
18 | color: ${({theme}) => theme.text_secondary};
19 | margin:1.4rem;
20 |
21 | `
22 | export const SearchCard = () => {
23 | return (
24 |
25 |
30 |
31 | Eminem
32 |
33 |
34 | Hello I am Eminem
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/client/src/redux/store.js:
--------------------------------------------------------------------------------
1 | import { configureStore, combineReducers } from "@reduxjs/toolkit";
2 | import userReducer from "./userSlice";
3 | import snackbarReducer from "./snackbarSlice";
4 | import audioReducer from "./audioplayerSlice";
5 | import signinReducer from './setSigninSlice';
6 | import {
7 | persistStore,
8 | persistReducer,
9 | FLUSH,
10 | REHYDRATE,
11 | PAUSE,
12 | PERSIST,
13 | PURGE,
14 | REGISTER,
15 | } from "redux-persist";
16 | import storage from "redux-persist/lib/storage";
17 | import { PersistGate } from "redux-persist/integration/react";
18 |
19 | const persistConfig = {
20 | key: "root",
21 | version: 1,
22 | storage,
23 | };
24 |
25 | const rootReducer = combineReducers({ user: userReducer, snackbar: snackbarReducer, audioplayer: audioReducer, signin: signinReducer});
26 |
27 | const persistedReducer = persistReducer(persistConfig, rootReducer);
28 |
29 | export const store = configureStore({
30 | reducer: persistedReducer,
31 | middleware: (getDefaultMiddleware) =>
32 | getDefaultMiddleware({
33 | serializableCheck: {
34 | ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
35 | },
36 | }),
37 | });
38 |
39 | export const persistor = persistStore(store)
--------------------------------------------------------------------------------
/client/src/components/DefaultCard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 |
4 | const Card = styled.div`
5 | width:150px;
6 | height:150px;
7 | border-radius:0.6rem;
8 | padding:1rem;
9 | &:hover{
10 | cursor: pointer;
11 | transform: translateY(-8px);
12 | transition: all 0.4s ease-in-out;
13 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.3);
14 | filter: brightness(1.3);
15 | }
16 | @media (max-width: 768px) {
17 | width: 250px;
18 | }
19 | `
20 | const DefaultCardText = styled.div`
21 | color: #F2F3F4;
22 | font-size:1.4rem;
23 | font-weight:600;
24 |
25 | `
26 | const DefaultCardImg=styled.img`
27 | height:90px;
28 | width:80px;
29 | object-fit: cover;
30 | clip-path: polygon(0 0, 100% 0, 100% 66%, 0 98%);
31 | transform:rotate(20deg);
32 | `
33 | const FlexContainer = styled.div`
34 | width: 100%;
35 | height: 100%;
36 | display: flex;
37 | justify-content: flex-end;
38 | align-items: flex-end;
39 | `
40 | export const DefaultCard = ({category}) => {
41 | return (
42 |
43 |
44 | {category.name}
45 |
46 |
47 |
51 |
52 |
53 | )
54 | }
55 |
--------------------------------------------------------------------------------
/server/controllers/user.js:
--------------------------------------------------------------------------------
1 | import { createError } from "../error.js";
2 | import User from "../models/User.js";
3 |
4 |
5 | export const update = async (req, res, next) => {
6 | if (req.params.id === req.user.id) {
7 | try {
8 | const updatedUser = await User.findByIdAndUpdate(
9 | req.params.id,
10 | {
11 | $set: req.body,
12 | },
13 | { new: true }
14 | );
15 | res.status(200).json(updatedUser);
16 | } catch (err) {
17 | next(err);
18 | }
19 | } else {
20 | return next(createError(403, "You can update only your account!"));
21 | }
22 | }
23 |
24 | export const getUser = async (req, res, next) => {
25 | try {
26 | const user = await User.findById(req.user.id).populate({
27 | path: "podcasts",
28 | populate: {
29 | path: "creator",
30 | select: "name img",
31 | }
32 | }
33 | ).populate(
34 | {
35 | path: "favorits",
36 | populate: {
37 | path: "creator",
38 | select: "name img",
39 | }
40 | }
41 | );
42 | res.status(200).json(user);
43 | } catch (err) {
44 | console.log(req.user)
45 | next(err);
46 | }
47 | }
--------------------------------------------------------------------------------
/client/src/redux/userSlice.jsx:
--------------------------------------------------------------------------------
1 | import { createSlice } from "@reduxjs/toolkit"
2 |
3 | const initialState = {
4 | currentUser: null,
5 | loading: false,
6 | error: false,
7 | };
8 |
9 | export const userSlice = createSlice({
10 | name: "user",
11 | initialState,
12 | reducers: {
13 | loginStart: (state) => {
14 | state.loading = true;
15 | },
16 | loginSuccess: (state, action) => {
17 | state.loading = false;
18 | state.currentUser = action.payload.user;
19 | localStorage.setItem('podstreamtoken', action.payload.token);
20 | },
21 | loginFailure: (state) => {
22 | state.loading = false;
23 | state.error = true;
24 | },
25 | logout: (state) => {
26 | state.currentUser = null;
27 | state.loading = false;
28 | state.error = false;
29 | localStorage.removeItem('token');
30 | },
31 | verified: (state, action) => {
32 | if(state.currentUser){
33 | state.currentUser.verified = action.payload;
34 | }
35 | },
36 | displayPodcastFailure: (state) => {
37 | state.loading = false;
38 | state.error = true;
39 | },
40 | subscription: (state, action) => {
41 | if (state.currentUser.subscribedUsers.includes(action.payload)) {
42 | state.currentUser.subscribedUsers.splice(
43 | state.currentUser.subscribedUsers.findIndex(
44 | (channelId) => channelId === action.payload
45 | ),
46 | 1
47 | );
48 | } else {
49 | state.currentUser.subscribedUsers.push(action.payload);
50 | }
51 | },
52 | },
53 | });
54 |
55 | export const { loginStart, loginSuccess, loginFailure, logout, displayPodcastFailure, subscription,verified } =
56 | userSlice.actions;
57 |
58 | export default userSlice.reducer;
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Podstream
2 |
3 | ## Podcast Streaming Application
4 |
5 | ### Deployed Link : [View Webapp](https://podstream.netlify.app/)
6 |
7 | ### This is a web application for streaming podcasts with the following features:
8 | - User authentication
9 | - Podcast search functionality
10 | - Popular podcasts displayed on the main dashboard
11 | - Admin panel for creating and adding new podcasts
12 | - Favourite Podcasts
13 | - Pause podcasts and come back play at same time
14 |
15 | ### Getting Started
16 | To use the application, simply create an account and log in. From there, you can search for podcasts, view popular podcasts on the main dashboard, and listen to podcasts.
17 |
18 | Admin users can access the admin panel to create and add new podcasts, which will be displayed on the main dashboard.
19 |
20 | ### Technologies Used
21 | The application is built using the following technologies:
22 |
23 | - React Js
24 | - Node Js
25 | - Mongo Db
26 | - Google Auth
27 | - Firebase
28 |
29 | ## Steps to start the app
30 |
31 | ### Start the react app
32 | Go to client folder
33 | ```
34 | cd client
35 | ```
36 | Install the node modules
37 | ```
38 | npm install
39 | ```
40 | Start the react app
41 | ```
42 | npm start
43 | ```
44 |
45 | ### Start the backend server
46 | Open a new terminal
47 | Go to server folder
48 | ```
49 | cd server
50 | ```
51 | Install the node modules
52 | ```
53 | npm start
54 | ```
55 | Start the server
56 | ```
57 | npm start
58 | ```
59 |
60 | ## Conclusion
61 |
62 | In conclusion, this podcast streaming application provides users with an easy-to-use platform for listening to their favorite podcasts. With features such as user authentication, podcast search, and an admin panel for creating and adding new podcasts, this application offers a comprehensive solution for podcast streaming.
63 |
--------------------------------------------------------------------------------
/client/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 | Podstream
28 |
29 |
30 |
31 |
32 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/client/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "client",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@emotion/react": "^11.10.6",
7 | "@emotion/styled": "^11.10.6",
8 | "@mui/icons-material": "^5.11.16",
9 | "@mui/material": "^5.12.1",
10 | "@mui/styled-engine": "^5.12.0",
11 | "@react-oauth/google": "^0.10.0",
12 | "@reduxjs/toolkit": "^1.9.5",
13 | "@testing-library/jest-dom": "^5.16.5",
14 | "@testing-library/react": "^13.4.0",
15 | "@testing-library/user-event": "^13.5.0",
16 | "axios": "^1.3.6",
17 | "firebase": "^9.20.0",
18 | "install": "^0.13.0",
19 | "jwt-decode": "^3.1.2",
20 | "npm": "^9.6.5",
21 | "react": "^18.2.0",
22 | "react-dom": "^18.2.0",
23 | "react-ellipsis-text": "^1.2.1",
24 | "react-file-image-to-base64": "^1.0.1",
25 | "react-horizontal-scrolling": "^0.1.13",
26 | "react-horizontal-scrolling-menu": "^4.0.4",
27 | "react-otp-input": "^3.0.0",
28 | "react-redux": "^8.0.5",
29 | "react-router-dom": "^6.10.0",
30 | "react-scripts": "5.0.1",
31 | "redux-persist": "^6.0.0",
32 | "styled-components": "^5.3.9",
33 | "timeago.js": "^4.0.2",
34 | "toolkit": "^1.5.4",
35 | "validator": "^13.9.0",
36 | "video.js": "^8.0.4",
37 | "web-vitals": "^2.1.4"
38 | },
39 | "scripts": {
40 | "start": "react-scripts start",
41 | "build": "react-scripts build",
42 | "test": "react-scripts test",
43 | "eject": "react-scripts eject"
44 | },
45 | "eslintConfig": {
46 | "extends": [
47 | "react-app",
48 | "react-app/jest"
49 | ]
50 | },
51 | "browserslist": {
52 | "production": [
53 | ">0.2%",
54 | "not dead",
55 | "not op_mini all"
56 | ],
57 | "development": [
58 | "last 1 chrome version",
59 | "last 1 firefox version",
60 | "last 1 safari version"
61 | ]
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/client/src/components/MoreResult.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import styled from 'styled-components';
3 | import { Link } from 'react-router-dom';
4 | import {format} from 'timeago.js';
5 |
6 | const Results = styled(Link)`
7 | background-color: ${({ theme }) => theme.bgLight};
8 | display: flex;
9 | align-items: center;
10 | padding: 8px;
11 | border-radius: 6px;
12 | gap: 20px;
13 | &:hover{
14 | cursor: pointer;
15 | transform: translateY(-8px);
16 | transition: all 0.4s ease-in-out;
17 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.3);
18 | filter: brightness(1.3);
19 | }
20 | `
21 | const PodcastImage = styled.img`
22 | height: 80px;
23 | border-radius: 8px;
24 | width: 150px;
25 | object-fit: cover;
26 | @media (max-width: 768px) {
27 | height: 60px;
28 | width: 100px;
29 | }
30 | `
31 | const PodcastInfo = styled.div`
32 | display: flex;
33 | flex-direction: column;
34 | gap: 8px;
35 | `
36 | const PodcastName = styled.div`
37 | display: flex;
38 | flex-direction: column;
39 | color: ${({ theme }) => theme.text_primary};
40 | `
41 | const Creator = styled.div`
42 | color: ${({ theme }) => theme.text_secondary};
43 | font-size: 12px;
44 | @media (max-width: 768px) {
45 | font-size: 10px;
46 | }
47 |
48 | `
49 | const Time = styled.div`
50 | color: ${({ theme }) => theme.text_secondary};
51 | font-size: 12px;
52 | @media (max-width: 768px) {
53 | font-size: 10px;
54 | }
55 | `
56 | const Desciption = styled.div`
57 | display: flex;
58 | gap: 8px;
59 | `
60 |
61 | const MoreResult = ({ podcast }) => {
62 | return (
63 |
64 |
65 |
66 | {podcast?.name}
67 |
68 | {podcast?.creator.name}
69 |
72 |
75 |
76 |
77 |
78 | )
79 | }
80 |
81 | export default MoreResult
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | import express from 'express';
2 | import dotenv from 'dotenv';
3 | import mongoose from 'mongoose';
4 | import cors from 'cors';
5 | import morgan from 'morgan';
6 |
7 | //routes
8 | import authRoutes from './routes/auth.js';
9 | import podcastsRoutes from './routes/podcast.js';
10 | import userRoutes from './routes/user.js';
11 |
12 | const app = express();
13 | dotenv.config();
14 |
15 | /** Middlewares */
16 | app.use(express.json());
17 | const corsConfig = {
18 | credentials: true,
19 | origin: true,
20 | };
21 | app.use(cors(corsConfig));
22 | // app.use(morgan('tiny'));
23 | // app.disable('x-powered-by');
24 | // app.use(function (request, response, next) {
25 | // response.header("Access-Control-Allow-Origin", "*");
26 | // response.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
27 | // next();
28 | // });
29 |
30 | const port = process.env.PORT || 8700;
31 |
32 | const connect = () => {
33 | mongoose.set('strictQuery', true);
34 | mongoose.connect(process.env.MONGO_URL).then(() => {
35 | console.log('MongoDB connected');
36 | }).catch((err) => {
37 | console.log(err);
38 | });
39 | };
40 |
41 |
42 | app.use(express.json())
43 | // app.enable('trust proxy'); // optional, not needed for secure cookies
44 | // app.use(express.session({
45 | // secret : '123456',
46 | // key : 'sid',
47 | // proxy : true, // add this when behind a reverse proxy, if you need secure cookies
48 | // cookie : {
49 | // secure : true,
50 | // maxAge: 5184000000 // 2 months
51 | // }
52 | // }));
53 |
54 | app.use("/api/auth", authRoutes)
55 | app.use("/api/podcasts", podcastsRoutes)
56 | app.use("/api/user", userRoutes)
57 | // app.use("/api/project", projectRoutes)
58 | // app.use("/api/team", teamRoutes)
59 |
60 | app.use((err, req, res, next) => {
61 | const status = err.status || 500;
62 | const message = err.message || "Something went wrong";
63 | return res.status(status).json({
64 | success: false,
65 | status,
66 | message
67 | })
68 | })
69 |
70 | app.listen(port, () => {
71 | console.log("Connected")
72 | connect();
73 | })
74 |
--------------------------------------------------------------------------------
/client/src/components/ImageSelector.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components';
3 | import { useState, useEffect } from 'react';
4 | import ReactImageFileToBase64 from "react-file-image-to-base64";
5 | import CloudUploadIcon from '@mui/icons-material/CloudUpload';
6 |
7 | const Container = styled.div`
8 | height: 120px;
9 | display: flex;
10 | flex-direction: column;
11 | justify-content: center;
12 | gap: 6px;
13 | align-items: center;
14 | border: 2px dashed ${({ theme }) => theme.text_primary + "80"}};
15 | border-radius: 12px;
16 | color: ${({ theme }) => theme.text_primary + "80"};
17 | margin: 30px 20px 0px 20px;
18 | `;
19 |
20 | const Typo = styled.div`
21 | font-size: 14px;
22 | font-weight: 600;
23 | `;
24 |
25 | const TextBtn = styled.div`
26 | font-size: 14px;
27 | font-weight: 600;
28 | color: ${({ theme }) => theme.primary};
29 | cursor: pointer;
30 | `;
31 |
32 | const Img = styled.img`
33 | height: 120px !important;
34 | width: 100%;
35 | object-fit: cover;
36 | border-radius: 12px;
37 | `;
38 |
39 | const ImageSelector = ({ podcast, setPodcast }) => {
40 | const handleOnCompleted = files => {
41 | setPodcast((prev) => {
42 | return { ...prev, thumbnail: files[0].base64_file };
43 | });
44 | };
45 |
46 | const CustomisedButton = ({ triggerInput }) => {
47 | return (
48 |
49 | Browse Image
50 |
51 | );
52 | };
53 | return (
54 |
55 | {podcast.thumbnail !== "" ?
: <>
56 |
57 | Click here to upload thumbnail
58 |
59 | or
60 |
65 |
66 | >}
67 |
68 | )
69 | }
70 |
71 | export default ImageSelector
--------------------------------------------------------------------------------
/client/src/utils/Data.js:
--------------------------------------------------------------------------------
1 | export const Category = [
2 | {
3 | name: "Culture",
4 | img: "https://media.npr.org/assets/img/2020/12/07/99percent_custom-ad44d7569e602b2698267142396e71e91c539149.jpg",
5 | color: "#e8562a"
6 | },
7 | {
8 | name: "Business",
9 | img: "https://m.media-amazon.com/images/I/41-7FShV-3L.jpg",
10 | color: "#c9b2ab"
11 | },
12 | {
13 | name: "Education",
14 | img: "https://m.media-amazon.com/images/M/MV5BMTc0Mjg1Nzc0MF5BMl5BanBnXkFtZTcwODM5OTcwOQ@@._V1_.jpg",
15 | color: "#8cabaa"
16 | },
17 | {
18 | name: "Health",
19 | img: "https://m.media-amazon.com/images/M/MV5BMjNjYjJkYTYtYjI5Zi00NWE4LWFiZjItMjM0N2VlZjgxY2U0XkEyXkFqcGdeQXVyNzg3NjQyOQ@@._V1_.jpg",
20 | color: "#62bf62"
21 | },
22 | {
23 | name: "Comedy",
24 | img: "https://deadline.com/wp-content/uploads/2023/03/LLZA-_Prime-Video-Brings-Trevor-Noah-Home-To-Host-Its-First-South-African-Original_LOL-Last-One-Laughing-South-Africa.jpg?w=1024",
25 | color: "#ed4c59"
26 | },
27 | {
28 | name: "News",
29 | img: "https://i.scdn.co/image/1b5af843be11feb6c563e0d95f5fe0dad659b757",
30 | color: "#ba7538",
31 | },
32 | {
33 | name: "Science",
34 | img: "https://t3.ftcdn.net/jpg/02/06/22/40/360_F_206224040_ejMSpHtBCxGpzM96b3rKPCkbqhfZNUpr.jpg",
35 | color: "#6c9dad",
36 | },
37 | {
38 | name: "History",
39 | img: "https://ssl-static.libsyn.com/p/assets/6/b/f/e/6bfe939ed4336498/HHA-1400px_b.jpg?crop=1:1,offset-y0",
40 | color: "#de577f"
41 | },
42 | {
43 | name: "Religion",
44 | img: "https://d1bsmz3sdihplr.cloudfront.net/media/podcast-shows/BP_podcast_cover_2-optimized.jpg",
45 | color: "#aeb4b5"
46 | },
47 | {
48 | name: "Development",
49 | img: "https://i.scdn.co/image/ab6765630000ba8a1d971613512218740199a755",
50 | color: "#74d0d6"
51 | },
52 | {
53 | name: "Sports",
54 | img: "https://api.wbez.org/v2/images/5f8278f7-6fbf-46b5-bdc1-dab0e31bf050.jpg?mode=FILL&width=696&height=696",
55 | color: "#7dba3c"
56 | },
57 | {
58 | name: "Crime",
59 | img: "https://images.squarespace-cdn.com/content/v1/5b6a11479f87707f6722bd01/1541786848970-Y0SCCZBCEY6OAE790VFB/MFM.jpg?format=1000w",
60 | color: "#6c4bb8"
61 | },
62 |
63 | ]
--------------------------------------------------------------------------------
/client/src/components/TopResult.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import styled from "styled-components";
3 | import {format} from 'timeago.js';
4 | import {Link} from 'react-router-dom';
5 |
6 | const SearchedCard = styled(Link)`
7 | width: 500px;
8 | display: flex;
9 | flex-direction: column;
10 | padding: 18px 18px 30px 18px;
11 | border-radius: 6px;
12 | gap: 12px;
13 | background-color: ${({ theme }) => theme.card};
14 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.1);
15 | cursor: pointer;
16 | text-decoration: none;
17 | &:hover{
18 | cursor: pointer;
19 | transform: translateY(-8px);
20 | transition: all 0.4s ease-in-out;
21 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.3);
22 | filter: brightness(1.3);
23 | }
24 | @media (max-width: 768px) {
25 | width: 290px;
26 | }
27 | `
28 | const PodcastImage = styled.img`
29 | object-fit:cover;
30 | width: 50%;
31 | border-radius: 6px;
32 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.3);
33 | `
34 | const PodcastTitle = styled.div`
35 | color: ${({ theme }) => theme.text_primary};
36 | display: -webkit-box;
37 | font-size: 24px;
38 | font-weight: 520;
39 |
40 | `
41 | const UploadInfo = styled.div`
42 | display: flex;
43 | width: 80%;
44 | gap: 12px;
45 |
46 | `
47 | const Time = styled.div`
48 | color: ${({ theme }) => theme.text_secondary};
49 | font-size: 14px;
50 | @media (max-width: 768px) {
51 | font-size: 12px;
52 | }
53 | @media (max-width: 560px) {
54 | font-size: 10px;
55 | }
56 | `
57 | const CreatorName = styled.div`
58 | color: ${({ theme }) => theme.text_primary};
59 | font-size: 14px;
60 | @media (max-width: 768px) {
61 | font-size: 12px;
62 | }
63 | @media (max-width: 560px) {
64 | font-size: 10px;
65 | }
66 | `
67 | const Description = styled.div`
68 | color: ${({ theme }) => theme.text_secondary};
69 | display: -webkit-box;
70 | max-width: 100%;
71 | font-size: 14px;
72 | -webkit-line-clamp: 3;
73 | -webkit-box-orient: vertical;
74 | overflow: hidden;
75 | text-overflow: ellipsis;
76 | `
77 | const TopResult = ({podcast}) => {
78 | return (
79 |
80 |
81 | {podcast?.name}
82 |
83 |
86 |
89 | {podcast?.creator.name}
90 |
91 | {podcast?.desc}
92 |
93 |
94 | );
95 | }
96 |
97 | export default TopResult;
--------------------------------------------------------------------------------
/client/src/api/index.js:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import jwt_decode from 'jwt-decode';
3 | //https://podstream.onrender.com/api
4 | const API = axios.create({ baseURL: `https://podstream-giym.onrender.com/api` });
5 |
6 | //auth
7 | export const signIn = async ({ email, password }) => await API.post('/auth/signin', { email, password });
8 | export const signUp = async ({
9 | name,
10 | email,
11 | password,
12 | }) => await API.post('/auth/signup', {
13 | name,
14 | email,
15 | password,
16 | });
17 | export const googleSignIn = async ({
18 | name,
19 | email,
20 | img,
21 | }) => await API.post('/auth/google', {
22 | name,
23 | email,
24 | img,
25 | });
26 | export const findUserByEmail = async (email) => await API.get(`/auth/findbyemail?email=${email}`);
27 | export const generateOtp = async (email,name,reason) => await API.get(`/auth/generateotp?email=${email}&name=${name}&reason=${reason}`);
28 | export const verifyOtp = async (otp) => await API.get(`/auth/verifyotp?code=${otp}`);
29 | export const resetPassword = async (email,password) => await API.put(`/auth/forgetpassword`,{email,password});
30 |
31 | //user api
32 | export const getUsers = async (token) => await API.get('/user', { headers: { "Authorization" : `Bearer ${token}` }},{
33 | withCredentials: true
34 | });
35 | export const searchUsers = async (search,token) => await API.get(`users/search/${search}`,{ headers: { "Authorization" : `Bearer ${token}` }},{ withCredentials: true });
36 |
37 |
38 | //podcast api
39 | export const createPodcast = async (podcast,token) => await API.post('/podcasts', podcast, { headers: { "Authorization" : `Bearer ${token}` } },{ withCredentials: true });
40 | export const getPodcasts = async () => await API.get('/podcasts');
41 | export const addEpisodes = async (podcast,token) => await API.post('/podcasts/episode', podcast, { headers: { "Authorization" : `Bearer ${token}` } },{ withCredentials: true });
42 | export const favoritePodcast = async (id,token) => await API.post(`/podcasts/favorit`,{id: id}, { headers: { "Authorization" : `Bearer ${token}` } },{ withCredentials: true });
43 | export const getRandomPodcast = async () => await API.get('/podcasts/random');
44 | export const getPodcastByTags = async (tags) => await API.get(`/podcasts/tags?tags=${tags}`);
45 | export const getPodcastByCategory = async (category) => await API.get(`/podcasts/category?q=${category}`);
46 | export const getMostPopularPodcast = async () => await API.get('/podcasts/mostpopular');
47 | export const getPodcastById = async (id) => await API.get(`/podcasts/get/${id}`);
48 | export const addView = async (id) => await API.post(`/podcasts/addview/${id}`);
49 | export const searchPodcast = async (search) => await API.get(`/podcasts/search?q=${search}`);
50 |
51 |
--------------------------------------------------------------------------------
/client/src/pages/Favourites.jsx:
--------------------------------------------------------------------------------
1 | import styled from 'styled-components';
2 | import React, { useState, useEffect } from 'react';
3 | import { useDispatch, useSelector } from 'react-redux';
4 | import { PodcastCard } from '../components/PodcastCard';
5 | import { getUsers } from '../api/index';
6 | import { CircularProgress } from '@mui/material';
7 |
8 | const Container = styled.div`
9 | padding: 20px 30px;
10 | padding-bottom: 200px;
11 | height: 100%;
12 | overflow-y: scroll;
13 | display: flex;
14 | flex-direction: column;
15 | gap: 20px;
16 | `
17 | const Topic = styled.div`
18 | color: ${({ theme }) => theme.text_primary};
19 | font-size: 24px;
20 | font-weight: 540;
21 | display: flex;
22 | justify-content: space-between;
23 | align-items: center;
24 | `;
25 |
26 | const FavouritesContainer = styled.div`
27 | display: flex;
28 | flex-wrap: wrap;
29 | gap: 14px;
30 | padding: 18px 6px;
31 | @media (max-width: 550px){
32 | justify-content: center;
33 | }
34 | `
35 |
36 | const Loader = styled.div`
37 | display: flex;
38 | justify-content: center;
39 | align-items: center;
40 | height: 100%;
41 | width: 100%;
42 | `
43 |
44 | const DisplayNo = styled.div`
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | height: 100%;
49 | width: 100%;
50 | color: ${({ theme }) => theme.text_primary};
51 | `
52 |
53 |
54 | const Favourites = () => {
55 | const [user, setUser] = useState();
56 | const [Loading, setLoading] = useState(false);
57 | const dispatch = useDispatch();
58 | //user
59 | const { currentUser } = useSelector(state => state.user);
60 |
61 | const token = localStorage.getItem("podstreamtoken");
62 | const getUser = async () => {
63 | await getUsers(token).then((res) => {
64 | setUser(res.data)
65 | }).then((error) => {
66 | console.log(error)
67 | });
68 | }
69 |
70 | const getuser = async () => {
71 |
72 | if (currentUser) {
73 | setLoading(true);
74 | await getUser();
75 | setLoading(false);
76 | }
77 | }
78 |
79 | useEffect(() => {
80 | getuser();
81 | }, [currentUser]);
82 |
83 | return (
84 |
85 |
86 | Favourites
87 |
88 | {Loading ?
89 |
90 |
91 |
92 | :
93 |
94 | {user?.favorits?.length === 0 && No Favourites}
95 | {user && user?.favorits.map((podcast) => (
96 |
97 | ))}
98 |
99 | }
100 |
101 | )
102 | }
103 |
104 | export default Favourites
--------------------------------------------------------------------------------
/client/src/components/Navbar.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import styled from "styled-components";
3 | import { Link } from 'react-router-dom';
4 | import { useDispatch, useSelector } from 'react-redux';
5 | import { Avatar } from '@mui/material';
6 | import PersonIcon from '@mui/icons-material/Person';
7 | import MenuIcon from "@mui/icons-material/Menu";
8 | import { IconButton } from "@mui/material";
9 | import { openSignin } from '../redux/setSigninSlice';
10 |
11 | const NavbarDiv = styled.div`
12 | display: flex;
13 | justify-content: space-between;
14 | width: 100%;
15 | padding: 16px 40px;
16 | align-items: center;
17 | box-sizing: border-box;
18 | color: ${({ theme }) => theme.text_primary};
19 | gap: 30px;
20 | background: ${({ theme }) => theme.bg}
21 | border-radius: 16px;
22 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
23 | backdrop-filter: blur(5.7px);
24 | -webkit-backdrop-filter: blur(5.7px);
25 | @media (max-width: 768px) {
26 | padding: 16px;
27 | }
28 |
29 | `;
30 | const ButtonDiv = styled.div`
31 | font-size: 14px;
32 | cursor: pointer;
33 | text-decoration: none;
34 | color: ${({ theme }) => theme.primary};
35 | border: 1px solid ${({ theme }) => theme.primary};
36 | border-radius: 12px;
37 | width: 100%;
38 | max-width: 70px;
39 | padding: 8px 10px;
40 | text-align: center;
41 | display: flex;
42 | align-items: center;
43 | justify-content: center;
44 | gap: 6px;
45 | &:hover{
46 | background-color: ${({ theme }) => theme.primary};
47 | color: ${({ theme }) => theme.text_primary};
48 | }
49 | `;
50 |
51 | const Welcome = styled.div`
52 | font-size: 26px;
53 | font-weight: 600;
54 | @media (max-width: 768px) {
55 | font-size: 16px;
56 | }
57 | `;
58 |
59 | const IcoButton = styled(IconButton)`
60 | color: ${({ theme }) => theme.text_secondary} !important;
61 | `;
62 |
63 |
64 | const Navbar = ({ menuOpen, setMenuOpen, setSignInOpen, setSignUpOpen }) => {
65 |
66 | const { currentUser } = useSelector(state => state.user);
67 | const dispatch = useDispatch();
68 |
69 | return (
70 |
71 | setMenuOpen(!menuOpen)}>
72 |
73 |
74 | {currentUser ?
75 |
76 | Welcome, {currentUser.name}
77 |
78 | :
79 | <> >}
80 | {
81 | currentUser ? <>
82 |
83 | {currentUser.name.charAt(0).toUpperCase()}
84 |
85 | >
86 | :
87 | dispatch(openSignin())}>
88 |
89 | Login
90 |
91 | }
92 |
93 | )
94 | }
95 |
96 | export default Navbar
--------------------------------------------------------------------------------
/client/README.md:
--------------------------------------------------------------------------------
1 | # Getting Started with Create React App
2 |
3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
4 |
5 | ## Available Scripts
6 |
7 | In the project directory, you can run:
8 |
9 | ### `npm start`
10 |
11 | Runs the app in the development mode.\
12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
13 |
14 | The page will reload when you make changes.\
15 | You may also see any lint errors in the console.
16 |
17 | ### `npm test`
18 |
19 | Launches the test runner in the interactive watch mode.\
20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
21 |
22 | ### `npm run build`
23 |
24 | Builds the app for production to the `build` folder.\
25 | It correctly bundles React in production mode and optimizes the build for the best performance.
26 |
27 | The build is minified and the filenames include the hashes.\
28 | Your app is ready to be deployed!
29 |
30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
31 |
32 | ### `npm run eject`
33 |
34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!**
35 |
36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
37 |
38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own.
39 |
40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it.
41 |
42 | ## Learn More
43 |
44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
45 |
46 | To learn React, check out the [React documentation](https://reactjs.org/).
47 |
48 | ### Code Splitting
49 |
50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
51 |
52 | ### Analyzing the Bundle Size
53 |
54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
55 |
56 | ### Making a Progressive Web App
57 |
58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
59 |
60 | ### Advanced Configuration
61 |
62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
63 |
64 | ### Deployment
65 |
66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
67 |
68 | ### `npm run build` fails to minify
69 |
70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)
71 |
--------------------------------------------------------------------------------
/client/src/components/Episodecard.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { useDispatch } from 'react-redux';
3 | import styled from 'styled-components'
4 | import { closePlayer, openPlayer } from '../redux/audioplayerSlice';
5 | import { addView } from '../api';
6 | import { openSnackbar } from '../redux/snackbarSlice';
7 | import PlayCircleOutlineIcon from '@mui/icons-material/PlayCircleOutline';
8 |
9 | const Card = styled.div`
10 | display: flex;
11 | flex-direction: row;
12 | gap: 20px;
13 | align-items: center;
14 | padding: 20px 30px;
15 | border-radius: 6px;
16 | background-color: ${({ theme }) => theme.card};
17 | cursor: pointer;
18 | &:hover{
19 | cursor: pointer;
20 | transform: translateY(-8px);
21 | transition: all 0.4s ease-in-out;
22 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.3);
23 | filter: brightness(1.3);
24 | }
25 | @media (max-width: 768px){
26 | flex-direction: column;
27 | align-items: flex-start;
28 | }
29 | `;
30 |
31 | const Image = styled.img`
32 | width: 100px;
33 | height: 100px;
34 | border-radius: 6px;
35 | background-color: ${({ theme }) => theme.text_secondary};
36 | object-fit: cover;
37 | `;
38 |
39 | const Details = styled.div`
40 | display: flex;
41 | flex-direction: column;
42 | gap: 10px;
43 | width: 100%;
44 | `;
45 |
46 | const Title = styled.div`
47 | font-size: 18px;
48 | font-weight: 800;
49 | color: ${({ theme }) => theme.text_primary};
50 | width: 100%;
51 | display: flex;
52 | justify-content: space-between;
53 | `;
54 |
55 | const Description = styled.div`
56 | font-size: 14px;
57 | font-weight: 500;
58 | color: ${({ theme }) => theme.text_secondary};
59 | `;
60 | const ImageContainer = styled.div`
61 | position: relative;
62 | width: 100px;
63 | height: 100px;
64 | `;
65 |
66 |
67 | const Episodecard = ({ episode, podid, user, type, index }) => {
68 | const dispatch = useDispatch();
69 |
70 | const addviewtToPodcast = async () => {
71 | await addView(podid._id).catch((err) => {
72 | dispatch(
73 | openSnackbar({
74 | message: err.message,
75 | type: "error",
76 | })
77 | );
78 | });
79 |
80 | }
81 |
82 | return (
83 | {
84 | await addviewtToPodcast();
85 | if (type === "audio") {
86 | //open audio player
87 | dispatch(
88 | openPlayer({
89 | type: "audio",
90 | episode: episode,
91 | podid: podid,
92 | index: index,
93 | currenttime: 0
94 | })
95 | )
96 | } else {
97 | //open video player
98 | dispatch(
99 | dispatch(
100 | openPlayer({
101 | type: "video",
102 | episode: episode,
103 | podid: podid,
104 | index: index,
105 | currenttime: 0
106 | })
107 | )
108 | )
109 | }
110 | }}>
111 |
112 |
113 |
114 |
115 |
116 | {episode.name}
117 | {episode.desc}
118 |
119 |
120 | )
121 | }
122 |
123 | export default Episodecard
--------------------------------------------------------------------------------
/client/src/pages/DisplayPodcasts.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react'
2 | import { useParams } from 'react-router-dom';
3 | import { getPodcastByCategory, getMostPopularPodcast } from '../api/index.js';
4 | import styled from 'styled-components';
5 | import { PodcastCard } from '../components/PodcastCard.jsx';
6 | import { useDispatch } from "react-redux";
7 | import { openSnackbar } from "../redux/snackbarSlice";
8 | import { displayPodcastFailure } from '../redux/userSlice.jsx';
9 | import { CircularProgress } from '@mui/material';
10 |
11 | const DisplayMain = styled.div`
12 | display: flex;
13 | padding: 30px 30px;
14 | flex-direction: column;
15 | height: 100%;
16 | overflow-y: scroll;
17 | `
18 | const Topic = styled.div`
19 | color: ${({ theme }) => theme.text_primary};
20 | font-size: 24px;
21 | font-weight: 540;
22 | display: flex;
23 | justify-content: space-between;
24 | align-items: center;
25 | `;
26 | const Podcasts = styled.div`
27 | display: flex;
28 | flex-wrap: wrap;
29 | height: 100%;
30 | gap: 10px;
31 | padding: 30px 0px;
32 | `
33 | const Container = styled.div`
34 | background-color: ${({ theme }) => theme.bg};
35 | padding: 20px;
36 | border-radius: 6px;
37 | min-height: 400px;
38 | `
39 |
40 | const Loader = styled.div`
41 | display: flex;
42 | justify-content: center;
43 | align-items: center;
44 | height: 100%;
45 | width: 100%;
46 | `
47 | const DisplayNo = styled.div`
48 | display: flex;
49 | justify-content: center;
50 | align-items: center;
51 | height: 100%;
52 | width: 100%;
53 | color: ${({ theme }) => theme.text_primary};
54 | `
55 |
56 |
57 |
58 |
59 | const DisplayPodcasts = () => {
60 | const { type } = useParams();
61 | const [podcasts, setPodcasts] = useState([]);
62 | const [string, setString] = useState("");
63 | const dispatch = useDispatch();
64 | const [Loading, setLoading] = useState(false);
65 |
66 | const mostPopular = async () => {
67 | await getMostPopularPodcast()
68 | .then((res) => {
69 | setPodcasts(res.data)
70 | })
71 | .catch((err) => {
72 | dispatch(
73 | openSnackbar({
74 | message: err.message,
75 | severity: "error",
76 | })
77 | );
78 | });
79 | }
80 | const getCategory = async () => {
81 | await getPodcastByCategory(type)
82 | .then((res) => {
83 | setPodcasts(res.data)
84 | })
85 | .catch((err) => {
86 | dispatch(
87 | openSnackbar({
88 | message: err.message,
89 | severity: "error",
90 | })
91 | );
92 | });
93 |
94 | }
95 |
96 | const getallpodcasts = async () => {
97 | if (type === 'mostpopular') {
98 | setLoading(true);
99 | let arr = type.split("");
100 | arr[0] = arr[0].toUpperCase();
101 | arr.splice(4, 0, " ");
102 | setString(arr.join(""));
103 | console.log(string);
104 | await mostPopular();
105 | setLoading(false);
106 | }
107 | else {
108 | setLoading(true);
109 | let arr = type.split("");
110 | arr[0] = arr[0].toUpperCase();
111 | setString(arr);
112 | await getCategory();
113 | setLoading(false);
114 | }
115 | }
116 |
117 | useEffect(() => {
118 | getallpodcasts();
119 |
120 | }, [])
121 | return (
122 |
123 |
124 | {string}
125 | {Loading ?
126 |
127 |
128 |
129 | :
130 |
131 | {podcasts.length === 0 && No Podcasts}
132 | {podcasts.map((podcast) => (
133 |
134 | ))}
135 |
136 | }
137 |
138 |
139 | )
140 | }
141 |
142 | export default DisplayPodcasts
--------------------------------------------------------------------------------
/client/src/App.js:
--------------------------------------------------------------------------------
1 | import { ThemeProvider } from "styled-components";
2 | import { useState, useEffect } from "react";
3 | import { darkTheme, lightTheme } from './utils/Themes.js'
4 | import Signup from '../src/components/Signup.jsx';
5 | import Signin from '../src/components/Signin.jsx';
6 | import OTP from '../src/components/OTP.jsx'
7 | import Navbar from '../src/components/Navbar.jsx';
8 | import Menu from '../src/components/Menu.jsx';
9 | import Dashboard from '../src/pages/Dashboard.jsx'
10 | import ToastMessage from './components/ToastMessage.jsx';
11 | import Search from '../src/pages/Search.jsx';
12 | import Favourites from '../src/pages/Favourites.jsx';
13 | import Profile from '../src/pages/Profile.jsx';
14 | import Upload from '../src/components/Upload.jsx';
15 | import DisplayPodcasts from '../src/pages/DisplayPodcasts.jsx';
16 | import { BrowserRouter, Routes, Route } from 'react-router-dom'
17 | import { useDispatch, useSelector } from "react-redux";
18 | import styled from 'styled-components';
19 | import AudioPlayer from "./components/AudioPlayer.jsx";
20 | import VideoPlayer from "./components/VideoPlayer.jsx";
21 | import PodcastDetails from "./pages/PodcastDetails.jsx";
22 | import { closeSignin } from "./redux/setSigninSlice.jsx";
23 |
24 | const Frame = styled.div`
25 | display: flex;
26 | flex-direction: column;
27 | flex: 3;
28 | `;
29 |
30 | const Podstream = styled.div`
31 | display: flex;
32 | flex-direction: row;
33 | width: 100%;
34 | height: 100vh;
35 | background: ${({ theme }) => theme.bgLight};
36 | overflow-y: hidden;
37 | overflow-x: hidden;
38 | `;
39 |
40 | function App() {
41 |
42 | const [darkMode, setDarkMode] = useState(true);
43 | const { open, message, severity } = useSelector((state) => state.snackbar);
44 | const {openplayer,type, episode, podid, currenttime,index} = useSelector((state) => state.audioplayer);
45 | const {opensi} = useSelector((state) => state.signin);
46 | const [SignUpOpen, setSignUpOpen] = useState(false);
47 | const [SignInOpen, setSignInOpen] = useState(false);
48 | const [menuOpen, setMenuOpen] = useState(true);
49 | const [uploadOpen, setUploadOpen] = useState(false);
50 |
51 |
52 | const { currentUser } = useSelector(state => state.user);
53 | const dispatch = useDispatch()
54 | //set the menuOpen state to false if the screen size is less than 768px
55 | useEffect(() => {
56 | const resize = () => {
57 | if (window.innerWidth < 1110) {
58 | setMenuOpen(false);
59 | } else {
60 | setMenuOpen(true);
61 | }
62 | }
63 | resize();
64 | window.addEventListener("resize", resize);
65 | return () => window.removeEventListener("resize", resize);
66 | }, []);
67 |
68 | useEffect(()=>{
69 | dispatch(
70 | closeSignin()
71 | )
72 | },[])
73 |
74 | return (
75 |
76 |
77 |
78 |
79 | {opensi && }
80 | {SignUpOpen && }
81 | {uploadOpen && }
82 | {openplayer && type === 'video' && }
83 | {openplayer && type === 'audio' && }
84 |
85 | {menuOpen &&
101 |
102 |
103 |
104 |
105 |
106 | );
107 | }
108 |
109 | export default App;
110 |
--------------------------------------------------------------------------------
/client/src/pages/Profile.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from "react";
2 | import { useSelector } from 'react-redux';
3 | import styled from "styled-components";
4 | import Avatar from '@mui/material/Avatar';
5 | import { getUsers } from '../api/index';
6 | import { PodcastCard } from '../components/PodcastCard.jsx'
7 |
8 | const ProfileAvatar = styled.div`
9 | padding-left:3rem;
10 | @media (max-width: 768px) {
11 | padding-left:0rem;
12 | }
13 | `
14 | const ProfileContainer = styled.div`
15 | display: flex;
16 | flex-direction: column;
17 | justify-content: center;
18 | @media (max-width: 768px) {
19 | align-items: center;
20 | }
21 | `
22 | const ProfileName = styled.div`
23 | color: ${({ theme }) => theme.text_primary};
24 | font-size:34px;
25 | font-weight:500;
26 | `
27 | const Profile_email = styled.div`
28 | color:#2b6fc2;
29 | font-size:14px;
30 | font-weight:400;
31 | `
32 | const FilterContainer = styled.div`
33 | display: flex;
34 | flex-direction: column;
35 | justify-content: flex-start;
36 | ${({ box, theme }) => box && `
37 | background-color: ${theme.bg};
38 | border-radius: 10px;
39 | padding: 20px 30px;
40 | `}
41 | `;
42 | const Topic = styled.div`
43 | color: ${({ theme }) => theme.text_primary};
44 | font-size: 24px;
45 | font-weight: 540;
46 | display: flex;
47 | justify-content: space-between;
48 | align-items: center;
49 | `;
50 | const Span = styled.span`
51 | color: ${({ theme }) => theme.text_secondary};
52 | font-size: 16px;
53 | font-weight: 400;
54 | cursor: pointer;
55 | color: ${({ theme }) => theme.primary};
56 | &:hover{
57 | transition: 0.2s ease-in-out;
58 | }
59 | `;
60 | const Podcasts = styled.div`
61 | display: flex;
62 | flex-wrap: wrap;
63 | gap: 14px;
64 | padding: 18px 6px;
65 | @media (max-width: 550px){
66 | justify-content: center;
67 | }
68 | `;
69 | const ProfileMain = styled.div`
70 | padding: 20px 30px;
71 | padding-bottom: 200px;
72 | height: 100%;
73 | overflow-y: scroll;
74 | display: flex;
75 | flex-direction: column;
76 | gap: 20px;
77 | `
78 | const UserDetails = styled.div`
79 | display flex;
80 | gap: 120px;
81 | @media (max-width: 768px) {
82 | width: fit-content;
83 | flex-direction: column;
84 | gap: 20px;
85 | justify-content: center;
86 | align-items: center;
87 | }
88 | `
89 | const Container = styled.div`
90 | display: flex;
91 | justify-content: center;
92 | align-items: center;
93 | `
94 | const ButtonContainer = styled.div`
95 | font-size: 14px;
96 | cursor: pointer;
97 | text-decoration: none;
98 | color: ${({ theme }) => theme.primary};
99 | border: 1px solid ${({ theme }) => theme.primary};
100 | border-radius: 12px;
101 | width: 100%;
102 | max-width: 70px;
103 | padding: 8px 10px;
104 | text-align: center;
105 | display: flex;
106 | align-items: center;
107 | justify-content: center;
108 | gap: 6px;
109 | &:hover{
110 | background-color: ${({ theme }) => theme.primary};
111 | color: ${({ theme }) => theme.text_primary};
112 | }
113 | `
114 |
115 | const Profile = () => {
116 |
117 | const [user, setUser] = useState();
118 | const { currentUser } = useSelector(state => state.user);
119 | const [name, setName] = useState("");
120 |
121 | const token = localStorage.getItem("podstreamtoken");
122 | const getUser = async () => {
123 | await getUsers(token).then((res) => {
124 | setUser(res.data)
125 | setName(res.data.name);
126 | }).catch((error) => {
127 | console.log(error)
128 | });
129 | }
130 |
131 | useEffect(() => {
132 | if (currentUser) {
133 | getUser();
134 | // setName(user?.name.split("")[0].toUpperCase());
135 | }
136 | }, [currentUser])
137 |
138 |
139 | return (
140 |
141 |
142 |
143 | {user?.name.charAt(0).toUpperCase()}
144 |
145 |
146 |
147 | {name}
148 | Email: {user?.email}
149 |
150 |
151 | {currentUser && user?.podcasts.length > 0 &&
152 |
153 | Your Uploads
154 |
155 |
156 | {user?.podcasts.map((podcast) => (
157 |
158 | ))}
159 |
160 |
161 |
162 | }
163 | {currentUser && user?.podcasts.length === 0 &&
164 |
165 | Your Uploads
166 |
167 |
168 | Upload
169 |
170 |
171 | }
172 |
173 | Your Favourites
174 |
175 |
176 | {user && user?.favorits.map((podcast) => (
177 |
178 | ))}
179 |
180 |
181 |
182 | );
183 | }
184 |
185 | export default Profile;
--------------------------------------------------------------------------------
/client/src/pages/Search.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import styled from 'styled-components'
3 | import SearchOutlinedIcon from '@mui/icons-material/SearchOutlined';
4 | import { DefaultCard } from '../components/DefaultCard.jsx';
5 | import { Category } from '../utils/Data.js';
6 | import { searchPodcast } from '../api/index.js';
7 | import { PodcastCard } from '../components/PodcastCard.jsx';
8 | import TopResult from '../components/TopResult.jsx';
9 | import MoreResult from '../components/MoreResult.jsx';
10 | import { Link } from 'react-router-dom';
11 | import { useDispatch } from 'react-redux';
12 | import { openSnackbar } from '../redux/snackbarSlice.jsx';
13 | import { CircularProgress } from '@mui/material';
14 |
15 | const SearchMain = styled.div`
16 | padding: 20px 30px;
17 | padding-bottom: 200px;
18 | height: 100%;
19 | overflow-y: scroll;
20 | overflow-x: hidden;
21 | display: flex;
22 | flex-direction: column;
23 | gap: 20px;
24 | @media (max-width: 768px) {
25 | padding: 20px 9px;
26 | }
27 |
28 | `;
29 | const Heading = styled.div`
30 | align-items: flex-start;
31 | color: ${({ theme }) => theme.text_primary};
32 | font-size: 22px;
33 | font-weight: 540;
34 | margin: 10px 14px;
35 | `;
36 | const BrowseAll = styled.div`
37 | display: flex;
38 | flex-wrap: wrap;
39 | gap: 20px;
40 | padding: 14px;
41 | `;
42 | const SearchedCards = styled.div`
43 | display: flex;
44 | flex-direction: row;
45 | align-items: flex-start;
46 | gap: 20px;
47 | padding: 14px;
48 | @media (max-width: 768px) {
49 | flex-direction: column;
50 | justify-content: center;
51 | padding: 6px;
52 | }
53 | `;
54 | const Categories = styled.div`
55 | margin: 20px 10px;
56 | `;
57 | const Search_whole = styled.div`
58 | max-width: 700px;
59 | display:flex;
60 | width: 100%;
61 | border: 1px solid ${({ theme }) => theme.text_secondary};
62 | border-radius:30px;
63 | cursor:pointer;
64 | padding:12px 16px;
65 | justify-content: flex-start;
66 | align-items: center;
67 | gap: 6px;
68 | color: ${({ theme }) => theme.text_secondary};
69 | `;
70 | const OtherResults = styled.div`
71 | display: flex;
72 | flex-direction: column;
73 | height: 700px;
74 | overflow-y: scroll;
75 | overflow-x: hidden;
76 | gap: 6px;
77 | padding: 4px 4px;
78 | @media (max-width: 768px) {
79 | height: 100%;
80 | padding: 4px 0px;
81 | }
82 | `;
83 |
84 | const Loader = styled.div`
85 | display: flex;
86 | justify-content: center;
87 | align-items: center;
88 | height: 100%;
89 | width: 100%;
90 | `
91 | const DisplayNo = styled.div`
92 | display: flex;
93 | justify-content: center;
94 | align-items: center;
95 | height: 100%;
96 | width: 100%;
97 | color: ${({ theme }) => theme.text_primary};
98 | `
99 |
100 | const Search = () => {
101 |
102 | const [searched, setSearched] = useState("");
103 | const [searchedPodcasts, setSearchedPodcasts] = useState([]);
104 | const [loading, setLoading] = useState(false);
105 | const dispatch = useDispatch();
106 | const handleChange = async (e) => {
107 | setSearchedPodcasts([]);
108 | setLoading(true);
109 | setSearched(e.target.value);
110 | await searchPodcast(e.target.value)
111 | .then((res) => {
112 | setSearchedPodcasts(res.data);
113 | console.log(res.data);
114 | })
115 | .catch((err) => {
116 | dispatch(
117 | openSnackbar({
118 | message: err.message,
119 | severity: "error",
120 | })
121 | );
122 | });
123 | setLoading(false);
124 | }
125 |
126 | return (
127 |
128 |
129 |
130 |
131 | handleChange(e)} />
135 |
136 |
137 |
138 | {searched === "" ?
139 |
140 | Browse All
141 |
142 | {Category.map((category) => (
143 |
144 |
145 |
146 | ))}
147 |
148 |
149 | :
150 | <>
151 | {loading ?
152 |
153 |
154 |
155 | :
156 |
157 | {searchedPodcasts.length === 0 ?
158 | No Podcasts Found
159 | :
160 | <>
161 |
162 |
163 | {searchedPodcasts.map((podcast) => (
164 |
165 | ))}
166 |
167 | >
168 | }
169 |
170 | }
171 | >
172 | }
173 |
174 | )
175 | }
176 |
177 | export default Search
--------------------------------------------------------------------------------
/server/controllers/podcasts.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import { createError } from "../error.js";
3 | import Podcasts from "../models/Podcasts.js";
4 | import Episodes from "../models/Episodes.js";
5 | import User from "../models/User.js";
6 |
7 |
8 | export const createPodcast = async (req, res, next) => {
9 | try {
10 | const user = await User.findById(req.user.id);
11 |
12 | let episodeList = []
13 | await Promise.all(req.body.episodes.map(async (item) => {
14 |
15 | const episode = new Episodes(
16 | { creator: user.id, ...item }
17 | );
18 | const savedEpisode = await episode.save();
19 | episodeList.push(savedEpisode._id)
20 | }));
21 |
22 | // Create a new podcast
23 | const podcast = new Podcasts(
24 | {
25 | creator: user.id, episodes: episodeList,
26 | name: req.body.name,
27 | desc: req.body.desc,
28 | thumbnail: req.body.thumbnail,
29 | tags: req.body.tags,
30 | type: req.body.type,
31 | category: req.body.category
32 | }
33 | );
34 | const savedPodcast = await podcast.save();
35 |
36 | //save the podcast to the user
37 | await User.findByIdAndUpdate(user.id, {
38 | $push: { podcasts: savedPodcast.id },
39 |
40 | }, { new: true });
41 |
42 | res.status(201).json(savedPodcast);
43 | } catch (err) {
44 | next(err);
45 | }
46 | };
47 |
48 | export const addepisodes = async (req, res, next) => {
49 | try {
50 | const user = await User.findById(req.user.id);
51 |
52 | await Promise.all(req.body.episodes.map(async (item) => {
53 |
54 | const episode = new Episodes(
55 | { creator: user.id, ...item }
56 | );
57 | const savedEpisode = await episode.save();
58 |
59 |
60 | // update the podcast
61 | await Podcasts.findByIdAndUpdate(
62 | req.body.podid, {
63 | $push: { episodes: savedEpisode.id },
64 |
65 | }, { new: true }
66 | )
67 | }));
68 |
69 | res.status(201).json({ message: "Episode added successfully" });
70 |
71 | } catch (err) {
72 | next(err);
73 | }
74 | }
75 |
76 |
77 |
78 | export const getPodcasts = async (req, res, next) => {
79 | try {
80 | // Get all podcasts from the database
81 | const podcasts = await Podcasts.find().populate("creator", "name img").populate("episodes");
82 | return res.status(200).json(podcasts);
83 | } catch (err) {
84 | next(err);
85 | }
86 | };
87 |
88 | export const getPodcastById = async (req, res, next) => {
89 | try {
90 | // Get the podcasts from the database
91 | const podcast = await Podcasts.findById(req.params.id).populate("creator", "name img").populate("episodes");
92 | return res.status(200).json(podcast);
93 | } catch (err) {
94 | next(err);
95 | }
96 | };
97 |
98 | export const favoritPodcast = async (req, res, next) => {
99 | // Check if the user is the creator of the podcast
100 | const user = await User.findById(req.user.id);
101 | const podcast = await Podcasts.findById(req.body.id);
102 | let found = false;
103 | if (user.id === podcast.creator) {
104 | return next(createError(403, "You can't favorit your own podcast!"));
105 | }
106 |
107 | // Check if the podcast is already in the user's favorits
108 | await Promise.all(user.favorits.map(async (item) => {
109 | if (req.body.id == item) {
110 | //remove from favorite
111 | found = true;
112 | console.log("this")
113 | await User.findByIdAndUpdate(user.id, {
114 | $pull: { favorits: req.body.id },
115 |
116 | }, { new: true })
117 | res.status(200).json({ message: "Removed from favorit" });
118 |
119 | }
120 | }));
121 |
122 |
123 | if (!found) {
124 | await User.findByIdAndUpdate(user.id, {
125 | $push: { favorits: req.body.id },
126 |
127 | }, { new: true });
128 | res.status(200).json({ message: "Added to favorit" });
129 | }
130 | }
131 |
132 | //add view
133 |
134 | export const addView = async (req, res, next) => {
135 | try {
136 | await Podcasts.findByIdAndUpdate(req.params.id, {
137 | $inc: { views: 1 },
138 | });
139 | res.status(200).json("The view has been increased.");
140 | } catch (err) {
141 | next(err);
142 | }
143 | };
144 |
145 |
146 |
147 | //searches
148 | export const random = async (req, res, next) => {
149 | try {
150 | const podcasts = await Podcasts.aggregate([{ $sample: { size: 40 } }]).populate("creator", "name img").populate("episodes");
151 | res.status(200).json(podcasts);
152 | } catch (err) {
153 | next(err);
154 | }
155 | };
156 |
157 | export const mostpopular = async (req, res, next) => {
158 | try {
159 | const podcast = await Podcasts.find().sort({ views: -1 }).populate("creator", "name img").populate("episodes");
160 | res.status(200).json(podcast);
161 | } catch (err) {
162 | next(err);
163 | }
164 | };
165 |
166 | export const getByTag = async (req, res, next) => {
167 | const tags = req.query.tags.split(",");
168 | try {
169 | const podcast = await Podcasts.find({ tags: { $in: tags } }).populate("creator", "name img").populate("episodes");
170 | res.status(200).json(podcast);
171 | } catch (err) {
172 | next(err);
173 | }
174 | };
175 |
176 | export const getByCategory = async (req, res, next) => {
177 | const query = req.query.q;
178 | try {
179 | const podcast = await Podcasts.find({
180 |
181 | category: { $regex: query, $options: "i" },
182 | }).populate("creator", "name img").populate("episodes");
183 | res.status(200).json(podcast);
184 | } catch (err) {
185 | next(err);
186 | }
187 | };
188 |
189 | export const search = async (req, res, next) => {
190 | const query = req.query.q;
191 | try {
192 | const podcast = await Podcasts.find({
193 | name: { $regex: query, $options: "i" },
194 | }).populate("creator", "name img").populate("episodes").limit(40);
195 | res.status(200).json(podcast);
196 | } catch (err) {
197 | next(err);
198 | }
199 | };
--------------------------------------------------------------------------------
/client/src/components/VideoPlayer.jsx:
--------------------------------------------------------------------------------
1 | import { CloseRounded } from '@mui/icons-material';
2 | import { Modal } from '@mui/material';
3 | import React, { useRef, useState } from 'react';
4 | import styled from 'styled-components';
5 | import PlayArrowIcon from '@mui/icons-material/PlayArrow';
6 | import PauseIcon from '@mui/icons-material/Pause';
7 | import VolumeUpIcon from '@mui/icons-material/VolumeUp';
8 | import { useDispatch } from 'react-redux';
9 | import { closePlayer, openPlayer, setCurrentTime } from '../redux/audioplayerSlice';
10 | import { openSnackbar } from '../redux/snackbarSlice';
11 |
12 |
13 | const Container = styled.div`
14 | width: 100%;
15 | height: 100%;
16 | position: absolute;
17 | top: 0;
18 | left: 0;
19 | background-color: #000000a7;
20 | display: flex;
21 | align-items: top;
22 | justify-content: center;
23 | overflow-y: scroll;
24 | transition: all 0.5s ease;
25 | `;
26 |
27 | const Wrapper = styled.div`
28 | max-width: 800px;
29 | width: 100%;
30 | border-radius: 16px;
31 | margin: 50px 20px;
32 | height: min-content;
33 | background-color: ${({ theme }) => theme.card};
34 | color: ${({ theme }) => theme.text_primary};
35 | padding: 10px;
36 | display: flex;
37 | flex-direction: column;
38 | position: relative;
39 | padding: 10px;
40 | `;
41 |
42 | const Title = styled.div`
43 | font-size: 22px;
44 | font-weight: 500;
45 | color: ${({ theme }) => theme.text_primary};
46 | margin: 12px 20px;
47 | `;
48 |
49 | const Videoplayer = styled.video`
50 | height: 100%;
51 | max-height: 500px;
52 | border-radius: 16px;
53 | margin: 0px 20px;
54 | object-fit: cover;
55 | margin-top: 30px;
56 | `;
57 |
58 | const EpisodeName = styled.div`
59 | font-size: 18px;
60 | font-weight: 500;
61 | color: ${({ theme }) => theme.text_primary};
62 | margin: 12px 20px 0px 20px;
63 | `;
64 | const EpisodeDescription = styled.div`
65 | font-size: 14px;
66 | font-weight: 400;
67 | color: ${({ theme }) => theme.text_secondary};
68 | margin: 6px 20px 20px 20px;
69 | `;
70 |
71 | const BtnContainer = styled.div`
72 | display: flex;
73 | justify-content: space-between;
74 | margin: 12px 20px 20px 20px;
75 | gap: 14px;
76 | `;
77 |
78 | const Btn = styled.div`
79 | border: none;
80 | font-size: 22px;
81 | font-weight: 600;
82 | text-align: center;
83 | width: 100%;
84 | background-color: ${({ theme }) => theme.primary};
85 | color: ${({ theme }) => theme.text_primary};
86 | padding: 14px 16px;
87 | border-radius: 8px;
88 | cursor: pointer;
89 | font-size: 14px;
90 | font-weight: 500;
91 | &:hover {
92 | background-color: ${({ theme }) => theme.card_hover};
93 | }
94 | `;
95 |
96 |
97 | const VideoPlayer = ({ episode, podid, currenttime, index }) => {
98 | const dispatch = useDispatch();
99 | const videoref = useRef(null);
100 |
101 | const handleTimeUpdate = () => {
102 | const currentTime = videoref.current.currentTime;
103 | dispatch(
104 | setCurrentTime({
105 | currenttime: currentTime
106 | })
107 | )
108 | }
109 |
110 | const goToNextPodcast = () => {
111 | //from the podid and index, get the next podcast
112 | //dispatch the next podcast
113 | if (podid.episodes.length === index + 1) {
114 | dispatch(
115 | openSnackbar({
116 | message: "This is the last episode",
117 | severity: "info",
118 | })
119 | )
120 | return
121 | }
122 | dispatch(closePlayer());
123 | setTimeout(() => {
124 | dispatch(
125 | openPlayer({
126 | type: "video",
127 | podid: podid,
128 | index: index + 1,
129 | currenttime: 0,
130 | episode: podid.episodes[index + 1]
131 | })
132 | )
133 | }, 10);
134 | }
135 |
136 | const goToPreviousPodcast = () => {
137 | //from the podid and index, get the next podcast
138 | //dispatch the next podcast
139 | if (index === 0) {
140 | dispatch(
141 | openSnackbar({
142 | message: "This is the first episode",
143 | severity: "info",
144 | })
145 | )
146 | return;
147 | }
148 | dispatch(closePlayer());
149 | setTimeout(() => {
150 | dispatch(
151 | openPlayer({
152 | type: "video",
153 | podid: podid,
154 | index: index - 1,
155 | currenttime: 0,
156 | episode: podid.episodes[index - 1]
157 | })
158 | )
159 | }, 10);
160 | }
161 |
162 | return (
163 |
164 | dispatch(closePlayer())
165 | }>
166 |
167 |
168 | {
176 | dispatch(closePlayer());
177 | }}
178 | />
179 | goToNextPodcast()}
183 | autoPlay
184 | onPlay={() => {videoref.current.currentTime = currenttime}}
185 | >
186 |
187 |
188 |
189 | Your browser does not support the video tag.
190 |
191 | {episode.name}
192 | {episode.desc}
193 |
194 | goToPreviousPodcast()}>
195 | Previous
196 |
197 | goToNextPodcast()}>
198 | Next
199 |
200 |
201 |
202 |
203 |
204 | )
205 | }
206 |
207 | export default VideoPlayer
--------------------------------------------------------------------------------
/client/src/components/Menu.jsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import styled from 'styled-components'
3 | import { useDispatch } from "react-redux";
4 | import { useNavigate } from 'react-router-dom';
5 | import { logout } from "../redux/userSlice";
6 | import { Link } from 'react-router-dom'
7 | import { useSelector } from 'react-redux';
8 | import HomeRoundedIcon from '@mui/icons-material/HomeRounded';
9 | import SearchRoundedIcon from '@mui/icons-material/SearchRounded';
10 | import FavoriteRoundedIcon from '@mui/icons-material/FavoriteRounded';
11 | import BackupRoundedIcon from '@mui/icons-material/BackupRounded';
12 | import LightModeRoundedIcon from '@mui/icons-material/LightModeRounded';
13 | import DarkModeRoundedIcon from '@mui/icons-material/DarkModeRounded';
14 | import ExitToAppRoundedIcon from '@mui/icons-material/ExitToAppRounded';
15 | import CloseRounded from '@mui/icons-material/CloseRounded';
16 | import LogoIcon from '../Images/Logo.png'
17 | import { openSignin } from '../redux/setSigninSlice';
18 |
19 | const MenuContainer = styled.div`
20 | flex: 0.5;
21 | flex-direction: column;
22 | height: 100vh;
23 | display: flex;
24 | box-sizing: border-box;
25 | align-items: flex-start;
26 | background-color: ${({ theme }) => theme.bg};
27 | color: ${({ theme }) => theme.text_primary};
28 | @media (max-width: 1100px) {
29 | position: fixed;
30 | z-index: 1000;
31 | width: 100%;
32 | max-width: 250px;
33 | left: ${({ setMenuOpen }) => (setMenuOpen ? "0" : "-100%")};
34 | transition: 0.3s ease-in-out;
35 | }
36 | `;
37 | const Elements = styled.div`
38 | padding: 4px 16px;
39 | display: flex;
40 | flex-direction: row;
41 | box-sizing: border-box;
42 | justify-content: flex-start;
43 | align-items: center;
44 | gap: 12px;
45 | cursor: pointer;
46 | color: ${({ theme }) => theme.text_secondary};
47 | width: 100%;
48 | &:hover{
49 | background-color: ${({ theme }) => theme.text_secondary + 50};
50 | }
51 | `;
52 | const NavText = styled.div`
53 | padding: 12px 0px;
54 | `;
55 | const HR = styled.div`
56 | width: 100%;
57 | height: 1px;
58 | background-color: ${({ theme }) => theme.text_secondary + 50};
59 | margin: 10px 0px;
60 | `;
61 | const Flex = styled.div`
62 | justify-content: space-between;
63 | display: flex;
64 | align-items: center;
65 | padding: 0px 16px;
66 | width: 86%;
67 | `;
68 | const Close = styled.div`
69 | display: none;
70 | @media (max-width: 1100px) {
71 | display: block;
72 |
73 | }
74 | `;
75 | const Logo = styled.div`
76 | color: ${({ theme }) => theme.primary};
77 | display: flex;
78 | align-items: center;
79 | justify-content: center;
80 | gap: 6px;
81 | font-weight: bold;
82 | font-size: 20px;
83 | margin: 16px 0px;
84 | `;
85 | const Image = styled.img`
86 | height: 40px;
87 | `;
88 | const Menu = ({ setMenuOpen, darkMode, setDarkMode, setUploadOpen }) => {
89 |
90 | const dispatch = useDispatch();
91 | const navigate = useNavigate();
92 | const { currentUser } = useSelector(state => state.user);
93 | const logoutUser = () => {
94 | dispatch(logout());
95 | navigate(`/`);
96 | };
97 |
98 | return (
99 |
100 |
101 |
102 |
103 |
104 | PODSTREAM
105 |
106 |
107 |
108 | setMenuOpen(false)} style={{ cursor: "pointer" }} />
109 |
110 |
111 |
112 |
113 |
114 | Dashboard
115 |
116 |
117 |
118 |
119 |
120 | Search
121 |
122 |
123 | {
124 | currentUser ?
125 |
126 |
127 |
128 | Favourites
129 |
130 |
131 | :
132 |
133 | dispatch(
134 | openSignin()
135 | )
136 | } style={{ textDecoration: "none", color: "inherit", width: '100%' }}>
137 |
138 |
139 | Favourites
140 |
141 |
142 | }
143 |
144 | {
145 | if (currentUser) {
146 | setUploadOpen(true)
147 | } else {
148 | dispatch(
149 | openSignin()
150 | )
151 | }
152 | }} style={{ textDecoration: "none", color: "inherit", width: '100%' }}>
153 |
154 |
155 | Upload
156 |
157 |
158 |
159 |
160 | {
161 | darkMode ?
162 | <>
163 | setDarkMode(false)}>
164 |
165 | Light Mode
166 |
167 | >
168 | :
169 | <>
170 | setDarkMode(true)}>
171 |
172 | Dark Mode
173 |
174 | >
175 | }
176 | {
177 | currentUser ?
178 | logoutUser()}>
179 |
180 | Log Out
181 |
182 |
183 | :
184 | dispatch(openSignin())}>
185 |
186 | Log In
187 |
188 | }
189 |
190 |
191 | )
192 | }
193 |
194 | export default Menu
--------------------------------------------------------------------------------
/client/src/components/PodcastCard.jsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import Avatar from '@mui/material/Avatar';
3 | import styled from 'styled-components';
4 | import FavoriteIcon from '@mui/icons-material/Favorite';
5 | import { useState } from 'react';
6 | import { IconButton } from '@mui/material';
7 | import { favoritePodcast } from '../api';
8 | import { useDispatch, useSelector } from 'react-redux';
9 | import { Link } from 'react-router-dom';
10 | import { useNavigate } from 'react-router-dom';
11 | import { openSignin } from '../redux/setSigninSlice';
12 | import HeadphonesIcon from '@mui/icons-material/Headphones';
13 | import PlayArrowIcon from '@mui/icons-material/PlayArrow';
14 |
15 |
16 | const PlayIcon = styled.div`
17 | padding: 10px;
18 | border-radius: 50%;
19 | z-index: 100;
20 | display: flex;
21 | align-items: center;
22 | background: #9000ff !important;
23 | color: white !important;
24 | backdrop-filter: blur(4px);
25 | -webkit-backdrop-filter: blur(4px);
26 | position: absolute !important;
27 | top: 45%;
28 | right: 10%;
29 | display: none;
30 | transition: all 0.4s ease-in-out;
31 | box-shadow: 0 0 16px 4px #9000ff50 !important;
32 | `;
33 |
34 |
35 |
36 | const Card = styled(Link)`
37 | position: relative;
38 | text-decoration: none;
39 | background-color: ${({ theme }) => theme.card};
40 | max-width: 220px;
41 | height: 280px;
42 | display: flex;
43 | flex-direction: column;
44 | justify-content: flex-start;
45 | align-items: center;
46 | padding: 16px;
47 | border-radius: 6px;
48 | box-shadow: 0 0 16px 0 rgba(0, 0, 0, 0.1);
49 | &:hover{
50 | cursor: pointer;
51 | transform: translateY(-8px);
52 | transition: all 0.4s ease-in-out;
53 | box-shadow: 0 0 18px 0 rgba(0, 0, 0, 0.3);
54 | filter: brightness(1.3);
55 | }
56 | &:hover ${PlayIcon}{
57 | display: flex;
58 | }
59 | `
60 |
61 | const Top = styled.div`
62 | display: flex;
63 | justify-content: center;
64 | align-items: center;
65 | height: 150px;
66 | position: relative;
67 | `
68 | const Title = styled.div`
69 | overflow: hidden;
70 | display: -webkit-box;
71 | max-width: 100%;
72 | -webkit-line-clamp: 2;
73 | -webkit-box-orient: vertical;
74 | overflow: hidden;
75 | text-overflow: ellipsis;
76 | color: ${({ theme }) => theme.text_primary};
77 | `
78 |
79 | const Description = styled.div`
80 | overflow: hidden;
81 | display: -webkit-box;
82 | max-width: 100%;
83 | -webkit-line-clamp: 2;
84 | -webkit-box-orient: vertical;
85 | overflow: hidden;
86 | text-overflow: ellipsis;
87 | color: ${({ theme }) => theme.text_secondary};
88 | font-size: 12px;
89 | `
90 |
91 | const CardImage = styled.img`
92 | object-fit: cover;
93 | width: 220px;
94 | height: 140px;
95 | border-radius: 6px;
96 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.3);
97 | &:hover{
98 | box-shadow: 0 4px 30px rgba(0, 0, 0, 0.4);
99 | }
100 | `
101 | const CardInformation = styled.div`
102 | display:flex;
103 | align-items: flex-end;
104 | font-weight:450;
105 | padding: 14px 0px 0px 0px;
106 | width: 100%;
107 | `
108 | const MainInfo = styled.div`
109 | display: flex;
110 | width: 100%;
111 | flex-direction:column;
112 | justify-content: flex-start;
113 | gap: 4px;
114 | `
115 | const CreatorInfo = styled.div`
116 | display: flex;
117 | align-items: center;
118 | justify-content: space-between;
119 | gap: 8px;
120 | margin-top: 6px;
121 |
122 | `
123 | const CreatorName = styled.div`
124 | font-size:12px;
125 | overflow: hidden;
126 | white-space: nowrap;
127 | text-overflow: ellipsis;
128 | color: ${({ theme }) => theme.text_secondary};
129 | `
130 | const TimePosted = styled.div`
131 | color: ${({ theme }) => theme.text_secondary};
132 | `
133 |
134 | const Views = styled.div`
135 | font-size:10px;
136 | color: ${({ theme }) => theme.text_secondary};
137 | width: max-content;
138 | `
139 | const Favorite = styled(IconButton)`
140 | color:white;
141 | top: 8px;
142 | right: 6px;
143 | padding: 6px !important;
144 | border-radius: 50%;
145 | z-index: 100;
146 | display: flex;
147 | align-items: center;
148 | background: ${({ theme }) => theme.text_secondary + 95} !important;
149 | color: white !important;
150 | position: absolute !important;
151 | backdrop-filter: blur(4px);
152 | box-shadow: 0 0 16px 6px #222423 !important;
153 | `
154 |
155 | export const PodcastCard = ({ podcast, user, setSignInOpen }) => {
156 | const [favourite, setFavourite] = useState(false)
157 | const dispatch = useDispatch();
158 |
159 | const token = localStorage.getItem("podstreamtoken");
160 |
161 | const favoritpodcast = async () => {
162 | await favoritePodcast(podcast._id, token).then((res) => {
163 | if (res.status === 200) {
164 | setFavourite(!favourite)
165 | }
166 | }
167 | ).catch((err) => {
168 | console.log(err)
169 | })
170 | }
171 |
172 | React.useEffect(() => {
173 | //favorits is an array of objects in which each object has a podcast id match it to the current podcast id
174 | if (user?.favorits?.find((fav) => fav._id === podcast._id)) {
175 | setFavourite(true)
176 | }
177 | }, [user])
178 |
179 | const navigate = useNavigate();
180 | const { currentUser } = useSelector(state => state.user);
181 |
182 | return (
183 |
184 |
185 |
186 |
187 | {
188 | if (!currentUser) {
189 | dispatch(
190 | openSignin()
191 | )
192 | } else {
193 | favoritpodcast()
194 | }
195 | }}>
196 |
197 | {favourite ?
198 |
199 | :
200 |
201 | }
202 |
203 |
204 |
205 |
206 |
207 |
208 | {podcast.name}
209 | {podcast.desc}
210 |
211 |
212 |
{podcast.creator.name?.charAt(0).toUpperCase()}
214 |
215 | {podcast.creator.name}
216 |
217 |
218 |
219 | • {podcast.views} Views
220 |
221 |
222 |
223 |
224 |
225 | {podcast?.type === 'video' ?
226 |
227 | :
228 |
229 | }
230 |
231 |
232 | );
233 | }
--------------------------------------------------------------------------------
/client/src/pages/Dashboard.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import { useSelector } from 'react-redux';
3 | import styled from 'styled-components';
4 | import { getMostPopularPodcast } from '../api/index';
5 | import { getPodcastByCategory } from '../api';
6 | import { PodcastCard } from '../components/PodcastCard.jsx'
7 | import { getUsers } from '../api/index';
8 | import { Link } from 'react-router-dom';
9 | import { CircularProgress } from '@mui/material';
10 |
11 | const DashboardMain = styled.div`
12 | padding: 20px 30px;
13 | padding-bottom: 200px;
14 | height: 100%;
15 | overflow-y: scroll;
16 | display: flex;
17 | flex-direction: column;
18 | gap: 20px;
19 | @media (max-width: 768px){
20 | padding: 6px 10px;
21 | }
22 | `;
23 | const FilterContainer = styled.div`
24 | display: flex;
25 | flex-direction: column;
26 | ${({ box, theme }) => box && `
27 | background-color: ${theme.bg};
28 | border-radius: 10px;
29 | padding: 20px 30px;
30 | `}
31 | background-color: ${({ theme }) => theme.bg};
32 | border-radius: 10px;
33 | padding: 20px 30px;
34 | `;
35 | const Topic = styled.div`
36 | color: ${({ theme }) => theme.text_primary};
37 | font-size: 24px;
38 | font-weight: 540;
39 | display: flex;
40 | justify-content: space-between;
41 | align-items: center;
42 | @maedia (max-width: 768px){
43 | font-size: 18px;
44 | }
45 | `;
46 | const Span = styled.span`
47 | color: ${({ theme }) => theme.text_secondary};
48 | font-size: 16px;
49 | font-weight: 400;
50 | cursor: pointer;
51 | @media (max-width: 768px){
52 | font-size: 14px;
53 | }
54 | color: ${({ theme }) => theme.primary};
55 | &:hover{
56 | transition: 0.2s ease-in-out;
57 | }
58 | `;
59 | const Podcasts = styled.div`
60 | display: flex;
61 | flex-wrap: wrap;
62 | gap: 14px;
63 | padding: 18px 6px;
64 | //center the items if only one item present
65 | @media (max-width: 550px){
66 | justify-content: center;
67 | }
68 | `;
69 |
70 | const Loader = styled.div`
71 | display: flex;
72 | justify-content: center;
73 | align-items: center;
74 | height: 100%;
75 | width: 100%;
76 | `
77 | const DisplayNo = styled.div`
78 | display: flex;
79 | justify-content: center;
80 | align-items: center;
81 | height: 100%;
82 | width: 100%;
83 | color: ${({ theme }) => theme.text_primary};
84 | `
85 |
86 | const Dashboard = ({ setSignInOpen }) => {
87 | const [mostPopular, setMostPopular] = useState([]);
88 | const [user, setUser] = useState();
89 | const [comedy, setComedy] = useState([]);
90 | const [news, setNews] = useState([]);
91 | const [sports, setsports] = useState([]);
92 | const [crime, setCrime] = useState([]);
93 | const [loading, setLoading] = useState(false);
94 |
95 | //user
96 | const { currentUser } = useSelector(state => state.user);
97 |
98 | const token = localStorage.getItem("podstreamtoken");
99 | const getUser = async () => {
100 | await getUsers(token).then((res) => {
101 | setUser(res.data)
102 | }).then((error) => {
103 | console.log(error)
104 | });
105 | }
106 |
107 | const getPopularPodcast = async () => {
108 | await getMostPopularPodcast()
109 | .then((res) => {
110 | setMostPopular(res.data)
111 | console.log(res.data)
112 | })
113 | .catch((error) => {
114 | console.log(error)
115 | });
116 | }
117 |
118 | const getCommedyPodcasts = async () => {
119 | getPodcastByCategory("comedy")
120 | .then((res) => {
121 | setComedy(res.data)
122 | console.log(res.data)
123 | })
124 | .catch((error) => console.log(error));
125 | }
126 |
127 | const getNewsPodcasts = async () => {
128 | getPodcastByCategory("news")
129 | .then((res) => {
130 | setNews(res.data)
131 | console.log(res.data)
132 | })
133 | .catch((error) => console.log(error));
134 | }
135 |
136 | const getSportsPodcasts = async () => {
137 | getPodcastByCategory("sports")
138 | .then((res) => {
139 | setsports(res.data)
140 | console.log(res.data)
141 | })
142 | .catch((error) => console.log(error));
143 | }
144 |
145 | const getCrimePodcasts = async () => {
146 | getPodcastByCategory("crime")
147 | .then((res) => {
148 | setCrime(res.data)
149 | console.log(res.data)
150 | })
151 | .catch((error) => console.log(error));
152 | }
153 |
154 | const getallData = async () => {
155 | setLoading(true);
156 | if (currentUser) {
157 | setLoading(true);
158 | await getUser();
159 | }
160 | await getPopularPodcast();
161 | await getCommedyPodcasts();
162 | await getNewsPodcasts();
163 | await getCommedyPodcasts();
164 | await getCrimePodcasts();
165 | await getSportsPodcasts();
166 | setLoading(false);
167 | }
168 |
169 | useEffect(() => {
170 | getallData();
171 | }, [currentUser])
172 |
173 | return (
174 |
175 | {loading ?
176 |
177 |
178 |
179 | :
180 | <>
181 | {currentUser && user?.podcasts?.length > 0 &&
182 |
183 | Your Uploads
184 |
185 | Show All
186 |
187 |
188 |
189 | {user?.podcasts.slice(0, 10).map((podcast) => (
190 |
191 | ))}
192 |
193 |
194 | }
195 |
196 | Most Popular
197 |
198 | Show All
199 |
200 |
201 |
202 | {mostPopular.slice(0, 10).map((podcast) => (
203 |
204 | ))}
205 |
206 |
207 |
208 | Comedy
209 |
210 | Show All
211 |
212 |
213 |
214 | {comedy.slice(0, 10).map((podcast) => (
215 |
216 | ))}
217 |
218 |
219 |
220 |
221 | News
222 | Show All
223 |
224 |
225 |
226 | {news.slice(0, 10).map((podcast) => (
227 |
228 | ))}
229 |
230 |
231 |
232 |
233 | Crime
234 | Show All
235 |
236 |
237 |
238 | {crime.slice(0, 10).map((podcast) => (
239 |
240 | ))}
241 |
242 |
243 |
244 |
245 | Sports
246 | Show All
247 |
248 |
249 |
250 | {sports.slice(0, 10).map((podcast) => (
251 |
252 | ))}
253 |
254 |
255 | >
256 | }
257 |
258 | )
259 | }
260 |
261 | export default Dashboard
--------------------------------------------------------------------------------
/client/src/components/AudioPlayer.jsx:
--------------------------------------------------------------------------------
1 | import { Pause, PlayArrow, SkipNextRounded, SkipPreviousRounded, SouthRounded, VolumeUp } from '@mui/icons-material'
2 | import { IconButton } from '@mui/material'
3 | import React, { useState, useRef } from 'react'
4 | import { useDispatch } from 'react-redux'
5 | import styled from 'styled-components'
6 | import { closePlayer, openPlayer, setCurrentTime } from '../redux/audioplayerSlice'
7 | import { openSnackbar } from '../redux/snackbarSlice'
8 |
9 | const Container = styled.div`
10 | display: flex;
11 | align-items: center;
12 | justify-content: space-between;
13 | height: 70px;
14 | width: 100%;
15 | background-color: ${({ theme }) => theme.card};
16 | color: white;
17 | position: fixed;
18 | bottom: 0;
19 | left: 0;
20 | padding: 10px 0px;
21 | transition: all 0.5s ease;
22 | @media (max-width: 768px) {
23 | height: 60px;
24 | gap: 6px;
25 | padding: 4px 0px;
26 | }
27 | z-index: 999;
28 | `
29 | const Left = styled.div`
30 | display: flex;
31 | align-items: center;
32 | gap: 20px;
33 | margin-left: 20px;
34 | @media (max-width: 768px) {
35 | gap: 10px;
36 | margin-left: 10px;
37 | }
38 | flex: 0.2;
39 | `
40 |
41 | const Image = styled.img`
42 | width: 60px;
43 | height: 60px;
44 | border-radius: 6px;
45 | object-fit: cover;
46 | @media (max-width: 768px) {
47 | width: 34px;
48 | height: 34px;
49 | }
50 | `
51 | const PodData = styled.div`
52 | display: flex;
53 | flex-direction: column;
54 | `
55 | const Title = styled.span`
56 | font-size: 14px;
57 | font-weight: 500;
58 | overflow: hidden;
59 | display: -webkit-box;
60 | -webkit-line-clamp: 1;
61 | -webkit-box-orient: vertical;
62 | overflow: hidden;
63 | text-overflow: ellipsis;
64 | @media (max-width: 768px) {
65 | font-size: 12px;
66 | }
67 | `
68 | const Artist = styled.span`
69 | font-size: 12px;
70 | margin-top: 3px;
71 | `
72 | const Player = styled.div`
73 | width: 100%;
74 | max-width: 500px;
75 | display: flex;
76 | flex-direction: column;
77 | gap: 10px;
78 | flex: 0.6;
79 | align-items: center;
80 | justify-content: space-between;
81 | @media (max-width: 768px) {
82 | flex: 0.8;
83 | }
84 | `
85 |
86 | const Controls = styled.div`
87 | width: 100%;
88 | display: flex;
89 | align-items: center;
90 | gap: 30px;
91 | @media (max-width: 768px) {
92 | gap: 10px;
93 | margin-right: 10px;
94 | }
95 | `
96 |
97 | const Audio = styled.audio`
98 | height: 46px;
99 | width: 100%;
100 | font-size: 12px;
101 | @media (max-width: 768px) {
102 | height: 40px;
103 | font-size: 10px;
104 | }
105 | `
106 |
107 | const IcoButton = styled(IconButton)`
108 | background-color: ${({ theme }) => theme.text_primary} !important;
109 | color: ${({ theme }) => theme.bg} !important;
110 | font-size: 60px !important;
111 | padding: 10px !important;
112 | @media (max-width: 768px) {
113 | font-size: 20px !important;
114 | padding: 4px !important;
115 | }
116 | `;
117 |
118 | const Sound = styled.div`
119 | display: flex;
120 | align-items: center;
121 | gap: 10px;
122 | width: 50%;
123 | flex: 0.2;
124 | max-width: 150px;
125 | justify-content: space-between;
126 | margin-right: 20px;
127 | @media (max-width: 768px) {
128 | display: none;
129 | margin-right: 10px;
130 | }
131 | `
132 |
133 | const VolumeBar = styled.input.attrs({
134 | type: 'range',
135 | min: 0,
136 | max: 1,
137 | step: 0.1,
138 | })`
139 |
140 | -webkit-appearance: none;
141 | width: 100%;
142 | height: 2px;
143 | border-radius: 10px;
144 | background-color: ${({ theme }) => theme.text_primary};
145 | outline: none;
146 | &::-webkit-slider-thumb {
147 | -webkit-appearance: none;
148 | appearance: none;
149 | width: 12px;
150 | height: 12px;
151 | border-radius: 50%;
152 | background-color: ${({ theme }) => theme.primary};
153 | cursor: pointer;
154 | }
155 | &::-moz-range-thumb {
156 | width: 12px;
157 | height: 12px;
158 | border-radius: 50%;
159 | background-color: ${({ theme }) => theme.primary};;
160 | cursor: pointer;
161 | }
162 | `;
163 |
164 | const AudioPlayer = ({ episode, podid, currenttime, index }) => {
165 | const [isPlaying, setIsPlaying] = useState(false);
166 | const [progressWidth, setProgressWidth] = useState(0);
167 | const [duration, setDuration] = useState(0);
168 | const [volume, setVolume] = useState(1);
169 | const audioRef = useRef(null);
170 | const dispatch = useDispatch();
171 |
172 | const handleTimeUpdate = () => {
173 | const duration = audioRef.current.duration;
174 | const currentTime = audioRef.current.currentTime;
175 | const progress = (currentTime / duration) * 100;
176 | setProgressWidth(progress);
177 | setDuration(duration);
178 | dispatch(
179 | setCurrentTime({
180 | currenttime: currentTime
181 | })
182 | )
183 | }
184 |
185 | const handleVolumeChange = (event) => {
186 | const volume = event.target.value;
187 | setVolume(volume);
188 | audioRef.current.volume = volume;
189 | };
190 |
191 | const goToNextPodcast = () => {
192 | //from the podid and index, get the next podcast
193 | //dispatch the next podcast
194 | if (podid.episodes.length === index + 1) {
195 | dispatch(
196 | openSnackbar({
197 | message: "This is the last episode",
198 | severity: "info",
199 | })
200 | )
201 | return
202 | }
203 | dispatch(closePlayer());
204 | setTimeout(() => {
205 | dispatch(
206 | openPlayer({
207 | type: "audio",
208 | podid: podid,
209 | index: index + 1,
210 | currenttime: 0,
211 | episode: podid.episodes[index + 1]
212 | })
213 | )
214 | }, 10);
215 | }
216 |
217 | const goToPreviousPodcast = () => {
218 | //from the podid and index, get the next podcast
219 | //dispatch the next podcast
220 | if (index === 0) {
221 | dispatch(
222 | openSnackbar({
223 | message: "This is the first episode",
224 | severity: "info",
225 | })
226 | )
227 | return;
228 | }
229 | dispatch(closePlayer());
230 | setTimeout(() => {
231 | dispatch(
232 | openPlayer({
233 | type: "audio",
234 | podid: podid,
235 | index: index - 1,
236 | currenttime: 0,
237 | episode: podid.episodes[index - 1]
238 | })
239 | )
240 | }, 10);
241 | }
242 |
243 | return (
244 |
245 |
246 |
247 |
248 | {episode?.name}
249 | {episode?.creator.name}
250 |
251 |
252 |
253 |
254 |
255 |
256 | goToPreviousPodcast()} />
257 |
258 |
271 |
272 |
273 |
274 |
275 |
276 |
277 | )
278 | }
279 |
280 | export default AudioPlayer
281 |
--------------------------------------------------------------------------------
/client/src/pages/PodcastDetails.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react'
2 | import styled from 'styled-components'
3 | import FavoriteIcon from '@mui/icons-material/Favorite';
4 | import { CircularProgress, IconButton } from '@mui/material';
5 | import { favoritePodcast, getPodcastById, getUsers } from '../api';
6 | import { useParams } from 'react-router-dom';
7 | import { useDispatch, useSelector } from 'react-redux';
8 | import Episodecard from '../components/Episodecard';
9 | import { openSnackbar } from '../redux/snackbarSlice';
10 | import Avatar from '@mui/material/Avatar';
11 | import { format } from 'timeago.js';
12 | import PlayArrowIcon from '@mui/icons-material/PlayArrow';
13 | import HeadphonesIcon from '@mui/icons-material/Headphones';
14 |
15 | const Container = styled.div`
16 | padding: 20px 30px;
17 | padding-bottom: 200px;
18 | height: 100%;
19 | overflow-y: scroll;
20 | display: flex;
21 | flex-direction: column;
22 | gap: 20px;
23 | `;
24 |
25 | const Top = styled.div`
26 | display: flex;
27 | flex-direction: row;
28 | gap: 20px;
29 | @media (max-width: 768px) {
30 | flex-direction: column;
31 | }
32 | `;
33 |
34 | const Image = styled.img`
35 | width: 250px;
36 | height: 250px;
37 | border-radius: 6px;
38 | background-color: ${({ theme }) => theme.text_secondary};
39 | object-fit: cover;
40 | `;
41 |
42 | const Details = styled.div`
43 | display: flex;
44 | flex-direction: column;
45 | gap: 10px;
46 | width: 100%;
47 | `;
48 |
49 | const Title = styled.div`
50 | font-size: 32px;
51 | font-weight: 800;
52 | color: ${({ theme }) => theme.text_primary};
53 | width: 100%;
54 | display: flex;
55 | justify-content: space-between;
56 | `;
57 |
58 | const Description = styled.div`
59 | font-size: 14px;
60 | font-weight: 500;
61 | color: ${({ theme }) => theme.text_secondary};
62 | `;
63 |
64 | const Tags = styled.div`
65 | display: flex;
66 | flex-direction: row;
67 | gap: 10px;
68 | flex-wrap: wrap;
69 | `;
70 |
71 | const Tag = styled.div`
72 | background-color: ${({ theme }) => theme.text_secondary + 50};
73 | color: ${({ theme }) => theme.text_primary};
74 | padding: 4px 12px;
75 | border-radius: 20px;
76 | font-size: 12px;
77 | `;
78 |
79 |
80 | const Episodes = styled.div`
81 | display: flex;
82 | flex-direction: column;
83 | gap: 20px;
84 | `;
85 |
86 | const Topic = styled.div`
87 | color: ${({ theme }) => theme.text_primary};
88 | font-size: 22px;
89 | font-weight: 540;
90 | display: flex;
91 | justify-content space-between;
92 | align-items: center;
93 | `;
94 |
95 | const EpisodeWrapper = styled.div`
96 | display: flex;
97 | flex-direction: column;
98 | gap: 20px;
99 | `;
100 |
101 |
102 | const Favorite = styled(IconButton)`
103 | color:white;
104 | border-radius: 50%;
105 | display: flex;
106 | align-items: center;
107 | background: ${({ theme }) => theme.text_secondary + 95} !important;
108 | color: ${({ theme }) => theme.text_primary} !important;
109 | `
110 |
111 | const Loader = styled.div`
112 | display: flex;
113 | justify-content: center;
114 | align-items: center;
115 | height: 100%;
116 | width: 100%;
117 | `
118 | const Creator = styled.div`
119 | color: ${({ theme }) => theme.text_secondary};
120 | font-size: 12px;
121 | `
122 | const CreatorContainer = styled.div`
123 | display: flex;
124 | flex-direction: row;
125 | align-items: center;
126 | `
127 | const CreatorDetails = styled.div`
128 | display: flex;
129 | flex-direction: row;
130 | align-items: center;
131 | gap: 8px;
132 | `
133 | const Views = styled.div`
134 | color: ${({ theme }) => theme.text_secondary};
135 | font-size: 12px;
136 | margin-left: 20px;
137 | `
138 | const Icon = styled.div`
139 | color: white;
140 | font-size: 12px;
141 | margin-left: 20px;
142 | border-radius: 50%;
143 | background: #9000ff !important;
144 | display: flex;
145 | align-items: center;
146 | justify-content: center;
147 | padding: 6px;
148 | `
149 |
150 | const PodcastDetails = () => {
151 |
152 | const { id } = useParams();
153 | const [favourite, setFavourite] = useState(false);
154 | const [podcast, setPodcast] = useState();
155 | const [user, setUser] = useState();
156 | const [loading, setLoading] = useState();
157 |
158 | const dispatch = useDispatch();
159 |
160 | const token = localStorage.getItem("podstreamtoken");
161 | //user
162 | const { currentUser } = useSelector(state => state.user);
163 |
164 | const favoritpodcast = async () => {
165 | setLoading(true);
166 | if (podcast !== undefined && podcast !== null) {
167 | await favoritePodcast(podcast?._id, token).then((res) => {
168 | if (res.status === 200) {
169 | setFavourite(!favourite)
170 | setLoading(false)
171 | }
172 | }
173 | ).catch((err) => {
174 | console.log(err)
175 | setLoading(false)
176 | dispatch(
177 | openSnackbar(
178 | {
179 | message: err.message,
180 | severity: "error"
181 | }
182 | )
183 | )
184 | })
185 | }
186 | }
187 |
188 | const getUser = async () => {
189 | setLoading(true)
190 | await getUsers(token).then((res) => {
191 | setUser(res.data)
192 | setLoading(false)
193 | }).then((err) => {
194 | console.log(err)
195 | setLoading(false)
196 | dispatch(
197 | openSnackbar(
198 | {
199 | message: err.message,
200 | severity: "error"
201 | }
202 | )
203 | )
204 | });
205 | }
206 |
207 | const getPodcast = async () => {
208 |
209 | setLoading(true)
210 | await getPodcastById(id).then((res) => {
211 | if (res.status === 200) {
212 | setPodcast(res.data)
213 | setLoading(false)
214 | }
215 | }
216 | ).catch((err) => {
217 | console.log(err)
218 | setLoading(false)
219 | dispatch(
220 | openSnackbar(
221 | {
222 | message: err.message,
223 | severity: "error"
224 | }
225 | )
226 | )
227 | })
228 | }
229 |
230 |
231 | useState(() => {
232 | getPodcast();
233 | }, [currentUser])
234 |
235 | React.useEffect(() => {
236 | //favorits is an array of objects in which each object has a podcast id match it to the current podcast id
237 | if (currentUser) {
238 | getUser();
239 | }
240 | if (user?.favorits?.find((fav) => fav._id === podcast?._id)) {
241 | setFavourite(true)
242 | }
243 | }, [currentUser, podcast])
244 |
245 | return (
246 |
247 | {loading ?
248 |
249 |
250 |
251 | :
252 | <>
253 |
254 | favoritpodcast()}>
255 | {favourite ?
256 |
257 | :
258 |
259 | }
260 |
261 |
262 |
263 |
264 |
265 | {podcast?.name}
266 |
267 | {podcast?.desc}
268 |
269 | {podcast?.tags.map((tag) => (
270 | {tag}
271 | ))}
272 |
273 |
274 |
275 | {podcast?.creator?.name.charAt(0).toUpperCase()}
276 | {podcast?.creator?.name}
277 |
278 | • {podcast?.views} Views
279 |
280 | • {format(podcast?.createdAt)}
281 |
282 |
283 | {podcast?.type === "audio" ?
284 |
285 | :
286 |
287 | }
288 |
289 |
290 |
291 |
292 |
293 | All Episodes
294 |
295 | {podcast?.episodes.map((episode, index) => (
296 |
297 | ))}
298 |
299 |
300 | >
301 | }
302 |
303 | )
304 | }
305 |
306 | export default PodcastDetails
--------------------------------------------------------------------------------
/client/src/components/OTP.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef } from 'react'
2 | import styled from "styled-components";
3 | import { useTheme } from "styled-components";
4 | import OtpInput from 'react-otp-input';
5 | import CircularProgress from "@mui/material/CircularProgress";
6 | import { useDispatch } from 'react-redux';
7 | import { openSnackbar } from "../redux/snackbarSlice";
8 | import { generateOtp, verifyOtp } from '../api';
9 |
10 |
11 | const Title = styled.div`
12 | font-size: 22px;
13 | font-weight: 500;
14 | color: ${({ theme }) => theme.text_primary};
15 | margin: 16px 22px;
16 | `;
17 |
18 |
19 | const OutlinedBox = styled.div`
20 | height: 44px;
21 | border-radius: 12px;
22 | border: 1px solid ${({ theme }) => theme.text_secondary};
23 | color: ${({ theme }) => theme.text_secondary};
24 | ${({ googleButton, theme }) =>
25 | googleButton &&
26 | `
27 | user-select: none;
28 | gap: 16px;`}
29 | ${({ button, theme }) =>
30 | button &&
31 | `
32 | user-select: none;
33 | border: none;
34 | background: ${theme.button};
35 | color: '${theme.text_secondary}';`}
36 | ${({ activeButton, theme }) =>
37 | activeButton &&
38 | `
39 | user-select: none;
40 | border: none;
41 | background: ${theme.primary};
42 | color: white;`}
43 | margin: 3px 20px;
44 | font-size: 14px;
45 | display: flex;
46 | justify-content: center;
47 | align-items: center;
48 | font-weight: 500;
49 | padding: 0px 14px;
50 | `;
51 |
52 |
53 | const LoginText = styled.div`
54 | font-size: 14px;
55 | font-weight: 500;
56 | color: ${({ theme }) => theme.text_secondary};
57 | margin: 0px 26px 0px 26px;
58 | `;
59 | const Span = styled.span`
60 | color: ${({ theme }) => theme.primary};
61 | font-size: 12px;
62 | margin: 0px 26px 0px 26px;
63 | `;
64 |
65 | const Error = styled.div`
66 | color: red;
67 | font-size: 12px;
68 | margin: 2px 26px 8px 26px;
69 | display: block;
70 | ${({ error, theme }) =>
71 | error === "" &&
72 | ` display: none;
73 | `}
74 | `;
75 |
76 | const Timer = styled.div`
77 | color: ${({ theme }) => theme.text_secondary};
78 | font-size: 12px;
79 | margin: 2px 26px 8px 26px;
80 | display: block;
81 | `;
82 |
83 | const Resend = styled.div`
84 | color: ${({ theme }) => theme.primary};
85 | font-size: 14px;
86 | margin: 2px 26px 8px 26px;
87 | display: block;
88 | cursor: pointer;
89 | `;
90 |
91 |
92 | const OTP = ({ email, name, otpVerified, setOtpVerified, reason }) => {
93 | const theme = useTheme();
94 | const dispatch = useDispatch();
95 |
96 |
97 | const [otp, setOtp] = useState('');
98 | const [otpError, setOtpError] = useState('');
99 | const [otpLoading, setOtpLoading] = useState(false);
100 | const [disabled, setDisabled] = useState(true);
101 | const [showTimer, setShowTimer] = useState(false);
102 | const [otpSent, setOtpSent] = useState(false);
103 | const [timer, setTimer] = useState('00:00');
104 |
105 |
106 | const Ref = useRef(null);
107 |
108 | const getTimeRemaining = (e) => {
109 | const total = Date.parse(e) - Date.parse(new Date());
110 | const seconds = Math.floor((total / 1000) % 60);
111 | const minutes = Math.floor((total / 1000 / 60) % 60);
112 | const hours = Math.floor((total / 1000 / 60 / 60) % 24);
113 | return {
114 | total, hours, minutes, seconds
115 | };
116 | }
117 |
118 | const startTimer = (e) => {
119 | let { total, hours, minutes, seconds }
120 | = getTimeRemaining(e);
121 | if (total >= 0) {
122 |
123 | // update the timer
124 | // check if less than 10 then we need to
125 | // add '0' at the beginning of the variable
126 | setTimer(
127 | (minutes > 9 ? minutes : '0' + minutes) + ':'
128 | + (seconds > 9 ? seconds : '0' + seconds)
129 | )
130 |
131 | }
132 | }
133 |
134 | const clearTimer = (e) => {
135 |
136 | // If you adjust it you should also need to
137 | // adjust the Endtime formula we are about
138 | // to code next
139 | setTimer('01:00');
140 |
141 | // If you try to remove this line the
142 | // updating of timer Variable will be
143 | // after 1000ms or 1sec
144 | if (Ref.current) clearInterval(Ref.current);
145 | const id = setInterval(() => {
146 | startTimer(e);
147 | }, 1000)
148 | Ref.current = id;
149 | }
150 |
151 | const getDeadTime = () => {
152 | let deadline = new Date();
153 |
154 | // This is where you need to adjust if
155 | // you entend to add more time
156 | deadline.setSeconds(deadline.getSeconds() + 60);
157 | return deadline;
158 | }
159 |
160 | const resendOtp = () => {
161 | setShowTimer(true);
162 | clearTimer(getDeadTime());
163 | sendOtp();
164 | }
165 |
166 | const sendOtp = async () => {
167 | await generateOtp(email, name, reason).then((res) => {
168 | if (res.status === 200) {
169 | dispatch(
170 | openSnackbar({
171 | message: "OTP sent Successfully",
172 | severity: "success",
173 | })
174 | );
175 | setDisabled(true);
176 | setOtp('');
177 | setOtpError('');
178 | setOtpLoading(false);
179 | setOtpSent(true);
180 | console.log(res.data);
181 | } else {
182 | dispatch(
183 | openSnackbar({
184 | message: res.status,
185 | severity: "error",
186 | })
187 | );
188 | setOtp('');
189 | setOtpError('');
190 | setOtpLoading(false);
191 | }
192 | }).catch((err) => {
193 | dispatch(
194 | openSnackbar({
195 | message: err.message,
196 | severity: "error",
197 | })
198 | );
199 | });
200 | }
201 |
202 | const validateOtp = () => {
203 | setOtpLoading(true);
204 | setDisabled(true);
205 | verifyOtp(otp).then((res) => {
206 | if (res.status === 200) {
207 | setOtpVerified(true);
208 | setOtp('');
209 | setOtpError('');
210 | setDisabled(false);
211 | setOtpLoading(false);
212 | } else {
213 | setOtpError(res.data.message);
214 | setDisabled(false);
215 | setOtpLoading(false);
216 | }
217 | }).catch((err) => {
218 | dispatch(
219 | openSnackbar({
220 | message: err.message,
221 | severity: "error",
222 | })
223 | );
224 | setOtpError(err.message);
225 | setDisabled(false);
226 | setOtpLoading(false);
227 | });
228 | }
229 |
230 | useEffect(() => {
231 | sendOtp();
232 | clearTimer(getDeadTime());
233 | }, []);
234 |
235 | useEffect(() => {
236 | if (timer === '00:00') {
237 | setShowTimer(false);
238 | } else {
239 | setShowTimer(true);
240 | }
241 | }, [timer]);
242 |
243 |
244 | useEffect(() => {
245 | if (otp.length === 6) {
246 | setDisabled(false);
247 | } else {
248 | setDisabled(true);
249 | }
250 | }, [otp]);
251 |
252 |
253 |
254 |
255 | return (
256 |
257 |
VERIFY OTP
258 |
A verification OTP has been sent to:
259 |
{email}
260 | {!otpSent ?
261 |
Sending OTP
262 | :
263 |
264 | }
272 | />
273 | {otpError}
274 |
275 |
276 | validateOtp()}
281 | >
282 | {otpLoading ? (
283 |
284 | ) : (
285 | "Submit"
286 | )}
287 |
288 |
289 | {showTimer ?
290 | Resend in {timer}
291 | :
292 | resendOtp()}>Resend
293 | }
294 |
295 | }
296 |
297 | )
298 | }
299 |
300 | export default OTP
--------------------------------------------------------------------------------
/server/controllers/auth.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 | import User from "../models/User.js";
3 | import bcrypt from "bcrypt";
4 | import { createError } from "../error.js";
5 | import jwt from "jsonwebtoken";
6 | import nodemailer from "nodemailer";
7 | import dotenv from 'dotenv';
8 | import otpGenerator from 'otp-generator';
9 |
10 |
11 | dotenv.config();
12 |
13 | const transporter = nodemailer.createTransport({
14 | service: "gmail",
15 | auth: {
16 | user: process.env.EMAIL_USERNAME,
17 | pass: process.env.EMAIL_PASSWORD
18 | },
19 | port: 465,
20 | host: 'smtp.gmail.com'
21 | });
22 |
23 | export const signup = async (req, res, next) => {
24 | const { email } = req.body
25 | // Check we have an email
26 | if (!email) {
27 | return res.status(422).send({ message: "Missing email." });
28 | }
29 | try {
30 | // Check if the email is in use
31 | const existingUser = await User.findOne({ email }).exec();
32 | if (existingUser) {
33 | return res.status(409).send({
34 | message: "Email is already in use."
35 | });
36 | }
37 | // Step 1 - Create and save the userconst salt = bcrypt.genSaltSync(10);
38 | const salt = bcrypt.genSaltSync(10);
39 | const hashedPassword = bcrypt.hashSync(req.body.password, salt);
40 | const newUser = new User({ ...req.body, password: hashedPassword });
41 |
42 | newUser.save().then((user) => {
43 |
44 | // create jwt token
45 | const token = jwt.sign({ id: user._id }, process.env.JWT, { expiresIn: "9999 years" });
46 | res.status(200).json({ token, user });
47 | }).catch((err) => {
48 | next(err);
49 | });
50 | } catch (err) {
51 | next(err);
52 | }
53 | }
54 |
55 | export const signin = async (req, res, next) => {
56 | try {
57 | const user = await User.findOne({ email: req.body.email });
58 | if (!user) {
59 | return next(createError(201, "User not found"));
60 | }
61 | if (user.googleSignIn) {
62 | return next(createError(201, "Entered email is Signed Up with google account. Please SignIn with google."));
63 | }
64 | const validPassword = await bcrypt.compareSync(req.body.password, user.password);
65 | if (!validPassword) {
66 | return next(createError(201, "Wrong password"));
67 | }
68 |
69 | // create jwt token
70 | const token = jwt.sign({ id: user._id }, process.env.JWT, { expiresIn: "9999 years" });
71 | res.status(200).json({ token, user });
72 |
73 | } catch (err) {
74 | next(err);
75 | }
76 | }
77 |
78 |
79 |
80 | export const googleAuthSignIn = async (req, res, next) => {
81 | try {
82 | const user = await User.findOne({ email: req.body.email });
83 |
84 | if (!user) {
85 | try {
86 | const user = new User({ ...req.body, googleSignIn: true });
87 | await user.save();
88 | const token = jwt.sign({ id: user._id }, process.env.JWT, { expiresIn: "9999 years" });
89 | res.status(200).json({ token, user: user });
90 | } catch (err) {
91 | next(err);
92 | }
93 | } else if (user.googleSignIn) {
94 | const token = jwt.sign({ id: user._id }, process.env.JWT, { expiresIn: "9999 years" });
95 | res.status(200).json({ token, user });
96 | } else if (user.googleSignIn === false) {
97 | return next(createError(201, "User already exists with this email can't do google auth"));
98 | }
99 | } catch (err) {
100 | next(err);
101 | }
102 | }
103 |
104 | export const logout = (req, res) => {
105 | res.clearCookie("access_token").json({ message: "Logged out" });
106 | }
107 |
108 | export const generateOTP = async (req, res) => {
109 | req.app.locals.OTP = await otpGenerator.generate(6, { upperCaseAlphabets: false, specialChars: false, lowerCaseAlphabets: false, digits: true, });
110 | const { email } = req.query;
111 | const { name } = req.query;
112 | const { reason } = req.query;
113 | const verifyOtp = {
114 | to: email,
115 | subject: 'Account Verification OTP',
116 | html: `
117 |
118 |
Verify Your PODSTREAM Account
119 |
120 |
121 |
Verification Code
122 | ${req.app.locals.OTP}
123 |
124 |
125 |
Dear ${name},
126 |
Thank you for creating a PODSTREAM account. To activate your account, please enter the following verification code:
127 |
${req.app.locals.OTP}
128 |
Please enter this code in the PODSTREAM app to activate your account.
129 |
If you did not create a PODSTREAM account, please disregard this email.
130 |
131 |
132 |
133 |
Best regards,
The Podstream Team
134 |
135 | `
136 | };
137 |
138 | const resetPasswordOtp = {
139 | to: email,
140 | subject: 'PODSTREAM Reset Password Verification',
141 | html: `
142 |
143 |
Reset Your PODSTREAM Account Password
144 |
145 |
146 |
Verification Code
147 | ${req.app.locals.OTP}
148 |
149 |
150 |
Dear ${name},
151 |
To reset your PODSTREAM account password, please enter the following verification code:
152 |
${req.app.locals.OTP}
153 |
Please enter this code in the PODSTREAM app to reset your password.
154 |
If you did not request a password reset, please disregard this email.
155 |
156 |
157 |
158 |
Best regards,
The PODSTREAM Team
159 |
160 | `
161 | };
162 | if (reason === "FORGOTPASSWORD") {
163 | transporter.sendMail(resetPasswordOtp, (err) => {
164 | if (err) {
165 | next(err)
166 | } else {
167 | return res.status(200).send({ message: "OTP sent" });
168 | }
169 | })
170 | } else {
171 | transporter.sendMail(verifyOtp, (err) => {
172 | if (err) {
173 | next(err)
174 | } else {
175 | return res.status(200).send({ message: "OTP sent" });
176 | }
177 | })
178 | }
179 | }
180 |
181 | export const verifyOTP = async (req, res, next) => {
182 | const { code } = req.query;
183 | if (parseInt(code) === parseInt(req.app.locals.OTP)) {
184 | req.app.locals.OTP = null;
185 | req.app.locals.resetSession = true;
186 | res.status(200).send({ message: "OTP verified" });
187 | }
188 | return next(createError(201, "Wrong OTP"));
189 | }
190 |
191 | export const createResetSession = async (req, res, next) => {
192 | if (req.app.locals.resetSession) {
193 | req.app.locals.resetSession = false;
194 | return res.status(200).send({ message: "Access granted" });
195 | }
196 |
197 | return res.status(400).send({ message: "Session expired" });
198 | }
199 |
200 | export const findUserByEmail = async (req, res, next) => {
201 | const { email } = req.query;
202 | try {
203 | const user = await User.findOne({ email: email });
204 | if (user) {
205 | return res.status(200).send({
206 | message: "User found"
207 | });
208 | } else {
209 | return res.status(202).send({
210 | message: "User not found"
211 | });
212 | }
213 | } catch (err) {
214 | next(err);
215 | }
216 | }
217 |
218 | export const resetPassword = async (req, res, next) => {
219 |
220 | if (!req.app.locals.resetSession) return res.status(440).send({ message: "Session expired" });
221 |
222 | const { email, password } = req.body;
223 | try {
224 | await User.findOne({ email }).then(user => {
225 | if (user) {
226 |
227 | const salt = bcrypt.genSaltSync(10);
228 | const hashedPassword = bcrypt.hashSync(password, salt);
229 | User.updateOne({ email: email }, { $set: { password: hashedPassword } }).then(() => {
230 |
231 | req.app.locals.resetSession = false;
232 | return res.status(200).send({
233 | message: "Password reset successful"
234 | });
235 |
236 | }).catch(err => {
237 | next(err);
238 | });
239 | } else {
240 | return res.status(202).send({
241 | message: "User not found"
242 | });
243 | }
244 | });
245 | } catch (err) {
246 | next(err);
247 | }
248 | }
--------------------------------------------------------------------------------
/client/src/components/Signup.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | CloseRounded,
3 | EmailRounded,
4 | PasswordRounded,
5 | Person,
6 | Visibility,
7 | VisibilityOff,
8 | TroubleshootRounded,
9 | } from "@mui/icons-material";
10 | import React, { useState, useEffect } from "react";
11 | import styled from "styled-components";
12 | import { useTheme } from "styled-components";
13 | import Google from "../Images/google.webp";
14 | import { IconButton, Modal } from "@mui/material";
15 | import { loginFailure, loginStart, loginSuccess } from "../redux/userSlice";
16 | import { openSnackbar } from "../redux/snackbarSlice";
17 | import { useDispatch } from "react-redux";
18 | import axios from "axios";
19 | import CircularProgress from "@mui/material/CircularProgress";
20 | import validator from "validator";
21 | import { googleSignIn, signUp } from "../api/index";
22 | import OTP from "./OTP";
23 | import { useGoogleLogin } from "@react-oauth/google";
24 | import { closeSignin, openSignin } from "../redux/setSigninSlice";
25 |
26 |
27 | const Container = styled.div`
28 | width: 100%;
29 | height: 100%;
30 | position: absolute;
31 | top: 0;
32 | left: 0;
33 | background-color: #000000a7;
34 | display: flex;
35 | align-items: center;
36 | justify-content: center;
37 | `;
38 |
39 | const Wrapper = styled.div`
40 | width: 380px;
41 | border-radius: 16px;
42 | background-color: ${({ theme }) => theme.card};
43 | color: ${({theme}) => theme.text_secondary};
44 | padding: 10px;
45 | display: flex;
46 | flex-direction: column;
47 | position: relative;
48 | `;
49 |
50 | const Title = styled.div`
51 | font-size: 22px;
52 | font-weight: 500;
53 | color: ${({ theme }) => theme.text_primary};
54 | margin: 16px 28px;
55 | `;
56 | const OutlinedBox = styled.div`
57 | height: 44px;
58 | border-radius: 12px;
59 | border: 1px solid ${({ theme }) => theme.text_secondary};
60 | color: ${({ theme }) => theme.text_secondary};
61 | ${({ googleButton, theme }) =>
62 | googleButton &&
63 | `
64 | user-select: none;
65 | gap: 16px;`}
66 | ${({ button, theme }) =>
67 | button &&
68 | `
69 | user-select: none;
70 | border: none;
71 | background: ${theme.button};
72 | color: '${theme.text_secondary}';`}
73 | ${({ activeButton, theme }) =>
74 | activeButton &&
75 | `
76 | user-select: none;
77 | border: none;
78 | background: ${theme.primary};
79 | color: white;`}
80 | margin: 3px 20px;
81 | font-size: 14px;
82 | display: flex;
83 | justify-content: center;
84 | align-items: center;
85 | font-weight: 500;
86 | padding: 0px 14px;
87 | `;
88 | const GoogleIcon = styled.img`
89 | width: 22px;
90 | `;
91 | const Divider = styled.div`
92 | display: flex;
93 | display: flex;
94 | justify-content: center;
95 | align-items: center;
96 | color: ${({ theme }) => theme.text_secondary};
97 | font-size: 14px;
98 | font-weight: 600;
99 | `;
100 | const Line = styled.div`
101 | width: 80px;
102 | height: 1px;
103 | border-radius: 10px;
104 | margin: 0px 10px;
105 | background-color: ${({ theme }) => theme.text_secondary};
106 | `;
107 |
108 | const TextInput = styled.input`
109 | width: 100%;
110 | border: none;
111 | font-size: 14px;
112 | border-radius: 3px;
113 | background-color: transparent;
114 | outline: none;
115 | color: ${({ theme }) => theme.text_secondary};
116 | `;
117 |
118 | const LoginText = styled.div`
119 | font-size: 14px;
120 | font-weight: 500;
121 | color: ${({ theme }) => theme.text_secondary};
122 | margin: 20px 20px 38px 20px;
123 | display: flex;
124 | justify-content: center;
125 | align-items: center;
126 | `;
127 | const Span = styled.span`
128 | color: ${({ theme }) => theme.primary};
129 | `;
130 |
131 | const Error = styled.div`
132 | color: red;
133 | font-size: 10px;
134 | margin: 2px 26px 8px 26px;
135 | display: block;
136 | ${({ error, theme }) =>
137 | error === "" &&
138 | ` display: none;
139 | `}
140 | `;
141 |
142 | const SignUp = ({ setSignUpOpen, setSignInOpen }) => {
143 |
144 | const [nameValidated, setNameValidated] = useState(false);
145 | const [name, setName] = useState("");
146 | const [email, setEmail] = useState("");
147 | const [password, setPassword] = useState("");
148 | const [Loading, setLoading] = useState(false);
149 | const [disabled, setDisabled] = useState(true);
150 | const [emailError, setEmailError] = useState("");
151 | const [credentialError, setcredentialError] = useState("");
152 | const [passwordCorrect, setPasswordCorrect] = useState(false);
153 | const [nameCorrect, setNameCorrect] = useState(false);
154 | const [values, setValues] = useState({
155 | password: "",
156 | showPassword: false,
157 | });
158 |
159 | const [otpSent, setOtpSent] = useState(false);
160 | const [otpVerified, setOtpVerified] = useState(false);
161 |
162 | const dispatch = useDispatch();
163 |
164 | const createAccount = () => {
165 | if (otpVerified) {
166 | dispatch(loginStart());
167 | setDisabled(true);
168 | setLoading(true);
169 | try {
170 | signUp({ name, email, password }).then((res) => {
171 | if (res.status === 200) {
172 | dispatch(loginSuccess(res.data));
173 | dispatch(
174 | openSnackbar({ message: `OTP verified & Account created successfully`, severity: "success" })
175 | );
176 | setLoading(false);
177 | setDisabled(false);
178 | setSignUpOpen(false);
179 | dispatch(closeSignin())
180 | } else {
181 | dispatch(loginFailure());
182 | setcredentialError(`${res.data.message}`);
183 | setLoading(false);
184 | setDisabled(false);
185 | }
186 | });
187 | } catch (err) {
188 | dispatch(loginFailure());
189 | setLoading(false);
190 | setDisabled(false);
191 | dispatch(
192 | openSnackbar({
193 | message: err.message,
194 | severity: "error",
195 | })
196 | );
197 | }
198 | }
199 | };
200 |
201 | const handleSignUp = async (e) => {
202 | e.preventDefault();
203 | if (!disabled) {
204 | setOtpSent(true);
205 | }
206 |
207 | if (name === "" || email === "" || password === "") {
208 | dispatch(
209 | openSnackbar({
210 | message: "Please fill all the fields",
211 | severity: "error",
212 | })
213 | );
214 | }
215 | };
216 |
217 | useEffect(() => {
218 | if (email !== "") validateEmail();
219 | if (password !== "") validatePassword();
220 | if (name !== "") validateName();
221 | if (
222 | name !== "" &&
223 | validator.isEmail(email) &&
224 | passwordCorrect &&
225 | nameCorrect
226 | ) {
227 | setDisabled(false);
228 | } else {
229 | setDisabled(true);
230 | }
231 | }, [name, email, passwordCorrect, password, nameCorrect]);
232 |
233 | useEffect(() => {
234 | createAccount();
235 | }, [otpVerified]);
236 |
237 | //validate email
238 | const validateEmail = () => {
239 | if (validator.isEmail(email)) {
240 | setEmailError("");
241 | } else {
242 | setEmailError("Enter a valid Email Id!");
243 | }
244 | };
245 |
246 | //validate password
247 | const validatePassword = () => {
248 | if (password.length < 8) {
249 | setcredentialError("Password must be atleast 8 characters long!");
250 | setPasswordCorrect(false);
251 | } else if (password.length > 16) {
252 | setcredentialError("Password must be less than 16 characters long!");
253 | setPasswordCorrect(false);
254 | } else if (
255 | !password.match(/[a-z]/g) ||
256 | !password.match(/[A-Z]/g) ||
257 | !password.match(/[0-9]/g) ||
258 | !password.match(/[^a-zA-Z\d]/g)
259 | ) {
260 | setPasswordCorrect(false);
261 | setcredentialError(
262 | "Password must contain atleast one lowercase, uppercase, number and special character!"
263 | );
264 | } else {
265 | setcredentialError("");
266 | setPasswordCorrect(true);
267 | }
268 | };
269 |
270 | //validate name
271 | const validateName = () => {
272 | if (name.length < 4) {
273 | setNameValidated(false);
274 | setNameCorrect(false);
275 | setcredentialError("Name must be atleast 4 characters long!");
276 | } else {
277 | setNameCorrect(true);
278 | if (!nameValidated) {
279 | setcredentialError("");
280 | setNameValidated(true);
281 | }
282 |
283 | }
284 | };
285 |
286 | //Google SignIn
287 | const googleLogin = useGoogleLogin({
288 | onSuccess: async (tokenResponse) => {
289 | setLoading(true);
290 | const user = await axios.get(
291 | 'https://www.googleapis.com/oauth2/v3/userinfo',
292 | { headers: { Authorization: `Bearer ${tokenResponse.access_token}` } },
293 | ).catch((err) => {
294 | dispatch(loginFailure());
295 | dispatch(
296 | openSnackbar({
297 | message: err.message,
298 | severity: "error",
299 | })
300 | );
301 | });
302 |
303 | googleSignIn({
304 | name: user.data.name,
305 | email: user.data.email,
306 | img: user.data.picture,
307 | }).then((res) => {
308 | console.log(res);
309 | if (res.status === 200) {
310 | dispatch(loginSuccess(res.data));
311 | dispatch(closeSignin())
312 | setSignUpOpen(false);
313 | dispatch(
314 | openSnackbar({
315 | message: "Logged In Successfully",
316 | severity: "success",
317 | })
318 | );
319 |
320 | setLoading(false);
321 | } else {
322 | dispatch(loginFailure(res.data));
323 | dispatch(
324 | openSnackbar({
325 | message: res.data.message,
326 | severity: "error",
327 | })
328 | );
329 | setLoading(false);
330 | }
331 | });
332 | },
333 | onError: errorResponse => {
334 | dispatch(loginFailure());
335 | dispatch(
336 | openSnackbar({
337 | message: errorResponse.error,
338 | severity: "error",
339 | })
340 | );
341 | setLoading(false);
342 | },
343 | });
344 |
345 |
346 | const theme = useTheme();
347 | //ssetSignInOpen(false)
348 | return (
349 | dispatch(closeSignin())}>
350 |
351 |
352 | setSignUpOpen(false)}
361 | />
362 | {!otpSent ?
363 | <>
364 | Sign Up
365 | googleLogin()}
369 | >
370 | {Loading ? (
371 |
372 | ) : (
373 | <>
374 |
375 | Sign In with Google>
376 | )}
377 |
378 |
379 |
380 | or
381 |
382 |
383 |
384 |
388 | setName(e.target.value)}
392 | />
393 |
394 |
395 |
399 | setEmail(e.target.value)}
403 | />
404 |
405 | {emailError}
406 |
407 |
411 | setPassword(e.target.value)}
415 | />
416 |
419 | setValues({ ...values, showPassword: !values.showPassword })
420 | }
421 | >
422 | {values.showPassword ? (
423 |
424 | ) : (
425 |
426 | )}
427 |
428 |
429 | {credentialError}
430 |
436 | {Loading ? (
437 |
438 | ) : (
439 | "Create Account"
440 | )}
441 |
442 |
443 |
444 |
445 | >
446 |
447 | :
448 |
449 | }
450 |
451 | Already have an account ?
452 | {
454 | setSignUpOpen(false);
455 | dispatch(openSignin());
456 | }}
457 | style={{
458 | fontWeight: "500",
459 | marginLeft: "6px",
460 | cursor: "pointer",
461 | }}
462 | >
463 | Sign In
464 |
465 |
466 |
467 |
468 |
469 | );
470 | };
471 |
472 | export default SignUp;
473 |
--------------------------------------------------------------------------------
/client/src/components/Upload.jsx:
--------------------------------------------------------------------------------
1 | import { BackupRounded, CloseRounded, CloudDone, CloudDoneRounded, Create } from '@mui/icons-material';
2 | import { CircularProgress, IconButton, LinearProgress, Modal } from "@mui/material";
3 | import React, { useEffect } from 'react'
4 | import styled from 'styled-components'
5 | import {
6 | getStorage,
7 | ref,
8 | uploadBytesResumable,
9 | getDownloadURL,
10 | } from "firebase/storage";
11 | import app from "../firebase";
12 | import ImageSelector from "./ImageSelector";
13 | import { useDispatch } from "react-redux";
14 | import { openSnackbar } from "../redux/snackbarSlice";
15 | import { createPodcast } from '../api';
16 | import { Category } from '../utils/Data';
17 |
18 | const Container = styled.div`
19 | width: 100%;
20 | height: 100%;
21 | position: absolute;
22 | top: 0;
23 | left: 0;
24 | background-color: #000000a7;
25 | display: flex;
26 | align-items: top;
27 | justify-content: center;
28 | overflow-y: scroll;
29 | `;
30 |
31 | const Wrapper = styled.div`
32 | max-width: 500px;
33 | width: 100%;
34 | border-radius: 16px;
35 | margin: 50px 20px;
36 | height: min-content;
37 | background-color: ${({ theme }) => theme.card};
38 | color: ${({ theme }) => theme.text_primary};
39 | padding: 10px;
40 | display: flex;
41 | flex-direction: column;
42 | position: relative;
43 | `;
44 |
45 | const Title = styled.div`
46 | font-size: 22px;
47 | font-weight: 500;
48 | color: ${({ theme }) => theme.text_primary};
49 | margin: 12px 20px;
50 | `;
51 |
52 | const TextInput = styled.input`
53 | width: 100%;
54 | border: none;
55 | font-size: 14px;
56 | border-radius: 3px;
57 | background-color: transparent;
58 | outline: none;
59 | color: ${({ theme }) => theme.text_secondary};
60 | `;
61 |
62 | const Desc = styled.textarea`
63 | width: 100%;
64 | border: none;
65 | font-size: 14px;
66 | border-radius: 3px;
67 | background-color: transparent;
68 | outline: none;
69 | padding: 10px 0px;
70 | color: ${({ theme }) => theme.text_secondary};
71 | `;
72 |
73 |
74 | const Label = styled.div`
75 | font-size: 16px;
76 | font-weight: 500;
77 | color: ${({ theme }) => theme.text_primary + 80};
78 | margin: 12px 20px 0px 20px;
79 | `;
80 |
81 |
82 | const OutlinedBox = styled.div`
83 | min-height: 48px;
84 | border-radius: 8px;
85 | border: 1px solid ${({ theme }) => theme.text_secondary};
86 | color: ${({ theme }) => theme.text_secondary};
87 | ${({ googleButton, theme }) =>
88 | googleButton &&
89 | `
90 | user-select: none;
91 | gap: 16px;`}
92 | ${({ button, theme }) =>
93 | button &&
94 | `
95 | user-select: none;
96 | border: none;
97 | font-weight: 600;
98 | font-size: 16px;
99 | background: ${theme.button};
100 | color:'${theme.bg}';`}
101 | ${({ activeButton, theme }) =>
102 | activeButton &&
103 | `
104 | user-select: none;
105 | border: none;
106 | background: ${theme.primary};
107 | color: white;`}
108 | margin: 3px 20px;
109 | font-weight: 600;
110 | font-size: 16px;
111 | display: flex;
112 | justify-content: center;
113 | align-items: center;
114 | padding: 0px 14px;
115 | `;
116 |
117 | const Select = styled.select`
118 | width: 100%;
119 | border: none;
120 | font-size: 14px;
121 | border-radius: 3px;
122 | background-color: transparent;
123 | outline: none;
124 | color: ${({ theme }) => theme.text_secondary};
125 | `;
126 |
127 | const Option = styled.option`
128 | width: 100%;
129 | border: none;
130 | font-size: 14px;
131 | border-radius: 3px;
132 | background-color: ${({ theme }) => theme.card};
133 | outline: none;
134 | color: ${({ theme }) => theme.text_secondary};
135 | `;
136 |
137 | const ButtonContainer = styled.div`
138 | display: flex;
139 | gap: 0px;
140 | margin: 6px 20px 20px 20px;
141 | align-items: center;
142 | gap: 12px;
143 |
144 | `;
145 |
146 | const FileUpload = styled.label`
147 | display: flex;
148 | min-height: 48px;
149 | align-items: center;
150 | justify-content: center;
151 | gap: 12px;
152 | margin: 16px 20px 3px 20px;
153 | border: 1px dashed ${({ theme }) => theme.text_secondary};
154 | border-radius: 8px;
155 | padding: 10px;
156 | cursor: pointer;
157 | color: ${({ theme }) => theme.text_secondary};
158 | &:hover {
159 | background-color: ${({ theme }) => theme.text_secondary + 20};
160 | }
161 | `;
162 |
163 | const File = styled.input`
164 | display: none;
165 | `;
166 |
167 | const Uploading = styled.div`
168 | width: 100%;
169 | display: flex;
170 | flex-direction: column;
171 | align-items: center;
172 | justify-content: center;
173 | gap: 12px;
174 | padding: 12px;
175 | `;
176 |
177 |
178 |
179 | const Upload = ({ setUploadOpen }) => {
180 | const [podcast, setPodcast] = React.useState({
181 | name: "",
182 | desc: "",
183 | thumbnail: "",
184 | tags: [],
185 | category: "",
186 | type: "audio",
187 | episodes: [
188 | {
189 | name: "",
190 | desc: "",
191 | type: "audio",
192 | file: "",
193 | }
194 | ],
195 | });
196 | const [showEpisode, setShowEpisode] = React.useState(false);
197 | const [disabled, setDisabled] = React.useState(true);
198 | const [backDisabled, setBackDisabled] = React.useState(false);
199 | const [createDisabled, setCreateDisabled] = React.useState(true);
200 | const [loading, setLoading] = React.useState(false);
201 |
202 | const dispatch = useDispatch();
203 |
204 | const token = localStorage.getItem("podstreamtoken");
205 |
206 | const goToAddEpisodes = () => {
207 | setShowEpisode(true);
208 | };
209 |
210 | const goToPodcast = () => {
211 | setShowEpisode(false);
212 | };
213 |
214 | useEffect(() => {
215 | if (podcast === null) {
216 | setDisabled(true);
217 | setPodcast({
218 | name: "",
219 | desc: "",
220 | thumbnail: "",
221 | tags: [],
222 | episodes: [
223 | {
224 | name: "",
225 | desc: "",
226 | type: "audio",
227 | file: "",
228 | }
229 | ],
230 | });
231 | } else {
232 | if (podcast.name === "" && podcast.desc === "") {
233 | setDisabled(true);
234 | } else {
235 | setDisabled(false);
236 | }
237 | }
238 | }, [podcast]);
239 |
240 | const uploadFile = (file, index) => {
241 | const storage = getStorage(app);
242 | const fileName = new Date().getTime() + file.name;
243 | const storageRef = ref(storage, fileName);
244 | const uploadTask = uploadBytesResumable(storageRef, file);
245 |
246 | uploadTask.on(
247 | "state_changed",
248 | (snapshot) => {
249 | const progress =
250 | (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
251 | podcast.episodes[index].file.uploadProgress = Math.round(progress);
252 | setPodcast({ ...podcast, episodes: podcast.episodes });
253 | switch (snapshot.state) {
254 | case "paused":
255 | console.log("Upload is paused");
256 | break;
257 | case "running":
258 | console.log("Upload is running");
259 | break;
260 | default:
261 | break;
262 | }
263 | },
264 | (error) => { },
265 | () => {
266 | getDownloadURL(uploadTask.snapshot.ref).then((downloadURL) => {
267 |
268 | const newEpisodes = podcast.episodes;
269 | newEpisodes[index].file = downloadURL;
270 | setPodcast({ ...podcast, episodes: newEpisodes });
271 | });
272 | }
273 | );
274 | };
275 |
276 | const createpodcast = async () => {
277 | console.log(podcast);
278 | setLoading(true);
279 | await createPodcast(podcast, token).then((res) => {
280 | console.log(res);
281 | setDisabled(true);
282 | setBackDisabled(true);
283 | setUploadOpen(false);
284 | setLoading(false);
285 | dispatch(
286 | openSnackbar({
287 | open: true,
288 | message: "Podcast created successfully",
289 | severity: "success",
290 | })
291 | )
292 | }
293 | ).catch((err) => {
294 | setDisabled(false);
295 | setBackDisabled(false);
296 | setLoading(false);
297 | console.log(err);
298 | dispatch(
299 | openSnackbar({
300 | open: true,
301 | message: "Error creating podcast",
302 | severity: "error",
303 | })
304 | )
305 | });
306 | };
307 |
308 | useEffect(() => {
309 | if (podcast.episodes.length > 0 && podcast.episodes.every(episode => episode.file !== "" && episode.name !== "" && episode.desc !== "" && podcast.name !== "" && podcast.desc !== "" && podcast.tags !== "" && podcast.image !== "" && podcast.image !== undefined && podcast.image !== null)) {
310 | if (podcast.episodes.every(episode => episode.file.name === undefined))
311 | setCreateDisabled(false);
312 | else
313 | setCreateDisabled(true);
314 | }
315 | }, [podcast]);
316 |
317 | return (
318 | setUploadOpen(false)}>
319 |
320 |
321 | setUploadOpen(false)}
329 | />
330 | Upload Podcast
331 | {!showEpisode ? (
332 | <>
333 |
334 |
335 |
336 |
337 | setPodcast({ ...podcast, name: e.target.value })}
342 | />
343 |
344 |
345 | setPodcast({ ...podcast, desc: e.target.value })}
351 | />
352 |
353 |
354 | setPodcast({ ...podcast, tags: e.target.value.split(",") })}
360 | />
361 |
362 |
363 |
364 |
371 |
372 |
373 |
383 |
384 |
385 |
386 | {
391 | !disabled && goToAddEpisodes();
392 | }}
393 | >
394 | Next
395 |
396 | >
397 | ) : (
398 | <>
399 |
400 | {podcast.episodes.map((episode, index) => (
401 | <>
402 |
403 | {podcast.episodes[index].file === "" ? (
404 |
405 |
406 | Select Audio / Video
407 |
408 | ) : (
409 |
410 | {podcast.episodes[index].file.name === undefined ? (
411 |
412 |
413 | File Uploaded Successfully
414 |
415 | ) : (
416 | <>
417 | File: {podcast.episodes[index].file.name}
418 |
424 | {podcast.episodes[index].file.uploadProgress}% Uploaded
425 | >
426 | )}
427 |
428 | )}
429 |
430 | {
432 | podcast.episodes[index].file = e.target.files[0];
433 | setPodcast({ ...podcast, episodes: podcast.episodes });
434 | uploadFile(podcast.episodes[index].file, index);
435 | }}
436 | />
437 |
438 | {
443 | const newEpisodes = podcast.episodes;
444 | newEpisodes[index].name = e.target.value;
445 | setPodcast({ ...podcast, episodes: newEpisodes });
446 | }}
447 | />
448 |
449 |
450 | {
456 | const newEpisodes = podcast.episodes;
457 | newEpisodes[index].desc = e.target.value;
458 | setPodcast({ ...podcast, episodes: newEpisodes });
459 | }}
460 | />
461 |
462 |
467 | setPodcast({
468 | ...podcast, episodes: podcast.episodes.filter((_, i) => i !== index)
469 | })
470 | }
471 | >
472 | Delete
473 |
474 | >
475 | ))}
476 |
481 | setPodcast({ ...podcast, episodes: [...podcast.episodes, { name: "", desc: "", file: "" }] })
482 | }
483 | >
484 | Add Episode
485 |
486 |
487 |
488 | {
493 | !backDisabled && goToPodcast();
494 | }}
495 | >
496 | Back
497 |
498 | {
503 | !disabled && createpodcast();
504 | }}
505 | >
506 | {loading ? (
507 |
508 | ) : (
509 | "Create"
510 | )}
511 |
512 |
513 |
514 | >
515 | )}
516 |
517 |
518 |
519 | )
520 | }
521 |
522 | export default Upload
--------------------------------------------------------------------------------
/client/src/components/Signin.jsx:
--------------------------------------------------------------------------------
1 | import {
2 | Block,
3 | CloseRounded,
4 | EmailRounded,
5 | Visibility,
6 | VisibilityOff,
7 | PasswordRounded,
8 | TroubleshootRounded,
9 | } from "@mui/icons-material";
10 | import React, { useState, useEffect } from "react";
11 | import styled from "styled-components";
12 | import { IconButton, Modal } from "@mui/material";
13 | import CircularProgress from "@mui/material/CircularProgress";
14 | import { loginFailure, loginStart, loginSuccess } from "../redux/userSlice";
15 | import { openSnackbar } from "../redux/snackbarSlice";
16 | import { useDispatch } from "react-redux";
17 | import validator from "validator";
18 | import { signIn, googleSignIn, findUserByEmail, resetPassword } from "../api/index";
19 | import OTP from "./OTP";
20 | import { useGoogleLogin } from "@react-oauth/google";
21 | import axios from "axios";
22 | import { closeSignin } from "../redux/setSigninSlice";
23 |
24 |
25 | const Container = styled.div`
26 | width: 100%;
27 | height: 100%;
28 | position: absolute;
29 | top: 0;
30 | left: 0;
31 | background-color: #000000a7;
32 | display: flex;
33 | align-items: center;
34 | justify-content: center;
35 | `;
36 |
37 | const Wrapper = styled.div`
38 | width: 380px;
39 | border-radius: 16px;
40 | background-color: ${({ theme }) => theme.card};
41 | color: ${({ theme }) => theme.text_primary};
42 | padding: 10px;
43 | display: flex;
44 | flex-direction: column;
45 | position: relative;
46 | `;
47 |
48 | const Title = styled.div`
49 | font-size: 22px;
50 | font-weight: 500;
51 | color: ${({ theme }) => theme.text_primary};
52 | margin: 16px 28px;
53 | `;
54 | const OutlinedBox = styled.div`
55 | height: 44px;
56 | border-radius: 12px;
57 | border: 1px solid ${({ theme }) => theme.text_secondary};
58 | color: ${({ theme }) => theme.text_secondary};
59 | ${({ googleButton, theme }) =>
60 | googleButton &&
61 | `
62 | user-select: none;
63 | gap: 16px;`}
64 | ${({ button, theme }) =>
65 | button &&
66 | `
67 | user-select: none;
68 | border: none;
69 | background: ${theme.button};
70 | color:'${theme.bg}';`}
71 | ${({ activeButton, theme }) =>
72 | activeButton &&
73 | `
74 | user-select: none;
75 | border: none;
76 | background: ${theme.primary};
77 | color: white;`}
78 | margin: 3px 20px;
79 | font-size: 14px;
80 | display: flex;
81 | justify-content: center;
82 | align-items: center;
83 | font-weight: 500;
84 | padding: 0px 14px;
85 | `;
86 | const GoogleIcon = styled.img`
87 | width: 22px;
88 | `;
89 | const Divider = styled.div`
90 | display: flex;
91 | display: flex;
92 | justify-content: center;
93 | align-items: center;
94 | color: ${({ theme }) => theme.text_secondary};
95 | font-size: 14px;
96 | font-weight: 600;
97 | `;
98 | const Line = styled.div`
99 | width: 80px;
100 | height: 1px;
101 | border-radius: 10px;
102 | margin: 0px 10px;
103 | background-color: ${({ theme }) => theme.text_secondary};
104 | `;
105 |
106 | const TextInput = styled.input`
107 | width: 100%;
108 | border: none;
109 | font-size: 14px;
110 | border-radius: 3px;
111 | background-color: transparent;
112 | outline: none;
113 | color: ${({ theme }) => theme.text_secondary};
114 | `;
115 |
116 | const LoginText = styled.div`
117 | font-size: 14px;
118 | font-weight: 500;
119 | color: ${({ theme }) => theme.text_secondary};
120 | margin: 20px 20px 30px 20px;
121 | display: flex;
122 | justify-content: center;
123 | align-items: center;
124 | `;
125 | const Span = styled.span`
126 | color: ${({ theme }) => theme.primary};
127 | `;
128 |
129 | const Error = styled.div`
130 | color: red;
131 | font-size: 10px;
132 | margin: 2px 26px 8px 26px;
133 | display: block;
134 | ${({ error, theme }) =>
135 | error === "" &&
136 | ` display: none;
137 | `}
138 | `;
139 |
140 | const ForgetPassword = styled.div`
141 | color: ${({ theme }) => theme.text_secondary};
142 | font-size: 13px;
143 | margin: 8px 26px;
144 | display: block;
145 | cursor: pointer;
146 | text-align: right;
147 | &:hover {
148 | color: ${({ theme }) => theme.primary};
149 | }
150 | `;
151 |
152 | const SignIn = ({ setSignInOpen, setSignUpOpen }) => {
153 | const [email, setEmail] = useState("");
154 | const [password, setPassword] = useState("");
155 | const [Loading, setLoading] = useState(false);
156 | const [disabled, setDisabled] = useState(true);
157 | const [values, setValues] = useState({
158 | password: "",
159 | showPassword: false,
160 | });
161 |
162 | //verify otp
163 | const [showOTP, setShowOTP] = useState(false);
164 | const [otpVerified, setOtpVerified] = useState(false);
165 | //reset password
166 | const [showForgotPassword, setShowForgotPassword] = useState(false);
167 | const [samepassword, setSamepassword] = useState("");
168 | const [newpassword, setNewpassword] = useState("");
169 | const [confirmedpassword, setConfirmedpassword] = useState("");
170 | const [passwordCorrect, setPasswordCorrect] = useState(false);
171 | const [resetDisabled, setResetDisabled] = useState(true);
172 | const [resettingPassword, setResettingPassword] = useState(false);
173 | const dispatch = useDispatch();
174 |
175 | useEffect(() => {
176 | if (email !== "") validateEmail();
177 | if (validator.isEmail(email) && password.length > 5) {
178 | setDisabled(false);
179 | } else {
180 | setDisabled(true);
181 | }
182 | }, [email, password]);
183 |
184 | const handleLogin = async (e) => {
185 | e.preventDefault();
186 | if (!disabled) {
187 | dispatch(loginStart());
188 | setDisabled(true);
189 | setLoading(true);
190 | try {
191 | signIn({ email, password }).then((res) => {
192 | if (res.status === 200) {
193 | dispatch(loginSuccess(res.data));
194 | setLoading(false);
195 | setDisabled(false);
196 | dispatch(
197 | closeSignin()
198 | )
199 | dispatch(
200 | openSnackbar({
201 | message: "Logged In Successfully",
202 | severity: "success",
203 | })
204 | );
205 | } else if (res.status === 203) {
206 | dispatch(loginFailure());
207 | setLoading(false);
208 | setDisabled(false);
209 | setcredentialError(res.data.message);
210 | dispatch(
211 | openSnackbar({
212 | message: "Account Not Verified",
213 | severity: "error",
214 | })
215 | );
216 | } else {
217 | dispatch(loginFailure());
218 | setLoading(false);
219 | setDisabled(false);
220 | setcredentialError(`Invalid Credentials : ${res.data.message}`);
221 | }
222 | });
223 | } catch (err) {
224 | dispatch(loginFailure());
225 | setLoading(false);
226 | setDisabled(false);
227 | dispatch(
228 | openSnackbar({
229 | message: err.message,
230 | severity: "error",
231 | })
232 | );
233 | }
234 | }
235 | if (email === "" || password === "") {
236 | dispatch(
237 | openSnackbar({
238 | message: "Please fill all the fields",
239 | severity: "error",
240 | })
241 | );
242 | }
243 | };
244 |
245 | const [emailError, setEmailError] = useState("");
246 | const [credentialError, setcredentialError] = useState("");
247 |
248 | const validateEmail = () => {
249 | if (validator.isEmail(email)) {
250 | setEmailError("");
251 | } else {
252 | setEmailError("Enter a valid Email Id!");
253 | }
254 | };
255 |
256 |
257 | //validate password
258 | const validatePassword = () => {
259 | if (newpassword.length < 8) {
260 | setSamepassword("Password must be atleast 8 characters long!");
261 | setPasswordCorrect(false);
262 | } else if (newpassword.length > 16) {
263 | setSamepassword("Password must be less than 16 characters long!");
264 | setPasswordCorrect(false);
265 | } else if (
266 | !newpassword.match(/[a-z]/g) ||
267 | !newpassword.match(/[A-Z]/g) ||
268 | !newpassword.match(/[0-9]/g) ||
269 | !newpassword.match(/[^a-zA-Z\d]/g)
270 | ) {
271 | setPasswordCorrect(false);
272 | setSamepassword(
273 | "Password must contain atleast one lowercase, uppercase, number and special character!"
274 | );
275 | }
276 | else {
277 | setSamepassword("");
278 | setPasswordCorrect(true);
279 | }
280 | };
281 |
282 | useEffect(() => {
283 | if (newpassword !== "") validatePassword();
284 | if (
285 | passwordCorrect
286 | && newpassword === confirmedpassword
287 | ) {
288 | setSamepassword("");
289 | setResetDisabled(false);
290 | } else if (confirmedpassword !== "" && passwordCorrect) {
291 | setSamepassword("Passwords do not match!");
292 | setResetDisabled(true);
293 | }
294 | }, [newpassword, confirmedpassword]);
295 |
296 | const sendOtp = () => {
297 | if (!resetDisabled) {
298 | setResetDisabled(true);
299 | setLoading(true);
300 | findUserByEmail(email).then((res) => {
301 | if (res.status === 200) {
302 | setShowOTP(true);
303 | setResetDisabled(false);
304 | setLoading(false);
305 | }
306 | else if (res.status === 202) {
307 | setEmailError("User not found!")
308 | setResetDisabled(false);
309 | setLoading(false);
310 | }
311 | }).catch((err) => {
312 | setResetDisabled(false);
313 | setLoading(false);
314 | dispatch(
315 | openSnackbar({
316 | message: err.message,
317 | severity: "error",
318 | })
319 | );
320 | });
321 | }
322 | }
323 |
324 | const performResetPassword = async () => {
325 | if (otpVerified) {
326 | setShowOTP(false);
327 | setResettingPassword(true);
328 | await resetPassword(email, confirmedpassword).then((res) => {
329 | if (res.status === 200) {
330 | dispatch(
331 | openSnackbar({
332 | message: "Password Reset Successfully",
333 | severity: "success",
334 | })
335 | );
336 | setShowForgotPassword(false);
337 | setEmail("");
338 | setNewpassword("");
339 | setConfirmedpassword("");
340 | setOtpVerified(false);
341 | setResettingPassword(false);
342 | }
343 | }).catch((err) => {
344 | dispatch(
345 | openSnackbar({
346 | message: err.message,
347 | severity: "error",
348 | })
349 | );
350 | setShowOTP(false);
351 | setOtpVerified(false);
352 | setResettingPassword(false);
353 | });
354 | }
355 | }
356 | const closeForgetPassword = () => {
357 | setShowForgotPassword(false)
358 | setShowOTP(false)
359 | }
360 | useEffect(() => {
361 | performResetPassword();
362 | }, [otpVerified]);
363 |
364 |
365 | //Google SignIn
366 | const googleLogin = useGoogleLogin({
367 | onSuccess: async (tokenResponse) => {
368 | setLoading(true);
369 | const user = await axios.get(
370 | 'https://www.googleapis.com/oauth2/v3/userinfo',
371 | { headers: { Authorization: `Bearer ${tokenResponse.access_token}` } },
372 | ).catch((err) => {
373 | dispatch(loginFailure());
374 | dispatch(
375 | openSnackbar({
376 | message: err.message,
377 | severity: "error",
378 | })
379 | );
380 | });
381 |
382 | googleSignIn({
383 | name: user.data.name,
384 | email: user.data.email,
385 | img: user.data.picture,
386 | }).then((res) => {
387 | console.log(res);
388 | if (res.status === 200) {
389 | dispatch(loginSuccess(res.data));
390 | dispatch(
391 | closeSignin()
392 | );
393 | dispatch(
394 | openSnackbar({
395 | message: "Logged In Successfully",
396 | severity: "success",
397 | })
398 | );
399 | setLoading(false);
400 | } else {
401 | dispatch(loginFailure(res.data));
402 | dispatch(
403 | openSnackbar({
404 | message: res.data.message,
405 | severity: "error",
406 | })
407 | );
408 | setLoading(false);
409 | }
410 | });
411 | },
412 | onError: errorResponse => {
413 | dispatch(loginFailure());
414 | setLoading(false);
415 | dispatch(
416 | openSnackbar({
417 | message: errorResponse.error,
418 | severity: "error",
419 | })
420 | );
421 | },
422 | });
423 |
424 |
425 | return (
426 | dispatch(
427 | closeSignin()
428 | )}>
429 |
430 | {!showForgotPassword ? (
431 |
432 | dispatch(
440 | closeSignin()
441 | )}
442 | />
443 | <>
444 | Sign In
445 | googleLogin()}
449 | >
450 | {Loading ? (
451 |
452 | ) : (
453 | <>
454 |
455 | Sign In with Google>
456 | )}
457 |
458 |
459 |
460 | or
461 |
462 |
463 |
464 |
468 | setEmail(e.target.value)}
472 | />
473 |
474 | {emailError}
475 |
476 |
480 | setPassword(e.target.value)}
484 | />
485 |
488 | setValues({ ...values, showPassword: !values.showPassword })
489 | }
490 | >
491 | {values.showPassword ? (
492 |
493 | ) : (
494 |
495 | )}
496 |
497 |
498 | {credentialError}
499 | { setShowForgotPassword(true) }}>Forgot password ?
500 |
506 | {Loading ? (
507 |
508 | ) : (
509 | "Sign In"
510 | )}
511 |
512 | >
513 |
514 | Don't have an account ?
515 | {
517 | setSignUpOpen(true);
518 | dispatch(
519 | closeSignin({
520 |
521 | })
522 | );
523 | }}
524 | style={{
525 | fontWeight: "500",
526 | marginLeft: "6px",
527 | cursor: "pointer",
528 | }}
529 | >
530 | Create Account
531 |
532 |
533 |
534 | ) : (
535 |
536 | { closeForgetPassword() }}
544 | />
545 | {!showOTP ?
546 | <>
547 | Reset Password
548 | {resettingPassword ?
549 | Updating password
550 | :
551 | <>
552 |
553 |
554 |
558 | setEmail(e.target.value)}
562 | />
563 |
564 | {emailError}
565 |
566 |
570 | setNewpassword(e.target.value)}
574 | />
575 |
576 |
577 |
581 | setConfirmedpassword(e.target.value)}
585 | />
586 |
589 | setValues({ ...values, showPassword: !values.showPassword })
590 | }
591 | >
592 | {values.showPassword ? (
593 |
594 | ) : (
595 |
596 | )}
597 |
598 |
599 | {samepassword}
600 | sendOtp()}
605 | >
606 | {Loading ? (
607 |
608 | ) : (
609 | "Submit"
610 | )}
611 |
612 |
613 | Don't have an account ?
614 | {
616 | setSignUpOpen(true);
617 | dispatch(
618 | closeSignin()
619 | )
620 | }}
621 | style={{
622 | fontWeight: "500",
623 | marginLeft: "6px",
624 | cursor: "pointer",
625 | }}
626 | >
627 | Create Account
628 |
629 |
630 | >
631 | }
632 | >
633 |
634 | :
635 |
636 | }
637 |
638 |
639 |
640 | )}
641 |
642 |
643 | );
644 | };
645 |
646 | export default SignIn;
--------------------------------------------------------------------------------