@underpostnet/underpost
Version:
Underpost Platform — end-to-end CI/CD and application-delivery toolchain CLI. Covers bare metal, Kubernetes, K3s, kubeadm, LXD, container/image orchestration, secrets, databases, cron jobs, monitoring, SSH, runners, PWA + Workbox delivery, and release orc
766 lines (718 loc) • 26.7 kB
JavaScript
import { Account } from '../core/Account.js';
import { BtnIcon } from '../core/BtnIcon.js';
import { getId, newInstance } from '../core/CommonJs.js';
import { Css, darkTheme, ThemeEvents, Themes } from '../core/Css.js';
import { EventsUI } from '../core/EventsUI.js';
import { LogIn } from '../core/LogIn.js';
import { LogOut } from '../core/LogOut.js';
import { buildBadgeToolTipMenuOption, Modal, renderMenuLabel, renderViewTitle } from '../core/Modal.js';
import { SignUp } from '../core/SignUp.js';
import { Translate } from '../core/Translate.js';
import { htmls, s } from '../core/VanillaJs.js';
import { extractUsernameFromPath, getProxyPath, getQueryParams } from '../core/Router.js';
import { AppStoreDefault } from './AppStoreDefault.js';
import Sortable from 'sortablejs';
import { RouterDefault, BannerAppTemplate } from './RouterDefault.js';
import { SettingsDefault } from './SettingsDefault.js';
import { Badge } from '../core/Badge.js';
import { Recover } from '../core/Recover.js';
import { DefaultManagement } from '../../services/default/default.management.js';
import { Page500 } from '../core/500.js';
import { Page404 } from '../core/404.js';
import { PanelForm } from '../core/PanelForm.js';
import { Chat } from '../core/Chat.js';
import { PublicProfile } from '../core/PublicProfile.js';
class AppShellDefault {
static Data = {};
static async instance(options = { htmlMainBody: () => html`` }) {
const id = getId(AppShellDefault.Data, 'menu-');
AppShellDefault.Data[id] = {};
const RouterInstance = RouterDefault.instance();
const { barConfig } = await Themes[Css.currentTheme]();
const badgeNotificationMenuStyle = { top: '-33px', left: '24px' };
const barMode = undefined; // 'top-bottom-bar';
await Modal.instance({
id: 'modal-menu',
html: html`
<div class="style-lading-render"></div>
<div class="fl menu-btn-container">
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-home main-btn-menu-active',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-home"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('home')}</span>`,
}),
// style: 'display: none',
attrs: `data-id="home"`,
tabHref: `${getProxyPath()}`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('home')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-log-in',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-sign-in-alt"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('log-in')}</span>`,
}),
attrs: `data-id="log-in"`,
tabHref: `${getProxyPath()}log-in`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('log-in')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-sign-up',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-user-plus"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('sign-up')}</span>`,
}),
attrs: `data-id="sign-up"`,
tabHref: `${getProxyPath()}sign-up`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('sign-up')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-log-out',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-sign-out-alt"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('log-out')}</span>`,
}),
attrs: `data-id="log-out"`,
style: 'display: none',
tabHref: `${getProxyPath()}log-out`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('log-out')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-account',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-user-circle"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('account')}</span>`,
}),
style: 'display: none',
attrs: `data-id="account"`,
tabHref: `${getProxyPath()}account`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('account')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-public-profile',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-user-tag"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('public-profile')}</span>`,
}),
style: 'display: none',
attrs: `data-id="public-profile"`,
tabHref: `${getProxyPath()}u`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('public-profile')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-settings',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fas fa-sliders-h"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('settings')}</span>`,
}),
attrs: `data-id="settings"`,
tabHref: `${getProxyPath()}settings`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('settings')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-recover hide',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fa-solid fa-arrow-rotate-left"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('recover')}</span>`,
}),
attrs: `data-id="recover"`,
tabHref: `${getProxyPath()}recover`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('recover')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-default-management',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fa-solid fa-rectangle-list"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('default-management')}</span>`,
}),
attrs: `data-id="default-management"`,
tabHref: `${getProxyPath()}default-management`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('default-management')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-404 hide',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fa-solid fa-triangle-exclamation"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('404')}</span>`,
}),
attrs: `data-id="404"`,
tabHref: `${getProxyPath()}404`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('404')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-500 hide',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fa-solid fa-circle-exclamation"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('500')}</span>`,
}),
attrs: `data-id="500"`,
tabHref: `${getProxyPath()}500`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('500')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-blog',
useMenuBtn: true,
label: renderMenuLabel({
icon: html`<i class="fa-solid fa-file-invoice"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('blog')}</span>`,
}),
attrs: `data-id="blog"`,
tabHref: `${getProxyPath()}blog`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('blog')),
})}
${await BtnIcon.instance({
class: 'in wfa main-btn-menu main-btn-chat',
useMenuBtn: true,
label: html`${renderMenuLabel({
icon: html`<i class="far fa-comments"></i>`,
text: html`<span class="menu-label-text">${Translate.instance('chat')}</span>`,
})}
${await Badge.instance({
id: 'main-btn-chat',
type: 'circle-red',
style: badgeNotificationMenuStyle,
classList: 'hide',
})}`,
attrs: `data-id="chat"`,
tabHref: `${getProxyPath()}chat`,
handleContainerClass: 'handle-btn-container',
tooltipHtml: await Badge.instance(buildBadgeToolTipMenuOption('chat')),
})}
</div>
`,
barConfig: newInstance(barConfig),
title: BannerAppTemplate,
// titleClass: 'hide',
titleRender: () => {
ThemeEvents['titleRender'] = () => {
const srcLogo = `${getProxyPath()}apple-touch-icon-114x114-precomposed.png`;
htmls(
'.action-btn-app-icon-render',
html`<img class="inl top-bar-app-icon ${darkTheme ? 'negative-color' : ''}" src="${srcLogo}" />`,
);
};
setTimeout(ThemeEvents['titleRender']);
return '';
},
mode: 'slide-menu',
RouterInstance,
htmlMainBody: options.htmlMainBody,
});
ThemeEvents['main-theme-handler'] = () => {
if (darkTheme) {
const backgroundImage = `${getProxyPath()}assets/background/dark.svg`;
htmls(
`.style-ssr-background-image`,
css`
.ssr-background-image {
background-image: url('${backgroundImage}');
}
`,
);
} else {
const backgroundImage = `${getProxyPath()}assets/background/white0-min.jpg`;
htmls(
`.style-ssr-background-image`,
css`
.ssr-background-image {
background-image: url('${backgroundImage}');
}
`,
);
}
htmls(
`.style-lading-render`,
html` <style>
.landing-container {
min-height: calc(100vh - 100px);
display: flex;
/* align-items: center; */
justify-content: center;
padding: 2rem;
color: ${darkTheme ? '#fff' : '#333'};
transition: all 0.3s ease;
}
.content-wrapper {
text-align: center;
max-width: 1200px;
width: 100%;
padding: 2rem;
animation: fadeIn 1s ease-out;
}
.animated-text {
margin-bottom: 3rem;
}
.greeting {
display: block;
font-size: 3.5rem;
font-weight: 700;
margin-bottom: 1rem;
background: linear-gradient(90deg, #4f46e5, #7c3aed);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
animation: slideIn 1s ease-out;
}
.subtitle {
display: block;
font-size: 1.5rem;
color: ${darkTheme ? '#a0aec0' : '#4a5568'};
margin-top: 1rem;
opacity: 0;
animation: fadeInUp 0.8s ease-out 0.3s forwards;
}
.features {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 2rem;
margin: 4rem 0;
}
.feature-card {
background: ${darkTheme ? 'rgba(255, 255, 255, 0.05)' : 'white'};
padding: 2rem;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
transition:
transform 0.3s ease,
box-shadow 0.3s ease;
opacity: 0;
animation: fadeInUp 0.6s ease-out forwards;
}
.feature-card:nth-child(1) {
animation-delay: 0.5s;
}
.feature-card:nth-child(2) {
animation-delay: 0.7s;
}
.feature-card:nth-child(3) {
animation-delay: 0.9s;
}
.feature-card:hover {
transform: translateY(-5px);
box-shadow: 0 10px 15px rgba(0, 0, 0, 0.1);
}
.feature-card .icon {
font-size: 2.5rem;
margin-bottom: 1rem;
display: inline-block;
}
.feature-card h3 {
font-size: 1.25rem;
margin-bottom: 0.75rem;
color: ${darkTheme ? '#e2e8f0' : '#2d3748'};
}
.feature-card p {
color: ${darkTheme ? '#a0aec0' : '#4a5568'};
line-height: 1.6;
}
.cta-button {
background: linear-gradient(90deg, #4f46e5, #7c3aed);
color: white;
border: none;
padding: 1rem 2.5rem;
font-size: 1.1rem;
border-radius: 50px;
cursor: pointer;
display: inline-flex;
align-items: center;
gap: 0.5rem;
transition: all 0.3s ease;
box-shadow: 0 4px 15px rgba(79, 70, 229, 0.3);
opacity: 0;
animation: fadeIn 0.8s ease-out 1.2s forwards;
}
.cta-button:hover {
transform: translateY(-2px);
box-shadow: 0 6px 20px rgba(79, 70, 229, 0.4);
background: linear-gradient(90deg, #3e38b4, #602bbc);
color: white;
}
.cta-button:active {
transform: translateY(0);
}
.button-icon {
transition: transform 0.3s ease;
}
.cta-button:hover .button-icon {
transform: translateX(4px);
}
@keyframes fadeIn {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slideIn {
from {
opacity: 0;
transform: translateX(-30px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
@media (max-width: 768px) {
.greeting {
font-size: 2.5rem;
}
.subtitle {
font-size: 1.25rem;
}
.features {
grid-template-columns: 1fr;
}
}
</style>`,
);
};
setTimeout(ThemeEvents['main-theme-handler']);
AppShellDefault.Data[id].sortable = new Sortable(s(`.menu-btn-container`), {
animation: 150,
group: `menu-sortable`,
forceFallback: true,
fallbackOnBody: true,
handle: '.handle-btn-container',
store: {
/**
* Get the order of elements. Called once during initialization.
* @param {Sortable} sortable
* @returns {Array}
*/
get: function (sortable) {
const order = localStorage.getItem(sortable.options.group.name);
return order ? order.split('|') : [];
},
/**
* Save the order of elements. Called onEnd (when the item is dropped).
* @param {Sortable} sortable
*/
set: function (sortable) {
const order = sortable.toArray();
localStorage.setItem(sortable.options.group.name, order.join('|'));
},
},
// chosenClass: 'css-class',
// ghostClass: 'css-class',
// Element dragging ended
onEnd: function (/**Event*/ evt) {
// console.log('Sortable onEnd', evt);
// console.log('evt.oldIndex', evt.oldIndex);
// console.log('evt.newIndex', evt.newIndex);
const slotId = Array.from(evt.item.classList).pop();
// console.log('slotId', slotId);
if (evt.oldIndex === evt.newIndex) s(`.${slotId}`).click();
// var itemEl = evt.item; // dragged HTMLElement
// evt.to; // target list
// evt.from; // previous list
// evt.oldIndex; // element's old index within old parent
// evt.newIndex; // element's new index within new parent
// evt.oldDraggableIndex; // element's old index within old parent, only counting draggable elements
// evt.newDraggableIndex; // element's new index within new parent, only counting draggable elements
// evt.clone; // the clone element
// evt.pullMode; // when item is in another sortable: `"clone"` if cloning, `true` if moving
},
});
EventsUI.onClick(`.main-btn-sign-up`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-sign-up',
route: 'sign-up',
barConfig,
title: renderViewTitle({
icon: html`<i class="fas fa-user-plus"></i>`,
text: Translate.instance('sign-up'),
}),
html: async () => await SignUp.instance({ idModal: 'modal-sign-up' }),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
});
});
EventsUI.onClick(`.main-btn-log-out`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-log-out',
route: 'log-out',
barConfig,
title: renderViewTitle({
icon: html`<i class="fas fa-sign-out-alt"></i>`,
text: Translate.instance('log-out'),
}),
html: async () => await LogOut.instance(),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
});
});
EventsUI.onClick(`.main-btn-log-in`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-log-in',
route: 'log-in',
barConfig,
title: renderViewTitle({
icon: html`<i class="fas fa-sign-in-alt"></i>`,
text: Translate.instance('log-in'),
}),
html: async () => await LogIn.instance(),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
});
});
EventsUI.onClick(`.main-btn-account`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-account',
route: 'account',
barConfig,
title: renderViewTitle({
icon: html`<i class="fas fa-user-circle"></i>`,
text: Translate.instance('account'),
}),
html: async () =>
await Account.instance({
idModal: 'modal-account',
user: AppStoreDefault.Data.user.main.model.user,
disabled: [],
}),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
});
});
EventsUI.onClick(`.main-btn-public-profile`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
const idModal = 'modal-public-profile';
const user = AppStoreDefault.Data.user.main.model.user;
// Check if modal already exists
const existingModal = s(`.${idModal}`);
if (existingModal) {
const usernameFromPath = extractUsernameFromPath();
const queryParams = getQueryParams();
const cid = usernameFromPath || queryParams.cid || user.username || null;
if (cid) {
await PublicProfile.Update({
idModal: 'modal-public-profile',
user: { username: cid },
});
return;
}
}
await Modal.instance({
id: idModal,
route: 'u',
barConfig,
title: '',
// renderViewTitle({
// icon: html`<i class="fas fa-user-circle"></i>`,
// text: Translate.instance('public-profile'),
// }),
html: async () =>
await PublicProfile.instance({
idModal,
user,
}),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
observer: true,
});
});
EventsUI.onClick(`.main-btn-settings`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-settings',
route: 'settings',
barConfig,
title: renderViewTitle({
icon: html` <i class="fas fa-sliders-h"></i>`,
text: Translate.instance('settings'),
}),
html: async () => await SettingsDefault.instance({ idModal: 'modal-settings' }),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
});
});
EventsUI.onClick(`.main-btn-recover`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-recover',
route: 'recover',
barConfig,
title: renderViewTitle({
icon: html`<i class="fa-solid fa-arrow-rotate-left"></i>`,
text: Translate.instance('recover'),
}),
html: async () =>
await Recover.instance({ idModal: 'modal-recover', user: AppStoreDefault.Data.user.main.model.user }),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
});
});
EventsUI.onClick(`.main-btn-default-management`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-default-management',
route: 'default-management',
barConfig,
title: renderViewTitle({
icon: html`<i class="fa-solid fa-rectangle-list"></i>`,
text: Translate.instance('default-management'),
}),
html: async () => await DefaultManagement.instance(),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
observer: true,
});
});
EventsUI.onClick(`.main-btn-404`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-404',
route: '404',
barConfig,
title: renderViewTitle({
icon: html`<i class="fa-solid fa-triangle-exclamation"></i>`,
text: Translate.instance('404'),
}),
html: async () => await Page404.instance({ idModal: 'modal-404' }),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
observer: true,
});
});
EventsUI.onClick(`.main-btn-500`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-500',
route: '500',
barConfig,
title: renderViewTitle({
icon: html`<i class="fa-solid fa-circle-exclamation"></i>`,
text: Translate.instance('500'),
}),
html: async () => await Page500.instance({ idModal: 'modal-500' }),
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
observer: true,
});
});
EventsUI.onClick(`.main-btn-blog`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
const idModal = 'modal-blog';
const routeModal = 'blog';
const idEvent = `form-panel-${idModal}`;
await Modal.instance({
id: idModal,
route: routeModal,
barConfig,
title: renderViewTitle({
icon: html`<i class="fa-solid fa-file-invoice"></i>`,
text: Translate.instance('blog'),
}),
observer: true,
html: async () => {
setTimeout(async () => {
await PanelForm.instance({
idPanel: 'default-blog',
defaultUrlImage: `${getProxyPath()}android-chrome-96x96.png`,
appStore: AppStoreDefault,
parentIdModal: idModal,
scrollClassContainer: `html-${idModal}`,
route: routeModal,
});
});
},
handleType: 'bar',
maximize: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
barMode,
});
});
EventsUI.onClick(`.main-btn-chat`, async () => {
const { barConfig } = await Themes[Css.currentTheme]();
await Modal.instance({
id: 'modal-chat',
route: 'chat',
barConfig,
title: renderViewTitle({
icon: html` <i class="far fa-comments"></i>`,
text: Translate.instance('chat'),
}),
html: async () => await Chat.instance({ idModal: 'modal-chat' }),
handleType: 'bar',
maximize: true,
observer: true,
mode: 'view',
slideMenu: 'modal-menu',
RouterInstance,
barMode,
});
});
}
}
export { AppShellDefault };