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']}>← 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