Update article

This commit is contained in:
Adrian Woźniak 2022-07-14 14:07:04 +02:00
parent 4f9749949c
commit da4ec39358
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
22 changed files with 484 additions and 131 deletions

62
Cargo.lock generated
View File

@ -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",
]

View File

@ -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 = [] }

View 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 %}

View 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 %}

View File

@ -1,4 +1,4 @@
{% extends "base.html" %}
{% extends "../base.html" %}
{% block head %}
<script type="module" src=/assets/js/admin.js></script>
{% endblock %}

View File

@ -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 %}

View File

@ -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>

View File

@ -1,3 +1,4 @@
import "./admin/ow-admin";
import "./admin/article-form";
import "./admin/edit-news-article";

View File

@ -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;
}
});

View 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}`;
}
});

View File

@ -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() {

View File

@ -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;

View File

@ -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,13 +147,13 @@ export class Component extends HTMLElement {
}
static attr2Field(name) {
if ((this.constructor.attr2FieldBlacklist || []).includes(name))
if ((this.constructor['attr2FieldBlacklist'] || []).includes(name))
return;
return name.replace('-', '_');
}
static get attr2FieldBlacklist() {
return []
return []
}
}

View File

@ -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;

View File

@ -0,0 +1,2 @@
ALTER TABLE news
ALTER COLUMN published_at DROP NOT NULL;

View File

@ -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<()> {

View File

@ -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,

View File

@ -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,

View File

@ -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 }
})
}

View File

@ -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),
);
}

View File

@ -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
View 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())
}
}