diff --git a/lesson_24/mattieweathersby/template/package.json b/lesson_24/mattieweathersby/template/package.json new file mode 100644 index 000000000..60af2f7a0 --- /dev/null +++ b/lesson_24/mattieweathersby/template/package.json @@ -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" + } +} diff --git a/lesson_24/mattieweathersby/template/src/components/header/Header.tsx b/lesson_24/mattieweathersby/template/src/components/header/Header.tsx new file mode 100644 index 000000000..b1ba0af29 --- /dev/null +++ b/lesson_24/mattieweathersby/template/src/components/header/Header.tsx @@ -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 ( +
+
+ + Code Differently Logo + +
+ +
+ + Sign Up + +
+
+ ); +}; diff --git a/lesson_24/mattieweathersby/template/src/main.tsx b/lesson_24/mattieweathersby/template/src/main.tsx new file mode 100644 index 000000000..1b20127f2 --- /dev/null +++ b/lesson_24/mattieweathersby/template/src/main.tsx @@ -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: , + children: [ + { + path: '/', + element: , + }, + { + path: '/add-program', + element: , + }, + ], + }, +]); + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + + + +); diff --git a/lesson_24/mattieweathersby/template/src/pages/AddProgram/AddProgram.scss b/lesson_24/mattieweathersby/template/src/pages/AddProgram/AddProgram.scss new file mode 100644 index 000000000..0ec4771dd --- /dev/null +++ b/lesson_24/mattieweathersby/template/src/pages/AddProgram/AddProgram.scss @@ -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; + } +} diff --git a/lesson_24/mattieweathersby/template/src/pages/AddProgram/AddProgram.tsx b/lesson_24/mattieweathersby/template/src/pages/AddProgram/AddProgram.tsx new file mode 100644 index 000000000..09c4303d2 --- /dev/null +++ b/lesson_24/mattieweathersby/template/src/pages/AddProgram/AddProgram.tsx @@ -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): Promise => { + 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 ( +
+
+

+ Add a New Program +

+
+
+ + setTitle(e.target.value)} + placeholder="Enter program title" + required + /> +
+ +
+ +