Compare commits

...

10 Commits

Author SHA1 Message Date
d6899a03e6 Work on Sycamore 2023-04-24 06:31:00 +02:00
50fffa9158 Add tmp 2023-04-23 06:28:54 +02:00
b9569229fb
Better UI, better admin 2022-08-05 16:15:14 +02:00
bbf774d8d9
Add link as contact, ui fixes 2022-08-05 14:25:50 +02:00
6cff8e7a37
Add business view 2022-08-03 16:12:01 +02:00
9cf85bece3 Fix register business 2022-08-02 16:16:35 +02:00
ac49905e0b
Admin 2022-08-01 16:00:29 +02:00
a9d223d976
Improve admin 2022-08-01 12:00:18 +02:00
670c1f7cf2
Fix order 2022-08-01 09:47:36 +02:00
ee61a10282 Fixes and better publish offer UI 2022-08-01 08:12:42 +02:00
59 changed files with 2474 additions and 313 deletions

3
.gitignore vendored
View File

@ -9,3 +9,6 @@ client/dist/admin.js
web/tmp
web/build
**/*.wasm
tmp
web/node_modules
web/dist

100
Cargo.lock generated
View File

@ -1026,6 +1026,15 @@ dependencies = [
"digest",
]
[[package]]
name = "html-escape"
version = "0.2.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d1ad449764d627e22bfd7cd5e8868264fc9236e07c752972b4080cd351cb476"
dependencies = [
"utf8-width",
]
[[package]]
name = "http"
version = "0.2.8"
@ -1815,6 +1824,15 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32"
[[package]]
name = "slotmap"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
dependencies = [
"version_check",
]
[[package]]
name = "smallvec"
version = "1.9.0"
@ -1959,6 +1977,75 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "sycamore"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67817393b3c9828db84614f64db9a1ebb94729ce3a3751c41e7ff23d3f8e7f00"
dependencies = [
"ahash",
"indexmap",
"js-sys",
"paste",
"sycamore-core",
"sycamore-macro",
"sycamore-reactive",
"sycamore-web",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "sycamore-core"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dce7f0440c5ea2b74a544deb5423708c023fade36e63515423d3f3ab5e1a998"
dependencies = [
"ahash",
"sycamore-reactive",
]
[[package]]
name = "sycamore-macro"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f3abd3f402c1a943cf70860b91a40c79c713e269156445998dbfd647deac8a5"
dependencies = [
"once_cell",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "sycamore-reactive"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6376b578ad32f5f3ab6943bccec906fb0e1f0258a8bedf811afdec8c3330ef80"
dependencies = [
"ahash",
"bumpalo",
"indexmap",
"slotmap",
"smallvec",
]
[[package]]
name = "sycamore-web"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6db9520735f765e60718df8125eb27db19b990bed1116c9b3e8aae374dde1fe8"
dependencies = [
"html-escape",
"indexmap",
"js-sys",
"once_cell",
"sycamore-core",
"sycamore-reactive",
"wasm-bindgen",
"web-sys",
]
[[package]]
name = "syn"
version = "1.0.98"
@ -2305,6 +2392,12 @@ dependencies = [
"percent-encoding",
]
[[package]]
name = "utf8-width"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5190c9442dcdaf0ddd50f37420417d219ae5261bbf5db120d0f9bab996c9cba1"
[[package]]
name = "uuid"
version = "1.1.2"
@ -2436,6 +2529,13 @@ version = "0.2.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be"
[[package]]
name = "web"
version = "0.1.0"
dependencies = [
"sycamore",
]
[[package]]
name = "web-sys"
version = "0.3.58"

View File

@ -1,4 +1,5 @@
[workspace]
members = [
'server',
'web',
]

View File

@ -8,3 +8,4 @@ import "./admin/businesses/admin-businesses";
import "./admin/businesses/admin-edit-business";
import "./admin/offers/admin-edit-offer";
import "./admin/offers/ow-admin-offers";

View File

@ -1,12 +1,66 @@
import { Component } from "../../shared";
import { Component, FORM_STYLE } from "../../shared";
customElements.define('admin-businesses', class extends Component {
static get observedAttributes() {
return ['state-filter'];
}
constructor() {
super(`
<style>
:host { display: block; }
::slotted([state]) {
display: none;
}
:host([state-filter="Pending"]) ::slotted([state="Pending"]) {
display: block;
}
:host([state-filter="Approved"]) ::slotted([state="Approved"]) {
display: block;
}
:host([state-filter="Banned"]) ::slotted([state="Banned"]) {
display: block;
}
:host([state-filter="Pinned"]) ::slotted([state="Pinned"]) {
display: block;
}
:host([state-filter="Internal"]) ::slotted([state="Internal"]) {
display: block;
}
${ FORM_STYLE }
</style>
<slot></slot>
<article>
<section>
<select id="state">
<option value="Pending">Pending</option>
<option value="Approved">Approved</option>
<option value="Banned">Banned</option>
<option value="Pinned">Pinned</option>
<option value="Internal">Internal</option>
</select>
</section>
<section>
<slot></slot>
</section>
</article>
`);
this.shadowRoot.querySelector('#state').addEventListener('change', ev => {
ev.stopPropagation();
this.state_filter = ev.target.value;
});
}
connectedCallback() {
super.connectedCallback();
this.state_filter = 'Pending';
}
get state_filter() {
return this.getAttribute('state-filter');
}
set state_filter(v) {
this.setAttribute('state-filter', v);
this.shadowRoot.querySelector('#state').value = v;
}
});

View File

@ -8,26 +8,47 @@ customElements.define('admin-edit-business', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
section {
:host {
display: block;
border-bottom: 2px solid var(--border-slim-color);
padding: 8px 0;
}
:host(:first-child) {
border-top: 2px solid var(--border-slim-color);
}
article {
display: flex;
justify-content: space-between;
}
#state {
min-width: 200px;
}
#view {
width: calc(100% - 220px);
}
#actions {
width: 200px
}
#actions > input:not(:last-child) {
margin-right: .5rem;
}
#actions > input {
margin-bottom: 8px;
width: 100%;
}
#actions select {
width: 100%;
}
::slotted(admin-business) {
width: 100%;
}
${ BUTTON_STYLE }
${ INPUT_STYLE }
${ BUTTON_STYLE }${ INPUT_STYLE }
</style>
<section>
<slot></slot>
<div id="actions">
<article>
<section id="view">
<slot></slot>
</section>
<section id="actions">
<input value="Usuń" type="button" />
<form id="change-state" action="/admin/businesses/set-state" method="post">
<input name="id" id="id" type="hidden" />
@ -39,8 +60,8 @@ customElements.define('admin-edit-business', class extends Component {
<option value="Internal">Internal</option>
</select>
</form>
</div>
</section>
</section>
</article>
`);
const form = this.shadowRoot.querySelector('#change-state');

View File

@ -10,6 +10,8 @@ customElements.define('admin-edit-offer', class extends Component {
<style>
:host {
display: block;
border-bottom: 2px solid var(--border-slim-color);
padding: 8px 0;
}
#view { display: block; }
#actions form {
@ -17,6 +19,7 @@ customElements.define('admin-edit-offer', class extends Component {
}
#actions form input {
width: 100%;
margin-bottom: 8px;
}
#state {
font-weight: bold;

View File

@ -0,0 +1,64 @@
import { Component, FORM_STYLE } from "../../shared.js";
customElements.define('ow-admin-offers', class extends Component {
static get observedAttributes() {
return ['state-filter'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
::slotted([state]) {
display: none;
}
:host([state-filter='Pending']) ::slotted([state="Pending"]) {
display: block;
}
:host([state-filter='Approved']) ::slotted([state="Approved"]) {
display: block;
}
:host([state-filter='Banned']) ::slotted([state="Banned"]) {
display: block;
}
:host([state-filter='Finished']) ::slotted([state="Finished"]) {
display: block;
}
${ FORM_STYLE }
</style>
<article>
<section>
<select id="state">
<option value="Pending">Pending</option>
<option value="Approved">Approved</option>
<option value="Banned">Banned</option>
<option value="Finished">Finished</option>
</select>
</section>
<section>
<slot></slot>
</section>
</article>
`);
this.shadowRoot.querySelector('#state').addEventListener('change', ev => {
ev.stopPropagation();
this.state_filter = ev.target.value;
});
}
connectedCallback() {
super.connectedCallback();
this.state_filter = 'Pending';
}
get state_filter() {
return this.getAttribute('state-filter');
}
set state_filter(v) {
this.setAttribute('state-filter', v);
this.shadowRoot.querySelector('#state').value = v;
}
});

View File

@ -18,8 +18,10 @@ import "./ow-account/account-view.js";
import "./local-businesses/local-businesses.js";
import "./local-businesses/local-business-item.js";
import "./local-businesses/local-business.js";
import "./local-businesses/single-local-business.js";
import "./login-form.js";
import "./register-form.js";
import "./register-form/register-business-account-form";
import "./register-form/register-business-item-form.js";

View File

@ -81,19 +81,23 @@ customElements.define('business-item-editor', class extends Component {
ev.stopPropagation();
ev.preventDefault();
if (this.shadowRoot.querySelector('register-item-form-row').reportValidity()) {
// console.info(this);
// if (this.shadowRoot.querySelector('register-item-form-row').reportValidity()) {
if (form.reportValidity()) {
const {
name,
price,
picture_url,
item_order,
} = ev.detail;
form.querySelector('#name').value = name;
form.querySelector('#price').value = price;
form.querySelector('#picture_url').value = picture_url;
form.querySelector('#item_order').value = item_order;
form.submit();
}
// }
});
const moveForm = this.shadowRoot.querySelector('#moveForm');
this.addEventListener('item:up', ev => {

View File

@ -147,10 +147,6 @@ customElements.define('business-item', class extends Component {
}
}
connectedCallback() {
super.connectedCallback();
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);

View File

@ -160,6 +160,9 @@ customElements.define('contact-info-editor', class extends Component {
if (s.match(/^[a-zA-Z\d.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z\d-]+(?:\.[a-zA-Z\d-]+)*$/)) {
return 'email';
}
if (s.match(/https?:\/\//)) {
return 'link';
}
return 'other';
}
});

View File

@ -23,7 +23,8 @@ customElements.define('contact-type-icon',
:host([type="mobile"]) #mobile-icon {
display: block;
}
path {
:host([type='link']) #link-icon {
display: block;
}
</style>
@ -45,6 +46,9 @@ customElements.define('contact-type-icon',
<circle cx="272.723" cy="55.769" r="5.977"/>
<circle cx="312.426" cy="55.769" r="5.977"/>
</svg>
<svg id="link-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 162.656 162.656" xml:space="preserve">
<path d="M151.764 10.894c-14.522-14.522-38.152-14.525-52.676-.008l.003.003-22.979 22.983 10.607 10.605 22.983-22.988-.002-.002c8.678-8.663 22.785-8.658 31.457.014 8.673 8.672 8.672 22.786 0 31.461l-34.486 34.484a22.095 22.095 0 0 1-15.729 6.516 22.098 22.098 0 0 1-15.73-6.516L64.605 98.052c7.035 7.035 16.389 10.91 26.338 10.91 9.949 0 19.303-3.875 26.335-10.91l34.487-34.484c14.519-14.525 14.519-38.155-.001-52.674z"/><path d="M52.96 141.162c-8.675 8.67-22.788 8.668-31.461-.005-8.673-8.675-8.673-22.791-.001-31.465L55.98 75.21c8.675-8.674 22.789-8.674 31.462 0L98.05 64.604c-14.524-14.523-38.154-14.524-52.676 0L10.89 99.086c-14.519 14.523-14.519 38.154.001 52.678 7.263 7.262 16.801 10.893 26.341 10.892 9.536 0 19.074-3.629 26.333-10.887l.002-.001 22.984-22.99-10.608-10.606-22.983 22.99z"/>
</svg>
`);
}

View File

@ -8,8 +8,15 @@ customElements.define('local-business-item', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
:host {
display: block;
}
* {
font-family: 'Cardo', sans-serif;
--img-width: 128px;
--price-width: 160px;
--name-width: calc(100% - var(--price-width) - var(--img-width) - 20px);
}
:host([picture-url = '']) #img {
display: none;
}
@ -17,40 +24,47 @@ customElements.define('local-business-item', class extends Component {
display: grid;
grid-template-areas: 'img name' 'img price';
}
h3 {
h3#name {
font-weight: normal;
grid-area: name;
line-height: 1;
margin: 0;
text-align: right;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 16px;
width: var(--name-width);
}
#price {
grid-area: price; text-align: right;
grid-area: price;
text-align: right;
width: var(--price-width);
}
#img {
width: 128px;
max-width: 128px;
width: var(--img-width);
max-width: var(--img-width);
grid-area: img;
border-radius: 6px;
}
@media (min-width: 1200px) {
@media (min-width: 1000px) {
#item {
display: flex;
justify-content: space-between;
}
h3 {
h3#name {
font-weight: normal;
line-height: 1.6;
width: calc(100% - 450px);
width: var(--name-width);
text-align: left;
}
#price {
font-weight: bold;
width: 180px;
width: var(--price-width);
}
#img {
width: 128px;
width: var(--img-width);
max-width: 128px;
}
}

View File

@ -2,7 +2,7 @@ import { Component } from "../shared";
customElements.define('local-business', class extends Component {
static get observedAttributes() {
return ['name', 'service-id', 'state']
return ['name', 'business-id', 'state']
}
constructor() {
@ -42,7 +42,7 @@ customElements.define('local-business', class extends Component {
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('#name').textContent = v;
this.#setNameHeader();
}
get state() {
@ -53,12 +53,13 @@ customElements.define('local-business', class extends Component {
this.setAttribute('state', v);
}
get service_id() {
return this.getAttribute('service-id');
get business_id() {
return this.getAttribute('business-id');
}
set service_id(v) {
this.setAttribute('service-id', v);
set business_id(v) {
this.setAttribute('business-id', v);
this.#setNameHeader();
}
get description() {
@ -70,4 +71,51 @@ customElements.define('local-business', class extends Component {
this.description.any(s => s.match(regex)) ||
Array.from(this.querySelectorAll('local-business-item')).any(el => el.matches(regex));
}
#setNameHeader() {
this.shadowRoot.querySelector('#name').innerHTML = `<a href="/local-businesses/${ this.business_id }">${ this.name }</a>`;
}
});
customElements.define('business-description', class extends Component {
static get observedAttributes() {
return ['truncate'];
}
constructor() {
super(`<style>:host{display:block;}</style><p><article></article>`);
}
connectedCallback() {
super.connectedCallback();
this.#renderContent();
}
get truncate() {
const v = parseInt(this.getAttribute('truncate'));
return isNaN(v) ? 0 : v;
}
set truncate(v) {
if (v === false || v === 'false' || v == undefined) {
this.removeAttribute('truncate');
} else {
this.setAttribute('truncate', v);
}
this.#renderContent();
}
#renderContent() {
let text = this.textContent;
const max = this.truncate;
const view = this.shadowRoot.querySelector('article');
const tail = text.length > max ? '...' : '';
if (max > 0) text = text.substring(0, max);
view.innerHTML = text
.trim()
.split("\n")
.filter(s => s && s.length)
.map((s, idx, a) => `<p>${ idx + 1 === a.length ? `${s}${tail}` : s }</p>`)
.join('')
}
});

View File

@ -27,14 +27,35 @@ customElements.define('local-business-list', class extends Component {
article {
margin: 8px;
}
#items {
display: block;
padding: 8px;
}
::slotted(local-business) {
margin-bottom: 20px;
}
#search {
margin-bottom: 16px;;
margin-top: 16px;;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
}
#items {
display: flex;
justify-content: space-between;
flex-wrap: wrap;
justify-items: stretch;
}
::slotted(local-business) {
width: calc(50% - 40px);
margin: 0 20px 20px;
}
}
</style>
<article>
<section>
<section id="search">
<search-input target="local-business"></search-input>
</section>
<section id="items">

View File

@ -0,0 +1,7 @@
import { Component } from '../shared.js';
customElements.define('single-local-business', class extends Component {
constructor() {
super(`<style>:host{display:block;margin:0 8px;}@media only screen and (min-device-width: 100px){:host{margin:0;}}</style><article><slot></slot></article>`);
}
});

View File

@ -20,6 +20,9 @@ customElements.define('marketplace-offer', class extends Component {
section {
margin-bottom: 16px;
text-decoration: none;
color: black;
font-style: normal;
}
#preview {
@ -37,8 +40,7 @@ customElements.define('marketplace-offer', class extends Component {
}
:host([price-range-max="0"]) #sep, :host([price-range]) #sep,
:host([price-range-max="0"]) #price-min, :host([price-range]) #price-min
{
:host([price-range-max="0"]) #price-min, :host([price-range]) #price-min {
display: none;
}
@ -87,7 +89,7 @@ customElements.define('marketplace-offer', class extends Component {
justify-content: end;
}
@media only screen and (min-device-width: 1200px) {
@media only screen and (min-device-width: 1000px) {
#details {
display: grid;
column-gap: 16px;
@ -125,24 +127,32 @@ customElements.define('marketplace-offer', class extends Component {
width: 100px;
text-align: right;
}
a {
font-style: normal;
text-decoration: none;
color: black;
}
}
${ INPUT_STYLE }
</style>
<section id="details">
<div id="preview">
<image-popup src="" id="picture">
</image-popup>
</div>
<p id="description"></p>
<span id="price-min"></span>
<span id="sep">-</span>
<span id="price-max"></span>
</section>
<section id="contacts">
<span style="margin-right: 10px; font-weight: bold">Kontakt:</span>
<slot name="contacts"></slot>
</section>
<article>
<section id="details">
<div id="preview">
<image-popup src="" id="picture">
</image-popup>
</div>
<p id="description"></p>
<span id="price-min"></span>
<span id="sep">-</span>
<span id="price-max"></span>
</section>
<section id="contacts">
<span style="margin-right: 10px; font-weight: bold">Kontakt:</span>
<slot name="contacts"></slot>
</section>
</article>
`);
this.#price_range = new PriceRange(0, 0);
}

View File

@ -26,12 +26,19 @@ customElements.define('marketplace-offers', class extends Component {
:host([account-id]) #publishSection {
display: block;
}
::slotted(marketplace-offer), ::slotted(user-edit-offer) {
::slotted(a), ::slotted(marketplace-offer), ::slotted(user-edit-offer) {
margin-bottom: 20px;
text-decoration: none;
color: black;
font-style: normal;
}
::slotted([search-visible='invisible']) {
display: none;
}
#search {
margin-bottom: 16px;
margin-top: 16px;
}
@media only screen and (min-device-width: 1000px) {
#offers {
display: flex;
@ -39,9 +46,12 @@ customElements.define('marketplace-offers', class extends Component {
flex-wrap: wrap;
justify-items: stretch;
}
::slotted(marketplace-offer), ::slotted(user-edit-offer) {
::slotted(a), ::slotted(marketplace-offer), ::slotted(user-edit-offer) {
width: calc(33% - 40px);
margin: 0 20px 20px;
text-decoration: none;
color: black;
font-style: normal;
}
}
${ BUTTON_STYLE }
@ -51,7 +61,7 @@ customElements.define('marketplace-offers', class extends Component {
<button id="publish" class="btn">Dodaj ogłoszenie</button>
</section>
<section><slot></slot></section>
<section>
<section id="search">
<search-input target="user-edit-offer, marketplace-offer"></search-input>
</section>
<section id="offers">

View File

@ -2,7 +2,7 @@ import { Component, FORM_STYLE, TIP_STYLE } from "../shared.js";
customElements.define('offer-form', class extends Component {
static get observedAttributes() {
return ['offer-id', 'description', 'picture-url', 'price-range-min', 'price-range-max'];
return ['offer-id', 'description', 'picture-url', 'price-range-min', 'price-range-max', 'free'];
}
constructor() {
@ -16,6 +16,9 @@ customElements.define('offer-form', class extends Component {
section {
padding: 8px;
}
:host([free]) #priceMinSection {
display: none;
}
@media only screen and (min-device-width: 1000px) {
section > form {
display: flex;
@ -52,17 +55,15 @@ customElements.define('offer-form', class extends Component {
></textarea>
</div>
<div id="priceSection">
<div>
<label>Cena minimalna</label>
<div id="priceMinSection">
<label>Cena</label>
<price-input id="priceMinUI" value="0"></price-input>
<input name="price_min" id="priceMin" type="hidden" value="0" />
<span class="tip">Jeżeli cena minimalna i maksymalna wynoszą 0 produkt będzie dostępny za darmo</span>
</div>
<div>
<label>Cena maksymalna</label>
<price-input id="priceMaxUI" value="0"></price-input>
<label>Za darmo</label>
<input type="checkbox" id="free" />
<input name="price_max" id="priceMax" type="hidden" value="0" />
<span class="tip">Pozostaw 0, żeby cena minimalna była jedyną dostępną ceną</span>
</div>
</div>
<div>
@ -80,13 +81,22 @@ customElements.define('offer-form', class extends Component {
this.shadowRoot.querySelector('#priceMin').value = ev.target.value;
});
this.shadowRoot.querySelector('#priceMaxUI').addEventListener('change', ev => {
ev.stopPropagation();
this.shadowRoot.querySelector('#priceMax').value = ev.target.value;
});
// this.shadowRoot.querySelector('#priceMaxUI').addEventListener('change', ev => {
// ev.stopPropagation();
// this.shadowRoot.querySelector('#priceMax').value = ev.target.value;
// });
this.addEventListener('image-input:uploaded', ev => {
this.picture_url = ev.detail;
});
this.shadowRoot.querySelector('#free').addEventListener('change', ev => {
ev.preventDefault();
ev.stopPropagation();
this.free = ev.target.checked;
if (this.free) {
this.price_range_min = 0;
this.price_range_max = 0;
}
});
}
get offer_id() {
@ -120,7 +130,7 @@ customElements.define('offer-form', class extends Component {
set price_range_max(v) {
this.setAttribute('price-range-max', v);
this.shadowRoot.querySelector('#priceMaxUI').value = v;
// this.shadowRoot.querySelector('#priceMaxUI').value = v;
this.shadowRoot.querySelector('#priceMax').value = v;
}
@ -143,11 +153,25 @@ customElements.define('offer-form', class extends Component {
this.shadowRoot.querySelector('image-input').url = v;
}
get free() {
return this.getAttribute('free') === 'true';
}
set free(v) {
if (v === true || v === 'true') {
this.setAttribute('free', 'true');
this.shadowRoot.querySelector('#free').checked = true;
} else {
this.removeAttribute('free');
this.shadowRoot.querySelector('#free').checked = false;
}
}
#removeId() {
const form = this.shadowRoot.querySelector('form');
const input = form.querySelector('#offer-id');
input && input.remove();
form.action = '/marketplace/create';
form.action = '/offers/create';
form.querySelector('input[type=submit]').value = 'Utwórz';
}
@ -159,7 +183,7 @@ customElements.define('offer-form', class extends Component {
input.setAttribute('name', 'id');
input.setAttribute('id', 'offer-id');
input.value = v;
form.action = '/marketplace/update';
form.action = '/offers/update';
form.querySelector('input[type=submit]').value = 'Zmień';
}
});

View File

@ -15,6 +15,11 @@ customElements.define('user-edit-offer', class extends Component {
:host([mode='view']) #view { display: block; }
:host([mode='form']) #form { display: block; }
:host([state='Finished']) #finishForm { display: none; }
section, a, ::slotted(a) {
text-decoration: none;
color: black;
font-style: normal;
}
#actions {
display: flex;
justify-content: space-between;

View File

@ -18,12 +18,15 @@ export class RegisterFormComponent extends PseudoForm {
ev.preventDefault();
ev.stopPropagation();
if (form.reportValidity()) {
this.shadowRoot.querySelector('form-navigation').next();
}
this.shadowRoot.querySelector('form-navigation').next();
});
this.addEventListener('form:next', () => {
dispatchForm()
this.addEventListener('form:next', ev => {
if (form.reportValidity()) {
dispatchForm()
} else {
ev.stopPropagation();
ev.preventDefault();
}
});
}

View File

@ -28,7 +28,7 @@ customElements.define('register-business-account-form', class extends RegisterFo
<input id="password" name="pass" placeholder="Hasło" type="password" required />
</div>
<input type="submit" style="display: none">
<form-navigation></form-navigation>
<form-navigation prev="hidden" next="right"></form-navigation>
</form>
`);

View File

@ -24,6 +24,10 @@ customElements.define('register-business-contacts-form', class extends RegisterF
</style>
<article>
<h2>Edycja listy danych kontaktowych</h2>
<p>
Adres e-mail podany w formularzu będzie domyślnym adresem kontaktowym.
Tutaj możesz dodać dodatkowe sposoby kontaktu takie jak numer telefonu lub link do facebook'a.
</p>
<form>
<section id="form">
<contact-info-editor

View File

@ -88,7 +88,7 @@ customElements.define('register-business-submit-form', class extends PseudoForm
const { error } = await api.register(this.#form);
console.info(error);
if (error) {
if (!error) {
// Router.goTo("/account?success");
location.href = '/account?success';
} else {

View File

@ -22,6 +22,9 @@ customElements.define('form-navigation', class extends Component {
width: calc(50% - 16px);
max-width: 200px;
}
:host([next=right]) .actions {
justify-content: end;
}
@media only screen and (min-device-width: 1000px) {
form > .actions > input {
width: auto;
@ -54,11 +57,11 @@ customElements.define('form-navigation', class extends Component {
if (oldV === newV) return;
switch (name) {
case 'next': {
this.shadowRoot.querySelector('#next').className = newV === 'hidden' ? 'hidden' : '';
this.shadowRoot.querySelector('#next').className = newV;
break;
}
case 'prev': {
this.shadowRoot.querySelector('#prev').className = newV === 'hidden' ? 'hidden' : '';
this.shadowRoot.querySelector('#prev').className = newV;
break;
}
}

View File

@ -25,8 +25,25 @@ customElements.define('search-input', class extends Component {
border-bottom: 1px solid #ccc;
text-indent: 20px;
}
svg { height: 24px; }
section {
display: flex;
justify-content: start;
align-content: center;
align-items: center;
}
</style>
<section>
<svg
viewBox="0 0 310.42 310.42"
xml:space="preserve"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
>
<path
d="M273.587 214.965c49.11-49.111 49.11-129.021 0-178.132s-129.021-49.111-178.131 0C53.792 78.497 47.482 140.462 76.509 188.85c0 0 2.085 3.496-.731 6.312l-64.263 64.263c-12.791 12.79-15.837 30.675-4.493 42.02l1.953 1.951c11.343 11.345 29.229 8.301 42.019-4.49l64.128-64.128c2.951-2.951 6.448-.866 6.448-.866 48.387 29.026 110.353 22.717 152.017-18.947zM118.71 191.71c-36.288-36.288-36.287-95.332 0-131.62 36.288-36.287 95.333-36.288 131.62 0 36.288 36.287 36.288 95.332 0 131.62-36.287 36.287-95.331 36.287-131.62 0z"
/>
</svg>
<input type="text" id="filter" placeholder="Znajdź (wyrażenia regularne są wspierane)" />
</section>
`);
@ -52,10 +69,8 @@ customElements.define('search-input', class extends Component {
super.connectedCallback();
let node = this;
while (node) {
console.warn(node)
if (node == null) return console.warn('no parent node', node);
if (node instanceof ShadowRoot) {
console.warn('node is shadow')
this.#host = node.host;
break;
}

View File

@ -1,7 +1,7 @@
{% extends "../layout.html" %}
{% block content %}
<ow-admin>
<ow-offers>
<ow-admin-offers>
<h1>Admin - Sprzedaż niepotrzebnych rzeczy</h1>
{% for offer in offers %}
@ -27,6 +27,6 @@
></marketplace-offer>
</admin-edit-offer>
{% endfor %}
</ow-offers>
</ow-admin-offers>
</ow-admin>
{% endblock %}

View File

@ -4,14 +4,12 @@
<local-business-list {{h.account_id_tag(account)}}>
{% for business in businesses %}
<local-business
slot="business"
business-id="{{business.id}}"
name="{{business.name}}"
state="{{business.state.as_str()}}"
slot="business"
business-id="{{business.id}}"
name="{{business.name}}"
state="{{business.state.as_str()}}"
>
{% for line in business.description.lines() %}
<p slot="description">{{line}}</p>
{% endfor %}
<business-description slot="description" truncate="100">{{business.description}}</business-description>
{% for item in business.items %}
<local-business-item
@ -28,7 +26,7 @@
mode="icon"
contact-id="{{contact.id}}"
content="{{h.render_contact(contact.contact_type.as_str(), contact.content.as_str())}}"
contact-type="{{contact.contact_type}}"
type="{{contact.contact_type}}"
></contact-info>
{% endfor %}
</contact-info-list>

View File

@ -0,0 +1,34 @@
{% extends "../base.html" %}
{% block content %}
<single-local-business {{h.account_id_tag(account)}}>
<local-business
business-id="{{business.id}}"
name="{{business.name}}"
state="{{business.state.as_str()}}"
>
{% for line in business.description.lines() %}
<p slot="description">{{line}}</p>
{% endfor %}
{% for item in business.items %}
<local-business-item
slot="item"
name="{{item.name}}"
price="{{item.price}}"
picture-url="{{item.picture_url}}"
>
</local-business-item>
{% endfor %}
<contact-info-list slot="contacts">
{% for contact in business.contacts %}
<contact-info
mode="icon"
contact-id="{{contact.id}}"
content="{{h.render_contact(contact.contact_type.as_str(), contact.content.as_str())}}"
type="{{contact.contact_type}}"
></contact-info>
{% endfor %}
</contact-info-list>
</local-business>
</single-local-business>
{% endblock %}

View File

@ -1,8 +1,9 @@
{% extends "../base.html" %}
{% block content %}
<marketplace-offers {{h.account_id_tag(account)}}>
<article {{h.account_id_tag(account)}}>
<h2>Edycja oferty</h2>
<offer-form
{{h.account_id_tag(account)}}
state="{{offer.state.as_str()}}"
offer-id="{{offer.id}}"
description="{{offer.description}}"
@ -11,6 +12,7 @@
{% when PriceRange::Free %}
price-range-min="0"
price-range-max="0"
free="true"
{% when PriceRange::Fixed with { value } %}
price-range-min="{{value}}"
price-range-max="0"
@ -19,5 +21,5 @@
price-range-max="{{max}}"
{% endmatch %}
></offer-form>
</marketplace-offers>
</article>
{% endblock %}

View File

@ -22,17 +22,50 @@
price-range-max="{{max}}"
{% endmatch %}
>
<a href="/marketplace/{{offer.id}}">
<marketplace-offer
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"
{% match offer.price_range %}
{% when PriceRange::Free %}
price-range-min="0"
price-range-max="0"
{% when PriceRange::Fixed with { value } %}
price-range-min="{{value}}"
price-range-max="0"
{% when PriceRange::Range with { min, max } %}
price-range-min="{{min}}"
price-range-max="{{max}}"
{% endmatch %}
>
<contact-info-list slot="contacts">
{% for contact in offer.contacts %}
<contact-info
mode="icon"
contact-id="{{contact.id}}"
type="{{contact.contact_type}}"
content="{{h.render_contact(contact.contact_type.as_str(), contact.content.as_str())}}"
></contact-info>
{% endfor %}
</contact-info-list>
</marketplace-offer>
</a>
</user-edit-offer>
{% endfor %}
{% for offer in offers %}
<a href="/marketplace/{{offer.id}}" slot="offer">
<marketplace-offer
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"
{% match offer.price_range %}
{% when PriceRange::Free %}
price-range-min="0"
price-range-max="0"
price-range="free"
{% when PriceRange::Fixed with { value } %}
price-range-min="{{value}}"
price-range-max="0"
price-range="{{value}}"
{% when PriceRange::Range with { min, max } %}
price-range-min="{{min}}"
price-range-max="{{max}}"
@ -49,36 +82,7 @@
{% endfor %}
</contact-info-list>
</marketplace-offer>
</user-edit-offer>
{% endfor %}
{% for offer in offers %}
<marketplace-offer
slot="offer"
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"
{% match offer.price_range %}
{% when PriceRange::Free %}
price-range="free"
{% when PriceRange::Fixed with { value } %}
price-range="{{value}}"
{% when PriceRange::Range with { min, max } %}
price-range-min="{{min}}"
price-range-max="{{max}}"
{% endmatch %}
>
<contact-info-list slot="contacts">
{% for contact in offer.contacts %}
<contact-info
mode="icon"
contact-id="{{contact.id}}"
type="{{contact.contact_type}}"
content="{{h.render_contact(contact.contact_type.as_str(), contact.content.as_str())}}"
></contact-info>
{% endfor %}
</contact-info-list>
</marketplace-offer>
</a>
{% endfor %}
</marketplace-offers>
{% endblock %}

View File

@ -0,0 +1,31 @@
{% extends "../base.html" %}
{% block content %}
<marketplace-offer
{{h.account_id_tag(account)}}
offer-id="{{offer.id}}"
description="{{offer.description}}"
picture-url="{{offer.picture_url}}"
{% match offer.price_range %}
{% when PriceRange::Free %}
price-range-min="0"
price-range-max="0"
{% when PriceRange::Fixed with { value } %}
price-range-min="{{value}}"
price-range-max="0"
{% when PriceRange::Range with { min, max } %}
price-range-min="{{min}}"
price-range-max="{{max}}"
{% endmatch %}
>
<contact-info-list slot="contacts">
{% for contact in offer.contacts %}
<contact-info
mode="icon"
contact-id="{{contact.id}}"
type="{{contact.contact_type}}"
content="{{h.render_contact(contact.contact_type.as_str(), contact.content.as_str())}}"
></contact-info>
{% endfor %}
</contact-info-list>
</marketplace-offer>
{% endblock %}

View File

@ -22,20 +22,20 @@ pub enum AccountType {
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, Type)]
pub enum LocalBusinessState {
#[default]
Pending,
Approved,
Banned,
Pinned,
Internal,
Pending = 1,
Approved = 2,
Banned = 3,
Pinned = 4,
Internal = 5,
}
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize, Type)]
#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize, Type)]
pub enum OfferState {
#[default]
Pending,
Approved,
Banned,
Finished,
Pending = 0,
Approved = 1,
Banned = 2,
Finished = 3,
}
impl OfferState {
@ -288,6 +288,7 @@ pub struct Offer {
#[derive(Debug)]
pub struct CreateLocalBusinessItemInput {
pub account_id: i32,
pub local_business_id: i32,
pub name: String,
pub price: i64,

View File

@ -22,6 +22,7 @@ pub enum Page {
AdminOffers,
Terms,
Privacy,
Business,
}
impl Page {
@ -106,7 +107,7 @@ pub struct SetStateBusinessInput {
pub state: db::LocalBusinessState,
}
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct LocalBusiness {
pub id: i32,
pub owner_id: i32,

View File

@ -88,9 +88,9 @@ pub async fn visible_businesses(t: &mut T<'_>) -> Result<Vec<db::LocalBusiness>>
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
WHERE state != 'Banned'
WHERE state != 'Banned' AND state != 'Pending'
GROUP BY id, state
ORDER BY id DESC
ORDER BY name ASC
"#,
)
.fetch_all(t)
@ -126,6 +126,25 @@ ORDER BY id DESC
})
}
#[tracing::instrument]
pub async fn business_by_id(t: &mut T<'_>, id: i32) -> Result<db::LocalBusiness> {
sqlx::query_as(
r#"
SELECT id, owner_id, name, description, state
FROM local_businesses
WHERE state != 'Banned' AND id = $1
"#,
)
.bind(id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Business { id }
})
}
#[tracing::instrument]
pub async fn update_business(
t: &mut T<'_>,

View File

@ -14,6 +14,7 @@ SELECT
content
FROM
contacts
ORDER BY contact_type, id ASC
"#,
)
.fetch_all(t)

View File

@ -122,8 +122,92 @@ pub async fn move_item(
item_id: i32,
item_order: i32,
) -> Result<db::LocalBusinessItem> {
let mut current = item_by_id(t, account_id, item_id).await?;
dbg!(item_id, item_order);
let _idx = reorder_items(t, account_id).await;
let mut current = item_by_id(t, account_id, item_id).await?;
dbg!(&current);
match item_order.cmp(&current.item_order) {
Ordering::Less => {
if let Some(prev_idx) = current.item_order.checked_sub(1) {
dbg!(prev_idx);
let prev = find_by_item_order(t, account_id, prev_idx).await?;
dbg!(
"Less and found",
current.id,
current.item_order,
prev.id,
prev.item_order,
);
dbg!(update_item_order(&mut *t, current.id, prev.item_order).await?);
dbg!(update_item_order(&mut *t, prev.id, current.item_order).await?);
} else {
dbg!("Less and not found, skipping...");
}
}
Ordering::Equal => {
dbg!("Equal, skipping...");
}
Ordering::Greater => {
if let Some(next_idx) = current.item_order.checked_add(1) {
dbg!(next_idx);
let next = find_by_item_order(t, account_id, next_idx).await?;
dbg!(
"Greater and found",
current.id,
current.item_order,
next.id,
next.item_order,
);
dbg!(update_item_order(&mut *t, current.id, next.item_order).await?);
dbg!(update_item_order(&mut *t, next.id, current.item_order).await?);
} else {
dbg!("Greater and not found, skipping...");
}
}
};
current.item_order = item_order;
Ok(current)
}
#[tracing::instrument]
async fn find_by_item_order(
t: &mut T<'_>,
account_id: i32,
item_order: i32,
) -> Result<db::LocalBusinessItem> {
sqlx::query_as(
r#"
SELECT
local_business_items.id,
local_business_items.local_business_id,
local_business_items.name,
local_business_items.price,
local_business_items.item_order,
local_business_items.picture_url
FROM local_business_items
INNER JOIN local_businesses
ON local_businesses.id = local_business_items.local_business_id
WHERE local_business_items.item_order = $1 AND owner_id = $2
ORDER BY item_order ASC
"#,
)
.bind(item_order)
.bind(account_id)
.fetch_one(t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::AllItems
})
}
#[tracing::instrument]
async fn reorder_items(t: &mut T<'_>, account_id: i32) -> i32 {
let all: Vec<db::LocalBusinessItem> = sqlx::query_as(
r#"
SELECT
@ -143,63 +227,28 @@ ORDER BY item_order ASC
.bind(account_id)
.fetch_all(&mut *t)
.await
.map_err(|e| {
error!("{e}");
dbg!(e);
Error::Item { item_id }
})?;
.unwrap_or_default();
let idx = all
.iter()
.position(|p| p.id == item_id)
.ok_or(Error::Item { item_id })?;
dbg!(idx);
match item_order.cmp(&current.item_order) {
Ordering::Less => {
if let Some(prev) = idx.checked_sub(1).and_then(|prev_idx| {
dbg!(prev_idx);
all.get(prev_idx)
}) {
dbg!(
"Less and found",
current.id,
current.item_order,
prev.id,
prev.item_order,
);
dbg!(update_item_order(&mut *t, current.id, prev.item_order).await?);
dbg!(update_item_order(&mut *t, prev.id, current.item_order).await?);
} else {
dbg!("Less and not found, skipping...");
}
if all.is_empty() {
return 0;
}
let mut sql = String::from(
r#"
UPDATE local_business_items AS a
SET item_order = b.item_order
FROM (VALUES "#,
);
all.iter().enumerate().fold(&mut sql, |memo, (idx, row)| {
if idx != 0 {
memo.push_str(", ");
}
Ordering::Equal => {
dbg!("Equal, skipping...");
}
Ordering::Greater => {
if let Some(next) = idx.checked_add(1).and_then(|next_idx| {
dbg!(next_idx);
all.get(next_idx)
}) {
dbg!(
"Greater and found",
current.id,
current.item_order,
next.id,
next.item_order,
);
dbg!(update_item_order(&mut *t, current.id, next.item_order).await?);
dbg!(update_item_order(&mut *t, next.id, current.item_order).await?);
} else {
dbg!("Greater and not found, skipping...");
}
}
};
current.item_order = item_order;
Ok(current)
memo.push_str(format!("({}, {})", row.id, idx).as_str());
memo
});
sql.push_str(") AS b (id, item_order) WHERE a.id = b.id");
dbg!(&sql);
sqlx::query(&sql).execute(&mut *t).await.ok();
all.len() as i32
}
#[tracing::instrument]
@ -284,6 +333,7 @@ pub async fn create_item(
t: &mut T<'_>,
input: db::CreateLocalBusinessItemInput,
) -> Result<db::LocalBusinessItem> {
let item_order = reorder_items(t, input.account_id).await;
sqlx::query_as(
r#"
INSERT INTO local_business_items (local_business_id, name, price, picture_url, item_order)
@ -307,7 +357,7 @@ RETURNING
.map(String::from)
.unwrap_or_else(|| format!("--{}", uuid::Uuid::new_v4())),
)
.bind(input.item_order)
.bind(item_order)
.fetch_one(t)
.await
.map_err(|e| {

View File

@ -52,6 +52,9 @@ pub enum Error {
OwnedBusiness {
account_id: i32,
},
Business {
id: i32,
},
AccountByEmail {
email: String,
},

View File

@ -57,9 +57,14 @@ ORDER BY id DESC
}
#[tracing::instrument]
pub async fn offer_by_id(t: &mut T<'_>, account_id: i32, offer_id: i32) -> Result<db::Offer> {
sqlx::query_as(
r#"
pub async fn offer_by_id(
t: &mut T<'_>,
account_id: Option<i32>,
offer_id: i32,
) -> Result<db::Offer> {
match account_id {
Some(account_id) => sqlx::query_as(
r#"
SELECT
id,
owner_id,
@ -72,9 +77,26 @@ FROM offers
WHERE owner_id = $1 AND id = $2
LIMIT 1
"#,
)
.bind(account_id)
.bind(offer_id)
)
.bind(account_id)
.bind(offer_id),
None => sqlx::query_as(
r#"
SELECT
id,
owner_id,
price_range,
description,
picture_url,
state,
created_at
FROM offers
WHERE id = $1
LIMIT 1
"#,
)
.bind(offer_id),
}
.fetch_one(t)
.await
.map_err(|e| {

View File

@ -314,9 +314,15 @@ impl Responder for HttpResult {
let status_code = self.status_code();
match self {
HttpResult::Json { body, code } => HttpResponse::build(code)
.append_header(("Sec-GPC-field-name", "Sec-GPC"))
.append_header(("Sec-GPC-field-value", "1"))
.append_header(("Sec-GPC", "1"))
.content_type("application/json")
.body(body),
HttpResult::Html { body, code } => HttpResponse::build(code)
.append_header(("Sec-GPC-field-name", "Sec-GPC"))
.append_header(("Sec-GPC-field-value", "1"))
.append_header(("Sec-GPC", "1"))
.content_type("text/html")
.body(body),
HttpResult::Err {

View File

@ -28,7 +28,8 @@ async fn admin_offers(req: HttpRequest, db: Data<PgPool>, id: Identity) -> HttpR
let mut t = ok_or_internal!(&req, pool.begin().await);
let account = require_admin!(&req, &mut t, id);
let offers = queries::all_offers(&mut t).await.unwrap_or_default();
let mut offers = queries::all_offers(&mut t).await.unwrap_or_default();
offers.sort_by(|a, b| (a.state as u8).cmp(&(b.state as u8)));
t.commit().await.ok();

View File

@ -155,6 +155,7 @@ async fn new_business_item(
if let Err(e) = queries::create_item(
&mut t,
db::CreateLocalBusinessItemInput {
account_id: account.id,
local_business_id: business.id,
name: form.name,
price: form.price,

View File

@ -37,6 +37,13 @@ pub async fn render_index() -> HttpResponse {
)
}
#[get("/.well-known/gpc.json")]
async fn gpc() -> HttpResponse {
HttpResponse::Ok()
.content_type("application/json")
.body("{\"gpc\":true,\"lastUpdate\":\"1997-03-10\"}")
}
#[get("/")]
async fn index(req: HttpRequest) -> HttpResult {
HttpResult::goto(&req, "/marketplace")
@ -62,5 +69,6 @@ pub fn configure(config: &mut ServiceConfig) {
.prefer_utf8(true)
.show_files_listing(),
)
.service(index);
.service(index)
.service(gpc);
}

View File

@ -417,6 +417,7 @@ async fn save_account_details(
let res = queries::create_item(
&mut t,
db::CreateLocalBusinessItemInput {
account_id: account.id,
local_business_id: business.id,
name: item.name,
price: item.price as i64,

View File

@ -1,15 +1,18 @@
use actix_http::StatusCode;
use actix_web::web::{Data, ServiceConfig};
use actix_web::{get, HttpRequest};
use actix_web::{get, web, HttpRequest};
use askama::Template;
use serde::Serialize;
use sqlx::PgPool;
use crate::model::view;
use crate::model::view::Page;
use crate::model::{db, view};
use crate::queries;
use crate::routes::unrestricted::IndexTemplate;
use crate::routes::{HttpResult, Identity};
use crate::view::Helper;
#[get("/local-businesses")]
#[get("")]
#[tracing::instrument]
pub async fn businesses_page(req: HttpRequest, db: Data<PgPool>, id: Identity) -> HttpResult {
let pool = db.into_inner();
@ -51,6 +54,78 @@ pub async fn businesses_page(req: HttpRequest, db: Data<PgPool>, id: Identity) -
)
}
pub fn configure(config: &mut ServiceConfig) {
config.service(businesses_page);
#[derive(Default, Debug, Serialize, Template)]
#[template(path = "businesses/show.html")]
pub struct ShowTemplate {
business: view::LocalBusiness,
account: Option<db::Account>,
error: Option<String>,
page: Page,
h: Helper,
}
#[get("/{id}")]
#[tracing::instrument]
pub async fn show_business_page(
req: HttpRequest,
db: Data<PgPool>,
id: Identity,
path: web::Path<(i32,)>,
) -> HttpResult {
let business_id = path.into_inner().0;
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(&req, pool.begin().await);
let account = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await,
_ => None,
};
let (business, mut items, mut contacts) = {
use crate::model::db::{LocalBusiness, LocalBusinessItem};
let business: LocalBusiness = match queries::business_by_id(&mut t, business_id).await {
Ok(business) => business,
Err(e) => {
dbg!(e);
return HttpResult::res(
&req,
StatusCode::BAD_REQUEST,
ShowTemplate {
account,
page: Page::Business,
error: Some("Page not found".into()),
..Default::default()
},
);
}
};
let items: Vec<LocalBusinessItem> = queries::visible_business_items(&mut t)
.await
.unwrap_or_default();
let contacts = queries::all_contacts(&mut t).await.unwrap_or_default();
(business, items, contacts)
};
let business = view::LocalBusiness::from((business, &mut items, &mut contacts));
t.commit().await.ok();
HttpResult::res(
&req,
StatusCode::OK,
ShowTemplate {
business,
account,
page: Page::LocalBusinesses,
..Default::default()
},
)
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
web::scope("/local-businesses")
.service(show_business_page)
.service(businesses_page),
);
}

View File

@ -114,7 +114,7 @@ async fn edit_marketplace(
}
};
let offer = match queries::offer_by_id(&mut t, account.id, offer_id).await {
let offer = match queries::offer_by_id(&mut t, Some(account.id), offer_id).await {
Ok(offer) => offer,
Err(e) => {
dbg!(e);
@ -145,10 +145,83 @@ async fn edit_marketplace(
)
}
#[derive(Default, Serialize, Template)]
#[template(path = "./marketplace/show.html")]
struct ShowOfferTemplate {
account: Option<db::Account>,
error: Option<String>,
page: Page,
#[serde(skip)]
h: Helper,
offer: view::Offer,
}
#[get("/{id}")]
async fn show_marketplace(
req: HttpRequest,
db: Data<PgPool>,
id: Identity,
path: web::Path<(i32,)>,
) -> HttpResult {
let offer_id = path.into_inner().0;
let pool = db.into_inner();
let mut t = crate::ok_or_internal!(&req, pool.begin().await);
let account = match id.identity() {
Some(id) => queries::account_by_id(&mut t, id).await,
_ => None,
};
let account = match account {
Some(a) => a,
_ => {
return HttpResult::res(
&req,
StatusCode::NOT_FOUND,
ShowOfferTemplate {
page: Page::Marketplace,
account,
error: Some("Oferta nie istnieje".into()),
..Default::default()
},
)
}
};
let offer = match queries::offer_by_id(&mut t, None, offer_id).await {
Ok(offer) => offer,
Err(e) => {
dbg!(e);
return HttpResult::res(
&req,
StatusCode::NOT_FOUND,
ShowOfferTemplate {
page: Page::Marketplace,
account: Some(account),
error: Some("Oferta nie istnieje".into()),
..Default::default()
},
);
}
};
t.commit().await.ok();
HttpResult::res(
&req,
StatusCode::OK,
ShowOfferTemplate {
page: Page::Marketplace,
account: Some(account),
offer: view::Offer::from((offer, &vec![])),
..Default::default()
},
)
}
pub fn configure(config: &mut ServiceConfig) {
config.service(
web::scope("/marketplace")
.service(edit_marketplace)
.service(show_marketplace)
.service(marketplace),
);
}

View File

@ -3,41 +3,5 @@ name = "web"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
name = "web"
path = "src/lib.rs"
[dependencies]
[dependencies.serde]
version = "1.0.140"
features = ['derive']
[dependencies.stdweb]
version = "0.4.20"
features = []
[dependencies.stdweb-derive]
version = "0.5.3"
features = []
[dependencies.stdweb-logger]
version = "0.1.1"
features = []
#[dependencies.wasm-bindgen]
#version = "0.2.81"
#features = []
#
#[dependencies.web-sys]
#version = "0.3.58"
#features = ['HtmlElement']
#
#[dependencies.js-sys]
#version = "0.3.58"
#features = []
[dependencies.wee_alloc]
version = "*"
features = ["static_array_backend"]
sycamore = "0.8.2"

8
web/Trunk.toml Normal file
View File

@ -0,0 +1,8 @@
[build]
target = "index.html"
dist = "dist"
[[hooks]]
stage = "build"
command = "zsh"
command_arguments = ["-c", "tailwindcss -i assets/tailwind.css -o $TRUNK_STAGING_DIR/tailwind.css"]

3
web/assets/tailwind.css Normal file
View File

@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

9
web/index.html Normal file
View File

@ -0,0 +1,9 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<meta charset="utf-8" />
<title>My first Sycamore app</title>
<link rel="stylesheet" href="/tailwind.css"/>
</head>
<body></body>
</html>

5
web/package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"tailwind": "^4.0.0"
}
}

1389
web/pnpm-lock.yaml Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,65 +0,0 @@
use wasm_bindgen::prelude::*;
use web_sys::HtmlElement;
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
pub trait Component {
fn tag_name() -> String;
fn created(&mut self) {}
fn connected_callback(&self) {}
fn disconnected_callback(&mut self) {}
fn attribute_changed(
&mut self,
_name: String,
_old_value: Option<String>,
_new_value: Option<String>,
) {
}
}
#[wasm_bindgen(extends = web_sys::HtmlElement, extends = web_sys::Object)]
pub struct FooElement {
el: HtmlElement,
}
#[wasm_bindgen]
impl FooElement {
#[wasm_bindgen]
pub fn tag_name() -> String {
"foo-element".into()
}
#[wasm_bindgen(constructor)]
pub fn new(el: HtmlElement) -> Self {
Self { el }
}
#[wasm_bindgen(js_name = connectedCallback)]
pub fn connected_callback(&self) {}
}
#[wasm_bindgen(extends = web_sys::HtmlElement, extends = web_sys::Object)]
pub struct BarElement {
el: HtmlElement,
}
#[wasm_bindgen]
impl BarElement {
#[wasm_bindgen]
pub fn tag_name() -> String {
"bar-element".into()
}
#[wasm_bindgen(constructor)]
pub fn new(el: HtmlElement) -> Self {
Self { el }
}
#[wasm_bindgen(js_name = connectedCallback)]
pub fn connected_callback(&self) {}
}

View File

@ -0,0 +1,63 @@
use sycamore::prelude::*;
#[component]
pub fn Card<G: Html>(cx: Scope) -> View<G> {
view! {
cx,
div(class = "px-4 relative w-full md:w-4/12") {
div(class = "mb-6 text-center shadow-lg rounded-lg relative flex flex-col bg-white p-6 w-full mb-6") {
div(class = "flex items-center justify-between pb-4 pt-2 border-b border-blueGray-300") {
div(class="float-left ml-1") {
i(class="opacity-75 inline mr-2 fas fa-shopping-basket undefined") {}
p(class="inline text-lg") { "Work in Progress" }
}
}
div(class = "py-6 flex-auto") {
div(class = "shadow-lg mt-6 rounded-full my-6 mx-auto w-100-px p-6 bg-white") {
img(class = "mx-auto", src = "data:image/png;base64,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") {}
}
h4(class = "text-2xl font-bold leading-tight mt-0 mb-2") { "Slack" }
p(class="text-blueGray-500 px-8") {"We are more than happy to work at such a great project." }
div(class = "flex justify-center mt-8 mb-2 text-blueGray-400") {
//
div(class = "flex items-center") {
a(class = "text-white bg-blueGray-500 inline-flex items-center justify-center shadow-lg rounded rounded-full relative border-2 border-white -ml-4 hover:z-1 w-10 h-10") {
img(class="rounded-full w-full", src="https://demos.creative-tim.com/notus-pro-react/static/media/team-1.26905a67.jpg") {}
div(class = "hidden") {
div(class = "border-0 mb-3 block z-50 font-normal leading-normal text-sm text-left no-underline break-words rounded") {
div(class = "py-1 px-2 text-center rounded text-white bg-black") { "Slack" }
}
}
}
a(class = "text-white bg-blueGray-500 inline-flex items-center justify-center shadow-lg rounded rounded-full relative border-2 border-white -ml-4 hover:z-1 w-10 h-10") {
img(class="rounded-full w-full", src="https://demos.creative-tim.com/notus-pro-react/static/media/face-2.33e80fee.jpg") {}
div(class = "hidden") {
div(class = "border-0 mb-3 block z-50 font-normal leading-normal text-sm text-left no-underline break-words rounded") {
div(class = "py-1 px-2 text-center rounded text-white bg-black") { "Slack" }
}
}
}
a(class = "text-white bg-blueGray-500 inline-flex items-center justify-center shadow-lg rounded rounded-full relative border-2 border-white -ml-4 hover:z-1 w-10 h-10") {
img(class="rounded-full w-full", src="https://demos.creative-tim.com/notus-pro-react/static/media/team-3.c5d0c11c.jpg") {}
div(class = "hidden") {
div(class = "border-0 mb-3 block z-50 font-normal leading-normal text-sm text-left no-underline break-words rounded") {
div(class = "py-1 px-2 text-center rounded text-white bg-black") { "Slack" }
}
}
}
a(class = "text-white bg-blueGray-500 inline-flex items-center justify-center shadow-lg rounded rounded-full relative border-2 border-white -ml-4 hover:z-1 w-10 h-10") {
img(class="rounded-full w-full", src="https://demos.creative-tim.com/notus-pro-react/static/media/team-4.639c2559.jpg") {}
div(class = "hidden") {
div(class = "border-0 mb-3 block z-50 font-normal leading-normal text-sm text-left no-underline break-words rounded") {
div(class = "py-1 px-2 text-center rounded text-white bg-black") { "Slack" }
}
}
}
}
small(class="pl-2 font-bold mb-1") {"and 30+ more"}
}
}
}
}
}
}

View File

@ -0,0 +1,3 @@
mod card;
pub use card::*;

View File

@ -1 +0,0 @@

20
web/src/main.rs Normal file
View File

@ -0,0 +1,20 @@
mod components;
use components::Card;
use sycamore::prelude::*;
fn main() {
sycamore::render(|cx| {
view! { cx,
div(class = "mb-12 flex flex-wrap -mx-4") {
Card {}
Card {}
Card {}
Card {}
Card {}
Card {}
Card {}
}
}
});
}

12
web/tailwind.config.js Normal file
View File

@ -0,0 +1,12 @@
module.exports = {
content: [
"./src/**/*.rs",
"./index.html",
"./src/**/*.html",
"./src/**/*.css",
],
theme: {},
variants: {},
plugins: [],
};