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

@ -1,64 +1,69 @@
/* @media (min-width: 1200px) {
.bg {
display: block;
text-align: center;
width: 100%;
background:
radial-gradient(circle, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 45%, rgba(255,255,255,1) 100%),
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
height: 200px;
}
.bg {
display: block;
text-align: center;
width: 100%;
background:
radial-gradient(circle, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 45%, rgba(255,255,255,1) 100%),
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
height: 200px;
}
.bg h1 {
text-align: center;
font-weight: bold;
font-size: 50px;
text-shadow: 2px 2px 2px #c5d1d8;
color: #FFF;
padding-top: 60px;
}
.bg h1 {
text-align: center;
font-weight: bold;
font-size: 50px;
text-shadow: 2px 2px 2px #c5d1d8;
color: #FFF;
padding-top: 60px;
}
}*/
@import url('http://fonts.cdnfonts.com/css/noto-sans');
* {
--hover-color: #f18902;
--border-slim-color: #495057;
}
main {
font-family: 'Noto Sans', sans-serif;
font-family: 'Noto Sans', sans-serif;
}
* {
font-family: 'Noto Sans', sans-serif;
font-family: 'Noto Sans', sans-serif;
}
@media (min-width: 1200px) {
article {
width: 1280px;
margin: auto auto;
}
.bg {
height: 200px;
display: flex;
justify-content: space-between;
}
.bg::after {
display: block;
text-align: center;
width: 100%;
background:
linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(255,255,255,1) 1%, rgba(255,255,255,0) 100%),
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
height: 200px;
width: calc(100% - 300px);
max-width: 1200px;
content: ' ';
}
article {
width: 1280px;
margin: auto auto;
}
.bg {
height: 200px;
display: flex;
justify-content: space-between;
}
.bg::after {
display: block;
text-align: center;
width: 100%;
background:
linear-gradient(90deg, rgba(255,255,255,1) 0%, rgba(255,255,255,1) 1%, rgba(255,255,255,0) 100%),
no-repeat center image-set(url("/assets/images/background.webp"), url("/assets/images/background.jpeg"));
height: 200px;
width: calc(100% - 300px);
max-width: 1200px;
content: ' ';
}
.bg h1 {
font-weight: bold;
font-size: 50px;
text-shadow: 2px 2px 2px #c5d1d8;
text-align: center;
display: block;
width: 300px;
line-height: 4;
}
.bg h1 {
font-weight: bold;
font-size: 50px;
text-shadow: 2px 2px 2px #c5d1d8;
text-align: center;
display: block;
width: 300px;
line-height: 4;
}
}

View File

