Add contacts, improve UX

This commit is contained in:
Adrian Woźniak 2022-07-15 16:00:00 +02:00
parent 2f7e6358e8
commit f90c46dfcb
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
40 changed files with 1495 additions and 688 deletions

View File

@ -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; --hover-color: #f18902;
@ -6,46 +7,60 @@
--border-light-gray-color: #e5e5e5; --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 { main {
font-family: 'Noto Sans', sans-serif; font-family: 'Noto Sans', sans-serif;
} }
* { ow-nav > ow-path {
font-family: 'Noto Sans', sans-serif; text-align: center;
width: 48px;
}
ow-nav > ow-path > div {
display: none;
} }
@media (min-width: 1200px) { ow-nav > ow-path > svg {
article { fill: black;
width: 1280px; min-width: 32px;
margin: auto auto; max-width: 48px;
} margin: auto;
}
.bg { article {
height: 200px; padding: 4px;
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;
}
} }
* { * {
@ -77,7 +92,7 @@ h3 {
} }
p { p {
font-size: 1.3em; font-size: 1rem;
line-height: 1.5; line-height: 1.5;
margin-bottom: 1.3em; margin-bottom: 1.3em;
} }
@ -104,3 +119,33 @@ blockquote p {
local-businesses local-business p { local-businesses local-business p {
font-family: 'Noto Sans', sans-serif; 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);
}

View 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
View 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

View File

@ -8,25 +8,11 @@
<link href="/assets/css/app.css" rel="stylesheet"/> <link href="/assets/css/app.css" rel="stylesheet"/>
<link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico"/> <link rel="icon" type="image/x-icon" href="/assets/images/favicon.ico"/>
<script type="module" src=/assets/js/app.js></script> <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 %} {% block head %}{% endblock %}
</head> </head>
<body> <body>
<main> <main>
<header>
<div class="bg">
<h1>OS Wilno</h1>
</div>
</header>
{% match error %} {% match error %}
{% when Some with (e) %} {% when Some with (e) %}
<p class="error">{{e}}</p> <p class="error">{{e}}</p>
@ -34,23 +20,90 @@
{% endmatch %} {% endmatch %}
<article> <article>
<ow-nav> <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() %} {% if page.is_public() %}
<ow-path path="/" selected="{{ page.select_index() }}">Lokalne Usługi</ow-path> <ow-path path="/" selected="{{ page.select_index() }}">
<ow-path path="/marketplace" selected="{{ page.select_marketplace() }}">Targ</ow-path> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 511.999 511.999" xml:space="preserve">
<ow-path path="/news" selected="{{ page.select_news() }}">Aktualności</ow-path> <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"/>
<ow-path path="/account" selected="{{ page.select_account() }}">Konto</ow-path> <circle cx="304.734" cy="129.707" r="34.286"/>
{% match account.as_ref() %} </svg>
{% when Some with (a) %} <div>Lokalne Usługi</div>
<ow-path path="/account/business-items" selected="{{ page.select_business_items() }}">Moje usługi</ow-path> </ow-path>
{% when None %} <ow-path path="/marketplace" selected="{{ page.select_marketplace() }}">
{% endmatch %} <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 484.909 484.909" xml:space="preserve">
{% if h.is_admin(account) %} <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"/>
<ow-path path="/admin" selected="{{ page.select_admin_news() }}">Admin</ow-path> <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 %} {% 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() %} {% else if page.is_admin() %}
<ow-path path="/">Home</ow-path> <ow-path path="/admin/news" selected="{{ page.select_admin_news() }}">
<ow-path path="/admin/news" selected="{{ page.select_admin_news() }}">News</ow-path> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 508 508" xml:space="preserve">
<ow-path path="/admin/businesses" selected="{{ page.select_admin_businesses() }}">Localne Usługi</ow-path> <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 %} {% endif %}
</ow-nav> </ow-nav>
{% block content %}{% endblock %} {% block content %}{% endblock %}

View File

@ -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 %}

View 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 %}

View 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 %}

View File

@ -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 %}

View File

@ -0,0 +1,3 @@
{% extends "../base.html" %}
{% block content %}
{% endblock %}

View File

@ -13,6 +13,7 @@ customElements.define('admin-business', class extends Component {
} }
::slotted(local-business-item) { ::slotted(local-business-item) {
width: 100%; width: 100%;
margin-bottom: .5rem;
} }
</style> </style>
<article> <article>

View File

