frontend-hamroun 1.2.27 → 1.2.28

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +7 -0
  2. package/package.json +1 -1
  3. package/templates/fullstack-app/README.md +37 -0
  4. package/templates/fullstack-app/build/main.css +664 -0
  5. package/templates/fullstack-app/build/main.css.map +7 -0
  6. package/templates/fullstack-app/build/main.js +682 -0
  7. package/templates/fullstack-app/build/main.js.map +7 -0
  8. package/templates/fullstack-app/build.ts +211 -0
  9. package/templates/fullstack-app/index.html +26 -3
  10. package/templates/fullstack-app/package-lock.json +2402 -438
  11. package/templates/fullstack-app/package.json +24 -9
  12. package/templates/fullstack-app/postcss.config.js +6 -0
  13. package/templates/fullstack-app/process-tailwind.js +45 -0
  14. package/templates/fullstack-app/public/_redirects +1 -0
  15. package/templates/fullstack-app/public/route-handler.js +47 -0
  16. package/templates/fullstack-app/public/spa-fix.html +17 -0
  17. package/templates/fullstack-app/public/styles.css +768 -0
  18. package/templates/fullstack-app/public/tailwind.css +15 -0
  19. package/templates/fullstack-app/server.js +101 -44
  20. package/templates/fullstack-app/server.ts +402 -39
  21. package/templates/fullstack-app/src/README.md +55 -0
  22. package/templates/fullstack-app/src/client.js +83 -16
  23. package/templates/fullstack-app/src/components/Layout.tsx +45 -0
  24. package/templates/fullstack-app/src/components/UserList.tsx +27 -0
  25. package/templates/fullstack-app/src/config.ts +42 -0
  26. package/templates/fullstack-app/src/data/api.ts +71 -0
  27. package/templates/fullstack-app/src/main.tsx +219 -7
  28. package/templates/fullstack-app/src/pages/about/index.tsx +67 -0
  29. package/templates/fullstack-app/src/pages/index.tsx +30 -60
  30. package/templates/fullstack-app/src/pages/users.tsx +60 -0
  31. package/templates/fullstack-app/src/router.ts +255 -0
  32. package/templates/fullstack-app/src/styles.css +5 -0
  33. package/templates/fullstack-app/tailwind.config.js +11 -0
  34. package/templates/fullstack-app/tsconfig.json +18 -0
  35. package/templates/fullstack-app/vite.config.js +53 -6
  36. package/templates/ssr-template/readme.md +50 -0
  37. package/templates/ssr-template/src/client.ts +46 -14
  38. package/templates/ssr-template/src/server.ts +190 -18
@@ -0,0 +1,55 @@
1
+ # Frontend Hamroun Fullstack Template
2
+
3
+ This template uses a file-system based routing system similar to Next.js. Here's how to use it:
4
+
5
+ ## Creating New Pages
6
+
7
+ 1. Add new pages in the `src/pages` directory
8
+ 2. File names become routes automatically:
9
+ - `src/pages/index.tsx` → `/`
10
+ - `src/pages/about.tsx` → `/about`
11
+ - `src/pages/users/[id].tsx` → `/users/:id`
12
+
13
+ ## Page Structure
14
+
15
+ Each page should follow this structure:
16
+
17
+ ```tsx
18
+ import { jsx } from 'frontend-hamroun';
19
+ import Layout from '../components/Layout';
20
+
21
+ const YourPage = ({ initialState }) => (
22
+ <Layout title="Your Page Title">
23
+ {/* Your content here */}
24
+ </Layout>
25
+ );
26
+
27
+ // Optional: Add data fetching
28
+ YourPage.getInitialData = async (path) => {
29
+ // Fetch any data your page needs
30
+ return {
31
+ yourData: await fetchSomething()
32
+ };
33
+ };
34
+
35
+ export default YourPage;
36
+ ```
37
+
38
+ ## Data Fetching
39
+
40
+ Use the API utilities in `src/data/api.ts` to fetch data in a consistent way:
41
+
42
+ ```tsx
43
+ import { UserApi } from '../data/api';
44
+
45
+ // In your getInitialData method:
46
+ const users = await UserApi.getAll();
47
+ ```
48
+
49
+ ## Components
50
+
51
+ Store reusable components in the `src/components` directory and import them in your pages.
52
+
53
+ ## Navigation
54
+
55
+ Client-side navigation is handled automatically. Just use regular `<a href="...">` links.
@@ -1,20 +1,87 @@
1
- import { hydrate, createElement } from 'frontend-hamroun';
1
+ import { hydrate, createElement, jsx } from 'frontend-hamroun';
2
2
 
