pi-kiosk-shared 1.0.0

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 ADDED
@@ -0,0 +1,85 @@
1
+ # @pi-kiosk/shared
2
+
3
+ Shared components, utilities, and types for the Pi Kiosk system.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @pi-kiosk/shared
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### Components
14
+
15
+ ```tsx
16
+ import { ErrorDisplay, LoadingSpinner } from '@pi-kiosk/shared';
17
+
18
+ function MyComponent() {
19
+ return (
20
+ <div>
21
+ <LoadingSpinner />
22
+ <ErrorDisplay error={new Error('Something went wrong')} />
23
+ </div>
24
+ );
25
+ }
26
+ ```
27
+
28
+ ### Types
29
+
30
+ ```tsx
31
+ import { Product, ApiResponse, KioskStatus } from '@pi-kiosk/shared';
32
+
33
+ const product: Product = {
34
+ id: 1,
35
+ name: 'Coffee',
36
+ price: 25.0,
37
+ description: 'Fresh coffee',
38
+ image: '☕',
39
+ quantityInStock: 10,
40
+ clickedOn: 0,
41
+ numberOfPurchases: 0,
42
+ };
43
+ ```
44
+
45
+ ### Hooks
46
+
47
+ ```tsx
48
+ import { useApi, useErrorHandler } from '@pi-kiosk/shared';
49
+
50
+ function MyComponent() {
51
+ const api = useApi();
52
+ const { handleError } = useErrorHandler();
53
+
54
+ // Use the hooks...
55
+ }
56
+ ```
57
+
58
+ ### Utilities
59
+
60
+ ```tsx
61
+ import { formatPrice, validateEmail } from '@pi-kiosk/shared';
62
+
63
+ const price = formatPrice(25.5); // "25,50 Kč"
64
+ const isValid = validateEmail('user@example.com'); // true
65
+ ```
66
+
67
+ ## Development
68
+
69
+ ```bash
70
+ # Install dependencies
71
+ npm install
72
+
73
+ # Build the package
74
+ npm run build
75
+
76
+ # Run tests
77
+ npm test
78
+
79
+ # Watch mode for development
80
+ npm run dev
81
+ ```
82
+
83
+ ## License
84
+
85
+ MIT
package/dist/api.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export declare const API_ENDPOINTS: {
2
+ readonly PRODUCTS: "/api/products";
3
+ readonly PRODUCT_CLICK: "/api/products/:id/click";
4
+ readonly ADMIN_LOGIN: "/admin/login";
5
+ readonly ADMIN_PRODUCTS: "/admin/products";
6
+ readonly ADMIN_KIOSKS: "/admin/kiosks";
7
+ readonly HEALTH: "/health";
8
+ readonly CHECK_TRANSACTIONS: "/api/check-new-transactions";
9
+ };
10
+ export declare class APIClient {
11
+ private baseUrl;
12
+ private kioskSecret?;
13
+ constructor(baseUrl: string, kioskSecret?: string);
14
+ private request;
15
+ get<T>(endpoint: string): Promise<T>;
16
+ post<T>(endpoint: string, data?: any): Promise<T>;
17
+ put<T>(endpoint: string, data?: any): Promise<T>;
18
+ delete<T>(endpoint: string): Promise<T>;
19
+ }
20
+ export declare const createAPIClient: (baseUrl?: string, kioskSecret?: string) => APIClient;
package/dist/api.js ADDED
@@ -0,0 +1,57 @@
1
+ // Shared API utilities
2
+ export const API_ENDPOINTS = {
3
+ PRODUCTS: '/api/products',
4
+ PRODUCT_CLICK: '/api/products/:id/click',
5
+ ADMIN_LOGIN: '/admin/login',
6
+ ADMIN_PRODUCTS: '/admin/products',
7
+ ADMIN_KIOSKS: '/admin/kiosks',
8
+ HEALTH: '/health',
9
+ CHECK_TRANSACTIONS: '/api/check-new-transactions'
10
+ };
11
+ export class APIClient {
12
+ constructor(baseUrl, kioskSecret) {
13
+ this.baseUrl = baseUrl.replace(/\/$/, ''); // Remove trailing slash
14
+ this.kioskSecret = kioskSecret;
15
+ }
16
+ async request(endpoint, options = {}) {
17
+ const url = `${this.baseUrl}${endpoint}`;
18
+ const headers = {
19
+ 'Content-Type': 'application/json',
20
+ ...(options.headers || {}),
21
+ };
22
+ // Add kiosk secret if available
23
+ if (this.kioskSecret) {
24
+ headers['X-Kiosk-Secret'] = this.kioskSecret;
25
+ }
26
+ const response = await fetch(url, {
27
+ ...options,
28
+ headers,
29
+ });
30
+ if (!response.ok) {
31
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
32
+ }
33
+ return response.json();
34
+ }
35
+ async get(endpoint) {
36
+ return this.request(endpoint, { method: 'GET' });
37
+ }
38
+ async post(endpoint, data) {
39
+ return this.request(endpoint, {
40
+ method: 'POST',
41
+ body: data ? JSON.stringify(data) : undefined,
42
+ });
43
+ }
44
+ async put(endpoint, data) {
45
+ return this.request(endpoint, {
46
+ method: 'PUT',
47
+ body: data ? JSON.stringify(data) : undefined,
48
+ });
49
+ }
50
+ async delete(endpoint) {
51
+ return this.request(endpoint, { method: 'DELETE' });
52
+ }
53
+ }
54
+ export const createAPIClient = (baseUrl, kioskSecret) => {
55
+ const url = baseUrl || process.env.REACT_APP_API_URL || 'http://localhost:3015';
56
+ return new APIClient(url, kioskSecret);
57
+ };
@@ -0,0 +1,12 @@
1
+ import { AppError } from '../errors';
2
+ interface ErrorDisplayProps {
3
+ error: Error | AppError;
4
+ onRetry?: () => void;
5
+ onDismiss?: () => void;
6
+ className?: string;
7
+ showDetails?: boolean;
8
+ retryLabel?: string;
9
+ dismissLabel?: string;
10
+ }
11
+ export declare function ErrorDisplay({ error, onRetry, onDismiss, className, showDetails, retryLabel, dismissLabel }: ErrorDisplayProps): import("react/jsx-runtime").JSX.Element;
12
+ export {};
@@ -0,0 +1,8 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getErrorMessage } from '../errors';
3
+ import { CSS_CLASSES } from '../constants';
4
+ export function ErrorDisplay({ error, onRetry, onDismiss, className = '', showDetails = false, retryLabel = 'Zkusit znovu', dismissLabel = 'Zavřít' }) {
5
+ const userMessage = getErrorMessage(error);
6
+ const isNetworkError = error.name === 'NetworkError';
7
+ return (_jsx("div", { className: `error-display ${CSS_CLASSES.ERROR} ${className}`, role: "alert", "aria-live": "assertive", children: _jsxs("div", { className: "error-content", children: [_jsx("div", { className: "error-icon", "aria-hidden": "true", children: isNetworkError ? '🌐' : '❌' }), _jsxs("div", { className: "error-text", children: [_jsx("h3", { className: "error-title", children: isNetworkError ? 'Problém s připojením' : 'Došlo k chybě' }), _jsx("p", { className: "error-message", children: userMessage })] }), showDetails && process.env.NODE_ENV === 'development' && (_jsxs("details", { className: "error-details", children: [_jsx("summary", { children: "Technick\u00E9 detaily" }), _jsx("pre", { className: "error-stack", children: _jsx("code", { children: error.stack || error.message }) })] })), _jsxs("div", { className: "error-actions", children: [onRetry && (_jsxs("button", { onClick: onRetry, className: `retry-btn ${CSS_CLASSES.BUTTON_PRIMARY}`, type: "button", children: ["\uD83D\uDD04 ", retryLabel] })), onDismiss && (_jsxs("button", { onClick: onDismiss, className: `dismiss-btn ${CSS_CLASSES.BUTTON_SECONDARY}`, type: "button", children: ["\u2715 ", dismissLabel] }))] })] }) }));
8
+ }
@@ -0,0 +1,8 @@
1
+ interface LoadingSpinnerProps {
2
+ size?: 'small' | 'medium' | 'large';
3
+ message?: string;
4
+ className?: string;
5
+ 'aria-label'?: string;
6
+ }
7
+ export declare function LoadingSpinner({ size, message, className, 'aria-label': ariaLabel }: LoadingSpinnerProps): import("react/jsx-runtime").JSX.Element;
8
+ export {};
@@ -0,0 +1,10 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { CSS_CLASSES } from '../constants';
3
+ export function LoadingSpinner({ size = 'medium', message, className = '', 'aria-label': ariaLabel = 'Loading' }) {
4
+ const sizeClass = {
5
+ small: 'spinner-small',
6
+ medium: 'spinner-medium',
7
+ large: 'spinner-large'
8
+ }[size];
9
+ return (_jsxs("div", { className: `loading-container ${CSS_CLASSES.LOADING} ${className}`, children: [_jsx("div", { className: `spinner ${sizeClass}`, role: "status", "aria-label": ariaLabel, children: _jsx("span", { className: "sr-only", children: ariaLabel }) }), message && (_jsx("p", { className: "loading-message", "aria-live": "polite", children: message }))] }));
10
+ }
@@ -0,0 +1,96 @@
1
+ export interface DeploymentConfig {
2
+ railway: {
3
+ testService: string;
4
+ stagingService: string;
5
+ productionService: string;
6
+ };
7
+ environmentVariables: {
8
+ test: Record<string, string>;
9
+ staging: Record<string, string>;
10
+ production: Record<string, string>;
11
+ };
12
+ database: {
13
+ test: string;
14
+ staging: string;
15
+ production: string;
16
+ };
17
+ }
18
+ export declare const deploymentConfig: DeploymentConfig;
19
+ export declare const railwayConfigs: {
20
+ test: {
21
+ build: {
22
+ builder: string;
23
+ };
24
+ deploy: {
25
+ healthcheckPath: string;
26
+ healthcheckTimeout: number;
27
+ restartPolicyType: string;
28
+ };
29
+ services: ({
30
+ name: string;
31
+ source: string;
32
+ env: {
33
+ PORT: string;
34
+ FIO_API_MODE: string;
35
+ };
36
+ buildCommand?: undefined;
37
+ startCommand?: undefined;
38
+ } | {
39
+ name: string;
40
+ source: string;
41
+ buildCommand: string;
42
+ startCommand: string;
43
+ env: Record<string, string>;
44
+ })[];
45
+ };
46
+ staging: {
47
+ build: {
48
+ builder: string;
49
+ };
50
+ deploy: {
51
+ healthcheckPath: string;
52
+ healthcheckTimeout: number;
53
+ restartPolicyType: string;
54
+ };
55
+ services: ({
56
+ name: string;
57
+ source: string;
58
+ env: {
59
+ PORT: string;
60
+ };
61
+ buildCommand?: undefined;
62
+ startCommand?: undefined;
63
+ } | {
64
+ name: string;
65
+ source: string;
66
+ buildCommand: string;
67
+ startCommand: string;
68
+ env: Record<string, string>;
69
+ })[];
70
+ };
71
+ production: {
72
+ build: {
73
+ builder: string;
74
+ };
75
+ deploy: {
76
+ healthcheckPath: string;
77
+ healthcheckTimeout: number;
78
+ restartPolicyType: string;
79
+ };
80
+ services: ({
81
+ name: string;
82
+ source: string;
83
+ env: {
84
+ PORT: string;
85
+ };
86
+ buildCommand?: undefined;
87
+ startCommand?: undefined;
88
+ } | {
89
+ name: string;
90
+ source: string;
91
+ buildCommand: string;
92
+ startCommand: string;
93
+ env: Record<string, string>;
94
+ })[];
95
+ };
96
+ };
@@ -0,0 +1,153 @@
1
+ // Deployment configuration for different modes and platforms
2
+ export const deploymentConfig = {
3
+ railway: {
4
+ testService: 'kiosk-test',
5
+ stagingService: 'kiosk-staging',
6
+ productionService: 'kiosk-production'
7
+ },
8
+ environmentVariables: {
9
+ test: {
10
+ 'NODE_ENV': 'test',
11
+ 'REACT_APP_ENVIRONMENT': 'test',
12
+ 'REACT_APP_API_URL': 'https://kiosk-test.railway.app',
13
+ 'REACT_APP_WS_URL': 'wss://kiosk-test.railway.app',
14
+ 'REACT_APP_PAYMENT_MODE': 'mock',
15
+ 'REACT_APP_ENABLE_MOCK_PAYMENTS': 'true',
16
+ 'REACT_APP_SHOW_DEBUG_INFO': 'true',
17
+ 'REACT_APP_LOG_LEVEL': 'debug'
18
+ },
19
+ staging: {
20
+ 'NODE_ENV': 'production',
21
+ 'REACT_APP_ENVIRONMENT': 'staging',
22
+ 'REACT_APP_API_URL': 'https://kiosk-staging.railway.app',
23
+ 'REACT_APP_WS_URL': 'wss://kiosk-staging.railway.app',
24
+ 'REACT_APP_PAYMENT_MODE': 'sandbox',
25
+ 'REACT_APP_ENABLE_MOCK_PAYMENTS': 'false',
26
+ 'REACT_APP_SHOW_DEBUG_INFO': 'false',
27
+ 'REACT_APP_LOG_LEVEL': 'info',
28
+ 'REACT_APP_ENABLE_ERROR_REPORTING': 'true'
29
+ },
30
+ production: {
31
+ 'NODE_ENV': 'production',
32
+ 'REACT_APP_ENVIRONMENT': 'production',
33
+ 'REACT_APP_API_URL': 'https://kiosk-prod.railway.app',
34
+ 'REACT_APP_WS_URL': 'wss://kiosk-prod.railway.app',
35
+ 'REACT_APP_PAYMENT_MODE': 'production',
36
+ 'REACT_APP_ENABLE_MOCK_PAYMENTS': 'false',
37
+ 'REACT_APP_SHOW_DEBUG_INFO': 'false',
38
+ 'REACT_APP_LOG_LEVEL': 'warn',
39
+ 'REACT_APP_ENABLE_ERROR_REPORTING': 'true'
40
+ }
41
+ },
42
+ database: {
43
+ test: 'postgresql://test_user:test_pass@localhost:5432/kiosk_test',
44
+ staging: '${DATABASE_URL}', // Railway provides this
45
+ production: '${DATABASE_URL}' // Railway provides this
46
+ }
47
+ };
48
+ // Railway deployment configuration files
49
+ export const railwayConfigs = {
50
+ test: {
51
+ build: {
52
+ builder: 'NIXPACKS'
53
+ },
54
+ deploy: {
55
+ healthcheckPath: '/health',
56
+ healthcheckTimeout: 300,
57
+ restartPolicyType: 'ON_FAILURE'
58
+ },
59
+ services: [
60
+ {
61
+ name: 'kiosk-backend-test',
62
+ source: './packages/backend',
63
+ env: {
64
+ ...deploymentConfig.environmentVariables.test,
65
+ PORT: '3000',
66
+ FIO_API_MODE: 'test'
67
+ }
68
+ },
69
+ {
70
+ name: 'kiosk-frontend-test',
71
+ source: './packages/kiosk-app',
72
+ buildCommand: 'npm run build',
73
+ startCommand: 'npx serve -s dist -l 5173',
74
+ env: deploymentConfig.environmentVariables.test
75
+ },
76
+ {
77
+ name: 'admin-frontend-test',
78
+ source: './packages/admin-app',
79
+ buildCommand: 'npm run build',
80
+ startCommand: 'npx serve -s dist -l 5174',
81
+ env: deploymentConfig.environmentVariables.test
82
+ }
83
+ ]
84
+ },
85
+ staging: {
86
+ build: {
87
+ builder: 'NIXPACKS'
88
+ },
89
+ deploy: {
90
+ healthcheckPath: '/health',
91
+ healthcheckTimeout: 300,
92
+ restartPolicyType: 'ON_FAILURE'
93
+ },
94
+ services: [
95
+ {
96
+ name: 'kiosk-backend-staging',
97
+ source: './packages/backend',
98
+ env: {
99
+ ...deploymentConfig.environmentVariables.staging,
100
+ PORT: '3000'
101
+ }
102
+ },
103
+ {
104
+ name: 'kiosk-frontend-staging',
105
+ source: './packages/kiosk-app',
106
+ buildCommand: 'npm run build',
107
+ startCommand: 'npx serve -s dist -l 5173',
108
+ env: deploymentConfig.environmentVariables.staging
109
+ },
110
+ {
111
+ name: 'admin-frontend-staging',
112
+ source: './packages/admin-app',
113
+ buildCommand: 'npm run build',
114
+ startCommand: 'npx serve -s dist -l 5174',
115
+ env: deploymentConfig.environmentVariables.staging
116
+ }
117
+ ]
118
+ },
119
+ production: {
120
+ build: {
121
+ builder: 'NIXPACKS'
122
+ },
123
+ deploy: {
124
+ healthcheckPath: '/health',
125
+ healthcheckTimeout: 300,
126
+ restartPolicyType: 'ON_FAILURE'
127
+ },
128
+ services: [
129
+ {
130
+ name: 'kiosk-backend-prod',
131
+ source: './packages/backend',
132
+ env: {
133
+ ...deploymentConfig.environmentVariables.production,
134
+ PORT: '3000'
135
+ }
136
+ },
137
+ {
138
+ name: 'kiosk-frontend-prod',
139
+ source: './packages/kiosk-app',
140
+ buildCommand: 'npm run build',
141
+ startCommand: 'npx serve -s dist -l 5173',
142
+ env: deploymentConfig.environmentVariables.production
143
+ },
144
+ {
145
+ name: 'admin-frontend-prod',
146
+ source: './packages/admin-app',
147
+ buildCommand: 'npm run build',
148
+ startCommand: 'npx serve -s dist -l 5174',
149
+ env: deploymentConfig.environmentVariables.production
150
+ }
151
+ ]
152
+ }
153
+ };
@@ -0,0 +1,13 @@
1
+ export type Environment = 'development' | 'production';
2
+ export interface EnvironmentConfig {
3
+ apiUrl: string;
4
+ wsUrl: string;
5
+ enableMockPayments: boolean;
6
+ paymentAccountNumber: string;
7
+ showDebugInfo: boolean;
8
+ }
9
+ export declare const environments: Record<Environment, EnvironmentConfig>;
10
+ export declare const getCurrentEnvironment: () => Environment;
11
+ export declare const getEnvironmentConfig: () => EnvironmentConfig;
12
+ export declare const isDevelopment: () => boolean;
13
+ export declare const isProduction: () => boolean;
@@ -0,0 +1,29 @@
1
+ // Simple environment configuration
2
+ export const environments = {
3
+ development: {
4
+ apiUrl: 'http://localhost:3015',
5
+ wsUrl: 'ws://localhost:3015',
6
+ enableMockPayments: true,
7
+ paymentAccountNumber: '1234567890',
8
+ showDebugInfo: true,
9
+ },
10
+ production: {
11
+ apiUrl: process.env.REACT_APP_API_URL || 'https://kiosk-prod.railway.app',
12
+ wsUrl: process.env.REACT_APP_WS_URL || 'wss://kiosk-prod.railway.app',
13
+ enableMockPayments: false,
14
+ paymentAccountNumber: process.env.REACT_APP_PAYMENT_ACCOUNT || '1234567890',
15
+ showDebugInfo: false,
16
+ }
17
+ };
18
+ // Simple environment detection
19
+ export const getCurrentEnvironment = () => {
20
+ return process.env.NODE_ENV === 'production' ? 'production' : 'development';
21
+ };
22
+ // Get current environment configuration
23
+ export const getEnvironmentConfig = () => {
24
+ const env = getCurrentEnvironment();
25
+ return environments[env];
26
+ };
27
+ // Simple environment checks
28
+ export const isDevelopment = () => getCurrentEnvironment() === 'development';
29
+ export const isProduction = () => getCurrentEnvironment() === 'production';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,49 @@
1
+ // Tests for environment configuration
2
+ import { getCurrentEnvironment, getEnvironmentConfig, isDevelopment, isProduction } from './environments';
3
+ describe('Environment Configuration', () => {
4
+ const originalEnv = process.env.NODE_ENV;
5
+ afterEach(() => {
6
+ process.env.NODE_ENV = originalEnv;
7
+ });
8
+ describe('getCurrentEnvironment', () => {
9
+ test('returns development for non-production environments', () => {
10
+ process.env.NODE_ENV = 'development';
11
+ expect(getCurrentEnvironment()).toBe('development');
12
+ process.env.NODE_ENV = 'test';
13
+ expect(getCurrentEnvironment()).toBe('development');
14
+ delete process.env.NODE_ENV;
15
+ expect(getCurrentEnvironment()).toBe('development');
16
+ });
17
+ test('returns production for production environment', () => {
18
+ process.env.NODE_ENV = 'production';
19
+ expect(getCurrentEnvironment()).toBe('production');
20
+ });
21
+ });
22
+ describe('getEnvironmentConfig', () => {
23
+ test('returns development config by default', () => {
24
+ process.env.NODE_ENV = 'development';
25
+ const config = getEnvironmentConfig();
26
+ expect(config.apiUrl).toBe('http://localhost:3015');
27
+ expect(config.enableMockPayments).toBe(true);
28
+ expect(config.showDebugInfo).toBe(true);
29
+ });
30
+ test('returns production config for production', () => {
31
+ process.env.NODE_ENV = 'production';
32
+ const config = getEnvironmentConfig();
33
+ expect(config.enableMockPayments).toBe(false);
34
+ expect(config.showDebugInfo).toBe(false);
35
+ });
36
+ });
37
+ describe('environment checks', () => {
38
+ test('isDevelopment works correctly', () => {
39
+ process.env.NODE_ENV = 'development';
40
+ expect(isDevelopment()).toBe(true);
41
+ expect(isProduction()).toBe(false);
42
+ });
43
+ test('isProduction works correctly', () => {
44
+ process.env.NODE_ENV = 'production';
45
+ expect(isDevelopment()).toBe(false);
46
+ expect(isProduction()).toBe(true);
47
+ });
48
+ });
49
+ });
@@ -0,0 +1,40 @@
1
+ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
2
+ export interface LogEntry {
3
+ level: LogLevel;
4
+ message: string;
5
+ context?: string;
6
+ data?: any;
7
+ timestamp: string;
8
+ environment: string;
9
+ sessionId: string;
10
+ }
11
+ declare class Logger {
12
+ private sessionId;
13
+ private config;
14
+ constructor();
15
+ private shouldLog;
16
+ private formatMessage;
17
+ private log;
18
+ private sendToExternalService;
19
+ private storeLogEntry;
20
+ debug(message: string, context?: string, data?: any): void;
21
+ info(message: string, context?: string, data?: any): void;
22
+ warn(message: string, context?: string, data?: any): void;
23
+ error(message: string, context?: string, data?: any): void;
24
+ kioskAction(action: string, kioskId: number, data?: any): void;
25
+ paymentEvent(event: string, paymentId: string, data?: any): void;
26
+ apiCall(method: string, endpoint: string, duration?: number, error?: Error): void;
27
+ getLogs(): LogEntry[];
28
+ clearLogs(): void;
29
+ }
30
+ export declare const logger: Logger;
31
+ export declare const log: {
32
+ debug: (message: string, context?: string, data?: any) => void;
33
+ info: (message: string, context?: string, data?: any) => void;
34
+ warn: (message: string, context?: string, data?: any) => void;
35
+ error: (message: string, context?: string, data?: any) => void;
36
+ kioskAction: (action: string, kioskId: number, data?: any) => void;
37
+ paymentEvent: (event: string, paymentId: string, data?: any) => void;
38
+ apiCall: (method: string, endpoint: string, duration?: number, error?: Error) => void;
39
+ };
40
+ export {};