How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4

How to Build a Full-Stack Demo App for Interviews with Laravel + React (Dockerized) – Part 1

Building a Full-Stack Demo App for Interviews (Laravel + React, Dockerized) – Part 2

How to Build a Full-Stack Demo App for Interviews with Laravel + React (…


This content originally appeared on DEV Community and was authored by pirvanm

How to Build a Full-Stack Demo App for Interviews with Laravel + React (Dockerized) – Part 1

Building a Full-Stack Demo App for Interviews (Laravel + React, Dockerized) – Part 2

How to Build a Full-Stack Demo App for Interviews with Laravel + React (Dockerized) – Part 3

This is the second main Part B, and Part 4 of the full series. You can access the previous parts above. I think I’m speaking more like someone recording a personal journal — maybe with a bit too much imagination.

Let’s start coding! Just type this command in the terminal. (Of course, this is a Linux terminal — for Windows users, you can use Cmder or Laragon, by the way.)npm create vite@latest my-app

Then I selected the React framework for the lightweight version, and chose TypeScript — because it seems TypeScript is loved even more than Go now.

After pressing Enter in the terminal a few times, I ended up with this familiar JavaScript package manager file: package.json.

{
  "name": "my-app",
  "private": true,
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^19.1.1",
    "react-dom": "^19.1.1"
  },
  "devDependencies": {
    "@eslint/js": "^9.33.0",
    "@types/react": "^19.1.10",
    "@types/react-dom": "^19.1.7",
    "@vitejs/plugin-react": "^5.0.0",
    "eslint": "^9.33.0",
    "eslint-plugin-react-hooks": "^5.2.0",
    "eslint-plugin-react-refresh": "^0.4.20",
    "globals": "^16.3.0",
    "typescript": "~5.8.3",
    "typescript-eslint": "^8.39.1",
    "vite": "^7.1.2"
  }
}

Corrected:
In my package.json, at the end I have:

First, checking one by one:

In "scripts":

Next, in "dependencies", I added:

"@tanstack/react-query": "^5.84.1",
"axios": "^1.11.0",
"react-router": "^7.7.1"

And in the last step for packages, I just added one line in "scripts":

"test": "vitest"

In "devDependencies", I added:

"vitest": "^3.2.4"

After that, I think all the magic happens in frontend/src/, which is in the same folder as the previous backend folder.

In frontend/src
/.env
I get it, but I think this is more logical in Docker, which I’ll talk about later — maybe in the next part.

frontend/src
/main.tsx before i start overwrite this i have :

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <App />
  </StrictMode>,
)

After some work, some ambition — maybe too much work...

import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { BrowserRouter } from 'react-router'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'

const queryClient = new QueryClient();

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <BrowserRouter>
      <QueryClientProvider client={queryClient}>
        <App />
      </QueryClientProvider>
    </BrowserRouter>,
  </StrictMode>,
)

Maybe it’s because I’m old — but not that old — I’m still a fan of ESLint config. So I made this one for formatting, in the eslint.config.js file:

import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { globalIgnores } from 'eslint/config'

export default tseslint.config([
  globalIgnores(['dist']),
  {
    files: ['**/*.{ts,tsx}'],
    extends: [
      js.configs.recommended,
      tseslint.configs.recommended,
      reactHooks.configs['recommended-latest'],
      reactRefresh.configs.vite,
    ],
    languageOptions: {
      ecmaVersion: 2020,
      globals: globals.browser,
    },
  },
])

Some basic HTML for the main page: index.html

<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>

After that, to run tests properly, I created a file setupTests.ts with the following code:

import '@testing-library/jest-dom';

As the next sub-stage of Stage B, I made a pages folder inside src, like this:

src
└── pages/

Inside it i have AUth, Home, MentorProfile
In Auth , more precise on :
frontend/src/pages/Auth
/Login.tsx

const Login: React.FC = () => {
  return (
    <div>

    </div>
  );
}

export default Login;

On register :

const Register: React.FC = () => {
  return (
    <div>

    </div>
  );
}

export default Register;

One step closer to finishing this part, we go to Home.tsx

Path: frontend/src/pages/Home/Home.tsx
And the following code:

import MentorList from "../../features/mentors/MentorsList";

const Home: React.FC = () => {
  return (
    <div>
        <header className="page-header">
            <h1>Find a Mentor</h1>
            <p>Get guidance from experienced developers, designers, and leaders.</p>
        </header>
        <MentorList />
    </div>
  );
}

export default Home;

And on the latest step of this part 4 of series, we made a new folder

src/pages
/MentorProfile/

Some CSS for beginners in web development — boring or seemingly useless stuff, but still necessary:

.back-link {
    display: inline-block;
    margin-bottom: 1.5rem;
    color: #475569;
    text-decoration: none;
    font-size: 0.95rem;
}

.back-link:hover {
    color: #1e293b;
}

.mentor-header {
    display: flex;
    gap: 1.5rem;
    align-items: flex-start;
    margin-bottom: 1.5rem;
}

