frontend-hamroun 1.1.90 → 1.2.1

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 (97) hide show
  1. package/dist/{src/backend → backend}/api-utils.d.ts +2 -2
  2. package/dist/backend/api-utils.js +135 -0
  3. package/dist/backend/auth.js +387 -0
  4. package/dist/{src/backend → backend}/database.d.ts +1 -1
  5. package/dist/backend/database.js +91 -0
  6. package/dist/{src/backend → backend}/model.d.ts +2 -2
  7. package/dist/backend/model.js +176 -0
  8. package/dist/{src/backend → backend}/router.d.ts +1 -1
  9. package/dist/backend/router.js +137 -0
  10. package/dist/backend/server.js +268 -0
  11. package/dist/batch.js +22 -0
  12. package/dist/cli/index.js +1 -0
  13. package/dist/{src/component.d.ts → component.d.ts} +1 -1
  14. package/dist/component.js +84 -0
  15. package/dist/components/Counter.js +2 -0
  16. package/dist/context.js +20 -0
  17. package/dist/frontend-hamroun.es.js +1680 -0
  18. package/dist/frontend-hamroun.es.js.map +1 -0
  19. package/dist/frontend-hamroun.umd.js +2 -0
  20. package/dist/frontend-hamroun.umd.js.map +1 -0
  21. package/dist/hooks.js +164 -0
  22. package/dist/index.d.ts +46 -0
  23. package/dist/index.js +52 -355
  24. package/dist/jsx-runtime/index.d.ts +9 -0
  25. package/dist/jsx-runtime/index.js +16 -0
  26. package/dist/jsx-runtime/jsx-dev-runtime.js +1 -0
  27. package/dist/jsx-runtime/jsx-runtime.js +91 -0
  28. package/dist/{src/jsx-runtime.d.ts → jsx-runtime.d.ts} +1 -1
  29. package/dist/jsx-runtime.js +192 -0
  30. package/dist/renderer.js +51 -0
  31. package/dist/{src/server-renderer.d.ts → server-renderer.d.ts} +3 -0
  32. package/dist/server-renderer.js +102 -0
  33. package/dist/vdom.js +27 -0
  34. package/package.json +40 -53
  35. package/scripts/build-cli.js +81 -0
  36. package/scripts/generate.js +134 -0
  37. package/src/backend/api-utils.ts +178 -0
  38. package/src/backend/auth.ts +543 -0
  39. package/src/backend/database.ts +104 -0
  40. package/src/backend/model.ts +196 -0
  41. package/src/backend/router.ts +176 -0
  42. package/src/backend/server.ts +330 -0
  43. package/src/backend/types.ts +257 -0
  44. package/src/batch.ts +24 -0
  45. package/src/cli/index.js +22 -40
  46. package/src/cli/index.ts +251 -0
  47. package/src/component.ts +98 -0
  48. package/src/components/Counter.tsx +4 -0
  49. package/src/context.ts +32 -0
  50. package/src/hooks.ts +211 -0
  51. package/src/index.ts +113 -0
  52. package/src/jsx-runtime/index.ts +24 -0
  53. package/src/jsx-runtime/jsx-runtime.ts +99 -0
  54. package/src/jsx-runtime.ts +226 -0
  55. package/src/renderer.ts +55 -0
  56. package/src/server-renderer.ts +114 -0
  57. package/src/types/bcrypt.d.ts +30 -0
  58. package/src/types/jsonwebtoken.d.ts +55 -0
  59. package/src/types.d.ts +26 -0
  60. package/src/types.ts +21 -0
  61. package/src/vdom.ts +34 -0
  62. package/templates/basic-app/package.json +17 -15
  63. package/templates/basic-app/postcss.config.js +1 -0
  64. package/templates/basic-app/src/App.tsx +65 -0
  65. package/templates/basic-app/src/api.ts +58 -0
  66. package/templates/basic-app/src/components/Counter.tsx +26 -0
  67. package/templates/basic-app/src/components/Header.tsx +9 -0
  68. package/templates/basic-app/src/components/TodoList.tsx +90 -0
  69. package/templates/basic-app/src/main.ts +20 -0
  70. package/templates/basic-app/src/server.ts +99 -0
  71. package/templates/basic-app/tailwind.config.js +23 -2
  72. package/templates/full-stack/index.html +13 -0
  73. package/templates/full-stack/package.json +25 -18
  74. package/templates/full-stack/src/main.css +3 -0
  75. package/templates/full-stack/src/main.ts +20 -0
  76. package/bin/cli.js +0 -371
  77. package/dist/index.js.map +0 -1
  78. package/dist/index.mjs +0 -139269
  79. package/dist/index.mjs.map +0 -1
  80. package/dist/src/index.d.ts +0 -16
  81. package/dist/test/setupTests.d.ts +0 -4
  82. /package/dist/{src/backend → backend}/auth.d.ts +0 -0
  83. /package/dist/{src/backend → backend}/server.d.ts +0 -0
  84. /package/dist/{src/backend → backend}/types.d.ts +0 -0
  85. /package/dist/{test/backend.test.d.ts → backend/types.js} +0 -0
  86. /package/dist/{src/batch.d.ts → batch.d.ts} +0 -0
  87. /package/dist/{src/cli → cli}/index.d.ts +0 -0
  88. /package/dist/{src/components → components}/Counter.d.ts +0 -0
  89. /package/dist/{src/context.d.ts → context.d.ts} +0 -0
  90. /package/dist/{src/hooks.d.ts → hooks.d.ts} +0 -0
  91. /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-dev-runtime.d.ts +0 -0
  92. /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-runtime.d.ts +0 -0
  93. /package/dist/{src/renderer.d.ts → renderer.d.ts} +0 -0
  94. /package/dist/{src/types.d.ts → types.d.ts} +0 -0
  95. /package/dist/{test/mockTest.d.ts → types.js} +0 -0
  96. /package/dist/{src/vdom.d.ts → vdom.d.ts} +0 -0
  97. /package/{dist/test/mongooseSetup.d.ts → src/jsx-runtime/jsx-dev-runtime.ts} +0 -0
