diff --git a/static/icons/favicon.svg b/static/icons/favicon.svg new file mode 100644 index 0000000..8cd2874 --- /dev/null +++ b/static/icons/favicon.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/static/scripts/main.js b/static/scripts/main.js new file mode 100644 index 0000000..f9a1068 --- /dev/null +++ b/static/scripts/main.js @@ -0,0 +1,4 @@ +document.addEventListener('DOMContentLoaded', () => { + window.application = new Application(); + application.open(Application.DEFAULT_PAGE); +}); diff --git a/static/scripts/vendor.js b/static/scripts/vendor.js new file mode 100644 index 0000000..87d0da0 --- /dev/null +++ b/static/scripts/vendor.js @@ -0,0 +1,42 @@ +class Menu { + #application; + #root; + #items; + + constructor(application) { + this.#application = application; + this.#root = document.querySelector('#menu'); + this.#items = this.#root.querySelectorAll('input[data-page]'); + this.#items.forEach(item => { + item.addEventListener('click', () => this.#application.open(item.dataset.page)); + }); + }; + + setActive(pageId) { + this.#items.forEach(item => item.classList.remove('active')); + this.#items.forEach(item => item.dataset.page === pageId ? item.classList.add('active') : null); + }; +} + +class Application { + static DEFAULT_PAGE = 'dashboard'; + + #inputContainer; + #outputContainer; + #menu; + #pageId; + + constructor() { + this.#inputContainer = document.querySelector('#input'); + this.#outputContainer = document.querySelector('#output'); + this.#menu = new Menu(this); + }; + + open(pageId) { + this.#pageId = pageId; + document.querySelectorAll('#input [data-page]').forEach(page => page.classList.remove('active')); + document.querySelector(`#input [data-page="${pageId}"]`).classList.add('active'); + document.querySelectorAll('#output [data-page]').forEach(page => page.classList.remove('active')); + this.#menu.setActive(pageId); + }; +} diff --git a/static/styles/main.css b/static/styles/main.css new file mode 100644 index 0000000..348a882 --- /dev/null +++ b/static/styles/main.css @@ -0,0 +1,307 @@ +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('https://cdn.csasq.ru/common/fonts/exo-2/cyrillic-ext.woff2') format('woff2'); + unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('https://cdn.csasq.ru/common/fonts/exo-2/cyrillic.woff2') format('woff2'); + unicode-range: U+0301, U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('https://cdn.csasq.ru/common/fonts/exo-2/vietnamese.woff2') format('woff2'); + unicode-range: U+0102-0103, U+0110-0111, U+0128-0129, U+0168-0169, U+01A0-01A1, U+01AF-01B0, U+0300-0301, U+0303-0304, U+0308-0309, U+0323, U+0329, U+1EA0-1EF9, U+20AB; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('https://cdn.csasq.ru/common/fonts/exo-2/latin-ext.woff2') format('woff2'); + unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; +} + +@font-face { + font-family: 'Exo 2'; + font-style: normal; + font-weight: 100 900; + src: url('https://cdn.csasq.ru/common/fonts/exo-2/latin.woff2') format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; +} + +:root { + --color-1: #2E436E; + --color-2: #1E1F22; + --color-3: #2B2D30; + --color-4: #43454A; + --color-5: #575757; + --color-6: #CED0D6; + --color-7: #569ACC; + --color-8: #3574F0; + --color-9: #528D58; + --color-10: #C94F4F; +} + +html, +input, +button, +textarea, +select { + font-family: 'Exo 2', sans-serif; +} + +html, +body { + height: 100%; + color: var(--color-6); + background-color: var(--color-2); + color-scheme: dark light; +} + +body { + display: flex; + flex-direction: column; + margin: 0; +} + +input[type="text"], +input[type="number"] { + color: var(--color-6); +} + +a { + color: var(--color-7); + text-decoration: none; + outline: none; +} + +a:hover, +a:focus { + text-decoration: underline; +} + +.hidden { + display: none; +} + +#top-bar { + display: flex; + justify-content: space-between; + align-items: center; + padding: .5rem; + background-color: var(--color-3); + border-bottom: solid 2px var(--color-2); +} + +#logo { + display: flex; + align-items: center; +} + +#icon { + width: 1.5rem; + height: 1.5rem; +} + +#title { + margin-left: .5rem; + font-size: 1.2rem; + font-weight: bold; +} + +#actions { + display: flex; + align-items: center; +} + +#actions button { + display: flex; + justify-content: center; + align-items: center; + padding: .4rem .6rem; + color: var(--color-6); + background: none; + border: none; + border-radius: .25rem; + outline: none; + font-size: 1rem; + cursor: pointer; +} + +#actions button:hover, +#actions button:focus { + background: var(--color-4); +} + +#actions .icon { + width: 1.2rem; + height: 1.2rem; +} + +#content { + position: sticky; + display: flex; + height: 100%; + overflow-y: auto; +} + +#menu { + display: flex; + flex-direction: column; + justify-content: stretch; + min-width: 12rem; + max-width: 16rem; + background-color: var(--color-3); +} + +#menu .header { + padding: .75rem; + font-weight: bold; +} + +#menu .item { + display: flex; + align-items: stretch; +} + +#menu input[type="button"] { + padding: .5rem .75rem; + width: 100%; + color: var(--color-6); + background: none; + border: none; + outline: none; + white-space: pre-wrap; + text-align: start; + cursor: pointer; +} + +#menu input[type="button"]:hover, +#menu input[type="button"]:focus { + color: white; +} + +#menu input[type="button"].active { + color: white; +} + +#menu .marker { + width: 3px; + background: none; + border-radius: 3px; +} + +#menu input[type="button"].active + .marker { + background: var(--color-8); +} + +#main { + position: sticky; + display: flex; + width: 100%; + overflow: auto; +} + +#input { + flex: 1 1 auto; + padding: .75rem; + min-width: 24rem; + overflow: auto; +} + +#input [data-page]:not(.active), +#output [data-page]:not(.active) { + display: none; +} + +#main input { + width: 10rem; + background: none; + border: none; + outline: solid 1px var(--color-5); +} + +#main input:focus { + outline: solid 2px var(--color-8); +} + +#main input[type="button"] { + padding: .25rem 1.5rem; + width: 100%; + color: white; + border-radius: .25rem; + outline: none; + cursor: pointer; +} + +#main input[type="button"].primary { + padding: .25rem 1.5rem; + background: var(--color-8); + border: solid 1px var(--color-2); +} + +#main input[type="button"].secondary { + padding: .25rem 1.5rem; + background: none; + border: solid 1px var(--color-4); +} + +#main input[type="button"]:focus { + outline: solid 2px var(--color-8); +} + +#output { + flex: 1 1 auto; + padding: .75rem; + border-left: solid 1px var(--color-3); + overflow: auto; +} + +#bottom-bar { + display: flex; + justify-content: end; + align-items: center; + padding: .5rem; + background-color: var(--color-3); + border-top: solid .15rem var(--color-2); + font-size: .8rem; +} + +table.properties th { + font-weight: normal; + text-align: start; +} + +table.properties th[colspan="2"] { + padding: 1rem 0 .5rem 0; + font-weight: bold; +} + +table.properties td:nth-child(2) { + padding-left: 1rem; +} + +table.properties td[colspan="2"] { + padding: .5rem 0 .5rem 0; +} + +table.properties td[colspan="2"] > div { + display: flex; +} + +table.properties td[colspan="2"] > div > :not(:first-child) { + margin-left: .5rem; +} + +table.matrix input { + width: 3rem !important; +} diff --git a/templates/main.jinja2 b/templates/main.jinja2 new file mode 100644 index 0000000..11d1995 --- /dev/null +++ b/templates/main.jinja2 @@ -0,0 +1,63 @@ + + + + + + + + + {{ title }} — {{ app_name }} + + + + + + + +
+ +
+
+
Дашборд
+
Опросы
+
Статистика
+
+
+
+
+
+
+
+
+ + + diff --git a/web/main.py b/web/main.py index f6846a1..51d994d 100644 --- a/web/main.py +++ b/web/main.py @@ -1,15 +1,35 @@ +import os + from fastapi import FastAPI +from fastapi.responses import HTMLResponse +from jinja2 import Environment, FileSystemLoader import config app = FastAPI( - title=config.Main.title, + title=config.Main.app_name, ) +env = Environment( + loader=FileSystemLoader( + searchpath=os.path.join( + config.Main.cwd, + 'templates', + ), + ), + enable_async=True, +) +env.globals['app_name'] = config.Main.app_name + @app.get( path='/', ) async def _(): - return ';-)' + template = env.get_template('main.jinja2') + return HTMLResponse( + content=await template.render_async( + title='Дашборд', + ), + )