Skip to content

Commit

Permalink
Merge pull request #2 from lorenzofox3/examples__spa
Browse files Browse the repository at this point in the history
add first module of spa app example
  • Loading branch information
lorenzofox3 authored Jan 29, 2024
2 parents a280bb5 + 97ab238 commit 7ad688a
Show file tree
Hide file tree
Showing 61 changed files with 2,706 additions and 7 deletions.
133 changes: 133 additions & 0 deletions apps/restaurant-cashier/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { define } from '@cofn/core';
import { UIIcon } from './components/ui-icon.component.js';
import { PageLink } from './router/page-link.component.js';
import { PageOutlet } from './router/page-outlet.component.js';
import { navigationEvents } from './router/router.js';
import { createAnimationsService } from './utils/animations.service.js';
import { createStorageService } from './utils/storage.service.js';
import {
createPreferencesService,
motionSettings,
preferencesEvents,
themeSettings,
} from './users/preferences.service.js';
import { querySelector } from './utils/dom.js';
import { compose } from './utils/functions.js';
import { mapValues } from './utils/objects.js';
import { UiLabelComponent } from './components/ui-label.component.js';
import { notificationsService } from './utils/notifications.service.js';
import { UIAlert } from './components/ui-alert.component.js';

const togglePreferences = ({ motion, theme }) => {
const classList = querySelector('body').classList;
classList.toggle('dark', theme === themeSettings.dark);
classList.toggle('motion-reduced', motion === motionSettings.reduced);
};

export const createApp = ({ router }) => {
const storageService = createStorageService();
const preferencesService = createPreferencesService({
storageService,
});
const animationService = createAnimationsService({
preferencesService,
});

preferencesService.on(
preferencesEvents.PREFERENCES_CHANGED,
compose([
togglePreferences,
mapValues(({ computed }) => computed),
preferencesService.getState,
]),
);

const root = {
animationService,
router,
storageService,
preferencesService,
notificationsService,
};
const withRoot = (comp) =>
function* (deps) {
yield* comp({
...root,
...deps,
});
};

const _define = (tag, comp, ...rest) => define(tag, withRoot(comp), ...rest);

_define('ui-icon', UIIcon, {
shadow: { mode: 'open' },
observedAttributes: ['name'],
});
_define('ui-label', UiLabelComponent, {
extends: 'label',
});
_define('ui-page-link', PageLink, {
extends: 'a',
});
_define('ui-page-outlet', PageOutlet);
_define('ui-alert', UIAlert, {
shadow: {
mode: 'open',
},
});

const usePageLoader =
({ pagePath }) =>
async (ctx, next) => {
const module = await import(pagePath);
const page = await module.loadPage({
state: ctx.state,
...root,
define: _define,
});
router.emit({
type: navigationEvents.PAGE_LOADED,
detail: { page },
});
return next();
};

router
.addRoute({ pattern: 'me' }, [
usePageLoader({ pagePath: '/users/me.page.js' }),
])
.addRoute({ pattern: 'dashboard' }, [
usePageLoader({ pagePath: '/not-available.page.js' }),
])
.addRoute({ pattern: 'products' }, [
usePageLoader({ pagePath: '/products/list/product-list.page.js' }),
])
.addRoute({ pattern: 'products/new' }, [
usePageLoader({ pagePath: '/products/new/new-product.page.js' }),
])
.addRoute({ pattern: 'products/:product-sku' }, [
usePageLoader({ pagePath: '/products/edit/edit-product.page.js' }),
])
.addRoute({ pattern: 'sales' }, [
usePageLoader({ pagePath: '/not-available.page.js' }),
])
.notFound(() => {
router.redirect('/products');
});

// trigger initial preference state
preferencesService.emit({
type: preferencesEvents.PREFERENCES_CHANGED,
});

return {
start() {
router.redirect(location.pathname + location.search + location.hash);
},
};
};

const useLogger = (ctx, next) => {
console.debug(`loading route: ${ctx.state.navigation?.URL}`);
return next();
};
Binary file added apps/restaurant-cashier/assets/burger.webp
Binary file not shown.
66 changes: 66 additions & 0 deletions apps/restaurant-cashier/components/ui-alert.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { createElement, createTextNode } from '../utils/dom.js';
import { notificationsEvents } from '../utils/notifications.service.js';

