Remove ckeditor

Remove tinymce
Begin migrate to mithril.js
This commit is contained in:
2023-02-18 07:35:06 +03:00
parent 7f4130fe19
commit 024d7fb10d
434 changed files with 822 additions and 25765 deletions

View File

@@ -0,0 +1,44 @@
function Backtotop() {
let data = {
get visible() {
return data.raw_visible;
},
set visible(value) {
if (data.raw_visible!=value) {
m.redraw();
}
data.raw_visible = value;
},
raw_visible: false,
}
function backToTop() {
let currentScroll = document.documentElement.scrollTop || document.body.scrollTop
if (currentScroll > 0) {
window.scrollTo(0, 0)
}
};
function catchScroll() {
data.visible = (window.pageYOffset > 100);
};
return {
oninit: function(vnode) {
window.addEventListener('scroll', catchScroll);
let currentScroll = document.documentElement.scrollTop || document.body.scrollTop
data.visible = (currentScroll > 100);
},
onremove: function(vnode) {
window.removeEventListener('scroll', catchScroll)
},
view: function(vnode) {
if (data.visible) {
return m('div', {class: 'scrollToTop', style: 'z-index: 10;', onclick: backToTop},
m('div', {class: 'card'},
m('div', {class: 'card-body py-2 px-2'},
m('i', {class: 'fa fa-chevron-up'})
)
)
);
}
}
};
};

View File

@@ -0,0 +1,52 @@
function PanelFilter() {
let data = {
visible: false,
value: '',
isregex: false,
};
function filter_clear() {
data.value = '';
};
function filter_apply() {};
function form_submit(e) {
e.preventDefault();
filter_apply();
};
function button_isregex_render() {
if (data.isregex) {
return m('button', {class: 'btn btn-outline-secondary', type: 'button', onclick: function() { data.isregex = false;}}, '(.*)');
} else {
return m('button', {class: 'btn btn-outline-secondary', type: 'button', onclick: function() { data.isregex = true;}}, 'T');
}
};
return {
data: data,
oninit: function(vnode) {
console.log('PanelFilter.oninit');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
},
view: function() {
console.log('PanelFilter.view');
result = [];
if (data.visible){
result.push(
m('div', {class: "row"},
m('div', {class: "col py-2"},
m('form', {onsubmit: form_submit},
m('div', {class: "input-group mb-3"}, [
m('button', {class: 'btn btn-outline-danger', onclick: function() {filter_clear()}}, m('i', {class: 'fa fa-remove'})),
button_isregex_render(),
m('input', {class: 'form-control', oninput: function (e) {data.value = e.target.value}, value: data.value}),
m('button', {class: 'btn btn-outline-success', onclick: function() {filter_apply()}}, m('i', {class: 'fa fa-check'})),
])
)
)
)
);
};
return result;
}
}
};

View File

@@ -0,0 +1,4 @@
{% include '/components/backtotop.js' %}
{% include '/components/filter.js' %}
{% include '/components/order_by.js' %}
{% include '/components/pagination.js' %}

View File

@@ -0,0 +1,57 @@
function PanelOrderBy(arguments) {
let data = {
visible: false,
field: 'id',
order: 'desc',
fields: [{value: 'id', text: 'ID'}],
clickHandler: function() {}
}
for (let key in arguments){
data[key] = arguments[key];
}
return {
data: data,
value: function() {
return {
"field": data.field,
"order": data.order
};
},
oninit: function(vnode) {
console.log('PanelOrderBy.oninit');
for (let key in vnode.attrs){
data[key] = vnode.attrs[key];
};
},
view: function(vnode) {
console.log('PanelOrderBy.view');
if (data.visible) {
let options = data.fields.map(
function(item) {
return m('option', {value: item.value}, item.text);
}
);
let order_button = null;
if (data.order==='asc') {
order_button = m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { data.order = 'desc'; data.clickHandler() }},
m('i', {class: "fa fa-sort-alpha-asc"})
);
} else {
order_button = m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { data.order = 'asc'; data.clickHandler() }},
m('i', {class: "fa fa-sort-alpha-desc"})
);
}
return [
m('div', {class: "row"}, [
m('div', {class: "col py-2"}, [
m('div', {class: "input-group"}, [
order_button,
m('select', {class: "form-select", onchange: function(e) { data.field = e.target.value; data.clickHandler() }}, [...options])
])
])
])
];
};
}
}
};

