Building a SaaS Platform for Recruiters: Streamlining Email Crafting, Job Management, and Analytics

nadim_ch0wdhury

Nadim Chowdhury

Posted on October 19, 2024

Building a SaaS Platform for Recruiters: Streamlining Email Crafting, Job Management, and Analytics

To Develop a SaaS (Software as a Service) application designed to assist recruiters in streamlining their email crafting and recruitment processes. The platform offers advanced features such as personalized email templates, AI-powered candidate outreach, job management, interview scheduling, and recruitment analytics. This software aims to enhance recruiter productivity by automating repetitive tasks and providing insights into candidate engagement, making the recruitment process more efficient.To Develop

Key aspects of the PDF:

  1. Target Audience: Recruiters and HR professionals.
  2. Main Features:

    • Email Crafting: AI-powered tools to write personalized and effective emails to candidates.
    • Networking and Engagement: Tools to expand professional networks and engage with potential candidates.
    • Interview Questions: AI-generated tailored interview questions based on job descriptions.
    • Candidate Personas: Automatic generation of candidate personas from job descriptions.
    • Recruitment Metrics and Analysis: Insights and recommendations based on recruitment performance data.
    • Affordable Pricing: It offers free and paid plans with different levels of access to features.
  3. Purpose: It promotes the features of the TEST platform, emphasizing how it can optimize recruitment efforts by improving communication, engagement, and decision-making for recruiters.

It seems to be primarily a marketing document aimed at convincing recruiters to try the product, outlining both the features and benefits of the software.

To develop a project like TEST using Next.js 14 App Router and Tailwind CSS, here is a breakdown of functionality and the recommended file and folder structure.

Core Functionality Details

  1. Authentication and Authorization:

    • OAuth/Social Logins (Google, LinkedIn, etc.)
    • Email/Password Authentication
    • Role-based access control (Admin, Recruiter, etc.)
  2. Dashboard:

    • Display key recruitment metrics (Jobs created, Candidates contacted, Open Positions)
    • Recruiter tools such as job creation, candidate management, email crafting, etc.
  3. Job Management:

    • Create New Job: Form for recruiters to input job details (title, description, requirements, etc.)
    • Job Listing: Display jobs with filters (status: open/closed, urgency, etc.)
    • Job Editing: Ability to modify job details.
  4. Candidate Outreach:

    • Email Crafting: AI-generated email templates for initial reach-outs, interview invites, etc.
    • Email Sending: Integration with an email service (SendGrid, AWS SES) for delivering emails.
  5. Interview Question Generator:

    • Generate relevant interview questions based on job descriptions.
  6. Candidate Persona Creation:

    • Auto-generate candidate personas by analyzing job descriptions.
  7. Metrics and Analytics:

    • Display of recruitment performance metrics: open jobs, candidate responses, email open rates, etc.
  8. Pricing Plans:

    • Subscription with features gated by free, day pass, and premium plans.
    • Payment gateway integration (Stripe, PayPal, etc.)

File and Folder Structure

Here is the ideal structure using Next.js 14 App Router and Tailwind CSS for scalability and modularity:

/project-root
│
├── /app
│   ├── /api
│   │   ├── /auth
│   │   │   └── route.ts  # API routes for authentication (login, signup)
│   │   ├── /jobs
│   │   │   ├── route.ts  # API route to create and list jobs
│   │   │   └── [id].ts   # API route to get/edit/delete specific job
│   │   ├── /emails
│   │   │   ├── route.ts  # API to send emails (SendGrid, SES integration)
│   │   └── /metrics
│   │       └── route.ts  # API route for recruitment analytics
│   │
│   ├── /dashboard
│   │   ├── /page.tsx    # Main dashboard page
│   │   └── /metrics
│   │       └── page.tsx # Page to display recruitment analytics
│   │
│   ├── /jobs
│   │   ├── /create
│   │   │   └── page.tsx # Page to create a new job
│   │   ├── /[id]
│   │   │   ├── /edit
│   │   │   │   └── page.tsx # Edit a job
│   │   │   └── page.tsx     # View a specific job
│   │   └── page.tsx        # List of jobs
│   │
│   ├── /auth
│   │   ├── /login
│   │   │   └── page.tsx  # Login page
│   │   ├── /register
│   │   │   └── page.tsx  # Register page
│   │   └── /forgot-password
│   │       └── page.tsx  # Forgot password page
│   │
│   ├── /settings
│   │   ├── /account
│   │   │   └── page.tsx  # Account settings page
│   │   ├── /subscription
│   │   │   └── page.tsx  # Subscription and pricing page
│   │   └── /profile
│   │       └── page.tsx  # User profile page
│   │
│   └── /emails
│       └── page.tsx      # Email crafting page
│
├── /components
│   ├── JobCard.tsx       # Component to display job summary
│   ├── CandidateCard.tsx # Component to display candidate summary
│   ├── MetricsCard.tsx   # Component to show analytics
│   └── EmailForm.tsx     # Component to craft and send email
│
├── /lib
│   ├── db.ts             # Prisma/PostgreSQL configuration
│   ├── auth.ts           # NextAuth.js configuration (OAuth, JWT, etc.)
│   └── email.ts          # Email utility functions
│
├── /styles
│   ├── globals.css       # Global CSS and Tailwind imports
│   └── tailwind.config.js # Tailwind CSS configuration
│
├── /public
│   └── /images           # Static assets (logo, icons)
│
├── prisma
│   └── schema.prisma     # Prisma schema for DB structure
│
├── .env                  # Environment variables (DB connection, API keys)
├── package.json          # Project dependencies
└── tsconfig.json         # TypeScript configuration
Enter fullscreen mode Exit fullscreen mode

Functional Breakdown

Authentication

  • NextAuth.js for OAuth/Social logins (Google, LinkedIn).
  • JWT-based authentication for secured API routes.

Dashboard

  • React components with Tailwind CSS for metrics and job listings.
  • SSR/SSG using getServerSideProps to fetch job and user data.

Job Management

  • CRUD operations using Prisma for PostgreSQL.
  • Job creation form with input fields for title, description, and skills.

Email Crafting

  • React form for writing emails and a button to send via a connected email service (e.g., SendGrid).

Interview Question Generator

  • Use OpenAI API to generate relevant interview questions from job descriptions.

Candidate Persona Creation

  • Implement NLP analysis on job descriptions to create candidate personas.

Subscription Plans

  • Stripe integration for managing payments and subscription models (free, day pass, monthly).

Tech Stack

  1. Next.js 14: For server-side rendering (SSR) and API routes.
  2. Tailwind CSS: For responsive and custom styling.
  3. Prisma ORM: For database management (PostgreSQL).
  4. NextAuth.js: For authentication.
  5. SendGrid/AWS SES: For email sending.
  6. Stripe: For payment processing.
  7. OpenAI API: For AI-generated email templates and interview questions.

With this structure and functionality, you can develop a scalable recruiter assistance platform similar to TEST, leveraging modern web technologies.

To create the backend of the TEST-like project using NestJS, Prisma, and PostgreSQL, here is the recommended folder structure along with key details for the project:

Folder Structure for NestJS Backend with Prisma and PostgreSQL

/project-backend
├── /src
│   ├── /auth                   # Authentication module (OAuth, JWT)
│   │   ├── /dto
│   │   │   └── login.dto.ts     # DTO for login payload
│   │   ├── auth.controller.ts   # Authentication controller
│   │   ├── auth.service.ts      # Authentication service (JWT, OAuth)
│   │   └── auth.module.ts       # Auth module declaration
│   │
│   ├── /users                  # Users module
│   │   ├── /dto
│   │   │   └── create-user.dto.ts  # DTO for creating a new user
│   │   ├── users.controller.ts  # Controller for user-related routes
│   │   ├── users.service.ts     # Service for user business logic
│   │   ├── users.entity.ts      # User entity definition for Prisma
│   │   └── users.module.ts      # User module declaration
│   │
│   ├── /jobs                   # Jobs module
│   │   ├── /dto
│   │   │   ├── create-job.dto.ts   # DTO for creating a job
│   │   │   └── update-job.dto.ts   # DTO for updating a job
│   │   ├── jobs.controller.ts   # Controller for job routes (CRUD)
│   │   ├── jobs.service.ts      # Business logic for managing jobs
│   │   ├── jobs.entity.ts       # Prisma schema for Job entity
│   │   └── jobs.module.ts       # Job module declaration
│   │
│   ├── /emails                 # Email module (for crafting and sending emails)
│   │   ├── /dto
│   │   │   └── send-email.dto.ts   # DTO for sending email
│   │   ├── emails.controller.ts # Controller for email sending APIs
│   │   ├── emails.service.ts    # Service for email logic (using SendGrid or AWS SES)
│   │   └── emails.module.ts     # Email module declaration
│   │
│   ├── /metrics                # Metrics and analytics module
│   │   ├── metrics.controller.ts # Controller for retrieving recruitment metrics
│   │   ├── metrics.service.ts    # Service to handle metric calculations
│   │   └── metrics.module.ts     # Metrics module declaration
│   │
│   ├── /interviews             # Interviews module
│   │   ├── /dto
│   │   │   └── schedule-interview.dto.ts # DTO for scheduling interviews
│   │   ├── interviews.controller.ts # Controller for interview-related routes
│   │   ├── interviews.service.ts  # Business logic for interview scheduling
│   │   └── interviews.module.ts   # Interview module declaration
│   │
│   ├── /payments               # Payments module for Stripe integration
│   │   ├── /dto
│   │   │   └── payment.dto.ts   # DTO for handling payment payloads
│   │   ├── payments.controller.ts # Controller for handling subscription payments
│   │   ├── payments.service.ts  # Service to integrate Stripe for subscription management
│   │   └── payments.module.ts   # Payments module declaration
│   │
│   ├── /prisma                 # Prisma module for database management
│   │   ├── prisma.module.ts     # Prisma module for database connection
│   │   ├── prisma.service.ts    # Prisma service (initialize and handle DB operations)
│   │   └── schema.prisma        # Prisma schema file for defining DB structure
│   │
│   ├── /common                 # Common module (utilities, guards, interceptors)
│   │   ├── /guards              # Authorization guards (e.g., RoleGuard)
│   │   ├── /interceptors        # Custom interceptors for logging or transformation
│   │   └── /utils               # Shared utility functions
│   │
│   ├── app.module.ts           # Root module that imports and registers all other modules
│   ├── main.ts                 # Application entry point
│   └── env.validation.ts       # Environment variable validation schema
│
├── prisma
│   └── schema.prisma           # Prisma schema file for database models
│
├── .env                        # Environment variables (DB connection, API keys, etc.)
├── .env.example                # Example environment file for developers
├── .gitignore                  # Git ignore rules
├── package.json                # Node.js dependencies and scripts
└── tsconfig.json               # TypeScript configuration
Enter fullscreen mode Exit fullscreen mode

