Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 49 additions & 0 deletions lesson_24/mattieweathersby/template/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"name": "my-app",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"build:deps": "cd ../../types && npm install && npm run build",
"dev": "npm run build:deps && vite",
"build": "npm run build:deps && tsc && vite build",
"lint": "eslint . --report-unused-disable-directives --max-warnings 0",
"fix": "prettier --write .",
"preview": "npm run build:deps && vite preview",
"test": "node --experimental-vm-modules node_modules/.bin/jest",
"cy:open": "cypress open"
},
"dependencies": {
"@code-differently/types": "file:../../types",
"@tanstack/react-query": "^5.62.7",
"react": "^19.0.0",
"react-dom": "^19.0.0",
"react-router-dom": "^7.0.2"
},
"devDependencies": {
"@stylistic/eslint-plugin": "^2.12.1",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.1.0",
"@trivago/prettier-plugin-sort-imports": "^5.2.0",
"@types/jest": "^29.5.14",
"@types/react": "^19.0.1",
"@types/react-dom": "^19.0.2",
"@typescript-eslint/eslint-plugin": "^8.18.0",
"@typescript-eslint/parser": "^8.18.0",
"@vitejs/plugin-react": "^4.3.4",
"cypress": "^13.17.0",
"eslint": "^9.17.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^5.1.0",
"eslint-plugin-react-refresh": "^0.4.16",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fixed-jsdom": "^0.0.9",
"prettier": "3.4.2",
"sass": "^1.83.0",
"ts-jest": "^29.2.5",
"typescript": "^5.7.2",
"vite": "^6.0.3"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import './Header.scss';
import logoImg from '@/assets/logo.png';
import * as React from 'react';
import {Link} from 'react-router-dom';

export const Header: React.FC = () => {
return (
<header className="header">
<div className="header-logo">
<Link to="/">
<img src={logoImg} alt="Code Differently Logo" />
</Link>
</div>
<ul className="header-top-menu">
<li>
<Link to="/">Home</Link>
</li>
<li>
<Link to="/add-program">Add Program</Link>
</li>
<li>
<a href="#">About</a>
</li>
<li>
<a href="#">Contact</a>
</li>
</ul>
<div className="header-cta">
<a className="sign-up-button" href="#">
Sign Up
</a>
</div>
</header>
);
};
36 changes: 36 additions & 0 deletions lesson_24/mattieweathersby/template/src/main.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import App from './App.tsx';
import {AddProgram} from './pages/AddProgram';
import {Home} from './pages/Home/Home.tsx';
import {QueryClient, QueryClientProvider} from '@tanstack/react-query';
import React from 'react';
import ReactDOM from 'react-dom/client';
import {RouterProvider, createBrowserRouter} from 'react-router-dom';

import './index.scss';

const queryClient = new QueryClient();

const router = createBrowserRouter([
{
path: '/',
element: <App />,
children: [
{
path: '/',
element: <Home />,
},
{
path: '/add-program',
element: <AddProgram />,
},
],
},
]);

ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<QueryClientProvider client={queryClient}>
<RouterProvider router={router} />
</QueryClientProvider>
</React.StrictMode>
);
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
.add-program-page {
padding: 2rem 0;
max-width: 800px;
margin: 0 auto;
}

.add-program-section {
background: white;
padding: 2rem;
border-radius: 8px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
}

.add-program-section h2 {
margin-bottom: 2rem;
text-align: center;
font-size: 2rem;
color: #333;
}

.highlight {
color: #e74c3c;
}

.add-program-form {
display: flex;
flex-direction: column;
gap: 1.5rem;
}

.form-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}

.form-group label {
font-weight: bold;
color: #333;
font-size: 1.1rem;
}

.form-group input,
.form-group textarea {
padding: 0.75rem;
border: 2px solid #ddd;
border-radius: 4px;
font-size: 1rem;
transition: border-color 0.3s ease;
}

