Разработан каркас клиентской части веб-приложения
Этот коммит содержится в:
родитель
96d48f9dc2
Коммит
39c6b5cca0
8
static/icons/favicon.svg
Обычный файл
8
static/icons/favicon.svg
Обычный файл
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#19a1e6">
|
||||||
|
<path d="M11.82,20.97c-4.77,0-8.64-3.87-8.64-8.64S7.05,3.69,11.82,3.69s8.64,3.87,8.64,8.64-3.87,8.64-8.64,8.64ZM11.19,5.52c-3.96,0-7.2,3.24-7.2,7.2s3.24,7.2,7.2,7.2,7.2-3.24,7.2-7.2-3.24-7.2-7.2-7.2Z" />
|
||||||
|
<circle cx="17.25" cy="6" r="6" />
|
||||||
|
<circle cx="4.95" cy="8.19" r="4.2" />
|
||||||
|
<circle cx="5.55" cy="17.7" r="3.3" />
|
||||||
|
<circle cx="16.05" cy="18.9" r="5.1" />
|
||||||
|
</svg>
|
После Ширина: | Высота: | Размер: 499 B |
4
static/scripts/main.js
Обычный файл
4
static/scripts/main.js
Обычный файл
@ -0,0 +1,4 @@
|
|||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
window.application = new Application();
|
||||||
|
application.open(Application.DEFAULT_PAGE);
|
||||||
|
});
|
42
static/scripts/vendor.js
Обычный файл
42
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);
|
||||||
|
};
|
||||||
|
}
|
307
static/styles/main.css
Обычный файл
307
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;
|
||||||
|
}
|
63
templates/main.jinja2
Обычный файл
63
templates/main.jinja2
Обычный файл
@ -0,0 +1,63 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru-RU">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="color-scheme" content="dark light" />
|
||||||
|
<meta name="application-name" content="{{ app_name }}" />
|
||||||
|
<meta name="author" content="Иваницкий Глеб Олегович" />
|
||||||
|
<meta name="robots" content="none" />
|
||||||
|
<title>{{ title }} — {{ app_name }}</title>
|
||||||
|
<link rel="icon" href="/static/icons/favicon.svg" type="image/svg+xml" />
|
||||||
|
<link rel="stylesheet" href="/static/styles/main.css" />
|
||||||
|
<script src="/static/scripts/vendor.js"></script>
|
||||||
|
<script src="/static/scripts/main.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<aside id="top-bar">
|
||||||
|
<div id="logo">
|
||||||
|
<svg id="icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#19a1e6">
|
||||||
|
<path d="M11.82,20.97c-4.77,0-8.64-3.87-8.64-8.64S7.05,3.69,11.82,3.69s8.64,3.87,8.64,8.64-3.87,8.64-8.64,8.64ZM11.19,5.52c-3.96,0-7.2,3.24-7.2,7.2s3.24,7.2,7.2,7.2,7.2-3.24,7.2-7.2-3.24-7.2-7.2-7.2Z"></path>
|
||||||
|
<circle cx="17.25" cy="6" r="6"></circle>
|
||||||
|
<circle cx="4.95" cy="8.19" r="4.2"></circle>
|
||||||
|
<circle cx="5.55" cy="17.7" r="3.3"></circle>
|
||||||
|
<circle cx="16.05" cy="18.9" r="5.1"></circle>
|
||||||
|
</svg>
|
||||||
|
<span id="title">{{ app_name }}</span>
|
||||||
|
</div>
|
||||||
|
<div id="actions"></div>
|
||||||
|
</aside>
|
||||||
|
<div id="content">
|
||||||
|
<aside id="menu">
|
||||||
|
<span class="header">Меню</span>
|
||||||
|
<div class="item">
|
||||||
|
<input data-page="dashboard" type="button" value="Дашборд" />
|
||||||
|
<div class="marker"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<input data-page="polls" type="button" value="Опросы" />
|
||||||
|
<div class="marker"></div>
|
||||||
|
</div>
|
||||||
|
<div class="item">
|
||||||
|
<input data-page="statistics" type="button" value="Статистика" />
|
||||||
|
<div class="marker"></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main id="main">
|
||||||
|
<section id="input">
|
||||||
|
<div data-page="dashboard">Дашборд</div>
|
||||||
|
<div data-page="polls">Опросы</div>
|
||||||
|
<div data-page="statistics">Статистика</div>
|
||||||
|
</section>
|
||||||
|
<section id="output">
|
||||||
|
<div data-page="dashboard"></div>
|
||||||
|
<div data-page="polls"></div>
|
||||||
|
<div data-page="statistics"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
<aside id="bottom-bar">
|
||||||
|
<span>CC BY-NC 4.0 • </span>
|
||||||
|
<a href="https://git.csasq.ru/csasq/cit-is-bot" target="_blank">Репозиторий проекта</a>
|
||||||
|
</aside>
|
||||||
|
</body>
|
||||||
|
</html>
|
24
web/main.py
24
web/main.py
@ -1,15 +1,35 @@
|
|||||||
|
import os
|
||||||
|
|
||||||
from fastapi import FastAPI
|
from fastapi import FastAPI
|
||||||
|
from fastapi.responses import HTMLResponse
|
||||||
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
import config
|
import config
|
||||||
|
|
||||||
|
|
||||||
app = FastAPI(
|
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(
|
@app.get(
|
||||||
path='/',
|
path='/',
|
||||||
)
|
)
|
||||||
async def _():
|
async def _():
|
||||||
return ';-)'
|
template = env.get_template('main.jinja2')
|
||||||
|
return HTMLResponse(
|
||||||
|
content=await template.render_async(
|
||||||
|
title='Дашборд',
|
||||||
|
),
|
||||||
|
)
|
||||||
|
Загрузка…
Ссылка в новой задаче
Block a user