Skip to content

Commit 49c6c57

Browse files
feat(lesson-24): implement API integration for program management
1 parent 9bc31a4 commit 49c6c57

File tree

7 files changed

+380
-0
lines changed

7 files changed

+380
-0
lines changed
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
{
2+
"name": "my-app",
3+
"private": true,
4+
"version": "0.0.0",
5+
"type": "module",
6+
"scripts": {
7+
"build:deps": "cd ../../types && npm install && npm run build",
8+
"dev": "npm run build:deps && vite",
9+
"build": "npm run build:deps && tsc && vite build",
10+
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
11+
"fix": "prettier --write .",
12+
"preview": "npm run build:deps && vite preview",
13+
"test": "node --experimental-vm-modules node_modules/.bin/jest",
14+
"cy:open": "cypress open"
15+
},
16+
"dependencies": {
17+
"@code-differently/types": "file:../../types",
18+
"@tanstack/react-query": "^5.62.7",
19+
"react": "^19.0.0",
20+
"react-dom": "^19.0.0",
21+
"react-router-dom": "^7.0.2"
22+
},
23+
"devDependencies": {
24+
"@stylistic/eslint-plugin": "^2.12.1",
25+
"@testing-library/dom": "^10.4.0",
26+
"@testing-library/react": "^16.1.0",
27+
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
28+
"@types/jest": "^29.5.14",
29+
"@types/react": "^19.0.1",
30+
"@types/react-dom": "^19.0.2",
31+
"@typescript-eslint/eslint-plugin": "^8.18.0",
32+
"@typescript-eslint/parser": "^8.18.0",
33+
"@vitejs/plugin-react": "^4.3.4",
34+
"cypress": "^13.17.0",
35+
"eslint": "^9.17.0",
36+
"eslint-config-prettier": "^9.1.0",
37+
"eslint-plugin-react-hooks": "^5.1.0",
38+
"eslint-plugin-react-refresh": "^0.4.16",
39+
"identity-obj-proxy": "^3.0.0",
40+
"jest": "^29.7.0",
41+
"jest-environment-jsdom": "^29.7.0",
42+
"jest-fixed-jsdom": "^0.0.9",
43+
"prettier": "3.4.2",
44+
"sass": "^1.83.0",
45+
"ts-jest": "^29.2.5",
46+
"typescript": "^5.7.2",
47+
"vite": "^6.0.3"
48+
}
49+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import './Header.scss';
2+
import logoImg from '@/assets/logo.png';
3+
import * as React from 'react';
4+
import {Link} from 'react-router-dom';
5+
6+
export const Header: React.FC = () => {
7+
return (
8+
<header className="header">
9+
<div className="header-logo">
10+
<Link to="/">
11+
<img src={logoImg} alt="Code Differently Logo" />
12+
</Link>
13+
</div>
14+
<ul className="header-top-menu">
15+
<li>
16+
<Link to="/">Home</Link>
17+
</li>
18+
<li>
19+
<Link to="/add-program">Add Program</Link>
20+
</li>
21+
<li>
22+
<a href="#">About</a>
23+
</li>
24+
<li>
25+
<a href="#">Contact</a>
26+
</li>
27+
</ul>
28+
<div className="header-cta">
29+
<a className="sign-up-button" href="#">
30+
Sign Up
31+
</a>
32+
</div>
33+
</header>
34+
);
35+
};
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
import App from './App.tsx';
2+
import {AddProgram} from './pages/AddProgram';
3+
import {Home} from './pages/Home/Home.tsx';
4+
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
5+
import React from 'react';
6+
import ReactDOM from 'react-dom/client';
7+
import {RouterProvider, createBrowserRouter} from 'react-router-dom';
8+
9+
import './index.scss';
10+
11+
const queryClient = new QueryClient();
12+
13+
const router = createBrowserRouter([
14+
{
15+
path: '/',
16+
element: <App />,
17+
children: [
18+
{
19+
path: '/',
20+
element: <Home />,
21+
},
22+
{
23+
path: '/add-program',
24+
element: <AddProgram />,
25+
},
26+
],
27+
},
28+
]);
29+
30+
ReactDOM.createRoot(document.getElementById('root')!).render(
31+
<React.StrictMode>
32+
<QueryClientProvider client={queryClient}>
33+
<RouterProvider router={router} />
34+
</QueryClientProvider>
35+
</React.StrictMode>
36+
);
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
.add-program-page {
2+
padding: 2rem 0;
3+
max-width: 800px;
4+
margin: 0 auto;
5+
}
6+
7+
.add-program-section {
8+
background: white;
9+
padding: 2rem;
10+
border-radius: 8px;
11+
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
12+
}
13+
14+
.add-program-section h2 {
15+
margin-bottom: 2rem;
16+
text-align: center;
17+
font-size: 2rem;
18+
color: #333;
19+
}
20+
21+
.highlight {
22+
color: #e74c3c;
23+
}
24+
25+
.add-program-form {
26+
display: flex;
27+
flex-direction: column;
28+
gap: 1.5rem;
29+
}
30+
31+
.form-group {
32+
display: flex;
33+
flex-direction: column;
34+
gap: 0.5rem;
35+
}
36+
37+
.form-group label {
38+
font-weight: bold;
39+
color: #333;
40+
font-size: 1.1rem;
41+
}
42+
43+
.form-group input,
44+
.form-group textarea {
45+
padding: 0.75rem;
46+
border: 2px solid #ddd;
47+
border-radius: 4px;
48+
font-size: 1rem;
49+
transition: border-color 0.3s ease;
50+
}
51+
52+
.form-group input:focus,
53+
.form-group textarea:focus {
54+
outline: none;
55+
border-color: #e74c3c;
56+
}
57+
58+
.form-group textarea {
59+
resize: vertical;
60+
min-height: 120px;
61+
font-family: inherit;
62+
}
63+
64+
.form-actions {
65+
display: flex;
66+
gap: 1rem;
67+
justify-content: center;
68+
margin-top: 1rem;
69+
}
70+
71+
.submit-button,
72+
.cancel-button {
73+
padding: 0.75rem 2rem;
74+
border: none;
75+
border-radius: 4px;
76+
font-size: 1rem;
77+
font-weight: bold;
78+
cursor: pointer;
79+
transition: background-color 0.3s ease;
80+
}
81+
82+
.submit-button {
83+
background-color: #e74c3c;
84+
color: white;
85+
}
86+
87+
.submit-button:hover:not(:disabled) {
88+
background-color: #c0392b;
89+
}
90+
91+
.submit-button:disabled {
92+
background-color: #bdc3c7;
93+
cursor: not-allowed;
94+
}
95+
96+
.cancel-button {
97+
background-color: #95a5a6;
98+
color: white;
99+
}
100+
101+
.cancel-button:hover {
102+
background-color: #7f8c8d;
103+
}
104+
105+
@media (max-width: 768px) {
106+
.add-program-page {
107+
padding: 1rem;
108+
}
109+
110+
.add-program-section {
111+
padding: 1.5rem;
112+
}
113+
114+
.form-actions {
115+
flex-direction: column;
116+
}
117+
}
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
import './AddProgram.scss';
2+
3+
import {Program} from '@code-differently/types';
4+
import {useMutation, useQueryClient} from '@tanstack/react-query';
5+
import React, {useState} from 'react';
6+
import {useNavigate} from 'react-router-dom';
7+
8+
const addProgram = async (program: Omit<Program, 'id'>): Promise<void> => {
9+
const response = await fetch('http://localhost:4000/programs', {
10+
method: 'POST',
11+
headers: {
12+
'Content-Type': 'application/json',
13+
},
14+
body: JSON.stringify(program),
15+
});
16+
17+
if (!response.ok) {
18+
throw new Error('Failed to add program');
19+
}
20+
};
21+
22+
export const AddProgram: React.FC = () => {
23+
const [title, setTitle] = useState('');
24+
const [description, setDescription] = useState('');
25+
const navigate = useNavigate();
26+
const queryClient = useQueryClient();
27+
28+
const mutation = useMutation({
29+
mutationFn: addProgram,
30+
onSuccess: () => {
31+
// Invalidate and refetch the programs list
32+
queryClient.invalidateQueries({ queryKey: ['programs'] });
33+
// Navigate back to home page
34+
navigate('/');
35+
},
36+
onError: (error) => {
37+
console.error('Error adding program:', error);
38+
alert('Failed to add program. Please try again.');
39+
},
40+
});
41+
42+
const handleSubmit = (e: React.FormEvent) => {
43+
e.preventDefault();
44+
45+
if (!title.trim() || !description.trim()) {
46+
alert('Please fill in both title and description');
47+
return;
48+
}
49+
50+
mutation.mutate({ title: title.trim(), description: description.trim() });
51+
};
52+
53+
return (
54+
<article className="add-program-page">
55+
<section className="add-program-section">
56+
<h2>
57+
Add a New <em className="highlight">Program</em>
58+
</h2>
59+
<form onSubmit={handleSubmit} className="add-program-form">
60+
<div className="form-group">
61+
<label htmlFor="title">Program Title:</label>
62+
<input
63+
type="text"
64+
id="title"
65+
value={title}
66+
onChange={(e) => setTitle(e.target.value)}
67+
placeholder="Enter program title"
68+
required
69+
/>
70+
</div>
71+
72+
<div className="form-group">
73+
<label htmlFor="description">Program Description:</label>
74+
<textarea
75+
id="description"
76+
value={description}
77+
onChange={(e) => setDescription(e.target.value)}
78+
placeholder="Enter program description"
79+
rows={5}
80+
required
81+
/>
82+
</div>
83+
84+
<div className="form-actions">
85+
<button
86+
type="submit"
87+
disabled={mutation.isPending}
88+
className="submit-button"
89+
>
90+
{mutation.isPending ? 'Adding...' : 'Add Program'}
91+
</button>
92+
<button
93+
type="button"
94+
onClick={() => navigate('/')}
95+
className="cancel-button"
96+
>
97+
Cancel
98+
</button>
99+
</div>
100+
</form>
101+
</section>
102+
</article>
103+
);
104+
};
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export {AddProgram} from './AddProgram';
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import './ProgramList.scss';
2+
3+
import {Program} from '../Program';
4+
import {Program as ProgramType} from '@code-differently/types';
5+
import {useQuery} from '@tanstack/react-query';
6+
7+
const fetchPrograms = async (): Promise<ProgramType[]> => {
8+
const response = await fetch('http://localhost:4000/programs');
9+
if (!response.ok) {
10+
throw new Error('Failed to fetch programs');
11+
}
12+
return response.json();
13+
};
14+
15+
export const ProgramList: React.FC = () => {
16+
const { data: programs, isLoading, error } = useQuery({
17+
queryKey: ['programs'],
18+
queryFn: fetchPrograms,
19+
});
20+
21+
if (isLoading) {
22+
return <div>Loading programs...</div>;
23+
}
24+
25+
if (error) {
26+
return <div>Error loading programs: {error.message}</div>;
27+
}
28+
29+
return (
30+
<ul className="programs">
31+
{programs?.map((program) => (
32+
<Program key={program.id} title={program.title}>
33+
<p>{program.description}</p>
34+
</Program>
35+
))}
36+
</ul>
37+
);
38+
};

0 commit comments

Comments
 (0)