.form-group input:focus,
.form-group textarea:focus {
outline: none;
border-color: #e74c3c;
}

.form-group textarea {
resize: vertical;
min-height: 120px;
font-family: inherit;
}

.form-actions {
display: flex;
gap: 1rem;
justify-content: center;
margin-top: 1rem;
}

.submit-button,
.cancel-button {
padding: 0.75rem 2rem;
border: none;
border-radius: 4px;
font-size: 1rem;
font-weight: bold;
cursor: pointer;
transition: background-color 0.3s ease;
}

.submit-button {
background-color: #e74c3c;
color: white;
}

.submit-button:hover:not(:disabled) {
background-color: #c0392b;
}

.submit-button:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}

.cancel-button {
background-color: #95a5a6;
color: white;
}

.cancel-button:hover {
background-color: #7f8c8d;
}

@media (max-width: 768px) {
.add-program-page {
padding: 1rem;
}

.add-program-section {
padding: 1.5rem;
}

.form-actions {
flex-direction: column;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import './AddProgram.scss';

import {Program} from '@code-differently/types';
import {useMutation, useQueryClient} from '@tanstack/react-query';
import React, {useState} from 'react';
import {useNavigate} from 'react-router-dom';

const addProgram = async (program: Omit<Program, 'id'>): Promise<void> => {
const response = await fetch('http://localhost:4000/programs', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(program),
});

if (!response.ok) {
throw new Error('Failed to add program');
}
};

export const AddProgram: React.FC = () => {
const [title, setTitle] = useState('');
const [description, setDescription] = useState('');
const navigate = useNavigate();
const queryClient = useQueryClient();

const mutation = useMutation({
mutationFn: addProgram,
onSuccess: () => {
// Invalidate and refetch the programs list
queryClient.invalidateQueries({ queryKey: ['programs'] });
// Navigate back to home page
navigate('/');
},
onError: (error) => {
console.error('Error adding program:', error);
alert('Failed to add program. Please try again.');
},
});

const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();

if (!title.trim() || !description.trim()) {
alert('Please fill in both title and description');
return;
}

mutation.mutate({ title: title.trim(), description: description.trim() });
};

return (
<article className="add-program-page">
<section className="add-program-section">
<h2>
Add a New <em className="highlight">Program</em>
</h2>
<form onSubmit={handleSubmit} className="add-program-form">
<div className="form-group">
<label htmlFor="title">Program Title:</label>
<input
type="text"
id="title"
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder="Enter program title"
required
/>
</div>

<div className="form-group">
<label htmlFor="description">Program Description:</label>
<textarea
id="description"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Enter program description"
rows={5}
required
/>
</div>

<div className="form-actions">
<button
type="submit"
disabled={mutation.isPending}
className="submit-button"
>
{mutation.isPending ? 'Adding...' : 'Add Program'}
</button>
<button
type="button"
onClick={() => navigate('/')}
className="cancel-button"
>
Cancel
</button>
</div>
</form>
</section>
</article>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export {AddProgram} from './AddProgram';
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import './ProgramList.scss';

import {Program} from '../Program';
import {Program as ProgramType} from '@code-differently/types';
import {useQuery} from '@tanstack/react-query';

const fetchPrograms = async (): Promise<ProgramType[]> => {
const response = await fetch('http://localhost:4000/programs');
if (!response.ok) {
throw new Error('Failed to fetch programs');
}
return response.json();
};

export const ProgramList: React.FC = () => {
const { data: programs, isLoading, error } = useQuery({
queryKey: ['programs'],
queryFn: fetchPrograms,
});

if (isLoading) {
return <div>Loading programs...</div>;
}

if (error) {
return <div>Error loading programs: {error.message}</div>;
}

return (
<ul className="programs">
{programs?.map((program) => (
<Program key={program.id} title={program.title}>
<p>{program.description}</p>
</Program>
))}
</ul>
);
};