├── .gitignore ├── .env ├── public ├── uploads │ └── chatapp.jpg ├── index.html ├── vite.svg └── assets │ ├── main-ewHsHjHH.svg │ ├── index-2SqZJ_mH.css │ └── not-found-KNix-0js.svg ├── client ├── src │ ├── assets │ │ ├── images │ │ │ ├── favicon.ico │ │ │ ├── avatar-1.jpg │ │ │ ├── avatar-2.jpg │ │ │ ├── logo.svg │ │ │ ├── main-alternative.svg │ │ │ ├── main.svg │ │ │ └── not-found.svg │ │ ├── wrappers │ │ │ ├── StatsContainer.js │ │ │ ├── ThemeToggle.js │ │ │ ├── JobInfo.js │ │ │ ├── ChartsContainer.js │ │ │ ├── Dashboard.js │ │ │ ├── JobsContainer.js │ │ │ ├── ErrorPage.js │ │ │ ├── RegisterAndLoginPage.js │ │ │ ├── Testing.js │ │ │ ├── DashboardFormPage.js │ │ │ ├── LogoutContainer.js │ │ │ ├── LandingPage.js │ │ │ ├── StatItem.js │ │ │ ├── Navbar.js │ │ │ ├── PageBtnContainer.js │ │ │ ├── BigSidebar.js │ │ │ ├── SmallSidebar.js │ │ │ └── Job.js │ │ ├── react.svg │ │ └── css │ │ │ └── index.css │ ├── components │ │ ├── Loading.jsx │ │ ├── Logo.jsx │ │ ├── ErrorElement.jsx │ │ ├── JobInfo.jsx │ │ ├── StatItem.jsx │ │ ├── SubmitBtn.jsx │ │ ├── FormRow.jsx │ │ ├── ThemeToggle.jsx │ │ ├── ChartsContainer.jsx │ │ ├── BarChart.jsx │ │ ├── AreaChart.jsx │ │ ├── BigSidebar.jsx │ │ ├── JobsContainer.jsx │ │ ├── index.js │ │ ├── FormRowSelect.jsx │ │ ├── Navbar.jsx │ │ ├── NavLinks.jsx │ │ ├── SmallSidebar.jsx │ │ ├── StatsContainer.jsx │ │ ├── LogoutContainer.jsx │ │ ├── Job.jsx │ │ ├── SearchContainer.jsx │ │ └── PageBtnContainer.jsx │ ├── utils │ │ ├── customFetch.js │ │ └── links.jsx │ ├── pages │ │ ├── HomeLayout.jsx │ │ ├── DeleteJob.jsx │ │ ├── index.js │ │ ├── Stats.jsx │ │ ├── Error.jsx │ │ ├── Alljobs.jsx │ │ ├── Admin.jsx │ │ ├── Register.jsx │ │ ├── Login.jsx │ │ ├── DashboardLayout.jsx │ │ ├── Landing.jsx │ │ ├── AddJob.jsx │ │ ├── Profile.jsx │ │ └── EditJob.jsx │ ├── main.jsx │ ├── App.jsx │ └── index.css ├── index.html ├── vite.config.js ├── .eslintrc.cjs ├── .gitignore ├── package.json └── public │ └── vite.svg ├── temp.md ├── utils ├── tokenUtils.js ├── PasswordUtils.js └── constants.js ├── Middleware ├── ErrorHandler.js ├── multerMiddleware.js ├── authMiddleware.js └── ValidationMiddleware.js ├── Router ├── authRouter.js ├── jobRouter.js └── userRouter.js ├── Models ├── UserModel.js └── JobModel.js ├── CustomError └── customError.js ├── package.json ├── controllers ├── authController.js ├── userController.js └── jobcontroller.js └── index.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | JWT_SECRET="secret" 2 | JWT_EXPIRES_IN = "1d" -------------------------------------------------------------------------------- /public/uploads/chatapp.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandanegc/Job_Seeker_App/HEAD/public/uploads/chatapp.jpg -------------------------------------------------------------------------------- /client/src/assets/images/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandanegc/Job_Seeker_App/HEAD/client/src/assets/images/favicon.ico -------------------------------------------------------------------------------- /client/src/assets/images/avatar-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandanegc/Job_Seeker_App/HEAD/client/src/assets/images/avatar-1.jpg -------------------------------------------------------------------------------- /client/src/assets/images/avatar-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chandanegc/Job_Seeker_App/HEAD/client/src/assets/images/avatar-2.jpg -------------------------------------------------------------------------------- /client/src/components/Loading.jsx: -------------------------------------------------------------------------------- 1 | const Loading = () => { 2 | return
; 3 | }; 4 | export default Loading; 5 | -------------------------------------------------------------------------------- /client/src/utils/customFetch.js: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | const customFetch= axios.create({ 3 | baseURL:"/api/v1" 4 | }) 5 | export default customFetch; -------------------------------------------------------------------------------- /temp.md: -------------------------------------------------------------------------------- 1 | #### express-async-errors 2 | 3 | 4 | const navigation = useNavigation(); 5 | const isSubmitting = navigation.state === 'submitting'; 6 | {isSubmitting ? 'submitting...' : 'submit'} -------------------------------------------------------------------------------- /client/src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | import logo from '../assets/images/logo.svg'; 2 | 3 | const Logo = () => { 4 | return jobify; 5 | }; 6 | 7 | export default Logo; 8 | -------------------------------------------------------------------------------- /client/src/pages/HomeLayout.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Outlet } from 'react-router-dom'; 3 | 4 | const HomeLayout = () => { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | 12 | export default HomeLayout; -------------------------------------------------------------------------------- /client/src/components/ErrorElement.jsx: -------------------------------------------------------------------------------- 1 | import { useRouteError } from 'react-router-dom'; 2 | 3 | const ErrorElement = () => { 4 | const error = useRouteError(); 5 | console.log(error); 6 | return

There was an error...

