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 ( +
+
+ {/* Logo/Brand */} +
+ +
+ + {/* Navigation */} + + + {/* Mobile Menu Toggle (optional) */} + +
+
+ ); +}; + +// 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.

+
+ +
+ {/* Title Field */} +
+ + + {errors.title && ( + {errors.title} + )} +
+ + {/* Description Field */} +
+ +