View File

@@ -0,0 +1,165 @@
/*
pagination: Pagination({
clickHandler: resumes_get,
prefix_url: '/resumes'
}),
oninit: function(vnode) {
let pagination = data.pagination.data;
if (vnode.attrs.page!==undefined) {
pagination.page = Number(vnode.attrs.page);
}
resumes_get();
},
*/
function Pagination(arguments) {
let data = {
page: 1,
per_page: SETTINGS.ITEMS_ON_PAGE,
size: 0,
prefix_url: '',
clickHandler: function() {}
};
for (let key in arguments){
data[key] = arguments[key];
};
function pages() {
return Math.ceil(data.size/data.per_page);
};
function handlePageSelected(selected) {
data.page = selected;
m.route.set(`${data.prefix_url}/${selected}`)
data.clickHandler();
};
function has_prev() {
return data.page > 1;
};
function has_next() {
console.log('has_next');
console.log(data.page);
console.log(pages());
console.log(data.page < pages());
return data.page < pages();
};
function iter_pages() {
/* */
let last = 0;
let left_edge=2, left_current=2,
right_current=5, right_edge=2;
let result = [];
for (let num = 1; num < pages()+1; num++) {
if (num <= left_edge ||
(num > data.page - left_current - 1 &&
num < data.page + right_current) ||
num > pages() - right_edge) {
if (last + 1 != num) {
result.push(null);
} else {
result.push(num);
}
last = num
}
};
return result;
};
return {
data: data,
view: function(vnode) {
// console.log(data.page);
let result = [
m('div', {class: "row"},
m('div', {class: "col py-2 text-center"},
(function() {
// console.log(`pages: ${pages()}`)
if (pages()<=1) {
return m('button', {class: "btn btn-outline-secondary", type: "button", onclick: function() { data.clickHandler(1)}}, m('i', {class: "fa fa-refresh"}));
} else {
return [
m('div', {class: "d-none d-lg-block"},
(function() {
let result = [];
if (has_prev()) {
result.push(
m('button', {class: "btn btn-outline-secondary pull-left", type: "button", onclick: function() { handlePageSelected(data.page-1)}}, 'Предыдущая')
);
}
let buttons = iter_pages().map(
function(item) {
if (item!==null) {
return m('button', {class: "btn btn-outline-secondary me-1", type: 'button', onclick: function() { handlePageSelected(item)} }, item);
} else {
return m('button', {class: "btn btn-outline-secondary me-1", type: 'button', onclick: function() { handlePageSelected(data.page)} }, m('i', {class: "fa fa-refresh"}));
}
}
);
result.push(...buttons);
if (has_next()) {
result.push(
m('button', {type: "button", class: "btn btn-outline-secondary float-end", onclick: function() { handlePageSelected(data.page+1)}}, 'Следующая')
);
}
// console.log(`result: ${result}`)
return result;
})()
),
m('div', {class: "d-lg-none"},
m('div', {class: "btn-group w-100"},
(function() {
let result = [];
if (has_prev()) {
result.push(
m('button', {class: "btn btn-outline-secondary pull-left", type: "button", onclick: function() { handlePageSelected(data.page-1)}}, m('i', {class: "fa fa-chevron-left"}))
);
}
result.push(
m('button', {class: "btn btn-outline-secondary w-100", type: "button"}, data.page)
);
if (has_next()) {
result.push(
m('button', {class: "btn btn-outline-secondary float-end", type: "button", onclick: function() { handlePageSelected(data.page+1)}}, m('i', {class: "fa fa-chevron-right"}))
)
}
return result;
})()
)
)
]
}
})()
)
)
];
return result;
}
}
};
/*
<div class="row">
<div class="col py-2 text-center">
<button type="button" class="btn btn-outline-secondary" v-if="pages()<=1" v-on:click="refresh"><i class="fa fa-refresh"></i></button>
<template v-else>
<div class="d-none d-lg-block">
<button type="button" class="btn btn-outline-secondary pull-left" v-if="has_prev" v-on:click="handlePageSelected(pagination.page-1)">Предыдущая</button>
<template v-for="page in iter_pages">
<button type="button" class="btn btn-outline-secondary me-1" v-if="page" v-on:click="handlePageSelected(page)">{# page #}</button>
<button type="button" class="btn btn-outline-secondary me-1" v-else v-on:click="refresh"><i class="fa fa-refresh"></i></button>
</template>
<button type="button" class="btn btn-outline-secondary float-end" v-if="has_next" v-on:click="handlePageSelected(pagination.page+1)">Следующая</button>
</div>
<div class="d-lg-none">
<div class="btn-group w-100">
<button type="button" class="btn btn-outline-secondary" v-if="has_prev" v-on:click="handlePageSelected(pagination.page-1)"><i class="fa fa-chevron-left"></i></button>
<button type="button" class="btn btn-outline-secondary w-100">{# pagination.page #}</button>
<button type="button" class="btn btn-outline-secondary" v-if="has_next" v-on:click="handlePageSelected(pagination.page+1)"><i class="fa fa-chevron-right"></i></button>
</div>
</div>
</template>
</div>
</div>
*/

