Add image handler
This commit is contained in:
parent
4ce82d15fb
commit
25986ec594
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,2 +1,4 @@
|
|||||||
/target
|
/target
|
||||||
node_modules
|
node_modules
|
||||||
|
uploads
|
||||||
|
dist
|
||||||
|
35
Cargo.lock
generated
35
Cargo.lock
generated
@ -143,6 +143,24 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "actix-multipart"
|
||||||
|
version = "0.4.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c9edfb0e7663d7fe18c8d5b668c9c1bcf79176b1dcc9d4da9592503209a6bfb0"
|
||||||
|
dependencies = [
|
||||||
|
"actix-utils",
|
||||||
|
"actix-web",
|
||||||
|
"bytes",
|
||||||
|
"derive_more",
|
||||||
|
"futures-core",
|
||||||
|
"httparse",
|
||||||
|
"local-waker",
|
||||||
|
"log",
|
||||||
|
"mime",
|
||||||
|
"twoway",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@ -1282,6 +1300,7 @@ dependencies = [
|
|||||||
"actix-files",
|
"actix-files",
|
||||||
"actix-http",
|
"actix-http",
|
||||||
"actix-identity",
|
"actix-identity",
|
||||||
|
"actix-multipart",
|
||||||
"actix-rt",
|
"actix-rt",
|
||||||
"actix-utils",
|
"actix-utils",
|
||||||
"actix-web",
|
"actix-web",
|
||||||
@ -2080,12 +2099,28 @@ dependencies = [
|
|||||||
"tracing-log",
|
"tracing-log",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "twoway"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c57ffb460d7c24cd6eda43694110189030a3d1dfe418416d9468fd1c1d290b47"
|
||||||
|
dependencies = [
|
||||||
|
"memchr",
|
||||||
|
"unchecked-index",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typenum"
|
name = "typenum"
|
||||||
version = "1.15.0"
|
version = "1.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unchecked-index"
|
||||||
|
version = "0.2.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "eeba86d422ce181a719445e51872fa30f1f7413b62becb52e95ec91aa262d85c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.6.0"
|
version = "2.6.0"
|
||||||
|
29
Cargo.toml
29
Cargo.toml
@ -5,26 +5,27 @@ edition = "2021"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = { version = "*" }
|
actix = { version = "*" }
|
||||||
actix-web = { version = "*" }
|
|
||||||
actix-http = { version = "3.2.1" }
|
|
||||||
actix-cors = { version = "*" }
|
actix-cors = { version = "*" }
|
||||||
|
actix-files = { version = "*" }
|
||||||
|
actix-http = { version = "3.2.1" }
|
||||||
|
actix-identity = { version = "0.4.0" }
|
||||||
|
actix-multipart = { version = "0.4.0" }
|
||||||
actix-rt = { version = "*" }
|
actix-rt = { version = "*" }
|
||||||
actix-utils = { version = "3.0.0" }
|
actix-utils = { version = "3.0.0" }
|
||||||
actix-files = { version = "*" }
|
actix-web = { version = "*" }
|
||||||
actix-identity = { version = "0.4.0" }
|
argon2 = { version = "0.4.1" }
|
||||||
askama = { version = "*" }
|
askama = { version = "*" }
|
||||||
validator = { version = "0.14", features = ["derive"] }
|
chrono = { version = "*", features = ["serde"] }
|
||||||
|
futures = { version = "0.3.21", features = ["async-await", "std"] }
|
||||||
|
futures-util = { version = "0.3.21", features = [] }
|
||||||
|
gumdrop = { version = "*" }
|
||||||
|
password-hash = { version = "0.4.2" }
|
||||||
|
rand = { version = "0.8.5", features = [] }
|
||||||
serde = { version = "*", features = ["derive"] }
|
serde = { version = "*", features = ["derive"] }
|
||||||
serde_json = { version = "*" }
|
serde_json = { version = "*" }
|
||||||
sqlx = { version = "*", features = ["runtime-actix-rustls", "postgres", "uuid", "chrono"] }
|
sqlx = { version = "*", features = ["runtime-actix-rustls", "postgres", "uuid", "chrono"] }
|
||||||
uuid = { version = "*", features = ["serde"] }
|
|
||||||
chrono = { version = "*", features = ["serde"] }
|
|
||||||
gumdrop = { version = "*" }
|
|
||||||
tracing = { version = "*" }
|
tracing = { version = "*" }
|
||||||
tracing-subscriber = { version = "*" }
|
|
||||||
tracing-actix-web = { version = "*" }
|
tracing-actix-web = { version = "*" }
|
||||||
argon2 = { version = "0.4.1" }
|
tracing-subscriber = { version = "*" }
|
||||||
password-hash = { version = "0.4.2" }
|
uuid = { version = "*", features = ["serde"] }
|
||||||
rand = { version = "0.8.5", features = [] }
|
validator = { version = "0.14", features = ["derive"] }
|
||||||
futures = { version = "0.3.21", features = ["async-await", "std"] }
|
|
||||||
futures-util = { version = "0.3.21", features = [] }
|
|
||||||
|
@ -2,7 +2,11 @@
|
|||||||
{% block content %}
|
{% block content %}
|
||||||
<business-items>
|
<business-items>
|
||||||
{% for item in items %}
|
{% for item in items %}
|
||||||
<business-item name="{{item.name}}" price="{{item.price}}">
|
<business-item
|
||||||
|
name="{{item.name}}"
|
||||||
|
price="{{item.price}}"
|
||||||
|
url="{{item.picture_url}}"
|
||||||
|
>
|
||||||
</business-item>
|
</business-item>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
|
88
client/dist/app.js
vendored
88
client/dist/app.js
vendored
@ -1266,6 +1266,14 @@ customElements.define("register-form", class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
customElements.define("image-input", class extends HTMLElement {
|
customElements.define("image-input", class extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return [
|
||||||
|
"width",
|
||||||
|
"height",
|
||||||
|
"account-id",
|
||||||
|
"url"
|
||||||
|
];
|
||||||
|
}
|
||||||
constructor(){
|
constructor(){
|
||||||
super();
|
super();
|
||||||
let b = this[S] = this.attachShadow({
|
let b = this[S] = this.attachShadow({
|
||||||
@ -1275,7 +1283,7 @@ customElements.define("image-input", class extends HTMLElement {
|
|||||||
<style>
|
<style>
|
||||||
:host { display: block; border: 1px solid black; }
|
:host { display: block; border: 1px solid black; }
|
||||||
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
||||||
input { position: absolute; top: -10px; left: -10px; display: none; }
|
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
|
||||||
#view { width: 200px; height: 200px; cursor: pointer; }
|
#view { width: 200px; height: 200px; cursor: pointer; }
|
||||||
canvas { width: 200px; height: 200px; }
|
canvas { width: 200px; height: 200px; }
|
||||||
</style>
|
</style>
|
||||||
@ -1284,13 +1292,31 @@ customElements.define("image-input", class extends HTMLElement {
|
|||||||
<input id="file" type="file" accept="image/*" />
|
<input id="file" type="file" accept="image/*" />
|
||||||
<img alt="" src="" />
|
<img alt="" src="" />
|
||||||
</section>
|
</section>
|
||||||
<div id="view"><canvas width="200" height="200"></canvas></div>
|
<div id="view">
|
||||||
|
<canvas width="200" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="save" type="button" value="Zapisz" />
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`;
|
`, b.querySelector("#save").addEventListener("click", (a)=>{
|
||||||
|
a.preventDefault(), a.stopPropagation();
|
||||||
|
let b = atob(g.toDataURL("image/webp", 1.0).split(",")[1]), c = [];
|
||||||
|
for(let d = 0; d < b.length; d++)c.push(b.charCodeAt(d));
|
||||||
|
let e = new Blob([
|
||||||
|
new Uint8Array(c)
|
||||||
|
], {
|
||||||
|
type: "image/webp"
|
||||||
|
}), f = new FormData;
|
||||||
|
f.append(`${crypto.randomUUID()}.webp`, e), fetch("/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: f
|
||||||
|
}).then((a)=>a.json()).then(({ path: a })=>this.url = a);
|
||||||
|
});
|
||||||
let c = new FileReader(), d = b.querySelector("#file"), e = b.querySelector("#view"), f = b.querySelector("img"), g = b.querySelector("canvas"), h = g.getContext("2d");
|
let c = new FileReader(), d = b.querySelector("#file"), e = b.querySelector("#view"), f = b.querySelector("img"), g = b.querySelector("canvas"), h = g.getContext("2d");
|
||||||
f.addEventListener("load", ()=>{
|
f.addEventListener("load", ()=>{
|
||||||
let a, b;
|
let a, b;
|
||||||
f.width > f.height ? (a = 200, b = 200 * f.height / f.width) : (a = 200 * f.width / f.height, b = 200), console.log(f.width, f.height), console.log(a, b), f.width = a, f.height = b, h.fillStyle = "#F00", h.rect(0, 0, 200, 200), h.drawImage(f, 0, 0, a, b);
|
f.width > f.height ? (a = 200, b = 200 * f.height / f.width) : (a = 200 * f.width / f.height, b = 200), this.setAttribute("width", a), this.setAttribute("height", b), f.width = a, f.height = b, h.clearRect(0, 0, 200, 200), h.drawImage(f, 0, 0, a, b);
|
||||||
}), d.addEventListener("change", (a)=>{
|
}), d.addEventListener("change", (a)=>{
|
||||||
a.stopPropagation(), c.addEventListener("loadend", (a)=>{
|
a.stopPropagation(), c.addEventListener("loadend", (a)=>{
|
||||||
a.total === a.loaded && (f.src = a.target.result || "");
|
a.total === a.loaded && (f.src = a.target.result || "");
|
||||||
@ -1299,6 +1325,37 @@ customElements.define("image-input", class extends HTMLElement {
|
|||||||
a.stopPropagation(), d.click();
|
a.stopPropagation(), d.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
connectedCallback() {
|
||||||
|
this.account_id = this.account_id, this.url = this.url;
|
||||||
|
}
|
||||||
|
attributeChangedCallback(a, b, c) {
|
||||||
|
if (b !== c) switch(a){
|
||||||
|
case "account-id":
|
||||||
|
return this.account_id = c;
|
||||||
|
case "url":
|
||||||
|
return this.url = c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get account_id() {
|
||||||
|
return this.getAttribute("account-id");
|
||||||
|
}
|
||||||
|
set account_id(a) {
|
||||||
|
this.setAttribute("account-id", a);
|
||||||
|
}
|
||||||
|
get width() {
|
||||||
|
let a = parseInt(this.getAttribute("width"));
|
||||||
|
return isNaN(a) ? 0 : a;
|
||||||
|
}
|
||||||
|
get height() {
|
||||||
|
let a = parseInt(this.getAttribute("height"));
|
||||||
|
return isNaN(a) ? 0 : a;
|
||||||
|
}
|
||||||
|
get url() {
|
||||||
|
return this.getAttribute("url");
|
||||||
|
}
|
||||||
|
set url(b) {
|
||||||
|
this.setAttribute("url", b), this[S].querySelector("img").src = b;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
customElements.define("business-item", class extends HTMLElement {
|
customElements.define("business-item", class extends HTMLElement {
|
||||||
static get observedAttributes() {
|
static get observedAttributes() {
|
||||||
@ -1326,28 +1383,35 @@ customElements.define("business-item", class extends HTMLElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.filter = this.getAttribute("filter");
|
this.name = this.name, this.price = this.price, this.picture_url = this.picture_url;
|
||||||
}
|
}
|
||||||
attributeChangedCallback(a, b, c) {
|
attributeChangedCallback(a, b, c) {
|
||||||
if (b !== c && "filter" === a) return this.filter = c;
|
if (b !== c) switch(a){
|
||||||
|
case "name":
|
||||||
|
return this.name = c;
|
||||||
|
case "price":
|
||||||
|
return this.price = c / 100.0;
|
||||||
|
case "picture-url":
|
||||||
|
return this.picture_url = c;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
get name() {
|
get name() {
|
||||||
return this.getAttribute("name");
|
return this.getAttribute("name");
|
||||||
}
|
}
|
||||||
set name(a) {
|
set name(b) {
|
||||||
this.setAttribute("name", a), this.querySelector("#name").textContent = a;
|
this.setAttribute("name", b), this[S].querySelector("#name").textContent = b;
|
||||||
}
|
}
|
||||||
get price() {
|
get price() {
|
||||||
return this.getAttribute("price");
|
return this.getAttribute("price");
|
||||||
}
|
}
|
||||||
set price(a) {
|
set price(b) {
|
||||||
this.setAttribute("price", a), this.querySelector("price-input").value = a;
|
this.setAttribute("price", b), this[S].querySelector("price-input").value = b;
|
||||||
}
|
}
|
||||||
get picture_url() {
|
get picture_url() {
|
||||||
return this.getAttribute("picture-url");
|
return this.getAttribute("picture-url");
|
||||||
}
|
}
|
||||||
set picture_url(a) {
|
set picture_url(b) {
|
||||||
this.setAttribute("picture-url", a), this.querySelector("image-input").src = a;
|
this.setAttribute("picture-url", b), this[S].querySelector("image-input").src = b;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
customElements.define("business-items", class extends HTMLElement {
|
customElements.define("business-items", class extends HTMLElement {
|
||||||
|
2
client/dist/app.js.map
vendored
2
client/dist/app.js.map
vendored
File diff suppressed because one or more lines are too long
@ -25,14 +25,17 @@ customElements.define('business-item', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.filter = this.getAttribute('filter');
|
this.name = this.name;
|
||||||
|
this.price = this.price;
|
||||||
|
this.picture_url = this.picture_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
attributeChangedCallback(name, oldV, newV) {
|
attributeChangedCallback(name, oldV, newV) {
|
||||||
if (oldV === newV) return;
|
if (oldV === newV) return;
|
||||||
switch (name) {
|
switch (name) {
|
||||||
case 'filter':
|
case 'name': return this.name = newV;
|
||||||
return this.filter = newV;
|
case 'price': return this.price = newV / 100.0;
|
||||||
|
case 'picture-url': return this.picture_url = newV;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,7 +44,7 @@ customElements.define('business-item', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
set name(v) {
|
set name(v) {
|
||||||
this.setAttribute('name', v);
|
this.setAttribute('name', v);
|
||||||
this.querySelector('#name').textContent = v;
|
this[S].querySelector('#name').textContent = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -50,7 +53,7 @@ customElements.define('business-item', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
set price(v) {
|
set price(v) {
|
||||||
this.setAttribute('price', v);
|
this.setAttribute('price', v);
|
||||||
this.querySelector('price-input').value = v;
|
this[S].querySelector('price-input').value = v;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -59,6 +62,6 @@ customElements.define('business-item', class extends HTMLElement {
|
|||||||
}
|
}
|
||||||
set picture_url(v) {
|
set picture_url(v) {
|
||||||
this.setAttribute('picture-url', v);
|
this.setAttribute('picture-url', v);
|
||||||
this.querySelector('image-input').src = v;
|
this[S].querySelector('image-input').src = v;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
import { S } from "../shared.js";
|
import { S } from "../shared.js";
|
||||||
|
|
||||||
customElements.define('image-input', class extends HTMLElement {
|
customElements.define('image-input', class extends HTMLElement {
|
||||||
|
static get observedAttributes() {
|
||||||
|
return ['width', 'height', "account-id", "url"]
|
||||||
|
}
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
@ -10,7 +14,7 @@ customElements.define('image-input', class extends HTMLElement {
|
|||||||
<style>
|
<style>
|
||||||
:host { display: block; border: 1px solid black; }
|
:host { display: block; border: 1px solid black; }
|
||||||
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
|
||||||
input { position: absolute; top: -10px; left: -10px; display: none; }
|
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
|
||||||
#view { width: 200px; height: 200px; cursor: pointer; }
|
#view { width: 200px; height: 200px; cursor: pointer; }
|
||||||
canvas { width: 200px; height: 200px; }
|
canvas { width: 200px; height: 200px; }
|
||||||
</style>
|
</style>
|
||||||
@ -19,10 +23,32 @@ customElements.define('image-input', class extends HTMLElement {
|
|||||||
<input id="file" type="file" accept="image/*" />
|
<input id="file" type="file" accept="image/*" />
|
||||||
<img alt="" src="" />
|
<img alt="" src="" />
|
||||||
</section>
|
</section>
|
||||||
<div id="view"><canvas width="200" height="200"></canvas></div>
|
<div id="view">
|
||||||
|
<canvas width="200" height="200"></canvas>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<input id="save" type="button" value="Zapisz" />
|
||||||
|
</div>
|
||||||
</article>
|
</article>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
shadow.querySelector('#save').addEventListener('click', ev => {
|
||||||
|
ev.preventDefault();
|
||||||
|
ev.stopPropagation();
|
||||||
|
const blobBin = atob(canvas.toDataURL("image/webp", 1.0).split(',')[1]);
|
||||||
|
const array = [];
|
||||||
|
for (let i = 0; i < blobBin.length; i++) {
|
||||||
|
array.push(blobBin.charCodeAt(i));
|
||||||
|
}
|
||||||
|
const file = new Blob([new Uint8Array(array)], { type: 'image/webp' });
|
||||||
|
const form = new FormData;
|
||||||
|
form.append(`${ crypto.randomUUID() }.webp`, file);
|
||||||
|
fetch("/upload", {
|
||||||
|
method: "POST",
|
||||||
|
body: form,
|
||||||
|
}).then(res => res.json()).then(({ path }) => this.url = path);
|
||||||
|
});
|
||||||
|
|
||||||
const f = new FileReader();
|
const f = new FileReader();
|
||||||
const input = shadow.querySelector('#file');
|
const input = shadow.querySelector('#file');
|
||||||
const view = shadow.querySelector('#view');
|
const view = shadow.querySelector('#view');
|
||||||
@ -39,13 +65,12 @@ customElements.define('image-input', class extends HTMLElement {
|
|||||||
width = (img.width * 200) / img.height;
|
width = (img.width * 200) / img.height;
|
||||||
height = 200;
|
height = 200;
|
||||||
}
|
}
|
||||||
console.log(img.width, img.height);
|
this.setAttribute('width', width);
|
||||||
console.log(width, height);
|
this.setAttribute('height', height);
|
||||||
|
|
||||||
img.width = width;
|
img.width = width;
|
||||||
img.height = height;
|
img.height = height;
|
||||||
// ctx.drawImage(img, 0, 0);
|
ctx.clearRect(0, 0, 200, 200);
|
||||||
ctx.fillStyle = '#F00';
|
|
||||||
ctx.rect(0, 0, 200, 200);
|
|
||||||
ctx.drawImage(img, 0, 0, width, height);
|
ctx.drawImage(img, 0, 0, width, height);
|
||||||
});
|
});
|
||||||
input.addEventListener('change', ev => {
|
input.addEventListener('change', ev => {
|
||||||
@ -64,4 +89,46 @@ customElements.define('image-input', class extends HTMLElement {
|
|||||||
input.click();
|
input.click();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
connectedCallback() {
|
||||||
|
this.account_id = this.account_id;
|
||||||
|
this.url = this.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
attributeChangedCallback(name, oldV, newV) {
|
||||||
|
if (oldV === newV) return;
|
||||||
|
switch (name) {
|
||||||
|
case 'account-id':
|
||||||
|
return this.account_id = newV;
|
||||||
|
case 'url':
|
||||||
|
return this.url = newV;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get account_id() {
|
||||||
|
return this.getAttribute('account-id');
|
||||||
|
}
|
||||||
|
|
||||||
|
set account_id(v) {
|
||||||
|
this.setAttribute('account-id', v);
|
||||||
|
}
|
||||||
|
|
||||||
|
get width() {
|
||||||
|
const v = parseInt(this.getAttribute('width'));
|
||||||
|
return isNaN(v) ? 0 : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
get height() {
|
||||||
|
const v = parseInt(this.getAttribute('height'));
|
||||||
|
return isNaN(v) ? 0 : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
get url() {
|
||||||
|
return this.getAttribute('url');
|
||||||
|
}
|
||||||
|
|
||||||
|
set url(v) {
|
||||||
|
this.setAttribute('url', v);
|
||||||
|
this[S].querySelector('img').src = v;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
2
migrations/20220707203500_add_picture_url.sql
Normal file
2
migrations/20220707203500_add_picture_url.sql
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE local_business_items
|
||||||
|
ADD COLUMN picture_url TEXT NOT NULL UNIQUE;
|
@ -78,4 +78,5 @@ pub struct LocalBusinessItem {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
pub price: i64,
|
pub price: i64,
|
||||||
pub item_order: i32,
|
pub item_order: i32,
|
||||||
|
pub picture_url: String,
|
||||||
}
|
}
|
||||||
|
@ -49,19 +49,21 @@ impl Page {
|
|||||||
pub struct BusinessItemInput {
|
pub struct BusinessItemInput {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub price: u32,
|
pub price: u32,
|
||||||
|
pub picture_url: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BusinessItemInput {
|
impl BusinessItemInput {
|
||||||
pub fn new<S: Into<String>>(name: S, price: u32) -> Self {
|
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
|
||||||
Self {
|
Self {
|
||||||
name: name.into(),
|
name: name.into(),
|
||||||
price,
|
price,
|
||||||
|
picture_url: picture_url.into(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct LocalService {
|
pub struct LocalBusiness {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub owner_id: i32,
|
pub owner_id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@ -70,7 +72,7 @@ pub struct LocalService {
|
|||||||
pub items: Vec<db::LocalBusinessItem>,
|
pub items: Vec<db::LocalBusinessItem>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalService {
|
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalBusiness {
|
||||||
fn from((service, items): (db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)) -> Self {
|
fn from((service, items): (db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
id: service.id,
|
id: service.id,
|
||||||
|
@ -6,14 +6,17 @@ use actix_files::Files;
|
|||||||
use actix_web::web::{Data, ServiceConfig};
|
use actix_web::web::{Data, ServiceConfig};
|
||||||
use actix_web::*;
|
use actix_web::*;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
use futures_util::stream::StreamExt as _;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::path::PathBuf;
|
||||||
use tracing::*;
|
use tracing::*;
|
||||||
|
|
||||||
#[derive(Template)]
|
#[derive(Template)]
|
||||||
#[template(path = "index.html")]
|
#[template(path = "index.html")]
|
||||||
pub struct IndexTemplate {
|
pub struct IndexTemplate {
|
||||||
services: Vec<view::LocalService>,
|
services: Vec<view::LocalBusiness>,
|
||||||
account: Option<db::Account>,
|
account: Option<db::Account>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
page: Page,
|
page: Page,
|
||||||
@ -68,7 +71,8 @@ SELECT
|
|||||||
local_business_id,
|
local_business_id,
|
||||||
name,
|
name,
|
||||||
price,
|
price,
|
||||||
item_order
|
item_order,
|
||||||
|
picture_url
|
||||||
FROM local_business_items
|
FROM local_business_items
|
||||||
ORDER BY item_order DESC
|
ORDER BY item_order DESC
|
||||||
"#,
|
"#,
|
||||||
@ -88,7 +92,7 @@ ORDER BY item_order DESC
|
|||||||
use crate::model::view::*;
|
use crate::model::view::*;
|
||||||
services
|
services
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|service| LocalService::from((service, &mut items)))
|
.map(|service| LocalBusiness::from((service, &mut items)))
|
||||||
.collect()
|
.collect()
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -160,7 +164,7 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
|
|||||||
.map(|s| s.strip_suffix(']').unwrap_or(s));
|
.map(|s| s.strip_suffix(']').unwrap_or(s));
|
||||||
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
|
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
|
||||||
match name.next() {
|
match name.next() {
|
||||||
Some(s @ ("name" | "price")) => Some((idx, s.to_string(), value)),
|
Some(s @ ("name" | "price" | "picture_url")) => Some((idx, s.to_string(), value)),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@ -177,6 +181,9 @@ fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String
|
|||||||
"price" => {
|
"price" => {
|
||||||
item.price = value.parse().unwrap_or_default();
|
item.price = value.parse().unwrap_or_default();
|
||||||
}
|
}
|
||||||
|
"picture_url" => {
|
||||||
|
item.picture_url = value;
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
};
|
};
|
||||||
memo
|
memo
|
||||||
@ -304,15 +311,16 @@ RETURNING id, owner_id, name, description, state
|
|||||||
for (idx, item) in form.items.unwrap_or_default().iter().enumerate() {
|
for (idx, item) in form.items.unwrap_or_default().iter().enumerate() {
|
||||||
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
|
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
INSERT INTO local_business_items (local_business_id, name, price, item_order)
|
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
|
||||||
VALUES ($1, $2, $3, $4)
|
VALUES ($1, $2, $3, $4, $5)
|
||||||
RETURNING id, local_business_id, name, price, item_order
|
RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||||
"#,
|
"#,
|
||||||
)
|
)
|
||||||
.bind(business.id)
|
.bind(business.id)
|
||||||
.bind(&item.name)
|
.bind(&item.name)
|
||||||
.bind(item.price as i32)
|
.bind(item.price as i32)
|
||||||
.bind(idx as i32)
|
.bind(idx as i32)
|
||||||
|
.bind(item.picture_url)
|
||||||
.fetch_one(&mut t)
|
.fetch_one(&mut t)
|
||||||
.await;
|
.await;
|
||||||
match res {
|
match res {
|
||||||
@ -427,8 +435,47 @@ WHERE email = $1
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct UploadResponse {
|
||||||
|
path: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/upload")]
|
||||||
|
async fn upload(
|
||||||
|
mut payload: actix_multipart::Multipart,
|
||||||
|
id: Identity,
|
||||||
|
) -> Result<HttpResponse, actix_web::Error> {
|
||||||
|
let path = PathBuf::new().join(
|
||||||
|
id.identity()
|
||||||
|
.map(|id| format!("./uploads/{id}"))
|
||||||
|
.unwrap_or_else(|| "./uploads/tmp".into()),
|
||||||
|
);
|
||||||
|
std::fs::create_dir_all(&path)?;
|
||||||
|
|
||||||
|
if let Some(item) = payload.next().await {
|
||||||
|
let mut field = item?;
|
||||||
|
let name = field.name();
|
||||||
|
tracing::info!("Writing file {:?}", name);
|
||||||
|
let path = path.join(name);
|
||||||
|
|
||||||
|
while let Some(chunk) = field.next().await {
|
||||||
|
let chunk = chunk?;
|
||||||
|
std::fs::write(&path, chunk)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(HttpResponse::Ok().json(UploadResponse {
|
||||||
|
path: path.to_str().unwrap_or_default().into(),
|
||||||
|
}))
|
||||||
|
} else {
|
||||||
|
Ok(HttpResponse::BadRequest().finish())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn configure(config: &mut ServiceConfig) {
|
pub fn configure(config: &mut ServiceConfig) {
|
||||||
|
std::fs::create_dir_all("./uploads").expect("Failed to create ./uploads directory");
|
||||||
|
|
||||||
config
|
config
|
||||||
|
.service(Files::new("/uploads", "./uploads"))
|
||||||
.service(Files::new("/assets/images", "./assets/images"))
|
.service(Files::new("/assets/images", "./assets/images"))
|
||||||
.service(Files::new("/assets/css", "./assets/css"))
|
.service(Files::new("/assets/css", "./assets/css"))
|
||||||
.service(
|
.service(
|
||||||
@ -441,7 +488,8 @@ pub fn configure(config: &mut ServiceConfig) {
|
|||||||
.service(account_page)
|
.service(account_page)
|
||||||
.service(register)
|
.service(register)
|
||||||
.service(logout)
|
.service(logout)
|
||||||
.service(login);
|
.service(login)
|
||||||
|
.service(upload);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
Loading…
Reference in New Issue
Block a user