├── 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 && } 86 | 87 | 88 | 89 | } /> 90 | } /> 91 | } /> 92 | } /> 93 | } /> 94 | } /> 95 | 96 | 97 | 98 | 99 | {open && } 100 | 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; --------------------------------------------------------------------------------