@ -1,207 +1,705 @@
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' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
::slotted(local-service[local-services-visible="invisible"]) {
display: none;
}
</style>
<section>
<input type="text" id="filter" />
</section>
<section id="items">
<slot name="services"></slot>
</section>
`;
{
const filter = shadow.querySelector('#filter');
let t = null;
filter.addEventListener('change', ev => {
ev.stopPropagation();
this.filter = ev.target.value;
});
filter.addEventListener('keyup', ev => {
ev.stopPropagation();
if (t) clearTimeout(t);
t = setTimeout(() => {
this.filter = ev.target.value;
t = null;
}, 1000 / 3);
});
}
}
static get observedAttributes() {
return ['filter']
}
connectedCallback() {
this.filter = this.getAttribute('filter');
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
::slotted(local-service[local-services-visible="invisible"]) {
display: none;
}
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" placeholder="Filtruj" />
</section>
<section id="items">
<slot name="services"></slot>
</section>
`;
{
const filter = shadow.querySelector('#filter');
let t = null;
filter.addEventListener('change', ev => {
ev.stopPropagation();
this.filter = ev.target.value;
});
filter.addEventListener('keyup', ev => {
ev.stopPropagation();
const value = ev.target.value;
attributeChangedCallback(name, oldV, newV) {
if (oldV == newV) return;
switch (name) {
case 'filter': return this.filter = newV;
}
}
if (t) clearTimeout(t);
t = setTimeout(() => {
this.filter = value;
t = null;
}, 1000 / 3);
});
}
}
get filter() {
return this.getAttribute('filter');
}
connectedCallback() {
this.filter = this.getAttribute('filter');
}
set filter(value) {
this.setAttribute('filter', value);
for (const el of this.querySelectorAll('local-service')) {
if (!el.name) continue;
if (el.name.includes(value)) {
el.setAttribute('local-services-visible', 'visible');
} else {
el.setAttribute('local-services-visible', 'invisible');
}
}
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'filter':
return this.filter = newV;
}
}
get filter() {
return this.getAttribute('filter');
}
set filter(value) {
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;
if (el.name.includes(value)) {
el.setAttribute('local-services-visible', 'visible');
} else {
el.setAttribute('local-services-visible', 'invisible');
}
}
}
}
});
customElements.define('local-service', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'service-id', 'state']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
`;
}
static get observedAttributes() {
return ['name', 'service-id', 'state']
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#items {
margin-top: 16px;
}
</style>
<h2 id="name"></h2>
<slot name="description"></slot>
<section id="items">
<slot name="item"></slot>
</section>
`;
}
attributeChangedCallback(name, oldV, newV) {
switch (name) {
case 'name': return this[S].querySelector('#name').textContent = newV;
}
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
}
get name() {
return this.getAttribute('name') || ''
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'name':
return this[S].querySelector('#name').textContent = newV;
}
}
get name() {
return this.getAttribute('name') || ''
}
});
customElements.define('local-service-item', class extends HTMLElement {
static get observedAttributes() {
return ['name', 'price']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#item {
display: flex;
justify-content: space-between;
}
h3 {
font-weight: normal;
}
#price {
font-weight: bold;
}
</style>
<section id="item">
<h3 id="name"></h3>
<ow-price id="price" />
</section>
`;
}
static get observedAttributes() {
return ['name', 'price']
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
this[S].querySelector('#price').value = this.price();
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#item {
display: flex;
justify-content: space-between;
}
h3 {
font-weight: normal;
}
#price {
font-weight: bold;
}
</style>
<section id="item">
<h3 id="name"></h3>
<ow-price id="price" />
</section>
`;
}
attributeChangedCallback(name, oldV, newV) {
switch (name) {
case 'name': return this[S].querySelector('#name').textContent = newV;
case 'price': return this[S].querySelector('#price').value = newV;
}
}
connectedCallback() {
this[S].querySelector('#name').textContent = this.getAttribute('name');
this[S].querySelector('#price').value = this.price();
}
price(s) {
const n = parseInt(s || this.getAttribute('price'));
return isNaN(n) ? 0 : n;
}
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;
}
}
price(s) {
const n = parseInt(s || this.getAttribute('price'));
return isNaN(n) ? 0 : n;
}
});
customElements.define('ow-price', class extends HTMLElement {
static get observedAttributes() {
return ['value', 'currency']
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#price {
font-weight: bold;
}
</style>
<span id="price"></span>
`;
}
static get observedAttributes() {
return ['value', 'currency']
}
connectedCallback() {
this[S].querySelector('#price').textContent = this.formatted;
}
constructor() {
super();
const shadow = this[S] = this.attachShadow({ mode: 'closed' });
shadow.innerHTML = `
<style>
:host { display: block; }
* { font-family: 'Noto Sans', sans-serif; }
#price {
font-weight: bold;
}
</style>
<span id="price"></span>
`;
}
attributeChangedCallback(name, oldV, newV) {
switch (name) {
case 'price': {
this.value = newV;
this[S].querySelector('#price').textContent = this.formatted;
break;
}
}
}
connectedCallback() {
this[S].querySelector('#price').textContent = this.formatted;
}
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}`
}
attributeChangedCallback(name, oldV, newV) {
if (oldV === newV) return;
switch (name) {
case 'price': {
this.value = newV;
this[S].querySelector('#price').textContent = this.formatted;
break;
}
}
}
get value() {
const n = parseInt(this.getAttribute('value'));
return isNaN(n) ? 0 : n;
}
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 }`
}
set value(v) {
this.setAttribute('value', v)
}
get value() {
const n = parseInt(this.getAttribute('value'));
return isNaN(n) ? 0 : n;
}
get currency() {
return this.getAttribute('currency') || 'PLN'
}
set value(v) {
this.setAttribute('value', v)
}
get currency() {
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,38 +1,47 @@
<!DOCTYPE html>
<html>
<head>
<title>OSWilno</title>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/assets/css/reset.css" rel="stylesheet" />
<link href="/assets/css/app.css" rel="stylesheet" />
<script type="module" src=/assets/js/app.js></script>
</head>
<body>
<main>
<header>
<div class="bg">
<h1>OS Wilno</h1>
</div>
</header>
<article>
<h1>Lokalne usługi</h1>
<local-services>
{% for service in services %}
<local-service slot="services" service-id="{{service.id}}" name="{{service.name}}" state="{{service.state.to_str()}}">
{% for line in service.description.lines() %}
<p slot="description">{{line}}</p>
{% endfor %}
<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="/" 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.as_str()}}"
>
{% for line in service.description.lines() %}
<p slot="description">{{line}}</p>
{% endfor %}
{% for item in service.items %}
<local-service-item slot="item" name="{{item.name}}" price="{{item.price}}">
</local-service-item>
{% endfor %}
{% for item in service.items %}
<local-service-item slot="item" name="{{item.name}}" price="{{item.price}}">
</local-service-item>
{% endfor %}
</local-service>
{% endfor %}
</local-services>
</article>
</main>
</body>
</local-service>
{% endfor %}
</local-services>
</article>
</main>
</body>
</html>

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,
}
@ -19,15 +20,15 @@ pub enum Role {
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Type)]
pub enum LocalServiceState {
Pending,
Approved,
Banned,
Pinned,
Internal,
Pending,
Approved,
Banned,
Pinned,
Internal,
}
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);
}