├── .env.example
├── .eslintignore
├── .eslintrc.json
├── .github
├── ISSUE_TEMPLATE
│ ├── bug_report.yml
│ ├── feature_request.yml
│ └── improvement.yml
└── workflows
│ ├── cd.yaml
│ ├── cd_prod.yaml
│ └── ci.yaml
├── .gitignore
├── .husky
└── pre-commit
├── .lintstagedrc.json
├── .prettierignore
├── .prettierrc
├── CONTRIBUTING.md
├── Dockerfile
├── Dockerfile.prod
├── README.md
├── components.json
├── docker-compose.yaml
├── next.config.js
├── package.json
├── postcss.config.mjs
├── prisma
├── migrations
│ ├── 20240929090953_dev
│ │ └── migration.sql
│ ├── 20241007041046_added_deleted_field_job
│ │ └── migration.sql
│ ├── 20241014172510_job_upate
│ │ └── migration.sql
│ ├── 20241015192257_added_cascading_job
│ │ └── migration.sql
│ ├── 20241016110618_expiry_job
│ │ └── migration.sql
│ ├── 20241019104716_
│ │ └── migration.sql
│ ├── 20241019174425_add_onboar_field
│ │ └── migration.sql
│ ├── 20241024174828_profileupdate
│ │ └── migration.sql
│ ├── 20241025095014_user_updated
│ │ └── migration.sql
│ ├── 20241025120951_resume_update_date
│ │ └── migration.sql
│ ├── 20241031043344_username_remove
│ │ └── migration.sql
│ ├── 20241031064849_company
│ │ └── migration.sql
│ └── migration_lock.toml
├── schema.prisma
└── seed.ts
├── public
├── BG-Grid-Light.svg
├── BG-Grid.svg
├── adobe.svg
├── atlassian.svg
├── coinbase.svg
├── companies.png
├── fonts
│ ├── font.woff2
│ └── satoshi.ttf
├── framer.svg
├── google.svg
├── main.png
├── main.svg
├── medium.svg
├── microsoft.svg
├── next.svg
├── robots.txt
├── solana.svg
├── spotify.png
├── spotify.svg
└── vercel.svg
├── src
├── actions
│ ├── auth.actions.ts
│ ├── corn.ts
│ ├── job.action.ts
│ ├── skills.cron.ts
│ ├── upload-to-cdn.ts
│ └── user.profile.actions.ts
├── app
│ ├── (auth)
│ │ ├── forgot-password
│ │ │ └── page.tsx
│ │ ├── layout.tsx
│ │ ├── reset-password
│ │ │ └── [token]
│ │ │ │ └── page.tsx
│ │ ├── signin
│ │ │ └── page.tsx
│ │ ├── signup
│ │ │ └── page.tsx
│ │ ├── verify-email
│ │ │ └── [token]
│ │ │ │ ├── EmailVerificationLinkExpired.tsx
│ │ │ │ └── page.tsx
│ │ └── welcome
│ │ │ └── page.tsx
│ ├── [...404]
│ │ └── page.tsx
│ ├── admin
│ │ └── page.tsx
│ ├── api
│ │ └── auth
│ │ │ └── [...nextauth]
│ │ │ └── route.ts
│ ├── create-profile
│ │ └── page.tsx
│ ├── create
│ │ └── page.tsx
│ ├── editDetails
│ │ └── page.tsx
│ ├── favicon.ico
│ ├── globals.css
│ ├── jobs
│ │ ├── [id]
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── loading.tsx
│ │ └── page.tsx
│ ├── layout.tsx
│ ├── manage
│ │ ├── jobs
│ │ │ └── page.tsx
│ │ └── recruiters
│ │ │ └── page.tsx
│ ├── page.tsx
│ └── profile
│ │ ├── [userId]
│ │ ├── loading.tsx
│ │ └── page.tsx
│ │ └── layout.tsx
├── components
│ ├── ApproveJobDialog.tsx
│ ├── BackgroundSvg.tsx
│ ├── BookmarkCardSkeletion.tsx
│ ├── DeleteDialog.tsx
│ ├── DescriptionEditor.tsx
│ ├── Faqs.tsx
│ ├── FaqsGetintouchCard.tsx
│ ├── HalfCircleGradient.tsx
│ ├── JobManagement.tsx
│ ├── JobManagementHeader.tsx
│ ├── JobManagementTable.tsx
│ ├── Jobcard.tsx
│ ├── JobcardSkeleton.tsx
│ ├── ManageRecruiters.tsx
│ ├── RecentJobs.tsx
│ ├── ScrollToTop.tsx
│ ├── ShareJobDialog.tsx
│ ├── TestimonialCard.tsx
│ ├── Testimonials.tsx
│ ├── ToggleApproveJobButton.tsx
│ ├── Toploader.tsx
│ ├── UserCard.tsx
│ ├── all-jobs.tsx
│ ├── auth
│ │ ├── forgot-password.tsx
│ │ ├── reset-password.tsx
│ │ ├── signin.tsx
│ │ ├── signup.tsx
│ │ ├── social-auth.tsx
│ │ └── welcome.tsx
│ ├── comboBox.tsx
│ ├── gmaps-autosuggest.tsx
│ ├── hero-section.tsx
│ ├── infinitescroll.tsx
│ ├── job-card-rec.tsx
│ ├── job-creation-success.tsx
│ ├── job-form.tsx
│ ├── job-landing.tsx
│ ├── job-skill.tsx
│ ├── job-skills.tsx
│ ├── job.tsx
│ ├── loader.tsx
│ ├── loading-spinner.tsx
│ ├── navitem.tsx
│ ├── pagination-client.tsx
│ ├── password-input.tsx
│ ├── profile-menu.tsx
│ ├── profile
│ │ ├── AboutMe.tsx
│ │ ├── AccountSettings.tsx
│ │ ├── ChangePassword.tsx
│ │ ├── DeleteAccountDialog.tsx
│ │ ├── EditProfile.tsx
│ │ ├── EditProfilePicture.tsx
│ │ ├── EducationDeleteDialog.tsx
│ │ ├── ExperienceDeleteDialog.tsx
│ │ ├── ProfileEducation.tsx
│ │ ├── ProfileExperience.tsx
│ │ ├── ProfileHeroSection.tsx
│ │ ├── ProfileHireme.tsx
│ │ ├── ProfileInfo.tsx
│ │ ├── ProfileProject.tsx
│ │ ├── ProfileProjects.tsx
│ │ ├── ProfileResume.tsx
│ │ ├── ProfileShare.tsx
│ │ ├── ProfileSkills.tsx
│ │ ├── ProfileSocials.tsx
│ │ ├── UserExperience.tsx
│ │ ├── UserProject.tsx
│ │ ├── UserResume.tsx
│ │ ├── UserSkills.tsx
│ │ ├── emptycontainers
│ │ │ └── ProfileEmptyContainers.tsx
│ │ ├── forms
│ │ │ ├── AccountSeetingForm.tsx
│ │ │ ├── EditProfileForm.tsx
│ │ │ ├── EducationForm.tsx
│ │ │ ├── ExperienceForm.tsx
│ │ │ ├── ProjectForm.tsx
│ │ │ ├── ReadMeForm.tsx
│ │ │ ├── SkillsForm.tsx
│ │ │ └── UploadResumeForm.tsx
│ │ ├── profile-skills-combobox.tsx
│ │ ├── profileComboBox.tsx
│ │ ├── projectDeleteDialog.tsx
│ │ ├── resumeDeleteDialog.tsx
│ │ ├── sheets
│ │ │ └── SheetWrapper.tsx
│ │ └── sidebar.tsx
│ ├── skills-combobox.tsx
│ ├── toggleJobButton.tsx
│ ├── ui
│ │ ├── Marquee.tsx
│ │ ├── accordion.tsx
│ │ ├── alert-dialog.tsx
│ │ ├── avatar.tsx
│ │ ├── badge.tsx
│ │ ├── button.tsx
│ │ ├── calendar.tsx
│ │ ├── card.tsx
│ │ ├── checkbox.tsx
│ │ ├── command.tsx
│ │ ├── dialog.tsx
│ │ ├── drawer.tsx
│ │ ├── dropdown-menu.tsx
│ │ ├── form.tsx
│ │ ├── icon.tsx
│ │ ├── infinite-moving-cards.tsx
│ │ ├── input.tsx
│ │ ├── label.tsx
│ │ ├── pagination.tsx
│ │ ├── paginator.tsx
│ │ ├── popover.tsx
│ │ ├── radio-group.tsx
│ │ ├── scroll-area.tsx
│ │ ├── select.tsx
│ │ ├── separator.tsx
│ │ ├── sheet.tsx
│ │ ├── skeleton.tsx
│ │ ├── slider.tsx
│ │ ├── switch.tsx
│ │ ├── table.tsx
│ │ ├── textarea.tsx
│ │ ├── theme-toggle.tsx
│ │ ├── toast.tsx
│ │ ├── toaster.tsx
│ │ └── use-toast.ts
│ ├── user-multistep-form
│ │ ├── add-project-form.tsx
│ │ ├── add-resume-form.tsx
│ │ ├── add-skills-form.tsx
│ │ ├── addExperience-form.tsx
│ │ └── user-multistep-form.tsx
│ └── userDetails.tsx
├── config
│ ├── app.config.ts
│ ├── auth.config.ts
│ ├── error.config.ts
│ ├── path.config.ts
│ ├── prisma.config.ts
│ └── skillapi.auth.token.ts
├── env
│ ├── client.ts
│ └── server.ts
├── hooks
│ ├── sample.ts
│ ├── useFilterCheck.ts
│ └── useSetQueryParams.ts
├── layouts
│ ├── footer.tsx
│ ├── form-container.tsx
│ ├── header.tsx
│ ├── job-filters.tsx
│ ├── jobs-header.tsx
│ └── mobile-nav.tsx
├── lib
│ ├── admin.ts
│ ├── async-catch.ts
│ ├── auth.ts
│ ├── authOptions.ts
│ ├── constant
│ │ ├── app.constant.ts
│ │ ├── faqs.constants.ts
│ │ ├── jobs.constant.ts
│ │ ├── profile.constant.ts
│ │ └── testimonials.constants.ts
│ ├── error.ts
│ ├── icons.ts
│ ├── sendConfirmationEmail.ts
│ ├── session.ts
│ ├── success.ts
│ ├── utils.ts
│ └── validators
│ │ ├── auth.validator.ts
│ │ ├── jobs.validator.ts
│ │ └── user.profile.validator.ts
├── middleware.ts
├── providers
│ ├── auth-provider.tsx
│ ├── providers.tsx
│ └── theme-provider.tsx
├── services
│ └── jobs.services.ts
└── types
│ ├── api.types.ts
│ ├── faqs.types.ts
│ ├── jobs.types.ts
│ ├── next-auth.d.ts
│ ├── recruiters.types.ts
│ ├── testimonials.types.ts
│ └── user.types.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | #
2 | # Database
3 | #
4 | DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"
5 | #
6 | # AUTH
7 | #
8 | NEXTAUTH_SECRET="koXrQGB5TFD4KALDX4kAvnQ5RHHvAOIzB"
9 | NEXTAUTH_URL="http://localhost:3000"
10 | #
11 | # Bunny CDN
12 | #
13 | CDN_API_KEY=api-key
14 | CDN_BASE_UPLOAD_URL=https://sg.storage.bunnycdn.com/job-board/assets
15 | CDN_BASE_ACCESS_URL=https://job-board.b-cdn.net/assets
16 |
17 | NEXT_PUBLIC_GOOGLE_MAPS_API_KEY=maps-api-key
18 |
19 | #
20 | # Email SMTP credentials
21 | #
22 | EMAIL_USER= # your email ex: vineetagarwal@gmail.com
23 | EMAIL_PASSWORD=
24 | EMAIL_SERVICE=gmail
25 | EMAIL_HOST=smtp.gmail.com
26 | EMAIL_PORT=587
27 |
28 | #
29 | # Google OAuth credentials
30 | #
31 | GOOGLE_CLIENT_ID=
32 | GOOGLE_CLIENT_SECRET=
33 |
34 | # go to https://lightcast.io/open-skills and signup to recieve your credentials
35 | LIGHTCAST_CLIENT_ID=
36 | LIGHTCAST_CLIENT_SECRET=
37 |
38 | # To run the application in production environment / check the envs
39 | # SKIP_ENV_CHECK=true npm run [replace with your script name]
40 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | *.d.ts
2 | .eslintrc
3 | .prettierrc
4 | .prettierignore
5 | .next
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals",
3 | "parser": "@typescript-eslint/parser",
4 | "plugins": ["@typescript-eslint"],
5 | "rules": {
6 | "no-console": ["error", { "allow": ["error"] }],
7 | "eqeqeq": "error",
8 | "@typescript-eslint/no-unused-vars": [
9 | "error",
10 | {
11 | "vars": "all",
12 | "args": "after-used",
13 | "caughtErrors": "all",
14 | "ignoreRestSiblings": true,
15 | "reportUsedIgnorePattern": false,
16 | "caughtErrorsIgnorePattern": "^_"
17 | }
18 | ]
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/feature_request.yml:
--------------------------------------------------------------------------------
1 | name: 🚀 Feature
2 | description: 'Submit a proposal for a new feature'
3 | title: '🚀 Feature: '
4 | labels: [feature]
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: |
9 | We value your time and efforts to submit this Feature request form. 🙏
10 | - type: textarea
11 | id: feature-description
12 | validations:
13 | required: true
14 | attributes:
15 | label: '🔖 Feature description'
16 | description: 'A clear and concise description of what the feature is.'
17 | placeholder: 'You should add ...'
18 | - type: textarea
19 | id: pitch
20 | validations:
21 | required: true
22 | attributes:
23 | label: '🎤 Why is this feature needed ?'
24 | description: 'Please explain why this feature should be implemented and how it would be used. Add examples, if applicable.'
25 | placeholder: 'In my use-case, ...'
26 | - type: textarea
27 | id: solution
28 | validations:
29 | required: true
30 | attributes:
31 | label: '✌️ How do you aim to achieve this?'
32 | description: 'A clear and concise description of what you want to happen.'
33 | placeholder: 'I want this feature to, ...'
34 | - type: textarea
35 | id: alternative
36 | validations:
37 | required: false
38 | attributes:
39 | label: '🔄️ Additional Information'
40 | description: "A clear and concise description of any alternative solutions or additional solutions you've considered."
41 | placeholder: 'I tried, ...'
42 | - type: checkboxes
43 | id: no-duplicate-issues
44 | attributes:
45 | label: '👀 Have you spent some time to check if this feature request has been raised before?'
46 | options:
47 | - label: "I checked and didn't find similar issue"
48 | required: true
49 | - type: checkboxes
50 | id: read-code-of-conduct
51 | attributes:
52 | label: '🏢 Have you read the Code of Conduct?'
53 | options:
54 | - label: 'I have read the [Contributing Guidelines](https://github.com/code100x/job-board/blob/main/CONTRIBUTING.md)'
55 | required: true
56 | - type: dropdown
57 | id: willing-to-submit-pr
58 | attributes:
59 | label: Are you willing to submit PR?
60 | description: This is absolutely not required, but we are happy to guide you in the contribution process.
61 | options:
62 | - 'Yes I am willing to submit a PR!'
63 |
--------------------------------------------------------------------------------
/.github/ISSUE_TEMPLATE/improvement.yml:
--------------------------------------------------------------------------------
1 | name: '✨ Improvement'
2 | description: 'Submit a improvement report to help us improve'
3 | title: '✨ Improvement: '
4 | labels: ['improvement']
5 | body:
6 | - type: markdown
7 | attributes:
8 | value: We value your time and effort to submit this improvement report. 🙏
9 | - type: textarea
10 | id: description
11 | validations:
12 | required: true
13 | attributes:
14 | label: '📜 Description'
15 | description: 'A clear and concise description of what the improvement is.'
16 | placeholder: "It's a improvement when ..."
17 | - type: textarea
18 | id: steps-to-reproduce
19 | attributes:
20 | label: '👟 Reproduction steps'
21 | description: 'How do you encountered this behaviour? Please walk us through it step by step.'
22 | placeholder: "1. Go to '...'
23 | 2. Click on '....'
24 | 3. Scroll down to '....'
25 | 4. See error"
26 | - type: textarea
27 | id: expected-behavior
28 | attributes:
29 | label: '👍 Expected behavior'
30 | description: 'What did you think should happen?'
31 | placeholder: 'It should ...'
32 | - type: textarea
33 | id: additional-context
34 | validations:
35 | required: false
36 | attributes:
37 | label: '📃 Provide any additional context for the Bug.'
38 | description: 'Add any other context about the problem here.'
39 | placeholder: 'It actually ...'
40 | - type: checkboxes
41 | id: no-duplicate-issues
42 | attributes:
43 | label: '👀 Have you spent some time to check if this bug has been raised before?'
44 | options:
45 | - label: "I checked and didn't find similar issue"
46 | required: true
47 | - type: checkboxes
48 | id: read-code-of-conduct
49 | attributes:
50 | label: '🏢 Have you read the Contributing Guidelines?'
51 | options:
52 | - label: 'I have read the [Contributing Guidelines](https://github.com/code100x/job-board/blob/main/CONTRIBUTING.md)'
53 | required: true
54 |
--------------------------------------------------------------------------------
/.github/workflows/cd.yaml:
--------------------------------------------------------------------------------
1 | name: Continuous Deployment
2 | on:
3 | push:
4 | branches: [ main ]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | with:
11 | fetch-depth: 0
12 |
13 | - name: Docker login
14 | uses: docker/login-action@v2
15 | with:
16 | username: ${{ secrets.DOCKERHUB_USERNAME }}
17 | password: ${{ secrets.DOCKERHUB_TOKEN }}
18 |
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v2
21 |
22 | - name: Build and push
23 | uses: docker/build-push-action@v4
24 | with:
25 | context: .
26 | file: ./Dockerfile.prod
27 | push: true
28 | tags: 100xdevs/job-board-staging:${{ github.sha }}
29 |
30 | - name: Clone staging-ops repo, update, and push
31 | env:
32 | PAT: ${{ secrets.PAT }}
33 | run: |
34 | git clone https://github.com/code100x/staging-ops.git
35 | cd staging-ops
36 | sed -i 's|image: 100xdevs/job-board-staging:.*|image: 100xdevs/job-board-staging:${{ github.sha }}|' staging/job-board/deployment.yml
37 | git config user.name "GitHub Actions Bot"
38 | git config user.email "actions@github.com"
39 | git add staging/job-board/deployment.yml
40 | git commit -m "Update job-board-staging image to ${{ github.sha }}"
41 | git push https://${PAT}@github.com/code100x/staging-ops.git main
--------------------------------------------------------------------------------
/.github/workflows/cd_prod.yaml:
--------------------------------------------------------------------------------
1 | name: Continuous Deployment (Prod)
2 | on:
3 | push:
4 | branches: [ production ]
5 | jobs:
6 | build:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v2
10 | with:
11 | fetch-depth: 0
12 |
13 | - name: Docker login
14 | uses: docker/login-action@v2
15 | with:
16 | username: ${{ secrets.DOCKERHUB_USERNAME }}
17 | password: ${{ secrets.DOCKERHUB_TOKEN }}
18 |
19 | - name: Set up Docker Buildx
20 | uses: docker/setup-buildx-action@v2
21 |
22 | - name: Build and push
23 | uses: docker/build-push-action@v4
24 | with:
25 | context: .
26 | file: ./Dockerfile.prod
27 | push: true
28 | tags: 100xdevs/job-board:${{ github.sha }}
29 |
30 | - name: Clone staging-ops repo, update, and push
31 | env:
32 | PAT: ${{ secrets.PAT }}
33 | run: |
34 | git clone https://github.com/code100x/staging-ops.git
35 | cd staging-ops
36 | sed -i 's|image: 100xdevs/job-board:.*|image: 100xdevs/job-board:${{ github.sha }}|' prod/job-board/deployment.yml
37 | git config user.name "GitHub Actions Bot"
38 | git config user.email "actions@github.com"
39 | git add prod/job-board/deployment.yml
40 | git commit -m "Update job-board image to ${{ github.sha }}"
41 | git push https://${PAT}@github.com/code100x/staging-ops.git main
--------------------------------------------------------------------------------
/.github/workflows/ci.yaml:
--------------------------------------------------------------------------------
1 | name: Continuous Integration
2 |
3 | env:
4 | SKIP_ENV_CHECK: 'true'
5 |
6 | on:
7 | push:
8 | branches: [main]
9 | pull_request:
10 | branches: [main]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | steps:
17 | - uses: actions/checkout@v2
18 | with:
19 | fetch-depth: 0
20 |
21 | - name: Use Node.js
22 | uses: actions/setup-node@v2
23 | with:
24 | node-version: '20.9.0'
25 |
26 | - name: Install dependencies
27 | run: npm install
28 |
29 | - name: Run lint
30 | run: npm run lint
31 |
32 | - name: Run format
33 | run: npm run format
34 |
35 | - name: Run format check
36 | run: npm run check
37 |
38 | - name: Run build
39 | run: npm run build
40 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 | package-lock.json
9 |
10 | # testing
11 | /coverage
12 |
13 | # next.js
14 | /.next/
15 | /out/
16 |
17 | # production
18 | /build
19 |
20 | # misc
21 | .DS_Store
22 | *.pem
23 |
24 | # debug
25 | npm-debug.log*
26 | yarn-debug.log*
27 | yarn-error.log*
28 |
29 | # local env files
30 | .env*.local
31 | .env
32 |
33 | # vercel
34 | .vercel
35 |
36 | # typescript
37 | *.tsbuildinfo
38 | next-env.d.ts
39 | pnpm-lock.yaml
40 | bun.lockb
41 | package-lock.json
42 | yarn.lock
43 |
44 | **/public/sw.js
45 | **/public/workbox-*.js
46 | **/public/worker-*.js
47 | **/public/sw.js.map
48 | **/public/workbox-*.js.map
49 | **/public/worker-*.js.map
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | npx lint-staged
--------------------------------------------------------------------------------
/.lintstagedrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "src/**/*.{js,jsx,ts,tsx,json}": ["eslint --fix"],
3 | "src/**/*.{js,jsx,ts,tsx,json,css,md}": ["prettier --write"]
4 | }
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | dist
2 | node_modules
3 | .next
4 | /package-lock.json
5 | *.yaml
6 | build
7 | .env
8 | out
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "tabWidth": 2,
3 | "useTabs": false,
4 | "semi": true,
5 | "singleQuote": true,
6 | "trailingComma": "es5",
7 | "printWidth": 80,
8 | "bracketSpacing": true,
9 | "bracketSameLine": false,
10 | "arrowParens": "always",
11 | "endOfLine": "auto"
12 | }
13 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:20-alpine AS deps
2 | WORKDIR /app
3 | COPY package*.json ./
4 | RUN npm i
5 |
6 | FROM node:20-alpine AS builder
7 | WORKDIR /app
8 | COPY . .
9 | COPY --from=deps /app/node_modules ./node_modules
10 | RUN npx prisma generate
11 | RUN npm run build
12 |
13 | FROM node:20-alpine AS runner
14 | WORKDIR /app
15 | COPY --from=builder /app/.next/standalone ./
16 | COPY --from=builder /app/.next/static ./.next/static
17 | COPY --from=builder /app/public ./public
18 | COPY --from=builder /app/package.json ./
19 |
20 | EXPOSE 3000
21 | ENV PORT 3000
22 | CMD ["node", "server.js"]
23 |
--------------------------------------------------------------------------------
/Dockerfile.prod:
--------------------------------------------------------------------------------
1 |
2 | FROM node:20-alpine AS build
3 | ARG DATABASE_URL
4 | WORKDIR /usr/src/app
5 | COPY package*.json ./
6 | RUN npm install
7 | COPY . .
8 | RUN DATABASE_URL=$DATABASE_URL npx prisma generate
9 | RUN DATABASE_URL=$DATABASE_URL npm run build
10 |
11 | FROM node:20-alpine AS production
12 | WORKDIR /usr/src/app
13 | COPY --from=build /usr/src/app/.next ./.next
14 | COPY --from=build /usr/src/app/node_modules ./node_modules
15 | COPY --from=build /usr/src/app/public ./public
16 | COPY --from=build /usr/src/app/package.json ./package.json
17 | CMD ["npm", "run", "start"]
18 |
19 | EXPOSE 3000
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.ts",
8 | "css": "src/app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | services:
2 | app:
3 | build:
4 | dockerfile: Dockerfile
5 | target: development # Change this to production for prod builds (WIP)
6 | container_name: job-board-app
7 | environment:
8 | - DATABASE_URL=$DATABASE_URL
9 | - AUTH_SECRET=$AUTH_SECRET
10 | ports:
11 | - "3000:3000"
12 | volumes:
13 | - .:/usr/src/app
14 | - /usr/src/app/node_modules
15 | depends_on:
16 | db:
17 | condition: service_healthy
18 |
19 | db:
20 | image: postgres:16-alpine
21 | container_name: job-board-db
22 | restart: always
23 | environment:
24 | POSTGRES_USER: postgres
25 | POSTGRES_PASSWORD: postgres
26 | POSTGRES_DB: job-board-db
27 | ports:
28 | - 5432:5432
29 | volumes:
30 | - postgres-data:/var/lib/postgresql/data
31 | healthcheck:
32 | test: ["CMD-SHELL", "pg_isready -d $${POSTGRES_DB} -U $${POSTGRES_USER}"]
33 | interval: 10s
34 | timeout: 5s
35 | retries: 5
36 |
37 | prisma-studio:
38 | container_name: prisma-studio
39 | image: timothyjmiller/prisma-studio:latest
40 | restart: unless-stopped
41 | env_file:
42 | - .env
43 | depends_on:
44 | - app
45 | ports:
46 | - 5555:5555
47 |
48 | volumes:
49 | postgres-data:
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | // next.config.js
2 | import { fileURLToPath } from 'node:url';
3 | import createJiti from 'jiti';
4 |
5 | if (process.env.SKIP_ENV_CHECK !== 'true') {
6 | const jiti = createJiti(fileURLToPath(import.meta.url));
7 | jiti('./src/env/client');
8 | jiti('./src/env/server');
9 | }
10 |
11 | /** @type {import('next').NextConfig} */
12 | const nextConfig = {
13 | output: 'standalone',
14 | reactStrictMode: true,
15 | logging: {
16 | fetches: {
17 | fullUrl: true,
18 | },
19 | },
20 | images: {
21 | remotePatterns: [
22 | {
23 | protocol: 'https',
24 | hostname: 'job-board.b-cdn.net',
25 | },
26 | {
27 | protocol: 'https',
28 | hostname: 'lh3.googleusercontent.com',
29 | },
30 | {
31 | protocol: 'https',
32 | hostname: 'aakash2330.b-cdn.net',
33 | },
34 | {
35 | protocol: 'https',
36 | hostname: 'www.example.com',
37 | }
38 | ],
39 | },
40 | };
41 |
42 | export default nextConfig;
43 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/prisma/migrations/20240929090953_dev/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateEnum
2 | CREATE TYPE "OauthProvider" AS ENUM ('GOOGLE');
3 |
4 | -- CreateEnum
5 | CREATE TYPE "TokenType" AS ENUM ('EMAIL_VERIFICATION', 'RESET_PASSWORD');
6 |
7 | -- CreateEnum
8 | CREATE TYPE "Currency" AS ENUM ('INR', 'USD');
9 |
10 | -- CreateEnum
11 | CREATE TYPE "WorkMode" AS ENUM ('remote', 'hybrid', 'office');
12 |
13 | -- CreateEnum
14 | CREATE TYPE "Role" AS ENUM ('USER', 'ADMIN');
15 |
16 | -- CreateEnum
17 | CREATE TYPE "EmployementType" AS ENUM ('Full_time', 'Part_time', 'Internship', 'Contract');
18 |
19 | -- CreateTable
20 | CREATE TABLE "User" (
21 | "id" TEXT NOT NULL,
22 | "name" TEXT NOT NULL,
23 | "password" TEXT,
24 | "avatar" TEXT,
25 | "isVerified" BOOLEAN NOT NULL DEFAULT false,
26 | "role" "Role" NOT NULL DEFAULT 'USER',
27 | "email" TEXT NOT NULL,
28 | "emailVerified" TIMESTAMP(3),
29 | "oauthProvider" "OauthProvider",
30 | "oauthId" TEXT,
31 | "blockedByAdmin" TIMESTAMP(3),
32 |
33 | CONSTRAINT "User_pkey" PRIMARY KEY ("id")
34 | );
35 |
36 | -- CreateTable
37 | CREATE TABLE "VerificationToken" (
38 | "token" TEXT NOT NULL,
39 | "identifier" TEXT NOT NULL,
40 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
41 | "updatedAt" TIMESTAMP(3) NOT NULL,
42 | "type" "TokenType" NOT NULL
43 | );
44 |
45 | -- CreateTable
46 | CREATE TABLE "Job" (
47 | "id" TEXT NOT NULL,
48 | "userId" TEXT NOT NULL,
49 | "title" TEXT NOT NULL,
50 | "description" TEXT,
51 | "company_name" TEXT NOT NULL,
52 | "company_bio" TEXT NOT NULL,
53 | "company_email" TEXT NOT NULL,
54 | "category" TEXT NOT NULL,
55 | "type" "EmployementType" NOT NULL,
56 | "work_mode" "WorkMode" NOT NULL,
57 | "currency" "Currency" NOT NULL DEFAULT 'INR',
58 | "city" TEXT NOT NULL,
59 | "address" TEXT NOT NULL,
60 | "application" TEXT NOT NULL,
61 | "companyLogo" TEXT NOT NULL,
62 | "skills" TEXT[],
63 | "has_salary_range" BOOLEAN NOT NULL DEFAULT false,
64 | "minSalary" INTEGER,
65 | "maxSalary" INTEGER,
66 | "has_experience_range" BOOLEAN NOT NULL DEFAULT false,
67 | "minExperience" INTEGER,
68 | "maxExperience" INTEGER,
69 | "is_verified_job" BOOLEAN NOT NULL DEFAULT false,
70 | "postedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
71 | "updatedAt" TIMESTAMP(3) NOT NULL,
72 |
73 | CONSTRAINT "Job_pkey" PRIMARY KEY ("id")
74 | );
75 |
76 | -- CreateIndex
77 | CREATE UNIQUE INDEX "User_email_key" ON "User"("email");
78 |
79 | -- CreateIndex
80 | CREATE UNIQUE INDEX "VerificationToken_token_identifier_key" ON "VerificationToken"("token", "identifier");
81 |
82 | -- AddForeignKey
83 | ALTER TABLE "Job" ADD CONSTRAINT "Job_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
84 |
--------------------------------------------------------------------------------
/prisma/migrations/20241007041046_added_deleted_field_job/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "Job" ADD COLUMN "deleted" BOOLEAN NOT NULL DEFAULT false;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20241014172510_job_upate/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterEnum
2 | ALTER TYPE "Role" ADD VALUE 'HR';
3 |
4 | -- AlterTable
5 | ALTER TABLE "User" ADD COLUMN "onBoard" BOOLEAN NOT NULL DEFAULT false,
6 | ADD COLUMN "resume" TEXT,
7 | ADD COLUMN "skills" TEXT[];
8 |
9 | -- CreateTable
10 | CREATE TABLE "Experience" (
11 | "id" SERIAL NOT NULL,
12 | "companyName" TEXT NOT NULL,
13 | "designation" TEXT NOT NULL,
14 | "EmploymentType" "EmployementType" NOT NULL,
15 | "address" TEXT NOT NULL,
16 | "workMode" "WorkMode" NOT NULL,
17 | "currentWorkStatus" BOOLEAN NOT NULL,
18 | "startDate" TIMESTAMP(3) NOT NULL,
19 | "endDate" TIMESTAMP(3),
20 | "description" TEXT NOT NULL,
21 | "userId" TEXT NOT NULL,
22 |
23 | CONSTRAINT "Experience_pkey" PRIMARY KEY ("id")
24 | );
25 |
26 | -- CreateTable
27 | CREATE TABLE "Project" (
28 | "id" SERIAL NOT NULL,
29 | "projectName" TEXT NOT NULL,
30 | "projectSummary" TEXT NOT NULL,
31 | "projectLiveLink" TEXT,
32 | "projectGithub" TEXT NOT NULL,
33 | "userId" TEXT NOT NULL,
34 |
35 | CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
36 | );
37 |
38 | -- AddForeignKey
39 | ALTER TABLE "Experience" ADD CONSTRAINT "Experience_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
40 |
41 | -- AddForeignKey
42 | ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
43 |
--------------------------------------------------------------------------------
/prisma/migrations/20241015192257_added_cascading_job/migration.sql:
--------------------------------------------------------------------------------
1 | -- DropForeignKey
2 | ALTER TABLE "Job" DROP CONSTRAINT "Job_userId_fkey";
3 |
4 | -- AddForeignKey
5 | ALTER TABLE "Job" ADD CONSTRAINT "Job_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20241016110618_expiry_job/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `onBoard` on the `User` table. All the data in the column will be lost.
5 | - You are about to drop the column `resume` on the `User` table. All the data in the column will be lost.
6 | - You are about to drop the column `skills` on the `User` table. All the data in the column will be lost.
7 | - You are about to drop the `Experience` table. If the table is not empty, all the data it contains will be lost.
8 | - You are about to drop the `Project` table. If the table is not empty, all the data it contains will be lost.
9 |
10 | */
11 | -- DropForeignKey
12 | ALTER TABLE "Experience" DROP CONSTRAINT "Experience_userId_fkey";
13 |
14 | -- DropForeignKey
15 | ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";
16 |
17 | -- AlterTable
18 | ALTER TABLE "Job" ADD COLUMN "expired" BOOLEAN NOT NULL DEFAULT false,
19 | ADD COLUMN "expiryDate" TIMESTAMP(3),
20 | ADD COLUMN "has_expiry_date" BOOLEAN NOT NULL DEFAULT false;
21 |
22 | -- AlterTable
23 | ALTER TABLE "User" DROP COLUMN "onBoard",
24 | DROP COLUMN "resume",
25 | DROP COLUMN "skills";
26 |
27 | -- DropTable
28 | DROP TABLE "Experience";
29 |
30 | -- DropTable
31 | DROP TABLE "Project";
32 |
--------------------------------------------------------------------------------
/prisma/migrations/20241019104716_/migration.sql:
--------------------------------------------------------------------------------
1 | -- CreateTable
2 | CREATE TABLE "Bookmark" (
3 | "id" TEXT NOT NULL,
4 | "jobId" TEXT NOT NULL,
5 | "userId" TEXT NOT NULL,
6 |
7 | CONSTRAINT "Bookmark_pkey" PRIMARY KEY ("id")
8 | );
9 |
10 | -- AddForeignKey
11 | ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job"("id") ON DELETE CASCADE ON UPDATE CASCADE;
12 |
13 | -- AddForeignKey
14 | ALTER TABLE "Bookmark" ADD CONSTRAINT "Bookmark_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
15 |
--------------------------------------------------------------------------------
/prisma/migrations/20241019174425_add_onboar_field/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "onBoard" BOOLEAN NOT NULL DEFAULT false,
3 | ADD COLUMN "resume" TEXT,
4 | ADD COLUMN "skills" TEXT[];
5 |
6 | -- CreateTable
7 | CREATE TABLE "Experience" (
8 | "id" SERIAL NOT NULL,
9 | "companyName" TEXT NOT NULL,
10 | "designation" TEXT NOT NULL,
11 | "EmploymentType" "EmployementType" NOT NULL,
12 | "address" TEXT NOT NULL,
13 | "workMode" "WorkMode" NOT NULL,
14 | "currentWorkStatus" BOOLEAN NOT NULL,
15 | "startDate" TIMESTAMP(3) NOT NULL,
16 | "endDate" TIMESTAMP(3),
17 | "description" TEXT NOT NULL,
18 | "userId" TEXT NOT NULL,
19 |
20 | CONSTRAINT "Experience_pkey" PRIMARY KEY ("id")
21 | );
22 |
23 | -- CreateTable
24 | CREATE TABLE "Project" (
25 | "id" SERIAL NOT NULL,
26 | "projectName" TEXT NOT NULL,
27 | "projectSummary" TEXT NOT NULL,
28 | "projectLiveLink" TEXT,
29 | "projectGithub" TEXT NOT NULL,
30 | "userId" TEXT NOT NULL,
31 |
32 | CONSTRAINT "Project_pkey" PRIMARY KEY ("id")
33 | );
34 |
35 | -- AddForeignKey
36 | ALTER TABLE "Experience" ADD CONSTRAINT "Experience_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
37 |
38 | -- AddForeignKey
39 | ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
40 |
--------------------------------------------------------------------------------
/prisma/migrations/20241024174828_profileupdate/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[username]` on the table `User` will be added. If there are existing duplicate values, this will fail.
5 | - Added the required column `aboutMe` to the `User` table without a default value. This is not possible if the table is not empty.
6 | - Added the required column `contactEmail` to the `User` table without a default value. This is not possible if the table is not empty.
7 | - Added the required column `username` to the `User` table without a default value. This is not possible if the table is not empty.
8 |
9 | */
10 | -- CreateEnum
11 | CREATE TYPE "ProjectStack" AS ENUM ('GO', 'PYTHON', 'MERN', 'NEXTJS', 'AI_GPT_APIS', 'SPRINGBOOT', 'OTHERS');
12 |
13 | -- CreateEnum
14 | CREATE TYPE "DegreeType" AS ENUM ('BTech', 'MTech', 'BCA', 'MCA');
15 |
16 | -- CreateEnum
17 | CREATE TYPE "FieldOfStudyType" AS ENUM ('AI', 'Machine_Learning', 'CS', 'Mechanical');
18 |
19 | -- DropForeignKey
20 | ALTER TABLE "Experience" DROP CONSTRAINT "Experience_userId_fkey";
21 |
22 | -- DropForeignKey
23 | ALTER TABLE "Project" DROP CONSTRAINT "Project_userId_fkey";
24 |
25 | -- AlterTable
26 | ALTER TABLE "Job" ADD COLUMN "deletedAt" TIMESTAMP(3);
27 |
28 | -- AlterTable
29 | ALTER TABLE "Project" ADD COLUMN "isFeature" BOOLEAN NOT NULL DEFAULT false,
30 | ADD COLUMN "projectThumbnail" TEXT,
31 | ADD COLUMN "stack" "ProjectStack" NOT NULL DEFAULT 'OTHERS';
32 |
33 | -- AlterTable
34 | ALTER TABLE "User" ADD COLUMN "aboutMe" TEXT NOT NULL,
35 | ADD COLUMN "contactEmail" TEXT NOT NULL,
36 | ADD COLUMN "discordLink" TEXT,
37 | ADD COLUMN "githubLink" TEXT,
38 | ADD COLUMN "linkedinLink" TEXT,
39 | ADD COLUMN "portfolioLink" TEXT,
40 | ADD COLUMN "twitterLink" TEXT,
41 | ADD COLUMN "username" TEXT NOT NULL;
42 |
43 | -- CreateTable
44 | CREATE TABLE "Education" (
45 | "id" SERIAL NOT NULL,
46 | "instituteName" TEXT NOT NULL,
47 | "degree" "DegreeType" NOT NULL,
48 | "fieldOfStudy" "FieldOfStudyType" NOT NULL,
49 | "startDate" TIMESTAMP(3) NOT NULL,
50 | "endDate" TIMESTAMP(3),
51 | "userId" TEXT NOT NULL,
52 |
53 | CONSTRAINT "Education_pkey" PRIMARY KEY ("id")
54 | );
55 |
56 | -- CreateIndex
57 | CREATE UNIQUE INDEX "User_username_key" ON "User"("username");
58 |
59 | -- AddForeignKey
60 | ALTER TABLE "Experience" ADD CONSTRAINT "Experience_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
61 |
62 | -- AddForeignKey
63 | ALTER TABLE "Education" ADD CONSTRAINT "Education_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
64 |
65 | -- AddForeignKey
66 | ALTER TABLE "Project" ADD CONSTRAINT "Project_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE;
67 |
--------------------------------------------------------------------------------
/prisma/migrations/20241025095014_user_updated/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ALTER COLUMN "aboutMe" DROP NOT NULL,
3 | ALTER COLUMN "contactEmail" DROP NOT NULL;
4 |
--------------------------------------------------------------------------------
/prisma/migrations/20241025120951_resume_update_date/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "User" ADD COLUMN "resumeUpdateDate" TIMESTAMP(3);
3 |
--------------------------------------------------------------------------------
/prisma/migrations/20241031043344_username_remove/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - You are about to drop the column `username` on the `User` table. All the data in the column will be lost.
5 |
6 | */
7 | -- DropIndex
8 | DROP INDEX "User_username_key";
9 |
10 | -- AlterTable
11 | ALTER TABLE "User" DROP COLUMN "username";
12 |
--------------------------------------------------------------------------------
/prisma/migrations/20241031064849_company/migration.sql:
--------------------------------------------------------------------------------
1 | /*
2 | Warnings:
3 |
4 | - A unique constraint covering the columns `[companyId]` on the table `User` will be added. If there are existing duplicate values, this will fail.
5 |
6 | */
7 | -- AlterTable
8 | ALTER TABLE "User" ADD COLUMN "companyId" TEXT,
9 | ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP;
10 |
11 | -- CreateTable
12 | CREATE TABLE "Company" (
13 | "id" TEXT NOT NULL,
14 | "companyName" TEXT NOT NULL,
15 | "companyLogo" TEXT,
16 | "companyEmail" TEXT NOT NULL,
17 | "companyBio" TEXT NOT NULL,
18 |
19 | CONSTRAINT "Company_pkey" PRIMARY KEY ("id")
20 | );
21 |
22 | -- CreateIndex
23 | CREATE UNIQUE INDEX "User_companyId_key" ON "User"("companyId");
24 |
25 | -- AddForeignKey
26 | ALTER TABLE "User" ADD CONSTRAINT "User_companyId_fkey" FOREIGN KEY ("companyId") REFERENCES "Company"("id") ON DELETE SET NULL ON UPDATE CASCADE;
27 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (i.e. Git)
3 | provider = "postgresql"
--------------------------------------------------------------------------------
/public/companies.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/public/companies.png
--------------------------------------------------------------------------------
/public/fonts/font.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/public/fonts/font.woff2
--------------------------------------------------------------------------------
/public/fonts/satoshi.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/public/fonts/satoshi.ttf
--------------------------------------------------------------------------------
/public/main.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/public/main.png
--------------------------------------------------------------------------------
/public/main.svg:
--------------------------------------------------------------------------------
1 |
11 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | User-agent: *
2 | Allow: /
3 | Disallow: /admin/*
4 | Disallow: /manage/*
5 |
6 | Sitemap: https://job.vineet.tech/sitemap.xml
--------------------------------------------------------------------------------
/public/spotify.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/public/spotify.png
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/actions/corn.ts:
--------------------------------------------------------------------------------
1 | // lib/cron.ts
2 | import cron from 'node-cron';
3 | import { deleteOldDeltedJobs, updateExpiredJobs } from './job.action';
4 |
5 | let cronJobInitialized = false;
6 |
7 | export const startCronJob = () => {
8 | if (!cronJobInitialized) {
9 | cronJobInitialized = true;
10 |
11 | // Schedule the job to run at midnight (12:00 AM) every day
12 | cron.schedule('0 0 * * *', async () => {
13 | try {
14 | await updateExpiredJobs();
15 | await deleteOldDeltedJobs();
16 | } catch (error) {
17 | console.error('Error updating expired jobs:', error);
18 | }
19 | });
20 | }
21 | };
22 |
--------------------------------------------------------------------------------
/src/actions/skills.cron.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 | /* eslint-disable no-console */
3 | import { NextResponse } from 'next/server';
4 | import { bearerToken } from '@/config/skillapi.auth.token';
5 |
6 | var cron = require('node-cron');
7 |
8 | const url = 'https://auth.emsicloud.com/connect/token';
9 |
10 | async function startAuthTokenCronJob() {
11 | // don't run the cron job if its already running
12 | await fetchAuthTokenCronJob();
13 | // Schedule the cron to run every 45 minutes , the token resets after one hour
14 | cron.schedule('*/45 * * * *', async () => {
15 | await fetchAuthTokenCronJob();
16 | });
17 | return NextResponse.json({ data: 'Success', status: 200 });
18 | }
19 |
20 | const options = {
21 | method: 'POST',
22 | headers: {
23 | 'Content-Type': 'application/x-www-form-urlencoded',
24 | },
25 | body: new URLSearchParams({
26 | client_id: process.env.LIGHTCAST_CLIENT_ID ?? '',
27 | client_secret: process.env.LIGHTCAST_CLIENT_SECRET ?? '',
28 | grant_type: 'client_credentials',
29 | scope: 'emsi_open',
30 | }),
31 | };
32 |
33 | async function fetchAuthTokenCronJob() {
34 | try {
35 | const response = await fetch(url, options);
36 | if (!response.ok) {
37 | throw new Error(`HTTP error! Status: ${response.status}`);
38 | }
39 | const data = await response.json();
40 | bearerToken.value = data.access_token;
41 | } catch (error) {
42 | console.error('Error:', error);
43 | }
44 | }
45 |
46 | export async function getBearerToken() {
47 | return bearerToken.value;
48 | }
49 |
50 | startAuthTokenCronJob();
51 |
--------------------------------------------------------------------------------
/src/actions/upload-to-cdn.ts:
--------------------------------------------------------------------------------
1 | 'use server';
2 |
3 | import { v4 as uuidv4 } from 'uuid';
4 |
5 | type FileType = 'webp' | 'pdf';
6 |
7 | export async function uploadFileAction(formData: FormData, fileType: FileType) {
8 | const CDN_BASE_UPLOAD_URL = process.env.CDN_BASE_UPLOAD_URL!;
9 | const CDN_BASE_ACCESS_URL = process.env.CDN_BASE_ACCESS_URL!;
10 | const CDN_API_KEY = process.env.CDN_API_KEY!;
11 |
12 | try {
13 | const file = formData.get('file') as File;
14 | const uniqueFileName = formData.get('uniqueFileName') || uuidv4(); // Generate unique key if not provided
15 |
16 | if (!file) {
17 | return { error: 'File is required', status: 400 };
18 | }
19 | const uploadUrl = `${CDN_BASE_UPLOAD_URL}/${uniqueFileName}.${fileType}`;
20 | const fileBuffer = Buffer.from(await file.arrayBuffer());
21 |
22 | const response = await fetch(uploadUrl, {
23 | method: 'PUT',
24 | headers: {
25 | AccessKey: CDN_API_KEY,
26 | 'Content-Type': 'application/octet-stream',
27 | },
28 | body: fileBuffer,
29 | });
30 | if (response.ok) {
31 | return {
32 | message: 'File uploaded successfully',
33 | url: `${CDN_BASE_ACCESS_URL}/${uniqueFileName}.${fileType}`,
34 | };
35 | } else {
36 | return { error: 'Failed to upload file', status: response.status };
37 | }
38 | } catch (error) {
39 | console.error('Error uploading file:', error);
40 | return { error: 'Internal server error', status: 500 };
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/app/(auth)/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | import { ForgotPassword } from '@/components/auth/forgot-password';
2 | import { FormContainer } from '@/layouts/form-container';
3 | import React from 'react';
4 |
5 | const ForgootPasswordPage = async () => {
6 | return (
7 |
8 |
12 |
13 |
14 |
15 | );
16 | };
17 |
18 | export default ForgootPasswordPage;
19 |
--------------------------------------------------------------------------------
/src/app/(auth)/layout.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { authOptions } from '@/lib/authOptions';
3 | import { getServerSession } from 'next-auth';
4 | import { redirect } from 'next/navigation';
5 |
6 | export default async function AuthLayout({
7 | children,
8 | }: {
9 | children: React.ReactNode;
10 | }) {
11 | const auth = await getServerSession(authOptions);
12 |
13 | if (auth) redirect(`/`);
14 |
15 | return children;
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/(auth)/reset-password/[token]/page.tsx:
--------------------------------------------------------------------------------
1 | import { ResetPassword } from '@/components/auth/reset-password';
2 | import prisma from '@/config/prisma.config';
3 | import { FormContainer } from '@/layouts/form-container';
4 | import { isTokenExpiredUtil } from '@/lib/utils';
5 | import { notFound } from 'next/navigation';
6 | import React from 'react';
7 |
8 | const ResetPasswordPage = async ({
9 | params: { token },
10 | }: {
11 | params: { token: string };
12 | }) => {
13 | const verificatinToken = await prisma.verificationToken.findFirst({
14 | where: { token },
15 | });
16 |
17 | if (!verificatinToken) notFound();
18 |
19 | if (isTokenExpiredUtil(verificatinToken.createdAt))
20 | return link expried
;
21 |
22 | const user = await prisma.user.findFirst({
23 | where: { id: verificatinToken.identifier },
24 | });
25 |
26 | if (!user) notFound();
27 |
28 | return (
29 |
30 |
34 |
35 |
36 |
37 | );
38 | };
39 |
40 | export default ResetPasswordPage;
41 |
--------------------------------------------------------------------------------
/src/app/(auth)/signin/page.tsx:
--------------------------------------------------------------------------------
1 | import { Signin } from '@/components/auth/signin';
2 | import { FormContainer } from '@/layouts/form-container';
3 |
4 | const LoginPage = () => {
5 | return (
6 |
7 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default LoginPage;
18 |
--------------------------------------------------------------------------------
/src/app/(auth)/signup/page.tsx:
--------------------------------------------------------------------------------
1 | import { Signup } from '@/components/auth/signup';
2 | import { FormContainer } from '@/layouts/form-container';
3 |
4 | const SignupPage = () => {
5 | return (
6 |
7 |
11 |
12 |
13 |
14 | );
15 | };
16 |
17 | export default SignupPage;
18 |
--------------------------------------------------------------------------------
/src/app/(auth)/verify-email/[token]/EmailVerificationLinkExpired.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useState } from 'react';
3 | import { FormContainer } from '@/layouts/form-container';
4 | import Link from 'next/link';
5 | import APP_PATHS from '@/config/path.config';
6 | import { Button } from '@/components/ui/button';
7 | import { verifyEmail } from '@/actions/auth.actions';
8 | import { useToast } from '@/components/ui/use-toast';
9 |
10 | export const EmailVerificationLinkExpired = ({ token }: { token: string }) => {
11 | const [isEmailSent, setIsEmailSent] = useState(false);
12 | const [isLoading, setIsLoading] = useState(false);
13 | const { toast } = useToast();
14 |
15 | const handleResendClick = async () => {
16 | setIsLoading(true);
17 | try {
18 | await verifyEmail({ token, resend: true });
19 | setIsEmailSent(true);
20 | } catch {
21 | toast({
22 | variant: 'destructive',
23 | title: 'Something went wrong, please try again!',
24 | });
25 | } finally {
26 | setIsLoading(!true);
27 | }
28 | };
29 |
30 | return (
31 |
32 |
40 | {!isEmailSent ? (
41 |
42 |
49 |
50 | ) : (
51 |
52 |
55 |
56 | )}
57 |
58 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/src/app/(auth)/verify-email/[token]/page.tsx:
--------------------------------------------------------------------------------
1 | import { verifyEmail } from '@/actions/auth.actions';
2 | import { Button } from '@/components/ui/button';
3 | import APP_PATHS from '@/config/path.config';
4 | import { FormContainer } from '@/layouts/form-container';
5 | import Link from 'next/link';
6 | import { redirect } from 'next/navigation';
7 | import React from 'react';
8 | import { EmailVerificationLinkExpired } from './EmailVerificationLinkExpired';
9 |
10 | const Page = async ({ params: { token } }: { params: { token: string } }) => {
11 | const res = await verifyEmail({ token });
12 |
13 | if (res.status) return ;
14 | else if (res?.error?.notFound) return ;
15 | else if (res?.error?.linkExpired)
16 | return ;
17 | return redirect(APP_PATHS.SIGNIN);
18 | };
19 |
20 | export default Page;
21 |
22 | const EmailVerifiedSuccess = () => {
23 | return (
24 |
25 |
31 |
32 |
35 |
36 |
37 |
38 | );
39 | };
40 |
41 | const EmailVerificationLinkNotFound = () => {
42 | return (
43 |
44 |
48 |
49 |
52 |
53 |
54 |
55 | );
56 | };
57 |
--------------------------------------------------------------------------------
/src/app/(auth)/welcome/page.tsx:
--------------------------------------------------------------------------------
1 | import { Welcome } from '@/components/auth/welcome';
2 | import APP_PATHS from '@/config/path.config';
3 | import { FormContainer } from '@/layouts/form-container';
4 |
5 | import { cookies } from 'next/headers';
6 | import { redirect } from 'next/navigation';
7 | import { PENDING_EMAIL_VERIFICATION_USER_ID } from '@/config/auth.config';
8 |
9 | const WelcomePage = () => {
10 | const unverifiedUserId = cookies().get(PENDING_EMAIL_VERIFICATION_USER_ID);
11 | if (!unverifiedUserId) redirect(APP_PATHS.SIGNIN);
12 |
13 | return (
14 |
15 |
21 |
22 |
23 |
24 | );
25 | };
26 |
27 | export default WelcomePage;
28 |
--------------------------------------------------------------------------------
/src/app/[...404]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import Link from 'next/link';
4 | import { Poppins } from 'next/font/google';
5 | import { Home, AlertTriangle } from 'lucide-react';
6 | import { Button } from '@/components/ui/button';
7 | import { motion } from 'framer-motion';
8 |
9 | // Applying font from Google
10 | const fontOptions = Poppins({
11 | subsets: ['latin'],
12 | variable: '--font-poppins',
13 | weight: ['400', '700'],
14 | });
15 |
16 | const Custom404Page = () => {
17 | return (
18 |
21 |
27 |
31 |
32 |
33 |
34 | 404 - Page Not Found
35 |
36 |
37 | We are sorry, but we could not find the page you are looking for.
38 |
39 |
40 | The page may have been moved, removed, renamed, or might never have
41 | existed.
42 |
43 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default Custom404Page;
61 |
--------------------------------------------------------------------------------
/src/app/admin/page.tsx:
--------------------------------------------------------------------------------
1 | import UserCard from '@/components/UserCard';
2 |
3 | const page = async () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default page;
12 |
--------------------------------------------------------------------------------
/src/app/api/auth/[...nextauth]/route.ts:
--------------------------------------------------------------------------------
1 | import { authOptions } from '@/lib/authOptions';
2 | import NextAuth from 'next-auth/next';
3 |
4 | const handler = NextAuth(authOptions);
5 | // export default handler;
6 | export { handler as GET, handler as POST };
7 |
--------------------------------------------------------------------------------
/src/app/create-profile/page.tsx:
--------------------------------------------------------------------------------
1 | import VerticalLinearStepper from '@/components/user-multistep-form/user-multistep-form';
2 | import { authOptions } from '@/lib/authOptions';
3 | import { getServerSession } from 'next-auth';
4 | import { redirect } from 'next/navigation';
5 |
6 | export default async function Home() {
7 | const session = await getServerSession(authOptions);
8 | if (!session || session.user.role !== 'USER') redirect('/');
9 | if (session.user.onBoard === true) redirect('/jobs');
10 | return (
11 |
12 |
13 | Hey {session?.user.name} let's get you set up and started!
14 |
15 |
16 |
17 | );
18 | }
19 |
--------------------------------------------------------------------------------
/src/app/create/page.tsx:
--------------------------------------------------------------------------------
1 | import PostJobForm from '@/components/job-form';
2 | import React from 'react';
3 |
4 | const page = () => {
5 | return (
6 |
7 |
8 |
Post a job
9 |
10 | 100xJobs is trusted by leading companies
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default page;
20 |
--------------------------------------------------------------------------------
/src/app/editDetails/page.tsx:
--------------------------------------------------------------------------------
1 | import BackgroundSvg from '@/components/BackgroundSvg';
2 | import HalfCircleGradient from '@/components/HalfCircleGradient';
3 | import UserDetails from '@/components/userDetails';
4 | import React from 'react';
5 |
6 | const editDetails = () => {
7 | return (
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default editDetails;
20 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/jobs/[id]/loading.tsx:
--------------------------------------------------------------------------------
1 | import JobCardSkeleton from '@/components/JobcardSkeleton';
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
7 |
8 | );
9 | };
10 |
11 | export default Loading;
12 |
--------------------------------------------------------------------------------
/src/app/jobs/[id]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getJobById, getRecommendedJobs } from '@/actions/job.action';
2 | import { Job } from '@/components/job';
3 | import JobCard from '@/components/job-card-rec';
4 | import { JobByIdSchemaType } from '@/lib/validators/jobs.validator';
5 | import { ArrowLeft } from 'lucide-react';
6 | import Link from 'next/link';
7 | import { redirect } from 'next/navigation';
8 |
9 | const page = async ({ params }: { params: JobByIdSchemaType }) => {
10 | const job = await getJobById(params);
11 | if (!job.status) {
12 | return;
13 | }
14 |
15 | const jobDetail = job.additional?.job;
16 | if (!jobDetail) {
17 | return redirect('/jobs');
18 | }
19 |
20 | const curatedJobs = await getRecommendedJobs({
21 | id: jobDetail.id,
22 | category: jobDetail.category,
23 | });
24 |
25 | if (!curatedJobs.status) {
26 | return;
27 | }
28 |
29 | const recommendedJobs = curatedJobs.additional?.jobs;
30 |
31 | return (
32 |
33 |
34 |
38 |
39 | Back to All Jobs
40 |
41 |
42 |
43 | {/* the particular job details */}
44 |
45 |
46 | {/* job recommendations */}
47 |
58 |
59 |
60 | );
61 | };
62 |
63 | export default page;
64 |
--------------------------------------------------------------------------------
/src/app/jobs/loading.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from '@/components/ui/card';
2 | import { Skeleton } from '@/components/ui/skeleton';
3 |
4 | const Loading = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {[1, 2, 3, 4, 5].map((job) => (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {[1, 2, 3, 4, 5, 6].map((skill) => (
36 |
37 | ))}
38 |
39 |
40 |
41 |
42 |
43 | ))}
44 |
45 |
46 |
47 | );
48 | };
49 |
50 | export default Loading;
51 |
--------------------------------------------------------------------------------
/src/app/jobs/page.tsx:
--------------------------------------------------------------------------------
1 | import AllJobs from '@/components/all-jobs';
2 | import Loader from '@/components/loader';
3 | import JobFilters from '@/layouts/job-filters';
4 | import JobsHeader from '@/layouts/jobs-header';
5 | import {
6 | JobQuerySchema,
7 | JobQuerySchemaType,
8 | } from '@/lib/validators/jobs.validator';
9 | import { redirect } from 'next/navigation';
10 | import { Suspense } from 'react';
11 |
12 | const page = async ({ searchParams }: { searchParams: JobQuerySchemaType }) => {
13 | const parsedData = JobQuerySchema.safeParse(searchParams);
14 | if (!(parsedData.success && parsedData.data)) {
15 | console.error(parsedData.error);
16 | redirect('/jobs');
17 | }
18 | const parsedSearchParams = parsedData.data;
19 | return (
20 |
21 |
22 |
Explore Jobs
23 |
24 | Explore thousands of remote and onsite jobs that match your skills and
25 | aspirations.
26 |
27 |
28 |
29 |
30 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 | }
49 | >
50 |
51 |
52 |
53 |
54 |
55 |
56 | );
57 | };
58 |
59 | export default page;
60 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import Footer from '@/layouts/footer';
2 | import Header from '@/layouts/header';
3 | import { cn } from '@/lib/utils';
4 | import Providers from '@/providers/providers';
5 | import type { Metadata } from 'next';
6 | import NextTopLoader from 'nextjs-toploader';
7 | import './globals.css';
8 | import localFont from 'next/font/local';
9 |
10 | const satoshi = localFont({
11 | display: 'swap',
12 | src: [
13 | {
14 | path: '../../public/fonts/satoshi.ttf',
15 | },
16 | ],
17 | variable: '--font-satoshi',
18 | });
19 | export const metadata: Metadata = {
20 | title: '100xJobs',
21 | description: 'Get your dream job',
22 | // icons: '/main.png',
23 | };
24 |
25 | export default async function RootLayout({
26 | children,
27 | }: Readonly<{
28 | children: React.ReactNode;
29 | }>) {
30 | return (
31 |
32 |
38 | {' '}
39 |
40 |
41 |
42 | {children}
43 |
44 |
45 | {/* Commenting this out for temp basis */}
46 | {/* */}
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/app/manage/jobs/page.tsx:
--------------------------------------------------------------------------------
1 | import JobManagement from '@/components/JobManagement';
2 | import { options } from '@/lib/auth';
3 | import {
4 | JobQuerySchema,
5 | JobQuerySchemaType,
6 | } from '@/lib/validators/jobs.validator';
7 | import { getServerSession } from 'next-auth';
8 | import { redirect } from 'next/navigation';
9 | import React from 'react';
10 |
11 | const ManageJob = async ({
12 | searchParams,
13 | }: {
14 | searchParams: JobQuerySchemaType;
15 | }) => {
16 | const parsedData = JobQuerySchema.safeParse(searchParams);
17 | const server = await getServerSession(options);
18 | if (!server?.user) {
19 | redirect('/api/auth/signin');
20 | } else if (server.user.role !== 'ADMIN') {
21 | redirect('/jobs');
22 | }
23 | if (!(parsedData.success && parsedData.data)) {
24 | console.error(parsedData.error);
25 | redirect('/jobs');
26 | }
27 | const searchParamss = parsedData.data;
28 | return ;
29 | };
30 |
31 | export default ManageJob;
32 |
--------------------------------------------------------------------------------
/src/app/manage/recruiters/page.tsx:
--------------------------------------------------------------------------------
1 | import { getUserRecruiters } from '@/actions/user.profile.actions';
2 | import ManageRecruiters from '@/components/ManageRecruiters';
3 |
4 | import { options } from '@/lib/auth';
5 | import { getServerSession } from 'next-auth';
6 | import { redirect } from 'next/navigation';
7 | import React from 'react';
8 |
9 | const RecruitersPage = async () => {
10 | const server = await getServerSession(options);
11 | if (!server?.user) {
12 | redirect('/api/auth/signin');
13 | } else if (server.user.role !== 'ADMIN') {
14 | redirect('/jobs');
15 | }
16 |
17 | const Recruiters = await getUserRecruiters();
18 |
19 | return ;
20 | };
21 |
22 | export default RecruitersPage;
23 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { startCronJob } from '@/actions/corn';
2 | import Faqs from '@/components/Faqs';
3 | import HeroSection from '@/components/hero-section';
4 | import { JobLanding } from '@/components/job-landing';
5 | import Testimonials from '@/components/Testimonials';
6 |
7 | const HomePage = async () => {
8 | startCronJob();
9 | return (
10 |
11 |
12 |
13 |
14 |
15 |
16 | );
17 | };
18 |
19 | export default HomePage;
20 |
--------------------------------------------------------------------------------
/src/app/profile/[userId]/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Skeleton } from '@/components/ui/skeleton';
3 |
4 | const Loading = () => {
5 | return (
6 | <>
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | {Array.from({ length: 12 }).map((value, index) => {
19 | if (index % 2 === 0) {
20 | return ;
21 | } else {
22 | return ;
23 | }
24 | })}
25 | >
26 | );
27 | };
28 |
29 | export default Loading;
30 |
--------------------------------------------------------------------------------
/src/app/profile/[userId]/page.tsx:
--------------------------------------------------------------------------------
1 | import { getUserDetailsWithId } from '@/actions/user.profile.actions';
2 | import Custom404Page from '@/app/[...404]/page';
3 | import ProfileAboutMe from '@/components/profile/AboutMe';
4 | import ProfileEducation from '@/components/profile/ProfileEducation';
5 | import ProfileExperience from '@/components/profile/ProfileExperience';
6 | import ProfileHeroSection from '@/components/profile/ProfileHeroSection';
7 | import ProfileHireme from '@/components/profile/ProfileHireme';
8 | import ProfileProjects from '@/components/profile/ProfileProjects';
9 | import ProfileResume from '@/components/profile/ProfileResume';
10 | import ProfileSkills from '@/components/profile/ProfileSkills';
11 | import { authOptions } from '@/lib/authOptions';
12 | import { getServerSession } from 'next-auth';
13 |
14 | const Page = async ({ params: { userId } }: { params: { userId: string } }) => {
15 | const session = await getServerSession(authOptions);
16 |
17 | const isOwner = session?.user.id === userId;
18 | let userDetails;
19 | const res = await getUserDetailsWithId(userId);
20 | if (res.status) {
21 | userDetails = res.additional;
22 | }
23 |
24 | if (!res.status) {
25 | return ;
26 | }
27 |
28 | return (
29 | <>
30 | {userDetails && (
31 | <>
32 |
33 |
37 |
43 |
44 |
45 |
49 |
53 |
58 | >
59 | )}
60 | >
61 | );
62 | };
63 |
64 | export default Page;
65 |
--------------------------------------------------------------------------------
/src/app/profile/layout.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 |
4 | const ProfileLayout = ({ children }: { children: React.ReactNode }) => {
5 | return (
6 |
7 | {children}
8 |
9 | );
10 | };
11 |
12 | export default ProfileLayout;
13 |
--------------------------------------------------------------------------------
/src/components/ApproveJobDialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import {
4 | Dialog,
5 | DialogClose,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger,
12 | } from './ui/dialog';
13 | import { Button } from './ui/button';
14 |
15 | const ApproveJobDialog = ({
16 | title,
17 | description,
18 | handleClick,
19 | }: {
20 | title: string;
21 | description: string;
22 | handleClick: () => void;
23 | }) => {
24 | return (
25 | <>
26 |
54 | >
55 | );
56 | };
57 |
58 | export default ApproveJobDialog;
59 |
--------------------------------------------------------------------------------
/src/components/BookmarkCardSkeletion.tsx:
--------------------------------------------------------------------------------
1 | import { Card, CardContent } from '@/components/ui/card';
2 | import { Skeleton } from '@/components/ui/skeleton';
3 |
4 | export default function BookmarkCardSkeleton() {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | {[1, 2, 3, 4, 5, 6].map((skill) => (
20 |
21 | ))}
22 |
23 |
24 |
25 |
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/DeleteDialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import { Button } from './ui/button';
4 | import { useToast } from './ui/use-toast';
5 | import { toggleDeleteJobById } from '@/actions/job.action';
6 | import { JobType } from '@/types/jobs.types';
7 | import icons from '@/lib/icons';
8 | import {
9 | Dialog,
10 | DialogTrigger,
11 | DialogContent,
12 | DialogHeader,
13 | DialogTitle,
14 | DialogDescription,
15 | DialogFooter,
16 | } from './ui/dialog';
17 |
18 | const JobDialog = ({ job }: { job: JobType }) => {
19 | const [dialogOpen, setDialogOpen] = useState(false); // State to manage dialog visibility
20 | const { toast } = useToast();
21 |
22 | const handelToggle = async () => {
23 | try {
24 | const result = await toggleDeleteJobById({ id: job.id });
25 | toast({
26 | title: result.message,
27 | variant: 'default',
28 | });
29 | setDialogOpen(false);
30 | } catch (error) {
31 | console.error(error);
32 | toast({ title: 'An Error occurred', variant: 'destructive' });
33 | }
34 | };
35 |
36 | return (
37 |
88 | );
89 | };
90 |
91 | export default JobDialog;
92 |
--------------------------------------------------------------------------------
/src/components/DescriptionEditor.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState, useEffect } from 'react';
3 | import dynamic from 'next/dynamic';
4 | const ReactQuill = dynamic(() => import('react-quill'), { ssr: false });
5 | import 'react-quill/dist/quill.snow.css';
6 |
7 | interface DescriptionEditorProps {
8 | fieldName: string;
9 | initialValue?: string;
10 | onDescriptionChange: (fieldName: string, content: string) => void;
11 | placeholder?: string;
12 | }
13 |
14 | const DescriptionEditor: React.FC = ({
15 | fieldName,
16 | initialValue = '',
17 | onDescriptionChange,
18 | placeholder = '',
19 | }) => {
20 | const [description, setDescription] = useState(initialValue || '');
21 |
22 | useEffect(() => {
23 | setDescription(initialValue || '');
24 | }, [initialValue]);
25 |
26 | const handleChange = (content: string) => {
27 | setDescription(content);
28 | onDescriptionChange(fieldName, content); // Pass the content back to the parent form
29 | };
30 |
31 | const modules = {
32 | toolbar: [
33 | ['bold', 'italic', 'underline'],
34 | [{ header: '1' }, { header: '2' }],
35 | [{ list: 'ordered' }, { list: 'bullet' }],
36 | ['link'],
37 | ],
38 | };
39 |
40 | const formats = [
41 | 'bold',
42 | 'italic',
43 | 'underline',
44 | 'header',
45 | 'list',
46 | 'bullet',
47 | 'link',
48 | ];
49 |
50 | return (
51 |
52 |
63 |
64 | );
65 | };
66 |
67 | export default DescriptionEditor;
68 |
--------------------------------------------------------------------------------
/src/components/FaqsGetintouchCard.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from './ui/button';
2 |
3 | export default function FaqsGetintouchCard() {
4 | return (
5 |
6 |
7 | Can't find what you're looking for?
8 |
9 |
12 |
13 | );
14 | }
15 |
--------------------------------------------------------------------------------
/src/components/HalfCircleGradient.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 |
3 | export default function HalfCircleGradient({ position }: { position: string }) {
4 | return (
5 |
16 | );
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/JobManagement.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { getAllJobs } from '@/actions/job.action';
3 | import JobManagementTable from './JobManagementTable';
4 | import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
5 |
6 | const JobManagement = async ({
7 | searchParams,
8 | }: {
9 | searchParams: JobQuerySchemaType;
10 | }) => {
11 | const jobs = await getAllJobs(searchParams);
12 | if (!jobs.status) {
13 | return Error {jobs.message}
;
14 | }
15 |
16 | return (
17 |
18 |
19 |
20 | );
21 | };
22 | export default JobManagement;
23 |
--------------------------------------------------------------------------------
/src/components/JobManagementHeader.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import Link from 'next/link';
3 | import { Button } from './ui/button';
4 |
5 | const JobManagementHeader = () => {
6 | return (
7 | <>
8 |
21 | >
22 | );
23 | };
24 |
25 | export default JobManagementHeader;
26 |
--------------------------------------------------------------------------------
/src/components/RecentJobs.tsx:
--------------------------------------------------------------------------------
1 | import { getRecentJobs, GetUserBookmarksId } from '@/actions/job.action';
2 | import JobCard from './Jobcard';
3 |
4 | export default async function RecentJobs() {
5 | const [recentJobs, getUserBookmarks] = await Promise.all([
6 | await getRecentJobs(),
7 | await GetUserBookmarksId(),
8 | ]);
9 |
10 | const userbookmarkArr: { jobId: string }[] | null = getUserBookmarks.data;
11 |
12 | if (!recentJobs.status) {
13 | return {recentJobs.message}
;
14 | }
15 |
16 | return (
17 |
18 | {recentJobs.additional.recentJobs.map((job, index) => (
19 | e.jobId === job.id) || false
25 | }
26 | />
27 | ))}
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/ScrollToTop.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Button } from './ui/button';
3 | import { MoveUp } from 'lucide-react';
4 | import { useEffect, useState } from 'react';
5 |
6 | export default function ScrollToTop() {
7 | const [isVisible, setIsVisible] = useState(false);
8 |
9 | useEffect(() => {
10 | const handleScroll = () => {
11 | if (window.scrollY > 200) {
12 | setIsVisible(true);
13 | } else {
14 | setIsVisible(false);
15 | }
16 | };
17 | window.addEventListener('scroll', handleScroll);
18 |
19 | return () => {
20 | window.removeEventListener('scroll', handleScroll);
21 | };
22 | }, []);
23 |
24 | function toUp() {
25 | window.scrollTo({
26 | top: 0,
27 | behavior: 'smooth',
28 | });
29 | }
30 | return (
31 |
40 | );
41 | }
42 |
--------------------------------------------------------------------------------
/src/components/TestimonialCard.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { testimonialItem } from '@/types/testimonials.types';
3 | import { motion } from 'framer-motion';
4 | import { Quote } from 'lucide-react';
5 | import { useTheme } from 'next-themes';
6 |
7 | interface testimonialCardProps {
8 | testimonial: testimonialItem;
9 | }
10 |
11 | export default function TestimonialCard({ testimonial }: testimonialCardProps) {
12 | const { theme } = useTheme();
13 | return (
14 |
19 |
30 |
31 |
32 |
33 |
34 | {testimonial.name.charAt(0).toUpperCase()}
35 |
36 |
37 |
{testimonial.name}
38 |
39 | Talent Acquisition Lead at Spotify
40 |
41 |
42 |
43 |
{testimonial.testimonial}
44 |
45 |
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/src/components/Testimonials.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { InfiniteMovingCards } from './ui/infinite-moving-cards';
4 |
5 | const tweetIds = [
6 | '1837845866289209697',
7 | '1834930679877865784',
8 | '1834096118218801563',
9 | '1832413072960389558',
10 | '1829435530913714557',
11 | '1827998618256544145',
12 | '1826968639049724010',
13 | ];
14 |
15 | const finalTweetIds = [...tweetIds, ...tweetIds];
16 |
17 | export default function Testimonials() {
18 | return (
19 |
23 |
24 |
Testimonials
25 |
26 | Real Success Stories from Job Seekers and Employers
27 |
28 |
29 |
34 |
35 | );
36 | }
37 |
--------------------------------------------------------------------------------
/src/components/ToggleApproveJobButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useState } from 'react';
3 | import {
4 | Dialog,
5 | DialogClose,
6 | DialogContent,
7 | DialogDescription,
8 | DialogFooter,
9 | DialogHeader,
10 | DialogTitle,
11 | DialogTrigger,
12 | } from './ui/dialog';
13 | import { Button } from './ui/button';
14 | import { useToast } from './ui/use-toast';
15 | import { toggleApproveJob } from '@/actions/job.action';
16 | import { JobType } from '@/types/jobs.types';
17 | import { Switch } from './ui/switch';
18 |
19 | const ToggleApproveJobButton = ({ job }: { job: JobType }) => {
20 | const { toast } = useToast();
21 | const [dialogOpen, setDialogOpen] = useState(false);
22 |
23 | const isApproved = job.isVerifiedJob;
24 |
25 | const handleToggleJob = async () => {
26 | try {
27 | const result = await toggleApproveJob({ id: job.id });
28 | if (result.status) {
29 | toast({ title: result.message, variant: 'success' });
30 | } else {
31 | toast({ variant: 'destructive', title: result.message });
32 | }
33 | } catch (error) {
34 | console.error(error);
35 | toast({ title: 'An error occurred' });
36 | } finally {
37 | setDialogOpen(false);
38 | }
39 | };
40 |
41 | return (
42 |
71 | );
72 | };
73 |
74 | export default ToggleApproveJobButton;
75 |
--------------------------------------------------------------------------------
/src/components/Toploader.tsx:
--------------------------------------------------------------------------------
1 | import NextTopLoader from 'nextjs-toploader';
2 |
3 | export default function TopLoader() {
4 | return (
5 | <>
6 |
17 | >
18 | );
19 | }
20 |
--------------------------------------------------------------------------------
/src/components/auth/forgot-password.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Label } from '../ui/label';
3 | import { Input } from '../ui/input';
4 |
5 | import { Button } from '../ui/button';
6 | import { FormEvent, useState } from 'react';
7 | import { useToast } from '../ui/use-toast';
8 | import { forgetPassword } from '@/actions/auth.actions';
9 |
10 | export const ForgotPassword = () => {
11 | const [isLoading, setIsLoading] = useState(false);
12 | const [email, setEmail] = useState('');
13 | const { toast } = useToast();
14 |
15 | const handleSubmit = async (e: FormEvent) => {
16 | e.preventDefault();
17 | setIsLoading(true);
18 |
19 | try {
20 | const res = await forgetPassword({ email });
21 |
22 | toast({
23 | title: res.message,
24 | variant: res.status ? 'success' : 'destructive',
25 | });
26 | } catch {
27 | toast({
28 | title:
29 | "We're sorry for the inconvenience. Please report this issue to our support team",
30 | variant: 'destructive',
31 | });
32 | } finally {
33 | setIsLoading(false);
34 | }
35 | };
36 |
37 | return (
38 |
64 | );
65 | };
66 |
--------------------------------------------------------------------------------
/src/components/auth/social-auth.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 |
5 | import { signIn } from 'next-auth/react';
6 | export const DemarcationLine = () => (
7 |
8 |
9 |
or continue with
10 |
11 |
12 | );
13 |
14 | export const GoogleOauthButton = ({ label }: { label: string }) => (
15 |
40 | );
41 |
--------------------------------------------------------------------------------
/src/components/auth/welcome.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React, { useEffect, useState } from 'react';
3 | import { Button } from '../ui/button';
4 | import APP_PATHS from '@/config/path.config';
5 | import { useRouter } from 'next/navigation';
6 | import { resendVerificationEmail } from '@/actions/auth.actions';
7 | import { EMAIL_VERIFICATION_LINK_RESENT_TIME } from '@/config/auth.config';
8 |
9 | export const Welcome = () => {
10 | const router = useRouter();
11 | return (
12 |
13 |
14 | Didn’t receive the email? Click the button below to resend it.
15 |
16 |
17 |
25 |
26 | );
27 | };
28 |
29 | const CountdownButton = () => {
30 | const [isDisabled, setIsDisabled] = useState(true);
31 | const resentTime = EMAIL_VERIFICATION_LINK_RESENT_TIME;
32 | const [secondsRemaining, setSecondsRemaining] = useState(resentTime);
33 |
34 | // Handler when button is clicked
35 | const handleClick = async () => {
36 | setIsDisabled(true);
37 | setSecondsRemaining(resentTime);
38 | await resendVerificationEmail(null);
39 | };
40 |
41 | // useEffect to handle the countdown logic
42 | useEffect(() => {
43 | if (secondsRemaining === 0 && isDisabled) {
44 | setIsDisabled(false); // Enable the button once the countdown is over
45 | }
46 |
47 | let timer: NodeJS.Timeout;
48 |
49 | if (isDisabled && secondsRemaining > 0) {
50 | timer = setTimeout(() => {
51 | setSecondsRemaining((prev) => prev - 1);
52 | }, 1000); // Decrease the time every second
53 | }
54 |
55 | // Cleanup the timer
56 | return () => clearTimeout(timer);
57 | }, [isDisabled, secondsRemaining]);
58 |
59 | return (
60 |
70 | );
71 | };
72 |
--------------------------------------------------------------------------------
/src/components/gmaps-autosuggest.tsx:
--------------------------------------------------------------------------------
1 | import Script from 'next/script';
2 | import { Input } from './ui/input';
3 | import { useImperativeHandle, useRef } from 'react';
4 |
5 | export type TgmapsAddress = { city: string; fullAddress: string };
6 |
7 | export default function GmapsAutocompleteAddress({
8 | form,
9 | innerRef,
10 | }: {
11 | form: any;
12 | innerRef: any;
13 | }) {
14 | const inputRef = useRef(null);
15 |
16 | useImperativeHandle(innerRef, () => {
17 | return {
18 | reset: () => {
19 | if (inputRef.current) {
20 | inputRef.current.value = '';
21 | form.setValue('city', '');
22 | form.setValue('address', '');
23 | }
24 | },
25 | };
26 | });
27 |
28 | let autocomplete: any = null;
29 |
30 | function onPlaceChanged() {
31 | const { name, formatted_address } = autocomplete.getPlace();
32 | form.setValue('city', name);
33 | form.setValue('address', formatted_address);
34 | }
35 |
36 | function initializeGmaps() {
37 | if ((window as any).google) {
38 | autocomplete = new (window as any).google.maps.places.Autocomplete(
39 | document.getElementById('autocomplete'),
40 | {
41 | types: ['(cities)'],
42 | }
43 | );
44 | autocomplete.addListener('place_changed', onPlaceChanged);
45 | }
46 | }
47 | return (
48 | <>
49 |
54 |
55 |
62 | >
63 | );
64 | }
65 |
--------------------------------------------------------------------------------
/src/components/infinitescroll.tsx:
--------------------------------------------------------------------------------
1 | import Image from 'next/image';
2 | import Marquee from './ui/Marquee';
3 |
4 | const logos = [
5 | { src: '/microsoft.svg', alt: 'Microsoft', width: 190, height: 50 },
6 | { src: '/google.svg', alt: 'Google', width: 140, height: 25 },
7 | { src: '/solana.svg', alt: 'Solana', width: 180, height: 50 },
8 | // Add more logos here
9 | ];
10 |
11 | export function LogoMarquee() {
12 | return (
13 |
14 |
15 |
27 |
28 |
29 |
30 |
31 | );
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/job-card-rec.tsx:
--------------------------------------------------------------------------------
1 | import { JobType } from '@/types/jobs.types';
2 | import React from 'react';
3 | import Image from 'next/image';
4 | import { MapPin } from 'lucide-react';
5 | import Link from 'next/link';
6 | import _ from 'lodash';
7 |
8 | export default function JobCard({ job }: { job: JobType }) {
9 | return (
10 |
11 |
12 |
13 |
14 | {job.companyLogo && (
15 |
22 | )}
23 |
24 |
25 |
{job.title}
26 |
27 | {job.companyName}•
28 | {'Posted on ' + job.postedAt.toDateString()}
29 |
30 |
31 |
32 |
33 |
34 | {job.type && job?.type.toUpperCase().replace('_', ' ')}
35 |
36 |
37 |
38 |
39 | {job.address} - {_.startCase(job.workMode)}
40 |
41 |
42 |
43 |
44 |
45 | );
46 | }
47 |
--------------------------------------------------------------------------------
/src/components/job-creation-success.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from './ui/button';
3 |
4 | const JobCreateSuccess = ({ isVerifiedJob }: { isVerifiedJob: boolean }) => {
5 | const message = isVerifiedJob
6 | ? 'Thank you for posting a job with us.'
7 | : ' Your job will be visible to the public after admin approval.';
8 | return (
9 |
10 |
Job created successfully!
11 |
{message}
12 |
13 |
14 | );
15 | };
16 |
17 | export default JobCreateSuccess;
18 |
--------------------------------------------------------------------------------
/src/components/job-landing.tsx:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs';
2 | import relativeTime from 'dayjs/plugin/relativeTime';
3 | import { ChevronRight } from 'lucide-react';
4 | import RecentJobs from './RecentJobs';
5 | import Link from 'next/link';
6 | dayjs.extend(relativeTime);
7 |
8 | export const calculateTimeSincePosted = (postedAt: Date): string => {
9 | return dayjs(postedAt).fromNow();
10 | };
11 |
12 | export const getFirstLetterCaps = (str: string): string => {
13 | return str.charAt(0).toUpperCase();
14 | };
15 |
16 | export const JobLanding = () => {
17 | return (
18 |
22 |
23 |
Recently Added jobs
24 |
25 | Stay ahead with newly added jobs
26 |
27 |
28 |
29 |
30 |
39 |
40 |
41 | );
42 | };
43 |
--------------------------------------------------------------------------------
/src/components/job-skill.tsx:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/src/components/job-skill.tsx
--------------------------------------------------------------------------------
/src/components/job-skills.tsx:
--------------------------------------------------------------------------------
1 | import { JobType } from '@/types/jobs.types';
2 | import icons from '@/lib/icons';
3 | type JobSkillsProps = Pick;
4 | export const JobSkills = ({ skills }: JobSkillsProps) => {
5 | const maxSkills = 6;
6 |
7 | if (skills.length === 0) {
8 | return ;
9 | }
10 | const displayedSkills = skills.slice(0, maxSkills);
11 | const remainingSkillsCount = skills.length - maxSkills;
12 | return (
13 |
14 | {displayedSkills.map((skill, index) => (
15 |
16 | ))}
17 | {remainingSkillsCount > 0 && (
18 |
19 | +{remainingSkillsCount} more
20 |
21 | )}
22 |
23 | );
24 | };
25 | const NoSkills = () => (
26 |
27 | No skills provided
28 |
29 | );
30 | const SkillTag = ({ skill }: { skill: string }) => (
31 |
32 | {skill}
33 |
34 | );
35 |
--------------------------------------------------------------------------------
/src/components/loader.tsx:
--------------------------------------------------------------------------------
1 | interface LoaderParams {
2 | size?: number;
3 | }
4 |
5 | const Loader = ({ size = 8 }: LoaderParams) => {
6 | return (
7 | <>
8 |
24 | >
25 | );
26 | };
27 |
28 | export default Loader;
29 |
--------------------------------------------------------------------------------
/src/components/loading-spinner.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | export const LoadingSpinner = () => {
4 | return (
5 |
8 | );
9 | };
10 |
--------------------------------------------------------------------------------
/src/components/navitem.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { useSession } from 'next-auth/react';
3 | import Link from 'next/link';
4 | import { usePathname } from 'next/navigation';
5 |
6 | export const NavItem = ({
7 | path,
8 | label,
9 | roleRequired,
10 | isPrivate,
11 | }: {
12 | path: string;
13 | label: string;
14 | roleRequired?: string[];
15 | isPrivate?: boolean;
16 | }) => {
17 | const session = useSession();
18 | const pathname = usePathname();
19 | if (session.status === 'loading') {
20 | return;
21 | }
22 | if (!session.data?.user && isPrivate) {
23 | return;
24 | }
25 | if (
26 | session &&
27 | roleRequired &&
28 | session.data?.user.role &&
29 | !roleRequired.includes(session.data?.user.role)
30 | ) {
31 | return;
32 | }
33 |
34 | return (
35 |
36 |
41 | {label}
42 |
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/src/components/pagination-client.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { PaginationNext, PaginationPrevious } from './ui/pagination';
3 | import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
4 | import useSetQueryParams from '@/hooks/useSetQueryParams';
5 |
6 | const PAGE_INCREMENT = 1;
7 | const PaginationPreviousButton = ({
8 | currentPage,
9 | }: {
10 | searchParams: JobQuerySchemaType;
11 | currentPage: number;
12 | baseUrl: string;
13 | }) => {
14 | const setQueryParams = useSetQueryParams();
15 | return (
16 |
18 | setQueryParams({
19 | page: (currentPage - PAGE_INCREMENT).toString(),
20 | })
21 | }
22 | className=" border dark:bg-slate-400 dark:bg-opacity-5 dark:text-white text-black bg-slate-600 bg-opacity-15 "
23 | aria-disabled={currentPage - PAGE_INCREMENT < PAGE_INCREMENT}
24 | role="button"
25 | />
26 | );
27 | };
28 | const PaginationNextButton = ({
29 | currentPage,
30 | totalPages,
31 | }: {
32 | searchParams: JobQuerySchemaType;
33 | currentPage: number;
34 | totalPages: number;
35 | baseUrl: string;
36 | }) => {
37 | const setQueryParams = useSetQueryParams();
38 | return (
39 |
42 | setQueryParams({
43 | page: (currentPage + PAGE_INCREMENT).toString(),
44 | })
45 | }
46 | className=" border dark:bg-slate-400 dark:bg-opacity-5 dark:text-white text-black bg-slate-600 bg-opacity-15"
47 | aria-disabled={currentPage > totalPages - PAGE_INCREMENT}
48 | />
49 | );
50 | };
51 | export { PaginationPreviousButton, PaginationNextButton };
52 |
--------------------------------------------------------------------------------
/src/components/password-input.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { Input } from './ui/input';
3 | import { EyeIcon, EyeOffIcon } from 'lucide-react';
4 |
5 | interface PasswordInputProps {
6 | placeholder?: string;
7 | field: any;
8 | }
9 |
10 | export const PasswordInput = ({ placeholder, field }: PasswordInputProps) => {
11 | const [showPassword, setShowPassword] = useState(false);
12 |
13 | const togglePasswordVisibility = () => {
14 | setShowPassword(!showPassword);
15 | };
16 |
17 | return (
18 |
19 |
24 |
32 |
33 | );
34 | };
35 |
--------------------------------------------------------------------------------
/src/components/profile-menu.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { Button } from '@/components/ui/button';
4 | import {
5 | DropdownMenu,
6 | DropdownMenuContent,
7 | DropdownMenuItem,
8 | DropdownMenuTrigger,
9 | } from '@/components/ui/dropdown-menu';
10 | import { signOut } from 'next-auth/react';
11 | import Link from 'next/link';
12 | import Icon from './ui/icon';
13 | import { useToast } from '@/components/ui/use-toast'; // Add this import
14 | import APP_PATHS from '@/config/path.config';
15 | import { useRouter } from 'next/navigation';
16 |
17 | export function ProfileMenu() {
18 | const router = useRouter();
19 | const { toast } = useToast(); // Add this line
20 |
21 | const handleSignout = async () => {
22 | try {
23 | const res = await signOut({
24 | redirect: true,
25 | callbackUrl: APP_PATHS.HOME,
26 | });
27 | if (res) {
28 | return toast({
29 | title: 'Something went wrong',
30 | variant: 'destructive',
31 | });
32 | }
33 | toast({
34 | title: 'Logout successful!',
35 | variant: 'default', // Change this to match your toaster's variants
36 | });
37 | const redirect = APP_PATHS.HOME;
38 | router.push(redirect);
39 | } catch (_error) {
40 | return toast({
41 | title: 'Internal server error',
42 | variant: 'destructive',
43 | });
44 | }
45 | };
46 |
47 | const handleManageProfile = () => {
48 | toast({
49 | title: 'Coming soon!',
50 | description: 'This feature is not yet available.',
51 | duration: 2000,
52 | });
53 | };
54 |
55 | return (
56 |
57 |
58 |
67 |
68 |
69 |
70 | Manage Profile
71 |
72 |
73 | Create Job
74 |
75 |
76 | Setting
77 |
78 |
79 |
80 | Logout
81 |
85 |
86 |
87 |
88 |
89 | );
90 | }
91 |
--------------------------------------------------------------------------------
/src/components/profile/AboutMe.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { SquareUserRound, Pencil } from 'lucide-react';
3 | import React, { useState } from 'react';
4 | import { Button } from '@/components/ui/button';
5 | import SheetWrapper from './sheets/SheetWrapper';
6 | import { SHEETS } from '@/lib/constant/profile.constant';
7 | import ProfileEmptyContainers from './emptycontainers/ProfileEmptyContainers';
8 | import AboutMeForm from './forms/ReadMeForm';
9 |
10 | const ProfileAboutMe = ({
11 | aboutMe,
12 | isOwner,
13 | }: {
14 | aboutMe: string;
15 | isOwner: boolean;
16 | }) => {
17 | const [isSheetOpen, setIsSheetOpen] = useState(false);
18 |
19 | const title =
20 | aboutMe.length === 0
21 | ? SHEETS.aboutMe.title
22 | : SHEETS.aboutMe.title.replace('Add', 'Edit');
23 |
24 | const handleClose = () => {
25 | setIsSheetOpen(false);
26 | };
27 | const handleOpen = () => {
28 | setIsSheetOpen(true);
29 | };
30 |
31 | return (
32 | <>
33 |
34 |
About Me
35 | {isOwner && (
36 |
43 | )}
44 |
45 | {!aboutMe && (
46 |
60 | )}
61 | {aboutMe && (
62 |
65 | )}
66 | {isOwner && (
67 |
73 |
74 |
75 | )}
76 | >
77 | );
78 | };
79 |
80 | export default ProfileAboutMe;
81 |
--------------------------------------------------------------------------------
/src/components/profile/AccountSettings.tsx:
--------------------------------------------------------------------------------
1 | import { ChangePassword } from './ChangePassword';
2 |
3 | type Props = {};
4 | export const AccountSettings = ({}: Props) => {
5 | return (
6 |
7 |
8 |
Password and Authentication
9 |
10 |
11 |
12 | );
13 | };
14 |
--------------------------------------------------------------------------------
/src/components/profile/EducationDeleteDialog.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { deleteEducation } from '@/actions/user.profile.actions';
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from '@/components/ui/alert-dialog';
14 | import { Button } from '@/components/ui/button';
15 | import { Trash2 } from 'lucide-react';
16 | import { useToast } from '../ui/use-toast';
17 |
18 | export function EducationDeleteDialog({
19 | educationId,
20 | }: {
21 | educationId: number;
22 | }) {
23 | const { toast } = useToast();
24 | const [isOpen, setIsOpen] = useState(false);
25 | const [isLoading, setIsLoading] = useState(false);
26 |
27 | const handleContinueClick = async () => {
28 | try {
29 | setIsLoading(true);
30 | const response = await deleteEducation(educationId);
31 |
32 | if (!response.status) {
33 | toast({
34 | title: response.message || 'Error',
35 | variant: 'destructive',
36 | });
37 | return;
38 | }
39 |
40 | toast({
41 | title: response.message,
42 | variant: 'success',
43 | });
44 |
45 | setIsOpen(false);
46 | } catch (_error) {
47 | toast({
48 | title: 'Something went wrong while deleting the resume',
49 | description: 'Internal server error',
50 | variant: 'destructive',
51 | });
52 | } finally {
53 | setIsLoading(false);
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 |
66 |
67 |
68 |
69 | Are you absolutely sure?
70 |
71 | This action cannot be undone. This will delete your Education.
72 |
73 |
74 |
75 | setIsOpen(false)}>
76 | Cancel
77 |
78 |
79 | {isLoading ? 'Please wait...' : 'Continue'}
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/profile/ExperienceDeleteDialog.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { deleteExperience } from '@/actions/user.profile.actions';
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from '@/components/ui/alert-dialog';
14 | import { Button } from '@/components/ui/button';
15 | import { Trash2 } from 'lucide-react';
16 | import { useToast } from '../ui/use-toast';
17 |
18 | export function ExperienceDeleteDialog({
19 | experienceId,
20 | }: {
21 | experienceId: number;
22 | }) {
23 | const { toast } = useToast();
24 | const [isOpen, setIsOpen] = useState(false);
25 | const [isLoading, setIsLoading] = useState(false);
26 |
27 | const handleContinueClick = async () => {
28 | try {
29 | setIsLoading(true);
30 | const response = await deleteExperience(experienceId);
31 |
32 | if (!response.status) {
33 | toast({
34 | title: response.message || 'Error',
35 | variant: 'destructive',
36 | });
37 | return;
38 | }
39 |
40 | toast({
41 | title: response.message,
42 | variant: 'success',
43 | });
44 |
45 | setIsOpen(false);
46 | } catch (_error) {
47 | toast({
48 | title: 'Something went wrong while deleting the resume',
49 | description: 'Internal server error',
50 | variant: 'destructive',
51 | });
52 | } finally {
53 | setIsLoading(false);
54 | }
55 | };
56 |
57 | return (
58 |
59 |
60 |
66 |
67 |
68 |
69 | Are you absolutely sure?
70 |
71 | This action cannot be undone. This will delete your Experience.
72 |
73 |
74 |
75 | setIsOpen(false)}>
76 | Cancel
77 |
78 |
79 | {isLoading ? 'Please wait...' : 'Continue'}
80 |
81 |
82 |
83 |
84 | );
85 | }
86 |
--------------------------------------------------------------------------------
/src/components/profile/ProfileHireme.tsx:
--------------------------------------------------------------------------------
1 | import { ArrowRight, Mail } from 'lucide-react';
2 | import React from 'react';
3 | import Link from 'next/link';
4 |
5 | const ProfileHireme = ({
6 | contactEmail,
7 | email,
8 | resume,
9 | }: {
10 | contactEmail: string;
11 | email: string;
12 | resume: string;
13 | }) => {
14 | return (
15 | <>
16 |
17 |
18 |
19 | Hire Me, Let’s Make Magic Happen!
20 |
21 |
22 | Searching for talent that can drive success? I’m ready to contribute
23 | to your goals!
24 |
25 |
26 |
27 |
31 |
Contact Me
32 |
33 | {resume && (
34 |
39 | View Resume
40 |
41 |
42 | )}
43 |
44 |
45 | >
46 | );
47 | };
48 |
49 | export default ProfileHireme;
50 |
--------------------------------------------------------------------------------
/src/components/profile/ProfileInfo.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React, { useEffect } from 'react';
4 |
5 | import { useSession } from 'next-auth/react';
6 | import { useRouter } from 'next/navigation';
7 |
8 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
9 |
10 | import { Label } from '@/components/ui/label';
11 | import { Input } from '@/components/ui/input';
12 |
13 | import APP_PATHS from '@/config/path.config';
14 |
15 | import { getNameInitials } from '@/lib/utils';
16 |
17 | export const ProfileInfo = () => {
18 | const router = useRouter();
19 | const session = useSession();
20 |
21 | useEffect(() => {
22 | if (session.status !== 'loading' && session.status === 'unauthenticated')
23 | router.push(`${APP_PATHS.SIGNIN}?redirectTo=/profile`);
24 | }, [session.status]);
25 |
26 | return (
27 |
28 |
29 |
30 |
31 |
32 | {getNameInitials(session.data?.user.name ?? '')}
33 |
34 |
35 |
36 |
37 |
38 | Profile Info
39 |
40 |
41 |
42 |
47 |
48 |
49 |
50 |
55 |
56 |
57 |
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/src/components/profile/ProfileShare.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Button } from '@/components/ui/button';
3 | import {
4 | Dialog,
5 | DialogContent,
6 | DialogHeader,
7 | DialogTitle,
8 | DialogTrigger,
9 | } from '@/components/ui/dialog';
10 | import { Twitter, Linkedin, Share2, Copy } from 'lucide-react';
11 | import { useToast } from '../ui/use-toast';
12 |
13 | interface ShareOption {
14 | name: string;
15 | icon: React.ReactNode;
16 | shareFunction: () => void;
17 | }
18 |
19 | export const ProfileShareDialog = () => {
20 | const { toast } = useToast();
21 |
22 | const shareOptions: ShareOption[] = [
23 | {
24 | name: 'Twitter',
25 | icon: ,
26 | shareFunction: () => {
27 | const text = encodeURIComponent(
28 | `Check out my new profile at 100xdevs Job-Board: ${window.location.href}`
29 | );
30 | window.open(`https://twitter.com/intent/tweet?text=${text}`, '_blank');
31 | },
32 | },
33 | {
34 | name: 'LinkedIn',
35 | icon: ,
36 | shareFunction: () => {
37 | const url = encodeURIComponent(window.location.href);
38 | const title = encodeURIComponent('My New Profile');
39 | const summary = encodeURIComponent(
40 | `Excited to share my new profile on 100xdevs Job-Board! Check it out here: ${url} #JobSearch #Hiring #OpenToWork`
41 | );
42 | window.open(
43 | `https://www.linkedin.com/sharing/share-offsite/?url=${url}&title=${title}&summary=${summary}`,
44 | '_blank'
45 | );
46 | },
47 | },
48 | {
49 | name: 'Copy',
50 | icon: ,
51 | shareFunction: () => {
52 | window.navigator.clipboard.writeText(window.location.href);
53 | toast({
54 | variant: 'success',
55 | description: 'Successfully copied the Profile Url.',
56 | });
57 | },
58 | },
59 | ];
60 |
61 | return (
62 |
87 | );
88 | };
89 |
--------------------------------------------------------------------------------
/src/components/profile/ProfileSkills.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { Info, Pencil } from 'lucide-react';
3 | import React, { useState } from 'react';
4 | import { Button } from '../ui/button';
5 | import SheetWrapper from './sheets/SheetWrapper';
6 | import { SHEETS } from '@/lib/constant/profile.constant';
7 | import { SkillsForm } from './forms/SkillsForm';
8 | import ProfileEmptyContainers from './emptycontainers/ProfileEmptyContainers';
9 |
10 | const ProfileSkills = ({
11 | isOwner,
12 | skills,
13 | }: {
14 | isOwner: boolean;
15 | skills: string[];
16 | }) => {
17 | const [isSheetOpen, setIsSheetOpen] = useState(false);
18 |
19 | const handleClose = () => {
20 | setIsSheetOpen(false);
21 | };
22 | const handleOpen = () => {
23 | setIsSheetOpen(true);
24 | };
25 | return (
26 | <>
27 |
28 |
Skills
29 | {isOwner && (
30 |
37 | )}
38 |
39 |
40 | {skills.length === 0 && (
41 |
55 | )}
56 | {skills.length !== 0 && (
57 |
58 | {skills.map((title) => {
59 | return (
60 |
64 | {title}
65 |
66 | );
67 | })}
68 |
69 | )}
70 | {isOwner && (
71 |
77 |
78 |
79 | )}
80 | >
81 | );
82 | };
83 |
84 | export default ProfileSkills;
85 |
--------------------------------------------------------------------------------
/src/components/profile/UserResume.tsx:
--------------------------------------------------------------------------------
1 | import { File } from 'lucide-react';
2 | import Link from 'next/link';
3 | import { useEffect, useState } from 'react';
4 |
5 | export function UserResume() {
6 | const [resumeLink, setResumeLink] = useState(null);
7 |
8 | useEffect(() => {
9 | const storedResume = localStorage.getItem('resume');
10 | if (storedResume) setResumeLink(JSON.parse(storedResume));
11 | }, []);
12 |
13 | if (!resumeLink) {
14 | return null;
15 | }
16 |
17 | return (
18 |
19 |
25 |
26 |
Click here
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/components/profile/UserSkills.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react';
2 |
3 | export function UserSkills() {
4 | const [skills, setSkills] = useState();
5 | useEffect(() => {
6 | const storedSkills = localStorage.getItem('skills');
7 | if (storedSkills) {
8 | setSkills(JSON.parse(storedSkills));
9 | }
10 | }, []);
11 |
12 | if (!skills) {
13 | return null;
14 | }
15 |
16 | return (
17 |
18 | {skills.map((item, index) => (
19 |
23 | {item}
24 |
25 | ))}
26 |
27 | );
28 | }
29 |
--------------------------------------------------------------------------------
/src/components/profile/emptycontainers/ProfileEmptyContainers.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from '@/components/ui/button';
2 | import React from 'react';
3 |
4 | const ProfileEmptyContainers = ({
5 | title,
6 | isOwner,
7 | Icon,
8 | description,
9 | handleClick,
10 | buttonText,
11 | }: {
12 | title: string;
13 | Icon: React.ElementType;
14 | description: string;
15 | handleClick: () => void;
16 | buttonText: string;
17 | isOwner: boolean;
18 | }) => {
19 | return (
20 |
21 |
26 |
27 |
{title}
28 |
29 | {description}
30 |
31 |
32 | {isOwner && (
33 |
36 | )}
37 |
38 | );
39 | };
40 |
41 | export default ProfileEmptyContainers;
42 |
--------------------------------------------------------------------------------
/src/components/profile/projectDeleteDialog.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { deleteProject } from '@/actions/user.profile.actions';
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from '@/components/ui/alert-dialog';
14 | import { Button } from '@/components/ui/button';
15 | import { Trash2 } from 'lucide-react';
16 | import { useToast } from '../ui/use-toast';
17 |
18 | export function ProjectDeleteDialog({ projectId }: { projectId: number }) {
19 | const { toast } = useToast();
20 | const [isOpen, setIsOpen] = useState(false);
21 | const [isLoading, setIsLoading] = useState(false);
22 |
23 | const handleContinueClick = async () => {
24 | try {
25 | setIsLoading(true);
26 | const response = await deleteProject(projectId);
27 |
28 | if (!response.status) {
29 | toast({
30 | title: response.message || 'Error',
31 | variant: 'destructive',
32 | });
33 | return;
34 | }
35 |
36 | toast({
37 | title: response.message,
38 | variant: 'success',
39 | });
40 |
41 | setIsOpen(false);
42 | } catch (_error) {
43 | toast({
44 | title: 'Something went wrong while deleting the resume',
45 | description: 'Internal server error',
46 | variant: 'destructive',
47 | });
48 | } finally {
49 | setIsLoading(false);
50 | }
51 | };
52 |
53 | return (
54 |
55 |
56 |
62 |
63 |
64 |
65 | Are you absolutely sure?
66 |
67 | This action cannot be undone. This will delete your Project.
68 |
69 |
70 |
71 | setIsOpen(false)}>
72 | Cancel
73 |
74 |
75 | {isLoading ? 'Please wait...' : 'Continue'}
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/profile/resumeDeleteDialog.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { deleteResume } from '@/actions/user.profile.actions';
3 | import {
4 | AlertDialog,
5 | AlertDialogAction,
6 | AlertDialogCancel,
7 | AlertDialogContent,
8 | AlertDialogDescription,
9 | AlertDialogFooter,
10 | AlertDialogHeader,
11 | AlertDialogTitle,
12 | AlertDialogTrigger,
13 | } from '@/components/ui/alert-dialog';
14 | import { Button } from '@/components/ui/button';
15 | import { Trash2 } from 'lucide-react';
16 | import { useToast } from '../ui/use-toast';
17 |
18 | export function ResumeDeleteDialog() {
19 | const { toast } = useToast();
20 | const [isOpen, setIsOpen] = useState(false);
21 | const [isLoading, setIsLoading] = useState(false);
22 |
23 | const handleContinueClick = async () => {
24 | try {
25 | setIsLoading(true);
26 | const response = await deleteResume();
27 |
28 | if (!response.status) {
29 | toast({
30 | title: response.message || 'Error',
31 | variant: 'destructive',
32 | });
33 | return;
34 | }
35 |
36 | toast({
37 | title: response.message,
38 | variant: 'success',
39 | });
40 |
41 | setIsOpen(false);
42 | } catch (_error) {
43 | toast({
44 | title: 'Something went wrong while deleting the resume',
45 | description: 'Internal server error',
46 | variant: 'destructive',
47 | });
48 | } finally {
49 | setIsLoading(false);
50 | }
51 | };
52 |
53 | return (
54 |
55 |
56 |
62 |
63 |
64 |
65 | Are you absolutely sure?
66 |
67 | This action cannot be undone. This will delete your Resume.
68 |
69 |
70 |
71 | setIsOpen(false)}>
72 | Cancel
73 |
74 |
75 | {isLoading ? 'Please wait...' : 'Continue'}
76 |
77 |
78 |
79 |
80 | );
81 | }
82 |
--------------------------------------------------------------------------------
/src/components/profile/sheets/SheetWrapper.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Sheet,
3 | SheetContent,
4 | SheetDescription,
5 | SheetHeader,
6 | SheetTitle,
7 | } from '@/components/ui/sheet';
8 | import React from 'react';
9 |
10 | interface SheetWrapperProps {
11 | title: string;
12 | description: string;
13 | isOpen: boolean;
14 | handleClose: () => void;
15 | children: React.ReactNode;
16 | }
17 |
18 | const SheetWrapper: React.FC = ({
19 | isOpen,
20 | handleClose,
21 | children,
22 | title,
23 | description,
24 | }) => {
25 | return (
26 |
27 |
28 |
29 | {title}
30 | {description}
31 |
32 |
33 | {children}
34 |
35 |
36 | );
37 | };
38 |
39 | export default SheetWrapper;
40 |
--------------------------------------------------------------------------------
/src/components/profile/sidebar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useSession } from 'next-auth/react';
4 | import { usePathname } from 'next/navigation';
5 |
6 | import Link from 'next/link';
7 |
8 | import {
9 | Sheet,
10 | SheetClose,
11 | SheetContent,
12 | SheetTrigger,
13 | } from '@/components/ui/sheet';
14 | import { Separator } from '@/components/ui/separator';
15 |
16 | import { userProfileNavbar } from '@/lib/constant/app.constant';
17 | import { MenuIcon, XIcon } from 'lucide-react';
18 |
19 | const Sidebar = () => {
20 | return (
21 |
22 |
23 |
24 |
25 | );
26 | };
27 |
28 | const DesktopSidebar = () => {
29 | return (
30 |
35 | );
36 | };
37 |
38 | const MobileSidebar = () => {
39 | return (
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | const SidebarNavs = () => {
59 | return (
60 |
61 |
62 |
63 | User Settings
64 |
65 |
66 |
67 | {userProfileNavbar.map((nav) => (
68 |
69 | ))}
70 |
71 | );
72 | };
73 |
74 | const NavItem = ({ path, label }: { path: string; label: string }) => {
75 | const session = useSession();
76 | const pathname = usePathname();
77 | if (session.status === 'loading') return;
78 | if (!session.data?.user) return;
79 | if (!session) return;
80 | return (
81 |
86 | {label}
87 |
88 | );
89 | };
90 |
91 | export default Sidebar;
92 |
--------------------------------------------------------------------------------
/src/components/toggleJobButton.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import React from 'react';
3 | import { Button } from './ui/button';
4 | import { useToast } from './ui/use-toast';
5 | import { toggleDeleteJobById } from '@/actions/job.action';
6 | import { JobType } from '@/types/jobs.types';
7 |
8 | const ToggleDelete = ({ job }: { job: JobType }) => {
9 | const { toast } = useToast();
10 | const handelToggle = async () => {
11 | try {
12 | const result = await toggleDeleteJobById({ id: job.id });
13 | if (result.status) {
14 | toast({ title: result.message, variant: 'default' });
15 | } else {
16 | toast({ title: result.message, variant: 'default' });
17 | }
18 | } catch (error) {
19 | console.error(error);
20 | toast({ title: 'An Error occurred', variant: 'destructive' });
21 | }
22 | };
23 | return (
24 | <>
25 |
33 | >
34 | );
35 | };
36 |
37 | export default ToggleDelete;
38 |
--------------------------------------------------------------------------------
/src/components/ui/Marquee.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { cn } from '../../lib/utils';
3 |
4 | interface MarqueeProps {
5 | className?: string;
6 | reverse?: boolean;
7 | pauseOnHover?: boolean;
8 | children?: React.ReactNode;
9 | vertical?: boolean;
10 | repeat?: number;
11 | [key: string]: any;
12 | }
13 |
14 | export default function Marquee({
15 | className,
16 | reverse,
17 | pauseOnHover = false,
18 | children,
19 | vertical = false,
20 | repeat = 4,
21 | ...props
22 | }: MarqueeProps) {
23 | return (
24 |
35 | {Array(repeat)
36 | .fill(0)
37 | .map((_, i) => (
38 |
47 | {children}
48 |
49 | ))}
50 |
51 | );
52 | }
53 |
--------------------------------------------------------------------------------
/src/components/ui/accordion.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AccordionPrimitive from '@radix-ui/react-accordion';
5 | import { ChevronDown } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Accordion = AccordionPrimitive.Root;
10 |
11 | const AccordionItem = React.forwardRef<
12 | React.ElementRef,
13 | React.ComponentPropsWithoutRef
14 | >(({ className, ...props }, ref) => (
15 |
20 | ));
21 | AccordionItem.displayName = 'AccordionItem';
22 |
23 | const AccordionTrigger = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, children, ...props }, ref) => (
27 |
28 | svg]:rotate-180',
32 | className
33 | )}
34 | {...props}
35 | >
36 | {children}
37 |
38 |
39 |
40 | ));
41 | AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
42 |
43 | const AccordionContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, children, ...props }, ref) => (
47 |
52 | {children}
53 |
54 | ));
55 |
56 | AccordionContent.displayName = AccordionPrimitive.Content.displayName;
57 |
58 | export { Accordion, AccordionItem, AccordionTrigger, AccordionContent };
59 |
--------------------------------------------------------------------------------
/src/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as AvatarPrimitive from '@radix-ui/react-avatar';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ));
21 | Avatar.displayName = AvatarPrimitive.Root.displayName;
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ));
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName;
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ));
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
49 |
50 | export { Avatar, AvatarImage, AvatarFallback };
51 |
--------------------------------------------------------------------------------
/src/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { cva, type VariantProps } from 'class-variance-authority';
3 |
4 | import { cn } from '@/lib/utils';
5 |
6 | const badgeVariants = cva(
7 | 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | 'border-transparent bg-primary text-primary-foreground hover:bg-primary/80',
13 | secondary:
14 | 'border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80',
15 | destructive:
16 | 'border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80',
17 | outline: 'text-foreground',
18 | },
19 | },
20 | defaultVariants: {
21 | variant: 'default',
22 | },
23 | }
24 | );
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | );
34 | }
35 |
36 | export { Badge, badgeVariants };
37 |
--------------------------------------------------------------------------------
/src/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 | import { Slot } from '@radix-ui/react-slot';
3 | import { cva, type VariantProps } from 'class-variance-authority';
4 |
5 | import { cn } from '@/lib/utils';
6 |
7 | const buttonVariants = cva(
8 | 'inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50',
9 | {
10 | variants: {
11 | variant: {
12 | default: 'bg-primary text-primary-foreground hover:bg-primary/90',
13 | destructive:
14 | 'bg-destructive text-destructive-foreground hover:bg-destructive/90',
15 | outline:
16 | 'border border-input bg-background hover:bg-accent hover:text-accent-foreground',
17 | secondary:
18 | 'bg-secondary text-secondary-foreground hover:bg-secondary/80',
19 | ghost: 'hover:bg-accent hover:text-accent-foreground',
20 | link: 'text-primary underline-offset-4 hover:underline',
21 | },
22 | size: {
23 | default: 'h-10 px-4 py-2',
24 | sm: 'h-8 rounded-md px-3',
25 | lg: 'h-11 rounded-md px-8',
26 | icon: 'h-10 w-10',
27 | },
28 | },
29 | defaultVariants: {
30 | variant: 'default',
31 | size: 'default',
32 | },
33 | }
34 | );
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean;
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : 'button';
45 | return (
46 |
51 | );
52 | }
53 | );
54 | Button.displayName = 'Button';
55 |
56 | export { Button, buttonVariants };
57 |
--------------------------------------------------------------------------------
/src/components/ui/calendar.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import { ChevronLeft, ChevronRight } from 'lucide-react';
5 | import { DayPicker } from 'react-day-picker';
6 |
7 | import { cn } from '@/lib/utils';
8 | import { buttonVariants } from '@/components/ui/button';
9 |
10 | export type CalendarProps = React.ComponentProps;
11 |
12 | function Calendar({
13 | className,
14 | classNames,
15 | showOutsideDays = true,
16 | ...props
17 | }: CalendarProps) {
18 | return (
19 | ,
58 | IconRight: () => ,
59 | }}
60 | {...props}
61 | />
62 | );
63 | }
64 | Calendar.displayName = 'Calendar';
65 |
66 | export { Calendar };
67 |
--------------------------------------------------------------------------------
/src/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '../../lib/utils';
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ));
18 | Card.displayName = 'Card';
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ));
30 | CardHeader.displayName = 'CardHeader';
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ));
45 | CardTitle.displayName = 'CardTitle';
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ));
57 | CardDescription.displayName = 'CardDescription';
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ));
65 | CardContent.displayName = 'CardContent';
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ));
77 | CardFooter.displayName = 'CardFooter';
78 |
79 | export {
80 | Card,
81 | CardHeader,
82 | CardFooter,
83 | CardTitle,
84 | CardDescription,
85 | CardContent,
86 | };
87 |
--------------------------------------------------------------------------------
/src/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as CheckboxPrimitive from '@radix-ui/react-checkbox';
5 | import { Check } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ));
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName;
29 |
30 | export { Checkbox };
31 |
--------------------------------------------------------------------------------
/src/components/ui/icon.tsx:
--------------------------------------------------------------------------------
1 | import icons from '@/lib/icons';
2 | import React, { FC } from 'react';
3 | import { LucideProps } from 'lucide-react';
4 | export interface IconProps extends LucideProps {
5 | icon: keyof typeof icons;
6 | }
7 | const Icon: FC = ({ icon, ...props }) => {
8 | const Comp = icons[icon];
9 | return ;
10 | };
11 |
12 | export default Icon;
13 |
--------------------------------------------------------------------------------
/src/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | );
21 | }
22 | );
23 | Input.displayName = 'Input';
24 |
25 | export { Input };
26 |
--------------------------------------------------------------------------------
/src/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as LabelPrimitive from '@radix-ui/react-label';
5 | import { cva, type VariantProps } from 'class-variance-authority';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const labelVariants = cva(
10 | 'text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70'
11 | );
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ));
24 | Label.displayName = LabelPrimitive.Root.displayName;
25 |
26 | export { Label };
27 |
--------------------------------------------------------------------------------
/src/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as PopoverPrimitive from '@radix-ui/react-popover';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Popover = PopoverPrimitive.Root;
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger;
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ));
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName;
30 |
31 | export { Popover, PopoverTrigger, PopoverContent };
32 |
--------------------------------------------------------------------------------
/src/components/ui/radio-group.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as RadioGroupPrimitive from '@radix-ui/react-radio-group';
5 | import { Circle } from 'lucide-react';
6 |
7 | import { cn } from '@/lib/utils';
8 |
9 | const RadioGroup = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => {
13 | return (
14 |
19 | );
20 | });
21 | RadioGroup.displayName = RadioGroupPrimitive.Root.displayName;
22 |
23 | const RadioGroupItem = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => {
27 | return (
28 |
36 |
37 |
38 |
39 |
40 | );
41 | });
42 | RadioGroupItem.displayName = RadioGroupPrimitive.Item.displayName;
43 |
44 | export { RadioGroup, RadioGroupItem };
45 |
--------------------------------------------------------------------------------
/src/components/ui/scroll-area.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const ScrollArea = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, children, ...props }, ref) => (
12 |
17 |
18 | {children}
19 |
20 |
21 |
22 |
23 | ));
24 | ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName;
25 |
26 | const ScrollBar = React.forwardRef<
27 | React.ElementRef,
28 | React.ComponentPropsWithoutRef
29 | >(({ className, orientation = 'vertical', ...props }, ref) => (
30 |
43 |
44 |
45 | ));
46 | ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName;
47 |
48 | export { ScrollArea, ScrollBar };
49 |
--------------------------------------------------------------------------------
/src/components/ui/separator.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SeparatorPrimitive from '@radix-ui/react-separator';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Separator = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(
12 | (
13 | { className, orientation = 'horizontal', decorative = true, ...props },
14 | ref
15 | ) => (
16 |
27 | )
28 | );
29 | Separator.displayName = SeparatorPrimitive.Root.displayName;
30 |
31 | export { Separator };
32 |
--------------------------------------------------------------------------------
/src/components/ui/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils';
2 |
3 | function Skeleton({
4 | className,
5 | ...props
6 | }: React.HTMLAttributes) {
7 | return (
8 |
12 | );
13 | }
14 |
15 | export { Skeleton };
16 |
--------------------------------------------------------------------------------
/src/components/ui/slider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SliderPrimitive from '@radix-ui/react-slider';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Slider = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | ));
27 | Slider.displayName = SliderPrimitive.Root.displayName;
28 |
29 | export { Slider };
30 |
--------------------------------------------------------------------------------
/src/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import * as React from 'react';
4 | import * as SwitchPrimitives from '@radix-ui/react-switch';
5 |
6 | import { cn } from '@/lib/utils';
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ));
27 | Switch.displayName = SwitchPrimitives.Root.displayName;
28 |
29 | export { Switch };
30 |
--------------------------------------------------------------------------------
/src/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react';
2 |
3 | import { cn } from '@/lib/utils';
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | );
20 | }
21 | );
22 | Textarea.displayName = 'Textarea';
23 |
24 | export { Textarea };
25 |
--------------------------------------------------------------------------------
/src/components/ui/theme-toggle.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { useTheme } from 'next-themes';
4 |
5 | import { Button } from '@/components/ui/button';
6 | import {
7 | DropdownMenu,
8 | DropdownMenuContent,
9 | DropdownMenuItem,
10 | DropdownMenuTrigger,
11 | } from '@/components/ui/dropdown-menu';
12 | import Icon from './icon';
13 |
14 | export function ModeToggle() {
15 | const { setTheme } = useTheme();
16 |
17 | return (
18 |
19 |
20 |
36 |
37 |
38 | setTheme('light')}>
39 | Light
40 |
41 | setTheme('dark')}>
42 | Dark
43 |
44 | setTheme('system')}>
45 | System
46 |
47 |
48 |
49 | );
50 | }
51 |
--------------------------------------------------------------------------------
/src/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from '@/components/ui/toast';
11 | import { useToast } from '@/components/ui/use-toast';
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast();
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | );
31 | })}
32 |
33 |
34 | );
35 | }
36 |
--------------------------------------------------------------------------------
/src/components/user-multistep-form/add-skills-form.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from 'react';
2 | import { SkillsCombobox } from '../skills-combobox';
3 | import { useForm } from 'react-hook-form';
4 | import { zodResolver } from '@hookform/resolvers/zod';
5 | import { Button } from '../ui/button';
6 | import { Form } from '../ui/form';
7 | import {
8 | addSkillsSchema,
9 | addSkillsSchemaType,
10 | } from '@/lib/validators/user.profile.validator';
11 | import { useToast } from '../ui/use-toast';
12 | import { addUserSkills } from '@/actions/user.profile.actions';
13 | import { LoadingSpinner } from '../loading-spinner';
14 |
15 | export const AddSkills = () => {
16 | const [comboBoxSelectedValues, setComboBoxSelectedValues] = useState<
17 | string[]
18 | >([]);
19 | const [isLoading, setIsLoading] = useState(false);
20 |
21 | const form = useForm({
22 | resolver: zodResolver(addSkillsSchema),
23 | defaultValues: {
24 | skills: [],
25 | },
26 | });
27 | const { toast } = useToast();
28 | const onSubmit = async (data: addSkillsSchemaType) => {
29 | try {
30 | setIsLoading(true);
31 | const response = await addUserSkills(data);
32 | if (!response.status) {
33 | return toast({
34 | title: response.message || 'Error',
35 | variant: 'destructive',
36 | });
37 | }
38 | toast({
39 | title: response.message,
40 | variant: 'success',
41 | });
42 |
43 | form.reset(form.formState.defaultValues);
44 | setComboBoxSelectedValues([]);
45 | } catch (_error) {
46 | toast({
47 | title: 'Something went wrong while Adding Skills',
48 | description: 'Internal server error',
49 | variant: 'destructive',
50 | });
51 | } finally {
52 | setIsLoading(false);
53 | }
54 | };
55 |
56 | return (
57 |
74 |
75 | );
76 | };
77 |
--------------------------------------------------------------------------------
/src/components/userDetails.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Input } from './ui/input';
3 | import { Button } from './ui/button';
4 |
5 | const UserDetails = () => {
6 | return (
7 |
8 |
9 |
10 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 | );
58 | };
59 |
60 | export default UserDetails;
61 |
--------------------------------------------------------------------------------
/src/config/app.config.ts:
--------------------------------------------------------------------------------
1 | export const ADMIN_ROLE = 'ADMIN';
2 | export const HR_ROLE = 'HR';
3 | export const USER_ROLE = 'USER';
4 | export const JOBS_PER_PAGE = 10;
5 | export const DEFAULT_PAGE = 1;
6 |
--------------------------------------------------------------------------------
/src/config/auth.config.ts:
--------------------------------------------------------------------------------
1 | export const AUTH_TOKEN_EXPIRATION_TIME = 30 * 24 * 60 * 60;
2 | export const PASSWORD_HASH_SALT_ROUNDS = 10;
3 |
4 | export const PENDING_EMAIL_VERIFICATION_USER_ID =
5 | 'PENDING_EMAIL_VERIFICATION_USER_ID';
6 | export const EMAIL_VERIFICATION_LINK_EXPIRATION_TIME = 24 * 60 * 60;
7 | export const EMAIL_VERIFICATION_LINK_RESENT_TIME = 0.5 * 60;
8 |
--------------------------------------------------------------------------------
/src/config/error.config.ts:
--------------------------------------------------------------------------------
1 | export const ERROR_NAME = {
2 | UNAUTHORIZED: 'Unauthorized access',
3 | INTERNAL_SERVER_ERROR: 'Internal server error',
4 | BAD_REQUEST: 'Bad request',
5 | NOT_FOUND: 'Resource not found',
6 | FORBIDDEN: 'Access forbidden',
7 | CONFLICT: 'Resource conflict',
8 | UNPROCESSABLE_ENTITY: 'Unprocessable entity',
9 | TOO_MANY_REQUESTS: 'Too many requests',
10 | SERVICE_UNAVAILABLE: 'Service unavailable',
11 | GATEWAY_TIMEOUT: 'Gateway timeout',
12 | VALIDATION_ERROR: 'Validation error',
13 | AUTHENTICATION_FAILED: 'Authentication failed',
14 | INSUFFICIENT_PERMISSIONS: 'Insufficient permissions',
15 | REQUEST_TIMEOUT: 'Request timeout',
16 | UNSUPPORTED_MEDIA_TYPE: 'Unsupported media type',
17 | METHOD_NOT_ALLOWED: 'Method not allowed',
18 | DATABASE_ERROR: 'Database error',
19 | NETWORK_ERROR: 'Network error',
20 | RESOURCE_GONE: 'Resource gone',
21 | PRECONDITION_FAILED: 'Precondition failed',
22 | };
23 | export const ERROR_CODE = {
24 | UNAUTHORIZED: 401,
25 | INTERNAL_SERVER_ERROR: 500,
26 | BAD_REQUEST: 400,
27 | NOT_FOUND: 404,
28 | FORBIDDEN: 403,
29 | CONFLICT: 409,
30 | UNPROCESSABLE_ENTITY: 422,
31 | TOO_MANY_REQUESTS: 429,
32 | SERVICE_UNAVAILABLE: 503,
33 | GATEWAY_TIMEOUT: 504,
34 | VALIDATION_ERROR: 422,
35 | AUTHENTICATION_FAILED: 401,
36 | INSUFFICIENT_PERMISSIONS: 403,
37 | REQUEST_TIMEOUT: 408,
38 | UNSUPPORTED_MEDIA_TYPE: 415,
39 | METHOD_NOT_ALLOWED: 405,
40 | DATABASE_ERROR: 500,
41 | NETWORK_ERROR: 502,
42 | RESOURCE_GONE: 410,
43 | PRECONDITION_FAILED: 412,
44 | };
45 |
--------------------------------------------------------------------------------
/src/config/path.config.ts:
--------------------------------------------------------------------------------
1 | const APP_PATHS = {
2 | HOME: '/',
3 | POST_JOB: '/create',
4 | SIGNIN: '/signin',
5 | SIGNUP: '/signup',
6 | RESET_PASSWORD: '/reset-password',
7 | JOBS: '/jobs',
8 | MANAGE_RECRUITERS: '/manage/recruiters',
9 | MANAGE_JOBS: '/manage/jobs',
10 | CONTACT_US: 'mailto:vineetagarwal.now@gmail.com',
11 | TESTIMONIALS: '#testimonials',
12 | FAQS: '#faq',
13 | VERIFY_EMAIL: '/verify-email',
14 | FORGOT_PASSWORD: '/forgot-password',
15 | WELCOME: '/welcome',
16 | PROFILE: '/profile',
17 | EDIT_PROFILE: '/profile/edit',
18 | ACCOUNT_SETTINGS: '/profile/settings',
19 | BOOKMARK: '/profile/bookmarks',
20 | PROJECTS: '/profile/projects',
21 | RESUME: '/profile/resume',
22 | EXPERIENCE: '/profile/experience',
23 | SKILLS: '/profile/skills',
24 | };
25 | export default APP_PATHS;
26 |
--------------------------------------------------------------------------------
/src/config/prisma.config.ts:
--------------------------------------------------------------------------------
1 | import { PrismaClient } from '@prisma/client';
2 |
3 | const prismaClientSingleton = () => {
4 | return new PrismaClient();
5 | };
6 |
7 | declare const globalThis: {
8 | prismaGlobal: ReturnType;
9 | } & typeof global;
10 |
11 | const prisma = globalThis.prismaGlobal ?? prismaClientSingleton();
12 |
13 | export default prisma;
14 |
15 | if (process.env.NODE_ENV !== 'production') globalThis.prismaGlobal = prisma;
16 |
--------------------------------------------------------------------------------
/src/config/skillapi.auth.token.ts:
--------------------------------------------------------------------------------
1 | export const bearerToken = { value: '' };
2 |
--------------------------------------------------------------------------------
/src/env/client.ts:
--------------------------------------------------------------------------------
1 | // // client-env.ts
2 | // import { createEnv } from '@t3-oss/env-nextjs';
3 | // import { z } from 'zod';
4 |
5 | // export const clientEnv = createEnv({
6 | // client: {
7 | // NEXT_PUBLIC_GOOGLE_MAPS_API_KEY: z.string().min(1),
8 | // },
9 | // runtimeEnv: {
10 | // NEXT_PUBLIC_GOOGLE_MAPS_API_KEY:
11 | // process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY,
12 | // },
13 | // });
14 |
--------------------------------------------------------------------------------
/src/env/server.ts:
--------------------------------------------------------------------------------
1 | // // // server-env.ts
2 | // import { createEnv } from '@t3-oss/env-nextjs';
3 | // import { z } from 'zod';
4 |
5 | // export const serverEnv = createEnv({
6 | // server: {
7 | // DATABASE_URL: z.string().url(),
8 | // NEXTAUTH_SECRET: z.string().min(1),
9 | // NEXTAUTH_URL: z.string().url(),
10 | // CDN_API_KEY: z.string().min(1),
11 | // CDN_BASE_UPLOAD_URL: z.string().url(),
12 | // CDN_BASE_ACCESS_URL: z.string().url(),
13 | // BASE_URL: z.string().url(),
14 | // EMAIL_USER: z.string().min(1),
15 | // EMAIL_PASSWORD: z.string().min(1),
16 | // GOOGLE_CLIENT_ID: z.string().min(1),
17 | // GOOGLE_CLIENT_SECRET: z.string().min(1),
18 | // },
19 | // runtimeEnv: {
20 | // DATABASE_URL: process.env.DATABASE_URL,
21 | // NEXTAUTH_SECRET: process.env.NEXTAUTH_SECRET,
22 | // NEXTAUTH_URL: process.env.NEXTAUTH_URL,
23 | // CDN_API_KEY: process.env.CDN_API_KEY,
24 | // CDN_BASE_UPLOAD_URL: process.env.CDN_BASE_UPLOAD_URL,
25 | // CDN_BASE_ACCESS_URL: process.env.CDN_BASE_ACCESS_URL,
26 | // BASE_URL: process.env.BASE_URL,
27 | // EMAIL_USER: process.env.EMAIL_USER,
28 | // EMAIL_PASSWORD: process.env.EMAIL_PASSWORD,
29 | // GOOGLE_CLIENT_ID: process.env.GOOGLE_CLIENT_ID,
30 | // GOOGLE_CLIENT_SECRET: process.env.GOOGLE_CLIENT_SECRET,
31 | // },
32 | // });
33 |
--------------------------------------------------------------------------------
/src/hooks/sample.ts:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/code100x/job-board/2ed4b46a0d532fb8edab3d02a9fc416f53847f01/src/hooks/sample.ts
--------------------------------------------------------------------------------
/src/hooks/useFilterCheck.ts:
--------------------------------------------------------------------------------
1 | import { useMemo } from 'react';
2 | import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
3 |
4 | export const useFilterCheck = (formValues: JobQuerySchemaType) => {
5 | const isAnyFilterSelected = useMemo(() => {
6 | return ['workmode', 'EmpType', 'salaryrange', 'city'].some((key) => {
7 | const value = formValues[key as keyof JobQuerySchemaType];
8 | if (Array.isArray(value)) {
9 | return value.length > 0;
10 | }
11 | // For non-array values, check if they're truthy
12 | return !!value;
13 | });
14 | }, [formValues]);
15 |
16 | return isAnyFilterSelected;
17 | };
18 |
--------------------------------------------------------------------------------
/src/hooks/useSetQueryParams.ts:
--------------------------------------------------------------------------------
1 | import { usePathname, useRouter, useSearchParams } from 'next/navigation';
2 | import _ from 'lodash';
3 | import { useCallback } from 'react';
4 | import { debounce } from 'lodash';
5 |
6 | //pass in key value pairs to update query params
7 | export default function useSetQueryParams() {
8 | const router = useRouter();
9 | const searchParams = useSearchParams();
10 | const pathName = usePathname();
11 |
12 | const updateQueryParams = useCallback(
13 | debounce((params) => {
14 | const newSearchParams = new URLSearchParams(searchParams?.toString());
15 | for (const [key, value] of Object.entries(params)) {
16 | //isEmpty reads number as empty too
17 | if (_.isEmpty(value) && typeof value !== 'number') {
18 | newSearchParams.delete(key);
19 | } else {
20 | newSearchParams.set(key, String(value));
21 | }
22 | }
23 | router.push(`${pathName}?${newSearchParams}`, { scroll: false });
24 | }, 500), // 300ms debounce
25 | [router, searchParams, pathName]
26 | );
27 |
28 | return updateQueryParams;
29 | }
30 |
--------------------------------------------------------------------------------
/src/layouts/footer.tsx:
--------------------------------------------------------------------------------
1 | import Icon from '@/components/ui/icon';
2 | import { footerItems, socials } from '@/lib/constant/app.constant';
3 | import Link from 'next/link';
4 |
5 | const Footer = () => {
6 | return (
7 |
31 | );
32 | };
33 |
34 | export default Footer;
35 |
--------------------------------------------------------------------------------
/src/layouts/form-container.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react';
2 |
3 | interface FormContainerProps extends React.HTMLAttributes {
4 | heading: ReactNode;
5 | description: ReactNode;
6 | }
7 |
8 | export const FormContainer: React.FC = ({
9 | heading,
10 | description,
11 | children,
12 | }) => {
13 | return (
14 |
15 |
16 |
17 | {typeof heading === 'string' ? (
18 |
{heading}
19 | ) : (
20 | heading
21 | )}
22 | {typeof description === 'string' ? (
23 |
{description}
24 | ) : (
25 | description
26 | )}
27 |
28 | {children}
29 |
30 |
31 | );
32 | };
33 |
--------------------------------------------------------------------------------
/src/lib/admin.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession, Session } from 'next-auth';
2 | import { options } from './auth';
3 | import { ErrorHandler } from './error';
4 | import { withServerActionAsyncCatcher } from './async-catch';
5 |
6 | // Added session also if we want to use ID
7 | type withAdminServerActionType = (
8 | session: Session,
9 | args?: T
10 | ) => Promise;
11 |
12 | export function withAdminServerAction(
13 | serverAction: withAdminServerActionType
14 | ): (args?: T) => Promise {
15 | return withServerActionAsyncCatcher(async (args?: T) => {
16 | const session = await getServerSession(options);
17 | if (!session || session.user.role !== 'ADMIN') {
18 | throw new ErrorHandler(
19 | 'You must be authenticated to access this resource.',
20 | 'UNAUTHORIZED'
21 | );
22 | }
23 | return await serverAction(session, args);
24 | });
25 | }
26 |
--------------------------------------------------------------------------------
/src/lib/async-catch.ts:
--------------------------------------------------------------------------------
1 | import { standardizeApiError } from './error';
2 |
3 | type withServerActionAsyncCatcherType = (args: T) => Promise;
4 |
5 | export function withServerActionAsyncCatcher(
6 | serverAction: withServerActionAsyncCatcherType
7 | ): withServerActionAsyncCatcherType {
8 | return async (args: T): Promise => {
9 | try {
10 | return await serverAction(args);
11 | } catch (error) {
12 | return standardizeApiError(error) as R;
13 | }
14 | };
15 | }
16 |
17 | /**
18 | * Usage example for empty args:
19 | *
20 | * export const serverAction = withServerActionAsyncCatcher(
21 | * async () => {
22 | * return new SuccessResponse('message', 201, 'additional').serialize();
23 | * }
24 | * );
25 | * serverAction(null)
26 | * Usage example for args with a defined type:
27 | *
28 | * export const serverActionWithArgs = withServerActionAsyncCatcher<{ name: string }, ServerActionReturnType>(
29 | * async (data) => {
30 | * return new SuccessResponse('message', 200).serialize();
31 | * }
32 | * );
33 | */
34 |
--------------------------------------------------------------------------------
/src/lib/constant/app.constant.ts:
--------------------------------------------------------------------------------
1 | import { IconProps } from '@/components/ui/icon';
2 | import APP_PATHS from '@/config/path.config';
3 | import { PackageSearch } from 'lucide-react';
4 | import adobe from '../../../public/adobe.svg';
5 | import atlassian from '../../../public/atlassian.svg';
6 | import google from '../../../public/google.svg';
7 | import medium from '../../../public/medium.svg';
8 | import framer from '../../../public/framer.svg';
9 | import coinbase from '../../../public/coinbase.svg';
10 |
11 | export const GITHUB_REPO = 'https://github.com/code100x/job-board';
12 |
13 | export const nonUserNavbar = [
14 | { id: 1, label: 'Explore jobs', path: APP_PATHS.JOBS },
15 | { id: 2, label: 'Contact us', path: APP_PATHS.CONTACT_US },
16 | ];
17 |
18 | export const userNavbar = [
19 | { id: 1, label: 'Explore jobs', path: APP_PATHS.JOBS },
20 | { id: 2, label: 'Contact us', path: APP_PATHS.CONTACT_US },
21 | ];
22 | export const adminNavbar = [
23 | {
24 | id: 1,
25 | label: 'Manage Jobs',
26 | path: APP_PATHS.MANAGE_JOBS,
27 | roleRequired: ['ADMIN', 'HR'],
28 | icon: PackageSearch,
29 | },
30 | {
31 | id: 2,
32 | label: 'Manage Recruiters',
33 | path: APP_PATHS.MANAGE_RECRUITERS,
34 | roleRequired: ['ADMIN'],
35 | icon: PackageSearch,
36 | },
37 | ];
38 | export const userProfileNavbar = [
39 | { id: 1, label: 'My Account', path: APP_PATHS.PROFILE },
40 | { id: 2, label: 'Edit Profile', path: APP_PATHS.EDIT_PROFILE },
41 | { id: 3, label: 'Saved Jobs', path: APP_PATHS.BOOKMARK },
42 | { id: 4, label: 'Account Settings', path: APP_PATHS.ACCOUNT_SETTINGS },
43 | { id: 4, label: 'Experience', path: APP_PATHS.EXPERIENCE },
44 | { id: 5, label: 'Projects', path: APP_PATHS.PROJECTS },
45 | { id: 6, label: 'Skills', path: APP_PATHS.SKILLS },
46 | { id: 7, label: 'Resume', path: APP_PATHS.RESUME },
47 | ];
48 | export const socials: {
49 | href: string;
50 | icon: IconProps['icon'];
51 | }[] = [
52 | {
53 | icon: 'youtube',
54 | href: 'https://www.youtube.com/@100xDevs-n1w',
55 | },
56 | { icon: 'twitter', href: 'https://x.com/100xDevs' },
57 | ];
58 |
59 | export const footerItems = [
60 | {
61 | label: 'About Us',
62 | href: '/',
63 | },
64 | {
65 | label: 'Terms of Service',
66 | href: '/',
67 | },
68 | {
69 | label: 'Privacy Policy',
70 | href: '/',
71 | },
72 | ];
73 |
74 | export const trustedCompanies = [
75 | {
76 | icon: adobe,
77 | name: 'adobe',
78 | },
79 | {
80 | icon: atlassian,
81 | name: 'atlassian',
82 | },
83 | {
84 | icon: medium,
85 | name: 'medium',
86 | },
87 | {
88 | icon: coinbase,
89 | name: 'coinbase',
90 | },
91 | {
92 | icon: framer,
93 | name: 'framer',
94 | },
95 | {
96 | icon: google,
97 | name: 'google',
98 | },
99 | ];
100 |
--------------------------------------------------------------------------------
/src/lib/constant/faqs.constants.ts:
--------------------------------------------------------------------------------
1 | import { faqItem } from '@/types/faqs.types';
2 |
3 | export const faqData: faqItem[] = [
4 | {
5 | question: 'How do I apply for jobs?',
6 | answer:
7 | "Once logged in, search for jobs using keywords or filters. Click on the job title and hit the 'Apply' button, then follow the application instructions.",
8 | },
9 | {
10 | question: 'Can I save jobs to apply for later?',
11 | answer:
12 | "Yes, click the 'Save Job' button on the job listing to save it. You can find saved jobs in your dashboard under 'Saved Jobs.'",
13 | },
14 | {
15 | question: 'Can I receive job alerts?',
16 | answer:
17 | "Yes, set up job alerts by going to 'Job Alerts' in your account, specifying your preferences, and you’ll receive relevant job notifications via email.",
18 | },
19 | {
20 | question: 'Is there a fee to use this job portal?',
21 | answer:
22 | 'No, using the job portal to search and apply for jobs is completely free for job seekers.',
23 | },
24 | {
25 | question: 'Can I delete my account?',
26 | answer:
27 | "Yes, go to 'Account Settings' and select 'Delete Account.' Please note that this action is permanent.",
28 | },
29 | {
30 | question: 'How do I create an account?',
31 | answer:
32 | "Click the 'Sign Up' button on the homepage, fill in your details, and follow the instructions to verify your email.",
33 | },
34 | ];
35 |
--------------------------------------------------------------------------------
/src/lib/constant/jobs.constant.ts:
--------------------------------------------------------------------------------
1 | export enum SortByEnums {
2 | POSTEDAT_ASC = 'postedat_asc',
3 | POSTEDAT_DESC = 'postedat_desc',
4 | MAXSALARY_ASC = 'maxsalary_asc',
5 | MAXSALARY_DESC = 'maxsalary_desc',
6 | }
7 | export const filters = {
8 | salaryRange: [
9 | {
10 | id: 1,
11 | label: '$0-$5k',
12 | value: '0-5000',
13 | },
14 | {
15 | id: 2,
16 | label: '$5-$10k',
17 | value: '5000-10000',
18 | },
19 | {
20 | id: 3,
21 | label: '$10-$30k',
22 | value: '10000-30000',
23 | },
24 | {
25 | id: 4,
26 | label: '$30-$50k',
27 | value: '30000-50000',
28 | },
29 | {
30 | id: 5,
31 | label: '$50k or above',
32 | value: '50000-above',
33 | },
34 | ],
35 | };
36 |
37 | export const jobSorting = [
38 | {
39 | id: 1,
40 | label: 'Latest Jobs',
41 | value: 'postedat_desc',
42 | },
43 | {
44 | id: 2,
45 | label: 'Oldest Jobs',
46 | value: 'postedat_asc',
47 | },
48 | {
49 | id: 3,
50 | label: 'Lowest Salary',
51 | value: 'maxsalary_asc',
52 | },
53 | {
54 | id: 4,
55 | label: 'Highest Salary',
56 | value: 'maxsalary_desc',
57 | },
58 | ];
59 |
--------------------------------------------------------------------------------
/src/lib/constant/profile.constant.ts:
--------------------------------------------------------------------------------
1 | interface SheetDetails {
2 | title: string;
3 | description: string;
4 | }
5 |
6 | type Sheets = Record;
7 |
8 | export const SHEETS: Sheets = {
9 | expeience: {
10 | title: 'Add Work Experience',
11 | description:
12 | 'Share your work history to highlight your career journey and expertise for employers.',
13 | },
14 | aboutMe: {
15 | title: 'Add About Me',
16 | description:
17 | 'Share a brief introduction to let companies know who you are.',
18 | },
19 | editProfile: {
20 | title: 'Update Your Profile',
21 | description:
22 | 'Update your personal information, contact details, and social links to keep your profile current and professional.',
23 | },
24 | resume: {
25 | title: 'Upload Your Resume',
26 | description:
27 | 'Share your resume to give employers a full view of your qualifications and experiences.',
28 | },
29 | skills: {
30 | title: 'Add Your Skills',
31 | description:
32 | 'Showcase your strongest skills to make your profile stand out to recruiters.',
33 | },
34 | project: {
35 | title: 'Add New Project',
36 | description:
37 | 'Highlight key project that demonstrate your technical abilities and innovative problem-solving.',
38 | },
39 | expierence: {
40 | title: 'Add Work Experience',
41 | description:
42 | 'Share your work history to highlight your career journey and expertise for employers.',
43 | },
44 | education: {
45 | title: 'Add Your Education',
46 | description:
47 | 'Provide details about your academic background to showcase your qualifications.',
48 | },
49 | accountSetting: {
50 | title: 'Account Settings',
51 | description:
52 | 'Manage your account preferences, update your password, or delete your account. Keep your profile secure and up-to-date.',
53 | },
54 | };
55 |
--------------------------------------------------------------------------------
/src/lib/constant/testimonials.constants.ts:
--------------------------------------------------------------------------------
1 | import { testimonialItem } from '@/types/testimonials.types';
2 |
3 | export const testimonials: testimonialItem[] = [
4 | {
5 | name: 'Jane Doe',
6 | testimonial:
7 | 'The job board made my job search seamless. I found my dream job in just a few weeks, and the application process was super easy!',
8 | },
9 | {
10 | name: 'John Smith',
11 | testimonial:
12 | 'This platform is incredibly user-friendly and helped me land a great position. The filtering and search options are fantastic!',
13 | },
14 | {
15 | name: 'Emily Johnson',
16 | testimonial:
17 | 'I appreciate how easy it was to track my job applications. The job board helped me stay organized throughout my job search!',
18 | },
19 | {
20 | name: 'Michael Brown',
21 | testimonial:
22 | 'Thanks to the platform, I was able to connect with amazing companies. The job alerts feature made it easy to stay updated on new opportunities.',
23 | },
24 | {
25 | name: 'Sophia Green',
26 | testimonial:
27 | 'The job portal helped me transition to a new career. The interface is intuitive, and I loved the personalized job recommendations!',
28 | },
29 | {
30 | name: 'David Lee',
31 | testimonial:
32 | 'This job board stands out from the rest. It made the job application process smooth and connected me to roles that matched my skills perfectly.',
33 | },
34 | ];
35 |
--------------------------------------------------------------------------------
/src/lib/error.ts:
--------------------------------------------------------------------------------
1 | import { ERROR_CODE, ERROR_NAME } from '@/config/error.config';
2 | import { ZodError } from 'zod';
3 | import { generateErrorMessage } from 'zod-error';
4 | export type ErrorResponseType = {
5 | name: string;
6 | message: string;
7 | code: number;
8 | status: false;
9 | error?: any;
10 | };
11 | class ErrorHandler extends Error {
12 | status: false;
13 | error?: any;
14 | code: number;
15 | constructor(message: string, code: keyof typeof ERROR_CODE, error?: any) {
16 | super(message);
17 | this.status = false;
18 | this.error = error;
19 | this.code = ERROR_CODE[code];
20 | this.name = ERROR_NAME[code];
21 | }
22 | }
23 |
24 | function standardizeApiError(error: unknown): ErrorResponseType {
25 | if (error instanceof ErrorHandler) {
26 | return {
27 | name: error.name,
28 | message: error.message,
29 | code: error.code,
30 | status: false,
31 | error: error.error,
32 | };
33 | }
34 | if (error instanceof ZodError) {
35 | return {
36 | name: error.name,
37 | message: generateErrorMessage(error.issues, {
38 | maxErrors: 2,
39 | delimiter: {
40 | component: ': ',
41 | },
42 | message: {
43 | enabled: true,
44 | label: '',
45 | },
46 | path: {
47 | enabled: false,
48 | },
49 | code: {
50 | enabled: false,
51 | },
52 | }),
53 | code: ERROR_CODE.UNPROCESSABLE_ENTITY,
54 | status: false,
55 | };
56 | }
57 | return {
58 | name: ERROR_NAME.INTERNAL_SERVER_ERROR,
59 | message:
60 | "We're sorry for the inconvenience. Please report this issue to our support team ",
61 | code: ERROR_CODE.INTERNAL_SERVER_ERROR,
62 | status: false,
63 | };
64 | }
65 | export { ErrorHandler, standardizeApiError };
66 |
--------------------------------------------------------------------------------
/src/lib/icons.ts:
--------------------------------------------------------------------------------
1 | import { RiTwitterXFill } from 'react-icons/ri';
2 | import {
3 | FaGithub,
4 | FaInstagram,
5 | FaLinkedin,
6 | FaTelegramPlane,
7 | FaYoutube,
8 | FaSpinner,
9 | FaDiscord,
10 | } from 'react-icons/fa';
11 | import {
12 | ArrowRight,
13 | ChevronDown,
14 | Menu,
15 | Sparkles,
16 | Copyright,
17 | ArchiveRestore,
18 | Sun,
19 | Moon,
20 | type Icon as LucideIconType,
21 | Check,
22 | ChevronRight,
23 | DotIcon,
24 | MapPin,
25 | DollarSign,
26 | BookText,
27 | User,
28 | Trash2,
29 | LogOut,
30 | SlidersHorizontal,
31 | AlertCircle,
32 | } from 'lucide-react';
33 | const icons = {
34 | sparcle: Sparkles,
35 | rightarrow: ArrowRight,
36 | menu: Menu,
37 | dropdown: ChevronDown,
38 | twitter: RiTwitterXFill,
39 | youtube: FaYoutube,
40 | linkedin: FaLinkedin,
41 | github: FaGithub,
42 | discord: FaDiscord,
43 | instagram: FaInstagram,
44 | telergam: FaTelegramPlane,
45 | loading: FaSpinner,
46 | copyright: Copyright,
47 | sun: Sun,
48 | ArchiveRestore: ArchiveRestore,
49 | trash: Trash2,
50 | moon: Moon,
51 | check: Check,
52 | 'chevron-right': ChevronRight,
53 | 'dot-filled': DotIcon,
54 | location: MapPin,
55 | currency: DollarSign,
56 | description: BookText,
57 | profile: User,
58 | logout: LogOut,
59 | filter: SlidersHorizontal,
60 | alert: AlertCircle,
61 | };
62 | export type IconType = typeof LucideIconType;
63 | export default icons;
64 |
--------------------------------------------------------------------------------
/src/lib/sendConfirmationEmail.ts:
--------------------------------------------------------------------------------
1 | import { TokenType } from '@prisma/client';
2 | import nodemailer from 'nodemailer';
3 |
4 | export async function sendConfirmationEmail(
5 | email: string,
6 | confirmationLink: string,
7 | type: TokenType
8 | ) {
9 | try {
10 | const transporter = nodemailer.createTransport({
11 | host: process.env.EMAIL_HOST ?? 'smtp.gmail.com',
12 | port: parseInt(process.env.EMAIL_PORT as string) ?? 587,
13 | auth: {
14 | user: process.env.EMAIL_USER,
15 | pass: process.env.EMAIL_PASSWORD,
16 | },
17 | });
18 |
19 | if (type === 'EMAIL_VERIFICATION') {
20 | const mailOptions = {
21 | from: process.env.NEXTAUTH_URL,
22 | to: email,
23 | subject: 'Confirm your Email',
24 | text: `Click the following link to confirm your email: ${confirmationLink}`,
25 | html: `Click the following link to confirm your email:
Confirm Email`,
26 | };
27 |
28 | await transporter.sendMail(mailOptions);
29 | } else if (type === 'RESET_PASSWORD') {
30 | const mailOptions = {
31 | from: process.env.NEXTAUTH_URL,
32 | to: email,
33 | subject: 'Reset your password',
34 | text: `Click the following link to reset your password: ${confirmationLink}`,
35 | html: `Click the following link to reset your password:
Reset Password`,
36 | };
37 |
38 | await transporter.sendMail(mailOptions);
39 | }
40 | } catch (error) {
41 | console.error('Error sending email:', error);
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/src/lib/session.ts:
--------------------------------------------------------------------------------
1 | import { getServerSession, Session } from 'next-auth';
2 | import { options } from './auth';
3 | import { ErrorHandler } from './error';
4 | import { withServerActionAsyncCatcher } from './async-catch';
5 |
6 | type withSessionType = (session: Session, args?: T) => Promise;
7 |
8 | export function withSession(
9 | serverAction: withSessionType
10 | ): (args?: T) => Promise {
11 | return withServerActionAsyncCatcher(async (args?: T) => {
12 | const session = await getServerSession(options);
13 | if (!session || !session.user) {
14 | throw new ErrorHandler(
15 | 'You must be authenticated to access this resource.',
16 | 'UNAUTHORIZED'
17 | );
18 | }
19 | return await serverAction(session, args);
20 | });
21 | }
22 |
--------------------------------------------------------------------------------
/src/lib/success.ts:
--------------------------------------------------------------------------------
1 | class SuccessResponse {
2 | status: true;
3 | code: number;
4 | additional?: T;
5 | message: string;
6 | constructor(message: string, code: number, additional?: T) {
7 | this.message = message;
8 | this.status = true;
9 | this.code = code;
10 | this.additional = additional;
11 | }
12 | serialize() {
13 | return {
14 | status: this.status,
15 | code: this.code,
16 | message: this.message,
17 | additional: this.additional as T,
18 | };
19 | }
20 | }
21 | export type SuccessResponseType = {
22 | status: true;
23 | code: number;
24 | message: string;
25 | additional?: T;
26 | };
27 | export { SuccessResponse };
28 |
--------------------------------------------------------------------------------
/src/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { uploadFileAction } from '@/actions/upload-to-cdn';
2 | import { EMAIL_VERIFICATION_LINK_EXPIRATION_TIME } from '@/config/auth.config';
3 | import { type ClassValue, clsx } from 'clsx';
4 | import { twMerge } from 'tailwind-merge';
5 |
6 | export function cn(...inputs: ClassValue[]) {
7 | return twMerge(clsx(inputs));
8 | }
9 |
10 | export const getNameInitials = (name: string) => {
11 | const initials = name
12 | .split(' ')
13 | .map((n) => n[0])
14 | .join('');
15 | return initials.toUpperCase();
16 | };
17 | export const formatFilterSearchParams = (params: string[] | string) => {
18 | if (!Array.isArray(params)) {
19 | return [params];
20 | } else {
21 | return params;
22 | }
23 | };
24 |
25 | export const formatSalary = (salary: number) => {
26 | if (salary >= 1000) {
27 | return `${(salary / 1000).toFixed(0)}K`;
28 | }
29 | return salary;
30 | };
31 |
32 | export const isTokenExpiredUtil = (createdAt: Date) => {
33 | const now = new Date().getTime();
34 | const tokenCreationTime = new Date(createdAt).getTime();
35 | return (
36 | now - tokenCreationTime > EMAIL_VERIFICATION_LINK_EXPIRATION_TIME * 1000
37 | );
38 | };
39 |
40 | export const submitImage = async (file: File | null) => {
41 | if (!file) return;
42 |
43 | const formData = new FormData();
44 | formData.append('file', file);
45 |
46 | try {
47 | const uniqueFileName = `${Date.now()}-${file.name}`;
48 | formData.append('uniqueFileName', uniqueFileName);
49 |
50 | const res = await uploadFileAction(formData, 'webp');
51 | if (!res) {
52 | throw new Error('Failed to upload resume');
53 | }
54 |
55 | const uploadRes = res;
56 | return uploadRes.url;
57 | } catch (error) {
58 | console.error('Image upload failed:', error);
59 | }
60 | };
61 |
--------------------------------------------------------------------------------
/src/lib/validators/auth.validator.ts:
--------------------------------------------------------------------------------
1 | import { z } from 'zod';
2 |
3 | export const SigninSchema = z.object({
4 | email: z.string().email('Email is invalid').min(1, 'Email is required'),
5 | password: z.string().min(1, 'Password is required'),
6 | });
7 |
8 | export type SigninSchemaType = z.infer;
9 |
10 | export const SignupSchema = z.object({
11 | name: z.string().min(1, 'Name is required'),
12 | email: z.string().email('Email is invalid').min(1, 'Email is required'),
13 | password: z.string().min(1, 'Password is required'),
14 | });
15 |
16 | export type SignupSchemaType = z.infer;
17 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { getToken } from 'next-auth/jwt';
2 | import { NextResponse } from 'next/server';
3 | import { NextRequest } from 'next/server';
4 |
5 | export async function middleware(req: NextRequest) {
6 | const token = await getToken({ req, secret: process.env.SECRET });
7 | const { pathname } = new URL(req.url);
8 | if (!token && pathname === '/create') {
9 | return NextResponse.redirect(new URL('/signin', req.url));
10 | }
11 | if (
12 | pathname === '/create' &&
13 | token?.role !== 'ADMIN' &&
14 | token?.role !== 'HR'
15 | ) {
16 | return NextResponse.redirect(new URL('/', req.url));
17 | }
18 | if (
19 | pathname !== '/create-profile' &&
20 | token?.role === 'USER' &&
21 | !token.onBoard
22 | ) {
23 | return NextResponse.redirect(new URL('/create-profile', req.url));
24 | }
25 | return NextResponse.next();
26 | }
27 |
28 | export const config = {
29 | matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'],
30 | };
31 |
--------------------------------------------------------------------------------
/src/providers/auth-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import { SessionProvider } from 'next-auth/react';
4 | import React from 'react';
5 |
6 | const AuthProvider = ({ children }: { children: React.ReactNode }) => {
7 | return {children};
8 | };
9 | export default AuthProvider;
10 |
--------------------------------------------------------------------------------
/src/providers/providers.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 | import { FC, ReactNode } from 'react';
3 | import AuthProvider from './auth-provider';
4 | import { ThemeProvider } from './theme-provider';
5 | import { Toaster } from '@/components/ui/toaster';
6 |
7 | const Provider: FC<{ children: ReactNode }> = ({ children }) => {
8 | return (
9 |
10 |
16 | {children}
17 |
18 |
19 |
20 | );
21 | };
22 |
23 | export default Provider;
24 |
--------------------------------------------------------------------------------
/src/providers/theme-provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client';
2 |
3 | import React from 'react';
4 | import { ThemeProvider as NextThemesProvider } from 'next-themes';
5 | import { type ThemeProviderProps } from 'next-themes/dist/types';
6 |
7 | export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
8 | return {children};
9 | }
10 |
--------------------------------------------------------------------------------
/src/services/jobs.services.ts:
--------------------------------------------------------------------------------
1 | import { JOBS_PER_PAGE } from '@/config/app.config';
2 | import { JobQuerySchemaType } from '@/lib/validators/jobs.validator';
3 | import { Prisma } from '@prisma/client';
4 |
5 | function salaryRangeQuery(salaryrange: JobQuerySchemaType['salaryrange']) {
6 | const query = salaryrange!.map((range: string) => {
7 | const [minSalary, maxSalary] = range.split('-');
8 | const isAbove = isNaN(Number(maxSalary));
9 | return {
10 | AND: [
11 | {
12 | minSalary: {
13 | gte: Number(minSalary),
14 | },
15 | ...(!isAbove && {
16 | maxSalary: {
17 | lte: Number(maxSalary),
18 | },
19 | }),
20 | },
21 | ],
22 | };
23 | });
24 | return query;
25 | }
26 | export function getJobFilters({
27 | EmpType,
28 | workmode,
29 | city,
30 | salaryrange,
31 | search,
32 | sortby,
33 | page,
34 | limit,
35 | }: JobQuerySchemaType) {
36 | const filters = [
37 | EmpType && { type: { in: EmpType } },
38 | workmode && { workMode: { in: workmode } },
39 | city && { city: { in: city } },
40 | salaryrange && { OR: salaryRangeQuery(salaryrange) },
41 | search && {
42 | OR: [
43 | {
44 | title: {
45 | contains: search,
46 | mode: 'insensitive',
47 | },
48 | },
49 | {
50 | companyName: {
51 | contains: search,
52 | mode: 'insensitive',
53 | },
54 | },
55 | ],
56 | },
57 | ];
58 | const filterQueries: Prisma.JobWhereInput = {
59 | AND: filters.filter(
60 | (filter) => filter !== undefined && filter !== null && filter !== ''
61 | ) as Prisma.JobWhereInput[],
62 | };
63 |
64 | const sortFieldMapping: { [key: string]: string } = {
65 | postedat: 'postedAt',
66 | maxsalary: 'maxSalary',
67 | };
68 |
69 | const [sort, sortOrder] = sortby.split('_');
70 | const orderBy: Prisma.JobOrderByWithAggregationInput = sortby
71 | ? {
72 | [sortFieldMapping[sort]]:
73 | sort === 'maxsalary' ? { sort: sortOrder, nulls: 'last' } : sortOrder,
74 | }
75 | : {};
76 |
77 | const pagination = {
78 | skip: 0,
79 | take: limit || JOBS_PER_PAGE,
80 | };
81 | if (page) {
82 | pagination.skip = (page - 1) * JOBS_PER_PAGE;
83 | }
84 | return { filterQueries, orderBy, pagination };
85 | }
86 |
--------------------------------------------------------------------------------
/src/types/api.types.ts:
--------------------------------------------------------------------------------
1 | import { ErrorResponseType } from '@/lib/error';
2 | import { SuccessResponseType } from '@/lib/success';
3 |
4 | export type ServerActionReturnType =
5 | | SuccessResponseType
6 | | ErrorResponseType;
7 |
--------------------------------------------------------------------------------
/src/types/faqs.types.ts:
--------------------------------------------------------------------------------
1 | export type faqItem = {
2 | question: string;
3 | answer: string;
4 | };
5 |
--------------------------------------------------------------------------------
/src/types/jobs.types.ts:
--------------------------------------------------------------------------------
1 | export type JobType = {
2 | companyLogo: string;
3 | companyBio: string;
4 | workMode: 'remote' | 'office' | 'hybrid';
5 | city: string;
6 | address: string;
7 | minSalary: number | null;
8 | type: string;
9 | category: string;
10 | maxSalary: number | null;
11 | minExperience: number | null;
12 | maxExperience: number | null;
13 | skills: string[];
14 | id: string;
15 | title: string;
16 | expired: Boolean;
17 | description: string | null;
18 | companyName: string;
19 | postedAt: Date;
20 | isVerifiedJob?: Boolean;
21 | application?: string;
22 | deleted?: Boolean;
23 | };
24 | export type getAllJobsAdditonalType = {
25 | jobs: JobType[];
26 | totalJobs: number;
27 | };
28 | export type getAllRecommendedJobs = {
29 | jobs: JobType[];
30 | };
31 | export type getJobType = {
32 | job: JobType | null;
33 | };
34 |
--------------------------------------------------------------------------------
/src/types/next-auth.d.ts:
--------------------------------------------------------------------------------
1 | import NextAuth from 'next-auth';
2 |
3 | declare module 'next-auth' {
4 | interface User {
5 | isVerified: boolean;
6 | role: string;
7 | onBoard: boolean;
8 | }
9 | interface Session {
10 | user: {
11 | id: string;
12 | email: string;
13 | role: string;
14 | name: string;
15 | isVerified: boolean;
16 | image?: string;
17 | onBoard: boolean;
18 | };
19 | }
20 | }
21 |
22 | declare module 'next-auth/jwt' {
23 | interface JWT {
24 | id: string;
25 | isVerified: boolean;
26 | role: string;
27 | onBoard: boolean;
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/types/recruiters.types.ts:
--------------------------------------------------------------------------------
1 | export type RecruitersTypes = {
2 | id: string;
3 | email: string;
4 | name: string;
5 | createdAt: Date;
6 | _count: {
7 | jobs: number;
8 | };
9 | company: {
10 | companyName: string;
11 | companyEmail: string;
12 | } | null;
13 | };
14 |
15 | export type getAllRecruiters = {
16 | recruiters: RecruitersTypes[];
17 | };
18 |
--------------------------------------------------------------------------------
/src/types/testimonials.types.ts:
--------------------------------------------------------------------------------
1 | export type testimonialItem = {
2 | name: string;
3 | testimonial: string;
4 | };
5 |
--------------------------------------------------------------------------------
/src/types/user.types.ts:
--------------------------------------------------------------------------------
1 | import {
2 | DegreeType,
3 | EmployementType,
4 | FieldOfStudyType,
5 | WorkMode,
6 | } from '@prisma/client';
7 |
8 | export interface ProjectType {
9 | id: number;
10 | projectName: string;
11 | projectGithub: string;
12 | projectThumbnail: string | null;
13 | projectLiveLink: string | null;
14 | isFeature: boolean;
15 | stack: string;
16 | projectSummary: string;
17 | }
18 | export interface EducationType {
19 | id: number;
20 | startDate: Date;
21 | instituteName: string;
22 | degree: DegreeType;
23 | fieldOfStudy: FieldOfStudyType;
24 | endDate: Date | null;
25 | }
26 | export interface ExperienceType {
27 | id: number;
28 | startDate: Date;
29 | endDate: Date | null;
30 | companyName: string;
31 | currentWorkStatus: boolean;
32 | description: string;
33 | address: string;
34 | workMode: WorkMode;
35 | EmploymentType: EmployementType;
36 | designation: string;
37 | }
38 |
39 | export interface UserType {
40 | name: string;
41 | id: string;
42 | email: string;
43 | skills: string[];
44 | contactEmail: string | null;
45 | resume: string | null;
46 | avatar: string | null;
47 | aboutMe: string | null;
48 | experience: ExperienceType[];
49 | education: EducationType[];
50 | project: ProjectType[];
51 | resumeUpdateDate: Date | null;
52 | githubLink: string | null;
53 | portfolioLink: string | null;
54 | linkedinLink: string | null;
55 | twitterLink: string | null;
56 | discordLink: string | null;
57 | }
58 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "ESNext",
10 | "moduleResolution": "node",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | },
23 | "allowSyntheticDefaultImports": true,
24 | "outDir": "./dist",
25 | "target": "ES2020"
26 | },
27 | "ts-node": {
28 | "esm": true,
29 | "experimentalSpecifierResolution": "node"
30 | },
31 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
32 | "exclude": ["node_modules"]
33 | }
34 |
--------------------------------------------------------------------------------