@@ -0,0 +1,55 @@
1
+ import { createElement } from './jsx-runtime';
2
+ import { prepareRender, finishRender, setRenderCallback } from './hooks';
3
+ import { batchUpdates } from './batch';
4
+
5
+ // Track hydration state
6
+ let isHydrating = false;
7
+
8
+ /**
9
+ * Hydrates a server-rendered component
10
+ * @param element The element to hydrate
11
+ * @param container The container element that contains server-rendered HTML
12
+ */
13
+ export async function hydrate(element: any, container: HTMLElement) {
14
+ isHydrating = true;
15
+ try {
16
+ await render(element, container);
17
+ } finally {
18
+ isHydrating = false;
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Renders a component to the DOM
24
+ * @param element The element to render
25
+ * @param container The container to render into
26
+ */
27
+ export async function render(element: any, container: HTMLElement) {
28
+ console.log('Rendering to:', container.id || 'unnamed-container');
29
+
30
+ batchUpdates(async () => {
31
+ const rendererId = prepareRender();
32
+ try {
33
+ setRenderCallback(render, element, container);
34
+ const domNode = await createElement(element);
35
+
36
+ // Don't clear container if we're hydrating
37
+ if (!isHydrating) {
38
+ container.innerHTML = '';
39
+ }
40
+
41
+ // When hydrating, we should match and update existing nodes
42
+ // rather than appending new ones
43
+ if (isHydrating && container.firstChild) {
44
+ // During hydration, we assume the structure matches
45
+ // and just attach event handlers without replacing DOM
46
+ console.log('Hydrating existing DOM');
47
+ } else {
48
+ container.appendChild(domNode);
49
+ }
50
+
51
+ } finally {
52
+ finishRender();
53
+ }
54
+ });
55
+ }
@@ -0,0 +1,114 @@
1
+ import { prepareRender, finishRender, setRenderCallback } from './hooks';
2
+
3
+ /**
4
+ * Renders a component to a string
5
+ */
6
+ export async function renderToString(element: any): Promise<string> {
7
+ // Setup render context
8
+ const rendererId = prepareRender();
9
+ setRenderCallback(() => {}, element, null as any);
10
+
11
+ try {
12
+ if (element == null) return '';
13
+
14
+ if (typeof element === 'boolean') return '';
15
+
16
+ if (typeof element === 'number' || typeof element === 'string') {
17
+ return escapeHtml(String(element));
18
+ }
19
+
20
+ if (Array.isArray(element)) {
21
+ const children = await Promise.all(element.map(renderToString));
22
+ return children.join('');
23
+ }
24
+
25
+ if ('type' in element && element.props !== undefined) {
26
+ const { type, props } = element;
27
+
28
+ // Handle function components
29
+ if (typeof type === 'function') {
30
+ try {
31
+ // Prepare new render context for the component
32
+ prepareRender();
33
+ const result = await type(props || {});
34
+ const html = await renderToString(result);
35
+ finishRender();
36
+ return html;
37
+ } catch (error) {
38
+ console.error('Error rendering component:', error);
39
+ return '';
40
+ }
41
+ }
42
+
43
+ // Handle Fragment special case
44
+ if (type === Symbol.for('react.fragment') || type.name === 'Fragment') {
45
+ if (props.children) {
46
+ const children = Array.isArray(props.children) ? props.children : [props.children];
47
+ const renderedChildren = await Promise.all(children.map(renderToString));
48
+ return renderedChildren.join('');
49
+ }
50
+ return '';
51
+ }
52
+
53
+ // Handle regular elements
54
+ let html = `<${type}`;
55
+
56
+ // Add attributes, skipping internal ones like 'key'
57
+ for (const [key, value] of Object.entries(props || {})) {
58
+ if (key === 'children' || key === 'key') continue;
59
+ if (key === 'className') {
60
+ html += ` class="${escapeHtml(String(value))}"`;
61
+ } else if (key === 'style' && typeof value === 'object') {
62
+ html += ` style="${stringifyStyle(value || {})}"`;
63
+ } else if (!key.startsWith('on')) {
64
+ html += ` ${key}="${escapeHtml(String(value))}"`;
65
+ }
66
+ }
67
+
68
+ // Handle self-closing tags
69
+ const voidElements = new Set([
70
+ 'area', 'base', 'br', 'col', 'embed', 'hr', 'img', 'input',
71
+ 'link', 'meta', 'param', 'source', 'track', 'wbr'
72
+ ]);
73
+
74
+ if (voidElements.has(type)) {
75
+ return html + '/>';
76
+ }
77
+
78
+ html += '>';
79
+
80
+ // Add children
81
+ if (props?.children) {
82
+ const children = Array.isArray(props.children) ? props.children : [props.children];
83
+ for (const child of children) {
84
+ html += await renderToString(child);
85
+ }
86
+ }
87
+
88
+ return html + `</${type}>`;
89
+ }
90
+
91
+ return escapeHtml(String(element));
92
+ } finally {
93
+ finishRender();
94
+ }
95
+ }
96
+
97
+ function escapeHtml(str: string): string {
98
+ return str
99
+ .replace(/&/g, '&amp;')
100
+ .replace(/</g, '&lt;')
101
+ .replace(/>/g, '&gt;')
102
+ .replace(/"/g, '&quot;')
103
+ .replace(/'/g, '&#39;');
104
+ }
105
+
106
+ function stringifyStyle(style: Record<string, any>): string {
107
+ return Object.entries(style)
108
+ .map(([key, value]) => `${hyphenate(key)}:${value}`)
109
+ .join(';');
110
+ }
111
+
112
+ function hyphenate(str: string): string {
113
+ return str.replace(/[A-Z]/g, match => '-' + match.toLowerCase());
114
+ }
@@ -0,0 +1,30 @@
1
+ declare module 'bcrypt' {
2
+ /**
3
+ * Generate a salt
4
+ * @param rounds Number of rounds to use, defaults to 10
5
+ * @param callback Callback function (err, salt)
6
+ */
7
+ export function genSalt(rounds?: number, callback?: (err: Error | null, salt: string) => void): Promise<string>;
8
+
9
+ /**
10
+ * Generate a hash for the provided string
11
+ * @param data Data to hash
12
+ * @param saltOrRounds Salt or number of rounds to use
13
+ * @param callback Callback function (err, hash)
14
+ */
15
+ export function hash(data: string, saltOrRounds: string | number, callback?: (err: Error | null, hash: string) => void): Promise<string>;
16
+
17
+ /**
18
+ * Compare provided data with a hash
19
+ * @param data Data to compare
20
+ * @param encrypted Hash to compare against
21
+ * @param callback Callback function (err, result)
22
+ */
23
+ export function compare(data: string, encrypted: string, callback?: (err: Error | null, result: boolean) => void): Promise<boolean>;
24
+
25
+ /**
26
+ * Get the number of rounds used in the specified hash
27
+ * @param encrypted Hash to extract rounds from
28
+ */
29
+ export function getRounds(encrypted: string): number;
30
+ }
@@ -0,0 +1,55 @@
1
+ declare module 'jsonwebtoken' {
2
+ export interface JwtPayload {
3
+ [key: string]: any;
4
+ iat?: number;
5
+ exp?: number;
6
+ aud?: string | string[];
7
+ sub?: string;
8
+ iss?: string;
9
+ }
10
+
11
+ export interface VerifyOptions {
12
+ algorithms?: string[];
13
+ audience?: string | string[];
14
+ complete?: boolean;
15
+ issuer?: string | string[];
16
+ jwtid?: string;
17
+ ignoreExpiration?: boolean;
18
+ ignoreNotBefore?: boolean;
19
+ subject?: string;
20
+ clockTolerance?: number;
21
+ maxAge?: string | number;
22
+ clockTimestamp?: number;
23
+ }
24
+
25
+ export interface SignOptions {
26
+ algorithm?: string;
27
+ keyid?: string;
28
+ expiresIn?: string | number;
29
+ notBefore?: string | number;
30
+ audience?: string | string[];
31
+ subject?: string;
32
+ issuer?: string;
33
+ jwtid?: string;
34
+ noTimestamp?: boolean;
35
+ header?: object;
36
+ encoding?: string;
37
+ }
38
+
39
+ export function sign(
40
+ payload: string | Buffer | object,
41
+ secretOrPrivateKey: string | Buffer,
42
+ options?: SignOptions
43
+ ): string;
44
+
45
+ export function verify(
46
+ token: string,
47
+ secretOrPublicKey: string | Buffer,
48
+ options?: VerifyOptions
49
+ ): JwtPayload | string;
50
+
51
+ export function decode(
52
+ token: string,
53
+ options?: { complete?: boolean; json?: boolean }
54
+ ): null | JwtPayload | { [key: string]: any };
55
+ }
package/src/types.d.ts ADDED
@@ -0,0 +1,26 @@
1
+ declare namespace JSX {
2
+ interface IntrinsicElements {
3
+ }
4
+
5
+ interface Element extends Node {}
6
+
7
+ interface ElementAttributesProperty {
8
+ props: {};
9
+ }
10
+
11
+ interface ElementChildrenAttribute {
12
+ children: {};
13
+ }
14
+ }
15
+
16
+ interface Window {
17
+ jsx: any;
18
+ jsxs: any;
19
+ Fragment: any;
20
+ __renderStats?: {
21
+ elementsCreated: number;
22
+ textNodesCreated: number;
23
+ eventsAttached: number;
24
+ renderTime: number;
25
+ };
26
+ }
package/src/types.ts ADDED
@@ -0,0 +1,21 @@
1
+ export type { Context } from './context';
2
+
3
+ export interface VNode {
4
+ type: string | Function;
5
+ props: Record<string, any>;
6
+ key?: string | number;
7
+ }
8
+
9
+ // Global JSX namespace
10
+ declare global {
11
+ namespace JSX {
12
+ interface Element {
13
+ type: string | Function;
14
+ props: Record<string, any>;
15
+ key?: string | number;
16
+ }
17
+ interface IntrinsicElements {
18
+ [elemName: string]: any;
19
+ }
20
+ }
21
+ }
package/src/vdom.ts ADDED
@@ -0,0 +1,34 @@
1
+ interface VNode {
2
+ type: string | Function;
3
+ props: Record<string, any>;
4
+ key?: string | number;
5
+ }
6
+
7
+ function arePropsEqual(oldProps: any, newProps: any): boolean {
8
+ // Handle null/undefined props
9
+ oldProps = oldProps || {};
10
+ newProps = newProps || {};
11
+
12
+ const oldKeys = Object.keys(oldProps).filter(k => k !== 'children');
13
+ const newKeys = Object.keys(newProps).filter(k => k !== 'children');
14
+
15
+ if (oldKeys.length !== newKeys.length) return false;
16
+ return oldKeys.every(key => oldProps[key] === newProps[key]);
17
+ }
18
+
19
+ export function diff(oldNode: VNode | any, newNode: VNode | any): boolean {
20
+ if (oldNode == null || newNode == null) return oldNode !== newNode;
21
+ if (typeof oldNode !== typeof newNode) return true;
22
+ if (typeof newNode === 'string' || typeof newNode === 'number')
23
+ return oldNode !== newNode;
24
+
25
+ // Handle primitive values that are not objects
26
+ if (!oldNode.type || !newNode.type) return oldNode !== newNode;
27
+
28
+ if (newNode.type !== oldNode.type) return true;
29
+ return !arePropsEqual(oldNode.props, newNode.props);
30
+ }
31
+
32
+ export function shouldComponentUpdate(oldProps: any, newProps: any): boolean {
33
+ return !arePropsEqual(oldProps, newProps);
34
+ }
@@ -1,28 +1,30 @@
1
1
  {
2
- "name": "my-app",
2
+ "name": "front-package-app",
3
3
  "private": true,
4
- "version": "0.0.0",
4
+ "version": "0.1.0",
5
5
  "type": "module",
6
6
  "scripts": {
7
7
  "dev": "vite",
8
8
  "build": "tsc && vite build",
9
9
  "preview": "vite preview",
10
- "lint": "eslint src --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
11
- "lint:fix": "eslint src --ext ts,tsx --fix"
10
+ "start": "node dist/server.js",
11
+ "dev:full": "concurrently \"npm run dev\" \"npm run dev:server\"",
12
+ "dev:server": "tsx watch src/server.ts"
12
13
  },
13
14
  "dependencies": {
14
- "frontend-hamroun": "latest"
15
+ "front-package": "latest"
15
16
  },
16
17
  "devDependencies": {
17
- "typescript": "^5.0.0",
18
- "vite": "^4.4.9",
19
- "@typescript-eslint/eslint-plugin": "^6.0.0",
20
- "@typescript-eslint/parser": "^6.0.0",
21
- "eslint": "^8.45.0",
22
- "eslint-plugin-react-hooks": "^4.6.0",
23
- "eslint-plugin-react-refresh": "^0.4.3",
24
- "tailwindcss": "^3.3.3",
25
- "postcss": "^8.4.27",
26
- "autoprefixer": "^10.4.14"
18
+ "@tailwindcss/forms": "^0.5.7",
19
+ "@tailwindcss/typography": "^0.5.10",
20
+ "@types/node": "^20.10.0",
21
+ "autoprefixer": "^10.4.16",
22
+ "concurrently": "^8.2.2",
23
+ "cssnano": "^6.0.1",
24
+ "postcss": "^8.4.31",
25
+ "tailwindcss": "^3.3.5",
26
+ "tsx": "^4.6.0",
27
+ "typescript": "^5.3.2",
28
+ "vite": "^5.0.0"
27
29
  }
28
30
  }
@@ -2,5 +2,6 @@ export default {
2
2
  plugins: {
3
3
  tailwindcss: {},
4
4
  autoprefixer: {},
5
+ cssnano: process.env.NODE_ENV === 'production' ? { preset: 'default' } : false
5
6
  },
6
7
  }
@@ -0,0 +1,65 @@
1
+ import { useState, useEffect } from 'frontend-hamroun';
2
+ import { Header } from './components/Header';
3
+ import { Counter } from './components/Counter';
4
+ import { ApiClient } from './api.js';
5
+ import { TodoList } from './components/TodoList.js';
6
+
7
+ interface AppProps {
8
+ api: ApiClient;
9
+ }
10
+
11
+ export function App({ api }: AppProps) {
12
+ const [todos, setTodos] = useState([]);
13
+ const [isLoading, setIsLoading] = useState(true);
14
+ const [error, setError] = useState<string | null>(null);
15
+
16
+ useEffect(() => {
17
+ // Fetch data when component mounts
18
+ async function fetchData() {
19
+ try {
20
+ setIsLoading(true);
21
+ const data = await api.getTodos();
22
+ setTodos(data);
23
+ setError(null);
24
+ } catch (err) {
25
+ setError('Failed to fetch todos');
26
+ console.error(err);
27
+ } finally {
28
+ setIsLoading(false);
29
+ }
30
+ }
31
+
32
+ fetchData();
33
+ }, [api]);
34
+
35
+ return (
36
+ <div className="min-h-screen bg-gray-50">
37
+ <Header title="My Front Package App" />
38
+
39
+ <main className="container mx-auto px-4 py-8">
40
+ <h1 className="text-3xl font-bold text-center mb-8">
41
+ Welcome to your Front Package app!
42
+ </h1>
43
+
44
+ <div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl mb-8">
45
+ <div className="p-8">
46
+ <Counter />
47
+ </div>
48
+ </div>
49
+
50
+ <div className="max-w-md mx-auto bg-white rounded-xl shadow-md overflow-hidden md:max-w-2xl">
51
+ <div className="p-8">
52
+ <h2 className="text-xl font-bold mb-4">Todo List</h2>
53
+ {isLoading ? (
54
+ <div className="text-center py-4">Loading...</div>
55
+ ) : error ? (
56
+ <div className="text-red-500 py-4">{error}</div>
57
+ ) : (
58
+ <TodoList todos={todos} api={api} setTodos={setTodos} />
59
+ )}
60
+ </div>
61
+ </div>
62
+ </main>
63
+ </div>
64
+ );
65
+ }
@@ -0,0 +1,58 @@
1
+ export interface Todo {
2
+ id: string;
3
+ title: string;
4
+ completed: boolean;
5
+ }
6
+
7
+ export interface ApiClientOptions {
8
+ baseUrl: string;
9
+ }
10
+
11
+ export interface ApiClient {
12
+ getTodos: () => Promise<Todo[]>;
13
+ getTodo: (id: string) => Promise<Todo>;
14
+ addTodo: (todo: Omit<Todo, 'id'>) => Promise<Todo>;
15
+ updateTodo: (id: string, updates: Partial<Omit<Todo, 'id'>>) => Promise<Todo>;
16
+ deleteTodo: (id: string) => Promise<void>;
17
+ }
18
+
19
+ export function createApiClient(options: ApiClientOptions): ApiClient {
20
+ const { baseUrl } = options;
21
+
22
+ async function fetchJson<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
23
+ const response = await fetch(`${baseUrl}${endpoint}`, {
24
+ ...options,
25
+ headers: {
26
+ 'Content-Type': 'application/json',
27
+ ...options.headers,
28
+ },
29
+ });
30
+
31
+ if (!response.ok) {
32
+ const error = await response.text();
33
+ throw new Error(error || `API request failed with status ${response.status}`);
34
+ }
35
+
36
+ return response.json();
37
+ }
38
+
39
+ return {
40
+ getTodos: () => fetchJson<Todo[]>('/todos'),
41
+
42
+ getTodo: (id: string) => fetchJson<Todo>(`/todos/${id}`),
43
+
44
+ addTodo: (todo) => fetchJson<Todo>('/todos', {
45
+ method: 'POST',
46
+ body: JSON.stringify(todo),
47
+ }),
48
+
49
+ updateTodo: (id: string, updates) => fetchJson<Todo>(`/todos/${id}`, {
50
+ method: 'PATCH',
51
+ body: JSON.stringify(updates),
52
+ }),
53
+
54
+ deleteTodo: (id: string) => fetchJson<void>(`/todos/${id}`, {
55
+ method: 'DELETE',
56
+ }),
57
+ };
58
+ }
@@ -0,0 +1,26 @@
1
+ import { useState } from 'front-package';
2
+
3
+ export function Counter() {
4
+ const [count, setCount] = useState(0);
5
+
6
+ return (
7
+ <div className="text-center">
8
+ <h2 className="text-xl font-bold mb-4">Counter Example</h2>
9
+ <p className="mb-4">Count: <span className="font-bold">{count}</span></p>
10
+ <div className="flex justify-center gap-2">
11
+ <button
12
+ onClick={() => setCount(count - 1)}
13
+ className="px-4 py-2 bg-red-500 text-white rounded hover:bg-red-600 transition"
14
+ >
15
+ Decrement
16
+ </button>
17
+ <button
18
+ onClick={() => setCount(count + 1)}
19
+ className="px-4 py-2 bg-blue-500 text-white rounded hover:bg-blue-600 transition"
20
+ >
21
+ Increment
22
+ </button>
23
+ </div>
24
+ </div>
25
+ );
26
+ }
@@ -0,0 +1,9 @@
1
+ export function Header({ title }: { title: string }) {
2
+ return (
3
+ <header className="bg-primary-600 text-white shadow-lg">
4
+ <div className="container mx-auto px-4 py-6">
5
+ <h1 className="text-2xl font-bold">{title}</h1>
6
+ </div>
7
+ </header>
8
+ );
9
+ }
@@ -0,0 +1,90 @@
1
+ import { useState } from 'front-package';
2
+ import { ApiClient, Todo } from '../api';
3
+
4
+ interface TodoListProps {
5
+ todos: Todo[];
6
+ api: ApiClient;
7
+ setTodos: (todos: Todo[]) => void;
8
+ }
9
+
10
+ export function TodoList({ todos, api, setTodos }: TodoListProps) {
11
+ const [newTodoText, setNewTodoText] = useState('');
12
+ const [isSubmitting, setIsSubmitting] = useState(false);
13
+
14
+ async function handleAddTodo(e: Event) {
15
+ e.preventDefault();
16
+ if (!newTodoText.trim()) return;
17
+
18
+ try {
19
+ setIsSubmitting(true);
20
+ const newTodo = await api.addTodo({
21
+ title: newTodoText,
22
+ completed: false
23
+ });
24
+
25
+ setTodos([...todos, newTodo]);
26
+ setNewTodoText('');
27
+ } catch (err) {
28
+ console.error('Failed to add todo:', err);
29
+ } finally {
30
+ setIsSubmitting(false);
31
+ }
32
+ }
33
+
34
+ async function handleToggleTodo(id: string, completed: boolean) {
35
+ try {
36
+ await api.updateTodo(id, { completed });
37
+ setTodos(
38
+ todos.map(todo =>
39
+ todo.id === id ? { ...todo, completed } : todo
40
+ )
41
+ );
42
+ } catch (err) {
43
+ console.error('Failed to update todo:', err);
44
+ }
45
+ }
46
+
47
+ return (
48
+ <div>
49
+ <form onSubmit={handleAddTodo} className="mb-4">
50
+ <div className="flex gap-2">
51
+ <input
52
+ type="text"
53
+ value={newTodoText}
54
+ onChange={(e) => setNewTodoText(e.target.value)}
55
+ placeholder="Add a new todo..."
56
+ className="flex-1 border rounded px-3 py-2"
57
+ disabled={isSubmitting}
58
+ />
59
+ <button
60
+ type="submit"
61
+ className="px-4 py-2 bg-green-500 text-white rounded hover:bg-green-600 transition"
62
+ disabled={isSubmitting || !newTodoText.trim()}
63
+ >
64
+ {isSubmitting ? 'Adding...' : 'Add'}
65
+ </button>
66
+ </div>
67
+ </form>
68
+
69
+ <ul className="space-y-2">
70
+ {todos.length === 0 ? (
71
+ <li className="text-center py-2 text-gray-500">No todos yet</li>
72
+ ) : (
73
+ todos.map(todo => (
74
+ <li key={todo.id} className="flex items-center gap-2 p-2 border-b">
75
+ <input
76
+ type="checkbox"
77
+ checked={todo.completed}
78
+ onChange={(e) => handleToggleTodo(todo.id, e.target.checked)}
79
+ className="h-5 w-5 text-blue-600"
80
+ />
81
+ <span className={todo.completed ? 'line-through text-gray-500' : ''}>
82
+ {todo.title}
83
+ </span>
84
+ </li>
85
+ ))
86
+ )}
87
+ </ul>
88
+ </div>
89
+ );
90
+ }
@@ -0,0 +1,20 @@
1
+ import './main.css';
2
+ import { render } from 'frontend-hamroun';
3
+ import { App } from './App';
4
+ import { createApiClient } from './api';
5
+
6
+ // Initialize API client
7
+ const api = createApiClient({
8
+ baseUrl: import.meta.env.VITE_API_URL || '/api'
9
+ });
10
+
11
+ // Render the app into the DOM
12
+ document.addEventListener('DOMContentLoaded', () => {
13
+ const rootElement = document.getElementById('app');
14
+ if (rootElement) {
15
+ render(<App api={api} />, rootElement);
16
+ console.log('App rendered successfully');
17
+ } else {
18
+ console.error('Root element #app not found');
19
+ }
20
+ });