const template = createElement('template');
template.innerHTML = `<style>
:host {
display: grid;
grid-template-columns: auto 1fr;
align-items: center;
gap: var(--_offest , 1em);
font: inherit;
}
ui-icon{
width: 1rem;
height: 1rem;
}
p {
margin: 0;
}
</style>
<ui-icon name="exclamation-octagon"></ui-icon><p><slot></slot></p>
`;

const connectToNotifications = (comp) =>
function* ({ notificationsService, $signal, $host, ...rest }) {
notificationsService.on(
notificationsEvents.messagePublished,
({ detail }) => {
if (detail.level === 'error') {
$host.render({ notification: detail.payload });
}
},
{ signal: $signal },
);

yield* comp({
$host,
$signal,
...rest,
});
};
export const UIAlert = connectToNotifications(function* ({
$host,
$root,
$signal: signal,
}) {
const duration = $host.hasAttribute('duration')
? Number($host.getAttribute('duration'))
: 4_000;
const dismiss = () => $host.replaceChildren();

$root.appendChild(template.content.cloneNode(true));
$host.addEventListener('click', dismiss, { signal });
$host.setAttribute('role', 'alert');

while (true) {
const { notification } = yield;
if (!$host.hasChildNodes() && notification?.message) {
$host.replaceChildren(createTextNode(notification.message));
setTimeout(dismiss, duration, { signal });
}
}
});
30 changes: 30 additions & 0 deletions apps/restaurant-cashier/components/ui-icon.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
const template = document.createElement('template');
template.innerHTML = `
<style>
:host {
display: inline-block;
}
svg {
width: 100%;
height: 100%;
fill: currentColor;
}
</style>
<svg>
<use id="use" xlink:href=""></use>
</svg>
`;

const spriteURL = `/node_modules/bootstrap-icons/bootstrap-icons.svg`;

export const UIIcon = function* ({ $host, $root }) {
$root.replaceChildren(template.content.cloneNode(true));
$root.querySelector('svg').setAttribute('aria-hidden', 'true');
while (true) {
const iconName = $host.getAttribute('name');
$root
.getElementById('use')
.setAttribute('xlink:href', `${spriteURL}#${iconName}`);
yield;
}
};
38 changes: 38 additions & 0 deletions apps/restaurant-cashier/components/ui-label.component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { createRange, createTextNode } from '../utils/dom.js';

const errorTemplate = document.createElement('template');
errorTemplate.innerHTML = `<span class="input-error" aria-live="polite"><ui-icon name="exclamation-octagon"></ui-icon></span>`;