.mentor-avatar {
    width: 80px;
    height: 80px;
    border-radius: 50%;
    object-fit: cover;
}

.mentor-info {
    flex: 1;
}

.mentor-name {
    font-size: 1.8rem;
    font-weight: 600;
    color: #1e293b;
}

.mentor-title {
    font-size: 1rem;
    color: #475569;
    margin: 0.25rem 0;
}

.mentor-expertise {
    font-size: 0.95rem;
    color: #64748b;
}

.mentor-bio {
    line-height: 1.7;
    color: #334155;
    margin-bottom: 2rem;
}

.mentor-bio h2 {
    font-size: 1.3rem;
    margin: 1.5rem 0 1rem;
    color: #1e293b;
}

.mentor-bio p {
    margin-bottom: 1rem;
}

.mentor-requirements {
    background-color: #f8fafc;
    border: 1px solid #e2e8f0;
    border-radius: 0.5rem;
    padding: 1.25rem;
    margin-bottom: 2rem;
    font-size: 0.95rem;
    color: #475569;
}

.mentor-requirements h3 {
    font-size: 1.1rem;
    color: #1e293b;
    margin-bottom: 0.75rem;
}

.apply-section {
    text-align: center;
    margin-top: 1rem;
}

.btn-apply {
    display: inline-block;
    background-color: #4f46e5;
    color: white;
    padding: 0.75rem 2rem;
    font-size: 1rem;
    font-weight: 500;
    text-decoration: none;
    border-radius: 0.5rem;
    transition: background-color 0.2s;
}

.btn-apply:hover {
    background-color: #4338ca;
}

@media (max-width: 640px) {
    .mentor-header {
        flex-direction: column;
        align-items: center;
        text-align: center;
    }
    .mentor-name {
        font-size: 1.6rem;
    }
}

/MentorProfile.module.css

In the next file: frontend/src/pages/MentorProfile/MentorProfile.test.tsx

i got

import { render, screen } from '@testing-library/react';
import MentorProfile from './MentorProfile';
import { useMentor } from '../../api/queries/mentors/useMentor';
import { MemoryRouter } from 'react-router';


vi.mock('../../api/queries/mentors/useMentor');

describe('MentorProfile', () => {
  test('shows skeleton when loading', () => {
    (useMentor as jest.Mock).mockReturnValue({
      data: null,
      isLoading: true,
      error: null,
    });

    render(<MentorProfile />);

    expect(screen.getByTestId('skeleton-avatar')).toBeInTheDocument();
  });

  test('shows error component when error occurs', () => {
    (useMentor as jest.Mock).mockReturnValue({
      data: null,
      isLoading: false,
      error: new Error('Failed to fetch'),
    });

    render(<MemoryRouter>
        <MentorProfile />
    </MemoryRouter>);

    expect(screen.getByText('Whoops!')).toBeInTheDocument();
  });

  test('renders full profile when data is loaded', () => {
    (useMentor as jest.Mock).mockReturnValue({
      data: {
        id: 1,
        fullName: 'Sarah Chen',
        title: 'Senior Frontend Engineer',
        expertise: ['React', 'TypeScript'],
        bio: 'I love mentoring.',
        technicalBio: 'I love mentoring.',
        mentoringStyle: 'I love mentoring.',
        audience: 'I love mentoring.',
        availability: 'open',
      },
      isLoading: false,
      error: null,
    });

    render(<MemoryRouter>
        <MentorProfile />
    </MemoryRouter>);

    expect(screen.getByText('Sarah Chen')).toBeInTheDocument();
    expect(screen.getByText('Senior Frontend Engineer')).toBeInTheDocument();
    expect(screen.getByText('REACT • TYPESCRIPT')).toBeInTheDocument();
    expect(screen.getByText('Apply to Be My Mentee')).toBeInTheDocument();
  });
});

On MentorProfile.tsx

import { Link, useParams } from 'react-router';
import styles from './MentorProfile.module.css'
import { useMentor } from '../../api/queries/mentors/useMentor';
import SkeletonMentorProfile from './SkeletonMentorProfile';
import Error from '../../shared/ui/Error/Error';