Breakdown of the Folder Structure

  1. Auth Module (auth/):

    • Manages user authentication and authorization using JWT or OAuth (e.g., Google, LinkedIn).
    • Controllers for login, signup, and token refresh.
    • Integration with Next.js frontend for user authentication flow.
  2. Users Module (users/):

    • Manages user-related actions like creating, updating, or retrieving user data.
    • Works with Prisma to query and update the users table in the database.
    • Roles such as "Admin" or "Recruiter" are defined here.
  3. Jobs Module (jobs/):

    • Implements job-related functionality like job creation, job listing, and editing.
    • Integrates with Prisma to store jobs in PostgreSQL.
    • REST API to fetch jobs, filter them, and perform CRUD operations.
  4. Emails Module (emails/):

    • Handles email-related functionality such as sending personalized recruitment emails.
    • Integrates with email services like SendGrid or AWS SES to send emails.
    • AI-powered email generation (potentially integrated with OpenAI).
  5. Metrics Module (metrics/):

    • Collects and analyzes data about job performance, email engagement, and candidate responses.
    • Provides APIs to return metrics such as open job counts, email open rates, etc.
  6. Interviews Module (interviews/):

    • Handles the scheduling of interviews and management of interview sessions.
    • Provides APIs to schedule, update, or cancel interviews.
    • Integration with Google Calendar or Outlook for scheduling.
  7. Payments Module (payments/):

    • Manages subscription payments using Stripe.
    • Handles different pricing plans and enforces feature limits based on the plan.
    • Provides subscription management APIs to manage upgrades, downgrades, and renewals.
  8. Prisma Module (prisma/):

    • The Prisma ORM is configured here.
    • schema.prisma defines all the database models (e.g., users, jobs, subscriptions).
    • The PrismaService is injected into other services to handle database interactions.
  9. Common Module (common/):

    • Houses shared utilities, guards, interceptors, and other components used across the project.
    • For example, a RoleGuard can ensure certain routes are accessible only by specific roles (Admin/Recruiter).

Example Prisma Schema for Key Entities

Here’s an example Prisma schema (schema.prisma) to illustrate how models like User, Job, and Subscription might look:

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  role      String   // Admin, Recruiter, etc.
  jobs      Job[]    // A user can post multiple jobs
  createdAt DateTime @default(now())
}

model Job {
  id          Int      @id @default(autoincrement())
  title       String
  description String
  status      String   // Open, Closed, etc.
  createdBy   Int
  recruiter   User     @relation(fields: [createdBy], references: [id])
  createdAt   DateTime @default(now())
}

model Subscription {
  id          Int      @id @default(autoincrement())
  userId      Int      @unique
  plan        String   // Free, Day Pass, Monthly, etc.
  createdAt   DateTime @default(now())
  user        User     @relation(fields: [userId], references: [id])
}

model EmailHistory {
  id        Int      @id @default(autoincrement())
  userId    Int
  jobId     Int
  status    String   // Sent, Opened, Clicked, etc.
  createdAt DateTime @default(now())
  user      User     @relation(fields: [userId], references: [id])
  job       Job      @relation(fields: [jobId], references: [id])
}
Enter fullscreen mode Exit fullscreen mode

Sure! Here's an example of how to create a dashboard with a metrics page in Next.js 14 using Tailwind CSS for styling. I'll provide basic components, routing, and styling for a recruiter’s dashboard and the metrics page.

Folder Structure for Dashboard

/project-root
├── /app
│   ├── /dashboard
│   │   ├── /page.tsx      # Main dashboard page
│   │   └── /metrics
│   │       └── page.tsx   # Page to display recruitment analytics
│   └── /components        # Reusable components
│       ├── Navbar.tsx     # Navbar component for dashboard navigation
│       └── MetricCard.tsx # Metric card component for displaying metrics
├── /styles
│   └── globals.css        # Tailwind CSS imports and global styles
├── /public
│   └── /icons             # Any icons or images used in the UI
└── tailwind.config.js      # Tailwind CSS configuration
Enter fullscreen mode Exit fullscreen mode

Main Dashboard Page (dashboard/page.tsx)

This page will serve as the recruiter’s dashboard, displaying key metrics and options for navigating the app.

// src/app/dashboard/page.tsx

import Navbar from '@/components/Navbar';
import Link from 'next/link';

