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.
- package/dist/{src/backend → backend}/api-utils.d.ts +2 -2
- package/dist/backend/api-utils.js +135 -0
- package/dist/backend/auth.js +387 -0
- package/dist/{src/backend → backend}/database.d.ts +1 -1
- package/dist/backend/database.js +91 -0
- package/dist/{src/backend → backend}/model.d.ts +2 -2
- package/dist/backend/model.js +176 -0
- package/dist/{src/backend → backend}/router.d.ts +1 -1
- package/dist/backend/router.js +137 -0
- package/dist/backend/server.js +268 -0
- package/dist/batch.js +22 -0
- package/dist/cli/index.js +1 -0
- package/dist/{src/component.d.ts → component.d.ts} +1 -1
- package/dist/component.js +84 -0
- package/dist/components/Counter.js +2 -0
- package/dist/context.js +20 -0
- package/dist/frontend-hamroun.es.js +1680 -0
- package/dist/frontend-hamroun.es.js.map +1 -0
- package/dist/frontend-hamroun.umd.js +2 -0
- package/dist/frontend-hamroun.umd.js.map +1 -0
- package/dist/hooks.js +164 -0
- package/dist/index.d.ts +46 -0
- package/dist/index.js +52 -355
- package/dist/jsx-runtime/index.d.ts +9 -0
- package/dist/jsx-runtime/index.js +16 -0
- package/dist/jsx-runtime/jsx-dev-runtime.js +1 -0
- package/dist/jsx-runtime/jsx-runtime.js +91 -0
- package/dist/{src/jsx-runtime.d.ts → jsx-runtime.d.ts} +1 -1
- package/dist/jsx-runtime.js +192 -0
- package/dist/renderer.js +51 -0
- package/dist/{src/server-renderer.d.ts → server-renderer.d.ts} +3 -0
- package/dist/server-renderer.js +102 -0
- package/dist/vdom.js +27 -0
- package/package.json +40 -53
- package/scripts/build-cli.js +81 -0
- package/scripts/generate.js +134 -0
- package/src/backend/api-utils.ts +178 -0
- package/src/backend/auth.ts +543 -0
- package/src/backend/database.ts +104 -0
- package/src/backend/model.ts +196 -0
- package/src/backend/router.ts +176 -0
- package/src/backend/server.ts +330 -0
- package/src/backend/types.ts +257 -0
- package/src/batch.ts +24 -0
- package/src/cli/index.js +22 -40
- package/src/cli/index.ts +251 -0
- package/src/component.ts +98 -0
- package/src/components/Counter.tsx +4 -0
- package/src/context.ts +32 -0
- package/src/hooks.ts +211 -0
- package/src/index.ts +113 -0
- package/src/jsx-runtime/index.ts +24 -0
- package/src/jsx-runtime/jsx-runtime.ts +99 -0
- package/src/jsx-runtime.ts +226 -0
- package/src/renderer.ts +55 -0
- package/src/server-renderer.ts +114 -0
- package/src/types/bcrypt.d.ts +30 -0
- package/src/types/jsonwebtoken.d.ts +55 -0
- package/src/types.d.ts +26 -0
- package/src/types.ts +21 -0
- package/src/vdom.ts +34 -0
- package/templates/basic-app/package.json +17 -15
- package/templates/basic-app/postcss.config.js +1 -0
- package/templates/basic-app/src/App.tsx +65 -0
- package/templates/basic-app/src/api.ts +58 -0
- package/templates/basic-app/src/components/Counter.tsx +26 -0
- package/templates/basic-app/src/components/Header.tsx +9 -0
- package/templates/basic-app/src/components/TodoList.tsx +90 -0
- package/templates/basic-app/src/main.ts +20 -0
- package/templates/basic-app/src/server.ts +99 -0
- package/templates/basic-app/tailwind.config.js +23 -2
- package/templates/full-stack/index.html +13 -0
- package/templates/full-stack/package.json +25 -18
- package/templates/full-stack/src/main.css +3 -0
- package/templates/full-stack/src/main.ts +20 -0
- package/bin/cli.js +0 -371
- package/dist/index.js.map +0 -1
- package/dist/index.mjs +0 -139269
- package/dist/index.mjs.map +0 -1
- package/dist/src/index.d.ts +0 -16
- package/dist/test/setupTests.d.ts +0 -4
- /package/dist/{src/backend → backend}/auth.d.ts +0 -0
- /package/dist/{src/backend → backend}/server.d.ts +0 -0
- /package/dist/{src/backend → backend}/types.d.ts +0 -0
- /package/dist/{test/backend.test.d.ts → backend/types.js} +0 -0
- /package/dist/{src/batch.d.ts → batch.d.ts} +0 -0
- /package/dist/{src/cli → cli}/index.d.ts +0 -0
- /package/dist/{src/components → components}/Counter.d.ts +0 -0
- /package/dist/{src/context.d.ts → context.d.ts} +0 -0
- /package/dist/{src/hooks.d.ts → hooks.d.ts} +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-dev-runtime.d.ts +0 -0
- /package/dist/{src/jsx-runtime → jsx-runtime}/jsx-runtime.d.ts +0 -0
- /package/dist/{src/renderer.d.ts → renderer.d.ts} +0 -0
- /package/dist/{src/types.d.ts → types.d.ts} +0 -0
- /package/dist/{test/mockTest.d.ts → types.js} +0 -0
- /package/dist/{src/vdom.d.ts → vdom.d.ts} +0 -0
- /package/{dist/test/mongooseSetup.d.ts → src/jsx-runtime/jsx-dev-runtime.ts} +0 -0
package/src/renderer.ts
ADDED
|
@@ -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, '&')
|
|
100
|
+
.replace(/</g, '<')
|
|
101
|
+
.replace(/>/g, '>')
|
|
102
|
+
.replace(/"/g, '"')
|
|
103
|
+
.replace(/'/g, ''');
|
|
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": "
|
|
2
|
+
"name": "front-package-app",
|
|
3
3
|
"private": true,
|
|
4
|
-
"version": "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
|
-
"
|
|
11
|
-
"
|
|
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
|
-
"
|
|
15
|
+
"front-package": "latest"
|
|
15
16
|
},
|
|
16
17
|
"devDependencies": {
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"@
|
|
20
|
-
"
|
|
21
|
-
"
|
|
22
|
-
"
|
|
23
|
-
"
|
|
24
|
-
"tailwindcss": "^3.3.
|
|
25
|
-
"
|
|
26
|
-
"
|
|
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
|
}
|
|
@@ -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,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
|
+
});
|