const MentorProfile: React.FC = () => {
  const { id }  = useParams<{id: string}>();

  const { data: mentor, isLoading, error } = useMentor(parseInt(id ?? '0'));

  if (isLoading) {
    return (
      <SkeletonMentorProfile />
    )
  }

  if (error) return <Error />;

  return (
    <main className={styles['mentor-detail']}>
      <Link to={'/'} className={styles['back-link']}>&larr; Back to mentors</Link>


      <div className={styles['mentor-header']}>
        <img src={mentor?.avatar ?? 'https://randomuser.me/api/portraits/women/45.jpg'} alt={mentor?.fullName} className={styles['mentor-avatar']} />
        <div className={styles['mentor-info']}>
          <h1 className={styles['mentor-name']}>{mentor?.fullName}</h1>
          <div className={styles['mentor-title']}>{mentor?.title}</div>
          <div className={styles['mentor-expertise']}>{mentor?.expertise.slice(0, 3).join(' • ').toUpperCase()}</div>
        </div>
      </div>

      <section className={styles['mentor-bio']}>
        <h2>About {mentor?.fullName}</h2>
        <p>
          {mentor?.bio}
        </p>

        <h2>What I Can Help With</h2>
        <p>
          {mentor?.technicalBio}
        </p>

        <h2>My Mentoring Style</h2>
        <p>
            {mentor?.mentoringStyle}
        </p>
      </section>

      <section className={styles['mentor-requirements']}>
        <h3>Who I'm Looking For</h3>
        <p>
          {mentor?.audience}
        </p>
      </section>

      <div className={styles['apply-section']}>
        <Link to={`/mentor/${id}`} className={styles['btn-apply']}>
          Apply to Be My Mentee
        </Link>
      </div>
    </main>
  );
}

export default MentorProfile;

In frontend/src/pages/MentorProfile/SkeletonMentorProfile.module.css

.back-link {
  display: inline-block;
  width: 120px;
  height: 16px;
  background: #e2e8f0;
  border-radius: 0.375rem;
  margin-bottom: 1.5rem;
  animation: pulse 2s infinite ease-in-out;
}

.mentor-header {
  display: flex;
  gap: 1.5rem;
  align-items: flex-start;
  margin-bottom: 1.5rem;
}

.mentor-avatar {
  width: 80px;
  height: 80px;
  border-radius: 50%;
  background: #e2e8f0;
  animation: pulse 2s infinite ease-in-out;
}

.mentor-info {
  flex: 1;
}

.mentor-name {
  width: 180px;
  height: 36px;
  background: #e2e8f0;
  border-radius: 0.375rem;
  margin: 0.25rem 0;
  animation: pulse 2s infinite ease-in-out;
  animation-delay: 0.2s;
}

.mentor-title {
  width: 200px;
  height: 18px;
  background: #f7fafc;
  border-radius: 0.375rem;
  margin: 0.25rem 0;
  animation: pulse 2s infinite ease-in-out;
  animation-delay: 0.4s;
}

.mentor-expertise {
  width: 160px;
  height: 16px;
  background: #f7fafc;
  border-radius: 0.375rem;
  margin-top: 0.5rem;
  animation: pulse 2s infinite ease-in-out;
  animation-delay: 0.6s;
}

.apply-section {
  text-align: center;
  margin-top: 1rem;
}

.btn-apply {
  display: inline-block;
  width: 200px;
  height: 48px;
  background: #e2e8f0;
  border-radius: 0.5rem;
  animation: pulse .3s infinite ease-in-out;
  animation-delay: 0.8s;
}

/* Pulse Animation */
@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.7; }
}

And the latest file for this part.

import styles from './SkeletonMentorProfile.module.css';

const SkeletonMentorProfile: React.FC = () => {
  return (
    <main className={styles['mentor-detail']}>
      <div className={styles['back-link']}></div>

      <div className={styles['mentor-header']}>
        <div className={styles['mentor-avatar']} data-testid="skeleton-avatar"/>
        <div className={styles['mentor-info']}>
          <div className={styles['mentor-name']} />
          <div className={styles['mentor-title']} />
          <div className={styles['mentor-expertise']} />
        </div>
      </div>

      <div className={styles['apply-section']}>
        <div className={styles['btn-apply']} />
      </div>
    </main>
  );
};

export default SkeletonMentorProfile;

I think this can be seen as a full-stack course for someone who wants to move from mid-level to senior. For more awesome courses, I invite you to check out my live React/Laravel project, hosted on DigitalOcean with GitHub Actions and Docker.
CourseCasts!


This content originally appeared on DEV Community and was authored by pirvanm


Print Share Comment Cite Upload Translate Updates
APA

pirvanm | Sciencx (2025-08-29T13:59:38+00:00) How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4. Retrieved from https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/

MLA
" » How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4." pirvanm | Sciencx - Friday August 29, 2025, https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/
HARVARD
pirvanm | Sciencx Friday August 29, 2025 » How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4., viewed ,<https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/>
VANCOUVER
pirvanm | Sciencx - » How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4. [Internet]. [Accessed ]. Available from: https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/
CHICAGO
" » How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4." pirvanm | Sciencx - Accessed . https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/
IEEE
" » How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4." pirvanm | Sciencx [Online]. Available: https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/. [Accessed: ]
rf:citation
» How do you make FullStack Demo App for Interviews using Laravel + React with Dockerize Part 4 | pirvanm | Sciencx | https://www.scien.cx/2025/08/29/how-do-you-make-fullstack-demo-app-for-interviews-using-laravel-react-with-dockerize-part-4/ |

Please log in to upload a file.




There are no updates yet.
Click the Upload button above to add an update.

You must be logged in to translate posts. Please log in or register.