Articles
This commit is contained in:
parent
7cc47a7f30
commit
e434b89e9e
@ -1,6 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% block head %}
|
||||
<script src="//cdn.quilljs.com/1.3.6/quill.min.js"></script>
|
||||
<link href="//cdn.quilljs.com/1.3.6/quill.snow.css" rel="stylesheet">
|
||||
<script type="module" src=/assets/js/admin.js></script>
|
||||
{% endblock %}
|
||||
|
@ -1,5 +1,17 @@
|
||||
{% extends "admin.html" %}
|
||||
{% block content %}
|
||||
<ow-admin>
|
||||
<ow-articles>
|
||||
{% for article in news %}
|
||||
<news-article
|
||||
article-title="{{article.title}}"
|
||||
status="{{article.status.as_str()}}"
|
||||
published-at="{{article.published_at}}"
|
||||
created-at="{{article.created_at}}"
|
||||
>
|
||||
{{article.body}}
|
||||
</news-article>
|
||||
{% endfor %}
|
||||
</ow-articles>
|
||||
</ow-admin>
|
||||
{% endblock %}
|
||||
|
@ -8,6 +8,16 @@
|
||||
<link href="/assets/css/app.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico"/>
|
||||
<script type="module" src=/assets/js/app.js></script>
|
||||
<style>
|
||||
.error {
|
||||
width: 1280px;
|
||||
color: #ba3939;
|
||||
background: #ffe0e0;
|
||||
border: 1px solid #a33a3a;
|
||||
margin: 8px auto auto;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
|
@ -1 +1,3 @@
|
||||
import "./admin/ow-admin";
|
||||
import "./admin/ow-articles";
|
||||
import "./admin/news-article";
|
||||
|
@ -1,49 +1,46 @@
|
||||
import { Component, FORM_STYLE } from "../shared";
|
||||
import "../shared/rich-text-editor";
|
||||
|
||||
customElements.define('article-form', class extends Component {
|
||||
#editor;
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
@import "//cdn.quilljs.com/1.3.6/quill.snow.css";
|
||||
:host { display: block; }
|
||||
rich-text-editor {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
${ FORM_STYLE }
|
||||
</style>
|
||||
<article>
|
||||
<form action="/admin/news/create" method="post">
|
||||
<div>
|
||||
<label>Tytuł</label>
|
||||
<input placeholder="Tytuł" name="title" />
|
||||
</div>
|
||||
<section id="body-view">
|
||||
</section>
|
||||
<rich-text-editor></rich-text-editor>
|
||||
<input type="hidden" name="body" />
|
||||
</section>
|
||||
<section>
|
||||
<label>Status</label>
|
||||
<select name="status">
|
||||
<option selected value="pending">Oczekujący</option>
|
||||
<option value="published">Opublikowany</option>
|
||||
<option value="hidden">Ukryty</option>
|
||||
</select>
|
||||
</section>
|
||||
<input type="submit" value="Zapisz" />
|
||||
</article>
|
||||
</form>
|
||||
`);
|
||||
|
||||
const bodyInput = this.shadowRoot.querySelector('[name="body"]');
|
||||
this.shadowRoot.querySelector('rich-text-editor').addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
bodyInput.value = ev.target.value;
|
||||
});
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const view = this.shadowRoot.querySelector('#body-view');
|
||||
view.innerHTML = `
|
||||
<div id="scrolling-container">
|
||||
<div id="editor"></div>
|
||||
</div>
|
||||
`;
|
||||
const options = {
|
||||
debug: 'info',
|
||||
modules: {
|
||||
toolbar: [
|
||||
[{ header: [1, 2, false] }],
|
||||
['bold', 'italic', 'underline'],
|
||||
['image', 'code-block']
|
||||
]
|
||||
},
|
||||
// scrollingContainer: view.querySelector('#scrolling-container'),
|
||||
readOnly: false,
|
||||
theme: 'snow'
|
||||
};
|
||||
this.#editor = new Quill(view.querySelector('#editor'), options);
|
||||
}
|
||||
});
|
||||
|
94
client/src/admin/news-article.js
Normal file
94
client/src/admin/news-article.js
Normal file
@ -0,0 +1,94 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('news-article', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ["article-title", "status", "body", "created-at", "published-at"]
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
.time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
h1 {
|
||||
display: flex;
|
||||
}
|
||||
h1 #status {
|
||||
font-size: 14px;
|
||||
}
|
||||
#time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.time span:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
#title {
|
||||
margin-right: 16px;
|
||||
}
|
||||
#body {
|
||||
margin-bottom: 16px;
|
||||
margin-top: 16px;
|
||||
}
|
||||
</style>
|
||||
<article>
|
||||
<h1>
|
||||
<span id="title"></span>
|
||||
<span id="status"></span>
|
||||
</h1>
|
||||
<section id="time">
|
||||
<div class="time">
|
||||
<span>Created at:</span>
|
||||
<span id="created_at"></span>
|
||||
</div>
|
||||
<div class="time">
|
||||
<span>Published at:</span>
|
||||
<span id="published_at"></span>
|
||||
</div>
|
||||
</section>
|
||||
<section id="body">
|
||||
<slot></slot>
|
||||
</section>
|
||||
</article>
|
||||
`);
|
||||
}
|
||||
|
||||
get article_title() {
|
||||
return this.getAttribute('article-title');
|
||||
}
|
||||
|
||||
set article_title(v) {
|
||||
this.setAttribute('article-title', v);
|
||||
this.shadowRoot.querySelector('#title').textContent = v;
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this.getAttribute('status');
|
||||
}
|
||||
|
||||
set status(v) {
|
||||
this.setAttribute('status', v);
|
||||
this.shadowRoot.querySelector('#status').textContent = v;
|
||||
}
|
||||
|
||||
get created_at() {
|
||||
return this.getAttribute('created-at');
|
||||
}
|
||||
|
||||
set created_at(v) {
|
||||
this.setAttribute('created-at', v);
|
||||
this.shadowRoot.querySelector('#created_at').textContent = v;
|
||||
}
|
||||
|
||||
get published_at() {
|
||||
return this.getAttribute('published-at');
|
||||
}
|
||||
|
||||
set published_at(v) {
|
||||
this.setAttribute('published-at', v);
|
||||
this.shadowRoot.querySelector('#published_at').textContent = v;
|
||||
}
|
||||
});
|
@ -7,6 +7,7 @@ customElements.define('ow-admin', class extends Component {
|
||||
<style>
|
||||
:host { display: block; }
|
||||
</style>
|
||||
<slot></slot>
|
||||
<article-form></article-form>
|
||||
`);
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ customElements.define('ow-articles', class extends Component {
|
||||
<style>
|
||||
:host { display: block; }
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);
|
||||
}
|
||||
});
|
||||
|
@ -55,7 +55,7 @@ form > div {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
input, textarea {
|
||||
input, textarea, select, option {
|
||||
font-size: 16px;
|
||||
|
||||
border: none;
|
||||
|
420
client/src/shared/rich-text-editor.js
Normal file
420
client/src/shared/rich-text-editor.js
Normal file
@ -0,0 +1,420 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('rich-text-editor', class extends Component {
|
||||
#selection;
|
||||
#range;
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
#tools > #first-row {
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
#edit {
|
||||
border: 1px solid var(--border-slim-color);
|
||||
min-height: 90px;
|
||||
padding: 8px;
|
||||
}
|
||||
button, select {
|
||||
background: none;
|
||||
border: 1px solid var(--border-slim-color);
|
||||
margin-right: 8px;
|
||||
}
|
||||
button {
|
||||
padding: 4px;
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
}
|
||||
select {
|
||||
padding: 4px;
|
||||
height: 46px;
|
||||
}
|
||||
svg {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
section > div {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
section ul {
|
||||
list-style: circle;
|
||||
}
|
||||
section ol {
|
||||
list-style: decimal;
|
||||
}
|
||||
#image input {
|
||||
display: none;
|
||||
}
|
||||
#align, #colors, #lists, #insert {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#text-color input, #background-color input {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<article>
|
||||
<section id="tools">
|
||||
<div id="first-row">
|
||||
<div>
|
||||
<select id="header">
|
||||
<option value="normal">Normal</option>
|
||||
<option value="h1">H1</option>
|
||||
<option value="h2">H2</option>
|
||||
<option value="h3">H3</option>
|
||||
<option value="h4">H4</option>
|
||||
<option value="h5">H5</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="colors">
|
||||
<button id="text-color" title="Kolor czcionki">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 278 278" xml:space="preserve">
|
||||
<path d="M254.833 0h-231C15.549 0 8.5 6.716 8.5 15v33c0 8.284 6.716 15 15 15s15-6.716 15-15V30h69v218H89.833c-8.284 0-15 6.716-15 15s6.716 15 15 15h99c8.284 0 15-6.716 15-15s-6.716-15-15-15H170.5V30h69v18c0 8.284 6.716 15 15 15s15-6.716 15-15V15c0-8.284-6.383-15-14.667-15z"/>
|
||||
</svg>
|
||||
<input type="color" value="#000" />
|
||||
</button>
|
||||
<button id="background-color" title="Kolor tła">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" xml:space="preserve">
|
||||
<path d="m25.9 16 4.3 10h-9l3.9-10h.8zM48 8v36c0 2.2-1.8 4-4 4H8c-2.2 0-4-1.8-4-4V8c0-2.2 1.8-4 4-4h36c2.2 0 4 1.8 4 4zm-5.5 32.7L30.5 11c-.3-.6-.8-1-1.5-1h-7.1c-.6 0-1.2.4-1.4 1l-11 29.7c-.2.6.2 1.3.9 1.3h4.1c.6 0 1.2-.5 1.4-1.1l3.2-8.9h13.4l3.5 8.9c.2.6.8 1.1 1.4 1.1h4.1c.7 0 1.2-.7 1-1.3z"/>
|
||||
</svg>
|
||||
<input type="color" value="#000" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="decorations">
|
||||
<button id="underscore" title="Podkreślenie">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" xml:space="preserve">
|
||||
<path d="M10.4 36h4.1c.6 0 1.2-.5 1.4-1.1l3.2-8.9h13.4l3.5 8.9c.2.6.8 1.1 1.4 1.1h4.1c.7 0 1.2-.7.9-1.3L30.4 5c-.2-.6-.7-1-1.3-1H22c-.6 0-1.2.4-1.4 1l-11 29.7c-.3.6.2 1.3.8 1.3zm14.7-26h.9l4.3 10h-9l3.8-10zM48.5 42h-45c-.8 0-1.5.7-1.5 1.5v3c0 .8.7 1.5 1.5 1.5h45c.8 0 1.5-.7 1.5-1.5v-3c0-.8-.7-1.5-1.5-1.5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="superscript" title="Indeks górny">
|
||||
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="none" d="M0 0h24v24H0z"/>
|
||||
<path d="M11 7v13H9V7H3V5h12v2h-4zm8.55-.42a.8.8 0 1 0-1.32-.36l-1.154.33A2.001 2.001 0 0 1 19 4a2 2 0 0 1 1.373 3.454L18.744 9H21v1h-4V9l2.55-2.42z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="subscription" title="Indeks dolny">
|
||||
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M16.488 4.43a.75.75 0 0 1 .082 1.058L10.988 12l5.032 5.871c-.302.41-.555.906-.682 1.51L10 13.151l-5.43 6.336a.75.75 0 0 1-1.14-.976L9.013 12 3.431 5.488a.75.75 0 0 1 1.139-.976L10 10.848l5.43-6.336a.75.75 0 0 1 1.058-.081ZM17.75 15.523c0-.528.444-1.023.986-1.023.407 0 .735.19.893.434.136.21.218.566-.093 1.095-.15.255-.376.482-.682.724-.152.12-.316.237-.493.363l-.074.052c-.152.107-.315.222-.472.34-.744.56-1.565 1.346-1.565 2.742 0 .414.336.75.75.75h3.451a.75.75 0 0 0 0-1.5h-2.513c.16-.282.423-.525.779-.793.137-.103.279-.203.432-.312l.078-.054c.178-.127.37-.264.557-.41.372-.295.76-.658 1.045-1.142.557-.948.546-1.921.058-2.672C20.42 13.4 19.59 13 18.736 13c-1.478 0-2.486 1.278-2.486 2.523a.75.75 0 0 0 1.5 0Z" fill="#212121"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div id="lists">
|
||||
<button id="ordered-list" title="Lista uporządkowana">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 317.109 317.109" xml:space="preserve">
|
||||
<path d="M102.109 53.555h200c8.284 0 15-6.716 15-15s-6.716-15-15-15h-200c-8.284 0-15 6.716-15 15s6.716 15 15 15zM302.109 83.555h-200c-8.284 0-15 6.716-15 15s6.716 15 15 15h200c8.284 0 15-6.716 15-15s-6.715-15-15-15zM302.109 143.555h-200c-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15h200c8.284 0 15-6.716 15-15 0-8.285-6.715-15-15-15zM302.109 263.555h-200c-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15h200c8.284 0 15-6.716 15-15 0-8.284-6.715-15-15-15zM302.109 203.555h-200c-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15h200c8.284 0 15-6.716 15-15 0-8.285-6.715-15-15-15zM17.826 49.036V86.6c0 4.074 3.32 7.146 7.724 7.146 4.33 0 7.721-3.139 7.721-7.146V30.425c0-3.96-3.247-7.062-7.392-7.062-3.647 0-5.471 2.447-6.07 3.251a6.54 6.54 0 0 0-.074.102l-6.526 9.233c-1.267 1.378-2.394 3.582-2.394 5.696-.001 4.03 3.133 7.317 7.011 7.391zM7.63 193.746h29.406c3.849 0 6.981-3.391 6.981-7.559 0-4.124-3.131-7.479-6.981-7.479H15.684v-.122c0-2.246 5.148-5.878 9.285-8.797 8.229-5.807 18.47-13.033 18.47-25.565 0-11.893-9.216-20.86-21.438-20.86-11.703 0-20.527 8.044-20.527 18.711 0 6.19 4.029 8.387 7.479 8.387 4.938 0 7.889-3.676 7.889-7.23 0-2.21.568-4.746 4.994-4.746 5.979 0 6.151 5.298 6.151 5.902 0 4.762-6.18 9.213-12.157 13.519C8.442 163.228.068 169.26.068 178.587v8.011c-.001 4.276 3.91 7.148 7.562 7.148zM42.446 242.783c0-12.342-7.288-19.42-19.994-19.42-16.66 0-21.062 11.898-21.062 18.189 0 7.325 5.445 8.115 7.786 8.115 4.559 0 7.621-3.063 7.621-7.622 0-1.753.624-3.766 5.487-3.766 3.495 0 4.918.503 4.918 5.568 0 4.948-1.062 5.487-5.245 5.487-4.018 0-7.047 3.17-7.047 7.375 0 4.159 3.066 7.295 7.131 7.295 5.525 0 6.635 2.256 6.635 5.897v1.558c0 6.126-2.389 7.288-6.798 7.288-6.083 0-6.556-3.133-6.556-4.093 0-3.631-2.407-7.294-7.785-7.294-4.72 0-7.538 2.942-7.538 7.869 0 8.976 7.696 18.516 21.958 18.516 13.854 0 22.126-8.331 22.126-22.286v-1.558c0-5.722-1.83-10.465-5.264-13.876 2.352-3.403 3.627-7.944 3.627-13.242z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<button id="unordered-list" title="Lista nieuporządkowana">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 487.3 487.3" xml:space="preserve">
|
||||
<path d="M487.2 69.7c0 12.9-10.5 23.4-23.4 23.4h-322c-12.9 0-23.4-10.5-23.4-23.4s10.5-23.4 23.4-23.4h322.1c12.9.1 23.3 10.5 23.3 23.4zm-23.3 92.6H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zm0 116H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zm0 116H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zM38.9 30.8C17.4 30.8 0 48.2 0 69.7s17.4 39 38.9 39 38.9-17.5 38.9-39-17.4-38.9-38.9-38.9zm0 116C17.4 146.8 0 164.2 0 185.7s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9-17.4-38.9-38.9-38.9zm0 116C17.4 262.8 0 280.2 0 301.7s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9-17.4-38.9-38.9-38.9zm0 115.9C17.4 378.7 0 396.1 0 417.6s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9c0-21.4-17.4-38.9-38.9-38.9z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="insert">
|
||||
<button id="image" title="Wstaw obrazek">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" xml:space="preserve">
|
||||
<path d="M31 56h24V32H31v24zm2-22h20v20h-9V41.414l4.293 4.293 1.414-1.414L43 37.586l-6.707 6.707 1.414 1.414L42 41.414V54h-9V34zM21.569 13.569C21.569 10.498 19.071 8 16 8s-5.569 2.498-5.569 5.569c0 3.07 2.498 5.568 5.569 5.568s5.569-2.497 5.569-5.568zm-9.138 0C12.431 11.602 14.032 10 16 10s3.569 1.602 3.569 3.569-1.601 3.569-3.569 3.569-3.569-1.601-3.569-3.569zM6.25 36.661a.997.997 0 0 0 1.41.09l16.313-14.362 7.319 7.318a.999.999 0 1 0 1.414-1.414l-1.825-1.824 9.181-10.054 11.261 10.323a1 1 0 0 0 1.351-1.475l-12-11a1.002 1.002 0 0 0-1.414.063l-9.794 10.727-4.743-4.743a1.003 1.003 0 0 0-1.368-.044L6.339 35.249a1 1 0 0 0-.089 1.412z"/><path d="M57 2H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h24a1 1 0 1 0 0-2H2V4h54v23a1 1 0 1 0 2 0V3a1 1 0 0 0-1-1z"/>
|
||||
</svg>
|
||||
<input type="file" accept="image/*" />
|
||||
</button>
|
||||
</div>
|
||||
<div id="align">
|
||||
<button id="align-left" title="Wyrównaj do lewej">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200" xml:space="preserve">
|
||||
<path d="M618.75 99.202v178.006H0V99.202h618.75zm328.125 274.53v178.006H0V373.732h946.875zM731.25 648.262v178.006H0V648.262h731.25zM1200 922.792v178.006H0V922.792h1200z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="align-center" title="Wyrównaj do środka">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.668 31.668" xml:space="preserve">
|
||||
<path d="M25.501 5H6.167V0h19.334v5zM.168 8.889v5H31.5v-5H.168zm5.999 8.888v5h19.334v-5H6.167zM.168 31.668H31.5v-5H.168v5z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="align-right" title="Wyrównaj do prawej">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200" xml:space="preserve">
|
||||
<path d="M581.25 99.202v178.006H1200V99.202H581.25zm-328.125 274.53v178.006H1200V373.732H253.125zm215.625 274.53v178.006H1200V648.262H468.75zM0 922.792v178.006h1200V922.792H0z"/>
|
||||
</svg>
|
||||
</button>
|
||||
|
||||
<button id="align-justify" title="Wyrównaj do lewej i prawej">
|
||||
<svg viewBox="-32 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M432 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="second-row">
|
||||
</div>
|
||||
</section>
|
||||
<section id="edit" contenteditable="true"></section>
|
||||
</article>
|
||||
`);
|
||||
|
||||
const header = this.shadowRoot.querySelector('#tools #header');
|
||||
header.addEventListener('click', () => {
|
||||
this.#saveSelection();
|
||||
});
|
||||
header.addEventListener('change', ev => {
|
||||
switch (ev.target.value) {
|
||||
case 'normal':
|
||||
return this.#removeWrapper();
|
||||
case 'h1':
|
||||
return this.#wrapNode('H1');
|
||||
case 'h2':
|
||||
return this.#wrapNode('H2');
|
||||
case 'h3':
|
||||
return this.#wrapNode('H3');
|
||||
case 'h4':
|
||||
return this.#wrapNode('H4');
|
||||
case 'h5':
|
||||
return this.#wrapNode('H5');
|
||||
}
|
||||
});
|
||||
this.shadowRoot.querySelector('#ordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('ol', 'li');
|
||||
});
|
||||
{
|
||||
let timeout = null;
|
||||
this.shadowRoot.querySelector('#edit').addEventListener('keyup', (ev) => {
|
||||
ev.stopPropagation();
|
||||
if (timeout) clearTimeout(timeout);
|
||||
timeout = setTimeout(() => {
|
||||
timeout = null;
|
||||
this.#emitChange();
|
||||
}, 1000 / 3);
|
||||
});
|
||||
}
|
||||
this.shadowRoot.querySelector('#unordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('ul', 'li');
|
||||
});
|
||||
const imgBtn = this.shadowRoot.querySelector('#image');
|
||||
const imgInput = imgBtn.querySelector('input');
|
||||
imgInput.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
});
|
||||
imgInput.addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
const file = imgInput.files[0];
|
||||
const reader = new FileReader();
|
||||
reader.onloadend = () => {
|
||||
const selected = this.#selected;
|
||||
if (!this.constructor.#isEditNode(selected)) return;
|
||||
let el = selected;
|
||||
if (el.nodeType === Node.TEXT_NODE) el = el.parentElement;
|
||||
if (!el) return;
|
||||
const img = new Image();
|
||||
img.src = reader.result || '';
|
||||
el.appendChild(img);
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
imgBtn.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#saveSelection();
|
||||
imgInput.click();
|
||||
});
|
||||
{
|
||||
const el = this.shadowRoot.querySelector("#align-justify");
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#saveSelection();
|
||||
this.#setStyle('textAlign', 'justify');
|
||||
});
|
||||
}
|
||||
{
|
||||
const el = this.shadowRoot.querySelector("#align-left");
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#saveSelection();
|
||||
this.#setStyle('textAlign', 'left');
|
||||
});
|
||||
}
|
||||
{
|
||||
const el = this.shadowRoot.querySelector("#align-center");
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#saveSelection();
|
||||
this.#setStyle('textAlign', 'center');
|
||||
});
|
||||
}
|
||||
{
|
||||
const el = this.shadowRoot.querySelector("#align-right");
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
this.#saveSelection();
|
||||
this.#setStyle('textAlign', 'right');
|
||||
});
|
||||
}
|
||||
{
|
||||
const button = this.shadowRoot.querySelector("#text-color");
|
||||
const input = button.querySelector('input');
|
||||
input.addEventListener('click', ev => ev.stopPropagation());
|
||||
input.addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#setStyle('color', input.value);
|
||||
});
|
||||
button.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
input.click();
|
||||
this.#saveSelection();
|
||||
});
|
||||
}
|
||||
{
|
||||
const button = this.shadowRoot.querySelector("#background-color");
|
||||
const input = button.querySelector('input');
|
||||
input.addEventListener('click', ev => ev.stopPropagation());
|
||||
input.addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#setStyle('backgroundColor', input.value);
|
||||
});
|
||||
button.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
input.click();
|
||||
this.#saveSelection();
|
||||
});
|
||||
}
|
||||
{
|
||||
const el = this.shadowRoot.querySelector('#underscore');
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setStyle("textDecoration", 'underline');
|
||||
});
|
||||
}
|
||||
{
|
||||
const el = this.shadowRoot.querySelector('#superscript');
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('sup');
|
||||
});
|
||||
}
|
||||
{
|
||||
const el = this.shadowRoot.querySelector('#subscription');
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('sub');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static #isEditNode(el) {
|
||||
while (true) {
|
||||
if (!el) return false;
|
||||
if (el.nodeType === Node.TEXT_NODE) {
|
||||
el = el.parentElement;
|
||||
continue;
|
||||
}
|
||||
if (!el.tagName) return false;
|
||||
if (el.tagName.toLocaleLowerCase() === 'body') return false;
|
||||
if (el.id === 'edit') return true;
|
||||
el = el.parentElement;
|
||||
}
|
||||
}
|
||||
|
||||
#wrapNode(...tags) {
|
||||
const selected = this.#selected;
|
||||
if (!this.constructor.#isEditNode(selected)) return;
|
||||
let el = selected;
|
||||
if (el.nodeType === Node.TEXT_NODE) el = el.parentElement;
|
||||
if (!el) return;
|
||||
|
||||
for (let i = 0; i < tags.length; i++) {
|
||||
const tag = tags[i];
|
||||
const newNode = document.createElement(tag);
|
||||
el.appendChild(newNode);
|
||||
el = newNode;
|
||||
if (i + 1 === tags.length) {
|
||||
el.appendChild(selected);
|
||||
}
|
||||
el.focus();
|
||||
}
|
||||
this.#restoreSelection();
|
||||
return el;
|
||||
}
|
||||
|
||||
#removeWrapper() {
|
||||
const el = this.#selected;
|
||||
if (!this.constructor.#isEditNode(el)) return;
|
||||
const headerNode = el.parentElement;
|
||||
if (!headerNode) return;
|
||||
if (!headerNode.tagName.toLocaleLowerCase().startsWith('h')) {
|
||||
return;
|
||||
}
|
||||
const parent = headerNode.parentElement;
|
||||
parent.replaceChild(el, headerNode);
|
||||
this.#restoreSelection();
|
||||
}
|
||||
|
||||
#setStyle(setter, value) {
|
||||
const s = this.#selected;
|
||||
if (!this.constructor.#isEditNode(s)) return;
|
||||
let el = s;
|
||||
if (el.nodeType === Node.TEXT_NODE) el = el.parentElement;
|
||||
if (el.id === 'edit') {
|
||||
const div = el.appendChild(document.createElement('div'));
|
||||
div.appendChild(s);
|
||||
el = div;
|
||||
}
|
||||
if (el.style[setter] === value) {
|
||||
el.style[setter] = null;
|
||||
} else {
|
||||
el.style[setter] = value;
|
||||
}
|
||||
this.#restoreSelection();
|
||||
}
|
||||
|
||||
#saveSelection() {
|
||||
this.#selection = window.getSelection();
|
||||
this.#range = this.#selection.getRangeAt(0);
|
||||
this.#selection.removeAllRanges();
|
||||
this.shadowRoot.querySelector('#edit').blur();
|
||||
}
|
||||
|
||||
#restoreSelection() {
|
||||
this.shadowRoot.querySelector('#edit').blur();
|
||||
this.shadowRoot.querySelector('#edit').click();
|
||||
this.shadowRoot.querySelector('#edit').focus();
|
||||
if (this.#range) {
|
||||
const s = window.getSelection();
|
||||
s.removeAllRanges();
|
||||
s.addRange(this.#range);
|
||||
}
|
||||
this.#emitChange();
|
||||
}
|
||||
|
||||
get #selected() {
|
||||
return this.#range.startContainer
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this.shadowRoot.querySelector('#edit').innerHTML;
|
||||
}
|
||||
|
||||
set value(html) {
|
||||
this.shadowRoot.querySelector('#edit').innerHTML = html;
|
||||
}
|
||||
|
||||
#emitChange() {
|
||||
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
|
||||
}
|
||||
});
|
@ -1 +1,15 @@
|
||||
-- Add migration script here
|
||||
CREATE TYPE "NewsStatus" AS ENUM (
|
||||
'Pending',
|
||||
'Published',
|
||||
'Hidden'
|
||||
);
|
||||
|
||||
CREATE TABLE news
|
||||
(
|
||||
id serial unique not null primary key,
|
||||
title text not null unique,
|
||||
body text not null,
|
||||
status "NewsStatus" not null default 'Pending',
|
||||
published_at timestamp not null default now(),
|
||||
created_at timestamp not null default now()
|
||||
);
|
||||
|
@ -49,6 +49,24 @@ impl LocalBusinessState {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Type)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum NewsStatus {
|
||||
Pending,
|
||||
Published,
|
||||
Hidden,
|
||||
}
|
||||
|
||||
impl NewsStatus {
|
||||
pub fn as_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Pending => "Pending",
|
||||
Self::Published => "Published",
|
||||
Self::Hidden => "Hidden",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Token {
|
||||
pub id: i32,
|
||||
@ -82,6 +100,23 @@ pub struct LocalBusinessItem {
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct NewsArticle {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: NewsStatus,
|
||||
pub published_at: chrono::NaiveDateTime,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct CreateNewsArticleInput {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: NewsStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateLocalBusinessItemInput {
|
||||
pub local_business_id: i32,
|
||||
|
@ -7,6 +7,8 @@ pub enum Page {
|
||||
LocalBusinesses,
|
||||
News,
|
||||
Account,
|
||||
Admin,
|
||||
AdminCreateNews,
|
||||
Register,
|
||||
Login,
|
||||
BusinessItems,
|
||||
@ -114,3 +116,10 @@ pub struct MoveBusinessItemInput {
|
||||
pub id: i32,
|
||||
pub item_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateNewsInput {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: db::NewsStatus,
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ use std::cmp::Ordering;
|
||||
use tracing::error;
|
||||
|
||||
use crate::model::db;
|
||||
use crate::model::db::NewsArticle;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
@ -33,6 +34,11 @@ pub enum Error {
|
||||
Item {
|
||||
item_id: i32,
|
||||
},
|
||||
AllNews,
|
||||
PublishedNews,
|
||||
CreateNewsArticle {
|
||||
input: db::CreateNewsArticleInput,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@ -419,3 +425,80 @@ WHERE email = $1
|
||||
Error::AccountByEmail { email }
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn all_news(t: &mut T<'_>) -> Result<Vec<NewsArticle>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
status,
|
||||
published_at,
|
||||
created_at
|
||||
FROM
|
||||
news
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::AllNews
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn published_news(t: &mut T<'_>) -> Result<Vec<NewsArticle>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
status,
|
||||
published_at,
|
||||
created_at
|
||||
FROM
|
||||
news
|
||||
WHERE
|
||||
status = 'Published'
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::PublishedNews
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn create_news_article(
|
||||
t: &mut T<'_>,
|
||||
input: db::CreateNewsArticleInput,
|
||||
) -> Result<NewsArticle> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO news (title, body, status)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
status,
|
||||
published_at,
|
||||
created_at
|
||||
"#,
|
||||
)
|
||||
.bind(&input.title)
|
||||
.bind(&input.body)
|
||||
.bind(input.status)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::CreateNewsArticle { input }
|
||||
})
|
||||
}
|
||||
|
@ -82,6 +82,7 @@ pub type JsonResult<T> = std::result::Result<T, JsonError>;
|
||||
pub enum Error {
|
||||
Unauthorized,
|
||||
UploadFailed,
|
||||
Forbidden,
|
||||
OwnedBusinessNotFound { account_id: i32 },
|
||||
OwnedBusinessItemNotFound { account_id: i32, business_id: i32 },
|
||||
DatabaseQuery,
|
||||
@ -118,6 +119,7 @@ impl Display for Error {
|
||||
Error::DatabaseQuery => {
|
||||
f.write_str("Problem z zapisaniem zmian. Proszę spróbować później")
|
||||
}
|
||||
Error::Forbidden => f.write_str("Tylko admin może wejść na tę stronę"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -130,6 +132,7 @@ impl ResponseError for Error {
|
||||
Error::OwnedBusinessItemNotFound { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::UploadFailed => StatusCode::BAD_REQUEST,
|
||||
Error::DatabaseQuery => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Forbidden => StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -180,6 +183,7 @@ impl ResponseError for JsonError {
|
||||
Error::OwnedBusinessItemNotFound { .. } => StatusCode::BAD_REQUEST,
|
||||
Error::UploadFailed => StatusCode::BAD_REQUEST,
|
||||
Error::DatabaseQuery => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
Error::Forbidden => StatusCode::FORBIDDEN,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -60,7 +60,7 @@ async fn handle_business_items_page(
|
||||
items,
|
||||
};
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.content_type("text/html")
|
||||
.body(page.render().unwrap()))
|
||||
}
|
||||
|
||||
@ -267,12 +267,21 @@ mod admin {
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
use tracing::{error, info};
|
||||
|
||||
use crate::model::view::Page;
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::{Error, Identity, Result};
|
||||
use crate::{queries, routes};
|
||||
use crate::queries;
|
||||
use crate::routes::{Identity, Result};
|
||||
|
||||
macro_rules! require_admin {
|
||||
($t: expr, $id: expr) => {{
|
||||
let account = authorize!(&mut $t, $id);
|
||||
if account.account_type == crate::model::db::AccountType::Admin {
|
||||
return Err(crate::routes::Error::Forbidden);
|
||||
}
|
||||
account
|
||||
}};
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin_panel.html")]
|
||||
@ -280,25 +289,80 @@ mod admin {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
news: Vec<db::NewsArticle>,
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
async fn admin() -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
async fn admin(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let news = queries::all_news(&mut t).await.unwrap_or_default();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AdminTemplate {
|
||||
page: Page::Account,
|
||||
page: Page::Admin,
|
||||
error: None,
|
||||
account: None,
|
||||
news,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/create")]
|
||||
async fn create_news(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::CreateNewsInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
if let Err(e) = queries::create_news_article(
|
||||
&mut t,
|
||||
db::CreateNewsArticleInput {
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
status: form.status,
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AdminTemplate {
|
||||
page: Page::AdminCreateNews,
|
||||
error: Some("Failed".into()),
|
||||
account: None,
|
||||
news: vec![],
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(web::scope("/admin").service(admin));
|
||||
config.service(
|
||||
web::scope("/admin")
|
||||
.service(web::scope("/news").service(create_news))
|
||||
.service(admin),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,7 @@ use tracing::*;
|
||||
use crate::model::db;
|
||||
use crate::model::view::{self, Page};
|
||||
use crate::routes::{Error, Identity, JsonResult, Result};
|
||||
use crate::{queries, routes, utils};
|
||||
use crate::{queries, utils};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
@ -26,9 +26,7 @@ pub struct IndexTemplate {
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn render_index() -> HttpResponse {
|
||||
HttpResponse::NotFound()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
HttpResponse::NotFound().content_type("text/html").body(
|
||||
IndexTemplate {
|
||||
services: vec![],
|
||||
account: None,
|
||||
@ -112,9 +110,7 @@ ORDER BY item_order ASC
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(body))
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
@ -333,14 +329,11 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
error!("{e} {:?}", dir);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
return HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some(
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia."
|
||||
.into(),
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.".into(),
|
||||
),
|
||||
page: Page::Register,
|
||||
}
|
||||
@ -353,14 +346,11 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
error!("{e} {:?}", item.picture_url);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
return HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some(
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia."
|
||||
.into(),
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.".into(),
|
||||
),
|
||||
page: Page::Register,
|
||||
}
|
||||
@ -381,9 +371,7 @@ RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return HttpResponse::BadRequest()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
return HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: None,
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
@ -476,9 +464,7 @@ async fn login(form: web::Form<LoginForm>, db: Data<PgPool>, id: Identity) -> Re
|
||||
id.remember(format!("{}", record.id));
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.append_header(("Content-Type", "text/html"))
|
||||
.body(
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: Some(record),
|
||||
error: None,
|
||||
|
Loading…
Reference in New Issue
Block a user