@jtl-software/cloud-app-template-frontend-react 0.0.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/CHANGELOG.md ADDED
@@ -0,0 +1,7 @@
1
+ # @jtl/cloud-app-template-frontend-react
2
+
3
+ ## 0.0.1
4
+
5
+ ### Patch Changes
6
+
7
+ - [`845f751`](https://github.com/jtl-software/cloud-apps-cli/commit/845f751930774ebbd0af55abcba505443494befe) Thanks [@tobilen](https://github.com/tobilen)! - initial
package/Dockerfile ADDED
@@ -0,0 +1,39 @@
1
+ # ---------- Stage 1 & 2: Build js-core and frontend ----------
2
+ FROM node:23-alpine AS build
3
+
4
+ ARG VITE_API_URL
5
+
6
+ WORKDIR /work
7
+
8
+ # 1️⃣ Copy both project dirs for caching
9
+ COPY src/sdk/js-core/package.json src/sdk/js-core/yarn.lock ./src/sdk/js-core/
10
+ COPY examples/hello-world-app/packages/frontend/package.json ./examples/hello-world-app/packages/frontend/
11
+
12
+ # Yarn workspaces monorepo: single lockfile at repo root.
13
+ # Copy root package.json + yarn.lock so installs in /packages/frontend use the root lock.
14
+ COPY examples/hello-world-app/yarn.lock ./examples/hello-world-app/
15
+ COPY examples/hello-world-app/package.json ./examples/hello-world-app/
16
+
17
+ # 2️⃣ Install js-core deps (cacheable)
18
+ WORKDIR /work/src/sdk/js-core
19
+ RUN yarn install --frozen-lockfile --ignore-scripts
20
+
21
+ # 3️⃣ Copy full source
22
+ WORKDIR /work
23
+ COPY src/sdk/js-core ./src/sdk/js-core
24
+ COPY examples/hello-world-app/packages/frontend ./examples/hello-world-app/packages/frontend
25
+
26
+ # 4️⃣ Build js-core
27
+ WORKDIR /work/src/sdk/js-core
28
+ RUN yarn build
29
+
30
+ # 5️⃣ Build frontend with local dist dependency
31
+ WORKDIR /work/examples/hello-world-app/packages/frontend
32
+ RUN yarn install --frozen-lockfile && yarn build
33
+
34
+ # ---------- Stage 3: Runtime ----------
35
+ FROM nginx:alpine AS final
36
+ COPY --from=build /work/examples/hello-world-app/packages/frontend/dist /usr/share/nginx/html
37
+ COPY examples/hello-world-app/packages/frontend/nginx.conf /etc/nginx/conf.d/default.conf
38
+ EXPOSE 80
39
+ CMD ["nginx", "-g", "daemon off;"]
package/_package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "{{APP_NAME}}-frontend",
3
+ "private": true,
4
+ "version": "0.0.0",
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "tsc -b && vite build",
9
+ "lint": "eslint .",
10
+ "preview": "vite preview",
11
+ "test": "vitest run"
12
+ },
13
+ "dependencies": {
14
+ "@jtl-software/cloud-apps-core": "latest",
15
+ "@jtl-software/platform-ui-react": "latest",
16
+ "react": "^19.0.0",
17
+ "react-dom": "^19.0.0"
18
+ },
19
+ "devDependencies": {
20
+ "@eslint/js": "^9.21.0",
21
+ "@tailwindcss/vite": "^4.2.2",
22
+ "@types/react": "^19.0.10",
23
+ "@types/react-dom": "^19.0.4",
24
+ "@vitejs/plugin-react-swc": "^3.8.0",
25
+ "eslint": "^9.21.0",
26
+ "eslint-plugin-react-hooks": "^5.1.0",
27
+ "eslint-plugin-react-refresh": "^0.4.19",
28
+ "globals": "^15.15.0",
29
+ "tailwindcss": "^4.2.2",
30
+ "typescript": "~5.7.2",
31
+ "typescript-eslint": "^8.24.1",
32
+ "vite": "^6.2.0",
33
+ "vitest": "^2.1.8"
34
+ }
35
+ }
@@ -0,0 +1,25 @@
1
+ import js from '@eslint/js';
2
+ import globals from 'globals';
3
+ import reactHooks from 'eslint-plugin-react-hooks';
4
+ import reactRefresh from 'eslint-plugin-react-refresh';
5
+ import tseslint from 'typescript-eslint';
6
+
7
+ export default tseslint.config(
8
+ { ignores: ['dist'] },
9
+ {
10
+ extends: [js.configs.recommended, ...tseslint.configs.recommended],
11
+ files: ['**/*.{ts,tsx}'],
12
+ languageOptions: {
13
+ ecmaVersion: 2020,
14
+ globals: globals.browser,
15
+ },
16
+ plugins: {
17
+ 'react-hooks': reactHooks,
18
+ 'react-refresh': reactRefresh,
19
+ },
20
+ rules: {
21
+ ...reactHooks.configs.recommended.rules,
22
+ 'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
23
+ },
24
+ },
25
+ );
package/index.html ADDED
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>{{APP_NAME}}</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="/src/main.tsx"></script>
12
+ </body>
13
+ </html>
package/manifest.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "manifestVersion": "1.0.0",
3
+ "version": "1.0.0",
4
+ "name": {
5
+ "short": "{{APP_NAME}}",
6
+ "full": "{{APP_NAME}}"
7
+ },
8
+ "description": {
9
+ "short": "{{APP_DESCRIPTION}}",
10
+ "full": "{{APP_DESCRIPTION}}"
11
+ },
12
+ "defaultLocale": "de-DE",
13
+ "locales": {
14
+ "de-DE": {},
15
+ "en": {}
16
+ },
17
+ "icon": {
18
+ "light": "https://hub.jtl-cloud.com/assets/image-placeholder.png",
19
+ "dark": "https://hub.jtl-cloud.com/assets/image-placeholder.png"
20
+ },
21
+ "lifecycle": {
22
+ "setupUrl": "http://localhost:3004/setup"
23
+ },
24
+ "capabilities": {
25
+ "hub": {
26
+ "appLauncher": {
27
+ "redirectUrl": "http://localhost:3004/hub"
28
+ }
29
+ },
30
+ "erp": {
31
+ "menuItems": [
32
+ {
33
+ "id": "{{APP_NAME}}-menu",
34
+ "name": "{{APP_NAME}}",
35
+ "url": "http://localhost:3004/erp"
36
+ }
37
+ ],
38
+ "pane": [
39
+ {
40
+ "url": "http://localhost:3004/pane",
41
+ "title": "{{APP_NAME}}",
42
+ "context": "customers",
43
+ "matchChildContext": true
44
+ }
45
+ ]
46
+ }
47
+ }
48
+ }
package/nginx.conf ADDED
@@ -0,0 +1,52 @@
1
+ server {
2
+ listen 80;
3
+ server_name _;
4
+
5
+ root /usr/share/nginx/html;
6
+ index index.html;
7
+
8
+ # Gzip compression
9
+ gzip on;
10
+ gzip_static on; # serve .gz files if available
11
+ gzip_types
12
+ text/plain
13
+ text/css
14
+ application/json
15
+ application/javascript
16
+ text/javascript
17
+ application/x-javascript
18
+ application/xml
19
+ font/woff2
20
+ image/svg+xml;
21
+ gzip_proxied any;
22
+ gzip_vary on;
23
+ gzip_min_length 256;
24
+
25
+ # Cache aggressively for hashed assets (JS, CSS, fonts, images, etc.)
26
+ location ~* \.(?:js|css|woff2?|eot|ttf|otf|svg|ico|jpg|jpeg|png|gif|webp)$ {
27
+ expires 1y;
28
+ access_log off;
29
+ add_header Cache-Control "public, immutable";
30
+ try_files $uri =404;
31
+ }
32
+
33
+ # Cache /assets/ folder (if you store fonts/images here)
34
+ location ^~ /assets/ {
35
+ expires 1y;
36
+ access_log off;
37
+ add_header Cache-Control "public, immutable";
38
+ try_files $uri =404;
39
+ }
40
+
41
+
42
+ location / {
43
+ try_files $uri /index.html;
44
+ }
45
+
46
+ location /static/ {
47
+ expires 1y;
48
+ add_header Cache-Control "public, immutable";
49
+ }
50
+
51
+ error_page 404 /index.html;
52
+ }
package/package.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@jtl-software/cloud-app-template-frontend-react",
3
+ "version": "0.0.1",
4
+ "publishConfig": { "access": "public" },
5
+ "description": "React + Vite + Tailwind frontend template for JTL Platform cloud apps"
6
+ }
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
package/src/App.css ADDED
File without changes
@@ -0,0 +1,7 @@
1
+ import { describe, it, expect } from 'vitest';
2
+
3
+ describe('Basic tests', () => {
4
+ it('should pass basic test', () => {
5
+ expect(1 + 1).toBe(2);
6
+ });
7
+ });
package/src/App.tsx ADDED
@@ -0,0 +1,38 @@
1
+ import { AppBridge } from '@jtl-software/cloud-apps-core';
2
+ import './App.css';
3
+ import { ErpPage, HubPage, PanePage, SetupPage, WelcomePage } from './pages';
4
+ import { useEffect } from 'react';
5
+
6
+ type AppMode = 'setup' | 'erp' | 'pane' | 'hub';
7
+
8
+ const App: React.FC<{ appBridge: AppBridge | null }> = ({ appBridge }) => {
9
+ const mode: AppMode = location.pathname.substring(1) as AppMode;
10
+
11
+ useEffect((): void => {
12
+ if (appBridge) {
13
+ console.log('[HelloWorldApp] bridge created!');
14
+ }
15
+ }, [appBridge]);
16
+
17
+ // Hub page runs standalone (no iframe, no AppBridge)
18
+ if (mode === 'hub') {
19
+ return <HubPage />;
20
+ }
21
+
22
+ if (!appBridge) {
23
+ return <WelcomePage />;
24
+ }
25
+
26
+ switch (mode) {
27
+ case 'setup':
28
+ return <SetupPage appBridge={appBridge} />;
29
+ case 'erp':
30
+ return <ErpPage appBridge={appBridge} />;
31
+ case 'pane':
32
+ return <PanePage appBridge={appBridge} />;
33
+ default:
34
+ return <WelcomePage connected />;
35
+ }
36
+ };
37
+
38
+ export default App;
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
@@ -0,0 +1 @@
1
+ export const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3005';
package/src/index.css ADDED
@@ -0,0 +1,3 @@
1
+ @import '@jtl-software/platform-ui-react/dist/main.css';
2
+ @import 'tailwindcss';
3
+ @source '../../../node_modules/@jtl-software/platform-ui-react/dist';
package/src/main.tsx ADDED
@@ -0,0 +1,21 @@
1
+ import { StrictMode } from 'react';
2
+ import { createRoot } from 'react-dom/client';
3
+ import './index.css';
4
+ import App from './App.tsx';
5
+ import { createAppBridge, AppBridge } from '@jtl-software/cloud-apps-core';
6
+
7
+ const root = createRoot(document.getElementById('root')!);
8
+
9
+ const renderApp = (appBridge: AppBridge | null) => {
10
+ root.render(
11
+ <StrictMode>
12
+ <App appBridge={appBridge} />
13
+ </StrictMode>,
14
+ );
15
+ };
16
+
17
+ renderApp(null);
18
+
19
+ createAppBridge().then(appBridge => {
20
+ renderApp(appBridge);
21
+ });
@@ -0,0 +1,93 @@
1
+ import { useCallback, useState } from 'react';
2
+ import IErpPageProps from './IErpPageProps';
3
+ import {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardDescription,
8
+ CardContent,
9
+ Text,
10
+ Badge,
11
+ Stack,
12
+ Separator,
13
+ Box,
14
+ Button,
15
+ } from '@jtl-software/platform-ui-react';
16
+ import { Globe, Link as LinkIcon } from 'lucide-react';
17
+
18
+ const manifestMappings = [{ icon: LinkIcon, field: 'capabilities.erp.menuItems[].url', description: 'Sidebar menu entry' }];
19
+
20
+ const ErpPage: React.FC<IErpPageProps> = ({ appBridge }) => {
21
+ const [isRequesting, setIsRequesting] = useState(false);
22
+ const [time, setTime] = useState<string | null>(null);
23
+
24
+ const handleRequestTimestamp = useCallback(async (): Promise<void> => {
25
+ try {
26
+ setIsRequesting(true);
27
+ appBridge.method.expose('getCurrentTime', () => new Date());
28
+ const result = await appBridge.method.call<Date>('getCurrentTime');
29
+ setTime(result.toUTCString());
30
+ } finally {
31
+ setIsRequesting(false);
32
+ }
33
+ }, [appBridge]);
34
+
35
+ return (
36
+ <Box className="flex justify-center p-12">
37
+ <Card className="max-w-[520px] w-full">
38
+ <CardHeader className="items-center">
39
+ <Globe size={40} color="#1a56db" strokeWidth={1.5} />
40
+ <CardTitle>ERP Integration</CardTitle>
41
+ <CardDescription className="text-center">
42
+ This is the main app view. It loads when the user clicks your app's menu entry in the ERP sidebar.
43
+ </CardDescription>
44
+ </CardHeader>
45
+ <CardContent>
46
+ <Stack spacing="5" direction="column">
47
+ <Stack spacing="3" direction="column">
48
+ <Text type="xs" weight="semibold" color="muted">
49
+ MANIFEST MAPPING
50
+ </Text>
51
+ {manifestMappings.map(mapping => (
52
+ <Stack key={mapping.field} spacing="3" direction="row" itemAlign="start">
53
+ <mapping.icon size={16} color="#1a56db" className="shrink-0 mt-0.5" />
54
+ <Stack spacing="0" direction="column">
55
+ <Text type="inline-code">{mapping.field}</Text>
56
+ <Text type="xs" color="muted">
57
+ → {mapping.description}
58
+ </Text>
59
+ </Stack>
60
+ </Stack>
61
+ ))}
62
+ </Stack>
63
+
64
+ <Separator />
65
+
66
+ <Stack spacing="3" direction="column">
67
+ <Stack spacing="2" direction="row" itemAlign="center">
68
+ <Text type="small" weight="semibold">
69
+ DEMO: AppBridge Methods
70
+ </Text>
71
+ <Badge variant="default" label="Ready" />
72
+ </Stack>
73
+ <Text type="xs" color="muted">
74
+ The AppBridge lets you expose and call methods between your app and the platform.
75
+ </Text>
76
+ <Button onClick={handleRequestTimestamp} label={isRequesting ? 'Requesting...' : 'Request Current Time'} />
77
+ <Box className="w-full p-4 border border-dashed border-[var(--base-border)] rounded-lg flex items-center justify-center">
78
+ <Text type="small" color="muted">
79
+ {time ?? 'Click the button to test'}
80
+ </Text>
81
+ </Box>
82
+ <Text type="xs" color="muted" align="center">
83
+ <Text type="inline-code">{"appBridge.method.call('getCurrentTime')"}</Text>
84
+ </Text>
85
+ </Stack>
86
+ </Stack>
87
+ </CardContent>
88
+ </Card>
89
+ </Box>
90
+ );
91
+ };
92
+
93
+ export default ErpPage;
@@ -0,0 +1,5 @@
1
+ import { AppBridge } from '@jtl-software/cloud-apps-core';
2
+
3
+ export default interface IErpPageProps {
4
+ appBridge: AppBridge;
5
+ }
@@ -0,0 +1 @@
1
+ export { default as ErpPage } from './ErpPage';
@@ -0,0 +1,71 @@
1
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, Text, Stack, Separator, Box, Link } from '@jtl-software/platform-ui-react';
2
+ import { Rocket, ExternalLink, MonitorSmartphone } from 'lucide-react';
3
+
4
+ const manifestMapping = [{ field: 'capabilities.hub.appLauncher.redirectUrl', description: 'Opens this page in a new browser tab' }];
5
+
6
+ const HubPage: React.FC = () => {
7
+ return (
8
+ <Box className="flex justify-center p-12">
9
+ <Card className="max-w-[520px] w-full">
10
+ <CardHeader className="items-center">
11
+ <Rocket size={40} color="#1a56db" strokeWidth={1.5} />
12
+ <CardTitle>Launched from Hub</CardTitle>
13
+ <CardDescription className="text-center">
14
+ This page opens when a user clicks your app card in the JTL-Cloud Hub. It runs in a full browser tab, not inside the ERP iframe.
15
+ </CardDescription>
16
+ </CardHeader>
17
+ <CardContent>
18
+ <Stack spacing="5" direction="column">
19
+ <Stack spacing="3" direction="column">
20
+ <Text type="xs" weight="semibold" color="muted">
21
+ MANIFEST MAPPING
22
+ </Text>
23
+ {manifestMapping.map(mapping => (
24
+ <Stack key={mapping.field} spacing="3" direction="row" itemAlign="start">
25
+ <ExternalLink size={16} color="#1a56db" className="shrink-0 mt-0.5" />
26
+ <Stack spacing="0" direction="column">
27
+ <Text type="inline-code">{mapping.field}</Text>
28
+ <Text type="xs" color="muted">
29
+ → {mapping.description}
30
+ </Text>
31
+ </Stack>
32
+ </Stack>
33
+ ))}
34
+ </Stack>
35
+
36
+ <Separator />
37
+
38
+ <Stack spacing="3" direction="column">
39
+ <Text type="xs" weight="semibold" color="muted">
40
+ HOW THIS DIFFERS
41
+ </Text>
42
+ <Stack spacing="3" direction="row" itemAlign="start">
43
+ <MonitorSmartphone size={16} color="#1a56db" className="shrink-0 mt-0.5" />
44
+ <Text type="small" color="muted">
45
+ Unlike <Text type="inline-code">/erp</Text> and <Text type="inline-code">/pane</Text>, this page runs outside the ERP — there is no
46
+ AppBridge available. Use this for standalone features like dashboards, settings, or onboarding flows.
47
+ </Text>
48
+ </Stack>
49
+ </Stack>
50
+
51
+ <Separator />
52
+
53
+ <Stack spacing="3" direction="column">
54
+ <Text type="xs" weight="semibold" color="muted">
55
+ NEXT STEPS
56
+ </Text>
57
+ <Text type="small" color="muted">
58
+ Replace this page with your app's main standalone experience — a dashboard, configuration panel, or landing page.
59
+ </Text>
60
+ <Link url="https://hub.jtl-cloud.com/" target="_blank">
61
+ Open JTL-Cloud Hub
62
+ </Link>
63
+ </Stack>
64
+ </Stack>
65
+ </CardContent>
66
+ </Card>
67
+ </Box>
68
+ );
69
+ };
70
+
71
+ export default HubPage;
@@ -0,0 +1 @@
1
+ export default interface IHubPageProps {}
@@ -0,0 +1 @@
1
+ export { default as HubPage } from './HubPage';
@@ -0,0 +1,5 @@
1
+ export * from './setup-page';
2
+ export * from './erp-page';
3
+ export * from './pane-page';
4
+ export * from './hub-page';
5
+ export * from './welcome-page';
@@ -0,0 +1,5 @@
1
+ import { AppBridge } from '@jtl-software/cloud-apps-core';
2
+
3
+ export default interface IPanePageProps {
4
+ appBridge: AppBridge;
5
+ }
@@ -0,0 +1,86 @@
1
+ import { Button, Input, Stack, Text, Separator, Box, Badge } from '@jtl-software/platform-ui-react';
2
+ import { useState, useEffect, useCallback } from 'react';
3
+ import IPanePageProps from './IPanePageProps';
4
+ import { PanelRight } from 'lucide-react';
5
+
6
+ const manifestFields = [
7
+ { label: 'url', value: 'capabilities.erp.pane[].url' },
8
+ { label: 'context', value: 'customers' },
9
+ { label: 'matchChildContext', value: 'true' },
10
+ ];
11
+
12
+ const PanePage: React.FC<IPanePageProps> = ({ appBridge }) => {
13
+ const [customer, setCustomer] = useState<string | undefined>(undefined);
14
+
15
+ useEffect(() => {
16
+ const unsubscribe = appBridge.event.subscribe('CustomerChanged', (data: unknown) => {
17
+ return new Promise<void>(resolve => {
18
+ setCustomer((data as { customerId: string }).customerId);
19
+ resolve();
20
+ });
21
+ });
22
+
23
+ return () => {
24
+ unsubscribe();
25
+ };
26
+ }, [appBridge]);
27
+
28
+ const handleGetCurrentCustomer = useCallback(() => {
29
+ appBridge.method.call('getCurrentCustomerId').then(customerId => {
30
+ setCustomer(customerId as string);
31
+ });
32
+ }, [appBridge]);
33
+
34
+ return (
35
+ <Box className="p-4">
36
+ <Stack spacing="4" direction="column">
37
+ <Stack spacing="2" direction="column" itemAlign="center">
38
+ <PanelRight size={32} color="#1a56db" strokeWidth={1.5} />
39
+ <Text type="h4" align="center">
40
+ Customer Pane
41
+ </Text>
42
+ <Text type="xs" color="muted" align="center">
43
+ This sidebar panel loads when the user views customer data. It receives context events from the ERP.
44
+ </Text>
45
+ </Stack>
46
+
47
+ <Separator />
48
+
49
+ <Stack spacing="2" direction="column">
50
+ <Text type="xs" weight="semibold" color="muted">
51
+ MANIFEST MAPPING
52
+ </Text>
53
+ {manifestFields.map(field => (
54
+ <Stack key={field.label} spacing="2" direction="row" itemAlign="center">
55
+ <Badge variant="outline" label={field.label} />
56
+ <Text type="xs" color="muted">
57
+ {field.value}
58
+ </Text>
59
+ </Stack>
60
+ ))}
61
+ </Stack>
62
+
63
+ <Separator />
64
+
65
+ <Stack spacing="3" direction="column">
66
+ <Stack spacing="2" direction="row" itemAlign="center">
67
+ <Text type="small" weight="semibold">
68
+ DEMO: Events
69
+ </Text>
70
+ <Badge variant="default" label="Listening" />
71
+ </Stack>
72
+ <Text type="xs" color="muted">
73
+ The AppBridge sends a CustomerChanged event when the user selects a different customer.
74
+ </Text>
75
+ <Input disabled value={customer} placeholder="No customer selected" />
76
+ <Button variant="outline" onClick={handleGetCurrentCustomer} label="Get Current Customer" />
77
+ <Text type="xs" color="muted" align="center">
78
+ <Text type="inline-code">{"appBridge.event.subscribe('CustomerChanged', ...)"}</Text>
79
+ </Text>
80
+ </Stack>
81
+ </Stack>
82
+ </Box>
83
+ );
84
+ };
85
+
86
+ export default PanePage;
@@ -0,0 +1,2 @@
1
+ export type { default as IPanePageProps } from './IPanePageProps';
2
+ export { default as PanePage } from './PanePage';
@@ -0,0 +1,5 @@
1
+ import { AppBridge } from '@jtl-software/cloud-apps-core';
2
+
3
+ export default interface ISetupPageProps {
4
+ appBridge: AppBridge;
5
+ }
@@ -0,0 +1,114 @@
1
+ import { useCallback, useState } from 'react';
2
+ import ISetupPageProps from './ISetupPageProps';
3
+ import {
4
+ Card,
5
+ CardHeader,
6
+ CardTitle,
7
+ CardDescription,
8
+ CardContent,
9
+ Text,
10
+ Badge,
11
+ Stack,
12
+ Separator,
13
+ Box,
14
+ Button,
15
+ } from '@jtl-software/platform-ui-react';
16
+ import { Settings } from 'lucide-react';
17
+ import { apiUrl } from '../../common/constants';
18
+
19
+ const howItWorks = [
20
+ { step: '1', text: 'The platform loads this page via ', code: 'lifecycle.setupUrl', suffix: ' in your manifest' },
21
+ { step: '2', text: 'Your app requests a session token via ', code: "appBridge.method.call('getSessionToken')", suffix: '' },
22
+ { step: '3', text: 'Call ', code: "appBridge.method.call('setupCompleted')", suffix: ' to finish installation' },
23
+ ];
24
+
25
+ const SetupPage: React.FC<ISetupPageProps> = ({ appBridge }) => {
26
+ const [isLoading, setIsLoading] = useState(false);
27
+ const [isComplete, setIsComplete] = useState(false);
28
+ const [error, setError] = useState<string | null>(null);
29
+
30
+ const handleSetupCompleted = useCallback(async (): Promise<void> => {
31
+ try {
32
+ setIsLoading(true);
33
+ setError(null);
34
+ const sessionToken = await appBridge.method.call('getSessionToken');
35
+ const response = await fetch(`${apiUrl}/connect-tenant`, {
36
+ method: 'POST',
37
+ headers: { 'Content-Type': 'application/json' },
38
+ body: JSON.stringify({ sessionToken }),
39
+ });
40
+
41
+ const responseBody = await response.text();
42
+ console.log('Response from backend:', response.status, responseBody);
43
+
44
+ if (response.ok) {
45
+ await appBridge.method.call('setupCompleted');
46
+ setIsComplete(true);
47
+ } else {
48
+ setError(`Backend returned ${response.status}`);
49
+ }
50
+ } catch (err) {
51
+ console.error('An error occurred during setup:', err);
52
+ setError(String(err));
53
+ } finally {
54
+ setIsLoading(false);
55
+ }
56
+ }, [appBridge]);
57
+
58
+ return (
59
+ <Box className="flex justify-center p-12">
60
+ <Card className="max-w-[520px] w-full">
61
+ <CardHeader className="items-center">
62
+ <Settings size={40} color="#1a56db" strokeWidth={1.5} />
63
+ <CardTitle>App Setup</CardTitle>
64
+ <CardDescription className="text-center">
65
+ This page is shown during app installation. Complete the setup to connect your app to the platform.
66
+ </CardDescription>
67
+ </CardHeader>
68
+ <CardContent>
69
+ <Stack spacing="5" direction="column">
70
+ <Stack spacing="3" direction="column">
71
+ <Text type="xs" weight="semibold" color="muted">
72
+ HOW IT WORKS
73
+ </Text>
74
+ {howItWorks.map(item => (
75
+ <Stack key={item.step} spacing="3" direction="row" itemAlign="start">
76
+ <Badge variant="outline" label={item.step} />
77
+ <Text type="small" color="muted">
78
+ {item.text}
79
+ <Text type="inline-code">{item.code}</Text>
80
+ {item.suffix}
81
+ </Text>
82
+ </Stack>
83
+ ))}
84
+ </Stack>
85
+
86
+ <Separator />
87
+
88
+ <Stack spacing="3" direction="column">
89
+ <Text type="xs" weight="semibold" color="muted">
90
+ TRY IT
91
+ </Text>
92
+ <Button onClick={handleSetupCompleted} label={isLoading ? 'Setting up...' : 'Complete Setup'} />
93
+ {isComplete && (
94
+ <Text type="small" color="success">
95
+ Setup completed successfully.
96
+ </Text>
97
+ )}
98
+ {error && (
99
+ <Text type="small" color="danger">
100
+ {error}
101
+ </Text>
102
+ )}
103
+ <Text type="xs" color="muted" align="center">
104
+ This will call your backend and signal setup completion to the platform.
105
+ </Text>
106
+ </Stack>
107
+ </Stack>
108
+ </CardContent>
109
+ </Card>
110
+ </Box>
111
+ );
112
+ };
113
+
114
+ export default SetupPage;
@@ -0,0 +1 @@
1
+ export { default as SetupPage } from './SetupPage';
@@ -0,0 +1,129 @@
1
+ import { Card, CardHeader, CardTitle, CardDescription, CardContent, Text, Badge, Stack, Separator, Box, Link } from '@jtl-software/platform-ui-react';
2
+ import { CircleCheck, CircleAlert, Globe, UserPlus, KeyRound, CloudDownload, Settings, PanelRight } from 'lucide-react';
3
+
4
+ const routes = [
5
+ { path: '/setup', icon: Settings, description: 'Initial app setup and tenant connection' },
6
+ { path: '/erp', icon: Globe, description: 'ERP integration and data display' },
7
+ { path: '/pane', icon: PanelRight, description: 'Context-aware customer sidebar panel' },
8
+ ];
9
+
10
+ const gettingStartedSteps = [
11
+ {
12
+ icon: UserPlus,
13
+ title: 'Register your app',
14
+ description: 'Create your app in the Partner Portal and get your Client ID and Secret.',
15
+ linkUrl: 'https://partner.jtl-cloud.com/',
16
+ linkLabel: 'Open Partner Portal',
17
+ },
18
+ {
19
+ icon: KeyRound,
20
+ title: 'Configure environment',
21
+ description: 'Add your credentials to packages/backend/.env',
22
+ },
23
+ {
24
+ icon: CloudDownload,
25
+ title: 'Install in JTL-Cloud',
26
+ description: "Open the Hub, find your app under 'Apps in development', and install it.",
27
+ linkUrl: 'https://hub.jtl-cloud.com/',
28
+ linkLabel: 'Open JTL-Cloud Hub',
29
+ },
30
+ ];
31
+
32
+ const RoutesList: React.FC = () => (
33
+ <Stack spacing="3" direction="column">
34
+ <Text type="xs" weight="semibold" color="muted">
35
+ APP ROUTES
36
+ </Text>
37
+ {routes.map(route => (
38
+ <Stack key={route.path} spacing="3" direction="row" itemAlign="center">
39
+ <Badge variant="outline" label={route.path} />
40
+ <Text type="small" color="muted">
41
+ {route.description}
42
+ </Text>
43
+ </Stack>
44
+ ))}
45
+ </Stack>
46
+ );
47
+
48
+ const StandaloneWelcome: React.FC = () => (
49
+ <Card className="max-w-[520px] w-full">
50
+ <CardHeader className="items-center">
51
+ <CircleAlert size={40} color="#f59e0b" strokeWidth={1.5} />
52
+ <CardTitle>App is not connected</CardTitle>
53
+ <CardDescription className="text-center">No AppBridge detected. The app is running locally outside the JTL Platform.</CardDescription>
54
+ </CardHeader>
55
+ <CardContent>
56
+ <Stack spacing="5" direction="column">
57
+ <Stack spacing="4" direction="column">
58
+ <Text type="xs" weight="semibold" color="muted">
59
+ GET STARTED
60
+ </Text>
61
+ {gettingStartedSteps.map((step, i) => (
62
+ <Stack key={step.title} spacing="3" direction="row" itemAlign="start">
63
+ <Badge variant="outline" label={String(i + 1)} />
64
+ <Stack spacing="1" direction="column">
65
+ <Text type="small" weight="medium">
66
+ {step.title}
67
+ </Text>
68
+ <Text type="xs" color="muted">
69
+ {step.description}
70
+ </Text>
71
+ {step.linkUrl && (
72
+ <Link url={step.linkUrl} target="_blank">
73
+ {step.linkLabel}
74
+ </Link>
75
+ )}
76
+ </Stack>
77
+ </Stack>
78
+ ))}
79
+ </Stack>
80
+
81
+ <Separator />
82
+
83
+ <RoutesList />
84
+ </Stack>
85
+ </CardContent>
86
+ </Card>
87
+ );
88
+
89
+ const ConnectedWelcome: React.FC = () => (
90
+ <Card className="max-w-[520px] w-full">
91
+ <CardHeader className="items-center">
92
+ <CircleCheck size={40} color="#10b981" strokeWidth={1.5} />
93
+ <CardTitle>App is connected</CardTitle>
94
+ <CardDescription className="text-center">The AppBridge is active and your app is running inside the JTL Platform.</CardDescription>
95
+ </CardHeader>
96
+ <CardContent>
97
+ <Stack spacing="5" direction="column">
98
+ <RoutesList />
99
+
100
+ <Separator />
101
+
102
+ <Stack spacing="3" direction="column">
103
+ <Text type="xs" weight="semibold" color="muted">
104
+ STATUS
105
+ </Text>
106
+ <Stack spacing="2" direction="row" itemAlign="center">
107
+ <Text type="small">AppBridge:</Text>
108
+ <Badge variant="default" label="Connected" icon="check" />
109
+ </Stack>
110
+ <Stack spacing="2" direction="row" itemAlign="center">
111
+ <Text type="small">Mode:</Text>
112
+ <Text type="small" color="muted">
113
+ ERP Embedded
114
+ </Text>
115
+ </Stack>
116
+ <Text type="xs" color="muted">
117
+ Navigate using the ERP menu to access app features.
118
+ </Text>
119
+ </Stack>
120
+ </Stack>
121
+ </CardContent>
122
+ </Card>
123
+ );
124
+
125
+ const WelcomePage: React.FC<{ connected?: boolean }> = ({ connected = false }) => {
126
+ return <Box className="flex justify-center p-12">{connected ? <ConnectedWelcome /> : <StandaloneWelcome />}</Box>;
127
+ };
128
+
129
+ export default WelcomePage;
@@ -0,0 +1 @@
1
+ export { default as WelcomePage } from './WelcomePage';
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,26 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
4
+ "target": "ES2020",
5
+ "useDefineForClassFields": true,
6
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
7
+ "module": "ESNext",
8
+ "skipLibCheck": true,
9
+
10
+ /* Bundler mode */
11
+ "moduleResolution": "bundler",
12
+ "allowImportingTsExtensions": true,
13
+ "isolatedModules": true,
14
+ "moduleDetection": "force",
15
+ "noEmit": true,
16
+ "jsx": "react-jsx",
17
+
18
+ /* Linting */
19
+ "strict": true,
20
+ "noUnusedLocals": true,
21
+ "noUnusedParameters": true,
22
+ "noFallthroughCasesInSwitch": true,
23
+ "noUncheckedSideEffectImports": true
24
+ },
25
+ "include": ["src"]
26
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,4 @@
1
+ {
2
+ "files": [],
3
+ "references": [{ "path": "./tsconfig.app.json" }, { "path": "./tsconfig.node.json" }]
4
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
4
+ "target": "ES2022",
5
+ "lib": ["ES2023"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+
9
+ /* Bundler mode */
10
+ "moduleResolution": "bundler",
11
+ "allowImportingTsExtensions": true,
12
+ "isolatedModules": true,
13
+ "moduleDetection": "force",
14
+ "noEmit": true,
15
+
16
+ /* Linting */
17
+ "strict": true,
18
+ "noUnusedLocals": true,
19
+ "noUnusedParameters": true,
20
+ "noFallthroughCasesInSwitch": true,
21
+ "noUncheckedSideEffectImports": true
22
+ },
23
+ "include": ["vite.config.ts"]
24
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { defineConfig } from 'vite';
2
+ import react from '@vitejs/plugin-react-swc';
3
+ import tailwindcss from '@tailwindcss/vite';
4
+ import path from 'path';
5
+ import { createRequire } from 'node:module';
6
+
7
+ const require = createRequire(import.meta.url);
8
+
9
+ // https://vite.dev/config/
10
+ export default defineConfig({
11
+ server: {
12
+ port: 3004,
13
+ },
14
+ plugins: [tailwindcss(), react()],
15
+ resolve: {
16
+ alias: {
17
+ '/assets': path.join(path.dirname(require.resolve('@jtl-software/platform-ui-react')), 'assets'),
18
+ },
19
+ },
20
+ });