Compare commits

...

137 Commits

Author SHA1 Message Date
f14d27b93d Fix rent offers 2023-10-13 19:33:47 +02:00
de03009257 better display 2023-10-12 16:25:26 +02:00
bd61bee720 Create rent, display rent offers 2023-10-11 17:22:53 +02:00
9329c57970 Fix translations 2023-10-11 13:48:38 +02:00
d4135d425f Fix branch 2023-10-11 11:21:41 +02:00
1f18ba54d3 Rename 2023-10-11 11:20:50 +02:00
4799f92961 Insert defaul with 1 query 2023-10-11 11:20:24 +02:00
f27ed3e90e Rename 2023-10-11 11:20:24 +02:00
2489fed697 Display parking space info 2023-10-10 17:05:40 +02:00
ad976119af Display parking space info 2023-10-10 16:54:08 +02:00
34b31f6677 Display parking space info 2023-10-10 16:52:50 +02:00
4ed2eaa1ad Work on screen size and display rent form 2023-10-09 16:34:20 +02:00
0c60ec4b1c Insert defaul with 1 query 2023-10-08 07:20:11 +02:00
24751a3a17 Add rent request table 2023-09-27 18:17:38 +02:00
9158987469 Add rent request table 2023-09-27 18:17:27 +02:00
5e25d81308 Bump sea orm 2023-09-15 17:07:22 +02:00
1fc74988cf Back to oficial actix-admin 2023-09-14 17:12:41 +02:00
6a3378a6b5 Update own parking space 2023-09-14 14:25:29 +02:00
a5954e6966 Add edit 2023-09-13 18:05:17 +02:00
b9f94d0fd4 Start add tests 2023-09-12 18:03:42 +02:00
a7a02a227e render location 2023-09-05 16:09:26 +02:00
cacd56b31b Display own parking spaces 2023-09-04 15:02:53 +02:00
0b145bec5d Clear workspace 2023-09-04 12:59:23 +02:00
6d721886d0 Add docs.rs badge 2023-09-01 12:16:03 +02:00
96ddd2c51b Returns new pair after refresh session 2023-09-01 11:37:24 +02:00
0666c74068 Fix refresh 2023-09-01 09:01:00 +02:00
e10c903b52 Fix refresh 2023-09-01 08:58:06 +02:00
88fecb793c Documentation and tests 2023-09-01 06:52:11 +02:00
12c11c9a27 Documentation and tests 2023-09-01 06:51:06 +02:00
e32046ffec Documentation and tests 2023-09-01 06:46:48 +02:00
095bd30f62 Documentation and tests 2023-09-01 06:42:30 +02:00
f21040d507 Fix refresh 2023-08-31 12:58:12 +02:00
5f2bc7b5fa Fix refresh 2023-08-31 12:58:09 +02:00
aa10241d57 Test sign out, fix refresh token and access token 2023-08-30 22:53:12 +02:00
7fe4b285ce Improve JWT 2023-08-28 14:57:31 +02:00
7fa7fcda2b Add actix-jwt-session working docs 2023-08-24 22:16:40 +02:00
58a0239a05 Add refresh toke 2023-08-24 16:26:10 +02:00
877dcae1a9 Merge remote-tracking branch 'refs/remotes/origin/new-design' into new-design 2023-08-23 22:53:01 +02:00
38026a6f42 Improve session managment 2023-08-23 22:52:17 +02:00
2e688b7c49 Display locations 2023-08-23 16:27:32 +02:00
7a3471ec0a Display locations 2023-08-23 16:06:31 +02:00
4edccc7661 Display locations 2023-08-22 16:25:40 +02:00
5e806454f9 Add readme 2023-08-18 22:02:11 +02:00
e0525b3d82 Add readme 2023-08-18 22:02:07 +02:00
ecf3c3a344 Add more actix-jwt-session docs 2023-08-18 21:52:30 +02:00
2852f89895 Add actix-jwt-session working docs 2023-08-17 20:50:27 +02:00
6792a71820 Add documentation 2023-08-17 20:27:44 +02:00
a50cff494b Fix session 2023-08-17 08:07:11 +02:00
f20dbfc14b 2 kinds of session extractors 2023-08-17 07:58:15 +02:00
7069e58f1d Fix navbar 2023-08-16 16:54:09 +02:00
9a05b64eaf Working on signed in user 2023-08-16 16:53:27 +02:00
4d0e589a44 Working on session 2023-08-16 08:48:39 +02:00
dcb2276ed3 Working on session 2023-08-16 08:04:48 +02:00
97c8383313 Fix bearer prefix 2023-08-15 17:57:00 +02:00
1c8a536f92 Fix bearer prefix 2023-08-15 17:51:26 +02:00
13c64f58fd Working sign in 2023-08-15 13:36:35 +02:00
77cbb84885 Working sign in 2023-08-15 12:34:58 +02:00
2901da51f5 Working sign in 2023-08-15 12:33:53 +02:00
29e6e75e95 Fix navbar 2023-08-15 10:26:22 +02:00
c9fd7128f3 Improve layout, fix navbar 2023-08-14 22:23:18 +02:00
749ce35ab9 Add search to layout 2023-08-14 17:37:09 +02:00
da10be1553 Better session, add layout 2023-08-14 17:21:18 +02:00
f265d22b87 Using session 2023-08-14 12:30:32 +02:00
5560f068b1 Use new authenticator 2023-08-14 07:38:37 +02:00
e34a306668 Use new authenticator 2023-08-14 07:38:31 +02:00
932665a767 Use new authenticator 2023-08-13 15:34:31 +02:00
8526a45e13 Use new authenticator 2023-08-13 15:31:05 +02:00
7ec783651f Add middleware factory 2023-08-12 21:41:41 +02:00
f498846c53 New layout 2023-08-11 22:31:16 +02:00
6d42bb477a New layout 2023-08-11 22:31:16 +02:00
bb1f1e4a3e Using 2023-08-11 15:52:51 +02:00
dfb881f2ba New JWT implementation 2023-08-11 15:26:54 +02:00
6692df9aeb New JWT implementation 2023-08-11 15:25:36 +02:00
26dea34054 New JWT implementation 2023-08-11 15:25:26 +02:00
335a84838f Merge 2023-08-09 16:35:37 +02:00
c111d2f668 Fix rebase 2023-08-09 15:54:50 +02:00
a90c47bed3 Add changes 2023-08-09 15:52:35 +02:00
530980e2b7 Extract language 2023-08-09 15:52:35 +02:00
1c6dabcd77 Register and validations 2023-08-09 15:52:35 +02:00
36325a2de2 Add email 2023-08-09 15:52:35 +02:00
5e7ef00a99 Add translations 2023-08-09 15:52:35 +02:00
6564757800 Add css 2023-08-09 15:52:35 +02:00
16ffc5a46d Add styles 2023-08-09 15:52:35 +02:00
0da5817d9c Add missing files 2023-08-09 15:52:35 +02:00
beb3902cc5 handle sign in 2023-08-09 15:52:33 +02:00
ad918232c2 Add session 2023-08-09 15:51:38 +02:00
9384aa540f Fix field name 2023-08-09 15:51:37 +02:00
2c4f5c3b02 Fix field name 2023-08-09 15:49:58 +02:00
54ea6030e8 Display validation errors 2023-08-09 15:49:58 +02:00
e9c086a466 Display validation errors 2023-08-09 15:49:58 +02:00
71df209a4f Working session 2023-08-09 15:49:58 +02:00
da4e57d7ce Add session 2023-08-09 15:49:58 +02:00
e48e8ffc9b Some views 2023-08-09 15:49:58 +02:00
96c49299f6 Fix admin page 2023-08-09 15:49:58 +02:00
f07e5e3bd5 More admin 2023-08-09 15:49:58 +02:00
11d7432b07 Compile issues - bad versions 2023-08-09 15:49:56 +02:00
354856e222 Fix serialize issues 2023-08-09 15:49:12 +02:00
21ceab8861 Add rust-toolchain 2023-08-09 15:49:12 +02:00
cd044e810c Try display something 2023-08-09 15:49:11 +02:00
05c1a9abb0 Add actix-admin to entities 2023-08-09 15:48:41 +02:00
d48b39c5c3 Move to new design 2023-08-09 15:48:27 +02:00
29f8a421c4 Move to new design 2023-08-09 15:48:27 +02:00
87f4ee8c45 Add migrations 2023-08-09 15:48:27 +02:00
c1b98e536e Add migrations 2023-08-09 15:48:26 +02:00
d6cd82bf8e Move to new design 2023-08-09 15:47:49 +02:00
62dc015b99 Move to new design 2023-08-09 15:47:47 +02:00
0d6627d86a Add spot to parking space 2023-08-09 15:12:58 +02:00
c31776e46d Add spot to parking space 2023-08-09 14:56:46 +02:00
76090d4266 Extract language 2023-08-06 22:14:03 +02:00
daf472fd3e Register and validations 2023-08-05 22:20:23 +02:00
1317863cec Add email 2023-08-05 14:48:13 +02:00
08bdbbfded Add translations 2023-08-04 22:39:04 +02:00
a330b6ddd2 Add css 2023-08-04 16:36:02 +02:00
3daaaca521 Add styles 2023-08-04 16:32:10 +02:00
f634ac4891 Add missing files 2023-08-03 16:38:07 +02:00
86d4b2c6de handle sign in 2023-08-03 16:16:46 +02:00
7b3014b44d Add session 2023-08-02 21:31:33 +02:00
9ee4a35cab Fix field name 2023-08-02 16:37:03 +02:00
243b971539 Fix field name 2023-08-02 12:45:29 +02:00
bd73c2e9ea Display validation errors 2023-08-02 08:56:53 +02:00
3fcb854ff9 Display validation errors 2023-08-01 22:38:56 +02:00
0e541c2f52 Working session 2023-08-01 22:06:04 +02:00
8fe911c9dd Add session 2023-08-01 16:29:03 +02:00
b984caef76 Some views 2023-08-01 11:48:56 +02:00
0989a48065 Fix admin page 2023-07-31 17:04:17 +02:00
f314c4d63d More admin 2023-07-31 12:16:29 +02:00
b3a56558f2 Compile issues - bad versions 2023-07-27 22:54:55 +02:00
99a7f1c362 Fix serialize issues 2023-07-27 22:29:04 +02:00
2a9d2f812d Add rust-toolchain 2023-07-27 21:56:39 +02:00
a00602192a Try display something 2023-07-27 17:36:30 +02:00
e8d63c3b84 Add actix-admin to entities 2023-07-27 08:59:51 +02:00
8d9855d249 Move to new design 2023-07-26 21:49:37 +02:00
42bf3c58e3 Move to new design 2023-07-26 21:49:34 +02:00
07e0d95f12 Add migrations 2023-07-26 16:30:35 +02:00
9e30d854e8 Add migrations 2023-07-26 16:30:30 +02:00
5a155c0cd7 Move to new design 2023-07-26 11:16:29 +02:00
7a9f26c18f Move to new design 2023-07-26 11:16:20 +02:00
257 changed files with 13721 additions and 386551 deletions

9
.gitignore vendored
View File

