Add contacts, improve UX
This commit is contained in:
parent
2f7e6358e8
commit
f90c46dfcb
@ -1,4 +1,5 @@
|
||||
@import url('https://fonts.cdnfonts.com/css/noto-sans');
|
||||
@import url('/assets/css/noto-sans.css');
|
||||
@import url("/assets/css/beam-weapon.css");
|
||||
|
||||
* {
|
||||
--hover-color: #f18902;
|
||||
@ -6,46 +7,60 @@
|
||||
--border-light-gray-color: #e5e5e5;
|
||||
}
|
||||
|
||||
header > h1 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
header > h1 {
|
||||
font-family: 'Beam Weapon', sans-serif;
|
||||
font-weight: bold;
|
||||
text-shadow: 2px 1px 2px #fff, -1px -1px 2px #fff;
|
||||
display: block;
|
||||
font-size: 115px;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
background: no-repeat center url("/assets/images/background.webp");
|
||||
background: no-repeat center image-set(url("/assets/images/background.webp") 1x, url("/assets/images/background.jpeg") 1x);
|
||||
border-radius: 4px;
|
||||
background-size: 1280px 200px;
|
||||
width: 1280px;
|
||||
height: 200px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
article {
|
||||
width: 1280px;
|
||||
margin: auto auto;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
ow-nav > ow-path > div {
|
||||
display: block;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
ow-nav > ow-path {
|
||||
text-align: center;
|
||||
width: 48px;
|
||||
}
|
||||
ow-nav > ow-path > div {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
article {
|
||||
width: 1280px;
|
||||
margin: auto auto;
|
||||
}
|
||||
ow-nav > ow-path > svg {
|
||||
fill: black;
|
||||
min-width: 32px;
|
||||
max-width: 48px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.bg {
|
||||
height: 200px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.bg::after {
|
||||
display: block;
|
||||
text-align: center;
|
||||
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") 1x, url("/assets/images/background.jpeg") 1x);
|
||||
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;
|
||||
}
|
||||
article {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
* {
|
||||
@ -77,7 +92,7 @@ h3 {
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 1.3em;
|
||||
font-size: 1rem;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 1.3em;
|
||||
}
|
||||
@ -104,3 +119,33 @@ blockquote p {
|
||||
local-businesses local-business p {
|
||||
font-family: 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
.error {
|
||||
width: 1280px;
|
||||
color: #ba3939;
|
||||
background: #ffe0e0;
|
||||
border: 1px solid #a33a3a;
|
||||
margin: 8px auto auto;
|
||||
padding: 8px;
|
||||
}
|
||||
#home {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
#home span {
|
||||
position: absolute;
|
||||
display: block;
|
||||
left: 0;
|
||||
bottom: -3px;
|
||||
font-size: 8px;
|
||||
width: 60px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
ow-nav > ow-path[selected="selected"] {
|
||||
border: none;
|
||||
}
|
||||
|
||||
ow-nav > ow-path[selected="selected"] > svg {
|
||||
fill: var(--hover-color);
|
||||
}
|
||||
|
98
assets/css/beam-weapon.css
Normal file
98
assets/css/beam-weapon.css
Normal file
@ -0,0 +1,98 @@
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon'), url('https://fonts.cdnfonts.com/s/72008/beamweapon.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon 3D';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon 3D'), url('https://fonts.cdnfonts.com/s/72008/beamweapon3d.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Gradient';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Gradient'), url('https://fonts.cdnfonts.com/s/72008/beamweapongrad.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Halftone';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Halftone'), url('https://fonts.cdnfonts.com/s/72008/beamweaponhalf.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Laser';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Laser'), url('https://fonts.cdnfonts.com/s/72008/beamweaponlaser.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon 3D Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon 3D Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweapon3dital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Condensed';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Condensed'), url('https://fonts.cdnfonts.com/s/72008/beamweaponcond.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Condensed Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Condensed Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponcondital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Expanded';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Expanded'), url('https://fonts.cdnfonts.com/s/72008/beamweaponexpand.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Expanded Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Expanded Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponexpandital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Gradient Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Gradient Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweapongradital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Halftone Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Halftone Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponhalfital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Laser Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Laser Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponlaserital.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Leftalic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Leftalic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponleft.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Beam Weapon Super-Italic';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Beam Weapon Super-Italic'), url('https://fonts.cdnfonts.com/s/72008/beamweaponsuperital.woff') format('woff');
|
||||
}
|
||||
|
||||
|
26
assets/css/noto-sans.css
Normal file
26
assets/css/noto-sans.css
Normal file
@ -0,0 +1,26 @@
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 400;
|
||||
src: local('Noto Sans'), url('https://fonts.cdnfonts.com/s/15794/NotoSans-Regular.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
src: local('Noto Sans'), url('https://fonts.cdnfonts.com/s/15794/NotoSans-Italic.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
src: local('Noto Sans'), url('https://fonts.cdnfonts.com/s/15794/NotoSans-Bold.woff') format('woff');
|
||||
}
|
||||
@font-face {
|
||||
font-family: 'Noto Sans';
|
||||
font-style: italic;
|
||||
font-weight: 700;
|
||||
src: local('Noto Sans'), url('https://fonts.cdnfonts.com/s/15794/NotoSans-BoldItalic.woff') format('woff');
|
||||
}
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 218 KiB After Width: | Height: | Size: 110 KiB |
Binary file not shown.
Before Width: | Height: | Size: 170 KiB After Width: | Height: | Size: 91 KiB |
@ -8,25 +8,11 @@
|
||||
<link href="/assets/css/app.css" rel="stylesheet"/>
|
||||
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico"/>
|
||||
<script type="module" src=/assets/js/app.js></script>
|
||||
<style>
|
||||
.error {
|
||||
width: 1280px;
|
||||
color: #ba3939;
|
||||
background: #ffe0e0;
|
||||
border: 1px solid #a33a3a;
|
||||
margin: 8px auto auto;
|
||||
padding: 8px;
|
||||
}
|
||||
</style>
|
||||
|
||||
{% block head %}{% endblock %}
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<header>
|
||||
<div class="bg">
|
||||
<h1>OS Wilno</h1>
|
||||
</div>
|
||||
</header>
|
||||
{% match error %}
|
||||
{% when Some with (e) %}
|
||||
<p class="error">{{e}}</p>
|
||||
@ -34,23 +20,90 @@
|
||||
{% endmatch %}
|
||||
<article>
|
||||
<ow-nav>
|
||||
<ow-path path="/" id="home">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511 511" xml:space="preserve">
|
||||
<path d="M127.5 143h121.675a31.386 31.386 0 0 0-1.175 8.5 7.5 7.5 0 0 0 7.5 7.5h128a7.5 7.5 0 0 0 7.5-7.5c0-17.369-14.131-31.5-31.5-31.5-1.387 0-2.789.108-4.222.327C346.299 110.022 333.268 104 319.5 104s-26.799 6.022-35.778 16.327A27.875 27.875 0 0 0 279.5 120a31.314 31.314 0 0 0-17.439 5.284c-2.147-11.751-7.917-22.248-16.119-30.284H327.5a7.5 7.5 0 0 0 7.5-7.5c0-26.191-21.309-47.5-47.5-47.5-11.859 0-22.976 4.337-31.73 12.298A31.495 31.495 0 0 0 251.5 52c-16.921 0-31.106 11.904-34.643 27.775a56.764 56.764 0 0 0-10.588-1.006c-14.429 0-27.94 5.38-38.486 15.237a37.778 37.778 0 0 0-5.822-.468C138.824 93.539 120 112.362 120 135.5a7.5 7.5 0 0 0 7.5 7.5zm124-76c1.475 0 3.04.218 4.926.687a7.505 7.505 0 0 0 7.31-2.184C270.008 58.73 278.447 55 287.5 55c15.34 0 28.232 10.683 31.626 25H232.42c3-7.605 10.422-13 19.08-13zm28 68c1.482 0 3.046.276 4.923.868a7.502 7.502 0 0 0 8.413-2.87C298.933 124.233 308.901 119 319.5 119s20.567 5.233 26.665 13.998a7.5 7.5 0 0 0 8.413 2.87c1.877-.592 3.441-.868 4.923-.868 6.399 0 11.959 3.662 14.695 9h-109.39c2.735-5.338 8.295-9 14.694-9zm-117.539-26.461c1.96 0 4.019.285 6.479.896a7.498 7.498 0 0 0 7.311-2.183c8.051-8.694 18.889-13.482 30.518-13.482 20.45 0 37.512 14.788 41.056 34.231H136.061c3.257-11.23 13.635-19.462 25.9-19.462z"/>
|
||||
<path d="M510.905 262.363c.06-2.283.095-4.571.095-6.863 0-68.247-26.577-132.408-74.834-180.666C387.908 26.577 323.747 0 255.5 0S123.092 26.577 74.834 74.834C26.577 123.092 0 187.253 0 255.5s26.577 132.408 74.834 180.666C123.092 484.423 187.253 511 255.5 511s132.408-26.577 180.666-74.834c45.966-45.966 72.242-106.365 74.635-170.975a7.538 7.538 0 0 0 .198-1.691 7.486 7.486 0 0 0-.094-1.137zM85.441 85.441C130.865 40.016 191.26 15 255.5 15s124.635 25.016 170.059 70.441C470.984 130.865 496 191.26 496 255.5c0 .167-.006.333-.006.5H463v-8.5a7.5 7.5 0 0 0-15 0v8.5h-17v-25h.5a7.499 7.499 0 0 0 5.303-12.803l-32-32A7.497 7.497 0 0 0 399.5 184h-80c-1.989 0-3.897.79-5.303 2.197l-26.92 26.92-2.015-2.418A7.5 7.5 0 0 0 279.5 208H263v-32.5a7.5 7.5 0 0 0-15 0v.5h-17v-.5a7.5 7.5 0 0 0-15 0v.5h-65v-.5a7.5 7.5 0 0 0-15 0v.5h-17v-.5a7.5 7.5 0 0 0-15 0V224H17.046c6.8-52.302 30.482-100.646 68.395-138.559zM494.769 280H431v-9h64.494c-.19 3.01-.425 6.012-.725 9zm-15.521 64H431v-49h61.776a238.363 238.363 0 0 1-13.528 49zm-15.521 32.011c-.076-.002-.15-.011-.227-.011h-416c-.076 0-.151.009-.227.011A238.142 238.142 0 0 1 38.279 359h434.44a237.097 237.097 0 0 1-8.992 17.011zm-15.951 24.003c-.092-.003-.183-.014-.276-.014h-384c-.093 0-.184.01-.276.014A239.546 239.546 0 0 1 56.743 391h397.515a240.711 240.711 0 0 1-6.482 9.014zM15 255.5c0-5.532.199-11.032.567-16.5H104v17H15.5c-.167 0-.33.014-.494.025 0-.175-.006-.35-.006-.525zM143.5 199a7.5 7.5 0 0 0 7.5-7.5v-.5h65v.5a7.5 7.5 0 0 0 15 0v-.5h17v17H119v-17h17v.5a7.5 7.5 0 0 0 7.5 7.5zM416 231v113h-97v-73.027c.168.011.335.027.504.027a7.5 7.5 0 0 0 5.757-12.301L302.179 231H416zm-110.394-15 17-17h73.787l17 17H305.606zM207 344v-73h97v73h-97zm-88 0V223h96.487l-29.749 35.699a7.5 7.5 0 0 0 .96 10.563 7.464 7.464 0 0 0 5.301 1.715V344H119zm184.487-88h-95.975l27.5-33h40.975l27.5 33zM104 271v73H31.752c-9.135-23.111-14.646-47.678-16.247-73H104zm151.5 225c-64.24 0-124.635-25.016-170.059-70.441A244.667 244.667 0 0 1 75.51 415h359.98a245.917 245.917 0 0 1-9.931 10.559C380.135 470.984 319.74 496 255.5 496z"/>
|
||||
<path d="M143.5 240a7.5 7.5 0 0 0-7.5 7.5v16a7.5 7.5 0 0 0 15 0v-16a7.5 7.5 0 0 0-7.5-7.5zM167.5 240a7.5 7.5 0 0 0-7.5 7.5v16a7.5 7.5 0 0 0 15 0v-16a7.5 7.5 0 0 0-7.5-7.5zM143.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM167.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM55.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM39 319.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 15 0zM79.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM231.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM255.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM279.5 288a7.5 7.5 0 0 0-7.5 7.5v24a7.5 7.5 0 0 0 15 0v-24a7.5 7.5 0 0 0-7.5-7.5zM343.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM367.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM391.5 327a7.5 7.5 0 0 0 7.5-7.5v-24a7.5 7.5 0 0 0-15 0v24a7.5 7.5 0 0 0 7.5 7.5zM447.5 304a7.5 7.5 0 0 0-7.5 7.5v8a7.5 7.5 0 0 0 15 0v-8a7.5 7.5 0 0 0-7.5-7.5zM471.5 304a7.5 7.5 0 0 0-7.5 7.5v8a7.5 7.5 0 0 0 15 0v-8a7.5 7.5 0 0 0-7.5-7.5zM343.5 271a7.5 7.5 0 0 0 7.5-7.5v-8a7.5 7.5 0 0 0-15 0v8a7.5 7.5 0 0 0 7.5 7.5zM367.5 271a7.5 7.5 0 0 0 7.5-7.5v-8a7.5 7.5 0 0 0-15 0v8a7.5 7.5 0 0 0 7.5 7.5zM391.5 271a7.5 7.5 0 0 0 7.5-7.5v-8a7.5 7.5 0 0 0-15 0v8a7.5 7.5 0 0 0 7.5 7.5z"/>
|
||||
</svg>
|
||||
<span>OS Wilno</span>
|
||||
</ow-path>
|
||||
|
||||
{% if page.is_public() %}
|
||||
<ow-path path="/" selected="{{ page.select_index() }}">Lokalne Usługi</ow-path>
|
||||
<ow-path path="/marketplace" selected="{{ page.select_marketplace() }}">Targ</ow-path>
|
||||
<ow-path path="/news" selected="{{ page.select_news() }}">Aktualności</ow-path>
|
||||
<ow-path path="/account" selected="{{ page.select_account() }}">Konto</ow-path>
|
||||
{% match account.as_ref() %}
|
||||
{% when Some with (a) %}
|
||||
<ow-path path="/account/business-items" selected="{{ page.select_business_items() }}">Moje usługi</ow-path>
|
||||
{% when None %}
|
||||
{% endmatch %}
|
||||
{% if h.is_admin(account) %}
|
||||
<ow-path path="/admin" selected="{{ page.select_admin_news() }}">Admin</ow-path>
|
||||
{% endif %}
|
||||
<ow-path path="/" selected="{{ page.select_index() }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" xml:space="preserve">
|
||||
<path d="M440.482 50.916c-8.099-4.226-18.091-1.088-22.321 7.013l-41.925 80.336-46.147 36.662h-54.683l-57.033-18.756-33.145-53.595 5.854-5.694c3.554-3.457 3.634-9.143.176-12.697l-13.735-14.12 10.294-10.014c4.024-3.914 4.113-10.35.198-14.374l-41.438-42.6c-3.914-4.024-10.35-4.113-14.374-.198l-66.5 64.687c-4.024 3.914-4.113 10.35-.198 14.374l41.438 42.599c3.914 4.024 10.35 4.113 14.374.198l10.294-10.013 13.737 14.121c3.449 3.547 9.136 3.64 12.696.176l2.896-2.817 32.466 52.499a16.544 16.544 0 0 0 8.901 7.014l58.436 19.218.008 287.213c0 10.964 8.888 19.853 19.852 19.853s19.852-8.888 19.852-19.853V333.466l8.565-2.239 24.462 64.136-17.398 78.459c-2.373 10.704 4.38 21.306 15.084 23.679 10.69 2.373 21.303-4.371 23.679-15.083l18.771-84.647a19.858 19.858 0 0 0-1.096-12.026l-24.153-57.154V202.66l51.197-40.674a16.547 16.547 0 0 0 4.375-5.299l43.551-83.45c4.23-8.1 1.089-18.094-7.01-22.321zM171.921 90.467a16.48 16.48 0 0 0-12.357 2.058c-6.031 3.73-8.81 10.704-7.514 17.272l-.092.088-7.475-7.683 20.167-19.617 7.475 7.684-.204.198z"/>
|
||||
<circle cx="304.734" cy="129.707" r="34.286"/>
|
||||
</svg>
|
||||
<div>Lokalne Usługi</div>
|
||||
</ow-path>
|
||||
<ow-path path="/marketplace" selected="{{ page.select_marketplace() }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 484.909 484.909" xml:space="preserve">
|
||||
<path d="M204.993 438.478c-6.347 6.349-6.347 16.639 0 22.978a16.196 16.196 0 0 0 11.488 4.761c4.158 0 8.316-1.587 11.489-4.761l49.747-49.754-22.979-22.978-49.745 49.754zM317.642 325.807l-16.947 16.954 22.976 22.977 39.926-39.931zM260.77 325.807h-45.954l135.627 135.648a16.193 16.193 0 0 0 11.487 4.761c4.158 0 8.315-1.587 11.488-4.761 6.349-6.339 6.349-16.629 0-22.978L260.77 325.807zM102.294 107.658c21.471 0 38.878-19.915 38.878-44.478 0-24.564-17.407-44.487-38.878-44.487-21.486 0-38.877 19.923-38.877 44.487 0 24.563 17.391 44.478 38.877 44.478zM87.124 123.517v32.269a6.163 6.163 0 0 0 6.157 6.157h18.026a6.162 6.162 0 0 0 6.156-6.157v-32.269a2435.09 2435.09 0 0 0-30.339 0z"/>
|
||||
<path d="M77.762 296.157h-7.173c-6.87 0-12.44-5.57-12.44-12.442v-41.058H41.28l2.714-13.06h30.53V123.66c-7.062.128-11.934.302-12.44.539-5.554 1.365-10.505 4.951-12.885 10.656L1.421 250.377c-3.937 9.521.586 20.439 10.107 24.382a18.79 18.79 0 0 0 7.14 1.42c7.315 0 14.266-4.34 17.249-11.537l1.635-3.966-11.251 54.167a13.984 13.984 0 0 0 2.825 11.639 13.947 13.947 0 0 0 10.822 5.141h13.076v112.196c0 12.369 10.028 22.398 22.389 22.398 12.361 0 22.39-10.029 22.39-22.398V331.622h8.982v112.196c0 12.369 10.029 22.398 22.39 22.398s22.39-10.029 22.39-22.398V331.622h13.076c4.205 0 8.171-1.888 10.821-5.141.159-.198.19-.468.334-.674h-63.789c-17.425 0-31.752-12.918-34.245-29.65zM130.064 229.597h30.515l2.714 13.06h-16.819v13.662h57.838c-.127-1.992-.349-3.999-1.158-5.942l-47.778-115.521c-2.365-5.705-7.316-9.291-12.869-10.663-.508-.231-5.379-.413-12.441-.533v105.937z"/>
|
||||
<path d="M466.406 272.568h-14.13a28.036 28.036 0 0 0 6.552-18.05c0-15.549-12.604-28.154-28.153-28.154s-28.154 12.605-28.154 28.154c0 6.87 2.464 13.163 6.553 18.05h-22.131a28.031 28.031 0 0 0 6.553-18.05c0-15.549-12.604-28.154-28.154-28.154-15.549 0-28.153 12.605-28.153 28.154 0 6.87 2.464 13.163 6.553 18.05h-22.491a28.036 28.036 0 0 0 6.552-18.05c0-2.477-.322-4.877-.923-7.165a27.356 27.356 0 0 0-8.209-13.587 28.05 28.05 0 0 0-19.022-7.402c-15.549 0-28.154 12.605-28.154 28.154 0 6.87 2.464 13.163 6.553 18.05H112.007c-10.22 0-18.504 8.284-18.504 18.495s8.284 18.495 18.504 18.495h354.399c10.22 0 18.503-8.284 18.503-18.495s-8.283-18.495-18.503-18.495z"/>
|
||||
<path d="M370.467 205.351c0 15.115 12.25 27.373 27.374 27.373 15.121 0 27.371-12.258 27.371-27.373 0-15.115-12.25-27.373-27.371-27.373-15.124 0-27.374 12.258-27.374 27.373zM365.342 183.977c15.122 0 27.372-12.258 27.372-27.373 0-15.115-12.25-27.373-27.372-27.373-15.123 0-27.373 12.258-27.373 27.373 0 15.114 12.25 27.373 27.373 27.373zM332.844 232.724c15.122 0 27.372-12.258 27.372-27.373 0-15.115-12.25-27.373-27.372-27.373-15.123 0-27.373 12.258-27.373 27.373 0 15.114 12.25 27.373 27.373 27.373z"/>
|
||||
</svg>
|
||||
<div>Targ</div>
|
||||
</ow-path>
|
||||
<ow-path path="/news" selected="{{ page.select_news() }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508 508" xml:space="preserve">
|
||||
<path d="M437.4 197.6H307.6c-7.8 0-14.2 6.3-14.2 14.1v141.1c0 7.8 6.3 14.1 14.1 14.1h129.9c7.8 0 14.1-6.3 14.1-14.1V211.7c0-7.8-6.3-14.1-14.1-14.1zm-14 141.1H321.7V225.8h101.7v112.9zM360.7 409.3H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.7 6.3 14.1 14.1 14.1h290.1c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 338.7H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 268.1H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 197.6H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1z"/>
|
||||
<path d="M493.9 0H14.1C6.3 0 0 6.3 0 14.1v479.8c0 7.8 6.3 14.1 14.1 14.1h403c3.7 0 7.3-1.5 10-4.1l76.8-76.8c2.6-2.6 4.1-6.2 4.1-10v-403C508 6.3 501.7 0 493.9 0zm-62.7 459.8v-28.6h28.6l-28.6 28.6zm48.6-56.8h-62.7c-7.8 0-14.1 6.3-14.1 14.1v62.7H28.2V28.2h451.6V403z"/>
|
||||
<path d="M383.4 108.6c-5.8 0-10.6-4.7-10.6-10.6 0-5.8 4.7-10.6 10.6-10.6 2.9 0 5.7 1.2 7.7 3.3 2.7 2.8 7.1 3 10 .3 2.8-2.7 3-7.1.3-10-4.6-4.9-11.2-7.8-18-7.8-13.6 0-24.7 11.1-24.7 24.7s11.1 24.7 24.7 24.7c5.8 0 10.6 4.7 10.6 10.6 0 5.8-4.7 10.6-10.6 10.6-2.9 0-5.7-1.2-7.7-3.3-2.7-2.8-7.1-3-10-.3-2.8 2.7-3 7.1-.3 10 4.6 4.9 11.2 7.8 18 7.8 13.6 0 24.7-11.1 24.7-24.7s-11.1-24.7-24.7-24.7zM221.5 123c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-20.8V87.9h20.8c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-27.8c-3.9 0-7.1 3.2-7.1 7.1v70.5c.1 3.9 3.2 7.1 7.1 7.1h27.8c3.9 0 7.1-3.2 7.1-7.1s-3.2-7.1-7.1-7.1h-20.8V123h20.8zM149.4 73.7c-3.9 0-7.1 3.2-7.1 7.1v45L113 77.2c-1.6-2.7-4.9-4-7.9-3.2-3.1.8-5.2 3.6-5.2 6.8v70.5c0 3.9 3.2 7.1 7.1 7.1s7.1-3.2 7.1-7.1v-45l29.2 48.7c1.3 2.2 3.6 3.4 6.1 3.4.6 0 1.3-.1 1.9-.3 3.1-.8 5.2-3.6 5.2-6.8V80.8c0-3.9-3.2-7.1-7.1-7.1zM335.1 74.1c-3.8-1-7.6 1.2-8.6 5l-12.1 45S302.1 78.7 302 78.5c-.8-2.1-2.4-3.8-4.7-4.4-3.8-1-7.6 1.2-8.6 5l-12.1 45-12-45c-1-3.8-4.9-6-8.6-5-3.8 1-6 4.9-5 8.6l18.9 70.5c.8 3.1 3.6 5.2 6.8 5.2s6-2.1 6.8-5.2l12-45 12.1 45c.8 3.1 3.6 5.2 6.8 5.2 3.2 0 6-2.1 6.8-5.2l18.9-70.5c1-3.8-1.2-7.6-5-8.6z"/>
|
||||
</svg>
|
||||
<div>Aktualności</div>
|
||||
</ow-path>
|
||||
<ow-path path="/account" selected="{{ page.select_account() }}">
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" fill="currentColor">
|
||||
<path d="M16 7.992C16 3.58 12.416 0 8 0S0 3.58 0 7.992c0 2.43 1.104 4.62 2.832 6.09.016.016.032.016.032.032.144.112.288.224.448.336.08.048.144.111.224.175A7.98 7.98 0 0 0 8.016 16a7.98 7.98 0 0 0 4.48-1.375c.08-.048.144-.111.224-.16.144-.111.304-.223.448-.335.016-.016.032-.016.032-.032 1.696-1.487 2.8-3.676 2.8-6.106zm-8 7.001c-1.504 0-2.88-.48-4.016-1.279.016-.128.048-.255.08-.383a4.17 4.17 0 0 1 .416-.991c.176-.304.384-.576.64-.816.24-.24.528-.463.816-.639.304-.176.624-.304.976-.4A4.15 4.15 0 0 1 8 10.342a4.185 4.185 0 0 1 2.928 1.166c.368.368.656.8.864 1.295.112.288.192.592.24.911A7.03 7.03 0 0 1 8 14.993zm-2.448-7.4a2.49 2.49 0 0 1-.208-1.024c0-.351.064-.703.208-1.023.144-.32.336-.607.576-.847.24-.24.528-.431.848-.575.32-.144.672-.208 1.024-.208.368 0 .704.064 1.024.208.32.144.608.336.848.575.24.24.432.528.576.847.144.32.208.672.208 1.023 0 .368-.064.704-.208 1.023a2.84 2.84 0 0 1-.576.848 2.84 2.84 0 0 1-.848.575 2.715 2.715 0 0 1-2.064 0 2.84 2.84 0 0 1-.848-.575 2.526 2.526 0 0 1-.56-.848zm7.424 5.306c0-.032-.016-.048-.016-.08a5.22 5.22 0 0 0-.688-1.406 4.883 4.883 0 0 0-1.088-1.135 5.207 5.207 0 0 0-1.04-.608 2.82 2.82 0 0 0 .464-.383 4.2 4.2 0 0 0 .624-.784 3.624 3.624 0 0 0 .528-1.934 3.71 3.71 0 0 0-.288-1.47 3.799 3.799 0 0 0-.816-1.199 3.845 3.845 0 0 0-1.2-.8 3.72 3.72 0 0 0-1.472-.287 3.72 3.72 0 0 0-1.472.288 3.631 3.631 0 0 0-1.2.815 3.84 3.84 0 0 0-.8 1.199 3.71 3.71 0 0 0-.288 1.47c0 .352.048.688.144 1.007.096.336.224.64.4.927.16.288.384.544.624.784.144.144.304.271.48.383a5.12 5.12 0 0 0-1.04.624c-.416.32-.784.703-1.088 1.119a4.999 4.999 0 0 0-.688 1.406c-.016.032-.016.064-.016.08C1.776 11.636.992 9.91.992 7.992.992 4.14 4.144.991 8 .991s7.008 3.149 7.008 7.001a6.96 6.96 0 0 1-2.032 4.907z"/>
|
||||
</svg>
|
||||
<div>Konto</div>
|
||||
</ow-path>
|
||||
|
||||
{% if h.is_above_user(account) %}
|
||||
<ow-path path="/account/business-items" selected="{{ page.select_business_items() }}">
|
||||
<svg viewBox="0 0 32 32" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M25 26a1 1 0 0 1-1 1H8a1 1 0 0 1-1-1V5h10V3H5v23a3 3 0 0 0 3 3h16a3 3 0 0 0 3-3V13h-2Z"/>
|
||||
<path d="M27.12 2.88a3.08 3.08 0 0 0-4.24 0L17 8.75l-1 5.3L21.25 13l5.87-5.87a3 3 0 0 0 0-4.25Zm-6.86 8.27-1.76.35.35-1.76 3.32-3.33 1.42 1.42Zm5.45-5.44-.71.7L23.59 5l.7-.71a1 1 0 0 1 1.42 0 1 1 0 0 1 0 1.42Z"/>
|
||||
</svg>
|
||||
<div>Moje usługi</div>
|
||||
</ow-path>
|
||||
{% endif %}
|
||||
|
||||
{% if h.is_admin(account) %}
|
||||
<ow-path path="/admin" selected="{{ page.select_admin_news() }}">
|
||||
<svg viewBox="0 0 36 36" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M14.68 14.81a6.76 6.76 0 1 1 6.76-6.75 6.77 6.77 0 0 1-6.76 6.75Zm0-11.51a4.76 4.76 0 1 0 4.76 4.76 4.76 4.76 0 0 0-4.76-4.76Z"
|
||||
class="clr-i-outline clr-i-outline-path-1"/>
|
||||
<path d="M16.42 31.68A2.14 2.14 0 0 1 15.8 30H4v-5.78a14.81 14.81 0 0 1 11.09-4.68h.72a2.2 2.2 0 0 1 .62-1.85l.12-.11c-.47 0-1-.06-1.46-.06A16.47 16.47 0 0 0 2.2 23.26a1 1 0 0 0-.2.6V30a2 2 0 0 0 2 2h12.7Z"
|
||||
class="clr-i-outline clr-i-outline-path-2"/>
|
||||
<path d="M26.87 16.29a.37.37 0 0 1 .15 0 .42.42 0 0 0-.15 0Z"
|
||||
class="clr-i-outline clr-i-outline-path-3"/>
|
||||
<path d="m33.68 23.32-2-.61a7.21 7.21 0 0 0-.58-1.41l1-1.86A.38.38 0 0 0 32 19l-1.45-1.45a.36.36 0 0 0-.44-.07l-1.84 1a7.15 7.15 0 0 0-1.43-.61l-.61-2a.36.36 0 0 0-.36-.24h-2.05a.36.36 0 0 0-.35.26l-.61 2a7 7 0 0 0-1.44.6l-1.82-1a.35.35 0 0 0-.43.07L17.69 19a.38.38 0 0 0-.06.44l1 1.82a6.77 6.77 0 0 0-.63 1.43l-2 .6a.36.36 0 0 0-.26.35v2.05A.35.35 0 0 0 16 26l2 .61a7 7 0 0 0 .6 1.41l-1 1.91a.36.36 0 0 0 .06.43l1.45 1.45a.38.38 0 0 0 .44.07l1.87-1a7.09 7.09 0 0 0 1.4.57l.6 2a.38.38 0 0 0 .35.26h2.05a.37.37 0 0 0 .35-.26l.61-2.05a6.92 6.92 0 0 0 1.38-.57l1.89 1a.36.36 0 0 0 .43-.07L32 30.4a.35.35 0 0 0 0-.4l-1-1.88a7 7 0 0 0 .58-1.39l2-.61a.36.36 0 0 0 .26-.35v-2.1a.36.36 0 0 0-.16-.35ZM24.85 28a3.34 3.34 0 1 1 3.33-3.33A3.34 3.34 0 0 1 24.85 28Z"
|
||||
class="clr-i-outline clr-i-outline-path-4"/>
|
||||
<path fill="none" d="M0 0h36v36H0z"/>
|
||||
</svg>
|
||||
<div>Admin</div>
|
||||
</ow-path>
|
||||
{% endif %}
|
||||
|
||||
{% else if page.is_admin() %}
|
||||
<ow-path path="/">Home</ow-path>
|
||||
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}">News</ow-path>
|
||||
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}">Localne Usługi</ow-path>
|
||||
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508 508" xml:space="preserve">
|
||||
<path d="M437.4 197.6H307.6c-7.8 0-14.2 6.3-14.2 14.1v141.1c0 7.8 6.3 14.1 14.1 14.1h129.9c7.8 0 14.1-6.3 14.1-14.1V211.7c0-7.8-6.3-14.1-14.1-14.1zm-14 141.1H321.7V225.8h101.7v112.9zM360.7 409.3H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.7 6.3 14.1 14.1 14.1h290.1c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 338.7H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 268.1H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1zM251.2 197.6H70.6c-7.8 0-14.1 6.3-14.1 14.1-.1 7.8 6.3 14.1 14.1 14.1h180.6c7.8 0 14.1-6.3 14.1-14.1s-6.3-14.1-14.1-14.1z"/>
|
||||
<path d="M493.9 0H14.1C6.3 0 0 6.3 0 14.1v479.8c0 7.8 6.3 14.1 14.1 14.1h403c3.7 0 7.3-1.5 10-4.1l76.8-76.8c2.6-2.6 4.1-6.2 4.1-10v-403C508 6.3 501.7 0 493.9 0zm-62.7 459.8v-28.6h28.6l-28.6 28.6zm48.6-56.8h-62.7c-7.8 0-14.1 6.3-14.1 14.1v62.7H28.2V28.2h451.6V403z"/>
|
||||
<path d="M383.4 108.6c-5.8 0-10.6-4.7-10.6-10.6 0-5.8 4.7-10.6 10.6-10.6 2.9 0 5.7 1.2 7.7 3.3 2.7 2.8 7.1 3 10 .3 2.8-2.7 3-7.1.3-10-4.6-4.9-11.2-7.8-18-7.8-13.6 0-24.7 11.1-24.7 24.7s11.1 24.7 24.7 24.7c5.8 0 10.6 4.7 10.6 10.6 0 5.8-4.7 10.6-10.6 10.6-2.9 0-5.7-1.2-7.7-3.3-2.7-2.8-7.1-3-10-.3-2.8 2.7-3 7.1-.3 10 4.6 4.9 11.2 7.8 18 7.8 13.6 0 24.7-11.1 24.7-24.7s-11.1-24.7-24.7-24.7zM221.5 123c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-20.8V87.9h20.8c3.9 0 7.1-3.2 7.1-7.1 0-3.9-3.2-7.1-7.1-7.1h-27.8c-3.9 0-7.1 3.2-7.1 7.1v70.5c.1 3.9 3.2 7.1 7.1 7.1h27.8c3.9 0 7.1-3.2 7.1-7.1s-3.2-7.1-7.1-7.1h-20.8V123h20.8zM149.4 73.7c-3.9 0-7.1 3.2-7.1 7.1v45L113 77.2c-1.6-2.7-4.9-4-7.9-3.2-3.1.8-5.2 3.6-5.2 6.8v70.5c0 3.9 3.2 7.1 7.1 7.1s7.1-3.2 7.1-7.1v-45l29.2 48.7c1.3 2.2 3.6 3.4 6.1 3.4.6 0 1.3-.1 1.9-.3 3.1-.8 5.2-3.6 5.2-6.8V80.8c0-3.9-3.2-7.1-7.1-7.1zM335.1 74.1c-3.8-1-7.6 1.2-8.6 5l-12.1 45S302.1 78.7 302 78.5c-.8-2.1-2.4-3.8-4.7-4.4-3.8-1-7.6 1.2-8.6 5l-12.1 45-12-45c-1-3.8-4.9-6-8.6-5-3.8 1-6 4.9-5 8.6l18.9 70.5c.8 3.1 3.6 5.2 6.8 5.2s6-2.1 6.8-5.2l12-45 12.1 45c.8 3.1 3.6 5.2 6.8 5.2 3.2 0 6-2.1 6.8-5.2l18.9-70.5c1-3.8-1.2-7.6-5-8.6z"/>
|
||||
</svg>
|
||||
<div>Aktualności</div>
|
||||
</ow-path>
|
||||
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" xml:space="preserve">
|
||||
<path d="M440.482 50.916c-8.099-4.226-18.091-1.088-22.321 7.013l-41.925 80.336-46.147 36.662h-54.683l-57.033-18.756-33.145-53.595 5.854-5.694c3.554-3.457 3.634-9.143.176-12.697l-13.735-14.12 10.294-10.014c4.024-3.914 4.113-10.35.198-14.374l-41.438-42.6c-3.914-4.024-10.35-4.113-14.374-.198l-66.5 64.687c-4.024 3.914-4.113 10.35-.198 14.374l41.438 42.599c3.914 4.024 10.35 4.113 14.374.198l10.294-10.013 13.737 14.121c3.449 3.547 9.136 3.64 12.696.176l2.896-2.817 32.466 52.499a16.544 16.544 0 0 0 8.901 7.014l58.436 19.218.008 287.213c0 10.964 8.888 19.853 19.852 19.853s19.852-8.888 19.852-19.853V333.466l8.565-2.239 24.462 64.136-17.398 78.459c-2.373 10.704 4.38 21.306 15.084 23.679 10.69 2.373 21.303-4.371 23.679-15.083l18.771-84.647a19.858 19.858 0 0 0-1.096-12.026l-24.153-57.154V202.66l51.197-40.674a16.547 16.547 0 0 0 4.375-5.299l43.551-83.45c4.23-8.1 1.089-18.094-7.01-22.321zM171.921 90.467a16.48 16.48 0 0 0-12.357 2.058c-6.031 3.73-8.81 10.704-7.514 17.272l-.092.088-7.475-7.683 20.167-19.617 7.475 7.684-.204.198z"/>
|
||||
<circle cx="304.734" cy="129.707" r="34.286"/>
|
||||
</svg>
|
||||
<div>Lokalne Usługi</div>
|
||||
</ow-path>
|
||||
{% endif %}
|
||||
</ow-nav>
|
||||
{% block content %}{% endblock %}
|
||||
|
@ -1,20 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<business-items
|
||||
business-id="{{business.id}}"
|
||||
name="{{business.name}}"
|
||||
description="{{business.description}}"
|
||||
>
|
||||
{% for item in items %}
|
||||
<business-item
|
||||
item-id="{{item.id}}"
|
||||
name="{{item.name}}"
|
||||
price="{{item.price}}"
|
||||
picture-url="{{item.picture_url}}"
|
||||
item-order="{{item.item_order}}"
|
||||
>
|
||||
</business-item>
|
||||
{% endfor %}
|
||||
|
||||
</business-items>
|
||||
{% endblock %}
|
35
assets/templates/businesses/editor.html
Normal file
35
assets/templates/businesses/editor.html
Normal file
@ -0,0 +1,35 @@
|
||||
{% extends "../base.html" %}
|
||||
{% block content %}
|
||||
|
||||
<contact-info-editor type="email" value="{{h.email(account)}}">
|
||||
<contact-info-list>
|
||||
{% for contact in contacts %}
|
||||
<edit-contact-info contact-id="{{contact.id}}">
|
||||
<contact-info
|
||||
contact-id="{{contact.id}}"
|
||||
contact-type="{{contact.contact_type}}"
|
||||
content="{{contact.content}}"
|
||||
></contact-info>
|
||||
</edit-contact-info>
|
||||
{% endfor %}
|
||||
</contact-info-list>
|
||||
</contact-info-editor>
|
||||
|
||||
<business-item-editor
|
||||
business-id="{{business.id}}"
|
||||
name="{{business.name}}"
|
||||
description="{{business.description}}"
|
||||
>
|
||||
{% for item in items %}
|
||||
<business-item
|
||||
item-id="{{item.id}}"
|
||||
name="{{item.name}}"
|
||||
price="{{item.price}}"
|
||||
picture-url="{{item.picture_url}}"
|
||||
item-order="{{item.item_order}}"
|
||||
>
|
||||
</business-item>
|
||||
{% endfor %}
|
||||
|
||||
</business-item-editor>
|
||||
{% endblock %}
|
37
assets/templates/businesses/index.html
Normal file
37
assets/templates/businesses/index.html
Normal file
@ -0,0 +1,37 @@
|
||||
{% extends "../base.html" %}
|
||||
{% block content %}
|
||||
<local-business-list>
|
||||
{% for business in businesses %}
|
||||
<local-business
|
||||
slot="business"
|
||||
business-id="{{business.id}}"
|
||||
name="{{business.name}}"
|
||||
state="{{business.state.as_str()}}"
|
||||
>
|
||||
{% for line in business.description.lines() %}
|
||||
<p slot="description">{{line}}</p>
|
||||
{% endfor %}
|
||||
|
||||
{% for item in business.items %}
|
||||
<local-business-item
|
||||
slot="item"
|
||||
name="{{item.name}}"
|
||||
price="{{item.price}}"
|
||||
picture-url="{{item.picture_url}}"
|
||||
>
|
||||
</local-business-item>
|
||||
{% endfor %}
|
||||
<contact-info-list>
|
||||
{% for contact in business.contacts %}
|
||||
<contact-info
|
||||
contact-id="{{contact.id}}"
|
||||
contact-type="{{contact.contact_type}}"
|
||||
content="{{contact.content}}"
|
||||
></contact-info>
|
||||
{% endfor %}
|
||||
</contact-info-list>
|
||||
|
||||
</local-business>
|
||||
{% endfor %}
|
||||
</local-business-list>
|
||||
{% endblock %}
|
@ -1,28 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<local-businesses>
|
||||
{% for service in services %}
|
||||
<local-business
|
||||
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-business-item
|
||||
slot="item"
|
||||
name="{{item.name}}"
|
||||
price="{{item.price}}"
|
||||
picture-url="{{item.picture_url}}"
|
||||
>
|
||||
</local-business-item>
|
||||
{% endfor %}
|
||||
|
||||
</local-business>
|
||||
{% endfor %}
|
||||
</local-businesses>
|
||||
{% endblock %}
|
3
assets/templates/marketplace/index.html
Normal file
3
assets/templates/marketplace/index.html
Normal file
@ -0,0 +1,3 @@
|
||||
{% extends "../base.html" %}
|
||||
{% block content %}
|
||||
{% endblock %}
|
@ -13,6 +13,7 @@ customElements.define('admin-business', class extends Component {
|
||||
}
|
||||
::slotted(local-business-item) {
|
||||
width: 100%;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
</style>
|
||||
<article>
|
||||
|
@ -2,16 +2,27 @@ import "./shared/form-navigation.js";
|
||||
import "./local-businesses.js";
|
||||
import "./login-form.js";
|
||||
import "./ow-account.js";
|
||||
|
||||
import "./nav/ow-nav.js";
|
||||
import "./nav/ow-path.js";
|
||||
|
||||
import "./price/price-view";
|
||||
import "./price/price-input";
|
||||
|
||||
import "./register-form.js";
|
||||
import "./business-items";
|
||||
|
||||
import "./business-items/business-item";
|
||||
import "./business-items/business-item-editor";
|
||||
|
||||
import "./news/ow-articles";
|
||||
import "./news/news-article";
|
||||
|
||||
import "./shared/rich-text-editor";
|
||||
|
||||
import "./contacts/contact-info-list";
|
||||
import "./contacts/contact-info";
|
||||
import "./contacts/contact-info-editor";
|
||||
|
||||
import { fireFbReady } from "./shared.js";
|
||||
|
||||
if (!document.querySelector('#facebook-jssdk')) {
|
||||
|
@ -1,9 +1,8 @@
|
||||
import { Component, FORM_STYLE } from "./shared";
|
||||
import { Component, FORM_STYLE } from "../shared";
|
||||
|
||||
import "./business-items/business-item";
|
||||
import "./register-form/register-item-form-row";
|
||||
import "../register-form/register-item-form-row";
|
||||
|
||||
customElements.define('business-items', class extends Component {
|
||||
customElements.define('business-item-editor', class extends Component {
|
||||
#idx;
|
||||
|
||||
static get observedAttributes() {
|
@ -10,17 +10,6 @@ customElements.define('business-item', class extends Component {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
section > form { display: flex; justify-content: space-between; }
|
||||
#actions {
|
||||
width: 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
#drag {
|
||||
cursor: pointer;
|
||||
}
|
||||
#move {
|
||||
width: 33px;
|
||||
}
|
||||
:host(:first-child) #move-up {
|
||||
display: none;
|
||||
}
|
||||
@ -30,7 +19,20 @@ customElements.define('business-item', class extends Component {
|
||||
#move svg {
|
||||
cursor: pointer;
|
||||
}
|
||||
button {
|
||||
@media(min-width: 1280px) {
|
||||
section > form {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
#actions {
|
||||
width: 25%;
|
||||
max-width: 25%;
|
||||
}
|
||||
#move {
|
||||
width: 33px;
|
||||
}
|
||||
}
|
||||
#move-up, #move-down {
|
||||
border: none;
|
||||
background: none;
|
||||
}
|
||||
|
126
client/src/contacts/contact-info-editor.js
Normal file
126
client/src/contacts/contact-info-editor.js
Normal file
@ -0,0 +1,126 @@
|
||||
import { Component, FORM_STYLE } from "../shared";
|
||||
|
||||
customElements.define('contact-info-editor', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['type', "contact-id", "content"];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
#contact-wrapper svg {
|
||||
display: none;
|
||||
width: 24px;
|
||||
}
|
||||
#contact-wrapper #input {
|
||||
display: flex;
|
||||
justify-content: start;
|
||||
}
|
||||
#contact-wrapper #input svg {
|
||||
margin-right: 8px;
|
||||
}
|
||||
:host([type="email"]) #email-icon {
|
||||
display: block;
|
||||
}
|
||||
:host([type="facebook"]) #fb-icon {
|
||||
display: block;
|
||||
}
|
||||
:host([type="other"]) #other-icon {
|
||||
display: block;
|
||||
}
|
||||
${ FORM_STYLE }
|
||||
</style>
|
||||
<section>
|
||||
<form>
|
||||
<div>
|
||||
<label>Typ</label>
|
||||
<select name="contact_type" id="contact_type">
|
||||
<option value="email">E-Mail</option>
|
||||
<option value="facebook">Facebook</option>
|
||||
<option value="other">Other</option>
|
||||
</select>
|
||||
</div>
|
||||
<div id="contact-wrapper">
|
||||
<label>E-Mail</label>
|
||||
<div id="input">
|
||||
<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>
|
||||
|
||||
<input type="email" id="content" name="content" />
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<input type="submit" value="Dodaj" />
|
||||
</div>
|
||||
</form>
|
||||
</section>
|
||||
<section>
|
||||
<slot></slot>
|
||||
</section>
|
||||
`);
|
||||
|
||||
this.shadowRoot.querySelector('#contact_type').addEventListener('change', ev => {
|
||||
ev.stopPropagation();
|
||||
this.type = ev.target.value;
|
||||
});
|
||||
}
|
||||
|
||||
get type() {
|
||||
return this.getAttribute('type');
|
||||
}
|
||||
|
||||
set type(v) {
|
||||
this.setAttribute('type', v);
|
||||
this.shadowRoot.querySelector('#content').setAttribute('type', v === 'email' ? 'email' : 'text');
|
||||
}
|
||||
|
||||
get content() {
|
||||
return this.getAttribute('content');
|
||||
}
|
||||
|
||||
set content(v) {
|
||||
this.setAttribute('content', v);
|
||||
this.shadowRoot.querySelector('#content').value = v;
|
||||
}
|
||||
|
||||
get contact_id() {
|
||||
return this.getAttribute('contact-id');
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
|
||||
#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';
|
||||
}
|
||||
});
|
12
client/src/contacts/contact-info-list.js
Normal file
12
client/src/contacts/contact-info-list.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('contact-info-list', class extends Component {
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);
|
||||
}
|
||||
});
|
12
client/src/contacts/contact-info.js
Normal file
12
client/src/contacts/contact-info.js
Normal file
@ -0,0 +1,12 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('contact-info', class extends Component {
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
</style>
|
||||
<slot></slot>
|
||||
`);
|
||||
}
|
||||
});
|
38
client/src/contacts/edit-contact-info.js
Normal file
38
client/src/contacts/edit-contact-info.js
Normal file
@ -0,0 +1,38 @@
|
||||
import { Component } from "../shared";
|
||||
|
||||
customElements.define('edit-contact-info', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['contact-id'];
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super(`
|
||||
<style>
|
||||
:host { display: block; }
|
||||
article {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
</style>
|
||||
<article>
|
||||
<slot></slot>
|
||||
<section>
|
||||
<input type="button" value="Edytuj" id="edit" />
|
||||
<form>
|
||||
<input type="hidden" name="id" id="remove-id" />
|
||||
<input type="button" value="Usuń" id="remove" />
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
`);
|
||||
}
|
||||
|
||||
get contact_id() {
|
||||
return this.getAttribute('contact-id');
|
||||
}
|
||||
|
||||
set contact_id(v) {
|
||||
this.setAttribute('contact-id', v);
|
||||
this.shadowRoot.querySelector('#remove-id').value = v;
|
||||
}
|
||||
});
|
@ -2,7 +2,7 @@ import "./local-businesses/local-business-item";
|
||||
import "./local-businesses/local-business";
|
||||
import { Component } from "./shared";
|
||||
|
||||
customElements.define('local-businesses', class extends Component {
|
||||
customElements.define('local-business-list', class extends Component {
|
||||
static get observedAttributes() {
|
||||
return ['filter']
|
||||
}
|
||||
@ -16,7 +16,7 @@ customElements.define('local-businesses', class extends Component {
|
||||
display: none;
|
||||
}
|
||||
input {
|
||||
font-size: 20pt;
|
||||
font-size: 1rem;
|
||||
line-height: 2.6em;
|
||||
height: 2.6em;
|
||||
margin: 0;
|
||||
@ -34,7 +34,7 @@ customElements.define('local-businesses', class extends Component {
|
||||
<input type="text" id="filter" placeholder="Filtruj" />
|
||||
</section>
|
||||
<section id="items">
|
||||
<slot name="services"></slot>
|
||||
<slot name="business"></slot>
|
||||
</section>
|
||||
`);
|
||||
{
|
||||
|
@ -10,23 +10,52 @@ customElements.define('local-business-item', class extends Component {
|
||||
<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;
|
||||
}
|
||||
img {
|
||||
width: 200px;
|
||||
max-width: 200px;
|
||||
}
|
||||
:host([picture-url = '']) img {
|
||||
display: none;
|
||||
}
|
||||
img {
|
||||
width: 128px;
|
||||
max-width: 128px;
|
||||
}
|
||||
#item {
|
||||
display: grid;
|
||||
grid-template-areas: 'img name' 'img price';
|
||||
}
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
grid-area: name;
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
text-align: right;
|
||||
}
|
||||
#price {
|
||||
grid-area: price; text-align: right;
|
||||
}
|
||||
img {
|
||||
grid-area: img;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
#item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
h3 {
|
||||
font-weight: normal;
|
||||
line-height: 1.6;
|
||||
width: calc(100% - 450px);
|
||||
text-align: left;
|
||||
}
|
||||
#price {
|
||||
font-weight: bold;
|
||||
width: 180px;
|
||||
}
|
||||
img {
|
||||
width: 128px;
|
||||
max-width: 128px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
<section id="item">
|
||||
<img alt="" src="" />
|
||||
|
@ -14,16 +14,15 @@ customElements.define('ow-path', class extends Component {
|
||||
display: block;
|
||||
padding: 10px 18px;
|
||||
text-decoration: none;
|
||||
color: #495057;
|
||||
text-transform: uppercase;
|
||||
border: none;
|
||||
border-bottom: 1px solid var(--border-slim-color);
|
||||
color: var(--hover-color);
|
||||
}
|
||||
a:hover {
|
||||
color: var(--hover-color);
|
||||
}
|
||||
:host(:not([selected])) a {
|
||||
border: none;
|
||||
color: #495057;
|
||||
}
|
||||
</style>
|
||||
<a><slot></slot></a>
|
||||
|
@ -20,10 +20,6 @@ customElements.define('news-article', class extends Component {
|
||||
h1 #status {
|
||||
font-size: 14px;
|
||||
}
|
||||
#time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.time span:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
@ -41,6 +37,12 @@ customElements.define('news-article', class extends Component {
|
||||
:host([hide-status="true"]) #status {
|
||||
display: none;
|
||||
}
|
||||
@media (min-width: 1200px) {
|
||||
#time {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
${ BLOCK_QUOTE_STYLE }
|
||||
</style>
|
||||
<article>
|
||||
|
@ -1,7 +1,7 @@
|
||||
export const S = Symbol();
|
||||
|
||||
export const BUTTON_STYLE = `
|
||||
input[type="button"], input[type="submit"] {
|
||||
input[type="button"], input[type="submit"], button {
|
||||
cursor: pointer;
|
||||
border-radius: 5px;
|
||||
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);
|
||||
|
14
migrations/20220715071927_add_offers.sql
Normal file
14
migrations/20220715071927_add_offers.sql
Normal file
@ -0,0 +1,14 @@
|
||||
CREATE TYPE "OfferState" AS ENUM (
|
||||
'Pending',
|
||||
'Approved',
|
||||
'Banned'
|
||||
);
|
||||
|
||||
CREATE TABLE offers
|
||||
(
|
||||
id serial unique not null primary key,
|
||||
name text not null,
|
||||
picture_url text not null,
|
||||
state "OfferState" not null,
|
||||
created_at timestamp not null default now()
|
||||
);
|
7
migrations/20220715115332_add_contacts.sql
Normal file
7
migrations/20220715115332_add_contacts.sql
Normal file
@ -0,0 +1,7 @@
|
||||
CREATE TABLE contacts
|
||||
(
|
||||
id serial not null primary key unique,
|
||||
owner_id int not null references accounts (id),
|
||||
contact_type text not null,
|
||||
content text not null
|
||||
);
|
@ -8,9 +8,9 @@ use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, PartialOrd, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Type)]
|
||||
pub enum AccountType {
|
||||
User,
|
||||
Business,
|
||||
Admin,
|
||||
User = 1,
|
||||
Business = 10,
|
||||
Admin = 100,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
@ -108,6 +108,14 @@ pub struct LocalBusinessItem {
|
||||
pub picture_url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct ContactInfo {
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub contact_type: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, FromRow)]
|
||||
pub struct NewsArticle {
|
||||
pub id: i32,
|
||||
@ -177,3 +185,18 @@ pub struct UpdateLocalBusinessItemInput {
|
||||
pub struct DeleteNewsArticleInput {
|
||||
pub id: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateContactInput {
|
||||
pub owner_id: i32,
|
||||
pub contact_type: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateContactInput {
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub contact_type: String,
|
||||
pub content: String,
|
||||
}
|
||||
|
@ -116,10 +116,23 @@ pub struct LocalBusiness {
|
||||
pub description: String,
|
||||
pub state: db::LocalBusinessState,
|
||||
pub items: Vec<db::LocalBusinessItem>,
|
||||
pub contacts: Vec<db::ContactInfo>,
|
||||
}
|
||||
|
||||
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalBusiness {
|
||||
fn from((service, items): (db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)) -> Self {
|
||||
impl<'items, 'contacts>
|
||||
From<(
|
||||
db::LocalBusiness,
|
||||
&'items mut Vec<db::LocalBusinessItem>,
|
||||
&'contacts mut Vec<db::ContactInfo>,
|
||||
)> for LocalBusiness
|
||||
{
|
||||
fn from(
|
||||
(service, items, contacts): (
|
||||
db::LocalBusiness,
|
||||
&'items mut Vec<db::LocalBusinessItem>,
|
||||
&'contacts mut Vec<db::ContactInfo>,
|
||||
),
|
||||
) -> Self {
|
||||
Self {
|
||||
id: service.id,
|
||||
owner_id: service.owner_id,
|
||||
@ -129,6 +142,9 @@ impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for Local
|
||||
items: items
|
||||
.drain_filter(|i| i.local_business_id == service.id)
|
||||
.collect(),
|
||||
contacts: contacts
|
||||
.drain_filter(|c| c.owner_id == service.owner_id)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -29,6 +29,10 @@ pub enum Error {
|
||||
id: i32,
|
||||
item_order: i32,
|
||||
},
|
||||
BusinessItemState {
|
||||
id: i32,
|
||||
state: db::LocalBusinessState,
|
||||
},
|
||||
AllItems,
|
||||
OwnedBusiness {
|
||||
account_id: i32,
|
||||
@ -53,6 +57,19 @@ pub enum Error {
|
||||
DeleteNewsArticle {
|
||||
id: i32,
|
||||
},
|
||||
AllContacts,
|
||||
AccountContacts {
|
||||
account_id: i32,
|
||||
},
|
||||
DeleteContact {
|
||||
id: i32,
|
||||
},
|
||||
CreateContact {
|
||||
input: db::CreateContactInput,
|
||||
},
|
||||
UpdateContact {
|
||||
input: db::UpdateContactInput,
|
||||
},
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
@ -749,6 +766,137 @@ RETURNING
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::VisibleBusinessItems
|
||||
Error::BusinessItemState { id, state }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn all_contacts(t: &mut T<'_>) -> Result<Vec<db::ContactInfo>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
owner_id,
|
||||
contact_type,
|
||||
content
|
||||
FROM
|
||||
contacts
|
||||
"#,
|
||||
)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::AllContacts
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn account_contacts(t: &mut T<'_>, account_id: i32) -> Result<Vec<db::ContactInfo>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT
|
||||
id,
|
||||
owner_id,
|
||||
contact_type,
|
||||
content
|
||||
FROM
|
||||
contacts
|
||||
WHERE
|
||||
owner_id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(account_id)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::AccountContacts { account_id }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn create_contact(
|
||||
t: &mut T<'_>,
|
||||
input: db::CreateContactInput,
|
||||
) -> Result<Vec<db::ContactInfo>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO contacts ( owner_id, contact_type, content )
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING
|
||||
id,
|
||||
owner_id,
|
||||
contact_type,
|
||||
content
|
||||
"#,
|
||||
)
|
||||
.bind(input.owner_id)
|
||||
.bind(&input.contact_type)
|
||||
.bind(&input.content)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::CreateContact { input }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn update_contact(
|
||||
t: &mut T<'_>,
|
||||
input: db::UpdateContactInput,
|
||||
) -> Result<Vec<db::ContactInfo>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
UPDATE contacts
|
||||
VALUES
|
||||
owner_id = $2,
|
||||
contact_type = $3,
|
||||
content = $4
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
owner_id,
|
||||
contact_type,
|
||||
content
|
||||
"#,
|
||||
)
|
||||
.bind(input.id)
|
||||
.bind(input.owner_id)
|
||||
.bind(&input.contact_type)
|
||||
.bind(&input.content)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::UpdateContact { input }
|
||||
})
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
pub async fn delete_contact(t: &mut T<'_>, id: i32) -> Result<Vec<db::ContactInfo>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
DELETE FROM contacts
|
||||
WHERE id = $1
|
||||
RETURNING
|
||||
id,
|
||||
owner_id,
|
||||
contact_type,
|
||||
content
|
||||
"#,
|
||||
)
|
||||
.bind(id)
|
||||
.fetch_all(t)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
error!("{e}");
|
||||
dbg!(&e);
|
||||
Error::DeleteContact { id }
|
||||
})
|
||||
}
|
||||
|
@ -103,9 +103,9 @@ pub enum Error {
|
||||
|
||||
pub fn reject_xss(s: &str) -> Result<()> {
|
||||
if s.contains("<script ") || s.contains("<script>") {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(Error::XSS)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -8,17 +8,19 @@ use crate::queries;
|
||||
use crate::routes::{Identity, Result};
|
||||
pub mod admin;
|
||||
mod business_item;
|
||||
mod contacts;
|
||||
|
||||
use crate::view::Helper;
|
||||
|
||||
#[derive(Debug, Default, Template)]
|
||||
#[template(path = "business-items.html")]
|
||||
#[template(path = "businesses/editor.html")]
|
||||
struct BusinessItemsTemplate {
|
||||
page: view::Page,
|
||||
error: Option<String>,
|
||||
account: Option<db::Account>,
|
||||
items: Vec<db::LocalBusinessItem>,
|
||||
business: db::LocalBusiness,
|
||||
contacts: Vec<db::ContactInfo>,
|
||||
h: Helper,
|
||||
}
|
||||
|
||||
@ -71,12 +73,15 @@ async fn handle_business_items_page(
|
||||
|
||||
let business =
|
||||
crate::ok_or_internal!(queries::account_business_by_owner_id(t, account.id).await);
|
||||
let items: Vec<db::LocalBusinessItem> = queries::account_items(t, account.id).await;
|
||||
let items = queries::account_items(t, account.id).await;
|
||||
let contacts = crate::ok_or_internal!(queries::account_contacts(t, account.id).await);
|
||||
|
||||
let page = BusinessItemsTemplate {
|
||||
page: view::Page::BusinessItems,
|
||||
account: Some(account),
|
||||
items,
|
||||
business,
|
||||
contacts,
|
||||
..Default::default()
|
||||
};
|
||||
Ok(HttpResponse::Ok()
|
||||
@ -87,5 +92,6 @@ async fn handle_business_items_page(
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(business_items_page);
|
||||
business_item::configure(config);
|
||||
contacts::configure(config);
|
||||
admin::configure(config);
|
||||
}
|
||||
|
@ -257,7 +257,7 @@ async fn admin_businesses(db: Data<PgPool>, id: Identity) -> Result<HttpResponse
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let _account = require_admin!(&mut t, id);
|
||||
|
||||
let (services, mut items) = {
|
||||
let (services, mut items, mut contacts) = {
|
||||
use crate::model::db::{LocalBusiness, LocalBusinessItem};
|
||||
let services: Vec<LocalBusiness> =
|
||||
queries::all_businesses(&mut t).await.unwrap_or_default();
|
||||
@ -265,12 +265,13 @@ async fn admin_businesses(db: Data<PgPool>, id: Identity) -> Result<HttpResponse
|
||||
let items: Vec<LocalBusinessItem> = queries::all_business_items(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
(services, items)
|
||||
let contacts = queries::all_contacts(&mut t).await.unwrap_or_default();
|
||||
(services, items, contacts)
|
||||
};
|
||||
|
||||
let businesses: Vec<_> = services
|
||||
.into_iter()
|
||||
.map(|service| view::LocalBusiness::from((service, &mut items)))
|
||||
.map(|service| view::LocalBusiness::from((service, &mut items, &mut contacts)))
|
||||
.collect();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
@ -135,7 +135,7 @@ async fn new_business_item(
|
||||
error!("{e:?}");
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
return Err(routes::Error::OwnedBusinessNotFound {
|
||||
return Err(Error::OwnedBusinessNotFound {
|
||||
account_id: account.id,
|
||||
});
|
||||
}
|
||||
|
19
src/routes/restricted/contacts.rs
Normal file
19
src/routes/restricted/contacts.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{post, HttpResponse};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::routes::{Identity, Result};
|
||||
|
||||
#[post("/create")]
|
||||
async fn create_contact(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::NotImplemented().finish())
|
||||
}
|
||||
|
||||
#[post("/update")]
|
||||
async fn update_contact(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::NotImplemented().finish())
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(create_contact).service(update_contact);
|
||||
}
|
@ -1,23 +1,21 @@
|
||||
use std::collections::HashMap;
|
||||
mod account;
|
||||
mod businesses;
|
||||
mod marketplace;
|
||||
mod news;
|
||||
|
||||
use actix_files::Files;
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::web::ServiceConfig;
|
||||
use actix_web::*;
|
||||
use askama::Template;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use tracing::*;
|
||||
|
||||
use crate::model::db;
|
||||
use crate::model::view::{self, Page};
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::{filters, Helper};
|
||||
use crate::{not_xss, queries, routes, utils};
|
||||
use crate::view::Helper;
|
||||
|
||||
#[derive(Default, Template)]
|
||||
#[template(path = "index.html")]
|
||||
#[template(path = "businesses/index.html")]
|
||||
pub struct IndexTemplate {
|
||||
services: Vec<view::LocalBusiness>,
|
||||
businesses: Vec<view::LocalBusiness>,
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
@ -36,474 +34,12 @@ pub async fn render_index() -> HttpResponse {
|
||||
)
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
#[tracing::instrument]
|
||||
pub async fn index(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let record = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await,
|
||||
_ => None,
|
||||
};
|
||||
let (services, mut items) = {
|
||||
use crate::model::db::{LocalBusiness, LocalBusinessItem};
|
||||
let services: Vec<LocalBusiness> = queries::visible_businesses(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let items: Vec<LocalBusinessItem> = queries::visible_business_items(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
(services, items)
|
||||
};
|
||||
|
||||
let services: Vec<_> = {
|
||||
use crate::model::view::*;
|
||||
services
|
||||
.into_iter()
|
||||
.map(|service| LocalBusiness::from((service, &mut items)))
|
||||
.collect()
|
||||
};
|
||||
|
||||
let body = IndexTemplate {
|
||||
services,
|
||||
account: record,
|
||||
page: Page::LocalBusinesses,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
#[derive(Default, Template)]
|
||||
#[template(path = "account.html")]
|
||||
struct AccountTemplate {
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
h: Helper,
|
||||
}
|
||||
|
||||
#[get("/account")]
|
||||
#[tracing::instrument]
|
||||
async fn account_page(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let account = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await,
|
||||
_ => {
|
||||
id.forget();
|
||||
None
|
||||
}
|
||||
};
|
||||
t.commit().await.ok();
|
||||
Ok(HttpResponse::Ok().body(
|
||||
AccountTemplate {
|
||||
account,
|
||||
page: Page::Account,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct RegisterForm {
|
||||
email: String,
|
||||
login: String,
|
||||
password: String,
|
||||
facebook_id: Option<String>,
|
||||
account_type: db::AccountType,
|
||||
items: Option<Vec<view::BusinessItemInput>>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
#[serde(flatten)]
|
||||
names: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String, String>) {
|
||||
let mut h = names
|
||||
.into_iter()
|
||||
.filter_map(|(name, value)| {
|
||||
let mut name = name
|
||||
.strip_prefix("items")?
|
||||
.split('[')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.strip_suffix(']').unwrap_or(s));
|
||||
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
|
||||
match name.next() {
|
||||
Some(s @ ("name" | "price" | "picture_url")) => Some((idx, s.to_string(), value)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
HashMap::with_capacity(60),
|
||||
|mut memo, (idx, field, value)| {
|
||||
let item = memo
|
||||
.entry(idx)
|
||||
.or_insert_with(view::BusinessItemInput::default);
|
||||
match field.as_str() {
|
||||
"name" => {
|
||||
item.name = value;
|
||||
}
|
||||
"price" => {
|
||||
item.price = value.parse().unwrap_or_default();
|
||||
}
|
||||
"picture_url" => {
|
||||
item.picture_url = value;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
memo
|
||||
},
|
||||
);
|
||||
let mut ids = { h.keys().copied().collect::<Vec<_>>() };
|
||||
ids.sort();
|
||||
for id in ids {
|
||||
if let Some(item) = h.remove(&id) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[post("/register")]
|
||||
#[tracing::instrument]
|
||||
async fn register(
|
||||
form: web::Form<RegisterForm>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse> {
|
||||
let mut form = form.into_inner();
|
||||
dbg!(&form);
|
||||
process_items(form.items.get_or_insert_default(), form.names);
|
||||
|
||||
let pool = db.into_inner();
|
||||
if form.account_type == db::AccountType::Admin {
|
||||
return Ok(HttpResponse::BadRequest().body("Security breach attempt detected!"));
|
||||
}
|
||||
|
||||
let mut t = pool.begin().await.unwrap();
|
||||
|
||||
let pass = match utils::encrypt(&form.password) {
|
||||
Ok(pass) => pass,
|
||||
Err(e) => {
|
||||
tracing::error!("{:?}", e);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Zapisanie hasła nie powiodło się".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let res: sqlx::Result<db::Account> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO accounts (login, email, pass, facebook_id, account_type)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, login, email, pass, facebook_id, account_type
|
||||
"#,
|
||||
)
|
||||
.bind(form.login)
|
||||
.bind(form.email)
|
||||
.bind(pass)
|
||||
.bind(form.facebook_id)
|
||||
.bind(form.account_type)
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
let account = match res {
|
||||
Ok(res) => {
|
||||
id.remember(format!("{}", res.id));
|
||||
res
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(form.account_type, db::AccountType::Business) {
|
||||
let name = form.name.as_deref().unwrap_or_default();
|
||||
let owner_id = account.id;
|
||||
let description = form.description.as_deref().unwrap_or_default();
|
||||
|
||||
not_xss!(name, t);
|
||||
not_xss!(description, t);
|
||||
|
||||
let res: sqlx::Result<db::LocalBusiness> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_businesses (name, owner_id, description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, owner_id, name, description, state
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(owner_id)
|
||||
.bind(description)
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
let business = match res {
|
||||
Ok(business) => business,
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
for (idx, item) in form.items.unwrap_or_default().into_iter().enumerate() {
|
||||
not_xss!(&item.name, t);
|
||||
not_xss!(&item.picture_url, t);
|
||||
|
||||
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
"#,
|
||||
)
|
||||
.bind(business.id)
|
||||
.bind(&item.name)
|
||||
.bind(item.price as i32)
|
||||
.bind(idx as i32)
|
||||
.bind(if item.picture_url.is_empty() {
|
||||
format!("--{}", uuid::Uuid::new_v4())
|
||||
} else {
|
||||
let name = item.picture_url.split('/').last().unwrap_or_default();
|
||||
let dir = utils::item_picture_write_dir(format!("{}", account.id));
|
||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
||||
error!("{e} {:?}", dir);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
error: Some(
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.".into(),
|
||||
),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
let path = dir.join(name);
|
||||
if let Err(e) = std::fs::rename(format!(".{}", item.picture_url), &path) {
|
||||
error!("{e} {:?}", item.picture_url);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
error: Some(
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.".into(),
|
||||
),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
let path = path.to_str().map(String::from).unwrap_or_default();
|
||||
path.strip_prefix('.')
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| path)
|
||||
})
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.commit().await.unwrap();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.body(
|
||||
AccountTemplate {
|
||||
account: Some(account),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
#[tracing::instrument]
|
||||
async fn logout(id: Identity) -> HttpResponse {
|
||||
id.forget();
|
||||
HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.body(
|
||||
IndexTemplate {
|
||||
page: Page::LocalBusinesses,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LoginForm {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
#[tracing::instrument]
|
||||
async fn login(form: web::Form<LoginForm>, db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let form = form.into_inner();
|
||||
let record: db::Account = match queries::account_by_email(&mut t, form.email).await {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
return Ok(HttpResponse::Ok().body(
|
||||
AccountTemplate {
|
||||
error: Some("Nie znaleziono konta".into()),
|
||||
page: Page::Login,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Err(e) = utils::validate(&form.password, &record.pass) {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Hasło i/lub adres e-mail są nieprawidłowe".into()),
|
||||
page: Page::Login,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
id.remember(format!("{}", record.id));
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: Some(record),
|
||||
|
||||
page: Page::Login,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn upload(
|
||||
payload: actix_multipart::Multipart,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> JsonResult<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(json pool.begin().await);
|
||||
let id = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await.map(|a| a.id),
|
||||
_ => None,
|
||||
};
|
||||
t.commit().await.ok();
|
||||
routes::uploads::hande_upload(payload, id, "accounts").await
|
||||
}
|
||||
|
||||
#[derive(Default, Template)]
|
||||
#[template(path = "news.html")]
|
||||
pub struct NewsTemplate {
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
news: Vec<db::NewsArticle>,
|
||||
h: Helper,
|
||||
}
|
||||
|
||||
#[get("/news")]
|
||||
async fn news(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let account = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await,
|
||||
_ => {
|
||||
id.forget();
|
||||
None
|
||||
}
|
||||
};
|
||||
let news = queries::published_news(&mut t).await.unwrap_or_default();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
NewsTemplate {
|
||||
account,
|
||||
page: Page::News,
|
||||
news,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
std::fs::create_dir_all("./uploads").expect("Failed to create ./uploads directory");
|
||||
|
||||
account::configure(config);
|
||||
news::configure(config);
|
||||
marketplace::configure(config);
|
||||
config
|
||||
.service(Files::new("/uploads", "./uploads"))
|
||||
.service(Files::new("/assets/images", "./assets/images"))
|
||||
@ -513,46 +49,6 @@ pub fn configure(config: &mut ServiceConfig) {
|
||||
.use_etag(true)
|
||||
.prefer_utf8(true)
|
||||
.show_files_listing(),
|
||||
)
|
||||
.service(index)
|
||||
.service(account_page)
|
||||
.service(news)
|
||||
.service(register)
|
||||
.service(logout)
|
||||
.service(login)
|
||||
.service(upload);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::view;
|
||||
use crate::model::view::BusinessItemInput;
|
||||
|
||||
impl BusinessItemInput {
|
||||
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
price,
|
||||
picture_url: picture_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_items() {
|
||||
let mut items = Vec::with_capacity(0);
|
||||
let mut names: HashMap<String, String> = HashMap::with_capacity(4);
|
||||
names.insert("items[0][name]".into(), "a".into());
|
||||
names.insert("items[0][price]".into(), "10".into());
|
||||
names.insert("items[1][name]".into(), "b".into());
|
||||
names.insert("items[1][price]".into(), "20".into());
|
||||
super::process_items(&mut items, names);
|
||||
let expected = vec![
|
||||
view::BusinessItemInput::new("a", 10, "/a"),
|
||||
view::BusinessItemInput::new("b", 20, "/b"),
|
||||
];
|
||||
assert_eq!(items, expected);
|
||||
}
|
||||
);
|
||||
businesses::configure(config);
|
||||
}
|
||||
|
446
src/routes/unrestricted/account.rs
Normal file
446
src/routes/unrestricted/account.rs
Normal file
@ -0,0 +1,446 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{get, post, web, HttpResponse};
|
||||
use askama::*;
|
||||
use serde::Deserialize;
|
||||
use sqlx::PgPool;
|
||||
use tracing::error;
|
||||
|
||||
use crate::model::view::Page;
|
||||
use crate::model::{db, view};
|
||||
use crate::routes::unrestricted::IndexTemplate;
|
||||
use crate::routes::{Identity, JsonResult, Result};
|
||||
use crate::view::Helper;
|
||||
use crate::{not_xss, queries, routes, utils};
|
||||
|
||||
#[post("/register")]
|
||||
#[tracing::instrument]
|
||||
async fn register(
|
||||
form: web::Form<RegisterForm>,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> Result<HttpResponse> {
|
||||
let mut form = form.into_inner();
|
||||
dbg!(&form);
|
||||
process_items(form.items.get_or_insert_default(), form.names);
|
||||
|
||||
let pool = db.into_inner();
|
||||
if form.account_type == db::AccountType::Admin {
|
||||
return Ok(HttpResponse::BadRequest().body("Security breach attempt detected!"));
|
||||
}
|
||||
|
||||
let mut t = pool.begin().await.unwrap();
|
||||
|
||||
let pass = match utils::encrypt(&form.password) {
|
||||
Ok(pass) => pass,
|
||||
Err(e) => {
|
||||
tracing::error!("{:?}", e);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Zapisanie hasła nie powiodło się".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
let res: sqlx::Result<db::Account> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO accounts (login, email, pass, facebook_id, account_type)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, login, email, pass, facebook_id, account_type
|
||||
"#,
|
||||
)
|
||||
.bind(form.login)
|
||||
.bind(form.email)
|
||||
.bind(pass)
|
||||
.bind(form.facebook_id)
|
||||
.bind(form.account_type)
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
let account = match res {
|
||||
Ok(res) => {
|
||||
id.remember(format!("{}", res.id));
|
||||
res
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
if matches!(form.account_type, db::AccountType::Business) {
|
||||
let name = form.name.as_deref().unwrap_or_default();
|
||||
let owner_id = account.id;
|
||||
let description = form.description.as_deref().unwrap_or_default();
|
||||
|
||||
not_xss!(name, t);
|
||||
not_xss!(description, t);
|
||||
|
||||
let res: sqlx::Result<db::LocalBusiness> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_businesses (name, owner_id, description)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, owner_id, name, description, state
|
||||
"#,
|
||||
)
|
||||
.bind(name)
|
||||
.bind(owner_id)
|
||||
.bind(description)
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
let business = match res {
|
||||
Ok(business) => business,
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
|
||||
for (idx, item) in form.items.unwrap_or_default().into_iter().enumerate() {
|
||||
not_xss!(&item.name, t);
|
||||
not_xss!(&item.picture_url, t);
|
||||
|
||||
let res: sqlx::Result<db::LocalBusinessItem> = sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO local_business_items (local_business_id, name, price, item_order, picture_url)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING id, local_business_id, name, price, item_order, picture_url
|
||||
"#,
|
||||
)
|
||||
.bind(business.id)
|
||||
.bind(&item.name)
|
||||
.bind(item.price as i32)
|
||||
.bind(idx as i32)
|
||||
.bind(if item.picture_url.is_empty() {
|
||||
format!("--{}", uuid::Uuid::new_v4())
|
||||
} else {
|
||||
let name = item.picture_url.split('/').last().unwrap_or_default();
|
||||
let dir = utils::item_picture_write_dir(format!("{}", account.id));
|
||||
if let Err(e) = std::fs::create_dir_all(&dir) {
|
||||
error!("{e} {:?}", dir);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
error: Some(
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.".into(),
|
||||
),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
let path = dir.join(name);
|
||||
if let Err(e) = std::fs::rename(format!(".{}", item.picture_url), &path) {
|
||||
error!("{e} {:?}", item.picture_url);
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
error: Some(
|
||||
"Problem z utworzeniem konta. Nie można zapisać zdjęcia.".into(),
|
||||
),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
let path = path.to_str().map(String::from).unwrap_or_default();
|
||||
path.strip_prefix('.')
|
||||
.map(String::from)
|
||||
.unwrap_or_else(|| path)
|
||||
})
|
||||
.fetch_one(&mut t)
|
||||
.await;
|
||||
match res {
|
||||
Ok(_) => {}
|
||||
Err(e) => {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.unwrap();
|
||||
return Ok(HttpResponse::BadRequest().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
error: Some("Problem z utworzeniem konta".into()),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t.commit().await.unwrap();
|
||||
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.body(
|
||||
AccountTemplate {
|
||||
account: Some(account),
|
||||
page: Page::Register,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/logout")]
|
||||
#[tracing::instrument]
|
||||
async fn logout(id: Identity) -> HttpResponse {
|
||||
id.forget();
|
||||
HttpResponse::SeeOther()
|
||||
.append_header(("Location", "/"))
|
||||
.body(
|
||||
IndexTemplate {
|
||||
page: Page::LocalBusinesses,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct LoginForm {
|
||||
email: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
#[post("/login")]
|
||||
#[tracing::instrument]
|
||||
async fn login(form: web::Form<LoginForm>, db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let form = form.into_inner();
|
||||
let record: db::Account = match queries::account_by_email(&mut t, form.email).await {
|
||||
Ok(record) => record,
|
||||
Err(e) => {
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
return Ok(HttpResponse::Ok().body(
|
||||
AccountTemplate {
|
||||
error: Some("Nie znaleziono konta".into()),
|
||||
page: Page::Login,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
};
|
||||
if let Err(e) = utils::validate(&form.password, &record.pass) {
|
||||
tracing::error!("{e}");
|
||||
dbg!(e);
|
||||
t.rollback().await.ok();
|
||||
return Ok(HttpResponse::BadRequest().body(
|
||||
AccountTemplate {
|
||||
error: Some("Hasło i/lub adres e-mail są nieprawidłowe".into()),
|
||||
page: Page::Login,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
id.remember(format!("{}", record.id));
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
AccountTemplate {
|
||||
account: Some(record),
|
||||
|
||||
page: Page::Login,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[post("/upload")]
|
||||
async fn upload(
|
||||
payload: actix_multipart::Multipart,
|
||||
db: Data<PgPool>,
|
||||
id: Identity,
|
||||
) -> JsonResult<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(json pool.begin().await);
|
||||
let id = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await.map(|a| a.id),
|
||||
_ => None,
|
||||
};
|
||||
t.commit().await.ok();
|
||||
routes::uploads::hande_upload(payload, id, "accounts").await
|
||||
}
|
||||
|
||||
#[derive(Default, Template)]
|
||||
#[template(path = "account.html")]
|
||||
struct AccountTemplate {
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
h: Helper,
|
||||
}
|
||||
|
||||
#[get("/account")]
|
||||
#[tracing::instrument]
|
||||
async fn account_page(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let account = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await,
|
||||
_ => {
|
||||
id.forget();
|
||||
None
|
||||
}
|
||||
};
|
||||
t.commit().await.ok();
|
||||
Ok(HttpResponse::Ok().body(
|
||||
AccountTemplate {
|
||||
account,
|
||||
page: Page::Account,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
struct RegisterForm {
|
||||
email: String,
|
||||
login: String,
|
||||
password: String,
|
||||
facebook_id: Option<String>,
|
||||
account_type: db::AccountType,
|
||||
items: Option<Vec<view::BusinessItemInput>>,
|
||||
name: Option<String>,
|
||||
description: Option<String>,
|
||||
#[serde(flatten)]
|
||||
names: HashMap<String, String>,
|
||||
}
|
||||
|
||||
#[tracing::instrument]
|
||||
fn process_items(items: &mut Vec<view::BusinessItemInput>, names: HashMap<String, String>) {
|
||||
let mut h = names
|
||||
.into_iter()
|
||||
.filter_map(|(name, value)| {
|
||||
let mut name = name
|
||||
.strip_prefix("items")?
|
||||
.split('[')
|
||||
.filter(|s| !s.is_empty())
|
||||
.map(|s| s.strip_suffix(']').unwrap_or(s));
|
||||
let idx: u16 = name.next().and_then(|s| s.parse().ok())?;
|
||||
match name.next() {
|
||||
Some(s @ ("name" | "price" | "picture_url")) => Some((idx, s.to_string(), value)),
|
||||
_ => None,
|
||||
}
|
||||
})
|
||||
.fold(
|
||||
HashMap::with_capacity(60),
|
||||
|mut memo, (idx, field, value)| {
|
||||
let item = memo
|
||||
.entry(idx)
|
||||
.or_insert_with(view::BusinessItemInput::default);
|
||||
match field.as_str() {
|
||||
"name" => {
|
||||
item.name = value;
|
||||
}
|
||||
"price" => {
|
||||
item.price = value.parse().unwrap_or_default();
|
||||
}
|
||||
"picture_url" => {
|
||||
item.picture_url = value;
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
memo
|
||||
},
|
||||
);
|
||||
let mut ids = { h.keys().copied().collect::<Vec<_>>() };
|
||||
ids.sort();
|
||||
for id in ids {
|
||||
if let Some(item) = h.remove(&id) {
|
||||
items.push(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config
|
||||
.service(register)
|
||||
.service(logout)
|
||||
.service(login)
|
||||
.service(upload)
|
||||
.service(account_page);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::model::view;
|
||||
use crate::model::view::BusinessItemInput;
|
||||
|
||||
impl BusinessItemInput {
|
||||
pub fn new<S: Into<String>, P: Into<String>>(name: S, price: u32, picture_url: P) -> Self {
|
||||
Self {
|
||||
name: name.into(),
|
||||
price,
|
||||
picture_url: picture_url.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_items() {
|
||||
let mut items = Vec::with_capacity(0);
|
||||
let mut names: HashMap<String, String> = HashMap::with_capacity(4);
|
||||
names.insert("items[0][name]".into(), "a".into());
|
||||
names.insert("items[0][price]".into(), "10".into());
|
||||
names.insert("items[1][name]".into(), "b".into());
|
||||
names.insert("items[1][price]".into(), "20".into());
|
||||
super::process_items(&mut items, names);
|
||||
let expected = vec![
|
||||
view::BusinessItemInput::new("a", 10, "/a"),
|
||||
view::BusinessItemInput::new("b", 20, "/b"),
|
||||
];
|
||||
assert_eq!(items, expected);
|
||||
}
|
||||
}
|
56
src/routes/unrestricted/businesses.rs
Normal file
56
src/routes/unrestricted/businesses.rs
Normal file
@ -0,0 +1,56 @@
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::model::view;
|
||||
use crate::model::view::Page;
|
||||
use crate::queries;
|
||||
use crate::routes::unrestricted::IndexTemplate;
|
||||
use crate::routes::{Identity, Result};
|
||||
|
||||
#[get("/")]
|
||||
#[tracing::instrument]
|
||||
pub async fn businesses_page(db: Data<PgPool>, id: Identity) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let record = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await,
|
||||
_ => None,
|
||||
};
|
||||
let (services, mut items, mut contacts) = {
|
||||
use crate::model::db::{LocalBusiness, LocalBusinessItem};
|
||||
let services: Vec<LocalBusiness> = queries::visible_businesses(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let items: Vec<LocalBusinessItem> = queries::visible_business_items(&mut t)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
let contacts = queries::all_contacts(&mut t).await.unwrap_or_default();
|
||||
(services, items, contacts)
|
||||
};
|
||||
|
||||
let services: Vec<_> = services
|
||||
.into_iter()
|
||||
.map(|service| view::LocalBusiness::from((service, &mut items, &mut contacts)))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let body = IndexTemplate {
|
||||
businesses: services,
|
||||
account: record,
|
||||
page: Page::LocalBusinesses,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
|
||||
t.commit().await.ok();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(body))
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(businesses_page);
|
||||
}
|
33
src/routes/unrestricted/marketplace.rs
Normal file
33
src/routes/unrestricted/marketplace.rs
Normal file
@ -0,0 +1,33 @@
|
||||
use actix_web::web::{self, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use askama::*;
|
||||
|
||||
use crate::model::db;
|
||||
use crate::model::view::Page;
|
||||
use crate::routes::Result;
|
||||
use crate::view::Helper;
|
||||
|
||||
#[derive(Default, Template)]
|
||||
#[template(path = "./marketplace/index.html")]
|
||||
struct MarketplaceTemplate {
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
h: Helper,
|
||||
}
|
||||
|
||||
#[get("")]
|
||||
async fn marketplace() -> Result<HttpResponse> {
|
||||
Ok(HttpResponse::Ok().body(
|
||||
MarketplaceTemplate {
|
||||
page: Page::Marketplace,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(web::scope("/marketplace").service(marketplace));
|
||||
}
|
48
src/routes/unrestricted/news.rs
Normal file
48
src/routes/unrestricted/news.rs
Normal file
@ -0,0 +1,48 @@
|
||||
use actix_web::web::{Data, ServiceConfig};
|
||||
use actix_web::{get, HttpResponse};
|
||||
use askama::*;
|
||||
use sqlx::PgPool;
|
||||
|
||||
use crate::model::db;
|
||||
use crate::model::view::Page;
|
||||
use crate::queries;
|
||||
use crate::routes::{Identity, Result};
|
||||
use crate::view::{filters, Helper};
|
||||
|
||||
#[derive(Default, Template)]
|
||||
#[template(path = "news.html")]
|
||||
pub struct NewsTemplate {
|
||||
account: Option<db::Account>,
|
||||
error: Option<String>,
|
||||
page: Page,
|
||||
news: Vec<db::NewsArticle>,
|
||||
h: Helper,
|
||||
}
|
||||
|
||||
#[get("/news")]
|
||||
async fn news(id: Identity, db: Data<PgPool>) -> Result<HttpResponse> {
|
||||
let pool = db.into_inner();
|
||||
let mut t = crate::ok_or_internal!(pool.begin().await);
|
||||
let account = match id.identity() {
|
||||
Some(id) => queries::account_by_id(&mut t, id).await,
|
||||
_ => {
|
||||
id.forget();
|
||||
None
|
||||
}
|
||||
};
|
||||
let news = queries::published_news(&mut t).await.unwrap_or_default();
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
NewsTemplate {
|
||||
account,
|
||||
page: Page::News,
|
||||
news,
|
||||
..Default::default()
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
))
|
||||
}
|
||||
|
||||
pub fn configure(config: &mut ServiceConfig) {
|
||||
config.service(news);
|
||||
}
|
@ -1,3 +1,5 @@
|
||||
use crate::model::db::AccountType;
|
||||
|
||||
pub mod filters {
|
||||
|
||||
pub fn opt_time(s: &Option<chrono::NaiveDateTime>) -> ::askama::Result<String> {
|
||||
@ -12,11 +14,23 @@ pub struct Helper;
|
||||
|
||||
impl Helper {
|
||||
pub fn is_admin(&self, account: &Option<crate::model::db::Account>) -> bool {
|
||||
use crate::model::db::AccountType;
|
||||
|
||||
account
|
||||
.as_ref()
|
||||
.map(|a| a.account_type == AccountType::Admin)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn is_above_user(&self, account: &Option<crate::model::db::Account>) -> bool {
|
||||
account
|
||||
.as_ref()
|
||||
.map(|a| a.account_type > AccountType::User)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn email<'a>(&self, account: &'a Option<crate::model::db::Account>) -> &'a str {
|
||||
account
|
||||
.as_ref()
|
||||
.map(|a| a.email.as_str())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user