View File

@@ -1,7 +1,7 @@
<div class="row mt-3 py-3 bg-light">
<div class="col-md-1"></div>
<div class="col-md-10">
&copy; <a href="https://remizoffalex.ru/" target="_blank">RemiZOffAlex</a>
&copy; <a href="https://specialistoff.net/" target="_blank">RemiZOffAlex</a>
</div>
<div class="col-md-1"></div>
</div>

View File

@@ -3,8 +3,8 @@
<meta name="author" content="Ремизов Александр" />
<meta name="copyright" lang="ru" content="RemiZOffAlex" />
<link rel="shortcut icon" href="/static/favicon.ico">
<link rel="stylesheet" href="/static/css/bootstrap.min.css" />
<link rel="stylesheet" href="/static/css/font-awesome.min.css" />
<script type="text/javascript" src="/static/js/mithril.min.js"></script>
<link rel="stylesheet" href="{{ STATIC }}/css/bootstrap.min.css" />
<link rel="stylesheet" href="{{ STATIC }}/css/font-awesome.css" />
<script type="text/javascript" src="{{ STATIC }}/js/mithril.min.js"></script>
<title>{{ pagedata['title'] }}</title>
</head>

View File

@@ -1,80 +0,0 @@
{#
Подгрузить скрипт
{% import 'inc/editor.js' as editor %}
{{ editor.plugin('tinymce') }}
Получить данные
{{ editor.getValue('"text"', 'vm.note.text', type='tinymce') }}
{{ editor.getValue('"field" + field.id', 'field.cell.value', type='tinymce') }}
Получить данные
{{ editor.setValue('"text"', 'vm.note.text', type='tinymce') }}
{{ editor.setValue('"field" + field.id', 'field.cell.value', type='tinymce') }}
Инициализировать редактор
{{ editor.tinymce('"text"') }}
{{ editor.tinymce('"field" + field.id') }}
#}
{% macro plugin(type="tinymce") -%}
{% if type=="tinymce" %}
<script type="text/javascript" src="/static/tinymce/tinymce.min.js"></script>
{% elif type=="ckeditor" %}
<script type="text/javascript" src="/static/ckeditor/ckeditor.js"></script>
{% endif %}
{%- endmacro %}
{% macro ckeditor(name) -%}
CKEDITOR.replace( {{ name }}, {
customConfig: '/static/js/ckeditor-conf.js'
} );
{%- endmacro %}
{% macro tinymce(name) -%}
tinymce.init({
selector: '#' + {{ name }},
height: 400,
language: 'ru',
plugins: 'code importcss searchreplace autolink save directionality visualblocks visualchars fullscreen image link media table charmap hr pagebreak nonbreaking anchor toc advlist lists wordcount imagetools textpattern noneditable help charmap quickbars',
toolbar: 'code | undo redo | bold italic underline strikethrough removeformat | formatselect | alignleft aligncenter alignright alignjustify | outdent indent | numlist bullist | pagebreak | fullscreen | link image anchor charmap',
formats: {
img: { selector: 'img', classes: 'img-thumbnail', styles: {} },
table: { selector: 'table', classes: 'table', styles: {} },
},
style_formats: [
{ title: 'Картинка', format: 'img' },
{ title: 'Таблица', format: 'table' },
]
});
{%- endmacro %}
{% macro getValue(name, param, type="tinymce") -%}
{% if type=="tinymce" %}
let value = tinymce.get({{ name }}).getContent();
if (value != {{ param }}) {
{{ param }} = value;
}
{% elif type=="ckeditor" %}
var value = CKEDITOR.instances[{{ name }}].getData();
if (value != {{ param }}) {
{{ param }} = value;
}
{% endif %}
{%- endmacro %}
{% macro setValue(name, param, type="tinymce") -%}
{% if type=="tinymce" %}
tinymce.get({{ name }}).setContent({{ param }});
{% elif type=="ckeditor" %}
CKEDITOR.instances[{{ name }}].setData({{ param }});
{% endif %}
{%- endmacro %}

View File

@@ -1,31 +0,0 @@
<!-- Начало: Фильтр -->
<div class="row" v-if="panels.filter.visible">
<div class="col">
<div class="mb-3">
<form @submit.prevent="filter_apply">
<div class="input-group">
<button class="btn btn-outline-danger" type="button" v-on:click="filter_clear"><i class="fa fa-remove"></i></button>
<input class="form-control" placeholder="Фильтр" v-model="panels.filter.value" />
<button class="btn btn-outline-success" type="submit"><i class="fa fa-check"></i></button>
</div>
</form>
</div>
</div>
</div>
<script type="text/javascript">
Object.assign(root.data.panels, {
filter: {
visible: false,
value: ''
},
});
Object.assign(root.methods, {
filter_clear: function () {
/* Очистить фильтр */
let vm = this;
vm.panels.filter.value = '';
},
});
</script>
<!-- Конец: Фильтр -->

View File

@@ -1,24 +1,14 @@
{% raw %}
<div class="row" v-for="(page, pageIdx) in pages">
<div class="col py-2" :class="{'bg-light': pageIdx % 2}">
<a :href="'/page/' + page.id">{{ page.title }}</a>
<table>
{% for page in pagedata['pages'] %}
<tr>
<td>
<a href="/page/{{ page.id }}">{{ page.title }}</a>
<div class="row">
<div class="col small text-muted">
<span v-for="(tag, tagIdx) in page.tags">
<i class="fa fa-tag"></i> <a class="text-monospace" :href="'/tag/' + tag.id">{{ tag.name }}</a>&nbsp;
</span>
</div>
</div>
{% for tagLink in page.tags %}
<a href="/tag/{{ tagLink.tag.id }}">{{ tagLink.tag.name }}</a>
{% endfor %}
<div class="row">
<div class="col small text-muted">
<i class="fa fa-user"></i> <a :href="'/user/' + page.user.id">{{ page.user.name }}</a>&nbsp;
Создано: {{ page.created }}&nbsp;
Обновлено: {{ page.updated }}
</div>
</div>
</div>
</div>
{% endraw %}
</td>
</tr>
{% endfor %}
</table>

View File

@@ -1,12 +1,8 @@
{% extends "skeleton.html" %}
{% block content %}
<div class="card">
<div class="card-body">
<h3>{{ pagedata['info'] }}</h3>
<h1>{{ pagedata['info'] }}</h1>
<hr />
<p>Самурай без меча подобен самураю с мечом, но только без меча, однако как-будто с мечом, которого у него нет, но и без него он как с ним...</p>
</div>
</div>
{% endblock %}

10
myapp/templates/lib.js Normal file
View File

@@ -0,0 +1,10 @@
function arrayRemove(arr, value) {
/* Удаление элемента из списка */
return arr.filter(function(ele){
return ele.id != value.id;
});
};
function panel_show(panel) {
/* Показать/скрыть панель */
panel.visible = !panel.visible;
};

View File

@@ -0,0 +1,15 @@
{% include '/public/settings.js' %}
//let vr = document.body;
let vroot = document.getElementById("app");
let routes = {};
{% include '/lib.js' %}
{% include '/public/layout.js' %}
{% include '/components/inc.j2' %}
{% include '/public/components/inc.j2' %}
{% include '/public/domains/inc.j2' %}
{% include '/routes.js' %}

View File

@@ -0,0 +1,11 @@
let Footer = {
view: function() {
return {tag: '<', children: `<div class="row mt-3 py-3 bg-light">
<div class="col-md-1"></div>
<div class="col-md-10">
&copy; <a href="https://specialistoff.net/" target="_blank">RemiZOffAlex</a>
</div>
<div class="col-md-1"></div>
</div>`}
}
};

View File

@@ -0,0 +1,10 @@
{% include '/public/domains/auth/login.js' %}
{% include '/public/domains/auth/register.js' %}
Object.assign(
routes,
{
"/login": layout_decorator(Login),
"/register": layout_decorator(Register),
}
);

View File

@@ -0,0 +1,70 @@
function Login() {
let data = {
username: '',
password: ''
};
function login() {
if (data.username.length==0 || data.password.length==0) {
return;
}
m.request({
url: '/api',
method: "POST",
body: {
"jsonrpc": "2.0",
"method": 'auth.login',
"params": {
"username": data.username,
"password": data.password
},
"id": 1
}
}).then(
function(response) {
if ('result' in response) {
window.location.href = '/';
} else if ('error' in response) {
data.error = response['error'];
}
}
);
};
function form_submit(e) {
e.preventDefault();
login();
};
return {
data: data,
view: function(vnode) {
let result = [];
result.push(
m('div', {class: 'row justify-content-center my-3'},
m('div', {class: 'col-md-6'}, [
m('h3', [
{tag: '<', children: '<a class="btn btn-outline-secondary float-end" href="/forgot-password">Забыл пароль</a>'},
'Вход',
{tag: '<', children: '<hr />'},
]),
m('form', {onsubmit: form_submit}, [
m('div', {class: "input-group mb-3"}, [
{tag: '<', children: '<span class="input-group-text"><i class="fa fa-user"></i></span>'},
m('input', {class: 'form-control', placeholder: 'Логин', type: 'text', oninput: function (e) {data.username = e.target.value}, value: data.username}),
]),
m('div', {class: "input-group mb-3"}, [
{tag: '<', children: '<span class="input-group-text"><i class="fa fa-lock"></i></span>'},
m('input', {class: 'form-control', placeholder: 'Пароль', type: 'password', oninput: function (e) {data.password = e.target.value}, value: data.password}),
]),
m('div', {class: 'row'},
m('div', {class: "col py-2"}, [
m(m.route.Link, {class: 'btn btn-outline-secondary', href: '/register'}, 'Регистрация'),
m('button', {class: 'btn btn-outline-success float-end', type: 'submit'}, 'Войти')
]),
),
])
])
)
)
return result;
}
};
};

View File

@@ -0,0 +1,180 @@
function Pages() {
let data = {
filter: PanelFilter(),
order_by: PanelOrderBy({
field: 'title',
fields: [
{value: 'id', text: 'ID'},
{value: 'title', text: 'заголовку'},
{value: 'created', text: 'дате создания'},
{value: 'updated', text: 'дате обновления'}
],
clickHandler: pages_get,
order: 'asc',
}),
raw_pages: [],
get pages() {
/* Отфильтрованный список */
let value = data.filter.data.value;
if ( value.length<1 ) {
return data.raw_pages;
}
if (data.filter.data.isregex) {
try {
let regex = new RegExp(value, 'ig');
} catch (e) {
console.log(e);
return data.raw_pages;
}
}
let result = data.raw_pages.filter(page_filter);
return result;
},
pagination: Pagination({
clickHandler: pages_get,
prefix_url: '/pages'
}),
};
function page_filter(page) {
/* Фильтр статей */
let value = data.filter.data.value;
if ( value.length<1 ) {
return true;
}
let isTitle = null;
if ( data.filter.data.isregex) {
let regex = new RegExp(value, 'ig');
isTitle = regex.test(page.title.toLowerCase());
} else {
isTitle = page.title.toLowerCase().includes(value.toLowerCase());
}
if ( isTitle ) {
return true;
}
return false;
};
function breadcrumbs_render() {
let result = m('ul', {class: 'breadcrumb mt-3'}, [
m('li', {class: 'breadcrumb-item'}, m(m.route.Link, {href: '/'}, m('i', {class: 'fa fa-home'}))),
m('li', {class: 'breadcrumb-item active'}, 'Список статей'),
]);
return result;
};
function pages_get() {
let order_by = data.order_by.data;
let pagination = data.pagination.data;
m.request({
url: '/api',
method: "POST",
body: [
{
"jsonrpc": "2.0",
"method": 'pages',
"params": {
"page": pagination.page,
"order_by": {
"field": order_by.field,
'order': order_by.order
},
"fields": ["id", "title", "tags"]
},
"id": 1
},
{
"jsonrpc": "2.0",
"method": 'pages.count',
"id": 1
}
]
}).then(
function(response) {
if ('result' in response[0]) {
data.raw_pages = response[0]['result'];
}
if ('result' in response[1]) {
data.pagination.size = response[1]['result'];
}
}
);
};
function page_render(page, pageIdx) {
let odd = '';
if (pageIdx % 2) {
odd = ' bg-light'
};
return m('div', {class: 'row'},
m('div', {class: `col py-2 ${odd}`}, [
m(m.route.Link, {href: `/page/${page.id}`}, m.trust(page.title)),
m('div', {class: 'row'},
),
])
);
};
function pages_render() {
return data.pages.map(page_render);
};
return {
oninit: function(vnode) {
let pagination = data.pagination.data;
if (vnode.attrs.page!==undefined) {
pagination.page = Number(vnode.attrs.page);
};
document.title = `Список статей - ${SETTINGS.TITLE}`;
pages_get();
},
view: function(vnode) {
let result = [];
result.push(
breadcrumbs_render(),
m('div', {class: 'row'},
m('div', {class: 'col h1 py-1'}, [
m('div', {class: "btn-group btn-group-lg me-2"}, [
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.filter.data) }},
m('i', {class: "fa fa-filter"})
),
m('button', {type: "button", class: "btn btn-outline-secondary", onclick: function() { panel_show(data.order_by.data) }},
m('i', {class: "fa fa-sort-alpha-asc"})
)
]),
'Статьи',
])
),
m('hr'),
);
result.push(m(data.filter));
result.push(m(data.order_by));
result.push(m(data.pagination));
if (data.pages.length>0) {
result.push(m(ComponentPages, {pages: data.pages}));
result.push(m(data.pagination));
};
result.push(breadcrumbs_render());
return result
}
}
};
/*
<div class="row" v-for="() in pages">
<div class="col py-2" :class="{'bg-light': pageIdx % 2}">
<a :href="'/page/' + page.id">{ page.title }</a>
<div class="row">
<div class="col small text-muted">
<span v-for="(tag, tagIdx) in page.tags">
<i class="fa fa-tag"></i> <a class="text-monospace" :href="'/tag/' + tag.id">{ tag.name }</a>&nbsp;
</span>
</div>
</div>
<div class="row">
<div class="col small text-muted">
<i class="fa fa-user"></i> <a :href="'/user/' + page.user.id">{ page.user.name }</a>&nbsp;
Создано: { page.created }&nbsp;
Обновлено: { page.updated }
</div>
</div>
</div>
</div>
*/