@ -2,13 +2,14 @@
node_modules
uploads
client/dist
client/dist/app.js.map
client/dist/app.js
client/dist/admin.js.map
client/dist/admin.js
web/tmp
web/build
**/*.wasm
tmp
web/node_modules
web/dist
node_modules
/assets/build.js
/assets/build.js.map
/assets/style.css
config

4898
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,10 @@
[workspace]
members = [
'crates/server',
'crates/web',
'crates/contract',
'./crates/oswilno-server',
'./crates/oswilno-contract',
'./crates/web-assets',
'./crates/oswilno-parking-space',
'./crates/migration',
'./crates/oswilno-actix-admin',
]
resolver = "2"

7
assets/build.js Normal file
View File

@ -0,0 +1,7 @@
(()=>{var m=Object.create;var l=Object.defineProperty;var p=Object.getOwnPropertyDescriptor;var f=Object.getOwnPropertyNames;var h=Object.getPrototypeOf,u=Object.prototype.hasOwnProperty;var E=(e=>typeof require<"u"?require:typeof Proxy<"u"?new Proxy(e,{get:(t,s)=>(typeof require<"u"?require:t)[s]}):e)(function(e){if(typeof require<"u")return require.apply(this,arguments);throw Error('Dynamic require of "'+e+'" is not supported')});var g=(e,t,s,o)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of f(t))!u.call(e,n)&&n!==s&&l(e,n,{get:()=>t[n],enumerable:!(o=p(t,n))||o.enumerable});return e};var S=(e,t,s)=>(s=e!=null?m(h(e)):{},g(t||!e||!e.__esModule?l(s,"default",{value:e,enumerable:!0}):s,e));customElements.define("oswilno-price",class extends HTMLElement{});var a=e=>{let t="";for(let o=0;o<document.styleSheets.length;o++){let n=document.styleSheets[o];for(let r=0;r<n.rules.length;r++)t+=n.rules[r].cssText}let s=new CSSStyleSheet;s.replaceSync(t),e.adoptedStyleSheets=[s]};customElements.define("oswilno-error",class extends HTMLElement{constructor(){super();let e=this.attachShadow({mode:"open"});e.innerHTML=`
<style>:host{display:block;}</style>
<div class="flex bg-red-100 rounded-lg p-4 mb-4 text-sm text-red-700">
<slot></slot>
<div>
`,a(e)}});customElements.define("oswilno-parking-space-rents",class extends HTMLElement{});customElements.define("oswilno-parking-space-rent",class extends HTMLElement{});customElements.define("oswilno-parking-space",class extends HTMLElement{});customElements.define("oswilno-parking-space-location",class extends HTMLElement{});import("https://unpkg.com/htmx.org@1.9.4/dist/htmx.min.js");var c="Authorization",w="ACX-Authorization",i="ACX-Refresh",d=document.body;d.addEventListener("htmx:beforeOnLoad",function(e){let t=e.detail,s=t.xhr,o=s.status,n=t.successful;if(o===200){let r=s.getResponseHeader(c);r&&(console.log(s),localStorage.setItem("jwt",r.replace(/^Bearer /i,""))),s.getResponseHeader(i)&&localStorage.setItem("refresh",r.replace(/^Bearer /i,""))}else o===401&&localStorage.removeItem("jwt");(o===422||o===400)&&(t.shouldSwap=!0,t.isError=!1)});d.addEventListener("htmx:configRequest",function(e){localStorage.getItem("jwt")&&(e.detail.headers[w]="Bearer "+(localStorage.getItem("jwt")||""),e.detail.headers[c]="Bearer "+(localStorage.getItem("jwt")||""),e.detail.headers[i]=localStorage.getItem("refresh")||"")});})();
//# sourceMappingURL=build.js.map

7
assets/build.js.map Normal file

File diff suppressed because one or more lines are too long

1670
assets/style.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,69 +0,0 @@
{
"jsc": {
"parser": {
"syntax": "ecmascript",
"jsx": false
},
"target": "es2022",
"loose": true,
"minify": {
"compress": {
"arguments": false,
"arrows": true,
"booleans": true,
"booleans_as_integers": false,
"collapse_vars": true,
"comparisons": true,
"computed_props": false,
"conditionals": false,
"dead_code": false,
"directives": false,
"drop_console": false,
"drop_debugger": true,
"evaluate": true,
"expression": false,
"hoist_funs": false,
"hoist_props": true,
"hoist_vars": false,
"if_return": true,
"join_vars": true,
"keep_classnames": false,
"keep_fargs": true,
"keep_fnames": false,
"keep_infinity": false,
"loops": true,
"negate_iife": true,
"properties": true,
"reduce_funcs": false,
"reduce_vars": false,
"side_effects": true,
"switches": false,
"typeofs": true,
"unsafe": false,
"unsafe_arrows": false,
"unsafe_comps": false,
"unsafe_Function": false,
"unsafe_math": false,
"unsafe_symbols": false,
"unsafe_methods": false,
"unsafe_proto": false,
"unsafe_regexp": false,
"unsafe_undefined": false,
"unused": true
},
"mangle": {
"toplevel": false,
"keep_classnames": false,
"keep_fnames": false,
"keep_private_props": false,
"ie8": false,
"safari10": false
}
}
},
"module": {
"type": "es6"
},
"minify": true,
"isModule": true
}

View File

@ -1,15 +0,0 @@
{
"dependencies": {
"@swc/cli": "^0.1.57",
"@swc/core": "^1.2.218",
"@swc/core-linux-musl": "^1.2.42",
"@swc/helpers": "^0.4.3",
"@swc/jest": "^0.2.22",
"@swc/wasm-web": "^1.2.212",
"babel-minify": "^0.5.2",
"browserslist": "^4.21.1"
},
"resolutions": {
"terser": "npm:@swc/core"
}
}

View File

@ -1,22 +0,0 @@
const { config } = require("@swc/core/spack");
module.exports = config({
entry: {
app: `${__dirname}/src/app.js`,
admin: `${__dirname}/src/admin.js`,
},
output: {
path: `${__dirname}/dist`,
},
mode: 'production',
target: "browser",
options: {
swcrc: true,
swcrcRoots: __dirname,
configFile: `${__dirname}/.swcrc`,
plugin: (program) => {
console.error(program);
return program;
}
},
});

View File

@ -1,11 +0,0 @@
import "./admin/ow-admin";
import "./admin/news/article-form";
import "./admin/news/edit-news-article";
import "./admin/businesses/admin-business";
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,47 +0,0 @@
import { Component } from "../../shared";
customElements.define('admin-business', class extends Component {
static get observedAttributes() {
return ['business-id', "name"];
}
constructor() {
super(`
<style>
:host {
display: block;
}
::slotted(local-business-item) {
width: 100%;
margin-bottom: .5rem;
}
</style>
<article>
<h1 id="name"></h1>
</article>
<article>
<slot name="description"></slot>
</article>
<article>
<slot name="item"></slot>
</article>
`);
}
get business_id() {
return this.getAttribute('business-id');
}
set business_id(v) {
this.setAttribute('business-id', v);
}
get name() {
return this.getAttribute('name');
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('#name').textContent = v;
}
});

View File

@ -1,66 +0,0 @@
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>
<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

@ -1,90 +0,0 @@
import { Component, BUTTON_STYLE, INPUT_STYLE } from "../../shared";
customElements.define('admin-edit-business', class extends Component {
static get observedAttributes() {
return ['business-id', 'state'];
}
constructor() {
super(`
<style>
: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 }
</style>
<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" />
<select id="state" name="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>
</form>
</section>
</article>
`);
const form = this.shadowRoot.querySelector('#change-state');
this.shadowRoot.querySelector('#state').addEventListener('change', ev => {
form.submit();
});
}
get business_id() {
return this.getAttribute('business-id');
}
set business_id(v) {
this.setAttribute('business-id', v);
this.shadowRoot.querySelector('#id').value = v;
}
get state() {
return this.getAttribute('state');
}
set state(v) {
this.setAttribute('state', v);
this.shadowRoot.querySelector('#state').value = v;
}
});

View File

@ -1,110 +0,0 @@
import { Component, FORM_STYLE } from "../../shared";
customElements.define('article-form', class extends Component {
static get observedAttributes() {
return ['article-id', 'article-title', 'status'];
}
constructor() {
super(`
<style>
:host { display: block; }
rich-text-editor {
margin-top: 16px;
margin-bottom: 16px;
}
${ FORM_STYLE }
</style>
<form action="/admin/news/create" method="post">
<div>
<label>Tytuł</label>
<input placeholder="Tytuł" name="title" id="title" />
</div>
<div id="body-view">
<rich-text-editor id="body-rte" upload-url="/admin/news/upload">
</rich-text-editor>
<input type="hidden" name="body" />
</div>
<div>
<label>Status</label>
<select id="status" name="status">
<option value="pending">Oczekujący</option>
<option value="published">Opublikowany</option>
<option value="hidden">Ukryty</option>
</select>
</div>
<input type="submit" value="Zapisz" />
</form>
`);
const bodyInput = this.shadowRoot.querySelector('[name="body"]');
this.shadowRoot.querySelector('rich-text-editor').addEventListener('change', ev => {
ev.stopPropagation();
bodyInput.value = ev.target.value;
});
}
connectedCallback() {
super.connectedCallback();
this.body = this.innerHTML;
}
get article_id() {
const id = parseInt(this.getAttribute('article-id'));
return isNaN(id) ? null : id;
}
set article_id(v) {
this.setAttribute('article-id', v);
const form = this.shadowRoot.querySelector('form');
if (this.article_id === null) {
this.#removeIdInput();
form.action = "/admin/news/create";
} else {
this.#removeIdInput();
this.#createIdInput(v);
form.action = "/admin/news/update";
}
}
#createIdInput(v) {
const el = this.shadowRoot.querySelector('form').appendChild(document.createElement('input'));
el.id = 'id';
el.name = 'id';
el.value = v;
el.type = 'hidden';
}
#removeIdInput() {
const el = this.shadowRoot.querySelector('#id');
el && el.remove();
}
get article_title() {
return this.getAttribute('article-title');
}
set article_title(v) {
this.setAttribute('article-title', v);
this.shadowRoot.querySelector('#title').value = v;
}
get status() {
return this.getAttribute('status');
}
set status(v) {
this.setAttribute('status', v);
this.shadowRoot.querySelector('#status').value = (v || '').toLocaleLowerCase();
}
get body() {
return this.getAttribute('status');
}
set body(v) {
v = (v || '').trim();
this.shadowRoot.querySelector('#body-rte').value = v;
this.shadowRoot.querySelector('[name="body"]').value = v;
}
});

View File

@ -1,50 +0,0 @@
import { Component, BUTTON_STYLE } from "../../shared";
customElements.define('edit-news-article', class extends Component {
static get observedAttributes() {
return ['article-id'];
}
constructor() {
super(`
<style>
:host { display: block; }
article { display: flex; }
#edit {
margin-right: 8px;
}
${ BUTTON_STYLE }
</style>
<article>
<slot></slot>
</article>
<article>
<ow-path id="edit" path="/">
Edytuj
</ow-path>
<form method="post" action="/admin/news/delete">
<input name="id" id="id" type="hidden" />
<input class="link" type="submit" id="delete" value="Usuń" />
</form>
</article>
`);
this.shadowRoot.querySelector('#edit').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
});
}
get article_id() {
const v = parseInt(this.getAttribute('article-id'));
return isNaN(v) ? null : v;
}
set article_id(v) {
this.setAttribute('article-id', v);
const id = this.article_id;
if (id === null) return;
this.shadowRoot.querySelector('ow-path').path = `/admin/news/${id}`;
this.shadowRoot.querySelector('#id').value = id;
}
});

View File

@ -1,82 +0,0 @@
import { Component, BUTTON_STYLE } from "../../shared";
customElements.define('admin-edit-offer', class extends Component {
static get observedAttributes() {
return ['offer-id', 'state'];
}
constructor() {
super(`
<style>
:host {
display: block;
border-bottom: 2px solid var(--border-slim-color);
padding: 8px 0;
}
#view { display: block; }
#actions form {
width: 120px;
}
#actions form input {
width: 100%;
margin-bottom: 8px;
}
#state {
font-weight: bold;
margin: 8px;
}
:host([state="approved"]) #state { color: green; }
:host([state="banned"]) #state { color: darkred; }
:host([state="pending"]) #state { color: orange; }
:host([state="approved"]) #approve { display: none; }
:host([state="banned"]) #ban { display: none; }
@media only screen and (min-device-width: 1200px) {
#view { display: flex; }
#actions {
margin-left: 16px;
width: 120px;
}
}
${BUTTON_STYLE}
</style>
<section id="view">
<slot></slot>
<div id="actions">
<div id="state"></div>
<form action="/admin/offers/approve" method="post">
<input type="hidden" name="id" class="offer_id" />
<input type="submit" id="approve" value="Zaakceptuj" />
</form>
<form action="/admin/offers/ban" method="post">
<input type="hidden" name="id" class="offer_id" />
<input type="submit" id="ban" value="Zbanuj" />
</form>
</div>
</section>
`);
}
get offer_id() {
const v = parseInt(this.getAttribute('offer-id'));
return isNaN(v) ? null : v;
}
set offer_id(v) {
v = parseInt(v);
this.setAttribute('offer-id', v);
for (const el of this.shadowRoot.querySelectorAll('.offer_id')) {
el.value = v;
}
}
get state() {
return this.getAttribute('state');
}
set state(v) {
this.setAttribute('state', v);
this.shadowRoot.querySelector('#state').textContent = v;
}
});

View File

@ -1,64 +0,0 @@
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

@ -1,12 +0,0 @@
import { Component } from "../shared";
customElements.define('ow-admin', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<slot></slot>
`);
}
});

View File

@ -1,48 +0,0 @@
const JSON_HEADER = {
'Content-Type': 'application/json',
'Accept': 'application/json',
};
export const accountContacts = () =>
fetch('/contacts/list.json', {
headers: {
...JSON_HEADER,
},
method: 'GET',
}).then(res => res.json())
export const createContact = (body) =>
fetch('/contacts/create', {
headers: {
...JSON_HEADER,
},
method: 'POST',
body: JSON.stringify(body),
}).then(res => res.json())
export const updateContact = (body) =>
fetch('/contacts/update', {
headers: {
...JSON_HEADER,
},
method: 'POST',
body: JSON.stringify(body),
}).then(res => res.json())
export const deleteContact = async (body) =>
fetch('/contacts/delete', {
headers: {
...JSON_HEADER,
},
method: 'POST',
body: JSON.stringify(body),
}).then(res => res.json())
export const register = (body) =>
fetch('/register', {
headers: {
...JSON_HEADER,
},
method: 'POST',
body: JSON.stringify(body),
}).then(res => res.json())

View File

@ -1,76 +0,0 @@
import "./poly.js";
import "./shared/error-message.js";
import "./shared/rich-text-editor.js";
import "./shared/form-navigation.js";
import "./shared/image-popup.js";
import "./shared/facebook-button.js";
import "./shared/search-input.js";
import "./shared/nav/ow-nav.js";
import "./shared/nav/ow-path.js";
import "./shared/price/price-input.js";
import "./shared/price/price-view.js";
import "./ow-account/ow-account.js";
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";
import "./register-form/register-business-items-form.js";
import "./register-form/register-business-details-form.js";
import "./register-form/register-account-type.js";
import "./register-form/register-user-account-form.js";
import "./register-form/register-business-contacts-form.js";
import "./register-form/register-business-submit-form.js";
import "./business-editor.js";
import "./business-items/business-item.js";
import "./business-items/business-item-editor.js";
import "./news/ow-articles.js";
import "./news/news-article.js";
import "./contacts/contact-type-icon.js";
import "./contacts/contact-info.js";
import "./contacts/contact-info-editor.js";
import "./contacts/edit-contact-info.js";
import "./contacts/contact-info-list.js";
import "./marketplace/marketplace-offer.js";
import "./marketplace/marketplace-offers.js";
import "./marketplace/offer-form.js";
import "./marketplace/user-edit-offer.js";
import "./marketplace/marketplace-editor.js";
import "./terms_and_conditions/terms-and-conditions.js";
import "./terms_and_conditions/privacy-policy.js";
import "./terms_and_conditions/privacy-policy-bar.js"
import { fireFbReady } from "./shared.js";
if (!document.querySelector('#facebook-jssdk')) {
window.fbAsyncInit = () => {
FB.init({
appId: '1293538251053124',
cookie: true,
xfbml: true,
version: 'v14.0'
});
FB.AppEvents.logPageView();
fireFbReady();
};
const js = document.createElement('script');
js.id = 'facebook-jssdk';
js.src = "https://connect.facebook.net/en_US/sdk.js";
document.head.appendChild(js);
}

View File

@ -1,73 +0,0 @@
import { Component } from "./shared.js";
import * as api from "./api";
customElements.define('business-editor', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<article>
<section>
<slot name="items"></slot>
</section>
<section>
<slot name="contacts"></slot>
</section>
</article>
`);
this.addEventListener('contact:create', async ({ detail }) => {
await this.#createContact(detail);
});
this.addEventListener('contact:update', async ({ detail }) => {
await this.#updateContact(detail);
});
this.addEventListener('contact:delete', async ({ detail }) => {
await this.#deleteContact(detail)
});
}
async #createContact({ content, type }) {
await api.createContact({ content, type });
const { contacts } = await api.accountContacts();
this.#contacts = this.#formatContacts(contacts);
}
async #updateContact({ id, content, type }) {
console.info(1);
await api.updateContact({ id, content, type });
console.info(2);
const { contacts } = await api.accountContacts();
console.info(3, contacts);
this.#contacts = this.#formatContacts(contacts);
}
async #deleteContact({ id }) {
await api.deleteContact({ id });
const { contacts } = await api.accountContacts();
this.#contacts = this.#formatContacts(contacts);
}
#formatContacts(contacts) {
return contacts.map(({ id, content, contact_type }) => `
<edit-contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
mode="view"
>
<contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`).join('');
}
set #contacts(html) {
this.querySelector('contact-info-editor')
.innerHTML = html;
}
});

View File

@ -1,179 +0,0 @@
import { Component, FORM_STYLE } from "../shared";
import "../register-form/register-business-item-form";
customElements.define('business-item-editor', class extends Component {
#idx;
static get observedAttributes() {
return ['business-id', 'name', 'description']
}
constructor() {
super(`
<style>
:host { display: block; }
::slotted(business-item) {
margin-bottom: 16px;
}
register-item-form-row {
margin-bottom: 16px;
}
#shadow {
width: 33px;
}
#description {
min-height: 200px;
}
article {
margin: 8px;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
}
}
${ FORM_STYLE }
</style>
<article id="business-details">
<h2>Edycja konta przedsiębiorcy</h2>
<form method="post" action="/business-item/atomic-update">
<input type="hidden" name="id" id="id">
<div>
<label>Nazwa</label>
<input name="name" id="name" />
</div>
<div>
<label>Opis</label>
<textarea name="description" id="description"></textarea>
</div>
<div>
<input type="submit" value="Zapisz" />
</div>
</form>
</article>
<article id="list">
<slot></slot>
</article>
<article id="form">
<register-business-item-form remove="hidden" save="show" idx="0">
<div id="shadow" slot="head">&nbsp;</div>
</register-business-item-form>
</article>
<article id="new-business-item-form">
<form id="create-new-item-form" method="post" action="/business-item/new">
<input type="hidden" name="name" id="name" />
<input type="hidden" name="price" id="price" />
<input type="hidden" name="picture_url" id="picture_url" />
<input type="hidden" name="item_order" id="item_order" />
</form>
</article>
<form id="moveForm" action="/business-item/move" method="post">
<input name="id" type="hidden" />
<input name="item_order" type="hidden" />
</form>
`);
const form = this.shadowRoot.querySelector('#create-new-item-form');
this.addEventListener('item:submit', ev => {
ev.stopPropagation();
ev.preventDefault();
// 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 => {
ev.preventDefault();
ev.stopPropagation();
const item_id = ev.detail.id;
const current = this.querySelector(`business-item[item-id="${ item_id }"]`);
if (!current) return console.warn(`business-item[item-id="${ item_id }"] not found`);
let prev = current.previousElementSibling;
if (!prev) return console.warn(`prev of business-item[item-id="${ item_id }"] not found`);
moveForm.querySelector('[name=id]').value = item_id;
moveForm.querySelector('[name=item_order]').value = prev.item_order;
moveForm.submit();
});
this.addEventListener('item:down', ev => {
ev.preventDefault();
ev.stopPropagation();
const item_id = ev.detail.id;
const current = this.querySelector(`business-item[item-id="${ item_id }"]`);
if (!current) return;
let next = current.nextElementSibling;
if (!next) return;
moveForm.querySelector('[name=id]').value = item_id;
moveForm.querySelector('[name=item_order]').value = next.item_order;
moveForm.submit();
});
}
connectedCallback() {
super.connectedCallback();
this.#updateForm();
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
this.#updateForm();
}
#updateForm() {
const items = [...this.querySelectorAll('business-item')];
items.sort((a, b) => a.item_order - b.item_order);
for (const item of items) {
this.#idx = item.item_order;
}
const el = this.shadowRoot.querySelector('register-item-form-row');
if (!el) return;
el.idx = this.#idx + 1;
}
get business_id() {
return this.getAttribute('business-id');
}
set business_id(v) {
this.setAttribute('business-id', v);
this.shadowRoot.querySelector('#id').value = v;
}
get name() {
return this.getAttribute('name');
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('#name').value = v;
}
get description() {
return this.getAttribute('description');
}
set description(v) {
this.setAttribute('description', v);
this.shadowRoot.querySelector('#description').textContent = (v || '').trim();
}
});

View File

@ -1,212 +0,0 @@
import "../shared/image-input";
import { Component, FORM_STYLE } from "../shared";
customElements.define('business-item', class extends Component {
static get observedAttributes() {
return ['item-id', 'name', 'price', 'picture-url', 'item-order']
}
constructor() {
super(`
<style>
:host { display: block; }
:host(:first-child) #move-up {
display: none;
}
:host(:last-child) #move-down {
display: none;
}
#move svg {
cursor: pointer;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
@media(min-width: 1280px) {
section > form {
display: flex;
justify-content: space-between;
}
#actions {
width: 25%;
max-width: 25%;
}
form > #fields {
width: 50%;
}
#move {
width: 33px;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
}
#move-up, #move-down {
border: none;
background: none;
}
${ FORM_STYLE }
</style>
<section>
<form id="updateForm" action="/business-item/update" method="post">
<div id="move">
<button id="move-up">
<svg width="32" height="32" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 0 0 140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"/>
</svg>
<button id="move-down">
<svg width="32" height="32" viewBox="0 0 1024 1024" xmlns="http://www.w3.org/2000/svg" class="icon">
<path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"/>
</svg>
</button>
</div>
<image-input send-original="true"></image-input>
<div id="fields">
<div>
<label>Nazwa</label>
<input name="name" id="name" class="input" />
</div>
<div>
<label>Cena</label>
<price-input class="input"></price-input>
</div>
</div>
<div id="actions">
<input type="submit" id="save" value="Zapisz" />
<input type="button" id="delete" value="Usuń" />
</div>
<input type="hidden" name="id" id="id" />
<input type="hidden" name="price" id="price" />
<input type="hidden" name="item_order" id="item_order" />
<input type="hidden" name="picture_url" id="picture_url" />
</form>
<form id="deleteForm" action="/business-item/delete" method="post">
<input id="delete-id" name="id" type="hidden" />
</form>
</section>
`);
const imageInput = this.shadowRoot.querySelector('image-input');
this.addEventListener('image-input:uploaded', ev => {
ev.preventDefault();
ev.stopPropagation();
this.picture_url = imageInput.url;
const updateForm = this.shadowRoot.querySelector('#updateForm');
updateForm.querySelector('#id').value = this.item_id;
updateForm.querySelector('#name').value = this.name;
updateForm.querySelector('#price').value = this.price;
updateForm.querySelector('#picture_url').value = this.picture_url;
updateForm.querySelector('#item_order').value = this.item_order;
// updateForm.submit();
});
this.shadowRoot.querySelector('#delete').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
const form = this.shadowRoot.querySelector('#deleteForm');
form.querySelector('#delete-id').value = this.item_id;
form.submit();
});
const priceInput = this.shadowRoot.querySelector('price-input');
const price = this.shadowRoot.querySelector('#price');
priceInput.addEventListener('change', ev => {
ev.stopPropagation();
price.value = ev.detail.price;
});
{
const button = this.shadowRoot.querySelector('#move-up');
button.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:up', {
bubbles: true,
composed: true,
detail: { id: this.item_id, item_order: this.item_order }
}));
});
}
{
const button = this.shadowRoot.querySelector('#move-down');
button.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:down', {
bubbles: true,
composed: true,
detail: { id: this.item_id }
}));
});
}
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
if (oldV === newV) return;
switch (name) {
case 'price':
return this.price = newV;
}
}
static get attr2FieldBlacklist() {
return ['price'];
}
get item_id() {
return this.getAttribute('item-id');
}
set item_id(v) {
this.setAttribute('item-id', v);
this.shadowRoot.querySelector('#id').value = v;
}
get item_order() {
const v = parseInt(this.getAttribute('item-order'));
return isNaN(v) ? null : v;
}
set item_order(v) {
this.setAttribute('item-order', v);
this.shadowRoot.querySelector('#item_order').value = v;
}
get name() {
return this.getAttribute('name');
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('#name').value = v;
}
get price() {
return this.shadowRoot.querySelector('price-input').value;
}
set price(v) {
this.setAttribute('price', v);
this.shadowRoot.querySelector('price-input').value = v / 100.0;
this.shadowRoot.querySelector('#price').value = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
if (!v.startsWith("/")) v = "";
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('image-input').url = v;
this.shadowRoot.querySelector('#picture_url').value = v;
}
});

View File

@ -1,168 +0,0 @@
import { Component, FORM_STYLE, onKeyDown } from "../shared.js";
customElements.define('contact-info-editor', class extends Component {
static get observedAttributes() {
return ['type', "contact-id", "content", 'save'];
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
#contact-wrapper contact-type-icon {
width: 24px;
margin-right: 8px;
}
#contact-wrapper #input {
display: flex;
justify-content: start;
}
#icon {
align-self: flex-end;
position: relative;
}
article {
margin: 8px;
}
:host([save="false"]) #submit {
display: none;
}
input[type=submit] {
width: 100%;
max-width: 100%;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
}
}
${ FORM_STYLE }
</style>
<article>
<section>
<form method="post" action="/contacts/create">
<div id="contact-wrapper">
<label>E-Mail</label>
<div id="input">
<div id="icon">
<contact-type-icon type="email"></contact-type-icon>
</div>
<input type="text" id="content" name="content" />
<input type="hidden" id="type" name="type" />
</div>
</div>
<div id="submit">
<input type="submit" value="Dodaj" />
</div>
</form>
</section>
<section>
<slot></slot>
</section>
</article>
`);
onKeyDown(this.shadowRoot.querySelector('#content'), (ev, input) => {
this.#updateContactType(input.value, null);
this.content = input.value;
});
this.shadowRoot.querySelector('#type').addEventListener('change', ev => {
ev.stopPropagation();
this.#updateContactType(null, ev.target.value);
});
this.shadowRoot.querySelector('form').addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent(
this.contact_id
? 'contact:update'
: 'contact:create',
{
composed: true,
bubbles: true,
detail: { type: this.type, content: this.content, id: this.contact_id }
}
));
});
}
get type() {
return this.getAttribute('type');
}
set type(v) {
this.#updateContactType(null, v);
}
get content() {
return this.getAttribute('content');
}
set content(v) {
this.setAttribute('content', v);
this.shadowRoot.querySelector('#content').value = v;
}
get contact_id() {
const v = parseInt(this.getAttribute('contact-id'));
return isNaN(v) ? null : v;
}
set contact_id(v) {
this.setAttribute('contact-id', v);
const n = parseInt(v);
if (isNaN(n))
this.#removeId();
else
this.#addId(n);
}
#removeId() {
const form = this.shadowRoot.querySelector('form');
const input = form.querySelector('#contact-id');
input && input.remove();
form.action = '/contacts/create';
form.querySelector('input[type=submit]').value = 'Dodaj';
}
#addId(v) {
this.#removeId();
const form = this.shadowRoot.querySelector('form');
const input = form.appendChild(document.createElement('input'));
input.setAttribute('type', 'hidden');
input.setAttribute('name', 'id');
input.setAttribute('id', 'contact-id');
input.value = v;
form.action = '/contacts/update';
form.querySelector('input[type=submit]').value = 'Zmień';
}
#updateContactType(value, type) {
type = type || this.#resolveContactType(value);
this.setAttribute('type', type);
const icon = this.shadowRoot.querySelector('contact-type-icon');
icon.type = type;
this.shadowRoot.querySelector('#type').value = type;
}
#resolveContactType(s) {
s = s || '';
if (s.match(/http(s)?:\/\/(www\.)?facebook\.com/)) {
return 'facebook';
}
if (s.match(/(\+\d{2,3} )?\d{3}([ \-])?\d{3}([ \-])?\d{3}/)) {
return 'mobile';
}
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

@ -1,19 +0,0 @@
import { Component } from "../shared";
customElements.define('contact-info-list', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
article {
display: flex;
}
@media only screen and (min-device-width: 1000px) {
}
</style>
<article>
<slot></slot>
</article>
`);
}
});

View File

@ -1,106 +0,0 @@
import { Component } from "../shared";
import "./contact-type-icon.js";
customElements.define('contact-info', class extends Component {
static get observedAttributes() {
return ['type', 'content', 'contact-id', 'mode'];
}
constructor() {
super(`
<style>
:host { display: block; }
section a {
display: flex;
justify-content: start;
}
contact-type-icon {
width: 24px;
margin-right: 8px;
}
:host([mode="icon"]) #content {
display: none;
}
</style>
<slot></slot>
<section>
<a target="_blank">
<contact-type-icon type="email"></contact-type-icon>
<div id="content"></div>
</a>
</section>
`);
this.shadowRoot.querySelector('a').addEventListener('click', ev => {
if (this.type === 'mobile') {
ev.preventDefault();
const decoded = atob(this.content);
if (this.#isMobile()) {
const link = `tel:${ decoded }`;
window.open(link);
} else {
const match = decoded.match(/(\+\d+ ?)?(\d{3})([ \-])?(\d{3})([ \-])?(\d+)/);
this.shadowRoot.querySelector('section').innerHTML = `<div>${ match[2] } ${ match[4] } ${ match[6] }</div>`;
}
}
});
}
set type(v) {
if (!v) return;
this.setAttribute('type', v);
this.shadowRoot.querySelector('contact-type-icon').type = v;
this.#setHref();
}
get type() {
return this.getAttribute('type');
}
set content(v) {
this.setAttribute('content', v);
this.shadowRoot.querySelector('#content').textContent = v;
this.#setHref();
}
get content() {
return this.getAttribute('content')
}
get contact_id() {
return this.getAttribute('contact-id')
}
set contact_id(v) {
this.setAttribute('contact-id', v);
}
get mode() {
return this.getAttribute('mode');
}
// full or icon
set mode(v) {
this.setAttribute('mode', (v || '').toLocaleLowerCase())
}
#setHref() {
this.shadowRoot.querySelector('a').href = this.#createLinkPath();
}
#createLinkPath() {
const s = this.shadowRoot.querySelector('#content').textContent || '';
switch (this.type) {
case 'email':
return `mailto:${ s }`;
default:
return s;
}
}
#isMobile() {
return !!navigator.userAgent.toLowerCase().match(/mobile/i);
}
});

View File

@ -1,62 +0,0 @@
import { Component } from "../shared.js";
customElements.define('contact-type-icon',
class extends Component {
static get observedAttributes() {
return ['type'];
}
constructor() {
super(`
<style>
:host { display: block; }
svg { display: none; }
:host([type="email"]) #email-icon {
display: block;
}
:host([type="facebook"]) #fb-icon {
display: block;
}
:host([type="other"]) #other-icon {
display: block;
}
:host([type="mobile"]) #mobile-icon {
display: block;
}
:host([type='link']) #link-icon {
display: block;
}
</style>
<svg id="email-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 75.294 75.294" xml:space="preserve">
<path d="M66.097 12.089h-56.9C4.126 12.089 0 16.215 0 21.286v32.722c0 5.071 4.126 9.197 9.197 9.197h56.9c5.071 0 9.197-4.126 9.197-9.197V21.287c.001-5.072-4.125-9.198-9.197-9.198zm-4.494 6L37.647 33.523 13.691 18.089h47.912zm4.494 39.117h-56.9A3.201 3.201 0 0 1 6 54.009V21.457l29.796 19.16c.04.025.083.042.124.065.043.024.087.047.131.069.231.119.469.215.712.278.025.007.05.01.075.016.267.063.537.102.807.102h.006c.27 0 .54-.038.807-.102.025-.006.05-.009.075-.016.243-.063.48-.159.712-.278a3.27 3.27 0 0 0 .131-.069c.041-.023.084-.04.124-.065l29.796-19.16v32.551a3.204 3.204 0 0 1-3.199 3.198z"/>
</svg>
<svg id="fb-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 314.652 314.652" xml:space="preserve">
<path d="M157.326 0C70.576 0 0 70.576 0 157.326s70.576 157.326 157.326 157.326 157.326-70.576 157.326-157.326S244.076 0 157.326 0zm0 296.652C80.501 296.652 18 234.15 18 157.326S80.501 18 157.326 18s139.326 62.502 139.326 139.326-62.501 139.326-139.326 139.326z"/><path d="M193.764 71.952H172.43c-17.461 0-31.667 14.206-31.667 31.667v24h-19.875c-4.971 0-9 4.029-9 9s4.029 9 9 9h19.875v83.333c0 4.971 4.029 9 9 9s9-4.029 9-9v-83.333h30.75c4.971 0 9-4.029 9-9s-4.029-9-9-9h-30.75v-24c0-7.536 6.131-13.667 13.667-13.667h21.333c4.971 0 9-4.029 9-9s-4.029-9-8.999-9z"/>
</svg>
<svg id="other-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" stroke="#000" stroke-width="2" d="M1 2h21v16h-8l-8 4v-4H1V2Zm5 8h1v1H6v-1Zm5 0h1v1h-1v-1Zm5 0h1v1h-1v-1Z"/>
</svg>
<svg id="mobile-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 557.389 557.389" xml:space="preserve">
<path d="M368.318 0H189.061c-29.223 0-62.529 34.721-62.529 65.159V492.22c0 30.447 33.306 65.169 62.529 65.169h179.258c29.225 0 62.539-34.722 62.539-65.169V65.159C430.857 34.721 397.543 0 368.318 0zm48.196 492.229c0 23.275-26.125 50.825-48.195 50.825H189.061c-22.07 0-48.186-27.55-48.186-50.825V65.159c0-23.275 26.125-50.815 48.186-50.815h179.258c22.07 0 48.195 27.549 48.195 50.815v427.07z"/>
<path d="M289.15 477.341a1.079 1.079 0 0 0-1.49.172c-.363.469-.287 1.139.172 1.492a14.682 14.682 0 0 1 5.594 11.561c0 8.129-6.598 14.736-14.726 14.736-8.119 0-14.727-6.607-14.727-14.736 0-4.561 2.065-8.797 5.671-11.618a1.034 1.034 0 0 0 .191-1.473c-.363-.469-1.033-.564-1.492-.191a16.767 16.767 0 0 0-6.493 13.282c0 9.305 7.564 16.868 16.859 16.868 9.294 0 16.859-7.563 16.859-16.868a16.862 16.862 0 0 0-6.418-13.225z"/>
<path d="M278.69 491.627c.593 0 1.062-.469 1.062-1.062v-15.777c0-.594-.479-1.062-1.062-1.062s-1.062.468-1.062 1.062v15.777a1.06 1.06 0 0 0 1.062 1.062z"/>
<path d="M278.69 458.866c-18.063 0-32.761 14.697-32.761 32.761s14.697 32.762 32.761 32.762c18.064 0 32.761-14.698 32.761-32.762s-14.687-32.761-32.761-32.761zm0 60.741c-15.425 0-27.979-12.546-27.979-27.98 0-15.424 12.546-27.979 27.979-27.979s27.98 12.546 27.98 27.979c0 15.425-12.547 27.98-27.98 27.98zM361.053 87.927H196.347c-12.527 0-22.711 10.174-22.711 22.711v304.804c0 12.518 10.184 22.711 22.711 22.711h164.706c12.525 0 22.711-10.193 22.711-22.711V110.638c0-12.527-10.196-22.711-22.711-22.711zm15.539 327.515c0 8.568-6.973 15.539-15.539 15.539H196.347c-8.568 0-15.539-6.971-15.539-15.539V110.638c0-8.568 6.971-15.539 15.539-15.539h164.706c8.566 0 15.539 6.971 15.539 15.539v304.804z"/>
<circle cx="233.02" cy="55.769" r="5.977"/>
<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>
`);
}
set type(v) {
this.setAttribute('type', v || 'email');
}
get type() {
return this.getAttribute('type')
}
});

View File

@ -1,133 +0,0 @@
import { Component, BUTTON_STYLE } from "../shared";
customElements.define('edit-contact-info', class extends Component {
static get observedAttributes() {
return ['contact-id', "mode", "delete", 'type'];
}
constructor() {
super(`
<style>
:host { display: block; }
article {
display: block;
}
#buttons {
display: flex;
justify-content: start;
margin: 8px 0;
}
#actions input {
margin-right: 8px;
}
#cancel, #edit {
display: none;
}
:host([mode = 'view']) contact-info-editor {
display: none;
}
:host([mode = 'edit']) #actions {
width: 100%;
}
:host([mode = 'edit']) contact-info-editor {
display: block;
min-width: 50%;
}
:host([mode = 'edit']) #cancel {
display: block;
}
:host([mode = 'view']) #edit {
display: block;
}
:host([mode = 'view']) ::slotted(contact-info) {
display: block;
}
:host([mode = 'edit']) ::slotted(contact-info) {
display: none;
}
:host([delete = "false"]) #deleteButton {
display: none;
}
@media only screen and (min-device-width: 1000px) {
article {
display: flex;
justify-content: space-between;
width: 100%;
}
#actions {
display: flex;
justify-content: space-between;
}
}
${ BUTTON_STYLE }
</style>
<article>
<slot></slot>
<section id="actions">
<contact-info-editor></contact-info-editor>
<div id="buttons">
<input type="button" value="Edytuj" id="edit" />
<input type="button" value="Anuluj" id="cancel" />
<form id="deleteButton" action="/contacts/delete" method="post">
<input type="hidden" name="id" id="remove-id" />
<input type="submit" value="Usuń" id="remove" />
</form>
</div>
</section>
</article>
`);
const form = this.shadowRoot.querySelector('contact-info-editor');
this.shadowRoot.querySelector('#edit').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
const info = this.querySelector('contact-info');
if (!info) return;
form.contact_id = this.contact_id;
form.type = info.type;
form.content = info.content;
this.mode = 'edit';
});
this.shadowRoot.querySelector('#cancel').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.mode = 'view';
});
const deleteForm = this.shadowRoot.querySelector('#deleteButton');
deleteForm.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('contact:delete', {
composed: true,
bubbles: true,
detail: { id: this.contact_id }
}))
});
}
get contact_id() {
const v = parseInt(this.getAttribute('contact-id'));
return isNaN(v) ? null : v;
}
set contact_id(v) {
this.setAttribute('contact-id', v);
this.shadowRoot.querySelector('#remove-id').value = v;
}
get mode() {
return this.getAttribute('mode');
}
set mode(v) {
if (v !== 'edit' && v !== 'view') {
console.warn('wrong edit contact info mode', v);
this.setAttribute('mode', 'view');
return
}
this.setAttribute('mode', v);
}
});

View File

@ -1,116 +0,0 @@
import { Component } from "../shared";
customElements.define('local-business-item', class extends Component {
static get observedAttributes() {
return ['name', 'price', 'picture-url']
}
constructor() {
super(`
<style>
: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;
}
#item {
display: grid;
grid-template-areas: 'img name' 'img price';
}
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;
width: var(--price-width);
}
#img {
width: var(--img-width);
max-width: var(--img-width);
grid-area: img;
border-radius: 6px;
}
@media (min-width: 1000px) {
#item {
display: flex;
justify-content: space-between;
}
h3#name {
font-weight: normal;
line-height: 1.6;
width: var(--name-width);
text-align: left;
}
#price {
font-weight: bold;
width: var(--price-width);
}
#img {
width: var(--img-width);
max-width: 128px;
}
}
</style>
<section id="item">
<image-popup id="img"></image-popup>
<h3 id="name"></h3>
<price-view id="price"></price-view>
</section>
`);
}
get price() {
const n = parseInt(this.getAttribute('price'));
return isNaN(n) ? 0 : n;
}
set price(v) {
this.setAttribute('price', v);
this.shadowRoot.querySelector('#price').value = v;
}
get name() {
return this.getAttribute('name');
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('#name').textContent = v;
}
get picture_url() {
const v = this.getAttribute('picture-url');
if (!v || v === '') return;
return v;
}
set picture_url(v) {
v = v || '';
if (!(v || '').startsWith("/")) v = '';
this.setAttribute('picture-url', v);
const el = this.shadowRoot.querySelector('#img');
el.src = v;
}
matches(text) {
return !!this.name.match(text)
}
});

View File

@ -1,121 +0,0 @@
import { Component } from "../shared";
customElements.define('local-business', class extends Component {
static get observedAttributes() {
return ['name', 'business-id', 'state']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
#items {
margin-top: 16px;
}
#contacts {
display: flex;
justify-content: end;
}
::slotted(local-business-item) {
margin-bottom: .5rem;
}
::slotted(contact-info-list) {
display: flex;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
<section id="contacts">
<span style="margin-right: 10px; font-weight: bold">Kontakt:</span>
<slot name="contacts"></slot>
</section>
`);
}
get name() {
return this.getAttribute('name') || ''
}
set name(v) {
this.setAttribute('name', v);
this.#setNameHeader();
}
get state() {
return this.getAttribute('state');
}
set state(v) {
this.setAttribute('state', v);
}
get business_id() {
return this.getAttribute('business-id');
}
set business_id(v) {
this.setAttribute('business-id', v);
this.#setNameHeader();
}
get description() {
return Array.from(this.querySelectorAll('[slot=description]')).map(el => el.textContent);
}
matches(regex) {
return this.name.match(regex) ||
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

@ -1,67 +0,0 @@
import { Component } from "../shared.js";
import "../shared/search-input.js";
customElements.define('local-business-list', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
::slotted([search-visible='invisible']) {
display: none;
}
input {
font-size: 1rem;
line-height: 2.6em;
height: 2.6em;
margin: 0;
padding: 0;
width: 100%;
border:none;
outline:none;
display: block;
background: transparent;
border-bottom: 1px solid #ccc;
text-indent: 20px;
}
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 id="search">
<search-input target="local-business"></search-input>
</section>
<section id="items">
<slot name="business"></slot>
</section>
</article>
`);
}
});

View File

@ -1,7 +0,0 @@
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

@ -1,26 +0,0 @@
import { Component, FORM_STYLE } from "./shared";
customElements.define('login-form', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
${ FORM_STYLE }
</style>
<form action="/login" method="post">
<div>
<label>E-Mail</label>
<input name="email" placeholder="E-Mail" type="email" required />
</div>
<div>
<label>Hasło</label>
<input name="password" placeholder="Hasło" type="password" required />
</div>
<div>
<input type="submit" value="Zaloguj" />
</div>
</form>
`);
}
});

View File

@ -1,73 +0,0 @@
import { Component } from "../shared.js";
import * as api from "../api.js";
customElements.define('marketplace-editor', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<article>
<section>
<slot name="offers"></slot>
</section>
<section>
<slot name="contacts"></slot>
</section>
</article>
`);
this.addEventListener('contact:create', async ({ detail }) => {
await this.#createContact(detail);
});
this.addEventListener('contact:update', async ({ detail }) => {
await this.#updateContact(detail);
});
this.addEventListener('contact:delete', async ({ detail }) => {
await this.#deleteContact(detail)
});
}
async #createContact({ content, type }) {
await api.createContact({ content, type });
const { contacts } = await api.accountContacts();
this.#contacts = this.#formatContacts(contacts);
}
async #updateContact({ id, content, type }) {
await api.updateContact({ id, content, type });
const { contacts } = await api.accountContacts();
this.#contacts = this.#formatContacts(contacts);
}
async #deleteContact({ id }) {
await api.deleteContact({ id });
const { contacts } = await api.accountContacts();
this.#contacts = this.#formatContacts(contacts);
}
#formatContacts(contacts) {
return contacts.map(({ id, content, contact_type }) => `
<edit-contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
mode="view"
>
<contact-info
contact-id="${ id }"
type="${ contact_type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`).join('');
}
set #contacts(html) {
for (const el of this.querySelectorAll('edit-contact-info'))
el.remove();
const fragment = document.createElement('template');
fragment.innerHTML = html;
this.appendChild(fragment.content);
}
});

View File

@ -1,249 +0,0 @@
import { Component, INPUT_STYLE, PriceRange } from "../shared";
customElements.define('marketplace-offer', class extends Component {
#price_range;
static get observedAttributes() {
return ['offer-id', 'description', 'picture-url', "price-range", "price-range-min", "price-range-max"]
}
constructor() {
// language=HTML
super(`
<style>
:host {
display: block;
margin-bottom: 16px;
border-bottom: 1px solid var(--border-light-gray-color);
width: 100%;
}
section {
margin-bottom: 16px;
text-decoration: none;
color: black;
font-style: normal;
}
#preview {
width: 100%;
}
image-popup {
max-width: 100%;
}
#sep {
display: block;
grid-area: sep;
text-align: center;
}
:host([price-range-max="0"]) #sep, :host([price-range]) #sep,
:host([price-range-max="0"]) #price-min, :host([price-range]) #price-min {
display: none;
}
#details {
display: grid;
column-gap: 16px;
grid-template-areas: "img img img"
"desc desc desc"
"min sep max";
grid-template-columns: auto 10px auto;
}
#preview {
grid-area: img;
margin: auto;
}
#description {
grid-area: desc;
text-align: justify;
font-style: italic;
}
#price-min {
grid-area: min;
justify-self: end;
-webkit-transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
-o-transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s, -webkit-box-shadow .3s;
}
#price-max {
grid-area: max;
justify-self: start;
-webkit-transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, -webkit-box-shadow .3s;
-o-transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s;
transition: background .3s, border .3s, border-radius .3s, box-shadow .3s, -webkit-box-shadow .3s;
}
#contacts {
display: flex;
justify-content: end;
}
@media only screen and (min-device-width: 1000px) {
#details {
display: grid;
column-gap: 16px;
grid-template-areas: "img img img" "desc desc desc" "min sep max";
grid-template-columns: auto 10px auto;
margin: 0 20px 0 20px;
}
image-popup {
width: 100%;
}
#preview {
grid-area: img;
align-self: center;
text-align: center;
}
#description {
grid-area: desc;
justify-self: stretch;
margin-top: 0;
}
#price-min {
grid-area: min;
justify-self: end;
width: 100px;
text-align: right;
}
#price-max {
grid-area: max;
justify-self: start;
width: 100px;
text-align: right;
}
a {
font-style: normal;
text-decoration: none;
color: black;
}
}
${ INPUT_STYLE }
</style>
<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);
}
get offer_id() {
const v = parseInt(this.getAttribute('offer-id'));
return isNaN(v) ? null : v;
}
set offer_id(v) {
v = parseInt(v);
this.setAttribute('offer-id', v);
}
get description() {
return this.getAttribute('description');
}
set description(v) {
this.setAttribute('description', v);
this.shadowRoot.querySelector('#description').textContent = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('#picture').src = v;
}
get price_range() {
return this.#price_range
}
set price_range(v) {
if (v instanceof PriceRange) return;
v = v + '';
if (!v.match(/free|(\d+([,.]\d{2})?)(\|(\d+([,.]\d{2})?))?/i))
return console.warn('malformed price range');
if (v === 'free')
this.#price_range = new PriceRange(v, 0);
else if (v.includes(',')) {
const [min, max, ..._] = v.split(',');
this.#price_range = new PriceRange(parseInt(min), parseInt(max));
} else {
this.#price_range.min = parseInt(v);
}
this.#displayPrice();
}
get price_range_min() {
return this.#price_range.min
}
set price_range_min(v) {
this.#price_range.min = v;
this.#displayPrice();
}
get price_range_max() {
return this.#price_range.max
}
set price_range_max(v) {
this.#price_range.max = v;
this.#displayPrice();
}
#displayPrice() {
const min = this.shadowRoot.querySelector('#price-min');
const max = this.shadowRoot.querySelector('#price-max');
if (this.#price_range.isFree) {
min.innerHTML = ``;
max.innerHTML = `Za darmo`;
}
if (this.#price_range.isRange) {
min.innerHTML = `<price-view value="${ this.#price_range.min }"></price-view>`
max.innerHTML = `<price-view value="${ this.#price_range.max }"></price-view>`;
}
if (this.#price_range.isFixed) {
min.innerHTML = ``;
max.innerHTML = `<price-view value="${ this.#price_range.min }"></price-view>`;
}
return '';
}
matches(regex) {
return !!this.description.match(regex)
}
});

View File

@ -1,86 +0,0 @@
import { Component, BUTTON_STYLE } from "../shared.js";
import "../shared/search-input.js";
customElements.define('marketplace-offers', class extends Component {
static get observedAttributes() {
return ['account-id'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
h1 {
display: block;
text-align: center;
}
#offers {
display: block;
padding: 8px;
}
#publishSection {
display: none;
}
:host([account-id]) #publishSection {
display: block;
}
::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;
justify-content: space-between;
flex-wrap: wrap;
justify-items: stretch;
}
::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 }
</style>
<article>
<section id="publishSection">
<button id="publish" class="btn">Dodaj ogłoszenie</button>
</section>
<section><slot></slot></section>
<section id="search">
<search-input target="user-edit-offer, marketplace-offer"></search-input>
</section>
<section id="offers">
<slot name="offer"></slot>
</section>
</article>
`);
this.shadowRoot.querySelector('#publish').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
location.href = '/offers/new';
});
}
set account_id(v) {
this.setAttribute('account-id', v)
}
get account_id() {
return this.getAttribute('account-id');
}
});

View File

@ -1,189 +0,0 @@
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', 'free'];
}
constructor() {
super(`
<style>
:host { display: block; }
#description {
height: 200px;
width: 100%;
}
section {
padding: 8px;
}
:host([free]) #priceMinSection {
display: none;
}
@media only screen and (min-device-width: 1000px) {
section > form {
display: flex;
justify-content: space-between;
justify-items: stretch;
}
#imageSection {
margin-right: 16px;
}
#priceSection {
width: 300px;
margin-right: 16px;
}
#descriptionSection {
width: calc(100% - 550px);
margin-right: 16px;
}
}
${ FORM_STYLE }${ TIP_STYLE }
</style>
<section>
<form action="/offers/create" method="post">
<div id="imageSection">
<image-input send-original="true" width="800" height="800"
></image-input>
<input name="picture_url" id="picture_url" type="hidden" />
</div>
<div id="descriptionSection">
<label for="description">Opis</label>
<textarea
name="description"
id="description"
placeholder="Opisz przedmiot, który chcesz sprzedać"
></textarea>
</div>
<div id="priceSection">
<div id="priceMinSection">
<label>Cena</label>
<price-input id="priceMinUI" value="0"></price-input>
<input name="price_min" id="priceMin" type="hidden" value="0" />
</div>
<div>
<label>Za darmo</label>
<input type="checkbox" id="free" />
<input name="price_max" id="priceMax" type="hidden" value="0" />
</div>
</div>
<div>
<input id="submit" type="submit" value="Utwórz" />
<div>
<slot name="action"></slot>
</div>
</div>
</form>
</section>
`);
this.shadowRoot.querySelector('#priceMinUI').addEventListener('change', ev => {
ev.stopPropagation();
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.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() {
const v = parseInt(this.getAttribute('offer-id'));
return isNaN(v) ? null : v;
}
set offer_id(v) {
v = parseInt(v);
this.setAttribute('offer-id', v);
if (isNaN(v)) {
this.#removeId();
} else {
this.#addId(v)
}
}
get price_range_min() {
this.getAttribute('price-range-min');
}
set price_range_min(v) {
this.setAttribute('price-range-min', v);
this.shadowRoot.querySelector('#priceMinUI').value = v;
this.shadowRoot.querySelector('#priceMin').value = v;
}
get price_range_max() {
this.getAttribute('price-range-max');
}
set price_range_max(v) {
this.setAttribute('price-range-max', v);
// this.shadowRoot.querySelector('#priceMaxUI').value = v;
this.shadowRoot.querySelector('#priceMax').value = v;
}
get description() {
return this.getAttribute('description');
}
set description(v) {
this.setAttribute('description', v);
this.shadowRoot.querySelector('#description').value = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('#picture_url').value = v;
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 = '/offers/create';
form.querySelector('input[type=submit]').value = 'Utwórz';
}
#addId(v) {
this.#removeId();
const form = this.shadowRoot.querySelector('form');
const input = form.appendChild(document.createElement('input'));
input.setAttribute('type', 'hidden');
input.setAttribute('name', 'id');
input.setAttribute('id', 'offer-id');
input.value = v;
form.action = '/offers/update';
form.querySelector('input[type=submit]').value = 'Zmień';
}
});

View File

@ -1,159 +0,0 @@
import { Component, BUTTON_STYLE } from "../shared";
const MODES = { 'view': 'view', 'form': 'form' };
customElements.define('user-edit-offer', class extends Component {
static get observedAttributes() {
return ['mode', 'state', 'offer-id', 'description', 'picture-url', 'price-range-min', 'price-range-max'];
}
constructor() {
super(`
<style>
:host { display: block; }
#view, #form { display: none; }
: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;
}
#actions input {
width: 140px;
}
#state {
font-weight: bold;
margin-bottom: 8px;
display: block;
text-align: center;
}
@media only screen and (min-device-width: 1200px) {
:host([mode='view']) #view { display: flex; }
}
${ BUTTON_STYLE }
</style>
<section id="state"></section>
<section id="view">
<slot></slot>
</section>
<section id="actions">
<input type="button" value="Edytuj" id="edit" />
<form id="finishForm" action="/offers/finish" method="post">
<input type="hidden" name="id" class="id" value="" />
<input type="submit" slot="action" id="finish" value="Zakończ" />
</form>
</section>
<section id="form">
<offer-form>
<input type="button" slot="action" id="cancel" value="Anuluj" />
</offer-form>
</section>
`);
this.shadowRoot.querySelector('#edit').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
location.href = `/marketplace/${this.offer_id}/edit`
});
this.shadowRoot.querySelector('#cancel').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.mode = 'view';
});
}
get mode() {
const mode = this.getAttribute('mode');
return MODES[mode] || 'view';
}
set mode(mode) {
this.setAttribute('mode', MODES[mode] || 'view');
}
get offer_id() {
const v = parseInt(this.getAttribute('offer-id'));
return isNaN(v) ? null : v;
}
set offer_id(v) {
v = parseInt(v);
this.setAttribute('offer-id', v);
this.shadowRoot.querySelector('offer-form').offer_id = v;
for (const el of this.shadowRoot.querySelectorAll('.id'))
el.value = v;
}
get description() {
return this.getAttribute('description');
}
set description(v) {
this.setAttribute('description', v);
this.shadowRoot.querySelector('offer-form').description = v;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('offer-form').picture_url = v;
}
get price_range_min() {
this.getAttribute('price-range-min');
}
set price_range_min(v) {
this.setAttribute('price-range-min', v);
this.shadowRoot.querySelector('offer-form').price_range_min = v;
}
get price_range_max() {
this.getAttribute('price-range-max');
}
set price_range_max(v) {
this.setAttribute('price-range-max', v);
this.shadowRoot.querySelector('offer-form').price_range_max = v;
}
get state() {
return this.getAttribute('state');
}
set state(v) {
this.setAttribute('state', v);
const el = this.shadowRoot.querySelector('#state');
switch (v) {
case 'Pending': {
el.textContent = 'Oczekuje';
break;
}
case 'Approved': {
el.textContent = 'Zaakceptowane';
break;
}
case 'Banned': {
el.textContent = 'Odrzucone';
break;
}
case 'Finished': {
el.textContent = 'Zakończone';
break;
}
}
}
matches(regex) {
return !!this.description.match(regex)
}
});

View File

@ -1,130 +0,0 @@
import { BLOCK_QUOTE_STYLE, Component } from "../shared";
import "../shared/date-time";
customElements.define('news-article', class extends Component {
static get observedAttributes() {
return ["article-id", "article-title", "status", "body", "created-at", "published-at", "hide-status"]
}
constructor() {
super(`
<style>
:host { display: block; width: 100%; }
.time {
display: flex;
justify-content: space-between;
}
h1 {
display: flex;
}
h1 #status {
font-size: 14px;
}
.time span:first-child {
margin-right: 8px;
}
#title {
margin-right: 16px;
}
#body {
margin-bottom: 16px;
margin-top: 16px;
}
.time, date-time {
font-size: 12px;
font-style: italic;
}
:host([hide-status="true"]) #status {
display: none;
}
article {
margin: 8px;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
}
}
@media (min-width: 1200px) {
#time {
display: flex;
justify-content: space-between;
}
}
${ BLOCK_QUOTE_STYLE }
</style>
<article>
<h1>
<span id="title"></span>
<span id="status"></span>
</h1>
<section id="time">
<div class="time">
<span>Napisano:</span>
<date-time id="created_at" hide-date="false" hide-time="false">
</date-time>
</div>
<div class="time">
<span>Opublikowano:</span>
<date-time id="published_at" hide-date="false" hide-time="false">
</date-time>
</div>
</section>
<section id="body">
<slot></slot>
</section>
</article>
`);
}
get article_id() {
const id = parseInt(this.getAttribute('article-id'));
return isNaN(id) ? null : id;
}
set article_id(v) {
this.setAttribute('article-id', v);
}
get article_title() {
return this.getAttribute('article-title');
}
set article_title(v) {
this.setAttribute('article-title', v);
this.shadowRoot.querySelector('#title').textContent = v;
}
get status() {
return this.getAttribute('status');
}
set status(v) {
this.setAttribute('status', v);
this.shadowRoot.querySelector('#status').textContent = v;
}
get created_at() {
return this.getAttribute('created-at');
}
set created_at(v) {
this.shadowRoot.querySelector('#created_at').datetime = v;
}
get published_at() {
return this.getAttribute('published-at');
}
set published_at(v) {
this.shadowRoot.querySelector('#published_at').datetime = v;
}
get hide_status() {
return this.getAttribute('hide-status') === 'true';
}
set hide_status(v) {
this.setAttribute('hide-status', v);
}
});

View File

@ -1,12 +0,0 @@
import { Component } from "../shared"
customElements.define('ow-articles', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
</style>
<slot></slot>
`);
}
});

View File

@ -1,209 +0,0 @@
import { Component, FORM_STYLE, BUTTON_STYLE } from "../shared";
customElements.define('account-view', class extends Component {
static get observedAttributes() {
return ['facebook-id', 'id', 'name', 'email', 'register-success']
}
constructor() {
super(`
<style>
:host {
display: block;
}
:host(:not([facebook-id = ""])) #fb-id {
display: block;
}
:host([facebook-id = ""]) #fb-id {
display: none;
}
:host([facebook-id = ""]) #fb-button {
display: flex;
padding: .375rem .75rem;
}
#register-success {
display: none;
text-align: center;
color: darkgreen;
}
:host([register-success]) #register-success {
display: block;
}
#fb-button {
display: none;
}
facebook-button {
margin-right: 16px;
}
article {
margin: 8px;
}
#logout {
border-color: var(--red-color);
background: var(--red-color);
color: white;
}
#logoutSection {
margin-bottom: 16px;
}
section input[type="button"],
section input[type="button"],
section input[type="submit"] {
width: 100%;
}
section input[type="submit"],
section a {
color: black;
}
::slotted(#editService) {
padding-bottom: 0 !important;
padding-top: 0 !important;
display: block !important;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
}
#rules {
display: flex;
justify-content: space-between;
}
#rules a {
display: block;
min-width: calc(120px - 1.5rem);
width: auto;
}
input {
min-width: 120px;
width: auto;
}
::slotted(#editService) {
display: inline-block !important;
}
}
::slotted(button) {
cursor: pointer;
position: relative;
display: inline-block;
text-align: center;
vertical-align: middle;
user-select: none;
height: calc(1.5em + 0.75rem + 2px);
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
width: 100%;
}
#editServiceSection {
margin-bottom: 16px;
width: 100%;
}
label {
width: 120px;
}
.input {
margin-bottom: 16px;
}
${ FORM_STYLE }${ BUTTON_STYLE }
</style>
<article>
<h3 id="register-success">Konto utworzone!</h3>
<section>
<input id="id" name="id" readonly type="hidden" />
</section>
<section class="input">
<label>Login</label>
<input id="name" name="name" readonly />
</section>
<section class="input">
<label>E-Mail</label>
<input id="email" name="email" readonly />
</section>
<section id="fb-button">
<facebook-button width="100">
<p>Powiąż z kontem Facebook</p>
</facebook-button>
</section>
<section id="fb-id">
<label>Powiązane konto Facebook</label>
<input id="facebook_id" name="facebook_id" readonly />
</section>
<section id="editServiceSection">
<slot name="editService"></slot>
</section>
<section id="logoutSection">
<form action="/logout" method="post">
<input id="logout" value="Wyloguj" type="submit" />
</form>
</section>
<section id="rules">
<a class="" href="/terms-and-condition" target="_blank">Regulamin</a>
<a class="" href="/privacy-policy" target="_blank">Polityka prywatności</a>
</section>
</article>
`);
this.addEventListener('facebook:available', ev => {
ev.preventDefault();
ev.stopPropagation();
});
{
const input = this.querySelector('#editService');
if (input) input.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
location.href = "/account/business-items";
})
}
}
connectedCallback() {
super.connectedCallback();
this.register_success = (location.search || '').includes('success');
localStorage.removeItem('register');
}
get name() {
return this.getAttribute('name') || '';
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('#name').value = v;
}
get email() {
return this.getAttribute('email') || '';
}
set email(v) {
this.setAttribute('email', v);
this.shadowRoot.querySelector('#email').value = v;
}
get facebook_id() {
return this.getAttribute('facebook-id');
}
set facebook_id(v) {
this.setAttribute('facebook-id', v);
this.shadowRoot.querySelector('#facebook_id').value = v;
}
get register_success() {
return this.getAttribute('register-success') === 'true'
}
set register_success(v) {
if (v === true || v === 'true')
this.setAttribute('register-success', 'true');
else
this.removeAttribute('register-success');
}
});

View File

@ -1,101 +0,0 @@
import { Component, BUTTON_STYLE, Router } from "../shared";
customElements.define('ow-account', class extends Component {
static get observedAttributes() {
return ['mode']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
#switch-register, #switch-login {
display: none;
}
:host([mode="login"]) #switch-register {
display: block !important;
}
:host([mode="register"]) #switch-login {
display: block !important;
}
input {
display: block;
cursor: pointer;
}
section > input[type=button] {
width: 100%;
max-width: 100%;
}
@media only screen and (min-device-width: 1000px) {
}
${ BUTTON_STYLE }
</style>
<article>
<slot></slot>
<section id="switch-register">
<input type="button" class="btn" value="Nie masz konta? Utwórz nowe" />
</section>
<section id="switch-login">
<input type="button" class="btn" value="Posiadasz konto? Zaloguj się" />
</section>
</article>
`);
this.shadowRoot.querySelector('#switch-login > input').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
Router.goTo('/login');
});
this.shadowRoot.querySelector('#switch-register > input').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
Router.goTo('/register/account-type');
});
this.addEventListener('facebook:account', ev => {
ev.stopPropagation();
ev.preventDefault();
});
}
connectedCallback() {
super.connectedCallback();
this.listenHistory(Router.historyDetails());
}
listenHistory = ({ parts }) => {
switch (parts.first) {
case 'register': {
if (this.mode === 'register')
return;
this.mode = 'register';
break;
}
default: {
if (this.mode === 'login')
return;
this.mode = 'login';
break;
}
}
}
get mode() {
return this.getAttribute('mode') || '';
}
set mode(value) {
value = ['login', 'register', 'display'].includes(value) ? value : 'login';
this.setAttribute('mode', value);
switch (value) {
case 'register': {
this.innerHTML = `<register-form></register-form>`;
break;
}
default:
this.innerHTML = `<login-form></login-form>`;
break;
}
}
});

View File

@ -1,46 +0,0 @@
Object.defineProperties(Object.prototype, {
'entry': {
value(key) {
const owner = this;
return {
owner, orElse(v) {
if (owner[key] === undefined) owner[key] = v;
return owner[key];
}, get() {
return owner[key];
}
}
}
}
})
Object.defineProperties(Array.prototype, {
'last': {
get() {
return this[this.length - 1]
}
},
'tail': {
get() {
return this[this.length - 1]
}
},
'first': {
get() {
return this[0]
}
},
'head': {
get() {
return this[0]
}
},
'any': {
value(cb) {
for (const el of this) {
if (cb(el)) return true;
}
return false;
}
}
})

View File

@ -1,276 +0,0 @@
import { FORM_STYLE, Component, Router } from "./shared.js";
import { RegisterForm } from "./register-form/model.js";
customElements.define('register-form', class extends Component {
#form = new RegisterForm;
static get observedAttributes() {
return ['current']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
.actions {
display: flex;
justify-content: space-between;
}
.actions > input:first-child {
margin-right: 8px;
}
.actions > input:last-child {
margin-left: 8px;
}
#step-4 > #copied {
display: none;
}
article {
margin: 8px;
}
@media only screen and (min-device-width: 1000px) {
article {
margin: 0;
}
}
${ FORM_STYLE }
</style>
<article>
<slot></slot>
</article>
`);
this.shadowRoot.addEventListener('form:next', ev => {
ev.stopPropagation();
this.#transfer('next');
});
this.shadowRoot.addEventListener('form:prev', ev => {
ev.stopPropagation();
this.#transfer('prev');
});
this.shadowRoot.addEventListener('account:type', ev => {
ev.stopPropagation();
this.#copyDetail(ev);
});
this.addEventListener('account:basic', ev => {
this.#copyDetail(ev);
});
this.addEventListener('account:business', ev => {
this.#copyDetail(ev);
});
this.addEventListener('account:items', ev => {
this.#copyDetail(ev);
});
this.addEventListener('account:contacts', ev => {
this.#copyDetail(ev);
});
this.#loadCache();
}
#loadCache() {
let register = {};
try {
register = JSON.parse(localStorage.getItem('register') || '{}');
} catch (e) {
localStorage.removeItem('register');
}
this.#form.from(register);
}
#copyDetail(ev) {
ev.stopPropagation();
for (const [key, value] of Object.entries(ev.detail)) {
this.#form[key] = value;
}
localStorage.setItem('register', JSON.stringify(this.#form.payload));
}
listenHistory = ({ parts }) => {
this.current = parts.last;
}
#host(body) {
this.innerHTML = body;
}
#showAccountTypeForm() {
this.#host(`
<register-account-type></register-account-type>
`);
}
#showUserAccountForm() {
this.#host(`
<register-user-account-form
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
></register-user-account-form>
`);
}
#showBusinessAccountForm() {
this.#host(`
<register-business-account-form
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
></register-business-account-form>
`);
}
#showBusinessDetailsForm() {
this.#host(`
<register-business-details-form
name="${ this.#form.name || '' }"
description="${ this.#form.description || '' }"
></register-business-details-form>
`);
}
#showBusinessItemsForm() {
this.#host(`
<register-business-items-form>
${ this.#form.items.map(
({ name, price, picture_url }) => `
<register-business-item-form
name="${ name }"
price="${ price }"
picture-url="${ picture_url }"
></register-business-item-form>
`
).join('') }
</register-business-items-form>
`);
}
#showBusinessContactsForm() {
this.#host(`
<register-business-contacts-form>
${ this.#form.contacts.map(
({ type, content }) => `
<edit-contact-info
mode="view"
delete="false"
>
<contact-info
type="${ type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`).join('') }
</register-business-contacts-form>
`);
}
#showBusinessSubmitForm() {
this.#host(`
<register-business-submit-form
name="${ this.#form.name }"
description="${ this.#form.description }"
login="${ this.#form.login }"
email="${ this.#form.email }"
password="${ this.#form.password }"
account-type="${ this.#form.account_type }"
>
${ this.#form.items.map(
({ name, price, picture_url }) => `
<local-business-item
slot="items"
name="${ name }"
price="${ price }"
picture-url="${ picture_url }"
></local-business-item>
`
).join('') }
${ this.#form.contacts.map(
({ type, content }) => `
<contact-info
slot="contacts"
type="${ type }"
content="${ content }"
></contact-info>
`).join('') }
</register-business-submit-form>
`);
}
#transfer(direction) {
const current = direction === 'next'
? this.#nextPage()
: this.#prevPage();
this.current = current;
Router.goTo(`/register/${ current }`);
}
get current() {
return this.getAttribute('current');
}
set current(current) {
if (!current) return;
this.setAttribute('current', current);
switch (current) {
case "account-type":
return this.#showAccountTypeForm();
case "user-account":
return this.#showUserAccountForm();
case "business-account":
return this.#showBusinessAccountForm();
case "business-details":
return this.#showBusinessDetailsForm();
case "business-items":
return this.#showBusinessItemsForm();
case "business-contacts":
return this.#showBusinessContactsForm();
case "business-submit":
return this.#showBusinessSubmitForm();
default:
throw new Error(`Unknown page "${ current }"`);
}
}
#nextPage() {
switch (this.current) {
case "account-type":
return this.#form.account_type === 'User'
? 'user-account'
: 'business-account';
case "user-account":
return null;
case "business-account":
return 'business-details';
case "business-details":
return 'business-items';
case "business-items":
return 'business-contacts';
case "business-contacts":
return 'business-submit';
case "business-submit":
return null;
}
}
#prevPage() {
switch (this.current) {
case "account-type":
return null;
case "user-account":
return 'account-type';
case "business-account":
return 'account-type';
case "business-details":
return 'business-account';
case "business-items":
return 'business-details';
case "business-contacts":
return 'business-items';
case "business-submit":
return 'business-contacts';
}
}
});

View File

@ -1,183 +0,0 @@
import { PseudoForm } from "../shared.js";
export class RegisterFormComponent extends PseudoForm {
#mounted = false;
get submitEventName() {
return null;
}
mountFormHandler(dispatchForm) {
if (this.#mounted) return;
this.#mounted = true;
if (!dispatchForm) dispatchForm = () => this.#dispatchForm();
const form = this.shadowRoot.querySelector('form');
form.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
this.shadowRoot.querySelector('form-navigation').next();
});
this.addEventListener('form:next', ev => {
if (form.reportValidity()) {
dispatchForm()
} else {
ev.stopPropagation();
ev.preventDefault();
}
});
}
#dispatchForm() {
const detail = [...this.elements].filter(el => el.name && el.name.length).reduce((memo, el) => ({
...memo,
[el.name]: el.value,
}), {});
this.dispatchEvent(new CustomEvent(this.submitEventName, { composed: true, bubbles: true, detail }));
}
get elements() {
return this.shadowRoot.querySelector('form').elements;
}
}
export class RegisterForm {
#email = ''; //: String,
#login = ''; //: String,
#password = ''; //: String,
#facebook_id = null; //: Option<String>,
#account_type = 'User'; //: db::AccountType,
#items = null; //: Option<Vec<view::BusinessItemInput>>,
#contacts = null; //: Option<Vec<view::CreateContactInfoInput>>,
#name = null; //: Option<String>,
#description = null; //: Option<String>,
get email() {
return this.#email;
}
set email(v) {
this.#email = v;
}
get login() {
return this.#login;
}
set login(v) {
this.#login = v;
}
get pass() {
return this.#password;
}
set pass(v) {
this.#password = v;
}
get password() {
return this.#password;
}
set password(v) {
this.#password = v;
}
get facebook_id() {
return this.#facebook_id;
}
set facebook_id(v) {
this.#facebook_id = v;
}
get account_type() {
return this.#account_type;
}
set account_type(v) {
this.#account_type = v;
switch (v) {
case 'User' : {
this.#items = null;
this.#contacts = null;
break;
}
}
}
get items() {
return this.#items || [];
}
set items(a) {
this.#items = a;
}
get contacts() {
return this.#contacts || [];
}
set contacts(c) {
this.#contacts = c;
}
get name() {
return this.#name;
}
set name(v) {
this.#name = v;
}
get description() {
return this.#description;
}
set description(v) {
this.#description = v;
}
get payload() {
return [
'email',
'login',
'password',
'facebook_id',
'account_type',
'items',
'contacts',
'name',
'description'
].reduce((m, k) => {
const v = this[k];
if (v === undefined || v === null) {
return m;
}
m[k] = v;
return m;
}, {})
}
from(object = {}) {
object = object || {};
[
'email',
'login',
'password',
'facebook_id',
'account_type',
'items',
'contacts',
'name',
'description'
].forEach(key => {
const value = object[key];
if (!value) return;
this[key] = value;
});
}
}

View File

@ -1,138 +0,0 @@
import { Component, BUTTON_STYLE, TIP_STYLE } from "../shared";
customElements.define('register-account-type', class extends Component {
constructor() {
super(`
<style>
:host {
display: block;
}
ul, li {
list-style: none;
margin: 0;
padding: 0;
}
ul {
display: flex;
justify-content: space-between;
}
ul > li {
min-width: 48%;
}
a {
display: block;
padding: 16px;
text-align: center;
cursor: pointer;
color: var(--border-slim-color);
margin: 8px;
}
svg, path {
fill: var(--border-slim-color) !important;
}
svg {
width: 96px;
height: 96px;
}
button#accept-terms {
width: 80%;
height: auto;
display: block;
margin: auto auto 16px;
}
button#accept-terms > a {
border: none;
text-decoration: underline;
}
#rules {
display: flex;
justify-content: space-between;
}
@media only screen and (min-device-width: 1000px) {
svg {
width: 200px;
height: 200px;
}
#rules > a {
display: block;
font-size: 20px;
}
}
${ BUTTON_STYLE }${ TIP_STYLE }
</style>
<article>
<section id="rules">
<a target="_blank" href="/terms-and-condition">
Regulamin
</a>
<a target="_blank" href="/privacy-policy">
Polityka prywatności
</a>
</section>
<button id="accept-terms">
Zapoznałem się i zgadzam się
</button>
</article>
`);
const article = this.shadowRoot.querySelector('article');
article.querySelector('button').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#termsAccepted();
})
}
#termsAccepted() {
const article = this.shadowRoot.querySelector('article');
article.innerHTML = `
<ul>
<li>
<a id="user">
<svg viewBox="0 0 15 15" fill="none" xmlns="http://www.w3.org/2000/svg" xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" d="M7.5.875a3.625 3.625 0 0 0-1.006 7.109c-1.194.145-2.218.567-2.99 1.328-.982.967-1.479 2.408-1.479 4.288a.475.475 0 1 0 .95 0c0-1.72.453-2.88 1.196-3.612.744-.733 1.856-1.113 3.329-1.113s2.585.38 3.33 1.113c.742.733 1.195 1.892 1.195 3.612a.475.475 0 1 0 .95 0c0-1.88-.497-3.32-1.48-4.288-.77-.76-1.795-1.183-2.989-1.328A3.627 3.627 0 0 0 7.5.875ZM4.825 4.5a2.675 2.675 0 1 1 5.35 0 2.675 2.675 0 0 1-5.35 0Z" fill="currentColor"/>
</svg>
<div>Użytkownik</div>
<div class="tip">Zwykły użytkownik z opcją wystawiania niepotrzebnych prywatnych rzeczy na sprzedaż</div>
</a>
</li>
<li>
<a id="local-service">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 489.4 489.4" xml:space="preserve">
<path d="M347.7 263.75h-66.5c-18.2 0-33 14.8-33 33v51c0 18.2 14.8 33 33 33h66.5c18.2 0 33-14.8 33-33v-51c0-18.2-14.8-33-33-33zm9 84c0 5-4.1 9-9 9h-66.5c-5 0-9-4.1-9-9v-51c0-5 4.1-9 9-9h66.5c5 0 9 4.1 9 9v51z"/>
<path d="M489.4 171.05c0-2.1-.5-4.1-1.6-5.9l-72.8-128c-2.1-3.7-6.1-6.1-10.4-6.1H84.7c-4.3 0-8.3 2.3-10.4 6.1l-72.7 128c-1 1.8-1.6 3.8-1.6 5.9 0 28.7 17.3 53.3 42 64.2v211.1c0 6.6 5.4 12 12 12h381.3c6.6 0 12-5.4 12-12v-209.6c0-.5 0-.9-.1-1.3 24.8-10.9 42.2-35.6 42.2-64.4zM91.7 55.15h305.9l56.9 100.1H34.9l56.8-100.1zm256.6 124c-3.8 21.6-22.7 38-45.4 38s-41.6-16.4-45.4-38h90.8zm-116.3 0c-3.8 21.6-22.7 38-45.4 38s-41.6-16.4-45.5-38H232zm-207.2 0h90.9c-3.8 21.6-22.8 38-45.5 38-22.7.1-41.6-16.4-45.4-38zm176.8 255.2h-69v-129.5c0-9.4 7.6-17.1 17.1-17.1h34.9c9.4 0 17.1 7.6 17.1 17.1v129.5h-.1zm221.7 0H225.6v-129.5c0-22.6-18.4-41.1-41.1-41.1h-34.9c-22.6 0-41.1 18.4-41.1 41.1v129.6H66v-193.3c1.4.1 2.8.1 4.2.1 24.2 0 45.6-12.3 58.2-31 12.6 18.7 34 31 58.2 31s45.5-12.3 58.2-31c12.6 18.7 34 31 58.1 31 24.2 0 45.5-12.3 58.1-31 12.6 18.7 34 31 58.2 31 1.4 0 2.7-.1 4.1-.1v193.2zm-4.1-217.1c-22.7 0-41.6-16.4-45.4-38h90.9c-3.9 21.5-22.8 38-45.5 38z"/>
</svg>
<div>Usługodawca</div>
<div class="tip">Usługodawca posiadający stałe usługi lub produkty w ofercie</div>
</a>
</li>
</ul>
<form-navigation style="display: none;"></form-navigation>
`;
const user = this.shadowRoot.querySelector('#user');
user.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('account:type', {
bubbles: true,
composed: true,
detail: { account_type: 'User' }
}));
this.shadowRoot.querySelector('form-navigation').next();
});
const service = this.shadowRoot.querySelector('#local-service');
service.addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('account:type', {
bubbles: true,
composed: true,
detail: { account_type: 'Business' }
}));
this.shadowRoot.querySelector('form-navigation').next();
});
}
});

View File

@ -1,57 +0,0 @@
import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model";
customElements.define('register-business-account-form', class extends RegisterFormComponent {
static get observedAttributes() {
return ['login', 'password', 'email']
}
constructor() {
super(`
<style>
:host {
display: block;
}
${ FORM_STYLE }
</style>
<form id="step-1">
<div>
<label>Login</label>
<input id="login" name="login" placeholder="Login" type="text" required autofocus />
</div>
<div>
<label>E-Mail</label>
<input id="email" name="email" placeholder="Email" type="email" required />
</div>
<div>
<label>Hasło</label>
<input id="password" name="pass" placeholder="Hasło" type="password" required />
</div>
<input type="submit" style="display: none">
<form-navigation prev="hidden" next="right"></form-navigation>
</form>
`);
this.mountFormHandler();
}
get submitEventName() {
return 'account:basic';
}
set email(v) {
this.#input('email').value = v;
}
set login(v) {
this.#input('login').value = v;
}
set password(v) {
this.#input('password').value = v;
}
#input(id) {
return this.shadowRoot.querySelector(`#${ id }`);
}
});

View File

@ -1,90 +0,0 @@
import { BUTTON_STYLE } from "../shared.js";
import { RegisterFormComponent } from "./model.js";
customElements.define('register-business-contacts-form', class extends RegisterFormComponent {
constructor() {
super(`
<style>
:host { display: block; }
section {
display: flex;
justify-content: space-between;
}
contact-info-editor > input[type='button'] {
width: 100%;
max-width: 100%;
}
::slotted(edit-contact-info) {
margin-bottom: 16px;
}
#form {
margin-bottom: 16px;
}
${ BUTTON_STYLE }
</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
save="false"
>
<input type="button" id="addButton" value="Dodaj" />
</contact-info-editor>
</section>
<slot></slot>
<form-navigation></form-navigation>
</form>
</article>
`);
const editor = this.shadowRoot.querySelector('contact-info-editor');
editor.addEventListener('submit', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#addContact(editor);
});
editor.querySelector('#addButton').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#addContact(editor);
});
this.mountFormHandler(() => this.#emitChange());
}
get submitEventName() {
return 'account:contacts';
}
get #rows() {
return Array.from(this.querySelectorAll('contact-info'));
}
#addContact(editor) {
const { type, content } = editor;
this.innerHTML += `
<edit-contact-info mode="view" delete="false">
<contact-info
type="${ type }"
content="${ content }"
></contact-info>
</edit-contact-info>
`;
}
#emitChange() {
const rows = this.#rows;
const contacts = rows.map(({ type, content }) => ({
type,
content
}));
this.dispatchEvent(new CustomEvent(this.submitEventName, {
bubbles: true, composed: true, detail: { contacts: contacts.length ? contacts : null },
}));
}
});

View File

@ -1,53 +0,0 @@
import { FORM_STYLE, TIP_STYLE } from "../shared";
import { RegisterFormComponent } from "./model";
customElements.define('register-business-details-form', class extends RegisterFormComponent {
static get observedAttributes() {
return ["name", "description"];
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
textarea { min-height: 200px; }
form > div > input,
form > div > textarea {
width: 100%;
}
${ FORM_STYLE }${ TIP_STYLE }
</style>
<form id="step-2">
<div>
<label>Nazwa usługodawcy</label>
<input id="name" name="name" placeholder="Nazwa usługi" type="text" required autofocus />
</div>
<div>
<label>Opis usługodawcy</label>
<textarea id="description" name="description" required></textarea>
<div class="tip">Produkty dodawane w nastepnym kroku</div>
</div>
<form-navigation></form-navigation>
</form>
`);
this.mountFormHandler();
}
get submitEventName() {
return 'account:business';
}
set name(v) {
this.#input('name').value = v;
}
set description(v) {
this.#input('description').value = (v || '').trim();
}
#input(id) {
return this.shadowRoot.querySelector(`#${ id }`);
}
})

View File

@ -1,204 +0,0 @@
import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model";
customElements.define('register-business-item-form', class extends RegisterFormComponent {
static get observedAttributes() {
return ['idx', 'name', 'price', 'picture-url', 'action', 'remove', 'save']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
section > form input[type=button], section > form input[type=submit] {
width: 100%;
max-width: 100%;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
@media only screen and (min-device-width: 1200px) {
section > form {
display: flex;
justify-content: space-between;
}
section > form input[type=button], section > form input[type=submit] {
width: 25%;
max-width: 25%;
}
form > #fields {
width: 50%;
}
form > #fields > div, form > #fields > div > label, form > #fields > div > .input {
width: 90% !important;
}
}
img[src=""] { display: none; }
${ FORM_STYLE }
</style>
<section>
<form method="post">
<slot name="head"></slot>
<image-input send-original="true"></image-input>
<div id="fields">
<div id="name">
<label>Nazwa</label>
<input id="name" class="item-name" name="name" type="text" required />
</div>
<div id="priceWrapper">
<label>Cena</label>
<price-input id="price" class="item-price" name="price" required >
</price-input>
</div>
</div>
<input id="submit-button" type="submit" value="Zapisz" />
<input id="remove-button" type="submit" value="Usuń" />
<input type="hidden" name="picture_url" id="picture_url" />
<slot name="tail"></slot>
</form>
</section>
`);
const imageInput = this.shadowRoot.querySelector('image-input');
this.addEventListener('item:removed', () => {
this.setAttribute('removed', 'removed');
const parent = this.parentElement;
this.remove();
parent.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: true }));
});
this.addEventListener('image-input:uploaded', ev => {
ev.preventDefault();
ev.stopPropagation();
this.picture_url = imageInput.url;
});
this.shadowRoot.querySelector('form').addEventListener('change', ev => {
ev.stopPropagation();
const input = ev.target;
const { name, value } = input;
switch (name) {
case 'price': {
this.price = parseInt(value);
break;
}
case 'name': {
this.name = value;
break;
}
case 'picture_url': {
this.picture_url = value;
break;
}
default: {
break;
}
}
});
this.shadowRoot.querySelector('form').addEventListener('submit', ev => {
ev.stopPropagation();
ev.preventDefault();
if (this.reportValidity()) {
const detail = {
name: this.name,
price: this.price,
picture_url: this.picture_url,
item_order: this.idx,
};
this.dispatchEvent(new CustomEvent('item:submit', { bubbles: true, composed: true, detail }));
}
});
this.shadowRoot.querySelector('#remove-button').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.dispatchEvent(new CustomEvent('item:removed', { bubbles: true, composed: false }));
});
}
get submitEventName() {
return 'account:items';
}
static attr2Field(name) {
const field = super.attr2Field(name);
if (field === 'remove') return 'showRemove';
if (field === 'save') return 'showSave';
return field;
}
get idx() {
const idx = parseInt(this.getAttribute('idx'));
return isNaN(idx) ? null : idx;
}
set idx(idx) {
this.setAttribute('idx', idx);
}
get name() {
return this.getAttribute('name');
}
set name(v) {
this.setAttribute('name', v);
this.shadowRoot.querySelector('.item-name').value = v;
}
get price() {
return this.getAttribute('price');
}
set price(v) {
v = parseInt(v);
this.setAttribute('price', v);
this.shadowRoot.querySelector('#price').value = v / 100.0;
}
get picture_url() {
return this.getAttribute('picture-url');
}
set picture_url(v) {
if (!(v || '').startsWith("/")) v = '';
this.setAttribute('picture-url', v);
this.shadowRoot.querySelector('image-input').url = v;
this.shadowRoot.querySelector('#picture_url').value = v;
}
get action() {
return this.shadowRoot.querySelector('form').action;
}
set action(v) {
this.shadowRoot.querySelector('form').action = v;
}
get showRemove() {
return this.getAttribute('remove') !== 'hidden';
}
set showRemove(v) {
this.setAttribute('remove', v === 'show' || v === true ? 'show' : 'hidden');
this.shadowRoot.querySelector('#remove-button').setAttribute('type', this.showRemove ? 'submit' : 'hidden');
}
get showSave() {
return this.getAttribute('save') === 'show';
}
set showSave(v) {
this.setAttribute('save', v === 'show' || v === true ? 'show' : 'hidden');
this.shadowRoot.querySelector('#submit-button').setAttribute('type', this.showSave ? 'submit' : 'hidden');
}
reportValidity() {
return super.reportValidity() && this.shadowRoot.querySelector('price-input').reportValidity();
}
});
const extract = ({ name, value }) => ({ name, value })

View File

@ -1,78 +0,0 @@
import { FORM_STYLE } from "../shared";
import { RegisterFormComponent } from "./model";
const updateItems = (rows) => {
let idx = 0;
for (const el of rows) {
el.idx = idx++;
}
return idx;
}
customElements.define('register-business-items-form', class extends RegisterFormComponent {
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
::slotted(section) {
display: flex;
}
::slotted(register-business-item-form) {
margin-bottom: 18px;
}
form input[type=button], form input[type=submit] {
width: 100%;
max-width: 100%;
}
${ FORM_STYLE }
</style>
<form>
<article id="items">
<slot></slot>
</article>
<div>
<input type="button" id="add-item" value="Dodaj usługę/produkt" />
</div>
<input type="submit" style="display: none" />
<form-navigation></form-navigation>
</form>
`);
this.addEventListener('item:removed', ev => {
ev.stopPropagation();
updateItems(this.#rows)
});
this.addEventListener('form:next', ev => {
updateItems(this.#rows);
for (const el of this.#rows) {
if (!el.reportValidity()) {
ev.stopPropagation();
ev.preventDefault();
}
}
});
this.shadowRoot.querySelector('#add-item').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.appendChild(document.createElement('register-business-item-form'));
updateItems(this.#rows);
});
this.mountFormHandler(() => this.#emitChange());
}
get submitEventName() {
return 'account:items';
}
get #rows() {
return Array.from(this.querySelectorAll("register-business-item-form"));
}
#emitChange() {
const items = this.#rows.map(({ price, picture_url, name }) => ({ price, picture_url, name }));
this.dispatchEvent(new CustomEvent(this.submitEventName, { bubbles: true, composed: true, detail: { items } }));
}
});

View File

@ -1,173 +0,0 @@
import { FORM_STYLE, PseudoForm } from "../shared.js";
import { ErrorMessage } from "../shared/error-message.js";
import * as api from "../api.js";
customElements.define('register-business-submit-form', class extends PseudoForm {
static get observedAttributes() {
return ['name', 'description', 'login', 'email', 'password', 'account-type']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
${ FORM_STYLE }
img[src=''] { display: none; }
input[type=submit] {
width: 100%;
max-width: 100%;
}
form > .field, form > .field > label, form > .field > input {
display: block;
width: 100%;
}
@media only screen and (min-device-width: 1000px) {
form > .field {
display: flex;
}
form > .field > label {
min-width: 200px;
}
form > .field > input {
align-self: stretch;
width: calc(100% - 220px);
}
.item-view {
display: flex;
justify-content: space-between;
}
.item-view > * {
min-width: 100px;
max-width: 48%;
}
}
</style>
<form method="post" action="/register">
<div class="field">
<label>Login</label>
<input readonly id="login">
</div>
<div class="field">
<label>Email</label>
<input readonly id="email">
</div>
<div class="field">
<label>Password</label>
<input readonly id="password" type="password">
</div>
<div class="field">
<label>Name</label>
<input readonly id="name">
</div>
<div class="field">
<label>Description</label>
<input readonly id="description">
</div>
<input type="hidden" name="account_type" id="account_type" value="Business" />
<div id="contacts">
<h3>Dane kontaktowe</h3>
<slot name="contacts"></slot>
</div>
<div id="items">
<h3>Produkty/usługi</h3>
<slot name="items"></slot>
</div>
<div class="actions">
<form-navigation next="hidden"></form-navigation>
<input type="submit" value="Utwórz konto" />
</div>
</form>
`);
this.shadowRoot.querySelector('form').addEventListener('submit', async (ev) => {
ev.stopPropagation();
ev.preventDefault();
const { error } = await api.register(this.#form);
console.info(error);
if (!error) {
// Router.goTo("/account?success");
location.href = '/account?success';
} else {
ErrorMessage.errorMessage = error;
}
});
}
set account_type(v) {
this.shadowRoot.querySelector('#account_type').value = v;
}
get name() {
return this.#hidden('name').value
}
set name(v) {
this.#hidden('name').value = v;
}
get description() {
return this.#hidden('description').value
}
set description(v) {
this.#hidden('description').value = v;
}
get login() {
return this.#hidden('login').value
}
set login(v) {
this.#hidden('login').value = v;
}
get email() {
return this.#hidden('email').value
}
set email(v) {
this.#hidden('email').value = v;
}
get password() {
return this.#hidden('password').value
}
set password(v) {
this.#hidden('password').value = v;
}
#hidden(selector) {
return this.shadowRoot.querySelector(`#${ selector }`)
}
get #form() {
return [
'login',
'email',
'password',
'name',
'description',
'account_type',
].reduce((memo, name) => ({
...memo,
[name]: this.#hidden(name).value,
}), {
items: Array.from(this.querySelectorAll('local-business-item')).map(({ name, price, picture_url }) => ({
name, price, picture_url
})),
contacts: Array.from(this.querySelectorAll('contact-info')).map(({ content, type }) => ({
content,
type
})),
});
}
get payload() {
return this.#form
}
});

View File

@ -1,159 +0,0 @@
import { FORM_STYLE, Router } from "../shared.js";
import { RegisterFormComponent } from "./model.js";
import { ErrorMessage } from "../shared/error-message";
customElements.define('register-user-account-form', class extends RegisterFormComponent {
static get observedAttributes() {
return ['mode', 'login', 'password', 'email']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
svg > path {
fill: var(--border-slim-color);
}
.option {
display: block;
border-radius: 16px;
padding: 16px;
text-align: center;
cursor: pointer;
color: var(--border-slim-color);
margin: 8px;
}
#or {
text-align: center;
}
@media only screen and (min-device-width: 1200px) {
#formSection svg {
width: 200px;
display: inline-block;
}
#social {
display: flex;
justify-content: start;
}
#social > div {
margin-right: 16px;
}
facebook-button {
width: 64px;
}
facebook-button[fb-sdk="false"] {
width: auto;
}
}
${ FORM_STYLE }
</style>
<article>
<h2>Utwórz konto</h2>
<section id="formSection">
<form id="form" method="post" action="/register">
<input type="hidden" id="account_type" name="account_type" value="User">
<input type="hidden" id="facebook_id" name="facebook_id">
<div>
<label>E-Mail</label>
<input id="email" type="email" name="email">
</div>
<div>
<label>Login</label>
<input id="login" type="text" name="login">
</div>
<div>
<label>Hasło</label>
<input id="password" type="password" name="password">
</div>
<div>
<input type="submit" value="Zarejestruj" />
</div>
</form>
</section>
<section id="social">
<div id="or">lub zaloguj się za pomocą:</div>
<facebook-button></facebook-button>
</section>
</article>
`);
const form = this.shadowRoot.querySelector('form');
this.addEventListener('facebook:account', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'facebook';
const { id, name, email } = ev.detail;
form.querySelector('#email').value = email;
form.querySelector('#login').value = name;
form.querySelector('#password').value = crypto.randomUUID();
form.querySelector('#facebook_id').value = id;
form.querySelector('#account_type').value = 'User';
form.submit();
});
form.addEventListener('submit', async ev => {
ev.stopPropagation();
ev.preventDefault();
const json = Array.from(form.elements).filter(el => el.name && el.name.trim().length).reduce((memo, {
name,
value
}) => ({
...memo,
[name]: value.trim(),
}), {});
const res = await fetch('/register', {
method: 'POST',
body: JSON.stringify(json),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
}
});
if (res.ok) {
// Router.goTo("/account?success");
location.href ='/account?success';
} else {
const { error } = await res.json();
ErrorMessage.errorMessage = error;
}
});
}
get submitEventName() {
return 'account:user:details';
}
connectedCallback() {
super.connectedCallback();
this.mode = 'email';
}
get mode() {
return this.getAttribute('mode') || ''
}
set mode(v) {
this.setAttribute('mode', v);
}
set email(v) {
this.#input('email').value = v;
}
set login(v) {
this.#input('login').value = v;
}
set password(v) {
this.#input('password').value = v;
}
#input(id) {
return this.shadowRoot.querySelector(`#${ id }`);
}
});

View File

@ -1,328 +0,0 @@
export const BUTTON_STYLE = `
input[type="button"], input[type="submit"], button {
cursor: pointer;
position: relative;
display: inline-block;
text-align: center;
vertical-align: middle;
user-select: none;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
}
.btn {
cursor: pointer;
position: relative;
display: inline-block;
text-align: center;
vertical-align: middle;
user-select: none;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
border: 1px solid black;
color: white;
background: black;
padding: 10px 20px;
font-family: "Cardo", Sans-serif;
font-size: 20px;
font-weight: 500;
line-height: 1em;
letter-spacing: 0;
transition: all 0.2s;
}
a.btn {
text-decoration: none;
line-height: 2;
height: auto;
}
input.link {
border: none;
box-shadow: none;
text-transform: uppercase;
}
input.link:hover {
color: var(--hover-color);
}
`;
export const INPUT_STYLE = `
input, textarea, select, option {
box-sizing: border-box;
border: 1px solid var(--ast-border-color);
padding: 6px 6px 5px;
margin: 0 4px 0 0;
outline: 0;
line-height: 1;
color: #666;
padding: .75em;
height: auto;
border-width: 1px;
border-style: solid;
border-color: var(--ast-border-color);
border-radius: 2px;
background: #fafafa;
background-color: rgb(250, 250, 250);
box-shadow: none;
box-sizing: border-box;
transition: all .2s linear;
}
input[type="text"],
input[type="number"],
input[type="email"],
input[type="password"],
textarea {
width: calc(100% - 1.5rem - 2px);
}
`;
export const BLOCK_QUOTE_STYLE = `
blockquote {
font: 14px/22px normal helvetica, sans-serif;
margin-top: 10px;
margin-bottom: 10px;
margin-left: 50px;
padding-left: 15px;
border-left: 3px solid var(--border-slim-color);
}
`;
export const FORM_STYLE = `
form {
display: block;
}
form legend {
margin: 16px 0;
font-weight: bold;
font-size: 20px;
}
form.inline div {
display: flex;
}
form > div {
display: block;
margin-bottom: 1rem;
}
label {
color: #000;
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
display: inline-block;
margin-bottom: .5rem;
}
${ INPUT_STYLE }
${ BUTTON_STYLE }
`;
export const TIP_STYLE = `.tip { text-align: center; font-style: italic; font-size: 10px; color: var(--border-slim-color); }`;
export class Component extends HTMLElement {
#shadow;
static get observedAttributes() {
return []
}
constructor(html) {
super();
this.#shadow = this.attachShadow({ mode: 'open' });
this.#shadow.innerHTML = html;
}
connectedCallback() {
const observed = this.constructor.observedAttributes;
if (!Array.isArray(observed))
return;
for (const name of observed) {
const field = this.constructor.attr2Field(name);
if (!field) continue;
this.#setFieldValue(field, this[field]);
}
{
const listener = this.listenHistory;
listener && listener(Router.historyDetails());
this.#dropHistory = Router.subscribeHistory(listener);
}
}
disconnectedCallback() {
if (this.#dropHistory) this.#dropHistory();
}
#dropHistory;
listenHistory() {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV)
return;
const observed = this.constructor.observedAttributes;
if (!Array.isArray(observed))
return;
if (!observed.includes(name))
return;
const field = this.constructor.attr2Field(name);
if (!field)
return;
this.#setFieldValue(field, newV);
}
static attr2Field(name) {
if ((this.constructor['attr2FieldBlacklist'] || []).includes(name))
return;
return name.replace(/-/g, '_');
}
static get attr2FieldBlacklist() {
return []
}
#setFieldValue(name, value) {
if (value === undefined || value === null) return;
this[name] = value;
}
}
export class PseudoForm extends Component {
reportValidity() {
return this.shadowRoot.querySelector('form').reportValidity();
}
checkValidity() {
return this.shadowRoot.querySelector('form').checkValidity();
}
get elements() {
return this.shadowRoot.querySelector('form').elements;
}
}
export class PriceRange {
#min;
#max;
constructor(min, max) {
this.min = min || 0;
this.max = max || 0;
}
get isFree() {
return this.#min === 'free' || (this.#min === 0 && this.#max === 0)
}
get isRange() {
return this.#max > 0;
}
get isFixed() {
return !this.isFree && !this.isRange
}
get min() {
return this.#min
}
set min(v) {
v = parseInt(v);
if (isNaN(v)) v = 0;
this.#min = v;
}
get max() {
return this.#max
}
set max(v) {
v = parseInt(v);
if (isNaN(v)) v = 0;
this.#max = v;
}
[Symbol.toStringTag]() {
return this.toString()
}
toString() {
if (this.isFixed) return this.min.toString()
if (this.isFree) return 'free';
return `${ this.min }|${ this.max }`;
}
}
export const fireFbReady = () => {
fbReady = true;
for (const fn of fbQueue) fn();
};
export const runFbReady = (fn) => {
if (!fbReady) fbQueue.push(fn);
else fn();
};
const fbQueue = [];
let fbReady = false;
export class Router {
static historyQueue = new Set;
static goTo(url) {
history.pushState({}, document.title, url);
Router.onChange();
}
static historyDetails() {
return {
parts: location.pathname.split('/').filter(s => s && s.length),
}
}
static subscribeHistory(cb) {
if (cb) {
const call = () => {
cb(Router.historyDetails());
}
Router.historyQueue.add(call);
return () => Router.historyQueue.delete(call);
}
}
static onChange() {
for (const call of Router.historyQueue) call();
document.dispatchEvent(new CustomEvent('history:push', {
composed: true,
bubbles: true,
details: Router.historyDetails()
}));
}
}
window.addEventListener('popstate', () => Router.onChange());
export const onKeyDown = (input, callback) => {
let timeout;
input.addEventListener('change', ev => {
if (timeout) clearTimeout(timeout);
timeout = null;
ev.stopPropagation();
callback(ev, input)
});
input.addEventListener('keyup', ev => {
ev.stopPropagation();
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
callback(ev, input);
}, 1000 / 3)
});
}

View File

@ -1,76 +0,0 @@
import { Component } from "../shared";
customElements.define('date-time', class extends Component {
static get observedAttributes() {
return ['datetime', "hide-date", "hide-time"]
}
constructor() {
super(`
<style>
:host { display: block; }
section { display: flex; justify-content: flex-start; }
:host([hide-time = "true"]) #time { display: none; }
:host([hide-time = "false"]) #time { margin-left: 8px; }
:host([hide-date = "true"]) #date { display: none; }
</style>
<section>
<div id="date"></div>
<div id="time"></div>
</section>
`);
}
get datetime() {
return Date.parse(this.getAttribute('datetime'));
}
set datetime(v) {
this.setAttribute('datetime', v);
this.#format();
}
get hide_date() {
return this.getAttribute('hide-date') === 'true';
}
set hide_date(v) {
this.setAttribute('hide-date', v === true || v === 'true' ? 'true' : 'false');
this.#format();
}
get hide_time() {
return this.getAttribute('hide-time') === 'true';
}
set hide_time(v) {
this.setAttribute('hide-time', v === true || v === 'true' ? 'true' : 'false');
this.#format();
}
#format() {
{
const el = this.shadowRoot.querySelector('#date');
if (!this.hide_date)
el.textContent = new Date().toLocaleDateString(navigator.language || navigator.userLanguage, {
weekday: "long",
year: "numeric",
month: "short",
day: "numeric"
});
else
el.textContent = '';
}
{
const el = this.shadowRoot.querySelector('#time');
if (!this.hide_date)
el.textContent = new Date().toLocaleDateString(navigator.language || navigator.userLanguage, {
hour: "numeric",
minute: "numeric",
date: "short"
}).split(' ')[1];
else
el.textContent = '';
}
}
});

View File

@ -1,42 +0,0 @@
import { Component } from "../shared.js";
export class ErrorMessage extends Component {
static get observedAttributes() {
return ['message'];
}
constructor() {
super(`
<style>
:host { display: none; }
:host([message]) { display: block; }
div {
width: 1280px;
background: #ffe0e0;
border: 1px solid var(--red-color);
margin: 8px auto auto;
padding: 8px;
color: var(--red-color);
}
</style>
<div></div>
`);
}
get message() {
return this.getAttribute('message');
}
set message(m) {
this.setAttribute('message', m);
this.shadowRoot.querySelector('div').textContent = m;
}
static set errorMessage(v) {
const el = document.querySelector('error-message');
if (!el) return;
el.message = v;
}
}
customElements.define('error-message', ErrorMessage)

View File

@ -1,97 +0,0 @@
import { Component, FORM_STYLE, runFbReady } from "../shared";
customElements.define('facebook-button', class extends Component {
#fb_sdk;
static get observedAttributes() {
return ['width', 'height', 'fb-sdk']
}
constructor() {
super(`
<style>
:host {
display: block;
cursor: pointer;
}
#not-available {
display: block;
}
#svg {
display: none;
}
svg path {
fill: var(--border-slim-color);
}
:host([fb-sdk="available"]) #not-available {
display: none;
}
:host([fb-sdk="available"]) #svg {
display: block;
}
${ FORM_STYLE }
</style>
<p id="not-available">Skrypty Facebook zablokowane przez rozszerzenie przeglądarki</p>
<div id="svg">
<svg id="fb-icon" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512" xml:space="preserve">
<path fill-rule="evenodd" clip-rule="evenodd" d="M448 512h-87.999c-22.094 0-39.999-17.922-39.999-40V360.001c0-22.094 17.905-39.999 39.999-39.999h12.922c8.32 0 16.844-6.656 19.062-15.031l8.484-31.953c2.484-9.312-2.812-17.023-11.852-17.023H360c-22.094 0-39.999-17.902-39.999-39.988v-16C320.001 177.905 337.906 160 360 160h24.008c8.82 0 16-7.156 16-16v-32.004c0-8.828-7.18-16.004-16-16.004H336c-44.187 0-79.991 35.828-79.991 80.011v40.004c0 22.085-17.922 39.988-40.003 39.988h-7.625c-9.039 0-16.379 7.711-16.379 17.023v31.953c0 8.375 6.746 15.031 15.07 15.031h8.934c22.082 0 40.003 17.905 40.003 39.999V472c0 22.078-17.922 40-40.003 40h-8c-8.844 0-16.004-7.172-16.004-16 0-8.844 7.16-16 16.004-16 8.824 0 16-7.172 16-16v-95.999c0-8.844-7.175-16-16-16H197.51c-20.719 0-37.511-16.578-37.511-37.577v-47.922c0-23.156 18.371-42.512 41.027-42.512h6.98c8.824 0 16-7.16 16-15.984v-40.004c0-57.441 46.559-103.995 103.995-103.995h64.008c22.086 0 39.984 17.902 39.984 39.988v48.004c0 22.085-17.898 40.003-39.984 40.003H352.001v16.004c0 8.824 7.156 15.984 16 15.984h29.969c22.672 0 37.398 19.355 33.062 42.512l-8.992 47.922c-3.93 20.999-23.82 37.577-44.539 37.577h-9.5c-8.844 0-16 7.156-16 16V464c0 8.828 7.156 16 16 16H448c17.672 0 32-14.328 32-32V64.007c0-17.688-14.328-32.003-32-32.003H64.007c-17.672 0-32.003 14.316-32.003 32.003V448c0 17.672 14.332 32 32.003 32h16c8.828 0 16.003 7.156 16.003 16 0 8.828-7.175 16-16.003 16h-16C28.668 512 0 483.344 0 448V64.007C0 28.648 28.668 0 64.007 0H448c35.359 0 64 28.648 64 64.007V448c0 35.344-28.641 64-64 64zm-304.001-32c8.844 0 16 7.156 16 16 0 8.828-7.156 16-16 16-8.828 0-16.004-7.172-16.004-16 0-8.844 7.176-16 16.004-16z"/>
</svg>
<slot></slot>
</div>
`);
this.shadowRoot.querySelector('#fb-icon').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
if (!this.#fb_sdk)
return console.warn("Facebook SDK is not available");
FB.login((res) => {
if (res.status === 'connected') {
FB.api("/me?fields=id,name,email", ({ id, name, email }) => {
this.dispatchEvent(new CustomEvent('facebook:account', {
bubbles: true, composed: true, detail: {
id, name, email
}
}));
});
}
}, { scope: 'public_profile,email', return_scopes: true });
});
}
connectedCallback() {
super.connectedCallback();
runFbReady(() => {
this.setAttribute('fb-sdk', 'available');
this.dispatchEvent(new CustomEvent('facebook:available', { bubbles: true, composed: true }));
});
}
get width() {
return this.getAttribute('width')
}
set width(v) {
this.setAttribute('width', v);
this.shadowRoot.querySelector('svg').setAttribute('width', v);
}
get height() {
return this.getAttribute('width')
}
set height(v) {
this.setAttribute('height', v);
this.shadowRoot.querySelector('svg').setAttribute('height', v);
}
get fb_sdk() {
return !!this.#fb_sdk;
}
set fb_sdk(v) {
this.setAttribute('fb-sdk', v);
this.#fb_sdk = v === 'available';
}
});

View File

@ -1,85 +0,0 @@
import { Component, BUTTON_STYLE } from "../shared";
customElements.define('form-navigation', class extends Component {
static get observedAttributes() {
return ['next', 'prev']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
.actions {
display: flex;
justify-content: space-between;
margin: 16px 0;
}
form > .actions > input.hidden {
display: none !important;
}
form > .actions > input {
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;
max-width: 200px;
}
}
${ BUTTON_STYLE }
</style>
<form>
<div class="actions">
<input id="prev" type="button" value="Wróć" />
<input id="next" type="submit" value="Następny" />
</div>
</form>
`);
this.shadowRoot.querySelector('#prev').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.prev();
});
this.shadowRoot.querySelector('#next').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.next();
});
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'next': {
this.shadowRoot.querySelector('#next').className = newV;
break;
}
case 'prev': {
this.shadowRoot.querySelector('#prev').className = newV;
break;
}
}
}
next() {
this.dispatchEvent(new CustomEvent('form:next', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
}
prev() {
this.dispatchEvent(new CustomEvent('form:prev', {
bubbles: true,
composed: true,
detail: this.parentElement
}));
}
});

View File

@ -1,210 +0,0 @@
import { BUTTON_STYLE, Component } from "../shared.js";
customElements.define('image-input', class extends Component {
#file;
static get observedAttributes() {
return ['width', 'height', "account-id", "url", "send-original"];
}
constructor() {
super(`
<style>
:host {
display: block;
border: 1px solid var(--border-light-gray-color);
border-radius: 8px;
}
#hidden { overflow: hidden; width: 1px; height: 1px; position: relative; }
input[type=file] { position: absolute; top: -10px; left: -10px; display: none; }
#view { width: 200px; height: 200px; cursor: pointer; }
#view > span {
display: block;
text-align: center;
margin-top: 8px;
color: var(--border-slim-color)
}
div > input[type=button] {
margin: 0;
width: 100%;
border-left: none;
border-right: none;
border-bottom: none;
border-color: var(--border-light-gray-color);
}
img[src=""] { display: none; }
img {
max-width: 200px;
max-height: 200px;
display: block;
margin: auto;
}
${ BUTTON_STYLE }
</style>
<article>
<section id="hidden">
<input id="file" type="file" accept="image/*" />
</section>
<div id="view">
Kliknij żeby przesłać zdjęcie (opcjonalne)
</div>
<div style="display: none;">
<input id="save" type="button" value="Wyślij" />
</div>
</article>
`);
this.shadowRoot.querySelector('#save').addEventListener('click', async ev => {
ev.preventDefault();
ev.stopPropagation();
await this.#sendFile(this.#file);
});
const input = this.shadowRoot.querySelector('#file');
const view = this.shadowRoot.querySelector('#view');
const toFile = (canvas) => new Promise((resolve) => {
canvas.toBlob(async (blob) => {
this.#file = new File([blob], `${ crypto.randomUUID() }.webp`, { type: blob.type });
resolve();
await this.#sendFile(this.#file); // TODO: Send on form submit
}, 'image/webp');
})
input.addEventListener('change', ev => {
ev.stopPropagation();
const image = new Image();
const canvas = document.createElement('canvas');
let maxWidth = this.width;
let maxHeight = this.height;
image.onload = async () => {
if (this.send_original) {
maxWidth = maxWidth < image.naturalWidth ? maxWidth : image.naturalWidth;
maxHeight = maxHeight < image.naturalHeight ? maxHeight : image.naturalHeight;
}
console.warn(this.send_original, maxWidth, maxHeight)
const width = image.width > image.height
? maxWidth
: (image.width * maxHeight) / image.height;
const height = image.width > image.height
? (image.height * maxWidth) / image.width
: maxHeight;
canvas.width = image.width = width;
canvas.height = image.height = height;
canvas.getContext('2d').drawImage(image, 0, 0, width, height);
await toFile(canvas);
image.width = width > image ? 200 : (width * 200) / height;
image.height = width > image ? (width * 200) / height : 200;
view.appendChild(image);
};
image.src = URL.createObjectURL(input.files[0]);
view.innerHTML = '';
});
view.addEventListener('click', ev => {
ev.stopPropagation();
input.click();
});
}
connectedCallback() {
super.connectedCallback();
this.account_id = this.account_id;
this.url = this.url;
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, 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) ? 1000 : v;
}
set width(v) {
this.setAttribute('width', v);
}
get height() {
const v = parseInt(this.getAttribute('height'));
return isNaN(v) ? 1000 : v;
}
set height(v) {
this.setAttribute('height', v);
}
get url() {
return this.getAttribute('url');
}
set url(v) {
if (!(v || '').startsWith("/")) v = '';
this.setAttribute('url', v);
const view = this.shadowRoot.querySelector('#view');
if (v === '') {
view.innerHTML = '<span>Kliknij żeby przesłać zdjęcie (opcjonalne)</span>';
} else {
view.innerHTML = `<img src="${ v }" alt=""/>`;
}
}
get value() {
return this.url;
}
set value(v) {
this.url = v;
}
async #sendFile(file) {
if (!file) return;
const form = new FormData;
form.append(`${ crypto.randomUUID() }.webp`, file);
await fetch("/upload", {
method: "POST",
body: form,
headers: { 'Accept': 'application/json' },
}).then(res => res.json()).then(({ path }) => {
this.url = path;
this.dispatchEvent(new CustomEvent('image-input:uploaded', {
bubbles: true,
composed: true,
detail: path
}));
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: path
}));
});
}
get send_original() {
return this.hasAttribute('send-original');
}
set send_original(v) {
v = v === true || v === 'true';
if (v)
this.setAttribute('send-original', 'true')
else
this.removeAttribute('send-original');
}
});

View File

@ -1,120 +0,0 @@
import { Component } from "../shared";
customElements.define('image-popup', class extends Component {
#listener;
static get observedAttributes() {
return ['src', 'popup'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
#small {
display: block;
width: 100%;
height: 100%;
}
#large {
display: none;
}
#small > img {
max-width: 100%;
max-height: 100%;
}
img[src=''] {
display: none;
}
:host([popup="true"]) #large {
display: block;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 0;
}
#background {
background: rgba(120, 120, 120, .6);
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
z-index: -1;
}
#large > #image {
margin: 16px;
height: calc(100% - 32px);
width: calc(100% - 32px);
text-align: center;
}
#large > #image > img {
margin: auto;
max-height: 100%;
max-width: 100%;
}
</style>
<div id="small">
<img alt="" src="" />
</div>
<div id="large">
<div id="background"></div>
<div id="image">
<img src="" alt="">
</div>
</div>
`);
this.shadowRoot.querySelector('#small').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.popup = true;
});
this.shadowRoot.querySelector('#large').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.popup = false;
});
this.#listener = ({ key }) => {
if (key !== 'Escape') return;
if (!this.popup) return;
this.popup = false;
};
}
connectedCallback() {
super.connectedCallback();
window.addEventListener('keydown', this.#listener, true);
}
disconnectedCallback() {
super.disconnectedCallback();
window.removeEventListener('keydown', this.#listener, true);
}
get popup() {
return this.getAttribute('popup') === 'true';
}
set popup(v) {
if (v === true || v === 'true')
this.setAttribute('popup', 'true');
else
this.removeAttribute('popup');
}
get src() {
return this.getAttribute('src');
}
set src(v) {
this.setAttribute('src', v);
this.shadowRoot.querySelector('#small img').src = v;
this.shadowRoot.querySelector('#large img').src = v;
}
});

View File

@ -1,109 +0,0 @@
import { Component, Router } from "../../shared.js";
customElements.define('ow-nav', class extends Component {
constructor() {
super(`
<style>
:host { display: block; }
article {
border-style: solid;
border-width: 0 0 1px 0;
border-color: #F2F2F2;
transition: background 0.3s, border 0.3s, border-radius 0.3s, box-shadow 0.3s;
margin-top: 0;
margin-bottom: 16px;
padding: 13px 10px 13px 10px;
}
section, section > div {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
position: relative;
}
svg {
height: 48px;
}
#logo {
display: none;
}
@media print {
:host { display: none; }
}
@media only screen and (min-device-width: 1000px) {
section > div {
width: 33%;
margin: 0 auto;
}
#logo {
display: block;
text-align: center;
}
#right {
justify-content: flex-end;
}
}
</style>
<article>
<section>
<div class="left">
<slot name="left"></slot>
</div>
<div id="logo">
<a href="/">
<svg viewBox="0 0 911 550" xmlns="http://www.w3.org/2000/svg">
<ellipse cx="293" cy="335.2" rx="121" ry="170.61" fill="none" stroke="#555" style="opacity:.5" opacity=".5"/>
<ellipse cx="653.783" cy="113.849" rx="121.002" ry="170.613" fill="none" stroke="#555" transform="rotate(11.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="428.125" cy="166.014" rx="125.003" ry="176.254" fill="none" stroke="#555" transform="rotate(22.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="402.034" cy="23.889" rx="123.005" ry="173.437" fill="none" stroke="#555" transform="rotate(33.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="464.706" cy="-31.682" rx="124.999" ry="176.248" fill="none" stroke="#555" transform="rotate(45)" style="opacity:.5" opacity=".5"/>
<ellipse cx="540.521" cy="-306.337" rx="129.005" ry="181.898" fill="none" stroke="#555" transform="rotate(56.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="494.207" cy="-442.566" rx="124.003" ry="174.844" fill="none" stroke="#555" transform="rotate(67.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="239.113" cy="-150.236" rx="126.002" ry="177.663" fill="none" stroke="#555" transform="rotate(78.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="304.2" cy="-330" rx="128" ry="180.48" fill="none" stroke="#555" transform="rotate(90)" style="opacity:.5" opacity=".5"/>
<ellipse cx="143.181" cy="-572.932" rx="120.002" ry="169.203" fill="none" stroke="#555" transform="rotate(101.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="142" cy="-386.769" rx="123.003" ry="173.434" fill="none" stroke="#555" transform="rotate(112.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-91.151" cy="-617.377" rx="123.005" ry="173.437" fill="none" stroke="#555" transform="rotate(123.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-17.541" cy="-483.099" rx="124.999" ry="176.248" fill="none" stroke="#555" transform="rotate(135)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-89.814" cy="-445.118" rx="128.005" ry="180.488" fill="none" stroke="#555" transform="rotate(146.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-309.998" cy="-338.613" rx="125.003" ry="176.254" fill="none" stroke="#555" transform="rotate(157.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-514.833" cy="-456.414" rx="120.002" ry="169.203" fill="none" stroke="#555" transform="rotate(168.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-534" cy="-351.2" rx="126" ry="177.66" fill="none" stroke="#555" transform="scale(-1)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-434.371" cy="-246.176" rx="128.002" ry="180.483" fill="none" stroke="#555" transform="rotate(-168.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-710.531" cy="4.041" rx="126.003" ry="177.664" fill="none" stroke="#555" transform="rotate(-157.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-382.027" cy="-19.162" rx="130.005" ry="183.308" fill="none" stroke="#555" transform="rotate(-146.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-721.398" cy="240.271" rx="129.999" ry="183.298" fill="none" stroke="#555" transform="rotate(-135)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-304.791" cy="106.633" rx="123.005" ry="173.437" fill="none" stroke="#555" transform="rotate(-123.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-495.302" cy="413.961" rx="128.003" ry="180.485" fill="none" stroke="#555" transform="rotate(-112.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-265.515" cy="206.161" rx="125.002" ry="176.253" fill="none" stroke="#555" transform="rotate(-101.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-180.2" cy="605" rx="122" ry="172.02" fill="none" stroke="#555" transform="rotate(-90)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-137.223" cy="326.045" rx="121.002" ry="170.613" fill="none" stroke="#555" transform="rotate(-78.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-80.373" cy="352.613" rx="125.003" ry="176.254" fill="none" stroke="#555" transform="rotate(-67.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-50.619" cy="275.587" rx="126.005" ry="177.667" fill="none" stroke="#555" transform="rotate(-56.25)" style="opacity:.5" opacity=".5"/>
<ellipse cx="-22.773" cy="406.017" rx="126.999" ry="179.068" fill="none" stroke="#555" transform="rotate(-45)" style="opacity:.5" opacity=".5"/>
<ellipse cx="97.486" cy="309.523" rx="121.005" ry="170.617" fill="none" stroke="#555" transform="rotate(-33.75)" style="opacity:.5" opacity=".5"/>
<ellipse cx="507.007" cy="435.361" rx="129.003" ry="181.895" fill="none" stroke="#555" transform="rotate(-22.5)" style="opacity:.5" opacity=".5"/>
<ellipse cx="261.461" cy="254.093" rx="130.002" ry="183.303" fill="none" stroke="#555" transform="rotate(-11.25)" style="opacity:.5" opacity=".5"/>
<text xml:space="preserve" style="font-style:normal;font-weight:400;font-size:200px;line-height:1.25;font-family:sans-serif;fill:#000;fill-opacity:1;stroke:none" x="36.404" y="311.43">
<tspan x="36.404" y="311.43" style="font-style:normal;font-variant:normal;font-weight:400;font-stretch:normal;font-family:'Cardo';">OS Wilno</tspan>
</text>
</svg>
</a>
</div>
<div id="right">
<slot name="right"></slot>
</div>
</section>
</article>
`);
}
#mount(selector, path) {
const el = this.querySelector(selector);
if (!el) return;
el.addEventListener('click', (ev) => {
ev.stopPropagation();
ev.preventDefault();
Router.goTo(path);
});
}
});

View File

@ -1,65 +0,0 @@
import { Component } from "../../shared";
customElements.define('ow-path', class extends Component {
static get observedAttributes() {
return ['selected', 'path'];
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
a {
padding: 8px;
text-decoration: none;
text-transform: uppercase;
border: none;
color: var(--hover-color);
display: flex;
justify-content: center;
align-items: center;
align-content: center;
}
a:hover {
color: var(--hover-color);
}
:host(:not([selected])) a {
color: #495057;
}
@media print {
:host { display: none; }
}
</style>
<a><slot></slot></a>
`);
this.shadowRoot.querySelector('a').addEventListener('click', ev => {
document.location = this.path;
});
}
get selected() {
return this.getAttribute('selected') === 'selected';
}
set selected(value) {
if (value === true || value === 'selected')
this.setAttribute('selected', 'selected');
else
this.removeAttribute('selected');
}
get path() {
return this.getAttribute('path') || ''
}
set path(value) {
if (!value || value === '') {
this.removeAttribute('path');
return;
}
this.setAttribute('path', value);
this.shadowRoot.querySelector('a').setAttribute('href', value);
}
});

View File

@ -1,101 +0,0 @@
import { Component, BUTTON_STYLE } from "../shared.js";
customElements.define('popup-window', class extends Component {
static get observedAttributes() {
return ['index', 'required', 'open'];
}
constructor() {
super(`
<style>
:host {
display: block;
width: 100%;
height: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1;
}
article {
position: relative;
}
#bg {
display: block;
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
z-index: 1;
background: rgba(100,100,100, .6);
}
#content {
display: block;
width: 100%;
height: 100%;
}
${ BUTTON_STYLE }
</style>
<article>
<section id="bg">&nbsp;</section>
<section id="content">
<div><slot></slot></div>
<div id="required"><input id="submit" value="Zakończ" /></div>
<div id="optional">
<input id="yes" value="Tak" />
<input id="no" value="Nie" />
</div>
</section>
</article>
`);
}
static attr2Field(name) {
if (name === 'open')
return 'is_open';
super.attr2Field(name);
}
get required() {
return this.getAttribute('required') === 'yes';
}
set required(v) {
if (v === true || v === 'yes')
this.setAttribute('required', 'yes');
else
this.removeAttribute('required');
}
get index() {
const v = parseInt(this.getAttribute('index'));
return isNaN(v) ? null : v;
}
set index(v) {
v = parseInt(v);
if (isNaN(v)) return;
this.setAttribute('index', v.toString());
this.style.zIndex = v;
}
get is_open() {
return this.getAttribute('open') === 'true';
}
set is_open(v) {
if (v === 'true' || v === true)
this.setAttribute('open', 'true');
else
this.removeAttribute('open');
}
close() {
this.is_open = false;
}
open() {
this.is_open = true;
}
});

View File

@ -1,129 +0,0 @@
import { Component, FORM_STYLE } from "../../shared";
customElements.define('price-input', class extends Component {
static get observedAttributes() {
return ['value', 'currency', 'required', 'name']
}
constructor() {
super(`
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
#price {
font-weight: bold;
}
#view {
display: flex;
}
#value {
display: flex;
justify-content: start;
}
${ FORM_STYLE }
</style>
<div id="view">
<input
id="price"
type="number"
min="0.00"
max="10000.00"
step="0.01"
placeholder="Cena, np: 12.23"
/>
<span id="currency"></span>
</div>
`);
const price = this.shadowRoot.querySelector('#price');
price.addEventListener('change', ev => {
ev.stopPropagation();
this.value = price.value;
this.dispatchEvent(new CustomEvent('change', {
bubbles: true,
composed: true,
detail: { price: this.price }
}));
this.dispatchEvent(new CustomEvent('price:changed', {
bubbles: true,
composed: true,
detail: { price: this.price }
}));
});
}
connectedCallback() {
this.shadowRoot.querySelector('#currency').textContent = this.currency;
this.shadowRoot.querySelector('#price').value = this.value;
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
}
get value() {
return Math.floor(parseFloat(this.shadowRoot.querySelector('#price').value) * 100);
}
set value(v) {
this.setAttribute('value', v);
this.shadowRoot.querySelector('#price').value = v;
}
get price() {
return Math.floor(parseFloat(this.shadowRoot.querySelector('#price').value) * 100);
}
set price(v) {
this.setAttribute('price', v);
this.shadowRoot.querySelector('#price').value = v;
}
get currency() {
return this.getAttribute('currency') || 'PLN';
}
set currency(value) {
this.setAttribute('currency', value);
this.shadowRoot.querySelector('#currency').textContent = this.currency;
}
reportValidity() {
return this.shadowRoot.querySelector('input').reportValidity();
}
get name() {
return this.getAttribute('name');
}
set name(value) {
this.setAttribute('name', value);
}
get readonly() {
return this.hasAttribute('readonly');
}
set readolny(v) {
v = v === true || v === 'readonly';
const price = this.shadowRoot.querySelector('#price');
if (v) {
price.setAttribute('readonly', 'readonly')
} else {
price.removeAttribute('readonly');
}
}
get required() {
return this.hasAttribute('required');
}
set required(v) {
v = v === true || v === 'required';
const price = this.shadowRoot.querySelector('#price');
if (v) {
price.setAttribute('required', 'required')
} else {
price.removeAttribute('required');
}
}
});

View File

@ -1,58 +0,0 @@
customElements.define('price-view', class extends HTMLElement {
#form;
static get observedAttributes() {
return ['value', 'currency']
}
constructor() {
super();
const shadow = this.#form = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Cardo', sans-serif; }
#price {
font-weight: bold;
}
</style>
<span id="price"></span>
`;
}
connectedCallback() {
this.#form.querySelector('#price').textContent = this.formatted;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'price': {
this.value = newV;
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);
this.#form.querySelector('#price').textContent = this.formatted;
}
get currency() {
return this.getAttribute('currency') || 'PLN';
}
});

View File

@ -1,513 +0,0 @@
import { Component, BLOCK_QUOTE_STYLE } from "../shared";
customElements.define('rich-text-editor', class extends Component {
#selection;
#range;
static get observedAttributes() {
return ['upload-url'];
}
constructor() {
super(`
<style>
:host {
display: block;
}
#tools > #first-row {
display: flex;
justify-content: flex-start;
}
#edit {
border: 1px solid var(--border-slim-color);
min-height: 90px;
padding: 8px;
}
button, select {
background: none;
border: 1px solid var(--border-slim-color);
margin-right: 8px;
}
button {
padding: 4px;
width: 46px;
height: 46px;
}
select {
padding: 4px;
height: 46px;
}
svg {
width: 36px;
height: 36px;
}
section > div {
margin-bottom: 16px;
}
section ul {
list-style: circle;
}
section ol {
list-style: decimal;
}
#image input {
display: none;
}
#align, #colors, #lists, #insert {
display: flex;
justify-content: space-between;
}
#text-color input, #background-color input {
display: none;
}
${ BLOCK_QUOTE_STYLE }
</style>
<article>
<section id="tools">
<div id="first-row">
<div>
<select id="header">
<option value="normal">Normal</option>
<option value="h1">H1</option>
<option value="h2">H2</option>
<option value="h3">H3</option>
<option value="h4">H4</option>
<option value="h5">H5</option>
</select>
</div>
<div id="colors">
<button id="text-color" title="Kolor czcionki">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 278 278" xml:space="preserve">
<path d="M254.833 0h-231C15.549 0 8.5 6.716 8.5 15v33c0 8.284 6.716 15 15 15s15-6.716 15-15V30h69v218H89.833c-8.284 0-15 6.716-15 15s6.716 15 15 15h99c8.284 0 15-6.716 15-15s-6.716-15-15-15H170.5V30h69v18c0 8.284 6.716 15 15 15s15-6.716 15-15V15c0-8.284-6.383-15-14.667-15z"/>
</svg>
<input type="color" value="#000" />
</button>
<button id="background-color" title="Kolor tła">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" xml:space="preserve">
<path d="m25.9 16 4.3 10h-9l3.9-10h.8zM48 8v36c0 2.2-1.8 4-4 4H8c-2.2 0-4-1.8-4-4V8c0-2.2 1.8-4 4-4h36c2.2 0 4 1.8 4 4zm-5.5 32.7L30.5 11c-.3-.6-.8-1-1.5-1h-7.1c-.6 0-1.2.4-1.4 1l-11 29.7c-.2.6.2 1.3.9 1.3h4.1c.6 0 1.2-.5 1.4-1.1l3.2-8.9h13.4l3.5 8.9c.2.6.8 1.1 1.4 1.1h4.1c.7 0 1.2-.7 1-1.3z"/>
</svg>
<input type="color" value="#000" />
</button>
</div>
<div id="decorations">
<button id="underscore" title="Podkreślenie">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 52 52" xml:space="preserve">
<path d="M10.4 36h4.1c.6 0 1.2-.5 1.4-1.1l3.2-8.9h13.4l3.5 8.9c.2.6.8 1.1 1.4 1.1h4.1c.7 0 1.2-.7.9-1.3L30.4 5c-.2-.6-.7-1-1.3-1H22c-.6 0-1.2.4-1.4 1l-11 29.7c-.3.6.2 1.3.8 1.3zm14.7-26h.9l4.3 10h-9l3.8-10zM48.5 42h-45c-.8 0-1.5.7-1.5 1.5v3c0 .8.7 1.5 1.5 1.5h45c.8 0 1.5-.7 1.5-1.5v-3c0-.8-.7-1.5-1.5-1.5z"/>
</svg>
</button>
<button id="superscript" title="Indeks górny">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M11 7v13H9V7H3V5h12v2h-4zm8.55-.42a.8.8 0 1 0-1.32-.36l-1.154.33A2.001 2.001 0 0 1 19 4a2 2 0 0 1 1.373 3.454L18.744 9H21v1h-4V9l2.55-2.42z"/>
</svg>
</button>
<button id="subscription" title="Indeks dolny">
<svg viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16.488 4.43a.75.75 0 0 1 .082 1.058L10.988 12l5.032 5.871c-.302.41-.555.906-.682 1.51L10 13.151l-5.43 6.336a.75.75 0 0 1-1.14-.976L9.013 12 3.431 5.488a.75.75 0 0 1 1.139-.976L10 10.848l5.43-6.336a.75.75 0 0 1 1.058-.081ZM17.75 15.523c0-.528.444-1.023.986-1.023.407 0 .735.19.893.434.136.21.218.566-.093 1.095-.15.255-.376.482-.682.724-.152.12-.316.237-.493.363l-.074.052c-.152.107-.315.222-.472.34-.744.56-1.565 1.346-1.565 2.742 0 .414.336.75.75.75h3.451a.75.75 0 0 0 0-1.5h-2.513c.16-.282.423-.525.779-.793.137-.103.279-.203.432-.312l.078-.054c.178-.127.37-.264.557-.41.372-.295.76-.658 1.045-1.142.557-.948.546-1.921.058-2.672C20.42 13.4 19.59 13 18.736 13c-1.478 0-2.486 1.278-2.486 2.523a.75.75 0 0 0 1.5 0Z" fill="#212121"/>
</svg>
</button>
</div>
<div id="lists">
<button id="ordered-list" title="Lista uporządkowana">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 317.109 317.109" xml:space="preserve">
<path d="M102.109 53.555h200c8.284 0 15-6.716 15-15s-6.716-15-15-15h-200c-8.284 0-15 6.716-15 15s6.716 15 15 15zM302.109 83.555h-200c-8.284 0-15 6.716-15 15s6.716 15 15 15h200c8.284 0 15-6.716 15-15s-6.715-15-15-15zM302.109 143.555h-200c-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15h200c8.284 0 15-6.716 15-15 0-8.285-6.715-15-15-15zM302.109 263.555h-200c-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15h200c8.284 0 15-6.716 15-15 0-8.284-6.715-15-15-15zM302.109 203.555h-200c-8.284 0-15 6.716-15 15 0 8.284 6.716 15 15 15h200c8.284 0 15-6.716 15-15 0-8.285-6.715-15-15-15zM17.826 49.036V86.6c0 4.074 3.32 7.146 7.724 7.146 4.33 0 7.721-3.139 7.721-7.146V30.425c0-3.96-3.247-7.062-7.392-7.062-3.647 0-5.471 2.447-6.07 3.251a6.54 6.54 0 0 0-.074.102l-6.526 9.233c-1.267 1.378-2.394 3.582-2.394 5.696-.001 4.03 3.133 7.317 7.011 7.391zM7.63 193.746h29.406c3.849 0 6.981-3.391 6.981-7.559 0-4.124-3.131-7.479-6.981-7.479H15.684v-.122c0-2.246 5.148-5.878 9.285-8.797 8.229-5.807 18.47-13.033 18.47-25.565 0-11.893-9.216-20.86-21.438-20.86-11.703 0-20.527 8.044-20.527 18.711 0 6.19 4.029 8.387 7.479 8.387 4.938 0 7.889-3.676 7.889-7.23 0-2.21.568-4.746 4.994-4.746 5.979 0 6.151 5.298 6.151 5.902 0 4.762-6.18 9.213-12.157 13.519C8.442 163.228.068 169.26.068 178.587v8.011c-.001 4.276 3.91 7.148 7.562 7.148zM42.446 242.783c0-12.342-7.288-19.42-19.994-19.42-16.66 0-21.062 11.898-21.062 18.189 0 7.325 5.445 8.115 7.786 8.115 4.559 0 7.621-3.063 7.621-7.622 0-1.753.624-3.766 5.487-3.766 3.495 0 4.918.503 4.918 5.568 0 4.948-1.062 5.487-5.245 5.487-4.018 0-7.047 3.17-7.047 7.375 0 4.159 3.066 7.295 7.131 7.295 5.525 0 6.635 2.256 6.635 5.897v1.558c0 6.126-2.389 7.288-6.798 7.288-6.083 0-6.556-3.133-6.556-4.093 0-3.631-2.407-7.294-7.785-7.294-4.72 0-7.538 2.942-7.538 7.869 0 8.976 7.696 18.516 21.958 18.516 13.854 0 22.126-8.331 22.126-22.286v-1.558c0-5.722-1.83-10.465-5.264-13.876 2.352-3.403 3.627-7.944 3.627-13.242z"/>
</svg>
</button>
<button id="unordered-list" title="Lista nieuporządkowana">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 487.3 487.3" xml:space="preserve">
<path d="M487.2 69.7c0 12.9-10.5 23.4-23.4 23.4h-322c-12.9 0-23.4-10.5-23.4-23.4s10.5-23.4 23.4-23.4h322.1c12.9.1 23.3 10.5 23.3 23.4zm-23.3 92.6H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zm0 116H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zm0 116H141.8c-12.9 0-23.4 10.5-23.4 23.4s10.5 23.4 23.4 23.4h322.1c12.9 0 23.4-10.5 23.4-23.4-.1-12.9-10.5-23.4-23.4-23.4zM38.9 30.8C17.4 30.8 0 48.2 0 69.7s17.4 39 38.9 39 38.9-17.5 38.9-39-17.4-38.9-38.9-38.9zm0 116C17.4 146.8 0 164.2 0 185.7s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9-17.4-38.9-38.9-38.9zm0 116C17.4 262.8 0 280.2 0 301.7s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9-17.4-38.9-38.9-38.9zm0 115.9C17.4 378.7 0 396.1 0 417.6s17.4 38.9 38.9 38.9 38.9-17.4 38.9-38.9c0-21.4-17.4-38.9-38.9-38.9z"/>
</svg>
</button>
<button id="short-quote" title="Cytat">
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path fill="none" d="M0 0h24v24H0z"/>
<path d="M11.192 15.757c0-.88-.23-1.618-.69-2.217-.326-.412-.768-.683-1.327-.812-.55-.128-1.07-.137-1.54-.028-.16-.95.1-1.956.76-3.022.66-1.065 1.515-1.867 2.558-2.403L9.373 5c-.8.396-1.56.898-2.26 1.505-.71.607-1.34 1.305-1.9 2.094s-.98 1.68-1.25 2.69-.346 2.04-.217 3.1c.168 1.4.62 2.52 1.356 3.35.735.84 1.652 1.26 2.748 1.26.965 0 1.766-.29 2.4-.878.628-.576.94-1.365.94-2.368l.002.003zm9.124 0c0-.88-.23-1.618-.69-2.217-.326-.42-.77-.692-1.327-.817-.56-.124-1.074-.13-1.54-.022-.16-.94.09-1.95.75-3.02.66-1.06 1.514-1.86 2.557-2.4L18.49 5c-.8.396-1.555.898-2.26 1.505a11.29 11.29 0 0 0-1.894 2.094c-.556.79-.97 1.68-1.24 2.69a8.04 8.04 0 0 0-.217 3.1c.165 1.4.615 2.52 1.35 3.35.732.833 1.646 1.25 2.742 1.25.967 0 1.768-.29 2.402-.876.627-.576.942-1.365.942-2.368v.01z"/>
</svg>
</button>
</div>
<div id="insert">
<button id="image" title="Wstaw obrazek">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 58 58" xml:space="preserve">
<path d="M31 56h24V32H31v24zm2-22h20v20h-9V41.414l4.293 4.293 1.414-1.414L43 37.586l-6.707 6.707 1.414 1.414L42 41.414V54h-9V34zM21.569 13.569C21.569 10.498 19.071 8 16 8s-5.569 2.498-5.569 5.569c0 3.07 2.498 5.568 5.569 5.568s5.569-2.497 5.569-5.568zm-9.138 0C12.431 11.602 14.032 10 16 10s3.569 1.602 3.569 3.569-1.601 3.569-3.569 3.569-3.569-1.601-3.569-3.569zM6.25 36.661a.997.997 0 0 0 1.41.09l16.313-14.362 7.319 7.318a.999.999 0 1 0 1.414-1.414l-1.825-1.824 9.181-10.054 11.261 10.323a1 1 0 0 0 1.351-1.475l-12-11a1.002 1.002 0 0 0-1.414.063l-9.794 10.727-4.743-4.743a1.003 1.003 0 0 0-1.368-.044L6.339 35.249a1 1 0 0 0-.089 1.412z"/><path d="M57 2H1a1 1 0 0 0-1 1v44a1 1 0 0 0 1 1h24a1 1 0 1 0 0-2H2V4h54v23a1 1 0 1 0 2 0V3a1 1 0 0 0-1-1z"/>
</svg>
<input type="file" accept="image/*" />
</button>
</div>
<div id="align">
<button id="align-left" title="Wyrównaj do lewej">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200" xml:space="preserve">
<path d="M618.75 99.202v178.006H0V99.202h618.75zm328.125 274.53v178.006H0V373.732h946.875zM731.25 648.262v178.006H0V648.262h731.25zM1200 922.792v178.006H0V922.792h1200z"/>
</svg>
</button>
<button id="align-center" title="Wyrównaj do środka">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 31.668 31.668" xml:space="preserve">
<path d="M25.501 5H6.167V0h19.334v5zM.168 8.889v5H31.5v-5H.168zm5.999 8.888v5h19.334v-5H6.167zM.168 31.668H31.5v-5H.168v5z"/>
</svg>
</button>
<button id="align-right" title="Wyrównaj do prawej">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1200 1200" xml:space="preserve">
<path d="M581.25 99.202v178.006H1200V99.202H581.25zm-328.125 274.53v178.006H1200V373.732H253.125zm215.625 274.53v178.006H1200V648.262H468.75zM0 922.792v178.006h1200V922.792H0z"/>
</svg>
</button>
<button id="align-justify" title="Wyrównaj do lewej i prawej">
<svg viewBox="-32 0 512 512" xmlns="http://www.w3.org/2000/svg">
<path d="M432 416H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16a16 16 0 0 0-16 16v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16v-32a16 16 0 0 0-16-16zm0-128H16A16 16 0 0 0 0 48v32a16 16 0 0 0 16 16h416a16 16 0 0 0 16-16V48a16 16 0 0 0-16-16z"/>
</svg>
</button>
</div>
</div>
<div id="second-row">
</div>
</section>
<section id="edit" contenteditable="true"></section>
</article>
`);
const header = this.shadowRoot.querySelector('#tools #header');
header.addEventListener('click', () => {
this.#saveSelection();
});
header.addEventListener('change', ev => {
switch (ev.target.value) {
case 'normal':
return this.#removeWrapper();
case 'h1':
return this.#setWrap(['H1'], { repeat: 'ignore' });
case 'h2':
return this.#setWrap(['H2'], { repeat: 'ignore' });
case 'h3':
return this.#setWrap(['H3'], { repeat: 'ignore' });
case 'h4':
return this.#setWrap(['H4'], { repeat: 'ignore' });
case 'h5':
return this.#setWrap(['H5'], { repeat: 'ignore' });
}
});
{
this.shadowRoot.querySelector('#ordered-list').addEventListener('click', ev => {
ev.stopPropagation();
this.#saveSelection();
this.#setWrap(['ol', 'li'], { repeat: 'perform' });
});
this.shadowRoot.querySelector('#unordered-list').addEventListener('click', ev => {
ev.stopPropagation();
this.#saveSelection();
this.#setWrap(['ul', 'li'], { repeat: 'perform' });
});
this.shadowRoot.querySelector('#short-quote').addEventListener('click', ev => {
ev.stopPropagation();
this.#saveSelection();
this.#setWrap(['blockquote'], { repeat: 'drop' });
});
}
{
let timeout = null;
this.shadowRoot.querySelector('#edit').addEventListener('keyup', (ev) => {
ev.stopPropagation();
if (timeout) clearTimeout(timeout);
timeout = setTimeout(() => {
timeout = null;
this.#emitChange();
}, 1000 / 3);
});
}
{
const imgBtn = this.shadowRoot.querySelector('#image');
const imgInput = imgBtn.querySelector('input');
imgInput.addEventListener('click', ev => {
ev.stopPropagation();
});
imgInput.addEventListener('change', ev => {
ev.stopPropagation();
const file = imgInput.files[0];
const reader = new FileReader();
let uploaded = false;
const uuid = crypto.randomUUID();
reader.onloadend = () => {
let el = this.#targetElement;
if (!el) return;
const img = new Image();
img.id = uuid;
img.src = reader.result || '';
if (uploaded) return;
el.appendChild(img);
this.#emitChange();
};
reader.readAsDataURL(file);
const xhr = new XMLHttpRequest();
xhr.addEventListener('loadend', (ev) => {
uploaded = true;
reader.abort();
const response = JSON.parse(ev.target.response);
{
const img = this.shadowRoot.querySelector(`img[id="${ uuid }"]`);
img && img.remove();
}
let el = this.#targetElement;
if (!el) return;
const img = new Image();
img.id = uuid;
img.src = response.path;
el.appendChild(img);
this.#emitChange();
});
xhr.open("POST", this.getAttribute('upload-url'));
const f = new FormData;
let name;
if (file.name.includes('.')) {
name = `${ uuid }.${ file.name.split('.').pop() }`;
} else {
name = uuid;
}
f.append(name, file);
xhr.send(f);
});
imgBtn.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#saveSelection();
imgInput.click();
});
}
{
const el = this.shadowRoot.querySelector("#align-justify");
el.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#saveSelection();
this.#setStyle('textAlign', 'justify');
});
}
{
const el = this.shadowRoot.querySelector("#align-left");
el.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#saveSelection();
this.#setStyle('textAlign', 'left');
});
}
{
const el = this.shadowRoot.querySelector("#align-center");
el.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#saveSelection();
this.#setStyle('textAlign', 'center');
});
}
{
const el = this.shadowRoot.querySelector("#align-right");
el.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.#saveSelection();
this.#setStyle('textAlign', 'right');
});
}
{
const button = this.shadowRoot.querySelector("#text-color");
const input = button.querySelector('input');
input.addEventListener('click', ev => ev.stopPropagation());
input.addEventListener('change', ev => {
ev.stopPropagation();
this.#setStyle('color', input.value);
});
button.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
input.click();
this.#saveSelection();
});
}
{
const button = this.shadowRoot.querySelector("#background-color");
const input = button.querySelector('input');
input.addEventListener('click', ev => ev.stopPropagation());
input.addEventListener('change', ev => {
ev.stopPropagation();
this.#setStyle('backgroundColor', input.value);
});
button.addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
input.click();
this.#saveSelection();
});
}
{
const el = this.shadowRoot.querySelector('#underscore');
el.addEventListener('click', ev => {
ev.stopPropagation();
this.#saveSelection();
this.#setStyle("textDecoration", 'underline');
});
}
{
const el = this.shadowRoot.querySelector('#superscript');
el.addEventListener('click', ev => {
ev.stopPropagation();
this.#saveSelection();
this.#setWrap(['sup'], { repeat: 'drop' });
});
}
{
const el = this.shadowRoot.querySelector('#subscription');
el.addEventListener('click', ev => {
ev.stopPropagation();
this.#saveSelection();
this.#setWrap(['sub'], { repeat: 'drop' });
});
}
}
static #isEditNode(el) {
while (true) {
if (!el) return false;
if (el.nodeType === Node.TEXT_NODE) {
el = el.parentElement;
continue;
}
if (!el.tagName) return false;
if (el.tagName.toLocaleLowerCase() === 'body') return false;
if (el.id === 'edit') return true;
el = el.parentElement;
}
}
#setWrap(tags, { repeat }) {
tags = tags.map(s => s.toLowerCase());
this.#isWrapped(tags);
if (this.#isWrapped(tags)) {
switch (repeat) {
case 'drop':
return this.#removeWrap(tags);
case 'perform':
return this.#createWrap(tags);
case 'ignore':
return this.#targetElement;
}
} else {
return this.#createWrap(tags);
}
}
#isWrapped(tags) {
let el = this.#targetElement;
if (!el) return false;
for (let i = tags.length - 1; i >= 0; i--) {
if (!el || !el.tagName || el.tagName.toLowerCase() !== tags[i]) {
return false;
}
el = el.parentElement;
}
return true;
}
#createWrap(tags) {
if (!this.#range) return;
let el;
let root = el = document.createElement(tags.shift());
const content = this.#range.extractContents();
this.#range.insertNode(root);
for (const tag of tags) {
const current = document.createElement(tag);
if (el) el.appendChild(current);
el = current;
}
for (const node of content.childNodes) {
el.appendChild(node);
}
this.#restoreSelection(el);
return el;
}
#removeWrap(tags) {
let el = this.#targetElement;
if (!el) return false;
for (let i = tags.length - 1; i > 0; i--) {
if (!el || !el.tagName || el.tagName.toLowerCase() !== tags[i]) {
return false;
}
el = el.parentElement;
}
const parent = el.parentElement;
parent.replaceChild(this.#selected, el);
}
#removeWrapper() {
const el = this.#selected;
if (!this.constructor.#isEditNode(el)) return;
const headerNode = el.parentElement;
if (!headerNode) return;
if (!headerNode.tagName.toLocaleLowerCase().startsWith('h')) {
return;
}
const parent = headerNode.parentElement;
parent.replaceChild(el, headerNode);
this.#restoreSelection();
}
#setStyle(setter, value) {
let el = this.#targetElement;
if (!el) return;
if (el.id === 'edit') {
const div = el.appendChild(document.createElement('div'));
div.appendChild(el);
el = div;
}
if (el.style[setter] === value) {
el.style[setter] = null;
} else {
el.style[setter] = value;
}
this.#restoreSelection();
}
#saveSelection() {
this.#selection = window.getSelection();
this.#range = this.#selection.getRangeAt(0);
this.#selection.removeAllRanges();
this.shadowRoot.querySelector('#edit').blur();
}
#restoreSelection(node) {
if (node) this.#range.selectNodeContents(node);
this.#emitChange();
}
get #targetElement() {
const selected = this.#selected;
if (!this.constructor.#isEditNode(selected)) return;
let el = selected;
if (el.nodeType === Node.TEXT_NODE)
el = el.parentElement;
if (!el) return;
return el;
}
get #selected() {
return this.#range.startContainer
}
get value() {
return this.shadowRoot.querySelector('#edit').innerHTML;
}
set value(html) {
this.shadowRoot.querySelector('#edit').innerHTML = html;
}
#emitChange() {
this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true }));
}
});

View File

@ -1,116 +0,0 @@
import { Component } from "../shared.js";
customElements.define('search-input', class extends Component {
#host;
static get observedAttributes() {
return ['filter', 'target']
}
constructor() {
super(`
<style>
:host { display: block; }
input {
font-size: 1rem;
line-height: 2.6em;
height: 2.6em;
margin: 0;
padding: 0;
width: 100%;
border:none;
outline:none;
display: block;
background: transparent;
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>
`);
const filter = this.shadowRoot.querySelector('#filter');
let t = null;
filter.addEventListener('change', ev => {
ev.stopPropagation();
this.filter = ev.target.value;
});
filter.addEventListener('keyup', ev => {
ev.stopPropagation();
const value = ev.target.value;
if (t) clearTimeout(t);
t = setTimeout(() => {
this.filter = value;
t = null;
}, 1000 / 3);
});
}
connectedCallback() {
super.connectedCallback();
let node = this;
while (node) {
if (node == null) return console.warn('no parent node', node);
if (node instanceof ShadowRoot) {
this.#host = node.host;
break;
}
node = node.parentNode;
}
}
get target() {
return this.getAttribute('target');
}
set target(tagName) {
this.setAttribute('target', tagName);
}
get filter() {
return this.getAttribute('filter');
}
set filter(value) {
console.warn(this.#host, value);
if (!this.#host) return;
const v = this.#host.querySelectorAll(this.target);
if (!value || value === '') {
this.removeAttribute('filter');
for (const el of v) {
el.removeAttribute('search-visible');
}
} else {
this.setAttribute('filter', value);
value = value.split(' ').filter(s => s && s.length).map(s => `(${ s })`).join('|');
for (const el of v) {
if (!el.matches) continue;
if (el.matches(new RegExp(value, 'ig'))) {
el.setAttribute('search-visible', 'visible');
} else {
el.setAttribute('search-visible', 'invisible');
}
}
}
}
});

View File

@ -1,73 +0,0 @@
import { Component, BUTTON_STYLE } from "../shared";
customElements.define('privacy-policy-bar', class extends Component {
static get observedAttributes() {
return ['accepted'];
}
constructor() {
super(`
<style>
:host {
display: block;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
}
article {
background: white;
padding: 4px;
border: 1px solid var(--border-slim-color);
margin: 4px;
}
article > input#accept {
margin-left: 16px;
margin-bottom: 0;
}
:host([accepted]) {
display: none;
}
${BUTTON_STYLE}
</style>
<article>
Strona korzysta z plików cookies w celu realizacji usług i zgodnie z
<a href="/privacy-policy" target="_blank">Polityką Prywatności</a>.
Możesz określić warunki przechowywania lub dostępu do plików cookies w Twojej przeglądarce
<input id="accept" value="Akceptuję" type="button" />
</article>
`);
this.shadowRoot.querySelector('#accept').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.accept();
});
}
connectedCallback() {
super.connectedCallback();
this.accepted = localStorage.getItem('cookies-accepted') === 'true';
}
attributeChangedCallback(name, oldV, newV) {
super.attributeChangedCallback(name, oldV, newV);
}
accept() {
this.accepted = true;
}
get accepted() {
return this.getAttribute('accepted') === 'true';
}
set accepted(v) {
v = v || localStorage.getItem('cookies-accepted') === 'true';
if (v === true || v === 'true') {
this.setAttribute('accepted', 'true');
localStorage.setItem('cookies-accepted', 'true');
} else {
this.removeAttribute('accepted');
localStorage.removeItem('cookies-accepted');
}
}
});

View File

@ -1,7 +0,0 @@
import { Component } from "../shared";
customElements.define('privacy-policy', class extends Component {
constructor() {
super(`<style>:host{display:block;}</style> <slot></slot>`);
}
});

View File

@ -1,14 +0,0 @@
import { Component } from "../shared";
customElements.define('terms-and-condition-rule', class extends Component {
constructor() {
super(`
<style>
:host {
display: block;
}
</style>
<slot></slot>
`);
}
});

View File

@ -1,14 +0,0 @@
import { Component } from "../shared";
customElements.define('terms-and-conditions', class extends Component {
constructor() {
super(`
<style>
:host {
display: block;
}
</style>
<slot></slot>
`);
}
});

File diff suppressed because it is too large Load Diff

View File

@ -1,14 +0,0 @@
[package]
name = "contract"
version = "0.1.0"
edition = "2021"
[features]
db = ['sqlx', 'byteorder']
[dependencies]
serde = { version = "*", features = ['derive'] }
chrono = { version = "*", features = ['serde'] }
sqlx = { version = "*", optional = true }
byteorder = { version = "1.4.3", optional = true }
base64 = { version = "*" }

View File

@ -1,78 +0,0 @@
use byteorder::ByteOrder;
use sqlx::database::{HasArguments, HasValueRef};
use sqlx::encode::IsNull;
use sqlx::error::BoxDynError;
use sqlx::postgres::PgValueFormat;
use sqlx::Postgres;
use crate::PriceRange;
fn take_i32(bytes: &mut &[u8]) -> i32 {
let value = byteorder::BigEndian::read_i32(&bytes[0..4]);
*bytes = &bytes[4..];
value
}
impl<'l> sqlx::Decode<'l, Postgres> for PriceRange {
fn decode(value: <Postgres as HasValueRef<'l>>::ValueRef) -> Result<Self, BoxDynError> {
match value.format() {
PgValueFormat::Text => {
let s = value.as_str()?;
eprintln!("{s:?}");
Ok(Self::Free)
}
PgValueFormat::Binary => {
let mut bytes = value.as_bytes()?;
// println!("{bytes:?}");
let _len = take_i32(&mut bytes);
let _ty = take_i32(&mut bytes);
let _min_len = take_i32(&mut bytes);
let min = take_i32(&mut bytes);
let _ty = take_i32(&mut bytes);
let _max_len = take_i32(&mut bytes);
let max = take_i32(&mut bytes);
Ok((min, max).into())
}
}
}
}
fn encode(n: i32, buf: &mut <Postgres as HasArguments<'_>>::ArgumentBuffer) {
let _ = <i32 as sqlx::Encode<'_, Postgres>>::encode(n, buf);
}
impl<'l> sqlx::Encode<'l, Postgres> for PriceRange {
fn encode_by_ref(&self, buf: &mut <Postgres as HasArguments<'l>>::ArgumentBuffer) -> IsNull {
encode(2i32, buf);
fn write_value(n: &i32, buf: &mut <Postgres as HasArguments<'_>>::ArgumentBuffer) {
encode(23i32, buf);
encode(4i32, buf);
encode(*n, buf);
}
match self {
PriceRange::Free => {
write_value(&0, buf);
write_value(&0, buf);
}
PriceRange::Fixed { value } => {
write_value(value, buf);
write_value(&0, buf);
}
PriceRange::Range { min, max } => {
write_value(min, buf);
write_value(max, buf);
}
}
IsNull::No
}
}
impl sqlx::Type<Postgres> for PriceRange {
fn type_info() -> sqlx::postgres::PgTypeInfo {
sqlx::postgres::PgTypeInfo::with_name("PriceRange")
}
}

View File

@ -1,454 +0,0 @@
use std::fmt::{Display, Formatter};
use chrono::{NaiveDateTime, Utc};
#[cfg(feature = "db")]
pub use db::*;
use serde::{Deserialize, Serialize};
#[cfg(feature = "db")]
mod db;
#[derive(Debug, Serialize, Deserialize)]
pub struct Account {
pub id: i32,
pub login: String,
pub email: String,
pub pass: String,
pub facebook_id: Option<String>,
pub account_type: AccountType,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct NewsArticle {
pub id: i32,
pub title: String,
pub body: String,
pub status: NewsStatus,
pub published_at: Option<NaiveDateTime>,
pub created_at: NaiveDateTime,
}
impl Default for NewsArticle {
fn default() -> Self {
Self {
id: 0,
title: "".to_string(),
body: "".to_string(),
status: NewsStatus::Pending,
published_at: None,
created_at: Utc::now().naive_utc(),
}
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum NewsStatus {
Pending,
Published,
Hidden,
}
impl NewsStatus {
pub fn as_str(&self) -> &str {
match self {
Self::Pending => "Pending",
Self::Published => "Published",
Self::Hidden => "Hidden",
}
}
}
#[derive(Debug, Default, Copy, Clone, Serialize, Deserialize)]
pub enum PriceRange {
#[default]
Free,
Fixed {
value: i32,
},
Range {
min: i32,
max: i32,
},
}
impl From<(i32, i32)> for PriceRange {
fn from((min, max): (i32, i32)) -> Self {
match (min, max) {
(0, 0) => Self::Free,
(_, 0) => Self::Fixed { value: min },
_ => Self::Range { min, max },
}
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[derive(Debug, Default, PartialOrd, PartialEq, Eq, Copy, Clone, Serialize, Deserialize)]
pub enum AccountType {
#[default]
User = 1,
Business = 10,
Admin = 100,
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[derive(Debug, Default, Copy, Clone, PartialEq, Serialize, Deserialize)]
pub enum LocalBusinessState {
#[default]
Pending = 1,
Approved = 2,
Banned = 3,
Pinned = 4,
Internal = 5,
}
impl Display for LocalBusinessState {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
impl LocalBusinessState {
pub fn as_str(&self) -> &str {
match self {
Self::Pending => "Pending",
Self::Approved => "Approved",
Self::Banned => "Banned",
Self::Pinned => "Pinned",
Self::Internal => "Internal",
}
}
}
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[derive(Debug, Default, Copy, Clone, PartialEq, PartialOrd, Serialize, Deserialize)]
pub enum OfferState {
#[default]
Pending = 0,
Approved = 1,
Banned = 2,
Finished = 3,
}
impl OfferState {
pub fn as_str(&self) -> &str {
match self {
OfferState::Pending => "Pending",
OfferState::Approved => "Approved",
OfferState::Banned => "Banned",
OfferState::Finished => "Finished",
}
}
}
#[derive(Debug, Default, Serialize, Deserialize)]
pub enum Page {
#[default]
LocalBusinesses,
News,
Account,
Register,
Login,
AccountBusinessItems,
Marketplace,
AccountOffers,
AdminNews,
AdminCreateNews,
AdminBusinesses,
AdminOffers,
Terms,
Privacy,
Business,
}
impl Page {
pub fn is_public(&self) -> bool {
!self.is_admin()
}
pub fn is_admin(&self) -> bool {
matches!(
self,
Page::AdminNews | Page::AdminCreateNews | Page::AdminBusinesses | Page::AdminOffers
)
}
pub fn select_index(&self) -> &str {
if matches!(self, Page::LocalBusinesses) {
"selected"
} else {
""
}
}
pub fn select_news(&self) -> &str {
if matches!(self, Page::News) {
"selected"
} else {
""
}
}
pub fn select_account(&self) -> &str {
if matches!(self, Page::Account) {
"selected"
} else {
""
}
}
pub fn select_marketplace(&self) -> &str {
if matches!(self, Page::Marketplace | Page::AccountOffers) {
"selected"
} else {
""
}
}
pub fn select_admin_news(&self) -> &str {
if matches!(self, Page::AdminNews) {
"selected"
} else {
""
}
}
pub fn select_admin_businesses(&self) -> &str {
if matches!(self, Page::AdminBusinesses) {
"selected"
} else {
""
}
}
pub fn select_admin_offers(&self) -> &str {
if matches!(self, Page::AdminOffers) {
"selected"
} else {
""
}
}
}
#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct BusinessItemInput {
pub name: String,
pub price: u32,
pub picture_url: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct SetStateBusinessInput {
pub id: i32,
pub state: LocalBusinessState,
}
#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocalBusiness {
pub id: i32,
pub owner_id: i32,
pub name: String,
pub description: String,
pub state: LocalBusinessState,
pub items: Vec<LocalBusinessItem>,
pub contacts: Vec<ContactInfo>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct ContactInfo {
pub id: i32,
pub owner_id: i32,
pub contact_type: String,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LocalBusinessItem {
pub id: i32,
pub local_business_id: i32,
pub name: String,
pub price: i64,
pub item_order: i32,
pub picture_url: String,
}
#[derive(Debug, Deserialize)]
pub struct CreateBusinessItemInput {
pub name: String,
pub price: i64,
pub picture_url: String,
pub item_order: i32,
}
#[derive(Debug, Deserialize)]
pub struct AtomicUpdateBusinessItemInput {
pub id: i32,
pub name: String,
pub description: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateBusinessItemInput {
pub id: i32,
pub name: String,
pub price: i64,
pub picture_url: String,
pub item_order: i32,
}
#[derive(Debug, Deserialize)]
pub struct ModifyBusinessItemInput {
pub id: i32,
}
#[derive(Debug, Deserialize)]
pub struct MoveBusinessItemInput {
pub id: i32,
pub item_order: i32,
}
#[derive(Debug, Deserialize)]
pub struct UpdateNewsArticleInput {
pub id: i32,
pub title: String,
pub body: String,
pub status: NewsStatus,
}
#[derive(Debug, Deserialize)]
pub struct CreateNewsArticleInput {
pub title: String,
pub body: String,
pub status: NewsStatus,
}
#[derive(Debug, Deserialize)]
pub struct DeleteNewsArticleInput {
pub id: i32,
}
#[derive(Debug, Deserialize)]
pub struct CreateContactInfoInput {
#[serde(rename = "type")]
pub contact_type: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct UpdateContactInfoInput {
pub id: i32,
#[serde(rename = "type")]
pub contact_type: String,
pub content: String,
}
#[derive(Debug, Deserialize)]
pub struct DeleteContactInfoInput {
pub id: i32,
}
#[derive(Debug, Deserialize)]
pub struct CreateOfferInput {
pub description: String,
pub picture_url: String,
pub price_min: i32,
pub price_max: i32,
}
#[derive(Debug, Deserialize)]
pub struct UpdateOfferInput {
pub id: i32,
pub description: String,
pub picture_url: String,
pub price_min: i32,
pub price_max: i32,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Offer {
pub id: i32,
pub owner_id: i32,
pub price_range: PriceRange,
pub description: String,
pub picture_url: String,
pub state: OfferState,
pub created_at: NaiveDateTime,
pub contacts: Vec<ContactInfo>,
}
impl Default for Offer {
fn default() -> Self {
Self {
id: 0,
owner_id: 0,
price_range: Default::default(),
description: "".to_string(),
picture_url: "".to_string(),
state: Default::default(),
created_at: chrono::Utc::now().naive_utc(),
contacts: vec![],
}
}
}
pub mod businesses {
use serde::{Deserialize, Serialize};
use super::*;
#[derive(Debug, Default, Serialize, Deserialize)]
pub struct BusinessList {
pub businesses: Vec<LocalBusiness>,
pub account: Option<Account>,
pub error: Option<String>,
pub page: Page,
#[serde(skip)]
pub h: Helper,
}
}
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct Helper;
impl Helper {
pub fn is_admin(&self, account: &Option<Account>) -> bool {
account
.as_ref()
.map(|a| a.account_type == AccountType::Admin)
.unwrap_or_default()
}
pub fn is_above_user(&self, account: &Option<Account>) -> bool {
account
.as_ref()
.map(|a| a.account_type > AccountType::User)
.unwrap_or_default()
}
pub fn account_id_tag(&self, account: &Option<Account>) -> String {
account
.as_ref()
.map(|a| format!("account-id={}", a.id))
.unwrap_or_default()
}
pub fn email<'a>(&self, account: &'a Option<Account>) -> &'a str {
account
.as_ref()
.map(|a| a.email.as_str())
.unwrap_or_default()
}
pub fn render_contact(&self, ty: &str, val: &str) -> String {
if ty == "mobile" {
self.mobile(val)
} else {
val.into()
}
}
fn mobile(&self, phone: &str) -> String {
base64::encode(phone)
}
}

View File

@ -0,0 +1,19 @@
[package]
name = "migration"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "migration"
path = "src/lib.rs"
[dependencies]
async-std = { version = "1", features = ["attributes", "tokio1"] }
oswilno-contract = { path = "../oswilno-contract" }
sea-orm = { version = "0.11.3", features = ["runtime-actix-rustls", "sqlx-postgres", "postgres-array", "sqlx"] }
tokio = { version = "1.29.1", features = ["full"] }
[dependencies.sea-orm-migration]
version = "0.11.0"
features = ["sqlx-postgres", "runtime-actix-rustls", "sea-orm-cli"]

View File

@ -0,0 +1,41 @@
# Running Migrator CLI
- Generate a new migration file
```sh
cargo run -- migrate generate MIGRATION_NAME
```
- Apply all pending migrations
```sh
cargo run
```
```sh
cargo run -- up
```
- Apply first 10 pending migrations
```sh
cargo run -- up -n 10
```
- Rollback last applied migrations
```sh
cargo run -- down
```
- Rollback last 10 applied migrations
```sh
cargo run -- down -n 10
```
- Drop all tables from the database, then reapply all migrations
```sh
cargo run -- fresh
```
- Rollback all applied migrations, then reapply all migrations
```sh
cargo run -- refresh
```
- Rollback all applied migrations
```sh
cargo run -- reset
```
- Check the status of all migrations
```sh
cargo run -- status
```

View File

@ -0,0 +1,65 @@
pub use sea_orm::Iterable;
pub use sea_orm_migration::prelude::*;
use sea_orm_migration::sea_orm::{DbBackend, Statement};
mod m20220101_000001_create_table;
mod m20230726_124452_images;
mod m20230726_135630_parking_spaces;
mod m20230805_000001_add_email;
mod m20230809_135630_add_spot;
mod m20230810_105100_create_parking_space_locations;
mod m20230919_162830_create_rent_requests;
pub struct Migrator;
#[async_trait::async_trait]
impl MigratorTrait for Migrator {
fn migrations() -> Vec<Box<dyn MigrationTrait>> {
vec![
Box::new(m20220101_000001_create_table::Migration),
Box::new(m20230726_124452_images::Migration),
Box::new(m20230726_135630_parking_spaces::Migration),
Box::new(m20230805_000001_add_email::Migration),
Box::new(m20230809_135630_add_spot::Migration),
Box::new(m20230810_105100_create_parking_space_locations::Migration),
Box::new(m20230919_162830_create_rent_requests::Migration),
]
}
}
pub async fn drop_enum<'m, Enum>(m: &SchemaManager<'m>) -> Result<(), DbErr>
where
Enum: Iterable + Iden + IntoIden,
{
let db_postgres = DbBackend::Postgres;
let name = Enum::iter().next().unwrap().to_string();
let s = format!("DROP TYPE {name};");
let stmt = Statement::from_string(db_postgres, s);
m.get_connection().execute(stmt).await?;
Ok(())
}
pub async fn create_enum<'m, Enum>(m: &SchemaManager<'m>) -> Result<(), DbErr>
where
Enum: Iterable + Iden + IntoIden,
{
let db_postgres = DbBackend::Postgres;
let s = {
let name = Enum::iter().next().unwrap().to_string();
let mut s = Enum::iter().skip(1).enumerate().fold(
format!("CREATE TYPE {name} AS ENUM ("),
|mut s, (idx, variant)| {
if idx != 0 {
s.push(',');
}
format!("{s} '{}'", variant.into_iden().quoted('\''))
},
);
s.push_str(" );");
eprintln!("{s:?}");
s
};
let stmt = Statement::from_string(db_postgres, s);
m.get_connection().execute(stmt).await?;
Ok(())
}

View File

@ -0,0 +1,106 @@
use sea_orm::{EnumIter, Iterable};
use sea_orm_migration::prelude::*;
use crate::{create_enum, drop_enum};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_enum::<Role>(m).await?;
let table = Table::create()
.table(Account::Accounts)
.if_not_exists()
.col(
ColumnDef::new(Account::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Account::Login).string().not_null())
.col(ColumnDef::new(Account::PassHash).string().not_null())
.col(
ColumnDef::new(Account::Role)
.enumeration(
Role::Role, //
Role::iter().skip(1),
)
.default(Role::User.to_string())
.not_null(),
)
.col(
ColumnDef::new(Account::Banned)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(Account::Confirmed)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(Account::Verified)
.boolean()
.default(false)
.not_null(),
)
.col(
ColumnDef::new(Account::CreatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.col(
ColumnDef::new(Account::UpdatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.if_not_exists()
.to_owned();
m.create_table(table).await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.drop_table(Table::drop().table(Account::Accounts).to_owned())
.await?;
drop_enum::<Role>(m).await?;
Ok(())
}
}
#[derive(Iden, EnumIter)]
#[iden(rename = "UserRole")]
pub enum Role {
#[iden(rename = "UserRole")]
Role,
#[iden(rename = "User")]
User,
#[iden(rename = "Admin")]
Admin,
}
#[derive(Iden)]
pub enum Account {
Accounts,
Id,
Login,
Email,
PassHash,
Role,
Banned,
Confirmed,
Verified,
CreatedAt,
UpdatedAt,
}

View File

@ -0,0 +1,93 @@
use sea_orm::{EnumIter, Iterable};
use sea_orm_migration::prelude::*;
use crate::m20220101_000001_create_table::Account;
use crate::{create_enum, drop_enum};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_enum::<ImageState>(m).await?;
m.create_table(
Table::create()
.table(Image::Images)
.if_not_exists()
.col(
ColumnDef::new(Image::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(Image::LocalPath).string().not_null())
.col(ColumnDef::new(Image::PublicPath).string().not_null())
.col(
ColumnDef::new(Image::ImageState)
.enumeration(ImageState::ImageState, ImageState::iter().skip(1))
.default(ImageState::Pending.to_string())
.not_null(),
)
.col(ColumnDef::new(Image::AccountId).integer().not_null())
.col(
ColumnDef::new(Image::CreatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.col(
ColumnDef::new(Image::UpdatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.if_not_exists()
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(Image::Images)
.from_col(Image::AccountId)
.to_tbl(Account::Accounts)
.to_col(Account::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.drop_table(Table::drop().table(Image::Images).to_owned())
.await?;
drop_enum::<ImageState>(m).await?;
Ok(())
}
}
#[derive(Iden)]
pub enum Image {
Images,
Id,
LocalPath,
PublicPath,
ImageState,
AccountId,
CreatedAt,
UpdatedAt,
}
#[derive(Iden, EnumIter)]
pub enum ImageState {
ImageState,
Pending,
Approved,
Banned,
}

View File

@ -0,0 +1,178 @@
use sea_orm::{EnumIter, Iterable};
use sea_orm_migration::prelude::*;
use crate::m20220101_000001_create_table::Account;
use crate::{create_enum, drop_enum};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
create_enum::<ParkingSpaceState>(m).await?;
create_enum::<Side>(m).await?;
m.create_table(
Table::create()
.table(ParkingSpace::ParkingSpaces)
.if_not_exists()
.col(
ColumnDef::new(ParkingSpace::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(ParkingSpace::State)
.enumeration(
ParkingSpaceState::ParkingSpaceState,
ParkingSpaceState::iter().skip(1),
)
.default(ParkingSpaceState::Pending.to_string())
.not_null(),
)
.col(ColumnDef::new(ParkingSpace::Location).string().not_null())
.col(ColumnDef::new(ParkingSpace::AccountId).integer().not_null())
.col(
ColumnDef::new(ParkingSpace::CreatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.col(
ColumnDef::new(ParkingSpace::UpdatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.if_not_exists()
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(ParkingSpace::ParkingSpaces)
.from_col(ParkingSpace::AccountId)
.to_tbl(Account::Accounts)
.to_col(Account::Id)
.to_owned(),
)
.await?;
m.create_table(
Table::create()
.table(ParkingSpaceRent::ParkingSpaceRents)
.if_not_exists()
.col(
ColumnDef::new(ParkingSpaceRent::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(ParkingSpaceRent::Price).integer().not_null())
.col(
ColumnDef::new(ParkingSpaceRent::ParkingSpaceId)
.integer()
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceRent::Available)
.boolean()
.default(true)
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceRent::CreatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceRent::UpdatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.if_not_exists()
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(ParkingSpaceRent::ParkingSpaceRents)
.from_col(ParkingSpaceRent::ParkingSpaceId)
.to_tbl(ParkingSpace::ParkingSpaces)
.to_col(ParkingSpace::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.drop_table(
Table::drop()
.table(ParkingSpaceRent::ParkingSpaceRents)
.to_owned(),
)
.await?;
m.drop_table(Table::drop().table(ParkingSpace::ParkingSpaces).to_owned())
.await?;
drop_enum::<Side>(m).await?;
drop_enum::<ParkingSpaceState>(m).await?;
Ok(())
}
}
#[derive(Iden)]
pub enum ParkingSpace {
ParkingSpaces,
Id,
State,
Location,
LocationId,
Spot,
AccountId,
CreatedAt,
UpdatedAt,
}
#[derive(Iden)]
pub enum ParkingSpaceRent {
ParkingSpaceRents,
Id,
Price,
Available,
ParkingSpaceId,
CreatedAt,
UpdatedAt,
}
#[derive(Iden, EnumIter)]
pub enum ParkingSpaceState {
ParkingSpaceState,
#[iden(rename = "Pending")]
Pending,
#[iden(rename = "Verified")]
Verified,
#[iden(rename = "Banned")]
Banned,
}
#[derive(Iden, EnumIter)]
pub enum Side {
Side,
#[iden(rename = "Left")]
Left,
#[iden(rename = "Right")]
Right,
#[iden(rename = "Front")]
Front,
}

View File

@ -0,0 +1,37 @@
use sea_orm_migration::prelude::*;
use crate::m20220101_000001_create_table::Account;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
let table = Table::alter()
.table(Account::Accounts)
.add_column(
ColumnDef::new(Account::Email)
.string()
.unique_key()
.default("filler@example.com")
.not_null(),
)
.to_owned();
m.alter_table(table).await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.alter_table(
Table::alter()
.table(Account::Accounts)
.drop_column(Account::Email)
.to_owned(),
)
.await?;
Ok(())
}
}

View File

@ -0,0 +1,33 @@
use sea_orm_migration::prelude::*;
use crate::m20230726_135630_parking_spaces::ParkingSpace;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.alter_table(
Table::alter()
.table(ParkingSpace::ParkingSpaces)
.add_column(ColumnDef::new(ParkingSpace::Spot).integer())
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.alter_table(
Table::alter()
.table(ParkingSpace::ParkingSpaces)
.drop_column(ParkingSpace::Spot)
.to_owned(),
)
.await?;
Ok(())
}
}

View File

@ -0,0 +1,115 @@
use sea_orm_migration::prelude::*;
use crate::m20230726_135630_parking_spaces::ParkingSpace;
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.create_table(
Table::create()
.table(ParkingSpaceLocation::ParkingSpaceLocations)
.if_not_exists()
.col(
ColumnDef::new(ParkingSpaceLocation::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(
ColumnDef::new(ParkingSpaceLocation::Name)
.string()
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceLocation::Number)
.integer()
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceLocation::Stage)
.string()
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceLocation::CreatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.col(
ColumnDef::new(ParkingSpaceLocation::UpdatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.if_not_exists()
.to_owned(),
)
.await?;
m.alter_table(
Table::alter()
.table(ParkingSpace::ParkingSpaces)
.drop_column(ParkingSpace::Location)
.to_owned(),
)
.await?;
m.alter_table(
Table::alter()
.table(ParkingSpace::ParkingSpaces)
.add_column(ColumnDef::new(ParkingSpace::LocationId).integer())
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(ParkingSpace::ParkingSpaces)
.from_col(ParkingSpace::LocationId)
.to_tbl(ParkingSpaceLocation::ParkingSpaceLocations)
.to_col(ParkingSpaceLocation::Id)
.to_owned(),
)
.await?;
m.create_index(
IndexCreateStatement::default()
.unique()
.name("uniq-location")
.table(ParkingSpaceLocation::ParkingSpaceLocations)
.col(ParkingSpaceLocation::Name)
.col(ParkingSpaceLocation::Number)
.col(ParkingSpaceLocation::Stage)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.drop_table(
Table::drop()
.table(ParkingSpaceLocation::ParkingSpaceLocations)
.to_owned(),
)
.await?;
Ok(())
}
}
#[derive(Iden)]
pub enum ParkingSpaceLocation {
ParkingSpaceLocations,
Id,
Name,
Number,
Stage,
CreatedAt,
UpdatedAt,
}

View File

@ -0,0 +1,91 @@
use sea_orm_migration::prelude::*;
use crate::m20220101_000001_create_table::Account;
use crate::m20230726_135630_parking_spaces::{ParkingSpace, ParkingSpaceRent};
#[derive(DeriveMigrationName)]
pub struct Migration;
#[async_trait::async_trait]
impl MigrationTrait for Migration {
async fn up(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.create_table(
Table::create()
.table(RentRequest::RentRequests)
.if_not_exists()
.col(
ColumnDef::new(RentRequest::Id)
.integer()
.not_null()
.auto_increment()
.primary_key(),
)
.col(ColumnDef::new(RentRequest::ParkingSpaceId).integer())
.col(ColumnDef::new(RentRequest::ParkingSpaceRentId).integer())
.col(ColumnDef::new(RentRequest::AccountId).integer())
.col(
ColumnDef::new(RentRequest::CreatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.col(
ColumnDef::new(RentRequest::UpdatedAt)
.timestamp()
.default(SimpleExpr::Custom("NOW()".to_owned()))
.not_null(),
)
.if_not_exists()
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(RentRequest::RentRequests)
.from_col(RentRequest::ParkingSpaceId)
.to_tbl(ParkingSpace::ParkingSpaces)
.to_col(ParkingSpace::Id)
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(RentRequest::RentRequests)
.from_col(RentRequest::ParkingSpaceId)
.to_tbl(ParkingSpaceRent::ParkingSpaceRents)
.to_col(ParkingSpaceRent::Id)
.to_owned(),
)
.await?;
m.create_foreign_key(
ForeignKeyCreateStatement::new()
.from_tbl(RentRequest::RentRequests)
.from_col(RentRequest::AccountId)
.to_tbl(Account::Accounts)
.to_col(Account::Id)
.to_owned(),
)
.await?;
Ok(())
}
async fn down(&self, m: &SchemaManager) -> Result<(), DbErr> {
m.drop_table(Table::drop().table(RentRequest::RentRequests).to_owned())
.await?;
Ok(())
}
}
#[derive(Iden)]
pub enum RentRequest {
RentRequests,
Id,
ParkingSpaceId,
ParkingSpaceRentId,
AccountId,
CreatedAt,
UpdatedAt,
}

View File

@ -0,0 +1,6 @@
use sea_orm_migration::prelude::*;
#[async_std::main]
async fn main() {
cli::run_cli(migration::Migrator).await;
}

View File

@ -0,0 +1,10 @@
[package]
name = "oswilno-actix-admin"
version = "0.1.0"
edition = "2021"
[lib]
proc-macro = true
[dependencies]

View File

@ -0,0 +1,162 @@
extern crate proc_macro;
use std::iter::Peekable;
use proc_macro::token_stream::IntoIter;
use proc_macro::{TokenStream, TokenTree};
fn expect_iden<F: FnMut(String) -> bool>(s: &mut Peekable<IntoIter>, mut name: F) {
let token = s.next().unwrap();
match token {
TokenTree::Ident(iden) => {
if !name(iden.to_string()) {
panic!("expect identifier");
}
}
_ => panic!("expect identifier got {token:?}"),
};
}
fn skip_proc_macro(s: &mut Peekable<IntoIter>) {
while let Some(token) = s.peek() {
match token {
TokenTree::Punct(p) if p.to_string().as_str() == "#" => {
s.next();
s.next();
}
_ => return,
}
}
}
#[derive(Default, Debug)]
struct VariantInfo {
rs_name: String,
db_name: String,
}
#[derive(Default, Debug)]
struct EnumDef {
name: String,
variants: Vec<VariantInfo>,
}
#[proc_macro_derive(ActixAdminEnum)]
pub fn derive_actix_admin_enum(item: TokenStream) -> TokenStream {
let clone = item.clone();
let mut it = clone.into_iter().peekable();
skip_proc_macro(&mut it);
expect_iden(&mut it, |iden| &iden == "pub");
expect_iden(&mut it, |iden| &iden == "enum");
let mut def = EnumDef::default();
expect_iden(&mut it, |iden| {
def.name = iden;
true
});
let mut it = if let TokenTree::Group(g) = it.next().unwrap() {
g.stream().into_iter().peekable()
} else {
panic!("expect enum body");
};
// Parse macros
parse_enum_variants(&mut it, &mut def);
let mut buffer = String::new();
buffer.push_str(&format!("impl std::str::FromStr for {} {{\n type Err = ();\n fn from_str(s: &str) -> Result<Self, ()> {{\n match s {{\n", def.name));
for v in &def.variants {
buffer.push_str(&format!(
" {:?} => Ok({}::{}),\n",
v.db_name, def.name, v.rs_name
));
}
buffer.push_str(" _ => Err(()),\n");
buffer.push_str(" }\n }\n}");
buffer.push_str(&format!(
"impl ToString for {} {{\n fn to_string(&self) -> String {{\n match self {{\n",
def.name
));
for v in def.variants {
buffer.push_str(&format!(
" {}::{} => {:?},\n",
def.name, v.rs_name, v.db_name
));
}
buffer.push_str(" }.to_string()\n }\n}");
// eprintln!("{buffer}");
buffer.as_str().parse().unwrap()
}
fn parse_enum_variants(body_it: &mut Peekable<IntoIter>, def: &mut EnumDef) {
while body_it.peek().is_some() {
if let Some(variant) = parse_enum_variant(body_it) {
def.variants.push(variant);
}
if let Some(TokenTree::Punct(p)) = body_it.peek() {
if p.to_string().as_str() == "," {
body_it.next();
}
}
}
}
fn parse_enum_variant(body_it: &mut Peekable<IntoIter>) -> Option<VariantInfo> {
let mut variant_info = VariantInfo::default();
while body_it.peek().is_some() {
if let Some(db_name) = parse_enum_variant_macro(body_it) {
variant_info.db_name = db_name;
break;
}
if let Some(TokenTree::Punct(p)) = body_it.peek() {
if p.to_string().as_str() == "," {
body_it.next();
}
}
}
let iden = body_it.next().expect("No variant name");
variant_info.rs_name = iden.to_string();
Some(variant_info)
}
fn parse_enum_variant_macro(body_it: &mut Peekable<IntoIter>) -> Option<String> {
let (_punc, group) = (
body_it.next().expect("No # for macro"),
body_it.next().expect("No macro body"),
);
// eprintln!("punc {_punc:#?} group {group:#?}");
let mut it = if let TokenTree::Group(g) = group {
// parse #[sea_orm]
g.stream().into_iter().peekable()
} else {
panic!("Unexpected token after #: {group:?}");
};
let token = it.next();
if let Some(TokenTree::Ident(iden)) = token {
if iden.to_string().as_str() != "sea_orm" {
eprintln!("iden is not sea_orm");
return None;
}
} else {
eprintln!("token should be iden but is {token:#?}");
return None;
}
let it = if let Some(TokenTree::Group(g)) = it.next() {
g.stream().into_iter().peekable()
} else {
return None;
};
let mut it = it.skip_while(|t| match t {
TokenTree::Ident(id) => id.to_string().as_str() != "string_value",
_ => true,
});
let (_id, _punct, literal) = (it.next(), it.next(), it.next());
match literal {
Some(TokenTree::Literal(l)) => Some(l.to_string().replace('\"', "")),
_ => None,
}
}

View File

@ -0,0 +1,17 @@
[package]
name = "oswilno-admin"
version = "0.1.0"
edition = "2021"
[dependencies]
# actix-admin = "0.5.0"
actix-admin = { git = "https://github.com/mgugger/actix-admin.git" }
# actix-admin = { git = "https://code.ita-prog.pl/Tsumanu/actix-admin.git", features = ['enable-tracing'] }
actix-web = "4.3.1"
actix-web-grants = "3.0.2"
askama = "0.12.0"
chrono = "0.4.26"
oswilno-contract = { path = "../oswilno-contract" }
tera = "1.17.1"
uuid = { version = "1.4.1", features = ["v4"] }
tracing = "0.1.37"

View File

@ -0,0 +1,2 @@
[general]
dirs = ["templates", "../oswilno-view/templates"]

View File

@ -0,0 +1,44 @@
use actix_admin::prelude::*;
use actix_web::web::{Data, ServiceConfig};
pub fn mount(config: &mut ServiceConfig) {
let actix_admin_builder = create_actix_admin_builder();
config
.app_data(Data::new(actix_admin_builder.get_actix_admin()))
.service(actix_admin_builder.get_scope());
}
fn create_actix_admin_builder() -> ActixAdminBuilder {
let configuration = ActixAdminConfiguration {
enable_auth: false,
user_is_logged_in: None,
login_link: None,
logout_link: None,
file_upload_directory: "./file_uploads",
navbar_title: "oswilno - admin",
user_tenant_ref: None,
};
let mut admin_builder = ActixAdminBuilder::new(configuration);
{
use oswilno_contract::prelude::Accounts;
let view = ActixAdminViewModel::from(Accounts);
admin_builder.add_entity::<Accounts>(&view);
}
{
use oswilno_contract::prelude::ParkingSpaces;
let view = ActixAdminViewModel::from(ParkingSpaces);
admin_builder.add_entity_to_category::<ParkingSpaces>(&view, "Parking spaces");
}
{
use oswilno_contract::prelude::ParkingSpaceRents;
let view = ActixAdminViewModel::from(ParkingSpaceRents);
admin_builder.add_entity_to_category::<ParkingSpaceRents>(&view, "Parking spaces");
}
for scope in admin_builder.scopes.keys() {
tracing::trace!("Scope {scope:?}");
}
admin_builder
}

View File

@ -0,0 +1,10 @@
[package]
name = "oswilno-config"
version = "0.1.0"
edition = "2021"
[dependencies]
serde = { version = "1.0.175", features = ["derive"] }
serde_json = "1.0.103"
toml = "0.7.6"

View File

@ -0,0 +1,7 @@
use serde::{Deserialize, Serialize};
#[derive(Debug, PartialEq, Serialize, Deserialize)]
pub struct Config {
bind: Option<String>,
port: Option<u16>,
}

View File

@ -0,0 +1,18 @@
[package]
name = "oswilno-contract"
version = "0.1.0"
edition = "2021"
[dependencies]
actix = "0.13.0"
# actix-admin = "0.5.0"
# actix-admin = { git = "https://github.com/Eraden/actix-admin.git", features = ['enable-tracing'] }
actix-admin = { git = "https://github.com/mgugger/actix-admin.git" }
# actix-admin = { git = "https://code.ita-prog.pl/Tsumanu/actix-admin.git", features = ['enable-tracing'] }
actix-rt = { version = "2.8.0", features = [] }
chrono = { version = "0.4.26", features = ["serde"] }
oswilno-actix-admin = { path = "../oswilno-actix-admin" }
regex = "1.9.1"
sea-orm = { version = "0.12", features = ["postgres-array", "runtime-actix-rustls", "sqlx-postgres", "macros", "sqlx"] }
serde = { version = "1.0.175", features = ["derive"] }
uuid = { version = "1.4.1", features = ["v4"] }

View File

@ -0,0 +1,129 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
#[allow(unused_imports)]
use sea_orm::Iterable;
use super::sea_orm_active_enums::Userrole;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl EntityName for Entity {
fn table_name(&self) -> &str {
"accounts"
}
}
#[derive(
Clone,
Debug,
PartialEq,
DeriveModel,
DeriveActiveModel,
Eq,
DeriveActixAdmin,
DeriveActixAdminModel,
DeriveActixAdminViewModel,
)]
pub struct Model {
#[actix_admin(primary_key)]
pub id: i32,
pub login: String,
#[actix_admin(list_hide_column)]
pub pass_hash: String,
pub role: Userrole,
pub banned: bool,
pub confirmed: bool,
pub verified: bool,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub created_at: DateTime,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub updated_at: DateTime,
#[actix_admin(html_input_type = "email")]
pub email: String,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Login,
PassHash,
Role,
Banned,
Confirmed,
Verified,
CreatedAt,
UpdatedAt,
Email,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Images,
ParkingSpaces,
RentRequests,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Login => ColumnType::String(None).def(),
Self::PassHash => ColumnType::String(None).def(),
Self::Role => Userrole::db_type(),
Self::Banned => ColumnType::Boolean.def(),
Self::Confirmed => ColumnType::Boolean.def(),
Self::Verified => ColumnType::Boolean.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
Self::Email => ColumnType::String(None).def().unique(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Images => Entity::has_many(super::images::Entity).into(),
Self::ParkingSpaces => Entity::has_many(super::parking_spaces::Entity).into(),
Self::RentRequests => Entity::has_many(super::rent_requests::Entity).into(),
}
}
}
impl Related<super::images::Entity> for Entity {
fn to() -> RelationDef {
Relation::Images.def()
}
}
impl Related<super::parking_spaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaces.def()
}
}
impl Related<super::rent_requests::Entity> for Entity {
fn to() -> RelationDef {
Relation::RentRequests.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,105 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
#[allow(unused_imports)]
use sea_orm::Iterable;
use super::sea_orm_active_enums::ImageState;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl EntityName for Entity {
fn table_name(&self) -> &str {
"images"
}
}
#[derive(
Clone,
Debug,
PartialEq,
DeriveModel,
DeriveActiveModel,
Eq,
DeriveActixAdmin,
DeriveActixAdminModel,
DeriveActixAdminViewModel,
)]
pub struct Model {
#[actix_admin(primary_key)]
pub id: i32,
pub local_path: String,
pub public_path: String,
pub image_state: ImageState,
pub account_id: i32,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub created_at: DateTime,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
LocalPath,
PublicPath,
ImageState,
AccountId,
CreatedAt,
UpdatedAt,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Accounts,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::LocalPath => ColumnType::String(None).def(),
Self::PublicPath => ColumnType::String(None).def(),
Self::ImageState => ImageState::db_type(),
Self::AccountId => ColumnType::Integer.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Accounts => Entity::belongs_to(super::accounts::Entity)
.from(Column::AccountId)
.to(super::accounts::Column::Id)
.into(),
}
}
}
impl Related<super::accounts::Entity> for Entity {
fn to() -> RelationDef {
Relation::Accounts.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,12 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
pub mod prelude;
pub mod accounts;
pub mod images;
pub mod parking_space_locations;
pub mod parking_space_rents;
pub mod parking_spaces;
pub mod rent_requests;
pub mod sea_orm_active_enums;
pub use {::chrono, actix_admin, sea_orm};

View File

@ -0,0 +1,97 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
#[allow(unused_imports)]
use sea_orm::Iterable;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl EntityName for Entity {
fn table_name(&self) -> &str {
"parking_space_locations"
}
}
#[derive(
Clone,
Debug,
PartialEq,
DeriveModel,
DeriveActiveModel,
Eq,
DeriveActixAdmin,
DeriveActixAdminModel,
DeriveActixAdminViewModel,
)]
pub struct Model {
#[actix_admin(primary_key)]
pub id: i32,
pub name: String,
pub number: i32,
pub stage: String,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub created_at: DateTime,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Name,
Number,
Stage,
CreatedAt,
UpdatedAt,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
ParkingSpaces,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Name => ColumnType::String(None).def(),
Self::Number => ColumnType::Integer.def(),
Self::Stage => ColumnType::String(None).def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::ParkingSpaces => Entity::has_many(super::parking_spaces::Entity).into(),
}
}
}
impl Related<super::parking_spaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaces.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,109 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
#[allow(unused_imports)]
use sea_orm::Iterable;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl EntityName for Entity {
fn table_name(&self) -> &str {
"parking_space_rents"
}
}
#[derive(
Clone,
Debug,
PartialEq,
DeriveModel,
DeriveActiveModel,
Eq,
DeriveActixAdmin,
DeriveActixAdminModel,
DeriveActixAdminViewModel,
)]
pub struct Model {
#[actix_admin(primary_key)]
pub id: i32,
pub price: i32,
#[actix_admin(select_list=crate::parking_spaces::Entity)]
pub parking_space_id: i32,
pub available: bool,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub created_at: DateTime,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub updated_at: DateTime,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
Price,
ParkingSpaceId,
Available,
CreatedAt,
UpdatedAt,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
ParkingSpaces,
RentRequests,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::Price => ColumnType::Integer.def(),
Self::ParkingSpaceId => ColumnType::Integer.def(),
Self::Available => ColumnType::Boolean.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::ParkingSpaces => Entity::belongs_to(super::parking_spaces::Entity)
.from(Column::ParkingSpaceId)
.to(super::parking_spaces::Column::Id)
.into(),
Self::RentRequests => Entity::has_many(super::rent_requests::Entity).into(),
}
}
}
impl Related<super::parking_spaces::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaces.def()
}
}
impl Related<super::rent_requests::Entity> for Entity {
fn to() -> RelationDef {
Relation::RentRequests.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View File

@ -0,0 +1,136 @@
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
use actix_admin::prelude::*;
use sea_orm::entity::prelude::*;
#[allow(unused_imports)]
use sea_orm::Iterable;
use super::sea_orm_active_enums::ParkingSpaceState;
#[derive(Copy, Clone, Default, Debug, DeriveEntity)]
pub struct Entity;
impl ActixAdminModelValidationTrait<ActiveModel> for Entity {}
impl ActixAdminModelFilterTrait<Entity> for Entity {}
impl EntityName for Entity {
fn table_name(&self) -> &str {
"parking_spaces"
}
}
#[derive(
Clone,
Debug,
PartialEq,
DeriveModel,
DeriveActiveModel,
Eq,
DeriveActixAdmin,
DeriveActixAdminModel,
DeriveActixAdminViewModel,
)]
pub struct Model {
#[actix_admin(primary_key)]
pub id: i32,
#[actix_admin(select_list=crate::parking_spaces::ParkingSpaceState)]
pub state: ParkingSpaceState,
pub account_id: i32,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub created_at: DateTime,
#[actix_admin(list_hide_column, column_type = "NaiveDateTime")]
pub updated_at: DateTime,
pub spot: Option<i32>,
#[actix_admin(select_list=crate::parking_space_locations::Entity)]
pub location_id: Option<i32>,
}
#[derive(Copy, Clone, Debug, EnumIter, DeriveColumn)]
pub enum Column {
Id,
State,
AccountId,
CreatedAt,
UpdatedAt,
Spot,
LocationId,
}
#[derive(Copy, Clone, Debug, EnumIter, DerivePrimaryKey)]
pub enum PrimaryKey {
Id,
}
impl PrimaryKeyTrait for PrimaryKey {
type ValueType = i32;
fn auto_increment() -> bool {
true
}
}
#[derive(Copy, Clone, Debug, EnumIter)]
pub enum Relation {
Accounts,
ParkingSpaceLocations,
ParkingSpaceRents,
RentRequests,
}
impl ColumnTrait for Column {
type EntityName = Entity;
fn def(&self) -> ColumnDef {
match self {
Self::Id => ColumnType::Integer.def(),
Self::State => ParkingSpaceState::db_type(),
Self::AccountId => ColumnType::Integer.def(),
Self::CreatedAt => ColumnType::DateTime.def(),
Self::UpdatedAt => ColumnType::DateTime.def(),
Self::Spot => ColumnType::Integer.def().null(),
Self::LocationId => ColumnType::Integer.def().null(),
}
}
}
impl RelationTrait for Relation {
fn def(&self) -> RelationDef {
match self {
Self::Accounts => Entity::belongs_to(super::accounts::Entity)
.from(Column::AccountId)
.to(super::accounts::Column::Id)
.into(),
Self::ParkingSpaceLocations => {
Entity::belongs_to(super::parking_space_locations::Entity)
.from(Column::LocationId)
.to(super::parking_space_locations::Column::Id)
.into()
}
Self::ParkingSpaceRents => Entity::has_many(super::parking_space_rents::Entity).into(),
Self::RentRequests => Entity::has_many(super::rent_requests::Entity).into(),
}
}
}
impl Related<super::accounts::Entity> for Entity {
fn to() -> RelationDef {
Relation::Accounts.def()
}
}
impl Related<super::parking_space_locations::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaceLocations.def()
}
}
impl Related<super::parking_space_rents::Entity> for Entity {
fn to() -> RelationDef {
Relation::ParkingSpaceRents.def()
}
}
impl Related<super::rent_requests::Entity> for Entity {
fn to() -> RelationDef {
Relation::RentRequests.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

Some files were not shown because too many files have changed in this diff Show More