init
This commit is contained in:
commit
5469fda5de
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
@ -0,0 +1 @@
|
||||
/target
|
2754
Cargo.lock
generated
Normal file
2754
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Cargo.toml
Normal file
25
Cargo.toml
Normal file
@ -0,0 +1,25 @@
|
||||
[package]
|
||||
name = "oswilno"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
actix = "*"
|
||||
actix-web = "*"
|
||||
actix-cors = "*"
|
||||
actix-rt = "*"
|
||||
actix-files = "*"
|
||||
# actix-web-4-validator = { version = "3.2.0", default-features = false }
|
||||
actix-4-jwt-auth = "0.4.2"
|
||||
# actix-web-security = "*"
|
||||
askama = { version = "*" }
|
||||
validator = { version = "0.14", features = ["derive"] }
|
||||
serde = { version = "*", features = ["derive"] }
|
||||
serde_json = { version = "*" }
|
||||
sqlx = { version = "*", features = ["runtime-actix-rustls", "postgres", "uuid", "chrono"] }
|
||||
uuid = { version = "*", features = ["serde"] }
|
||||
chrono = { version = "*", features = ["serde"] }
|
||||
gumdrop = "*"
|
||||
tracing = "*"
|
||||
tracing-subscriber = "*"
|
||||
tracing-actix-web = "*"
|
3
askama.toml
Normal file
3
askama.toml
Normal file
@ -0,0 +1,3 @@
|
||||
[general]
|
||||
dirs = ["assets/templates"]
|
||||
whitespace = "preserve"
|
64
assets/css/app.css
Normal file
64
assets/css/app.css
Normal file
@ -0,0 +1,64 @@
|
||||
/* @media (min-width: 1200px) {
|
||||
.bg {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background:
|
||||
radial-gradient(circle, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 45%, rgba(255,255,255,1) 100%),
|
||||
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.bg h1 {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
font-size: 50px;
|
||||
text-shadow: 2px 2px 2px #c5d1d8;
|
||||
color: #FFF;
|
||||
padding-top: 60px;
|
||||
}
|
||||
}*/
|
||||
|
||||
@import url('http://fonts.cdnfonts.com/css/noto-sans');
|
||||
|
||||
main {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
article {
|
||||
width: 1280px;
|
||||
margin: auto auto;
|
||||
}
|
||||
.bg {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.bg::after {
|
||||
display: block;
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
background:
|
||||
linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(255,255,255,1) 1%, rgba(255,255,255,0) 100%),
|
||||
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
|
||||
height: 200px;
|
||||
width: calc(100% - 300px);
|
||||
max-width: 1200px;
|
||||
content: ' ';
|
||||
}
|
||||
|
||||
.bg h1 {
|
||||
font-weight: bold;
|
||||
font-size: 50px;
|
||||
text-shadow: 2px 2px 2px #c5d1d8;
|
||||
text-align: center;
|
||||
display: block;
|
||||
width: 300px;
|
||||
line-height: 4;
|
||||
}
|
||||
}
|
48
assets/css/reset.css
Normal file
48
assets/css/reset.css
Normal file
@ -0,0 +1,48 @@
|
||||
/* http://meyerweb.com/eric/tools/css/reset/
|
||||
v2.0 | 20110126
|
||||
License: none (public domain)
|
||||
*/
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre,
|
||||
a, abbr, acronym, address, big, cite, code,
|
||||
del, dfn, em, img, ins, kbd, q, s, samp,
|
||||
small, strike, strong, sub, sup, tt, var,
|
||||
b, u, i, center,
|
||||
dl, dt, dd, ol, ul, li,
|
||||
fieldset, form, label, legend,
|
||||
table, caption, tbody, tfoot, thead, tr, th, td,
|
||||
article, aside, canvas, details, embed,
|
||||
figure, figcaption, footer, header, hgroup,
|
||||
menu, nav, output, ruby, section, summary,
|
||||
time, mark, audio, video {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block;
|
||||
}
|
||||
body {
|
||||
line-height: 1;
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none;
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none;
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none;
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0;
|
||||
}
|
BIN
assets/images/background.jpeg
Normal file
BIN
assets/images/background.jpeg
Normal file
Binary file not shown.
After Width: | Height: | Size: 218 KiB |
BIN
assets/images/background.webp
Normal file
BIN
assets/images/background.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 170 KiB |
207
assets/js/app.js
Normal file
207
assets/js/app.js
Normal file
@ -0,0 +1,207 @@
|
||||
const S = Symbol();
|
||||
|
||||
customElements.define('local-services', class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['filter']
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
* { font-family: 'Noto Sans', sans-serif; }
|
||||
::slotted(local-service[local-services-visible="invisible"]) {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
<section>
|
||||
<input type="text" id="filter" />
|
||||
</section>
|
||||
<section id="items">
|
||||
<slot name="services"></slot>
|
||||
</section>
|
||||
`;
|
||||
{
|
||||
const filter = shadow.querySelector('#filter');
|
||||
let t = null;
|
||||
filter.addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
this.filter = ev.target.value;
|
||||
});
|
||||
filter.addEventListener('keyup', ev => {
|
||||
ev.stopPropagation();
|
||||
if (t) clearTimeout(t);
|
||||
t = setTimeout(() => {
|
||||
this.filter = ev.target.value;
|
||||
t = null;
|
||||
}, 1000 / 3);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.filter = this.getAttribute('filter');
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
if (oldV == newV) return;
|
||||
switch (name) {
|
||||
case 'filter': return this.filter = newV;
|
||||
}
|
||||
}
|
||||
|
||||
get filter() {
|
||||
return this.getAttribute('filter');
|
||||
}
|
||||
|
||||
set filter(value) {
|
||||
this.setAttribute('filter', value);
|
||||
for (const el of this.querySelectorAll('local-service')) {
|
||||
if (!el.name) continue;
|
||||
if (el.name.includes(value)) {
|
||||
el.setAttribute('local-services-visible', 'visible');
|
||||
} else {
|
||||
el.setAttribute('local-services-visible', 'invisible');
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
customElements.define('local-service', class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['name', 'service-id', 'state']
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
* { font-family: 'Noto Sans', sans-serif; }
|
||||
</style>
|
||||
<h2 id="name"></h2>
|
||||
<slot name="description"></slot>
|
||||
<section id="items">
|
||||
<slot name="item"></slot>
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#name').textContent = this.getAttribute('name');
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
switch (name) {
|
||||
case 'name': return this[S].querySelector('#name').textContent = newV;
|
||||
}
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.getAttribute('name') || ''
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
customElements.define('local-service-item', class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['name', 'price']
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
* { font-family: 'Noto Sans', sans-serif; }
|
||||
#item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
}
|
||||
#price {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<section id="item">
|
||||
<h3 id="name"></h3>
|
||||
<ow-price id="price" />
|
||||
</section>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#name').textContent = this.getAttribute('name');
|
||||
this[S].querySelector('#price').value = this.price();
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
switch (name) {
|
||||
case 'name': return this[S].querySelector('#name').textContent = newV;
|
||||
case 'price': return this[S].querySelector('#price').value = newV;
|
||||
}
|
||||
}
|
||||
|
||||
price(s) {
|
||||
const n = parseInt(s || this.getAttribute('price'));
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
});
|
||||
|
||||
customElements.define('ow-price', class extends HTMLElement {
|
||||
static get observedAttributes() {
|
||||
return ['value', 'currency']
|
||||
}
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
|
||||
shadow.innerHTML = `
|
||||
<style>
|
||||
:host { display: block; }
|
||||
* { font-family: 'Noto Sans', sans-serif; }
|
||||
#price {
|
||||
font-weight: bold;
|
||||
}
|
||||
</style>
|
||||
<span id="price"></span>
|
||||
`;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this[S].querySelector('#price').textContent = this.formatted;
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldV, newV) {
|
||||
switch (name) {
|
||||
case 'price': {
|
||||
this.value = newV;
|
||||
this[S].querySelector('#price').textContent = this.formatted;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
get formatted() {
|
||||
let v = this.value;
|
||||
let major = Math.ceil(v / 100);
|
||||
let minor = v % 100;
|
||||
let formatted = `${major},${ minor < 10 ? `0${minor}` : minor }`;
|
||||
return `${formatted}${this.currency}`
|
||||
}
|
||||
|
||||
get value() {
|
||||
const n = parseInt(this.getAttribute('value'));
|
||||
return isNaN(n) ? 0 : n;
|
||||
}
|
||||
|
||||
set value(v) {
|
||||
this.setAttribute('value', v)
|
||||
}
|
||||
|
||||
get currency() {
|
||||
return this.getAttribute('currency') || 'PLN'
|
||||
}
|
||||
});
|
38
assets/templates/index.html
Normal file
38
assets/templates/index.html
Normal file
@ -0,0 +1,38 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>OSWilno</title>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<link href="/assets/css/reset.css" rel="stylesheet" />
|
||||
<link href="/assets/css/app.css" rel="stylesheet" />
|
||||
<script type="module" src=/assets/js/app.js></script>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div class="bg">
|
||||
<h1>OS Wilno</h1>
|
||||
</div>
|
||||
</header>
|
||||
<article>
|
||||
<h1>Lokalne usługi</h1>
|
||||
<local-services>
|
||||
{% for service in services %}
|
||||
<local-service slot="services" service-id="{{service.id}}" name="{{service.name}}" state="{{service.state.to_str()}}">
|
||||
{% for line in service.description.lines() %}
|
||||
<p slot="description">{{line}}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% for item in service.items %}
|
||||
<local-service-item slot="item" name="{{item.name}}" price="{{item.price}}">
|
||||
</local-service-item>
|
||||
{% endfor %}
|
||||
|
||||
</local-service>
|
||||
{% endfor %}
|
||||
</local-services>
|
||||
</article>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
49
migrations/20220630202317_add_local_services.sql
Normal file
49
migrations/20220630202317_add_local_services.sql
Normal file
@ -0,0 +1,49 @@
|
||||
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
|
||||
|
||||
CREATE TABLE accounts (
|
||||
id serial unique not null primary key,
|
||||
login text not null,
|
||||
pass text not null
|
||||
);
|
||||
|
||||
CREATE TYPE "Role" AS ENUM (
|
||||
'User',
|
||||
'Admin'
|
||||
);
|
||||
|
||||
CREATE TYPE "LocalServiceState" AS ENUM (
|
||||
'Pending',
|
||||
'Approved',
|
||||
'Banned',
|
||||
'Pinned',
|
||||
'Internal'
|
||||
);
|
||||
|
||||
CREATE TABLE tokens (
|
||||
id serial unique not null primary key,
|
||||
claims jsonb not null,
|
||||
iss text not null default 'oswilno', /* issuer */
|
||||
sub int references accounts (id), /* subject */
|
||||
aud text not null default 'public', /* audience */
|
||||
exp timestamp not null, /* expiration time */
|
||||
nbt timestamp not null default now(), /* not before time */
|
||||
iat timestamp not null default now(), /* issued at time */
|
||||
jti uuid not null unique, /* JWT ID - unique */
|
||||
role "Role" not null default 'User'
|
||||
);
|
||||
|
||||
CREATE TABLE local_services (
|
||||
id serial unique not null primary key,
|
||||
owner_id int references accounts (id) not null,
|
||||
name text not null,
|
||||
description text not null,
|
||||
state "LocalServiceState" not null default 'Pending'
|
||||
);
|
||||
|
||||
CREATE TABLE local_service_items (
|
||||
id serial unique not null primary key,
|
||||
local_service_id int references local_services (id) not null,
|
||||
name text not null,
|
||||
price bigint not null,
|
||||
item_order int not null
|
||||
);
|
11
src/auth.rs
Normal file
11
src/auth.rs
Normal file
@ -0,0 +1,11 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
|
||||
pub struct FoundClaims {
|
||||
pub iss: String,
|
||||
pub sub: String,
|
||||
pub aud: String,
|
||||
pub name: String,
|
||||
pub email: Option<String>,
|
||||
pub email_verified: Option<bool>,
|
||||
}
|
34
src/main.rs
Normal file
34
src/main.rs
Normal file
@ -0,0 +1,34 @@
|
||||
#![feature(drain_filter)]
|
||||
use actix_web::{App, HttpServer, web::Data};
|
||||
|
||||
mod auth;
|
||||
mod model;
|
||||
mod routes;
|
||||
|
||||
#[actix_web::main] // or #[tokio::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
use actix_web::middleware::Logger;
|
||||
use tracing::{Level};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::DEBUG)
|
||||
.finish();
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
|
||||
let pool = sqlx::postgres::PgPoolOptions::new()
|
||||
.max_connections(8)
|
||||
.min_connections(8)
|
||||
.connect(&std::env::var("DATABASE_URL").expect("No database connection url"))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(Logger::default())
|
||||
.app_data(Data::new(pool.clone()))
|
||||
.configure(routes::configure)
|
||||
})
|
||||
.bind(("0.0.0.0", 8080))?
|
||||
.run()
|
||||
.await
|
||||
}
|
71
src/model/db.rs
Normal file
71
src/model/db.rs
Normal file
@ -0,0 +1,71 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::{FromRow, Type};
|
||||
use std::collections::HashMap;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Account {
|
||||
pub id: i32,
|
||||
pub login: String,
|
||||
pub pass: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Type)]
|
||||
pub enum Role {
|
||||
User,
|
||||
Admin,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Type)]
|
||||
pub enum LocalServiceState {
|
||||
Pending,
|
||||
Approved,
|
||||
Banned,
|
||||
Pinned,
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl LocalServiceState {
|
||||
pub fn to_str(&self) -> &str {
|
||||
match self {
|
||||
Self::Pending => "Pending",
|
||||
Self::Approved => "Approved",
|
||||
Self::Banned => "Banned",
|
||||
Self::Pinned => "Pinned",
|
||||
Self::Internal => "Internal",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct Token {
|
||||
pub id: i32,
|
||||
pub claims: HashMap<String, String>,
|
||||
pub iss: String, /* issuer */
|
||||
pub sub: i32, /* subject */
|
||||
pub aud: String, /* audience */
|
||||
pub exp: NaiveDateTime, /* expiration time */
|
||||
pub nbt: NaiveDateTime, /* not before time */
|
||||
pub iat: NaiveDateTime, /* issued at time */
|
||||
pub jti: Uuid, /* JWT ID - unique */
|
||||
pub role: Role,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct LocalService {
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub state: LocalServiceState,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct LocalServiceItem {
|
||||
pub id: i32,
|
||||
pub local_service_id: i32,
|
||||
pub name: String,
|
||||
pub price: i64,
|
||||
pub item_order: i32,
|
||||
}
|
2
src/model/mod.rs
Normal file
2
src/model/mod.rs
Normal file
@ -0,0 +1,2 @@
|
||||
pub mod db;
|
||||
pub mod view;
|
27
src/model/view.rs
Normal file
27
src/model/view.rs
Normal file
@ -0,0 +1,27 @@
|
||||
use crate::model::db;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct LocalService {
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub state: db::LocalServiceState,
|
||||
pub items: Vec<db::LocalServiceItem>,
|
||||
}
|
||||
|
||||
impl<'v> From<(db::LocalService, &'v mut Vec<db::LocalServiceItem>)> for LocalService {
|
||||
fn from((service, items): (db::LocalService, &'v mut Vec<db::LocalServiceItem>)) -> Self {
|
||||
Self {
|
||||
id: service.id,
|
||||
owner_id: service.owner_id,
|
||||
name: service.name,
|
||||
description: service.description,
|
||||
state: service.state,
|
||||
items: items
|
||||
.drain_filter(|i| i.local_service_id == service.id)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
7
src/routes/mod.rs
Normal file
7
src/routes/mod.rs
Normal file
@ -0,0 +1,7 @@
|
||||
use actix_web::web::ServiceConfig;
|
||||
|
||||
mod unrestricted;
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
unrestricted::configure(config);
|
||||
}
|
75
src/routes/unrestricted.rs
Normal file
75
src/routes/unrestricted.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::*;
|
||||
use askama::Template;
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "index.html")]
|
||||
pub struct IndexTemplate {
|
||||
pub services: Vec<crate::model::view::LocalService>,
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
pub async fn index(db: Data<sqlx::PgPool>) -> HttpResponse {
|
||||
let pool = db.into_inner();
|
||||
let (services, mut items) = {
|
||||
use crate::model::db::{LocalService, LocalServiceItem};
|
||||
let services: Vec<LocalService> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, owner_id, name, description, state
|
||||
FROM local_services
|
||||
WHERE state != 'Banned'
|
||||
GROUP BY id, state
|
||||
ORDER BY id
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("{e}");
|
||||
dbg!(&e);
|
||||
e
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
let items: Vec<LocalServiceItem> = sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
local_service_id,
|
||||
name,
|
||||
price,
|
||||
item_order
|
||||
FROM local_service_items
|
||||
"#,
|
||||
)
|
||||
.fetch_all(&*pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
eprintln!("{e}");
|
||||
dbg!(&e);
|
||||
e
|
||||
})
|
||||
.unwrap_or_default();
|
||||
(services, items)
|
||||
};
|
||||
|
||||
let services: Vec<_> = {
|
||||
use crate::model::view::*;
|
||||
services
|
||||
.into_iter()
|
||||
.map(|service| LocalService::from((service, &mut items)))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let body = IndexTemplate { services }.render().unwrap();
|
||||
HttpResponse::Ok().body(body)
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config
|
||||
.service(Files::new("/assets/images", "./assets/images"))
|
||||
.service(Files::new("/assets/css", "./assets/css"))
|
||||
.service(Files::new("/assets/js", "./assets/js").use_etag(true).prefer_utf8(true).show_files_listing())
|
||||
.service(index);
|
||||
}
|
Loading…
Reference in New Issue
Block a user