frontend-hamroun 1.2.27 → 1.2.29
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.
- package/README.md +7 -0
- package/bin/banner.js +0 -0
- package/bin/cli-utils.js +0 -0
- package/bin/cli.js +536 -598
- package/package.json +1 -1
- package/templates/fullstack-app/README.md +37 -0
- package/templates/fullstack-app/build/main.css +664 -0
- package/templates/fullstack-app/build/main.css.map +7 -0
- package/templates/fullstack-app/build/main.js +682 -0
- package/templates/fullstack-app/build/main.js.map +7 -0
- package/templates/fullstack-app/build.ts +211 -0
- package/templates/fullstack-app/index.html +26 -3
- package/templates/fullstack-app/package-lock.json +2402 -438
- package/templates/fullstack-app/package.json +24 -9
- package/templates/fullstack-app/postcss.config.js +6 -0
- package/templates/fullstack-app/process-tailwind.js +45 -0
- package/templates/fullstack-app/public/_redirects +1 -0
- package/templates/fullstack-app/public/route-handler.js +47 -0
- package/templates/fullstack-app/public/spa-fix.html +17 -0
- package/templates/fullstack-app/public/styles.css +768 -0
- package/templates/fullstack-app/public/tailwind.css +15 -0
- package/templates/fullstack-app/server.js +101 -44
- package/templates/fullstack-app/server.ts +402 -39
- package/templates/fullstack-app/src/README.md +55 -0
- package/templates/fullstack-app/src/client.js +83 -16
- package/templates/fullstack-app/src/components/Layout.tsx +45 -0
- package/templates/fullstack-app/src/components/UserList.tsx +27 -0
- package/templates/fullstack-app/src/config.ts +42 -0
- package/templates/fullstack-app/src/data/api.ts +71 -0
- package/templates/fullstack-app/src/main.tsx +219 -7
- package/templates/fullstack-app/src/pages/about/index.tsx +67 -0
- package/templates/fullstack-app/src/pages/index.tsx +30 -60
- package/templates/fullstack-app/src/pages/users.tsx +60 -0
- package/templates/fullstack-app/src/router.ts +255 -0
- package/templates/fullstack-app/src/styles.css +5 -0
- package/templates/fullstack-app/tailwind.config.js +11 -0
- package/templates/fullstack-app/tsconfig.json +18 -0
- package/templates/fullstack-app/vite.config.js +53 -6
- package/templates/ssr-template/readme.md +50 -0
- package/templates/ssr-template/src/client.ts +46 -14
- 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
|
-
//
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
-
//
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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} © {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 {
|
2
|
-
import
|
1
|
+
import { render, hydrate, jsx } from 'frontend-hamroun';
|
2
|
+
import { router } from './router';
|
3
|
+
// Import Tailwind CSS
|
4
|
+
import './styles.css';
|
3
5
|
|
4
|
-
//
|
5
|
-
|
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
|
-
//
|
8
|
-
|
9
|
-
|
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;
|