; 7 | }; 8 | export default ErrorElement; 9 | -------------------------------------------------------------------------------- /utils/tokenUtils.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken'; 2 | 3 | export const createJWT = (payload) => { 4 | const token = jwt.sign(payload, "abc",{ expiresIn: '1h' }); 5 | return token; 6 | }; 7 | 8 | export const verifyJWT = (token) => { 9 | const decoded = jwt.verify(token, "abc"); 10 | return decoded; 11 | }; -------------------------------------------------------------------------------- /client/src/components/JobInfo.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/JobInfo'; 2 | 3 | const JobInfo = ({ icon, text }) => { 4 | return ( 5 | 6 | {icon} 7 | {text} 8 | 9 | ); 10 | }; 11 | export default JobInfo; 12 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/StatsContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | display: grid; 5 | row-gap: 2rem; 6 | @media (min-width: 768px) { 7 | grid-template-columns: 1fr 1fr; 8 | column-gap: 1rem; 9 | } 10 | @media (min-width: 1120px) { 11 | grid-template-columns: 1fr 1fr 1fr; 12 | } 13 | `; 14 | export default Wrapper; 15 | -------------------------------------------------------------------------------- /Middleware/ErrorHandler.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from "http-status-codes"; 2 | const errorHandlerMiddleware = (err, req, res, next) => { 3 | console.log(err); 4 | const statusCode = err.statusCode || StatusCodes.INTERNAL_SERVER_ERROR; 5 | const msg = err.message || 'Something went wrong, try again later'; 6 | 7 | res.status(statusCode).json({ msg }); 8 | }; 9 | export default errorHandlerMiddleware; -------------------------------------------------------------------------------- /utils/PasswordUtils.js: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | export async function hashPassword(password) { 4 | const salt = await bcrypt.genSalt(10); 5 | const hashedPassword = await bcrypt.hash(password, salt); 6 | return hashedPassword; 7 | } 8 | 9 | export async function comparePassword(password, hashedPassword) { 10 | const isMatch = await bcrypt.compare(password, hashedPassword); 11 | return isMatch; 12 | } -------------------------------------------------------------------------------- /client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mern learn 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/ThemeToggle.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.button` 4 | background: transparent; 5 | border-color: transparent; 6 | width: 3.5rem; 7 | height: 2rem; 8 | display: grid; 9 | place-items: center; 10 | cursor: pointer; 11 | .toggle-icon { 12 | font-size: 1.15rem; 13 | color: var(--text-color); 14 | } 15 | `; 16 | export default Wrapper; 17 | -------------------------------------------------------------------------------- /Router/authRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import { register, login, logout } from '../controllers/authController.js'; 3 | import { validateLoginInput, validateRegisterInput } from '../Middleware/ValidationMiddleware.js'; 4 | 5 | const router = Router(); 6 | 7 | router.post('/register', validateRegisterInput, register); 8 | router.post('/login', validateLoginInput, login); 9 | router.get('/logout', logout); 10 | 11 | export default router; 12 | -------------------------------------------------------------------------------- /client/src/components/StatItem.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/StatItem'; 2 | 3 | const StatItem = ({ count, title, icon, color, bcg }) => { 4 | return ( 5 | 6 |
7 | {count} 8 | {icon} 9 |
10 |
{title}
11 |
12 | ); 13 | }; 14 | export default StatItem; 15 | -------------------------------------------------------------------------------- /utils/constants.js: -------------------------------------------------------------------------------- 1 | export const JOB_STATUS = { 2 | PENDING: 'pending', 3 | INTERVIEW: 'interview', 4 | DECLINED: 'declined', 5 | }; 6 | 7 | export const JOB_TYPE = { 8 | FULL_TIME: 'full-time', 9 | PART_TIME: 'part-time', 10 | INTERNSHIP: 'internship', 11 | }; 12 | 13 | export const JOB_SORT_BY = { 14 | NEWEST_FIRST: 'newest', 15 | OLDEST_FIRST: 'oldest', 16 | ASCENDING: 'a-z', 17 | DESCENDING: 'z-a', 18 | }; -------------------------------------------------------------------------------- /client/src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ReactDOM from 'react-dom/client' 3 | import App from './App.jsx' 4 | import './index.css' 5 | 6 | import 'react-toastify/dist/ReactToastify.css'; 7 | import { ToastContainer } from 'react-toastify'; 8 | 9 | 10 | 11 | ReactDOM.createRoot(document.getElementById('root')).render( 12 | 13 | 14 | 15 | , 16 | ) 17 | -------------------------------------------------------------------------------- /Middleware/multerMiddleware.js: -------------------------------------------------------------------------------- 1 | import multer from "multer"; 2 | 3 | const storage = multer.diskStorage({ 4 | destination: (req, file, cb) => { 5 | // set the directory where uploaded files will be stored 6 | cb(null, 'public/uploads'); 7 | }, 8 | filename: (req, file, cb) => { 9 | const fileName = file.originalname; 10 | // set the name of the uploaded file 11 | cb(null, fileName); 12 | }, 13 | }); 14 | const upload = multer({ storage }); 15 | 16 | export default upload; -------------------------------------------------------------------------------- /client/src/components/SubmitBtn.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigation } from 'react-router-dom'; 2 | const SubmitBtn = ({ formBtn }) => { 3 | const navigation = useNavigation(); 4 | const isSubmitting = navigation.state === 'submitting'; 5 | return ( 6 | 13 | ); 14 | }; 15 | export default SubmitBtn; 16 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/JobInfo.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | display: flex; 5 | align-items: center; 6 | .job-icon { 7 | font-size: 1rem; 8 | margin-right: 1rem; 9 | display: flex; 10 | align-items: center; 11 | svg { 12 | color: var(--text-secondary-color); 13 | } 14 | } 15 | .job-text { 16 | text-transform: capitalize; 17 | letter-spacing: var(--letter-spacing); 18 | } 19 | `; 20 | export default Wrapper; 21 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/ChartsContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | margin-top: 4rem; 5 | text-align: center; 6 | button { 7 | background: transparent; 8 | border-color: transparent; 9 | text-transform: capitalize; 10 | color: var(--primary-500); 11 | font-size: 1.25rem; 12 | cursor: pointer; 13 | } 14 | h4 { 15 | text-align: center; 16 | margin-bottom: 0.75rem; 17 | } 18 | `; 19 | 20 | export default Wrapper; 21 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/Dashboard.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | .dashboard { 5 | display: grid; 6 | grid-template-columns: 1fr; 7 | } 8 | .dashboard-page { 9 | width: 90vw; 10 | margin: 0 auto; 11 | padding: 2rem 0; 12 | } 13 | @media (min-width: 992px) { 14 | .dashboard { 15 | grid-template-columns: auto 1fr; 16 | } 17 | .dashboard-page { 18 | width: 90%; 19 | } 20 | } 21 | `; 22 | export default Wrapper; 23 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Mern learn 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/JobsContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | margin-top: 4rem; 5 | h2 { 6 | text-transform: none; 7 | } 8 | & > h5 { 9 | font-weight: 700; 10 | margin-bottom: 1.5rem; 11 | } 12 | .jobs { 13 | display: grid; 14 | grid-template-columns: 1fr; 15 | row-gap: 2rem; 16 | } 17 | @media (min-width: 1120px) { 18 | .jobs { 19 | grid-template-columns: 1fr 1fr; 20 | gap: 2rem; 21 | } 22 | } 23 | `; 24 | export default Wrapper; 25 | -------------------------------------------------------------------------------- /client/src/components/FormRow.jsx: -------------------------------------------------------------------------------- 1 | const FormRow = ({ type, name, labelText, defaultValue, onChange }) => { 2 | return ( 3 |
4 | 7 | 16 |
17 | ); 18 | }; 19 | export default FormRow; 20 | -------------------------------------------------------------------------------- /client/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite' 2 | import react from '@vitejs/plugin-react' 3 | 4 | // https://vitejs.dev/config/ 5 | 6 | export default defineConfig({ 7 | plugins: [react()], 8 | server: { 9 | proxy: { 10 | '/api': { 11 | // target: 'https://job-seeker-app-1.onrender.com/api', 12 | target: 'http://localhost:5100/api/', 13 | changeOrigin: true, 14 | rewrite: (path) => path.replace(/^\/api/, ''), 15 | }, 16 | }, 17 | }, 18 | }); 19 | 20 | // export default defineConfig({ 21 | // plugins: [react()], 22 | // }) 23 | -------------------------------------------------------------------------------- /client/src/components/ThemeToggle.jsx: -------------------------------------------------------------------------------- 1 | import { BsFillSunFill, BsFillMoonFill } from 'react-icons/bs'; 2 | import Wrapper from '../assets/wrappers/ThemeToggle'; 3 | import { useDashboardContext } from '../pages/DashboardLayout'; 4 | 5 | const ThemeToggle = () => { 6 | const { isDarkTheme, toggleDarkTheme } = useDashboardContext(); 7 | return ( 8 | 9 | {isDarkTheme ? ( 10 | 11 | ) : ( 12 | 13 | )} 14 | 15 | ); 16 | }; 17 | export default ThemeToggle; 18 | -------------------------------------------------------------------------------- /client/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react-refresh/only-export-components': [ 16 | 'warn', 17 | { allowConstantExport: true }, 18 | ], 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /client/src/pages/DeleteJob.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { redirect } from 'react-router-dom'; 3 | import customFetch from '../utils/customFetch'; 4 | import { toast } from 'react-toastify'; 5 | 6 | export async function action({ params }) { 7 | try { 8 | await customFetch.delete(`/jobs/${params.id}`); 9 | toast.success('Job deleted successfully'); 10 | } catch (error) { 11 | toast.error(error.response.data.msg); 12 | } 13 | return redirect('/dashboard/all-jobs'); 14 | } 15 | 16 | const DeleteJob = () => { 17 | return ( 18 |
DeleteJob
19 | ) 20 | } 21 | 22 | export default DeleteJob -------------------------------------------------------------------------------- /client/src/pages/index.js: -------------------------------------------------------------------------------- 1 | export { default as DashboardLayout } from './DashboardLayout'; 2 | export { default as Landing } from './Landing'; 3 | export { default as HomeLayout } from './HomeLayout'; 4 | export { default as Register } from './Register'; 5 | export { default as Login } from './Login'; 6 | export { default as Error } from './Error'; 7 | export { default as Stats } from './Stats'; 8 | export { default as AllJobs } from './AllJobs'; 9 | export { default as AddJob } from './AddJob'; 10 | export { default as EditJob } from './EditJob'; 11 | export { default as Profile } from './Profile'; 12 | export { default as Admin } from './Admin'; -------------------------------------------------------------------------------- /client/src/components/ChartsContainer.jsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | import BarChart from './BarChart'; 4 | import AreaChart from './AreaChart'; 5 | import Wrapper from '../assets/wrappers/ChartsContainer'; 6 | const ChartsContainer = ({ data }) => { 7 | const [barChart, setBarChart] = useState(true); 8 | 9 | return ( 10 | 11 |

Monthly Applications

12 | 15 | {barChart ? : } 16 |
17 | ); 18 | }; 19 | export default ChartsContainer; 20 | -------------------------------------------------------------------------------- /Router/jobRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { createJob, deletejob, getAllJobs, getJob, showStats, updateJob } from "../controllers/jobcontroller.js"; 3 | import { validateJobInput } from "../Middleware/ValidationMiddleware.js"; 4 | import { authenticateUser } from "../Middleware/authMiddleware.js"; 5 | 6 | const router = Router(); 7 | 8 | router.route("/").post(authenticateUser, validateJobInput,createJob).get(authenticateUser ,getAllJobs); 9 | router.route("/stats").get(authenticateUser ,showStats) 10 | router.route("/:id").get(authenticateUser ,getJob).delete(authenticateUser , deletejob).patch(authenticateUser ,validateJobInput , updateJob); 11 | 12 | export default router; -------------------------------------------------------------------------------- /client/src/components/BarChart.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | BarChart, 3 | Bar, 4 | XAxis, 5 | YAxis, 6 | CartesianGrid, 7 | Tooltip, 8 | ResponsiveContainer, 9 | } from 'recharts'; 10 | 11 | const BarChartComponent = ({ data }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | export default BarChartComponent; 25 | -------------------------------------------------------------------------------- /client/src/components/AreaChart.jsx: -------------------------------------------------------------------------------- 1 | import { 2 | ResponsiveContainer, 3 | AreaChart, 4 | Area, 5 | XAxis, 6 | YAxis, 7 | CartesianGrid, 8 | Tooltip, 9 | } from 'recharts'; 10 | 11 | const AreaChartComponent = ({ data }) => { 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | }; 24 | export default AreaChartComponent; 25 | -------------------------------------------------------------------------------- /Models/UserModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const UserSchema = new mongoose.Schema({ 4 | name: String, 5 | email: String, 6 | password: String, 7 | lastName: { 8 | type: String, 9 | default: 'lastName', 10 | }, 11 | location: { 12 | type: String, 13 | default: 'my city', 14 | }, 15 | role: { 16 | type: String, 17 | enum: ['user', 'admin'], 18 | default: 'user', 19 | }, 20 | avatar:String, 21 | avatarPublicId:String, 22 | }); 23 | 24 | // remove password (select) 25 | UserSchema.methods.toJSON = function () { 26 | var obj = this.toObject(); 27 | delete obj.password; 28 | return obj; 29 | }; 30 | 31 | 32 | export default mongoose.model('User', UserSchema); -------------------------------------------------------------------------------- /client/src/assets/wrappers/ErrorPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.main` 4 | min-height: 100vh; 5 | text-align: center; 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | img { 10 | width: 90vw; 11 | max-width: 600px; 12 | display: block; 13 | margin-bottom: 2rem; 14 | margin-top: -3rem; 15 | } 16 | h3 { 17 | margin-bottom: 0.5rem; 18 | } 19 | p { 20 | line-height: 1.5; 21 | margin-top: 0.5rem; 22 | margin-bottom: 1rem; 23 | color: var(--text-secondary-color); 24 | } 25 | a { 26 | color: var(--primary-500); 27 | text-transform: capitalize; 28 | } 29 | `; 30 | 31 | export default Wrapper; 32 | -------------------------------------------------------------------------------- /client/src/components/BigSidebar.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/BigSidebar'; 2 | import NavLinks from './NavLinks'; 3 | import Logo from './Logo'; 4 | import { useDashboardContext } from '../pages/DashboardLayout'; 5 | const BigSidebar = () => { 6 | const { showSidebar } = useDashboardContext(); 7 | 8 | return ( 9 | 10 |
15 |
16 |
17 | 18 |
19 | 20 |
21 |
22 |
23 | ); 24 | }; 25 | export default BigSidebar; 26 | -------------------------------------------------------------------------------- /client/src/pages/Stats.jsx: -------------------------------------------------------------------------------- 1 | import { ChartsContainer, StatsContainer } from '../components'; 2 | import customFetch from '../utils/customFetch'; 3 | import { useLoaderData } from 'react-router-dom'; 4 | 5 | export const loader = async () => { 6 | try { 7 | const response = await customFetch.get('/jobs/stats'); 8 | return response.data; 9 | } catch (error) { 10 | return error; 11 | } 12 | }; 13 | 14 | const Stats = () => { 15 | const { defaultStats, monthlyApplications } = useLoaderData(); 16 | return ( 17 | <> 18 | 19 | {monthlyApplications?.length > 0 && ( 20 | 21 | )} 22 | 23 | ); 24 | }; 25 | export default Stats; -------------------------------------------------------------------------------- /client/src/components/JobsContainer.jsx: -------------------------------------------------------------------------------- 1 | import Job from './Job'; 2 | import Wrapper from '../assets/wrappers/JobsContainer'; 3 | import PageBtnContainer from './PageBtnContainer'; 4 | import { useAllJobsContext } from '../pages/AllJobs'; 5 | 6 | 7 | const JobsContainer = () => { 8 | const { data } = useAllJobsContext(); 9 | const { jobs } = data; 10 | if (jobs.length === 0) { 11 | return ( 12 | 13 |

No jobs to display...

14 |
15 | ); 16 | } 17 | 18 | return ( 19 | 20 |
21 | {jobs.map((job) => { 22 | return ; 23 | })} 24 |
25 |
26 | ); 27 | }; 28 | 29 | export default JobsContainer; -------------------------------------------------------------------------------- /Router/userRouter.js: -------------------------------------------------------------------------------- 1 | import { Router } from "express"; 2 | import { getApplicationStats, getCurrentUser, updateUser } from "../controllers/userController.js"; 3 | import { authenticateUser, authorizePermissions } from "../Middleware/authMiddleware.js"; 4 | import upload from "../Middleware/multerMiddleware.js"; 5 | import {validateUpdateUserInput} from '../Middleware/ValidationMiddleware.js' 6 | 7 | const router = Router(); 8 | 9 | router.get('/current-user',authenticateUser, getCurrentUser); 10 | // router.get('/admin/app-stats', [authorizePermissions('admin') , getApplicationStats]);// doubt 11 | router.get('/admin/app-stats', getApplicationStats);// doubt 12 | router.patch('/update-user',authenticateUser, upload.single('avatar'), updateUser); 13 | export default router; 14 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | export { default as Logo } from './Logo'; 2 | export { default as FormRow } from './FormRow'; 3 | export { default as BigSidebar } from './BigSidebar'; 4 | export { default as SmallSidebar } from './SmallSidebar'; 5 | export { default as Navbar } from './Navbar'; 6 | export { default as FormRowSelect } from './FormRowSelect'; 7 | export { default as JobsContainer } from './JobsContainer'; 8 | export { default as SearchContainer } from './SearchContainer'; 9 | export { default as StatItem } from './StatItem'; 10 | export { default as SubmitBtn } from './SubmitBtn'; 11 | export { default as ChartsContainer } from './ChartsContainer'; 12 | export { default as StatsContainer } from './StatsContainer'; 13 | export { default as Loading } from './Loading'; 14 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | 26 | 27 | # copy from github 28 | # Logs 29 | logs 30 | *.log 31 | npm-debug.log* 32 | yarn-debug.log* 33 | yarn-error.log* 34 | pnpm-debug.log* 35 | lerna-debug.log* 36 | package-lock.json 37 | 38 | node_modules 39 | dist 40 | dist-ssr 41 | *.local 42 | 43 | # Editor directories and files 44 | .vscode/* 45 | !.vscode/extensions.json 46 | .idea 47 | .DS_Store 48 | *.suo 49 | *.ntvs* 50 | *.njsproj 51 | *.sln 52 | *.sw? -------------------------------------------------------------------------------- /Models/JobModel.js: -------------------------------------------------------------------------------- 1 | import mongoose from "mongoose" 2 | import { JOB_STATUS, JOB_TYPE } from "../utils/constants.js"; 3 | 4 | const jobSchema = mongoose.Schema({ 5 | company:String, 6 | position:String, 7 | jobStatus:{ 8 | type:String, 9 | enum:Object.values(JOB_STATUS), 10 | default:JOB_STATUS.PENDING, 11 | }, 12 | jobType:{ 13 | type:String, 14 | enum:Object.values(JOB_TYPE), 15 | default:JOB_TYPE.FULL_TIME, 16 | }, 17 | jobLocation:{ 18 | type:String, 19 | default:'my-city' 20 | }, 21 | createdBy: { 22 | type: mongoose.Types.ObjectId, 23 | ref: 'User', 24 | }, 25 | }, 26 | {timestamps:true} 27 | ); 28 | 29 | const Job = mongoose.model('Job' , jobSchema); 30 | export default Job; 31 | -------------------------------------------------------------------------------- /client/src/components/FormRowSelect.jsx: -------------------------------------------------------------------------------- 1 | const FormRowSelect = ({ 2 | name, 3 | labelText, 4 | list, 5 | defaultValue = '', 6 | onChange, 7 | }) => { 8 | return ( 9 |
10 | 13 | 28 |
29 | ); 30 | }; 31 | export default FormRowSelect; 32 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/RegisterAndLoginPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | min-height: 100vh; 5 | display: grid; 6 | align-items: center; 7 | .logo { 8 | display: block; 9 | margin: 0 auto; 10 | margin-bottom: 1.38rem; 11 | } 12 | .form { 13 | max-width: 400px; 14 | border-top: 5px solid var(--primary-500); 15 | } 16 | h4 { 17 | text-align: center; 18 | margin-bottom: 1.38rem; 19 | } 20 | p { 21 | margin-top: 1rem; 22 | text-align: center; 23 | line-height: 1.5; 24 | } 25 | .btn { 26 | margin-top: 1rem; 27 | } 28 | .member-btn { 29 | color: var(--primary-500); 30 | letter-spacing: var(--letter-spacing); 31 | margin-left: 0.25rem; 32 | } 33 | `; 34 | export default Wrapper; 35 | -------------------------------------------------------------------------------- /client/src/pages/Error.jsx: -------------------------------------------------------------------------------- 1 | import { Link, useRouteError } from 'react-router-dom'; 2 | import img from '../assets/images/not-found.svg'; 3 | import Wrapper from '../assets/wrappers/ErrorPage'; 4 | 5 | const Error = () => { 6 | const error = useRouteError(); 7 | console.log(error); 8 | if (error.status === 404) { 9 | return ( 10 | 11 |
12 | not found 13 |

Ohh! page not found

14 |

We can't seem to find the page you're looking for

15 | back home 16 |
17 |
18 | ); 19 | } 20 | return ( 21 | 22 |
23 |

something went wrong

24 |
25 |
26 | ); 27 | }; 28 | 29 | export default Error; -------------------------------------------------------------------------------- /client/src/utils/links.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { IoBarChartSharp } from 'react-icons/io5'; 4 | import { MdQueryStats } from 'react-icons/md'; 5 | import { FaWpforms } from 'react-icons/fa'; 6 | import { ImProfile } from 'react-icons/im'; 7 | import { MdAdminPanelSettings } from 'react-icons/md'; 8 | 9 | const links = [ 10 | { 11 | text: 'add job', 12 | path: '.', 13 | icon: , 14 | }, 15 | { 16 | text: 'all jobs', 17 | path: 'all-jobs', 18 | icon: , 19 | }, 20 | { 21 | text: 'stats', 22 | path: 'stats', 23 | icon: , 24 | }, 25 | { 26 | text: 'profile', 27 | path: 'profile', 28 | icon: , 29 | }, 30 | { 31 | text: 'admin', 32 | path: 'admin', 33 | icon: , 34 | }, 35 | ]; 36 | 37 | export default links; 38 | -------------------------------------------------------------------------------- /Middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | import { UnauthenticatedError, UnauthorizedError } from "../CustomError/customError.js"; 2 | import { verifyJWT } from "../utils/tokenUtils.js"; 3 | 4 | export const authenticateUser = async (req, res, next) => { 5 | const { token } = req.cookies; 6 | if (!token) { 7 | throw new UnauthenticatedError('authentication invalid'); 8 | } 9 | try { 10 | const { userId, role } = verifyJWT(token); 11 | req.user = { userId, role }; //doubt 12 | next(); 13 | } catch (error) { 14 | throw new UnauthenticatedError('authentication invalid'); 15 | } 16 | }; 17 | 18 | export const authorizePermissions = (...roles) => { 19 | return (req, res, next) => { 20 | if (!roles.includes(req.user.role)) { 21 | throw new UnauthorizedError('Unauthorized to access this route'); 22 | } 23 | next(); 24 | }; 25 | }; 26 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/Testing.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components' 2 | 3 | const Wrapper = styled.main` 4 | nav { 5 | width: var(--fluid-width); 6 | max-width: var(--max-width); 7 | margin: 0 auto; 8 | height: var(--nav-height); 9 | display: flex; 10 | align-items: center; 11 | } 12 | .page { 13 | min-height: calc(100vh - var(--nav-height)); 14 | display: grid; 15 | align-items: center; 16 | margin-top: -3rem; 17 | } 18 | h1 { 19 | font-weight: 700; 20 | span { 21 | color: var(--primary-500); 22 | } 23 | } 24 | p { 25 | color: var(--grey-600); 26 | } 27 | .main-img { 28 | display: none; 29 | } 30 | @media (min-width: 992px) { 31 | .page { 32 | grid-template-columns: 1fr 1fr; 33 | column-gap: 3rem; 34 | } 35 | .main-img { 36 | display: block; 37 | } 38 | } 39 | ` 40 | export default Wrapper 41 | -------------------------------------------------------------------------------- /client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | import Wrapper from '../assets/wrappers/Navbar'; 2 | import { FaAlignLeft } from 'react-icons/fa'; 3 | import Logo from './Logo'; 4 | import { useDashboardContext } from '../pages/DashboardLayout'; 5 | import LogoutContainer from './LogoutContainer'; 6 | import ThemeToggle from './ThemeToggle'; 7 | const Navbar = () => { 8 | const { toggleSidebar } = useDashboardContext(); 9 | return ( 10 | 11 |
12 | 15 |
16 | 17 |

dashboard

18 |
19 |
20 | 21 | 22 |
23 |
24 |
25 | ); 26 | }; 27 | export default Navbar; 28 | -------------------------------------------------------------------------------- /client/src/components/NavLinks.jsx: -------------------------------------------------------------------------------- 1 | import { useDashboardContext } from '../pages/DashboardLayout'; 2 | import links from '../utils/links'; 3 | import { NavLink } from 'react-router-dom'; 4 | 5 | const NavLinks = ({ isBigSidebar }) => { 6 | const { toggleSidebar, user } = useDashboardContext(); 7 | return ( 8 |
9 | {links.map((link) => { 10 | const { text, path, icon } = link; 11 | const { role } = user; 12 | if (path === 'admin' && role !== 'admin') return; 13 | return ( 14 | 21 | {icon} 22 | {text} 23 | 24 | ); 25 | })} 26 |
27 | ); 28 | }; 29 | export default NavLinks; 30 | -------------------------------------------------------------------------------- /client/src/components/SmallSidebar.jsx: -------------------------------------------------------------------------------- 1 | import { FaTimes } from 'react-icons/fa'; 2 | import Wrapper from '../assets/wrappers/SmallSidebar'; 3 | import { useDashboardContext } from '../pages/DashboardLayout'; 4 | import Logo from './Logo'; 5 | 6 | import NavLinks from './NavLinks'; 7 | const SmallSidebar = () => { 8 | const { showSidebar, toggleSidebar } = useDashboardContext(); 9 | 10 | return ( 11 | 12 |
17 |
18 | 21 |
22 | 23 |
24 | 25 |
26 |
27 |
28 | ); 29 | }; 30 | export default SmallSidebar; 31 | -------------------------------------------------------------------------------- /CustomError/customError.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | 3 | export class NotFoundError extends Error { 4 | constructor(message) { 5 | super(message); 6 | this.name = 'NotFoundError'; 7 | this.statusCode = StatusCodes.NOT_FOUND; 8 | } 9 | } 10 | 11 | export class BadRequestError extends Error { 12 | constructor(message) { 13 | super(message); 14 | this.name = 'BadRequestError'; 15 | this.statusCode = StatusCodes.BAD_REQUEST; 16 | } 17 | } 18 | 19 | export class UnauthenticatedError extends Error { 20 | constructor(message) { 21 | super(message); 22 | this.name = 'UnauthenticatedError'; 23 | this.statusCode = StatusCodes.UNAUTHORIZED; 24 | } 25 | } 26 | 27 | export class UnauthorizedError extends Error { 28 | constructor(message) { 29 | super(message); 30 | this.name = 'UnauthorizedError'; 31 | this.statusCode = StatusCodes.FORBIDDEN; 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern", 3 | "version": "1.0.0", 4 | "description": "Job seeker app", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "start": "node index.js" 9 | }, 10 | "author": "Chandan Kanaujiya", 11 | "license": "ISC", 12 | "dependencies": { 13 | "bcryptjs": "^2.4.3", 14 | "body-parser": "^1.20.2", 15 | "cloudinary": "^1.37.3", 16 | "concurrently": "^8.0.1", 17 | "cookie-parser": "^1.4.6", 18 | "datauri": "^4.1.0", 19 | "dayjs": "^1.11.9", 20 | "dotenv": "^16.0.3", 21 | "express": "^4.18.2", 22 | "express-async-errors": "^3.1.1", 23 | "express-mongo-sanitize": "^2.2.0", 24 | "express-rate-limit": "^6.8.0", 25 | "express-validator": "^7.0.1", 26 | "helmet": "^7.0.0", 27 | "http-status-codes": "^2.2.0", 28 | "jsonwebtoken": "^9.0.0", 29 | "mongoose": "^7.6.8", 30 | "morgan": "^1.10.0", 31 | "multer": "^1.4.5-lts.1", 32 | "nanoid": "^4.0.2", 33 | "nodemon": "^2.0.22" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /client/src/components/StatsContainer.jsx: -------------------------------------------------------------------------------- 1 | import { FaSuitcaseRolling, FaCalendarCheck, FaBug } from 'react-icons/fa'; 2 | import Wrapper from '../assets/wrappers/StatsContainer'; 3 | import StatItem from './StatItem'; 4 | const StatsContainer = ({ defaultStats }) => { 5 | const stats = [ 6 | { 7 | title: 'pending applications', 8 | count: defaultStats?.pending || 0, 9 | icon: , 10 | color: '#f59e0b', 11 | bcg: '#fef3c7', 12 | }, 13 | { 14 | title: 'interviews scheduled', 15 | count: defaultStats?.interview || 0, 16 | icon: , 17 | color: '#647acb', 18 | bcg: '#e0e8f9', 19 | }, 20 | { 21 | title: 'jobs declined', 22 | count: defaultStats?.declined || 0, 23 | icon: , 24 | color: '#d66a6a', 25 | bcg: '#ffeeee', 26 | }, 27 | ]; 28 | return ( 29 | 30 | {stats.map((item) => { 31 | return ; 32 | })} 33 | 34 | ); 35 | }; 36 | export default StatsContainer; 37 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/DashboardFormPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | border-radius: var(--border-radius); 5 | width: 100%; 6 | background: var(--background-secondary-color); 7 | padding: 3rem 2rem 4rem; 8 | .form-title { 9 | margin-bottom: 2rem; 10 | } 11 | .form { 12 | margin: 0; 13 | border-radius: 0; 14 | box-shadow: none; 15 | padding: 0; 16 | max-width: 100%; 17 | width: 100%; 18 | } 19 | .form-row { 20 | margin-bottom: 0; 21 | } 22 | .form-center { 23 | display: grid; 24 | row-gap: 1rem; 25 | } 26 | .form-btn { 27 | align-self: end; 28 | margin-top: 1rem; 29 | display: grid; 30 | place-items: center; 31 | } 32 | @media (min-width: 992px) { 33 | .form-center { 34 | grid-template-columns: 1fr 1fr; 35 | align-items: center; 36 | column-gap: 1rem; 37 | } 38 | } 39 | @media (min-width: 1120px) { 40 | .form-center { 41 | grid-template-columns: 1fr 1fr 1fr; 42 | } 43 | } 44 | `; 45 | 46 | export default Wrapper; 47 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/LogoutContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.div` 4 | position: relative; 5 | .logout-btn { 6 | display: flex; 7 | align-items: center; 8 | justify-content: center; 9 | gap: 0 0.5rem; 10 | } 11 | .img { 12 | width: 25px; 13 | height: 25px; 14 | border-radius: 50%; 15 | } 16 | .dropdown { 17 | position: absolute; 18 | top: 45px; 19 | left: 0; 20 | width: 100%; 21 | box-shadow: var(--shadow-2); 22 | text-align: center; 23 | visibility: hidden; 24 | border-radius: var(--border-radius); 25 | background: var(--primary-500); 26 | } 27 | .show-dropdown { 28 | visibility: visible; 29 | } 30 | .dropdown-btn { 31 | border-radius: var(--border-radius); 32 | padding: 0.5rem; 33 | background: transparent; 34 | border-color: transparent; 35 | color: var(--white); 36 | letter-spacing: var(--letter-spacing); 37 | text-transform: capitalize; 38 | cursor: pointer; 39 | width: 100%; 40 | height: 100%; 41 | } 42 | `; 43 | 44 | export default Wrapper; 45 | -------------------------------------------------------------------------------- /client/src/components/LogoutContainer.jsx: -------------------------------------------------------------------------------- 1 | import { FaUserCircle, FaCaretDown } from 'react-icons/fa'; 2 | import Wrapper from '../assets/wrappers/LogoutContainer'; 3 | import { useState } from 'react'; 4 | import { useDashboardContext } from '../pages/DashboardLayout'; 5 | 6 | const LogoutContainer = () => { 7 | const [showLogout, setShowLogout] = useState(false); 8 | const { user, logoutUser } = useDashboardContext(); 9 | 10 | return ( 11 | 12 | 25 |
26 | 29 |
30 |
31 | ); 32 | }; 33 | export default LogoutContainer; 34 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/LandingPage.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | nav { 5 | width: var(--fluid-width); 6 | max-width: var(--max-width); 7 | margin: 0 auto; 8 | height: var(--nav-height); 9 | display: flex; 10 | align-items: center; 11 | } 12 | .page { 13 | min-height: calc(100vh - var(--nav-height)); 14 | display: grid; 15 | align-items: center; 16 | margin-top: -3rem; 17 | } 18 | h1 { 19 | font-weight: 700; 20 | span { 21 | color: var(--primary-500); 22 | } 23 | margin-bottom: 1.5rem; 24 | } 25 | p { 26 | line-height: 2; 27 | color: var(--text-secondary-color); 28 | margin-bottom: 1.5rem; 29 | max-width: 35em; 30 | } 31 | .register-link { 32 | margin-right: 1rem; 33 | } 34 | .main-img { 35 | display: none; 36 | } 37 | .btn { 38 | padding: 0.75rem 1rem; 39 | } 40 | @media (min-width: 992px) { 41 | .page { 42 | grid-template-columns: 1fr 400px; 43 | column-gap: 3rem; 44 | } 45 | .main-img { 46 | display: block; 47 | } 48 | } 49 | `; 50 | export default Wrapper; 51 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/StatItem.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.article` 4 | padding: 2rem; 5 | background: var(--background-secondary-color); 6 | border-bottom: 5px solid ${(props) => props.color}; 7 | border-radius: var(--border-radius); 8 | 9 | header { 10 | display: flex; 11 | align-items: center; 12 | justify-content: space-between; 13 | } 14 | .count { 15 | display: block; 16 | font-weight: 700; 17 | font-size: 50px; 18 | color: ${(props) => props.color}; 19 | line-height: 2; 20 | } 21 | .title { 22 | margin: 0; 23 | text-transform: capitalize; 24 | letter-spacing: var(--letter-spacing); 25 | text-align: left; 26 | margin-top: 0.5rem; 27 | font-size: 1.25rem; 28 | } 29 | .icon { 30 | width: 70px; 31 | height: 60px; 32 | background: ${(props) => props.bcg}; 33 | border-radius: var(--border-radius); 34 | display: flex; 35 | align-items: center; 36 | justify-content: center; 37 | svg { 38 | font-size: 2rem; 39 | color: ${(props) => props.color}; 40 | } 41 | } 42 | `; 43 | 44 | export default Wrapper; 45 | -------------------------------------------------------------------------------- /client/src/pages/Alljobs.jsx: -------------------------------------------------------------------------------- 1 | import { toast } from 'react-toastify'; 2 | import { JobsContainer, SearchContainer } from '../components'; 3 | import customFetch from '../utils/customFetch'; 4 | import { useLoaderData } from 'react-router-dom'; 5 | import { useContext, createContext } from 'react'; 6 | 7 | const AllJobsContext = createContext(); 8 | 9 | export const loader = async ({ request }) => { 10 | try { 11 | const params = Object.fromEntries([ 12 | ...new URL(request.url).searchParams.entries(), 13 | ]); 14 | 15 | const { data } = await customFetch.get('/jobs', { 16 | params, 17 | }); 18 | 19 | return { 20 | data, 21 | searchValues: { ...params }, 22 | }; 23 | } catch (error) { 24 | toast.error(error.response.data.msg); 25 | return error; 26 | } 27 | }; 28 | 29 | const AllJobs = () => { 30 | const { data, searchValues } = useLoaderData(); 31 | 32 | return ( 33 | 34 | 35 | 36 | 37 | ); 38 | }; 39 | export default AllJobs; 40 | 41 | export const useAllJobsContext = () => useContext(AllJobsContext); -------------------------------------------------------------------------------- /client/src/assets/wrappers/Navbar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.nav` 4 | height: var(--nav-height); 5 | display: flex; 6 | align-items: center; 7 | justify-content: center; 8 | box-shadow: 0 1px 0 0 rgba(0, 0, 0, 0.1); 9 | background: var(--background-secondary-color); 10 | .nav-center { 11 | display: flex; 12 | width: 90vw; 13 | align-items: center; 14 | justify-content: space-between; 15 | } 16 | .toggle-btn { 17 | background: transparent; 18 | border-color: transparent; 19 | font-size: 1.75rem; 20 | color: var(--primary-500); 21 | cursor: pointer; 22 | display: flex; 23 | align-items: center; 24 | } 25 | .logo-text { 26 | display: none; 27 | } 28 | .logo { 29 | display: flex; 30 | align-items: center; 31 | width: 100px; 32 | } 33 | .btn-container { 34 | display: flex; 35 | align-items: center; 36 | } 37 | @media (min-width: 992px) { 38 | position: sticky; 39 | top: 0; 40 | .nav-center { 41 | width: 90%; 42 | } 43 | .logo { 44 | display: none; 45 | } 46 | .logo-text { 47 | display: block; 48 | } 49 | } 50 | `; 51 | export default Wrapper; 52 | -------------------------------------------------------------------------------- /client/src/pages/Admin.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FaSuitcaseRolling, FaCalendarCheck } from 'react-icons/fa'; 3 | import { useLoaderData, redirect } from 'react-router-dom'; 4 | import customFetch from '../utils/customFetch'; 5 | import Wrapper from '../assets/wrappers/StatsContainer'; 6 | import { toast } from 'react-toastify'; 7 | import {StatItem} from "../components" 8 | 9 | 10 | export const loader = async () => { 11 | try { 12 | const response = await customFetch.get('/user/admin/app-stats'); 13 | return response.data; 14 | } catch (error) { 15 | toast.error('You are not authorized to view this page'); 16 | return redirect('/dashboard'); 17 | } 18 | }; 19 | 20 | const Admin = () => { 21 | const { users, jobs } = useLoaderData(); 22 | 23 | return ( 24 | 25 | } 31 | /> 32 | } 38 | /> 39 | 40 | ); 41 | }; 42 | export default Admin; -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mern", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@tanstack/react-query": "^4.29.5", 14 | "@tanstack/react-query-devtools": "^4.29.6", 15 | "axios": "^1.3.6", 16 | "dayjs": "^1.11.7", 17 | "react": "^18.2.0", 18 | "react-dom": "^18.2.0", 19 | "react-icons": "^4.8.0", 20 | "react-router-dom": "^6.10.0", 21 | "react-toastify": "^9.1.2", 22 | "recharts": "^2.5.0", 23 | "styled-components": "^5.3.10" 24 | }, 25 | "devDependencies": { 26 | "@types/react": "^18.2.43", 27 | "@types/react-dom": "^18.2.17", 28 | "@vitejs/plugin-react": "^4.2.1", 29 | "eslint": "^8.55.0", 30 | "eslint-plugin-react": "^7.33.2", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-react-refresh": "^0.4.5", 33 | "vite": "^5.0.8" 34 | }, 35 | "description": "This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.", 36 | "main": "vite.config.js", 37 | "author": "", 38 | "license": "ISC" 39 | } 40 | -------------------------------------------------------------------------------- /controllers/authController.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from 'http-status-codes'; 2 | import { comparePassword, hashPassword } from '../utils/PasswordUtils.js'; 3 | import User from '../Models/UserModel.js' 4 | import { UnauthenticatedError } from '../CustomError/customError.js'; 5 | import { createJWT } from '../utils/tokenUtils.js'; 6 | 7 | 8 | export const register = async (req, res) => { 9 | const hashedPassword = await hashPassword(req.body.password); 10 | req.body.password = hashedPassword; 11 | 12 | const user = await User.create(req.body); 13 | res.status(StatusCodes.CREATED).json({ msg: 'user created' }); 14 | }; 15 | 16 | 17 | export const login = async (req, res) => { 18 | const user = await User.findOne({ email: req.body.email }); 19 | if (!user) throw new UnauthenticatedError('invalid credentials'); 20 | 21 | const isValidUser = user && (await comparePassword(req.body.password, user.password)); 22 | if (!isValidUser) throw new UnauthenticatedError('invalid credentials'); 23 | 24 | const token = createJWT({ userId: user._id, role: user.role }); 25 | res.cookie('token', token, {httpOnly: true,}); 26 | res.status(StatusCodes.CREATED).json({ msg: 'user logged in' }); 27 | }; 28 | 29 | export const logout = (req, res) => { 30 | res.cookie('token', 'logout', { 31 | httpOnly: true, 32 | expires: new Date(Date.now()), 33 | }); 34 | res.status(StatusCodes.OK).json({ msg: 'user logged out!' }); 35 | }; -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import 'express-async-errors'; 2 | import express from 'express'; 3 | const app = express(); 4 | import mongoose from "mongoose"; 5 | import jobRouter from './Router/jobRouter.js'; 6 | import errorHandlerMiddleware from './Middleware/ErrorHandler.js'; 7 | import authRouter from './Router/authRouter.js'; 8 | import cookieParser from 'cookie-parser'; 9 | import userRouter from "./Router/userRouter.js" 10 | import {v2 as cloudinary} from 'cloudinary'; 11 | 12 | import {dirname} from "path"; 13 | import { fileURLToPath } from 'url'; 14 | import path from 'path'; 15 | 16 | //midleware 17 | app.use(cookieParser()); 18 | app.use(express.json()); 19 | 20 | // Router 21 | app.use("/api/v1/jobs" , jobRouter); 22 | app.use("/api/v1/auth" ,authRouter); 23 | app.use("/api/v1/user", userRouter) 24 | 25 | app.use(errorHandlerMiddleware); 26 | 27 | 28 | const __dirname = dirname(fileURLToPath(import.meta.url)); 29 | app.use(express.static(path.resolve(__dirname , './public'))); 30 | app.use(express.static(path.resolve(__dirname, './client/dist'))); //optional 31 | 32 | app.get('*', (req, res) => { 33 | res.sendFile(path.resolve(__dirname, './public', 'index.html')); 34 | }); 35 | 36 | 37 | try { 38 | mongoose.connect(); 39 | // mongoose.connect("mongodb://127.0.0.1:27017/JonSeeker"); 40 | app.listen(process.env.PORT || 5100 , () => { 41 | console.log('server running.... 5100'); 42 | }); 43 | } catch (error) { 44 | console.log(error); 45 | process.exit(1); 46 | } 47 | 48 | -------------------------------------------------------------------------------- /controllers/userController.js: -------------------------------------------------------------------------------- 1 | import { StatusCodes } from "http-status-codes"; 2 | import User from "../Models/UserModel.js" 3 | import Job from "../Models/JobModel.js"; 4 | import cloudinary from 'cloudinary'; 5 | import { promises as fs } from 'fs'; 6 | 7 | export const getCurrentUser = async (req, res) => { 8 | const user = await User.findOne({ _id: req.user.userId }); 9 | const userWithoutPassword = user.toJSON(); 10 | res.status(StatusCodes.OK).json({ user: userWithoutPassword }); 11 | }; 12 | 13 | export const getApplicationStats = async (req, res) => { 14 | const users = await User.countDocuments(); 15 | console.log(users); 16 | const jobs = await Job.countDocuments(); 17 | res.status(StatusCodes.OK).json({ users, jobs }); 18 | }; 19 | 20 | export const updateUser = async (req, res) => { 21 | const newUser = { ...req.body }; 22 | delete newUser.password; 23 | if (req.file) { 24 | const response = await cloudinary.v2.uploader.upload(req.file.path); 25 | await fs.unlink(req.file.path); 26 | newUser.avatar = response.secure_url; 27 | newUser.avatarPublicId = response.public_id; 28 | } 29 | 30 | const updatedUser = await User.findByIdAndUpdate(req.user.userId, newUser); 31 | if (req.file && updatedUser.avatarPublicId) 32 | await cloudinary.v2.uploader.destroy(updatedUser.avatarPublicId); 33 | res.status(StatusCodes.OK).json({ msg: 'update user' }); 34 | }; 35 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/PageBtnContainer.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.section` 4 | height: 6rem; 5 | margin-top: 2rem; 6 | display: flex; 7 | align-items: center; 8 | justify-content: end; 9 | flex-wrap: wrap; 10 | gap: 1rem; 11 | .btn-container { 12 | background: var(--background-secondary-color); 13 | border-radius: var(--border-radius); 14 | display: flex; 15 | } 16 | .page-btn { 17 | background: transparent; 18 | border-color: transparent; 19 | width: 50px; 20 | height: 40px; 21 | font-weight: 700; 22 | font-size: 1.25rem; 23 | color: var(--primary-500); 24 | border-radius: var(--border-radius); 25 | cursor:pointer: 26 | } 27 | .active{ 28 | background:var(--primary-500); 29 | color: var(--white); 30 | 31 | } 32 | .prev-btn,.next-btn{ 33 | background: var(--background-secondary-color); 34 | border-color: transparent; 35 | border-radius: var(--border-radius); 36 | 37 | width: 100px; 38 | height: 40px; 39 | color: var(--primary-500); 40 | text-transform:capitalize; 41 | letter-spacing:var(--letter-spacing); 42 | display:flex; 43 | align-items:center; 44 | justify-content:center; 45 | gap:0.5rem; 46 | cursor:pointer; 47 | } 48 | .prev-btn:hover,.next-btn:hover{ 49 | background:var(--primary-500); 50 | color: var(--white); 51 | transition:var(--transition); 52 | } 53 | .dots{ 54 | display:grid; 55 | place-items:center; 56 | cursor:text; 57 | } 58 | `; 59 | export default Wrapper; 60 | -------------------------------------------------------------------------------- /client/src/components/Job.jsx: -------------------------------------------------------------------------------- 1 | import { FaLocationArrow, FaBriefcase, FaCalendarAlt } from 'react-icons/fa'; 2 | import { Link, Form } from 'react-router-dom'; 3 | import Wrapper from '../assets/wrappers/Job'; 4 | import JobInfo from './JobInfo'; 5 | import day from 'dayjs'; 6 | import advancedFormat from 'dayjs/plugin/advancedFormat'; 7 | day.extend(advancedFormat); 8 | 9 | const Job = ({ 10 | _id, 11 | position, 12 | company, 13 | jobLocation, 14 | jobType, 15 | createdAt, 16 | jobStatus, 17 | }) => { 18 | const date = day(createdAt).format('MMM Do, YYYY'); 19 | return ( 20 | 21 |
22 |
{company.charAt(0)}
23 |
24 |
{position}
25 |

{company}

26 |
27 |
28 |
29 |
30 | } text={jobLocation} /> 31 | } text={date} /> 32 | } text={jobType} /> 33 |
{jobStatus}
34 |
35 |
36 | 37 | Edit 38 | 39 |
40 | 43 |
44 |
45 |
46 |
47 | ); 48 | }; 49 | export default Job; 50 | -------------------------------------------------------------------------------- /client/src/assets/wrappers/BigSidebar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.aside` 4 | display: none; 5 | @media (min-width: 992px) { 6 | display: block; 7 | box-shadow: 1px 0px 0px 0px rgba(0, 0, 0, 0.1); 8 | .sidebar-container { 9 | background: var(--background-secondary-color); 10 | min-height: 100vh; 11 | height: 100%; 12 | width: 250px; 13 | margin-left: -250px; 14 | transition: margin-left 0.3s ease-in-out; 15 | } 16 | .content { 17 | position: sticky; 18 | top: 0; 19 | } 20 | .show-sidebar { 21 | margin-left: 0; 22 | } 23 | header { 24 | height: 6rem; 25 | display: flex; 26 | align-items: center; 27 | padding-left: 2.5rem; 28 | } 29 | .nav-links { 30 | padding-top: 2rem; 31 | display: flex; 32 | flex-direction: column; 33 | } 34 | .nav-link { 35 | display: flex; 36 | align-items: center; 37 | color: var(--text-secondary-color); 38 | padding: 1rem 0; 39 | padding-left: 2.5rem; 40 | text-transform: capitalize; 41 | transition: padding-left 0.3s ease-in-out; 42 | } 43 | .nav-link:hover { 44 | padding-left: 3rem; 45 | color: var(--primary-500); 46 | transition: var(--transition); 47 | } 48 | .icon { 49 | font-size: 1.5rem; 50 | margin-right: 1rem; 51 | display: grid; 52 | place-items: center; 53 | } 54 | .active { 55 | color: var(--primary-500); 56 | } 57 | .pending { 58 | background: var(--background-color); 59 | } 60 | } 61 | `; 62 | export default Wrapper; 63 | -------------------------------------------------------------------------------- /client/src/pages/Register.jsx: -------------------------------------------------------------------------------- 1 | import { Logo, FormRow } from '../components'; 2 | import Wrapper from '../assets/wrappers/RegisterAndLoginPage'; 3 | import { Form ,Link, redirect , useNavigation } from 'react-router-dom'; 4 | import customFetch from "../utils/customFetch" 5 | import { toast } from 'react-toastify'; 6 | 7 | export const action = async({request})=>{ 8 | const formData = await request.formData(); 9 | const data = Object.fromEntries(formData); 10 | 11 | try { 12 | await customFetch.post('/auth/register' , data); 13 | toast.success("Registration successful") 14 | return redirect('/login'); 15 | } catch (error) { 16 | toast.error(error.response.data.msg); 17 | return error; 18 | } 19 | } 20 | 21 | 22 | const Register = () => { 23 | const navigation = useNavigation() ; 24 | const isSubmitting = navigation.state === 'submitting'; 25 | return ( 26 | 27 |
28 | 29 |

Register

30 | 31 | 32 | 33 | 34 | 35 | 42 |

Already a member?Login

43 | 44 |
45 | ); 46 | }; 47 | export default Register; -------------------------------------------------------------------------------- /client/src/assets/wrappers/SmallSidebar.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.aside` 4 | @media (min-width: 992px) { 5 | display: none; 6 | } 7 | .sidebar-container { 8 | position: fixed; 9 | inset: 0; 10 | background: rgba(0, 0, 0, 0.7); 11 | display: flex; 12 | justify-content: center; 13 | align-items: center; 14 | z-index: -1; 15 | opacity: 0; 16 | transition: var(--transition); 17 | visibility: hidden; 18 | } 19 | .show-sidebar { 20 | z-index: 99; 21 | opacity: 1; 22 | visibility: visible; 23 | } 24 | .content { 25 | background: var(--background-secondary-color); 26 | width: var(--fluid-width); 27 | height: 95vh; 28 | border-radius: var(--border-radius); 29 | padding: 4rem 2rem; 30 | position: relative; 31 | display: flex; 32 | align-items: center; 33 | flex-direction: column; 34 | } 35 | .close-btn { 36 | position: absolute; 37 | top: 10px; 38 | left: 10px; 39 | background: transparent; 40 | border-color: transparent; 41 | font-size: 2rem; 42 | color: var(--red-dark); 43 | cursor: pointer; 44 | } 45 | .nav-links { 46 | padding-top: 2rem; 47 | display: flex; 48 | flex-direction: column; 49 | } 50 | .nav-link { 51 | display: flex; 52 | align-items: center; 53 | color: var(--text-secondary-color); 54 | padding: 1rem 0; 55 | text-transform: capitalize; 56 | transition: var(--transition); 57 | } 58 | .nav-link:hover { 59 | color: var(--primary-500); 60 | } 61 | .icon { 62 | font-size: 1.5rem; 63 | margin-right: 1rem; 64 | display: grid; 65 | place-items: center; 66 | } 67 | .active { 68 | color: var(--primary-500); 69 | } 70 | `; 71 | export default Wrapper; 72 | -------------------------------------------------------------------------------- /client/src/pages/Login.jsx: -------------------------------------------------------------------------------- 1 | import { Logo, FormRow } from '../components'; 2 | import Wrapper from '../assets/wrappers/RegisterAndLoginPage'; 3 | import { Form, Link, redirect , useNavigation } from 'react-router-dom'; 4 | import { toast } from 'react-toastify'; 5 | import customFetch from '../utils/customFetch'; 6 | 7 | export const action = async ({ request }) => { 8 | const formData = await request.formData(); 9 | const data = Object.fromEntries(formData); 10 | 11 | try { 12 | const res = await customFetch.post('/auth/login', data); 13 | toast.success(res.data.msg); 14 | console.log("chandan") 15 | return redirect("/dashboard"); 16 | } catch (error) { 17 | toast.error(error.response.data.msg); 18 | return error; 19 | } 20 | 21 | }; 22 | 23 | const Login = () => { 24 | const navigation = useNavigation() ; 25 | const isSubmitting = navigation.state === 'submitting'; 26 | return ( 27 | 28 |
29 | 30 |

Login

31 | 32 | 33 | 40 | 43 | 44 |

45 | Not a member yet? 46 | 47 | Register 48 | 49 |

50 | 51 |
52 | ); 53 | }; 54 | export default Login; -------------------------------------------------------------------------------- /client/src/assets/wrappers/Job.js: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components'; 2 | 3 | const Wrapper = styled.article` 4 | background: var(--background-secondary-color); 5 | border-radius: var(--border-radius); 6 | display: grid; 7 | grid-template-rows: 1fr auto; 8 | box-shadow: var(--shadow-2); 9 | header { 10 | padding: 1rem 1.5rem; 11 | border-bottom: 1px solid var(--grey-100); 12 | display: grid; 13 | grid-template-columns: auto 1fr; 14 | align-items: center; 15 | } 16 | .main-icon { 17 | width: 60px; 18 | height: 60px; 19 | display: grid; 20 | place-items: center; 21 | background: var(--primary-500); 22 | border-radius: var(--border-radius); 23 | font-size: 1.5rem; 24 | font-weight: 700; 25 | text-transform: uppercase; 26 | color: var(--white); 27 | margin-right: 2rem; 28 | } 29 | .info { 30 | h5 { 31 | margin-bottom: 0.5rem; 32 | } 33 | p { 34 | margin: 0; 35 | text-transform: capitalize; 36 | letter-spacing: var(--letter-spacing); 37 | color: var(--text-secondary-color); 38 | } 39 | } 40 | .content { 41 | padding: 1rem 1.5rem; 42 | } 43 | .content-center { 44 | display: grid; 45 | margin-top: 1rem; 46 | margin-bottom: 1.5rem; 47 | grid-template-columns: 1fr; 48 | row-gap: 1.5rem; 49 | align-items: center; 50 | @media (min-width: 576px) { 51 | grid-template-columns: 1fr 1fr; 52 | } 53 | } 54 | .status { 55 | border-radius: var(--border-radius); 56 | text-transform: capitalize; 57 | letter-spacing: var(--letter-spacing); 58 | text-align: center; 59 | width: 100px; 60 | height: 30px; 61 | display: grid; 62 | align-items: center; 63 | } 64 | .actions { 65 | margin-top: 1rem; 66 | display: flex; 67 | align-items: center; 68 | } 69 | .edit-btn, 70 | .delete-btn { 71 | height: 30px; 72 | font-size: 0.85rem; 73 | display: flex; 74 | align-items: center; 75 | } 76 | .edit-btn { 77 | margin-right: 0.5rem; 78 | } 79 | `; 80 | 81 | export default Wrapper; 82 | -------------------------------------------------------------------------------- /client/src/pages/DashboardLayout.jsx: -------------------------------------------------------------------------------- 1 | import { useNavigate, Outlet, redirect, useLoaderData } from 'react-router-dom'; 2 | import Wrapper from '../assets/wrappers/Dashboard'; 3 | import { Navbar, BigSidebar, SmallSidebar } from '../components'; 4 | import { useState, createContext, useContext } from 'react'; 5 | import customFetch from '../utils/customFetch'; 6 | import { toast } from 'react-toastify'; 7 | 8 | const DashboardContext = createContext(); 9 | 10 | export const loader = async()=>{ 11 | try { 12 | const {data} = await customFetch.get('/user/current-user'); 13 | return data; 14 | } catch (error) { 15 | console.log(error); 16 | return redirect("/"); 17 | } 18 | }; 19 | 20 | 21 | 22 | const Dashboard = () => { 23 | const navigate = useNavigate(); 24 | const data = useLoaderData(); 25 | 26 | const {user} = data; 27 | const [showSidebar, setShowSidebar] = useState(false); 28 | const [isDarkTheme, setIsDarkTheme] = useState(false); 29 | 30 | const toggleDarkTheme = () => { 31 | const newDarkTheme = !isDarkTheme; 32 | setIsDarkTheme(newDarkTheme); 33 | document.body.classList.toggle('dark-theme', newDarkTheme); 34 | localStorage.setItem('darkTheme', newDarkTheme); 35 | }; 36 | 37 | const toggleSidebar = () => { 38 | setShowSidebar(!showSidebar); 39 | }; 40 | 41 | const logoutUser = async () => { 42 | navigate("/"); 43 | await customFetch("/auth/logout"); 44 | toast.success("Loggin out..."); 45 | }; 46 | 47 | return ( 48 | 58 | 59 |
60 | 61 | 62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 |
70 |
71 | ); 72 | }; 73 | 74 | export const useDashboardContext = () => useContext(DashboardContext); 75 | export default Dashboard; -------------------------------------------------------------------------------- /client/src/pages/Landing.jsx: -------------------------------------------------------------------------------- 1 | import main from '../assets/images/main.svg'; 2 | import { Link } from 'react-router-dom'; 3 | import logo from '../assets/images/logo.svg'; 4 | import styled from 'styled-components'; 5 | const Landing = () => { 6 | return ( 7 | 8 | 11 |
12 | {/* info */} 13 |
14 |

15 | job tracking app 16 |

17 |

18 | I'm baby wayfarers hoodie next level taiyaki brooklyn cliche blue 19 | bottle single-origin coffee chia. Aesthetic post-ironic venmo, 20 | quinoa lo-fi tote bag adaptogen everyday carry meggings +1 brunch 21 | narwhal. 22 |

23 | 24 | Register 25 | 26 | 27 | Login / Demo User 28 | 29 |
30 | job hunt 31 |
32 |
33 | ); 34 | }; 35 | 36 | const StyledWrapper = styled.section` 37 | nav { 38 | width: var(--fluid-width); 39 | max-width: var(--max-width); 40 | margin: 0 auto; 41 | height: var(--nav-height); 42 | display: flex; 43 | align-items: center; 44 | } 45 | .page { 46 | min-height: calc(100vh - var(--nav-height)); 47 | display: grid; 48 | align-items: center; 49 | margin-top: -3rem; 50 | } 51 | h1 { 52 | font-weight: 700; 53 | span { 54 | color: var(--primary-500); 55 | } 56 | margin-bottom: 1.5rem; 57 | } 58 | p { 59 | line-height: 2; 60 | color: var(--text-secondary-color); 61 | margin-bottom: 1.5rem; 62 | max-width: 35em; 63 | } 64 | .register-link { 65 | margin-right: 1rem; 66 | } 67 | .main-img { 68 | display: none; 69 | } 70 | .btn { 71 | padding: 0.75rem 1rem; 72 | } 73 | @media (min-width: 992px) { 74 | .page { 75 | grid-template-columns: 1fr 400px; 76 | column-gap: 3rem; 77 | } 78 | .main-img { 79 | display: block; 80 | } 81 | } 82 | `; 83 | 84 | export default Landing; -------------------------------------------------------------------------------- /client/src/pages/AddJob.jsx: -------------------------------------------------------------------------------- 1 | import { FormRow } from '../components'; 2 | import Wrapper from '../assets/wrappers/DashboardFormPage'; 3 | import { useOutletContext } from 'react-router-dom'; 4 | import { JOB_STATUS, JOB_TYPE } from '../../../utils/constants'; 5 | import { Form, useNavigation, redirect } from 'react-router-dom'; 6 | import { toast } from 'react-toastify'; 7 | import customFetch from '../utils/customFetch'; 8 | import {FormRowSelect} from '../components/index.js'; 9 | 10 | export const action = async ({ request }) => { 11 | const formData = await request.formData(); 12 | const data = Object.fromEntries(formData); 13 | 14 | try { 15 | await customFetch.post(`/jobs`, data); 16 | toast.success('Job added successfully'); 17 | return redirect("/dashboard/all-jobs"); 18 | } catch (error) { 19 | toast.error(error?.response?.data?.msg); 20 | return error; 21 | } 22 | }; 23 | 24 | 25 | const AddJob = () => { 26 | const { user } = useOutletContext(); 27 | const navigation = useNavigation(); 28 | const isSubmitting = navigation.state === 'submitting'; 29 | 30 | return ( 31 | 32 |
33 |

add job

34 |
35 | 36 | 37 | 43 | 49 | 55 | 62 |
63 |
64 |
65 | ); 66 | }; 67 | 68 | export default AddJob; -------------------------------------------------------------------------------- /client/src/components/SearchContainer.jsx: -------------------------------------------------------------------------------- 1 | import { FormRow, FormRowSelect } from '.'; 2 | import Wrapper from '../assets/wrappers/DashboardFormPage'; 3 | import { Form, useSubmit, Link } from 'react-router-dom'; 4 | import { JOB_TYPE, JOB_STATUS, JOB_SORT_BY } from '../../../utils/constants'; 5 | import { useAllJobsContext } from '../pages/AllJobs'; 6 | const SearchContainer = () => { 7 | const { searchValues } = useAllJobsContext(); 8 | const { search, jobStatus, jobType, sort } = searchValues; 9 | 10 | const submit = useSubmit(); 11 | const debounce = (onChange) => { 12 | let timeout; 13 | return (e) => { 14 | const form = e.currentTarget.form; 15 | clearTimeout(timeout); 16 | timeout = setTimeout(() => { 17 | onChange(form); 18 | }, 2000); 19 | }; 20 | }; 21 | 22 | return ( 23 | 24 |
25 |
search form
26 |
27 | {/* search position */} 28 | 29 | { 34 | submit(form); 35 | })} 36 | /> 37 | { 43 | submit(e.currentTarget.form); 44 | }} 45 | /> 46 | { 52 | submit(e.currentTarget.form); 53 | }} 54 | /> 55 | { 60 | submit(e.currentTarget.form); 61 | }} 62 | /> 63 | 64 | Reset Search Values 65 | 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default SearchContainer; -------------------------------------------------------------------------------- /client/src/pages/Profile.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { FormRow } from '../components'; 3 | import Wrapper from '../assets/wrappers/DashboardFormPage'; 4 | import { useOutletContext } from 'react-router-dom'; 5 | import { useNavigation, Form } from 'react-router-dom'; 6 | import customFetch from '../utils/customFetch'; 7 | import { toast } from 'react-toastify'; 8 | 9 | export const action = async ({ request }) => { 10 | const formData = await request.formData(); 11 | 12 | const file = formData.get('avatar'); 13 | if (file && file.size > 500000) { 14 | toast.error('Image size too large'); 15 | return null; 16 | } 17 | 18 | try { 19 | await customFetch.patch('/user/update-user', formData); 20 | toast.success('Profile updated successfully'); 21 | } catch (error) { 22 | toast.error(error?.response?.data?.msg); 23 | } 24 | return null; 25 | }; 26 | 27 | const Profile = () => { 28 | const { user } = useOutletContext(); 29 | const { name, lastName, email, location } = user; 30 | const navigation = useNavigation(); 31 | const isSubmitting = navigation.state === 'submitting'; 32 | return ( 33 | 34 |
35 |

profile

36 | 37 |
38 |
39 | 42 | 49 |
50 | 51 | 57 | 58 | 59 | 66 |
67 |
68 |
69 | ); 70 | }; 71 | 72 | export default Profile; -------------------------------------------------------------------------------- /client/src/pages/EditJob.jsx: -------------------------------------------------------------------------------- 1 | import { FormRow, FormRowSelect } from '../components'; 2 | import Wrapper from '../assets/wrappers/DashboardFormPage'; 3 | import { useLoaderData, useParams } from 'react-router-dom'; 4 | import { JOB_STATUS, JOB_TYPE } from '../../../utils/constants'; 5 | import { Form, useNavigation, redirect } from 'react-router-dom'; 6 | import { toast } from 'react-toastify'; 7 | import customFetch from '../utils/customFetch'; 8 | 9 | 10 | export const loader = async ({ params }) => { 11 | try { 12 | const { data } = await customFetch.get(`/jobs/${params.id}`); 13 | return data; 14 | } catch (error) { 15 | toast.error(error.response.data.msg); 16 | return redirect('/dashboard/all-jobs'); 17 | } 18 | }; 19 | 20 | export const action = async ({ request, params }) => { 21 | const formData = await request.formData(); 22 | const data = Object.fromEntries(formData); 23 | 24 | try { 25 | await customFetch.patch(`/jobs/${params.id}`, data); 26 | toast.success('Job edited successfully'); 27 | return redirect('/dashboard/all-jobs'); 28 | } catch (error) { 29 | toast.error(error.response.data.msg); 30 | return error; 31 | } 32 | }; 33 | 34 | const EditJob = () => { 35 | 36 | const params = useParams(); 37 | console.log(params); 38 | 39 | const { job } = useLoaderData(); 40 | 41 | const navigation = useNavigation(); 42 | const isSubmitting = navigation.state === 'submitting'; 43 | 44 | return ( 45 | 46 |
47 |

edit job

48 |
49 | 50 | 51 | 57 | 58 | 64 | 70 | 77 |
78 |
79 |
80 | ); 81 | }; 82 | 83 | export default EditJob; -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | import { createBrowserRouter , RouterProvider } from "react-router-dom"; 2 | import { 3 | HomeLayout, 4 | Landing, 5 | Register, 6 | Login, 7 | DashboardLayout, 8 | Error, 9 | Stats, 10 | AddJob, 11 | Admin, 12 | Profile, 13 | AllJobs, 14 | EditJob 15 | } from './pages'; 16 | import { action as RegisterAction } from "./pages/Register"; 17 | import { action as LoginAction} from "./pages/Login"; 18 | import { loader as DashboardLoader } from "./pages/DashboardLayout"; 19 | import { action as Addjob } from "./pages/AddJob"; 20 | import {loader as allJobsloader} from "./pages/AllJobs"; 21 | 22 | import { loader as editJobLoader } from './pages/EditJob'; 23 | import { action as editJobAction } from './pages/EditJob'; 24 | import { action as deleteJobAction } from './pages/DeleteJob'; 25 | 26 | import { loader as adminLoader } from './pages/Admin'; 27 | import { action as profileAction } from './pages/Profile'; 28 | import { loader as statsLoader } from './pages/Stats'; 29 | 30 | 31 | const router = createBrowserRouter([ 32 | { 33 | path: '/', 34 | element: , 35 | errorElement: , 36 | children: [ 37 | { 38 | index: true, 39 | element: , 40 | }, 41 | { 42 | path: 'register', 43 | element: , 44 | action:RegisterAction 45 | }, 46 | 47 | { 48 | path: 'dashboard', 49 | element: , 50 | loader:DashboardLoader, 51 | children: [ 52 | { 53 | index: true, 54 | element: , 55 | action:Addjob 56 | }, 57 | { path: 'stats', 58 | element: , 59 | loader: statsLoader 60 | }, 61 | { 62 | path: 'all-jobs', 63 | element: , 64 | loader:allJobsloader, 65 | }, 66 | 67 | { 68 | path: 'profile', 69 | element: , 70 | action:profileAction 71 | }, 72 | { 73 | path: 'admin', 74 | element: , 75 | loader: adminLoader, 76 | }, 77 | { 78 | path: 'edit-job/:id', 79 | element: , 80 | loader: editJobLoader, 81 | action: editJobAction, 82 | }, 83 | { 84 | path: 'delete-job/:id', 85 | action: deleteJobAction 86 | }, 87 | ], 88 | }, 89 | { 90 | path: 'login', 91 | element: , 92 | action:LoginAction 93 | }, 94 | ], 95 | }, 96 | ]); 97 | const App = () => { 98 | const checkDefaultTheme = () => { 99 | const isDarkTheme = 100 | localStorage.getItem('darkTheme') === 'true' 101 | document.body.classList.toggle('dark-theme', isDarkTheme); 102 | return isDarkTheme; 103 | }; 104 | 105 | const isDarkThemeEnabled = checkDefaultTheme(); 106 | return <> 107 | 108 | 109 | 110 | } 111 | 112 | export default App -------------------------------------------------------------------------------- /client/src/assets/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /client/src/components/PageBtnContainer.jsx: -------------------------------------------------------------------------------- 1 | import { HiChevronDoubleLeft, HiChevronDoubleRight } from 'react-icons/hi'; 2 | import Wrapper from '../assets/wrappers/PageBtnContainer'; 3 | import { useLocation, Link, useNavigate } from 'react-router-dom'; 4 | import { useAllJobsContext } from '../pages/AllJobs'; 5 | 6 | const PageBtnContainer = () => { 7 | const { 8 | data: { numOfPages, currentPage }, 9 | } = useAllJobsContext(); 10 | const pages = Array.from({ length: numOfPages }, (_, index) => { 11 | return index + 1; 12 | }); 13 | 14 | const { search, pathname } = useLocation(); 15 | const navigate = useNavigate(); 16 | 17 | const handlePageChange = (pageNumber) => { 18 | const searchParams = new URLSearchParams(search); 19 | searchParams.set('page', pageNumber); 20 | navigate(`${pathname}?${searchParams.toString()}`); 21 | }; 22 | 23 | const addPageButton = ({ pageNumber, activeClass }) => { 24 | return ( 25 | 32 | ); 33 | }; 34 | 35 | const renderPageButtons = () => { 36 | const pageButtons = []; 37 | // first page 38 | pageButtons.push( 39 | addPageButton({ pageNumber: 1, activeClass: currentPage === 1 }) 40 | ); 41 | // dots 42 | 43 | if (currentPage > 3) { 44 | pageButtons.push( 45 | 46 | ... 47 | 48 | ); 49 | } 50 | // one before current page 51 | if (currentPage !== 1 && currentPage !== 2) { 52 | pageButtons.push( 53 | addPageButton({ 54 | pageNumber: currentPage - 1, 55 | activeClass: false, 56 | }) 57 | ); 58 | } 59 | // current page 60 | if (currentPage !== 1 && currentPage !== numOfPages) { 61 | pageButtons.push( 62 | addPageButton({ 63 | pageNumber: currentPage, 64 | activeClass: true, 65 | }) 66 | ); 67 | } 68 | // one after current page 69 | 70 | if (currentPage !== numOfPages && currentPage !== numOfPages - 1) { 71 | pageButtons.push( 72 | addPageButton({ 73 | pageNumber: currentPage + 1, 74 | activeClass: false, 75 | }) 76 | ); 77 | } 78 | if (currentPage < numOfPages - 2) { 79 | pageButtons.push( 80 | 81 | ... 82 | 83 | ); 84 | } 85 | pageButtons.push( 86 | addPageButton({ 87 | pageNumber: numOfPages, 88 | activeClass: currentPage === numOfPages, 89 | }) 90 | ); 91 | return pageButtons; 92 | }; 93 | 94 | return ( 95 | 96 | 107 |
{renderPageButtons()}
108 | 119 |
120 | ); 121 | }; 122 | export default PageBtnContainer; 123 | -------------------------------------------------------------------------------- /client/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /controllers/jobcontroller.js: -------------------------------------------------------------------------------- 1 | import Job from "../Models/JobModel.js" 2 | import StatusCodes from "http-status-codes"; 3 | import { NotFoundError } from "../CustomError/customError.js"; 4 | import mongoose from "mongoose"; 5 | import dayjs from "dayjs"; 6 | 7 | 8 | export const createJob = async (req, res) => { 9 | req.body.createdBy = req.user.userId; 10 | const job = await Job.create(req.body); 11 | res.status(StatusCodes.CREATED).json({ job }); 12 | }; 13 | 14 | export const getAllJobs = async (req, res) => { 15 | const { search, jobStatus, jobType, sort } = req.query; 16 | 17 | const queryObject = { 18 | createdBy: req.user.userId, 19 | }; 20 | 21 | if (search) { 22 | queryObject.$or = [ 23 | { position: { $regex: search, $options: 'i' } }, 24 | { company: { $regex: search, $options: 'i' } }, 25 | ]; 26 | } 27 | if (jobStatus && jobStatus !== 'all') { 28 | queryObject.jobStatus = jobStatus; 29 | } 30 | if (jobType && jobType !== 'all') { 31 | queryObject.jobType = jobType; 32 | } 33 | 34 | const sortOptions = { 35 | newest: '-createdAt', 36 | oldest: 'createdAt', 37 | 'a-z': 'position', 38 | 'z-a': '-position', 39 | }; 40 | 41 | const sortKey = sortOptions[sort] || sortOptions.newest; 42 | 43 | // setup pagination 44 | const page = Number(req.query.page) || 1; 45 | const limit = Number(req.query.limit) || 10; 46 | const skip = (page - 1) * limit; 47 | 48 | const jobs = await Job.find(queryObject) 49 | .sort(sortKey) 50 | .skip(skip) 51 | .limit(limit); 52 | 53 | const totalJobs = await Job.countDocuments(queryObject); 54 | const numOfPages = Math.ceil(totalJobs / limit); 55 | 56 | res 57 | .status(StatusCodes.OK) 58 | .json({ totalJobs, numOfPages, currentPage: page, jobs }); 59 | }; 60 | 61 | export const getJob = async (req, res) => { 62 | const { id } = req.params; 63 | console.log(id); 64 | const job = await Job.findById(id); 65 | if (!job) throw new NotFoundError(`no job with id : ${id}`); 66 | res.status(StatusCodes.OK).json({ job }); 67 | }; 68 | 69 | 70 | export const deletejob = async(req ,res)=>{ 71 | const { id } = req.params; 72 | const removedJob = await Job.findByIdAndDelete(id); 73 | 74 | if(!id) throw new NotFoundError(`no job with id : ${id}`); 75 | res.status(StatusCodes.OK).json({removedJob}); 76 | } 77 | 78 | export const updateJob = async (req, res) => { 79 | const { id } = req.params; 80 | const updatedJob = await Job.findByIdAndUpdate(id, req.body, { 81 | new: true, // show in response is updated values; 82 | }); 83 | 84 | if (!updatedJob) throw new NotFoundError(`no job with id : ${id}`); 85 | res.status(StatusCodes.OK).json({ job: updatedJob }); 86 | }; 87 | 88 | 89 | export const showStats = async(req , res)=>{ 90 | let stats = await Job.aggregate([ 91 | {$match:{createdBy:new mongoose.Types.ObjectId(req.user.userId)}}, 92 | {$group:{_id:'$jobStatus' , count:{$sum:1}}} 93 | ]) 94 | 95 | stats = stats.reduce((acc , curr)=>{ 96 | const {_id:title ,count} = curr; 97 | acc[title] = count; 98 | return acc ; 99 | } ,{}); 100 | 101 | const defaultStats = { 102 | pending: stats.pending || 0, 103 | interview: stats.interview || 0, 104 | declined:stats.declined || 0 105 | } 106 | 107 | 108 | let monthlyApplications = await Job.aggregate([ 109 | {$match:{createdBy: new mongoose.Types.ObjectId(req.user.userId)}}, 110 | {$group:{ 111 | _id:{year:{$year:'$createdAt'} , month:{$month:'$createdAt'}}, 112 | count:{$sum:1} 113 | }}, 114 | {$sort:{'_id.year':-1 ,'_id.month':-1}}, 115 | {$limit: 6}, 116 | ]); 117 | 118 | monthlyApplications = monthlyApplications 119 | .map((item) => { 120 | const { 121 | _id: { year, month }, 122 | count, 123 | } = item; 124 | 125 | const date = dayjs() 126 | .month(month - 1) 127 | .year(year) 128 | .format('MMM YY'); 129 | return { date, count }; 130 | }) 131 | .reverse(); 132 | 133 | res.status(StatusCodes.OK).json({ defaultStats, monthlyApplications }); 134 | }; 135 | -------------------------------------------------------------------------------- /Middleware/ValidationMiddleware.js: -------------------------------------------------------------------------------- 1 | import { body, validationResult ,param } from 'express-validator'; 2 | import { BadRequestError, UnauthorizedError } from '../CustomError/customError.js'; 3 | import { JOB_STATUS, JOB_TYPE } from '../utils/constants.js'; 4 | import mongoose from 'mongoose'; 5 | import Job from '../Models/JobModel.js'; 6 | import User from "../Models/UserModel.js" 7 | 8 | 9 | const withValidationErrors = (validateValues) => { 10 | return [ 11 | validateValues, 12 | (req, res, next) => { 13 | const errors = validationResult(req); 14 | if (!errors.isEmpty()) { 15 | const errorMessages = errors.array().map((error) => error.msg); 16 | if (errorMessages[0].startsWith('no job')) { 17 | throw new NotFoundError(errorMessages); 18 | } 19 | if (errorMessages[0].startsWith('not authorized')) 20 | throw new UnauthorizedError('not authorized to access this route'); 21 | 22 | throw new BadRequestError(errorMessages); 23 | } 24 | next(); 25 | }, 26 | ]; 27 | }; 28 | 29 | export const validateTest = withValidationErrors([ 30 | body('name') 31 | .notEmpty() 32 | .withMessage('name is required') 33 | .isLength({ min: 3, max: 50 }) 34 | .withMessage('name must be between 3 and 50 characters long') 35 | .trim(), 36 | ]); 37 | 38 | 39 | export const validateJobInput = withValidationErrors([ 40 | body('company').notEmpty().withMessage('company is required'), 41 | body('position').notEmpty().withMessage('position is required'), 42 | body('jobLocation').notEmpty().withMessage('job location is required'), 43 | body('jobStatus') 44 | .isIn(Object.values(JOB_STATUS)) 45 | .withMessage('invalid status value'), 46 | body('jobType').isIn(Object.values(JOB_TYPE)).withMessage('invalid job type'), 47 | ]); 48 | 49 | 50 | export const validateIdParam = withValidationErrors([ 51 | param('id').custom(async (value, { req }) => { 52 | const isValidMongoId = mongoose.Types.ObjectId.isValid(value); 53 | if (!isValidMongoId) throw new BadRequestError('invalid MongoDB id'); 54 | const job = await Job.findById(value); 55 | if (!job) throw new NotFoundError(`no job with id ${value}`); 56 | const isAdmin = req.user.role === 'admin'; 57 | const isOwner = req.user.userId === job.createdBy.toString(); 58 | if (!isAdmin && !isOwner) 59 | throw UnauthorizedError('not authorized to access this route'); 60 | }), 61 | ]); 62 | 63 | 64 | export const validateRegisterInput = withValidationErrors([ 65 | body('name').notEmpty().withMessage('name is required'), 66 | body('email') 67 | .notEmpty() 68 | .withMessage('email is required') 69 | .isEmail() 70 | .withMessage('invalid email format') 71 | .custom(async (email) => { 72 | const user = await User.findOne({ email }); 73 | if (user) { 74 | throw new BadRequestError('email already exists'); 75 | } 76 | }), 77 | body('password') 78 | .notEmpty() 79 | .withMessage('password is required') 80 | .isLength({ min: 8 }) 81 | .withMessage('password must be at least 8 characters long'), 82 | body('location').notEmpty().withMessage('location is required'), 83 | body('lastName').notEmpty().withMessage('last name is required'), 84 | ]); 85 | 86 | 87 | export const validateLoginInput = withValidationErrors([ 88 | body('email') 89 | .notEmpty() 90 | .withMessage('email is required') 91 | .isEmail() 92 | .withMessage('invalid email format'), 93 | body('password').notEmpty().withMessage('password is required'), 94 | ]); 95 | 96 | export const validateUpdateUserInput = withValidationErrors([ 97 | body('name').notEmpty().withMessage('name is required'), 98 | body('email') 99 | .notEmpty() 100 | .withMessage('email is required') 101 | .isEmail() 102 | .withMessage('invalid email format') 103 | .custom(async (email, { req }) => { 104 | const user = await User.findOne({ email }); 105 | if (user && user._id.toString() !== req.user.userId) { 106 | throw new Error('email already exists'); 107 | } 108 | }), 109 | body('lastName').notEmpty().withMessage('last name is required'), 110 | body('location').notEmpty().withMessage('location is required'), 111 | ]); -------------------------------------------------------------------------------- /client/src/index.css: -------------------------------------------------------------------------------- 1 | /* ============= GLOBAL CSS =============== */ 2 | 3 | *, 4 | ::after, 5 | ::before { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | font-size: 100%; 13 | } /*16px*/ 14 | 15 | :root { 16 | /* colors */ 17 | --primary-50: #e0fcff; 18 | --primary-100: #bef8fd; 19 | --primary-200: #87eaf2; 20 | --primary-300: #54d1db; 21 | --primary-400: #38bec9; 22 | --primary-500: #2cb1bc; 23 | --primary-600: #14919b; 24 | --primary-700: #0e7c86; 25 | --primary-800: #0a6c74; 26 | --primary-900: #044e54; 27 | 28 | /* grey */ 29 | --grey-50: #f8fafc; 30 | --grey-100: #f1f5f9; 31 | --grey-200: #e2e8f0; 32 | --grey-300: #cbd5e1; 33 | --grey-400: #94a3b8; 34 | --grey-500: #64748b; 35 | --grey-600: #475569; 36 | --grey-700: #334155; 37 | --grey-800: #1e293b; 38 | --grey-900: #0f172a; 39 | /* rest of the colors */ 40 | --black: #222; 41 | --white: #fff; 42 | --red-light: #f8d7da; 43 | --red-dark: #842029; 44 | --green-light: #d1e7dd; 45 | --green-dark: #0f5132; 46 | 47 | --small-text: 0.875rem; 48 | --extra-small-text: 0.7em; 49 | /* rest of the vars */ 50 | 51 | --border-radius: 0.25rem; 52 | --letter-spacing: 1px; 53 | --transition: 0.3s ease-in-out all; 54 | --max-width: 1120px; 55 | --fixed-width: 600px; 56 | --fluid-width: 90vw; 57 | --nav-height: 6rem; 58 | /* box shadow*/ 59 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 60 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 61 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 62 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 63 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 64 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 65 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 66 | /* DARK MODE */ 67 | 68 | --dark-mode-bg-color: #333; 69 | --dark-mode-text-color: #f0f0f0; 70 | --dark-mode-bg-secondary-color: #3f3f3f; 71 | --dark-mode-text-secondary-color: var(--grey-300); 72 | 73 | --background-color: var(--grey-50); 74 | --text-color: var(--grey-900); 75 | --background-secondary-color: var(--white); 76 | --text-secondary-color: var(--grey-500); 77 | } 78 | 79 | .dark-theme { 80 | --text-color: var(--dark-mode-text-color); 81 | --background-color: var(--dark-mode-bg-color); 82 | --text-secondary-color: var(--dark-mode-text-secondary-color); 83 | --background-secondary-color: var(--dark-mode-bg-secondary-color); 84 | } 85 | 86 | body { 87 | background: var(--background-color); 88 | color: var(--text-color); 89 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 90 | Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 91 | font-weight: 400; 92 | line-height: 1; 93 | } 94 | p { 95 | margin: 0; 96 | } 97 | h1, 98 | h2, 99 | h3, 100 | h4, 101 | h5 { 102 | margin: 0; 103 | font-weight: 400; 104 | line-height: 1; 105 | text-transform: capitalize; 106 | letter-spacing: var(--letter-spacing); 107 | } 108 | 109 | h1 { 110 | font-size: clamp(2rem, 5vw, 5rem); /* Large heading */ 111 | } 112 | 113 | h2 { 114 | font-size: clamp(1.5rem, 3vw, 3rem); /* Medium heading */ 115 | } 116 | 117 | h3 { 118 | font-size: clamp(1.25rem, 2.5vw, 2.5rem); /* Small heading */ 119 | } 120 | 121 | h4 { 122 | font-size: clamp(1rem, 2vw, 2rem); /* Extra small heading */ 123 | } 124 | 125 | h5 { 126 | font-size: clamp(0.875rem, 1.5vw, 1.5rem); /* Tiny heading */ 127 | } 128 | 129 | /* BIGGER FONTS */ 130 | /* h1 { 131 | font-size: clamp(3rem, 6vw, 6rem); 132 | } 133 | 134 | h2 { 135 | font-size: clamp(2.5rem, 5vw, 5rem); 136 | } 137 | 138 | h3 { 139 | font-size: clamp(2rem, 4vw, 4rem); 140 | } 141 | 142 | h4 { 143 | font-size: clamp(1.5rem, 3vw, 3rem); 144 | } 145 | 146 | h5 { 147 | font-size: clamp(1rem, 2vw, 2rem); 148 | } 149 | */ 150 | 151 | .text { 152 | margin-bottom: 1.5rem; 153 | max-width: 40em; 154 | } 155 | 156 | small, 157 | .text-small { 158 | font-size: var(--small-text); 159 | } 160 | 161 | a { 162 | text-decoration: none; 163 | } 164 | ul { 165 | list-style-type: none; 166 | padding: 0; 167 | } 168 | 169 | .img { 170 | width: 100%; 171 | display: block; 172 | object-fit: cover; 173 | } 174 | /* buttons */ 175 | 176 | .btn { 177 | cursor: pointer; 178 | color: var(--white); 179 | background: var(--primary-500); 180 | border: transparent; 181 | border-radius: var(--border-radius); 182 | letter-spacing: var(--letter-spacing); 183 | padding: 0.375rem 0.75rem; 184 | box-shadow: var(--shadow-1); 185 | transition: var(--transition); 186 | text-transform: capitalize; 187 | display: inline-block; 188 | } 189 | .btn:hover { 190 | background: var(--primary-700); 191 | box-shadow: var(--shadow-3); 192 | } 193 | .btn-hipster { 194 | color: var(--primary-500); 195 | background: var(--primary-200); 196 | } 197 | .btn-hipster:hover { 198 | color: var(--primary-200); 199 | background: var(--primary-700); 200 | } 201 | .btn-block { 202 | width: 100%; 203 | } 204 | button:disabled { 205 | cursor: wait; 206 | } 207 | .danger-btn { 208 | color: var(--red-dark); 209 | background: var(--red-light); 210 | } 211 | .danger-btn:hover { 212 | color: var(--white); 213 | background: var(--red-dark); 214 | } 215 | /* alerts */ 216 | .alert { 217 | padding: 0.375rem 0.75rem; 218 | margin-bottom: 1rem; 219 | border-color: transparent; 220 | border-radius: var(--border-radius); 221 | } 222 | 223 | .alert-danger { 224 | color: var(--red-dark); 225 | background: var(--red-light); 226 | } 227 | .alert-success { 228 | color: var(--green-dark); 229 | background: var(--green-light); 230 | } 231 | /* form */ 232 | 233 | .form { 234 | width: 90vw; 235 | max-width: var(--fixed-width); 236 | background: var(--background-secondary-color); 237 | border-radius: var(--border-radius); 238 | box-shadow: var(--shadow-2); 239 | padding: 2rem 2.5rem; 240 | margin: 3rem auto; 241 | } 242 | .form-label { 243 | display: block; 244 | font-size: var(--small-text); 245 | margin-bottom: 0.75rem; 246 | text-transform: capitalize; 247 | letter-spacing: var(--letter-spacing); 248 | line-height: 1.5; 249 | } 250 | .form-input, 251 | .form-textarea, 252 | .form-select { 253 | width: 100%; 254 | padding: 0.375rem 0.75rem; 255 | border-radius: var(--border-radius); 256 | background: var(--background-color); 257 | border: 1px solid var(--grey-300); 258 | color: var(--text-color); 259 | } 260 | .form-input, 261 | .form-select, 262 | .form-btn { 263 | height: 35px; 264 | } 265 | .form-row { 266 | margin-bottom: 1rem; 267 | } 268 | 269 | .form-textarea { 270 | height: 7rem; 271 | } 272 | ::placeholder { 273 | font-family: inherit; 274 | color: var(--grey-400); 275 | } 276 | .form-alert { 277 | color: var(--red-dark); 278 | letter-spacing: var(--letter-spacing); 279 | text-transform: capitalize; 280 | } 281 | /* alert */ 282 | 283 | @keyframes spinner { 284 | to { 285 | transform: rotate(360deg); 286 | } 287 | } 288 | 289 | .loading { 290 | width: 6rem; 291 | height: 6rem; 292 | border: 5px solid var(--grey-400); 293 | border-radius: 50%; 294 | border-top-color: var(--primary-500); 295 | animation: spinner 0.6s linear infinite; 296 | } 297 | 298 | /* title */ 299 | 300 | .title { 301 | text-align: center; 302 | } 303 | 304 | .title-underline { 305 | background: var(--primary-500); 306 | width: 7rem; 307 | height: 0.25rem; 308 | margin: 0 auto; 309 | margin-top: 1rem; 310 | } 311 | 312 | .container { 313 | width: var(--fluid-width); 314 | max-width: var(--max-width); 315 | margin: 0 auto; 316 | } 317 | 318 | /* BUTTONS AND BADGES */ 319 | .pending { 320 | background: #fef3c7; 321 | color: #f59e0b; 322 | } 323 | 324 | .interview { 325 | background: #e0e8f9; 326 | color: #647acb; 327 | } 328 | .declined { 329 | background: #ffeeee; 330 | color: #d66a6a; 331 | } 332 | -------------------------------------------------------------------------------- /client/src/assets/css/index.css: -------------------------------------------------------------------------------- 1 | /* ============= GLOBAL CSS =============== */ 2 | 3 | *, 4 | ::after, 5 | ::before { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | } 10 | 11 | html { 12 | font-size: 100%; 13 | } /*16px*/ 14 | 15 | :root { 16 | /* colors */ 17 | --primary-50: #e0fcff; 18 | --primary-100: #bef8fd; 19 | --primary-200: #87eaf2; 20 | --primary-300: #54d1db; 21 | --primary-400: #38bec9; 22 | --primary-500: #2cb1bc; 23 | --primary-600: #14919b; 24 | --primary-700: #0e7c86; 25 | --primary-800: #0a6c74; 26 | --primary-900: #044e54; 27 | 28 | /* grey */ 29 | --grey-50: #f8fafc; 30 | --grey-100: #f1f5f9; 31 | --grey-200: #e2e8f0; 32 | --grey-300: #cbd5e1; 33 | --grey-400: #94a3b8; 34 | --grey-500: #64748b; 35 | --grey-600: #475569; 36 | --grey-700: #334155; 37 | --grey-800: #1e293b; 38 | --grey-900: #0f172a; 39 | /* rest of the colors */ 40 | --black: #222; 41 | --white: #fff; 42 | --red-light: #f8d7da; 43 | --red-dark: #842029; 44 | --green-light: #d1e7dd; 45 | --green-dark: #0f5132; 46 | 47 | --small-text: 0.875rem; 48 | --extra-small-text: 0.7em; 49 | /* rest of the vars */ 50 | 51 | --border-radius: 0.25rem; 52 | --letter-spacing: 1px; 53 | --transition: 0.3s ease-in-out all; 54 | --max-width: 1120px; 55 | --fixed-width: 600px; 56 | --fluid-width: 90vw; 57 | --nav-height: 6rem; 58 | /* box shadow*/ 59 | --shadow-1: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); 60 | --shadow-2: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 61 | 0 2px 4px -1px rgba(0, 0, 0, 0.06); 62 | --shadow-3: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 63 | 0 4px 6px -2px rgba(0, 0, 0, 0.05); 64 | --shadow-4: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 65 | 0 10px 10px -5px rgba(0, 0, 0, 0.04); 66 | /* DARK MODE */ 67 | 68 | --dark-mode-bg-color: #333; 69 | --dark-mode-text-color: #f0f0f0; 70 | --dark-mode-bg-secondary-color: #3f3f3f; 71 | --dark-mode-text-secondary-color: var(--grey-300); 72 | 73 | --background-color: var(--grey-50); 74 | --text-color: var(--grey-900); 75 | --background-secondary-color: var(--white); 76 | --text-secondary-color: var(--grey-500); 77 | } 78 | 79 | .dark-theme { 80 | --text-color: var(--dark-mode-text-color); 81 | --background-color: var(--dark-mode-bg-color); 82 | --text-secondary-color: var(--dark-mode-text-secondary-color); 83 | --background-secondary-color: var(--dark-mode-bg-secondary-color); 84 | } 85 | 86 | body { 87 | background: var(--background-color); 88 | color: var(--text-color); 89 | font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 90 | Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; 91 | font-weight: 400; 92 | line-height: 1; 93 | } 94 | p { 95 | margin: 0; 96 | } 97 | h1, 98 | h2, 99 | h3, 100 | h4, 101 | h5 { 102 | margin: 0; 103 | font-weight: 400; 104 | line-height: 1; 105 | text-transform: capitalize; 106 | letter-spacing: var(--letter-spacing); 107 | } 108 | 109 | h1 { 110 | font-size: clamp(2rem, 5vw, 5rem); /* Large heading */ 111 | } 112 | 113 | h2 { 114 | font-size: clamp(1.5rem, 3vw, 3rem); /* Medium heading */ 115 | } 116 | 117 | h3 { 118 | font-size: clamp(1.25rem, 2.5vw, 2.5rem); /* Small heading */ 119 | } 120 | 121 | h4 { 122 | font-size: clamp(1rem, 2vw, 2rem); /* Extra small heading */ 123 | } 124 | 125 | h5 { 126 | font-size: clamp(0.875rem, 1.5vw, 1.5rem); /* Tiny heading */ 127 | } 128 | 129 | /* BIGGER FONTS */ 130 | /* h1 { 131 | font-size: clamp(3rem, 6vw, 6rem); 132 | } 133 | 134 | h2 { 135 | font-size: clamp(2.5rem, 5vw, 5rem); 136 | } 137 | 138 | h3 { 139 | font-size: clamp(2rem, 4vw, 4rem); 140 | } 141 | 142 | h4 { 143 | font-size: clamp(1.5rem, 3vw, 3rem); 144 | } 145 | 146 | h5 { 147 | font-size: clamp(1rem, 2vw, 2rem); 148 | } 149 | */ 150 | 151 | .text { 152 | margin-bottom: 1.5rem; 153 | max-width: 40em; 154 | } 155 | 156 | small, 157 | .text-small { 158 | font-size: var(--small-text); 159 | } 160 | 161 | a { 162 | text-decoration: none; 163 | } 164 | ul { 165 | list-style-type: none; 166 | padding: 0; 167 | } 168 | 169 | .img { 170 | width: 100%; 171 | display: block; 172 | object-fit: cover; 173 | } 174 | /* buttons */ 175 | 176 | .btn { 177 | cursor: pointer; 178 | color: var(--white); 179 | background: var(--primary-500); 180 | border: transparent; 181 | border-radius: var(--border-radius); 182 | letter-spacing: var(--letter-spacing); 183 | padding: 0.375rem 0.75rem; 184 | box-shadow: var(--shadow-1); 185 | transition: var(--transition); 186 | text-transform: capitalize; 187 | display: inline-block; 188 | } 189 | .btn:hover { 190 | background: var(--primary-700); 191 | box-shadow: var(--shadow-3); 192 | } 193 | .btn-hipster { 194 | color: var(--primary-500); 195 | background: var(--primary-200); 196 | } 197 | .btn-hipster:hover { 198 | color: var(--primary-200); 199 | background: var(--primary-700); 200 | } 201 | .btn-block { 202 | width: 100%; 203 | } 204 | button:disabled { 205 | cursor: wait; 206 | } 207 | .danger-btn { 208 | color: var(--red-dark); 209 | background: var(--red-light); 210 | } 211 | .danger-btn:hover { 212 | color: var(--white); 213 | background: var(--red-dark); 214 | } 215 | /* alerts */ 216 | .alert { 217 | padding: 0.375rem 0.75rem; 218 | margin-bottom: 1rem; 219 | border-color: transparent; 220 | border-radius: var(--border-radius); 221 | } 222 | 223 | .alert-danger { 224 | color: var(--red-dark); 225 | background: var(--red-light); 226 | } 227 | .alert-success { 228 | color: var(--green-dark); 229 | background: var(--green-light); 230 | } 231 | /* form */ 232 | 233 | .form { 234 | width: 90vw; 235 | max-width: var(--fixed-width); 236 | background: var(--background-secondary-color); 237 | border-radius: var(--border-radius); 238 | box-shadow: var(--shadow-2); 239 | padding: 2rem 2.5rem; 240 | margin: 3rem auto; 241 | } 242 | .form-label { 243 | display: block; 244 | font-size: var(--small-text); 245 | margin-bottom: 0.75rem; 246 | text-transform: capitalize; 247 | letter-spacing: var(--letter-spacing); 248 | line-height: 1.5; 249 | } 250 | .form-input, 251 | .form-textarea, 252 | .form-select { 253 | width: 100%; 254 | padding: 0.375rem 0.75rem; 255 | border-radius: var(--border-radius); 256 | background: var(--background-color); 257 | border: 1px solid var(--grey-300); 258 | color: var(--text-color); 259 | } 260 | .form-input, 261 | .form-select, 262 | .form-btn { 263 | height: 35px; 264 | } 265 | .form-row { 266 | margin-bottom: 1rem; 267 | } 268 | 269 | .form-textarea { 270 | height: 7rem; 271 | } 272 | ::placeholder { 273 | font-family: inherit; 274 | color: var(--grey-400); 275 | } 276 | .form-alert { 277 | color: var(--red-dark); 278 | letter-spacing: var(--letter-spacing); 279 | text-transform: capitalize; 280 | } 281 | /* alert */ 282 | 283 | @keyframes spinner { 284 | to { 285 | transform: rotate(360deg); 286 | } 287 | } 288 | 289 | .loading { 290 | width: 6rem; 291 | height: 6rem; 292 | border: 5px solid var(--grey-400); 293 | border-radius: 50%; 294 | border-top-color: var(--primary-500); 295 | animation: spinner 0.6s linear infinite; 296 | } 297 | 298 | /* title */ 299 | 300 | .title { 301 | text-align: center; 302 | } 303 | 304 | .title-underline { 305 | background: var(--primary-500); 306 | width: 7rem; 307 | height: 0.25rem; 308 | margin: 0 auto; 309 | margin-top: 1rem; 310 | } 311 | 312 | .container { 313 | width: var(--fluid-width); 314 | max-width: var(--max-width); 315 | margin: 0 auto; 316 | } 317 | 318 | /* BUTTONS AND BADGES */ 319 | .pending { 320 | background: #fef3c7; 321 | color: #f59e0b; 322 | } 323 | 324 | .interview { 325 | background: #e0e8f9; 326 | color: #647acb; 327 | } 328 | .declined { 329 | background: #ffeeee; 330 | color: #d66a6a; 331 | } 332 | -------------------------------------------------------------------------------- /client/src/assets/images/main-alternative.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/src/assets/images/main.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/main-ewHsHjHH.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/assets/index-2SqZJ_mH.css: -------------------------------------------------------------------------------- 1 | *,:after,:before{margin:0;padding:0;box-sizing:border-box}html{font-size:100%}:root{--primary-50: #e0fcff;--primary-100: #bef8fd;--primary-200: #87eaf2;--primary-300: #54d1db;--primary-400: #38bec9;--primary-500: #2cb1bc;--primary-600: #14919b;--primary-700: #0e7c86;--primary-800: #0a6c74;--primary-900: #044e54;--grey-50: #f8fafc;--grey-100: #f1f5f9;--grey-200: #e2e8f0;--grey-300: #cbd5e1;--grey-400: #94a3b8;--grey-500: #64748b;--grey-600: #475569;--grey-700: #334155;--grey-800: #1e293b;--grey-900: #0f172a;--black: #222;--white: #fff;--red-light: #f8d7da;--red-dark: #842029;--green-light: #d1e7dd;--green-dark: #0f5132;--small-text: .875rem;--extra-small-text: .7em;--border-radius: .25rem;--letter-spacing: 1px;--transition: .3s ease-in-out all;--max-width: 1120px;--fixed-width: 600px;--fluid-width: 90vw;--nav-height: 6rem;--shadow-1: 0 1px 3px 0 rgba(0, 0, 0, .1), 0 1px 2px 0 rgba(0, 0, 0, .06);--shadow-2: 0 4px 6px -1px rgba(0, 0, 0, .1), 0 2px 4px -1px rgba(0, 0, 0, .06);--shadow-3: 0 10px 15px -3px rgba(0, 0, 0, .1), 0 4px 6px -2px rgba(0, 0, 0, .05);--shadow-4: 0 20px 25px -5px rgba(0, 0, 0, .1), 0 10px 10px -5px rgba(0, 0, 0, .04);--dark-mode-bg-color: #333;--dark-mode-text-color: #f0f0f0;--dark-mode-bg-secondary-color: #3f3f3f;--dark-mode-text-secondary-color: var(--grey-300);--background-color: var(--grey-50);--text-color: var(--grey-900);--background-secondary-color: var(--white);--text-secondary-color: var(--grey-500)}.dark-theme{--text-color: var(--dark-mode-text-color);--background-color: var(--dark-mode-bg-color);--text-secondary-color: var(--dark-mode-text-secondary-color);--background-secondary-color: var(--dark-mode-bg-secondary-color)}body{background:var(--background-color);color:var(--text-color);font-family:system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Open Sans,Helvetica Neue,sans-serif;font-weight:400;line-height:1}p{margin:0}h1,h2,h3,h4,h5{margin:0;font-weight:400;line-height:1;text-transform:capitalize;letter-spacing:var(--letter-spacing)}h1{font-size:clamp(2rem,5vw,5rem)}h2{font-size:clamp(1.5rem,3vw,3rem)}h3{font-size:clamp(1.25rem,2.5vw,2.5rem)}h4{font-size:clamp(1rem,2vw,2rem)}h5{font-size:clamp(.875rem,1.5vw,1.5rem)}.text{margin-bottom:1.5rem;max-width:40em}small,.text-small{font-size:var(--small-text)}a{text-decoration:none}ul{list-style-type:none;padding:0}.img{width:100%;display:block;object-fit:cover}.btn{cursor:pointer;color:var(--white);background:var(--primary-500);border:transparent;border-radius:var(--border-radius);letter-spacing:var(--letter-spacing);padding:.375rem .75rem;box-shadow:var(--shadow-1);transition:var(--transition);text-transform:capitalize;display:inline-block}.btn:hover{background:var(--primary-700);box-shadow:var(--shadow-3)}.btn-hipster{color:var(--primary-500);background:var(--primary-200)}.btn-hipster:hover{color:var(--primary-200);background:var(--primary-700)}.btn-block{width:100%}button:disabled{cursor:wait}.danger-btn{color:var(--red-dark);background:var(--red-light)}.danger-btn:hover{color:var(--white);background:var(--red-dark)}.alert{padding:.375rem .75rem;margin-bottom:1rem;border-color:transparent;border-radius:var(--border-radius)}.alert-danger{color:var(--red-dark);background:var(--red-light)}.alert-success{color:var(--green-dark);background:var(--green-light)}.form{width:90vw;max-width:var(--fixed-width);background:var(--background-secondary-color);border-radius:var(--border-radius);box-shadow:var(--shadow-2);padding:2rem 2.5rem;margin:3rem auto}.form-label{display:block;font-size:var(--small-text);margin-bottom:.75rem;text-transform:capitalize;letter-spacing:var(--letter-spacing);line-height:1.5}.form-input,.form-textarea,.form-select{width:100%;padding:.375rem .75rem;border-radius:var(--border-radius);background:var(--background-color);border:1px solid var(--grey-300);color:var(--text-color)}.form-input,.form-select,.form-btn{height:35px}.form-row{margin-bottom:1rem}.form-textarea{height:7rem}::placeholder{font-family:inherit;color:var(--grey-400)}.form-alert{color:var(--red-dark);letter-spacing:var(--letter-spacing);text-transform:capitalize}@keyframes spinner{to{transform:rotate(360deg)}}.loading{width:6rem;height:6rem;border:5px solid var(--grey-400);border-radius:50%;border-top-color:var(--primary-500);animation:spinner .6s linear infinite}.title{text-align:center}.title-underline{background:var(--primary-500);width:7rem;height:.25rem;margin:0 auto;margin-top:1rem}.container{width:var(--fluid-width);max-width:var(--max-width);margin:0 auto}.pending{background:#fef3c7;color:#f59e0b}.interview{background:#e0e8f9;color:#647acb}.declined{background:#fee;color:#d66a6a}:root{--toastify-color-light: #fff;--toastify-color-dark: #121212;--toastify-color-info: #3498db;--toastify-color-success: #07bc0c;--toastify-color-warning: #f1c40f;--toastify-color-error: #e74c3c;--toastify-color-transparent: rgba(255, 255, 255, .7);--toastify-icon-color-info: var(--toastify-color-info);--toastify-icon-color-success: var(--toastify-color-success);--toastify-icon-color-warning: var(--toastify-color-warning);--toastify-icon-color-error: var(--toastify-color-error);--toastify-toast-width: 320px;--toastify-toast-background: #fff;--toastify-toast-min-height: 64px;--toastify-toast-max-height: 800px;--toastify-font-family: sans-serif;--toastify-z-index: 9999;--toastify-text-color-light: #757575;--toastify-text-color-dark: #fff;--toastify-text-color-info: #fff;--toastify-text-color-success: #fff;--toastify-text-color-warning: #fff;--toastify-text-color-error: #fff;--toastify-spinner-color: #616161;--toastify-spinner-color-empty-area: #e0e0e0;--toastify-color-progress-light: linear-gradient( to right, #4cd964, #5ac8fa, #007aff, #34aadc, #5856d6, #ff2d55 );--toastify-color-progress-dark: #bb86fc;--toastify-color-progress-info: var(--toastify-color-info);--toastify-color-progress-success: var(--toastify-color-success);--toastify-color-progress-warning: var(--toastify-color-warning);--toastify-color-progress-error: var(--toastify-color-error)}.Toastify__toast-container{z-index:var(--toastify-z-index);-webkit-transform:translate3d(0,0,var(--toastify-z-index));position:fixed;padding:4px;width:var(--toastify-toast-width);box-sizing:border-box;color:#fff}.Toastify__toast-container--top-left{top:1em;left:1em}.Toastify__toast-container--top-center{top:1em;left:50%;transform:translate(-50%)}.Toastify__toast-container--top-right{top:1em;right:1em}.Toastify__toast-container--bottom-left{bottom:1em;left:1em}.Toastify__toast-container--bottom-center{bottom:1em;left:50%;transform:translate(-50%)}.Toastify__toast-container--bottom-right{bottom:1em;right:1em}@media only screen and (max-width : 480px){.Toastify__toast-container{width:100vw;padding:0;left:0;margin:0}.Toastify__toast-container--top-left,.Toastify__toast-container--top-center,.Toastify__toast-container--top-right{top:0;transform:translate(0)}.Toastify__toast-container--bottom-left,.Toastify__toast-container--bottom-center,.Toastify__toast-container--bottom-right{bottom:0;transform:translate(0)}.Toastify__toast-container--rtl{right:0;left:initial}}.Toastify__toast{position:relative;min-height:var(--toastify-toast-min-height);box-sizing:border-box;margin-bottom:1rem;padding:8px;border-radius:4px;box-shadow:0 1px 10px #0000001a,0 2px 15px #0000000d;display:-ms-flexbox;display:flex;-ms-flex-pack:justify;justify-content:space-between;max-height:var(--toastify-toast-max-height);overflow:hidden;font-family:var(--toastify-font-family);cursor:default;direction:ltr;z-index:0}.Toastify__toast--rtl{direction:rtl}.Toastify__toast--close-on-click{cursor:pointer}.Toastify__toast-body{margin:auto 0;-ms-flex:1 1 auto;flex:1 1 auto;padding:6px;display:-ms-flexbox;display:flex;-ms-flex-align:center;align-items:center}.Toastify__toast-body>div:last-child{word-break:break-word;-ms-flex:1;flex:1}.Toastify__toast-icon{-webkit-margin-end:10px;margin-inline-end:10px;width:20px;-ms-flex-negative:0;flex-shrink:0;display:-ms-flexbox;display:flex}.Toastify--animate{animation-fill-mode:both;animation-duration:.7s}.Toastify--animate-icon{animation-fill-mode:both;animation-duration:.3s}@media only screen and (max-width : 480px){.Toastify__toast{margin-bottom:0;border-radius:0}}.Toastify__toast-theme--dark{background:var(--toastify-color-dark);color:var(--toastify-text-color-dark)}.Toastify__toast-theme--light,.Toastify__toast-theme--colored.Toastify__toast--default{background:var(--toastify-color-light);color:var(--toastify-text-color-light)}.Toastify__toast-theme--colored.Toastify__toast--info{color:var(--toastify-text-color-info);background:var(--toastify-color-info)}.Toastify__toast-theme--colored.Toastify__toast--success{color:var(--toastify-text-color-success);background:var(--toastify-color-success)}.Toastify__toast-theme--colored.Toastify__toast--warning{color:var(--toastify-text-color-warning);background:var(--toastify-color-warning)}.Toastify__toast-theme--colored.Toastify__toast--error{color:var(--toastify-text-color-error);background:var(--toastify-color-error)}.Toastify__progress-bar-theme--light{background:var(--toastify-color-progress-light)}.Toastify__progress-bar-theme--dark{background:var(--toastify-color-progress-dark)}.Toastify__progress-bar--info{background:var(--toastify-color-progress-info)}.Toastify__progress-bar--success{background:var(--toastify-color-progress-success)}.Toastify__progress-bar--warning{background:var(--toastify-color-progress-warning)}.Toastify__progress-bar--error{background:var(--toastify-color-progress-error)}.Toastify__progress-bar-theme--colored.Toastify__progress-bar--info,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--success,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--warning,.Toastify__progress-bar-theme--colored.Toastify__progress-bar--error{background:var(--toastify-color-transparent)}.Toastify__close-button{color:#fff;background:transparent;outline:none;border:none;padding:0;cursor:pointer;opacity:.7;transition:.3s ease;-ms-flex-item-align:start;align-self:flex-start}.Toastify__close-button--light{color:#000;opacity:.3}.Toastify__close-button>svg{fill:currentColor;height:16px;width:14px}.Toastify__close-button:hover,.Toastify__close-button:focus{opacity:1}@keyframes Toastify__trackProgress{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.Toastify__progress-bar{position:absolute;bottom:0;left:0;width:100%;height:5px;z-index:var(--toastify-z-index);opacity:.7;transform-origin:left}.Toastify__progress-bar--animated{animation:Toastify__trackProgress linear 1 forwards}.Toastify__progress-bar--controlled{transition:transform .2s}.Toastify__progress-bar--rtl{right:0;left:initial;transform-origin:right}.Toastify__spinner{width:20px;height:20px;box-sizing:border-box;border:2px solid;border-radius:100%;border-color:var(--toastify-spinner-color-empty-area);border-right-color:var(--toastify-spinner-color);animation:Toastify__spin .65s linear infinite}@keyframes Toastify__bounceInRight{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(3000px,0,0)}60%{opacity:1;transform:translate3d(-25px,0,0)}75%{transform:translate3d(10px,0,0)}90%{transform:translate3d(-5px,0,0)}to{transform:none}}@keyframes Toastify__bounceOutRight{20%{opacity:1;transform:translate3d(-20px,0,0)}to{opacity:0;transform:translate3d(2000px,0,0)}}@keyframes Toastify__bounceInLeft{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(-3000px,0,0)}60%{opacity:1;transform:translate3d(25px,0,0)}75%{transform:translate3d(-10px,0,0)}90%{transform:translate3d(5px,0,0)}to{transform:none}}@keyframes Toastify__bounceOutLeft{20%{opacity:1;transform:translate3d(20px,0,0)}to{opacity:0;transform:translate3d(-2000px,0,0)}}@keyframes Toastify__bounceInUp{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(0,3000px,0)}60%{opacity:1;transform:translate3d(0,-20px,0)}75%{transform:translate3d(0,10px,0)}90%{transform:translate3d(0,-5px,0)}to{transform:translateZ(0)}}@keyframes Toastify__bounceOutUp{20%{transform:translate3d(0,-10px,0)}40%,45%{opacity:1;transform:translate3d(0,20px,0)}to{opacity:0;transform:translate3d(0,-2000px,0)}}@keyframes Toastify__bounceInDown{0%,60%,75%,90%,to{animation-timing-function:cubic-bezier(.215,.61,.355,1)}0%{opacity:0;transform:translate3d(0,-3000px,0)}60%{opacity:1;transform:translate3d(0,25px,0)}75%{transform:translate3d(0,-10px,0)}90%{transform:translate3d(0,5px,0)}to{transform:none}}@keyframes Toastify__bounceOutDown{20%{transform:translate3d(0,10px,0)}40%,45%{opacity:1;transform:translate3d(0,-20px,0)}to{opacity:0;transform:translate3d(0,2000px,0)}}.Toastify__bounce-enter--top-left,.Toastify__bounce-enter--bottom-left{animation-name:Toastify__bounceInLeft}.Toastify__bounce-enter--top-right,.Toastify__bounce-enter--bottom-right{animation-name:Toastify__bounceInRight}.Toastify__bounce-enter--top-center{animation-name:Toastify__bounceInDown}.Toastify__bounce-enter--bottom-center{animation-name:Toastify__bounceInUp}.Toastify__bounce-exit--top-left,.Toastify__bounce-exit--bottom-left{animation-name:Toastify__bounceOutLeft}.Toastify__bounce-exit--top-right,.Toastify__bounce-exit--bottom-right{animation-name:Toastify__bounceOutRight}.Toastify__bounce-exit--top-center{animation-name:Toastify__bounceOutUp}.Toastify__bounce-exit--bottom-center{animation-name:Toastify__bounceOutDown}@keyframes Toastify__zoomIn{0%{opacity:0;transform:scale3d(.3,.3,.3)}50%{opacity:1}}@keyframes Toastify__zoomOut{0%{opacity:1}50%{opacity:0;transform:scale3d(.3,.3,.3)}to{opacity:0}}.Toastify__zoom-enter{animation-name:Toastify__zoomIn}.Toastify__zoom-exit{animation-name:Toastify__zoomOut}@keyframes Toastify__flipIn{0%{transform:perspective(400px) rotateX(90deg);animation-timing-function:ease-in;opacity:0}40%{transform:perspective(400px) rotateX(-20deg);animation-timing-function:ease-in}60%{transform:perspective(400px) rotateX(10deg);opacity:1}80%{transform:perspective(400px) rotateX(-5deg)}to{transform:perspective(400px)}}@keyframes Toastify__flipOut{0%{transform:perspective(400px)}30%{transform:perspective(400px) rotateX(-20deg);opacity:1}to{transform:perspective(400px) rotateX(90deg);opacity:0}}.Toastify__flip-enter{animation-name:Toastify__flipIn}.Toastify__flip-exit{animation-name:Toastify__flipOut}@keyframes Toastify__slideInRight{0%{transform:translate3d(110%,0,0);visibility:visible}to{transform:translateZ(0)}}@keyframes Toastify__slideInLeft{0%{transform:translate3d(-110%,0,0);visibility:visible}to{transform:translateZ(0)}}@keyframes Toastify__slideInUp{0%{transform:translate3d(0,110%,0);visibility:visible}to{transform:translateZ(0)}}@keyframes Toastify__slideInDown{0%{transform:translate3d(0,-110%,0);visibility:visible}to{transform:translateZ(0)}}@keyframes Toastify__slideOutRight{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(110%,0,0)}}@keyframes Toastify__slideOutLeft{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(-110%,0,0)}}@keyframes Toastify__slideOutDown{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(0,500px,0)}}@keyframes Toastify__slideOutUp{0%{transform:translateZ(0)}to{visibility:hidden;transform:translate3d(0,-500px,0)}}.Toastify__slide-enter--top-left,.Toastify__slide-enter--bottom-left{animation-name:Toastify__slideInLeft}.Toastify__slide-enter--top-right,.Toastify__slide-enter--bottom-right{animation-name:Toastify__slideInRight}.Toastify__slide-enter--top-center{animation-name:Toastify__slideInDown}.Toastify__slide-enter--bottom-center{animation-name:Toastify__slideInUp}.Toastify__slide-exit--top-left,.Toastify__slide-exit--bottom-left{animation-name:Toastify__slideOutLeft}.Toastify__slide-exit--top-right,.Toastify__slide-exit--bottom-right{animation-name:Toastify__slideOutRight}.Toastify__slide-exit--top-center{animation-name:Toastify__slideOutUp}.Toastify__slide-exit--bottom-center{animation-name:Toastify__slideOutDown}@keyframes Toastify__spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}} 2 | -------------------------------------------------------------------------------- /client/src/assets/images/not-found.svg: -------------------------------------------------------------------------------- 1 | page not found -------------------------------------------------------------------------------- /public/assets/not-found-KNix-0js.svg: -------------------------------------------------------------------------------- 1 | page not found --------------------------------------------------------------------------------