diff --git a/package.json b/package.json index 4ad6319..2745b62 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "editor-upgrade": "yarn add -D @editorjs/{editorjs,header,code,delimiter,list,link,image,table,inline-code,marker,warning,checklist,raw}@latest" }, "dependencies": { + "@codexteam/shortcuts": "^1.2.0", "@hawk.so/javascript": "^3.0.1", "@hawk.so/nodejs": "^3.1.2", "config": "^3.3.6", diff --git a/src/backend/controllers/aliases.ts b/src/backend/controllers/aliases.ts index 4416fce..2ee8e7e 100644 --- a/src/backend/controllers/aliases.ts +++ b/src/backend/controllers/aliases.ts @@ -14,10 +14,6 @@ class Aliases { public static async get(aliasName: string): Promise { const alias = await Alias.get(aliasName); - if (!alias.id) { - throw new Error('Entity with given alias does not exist'); - } - return alias; } } diff --git a/src/backend/models/pagesFlatArray.ts b/src/backend/models/pagesFlatArray.ts index c0b5366..e82d4c3 100644 --- a/src/backend/models/pagesFlatArray.ts +++ b/src/backend/models/pagesFlatArray.ts @@ -43,7 +43,7 @@ export interface PagesFlatArrayData { } /** - * @class PagesFlatArray model - flat array of pages, which are ordered like in sidebar + * @class PagesFlatArray model - flat array of pages, which are ordered like in sidebar */ class PagesFlatArray { /** diff --git a/src/backend/routes/aliases.ts b/src/backend/routes/aliases.ts index df467e8..71e58a4 100644 --- a/src/backend/routes/aliases.ts +++ b/src/backend/routes/aliases.ts @@ -1,9 +1,11 @@ import express, { Request, Response } from 'express'; -import Aliases from '../controllers/aliases.js'; -import Pages from '../controllers/pages.js'; -import Alias from '../models/alias.js'; -import verifyToken from './middlewares/token.js'; -import PagesFlatArray from '../models/pagesFlatArray.js'; +import Aliases from '../controllers/aliases'; +import Pages from '../controllers/pages'; +import Alias from '../models/alias'; +import verifyToken from './middlewares/token'; +import PagesFlatArray from '../models/pagesFlatArray'; +import HttpException from '../exceptions/httpException'; + const router = express.Router(); @@ -24,7 +26,7 @@ router.get('*', verifyToken, async (req: Request, res: Response) => { const alias = await Aliases.get(url); if (alias.id === undefined) { - throw new Error('Alias not found'); + throw new HttpException(404, 'Alias not found'); } switch (alias.type) { @@ -46,11 +48,18 @@ router.get('*', verifyToken, async (req: Request, res: Response) => { } } } catch (err) { - res.status(400).json({ - success: false, - error: err, - }); + if (err instanceof HttpException && err.status === 404) { + res.status(404).render('error', { + message: 'Page not found', + status: 404, + }); + } else { + res.status(500).json({ + success: false, + error: err, + }); + } } }); -export default router; +export default router; \ No newline at end of file diff --git a/src/backend/views/components/sidebar.twig b/src/backend/views/components/sidebar.twig index a735b89..28d6131 100644 --- a/src/backend/views/components/sidebar.twig +++ b/src/backend/views/components/sidebar.twig @@ -52,4 +52,8 @@ + +
+ {{ svg('arrow-left') }} +
diff --git a/src/backend/views/error.twig b/src/backend/views/error.twig index caf7ce0..33a552d 100644 --- a/src/backend/views/error.twig +++ b/src/backend/views/error.twig @@ -1,7 +1,10 @@ {% extends 'layout.twig' %} {% block body %} -

{{message}}

-

{{error.status}}

-
{{error.stack}}
+
+

+ ┬┴┬┴┤ {{status}} ├┬┴┬┴ +

+

{{message}}

+
{% endblock %} diff --git a/src/frontend/js/modules/sidebar.js b/src/frontend/js/modules/sidebar.js index dcb6d60..46d9782 100644 --- a/src/frontend/js/modules/sidebar.js +++ b/src/frontend/js/modules/sidebar.js @@ -1,10 +1,11 @@ import { Storage } from '../utils/storage'; +import Shortcut from '@codexteam/shortcuts'; /** * Local storage key */ const LOCAL_STORAGE_KEY = 'docs_sidebar_state'; - +const SIDEBAR_VISIBILITY_KEY = 'docs_sidebar_visibility'; /** * Section list item height in px @@ -31,6 +32,9 @@ export default class Sidebar { sectionList: 'docs-sidebar__section-list', sectionListItemActive: 'docs-sidebar__section-list-item--active', sidebarToggler: 'docs-sidebar__toggler', + sidebarSlider: 'docs-sidebar__slider', + sidebarCollapsed: 'docs-sidebar--collapsed', + sidebarAnimated: 'docs-sidebar--animated', sidebarContent: 'docs-sidebar__content', sidebarContentHidden: 'docs-sidebar__content--hidden', sidebarContentInvisible: 'docs-sidebar__content--invisible', @@ -45,14 +49,24 @@ export default class Sidebar { * Stores refs to HTML elements needed for correct sidebar work */ this.nodes = { + sidebar: null, sections: [], sidebarContent: null, toggler: null, + slider: null, }; this.sidebarStorage = new Storage(LOCAL_STORAGE_KEY); const storedState = this.sidebarStorage.get(); this.sectionsState = storedState ? JSON.parse(storedState) : {}; + + // Initialize localStorage that contains sidebar visibility + this.sidebarVisibilityStorage = new Storage(SIDEBAR_VISIBILITY_KEY); + // Get current sidebar visibility from storage + const storedVisibility = this.sidebarVisibilityStorage.get(); + + // Sidebar visibility + this.isVisible = storedVisibility !== 'false'; } /** @@ -62,11 +76,14 @@ export default class Sidebar { * @param {HTMLElement} moduleEl - module element */ init(settings, moduleEl) { + this.nodes.sidebar = moduleEl; this.nodes.sections = Array.from(moduleEl.querySelectorAll('.' + Sidebar.CSS.section)); this.nodes.sections.forEach(section => this.initSection(section)); this.nodes.sidebarContent = moduleEl.querySelector('.' + Sidebar.CSS.sidebarContent); this.nodes.toggler = moduleEl.querySelector('.' + Sidebar.CSS.sidebarToggler); this.nodes.toggler.addEventListener('click', () => this.toggleSidebar()); + this.nodes.slider = moduleEl.querySelector('.' + Sidebar.CSS.sidebarSlider); + this.nodes.slider.addEventListener('click', () => this.handleSliderClick()); this.ready(); } @@ -104,7 +121,7 @@ export default class Sidebar { const itemsCount = sectionList.children.length; - sectionList.style.maxHeight = `${ itemsCount * ITEM_HEIGHT }px`; + sectionList.style.maxHeight = `${itemsCount * ITEM_HEIGHT}px`; } /** @@ -168,12 +185,52 @@ export default class Sidebar { this.nodes.sidebarContent.classList.toggle(Sidebar.CSS.sidebarContentHidden); } + /** + * Initializes sidebar + * + * @returns {void} + */ + initSidebar() { + if (!this.isVisible) { + this.nodes.sidebar.classList.add(Sidebar.CSS.sidebarCollapsed); + } + + /** + * prevent sidebar animation on page load + * Since animated class contains transition, hiding will be animated with it + * To prevent awkward animation when visibility is set to false, we need to remove animated class + */ + setTimeout(() => { + this.nodes.sidebar.classList.add(Sidebar.CSS.sidebarAnimated); + }, 200); + + // add event listener to execute keyboard shortcut + // eslint-disable-next-line no-new + new Shortcut({ + name: 'CMD+.', + on: document.body, + callback: () => this.handleSliderClick(), + }); + } + + /** + * Slides sidebar + * + * @returns {void} + */ + handleSliderClick() { + this.isVisible = !this.isVisible; + this.sidebarVisibilityStorage.set(this.isVisible); + this.nodes.sidebar.classList.toggle(Sidebar.CSS.sidebarCollapsed); + } + /** * Displays sidebar when ready * * @returns {void} */ ready() { + this.initSidebar(); this.nodes.sidebarContent.classList.remove(Sidebar.CSS.sidebarContentInvisible); } } diff --git a/src/frontend/styles/components/error.pcss b/src/frontend/styles/components/error.pcss new file mode 100644 index 0000000..9076122 --- /dev/null +++ b/src/frontend/styles/components/error.pcss @@ -0,0 +1,29 @@ +.error-page { + font-size: 15px; + text-align: center; + position: absolute; + top: 45%; + left: 50%; + + @media (--mobile) { + position: relative; + top: 30vh; + left: 0; + } + + @media (--tablet) { + position: relative; + top: 30vh; + left: 0; + } + + h1 { + @media (--mobile) { + font-size: 20px; + } + } + + p { + margin: 40px 0 20px; + } +} diff --git a/src/frontend/styles/components/sidebar.pcss b/src/frontend/styles/components/sidebar.pcss index 620b540..5ccbdc3 100644 --- a/src/frontend/styles/components/sidebar.pcss +++ b/src/frontend/styles/components/sidebar.pcss @@ -1,6 +1,34 @@ .docs-sidebar { width: 100vw; + &--animated { + .docs-sidebar__content { + transition: transform 200ms ease-in-out; + will-change: transform; + } + + .docs-sidebar__slider { + transition: transform 200ms ease-in-out; + will-change: transform; + } + } + + &--collapsed { + @media (--desktop) { + .docs-sidebar__content { + transform: translateX(-100%); + } + } + + .docs-sidebar__slider { + transform: translateX(20px); + + svg { + transform: rotate(180deg); + } + } + } + @media (--desktop) { width: var(--layout-sidebar-width); } @@ -196,6 +224,24 @@ } } + &__slider { + display: none; + position: fixed; + transform: translateX(calc(var(--layout-sidebar-width) + 20px)); + bottom: 20px; + width: 32px; + height: 32px; + border-radius: 8px; + cursor: pointer; + background-color: var(--color-link-hover); + + @media (--desktop) { + display: flex; + justify-content: center; + align-items: center; + } + } + &__logo { display: none; margin-top: auto; diff --git a/src/frontend/styles/main.pcss b/src/frontend/styles/main.pcss index 13fafb3..8805388 100644 --- a/src/frontend/styles/main.pcss +++ b/src/frontend/styles/main.pcss @@ -8,6 +8,7 @@ @import './components/page.pcss'; @import './components/landing.pcss'; @import './components/auth.pcss'; +@import './components/error.pcss'; @import './components/button.pcss'; @import './components/sidebar.pcss'; @import './components/navigator.pcss'; diff --git a/src/frontend/svg/arrow-left.svg b/src/frontend/svg/arrow-left.svg new file mode 100644 index 0000000..281b737 --- /dev/null +++ b/src/frontend/svg/arrow-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/yarn.lock b/yarn.lock index 0fb71ac..c5bf92d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -817,6 +817,10 @@ version "1.0.0" resolved "https://registry.yarnpkg.com/@codexteam/misprints/-/misprints-1.0.0.tgz#e5a7dec7389fe0f176cd51a040d6dc9bdc252086" +"@codexteam/shortcuts@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@codexteam/shortcuts/-/shortcuts-1.2.0.tgz#b8dd7396962b0bd845a5c8f8f19bc6119b520e19" + "@cspotcode/source-map-support@^0.8.0": version "0.8.1" resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1"