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