@ -2,16 +2,27 @@ import "./shared/form-navigation.js";
import "./local-businesses.js"; import "./local-businesses.js";
import "./login-form.js"; import "./login-form.js";
import "./ow-account.js"; import "./ow-account.js";
import "./nav/ow-nav.js"; import "./nav/ow-nav.js";
import "./nav/ow-path.js"; import "./nav/ow-path.js";
import "./price/price-view"; import "./price/price-view";
import "./price/price-input"; import "./price/price-input";
import "./register-form.js"; import "./register-form.js";
import "./business-items";
import "./business-items/business-item";
import "./business-items/business-item-editor";
import "./news/ow-articles"; import "./news/ow-articles";
import "./news/news-article"; import "./news/news-article";
import "./shared/rich-text-editor"; import "./shared/rich-text-editor";
import "./contacts/contact-info-list";
import "./contacts/contact-info";
import "./contacts/contact-info-editor";
import { fireFbReady } from "./shared.js"; import { fireFbReady } from "./shared.js";
if (!document.querySelector('#facebook-jssdk')) { if (!document.querySelector('#facebook-jssdk')) {

View File

@ -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; #idx;
static get observedAttributes() { static get observedAttributes() {

View File

@ -10,17 +10,6 @@ customElements.define('business-item', class extends Component {
super(` super(`
<style> <style>
:host { display: block; } :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 { :host(:first-child) #move-up {
display: none; display: none;
} }
@ -30,7 +19,20 @@ customElements.define('business-item', class extends Component {
#move svg { #move svg {
cursor: pointer; 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; border: none;
background: none; background: none;
} }

View 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';
}
});

View 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>
`);
}
});

View 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>
`);
}
});

View 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;
}
});

View File

