├── .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
;
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 |
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 |

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 |
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 |
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 |
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 |
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 |
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 |

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 |
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 |
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 |
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 |
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 |
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 |
--------------------------------------------------------------------------------
/public/assets/not-found-KNix-0js.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------