diff --git a/lesson_19/template/style.css b/lesson_19/template/style.css
index e69de29bb..14954e681 100644
--- a/lesson_19/template/style.css
+++ b/lesson_19/template/style.css
@@ -0,0 +1,358 @@
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+body {
+ font-family: 'Poppins', sans-serif;
+ line-height: 1.6;
+ color: #333;
+ background-color: #ffffff;
+}
+
+/* Typography */
+h1, h2, h3, h4, h5, h6 {
+ font-family: 'Montserrat', sans-serif;
+ font-weight: 700;
+ margin-bottom: 1rem;
+}
+
+h2 {
+ font-size: 2.5rem;
+ font-weight: 800;
+ color: #2c3e50;
+}
+
+h3 {
+ font-size: 1.5rem;
+ font-weight: 700;
+ color: #2c3e50;
+ margin-bottom: 1rem;
+}
+
+p {
+ font-size: 1.1rem;
+ line-height: 1.7;
+ color: #555;
+ margin-bottom: 1.5rem;
+}
+
+/* Highlight Text */
+.highlight {
+ color: #e74c3c;
+ font-style: normal;
+ font-weight: 900;
+}
+
+/* Header Styles */
+.header {
+ background-color: #ffffff;
+ padding: 1rem 2rem;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+}
+
+.header-logo img {
+ height: 50px;
+ width: auto;
+}
+
+.header-top-menu {
+ display: flex;
+ list-style: none;
+ gap: 2rem;
+}
+
+.header-top-menu li a {
+ text-decoration: none;
+ color: #2c3e50;
+ font-weight: 600;
+ font-size: 1rem;
+ transition: color 0.3s ease;
+}
+
+.header-top-menu li a:hover {
+ color: #e74c3c;
+}
+
+.sign-up-button {
+ background-color: #e74c3c;
+ color: white;
+ padding: 0.75rem 1.5rem;
+ text-decoration: none;
+ border-radius: 5px;
+ font-weight: 600;
+ transition: all 0.3s ease;
+ border: 2px solid #e74c3c;
+}
+
+.sign-up-button:hover {
+ background-color: transparent;
+ color: #e74c3c;
+ transform: translateY(-2px);
+ box-shadow: 0 4px 15px rgba(231, 76, 60, 0.3);
+}
+
+/* Main Content */
+.main {
+ min-height: calc(100vh - 120px);
+}
+
+.content {
+ max-width: 1200px;
+ margin: 0 auto;
+}
+
+/* Hero Section */
+.hero-section {
+ background: linear-gradient(135deg, #2c3e50 0%, #34495e 100%);
+ color: white;
+ padding: 4rem 2rem;
+ text-align: center;
+ position: relative;
+ overflow: hidden;
+}
+
+.hero-section::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: url('data:image/svg+xml,');
+ opacity: 0.3;
+}
+
+.hero-content {
+ position: relative;
+ z-index: 2;
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.hero-title {
+ font-size: 3rem;
+ font-weight: 800;
+ margin-bottom: 2rem;
+ line-height: 1.2;
+}
+
+.hero-title .highlight {
+ color: #e74c3c;
+ position: relative;
+}
+
+.hero-title .highlight::after {
+ content: '';
+ position: absolute;
+ bottom: -5px;
+ left: 0;
+ right: 0;
+ height: 3px;
+ background-color: #e74c3c;
+ border-radius: 2px;
+}
+
+.hero-text {
+ font-size: 1.3rem;
+ line-height: 1.7;
+ opacity: 0.95;
+ max-width: 700px;
+ margin: 0 auto;
+}
+
+.hero-text span {
+ font-weight: 600;
+ color: #e74c3c;
+}
+
+/* Programs Section */
+.programs-section {
+ padding: 4rem 2rem;
+ background-color: #f8f9fa;
+}
+
+.programs-section h2 {
+ text-align: center;
+ margin-bottom: 3rem;
+ font-size: 2.5rem;
+}
+
+.programs {
+ list-style: none;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 2rem;
+ max-width: 1000px;
+ margin: 0 auto;
+}
+
+.program {
+ background: white;
+ padding: 2rem;
+ border-radius: 10px;
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.08);
+ transition: all 0.3s ease;
+ border-top: 4px solid #e74c3c;
+}
+
+.program:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
+}
+
+.program h3 {
+ color: #2c3e50;
+ margin-bottom: 1rem;
+ font-size: 1.4rem;
+}
+
+.program p {
+ color: #666;
+ font-size: 1rem;
+ line-height: 1.6;
+}
+
+/* Footer */
+.footer {
+ background-color: #2c3e50;
+ color: white;
+ text-align: center;
+ padding: 2rem;
+ font-size: 1rem;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .header {
+ flex-direction: column;
+ gap: 1rem;
+ padding: 1rem;
+ }
+
+ .header-top-menu {
+ flex-direction: column;
+ gap: 1rem;
+ text-align: center;
+ }
+
+ .hero-title {
+ font-size: 2rem;
+ }
+
+ .hero-text {
+ font-size: 1.1rem;
+ }
+
+ .programs-section h2 {
+ font-size: 2rem;
+ }
+
+ .programs {
+ grid-template-columns: 1fr;
+ gap: 1.5rem;
+ }
+
+ .program {
+ padding: 1.5rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .header {
+ padding: 0.5rem;
+ }
+
+ .hero-section {
+ padding: 2rem 1rem;
+ }
+
+ .hero-title {
+ font-size: 1.8rem;
+ }
+
+ .programs-section {
+ padding: 2rem 1rem;
+ }
+
+ .program {
+ padding: 1rem;
+ }
+}
+
+/* Additional Interactive Elements */
+.header-top-menu li a::after {
+ content: '';
+ position: absolute;
+ bottom: -5px;
+ left: 0;
+ width: 0;
+ height: 2px;
+ background-color: #e74c3c;
+ transition: width 0.3s ease;
+}
+
+.header-top-menu li {
+ position: relative;
+}
+
+.header-top-menu li a:hover::after {
+ width: 100%;
+}
+
+/* Smooth scrolling */
+html {
+ scroll-behavior: smooth;
+}
+
+/* Focus styles for accessibility */
+.sign-up-button:focus,
+.header-top-menu li a:focus {
+ outline: 2px solid #e74c3c;
+ outline-offset: 2px;
+}
+
+/* Animation for hero content */
+@keyframes fadeInUp {
+ from {
+ opacity: 0;
+ transform: translateY(30px);
+ }
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.hero-content {
+ animation: fadeInUp 0.8s ease-out;
+}
+
+/* Animation for program cards */
+@keyframes fadeInScale {
+ from {
+ opacity: 0;
+ transform: scale(0.9);
+ }
+ to {
+ opacity: 1;
+ transform: scale(1);
+ }
+}
+
+.program {
+ animation: fadeInScale 0.6s ease-out;
+ animation-fill-mode: both;
+}
+
+.program:nth-child(1) { animation-delay: 0.1s; }
+.program:nth-child(2) { animation-delay: 0.2s; }
+.program:nth-child(3) { animation-delay: 0.3s; }
+.program:nth-child(4) { animation-delay: 0.4s; }
\ No newline at end of file
diff --git a/lesson_24/src/App.css b/lesson_24/src/App.css
new file mode 100644
index 000000000..5ebe15d74
--- /dev/null
+++ b/lesson_24/src/App.css
@@ -0,0 +1,100 @@
+/* Global App Styles */
+.app {
+ min-height: 100vh;
+ display: flex;
+ flex-direction: column;
+}
+
+.main-content {
+ flex: 1;
+}
+
+/* Home Page Styles */
+.home {
+ padding: 0;
+}
+
+.home__hero {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ text-align: center;
+ padding: 80px 20px;
+}
+
+.home__hero h1 {
+ font-size: 3rem;
+ margin: 0 0 20px 0;
+ font-weight: 700;
+}
+
+.home__hero p {
+ font-size: 1.25rem;
+ margin: 0;
+ opacity: 0.9;
+}
+
+.home__features {
+ padding: 80px 20px;
+ max-width: 1200px;
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+ gap: 40px;
+}
+
+.feature-card {
+ text-align: center;
+ padding: 40px 20px;
+ background: white;
+ border-radius: 12px;
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
+ transition: transform 0.3s, box-shadow 0.3s;
+}
+
+.feature-card:hover {
+ transform: translateY(-5px);
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
+}
+
+.feature-card h3 {
+ color: #333;
+ font-size: 1.5rem;
+ margin: 0 0 15px 0;
+ font-weight: 600;
+}
+
+.feature-card p {
+ color: #666;
+ line-height: 1.6;
+ margin: 0;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .home__hero {
+ padding: 60px 20px;
+ }
+
+ .home__hero h1 {
+ font-size: 2.5rem;
+ }
+
+ .home__features {
+ padding: 60px 20px;
+ gap: 30px;
+ }
+
+ .feature-card {
+ padding: 30px 20px;
+ }
+}
+
+@media (max-width: 480px) {
+ .home__hero h1 {
+ font-size: 2rem;
+ }
+
+ .home__hero p {
+ font-size: 1.1rem;
+ }
+}
\ No newline at end of file
diff --git a/lesson_24/src/App.tsx b/lesson_24/src/App.tsx
new file mode 100644
index 000000000..96627b10c
--- /dev/null
+++ b/lesson_24/src/App.tsx
@@ -0,0 +1,72 @@
+import React from 'react';
+// import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
+import './App.css';
+import Header from './components/Header';
+import ProgramList from './components/ProgramList';
+import AddProgram from './pages/AddProgram';
+
+// Example Home component (referenced in requirements)
+const Home: React.FC = () => {
+ return (
+
+
+
Welcome to Code Differently
+
Discover and manage educational programs
+
+
+
+
+
Browse Programs
+
Explore our extensive catalog of educational programs
+
+
+
Add Programs
+
Contribute by adding new programs to our collection
+
+
+
Manage Content
+
Keep program information up to date and relevant
+
+
+
+ );
+};
+
+// App component with React Router setup
+const App: React.FC = () => {
+ return (
+
+
+
+ {/* Option 1: React Router Setup (uncomment if using React Router) */}
+ {/*
+
+
+ } />
+ } />
+ } />
+
+
+ */}
+
+ {/* Option 2: Simple routing based on URL pathname */}
+
+ {(() => {
+ const path = window.location.pathname;
+ switch (path) {
+ case '/':
+ return ;
+ case '/programs':
+ return ;
+ case '/add-program':
+ return ;
+ default:
+ return ;
+ }
+ })()}
+
+
+ );
+};
+
+export default App;
\ No newline at end of file
diff --git a/lesson_24/src/components/Header.css b/lesson_24/src/components/Header.css
new file mode 100644
index 000000000..233c7f000
--- /dev/null
+++ b/lesson_24/src/components/Header.css
@@ -0,0 +1,172 @@
+/* Header Component Styles */
+.header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ position: sticky;
+ top: 0;
+ z-index: 1000;
+}
+
+.header__container {
+ max-width: 1200px;
+ margin: 0 auto;
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 0 20px;
+ min-height: 70px;
+}
+
+/* Brand/Logo */
+.header__brand {
+ flex-shrink: 0;
+}
+
+.brand-link {
+ background: none;
+ border: none;
+ color: white;
+ font-size: 1.5rem;
+ font-weight: 700;
+ cursor: pointer;
+ text-decoration: none;
+ transition: opacity 0.2s;
+}
+
+.brand-link:hover {
+ opacity: 0.8;
+}
+
+/* Navigation */
+.header__nav {
+ flex-grow: 1;
+ display: flex;
+ justify-content: center;
+}
+
+.nav-list {
+ display: flex;
+ list-style: none;
+ margin: 0;
+ padding: 0;
+ gap: 30px;
+}
+
+.nav-item {
+ position: relative;
+}
+
+.nav-link {
+ background: none;
+ border: none;
+ color: rgba(255, 255, 255, 0.9);
+ text-decoration: none;
+ font-size: 1rem;
+ font-weight: 500;
+ padding: 8px 16px;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: all 0.3s ease;
+ position: relative;
+}
+
+.nav-link:hover {
+ color: white;
+ background-color: rgba(255, 255, 255, 0.1);
+}
+
+.nav-link--active {
+ color: white;
+ background-color: rgba(255, 255, 255, 0.15);
+}
+
+.nav-link--active::after {
+ content: '';
+ position: absolute;
+ bottom: -2px;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 20px;
+ height: 2px;
+ background-color: white;
+ border-radius: 1px;
+}
+
+/* Mobile Menu Toggle */
+.header__mobile-toggle {
+ display: none;
+ flex-direction: column;
+ background: none;
+ border: none;
+ cursor: pointer;
+ padding: 8px;
+ gap: 3px;
+}
+
+.hamburger {
+ width: 20px;
+ height: 2px;
+ background-color: white;
+ border-radius: 1px;
+ transition: all 0.3s ease;
+}
+
+/* Mobile Styles */
+@media (max-width: 768px) {
+ .header__container {
+ padding: 0 15px;
+ }
+
+ .header__mobile-toggle {
+ display: flex;
+ }
+
+ .header__nav {
+ display: none; /* Hide by default on mobile */
+ position: absolute;
+ top: 100%;
+ left: 0;
+ right: 0;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ }
+
+ .header__nav--open {
+ display: block;
+ }
+
+ .nav-list {
+ flex-direction: column;
+ gap: 0;
+ padding: 20px;
+ }
+
+ .nav-link {
+ display: block;
+ padding: 15px 0;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.1);
+ }
+
+ .nav-link:last-child {
+ border-bottom: none;
+ }
+
+ .nav-link--active::after {
+ display: none;
+ }
+
+ .brand-link {
+ font-size: 1.3rem;
+ }
+}
+
+@media (max-width: 480px) {
+ .header__container {
+ padding: 0 10px;
+ min-height: 60px;
+ }
+
+ .brand-link {
+ font-size: 1.2rem;
+ }
+}
\ No newline at end of file
diff --git a/lesson_24/src/components/Header.tsx b/lesson_24/src/components/Header.tsx
new file mode 100644
index 000000000..1b7a2bdfb
--- /dev/null
+++ b/lesson_24/src/components/Header.tsx
@@ -0,0 +1,162 @@
+import React from 'react';
+// Assuming React Router is being used
+// import { Link, useLocation } from 'react-router-dom';
+import './Header.css';
+
+const Header: React.FC = () => {
+ // For React Router navigation
+ // const location = useLocation();
+
+ // For standard navigation without React Router, use:
+ const currentPath = window.location.pathname;
+
+ const isActive = (path: string): boolean => {
+ return currentPath === path;
+ };
+
+ const handleNavigation = (path: string) => {
+ window.location.href = path;
+ };
+
+ return (
+
+ );
+};
+
+// Alternative version using React Router (uncomment if using React Router)
+/*
+const Header: React.FC = () => {
+ const location = useLocation();
+
+ const isActive = (path: string): boolean => {
+ return location.pathname === path;
+ };
+
+ return (
+
+
+
+
+ Code Differently
+
+
+
+
+
+
+
+
+ );
+};
+*/
+
+export default Header;
\ No newline at end of file
diff --git a/lesson_24/src/components/ProgramList.css b/lesson_24/src/components/ProgramList.css
new file mode 100644
index 000000000..6177b59c7
--- /dev/null
+++ b/lesson_24/src/components/ProgramList.css
@@ -0,0 +1,122 @@
+/* ProgramList Component Styles */
+.program-list {
+ max-width: 1200px;
+ margin: 0 auto;
+ padding: 20px;
+}
+
+.program-list__header {
+ margin-bottom: 30px;
+ text-align: center;
+}
+
+.program-list__header h2 {
+ color: #333;
+ font-size: 2rem;
+ margin-bottom: 10px;
+}
+
+.program-count {
+ color: #666;
+ font-size: 1rem;
+}
+
+/* Loading State */
+.loading-container {
+ text-align: center;
+ padding: 60px 20px;
+}
+
+.loading-spinner {
+ width: 40px;
+ height: 40px;
+ border: 4px solid #f3f3f3;
+ border-top: 4px solid #3498db;
+ border-radius: 50%;
+ animation: spin 1s linear infinite;
+ margin: 0 auto 20px;
+}
+
+@keyframes spin {
+ 0% { transform: rotate(0deg); }
+ 100% { transform: rotate(360deg); }
+}
+
+/* Error State */
+.error-container {
+ text-align: center;
+ padding: 60px 20px;
+ background-color: #fee;
+ border-radius: 8px;
+ border: 1px solid #fcc;
+}
+
+.error-message {
+ color: #d32f2f;
+ margin: 15px 0;
+}
+
+.retry-button {
+ background-color: #3498db;
+ color: white;
+ border: none;
+ padding: 12px 24px;
+ border-radius: 4px;
+ cursor: pointer;
+ font-size: 1rem;
+}
+
+.retry-button:hover {
+ background-color: #2980b9;
+}
+
+/* Empty State */
+.empty-state {
+ text-align: center;
+ padding: 60px 20px;
+ color: #666;
+}
+
+/* Program Grid */
+.program-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
+ gap: 20px;
+ margin-top: 20px;
+}
+
+.program-card {
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: transform 0.2s, box-shadow 0.2s;
+ overflow: hidden;
+}
+
+.program-card:hover {
+ transform: translateY(-2px);
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.15);
+}
+
+.program-card__content {
+ padding: 20px;
+}
+
+.program-title {
+ color: #333;
+ font-size: 1.25rem;
+ margin: 0 0 10px 0;
+ font-weight: 600;
+}
+
+.program-description {
+ color: #666;
+ line-height: 1.5;
+ margin: 0 0 15px 0;
+}
+
+.program-date {
+ color: #999;
+ font-size: 0.875rem;
+ margin: 0;
+}
\ No newline at end of file
diff --git a/lesson_24/src/components/ProgramList.tsx b/lesson_24/src/components/ProgramList.tsx
new file mode 100644
index 000000000..0e2ab6063
--- /dev/null
+++ b/lesson_24/src/components/ProgramList.tsx
@@ -0,0 +1,121 @@
+import React, { useEffect, useState } from 'react';
+
+// Assuming these types exist in @code-differently/types
+interface Program {
+ id: string;
+ title: string;
+ description: string;
+ createdAt?: string;
+}
+
+interface ApiResponse {
+ data: T;
+ success: boolean;
+ message?: string;
+}
+
+const ProgramList: React.FC = () => {
+ const [programs, setPrograms] = useState([]);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState(null);
+
+ useEffect(() => {
+ fetchPrograms();
+ }, []);
+
+ const fetchPrograms = async () => {
+ try {
+ setLoading(true);
+ setError(null);
+
+ const response = await fetch('/api/programs', {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to fetch programs: ${response.status} ${response.statusText}`);
+ }
+
+ const result: ApiResponse = await response.json();
+
+ if (result.success) {
+ setPrograms(result.data);
+ } else {
+ throw new Error(result.message || 'Failed to load programs');
+ }
+ } catch (err) {
+ console.error('Error fetching programs:', err);
+ setError(err instanceof Error ? err.message : 'An unexpected error occurred');
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleRetry = () => {
+ fetchPrograms();
+ };
+
+ if (loading) {
+ return (
+
+
+
+
Loading programs...
+
+
+ );
+ }
+
+ if (error) {
+ return (
+
+
+
Error Loading Programs
+
{error}
+
+
+
+ );
+ }
+
+ return (
+
+
+
Available Programs
+
+ {programs.length} {programs.length === 1 ? 'program' : 'programs'} available
+
+
+
+ {programs.length === 0 ? (
+
+
No Programs Available
+
There are currently no programs to display.
+
+ ) : (
+
+ {programs.map((program) => (
+
+
+
{program.title}
+
{program.description}
+ {program.createdAt && (
+
+ Created: {new Date(program.createdAt).toLocaleDateString()}
+
+ )}
+
+
+ ))}
+
+ )}
+
+ );
+};
+
+export default ProgramList;
\ No newline at end of file
diff --git a/lesson_24/src/pages/AddProgram.css b/lesson_24/src/pages/AddProgram.css
new file mode 100644
index 000000000..9e455f482
--- /dev/null
+++ b/lesson_24/src/pages/AddProgram.css
@@ -0,0 +1,163 @@
+/* AddProgram Component Styles */
+.add-program {
+ min-height: 100vh;
+ background-color: #f8f9fa;
+ padding: 20px 0;
+}
+
+.add-program__container {
+ max-width: 600px;
+ margin: 0 auto;
+ background: white;
+ border-radius: 8px;
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
+ overflow: hidden;
+}
+
+.add-program__header {
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ padding: 40px;
+ text-align: center;
+}
+
+.add-program__header h1 {
+ margin: 0 0 10px 0;
+ font-size: 2rem;
+ font-weight: 600;
+}
+
+.add-program__header p {
+ margin: 0;
+ opacity: 0.9;
+ font-size: 1.1rem;
+}
+
+.add-program__form {
+ padding: 40px;
+}
+
+.form-group {
+ margin-bottom: 25px;
+}
+
+.form-label {
+ display: block;
+ margin-bottom: 8px;
+ font-weight: 600;
+ color: #333;
+ font-size: 1rem;
+}
+
+.form-input,
+.form-textarea {
+ width: 100%;
+ padding: 12px 16px;
+ border: 2px solid #e1e5e9;
+ border-radius: 6px;
+ font-size: 1rem;
+ transition: border-color 0.3s, box-shadow 0.3s;
+ box-sizing: border-box;
+ font-family: inherit;
+}
+
+.form-input:focus,
+.form-textarea:focus {
+ outline: none;
+ border-color: #667eea;
+ box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1);
+}
+
+.form-input--error,
+.form-textarea--error {
+ border-color: #e74c3c;
+}
+
+.form-input--error:focus,
+.form-textarea--error:focus {
+ border-color: #e74c3c;
+ box-shadow: 0 0 0 3px rgba(231, 76, 60, 0.1);
+}
+
+.form-textarea {
+ resize: vertical;
+ min-height: 120px;
+}
+
+.character-count {
+ font-size: 0.875rem;
+ color: #666;
+ text-align: right;
+ margin-top: 5px;
+}
+
+.form-error {
+ display: block;
+ color: #e74c3c;
+ font-size: 0.875rem;
+ margin-top: 5px;
+}
+
+.submit-button {
+ width: 100%;
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
+ color: white;
+ border: none;
+ padding: 16px;
+ border-radius: 6px;
+ font-size: 1.1rem;
+ font-weight: 600;
+ cursor: pointer;
+ transition: transform 0.2s, box-shadow 0.2s;
+}
+
+.submit-button:hover:not(:disabled) {
+ transform: translateY(-1px);
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3);
+}
+
+.submit-button:disabled {
+ opacity: 0.7;
+ cursor: not-allowed;
+ transform: none;
+}
+
+.message {
+ margin-top: 20px;
+ padding: 12px 16px;
+ border-radius: 6px;
+ font-weight: 500;
+}
+
+.message--success {
+ background-color: #d4edda;
+ color: #155724;
+ border: 1px solid #c3e6cb;
+}
+
+.message--error {
+ background-color: #f8d7da;
+ color: #721c24;
+ border: 1px solid #f5c6cb;
+}
+
+/* Responsive Design */
+@media (max-width: 768px) {
+ .add-program {
+ padding: 10px;
+ }
+
+ .add-program__container {
+ margin: 10px;
+ max-width: none;
+ }
+
+ .add-program__header,
+ .add-program__form {
+ padding: 20px;
+ }
+
+ .add-program__header h1 {
+ font-size: 1.5rem;
+ }
+}
\ No newline at end of file
diff --git a/lesson_24/src/pages/AddProgram.tsx b/lesson_24/src/pages/AddProgram.tsx
new file mode 100644
index 000000000..4f38c9cf4
--- /dev/null
+++ b/lesson_24/src/pages/AddProgram.tsx
@@ -0,0 +1,218 @@
+import React, { useState } from 'react';
+import './AddProgram.css';
+
+// Assuming these types exist in @code-differently/types
+interface Program {
+ id?: string;
+ title: string;
+ description: string;
+}
+
+interface FormData {
+ title: string;
+ description: string;
+}
+
+interface FormErrors {
+ title?: string;
+ description?: string;
+}
+
+interface ApiResponse {
+ data: T;
+ success: boolean;
+ message?: string;
+}
+
+const AddProgram: React.FC = () => {
+ const [formData, setFormData] = useState({
+ title: '',
+ description: ''
+ });
+ const [errors, setErrors] = useState({});
+ const [loading, setLoading] = useState(false);
+ const [successMessage, setSuccessMessage] = useState('');
+ const [errorMessage, setErrorMessage] = useState('');
+
+ const validateForm = (): boolean => {
+ const newErrors: FormErrors = {};
+
+ // Title validation
+ if (!formData.title.trim()) {
+ newErrors.title = 'Program title is required';
+ } else if (formData.title.trim().length < 3) {
+ newErrors.title = 'Title must be at least 3 characters long';
+ } else if (formData.title.trim().length > 100) {
+ newErrors.title = 'Title must be less than 100 characters';
+ }
+
+ // Description validation
+ if (!formData.description.trim()) {
+ newErrors.description = 'Program description is required';
+ } else if (formData.description.trim().length < 10) {
+ newErrors.description = 'Description must be at least 10 characters long';
+ } else if (formData.description.trim().length > 500) {
+ newErrors.description = 'Description must be less than 500 characters';
+ }
+
+ setErrors(newErrors);
+ return Object.keys(newErrors).length === 0;
+ };
+
+ const handleInputChange = (
+ e: React.ChangeEvent
+ ) => {
+ const { name, value } = e.target;
+ setFormData(prev => ({
+ ...prev,
+ [name]: value
+ }));
+
+ // Clear specific field error when user starts typing
+ if (errors[name as keyof FormErrors]) {
+ setErrors(prev => ({
+ ...prev,
+ [name]: undefined
+ }));
+ }
+
+ // Clear messages
+ setSuccessMessage('');
+ setErrorMessage('');
+ };
+
+ const handleSubmit = async (e: React.FormEvent) => {
+ e.preventDefault();
+
+ if (!validateForm()) {
+ return;
+ }
+
+ setLoading(true);
+ setSuccessMessage('');
+ setErrorMessage('');
+
+ try {
+ const response = await fetch('/api/programs', {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ title: formData.title.trim(),
+ description: formData.description.trim(),
+ }),
+ });
+
+ if (!response.ok) {
+ throw new Error(`Failed to create program: ${response.status} ${response.statusText}`);
+ }
+
+ const result: ApiResponse = await response.json();
+
+ if (result.success) {
+ setSuccessMessage('Program created successfully!');
+ // Reset form
+ setFormData({
+ title: '',
+ description: ''
+ });
+ setErrors({});
+ } else {
+ throw new Error(result.message || 'Failed to create program');
+ }
+ } catch (err) {
+ console.error('Error creating program:', err);
+ setErrorMessage(
+ err instanceof Error ? err.message : 'An unexpected error occurred'
+ );
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ return (
+
+
+
+
Add New Program
+
Create a new program by filling out the form below.
+
+
+
+
+
+ );
+};
+
+export default AddProgram;
\ No newline at end of file
diff --git a/lesson_24/src/types/index.ts b/lesson_24/src/types/index.ts
new file mode 100644
index 000000000..c51a9f225
--- /dev/null
+++ b/lesson_24/src/types/index.ts
@@ -0,0 +1,60 @@
+// Types for the React application
+// This would typically come from @code-differently/types package
+
+export interface Program {
+ id: string;
+ title: string;
+ description: string;
+ createdAt?: string;
+ updatedAt?: string;
+}
+
+export interface CreateProgramRequest {
+ title: string;
+ description: string;
+}
+
+export interface UpdateProgramRequest {
+ title?: string;
+ description?: string;
+}
+
+export interface ApiResponse {
+ data: T;
+ success: boolean;
+ message?: string;
+ error?: string;
+}
+
+export interface PaginatedResponse {
+ data: T[];
+ totalCount: number;
+ page: number;
+ pageSize: number;
+ totalPages: number;
+}
+
+// Form-related types
+export interface FormErrors {
+ [key: string]: string | undefined;
+}
+
+export interface LoadingState {
+ isLoading: boolean;
+ error: string | null;
+}
+
+// Navigation types
+export interface NavigationItem {
+ path: string;
+ label: string;
+ exact?: boolean;
+}
+
+// API endpoints configuration
+export interface ApiEndpoints {
+ programs: string;
+ createProgram: string;
+ updateProgram: (id: string) => string;
+ deleteProgram: (id: string) => string;
+}
\ No newline at end of file