@ -2,7 +2,7 @@ import "./local-businesses/local-business-item";
import "./local-businesses/local-business"; import "./local-businesses/local-business";
import { Component } from "./shared"; import { Component } from "./shared";
customElements.define('local-businesses', class extends Component { customElements.define('local-business-list', class extends Component {
static get observedAttributes() { static get observedAttributes() {
return ['filter'] return ['filter']
} }
@ -16,7 +16,7 @@ customElements.define('local-businesses', class extends Component {
display: none; display: none;
} }
input { input {
font-size: 20pt; font-size: 1rem;
line-height: 2.6em; line-height: 2.6em;
height: 2.6em; height: 2.6em;
margin: 0; margin: 0;
@ -34,7 +34,7 @@ customElements.define('local-businesses', class extends Component {
<input type="text" id="filter" placeholder="Filtruj" /> <input type="text" id="filter" placeholder="Filtruj" />
</section> </section>
<section id="items"> <section id="items">
<slot name="services"></slot> <slot name="business"></slot>
</section> </section>
`); `);
{ {

View File

@ -10,22 +10,51 @@ customElements.define('local-business-item', class extends Component {
<style> <style>
:host { display: block; } :host { display: block; }
* { font-family: 'Noto Sans', sans-serif; } * { font-family: 'Noto Sans', sans-serif; }
: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 { #item {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
h3 { h3 {
font-weight: normal; font-weight: normal;
line-height: 1.6;
width: calc(100% - 450px);
text-align: left;
} }
#price { #price {
font-weight: bold; font-weight: bold;
width: 180px;
} }
img { img {
width: 200px; width: 128px;
max-width: 200px; max-width: 128px;
} }
:host([picture-url = '']) img {
display: none;
} }
</style> </style>
<section id="item"> <section id="item">

View File

@ -14,16 +14,15 @@ customElements.define('ow-path', class extends Component {
display: block; display: block;
padding: 10px 18px; padding: 10px 18px;
text-decoration: none; text-decoration: none;
color: #495057;
text-transform: uppercase; text-transform: uppercase;
border: none; border: none;
border-bottom: 1px solid var(--border-slim-color); color: var(--hover-color);
} }
a:hover { a:hover {
color: var(--hover-color); color: var(--hover-color);
} }
:host(:not([selected])) a { :host(:not([selected])) a {
border: none; color: #495057;
} }
</style> </style>
<a><slot></slot></a> <a><slot></slot></a>

View File

@ -20,10 +20,6 @@ customElements.define('news-article', class extends Component {
h1 #status { h1 #status {
font-size: 14px; font-size: 14px;
} }
#time {
display: flex;
justify-content: space-between;
}
.time span:first-child { .time span:first-child {
margin-right: 8px; margin-right: 8px;
} }
@ -41,6 +37,12 @@ customElements.define('news-article', class extends Component {
:host([hide-status="true"]) #status { :host([hide-status="true"]) #status {
display: none; display: none;
} }
@media (min-width: 1200px) {
#time {
display: flex;
justify-content: space-between;
}
}
${ BLOCK_QUOTE_STYLE } ${ BLOCK_QUOTE_STYLE }
</style> </style>
<article> <article>

View File

@ -1,7 +1,7 @@
export const S = Symbol(); export const S = Symbol();
export const BUTTON_STYLE = ` export const BUTTON_STYLE = `
input[type="button"], input[type="submit"] { input[type="button"], input[type="submit"], button {
cursor: pointer; cursor: pointer;
border-radius: 5px; border-radius: 5px;
box-shadow: 0 10px 20px -6px rgba(0,0,0,.12); box-shadow: 0 10px 20px -6px rgba(0,0,0,.12);

View 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()
);

View 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
);

View File

@ -8,9 +8,9 @@ use uuid::Uuid;
#[derive(Debug, PartialOrd, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Type)] #[derive(Debug, PartialOrd, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, Type)]
pub enum AccountType { pub enum AccountType {
User, User = 1,
Business, Business = 10,
Admin, Admin = 100,
} }
#[derive(Debug, Serialize, Deserialize, FromRow)] #[derive(Debug, Serialize, Deserialize, FromRow)]
@ -108,6 +108,14 @@ pub struct LocalBusinessItem {
pub picture_url: String, 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)] #[derive(Debug, Serialize, Deserialize, FromRow)]
pub struct NewsArticle { pub struct NewsArticle {
pub id: i32, pub id: i32,
@ -177,3 +185,18 @@ pub struct UpdateLocalBusinessItemInput {
pub struct DeleteNewsArticleInput { pub struct DeleteNewsArticleInput {
pub id: i32, 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,
}

View File

@ -116,10 +116,23 @@ pub struct LocalBusiness {
pub description: String, pub description: String,
pub state: db::LocalBusinessState, pub state: db::LocalBusinessState,
pub items: Vec<db::LocalBusinessItem>, pub items: Vec<db::LocalBusinessItem>,
pub contacts: Vec<db::ContactInfo>,
} }
impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for LocalBusiness { impl<'items, 'contacts>
fn from((service, items): (db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)) -> Self { 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 { Self {
id: service.id, id: service.id,
owner_id: service.owner_id, owner_id: service.owner_id,
@ -129,6 +142,9 @@ impl<'v> From<(db::LocalBusiness, &'v mut Vec<db::LocalBusinessItem>)> for Local
items: items items: items
.drain_filter(|i| i.local_business_id == service.id) .drain_filter(|i| i.local_business_id == service.id)
.collect(), .collect(),
contacts: contacts
.drain_filter(|c| c.owner_id == service.owner_id)
.collect(),
} }
} }
} }

View File

@ -29,6 +29,10 @@ pub enum Error {
id: i32, id: i32,
item_order: i32, item_order: i32,
}, },
BusinessItemState {
id: i32,
state: db::LocalBusinessState,
},
AllItems, AllItems,
OwnedBusiness { OwnedBusiness {
account_id: i32, account_id: i32,
@ -53,6 +57,19 @@ pub enum Error {
DeleteNewsArticle { DeleteNewsArticle {
id: i32, 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>; pub type Result<T> = std::result::Result<T, Error>;
@ -749,6 +766,137 @@ RETURNING
.map_err(|e| { .map_err(|e| {
error!("{e}"); error!("{e}");
dbg!(&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 }
}) })
} }

View File

@ -103,9 +103,9 @@ pub enum Error {
pub fn reject_xss(s: &str) -> Result<()> { pub fn reject_xss(s: &str) -> Result<()> {
if s.contains("<script ") || s.contains("<script>") { if s.contains("<script ") || s.contains("<script>") {
Ok(())
} else {
Err(Error::XSS) Err(Error::XSS)
} else {
Ok(())
} }
} }

View File

@ -8,17 +8,19 @@ use crate::queries;
use crate::routes::{Identity, Result}; use crate::routes::{Identity, Result};
pub mod admin; pub mod admin;
mod business_item; mod business_item;
mod contacts;
use crate::view::Helper; use crate::view::Helper;
#[derive(Debug, Default, Template)] #[derive(Debug, Default, Template)]
#[template(path = "business-items.html")] #[template(path = "businesses/editor.html")]
struct BusinessItemsTemplate { struct BusinessItemsTemplate {
page: view::Page, page: view::Page,
error: Option<String>, error: Option<String>,
account: Option<db::Account>, account: Option<db::Account>,
items: Vec<db::LocalBusinessItem>, items: Vec<db::LocalBusinessItem>,
business: db::LocalBusiness, business: db::LocalBusiness,
contacts: Vec<db::ContactInfo>,
h: Helper, h: Helper,
} }
@ -71,12 +73,15 @@ async fn handle_business_items_page(
let business = let business =
crate::ok_or_internal!(queries::account_business_by_owner_id(t, account.id).await); 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 { let page = BusinessItemsTemplate {
page: view::Page::BusinessItems, page: view::Page::BusinessItems,
account: Some(account), account: Some(account),
items, items,
business, business,
contacts,
..Default::default() ..Default::default()
}; };
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
@ -87,5 +92,6 @@ async fn handle_business_items_page(
pub fn configure(config: &mut ServiceConfig) { pub fn configure(config: &mut ServiceConfig) {
config.service(business_items_page); config.service(business_items_page);
business_item::configure(config); business_item::configure(config);
contacts::configure(config);
admin::configure(config); admin::configure(config);
} }

View File

@ -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 mut t = crate::ok_or_internal!(pool.begin().await);
let _account = require_admin!(&mut t, id); let _account = require_admin!(&mut t, id);
let (services, mut items) = { let (services, mut items, mut contacts) = {
use crate::model::db::{LocalBusiness, LocalBusinessItem}; use crate::model::db::{LocalBusiness, LocalBusinessItem};
let services: Vec<LocalBusiness> = let services: Vec<LocalBusiness> =
queries::all_businesses(&mut t).await.unwrap_or_default(); 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) let items: Vec<LocalBusinessItem> = queries::all_business_items(&mut t)
.await .await
.unwrap_or_default(); .unwrap_or_default();
(services, items) let contacts = queries::all_contacts(&mut t).await.unwrap_or_default();
(services, items, contacts)
}; };
let businesses: Vec<_> = services let businesses: Vec<_> = services
.into_iter() .into_iter()
.map(|service| view::LocalBusiness::from((service, &mut items))) .map(|service| view::LocalBusiness::from((service, &mut items, &mut contacts)))
.collect(); .collect();
t.commit().await.ok(); t.commit().await.ok();

View File

@ -135,7 +135,7 @@ async fn new_business_item(
error!("{e:?}"); error!("{e:?}");
dbg!(e); dbg!(e);
t.rollback().await.ok(); t.rollback().await.ok();
return Err(routes::Error::OwnedBusinessNotFound { return Err(Error::OwnedBusinessNotFound {
account_id: account.id, account_id: account.id,
}); });
} }

View 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);
}

View File

@ -1,23 +1,21 @@
use std::collections::HashMap; mod account;
mod businesses;
mod marketplace;
mod news;
use actix_files::Files; use actix_files::Files;
use actix_web::web::{Data, ServiceConfig}; use actix_web::web::ServiceConfig;
use actix_web::*; use actix_web::*;
use askama::Template; use askama::Template;
use serde::Deserialize;
use sqlx::PgPool;
use tracing::*;
use crate::model::db; use crate::model::db;
use crate::model::view::{self, Page}; use crate::model::view::{self, Page};
use crate::routes::{Identity, JsonResult, Result}; use crate::view::Helper;
use crate::view::{filters, Helper};
use crate::{not_xss, queries, routes, utils};
#[derive(Default, Template)] #[derive(Default, Template)]
#[template(path = "index.html")] #[template(path = "businesses/index.html")]
pub struct IndexTemplate { pub struct IndexTemplate {
services: Vec<view::LocalBusiness>, businesses: Vec<view::LocalBusiness>,
account: Option<db::Account>, account: Option<db::Account>,
error: Option<String>, error: Option<String>,
page: Page, 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) { pub fn configure(config: &mut ServiceConfig) {
std::fs::create_dir_all("./uploads").expect("Failed to create ./uploads directory"); std::fs::create_dir_all("./uploads").expect("Failed to create ./uploads directory");
account::configure(config);
news::configure(config);
marketplace::configure(config);
config config
.service(Files::new("/uploads", "./uploads")) .service(Files::new("/uploads", "./uploads"))
.service(Files::new("/assets/images", "./assets/images")) .service(Files::new("/assets/images", "./assets/images"))
@ -513,46 +49,6 @@ pub fn configure(config: &mut ServiceConfig) {
.use_etag(true) .use_etag(true)
.prefer_utf8(true) .prefer_utf8(true)
.show_files_listing(), .show_files_listing(),
) );
.service(index) businesses::configure(config);
.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);
}
} }

View 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);
}
}

View 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);
}

View 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));
}

View 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);
}

View File

@ -1,3 +1,5 @@
use crate::model::db::AccountType;
pub mod filters { pub mod filters {
pub fn opt_time(s: &Option<chrono::NaiveDateTime>) -> ::askama::Result<String> { pub fn opt_time(s: &Option<chrono::NaiveDateTime>) -> ::askama::Result<String> {
@ -12,11 +14,23 @@ pub struct Helper;
impl Helper { impl Helper {
pub fn is_admin(&self, account: &Option<crate::model::db::Account>) -> bool { pub fn is_admin(&self, account: &Option<crate::model::db::Account>) -> bool {
use crate::model::db::AccountType;
account account
.as_ref() .as_ref()
.map(|a| a.account_type == AccountType::Admin) .map(|a| a.account_type == AccountType::Admin)
.unwrap_or_default() .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()
}
} }