export const UiLabelComponent = function* ({ $host, $signal: signal }) {
const { control } = $host;
$host.append(errorTemplate.content.cloneNode(true));
const textRange = createRange();
const inputError = $host.querySelector('.input-error');
const iconEl = $host.querySelector('.input-error > ui-icon');
textRange.setStartAfter(iconEl);
textRange.setEndAfter(iconEl);
control.addEventListener(
'invalid',
(ev) => {
if (textRange.collapsed) {
textRange.insertNode(createTextNode(control.validationMessage));
inputError.classList.toggle('active');
control.addEventListener(
'input',
() => {
inputError.classList.toggle('active');
control.setCustomValidity('');
textRange.deleteContents();
},
{
once: true,
signal,
},
);
}
},
{
signal,
},
);
};
1 change: 1 addition & 0 deletions apps/restaurant-cashier/config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const APIRootURL = 'http://localhost:5173/api/';
52 changes: 52 additions & 0 deletions apps/restaurant-cashier/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Restaurant Cashier</title>
<base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1" />
<script src="https://unpkg.com/@ungap/[email protected]/es.js"></script>
<link href="theme/main.css" rel="stylesheet" type="text/css" />
</head>
<body>
<header id="main-header" class="surface boxed">
<div class="wrapper">
<div id="logo">
<svg xmlns="http://www.w3.org/2000/svg" fill="orangered" viewBox="0 0 16 16">
<path
d="m15.734 6.1-.022-.058L13.534.358a.568.568 0 0 0-.563-.356.583.583 0 0 0-.328.122.582.582 0 0 0-.193.294l-1.47 4.499H5.025l-1.47-4.5A.572.572 0 0 0 2.47.358L.289 6.04l-.022.057A4.044 4.044 0 0 0 1.61 10.77l.007.006.02.014 3.318 2.485 1.64 1.242 1 .755a.673.673 0 0 0 .814 0l1-.755 1.64-1.242 3.338-2.5.009-.007a4.046 4.046 0 0 0 1.34-4.668Z" />
</svg>
</div>
<nav id="main-nav">
<ul>
<li><a is="ui-page-link" href="/me">
<ui-icon name="person-fill"></ui-icon>
<span>me</span></a></li>
<li><a href="/dashboard" is="ui-page-link">
<ui-icon name="bar-chart-fill"></ui-icon>
<span>dashboard</span></a></li>
<li><a is="ui-page-link" href="/products">
<ui-icon name="tag-fill"></ui-icon>
<span>products</span></a></li>
<li><a href="/sales" is="ui-page-link">
<ui-icon name="cash-coin"></ui-icon>
<span>sales</span></a></li>
</ul>
<!-- <div>-->
<!-- <a href="./"><ui-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">-->
<!-- <path fill-rule="evenodd" d="M15.528 2.973a.75.75 0 0 1 .472.696v8.662a.75.75 0 0 1-.472.696l-7.25 2.9a.75.75 0 0 1-.557 0l-7.25-2.9A.75.75 0 0 1 0 12.331V3.669a.75.75 0 0 1 .471-.696L7.443.184l.01-.003.268-.108a.75.75 0 0 1 .558 0l.269.108.01.003 6.97 2.789ZM10.404 2 4.25 4.461 1.846 3.5 1 3.839v.4l6.5 2.6v7.922l.5.2.5-.2V6.84l6.5-2.6v-.4l-.846-.339L8 5.961 5.596 5l6.154-2.461z"/>-->
<!-- </svg></ui-icon><span>carts</span></a>-->
<!-- <a href="./"><ui-icon><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">-->
<!-- <path d="M8 4a.5.5 0 0 1 .5.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3A.5.5 0 0 1 8 4"/>-->
<!-- </svg></ui-icon>new cart</a>-->
<!-- </div>-->
</nav>
</div>
</header>
<main class="wrapper">
<ui-page-outlet></ui-page-outlet>
</main>
<ui-alert duration="5000"></ui-alert>
<script type="module" src="./index.js"></script>
</body>
</html>
29 changes: 29 additions & 0 deletions apps/restaurant-cashier/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import { createApp } from './app.js';
import { defaultRouter } from './router/index.js';

const app = createApp({ router: defaultRouter });

app.start();

// service worker
const registerServiceWorker = async () => {
if ('serviceWorker' in navigator) {
try {
const registration = await navigator.serviceWorker.register(
'./service-worker.js',
{ type: 'module' },
);
if (registration.installing) {
console.log('Service worker installing');
} else if (registration.waiting) {
console.log('Service worker installed');
} else if (registration.active) {
console.log('Service worker active');
}
} catch (error) {
console.error(`Registration failed with ${error}`);
}
}
};

registerServiceWorker();
7 changes: 7 additions & 0 deletions apps/restaurant-cashier/not-available.page.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { createElement } from './utils/dom.js';

export const loadPage = () => {
const p = createElement('p');
p.textContent = 'not yet implemented';
return p;
};
20 changes: 20 additions & 0 deletions apps/restaurant-cashier/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
"description": "",
"type": "module",
"scripts": {
"dev": "vite"
},
"author": "",
"license": "ISC",
"devDependencies": {
"nanoid": "^5.0.4",
"typescript": "^5.2.2",
"vite": "^4.5.0"
},
"dependencies": {
"@cofn/controllers": "workspace:*",
"@cofn/core": "workspace:*",
"@cofn/view": "workspace:*",
"bootstrap-icons": "^1.11.2"
}
}
Loading

0 comments on commit 7ad688a

Please sign in to comment.