Introduce initial version of the Lonc app with core features, styling, and configurations.
- Add base app structure, including Bootstrap setup and Alpine.js integration. - Implement authentication flow with session handling. - Integrate stock management and label creation functionalities. - Include responsive styling and theme using CSS variables and custom components. - Add API clients for Tryton-based backend. - Set up kitchen and dashboard navigation workflows. - Configure service worker for PWA support.
This commit is contained in:
Vendored
+91
@@ -0,0 +1,91 @@
|
||||
import Alpine from 'alpinejs';
|
||||
|
||||
import { logout, restoreSession, verifyConnection } from '../api/auth.js';
|
||||
import { listKitchens } from '../api/kitchens.js';
|
||||
import { APP_NAME } from './config.js';
|
||||
import { createRouter, navigate } from './router.js';
|
||||
import { createAppStore } from './store.js';
|
||||
import { appShell } from '../components/app-shell.js';
|
||||
import { registerFeatureData } from '../features/register.js';
|
||||
|
||||
async function installServiceWorker() {
|
||||
if ('serviceWorker' in navigator) {
|
||||
await navigator.serviceWorker.register('/service-worker.js');
|
||||
}
|
||||
}
|
||||
|
||||
export function bootstrapApp() {
|
||||
const store = createAppStore();
|
||||
Alpine.store('app', store);
|
||||
|
||||
registerFeatureData(Alpine, store);
|
||||
|
||||
const appRoot = document.querySelector('#app');
|
||||
appRoot.innerHTML = appShell(APP_NAME);
|
||||
Alpine.initTree(appRoot);
|
||||
|
||||
const router = createRouter({
|
||||
Alpine,
|
||||
store,
|
||||
outlet: document.querySelector('#route-view'),
|
||||
});
|
||||
|
||||
window.__loncApp = {
|
||||
navigate,
|
||||
async refreshKitchens() {
|
||||
const kitchens = await listKitchens(store);
|
||||
store.setKitchens(kitchens);
|
||||
if (!store.activeKitchen && kitchens.length) {
|
||||
store.setActiveKitchen(kitchens[0]);
|
||||
}
|
||||
return kitchens;
|
||||
},
|
||||
async restoreSession() {
|
||||
try {
|
||||
await restoreSession(store);
|
||||
if (store.isConnected) {
|
||||
await window.__loncApp.refreshKitchens();
|
||||
}
|
||||
} catch (error) {
|
||||
if (window.location.hash !== '#/login') {
|
||||
navigate('/login');
|
||||
}
|
||||
}
|
||||
},
|
||||
async verifyConnection() {
|
||||
await verifyConnection(store);
|
||||
if (store.isConnected) {
|
||||
await window.__loncApp.refreshKitchens();
|
||||
}
|
||||
return store.session;
|
||||
},
|
||||
async logout() {
|
||||
await logout(store);
|
||||
navigate('/login');
|
||||
},
|
||||
router,
|
||||
};
|
||||
|
||||
window.addEventListener('online', () => {
|
||||
store.addAlert({ type: 'success', message: 'Connection restored.' });
|
||||
});
|
||||
|
||||
window.addEventListener('offline', () => {
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message: 'You are offline. Cached screens stay available, but API actions may fail.',
|
||||
});
|
||||
});
|
||||
|
||||
window.__loncApp
|
||||
.restoreSession()
|
||||
.finally(() => router.start())
|
||||
.catch(() => router.start());
|
||||
|
||||
installServiceWorker().catch(() => {
|
||||
store.addAlert({
|
||||
type: 'warning',
|
||||
message: 'PWA installation support could not be initialized.',
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
export const APP_NAME = 'Lonc';
|
||||
export const TRYTON_APPLICATION = 'kitchen';
|
||||
|
||||
export const CONNECTION_STATES = {
|
||||
notConnected: 'not_connected',
|
||||
pendingValidation: 'pending_validation',
|
||||
connected: 'connected',
|
||||
invalidKey: 'invalid_key',
|
||||
};
|
||||
|
||||
export const STORAGE_KEYS = {
|
||||
appConfig: 'lonc.app.config',
|
||||
session: 'lonc.auth.session',
|
||||
activeKitchen: 'lonc.kitchen.active',
|
||||
labelDraft: 'lonc.labels.draft',
|
||||
};
|
||||
|
||||
export const DEFAULT_CONFIG = {
|
||||
baseUrl: '',
|
||||
database: '',
|
||||
};
|
||||
|
||||
export const API_PATHS = {
|
||||
userApplication: 'user/application/',
|
||||
kitchens: 'kitchen/kitchens',
|
||||
items: 'kitchen/items',
|
||||
stockEntries: 'stock',
|
||||
locations: 'kitchen/locations',
|
||||
};
|
||||
|
||||
export const ROUTES = {
|
||||
login: '/login',
|
||||
home: '/',
|
||||
stock: '/stock',
|
||||
stockNew: '/stock/new',
|
||||
stockDetail: '/stock/:id',
|
||||
labelsNew: '/labels/new',
|
||||
settings: '/settings',
|
||||
};
|
||||
@@ -0,0 +1,95 @@
|
||||
import { ROUTES } from './config.js';
|
||||
import { renderDashboardPage } from '../features/dashboard/dashboard-page.js';
|
||||
import { renderKitchenSelector } from '../features/kitchens/kitchen-selector.js';
|
||||
import { renderLoginPage } from '../features/auth/login-page.js';
|
||||
import { renderLabelCreatePage } from '../features/labels/label-create-page.js';
|
||||
import { renderSettingsPage } from '../features/auth/settings-page.js';
|
||||
import { renderStockDetailPage } from '../features/stock/stock-detail-page.js';
|
||||
import { renderStockListPage } from '../features/stock/stock-list-page.js';
|
||||
|
||||
const routeDefinitions = [
|
||||
{ path: ROUTES.login, render: renderLoginPage, protected: false },
|
||||
{ path: ROUTES.home, render: renderDashboardPage, protected: true },
|
||||
{ path: ROUTES.stock, render: renderStockListPage, protected: true },
|
||||
{ path: ROUTES.stockNew, render: renderLabelCreatePage, protected: true },
|
||||
{ path: ROUTES.stockDetail, render: renderStockDetailPage, protected: true },
|
||||
{ path: ROUTES.labelsNew, render: renderLabelCreatePage, protected: true },
|
||||
{ path: ROUTES.settings, render: renderSettingsPage, protected: false },
|
||||
];
|
||||
|
||||
function normalizeHashRoute() {
|
||||
const route = window.location.hash.replace(/^#/, '') || ROUTES.home;
|
||||
return route.startsWith('/') ? route : `/${route}`;
|
||||
}
|
||||
|
||||
function matchRoute(pathname) {
|
||||
for (const definition of routeDefinitions) {
|
||||
const keys = [];
|
||||
const pattern = definition.path.replace(/:([^/]+)/g, (_, key) => {
|
||||
keys.push(key);
|
||||
return '([^/]+)';
|
||||
});
|
||||
|
||||
const regex = new RegExp(`^${pattern}$`);
|
||||
const match = pathname.match(regex);
|
||||
if (!match) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const params = keys.reduce((accumulator, key, index) => {
|
||||
accumulator[key] = decodeURIComponent(match[index + 1]);
|
||||
return accumulator;
|
||||
}, {});
|
||||
|
||||
return { ...definition, params };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function navigate(path) {
|
||||
window.location.hash = path;
|
||||
}
|
||||
|
||||
export function getRouteContext() {
|
||||
return window.__loncRouteContext || { path: ROUTES.home, params: {} };
|
||||
}
|
||||
|
||||
export function createRouter({ Alpine, store, outlet }) {
|
||||
const render = async () => {
|
||||
const pathname = normalizeHashRoute();
|
||||
const match = matchRoute(pathname);
|
||||
|
||||
if (!match) {
|
||||
navigate(ROUTES.home);
|
||||
return;
|
||||
}
|
||||
|
||||
if (match.protected && !store.isConnected) {
|
||||
navigate(ROUTES.login);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
store.isConnected &&
|
||||
!store.activeKitchen &&
|
||||
pathname !== ROUTES.login &&
|
||||
pathname !== ROUTES.settings
|
||||
) {
|
||||
outlet.innerHTML = renderKitchenSelector();
|
||||
Alpine.initTree(outlet);
|
||||
return;
|
||||
}
|
||||
|
||||
window.__loncRouteContext = { path: pathname, params: match.params };
|
||||
outlet.innerHTML = match.render();
|
||||
Alpine.initTree(outlet);
|
||||
};
|
||||
|
||||
window.addEventListener('hashchange', render);
|
||||
|
||||
return {
|
||||
start: render,
|
||||
render,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
import {
|
||||
CONNECTION_STATES,
|
||||
DEFAULT_CONFIG,
|
||||
STORAGE_KEYS,
|
||||
TRYTON_APPLICATION,
|
||||
} from './config.js';
|
||||
import {
|
||||
clearStoredValue,
|
||||
loadStoredValue,
|
||||
saveStoredValue,
|
||||
} from '../features/shared/storage.js';
|
||||
|
||||
const defaultState = {
|
||||
config: { ...DEFAULT_CONFIG },
|
||||
session: null,
|
||||
kitchens: [],
|
||||
activeKitchen: null,
|
||||
alerts: [],
|
||||
};
|
||||
|
||||
export function createAppStore() {
|
||||
const state = {
|
||||
...defaultState,
|
||||
config: loadStoredValue(STORAGE_KEYS.appConfig, { ...DEFAULT_CONFIG }),
|
||||
session: loadStoredValue(STORAGE_KEYS.session, null),
|
||||
activeKitchen: loadStoredValue(STORAGE_KEYS.activeKitchen, null),
|
||||
alerts: [],
|
||||
};
|
||||
|
||||
return {
|
||||
...state,
|
||||
get isAuthenticated() {
|
||||
return Boolean(this.session?.applicationKey);
|
||||
},
|
||||
get isConnected() {
|
||||
return this.session?.state === CONNECTION_STATES.connected;
|
||||
},
|
||||
get needsValidation() {
|
||||
return this.session?.state === CONNECTION_STATES.pendingValidation;
|
||||
},
|
||||
setConfig(nextConfig) {
|
||||
this.config = { ...this.config, ...nextConfig };
|
||||
saveStoredValue(STORAGE_KEYS.appConfig, this.config);
|
||||
},
|
||||
setSession(session) {
|
||||
this.session = session
|
||||
? {
|
||||
application: TRYTON_APPLICATION,
|
||||
state: CONNECTION_STATES.notConnected,
|
||||
hasValidated: false,
|
||||
...session,
|
||||
}
|
||||
: null;
|
||||
|
||||
if (session) {
|
||||
saveStoredValue(STORAGE_KEYS.session, this.session);
|
||||
return;
|
||||
}
|
||||
|
||||
clearStoredValue(STORAGE_KEYS.session);
|
||||
},
|
||||
setKitchens(kitchens) {
|
||||
this.kitchens = kitchens;
|
||||
if (
|
||||
this.activeKitchen &&
|
||||
!kitchens.some((kitchen) => kitchen.id === this.activeKitchen.id)
|
||||
) {
|
||||
this.activeKitchen = null;
|
||||
clearStoredValue(STORAGE_KEYS.activeKitchen);
|
||||
}
|
||||
},
|
||||
setActiveKitchen(kitchen) {
|
||||
this.activeKitchen = kitchen;
|
||||
if (kitchen) {
|
||||
saveStoredValue(STORAGE_KEYS.activeKitchen, kitchen);
|
||||
return;
|
||||
}
|
||||
|
||||
clearStoredValue(STORAGE_KEYS.activeKitchen);
|
||||
},
|
||||
addAlert(alert) {
|
||||
const nextAlert = {
|
||||
id: crypto.randomUUID(),
|
||||
type: 'info',
|
||||
timeout: 5000,
|
||||
...alert,
|
||||
};
|
||||
|
||||
this.alerts = [...this.alerts, nextAlert];
|
||||
|
||||
if (nextAlert.timeout) {
|
||||
window.setTimeout(() => this.removeAlert(nextAlert.id), nextAlert.timeout);
|
||||
}
|
||||
},
|
||||
removeAlert(alertId) {
|
||||
this.alerts = this.alerts.filter((alert) => alert.id !== alertId);
|
||||
},
|
||||
clearSessionState() {
|
||||
this.setSession(null);
|
||||
this.setKitchens([]);
|
||||
this.setActiveKitchen(null);
|
||||
},
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user