Update article
This commit is contained in:
parent
4f9749949c
commit
da4ec39358
62
Cargo.lock
generated
62
Cargo.lock
generated
@ -428,6 +428,7 @@ dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"syn",
|
||||
"toml",
|
||||
]
|
||||
@ -658,9 +659,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.3"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||
checksum = "2ccfd8c0ee4cce11e45b3fd6f9d5e69e0cc62912aa6a0cb1bf4617b0eba5a12f"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
@ -950,9 +951,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.12.1"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3"
|
||||
checksum = "607c8a29735385251a339424dd462993c0fed8fa09d378f259377df08c126022"
|
||||
dependencies = [
|
||||
"ahash",
|
||||
]
|
||||
@ -1281,9 +1282,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225"
|
||||
checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@ -1395,18 +1396,18 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e"
|
||||
checksum = "78203e83c48cffbe01e4a2d35d566ca4de445d79a85372fc64e378bfc812a260"
|
||||
dependencies = [
|
||||
"pin-project-internal",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-internal"
|
||||
version = "1.0.10"
|
||||
version = "1.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb"
|
||||
checksum = "710faf75e1b33345361201d36d04e98ac1ed8909151a017ed384700836104c74"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1537,9 +1538,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.5.6"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1"
|
||||
checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@ -1548,9 +1549,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.6.26"
|
||||
version = "0.6.27"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
|
||||
checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
@ -1621,24 +1622,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.11"
|
||||
version = "1.0.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d92beeab217753479be2f74e54187a6aed4c125ff0703a866c3147a02f0c6dd"
|
||||
checksum = "a2333e6df6d6598f2b1974829f853c2b4c5f4a6e503c10af918081aa6f8564e1"
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
checksum = "0171ebb889e45aa68b44aee0859b3eede84c6f5f5c228e6f140c0b2a0a46cad6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.137"
|
||||
version = "1.0.139"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||
checksum = "dc1d3230c1de7932af58ad8ffbe1d784bd55efd5a9d84ac24f69c72d83543dfb"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1955,10 +1956,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.19.2"
|
||||
version = "1.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439"
|
||||
checksum = "57aec3cfa4c296db7255446efb4928a6be304b431a806216105542a67b6ca82e"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
"libc",
|
||||
"memchr",
|
||||
@ -2045,9 +2047,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-attributes"
|
||||
version = "0.1.21"
|
||||
version = "0.1.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc6b8ad3567499f98a1db7a752b07a7c8c7c7c34c332ec00effb2b0027974b7c"
|
||||
checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -2087,9 +2089,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tracing-subscriber"
|
||||
version = "0.3.13"
|
||||
version = "0.3.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0a07fefe56b3bf1902c19c4eced76e43cdf40e6bac95cf71d873f6dd9a8a9af"
|
||||
checksum = "3a713421342a5a666b7577783721d3117f1b69a393df803ee17bb73b1e122a59"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"sharded-slab",
|
||||
@ -2144,9 +2146,9 @@ checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-normalization"
|
||||
version = "0.1.20"
|
||||
version = "0.1.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81dee68f85cab8cf68dec42158baf3a79a1cdc065a8b103025965d6ccb7f6cbd"
|
||||
checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6"
|
||||
dependencies = [
|
||||
"tinyvec",
|
||||
]
|
||||
@ -2344,9 +2346,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.22.3"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf"
|
||||
checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf"
|
||||
dependencies = [
|
||||
"webpki",
|
||||
]
|
||||
|
@ -14,7 +14,7 @@ actix-rt = { version = "*" }
|
||||
actix-utils = { version = "3.0.0" }
|
||||
actix-web = { version = "*" }
|
||||
argon2 = { version = "0.4.1" }
|
||||
askama = { version = "*" }
|
||||
askama = { version = "*", features = ["serde-json"] }
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
futures = { version = "0.3.21", features = ["async-await", "std"] }
|
||||
futures-util = { version = "0.3.21", features = [] }
|
||||
|
12
assets/templates/admin/edit.html
Normal file
12
assets/templates/admin/edit.html
Normal file
@ -0,0 +1,12 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<article-form
|
||||
article-id="{{article.id}}"
|
||||
article-title="{{article.title}}"
|
||||
status="{{article.status.as_str()}}"
|
||||
published-at="{{article.published_at|opt_time}}"
|
||||
created-at="{{article.created_at}}"
|
||||
>
|
||||
{{ article.body|safe }}
|
||||
</article-form>
|
||||
{% endblock %}
|
20
assets/templates/admin/index.html
Normal file
20
assets/templates/admin/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
{% extends "layout.html" %}
|
||||
{% block content %}
|
||||
<ow-admin>
|
||||
<ow-articles>
|
||||
{% for article in news %}
|
||||
<edit-news-article article-id="{{ article.id }}">
|
||||
<news-article
|
||||
article-id="{{ article.id }}"
|
||||
article-title="{{ article.title }}"
|
||||
status="{{ article.status.as_str() }}"
|
||||
published-at="{{ article.published_at|opt_time }}"
|
||||
created-at="{{ article.created_at }}"
|
||||
>
|
||||
{{article.body|safe}}
|
||||
</news-article>
|
||||
</edit-news-article>
|
||||
{% endfor %}
|
||||
</ow-articles>
|
||||
</ow-admin>
|
||||
{% endblock %}
|
@ -1,4 +1,4 @@
|
||||
{% extends "base.html" %}
|
||||
{% extends "../base.html" %}
|
||||
{% block head %}
|
||||
<script type="module" src=/assets/js/admin.js></script>
|
||||
{% endblock %}
|
@ -1,18 +0,0 @@
|
||||
{% extends "admin.html" %}
|
||||
{% block content %}
|
||||
<ow-admin>
|
||||
<ow-articles>
|
||||
{% for article in news %}
|
||||
<news-article
|
||||
article-id="{{article.id}}"
|
||||
article-title="{{article.title}}"
|
||||
status="{{article.status.as_str()}}"
|
||||
published-at="{{article.published_at}}"
|
||||
created-at="{{article.created_at}}"
|
||||
>
|
||||
{{article.body|safe}}
|
||||
</news-article>
|
||||
{% endfor %}
|
||||
</ow-articles>
|
||||
</ow-admin>
|
||||
{% endblock %}
|
@ -3,11 +3,11 @@
|
||||
<ow-articles>
|
||||
{% for article in news %}
|
||||
<news-article
|
||||
article-id="{{article.id}}"
|
||||
article-title="{{article.title}}"
|
||||
status="{{article.status.as_str()}}"
|
||||
published-at="{{article.published_at}}"
|
||||
created-at="{{article.created_at}}"
|
||||
article-id="{{ article.id }}"
|
||||
article-title="{{ article.title }}"
|
||||
status="{{ article.status.as_str() }}"
|
||||
published-at="{{ article.published_at|opt_time }}"
|
||||
created-at="{{ article.created_at }}"
|
||||
>
|
||||
{{article.body|safe}}
|
||||
</news-article>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import "./admin/ow-admin";
|
||||
|
||||
import "./admin/article-form";
|
||||
import "./admin/edit-news-article";
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { Component, FORM_STYLE } from "../shared";
|
||||
|
||||
customElements.define('article-form', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['article-id', 'article-title', 'status'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
@ -14,21 +18,21 @@ customElements.define('article-form', class extends Component {
|
||||
<form action="/admin/news/create" method="post">
|
||||
<div>
|
||||
<label>Tytuł</label>
|
||||
<input placeholder="Tytuł" name="title" />
|
||||
<input placeholder="Tytuł" name="title" id="title" />
|
||||
</div>
|
||||
<section id="body-view">
|
||||
<rich-text-editor upload-url="/admin/news/upload">
|
||||
<div id="body-view">
|
||||
<rich-text-editor id="body-rte" upload-url="/admin/news/upload">
|
||||
</rich-text-editor>
|
||||
<input type="hidden" name="body" />
|
||||
</section>
|
||||
<section>
|
||||
</div>
|
||||
<div>
|
||||
<label>Status</label>
|
||||
<select name="status">
|
||||
<option selected value="pending">Oczekujący</option>
|
||||
<select id="status" name="status">
|
||||
<option value="pending">Oczekujący</option>
|
||||
<option value="published">Opublikowany</option>
|
||||
<option value="hidden">Ukryty</option>
|
||||
</select>
|
||||
</section>
|
||||
</div>
|
||||
<input type="submit" value="Zapisz" />
|
||||
</form>
|
||||
`);
|
||||
@ -42,5 +46,65 @@ customElements.define('article-form', class extends Component {
|
||||
|
||||
connectedCallback() {
|
||||
super.connectedCallback();
|
||||
this.body = this.innerHTML;
|
||||
}
|
||||
|
||||
get article_id() {
|
||||
const id = parseInt(this.getAttribute('article-id'));
|
||||
return isNaN(id) ? null : id;
|
||||
}
|
||||
|
||||
set article_id(v) {
|
||||
this.setAttribute('article-id', v);
|
||||
const form = this.shadowRoot.querySelector('form');
|
||||
if (this.article_id === null) {
|
||||
this.#removeIdInput();
|
||||
form.action = "/admin/news/create";
|
||||
} else {
|
||||
this.#removeIdInput();
|
||||
this.#createIdInput(v);
|
||||
form.action = "/admin/news/update";
|
||||
}
|
||||
}
|
||||
|
||||
#createIdInput(v) {
|
||||
const el = this.shadowRoot.querySelector('form').appendChild(document.createElement('input'));
|
||||
el.id = 'id';
|
||||
el.name = 'id';
|
||||
el.value = v;
|
||||
el.type = 'hidden';
|
||||
}
|
||||
|
||||
#removeIdInput() {
|
||||
const el = this.shadowRoot.querySelector('#id');
|
||||
el && el.remove();
|
||||
}
|
||||
|
||||
get article_title() {
|
||||
return this.getAttribute('article-title');
|
||||
}
|
||||
|
||||
set article_title(v) {
|
||||
this.setAttribute('article-title', v);
|
||||
this.shadowRoot.querySelector('#title').value = v;
|
||||
}
|
||||
|
||||
get status() {
|
||||
return this.getAttribute('status');
|
||||
}
|
||||
|
||||
set status(v) {
|
||||
this.setAttribute('status', v);
|
||||
this.shadowRoot.querySelector('#status').value = (v || '').toLocaleLowerCase();
|
||||
}
|
||||
|
||||
get body() {
|
||||
return this.getAttribute('status');
|
||||
}
|
||||
|
||||
set body(v) {
|
||||
v = (v || '').trim();
|
||||
this.shadowRoot.querySelector('#body-rte').value = v;
|
||||
this.shadowRoot.querySelector('[name="body"]').value = v;
|
||||
}
|
||||
});
|
||||
|
52
client/src/admin/edit-news-article.js
Normal file
52
client/src/admin/edit-news-article.js
Normal file
@ -0,0 +1,52 @@
|
||||
import { Component, BUTTON_STYLE } from "../shared";
|
||||
|
||||
customElements.define('edit-news-article', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['article-id'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
article { display: flex; }
|
||||
#edit {
|
||||
margin-right: 8px;
|
||||
}
|
||||
${ BUTTON_STYLE }
|
||||
</style>
|
||||
<article>
|
||||
<slot></slot>
|
||||
</article>
|
||||
<article>
|
||||
<ow-path id="edit" path="/">
|
||||
Edytuj
|
||||
</ow-path>
|
||||
<form method="post" action="/admin/news/delete">
|
||||
<input class="link" type="submit" id="delete" value="Usuń" />
|
||||
</form>
|
||||
</article>
|
||||
`);
|
||||
|
||||
this.shadowRoot.querySelector('#edit').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
this.shadowRoot.querySelector('#delete').addEventListener('click', ev => {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
});
|
||||
}
|
||||
|
||||
get article_id() {
|
||||
const v = parseInt(this.getAttribute('article-id'));
|
||||
return isNaN(v) ? null : v;
|
||||
}
|
||||
|
||||
set article_id(v) {
|
||||
this.setAttribute('article-id', v);
|
||||
const id = this.article_id;
|
||||
if (id === null) return;
|
||||
this.shadowRoot.querySelector('ow-path').path = `/admin/news/${id}`;
|
||||
}
|
||||
});
|
@ -16,7 +16,6 @@ customElements.define('ow-path', class extends Component {
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
text-transform: uppercase;
|
||||
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border-slim-color);
|
||||
}
|
||||
@ -29,14 +28,9 @@ customElements.define('ow-path', class extends Component {
|
||||
</style>
|
||||
<a><slot></slot></a>
|
||||
`);
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.selected = this.getAttribute('selected');
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
super.attributeChangedCallback(name, oldV, newV);
|
||||
this.shadowRoot.querySelector('a').addEventListener('click', ev => {
|
||||
document.location = this.path;
|
||||
});
|
||||
}
|
||||
|
||||
get selected() {
|
||||
|
@ -9,7 +9,7 @@ customElements.define('news-article', class extends Component {
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
:host { display: block; width: 100%; }
|
||||
.time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -2,27 +2,18 @@ export const S = Symbol();
|
||||
|
||||
export const BUTTON_STYLE = `
|
||||
input[type="button"], input[type="submit"] {
|
||||
padding: 12px 16px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
border-width: 1px;
|
||||
border-radius: 5px;
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);
|
||||
position: relative;
|
||||
margin-bottom: 20px;
|
||||
transition: .3s;
|
||||
|
||||
background: #46b5d1;
|
||||
color: #fff;
|
||||
|
||||
display: inline-block;
|
||||
font-weight: 400;
|
||||
text-align: center;
|
||||
vertical-align: middle;
|
||||
user-select: none;
|
||||
padding: .375rem .75rem;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
transition: color .15s ease-in-out,
|
||||
@ -37,24 +28,17 @@ input[type="button"], input[type="submit"] {
|
||||
color: #495057;
|
||||
background: white;
|
||||
}
|
||||
input.link {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
input.link:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FORM_STYLE = `
|
||||
form {
|
||||
display: block;
|
||||
}
|
||||
form legend {
|
||||
margin: 16px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
form.inline div {
|
||||
display: flex;
|
||||
}
|
||||
form > div {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
export const INPUT_STYLE = `
|
||||
input, textarea, select, option {
|
||||
font-size: 16px;
|
||||
|
||||
@ -92,6 +76,24 @@ input[type="password"],
|
||||
textarea {
|
||||
width: calc(100% - 1.5rem - 2px);
|
||||
}
|
||||
`;
|
||||
|
||||
export const FORM_STYLE = `
|
||||
form {
|
||||
display: block;
|
||||
}
|
||||
form legend {
|
||||
margin: 16px 0;
|
||||
font-weight: bold;
|
||||
font-size: 20px;
|
||||
}
|
||||
form.inline div {
|
||||
display: flex;
|
||||
}
|
||||
form > div {
|
||||
display: block;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
label {
|
||||
color: #000;
|
||||
text-transform: uppercase;
|
||||
@ -101,7 +103,8 @@ label {
|
||||
display: inline-block;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
${BUTTON_STYLE}
|
||||
${ INPUT_STYLE }
|
||||
${ BUTTON_STYLE }
|
||||
`;
|
||||
|
||||
export class Component extends HTMLElement {
|
||||
@ -144,7 +147,7 @@ export class Component extends HTMLElement {
|
||||
}
|
||||
|
||||
static attr2Field(name) {
|
||||
if ((this.constructor.attr2FieldBlacklist || []).includes(name))
|
||||
if ((this.constructor['attr2FieldBlacklist'] || []).includes(name))
|
||||
return;
|
||||
return name.replace('-', '_');
|
||||
}
|
||||
|
@ -170,21 +170,26 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
case 'normal':
|
||||
return this.#removeWrapper();
|
||||
case 'h1':
|
||||
return this.#wrapNode('H1');
|
||||
return this.#setWrap(['H1'], { repeat: 'ignore' });
|
||||
case 'h2':
|
||||
return this.#wrapNode('H2');
|
||||
return this.#setWrap(['H2'], { repeat: 'ignore' });
|
||||
case 'h3':
|
||||
return this.#wrapNode('H3');
|
||||
return this.#setWrap(['H3'], { repeat: 'ignore' });
|
||||
case 'h4':
|
||||
return this.#wrapNode('H4');
|
||||
return this.#setWrap(['H4'], { repeat: 'ignore' });
|
||||
case 'h5':
|
||||
return this.#wrapNode('H5');
|
||||
return this.#setWrap(['H5'], { repeat: 'ignore' });
|
||||
}
|
||||
});
|
||||
this.shadowRoot.querySelector('#ordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('ol', 'li');
|
||||
this.#setWrap(['ol', 'li'], { repeat: 'perform' });
|
||||
});
|
||||
this.shadowRoot.querySelector('#unordered-list').addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#setWrap(['ul', 'li'], { repeat: 'perform' });
|
||||
});
|
||||
{
|
||||
let timeout = null;
|
||||
@ -197,11 +202,6 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
}, 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');
|
||||
@ -246,7 +246,7 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
const f = new FormData;
|
||||
let name;
|
||||
if (file.name.includes('.')) {
|
||||
name = `${uuid}.${file.name.split('.').pop()}`;
|
||||
name = `${ uuid }.${ file.name.split('.').pop() }`;
|
||||
} else {
|
||||
name = uuid;
|
||||
}
|
||||
@ -340,7 +340,7 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('sup');
|
||||
this.#setWrap(['sup'], { repeat: 'drop' });
|
||||
});
|
||||
}
|
||||
{
|
||||
@ -348,7 +348,7 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
el.addEventListener('click', ev => {
|
||||
ev.stopPropagation();
|
||||
this.#saveSelection();
|
||||
this.#wrapNode('sub');
|
||||
this.#setWrap(['sub'], { repeat: 'drop' });
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -367,7 +367,36 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
}
|
||||
}
|
||||
|
||||
#wrapNode(...tags) {
|
||||
#setWrap(tags, { repeat }) {
|
||||
tags = tags.map(s => s.toLowerCase());
|
||||
this.#isWrapped(tags);
|
||||
if (this.#isWrapped(tags)) {
|
||||
switch (repeat) {
|
||||
case 'drop':
|
||||
return this.#removeWrap(tags);
|
||||
case 'perform':
|
||||
return this.#createWrap(tags);
|
||||
case 'ignore':
|
||||
return this.#targetElement;
|
||||
}
|
||||
} else {
|
||||
return this.#createWrap(tags);
|
||||
}
|
||||
}
|
||||
|
||||
#isWrapped(tags) {
|
||||
let el = this.#targetElement;
|
||||
if (!el) return false;
|
||||
for (let i = tags.length - 1; i >= 0; i--) {
|
||||
if (!el || !el.tagName || el.tagName.toLowerCase() !== tags[i]) {
|
||||
return false;
|
||||
}
|
||||
el = el.parentElement;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
#createWrap(tags) {
|
||||
const selected = this.#selected;
|
||||
let el = this.#targetElement;
|
||||
if (!el) return;
|
||||
@ -386,6 +415,10 @@ customElements.define('rich-text-editor', class extends Component {
|
||||
return el;
|
||||
}
|
||||
|
||||
#removeWrap(tags) {
|
||||
|
||||
}
|
||||
|
||||
#removeWrapper() {
|
||||
const el = this.#selected;
|
||||
if (!this.constructor.#isEditNode(el)) return;
|
||||
|
@ -0,0 +1,2 @@
|
||||
ALTER TABLE news
|
||||
ALTER COLUMN published_at DROP NOT NULL;
|
@ -12,6 +12,7 @@ mod model;
|
||||
pub mod queries;
|
||||
mod routes;
|
||||
mod utils;
|
||||
pub mod view;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
|
@ -106,8 +106,8 @@ pub struct NewsArticle {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: NewsStatus,
|
||||
pub published_at: chrono::NaiveDateTime,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub published_at: Option<NaiveDateTime>,
|
||||
pub created_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
@ -117,6 +117,15 @@ pub struct CreateNewsArticleInput {
|
||||
pub status: NewsStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct UpdateNewsArticleInput {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: NewsStatus,
|
||||
pub published_at: Option<NaiveDateTime>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateLocalBusinessItemInput {
|
||||
pub local_business_id: i32,
|
||||
|
@ -89,7 +89,7 @@ impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for Local
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateBusinessItemInput {
|
||||
pub name: String,
|
||||
pub price: i64,
|
||||
@ -97,7 +97,7 @@ pub struct CreateBusinessItemInput {
|
||||
pub item_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateBusinessItemInput {
|
||||
pub id: i32,
|
||||
pub name: String,
|
||||
@ -106,19 +106,27 @@ pub struct UpdateBusinessItemInput {
|
||||
pub item_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct ModifyBusinessItemInput {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize)]
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct MoveBusinessItemInput {
|
||||
pub id: i32,
|
||||
pub item_order: i32,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct CreateNewsInput {
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UpdateNewsArticleInput {
|
||||
pub id: i32,
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: db::NewsStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct CreateNewsArticleInput {
|
||||
pub title: String,
|
||||
pub body: String,
|
||||
pub status: db::NewsStatus,
|
||||
|
@ -35,10 +35,16 @@ pub enum Error {
|
||||
item_id: i32,
|
||||
},
|
||||
AllNews,
|
||||
NewsArticleById {
|
||||
id: i32,
|
||||
},
|
||||
PublishedNews,
|
||||
CreateNewsArticle {
|
||||
input: db::CreateNewsArticleInput,
|
||||
},
|
||||
UpdateNewsArticle {
|
||||
input: db::UpdateNewsArticleInput,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@ -408,6 +414,7 @@ RETURNING
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn account_by_email(t: &mut T<'_>, email: String) -> Result<db::Account> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
@ -426,6 +433,7 @@ WHERE email = $1
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn all_news(t: &mut T<'_>) -> Result<Vec<NewsArticle>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
@ -449,6 +457,33 @@ FROM
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn news_article_by_id(t: &mut T<'_>, id: i32) -> Result<NewsArticle> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
status,
|
||||
published_at,
|
||||
created_at
|
||||
FROM
|
||||
news
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::NewsArticleById { id }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn published_news(t: &mut T<'_>) -> Result<Vec<NewsArticle>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
@ -474,6 +509,7 @@ WHERE
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn create_news_article(
|
||||
t: &mut T<'_>,
|
||||
input: db::CreateNewsArticleInput,
|
||||
@ -502,3 +538,35 @@ RETURNING
|
||||
Error::CreateNewsArticle { input }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn update_news_article(
|
||||
t: &mut T<'_>,
|
||||
input: db::UpdateNewsArticleInput,
|
||||
) -> Result<NewsArticle> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
UPDATE news
|
||||
SET title = $2, body = $3, status = $4
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
title,
|
||||
body,
|
||||
status,
|
||||
published_at,
|
||||
created_at
|
||||
"#,
|
||||
)
|
||||
.bind(input.id)
|
||||
.bind(&input.title)
|
||||
.bind(&input.body)
|
||||
.bind(input.status)
|
||||
.fetch_one(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(e);
|
||||
Error::UpdateNewsArticle { input }
|
||||
})
|
||||
}
|
||||
|
@ -273,7 +273,7 @@ mod business_item {
|
||||
}
|
||||
|
||||
mod admin {
|
||||
use actix_web::web::{Data, Form, ServiceConfig};
|
||||
use actix_web::web::{Data, Form, Path, ServiceConfig};
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
@ -282,6 +282,7 @@ mod admin {
|
||||
use crate::model::{db, view};
|
||||
use crate::queries;
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::filters;
|
||||
|
||||
macro_rules! require_admin {
|
||||
($t: expr, $id: expr) => {{
|
||||
@ -301,7 +302,7 @@ mod admin {
|
||||
}
|
||||
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin_panel.html")]
|
||||
#[template(path = "admin/index.html")]
|
||||
struct AdminTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
@ -331,11 +332,53 @@ mod admin {
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/create")]
|
||||
async fn create_news(
|
||||
#[derive(Debug, Template)]
|
||||
#[template(path = "admin/edit.html")]
|
||||
struct EditTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
article: db::NewsArticle,
|
||||
}
|
||||
|
||||
#[get("/{id}")]
|
||||
async fn edit_news_article(
|
||||
path: Path<(i32,)>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::CreateNewsInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let article_id = path.into_inner().0;
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let article = match queries::news_article_by_id(&mut t, article_id).await {
|
||||
Ok(article) => article,
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
|
||||
return Ok(HttpResponse::BadRequest().finish());
|
||||
}
|
||||
};
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
EditTemplate {
|
||||
page: Page::Admin,
|
||||
error: None,
|
||||
account: None,
|
||||
article,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/create")]
|
||||
async fn create_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::CreateNewsArticleInput>,
|
||||
) -> Result<HttpResponse> {
|
||||
let form = form.into_inner();
|
||||
let pool = db.into_inner();
|
||||
@ -374,8 +417,53 @@ mod admin {
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[post("/update")]
|
||||
async fn update_news_article(
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
form: Form<view::UpdateNewsArticleInput>,
|
||||
) -> 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);
|
||||
|
||||
match queries::update_news_article(
|
||||
&mut t,
|
||||
db::UpdateNewsArticleInput {
|
||||
id: form.id,
|
||||
title: form.title,
|
||||
body: form.body,
|
||||
status: form.status,
|
||||
published_at: if matches!(form.status, db::NewsStatus::Published) {
|
||||
Some(chrono::Utc::now().naive_utc())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
Ok(..) => {
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/admin"))
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn upload(
|
||||
async fn news_article_upload(
|
||||
payload: actix_multipart::Multipart,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
@ -390,7 +478,13 @@ mod admin {
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(
|
||||
web::scope("/admin")
|
||||
.service(web::scope("/news").service(create_news).service(upload))
|
||||
.service(
|
||||
web::scope("/news")
|
||||
.service(create_news_article)
|
||||
.service(news_article_upload)
|
||||
.service(edit_news_article)
|
||||
.service(update_news_article),
|
||||
)
|
||||
.service(admin),
|
||||
);
|
||||
}
|
||||
|
@ -11,6 +11,7 @@ use tracing::*;
|
||||
use crate::model::db;
|
||||
use crate::model::view::{self, Page};
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::filters;
|
||||
use crate::{queries, utils};
|
||||
|
||||
#[derive(Template)]
|
||||
|
7
src/view/mod.rs
Normal file
7
src/view/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
pub mod filters {
|
||||
pub fn opt_time(s: &Option<chrono::NaiveDateTime>) -> ::askama::Result<String> {
|
||||
Ok(s.as_ref()
|
||||
.map(|t| serde_json::to_string(t).unwrap_or_default())
|
||||
.unwrap_or_default())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user