3
- // Simple component for testing
4
- function App() {
5
- return createElement('div', null, [
6
- createElement('h1', null, 'Hello from Frontend Hamroun'),
7
- createElement('p', null, 'This is a fullstack app with SSR')
8
- ]);
3
+ // Add global jsx for compatibility
4
+ window.jsx = jsx;
5
+ window.createElement = createElement;
6
+
7
+ // Get the initial state from the server (if available)
8
+ const initialState = window.__INITIAL_STATE__ || {
9
+ route: window.location.pathname
10
+ };
11
+
12
+ // Dynamically import the right page component based on the route
13
+ async function loadAndHydrateComponent() {
14
+ try {
15
+ const path = initialState.route || '/';
16
+ const normalizedPath = path === '/' ? '/index' : path;
17
+
18
+ console.log(`Loading component for path: ${normalizedPath}`);
19
+
20
+ // Try to import the page component
21
+ const module = await import(`./pages${normalizedPath}.js`).catch(() =>
22
+ import(`./pages${normalizedPath}/index.js`)).catch(() => {
23
+ console.warn(`No component found for ${normalizedPath}, falling back to index`);
24
+ return import('./pages/index.js');
25
+ });
26
+
27
+ const PageComponent = module.default;
28
+
29
+ if (!PageComponent) {
30
+ throw new Error(`No default export found in module for ${normalizedPath}`);
31
+ }
32
+
33
+ // Find the root element - try both 'root' and 'app' IDs
34
+ const rootElement = document.getElementById('root') || document.getElementById('app');
35
+
36
+ if (!rootElement) {
37
+ throw new Error('Could not find root element with id "root" or "app"');
38
+ }
39
+
40
+ // Hydrate the application
41
+ hydrate(jsx(PageComponent, { initialState }), rootElement);
42
+ console.log('Hydration complete');
43
+
44
+ // Add navigation event listeners if using client-side routing
45
+ document.addEventListener('click', handleLinkClicks);
46
+ window.addEventListener('popstate', handleRouteChange);
47
+
48
+ return true;
49
+ } catch (error) {
50
+ console.error('Error during hydration:', error);
51
+ return false;
52
+ }
9
53
  }
10
54
 
11
- // Wait for DOM to be ready
12
- document.addEventListener('DOMContentLoaded', () => {
13
- const root = document.getElementById('root');
14
-
15
- // Hydrate the application
16
- if (root) {
17
- hydrate(createElement(App), root);
18
- console.log('App hydrated successfully');
55
+ // Handle client-side navigation for links
56
+ function handleLinkClicks(event) {
57
+ // Only handle links within our app
58
+ if (event.target.tagName === 'A' &&
59
+ event.target.origin === window.location.origin &&
60
+ !event.target.hasAttribute('external')) {
61
+
62
+ event.preventDefault();
63
+ const href = event.target.getAttribute('href');
64
+
65
+ // Update history and load the new component
66
+ window.history.pushState({}, '', href);
67
+ handleRouteChange();
19
68
  }
20
- });
69
+ }
70
+
71
+ // Handle route changes (back/forward navigation or pushState)
72
+ function handleRouteChange() {
73
+ // Update the initialState with the new route
74
+ initialState.route = window.location.pathname;
75
+
76
+ // Load and hydrate the new component
77
+ loadAndHydrateComponent().catch(err => {
78
+ console.error('Failed to load component after route change:', err);
79
+ });
80
+ }
81
+
82
+ // Wait for DOM to be ready before hydrating
83
+ if (document.readyState === 'loading') {
84
+ document.addEventListener('DOMContentLoaded', loadAndHydrateComponent);
85
+ } else {
86
+ loadAndHydrateComponent();
87
+ }
@@ -0,0 +1,45 @@
1
+ import { jsx } from 'frontend-hamroun';
2
+ import AppConfig from '../config';
3
+
4
+ export interface LayoutProps {
5
+ children: any;
6
+ title?: string;
7
+ showNavigation?: boolean;
8
+ }
9
+
10
+ export default function Layout({ children, title = AppConfig.title, showNavigation = true }: LayoutProps) {
11
+ const { navigation } = AppConfig;
12
+
13
+ return (
14
+ <div className="container mx-auto px-4 py-8 max-w-5xl">
15
+ <header className="mb-8">
16
+ <h1 className="text-4xl font-bold text-gray-800 mb-4">{title}</h1>
17
+
18
+ {showNavigation && (
19
+ <nav className="mb-6">
20
+ <ul className="flex space-x-6">
21
+ {navigation.map(item => (
22
+ <li key={item.path}>
23
+ <a
24
+ href={item.path}
25
+ className="text-blue-600 hover:text-blue-800 hover:underline transition-colors font-medium"
26
+ >
27
+ {item.label}
28
+ </a>
29
+ </li>
30
+ ))}
31
+ </ul>
32
+ </nav>
33
+ )}
34
+ </header>
35
+
36
+ <main className="min-h-[50vh]">
37
+ {children}
38
+ </main>
39
+
40
+ <footer className="mt-12 pt-6 border-t border-gray-200 text-gray-600 text-sm">
41
+ <p>{AppConfig.title} &copy; {new Date().getFullYear()}</p>
42
+ </footer>
43
+ </div>
44
+ );
45
+ }
@@ -0,0 +1,27 @@
1
+ import { jsx } from 'frontend-hamroun';
2
+
3
+ const UserList = ({ users }) => {
4
+ if (!users || users.length === 0) {
5
+ return (
6
+ <div className="p-4 bg-gray-50 rounded-lg border border-gray-200 my-4">
7
+ <p className="text-gray-500 italic">No users found or still loading...</p>
8
+ </div>
9
+ );
10
+ }
11
+
12
+ return (
13
+ <div className="rounded-lg overflow-hidden my-4">
14
+ <h3 className="text-lg font-medium mb-3">Users ({users.length})</h3>
15
+ <ul className="divide-y divide-gray-200 border border-gray-200 rounded-lg overflow-hidden">
16
+ {users.map(user => (
17
+ <li key={user.id} className="flex justify-between items-center p-4 hover:bg-gray-50">
18
+ <span className="font-medium text-gray-800">{user.name}</span>
19
+ <span className="text-gray-500 text-sm">{user.email}</span>
20
+ </li>
21
+ ))}
22
+ </ul>
23
+ </div>
24
+ );
25
+ };
26
+
27
+ export default UserList;
@@ -0,0 +1,42 @@
1
+ // Application configuration
2
+ // Modify this file to customize your application without changing core files
3
+
4
+ export const AppConfig = {
5
+ // App information
6
+ title: 'Frontend Hamroun App',
7
+ description: 'A full-stack application built with Frontend Hamroun',
8
+
9
+ // Navigation
10
+ navigation: [
11
+ { path: '/', label: 'Home' },
12
+ { path: '/about', label: 'About' },
13
+ { path: '/users', label: 'Users' }
14
+ ],
15
+
16
+ // API endpoints
17
+ api: {
18
+ baseUrl: '/api',
19
+ endpoints: {
20
+ users: '/users',
21
+ posts: '/posts'
22
+ }
23
+ },
24
+
25
+ // Default meta tags
26
+ meta: {
27
+ viewport: 'width=device-width, initial-scale=1.0',
28
+ charset: 'UTF-8',
29
+ author: 'Your Name',
30
+ keywords: 'frontend-hamroun, fullstack, template'
31
+ },
32
+
33
+ // Style customization
34
+ theme: {
35
+ primaryColor: '#0066cc',
36
+ backgroundColor: '#ffffff',
37
+ textColor: '#333333',
38
+ fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif'
39
+ }
40
+ };
41
+
42
+ export default AppConfig;
@@ -0,0 +1,71 @@
1
+ // General API utility for data fetching
2
+
3
+ /**
4
+ * Fetch data from an API endpoint
5
+ * @param endpoint The API endpoint to fetch from (without /api prefix)
6
+ * @param options Additional fetch options
7
+ * @returns The parsed JSON response or null if there was an error
8
+ */
9
+ export async function fetchApi(endpoint: string, options = {}) {
10
+ const url = endpoint.startsWith('/') ? `/api${endpoint}` : `/api/${endpoint}`;
11
+
12
+ try {
13
+ console.log(`[API] Fetching data from: ${url}`);
14
+ const response = await fetch(url, {
15
+ headers: {
16
+ 'Content-Type': 'application/json',
17
+ 'Accept': 'application/json'
18
+ },
19
+ ...options
20
+ });
21
+
22
+ if (!response.ok) {
23
+ throw new Error(`API request failed: ${response.status} ${response.statusText}`);
24
+ }
25
+
26
+ const data = await response.json();
27
+ console.log(`[API] Successfully fetched data from: ${url}`);
28
+ return data;
29
+ } catch (error) {
30
+ console.error(`[API] Error fetching from ${url}:`, error);
31
+ return null;
32
+ }
33
+ }
34
+
35
+ /**
36
+ * User-related API calls
37
+ */
38
+ export const UserApi = {
39
+ getAll: () => fetchApi('/users'),
40
+ getById: (id: number | string) => fetchApi(`/users/${id}`),
41
+ create: (data: any) => fetchApi('/users', {
42
+ method: 'POST',
43
+ body: JSON.stringify(data)
44
+ }),
45
+ update: (id: number | string, data: any) => fetchApi(`/users/${id}`, {
46
+ method: 'PUT',
47
+ body: JSON.stringify(data)
48
+ }),
49
+ delete: (id: number | string) => fetchApi(`/users/${id}`, {
50
+ method: 'DELETE'
51
+ })
52
+ };
53
+
54
+ /**
55
+ * Post-related API calls
56
+ */
57
+ export const PostApi = {
58
+ getAll: () => fetchApi('/posts'),
59
+ getById: (id: number | string) => fetchApi(`/posts/${id}`),
60
+ create: (data: any) => fetchApi('/posts', {
61
+ method: 'POST',
62
+ body: JSON.stringify(data)
63
+ }),
64
+ update: (id: number | string, data: any) => fetchApi(`/posts/${id}`, {
65
+ method: 'PUT',
66
+ body: JSON.stringify(data)
67
+ }),
68
+ delete: (id: number | string) => fetchApi(`/posts/${id}`, {
69
+ method: 'DELETE'
70
+ })
71
+ };
@@ -1,9 +1,221 @@
1
- import { jsx, render } from 'frontend-hamroun';
2
- import HomePage from './pages/index.tsx';
1
+ import { render, hydrate, jsx } from 'frontend-hamroun';
2
+ import { router } from './router';
3
+ // Import Tailwind CSS
4
+ import './styles.css';
3
5
 
4
- // Add global jsx for Vite to use
5
- window.jsx = jsx;
6
+ // Import pages directly to ensure they're available
7
+ import HomePage from './pages/index';
8
+ import AboutPage from './pages/about';
9
+ import UsersPage from './pages/users';
6
10
 
7
- // Fix: Pass the component properly to render
8
- // Don't call HomePage() directly - pass it as a component
9
- render(jsx(HomePage, {}, null), document.getElementById('root'));
11
+ // Create a mutable variable for hydration state
12
+ let isHydrating = document.getElementById('root')?.innerHTML.trim() !== '';
13
+
14
+ console.log('[Client] Running client-side code. Hydration state:', isHydrating);
15
+
16
+ // Get initial state from server
17
+ const initialState = window.__INITIAL_STATE__ || {
18
+ route: window.location.pathname,
19
+ timestamp: new Date().toISOString(),
20
+ serverRendered: false,
21
+ data: {
22
+ users: null,
23
+ posts: null
24
+ }
25
+ };
26
+
27
+ console.log('[Client] Initial state:', initialState);
28
+
29
+ // Function to fetch data from API
30
+ async function fetchData(path) {
31
+ try {
32
+ console.log(`[Client] Fetching data from API: ${path}`);
33
+ const response = await fetch(`/api${path}`);
34
+ if (!response.ok) {
35
+ throw new Error(`API request failed: ${response.status}`);
36
+ }
37
+ const data = await response.json();
38
+ console.log(`[Client] Received data:`, data);
39
+ return data;
40
+ } catch (error) {
41
+ console.error(`[Client] Error fetching data from ${path}:`, error);
42
+ return null;
43
+ }
44
+ }
45
+
46
+ // Initialize known routes
47
+ function initializeRouter() {
48
+ console.log('[Client] Initializing router with known pages');
49
+ router.register('index', HomePage);
50
+ router.register('about', AboutPage);
51
+ router.register('users', UsersPage);
52
+ }
53
+
54
+ // Call initialization before rendering
55
+ initializeRouter();
56
+
57
+ // Function to determine which page to render based on the route
58
+ async function renderApp() {
59
+ try {
60
+ // Get current path
61
+ const currentPath = window.location.pathname;
62
+
63
+ console.log(`[Client] Rendering for path: ${currentPath}`);
64
+
65
+ // Find the root element
66
+ const rootElement = document.getElementById('root');
67
+ if (!rootElement) {
68
+ console.error('[Client] Root element not found');
69
+ return;
70
+ }
71
+
72
+ // Fetch data based on the current route
73
+ console.log(`[Client] Fetching users data for ${currentPath}`);
74
+ const users = await fetchData('/users');
75
+
76
+ if (users) {
77
+ initialState.data = {
78
+ ...initialState.data,
79
+ users
80
+ };
81
+ console.log(`[Client] Updated state with ${users.length} users`);
82
+ }
83
+
84
+ // Get the component for the current path
85
+ let PageComponent = null;
86
+
87
+ // Special cases for known routes
88
+ if (currentPath === '/about') {
89
+ try {
90
+ const AboutPage = await import('./pages/about');
91
+ PageComponent = AboutPage.default;
92
+ } catch (err) {
93
+ console.error('[Client] Failed to load about page:', err);
94
+ }
95
+ } else if (currentPath === '/users') {
96
+ try {
97
+ const UsersPage = await import('./pages/users');
98
+ PageComponent = UsersPage.default;
99
+ } catch (err) {
100
+ console.error('[Client] Failed to load users page:', err);
101
+ }
102
+ } else {
103
+ // Try the router for other paths
104
+ PageComponent = await router.resolve(currentPath);
105
+ }
106
+
107
+ // If component wasn't found, handle special cases
108
+ if (!PageComponent) {
109
+ console.warn(`[Client] Page component not found for ${currentPath}, checking special cases`);
110
+
111
+ // Special case for /users
112
+ if (currentPath === '/users') {
113
+ try {
114
+ console.log('[Client] Attempting direct import of Users page');
115
+ const UsersModule = await import('./pages/users');
116
+ if (UsersModule.default) {
117
+ PageComponent = UsersModule.default;
118
+ }
119
+ } catch (err) {
120
+ console.error('[Client] Failed to import Users page:', err);
121
+ }
122
+ }
123
+ }
124
+
125
+ // Final fallback if still no component
126
+ if (!PageComponent) {
127
+ // Create a simple fallback component
128
+ PageComponent = ({ initialState }) => jsx('div', {
129
+ style: 'padding: 20px; max-width: 800px; margin: 0 auto;'
130
+ }, [
131
+ jsx('h1', {}, ['Page Not Found']),
132
+ jsx('p', {}, [`No component found for path: ${initialState.route}`]),
133
+ jsx('a', { href: '/', style: 'color: #0066cc;' }, ['Go to Home'])
134
+ ]);
135
+ }
136
+
137
+ // Update state with current route and timestamp
138
+ initialState.route = currentPath;
139
+ initialState.timestamp = new Date().toISOString();
140
+ initialState.serverRendered = false; // This is now a client render
141
+
142
+ // Render or hydrate
143
+ if (isHydrating) {
144
+ console.log('[Client] Hydrating server-rendered content');
145
+ hydrate(<PageComponent initialState={initialState} />, rootElement);
146
+ // After hydration is complete, set to false for future renders
147
+ isHydrating = false;
148
+ } else {
149
+ console.log('[Client] Rendering client-side');
150
+ render(<PageComponent initialState={initialState} />, rootElement);
151
+ }
152
+ } catch (error) {
153
+ console.error('[Client] Error rendering app:', error);
154
+
155
+ // Render error message
156
+ const rootElement = document.getElementById('root');
157
+ if (rootElement) {
158
+ rootElement.innerHTML = `
159
+ <div style="padding: 20px; color: red;">
160
+ <h1>Error</h1>
161
+ <p>${error.message}</p>
162
+ <pre>${error.stack}</pre>
163
+ </div>
164
+ `;
165
+ }
166
+ }
167
+ }
168
+
169
+ // Initial render/hydrate
170
+ renderApp();
171
+
172
+ // Set up client-side navigation
173
+ document.addEventListener('click', (e) => {
174
+ // Find if the clicked element is an anchor or inside an anchor
175
+ let target = e.target;
176
+ while (target && target.tagName !== 'A') {
177
+ target = target.parentElement;
178
+ if (!target) break;
179
+ }
180
+
181
+ // If we found an anchor that's for internal navigation
182
+ if (target &&
183
+ target.tagName === 'A' &&
184
+ target.getAttribute('href') &&
185
+ target.getAttribute('href').startsWith('/') &&
186
+ !target.getAttribute('href').startsWith('//') &&
187
+ !target.getAttribute('target')) {
188
+ e.preventDefault();
189
+ const href = target.getAttribute('href');
190
+
191
+ // Update history
192
+ window.history.pushState(null, '', href);
193
+
194
+ // Set to false since we're doing client-side navigation now
195
+ isHydrating = false;
196
+
197
+ // Re-render the app
198
+ renderApp();
199
+ }
200
+ });
201
+
202
+ // Handle back/forward navigation
203
+ window.addEventListener('popstate', () => {
204
+ // Not hydrating during history navigation
205
+ isHydrating = false;
206
+ renderApp();
207
+ });
208
+
209
+ // Setup live reload for development
210
+ if (typeof io !== 'undefined') {
211
+ const socket = io();
212
+
213
+ socket.on('reload', () => {
214
+ console.log('[Dev] Reloading page due to file changes');
215
+ window.location.reload();
216
+ });
217
+
218
+ socket.on('welcome', (data) => {
219
+ console.log('[Dev] Connected to development server:', data);
220
+ });
221
+ }
@@ -0,0 +1,67 @@
1
+ import { jsx } from 'frontend-hamroun';
2
+ import Layout from '../../components/Layout';
3
+
4
+ const AboutPage = ({ initialState }) => {
5
+ return (
6
+ <Layout title="About This App">
7
+ <div className="max-w-4xl mx-auto bg-white shadow-lg rounded-lg overflow-hidden">
8
+ <div className="p-8">
9
+ <p className="text-lg text-gray-700 mb-6">
10
+ This is a frontend application built with Frontend Hamroun framework and styled with Tailwind CSS.
11
+ </p>
12
+ <p className="text-gray-600 mb-8">
13
+ It features server-side rendering, client-side navigation, and websocket-based live reloading during development.
14
+ </p>
15
+
16
+ <div className="bg-gray-50 p-6 rounded-lg border border-gray-200 mb-8">
17
+ <h2 className="text-xl font-semibold text-gray-800 mb-4">Key Features</h2>
18
+ <ul className="space-y-2 text-gray-700">
19
+ <li className="flex items-center">
20
+ <svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
21
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
22
+ </svg>
23
+ Server-side rendering
24
+ </li>
25
+ <li className="flex items-center">
26
+ <svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
27
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
28
+ </svg>
29
+ Client-side navigation
30
+ </li>
31
+ <li className="flex items-center">
32
+ <svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
33
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
34
+ </svg>
35
+ Component-based architecture
36
+ </li>
37
+ <li className="flex items-center">
38
+ <svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
39
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
40
+ </svg>
41
+ Integrated API backend
42
+ </li>
43
+ <li className="flex items-center">
44
+ <svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
45
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
46
+ </svg>
47
+ Live reload during development
48
+ </li>
49
+ <li className="flex items-center">
50
+ <svg className="w-5 h-5 text-green-500 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
51
+ <path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7"></path>
52
+ </svg>
53
+ Tailwind CSS for styling
54
+ </li>
55
+ </ul>
56
+ </div>
57
+
58
+ <a href="/" className="inline-block px-6 py-3 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
59
+ Back to Home
60
+ </a>
61
+ </div>
62
+ </div>
63
+ </Layout>
64
+ );
65
+ };
66
+
67
+ export default AboutPage;