Sign in form

This commit is contained in:
Adrian Woźniak 2022-07-04 16:00:17 +02:00
parent 5469fda5de
commit 277aae9341
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
8 changed files with 844 additions and 268 deletions

View File

@ -21,6 +21,11 @@
@import url('http://fonts.cdnfonts.com/css/noto-sans');
* {
--hover-color: #f18902;
--border-slim-color: #495057;
}
main {
font-family: 'Noto Sans', sans-serif;
}

View File

@ -1,9 +1,119 @@
const S = Symbol();
customElements.define('ow-nav', class extends HTMLElement {
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
section {
display: flex;
align-items: stretch;
align-content: stretch;
margin-bottom: 12px;
color: #2b2727;
font-family: Raleway, sans-serif;
font-size: 14px;
line-height: 32px;
}
section::after {
display: block;
content: ' ';
border: none;
padding: 10px 18px;
text-decoration: none;
color: #2b2727;
text-transform: uppercase;
align-self: stretch;
flex-shrink: 20;
flex-grow: 20;
}
</style>
<section>
<slot></slot>
</section>
`;
}
});
customElements.define('ow-path', class extends HTMLElement {
static get observedAttributes() {
return ['selected', 'path'];
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
a {
display: block;
padding: 10px 18px;
text-decoration: none;
color: #495057;
text-transform: uppercase;
border: none;
border-bottom: 1px solid var(--border-slim-color);
}
a:hover {
color: var(--hover-color);
}
:host(:not([selected])) a {
border: none;
}
</style>
<a><slot></slot></a>
`;
}
connectedCallback() {
this.selected = this.getAttribute('selected');
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'selected':
return this.selected = newV;
case 'path':
return this.path = newV;
}
}
get selected() {
return this.getAttribute('selected') === 'selected';
}
set selected(value) {
if (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[S].querySelector('a').setAttribute('href', value);
}
});
customElements.define('local-services', class extends HTMLElement {
static get observedAttributes() {
return ['filter']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
@ -14,9 +124,23 @@ customElements.define('local-services', class extends HTMLElement {
::slotted(local-service[local-services-visible="invisible"]) {
display: none;
}
input {
font-size: 20pt;
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;
}
</style>
<section>
<input type="text" id="filter" />
<input type="text" id="filter" placeholder="Filtruj" />
</section>
<section id="items">
<slot name="services"></slot>
@ -31,9 +155,11 @@ customElements.define('local-services', class extends HTMLElement {
});
filter.addEventListener('keyup', ev => {
ev.stopPropagation();
const value = ev.target.value;
if (t) clearTimeout(t);
t = setTimeout(() => {
this.filter = ev.target.value;
this.filter = value;
t = null;
}, 1000 / 3);
});
@ -45,9 +171,10 @@ customElements.define('local-services', class extends HTMLElement {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV == newV) return;
if (oldV === newV) return;
switch (name) {
case 'filter': return this.filter = newV;
case 'filter':
return this.filter = newV;
}
}
@ -56,6 +183,12 @@ customElements.define('local-services', class extends HTMLElement {
}
set filter(value) {
if (!value || value === '') {
this.removeAttribute('filter');
for (const el of this.querySelectorAll('local-service')) {
el.removeAttribute('local-services-visible');
}
} else {
this.setAttribute('filter', value);
for (const el of this.querySelectorAll('local-service')) {
if (!el.name) continue;
@ -66,12 +199,14 @@ customElements.define('local-services', class extends HTMLElement {
}
}
}
}
});
customElements.define('local-service', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'service-id', 'state']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
@ -79,6 +214,9 @@ customElements.define('local-service', class extends HTMLElement {
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#items {
margin-top: 16px;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
@ -93,8 +231,10 @@ customElements.define('local-service', class extends HTMLElement {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name': return this[S].querySelector('#name').textContent = newV;
case 'name':
return this[S].querySelector('#name').textContent = newV;
}
}
@ -108,6 +248,7 @@ customElements.define('local-service-item', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'price']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
@ -139,9 +280,12 @@ customElements.define('local-service-item', class extends HTMLElement {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name': return this[S].querySelector('#name').textContent = newV;
case 'price': return this[S].querySelector('#price').value = newV;
case 'name':
return this[S].querySelector('#name').textContent = newV;
case 'price':
return this[S].querySelector('#price').value = newV;
}
}
@ -155,6 +299,7 @@ customElements.define('ow-price', class extends HTMLElement {
static get observedAttributes() {
return ['value', 'currency']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
@ -175,6 +320,7 @@ customElements.define('ow-price', class extends HTMLElement {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'price': {
this.value = newV;
@ -205,3 +351,355 @@ customElements.define('ow-price', class extends HTMLElement {
return this.getAttribute('currency') || 'PLN'
}
});
customElements.define('ow-account', class extends HTMLElement {
static get observedAttributes() {
return ['mode']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
article > * {
display: none;
}
:host([mode="login"]) login-form, :host([mode="login"]) #switch-register {
display: block;
}
:host([mode="register"]) register-form, :host([mode="register"]) #switch-login {
display: block;
}
a{
display: block;
cursor: pointer;
}
a:hover {
color: var(--hover-color);
border-bottom-color: var(--hover-color);
text-decoration: none;
}
</style>
<article>
<login-form></login-form>
<register-form></register-form>
<section id="switch-login">
<a>Nie masz konta? Utwórz nowe</a>
</section>
<section id="switch-register">
<a>Masz konta? Zaloguj się</a>
</section>
</article>
`;
shadow.querySelector('#switch-login > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'login';
});
shadow.querySelector('#switch-register > a').addEventListener('click', ev => {
ev.stopPropagation();
ev.preventDefault();
this.mode = 'register';
});
}
connectedCallback() {
this.mode = 'login';
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
get mode() {
return this.getAttribute('mode');
}
set mode(value) {
value = value === 'login' || value === 'register' ? value : 'login';
this.setAttribute('mode', value);
}
});
const FORM_STYLE = `
form {
display: block;
}
form > div {
display: block;
margin-bottom: 1rem;
}
form > div > input, form > div > textarea {
font-size: 16px;
border: none;
border-bottom-style: none;
border-bottom-width: medium;
border-bottom: 1px solid rgba(0,0,0,.1);
border-radius: 2px;
padding: 0;
height: 36px;
background: #fff;
color: rgba(0,0,0,.8);
font-size: 14px;
box-shadow: none !important;
display: block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-clip: padding-box;
transition: border-color .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out;
transition: border-color .15s ease-in-out , box-shadow .15s ease-in-out , -webkit-box-shadow .15s ease-in-out;
}
form > div > input[type="text"],
form > div > input[type="number"],
form > div > input[type="email"],
form > div > input[type="password"],
form > div > textarea {
width: calc(100% - 1.5rem - 2px);
}
form > div > label {
color: #000;
text-transform: uppercase;
font-size: 12px;
font-weight: 600;
display: inline-block;
margin-bottom: .5rem;
}
form > div > input[type="button"], form > div > input[type="submit"] {
padding: 12px 16px;
cursor: pointer;
border: none;
border-width: 1px;
border-radius: 5px;
font-size: 14px;
font-weight: 400;
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);
position: relative;
margin-bottom: 20px;
transition: .3s;
background: #46b5d1;
color: #fff;
display: inline-block;
font-weight: 400;
text-align: center;
vertical-align: middle;
user-select: none;
padding: .375rem .75rem;
font-size: 1rem;
line-height: 1.5;
transition: color .15s ease-in-out,
background-color .15s ease-in-out,
border-color .15s ease-in-out,
box-shadow .15s ease-in-out,
-webkit-box-shadow .15s ease-in-out;
width: auto;
height: calc(1.5em + 0.75rem + 2px);
padding: .375rem .75rem;
}
`;
customElements.define('register-form', class extends HTMLElement {
static get observedAttributes() {
return ['step']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
article > form { display: none; }
:host([step="1"]) #step-1 { display: block; }
:host([step="2"]) #step-2 { display: block; }
:host([step="3"]) #step-3 { display: block; }
${ FORM_STYLE }
.actions {
display: flex;
justify-content: space-between;
}
.actions > input:first-child {
margin-right: 8px;
}
.actions > input:last-child {
margin-left: 8px;
}
</style>
<article>
<form id="step-1">
<div>
<label>Login</label>
<input id="login" name="login" placeholder="Login" type="text" required />
</div>
<div>
<label>E-Mail</label>
<input name="email" placeholder="Email" type="email" required />
</div>
<div>
<label>Hasło</label>
<input name="pass" placeholder="Hasło" type="password" required />
</div>
<div>
<input type="submit" value="Następny" />
</div>
</form>
<form id="step-2">
<div>
<input name="name" placeholder="Nazwa usługi" type="text" required />
</div>
<div>
<label>description</label>
<textarea name="description" required></textarea>
</div>
<div class="actions">
<input type="button" value="Wróć" />
<input type="submit" value="Następny" />
</div>
</form>
<form id="step-3">
<input id="hidden-login" name="login" type="hidden" />
<input id="hidden-email" name="email" type="hidden" />
<input id="hidden-pass" name="pass" type="hidden" />
<input id="hidden-name" name="name" type="hidden" />
<div class="actions">
<input type="button" value="Wróć" />
<input type="submit" value="Dodaj usługi/produkty" />
</div>
</form>
</article>
`;
const copyForm = (form) => {
for (const el of form.elements) {
if (el.name === '') continue;
finalForm.querySelector(`[id="hidden-${el.name}"]`).value = el.value;
}
};
const finalForm = shadow.querySelector('#step-3');
{
const el = shadow.querySelector('#step-1');
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
copyForm(el);
this.step = 2;
});
}
{
const el = shadow.querySelector('#step-2');
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
copyForm(el);
this.step = 3;
});
el.querySelector('.actions > input[type="button"]').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.step = 1;
});
}
{
const el = finalForm;
el.addEventListener('submit', ev => {
ev.preventDefault();
ev.stopPropagation();
copyForm(el);
let content = [
...shadow.querySelector('#step-1').elements,
...shadow.querySelector('#step-2').elements,
...shadow.querySelector('#step-3').elements,
]
.filter(el => el && el.name !== '')
.reduce((mem, el) => ({ ...mem, [el.name]: el.value }), {});
console.info(content);
});
el.querySelector('.actions > input[type="button"]').addEventListener('click', ev => {
ev.preventDefault();
ev.stopPropagation();
this.step = 2;
});
}
}
connectedCallback() {
this.step = 1;
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
get step() {
const step = parseInt(this.getAttribute('step'));
return isNaN(step) ? 1 : step;
}
set step(n) {
this.setAttribute('step', n);
}
});
customElements.define('login-form', class extends HTMLElement {
static get observedAttributes() {
return []
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
${ FORM_STYLE }
</style>
<form>
<div>
<label>Login</label>
<input name="login" placeholder="Login" type="text" required />
</div>
<div>
<label>Hasło</label>
<input name="pass" placeholder="Hasło" type="password" required />
</div>
<div>
<input type="button" value="Zaloguj" />
</div>
</form>
`;
}
connectedCallback() {
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
}
}
});

View File

@ -0,0 +1,29 @@
<!DOCTYPE html>
<html lang="pl">
<head>
<title>OS Wilno</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/assets/css/reset.css" rel="stylesheet"/>
<link href="/assets/css/app.css" rel="stylesheet"/>
<script type="module" src=/assets/js/app.js></script>
</head>
<body>
<main>
<header>
<div class="bg">
<h1>OS Wilno</h1>
</div>
</header>
<article>
<ow-nav>
<ow-path path="/">Lokalne Usługi</ow-path>
<ow-path path="/">Aktualności</ow-path>
<ow-path path="/account" selected="selected">Konto</ow-path>
</ow-nav>
<ow-account>
</ow-account>
</article>
</main>
</body>
</html>

View File

@ -1,5 +1,5 @@
<!DOCTYPE html>
<html>
<html lang="pl">
<head>
<title>OS Wilno</title>
<meta charset="UTF-8">
@ -16,10 +16,19 @@
</div>
</header>
<article>
<h1>Lokalne usługi</h1>
<ow-nav>
<ow-path path="/" selected="selected">Lokalne Usługi</ow-path>
<ow-path path="/">Aktualności</ow-path>
<ow-path path="/account">Konto</ow-path>
</ow-nav>
<local-services>
{% for service in services %}
<local-service slot="services" service-id="{{service.id}}" name="{{service.name}}" state="{{service.state.to_str()}}">
<local-service
slot="services"
service-id="{{service.id}}"
name="{{service.name}}"
state="{{service.state.as_str()}}"
>
{% for line in service.description.lines() %}
<p slot="description">{{line}}</p>
{% endfor %}

18
db/seed/001_init.psql Normal file
View File

@ -0,0 +1,18 @@
INSERT INTO accounts (login, pass, email)
VALUES ('Foo', 'Bar', 'foo@example.com');
INSERT INTO local_services (name, description, owner_id)
VALUES ('Cheap Tees', 'Unlimited possiblities! You can move the water masks to create your own frame. It gives you an opportunity to fit the layers to your image. Create great final effect with seconds', 1),
('Tema Model Agency Ltd', 'Special for website developers and app ui designers, to preview their apps in a professional way, showcasing details and focus on Responsive Design for Website and apps, Vol 09.', 1),
('Neet Online Test Series - Nots', 'Advanced, easy to edit mockup. It contains everything you need to create a realistic look of your project. Guarantees the a good look for bright and dark designs and perfect fit to the shape. Easy to navigate, well described layers, friendly help file.', 1);
INSERT INTO local_service_items (name, price, local_service_id, item_order)
VALUES ('Water Frame', 23423, 1, 1),
('Macbook Laptop Display 2.0', 927, 1, 2),
('Paper Band', 920, 1, 3),
('Artbox', 2500, 2, 1),
('School Backpacks', 2150, 2, 2),
('Premium Paper Cup', 2090, 3, 1),
('Beer Black Bottle', 5000, 3, 2),
('Circle Photo Frame', 2300, 3, 3),
('Grey T-Shirt', 3430, 3, 4);

View File

@ -0,0 +1 @@
ALTER TABLE accounts ADD COLUMN email text not null default '' unique;

View File

@ -8,6 +8,7 @@ use uuid::Uuid;
pub struct Account {
pub id: i32,
pub login: String,
pub email: String,
pub pass: String,
}
@ -27,7 +28,7 @@ pub enum LocalServiceState {
}
impl LocalServiceState {
pub fn to_str(&self) -> &str {
pub fn as_str(&self) -> &str {
match self {
Self::Pending => "Pending",
Self::Approved => "Approved",

View File

@ -66,10 +66,25 @@ FROM local_service_items
HttpResponse::Ok().body(body)
}
#[derive(Template)]
#[template(path = "account.html")]
struct AccountTemplate;
#[get("/account")]
async fn account() -> HttpResponse {
HttpResponse::Ok().body(AccountTemplate.render().unwrap())
}
pub fn configure(config: &mut ServiceConfig) {
config
.service(Files::new("/assets/images", "./assets/images"))
.service(Files::new("/assets/css", "./assets/css"))
.service(Files::new("/assets/js", "./assets/js").use_etag(true).prefer_utf8(true).show_files_listing())
.service(index);
.service(
Files::new("/assets/js", "./assets/js")
.use_etag(true)
.prefer_utf8(true)
.show_files_listing(),
)
.service(index)
.service(account);
}