export default function DashboardPage() {
  return (
    <div className="min-h-screen bg-gray-100">
      <Navbar />
      <div className="container mx-auto p-6">
        <h1 className="text-3xl font-semibold text-gray-800 mb-4">Recruiter Dashboard</h1>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
          {/* Card for Metrics Page */}
          <Link href="/dashboard/metrics">
            <div className="bg-white shadow-lg p-6 rounded-lg hover:shadow-2xl transition-shadow duration-200 cursor-pointer">
              <h2 className="text-2xl font-medium text-gray-700">Metrics</h2>
              <p className="mt-2 text-gray-600">
                View detailed recruitment analytics and insights.
              </p>
            </div>
          </Link>

          {/* Other dashboard items can go here */}
          <div className="bg-white shadow-lg p-6 rounded-lg">
            <h2 className="text-2xl font-medium text-gray-700">Job Management</h2>
            <p className="mt-2 text-gray-600">Create, edit, and manage job listings.</p>
          </div>

          <div className="bg-white shadow-lg p-6 rounded-lg">
            <h2 className="text-2xl font-medium text-gray-700">Candidate Outreach</h2>
            <p className="mt-2 text-gray-600">Send AI-generated emails to candidates.</p>
          </div>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Metrics Page (dashboard/metrics/page.tsx)

The metrics page shows key analytics like job performance, email outreach, and interview scheduling.

// src/app/dashboard/metrics/page.tsx

import Navbar from '@/components/Navbar';
import MetricCard from '@/components/MetricCard';

export default function MetricsPage() {
  return (
    <div className="min-h-screen bg-gray-100">
      <Navbar />
      <div className="container mx-auto p-6">
        <h1 className="text-3xl font-semibold text-gray-800 mb-4">Recruitment Metrics</h1>
        <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
          <MetricCard title="Open Jobs" value="25" description="Jobs currently open" />
          <MetricCard title="Closed Jobs" value="12" description="Jobs closed successfully" />
          <MetricCard title="Emails Sent" value="45" description="Total outreach emails sent" />
          <MetricCard title="Interviews Scheduled" value="8" description="Upcoming interviews" />
          <MetricCard title="Response Rate" value="70%" description="Email response rate" />
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Reusable Components

Navbar Component (components/Navbar.tsx)

// src/components/Navbar.tsx

import Link from 'next/link';

export default function Navbar() {
  return (
    <nav className="bg-blue-600 p-4 shadow-lg">
      <div className="container mx-auto flex justify-between items-center">
        <Link href="/dashboard" className="text-white text-xl font-bold">
          Recruiter Dashboard
        </Link>
        <div className="space-x-4">
          <Link href="/dashboard" className="text-white hover:text-blue-300">
            Dashboard
          </Link>
          <Link href="/dashboard/metrics" className="text-white hover:text-blue-300">
            Metrics
          </Link>
          <Link href="/settings" className="text-white hover:text-blue-300">
            Settings
          </Link>
        </div>
      </div>
    </nav>
  );
}
Enter fullscreen mode Exit fullscreen mode

Metric Card Component (components/MetricCard.tsx)

// src/components/MetricCard.tsx

interface MetricCardProps {
  title: string;
  value: string;
  description: string;
}

export default function MetricCard({ title, value, description }: MetricCardProps) {
  return (
    <div className="bg-white shadow-lg p-6 rounded-lg">
      <h2 className="text-xl font-medium text-gray-700">{title}</h2>
      <p className="text-3xl font-bold text-gray-900">{value}</p>
      <p className="mt-2 text-gray-600">{description}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS Configuration (tailwind.config.js)

Make sure Tailwind is configured correctly by adding it to your project:

// tailwind.config.js

module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode

Global Styles (styles/globals.css)

Ensure Tailwind CSS is imported into your global styles:

/* styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Resulting Pages and Styling

  • The dashboard page contains cards linking to different sections such as job management, candidate outreach, and metrics.
  • The metrics page displays various key metrics about recruitment activities like open jobs, emails sent, and interview scheduling.
  • Both pages use Tailwind CSS for styling, providing a clean, responsive layout.
  • The Navbar component provides easy navigation across the dashboard.

With this setup, you have a basic recruiter dashboard and metrics page using Next.js and Tailwind CSS that can be further extended with additional features and data integrations.

To implement the Jobs feature with pages to create, edit, view, and list jobs, here's the full code with proper structure and styling using Next.js 14 and Tailwind CSS.

Folder Structure for Jobs Module

/project-root
├── /app
│   ├── /jobs
│   │   ├── /create
│   │   │   └── page.tsx        # Page to create a new job
│   │   ├── /[id]
│   │   │   ├── /edit
│   │   │   │   └── page.tsx    # Page to edit a job
│   │   │   └── page.tsx        # Page to view a specific job
│   │   └── page.tsx            # Page to list all jobs
├── /components
│   ├── JobCard.tsx             # Component to display job summary
│   └── JobForm.tsx             # Form for creating or editing a job
├── /styles
│   └── globals.css             # Tailwind CSS imports and global styles
└── tailwind.config.js          # Tailwind CSS configuration
Enter fullscreen mode Exit fullscreen mode

Job List Page (jobs/page.tsx)

This page will list all jobs with basic information and links to view or edit them.

// src/app/jobs/page.tsx

import Link from 'next/link';
import JobCard from '@/components/JobCard';

const jobs = [
  { id: 1, title: 'Frontend Developer', description: 'React, Tailwind CSS', status: 'Open' },
  { id: 2, title: 'Backend Developer', description: 'Node.js, PostgreSQL', status: 'Closed' },
  { id: 3, title: 'UI/UX Designer', description: 'Figma, Sketch', status: 'Open' },
];

export default function JobListPage() {
  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">Job Listings</h1>
      <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
        {jobs.map((job) => (
          <JobCard key={job.id} job={job} />
        ))}
      </div>
      <div className="mt-8">
        <Link href="/jobs/create" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
          Create New Job
        </Link>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Job Creation Page (jobs/create/page.tsx)

This page is for creating a new job, using a reusable job form.

// src/app/jobs/create/page.tsx

import JobForm from '@/components/JobForm';

export default function CreateJobPage() {
  const handleSubmit = (data: any) => {
    // Logic to submit form data to the server
    console.log('Job Created', data);
  };

  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">Create New Job</h1>
      <JobForm onSubmit={handleSubmit} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Job Edit Page (jobs/[id]/edit/page.tsx)

This page is for editing an existing job. The form is pre-filled with the job’s existing data.

// src/app/jobs/[id]/edit/page.tsx

import JobForm from '@/components/JobForm';

const jobData = { title: 'Backend Developer', description: 'Node.js, PostgreSQL', status: 'Open' };

export default function EditJobPage() {
  const handleSubmit = (data: any) => {
    // Logic to submit form data to the server
    console.log('Job Updated', data);
  };

  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">Edit Job</h1>
      <JobForm onSubmit={handleSubmit} job={jobData} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

View Job Page (jobs/[id]/page.tsx)

This page displays the details of a specific job.

// src/app/jobs/[id]/page.tsx

const jobData = { title: 'Frontend Developer', description: 'React, Tailwind CSS', status: 'Open' };

export default function ViewJobPage() {
  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">{jobData.title}</h1>
      <p className="text-gray-700 mb-4">Status: {jobData.status}</p>
      <p className="text-gray-600">{jobData.description}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Reusable Components

Job Card Component (components/JobCard.tsx)

This component displays job summaries in a card format.

// src/components/JobCard.tsx

import Link from 'next/link';

interface JobCardProps {
  job: {
    id: number;
    title: string;
    description: string;
    status: string;
  };
}

export default function JobCard({ job }: JobCardProps) {
  return (
    <div className="bg-white shadow-lg p-6 rounded-lg">
      <h2 className="text-xl font-medium text-gray-700">{job.title}</h2>
      <p className="text-gray-600">{job.description}</p>
      <p className={`mt-2 ${job.status === 'Open' ? 'text-green-500' : 'text-red-500'}`}>{job.status}</p>
      <div className="mt-4">
        <Link href={`/jobs/${job.id}`} className="text-blue-500 hover:underline">View</Link>
        <Link href={`/jobs/${job.id}/edit`} className="ml-4 text-blue-500 hover:underline">Edit</Link>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Job Form Component (components/JobForm.tsx)

This form component is used for both job creation and editing.

// src/components/JobForm.tsx

import { useState } from 'react';

interface JobFormProps {
  onSubmit: (data: any) => void;
  job?: {
    title: string;
    description: string;
    status: string;
  };
}

export default function JobForm({ onSubmit, job }: JobFormProps) {
  const [title, setTitle] = useState(job?.title || '');
  const [description, setDescription] = useState(job?.description || '');
  const [status, setStatus] = useState(job?.status || 'Open');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit({ title, description, status });
  };

  return (
    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg">
      <div className="mb-4">
        <label className="block text-gray-700">Job Title</label>
        <input
          type="text"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={title}
          onChange={(e) => setTitle(e.target.value)}
          required
        />
      </div>

      <div className="mb-4">
        <label className="block text-gray-700">Description</label>
        <textarea
          className="mt-2 w-full px-3 py-2 border rounded"
          rows={4}
          value={description}
          onChange={(e) => setDescription(e.target.value)}
          required
        ></textarea>
      </div>

      <div className="mb-4">
        <label className="block text-gray-700">Status</label>
        <select
          className="mt-2 w-full px-3 py-2 border rounded"
          value={status}
          onChange={(e) => setStatus(e.target.value)}
          required
        >
          <option value="Open">Open</option>
          <option value="Closed">Closed</option>
        </select>
      </div>

      <button type="submit" className="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
        {job ? 'Update Job' : 'Create Job'}
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS Configuration

Ensure you have Tailwind CSS configured and working:

  1. tailwind.config.js:
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode
  1. Global CSS (styles/globals.css):
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Resulting Features

  • Job List Page: Lists all

jobs with options to view or edit them.

  • Create Job Page: A form for recruiters to create new job listings.
  • Edit Job Page: Pre-filled form for recruiters to update an existing job.
  • View Job Page: Displays the details of a specific job.

This implementation uses Tailwind CSS for responsive styling and Next.js 14 app router for dynamic routing. You can further integrate API calls to fetch and manage jobs from the backend (e.g., with NestJS and Prisma).

Here’s the full implementation for the Authentication pages in Next.js 14 using Tailwind CSS. The folder structure includes login, register, and forgot password pages.

Folder Structure for Authentication

/project-root
├── /app
│   ├── /auth
│   │   ├── /login
│   │   │   └── page.tsx        # Login page
│   │   ├── /register
│   │   │   └── page.tsx        # Register page
│   │   └── /forgot-password
│   │       └── page.tsx        # Forgot password page
├── /components
│   └── AuthForm.tsx            # Reusable form component for authentication
├── /styles
│   └── globals.css             # Tailwind CSS imports and global styles
└── tailwind.config.js          # Tailwind CSS configuration
Enter fullscreen mode Exit fullscreen mode

Login Page (auth/login/page.tsx)

This page allows users to login using their email and password.

// src/app/auth/login/page.tsx

import AuthForm from '@/components/AuthForm';

export default function LoginPage() {
  const handleSubmit = (data: any) => {
    // Logic to authenticate user
    console.log('Login Data', data);
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <AuthForm
        type="login"
        onSubmit={handleSubmit}
        heading="Login to Your Account"
        buttonLabel="Login"
        footerMessage="Don't have an account?"
        footerLink="/auth/register"
        footerLinkText="Sign Up"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Register Page (auth/register/page.tsx)

This page allows new users to register for an account.

// src/app/auth/register/page.tsx

import AuthForm from '@/components/AuthForm';

export default function RegisterPage() {
  const handleSubmit = (data: any) => {
    // Logic to register user
    console.log('Register Data', data);
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <AuthForm
        type="register"
        onSubmit={handleSubmit}
        heading="Create a New Account"
        buttonLabel="Sign Up"
        footerMessage="Already have an account?"
        footerLink="/auth/login"
        footerLinkText="Login"
      />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Forgot Password Page (auth/forgot-password/page.tsx)

This page allows users to request a password reset.

// src/app/auth/forgot-password/page.tsx

import { useState } from 'react';

export default function ForgotPasswordPage() {
  const [email, setEmail] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    // Logic to handle forgot password
    console.log('Reset Password for:', email);
  };

  return (
    <div className="min-h-screen flex items-center justify-center bg-gray-100">
      <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
        <h2 className="text-3xl font-semibold text-gray-800 mb-6 text-center">Forgot Password</h2>
        <form onSubmit={handleSubmit}>
          <div className="mb-4">
            <label className="block text-gray-700">Email Address</label>
            <input
              type="email"
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className="mt-2 w-full px-3 py-2 border rounded"
              required
            />
          </div>
          <button type="submit" className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
            Send Reset Link
          </button>
        </form>
        <div className="mt-4 text-center">
          <a href="/auth/login" className="text-blue-500 hover:underline">
            Back to Login
          </a>
        </div>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Reusable Authentication Form Component (components/AuthForm.tsx)

This is a reusable form component for both login and register pages.

// src/components/AuthForm.tsx

import { useState } from 'react';
import Link from 'next/link';

interface AuthFormProps {
  type: 'login' | 'register';
  onSubmit: (data: { email: string; password: string }) => void;
  heading: string;
  buttonLabel: string;
  footerMessage: string;
  footerLink: string;
  footerLinkText: string;
}

export default function AuthForm({
  type,
  onSubmit,
  heading,
  buttonLabel,
  footerMessage,
  footerLink,
  footerLinkText,
}: AuthFormProps) {
  const [email, setEmail] = useState('');
  const [password, setPassword] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit({ email, password });
  };

  return (
    <div className="bg-white p-8 rounded-lg shadow-md w-full max-w-md">
      <h2 className="text-3xl font-semibold text-gray-800 mb-6 text-center">{heading}</h2>
      <form onSubmit={handleSubmit}>
        <div className="mb-4">
          <label className="block text-gray-700">Email Address</label>
          <input
            type="email"
            value={email}
            onChange={(e) => setEmail(e.target.value)}
            className="mt-2 w-full px-3 py-2 border rounded"
            required
          />
        </div>

        <div className="mb-4">
          <label className="block text-gray-700">Password</label>
          <input
            type="password"
            value={password}
            onChange={(e) => setPassword(e.target.value)}
            className="mt-2 w-full px-3 py-2 border rounded"
            required
          />
        </div>

        <button type="submit" className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
          {buttonLabel}
        </button>
      </form>

      {type === 'login' && (
        <div className="mt-4 text-right">
          <Link href="/auth/forgot-password" className="text-blue-500 hover:underline">
            Forgot Password?
          </Link>
        </div>
      )}

      <div className="mt-6 text-center">
        <p className="text-gray-600">
          {footerMessage}{' '}
          <Link href={footerLink} className="text-blue-500 hover:underline">
            {footerLinkText}
          </Link>
        </p>
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS Configuration

Ensure you have Tailwind CSS configured correctly in your project:

  1. tailwind.config.js:
// tailwind.config.js

module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode
  1. Global CSS (styles/globals.css):
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Resulting Pages

  • Login Page: Users can log in with email and password. There’s also a link to the Forgot Password page.
  • Register Page: New users can create an account by providing their email and password.
  • Forgot Password Page: Users can request a password reset link by providing their email.

All pages use Tailwind CSS for styling and share a consistent layout using the AuthForm component. You can integrate these forms with your NestJS backend to handle actual authentication and password recovery logic.

Here’s the implementation for the Settings module in Next.js 14 using Tailwind CSS, which includes:

  1. Account Settings Page: For changing password and managing account-related details.
  2. Subscription and Pricing Page: For managing user subscriptions and plans.
  3. User Profile Page: For updating profile information such as name, email, and avatar.

Folder Structure for Settings Module

/project-root
├── /app
│   ├── /settings
│   │   ├── /account
│   │   │   └── page.tsx        # Account settings page
│   │   ├── /subscription
│   │   │   └── page.tsx        # Subscription and pricing page
│   │   └── /profile
│   │       └── page.tsx        # User profile page
├── /components
│   ├── AccountForm.tsx         # Form for changing password
│   ├── ProfileForm.tsx         # Form for updating profile information
│   ├── SubscriptionCard.tsx    # Card component for subscription plans
│   └── SubscriptionForm.tsx    # Form for managing subscription changes
├── /styles
│   └── globals.css             # Tailwind CSS imports and global styles
└── tailwind.config.js          # Tailwind CSS configuration
Enter fullscreen mode Exit fullscreen mode

Account Settings Page (settings/account/page.tsx)

This page allows the user to change their password and update account-related settings.

// src/app/settings/account/page.tsx

import AccountForm from '@/components/AccountForm';

export default function AccountSettingsPage() {
  const handleAccountUpdate = (data: any) => {
    // Logic to handle account update (change password)
    console.log('Account updated:', data);
  };

  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">Account Settings</h1>
      <AccountForm onSubmit={handleAccountUpdate} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Subscription and Pricing Page (settings/subscription/page.tsx)

This page displays different subscription plans and allows users to upgrade or manage their plan.

// src/app/settings/subscription/page.tsx

import SubscriptionCard from '@/components/SubscriptionCard';

const subscriptionPlans = [
  { id: 1, name: 'Free Plan', price: '$0', features: ['Basic features', 'Limited access'] },
  { id: 2, name: 'Pro Plan', price: '$29/month', features: ['Full access', 'Premium support'] },
  { id: 3, name: 'Enterprise Plan', price: '$99/month', features: ['All features', 'Dedicated support'] },
];

export default function SubscriptionPage() {
  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">Subscription and Pricing</h1>
      <div className="grid grid-cols-1 md:grid-cols-3 gap-6">
        {subscriptionPlans.map((plan) => (
          <SubscriptionCard key={plan.id} plan={plan} />
        ))}
      </div>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

User Profile Page (settings/profile/page.tsx)

This page allows users to update their profile information like name, email, and avatar.

// src/app/settings/profile/page.tsx

import ProfileForm from '@/components/ProfileForm';

export default function ProfilePage() {
  const handleProfileUpdate = (data: any) => {
    // Logic to handle profile update
    console.log('Profile updated:', data);
  };

  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">User Profile</h1>
      <ProfileForm onSubmit={handleProfileUpdate} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Reusable Components

Account Form Component (components/AccountForm.tsx)

This form allows users to change their password.

// src/components/AccountForm.tsx

import { useState } from 'react';

interface AccountFormProps {
  onSubmit: (data: { currentPassword: string; newPassword: string }) => void;
}

export default function AccountForm({ onSubmit }: AccountFormProps) {
  const [currentPassword, setCurrentPassword] = useState('');
  const [newPassword, setNewPassword] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit({ currentPassword, newPassword });
  };

  return (
    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg w-full max-w-lg">
      <div className="mb-4">
        <label className="block text-gray-700">Current Password</label>
        <input
          type="password"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={currentPassword}
          onChange={(e) => setCurrentPassword(e.target.value)}
          required
        />
      </div>

      <div className="mb-4">
        <label className="block text-gray-700">New Password</label>
        <input
          type="password"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={newPassword}
          onChange={(e) => setNewPassword(e.target.value)}
          required
        />
      </div>

      <button type="submit" className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
        Update Password
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Profile Form Component (components/ProfileForm.tsx)

This form allows users to update their profile information like name, email, and avatar.

// src/components/ProfileForm.tsx

import { useState } from 'react';

interface ProfileFormProps {
  onSubmit: (data: { name: string; email: string }) => void;
}

export default function ProfileForm({ onSubmit }: ProfileFormProps) {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit({ name, email });
  };

  return (
    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg w-full max-w-lg">
      <div className="mb-4">
        <label className="block text-gray-700">Name</label>
        <input
          type="text"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={name}
          onChange={(e) => setName(e.target.value)}
          required
        />
      </div>

      <div className="mb-4">
        <label className="block text-gray-700">Email</label>
        <input
          type="email"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          required
        />
      </div>

      <button type="submit" className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
        Update Profile
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Subscription Card Component (components/SubscriptionCard.tsx)

This component displays a subscription plan with its features and price.

// src/components/SubscriptionCard.tsx

interface SubscriptionCardProps {
  plan: {
    name: string;
    price: string;
    features: string[];
  };
}

export default function SubscriptionCard({ plan }: SubscriptionCardProps) {
  return (
    <div className="bg-white shadow-lg p-6 rounded-lg">
      <h2 className="text-2xl font-medium text-gray-700">{plan.name}</h2>
      <p className="text-4xl font-bold text-gray-900 mt-2">{plan.price}</p>
      <ul className="mt-4 space-y-2">
        {plan.features.map((feature, index) => (
          <li key={index} className="text-gray-600">
            - {feature}
          </li>
        ))}
      </ul>
      <button className="mt-6 w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
        Choose Plan
      </button>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS Configuration

Ensure Tailwind CSS is configured correctly:

  1. tailwind.config.js:
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode
  1. Global CSS (styles/globals.css):
@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Resulting Pages

  1. Account Settings Page: Users can change their password. 2.

Subscription and Pricing Page: Displays available subscription plans with a pricing structure.

  1. User Profile Page: Users can update their profile information.

This setup uses Tailwind CSS for responsive and consistent styling across pages. Each form and card component can be easily extended to integrate with your backend services (e.g., NestJS with Prisma for account management and subscription handling).

Here’s the complete implementation for the Emails Page and the components involved in crafting and sending emails, along with the utility functions and configuration files for Prisma/PostgreSQL and NextAuth.js.

Folder Structure for Emails Feature

/project-root
├── /app
│   └── /emails
│       └── page.tsx       # Email crafting page
├── /components
│   ├── JobCard.tsx        # Component to display job summary
│   ├── CandidateCard.tsx  # Component to display candidate summary
│   ├── MetricsCard.tsx    # Component to show analytics
│   └── EmailForm.tsx      # Component to craft and send emails
├── /lib
│   ├── db.ts              # Prisma/PostgreSQL configuration
│   ├── auth.ts            # NextAuth.js configuration (OAuth, JWT, etc.)
│   └── email.ts           # Email utility functions
└── prisma
    └── schema.prisma      # Prisma schema
Enter fullscreen mode Exit fullscreen mode

Emails Page (emails/page.tsx)

This page allows users to craft and send emails to candidates.

// src/app/emails/page.tsx

import EmailForm from '@/components/EmailForm';

export default function EmailsPage() {
  const handleSendEmail = (data: any) => {
    // Logic to send email using the email utility function
    console.log('Email Sent:', data);
  };

  return (
    <div className="min-h-screen bg-gray-100 p-6">
      <h1 className="text-3xl font-semibold text-gray-800 mb-6">Craft and Send Emails</h1>
      <EmailForm onSubmit={handleSendEmail} />
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Email Form Component (components/EmailForm.tsx)

This form allows users to compose an email with fields for recipient, subject, and message.

// src/components/EmailForm.tsx

import { useState } from 'react';

interface EmailFormProps {
  onSubmit: (data: { recipient: string; subject: string; message: string }) => void;
}

export default function EmailForm({ onSubmit }: EmailFormProps) {
  const [recipient, setRecipient] = useState('');
  const [subject, setSubject] = useState('');
  const [message, setMessage] = useState('');

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    onSubmit({ recipient, subject, message });
  };

  return (
    <form onSubmit={handleSubmit} className="bg-white p-6 rounded-lg shadow-lg w-full max-w-lg">
      <div className="mb-4">
        <label className="block text-gray-700">Recipient</label>
        <input
          type="email"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={recipient}
          onChange={(e) => setRecipient(e.target.value)}
          required
        />
      </div>

      <div className="mb-4">
        <label className="block text-gray-700">Subject</label>
        <input
          type="text"
          className="mt-2 w-full px-3 py-2 border rounded"
          value={subject}
          onChange={(e) => setSubject(e.target.value)}
          required
        />
      </div>

      <div className="mb-4">
        <label className="block text-gray-700">Message</label>
        <textarea
          className="mt-2 w-full px-3 py-2 border rounded"
          rows={6}
          value={message}
          onChange={(e) => setMessage(e.target.value)}
          required
        ></textarea>
      </div>

      <button type="submit" className="w-full bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-700">
        Send Email
      </button>
    </form>
  );
}
Enter fullscreen mode Exit fullscreen mode

Prisma/PostgreSQL Configuration (lib/db.ts)

This file sets up Prisma and PostgreSQL for database interactions.

// src/lib/db.ts

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient();

export default prisma;
Enter fullscreen mode Exit fullscreen mode

Prisma Schema (prisma/schema.prisma)

You can define tables for users, emails, and other entities in the Prisma schema:

// prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")  // Set this in the .env file
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  name      String?
  createdAt DateTime @default(now())
}

model Email {
  id        Int      @id @default(autoincrement())
  recipient String
  subject   String
  message   String
  sentAt    DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

NextAuth.js Configuration (lib/auth.ts)

Here’s how to configure NextAuth.js for OAuth and JWT-based authentication.

// src/lib/auth.ts

import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import prisma from './db';  // Import Prisma client

export default NextAuth({
  providers: [
    Providers.Google({
      clientId: process.env.GOOGLE_CLIENT_ID,
      clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    }),
    // Add more providers here
  ],
  database: process.env.DATABASE_URL,
  callbacks: {
    async session(session, token) {
      session.user.id = token.sub;
      return session;
    },
  },
  session: {
    jwt: true,
  },
  jwt: {
    secret: process.env.JWT_SECRET,
  },
});
Enter fullscreen mode Exit fullscreen mode

Email Utility Functions (lib/email.ts)

This utility function sends emails via a service like SendGrid or AWS SES.

// src/lib/email.ts

import nodemailer from 'nodemailer';

export const sendEmail = async (recipient: string, subject: string, message: string) => {
  const transporter = nodemailer.createTransport({
    service: 'gmail', // You can also use SMTP service like SendGrid, AWS SES, etc.
    auth: {
      user: process.env.EMAIL_USER,
      pass: process.env.EMAIL_PASSWORD,
    },
  });

  const mailOptions = {
    from: process.env.EMAIL_USER,
    to: recipient,
    subject: subject,
    text: message,
  };

  await transporter.sendMail(mailOptions);
};
Enter fullscreen mode Exit fullscreen mode

Other Components

JobCard Component (components/JobCard.tsx)

Displays job summaries in a card format.

// src/components/JobCard.tsx

interface JobCardProps {
  job: {
    title: string;
    description: string;
    status: string;
  };
}

export default function JobCard({ job }: JobCardProps) {
  return (
    <div className="bg-white shadow-lg p-6 rounded-lg">
      <h2 className="text-xl font-medium text-gray-700">{job.title}</h2>
      <p className="text-gray-600">{job.description}</p>
      <p className={`mt-2 ${job.status === 'Open' ? 'text-green-500' : 'text-red-500'}`}>{job.status}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

CandidateCard Component (components/CandidateCard.tsx)

Displays candidate details in a card format.

// src/components/CandidateCard.tsx

interface CandidateCardProps {
  candidate: {
    name: string;
    skills: string[];
    experience: number;
  };
}

export default function CandidateCard({ candidate }: CandidateCardProps) {
  return (
    <div className="bg-white shadow-lg p-6 rounded-lg">
      <h2 className="text-xl font-medium text-gray-700">{candidate.name}</h2>
      <p className="text-gray-600">Skills: {candidate.skills.join(', ')}</p>
      <p className="text-gray-600">Experience: {candidate.experience} years</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

MetricsCard Component (components/MetricsCard.tsx)

Displays metrics in a card format.

// src/components/MetricsCard.tsx

interface MetricsCardProps {
  title: string;
  value: string | number;
  description: string;
}

export default function MetricsCard({ title, value, description }: MetricsCardProps) {
  return (
    <div className="bg-white shadow-lg p-6 rounded-lg">
      <h2 className="text-xl font-medium text-gray-700">{title}</h2>
      <p className="text-3xl font-bold text-gray-900">{value}</p>
      <p className="mt-2 text-gray-600">{description}</p>
    </div>
  );
}
Enter fullscreen mode Exit fullscreen mode

Tailwind CSS Configuration

Ensure Tailwind CSS is configured properly.

  1. tailwind.config.js:
module.exports = {
  content: [
    './app/**/*.{js,ts,jsx,tsx}',
    './components/**/*.{js,ts,jsx,tsx}',
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
Enter fullscreen mode Exit fullscreen mode
  1. **Global CSS (`

styles/globals.css`)**:

@tailwind base;
@tailwind components;
@tailwind utilities;
Enter fullscreen mode Exit fullscreen mode

Resulting Features

  • Emails Page: Users can craft and send emails using the EmailForm component.
  • Email Utility: Uses Nodemailer (or other services) to send emails to recipients.
  • Prisma & PostgreSQL: Configured for database interactions.
  • NextAuth.js: OAuth and JWT authentication for handling user sessions.
  • JobCard, CandidateCard, MetricsCard: Reusable components for displaying data in a card format.

This setup can be extended to integrate with the backend (e.g., NestJS, Prisma) for full functionality.


Here’s the full code for the NestJS backend with Prisma and PostgreSQL for both authentication and users modules. It includes the folder structure, DTOs, controllers, services, and Prisma integration for handling authentication and user management.

Folder Structure for NestJS Backend with Prisma and PostgreSQL

/project-backend
├── /prisma
│   └── schema.prisma           # Prisma schema for database models
├── /src
│   ├── /auth                   # Authentication module (OAuth, JWT)
│   │   ├── /dto
│   │   │   └── login.dto.ts     # DTO for login payload
│   │   ├── auth.controller.ts   # Authentication controller
│   │   ├── auth.service.ts      # Authentication service (JWT, OAuth)
│   │   └── auth.module.ts       # Auth module declaration
│   │
│   ├── /users                  # Users module
│   │   ├── /dto
│   │   │   └── create-user.dto.ts  # DTO for creating a new user
│   │   ├── users.controller.ts  # Controller for user-related routes
│   │   ├── users.service.ts     # Service for user business logic
│   │   ├── users.entity.ts      # User entity definition for Prisma
│   │   └── users.module.ts      # User module declaration
│   │
│   ├── /common                 # Common module (guards, interceptors, utilities)
│   │   └── jwt.guard.ts         # JWT guard for protected routes
│   ├── /app.module.ts           # Root application module
│   ├── /main.ts                 # Main entry point of the application
├── /lib
│   └── prisma.service.ts        # Prisma client for database interaction
├── .env                         # Environment variables (DB connection, JWT secret)
└── package.json                 # Project dependencies
Enter fullscreen mode Exit fullscreen mode

Prisma Schema (prisma/schema.prisma)

This defines the database models for users and other related entities.

// prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int      @id @default(autoincrement())
  email     String   @unique
  password  String
  name      String?
  createdAt DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

Prisma Service (lib/prisma.service.ts)

This is the Prisma service that provides access to the database.

// src/lib/prisma.service.ts

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Enter fullscreen mode Exit fullscreen mode

Authentication Module

DTO (auth/dto/login.dto.ts)

Data transfer object for login payload.

// src/auth/dto/login.dto.ts

import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class LoginDto {
  @IsEmail()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;
}
Enter fullscreen mode Exit fullscreen mode

Auth Controller (auth/auth.controller.ts)

Handles authentication-related requests like login.

// src/auth/auth.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { AuthService } from './auth.service';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
export class AuthController {
  constructor(private readonly authService: AuthService) {}

  @Post('login')
  async login(@Body() loginDto: LoginDto) {
    return this.authService.login(loginDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Auth Service (auth/auth.service.ts)

Contains business logic for authentication (JWT handling).

// src/auth/auth.service.ts

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import { PrismaService } from 'src/lib/prisma.service';
import { LoginDto } from './dto/login.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
  constructor(private prisma: PrismaService, private jwtService: JwtService) {}

  async login(loginDto: LoginDto) {
    const user = await this.prisma.user.findUnique({
      where: { email: loginDto.email },
    });

    if (!user || !(await bcrypt.compare(loginDto.password, user.password))) {
      throw new UnauthorizedException('Invalid credentials');
    }

    const payload = { email: user.email, sub: user.id };
    return {
      access_token: this.jwtService.sign(payload),
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Auth Module (auth/auth.module.ts)

The authentication module declaration that ties everything together.

// src/auth/auth.module.ts

import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PrismaService } from 'src/lib/prisma.service';

@Module({
  imports: [
    JwtModule.register({
      secret: process.env.JWT_SECRET,
      signOptions: { expiresIn: '1h' },
    }),
  ],
  controllers: [AuthController],
  providers: [AuthService, PrismaService],
})
export class AuthModule {}
Enter fullscreen mode Exit fullscreen mode

Users Module

DTO (users/dto/create-user.dto.ts)

DTO for creating a new user.

// src/users/dto/create-user.dto.ts

import { IsEmail, IsNotEmpty, IsString } from 'class-validator';

export class CreateUserDto {
  @IsEmail()
  email: string;

  @IsString()
  @IsNotEmpty()
  password: string;

  @IsString()
  name?: string;
}
Enter fullscreen mode Exit fullscreen mode

Users Controller (users/users.controller.ts)

Handles routes related to user management.

// src/users/users.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';

@Controller('users')
export class UsersController {
  constructor(private readonly usersService: UsersService) {}

  @Post('create')
  async createUser(@Body() createUserDto: CreateUserDto) {
    return this.usersService.createUser(createUserDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Users Service (users/users.service.ts)

Contains business logic for managing users (creating users, retrieving users).

// src/users/users.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/lib/prisma.service';
import { CreateUserDto } from './dto/create-user.dto';
import * as bcrypt from 'bcrypt';

@Injectable()
export class UsersService {
  constructor(private prisma: PrismaService) {}

  async createUser(createUserDto: CreateUserDto) {
    const hashedPassword = await bcrypt.hash(createUserDto.password, 10);
    return this.prisma.user.create({
      data: {
        email: createUserDto.email,
        password: hashedPassword,
        name: createUserDto.name,
      },
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Users Module (users/users.module.ts)

The users module declaration.

// src/users/users.module.ts

import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaService } from 'src/lib/prisma.service';

@Module({
  controllers: [UsersController],
  providers: [UsersService, PrismaService],
})
export class UsersModule {}
Enter fullscreen mode Exit fullscreen mode

JWT Guard (common/jwt.guard.ts)

A guard to protect routes that require authentication via JWT.

// src/common/jwt.guard.ts

import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private jwtService: JwtService) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    if (!token) {
      throw new UnauthorizedException('Missing token');
    }

    try {
      this.jwtService.verify(token);
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }

    return super.canActivate(context);
  }
}
Enter fullscreen mode Exit fullscreen mode

Root Module (app.module.ts)

The root application module that imports all other modules.

// src/app.module.ts

import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';

@Module({
  imports: [AuthModule, UsersModule],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Main Entry Point (main.ts)

The main entry point of the application.

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await

 app.listen(3000);
}
bootstrap();
Enter fullscreen mode Exit fullscreen mode

Environment Variables (.env)

Set up environment variables for PostgreSQL and JWT secret.

DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
JWT_SECRET=my_super_secret_jwt_key
Enter fullscreen mode Exit fullscreen mode

Project Dependencies (package.json)

Include the necessary dependencies for NestJS, Prisma, bcrypt, and JWT.

{
  "name": "project-backend",
  "version": "1.0.0",
  "main": "dist/main.js",
  "scripts": {
    "start": "nest start",
    "start:dev": "nest start --watch",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev"
  },
  "dependencies": {
    "@nestjs/common": "^8.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/jwt": "^8.0.0",
    "@nestjs/passport": "^8.0.0",
    "bcrypt": "^5.0.0",
    "prisma": "^3.0.0",
    "@prisma/client": "^3.0.0",
    "passport-jwt": "^4.0.0",
    "jsonwebtoken": "^8.0.0",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup provides a NestJS backend integrated with Prisma and PostgreSQL, handling:

  1. Authentication with JWT (login).
  2. User management (creating users).
  3. Protected routes using a JWT guard.
  4. Prisma ORM to interact with the PostgreSQL database.

You can extend this with more features such as registration, password resets, or additional user roles.

Here's the complete implementation for the Jobs module and Emails module in a NestJS project using Prisma and PostgreSQL for job management and email sending. This includes DTOs, controllers, services, and Prisma schema for both modules.

Folder Structure for Jobs and Emails Module

/project-backend
├── /src
│   ├── /jobs                   # Jobs module
│   │   ├── /dto
│   │   │   ├── create-job.dto.ts   # DTO for creating a job
│   │   │   └── update-job.dto.ts   # DTO for updating a job
│   │   ├── jobs.controller.ts   # Controller for job routes (CRUD)
│   │   ├── jobs.service.ts      # Business logic for managing jobs
│   │   ├── jobs.entity.ts       # Prisma schema for Job entity
│   │   └── jobs.module.ts       # Job module declaration
│   │
│   ├── /emails                 # Email module (for crafting and sending emails)
│   │   ├── /dto
│   │   │   └── send-email.dto.ts   # DTO for sending email
│   │   ├── emails.controller.ts # Controller for email sending APIs
│   │   ├── emails.service.ts    # Service for email logic (using SendGrid or AWS SES)
│   │   └── emails.module.ts     # Email module declaration
│
├── /lib
│   └── prisma.service.ts        # Prisma client for database interaction
├── /prisma
│   └── schema.prisma            # Prisma schema for Job entity
└── .env                         # Environment variables (DB connection, Email credentials)
Enter fullscreen mode Exit fullscreen mode

Jobs Module

DTOs for Jobs

Create Job DTO (jobs/dto/create-job.dto.ts)

This DTO defines the data required to create a job.

// src/jobs/dto/create-job.dto.ts

import { IsString, IsNotEmpty, IsOptional } from 'class-validator';

export class CreateJobDto {
  @IsString()
  @IsNotEmpty()
  title: string;

  @IsString()
  @IsNotEmpty()
  description: string;

  @IsString()
  @IsOptional()
  status?: string; // Open, Closed, etc.
}
Enter fullscreen mode Exit fullscreen mode

Update Job DTO (jobs/dto/update-job.dto.ts)

This DTO defines the data required to update an existing job.

// src/jobs/dto/update-job.dto.ts

import { IsString, IsOptional } from 'class-validator';

export class UpdateJobDto {
  @IsString()
  @IsOptional()
  title?: string;

  @IsString()
  @IsOptional()
  description?: string;

  @IsString()
  @IsOptional()
  status?: string;
}
Enter fullscreen mode Exit fullscreen mode

Jobs Controller (jobs/jobs.controller.ts)

The controller handles job-related CRUD operations like creating, updating, and fetching jobs.

// src/jobs/jobs.controller.ts

import { Controller, Get, Post, Body, Param, Patch, Delete } from '@nestjs/common';
import { JobsService } from './jobs.service';
import { CreateJobDto } from './dto/create-job.dto';
import { UpdateJobDto } from './dto/update-job.dto';

@Controller('jobs')
export class JobsController {
  constructor(private readonly jobsService: JobsService) {}

  @Post()
  async createJob(@Body() createJobDto: CreateJobDto) {
    return this.jobsService.createJob(createJobDto);
  }

  @Get()
  async findAll() {
    return this.jobsService.findAllJobs();
  }

  @Get(':id')
  async findOne(@Param('id') id: string) {
    return this.jobsService.findJobById(+id);
  }

  @Patch(':id')
  async updateJob(@Param('id') id: string, @Body() updateJobDto: UpdateJobDto) {
    return this.jobsService.updateJob(+id, updateJobDto);
  }

  @Delete(':id')
  async deleteJob(@Param('id') id: string) {
    return this.jobsService.deleteJob(+id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Jobs Service (jobs/jobs.service.ts)

The service contains business logic for job management, including database interactions via Prisma.

// src/jobs/jobs.service.ts

import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from 'src/lib/prisma.service';
import { CreateJobDto } from './dto/create-job.dto';
import { UpdateJobDto } from './dto/update-job.dto';

@Injectable()
export class JobsService {
  constructor(private prisma: PrismaService) {}

  async createJob(createJobDto: CreateJobDto) {
    return this.prisma.job.create({
      data: createJobDto,
    });
  }

  async findAllJobs() {
    return this.prisma.job.findMany();
  }

  async findJobById(id: number) {
    const job = await this.prisma.job.findUnique({ where: { id } });
    if (!job) throw new NotFoundException('Job not found');
    return job;
  }

  async updateJob(id: number, updateJobDto: UpdateJobDto) {
    return this.prisma.job.update({
      where: { id },
      data: updateJobDto,
    });
  }

  async deleteJob(id: number) {
    return this.prisma.job.delete({ where: { id } });
  }
}
Enter fullscreen mode Exit fullscreen mode

Jobs Module (jobs/jobs.module.ts)

The module declaration for jobs, which imports all the necessary parts.

// src/jobs/jobs.module.ts

import { Module } from '@nestjs/common';
import { JobsService } from './jobs.service';
import { JobsController } from './jobs.controller';
import { PrismaService } from 'src/lib/prisma.service';

@Module({
  controllers: [JobsController],
  providers: [JobsService, PrismaService],
})
export class JobsModule {}
Enter fullscreen mode Exit fullscreen mode

Prisma Schema for Job (prisma/schema.prisma)

The Prisma schema defines the Job model in PostgreSQL.

// prisma/schema.prisma

model Job {
  id          Int      @id @default(autoincrement())
  title       String
  description String
  status      String   @default("Open") // Open or Closed
  createdAt   DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

Emails Module

DTO for Sending Email (emails/dto/send-email.dto.ts)

This DTO defines the data required to send an email.

// src/emails/dto/send-email.dto.ts

import { IsEmail, IsString, IsNotEmpty } from 'class-validator';

export class SendEmailDto {
  @IsEmail()
  @IsNotEmpty()
  recipient: string;

  @IsString()
  @IsNotEmpty()
  subject: string;

  @IsString()
  @IsNotEmpty()
  message: string;
}
Enter fullscreen mode Exit fullscreen mode

Emails Controller (emails/emails.controller.ts)

The controller handles email sending requests using an external email service like SendGrid or AWS SES.

// src/emails/emails.controller.ts

import { Controller, Post, Body } from '@nestjs/common';
import { EmailsService } from './emails.service';
import { SendEmailDto } from './dto/send-email.dto';

@Controller('emails')
export class EmailsController {
  constructor(private readonly emailsService: EmailsService) {}

  @Post('send')
  async sendEmail(@Body() sendEmailDto: SendEmailDto) {
    return this.emailsService.sendEmail(sendEmailDto);
  }
}
Enter fullscreen mode Exit fullscreen mode

Emails Service (emails/emails.service.ts)

The service contains business logic for sending emails using SendGrid, AWS SES, or Nodemailer.

// src/emails/emails.service.ts

import { Injectable } from '@nestjs/common';
import { SendEmailDto } from './dto/send-email.dto';
import * as nodemailer from 'nodemailer';

@Injectable()
export class EmailsService {
  async sendEmail(sendEmailDto: SendEmailDto) {
    const transporter = nodemailer.createTransport({
      service: 'gmail', // Or use an SMTP provider like SendGrid
      auth: {
        user: process.env.EMAIL_USER,
        pass: process.env.EMAIL_PASSWORD,
      },
    });

    const mailOptions = {
      from: process.env.EMAIL_USER,
      to: sendEmailDto.recipient,
      subject: sendEmailDto.subject,
      text: sendEmailDto.message,
    };

    return transporter.sendMail(mailOptions);
  }
}
Enter fullscreen mode Exit fullscreen mode

Emails Module (emails/emails.module.ts)

The module declaration for the email module.

// src/emails/emails.module.ts

import { Module } from '@nestjs/common';
import { EmailsService } from './emails.service';
import { EmailsController } from './emails.controller';

@Module({
  controllers: [EmailsController],
  providers: [EmailsService],
})
export class EmailsModule {}
Enter fullscreen mode Exit fullscreen mode

Environment Variables (.env)

Include environment variables for the email service configuration (Gmail or SMTP).

DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-email-password
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup provides a fully functional Jobs module for job management and an Emails module for sending emails. You can extend this by integrating it with NestJS Guards for authentication and role-based access control, or by using

other email services like SendGrid or AWS SES.

Here’s the full code for the Metrics and Analytics module and the Interviews module in NestJS, including DTOs, controllers, services, and module declarations.

Folder Structure for Metrics and Interviews Modules

/project-backend
├── /src
│   ├── /metrics                # Metrics and analytics module
│   │   ├── metrics.controller.ts # Controller for retrieving recruitment metrics
│   │   ├── metrics.service.ts    # Service to handle metric calculations
│   │   └── metrics.module.ts     # Metrics module declaration
│   │
│   ├── /interviews             # Interviews module
│   │   ├── /dto
│   │   │   └── schedule-interview.dto.ts # DTO for scheduling interviews
│   │   ├── interviews.controller.ts # Controller for interview-related routes
│   │   ├── interviews.service.ts  # Business logic for interview scheduling
│   │   └── interviews.module.ts   # Interview module declaration
├── /lib
│   └── prisma.service.ts        # Prisma client for database interaction
├── /prisma
│   └── schema.prisma            # Prisma schema for Job and Interview models
└── .env                         # Environment variables (DB connection)
Enter fullscreen mode Exit fullscreen mode

Metrics and Analytics Module

The Metrics module will retrieve data from the database and perform calculations on job listings, candidates, emails sent, and more.

Metrics Controller (metrics/metrics.controller.ts)

The controller handles routes to fetch recruitment metrics such as job counts, email success rates, and more.

// src/metrics/metrics.controller.ts

import { Controller, Get } from '@nestjs/common';
import { MetricsService } from './metrics.service';

@Controller('metrics')
export class MetricsController {
  constructor(private readonly metricsService: MetricsService) {}

  @Get('recruitment')
  async getRecruitmentMetrics() {
    return this.metricsService.getRecruitmentMetrics();
  }

  @Get('email-performance')
  async getEmailPerformanceMetrics() {
    return this.metricsService.getEmailPerformanceMetrics();
  }
}
Enter fullscreen mode Exit fullscreen mode

Metrics Service (metrics/metrics.service.ts)

The service contains the logic to fetch metrics from the database, including job-related data, email performance, and interview statistics.

// src/metrics/metrics.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/lib/prisma.service';

@Injectable()
export class MetricsService {
  constructor(private prisma: PrismaService) {}

  // Fetch job-related metrics
  async getRecruitmentMetrics() {
    const openJobs = await this.prisma.job.count({ where: { status: 'Open' } });
    const closedJobs = await this.prisma.job.count({ where: { status: 'Closed' } });
    const totalJobs = await this.prisma.job.count();
    return {
      openJobs,
      closedJobs,
      totalJobs,
    };
  }

  // Fetch email performance metrics
  async getEmailPerformanceMetrics() {
    // Assume we have an Email table in the Prisma schema
    const sentEmails = await this.prisma.email.count();
    const openedEmails = await this.prisma.email.count({ where: { status: 'Opened' } });
    const clickedEmails = await this.prisma.email.count({ where: { status: 'Clicked' } });
    return {
      sentEmails,
      openedEmails,
      clickedEmails,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Metrics Module (metrics/metrics.module.ts)

This module declaration ties everything together.

// src/metrics/metrics.module.ts

import { Module } from '@nestjs/common';
import { MetricsService } from './metrics.service';
import { MetricsController } from './metrics.controller';
import { PrismaService } from 'src/lib/prisma.service';

@Module({
  controllers: [MetricsController],
  providers: [MetricsService, PrismaService],
})
export class MetricsModule {}
Enter fullscreen mode Exit fullscreen mode

Interviews Module

The Interviews module allows users to schedule interviews, manage them, and retrieve interview-related data.

Schedule Interview DTO (interviews/dto/schedule-interview.dto.ts)

This DTO defines the data required to schedule an interview.

// src/interviews/dto/schedule-interview.dto.ts

import { IsString, IsNotEmpty, IsDateString } from 'class-validator';

export class ScheduleInterviewDto {
  @IsString()
  @IsNotEmpty()
  candidateId: string;

  @IsString()
  @IsNotEmpty()
  jobId: string;

  @IsDateString()
  @IsNotEmpty()
  interviewDate: string;

  @IsString()
  @IsNotEmpty()
  interviewType: string; // e.g., Technical, Behavioral
}
Enter fullscreen mode Exit fullscreen mode

Interviews Controller (interviews/interviews.controller.ts)

The controller handles routes for interview-related tasks such as scheduling and retrieving interview details.

// src/interviews/interviews.controller.ts

import { Controller, Post, Get, Param, Body } from '@nestjs/common';
import { InterviewsService } from './interviews.service';
import { ScheduleInterviewDto } from './dto/schedule-interview.dto';

@Controller('interviews')
export class InterviewsController {
  constructor(private readonly interviewsService: InterviewsService) {}

  @Post('schedule')
  async scheduleInterview(@Body() scheduleInterviewDto: ScheduleInterviewDto) {
    return this.interviewsService.scheduleInterview(scheduleInterviewDto);
  }

  @Get(':id')
  async getInterviewById(@Param('id') id: string) {
    return this.interviewsService.getInterviewById(+id);
  }
}
Enter fullscreen mode Exit fullscreen mode

Interviews Service (interviews/interviews.service.ts)

The service contains business logic for scheduling interviews, managing interview data, and fetching scheduled interviews.

// src/interviews/interviews.service.ts

import { Injectable } from '@nestjs/common';
import { PrismaService } from 'src/lib/prisma.service';
import { ScheduleInterviewDto } from './dto/schedule-interview.dto';

@Injectable()
export class InterviewsService {
  constructor(private prisma: PrismaService) {}

  async scheduleInterview(scheduleInterviewDto: ScheduleInterviewDto) {
    return this.prisma.interview.create({
      data: {
        candidateId: scheduleInterviewDto.candidateId,
        jobId: scheduleInterviewDto.jobId,
        interviewDate: new Date(scheduleInterviewDto.interviewDate),
        interviewType: scheduleInterviewDto.interviewType,
      },
    });
  }

  async getInterviewById(id: number) {
    return this.prisma.interview.findUnique({ where: { id } });
  }
}
Enter fullscreen mode Exit fullscreen mode

Interviews Module (interviews/interviews.module.ts)

This module declaration ties everything together for the Interviews module.

// src/interviews/interviews.module.ts

import { Module } from '@nestjs/common';
import { InterviewsService } from './interviews.service';
import { InterviewsController } from './interviews.controller';
import { PrismaService } from 'src/lib/prisma.service';

@Module({
  controllers: [InterviewsController],
  providers: [InterviewsService, PrismaService],
})
export class InterviewsModule {}
Enter fullscreen mode Exit fullscreen mode

Prisma Schema (prisma/schema.prisma)

Here’s how the Prisma schema would look for defining jobs, emails, and interviews.

// prisma/schema.prisma

model Job {
  id          Int      @id @default(autoincrement())
  title       String
  description String
  status      String   @default("Open")
  createdAt   DateTime @default(now())
  interviews  Interview[]
}

model Interview {
  id            Int      @id @default(autoincrement())
  candidateId   String
  jobId         Int
  interviewDate DateTime
  interviewType String
  job           Job      @relation(fields: [jobId], references: [id])
}

model Email {
  id        Int      @id @default(autoincrement())
  recipient String
  subject   String
  message   String
  status    String   @default("Sent") // Status: Sent, Opened, Clicked
  sentAt    DateTime @default(now())
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup provides fully functional Metrics and Interviews modules in NestJS, integrated with Prisma and PostgreSQL. The Metrics module can calculate and return various recruitment-related metrics, while the Interviews module allows scheduling and managing job interviews. You can further extend this implementation by adding additional analytics, reports, and interview notifications.

Here's the complete implementation for the Payments module with Stripe integration and Prisma module for database management in a NestJS application. This includes DTOs, controllers, services, and Prisma configuration for both payments and database management.

Folder Structure for Payments and Prisma Modules

/project-backend
├── /src
│   ├── /payments               # Payments module for Stripe integration
│   │   ├── /dto
│   │   │   └── payment.dto.ts   # DTO for handling payment payloads
│   │   ├── payments.controller.ts # Controller for handling subscription payments
│   │   ├── payments.service.ts  # Service to integrate Stripe for subscription management
│   │   └── payments.module.ts   # Payments module declaration
│   │
│   ├── /prisma                 # Prisma module for database management
│   │   ├── prisma.module.ts     # Prisma module for database connection
│   │   ├── prisma.service.ts    # Prisma service (initialize and handle DB operations)
│   │   └── schema.prisma        # Prisma schema file for defining DB structure
├── /prisma
│   └── schema.prisma            # Prisma schema for database models (Users, Payments, Subscriptions)
└── .env                         # Environment variables (Stripe keys, DB connection)
Enter fullscreen mode Exit fullscreen mode

Payments Module with Stripe Integration

The Payments module will handle subscription management via Stripe. This module will allow users to make payments, subscribe to plans, and manage their subscriptions.

Payment DTO (payments/dto/payment.dto.ts)

This DTO defines the data required to process a payment.

// src/payments/dto/payment.dto.ts

import { IsString, IsNotEmpty, IsNumber } from 'class-validator';

export class PaymentDto {
  @IsString()
  @IsNotEmpty()
  token: string; // Stripe token or payment method ID

  @IsNumber()
  @IsNotEmpty()
  amount: number;

  @IsString()
  @IsNotEmpty()
  currency: string; // e.g., 'usd'
}
Enter fullscreen mode Exit fullscreen mode

Payments Controller (payments/payments.controller.ts)

The controller handles payment-related routes, such as creating payments and managing subscriptions.

// src/payments/payments.controller.ts

import { Controller, Post, Body, HttpException, HttpStatus } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { PaymentDto } from './dto/payment.dto';

@Controller('payments')
export class PaymentsController {
  constructor(private readonly paymentsService: PaymentsService) {}

  @Post('create')
  async createPayment(@Body() paymentDto: PaymentDto) {
    try {
      const payment = await this.paymentsService.createPayment(paymentDto);
      return payment;
    } catch (error) {
      throw new HttpException('Payment Failed', HttpStatus.BAD_REQUEST);
    }
  }

  @Post('subscribe')
  async createSubscription(@Body('email') email: string, @Body('planId') planId: string) {
    try {
      return this.paymentsService.createSubscription(email, planId);
    } catch (error) {
      throw new HttpException('Subscription Failed', HttpStatus.BAD_REQUEST);
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Payments Service (payments/payments.service.ts)

The service contains the logic to handle Stripe payments and subscriptions.

// src/payments/payments.service.ts

import { Injectable } from '@nestjs/common';
import { PaymentDto } from './dto/payment.dto';
import Stripe from 'stripe';

@Injectable()
export class PaymentsService {
  private stripe: Stripe;

  constructor() {
    this.stripe = new Stripe(process.env.STRIPE_SECRET_KEY, {
      apiVersion: '2022-08-01',
    });
  }

  // Create a Stripe payment charge
  async createPayment(paymentDto: PaymentDto) {
    const { token, amount, currency } = paymentDto;

    const charge = await this.stripe.charges.create({
      amount: amount * 100, // Stripe uses smallest currency unit
      currency: currency,
      source: token,
      description: 'Payment for subscription',
    });

    if (charge.status !== 'succeeded') {
      throw new Error('Payment failed');
    }

    return {
      success: true,
      message: 'Payment successful',
      chargeId: charge.id,
    };
  }

  // Create a subscription for a customer
  async createSubscription(email: string, planId: string) {
    const customer = await this.stripe.customers.create({
      email: email,
    });

    const subscription = await this.stripe.subscriptions.create({
      customer: customer.id,
      items: [
        {
          plan: planId, // Plan ID from Stripe dashboard
        },
      ],
    });

    return {
      success: true,
      message: 'Subscription created successfully',
      subscriptionId: subscription.id,
    };
  }
}
Enter fullscreen mode Exit fullscreen mode

Payments Module (payments/payments.module.ts)

The module declaration ties everything together for the Payments module.

// src/payments/payments.module.ts

import { Module } from '@nestjs/common';
import { PaymentsService } from './payments.service';
import { PaymentsController } from './payments.controller';

@Module({
  controllers: [PaymentsController],
  providers: [PaymentsService],
})
export class PaymentsModule {}
Enter fullscreen mode Exit fullscreen mode

Prisma Module for Database Management

The Prisma module is responsible for managing the database connection and operations for all the models (e.g., Users, Payments, Subscriptions).

Prisma Module (prisma/prisma.module.ts)

The Prisma module sets up the Prisma service that handles database connections and operations.

// src/prisma/prisma.module.ts

import { Module } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
Enter fullscreen mode Exit fullscreen mode

Prisma Service (prisma/prisma.service.ts)

The Prisma service contains the logic to initialize and handle the database connection.

// src/prisma/prisma.service.ts

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { PrismaClient } from '@prisma/client';

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }

  async onModuleDestroy() {
    await this.$disconnect();
  }
}
Enter fullscreen mode Exit fullscreen mode

Prisma Schema (prisma/schema.prisma)

This schema defines the models for Users, Payments, and Subscriptions.

// prisma/schema.prisma

datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  password  String
  name      String?
  createdAt DateTime  @default(now())
  subscriptions Subscription[]
}

model Subscription {
  id           Int       @id @default(autoincrement())
  userId       Int
  planId       String
  status       String     @default("active")
  createdAt    DateTime   @default(now())
  user         User       @relation(fields: [userId], references: [id])
}

model Payment {
  id           Int       @id @default(autoincrement())
  userId       Int
  amount       Float
  currency     String
  chargeId     String
  status       String     @default("completed")
  createdAt    DateTime   @default(now())
  user         User       @relation(fields: [userId], references: [id])
}
Enter fullscreen mode Exit fullscreen mode

Environment Variables (.env)

Include environment variables for Stripe API keys and PostgreSQL connection.

DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
STRIPE_SECRET_KEY=your_stripe_secret_key
Enter fullscreen mode Exit fullscreen mode

Project Dependencies (package.json)

Ensure you have the necessary dependencies for NestJS, Stripe, and Prisma in your project.

{
  "name": "project-backend",
  "version": "1.0.0",
  "scripts": {
    "start": "nest start",
    "start:dev": "nest start --watch",
    "prisma:generate": "prisma generate",
    "prisma:migrate": "prisma migrate dev"
  },
  "dependencies": {
    "@nestjs/common": "^8.0.0",
    "@nestjs/core": "^8.0.0",
    "@nestjs/jwt": "^8.0.0",
    "@nestjs/passport": "^8.0.0",
    "stripe": "^8.0.0",
    "prisma": "^3.0.0",
    "@prisma/client": "^3.0.0",
    "dotenv": "^8.0.0",
    "reflect-metadata": "^0.1.13",
    "rxjs": "^7.0.0"
  }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup provides a fully functional Payments module integrated with Stripe for handling subscription payments and regular payments. The Prisma module is configured to manage the database connection and handle entities such as Users, Payments, and Subscriptions. This can be extended with additional features such as handling webhook events from Stripe, managing refunds, or expanding subscription

tiers.

Here’s the complete implementation for the Common Module, which includes Guards, Interceptors, and Utility functions, along with the Root Module (app.module.ts), Application entry point (main.ts), and Environment variable validation (env.validation.ts).

Folder Structure

/project-backend
├── /src
│   ├── /common                 # Common module (utilities, guards, interceptors)
│   │   ├── /guards              # Authorization guards (e.g., RoleGuard, JwtAuthGuard)
│   │   ├── /interceptors        # Custom interceptors for logging or transformation
│   │   └── /utils               # Shared utility functions (e.g., helper functions)
│   │
│   ├── app.module.ts            # Root module that imports and registers all other modules
│   ├── main.ts                  # Application entry point
│   └── env.validation.ts        # Environment variable validation schema
└── .env                         # Environment variables (DB connection, JWT secret, etc.)
Enter fullscreen mode Exit fullscreen mode

Guards

JWT Auth Guard (common/guards/jwt-auth.guard.ts)

The JWT Auth Guard is used to protect routes that require authentication.

// src/common/guards/jwt-auth.guard.ts

import { Injectable, ExecutionContext, UnauthorizedException } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {
  constructor(private jwtService: JwtService) {
    super();
  }

  canActivate(context: ExecutionContext) {
    const request = context.switchToHttp().getRequest();
    const token = request.headers.authorization?.split(' ')[1];

    if (!token) {
      throw new UnauthorizedException('Missing token');
    }

    try {
      this.jwtService.verify(token);
    } catch (error) {
      throw new UnauthorizedException('Invalid token');
    }

    return super.canActivate(context);
  }
}
Enter fullscreen mode Exit fullscreen mode

Role Guard (common/guards/role.guard.ts)

The Role Guard checks if the authenticated user has the required role to access a particular route.

// src/common/guards/role.guard.ts

import { Injectable, CanActivate, ExecutionContext, ForbiddenException } from '@nestjs/common';
import { Reflector } from '@nestjs/core';

@Injectable()
export class RoleGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const requiredRoles = this.reflector.get<string[]>('roles', context.getHandler());
    if (!requiredRoles) {
      return true;
    }

    const request = context.switchToHttp().getRequest();
    const user = request.user; // Assuming the JWT auth guard already attached the user object to the request

    if (!requiredRoles.includes(user.role)) {
      throw new ForbiddenException('You do not have permission to access this resource');
    }

    return true;
  }
}
Enter fullscreen mode Exit fullscreen mode

Interceptors

Logging Interceptor (common/interceptors/logging.interceptor.ts)

The Logging Interceptor is used to log the incoming request and outgoing response times.

// src/common/interceptors/logging.interceptor.ts

import { Injectable, NestInterceptor, ExecutionContext, CallHandler } from '@nestjs/common';
import { Observable } from 'rxjs';
import { tap } from 'rxjs/operators';

@Injectable()
export class LoggingInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<any> {
    const now = Date.now();
    const request = context.switchToHttp().getRequest();
    const method = request.method;
    const url = request.url;

    return next.handle().pipe(
      tap(() => console.log(`${method} ${url} - ${Date.now() - now}ms`)),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Utility Functions

Helper Utility (common/utils/helper.util.ts)

Utility functions for common tasks.

// src/common/utils/helper.util.ts

export function formatResponse(data: any) {
  return {
    success: true,
    data,
  };
}

export function handleError(error: any) {
  console.error(error);
  return {
    success: false,
    message: 'Something went wrong!',
  };
}
Enter fullscreen mode Exit fullscreen mode

Root Module

App Module (app.module.ts)

The root module of the application, which imports and registers all other modules.

// src/app.module.ts

import { Module } from '@nestjs/common';
import { AuthModule } from './auth/auth.module';
import { UsersModule } from './users/users.module';
import { JobsModule } from './jobs/jobs.module';
import { PaymentsModule } from './payments/payments.module';
import { MetricsModule } from './metrics/metrics.module';
import { InterviewsModule } from './interviews/interviews.module';
import { PrismaModule } from './prisma/prisma.module';

@Module({
  imports: [
    AuthModule,
    UsersModule,
    JobsModule,
    PaymentsModule,
    MetricsModule,
    InterviewsModule,
    PrismaModule,
  ],
})
export class AppModule {}
Enter fullscreen mode Exit fullscreen mode

Application Entry Point

Main File (main.ts)

The entry point for the application, where NestFactory is used to create the app and apply global guards, interceptors, or middleware if necessary.

// src/main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { ValidationPipe } from '@nestjs/common';
import { LoggingInterceptor } from './common/interceptors/logging.interceptor';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);

  // Global validation pipe for handling validation via DTOs
  app.useGlobalPipes(new ValidationPipe());

  // Global logging interceptor for logging requests
  app.useGlobalInterceptors(new LoggingInterceptor());

  await app.listen(3000);
}

bootstrap();
Enter fullscreen mode Exit fullscreen mode

Environment Variable Validation

Environment Validation Schema (env.validation.ts)

This ensures that required environment variables are set, using Joi for validation.

// src/env.validation.ts

import * as Joi from 'joi';

export const envValidationSchema = Joi.object({
  DATABASE_URL: Joi.string().required(),
  JWT_SECRET: Joi.string().required(),
  STRIPE_SECRET_KEY: Joi.string().required(),
  EMAIL_USER: Joi.string().required(),
  EMAIL_PASSWORD: Joi.string().required(),
});
Enter fullscreen mode Exit fullscreen mode

Environment Variables (.env)

You can define the environment variables in the .env file:

DATABASE_URL=postgresql://user:password@localhost:5432/mydatabase
JWT_SECRET=my_super_secret_jwt_key
STRIPE_SECRET_KEY=your_stripe_secret_key
EMAIL_USER=your-email@gmail.com
EMAIL_PASSWORD=your-email-password
Enter fullscreen mode Exit fullscreen mode

Conclusion

This setup provides a robust structure for common utilities (guards, interceptors, and utility functions), a root module to register all application modules, a main entry point to bootstrap the NestJS app, and environment variable validation using Joi. This structure ensures maintainability, scalability, and proper security practices with JWT authentication and role-based access control.

If you enjoy my content and would like to support my work, you can buy me a coffee. Your support is greatly appreciated!

Disclaimer: This content is generated by AI.

💖 💪 🙅 🚩
nadim_ch0wdhury
Nadim Chowdhury

Posted on October 19, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related