View File

@@ -0,0 +1,20 @@
function layout_decorator(controller) {
return {
render: function(vnode) {
return m(Layout, m(controller, vnode.attrs))
}
}
};
function Layout() {
return {
view: function(vnode) {
let result = [
m(MenuGeneral),
m("section", vnode.children),
m(Footer),
m(Backtotop),
];
return result;
}
}
};

View File

@@ -0,0 +1,4 @@
const SETTINGS = {
ITEMS_ON_PAGE: {{ ITEMS_ON_PAGE }},
TITLE: 'Моя панель',
}

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="ru">
{% include 'header.html' %}
<body>
<section id="app" class="container">
{% include '/public/navbar.html' %}
{% block content %}
{% endblock content %}
{% block breadcrumb %}
{% endblock %}
{% include 'footer.html' %}
</section>
{% block script %}{% endblock %}
<script type="text/javascript" src="/app.js"></script>
</body>
</html>

14
myapp/templates/routes.js Normal file
View File

@@ -0,0 +1,14 @@
m.route.prefix = '';
function layout_decorator(controller) {
return {
render: function(vnode) {
return m(Layout, m(controller, vnode.attrs))
}
}
};
m.route(
vroot,
"/",
routes
);

View File

@@ -4,7 +4,7 @@
<body>
<section id="app" class="container">
{% include 'navbar.html' %}
{% include '/public/navbar.html' %}
{% block content %}
{% endblock content %}