Compare commits
45 commits
feature/ov
...
main
Author | SHA1 | Date | |
---|---|---|---|
d19e802c8c | |||
a27af9aefb | |||
eeed72270e | |||
8783b8467a | |||
f5fa5b2971 | |||
d8c28ceea1 | |||
eec6175e13 | |||
d7b624df4d | |||
d71e59b9c0 | |||
f75d66a0d0 | |||
0ccfe985ae | |||
2d31b019e9 | |||
3b67ce0e5a | |||
0a71a62af8 | |||
c7286a60da | |||
fb10e5b746 | |||
f60719fa9e | |||
55fc3fe4e0 | |||
704fdaf27d | |||
d34096d3c8 | |||
dd707bbf62 | |||
e591c276f5 | |||
38cd37cf74 | |||
1a5dd102e0 | |||
5e70236eac | |||
cc54cb8112 | |||
236397fc9c | |||
bb346773bd | |||
3083b99898 | |||
33713419f6 | |||
9746896a43 | |||
c95aa68ec2 | |||
1504b8bfe9 | |||
cef5330567 | |||
1bd69c9c97 | |||
4b07ebb2ec | |||
0aa495e05b | |||
9498911e7a | |||
27f051cf14 | |||
3f398a0081 | |||
0133475e2a | |||
9a953980dc | |||
4b22115159 | |||
70348d85ee | |||
11bcdce6cb |
50 changed files with 2848 additions and 741 deletions
app
app.vue
docker-compose.yamlnuxt.config.tspackage-lock.jsonpackage.jsonassets/styles
button.cssbuttonGroup.cssdialog.cssfooter.css
form
formInput.cssgeneral.cssheader.csslandingpage.cssnavigation.csspage.csspriceCard.csstimelineCard.csstoolbar.csscomponents/Pp
DeleteDialog.vueFooter.vue
Form
FormInput.vueHeader.vueNavigation.vuePriceCard.vuePriceCardDialog.vueTimelineCard.vuecomposables
layouts
pages
utils
public
server/api
shared
17
app/app.vue
Executable file → Normal file
17
app/app.vue
Executable file → Normal file
|
@ -1,5 +1,18 @@
|
||||||
<template>
|
<template>
|
||||||
<PpHeader />
|
<PpNavigation />
|
||||||
|
<NuxtLayout>
|
||||||
<NuxtPage />
|
<NuxtPage />
|
||||||
<PpFooter />
|
</NuxtLayout>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.page-enter-active,
|
||||||
|
.page-leave-active {
|
||||||
|
transition: all 200ms;
|
||||||
|
}
|
||||||
|
.page-enter-from,
|
||||||
|
.page-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
filter: blur(.5rem);
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,6 +1,6 @@
|
||||||
.Button {
|
.Button {
|
||||||
--padding: .2rem;
|
--padding: var(--padding-xs);
|
||||||
--background: var(--color-gradient-main-dark);
|
--background: var(--color-main-dark);
|
||||||
--color: var(--color-lightest);
|
--color: var(--color-lightest);
|
||||||
--background-hover: var(--color-main-dark);
|
--background-hover: var(--color-main-dark);
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: var(--transition-default);
|
transition: var(--transition-default);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -19,17 +19,17 @@
|
||||||
&.transparent {
|
&.transparent {
|
||||||
--background: transparent;
|
--background: transparent;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
padding: .5em 1.5em;
|
padding: var(--padding-s) var(--padding-l);
|
||||||
border-radius: var(--radius-default);
|
border-radius: var(--radius-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.raised {
|
&.raised {
|
||||||
box-shadow: var(--box-shadow-z2);
|
box-shadow: var(--box-shadow-z2);
|
||||||
padding: .5em 1.5em;
|
padding: var(--padding-s) var(--padding-l);
|
||||||
border-radius: var(--radius-default);
|
border-radius: var(--radius-default);
|
||||||
|
|
||||||
&.danger {
|
&.danger {
|
||||||
--background: var(--color-gradient-error);
|
--background: var(--color-error);
|
||||||
--color: var(--color-lightest);
|
--color: var(--color-lightest);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -37,7 +37,7 @@
|
||||||
&.text {
|
&.text {
|
||||||
--background: transparent;
|
--background: transparent;
|
||||||
--color: var(--color-darkest);
|
--color: var(--color-darkest);
|
||||||
padding: .5em 1.5em;
|
padding: var(--padding-s) var(--padding-l);
|
||||||
border-radius: var(--radius-default);
|
border-radius: var(--radius-default);
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -62,13 +62,13 @@
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
border-radius: 100%;
|
border-radius: 100%;
|
||||||
padding: .5rem;
|
padding: var(--padding-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.cta {
|
&.cta {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
padding: .5rem 1.5rem;
|
padding: var(--padding-s) var(--padding-l);
|
||||||
border-radius: var(--radius-default);
|
border-radius: var(--radius-default);
|
||||||
box-shadow: var(--box-shadow-z2);
|
box-shadow: var(--box-shadow-z2);
|
||||||
|
|
||||||
|
@ -93,18 +93,30 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.search-button {
|
||||||
|
--background: var(--color-lightest);
|
||||||
|
--color: var(--color-main-darkest);
|
||||||
|
border-radius: 100%;
|
||||||
|
padding: var(--padding);
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
scale: 1.2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.mini-button {
|
&.mini-button {
|
||||||
padding: .5rem 1.5rem;
|
padding: var(--padding-s) var(--padding-l);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: .5rem;
|
gap: var(--padding-xs);
|
||||||
|
|
||||||
& > .icon {
|
& > .icon {
|
||||||
font-size: 1.5rem;
|
font-size: var(--font-size-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
& > span {
|
& > span {
|
||||||
font-size: .8rem;
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,9 +1,6 @@
|
||||||
.ButtonGroup {
|
.ButtonGroup {
|
||||||
display: flex;
|
display: flex;
|
||||||
background: var(--color-main);
|
background: var(--color-main);
|
||||||
border-radius: var(--radius-default);
|
|
||||||
overflow: hidden;
|
|
||||||
box-shadow: var(--box-shadow-z2);
|
|
||||||
|
|
||||||
& button {
|
& button {
|
||||||
--color: var(--color-light);
|
--color: var(--color-light);
|
||||||
|
@ -12,8 +9,8 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: .5rem;
|
gap: var(--padding-xs);
|
||||||
padding: .5rem;
|
padding: var(--padding-s);
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--color);
|
color: var(--color);
|
||||||
|
@ -24,13 +21,5 @@
|
||||||
--color: var(--color-lightest);
|
--color: var(--color-lightest);
|
||||||
--background: var(--color-main-dark);
|
--background: var(--color-main-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
border-radius: var(--radius-default) 0 0 var(--radius-default);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-radius: 0 var(--radius-default) var(--radius-default) 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
71
app/assets/styles/dialog.css
Normal file
71
app/assets/styles/dialog.css
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
|
||||||
|
dialog {
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
width: clamp(400px, 100vw, calc(var(--page-max-width) - var(--padding-xxl) * 2));
|
||||||
|
translate: -50% -50%;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-default);
|
||||||
|
background: var(--color-lightest);
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
color: var(--color-darkest);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0;
|
||||||
|
transition:
|
||||||
|
opacity var(--transition-default) ease-out,
|
||||||
|
scale var(--transition-default) ease-out,
|
||||||
|
overlay var(--transition-default) ease-out allow-discrete,
|
||||||
|
display var(--transition-default) ease-out allow-discrete;
|
||||||
|
|
||||||
|
&[open] {
|
||||||
|
opacity: 1;
|
||||||
|
scale: 1;
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: rgb(0 0 0 / 25%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
z-index: 2000;
|
||||||
|
background-color: rgb(0 0 0 / 0%);
|
||||||
|
transition:
|
||||||
|
display var(--transition-default) allow-discrete,
|
||||||
|
overlay var(--transition-default) allow-discrete,
|
||||||
|
background-color var(--transition-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > .wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--padding-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
& header {
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--padding-default) var(--padding-default) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
padding: 0 var(--padding-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
& footer {
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 var(--padding-default) var(--padding-default);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
dialog[open] {
|
||||||
|
opacity: 0;
|
||||||
|
scale: 0;
|
||||||
|
|
||||||
|
&::backdrop {
|
||||||
|
background-color: rgb(0 0 0 / 0%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,38 +1,49 @@
|
||||||
.Footer {
|
.Footer {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--color-darkest);
|
background: var(--color-darkest);
|
||||||
padding: 1rem;
|
padding: var(--padding-default);
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
|
||||||
& h4 {
|
& h4 {
|
||||||
color: var(--color-lightest);
|
color: var(--color-lightest);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: var(--padding-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .bottom {
|
& .bottom {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 1rem;
|
justify-content: center;
|
||||||
justify-content: space-between;
|
|
||||||
color: var(--color-light);
|
color: var(--color-light);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
& .copy {
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
color: var(--color-light);
|
||||||
|
margin-top: var(--padding-default);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& a {
|
||||||
|
color: var(--color-main);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
& .socials {
|
& .socials {
|
||||||
font-size: 1.5rem;
|
font-size: var(--font-size-xl);
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: var(--padding-xl);
|
||||||
}
|
}
|
||||||
|
|
||||||
& .data-links {
|
& .data-links {
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
font-size: .8rem;
|
font-size: var(--font-size-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
& ul {
|
& ul {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
|
|
||||||
& a {
|
& a {
|
||||||
color: var(--color-lightest);
|
color: var(--color-lightest);
|
||||||
|
|
15
app/assets/styles/form/search.css
Normal file
15
app/assets/styles/form/search.css
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
.Search {
|
||||||
|
border-radius: 9999px;
|
||||||
|
background: var(--color-lightest);
|
||||||
|
padding: 0 var(--padding-default);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--padding-default);
|
||||||
|
|
||||||
|
& > input {
|
||||||
|
all: unset;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: var(--padding-s) 0;
|
||||||
|
}
|
||||||
|
}
|
72
app/assets/styles/form/textfield.css
Executable file
72
app/assets/styles/form/textfield.css
Executable file
|
@ -0,0 +1,72 @@
|
||||||
|
.TextField {
|
||||||
|
--border-color: var(--color-light);
|
||||||
|
--label-color: var(--color-middle);
|
||||||
|
--label-position-top: var(--font-size-default);
|
||||||
|
--label-position-left: 2.5rem;
|
||||||
|
--label-font-size: var(--font-size-default);
|
||||||
|
--message-color: var(--color-middle);
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
flex: 25% 1 0;
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
--border-color: var(--color-main-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:focus-within,
|
||||||
|
&:has(input:not(:placeholder-shown)) {
|
||||||
|
--label-color: var(--color-main-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.error {
|
||||||
|
--label-color: var(--color-error) !important;
|
||||||
|
--border-color: var(--color-error);
|
||||||
|
--message-color: var(--color-error);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .wrapper {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--padding-xxs) var(--padding-xs);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
& label {
|
||||||
|
position: absolute;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--label-color);
|
||||||
|
gap: 2px;
|
||||||
|
top: -6px;
|
||||||
|
left: calc(var(--padding-xs) - 2px);
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
transition: var(--transition-default);
|
||||||
|
|
||||||
|
& > * {
|
||||||
|
background: var(--color-lightest);
|
||||||
|
padding: 0 2px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .icon {
|
||||||
|
position: relative;
|
||||||
|
color: var(--label-color);
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
top: -1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& input {
|
||||||
|
all: unset;
|
||||||
|
padding: var(--padding-xxs) var(--padding-s) var(--padding-xxs) 0;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
width: 100%;
|
||||||
|
flex: 25% 1 0;
|
||||||
|
color: var(--color-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
& > span {
|
||||||
|
color: var(--message-color);
|
||||||
|
font-size: var(--font-size-xs);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,52 +0,0 @@
|
||||||
.Input {
|
|
||||||
&.error {
|
|
||||||
& .input-wrapper {
|
|
||||||
border-color: var(--color-error);
|
|
||||||
outline-width: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
color: var(--color-error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& span {
|
|
||||||
font-size: .65em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& .input-wrapper {
|
|
||||||
position: relative;
|
|
||||||
flex: 25% 1 0;
|
|
||||||
border: 2px solid var(--color-main-dark);
|
|
||||||
border-radius: var(--radius-default);
|
|
||||||
overflow: hidden;
|
|
||||||
transition: var(--transition-default);
|
|
||||||
outline: 0 solid var(--color-lightest);
|
|
||||||
|
|
||||||
& label {
|
|
||||||
position: absolute;
|
|
||||||
font-size: .8em;
|
|
||||||
top: .3rem;
|
|
||||||
left: .5rem;
|
|
||||||
transition: var(--transition-default);
|
|
||||||
}
|
|
||||||
|
|
||||||
& input {
|
|
||||||
all: unset;
|
|
||||||
width: calc(100% - 1rem);
|
|
||||||
padding: 1.3rem .5rem .5rem .5rem;
|
|
||||||
background: var(--color-lightest);
|
|
||||||
|
|
||||||
&[type="number"] {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& input:focus,
|
|
||||||
& input:not(:placeholder-shown) {
|
|
||||||
& + label {
|
|
||||||
color: var(--color-main-dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,30 +1,25 @@
|
||||||
:root {
|
:root {
|
||||||
--padding-default: 1rem;
|
|
||||||
--padding-small: .5rem;
|
|
||||||
--radius-default: 3px;
|
|
||||||
--transition-default: 150ms;
|
|
||||||
|
|
||||||
--color-success: #328104;
|
--color-success: #328104;
|
||||||
--color-error: #A20606;
|
--color-error: #a20606;
|
||||||
--color-blue-light: #0DDCE7;
|
--color-blue-light: #d7e1f1;
|
||||||
--color-blue: #05B0FF;
|
--color-blue: #05b0ff;
|
||||||
--color-blue-dark: #0266F2;
|
--color-blue-dark: #0266f2;
|
||||||
--color-blue-darkest: #013174;
|
--color-blue-darkest: #013174;
|
||||||
|
|
||||||
--color-darkest: #292929;
|
--color-darkest: #292929;
|
||||||
--color-dark: #404040;
|
--color-dark: #404040;
|
||||||
--color-middle: #707070;
|
--color-middle: #707070;
|
||||||
--color-light: #E0E0E6;
|
--color-light: #e0e0e6;
|
||||||
--color-lightest: #FAFAFF;
|
--color-lightest: #fafaff;
|
||||||
|
|
||||||
--color-green-light: #05FFC5;
|
|
||||||
--color-green: #02F276;
|
|
||||||
--color-green-dark: #09DC33;
|
|
||||||
--color-green-darkest: #07B029;
|
|
||||||
--color-green-darkest-most: #157C2A;
|
|
||||||
|
|
||||||
|
--color-green-light: #05ffc5;
|
||||||
|
--color-green: #02f276;
|
||||||
|
--color-green-dark: #09dc33;
|
||||||
|
--color-green-darkest: #07b029;
|
||||||
|
--color-green-darkest-most: #157c2a;
|
||||||
|
|
||||||
--color-main: var(--color-blue);
|
--color-main: var(--color-blue);
|
||||||
|
--color-main-lightest: var(--color-blue-light);
|
||||||
--color-main-light: var(--color-blue-light);
|
--color-main-light: var(--color-blue-light);
|
||||||
--color-main-dark: var(--color-blue-dark);
|
--color-main-dark: var(--color-blue-dark);
|
||||||
--color-main-darkest: var(--color-blue-darkest);
|
--color-main-darkest: var(--color-blue-darkest);
|
||||||
|
@ -34,17 +29,44 @@
|
||||||
--color-accent-dark: var(--color-green-dark);
|
--color-accent-dark: var(--color-green-dark);
|
||||||
--color-accent-darkest: var(--color-green-darkest);
|
--color-accent-darkest: var(--color-green-darkest);
|
||||||
|
|
||||||
|
--color-text: var(--color-darkest);
|
||||||
|
--color-text-invert: var(--color-lightest);
|
||||||
|
|
||||||
--color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light));
|
--color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light));
|
||||||
--color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark));
|
--color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark));
|
||||||
--color-gradient-accent: linear-gradient(to bottom right, var(--color-accent), var(--color-accent-light));
|
--color-gradient-accent: linear-gradient(to bottom right, var(--color-accent), var(--color-accent-light));
|
||||||
--color-gradient-accent-dark: linear-gradient(to bottom right, var(--color-accent-darkest), var(--color-accent-dark));
|
--color-gradient-accent-dark: linear-gradient(to bottom right, var(--color-accent-darkest), var(--color-accent-dark));
|
||||||
--color-gradient-error: linear-gradient(to bottom right, #B00707, #DC0909);
|
--color-gradient-error: linear-gradient(to bottom right, #b00707, #dc0909);
|
||||||
--color-gradient-error-reverse: linear-gradient(to top left, #B00707, #DC0909);
|
--color-gradient-error-reverse: linear-gradient(to top left, #b00707, #dc0909);
|
||||||
|
|
||||||
--box-shadow-upper: 0 -3px 6px rgba(0,0,0,0.16), 0 -3px 6px rgba(0,0,0,0.23);
|
--box-shadow-upper: 0 -3px 6px rgba(0, 0, 0, 0.16), 0 -3px 6px rgba(0, 0, 0, 0.23);
|
||||||
--box-shadow-z2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
|
--box-shadow-z2: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||||
--box-shadow-inset: inset 0 3px 6px rgba(0,0,0,0.16), inset 0 3px 6px rgba(0,0,0,0.23);
|
--box-shadow-inset: inset 0 3px 6px rgba(0, 0, 0, 0.16), inset 0 3px 6px rgba(0, 0, 0, 0.23);
|
||||||
|
|
||||||
|
/* Font Sizes & Scaling Factor*/
|
||||||
|
--scaling-factor: 1.25;
|
||||||
|
--font-size-xs: calc(var(--font-size-s) / var(--scaling-factor));
|
||||||
|
--font-size-s: calc(var(--font-size-default) / var(--scaling-factor));
|
||||||
|
--font-size-default: 1rem;
|
||||||
|
--font-size-l: calc(var(--font-size-default) * var(--scaling-factor));
|
||||||
|
--font-size-xl: calc(var(--font-size-l) * var(--scaling-factor));
|
||||||
|
--font-size-xxl: calc(var(--font-size-xl) * var(--scaling-factor));
|
||||||
|
|
||||||
|
/* Paddings depend on Font-Size */
|
||||||
|
--padding-xxs: calc(var(--padding-xs) / var(--scaling-factor));
|
||||||
|
--padding-xs: calc(var(--padding-s) / var(--scaling-factor));
|
||||||
|
--padding-default: var(--font-size-default);
|
||||||
|
--padding-s: calc(var(--padding-default) / var(--scaling-factor));
|
||||||
|
--padding-l: calc(var(--padding-default) * var(--scaling-factor));
|
||||||
|
--padding-xl: calc(var(--padding-l) * var(--scaling-factor));
|
||||||
|
--padding-xxl: calc(var(--padding-xl) * var(--scaling-factor));
|
||||||
|
|
||||||
|
--radius-default: calc(var(--font-size-default) / 3);
|
||||||
|
--radius-border: var(--font-size-default);
|
||||||
|
|
||||||
|
--transition-default: 150ms;
|
||||||
|
|
||||||
|
--page-max-width: 820px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
|
@ -58,43 +80,35 @@ body {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
}
|
color: var(--color-text);
|
||||||
|
font-size: var(--font-size-default);
|
||||||
.dot {
|
max-width: var(--page-max-width);
|
||||||
--size: 10px;
|
margin: 0 auto;
|
||||||
width: var(--size);
|
background-image: url("/img/desktop-background.svg");
|
||||||
height: var(--size);
|
background-position: center;
|
||||||
top: 5px;
|
|
||||||
right: 25%;
|
|
||||||
border-radius: 50%;
|
|
||||||
background-color: var(--color-accent);
|
|
||||||
display: inline-block;
|
|
||||||
margin-right: 0.5rem;
|
|
||||||
position: absolute;
|
|
||||||
box-shadow: var(--box-shadow-z2);
|
box-shadow: var(--box-shadow-z2);
|
||||||
scale: 0;
|
|
||||||
transition: var(--transition-default);
|
|
||||||
|
|
||||||
&.visible {
|
|
||||||
scale: 1;
|
|
||||||
animation: pulse 1s infinite;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes pulse {
|
h1, h2, h3 {
|
||||||
0% {
|
margin: var(--padding-xl) 0 var(--padding-default);
|
||||||
transform: scale(0.95);
|
text-wrap: balance;
|
||||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
|
hyphens: auto;
|
||||||
}
|
font-size: var(--font-size-xl);
|
||||||
70% {
|
}
|
||||||
transform: scale(1);
|
|
||||||
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
100% {
|
h2 {
|
||||||
transform: scale(0.95);
|
margin: var(--padding-l) 0 var(--padding-default);
|
||||||
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
|
font-size: var(--font-size-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
margin: var(--padding-default) 0 var(--padding-default);
|
||||||
|
font-size: var(--font-size-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
a:has(button) {
|
||||||
|
text-decoration: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
|
@ -103,20 +117,6 @@ body {
|
||||||
box-shadow: var(--box-shadow-z2);
|
box-shadow: var(--box-shadow-z2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
|
||||||
padding: 1rem;
|
|
||||||
min-height: 100dvh;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filter-bar {
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pc-wrapper {
|
|
||||||
gap: 1rem;
|
|
||||||
margin-bottom: 1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-col {
|
.flex-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
@ -128,7 +128,7 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-white {
|
.text-white {
|
||||||
color: var(--color-lightest);
|
color: var(--color-text-invert);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-main {
|
.bg-main {
|
||||||
|
@ -143,12 +143,16 @@ body {
|
||||||
background-color: var(--color-main-dark);
|
background-color: var(--color-main-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.bg-main-darkest {
|
||||||
|
background-color: var(--color-main-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
.bg-main-dark-hover:hover {
|
.bg-main-dark-hover:hover {
|
||||||
background-color: var(--color-main-dark);
|
background-color: var(--color-main-dark);
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-default {
|
.gap-default {
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-blue {
|
.bg-blue {
|
||||||
|
@ -160,35 +164,28 @@ body {
|
||||||
}
|
}
|
||||||
|
|
||||||
.padding {
|
.padding {
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
padding: var(--padding-default);
|
padding: var(--padding-default);
|
||||||
}
|
}
|
||||||
|
|
||||||
.padding-small {
|
.padding-small {
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
padding: var(--padding-small);
|
padding: var(--padding-s);
|
||||||
}
|
}
|
||||||
|
|
||||||
dialog {
|
.roboto-condensed {
|
||||||
top: 50%;
|
font-family: "Roboto Condensed", sans-serif;
|
||||||
left: 50%;
|
}
|
||||||
width: 100vw;
|
|
||||||
transform: translate(-50%, -50%);
|
.roboto {
|
||||||
border: none;
|
font-family: "Roboto", sans-serif;
|
||||||
border-radius: var(--radius-default);
|
}
|
||||||
|
|
||||||
font-size: 1rem;
|
.open-sans {
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
& header {
|
}
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
.grow {
|
||||||
}
|
flex-grow: 1;
|
||||||
|
height: var(--padding-s);
|
||||||
& footer {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::backdrop {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
}
|
}
|
|
@ -1,57 +1,88 @@
|
||||||
.Header {
|
.Header {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: var(--padding-default);
|
|
||||||
background-color: rgba(255 255 255 / .8);
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
box-shadow: var(--box-shadow-z2);
|
|
||||||
color: var(--color-darkest);
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
& strong {
|
|
||||||
font-size: 2em;
|
|
||||||
|
|
||||||
& span {
|
|
||||||
color: var(--color-main-dark);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
& input[type="checkbox"] {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
& input[type="checkbox"]:checked + nav {
|
|
||||||
translate: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
& nav,
|
|
||||||
& ul {
|
|
||||||
gap: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
& nav {
|
|
||||||
position: fixed;
|
|
||||||
padding: var(--padding-default);
|
|
||||||
translate: 100% 0;
|
|
||||||
width: 100vw;
|
|
||||||
right: 0;
|
|
||||||
top: 0;
|
|
||||||
height: 100dvh;
|
|
||||||
transition: 150ms ease-in-out;
|
|
||||||
background: var(--color-lightest);
|
|
||||||
font-size: 2em;
|
|
||||||
align-items: end;
|
|
||||||
z-index: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
& ul {
|
|
||||||
width: 100%;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
& li {
|
gap: var(--padding-default);
|
||||||
list-style: none;
|
padding: var(--padding-default);
|
||||||
|
|
||||||
|
&:not(.lp) {
|
||||||
|
background: var(--color-main-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.lp {
|
||||||
|
position: absolute;
|
||||||
|
background: rgba(0,0,0,.5);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
mask: linear-gradient(to top, transparent, black 30%);
|
||||||
|
width: 100%;
|
||||||
|
max-width: var(--page-max-width);
|
||||||
|
top: 0;
|
||||||
|
padding-bottom: var(--padding-xxl);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .logo {
|
||||||
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .burger-button {
|
||||||
|
all: unset;
|
||||||
|
color: var(--color-lightest);
|
||||||
|
}
|
||||||
|
|
||||||
|
& header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
width: 100%;
|
||||||
|
font-weight: bold;
|
||||||
|
|
||||||
|
& a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: var(--color-lightest);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .header-text {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--padding-default);
|
||||||
|
|
||||||
|
& > div {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .big {
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .small {
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.lp) {
|
||||||
|
&:after, &:before {
|
||||||
|
content: '';
|
||||||
|
display: block;
|
||||||
|
position: absolute;
|
||||||
|
bottom: calc(-1 * var(--radius-border));
|
||||||
|
background: var(--color-blue-darkest);
|
||||||
|
width: var(--radius-border);
|
||||||
|
height: var(--radius-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
right: 0;
|
||||||
|
mask: radial-gradient(var(--radius-border) at 0 100%,#0000 98%,#000);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:before {
|
||||||
|
left: 0;
|
||||||
|
mask: radial-gradient(var(--radius-border) at 100% 100%,#0000 98%,#000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
40
app/assets/styles/landingpage.css
Normal file
40
app/assets/styles/landingpage.css
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
.home-hero {
|
||||||
|
background-image: linear-gradient(rgba(0,0,0,0.4), rgba(0,0,0,0.6)), url("/img/hero-image.webp"); /* single color gradient for dark layer over image */
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center center;
|
||||||
|
background-size: cover;
|
||||||
|
color: var(--color-text-invert);
|
||||||
|
text-align: center;
|
||||||
|
height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
margin: var(--padding-xxl) auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
padding: var(--padding-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: var(--font-size-xxl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-text {
|
||||||
|
padding: var(--padding-xxl) var(--padding-default);
|
||||||
|
text-align: center;
|
||||||
|
|
||||||
|
& h3:has(+ .padding) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--padding-default);
|
||||||
|
}
|
||||||
|
|
69
app/assets/styles/navigation.css
Normal file
69
app/assets/styles/navigation.css
Normal file
|
@ -0,0 +1,69 @@
|
||||||
|
.Navigation {
|
||||||
|
position: fixed;
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
align-items: center;
|
||||||
|
z-index: 5000;
|
||||||
|
width: 100vw;
|
||||||
|
height: 100dvh;
|
||||||
|
right: 0;
|
||||||
|
top: 0;
|
||||||
|
transition: 150ms ease-in-out;
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
&.open {
|
||||||
|
pointer-events: all;
|
||||||
|
background: rgba(0, 0, 0, .5);
|
||||||
|
|
||||||
|
& nav {
|
||||||
|
translate: -1rem 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
background: var(--color-lightest);
|
||||||
|
align-items: end;
|
||||||
|
box-shadow: var(--box-shadow-z2);
|
||||||
|
padding: var(--padding-default);
|
||||||
|
height: calc(100% - var(--padding-xxl));
|
||||||
|
width: 70%;
|
||||||
|
translate: 100% 0;
|
||||||
|
transition: 150ms ease-in-out;
|
||||||
|
border-radius: var(--radius-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
& button {
|
||||||
|
justify-self: flex-end;
|
||||||
|
font-size: var(--font-size-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
& ul {
|
||||||
|
width: 100%;
|
||||||
|
align-items: flex-start;
|
||||||
|
font-size: var(--font-size-l);
|
||||||
|
gap: var(--padding-default);
|
||||||
|
|
||||||
|
& li {
|
||||||
|
list-style: none;
|
||||||
|
width: 100%;
|
||||||
|
|
||||||
|
& a {
|
||||||
|
text-decoration: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--padding-default);
|
||||||
|
color: var(--color-middle);
|
||||||
|
border-radius: var(--radius-default);
|
||||||
|
padding: var(--padding-xs) var(--padding-s);
|
||||||
|
transition: var(--transition-default);
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: var(--color-main-light);
|
||||||
|
color: var(--color-main-dark);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
84
app/assets/styles/page.css
Normal file
84
app/assets/styles/page.css
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
.page-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100vh;
|
||||||
|
|
||||||
|
& .page {
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
& .nuxt-page-wrapper {
|
||||||
|
height: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter-bar {
|
||||||
|
background: var(--color-lightest);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--padding-default);
|
||||||
|
|
||||||
|
& > button {
|
||||||
|
all: unset;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--color-main-darkest);
|
||||||
|
font-weight: bolder;
|
||||||
|
font-family: 'Roboto', sans-serif;
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
color: var(--color-main-darkest);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(.active) {
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-bar {
|
||||||
|
z-index: 100;
|
||||||
|
|
||||||
|
& p {
|
||||||
|
font-weight: 100;
|
||||||
|
color: var(--color-lightest);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--color-lightest);
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
h1:first-of-type,
|
||||||
|
h2:first-of-type,
|
||||||
|
h3:first-of-type,
|
||||||
|
p:first-of-type,
|
||||||
|
figure:first-of-type {
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-text {
|
||||||
|
padding: var(--padding-xl) var(--padding-default) 0;
|
||||||
|
color: var(--color-darkest);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-text {
|
||||||
|
padding: 0 var(--padding-default);
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
top: 40%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
||||||
|
.Legal {
|
||||||
|
padding: var(--padding-l) var(--padding-default);
|
||||||
|
color: var(--color-darkest);
|
||||||
|
|
||||||
|
ul {
|
||||||
|
padding-left: var(--padding-default);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,11 +1,8 @@
|
||||||
.PriceCard {
|
.PriceCard {
|
||||||
position: relative;
|
position: relative;
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: 150ms;
|
|
||||||
opacity: 1;
|
|
||||||
color: var(--color-darkest);
|
color: var(--color-darkest);
|
||||||
background: black;
|
border-bottom: 1px dashed var(--color-light);
|
||||||
|
|
||||||
.bottom {
|
.bottom {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
@ -18,7 +15,7 @@
|
||||||
& > * {
|
& > * {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
color: var(--color-lightest);
|
color: var(--color-lightest);
|
||||||
font-size: 2rem;
|
font-size: var(--font-size-xl);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|
||||||
|
@ -28,14 +25,14 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& .bg-edit {
|
& .bg-edit {
|
||||||
background: var(--color-gradient-main-dark);
|
background: var(--color-main-dark);
|
||||||
padding: 2rem;
|
padding: var(--padding-xl);
|
||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
& .bg-delete {
|
& .bg-delete {
|
||||||
background: var(--color-gradient-error-reverse);
|
background: var(--color-error);
|
||||||
padding: 2rem;
|
padding: var(--padding-xl);
|
||||||
text-align: right;
|
text-align: right;
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
}
|
}
|
||||||
|
@ -45,9 +42,8 @@
|
||||||
position: relative;
|
position: relative;
|
||||||
background: var(--color-lightest);
|
background: var(--color-lightest);
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
padding: 1rem;
|
padding: var(--padding-default);
|
||||||
border-radius: var(--radius-default);
|
|
||||||
|
|
||||||
&.animated {
|
&.animated {
|
||||||
transition: var(--transition-default);
|
transition: var(--transition-default);
|
||||||
|
@ -60,14 +56,14 @@
|
||||||
color: var(--color-darkest);
|
color: var(--color-darkest);
|
||||||
|
|
||||||
& .icon {
|
& .icon {
|
||||||
font-size: 1rem;
|
font-size: var(--font-size-default);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& .name-price {
|
& .name-price {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: .5rem;
|
gap: var(--padding-xs);
|
||||||
|
|
||||||
& > span:nth-child(1) {
|
& > span:nth-child(1) {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
@ -79,7 +75,7 @@
|
||||||
|
|
||||||
& > span:nth-child(2)::before {
|
& > span:nth-child(2)::before {
|
||||||
content: '•';
|
content: '•';
|
||||||
margin-right: .5rem;
|
margin-right: var(--padding-xs);
|
||||||
color: var(--color-middle);
|
color: var(--color-middle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,19 +84,19 @@
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
gap: 1rem;
|
gap: var(--padding-default);
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
& > .info {
|
& > .info {
|
||||||
flex-grow: 0;
|
flex-grow: 0;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .25rem;
|
gap: var(--padding-xxs);
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
|
||||||
& > .price {
|
& > .price {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: .5rem;
|
gap: var(--padding-xs);
|
||||||
|
|
||||||
& > .icon {
|
& > .icon {
|
||||||
color: var(--color-main-dark);
|
color: var(--color-main-dark);
|
||||||
|
@ -108,7 +104,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
& > .pro {
|
& > .pro {
|
||||||
font-size: .6rem;
|
font-size: var(--font-size-xs);
|
||||||
color: var(--color-middle);
|
color: var(--color-middle);
|
||||||
font-weight: lighter;
|
font-weight: lighter;
|
||||||
}
|
}
|
||||||
|
|
28
app/assets/styles/timelineCard.css
Normal file
28
app/assets/styles/timelineCard.css
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
.TimelineCard {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--padding-default);
|
||||||
|
border: 1px solid var(--color-light);
|
||||||
|
border-radius: var(--radius-default);
|
||||||
|
padding: var(--padding-xs);
|
||||||
|
|
||||||
|
& > .icon {
|
||||||
|
flex: 0 0 25%;
|
||||||
|
font-size: var(--font-size-xxl);
|
||||||
|
color: var(--color-main-dark);
|
||||||
|
}
|
||||||
|
|
||||||
|
& .text {
|
||||||
|
text-align: left;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .state {
|
||||||
|
--color: var(--color-darkest);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--padding-xxs);
|
||||||
|
margin-top: var(--padding-s);
|
||||||
|
color: var(--color);
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,11 +4,10 @@
|
||||||
background: var(--color-main-darkest);
|
background: var(--color-main-darkest);
|
||||||
position: sticky;
|
position: sticky;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
z-index: 100;
|
z-index: 1000;
|
||||||
box-shadow: var(--box-shadow-upper);
|
box-shadow: var(--box-shadow-upper);
|
||||||
|
|
||||||
& > .Button {
|
& > .Button {
|
||||||
--padding: 1rem;
|
font-size: var(--font-size-default);
|
||||||
font-size: 1rem;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -1,21 +1,21 @@
|
||||||
<template>
|
<template>
|
||||||
<dialog
|
<dialog
|
||||||
ref="dialog"
|
ref="dialog"
|
||||||
closedby="any"
|
closedby="none"
|
||||||
>
|
>
|
||||||
<form method="dialog">
|
<form method="dialog" class="wrapper" ref="wrapper">
|
||||||
<header class="flex-row padding">
|
<header class="flex-row">
|
||||||
Wirklich löschen?
|
Wirklich löschen?
|
||||||
<PpButton class="round text">
|
<PpButton class="round text">
|
||||||
<Icon name="uil:times" mode="svg" />
|
<Icon name="uil:times" mode="svg" />
|
||||||
</PpButton>
|
</PpButton>
|
||||||
</header>
|
</header>
|
||||||
<main>
|
<main>
|
||||||
<div class="padding flex-col">
|
<div class="flex-col">
|
||||||
<p>Bist du dir sicher, dass du diesen Eintrag löschen möchtest?</p>
|
<p>Bist du dir sicher, dass du diesen Eintrag löschen möchtest?</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer class="flex-row padding">
|
<footer class="flex-row">
|
||||||
<PpButton class="text">
|
<PpButton class="text">
|
||||||
<span>Abbrechen</span>
|
<span>Abbrechen</span>
|
||||||
</PpButton>
|
</PpButton>
|
||||||
|
@ -35,5 +35,11 @@ type Props = {
|
||||||
defineProps<Props>()
|
defineProps<Props>()
|
||||||
defineEmits(['delete'])
|
defineEmits(['delete'])
|
||||||
|
|
||||||
|
const dialog = useTemplateRef<HTMLDialogElement>('dialog')
|
||||||
|
const wrapper = useTemplateRef<HTMLElement>('wrapper')
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
onClickOutside(wrapper, () => dialog.value?.close())
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
|
@ -9,7 +9,6 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<small>© 2025 by webfussel</small>
|
|
||||||
<ul class="data-links">
|
<ul class="data-links">
|
||||||
<li v-for="dataLink in dataLinks">
|
<li v-for="dataLink in dataLinks">
|
||||||
<NuxtLink :to="dataLink.to">
|
<NuxtLink :to="dataLink.to">
|
||||||
|
@ -18,6 +17,8 @@
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
<p class="copy">ProPapier ist ein Gemeinschaftsprojekt von <NuxtLink to="https://webertoire.de" external>webertoire</NuxtLink> und <NuxtLink to="https://webfussel.de" external>webfussel</NuxtLink></p>
|
||||||
|
<p class="copy">© 2025 by webfussel, webertoire</p>
|
||||||
</footer>
|
</footer>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
25
app/components/Pp/Form/Search.vue
Executable file
25
app/components/Pp/Form/Search.vue
Executable file
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="Search">
|
||||||
|
<input
|
||||||
|
v-model="text"
|
||||||
|
:id="id"
|
||||||
|
:placeholder="label"
|
||||||
|
@blur="emit('search', text)"
|
||||||
|
/>
|
||||||
|
<PpButton class="search-button">
|
||||||
|
<Icon name="uil:search" mode="svg" />
|
||||||
|
</PpButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type Props = {
|
||||||
|
label : string
|
||||||
|
id : string
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>()
|
||||||
|
|
||||||
|
const emit = defineEmits(['search'])
|
||||||
|
const text = defineModel()
|
||||||
|
</script>
|
30
app/components/Pp/Form/TextField.vue
Executable file
30
app/components/Pp/Form/TextField.vue
Executable file
|
@ -0,0 +1,30 @@
|
||||||
|
<template>
|
||||||
|
<div class="TextField">
|
||||||
|
<div class="wrapper">
|
||||||
|
<input v-model="text" :type="type" :id="id" :placeholder="placeholder" @blur="emit('blur')" @input="emit('input')" :inputmode="mode" />
|
||||||
|
<label :for="id">
|
||||||
|
<Icon v-if="icon" class="icon" :name="icon" mode="svg" />
|
||||||
|
<span>{{ label }}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<span v-if="message">{{ message }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
type Props = {
|
||||||
|
type?: 'text' | 'number'
|
||||||
|
message?: string
|
||||||
|
icon?: string
|
||||||
|
label: string
|
||||||
|
placeholder: string
|
||||||
|
id: string
|
||||||
|
mode?: 'text' | 'email' | 'search' | 'tel' | 'url' | 'none' | 'numeric' | 'decimal'
|
||||||
|
};
|
||||||
|
|
||||||
|
const { type = "text", mode = "text" } = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits(['blur', 'input']);
|
||||||
|
|
||||||
|
const text = defineModel();
|
||||||
|
</script>
|
|
@ -1,43 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="Input">
|
|
||||||
<div class="input-wrapper flex-col">
|
|
||||||
<input
|
|
||||||
v-model="text"
|
|
||||||
:type="type"
|
|
||||||
:id="id"
|
|
||||||
:step="step"
|
|
||||||
:min="min"
|
|
||||||
:max="max"
|
|
||||||
:required="required"
|
|
||||||
placeholder=" "
|
|
||||||
@blur="emit('blur')"
|
|
||||||
/>
|
|
||||||
<label :for="id">{{ label }}</label>
|
|
||||||
</div>
|
|
||||||
<span v-if="message">{{ message }}</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
type Props = {
|
|
||||||
type ?: 'text' | 'number'
|
|
||||||
max ?: number
|
|
||||||
min ?: number
|
|
||||||
step ?: number
|
|
||||||
required ?: boolean
|
|
||||||
message ?: string
|
|
||||||
label : string
|
|
||||||
id : string
|
|
||||||
}
|
|
||||||
|
|
||||||
const {
|
|
||||||
type = 'text',
|
|
||||||
required = false,
|
|
||||||
step = 0.01,
|
|
||||||
min = 1,
|
|
||||||
} = defineProps<Props>()
|
|
||||||
|
|
||||||
const emit = defineEmits(['blur'])
|
|
||||||
|
|
||||||
const text = defineModel()
|
|
||||||
</script>
|
|
|
@ -1,31 +1,28 @@
|
||||||
<template>
|
<template>
|
||||||
<header class="Header">
|
<div class="Header" :class="[type]">
|
||||||
<NuxtLink to="/">
|
<header class="roboto-condensed">
|
||||||
<strong><span>Pro</span>Papier</strong>
|
<NuxtLink class="header-text" to="/">
|
||||||
|
<img class="logo" src="/img/propapier.svg" alt="ProPapier logo" />
|
||||||
|
<div>
|
||||||
|
<span class="big">ProPapier</span>
|
||||||
|
<span class="small">Vergleichen. Schnell. Unkompliziert.</span>
|
||||||
|
</div>
|
||||||
</NuxtLink>
|
</NuxtLink>
|
||||||
<label for="burger_nav_toggle" v-if="available">
|
<button class="burger-button" @click="open()">
|
||||||
<Icon name="solar:hamburger-menu-broken" size="2em" />
|
<Icon name="uil:bars" size="2em" mode="svg" />
|
||||||
</label>
|
</button>
|
||||||
<input type="checkbox" id="burger_nav_toggle" v-if="available" />
|
|
||||||
<nav class="flex-col" v-if="available">
|
|
||||||
<label for="burger_nav_toggle">
|
|
||||||
<Icon name="solar:close-circle-broken" />
|
|
||||||
</label>
|
|
||||||
<ul class="flex-col">
|
|
||||||
<li>Home</li>
|
|
||||||
<li>Übersicht</li>
|
|
||||||
</ul>
|
|
||||||
</nav>
|
|
||||||
</header>
|
</header>
|
||||||
|
<div id="subheader" />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
const available = false
|
type Props = {
|
||||||
</script>
|
type ?: 'lp'
|
||||||
|
}
|
||||||
|
|
||||||
<style scoped>
|
defineProps<Props>()
|
||||||
header a {
|
|
||||||
text-decoration: none;
|
const nav = useNavigation()
|
||||||
color: var(--color-black);
|
const open = () => { nav.showNavigation() }
|
||||||
}
|
</script>
|
||||||
</style>
|
|
||||||
|
|
41
app/components/Pp/Navigation.vue
Normal file
41
app/components/Pp/Navigation.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<section class="Navigation" :class="{ open }">
|
||||||
|
<nav>
|
||||||
|
<PpButton class="round text" @click="close()">
|
||||||
|
<Icon name="uil:times" mode="svg" />
|
||||||
|
</PpButton>
|
||||||
|
<ul class="flex-col">
|
||||||
|
<li v-for="page in pages">
|
||||||
|
<NuxtLink :to="page.route" @click="close()" active-class="active">
|
||||||
|
<Icon class="icon" :name="`uil:${page.icon}`" mode="svg" />
|
||||||
|
<span>{{ page.label }}</span>
|
||||||
|
</NuxtLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const nav = useNavigation()
|
||||||
|
const close = () => nav.hideNavigation()
|
||||||
|
const open = computed(() => nav.isNavigationVisible.value)
|
||||||
|
|
||||||
|
const pages = [
|
||||||
|
{
|
||||||
|
label: 'Home',
|
||||||
|
icon: 'home',
|
||||||
|
route: '/'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Schnellrechner',
|
||||||
|
icon: 'calculator',
|
||||||
|
route: '/rechner'
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// label: 'Über uns',
|
||||||
|
// icon: 'users-alt',
|
||||||
|
// route: '/about-us'
|
||||||
|
// }
|
||||||
|
]
|
||||||
|
</script>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<article class="PriceCard card">
|
<article class="PriceCard roboto-condensed" v-ripple="$device.isMobile ? { color: 'rgba(0, 0, 0, 0.1)' } : { duration: 0, scale: 0 }">
|
||||||
<div class="bottom">
|
<div class="bottom">
|
||||||
<div class="bg-edit">
|
<div class="bg-edit">
|
||||||
<Icon class="icon" name="uil:pen" mode="svg" />
|
<Icon class="icon" name="uil:pen" mode="svg" />
|
||||||
|
@ -13,6 +13,7 @@
|
||||||
class="top flex-col"
|
class="top flex-col"
|
||||||
:class="{ 'animated' : !isSwiping }"
|
:class="{ 'animated' : !isSwiping }"
|
||||||
:style="{ left }"
|
:style="{ left }"
|
||||||
|
@click="cardClick"
|
||||||
>
|
>
|
||||||
<header>
|
<header>
|
||||||
<div class="name-price">
|
<div class="name-price">
|
||||||
|
@ -20,10 +21,10 @@
|
||||||
<span>{{ intl.format(+replaceComma(card.price))}}</span>
|
<span>{{ intl.format(+replaceComma(card.price))}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="$device.isDesktop" class="flex-row gap-default">
|
<div v-if="$device.isDesktop" class="flex-row gap-default">
|
||||||
<PpButton class="icon-button" @click="update()">
|
<PpButton class="icon-button" @click="update">
|
||||||
<Icon class="icon" name="uil:pen" mode="svg" />
|
<Icon class="icon" name="uil:pen" mode="svg" />
|
||||||
</PpButton>
|
</PpButton>
|
||||||
<PpButton class="icon-button" @click="deleteCard()">
|
<PpButton class="icon-button" @click="deleteCard">
|
||||||
<Icon class="icon" name="uil:trash-alt" mode="svg" />
|
<Icon class="icon" name="uil:trash-alt" mode="svg" />
|
||||||
</PpButton>
|
</PpButton>
|
||||||
</div>
|
</div>
|
||||||
|
@ -33,23 +34,23 @@
|
||||||
<div class="info flex-col">
|
<div class="info flex-col">
|
||||||
<div class="price">
|
<div class="price">
|
||||||
<Icon class="icon" name="uil:toilet-paper" mode="svg" />
|
<Icon class="icon" name="uil:toilet-paper" mode="svg" />
|
||||||
<span class="value">{{ intl.format(card.ppr) }}</span>
|
<span class="value">{{ intl.format(ppr) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="pro">Pro 1 {{ card.roles ? `(${card.roles})` : '' }}</span>
|
<span class="pro">Pro 1 {{ card.roles ? `(${card.roles})` : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info flex-col">
|
<div class="info flex-col">
|
||||||
<div class="price">
|
<div class="price">
|
||||||
<Icon class="icon" name="uil:file-landscape" mode="svg" />
|
<Icon class="icon" name="uil:file-landscape" mode="svg" />
|
||||||
<span class="value">{{ intl.format(card.pps) }}</span>
|
<span class="value">{{ intl.format(pps) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="pro">Pro 100 {{ card.sheets ? `(${card.sheets})` : '' }}</span>
|
<span class="pro">Pro 100 {{ card.sheets ? `(${card.sheets})` : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="info flex-col">
|
<div class="info flex-col">
|
||||||
<div class="price">
|
<div class="price">
|
||||||
<Icon class="icon" name="uil:layer-group" mode="svg" />
|
<Icon class="icon" name="uil:layer-group" mode="svg" />
|
||||||
<span class="value">{{ intl.format(card.ppl) }}</span>
|
<span class="value">{{ intl.format(ppl) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="pro">Pro 100 {{ card.layers ? `(${card.layers})` : '' }}</span>
|
<span class="pro">Pro 1000 {{ card.layers ? `(${card.layers})` : '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
@ -57,16 +58,17 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Card } from '../../../shared/Card'
|
import type { PriceCard } from '../../../shared/PriceCard'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
deletable: boolean
|
deletable: boolean
|
||||||
card: Card
|
card: PriceCard
|
||||||
}
|
}
|
||||||
|
|
||||||
const { card } = defineProps<Props>()
|
const { card } = defineProps<Props>()
|
||||||
const emit = defineEmits(['remove', 'update'])
|
const emit = defineEmits(['remove', 'update'])
|
||||||
|
|
||||||
|
const { vibrate } = useVibrate()
|
||||||
const top = useTemplateRef('top')
|
const top = useTemplateRef('top')
|
||||||
const left = shallowRef<string>('0')
|
const left = shallowRef<string>('0')
|
||||||
|
|
||||||
|
@ -75,22 +77,40 @@ const intl = Intl.NumberFormat('de-DE', {
|
||||||
currency: 'EUR',
|
currency: 'EUR',
|
||||||
})
|
})
|
||||||
|
|
||||||
const { lengthX, isSwiping } = useSwipe(top, {
|
const { lengthX, direction, isSwiping } = useSwipe(top, {
|
||||||
passive: false,
|
passive: true,
|
||||||
threshold: 30,
|
threshold: 20,
|
||||||
onSwipe() {
|
onSwipe() {
|
||||||
if (lengthX.value > 50 || lengthX.value < -50) {
|
if (['down', 'up'].includes(direction.value)) return
|
||||||
left.value = `${-clamp(lengthX.value, -100, 100)}px`
|
left.value = `${-clamp(lengthX.value, 0, 100)}px`
|
||||||
}
|
|
||||||
},
|
},
|
||||||
onSwipeEnd() {
|
onSwipeEnd() {
|
||||||
if (lengthX.value < -50) update()
|
if (['down', 'up'].includes(direction.value)) return
|
||||||
if (lengthX.value > 50) deleteCard()
|
if (lengthX.value > 50) {
|
||||||
|
vibrate(100)
|
||||||
|
deleteCard()
|
||||||
|
}
|
||||||
left.value = '0'
|
left.value = '0'
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const update = () => emit('update')
|
const priceClean = computed<number>(() => +replaceComma(card.price))
|
||||||
|
const ppr = computed(() => priceClean.value / +card.roles)
|
||||||
|
const pps = computed(() => (ppr.value / +card.sheets) * 100)
|
||||||
|
const ppl = computed(() => (pps.value / +card.layers) * 10)
|
||||||
|
|
||||||
|
const { isDesktop } = useDevice()
|
||||||
|
|
||||||
|
const cardClick = () => {
|
||||||
|
if (isDesktop) return
|
||||||
|
emit('update')
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = () => emit('update')
|
||||||
const deleteCard = () => emit('remove')
|
const deleteCard = () => emit('remove')
|
||||||
|
|
||||||
|
defineExpose({
|
||||||
|
ppr, pps, ppl, uuid : card.uuid,
|
||||||
|
})
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,96 +1,134 @@
|
||||||
<template>
|
<template>
|
||||||
<dialog
|
<dialog ref="dialog" closedby="none">
|
||||||
ref="dialog"
|
<div class="wrapper" ref="wrapper">
|
||||||
closedby="any"
|
|
||||||
>
|
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<header class="flex-row padding">
|
<header class="flex-row">
|
||||||
{{ cardLabel }}
|
{{ currentCardIndex > -1 ? 'Bearbeiten' : 'Neues hinzufügen' }}
|
||||||
<PpButton class="round text">
|
<PpButton class="round text">
|
||||||
<Icon name="uil:times" mode="svg" />
|
<Icon name="uil:times" mode="svg" />
|
||||||
</PpButton>
|
</PpButton>
|
||||||
</header>
|
</header>
|
||||||
</form>
|
</form>
|
||||||
<main v-if="currentCard">
|
<main v-if="currentCard">
|
||||||
<div class="padding flex-col">
|
<div class="flex-col gap-default">
|
||||||
<div class="flex-row gap-default">
|
<div class="flex-row gap-default">
|
||||||
<PpFormInput
|
<PpFormTextField
|
||||||
v-model="currentCard.name"
|
v-model="currentCard.name"
|
||||||
id="card_name"
|
id="card_name"
|
||||||
label="Name"
|
label="Name"
|
||||||
:class="{'error': !validFields.name }"
|
icon="uil:pricetag-alt"
|
||||||
|
:placeholder="randomName"
|
||||||
|
:class="{ error: !validFields.name }"
|
||||||
:message="!validFields.name ? 'Feld darf nicht leer sein.' : ''"
|
:message="!validFields.name ? 'Feld darf nicht leer sein.' : ''"
|
||||||
|
@input="validFields.name = true"
|
||||||
/>
|
/>
|
||||||
<PpFormInput
|
<PpFormTextField
|
||||||
v-model="currentCard.price"
|
v-model="currentCard.price"
|
||||||
id="card_price"
|
id="card_price"
|
||||||
label="Preis"
|
label="Preis"
|
||||||
:class="{'error': !validFields.price }"
|
placeholder="2,49"
|
||||||
|
icon="uil:euro"
|
||||||
|
mode="decimal"
|
||||||
|
:class="{ error: !validFields.price }"
|
||||||
:message="!validFields.price ? 'Muss eine Zahl sein.' : ''"
|
:message="!validFields.price ? 'Muss eine Zahl sein.' : ''"
|
||||||
|
@input="validFields.price = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-row gap-default">
|
<div class="flex-row gap-default">
|
||||||
<PpFormInput
|
<PpFormTextField
|
||||||
v-model="currentCard.roles"
|
v-model="currentCard.roles"
|
||||||
id="card_roles"
|
id="card_roles"
|
||||||
label="Rollen"
|
label="Rollen"
|
||||||
:class="{'error': !validFields.roles }"
|
placeholder="8"
|
||||||
|
icon="uil:toilet-paper"
|
||||||
|
mode="decimal"
|
||||||
|
:class="{ error: !validFields.roles }"
|
||||||
:message="!validFields.roles ? 'Muss eine Ganzzahl sein.' : ''"
|
:message="!validFields.roles ? 'Muss eine Ganzzahl sein.' : ''"
|
||||||
|
@input="validFields.roles = true"
|
||||||
/>
|
/>
|
||||||
<PpFormInput
|
<PpFormTextField
|
||||||
v-model="currentCard.sheets"
|
v-model="currentCard.sheets"
|
||||||
id="card_sheets"
|
id="card_sheets"
|
||||||
label="Blätter"
|
label="Blatt"
|
||||||
:class="{'error': !validFields.sheets }"
|
placeholder="150"
|
||||||
|
icon="uil:file-landscape"
|
||||||
|
mode="decimal"
|
||||||
|
:class="{ error: !validFields.sheets }"
|
||||||
:message="!validFields.sheets ? 'Muss eine Ganzzahl sein.' : ''"
|
:message="!validFields.sheets ? 'Muss eine Ganzzahl sein.' : ''"
|
||||||
|
@input="validFields.sheets = true"
|
||||||
/>
|
/>
|
||||||
<PpFormInput
|
<PpFormTextField
|
||||||
v-model="currentCard.layers"
|
v-model="currentCard.layers"
|
||||||
id="card_layers"
|
id="card_layers"
|
||||||
label="Lagen"
|
label="Lagen"
|
||||||
:class="{'error': !validFields.layers }"
|
placeholder="3"
|
||||||
|
icon="uil:layer-group"
|
||||||
|
mode="decimal"
|
||||||
|
:class="{ error: !validFields.layers }"
|
||||||
:message="!validFields.layers ? 'Muss eine Ganzzahl sein.' : ''"
|
:message="!validFields.layers ? 'Muss eine Ganzzahl sein.' : ''"
|
||||||
|
@input="validFields.layers = true"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<footer class="flex-row padding">
|
<footer class="flex-row">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
<PpButton class="danger text">
|
<PpButton class="danger text">
|
||||||
<span>Abbrechen</span>
|
<span>Abbrechen</span>
|
||||||
</PpButton>
|
</PpButton>
|
||||||
</form>
|
</form>
|
||||||
<PpButton class="raised" @click="validate">
|
<PpButton class="raised" @click="validate">
|
||||||
<span>{{ cardLabel }}</span>
|
<span>{{ currentCardIndex > -1 ? 'Übernehmen' : 'Hinzufügen' }}</span>
|
||||||
</PpButton>
|
</PpButton>
|
||||||
</footer>
|
</footer>
|
||||||
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Card } from '../../../shared/Card'
|
import type { PriceCard } from '../../../shared/PriceCard'
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
currentCardIndex : number
|
currentCardIndex: number
|
||||||
currentCard ?: Card
|
currentCard?: PriceCard
|
||||||
}
|
}
|
||||||
|
|
||||||
const { currentCardIndex, currentCard } = defineProps<Props>()
|
const { currentCardIndex, currentCard } = defineProps<Props>()
|
||||||
const emit = defineEmits(['update'])
|
const emit = defineEmits(['update'])
|
||||||
|
|
||||||
const dialog = useTemplateRef<HTMLDialogElement>('dialog')
|
const dialog = useTemplateRef<HTMLDialogElement>('dialog')
|
||||||
const cardLabel = computed(() => currentCardIndex > -1 ? 'Bearbeiten' : 'Hinzufügen')
|
const wrapper = useTemplateRef<HTMLElement>('wrapper')
|
||||||
|
|
||||||
|
const market = [
|
||||||
|
'Lotl',
|
||||||
|
'Olda',
|
||||||
|
'Bäwä',
|
||||||
|
'Brutto',
|
||||||
|
]
|
||||||
|
|
||||||
|
const product = [
|
||||||
|
'Weichelig',
|
||||||
|
'Sau Rauh',
|
||||||
|
'Bissl Sanft',
|
||||||
|
'Ganz ok',
|
||||||
|
'Flauschi'
|
||||||
|
]
|
||||||
|
|
||||||
|
const generateRandomName = () => `${market[Math.floor(Math.random() * market.length)]} ${product[Math.floor(Math.random() * product.length)]}`
|
||||||
|
|
||||||
|
const randomName = useState('randomName', () => generateRandomName())
|
||||||
|
|
||||||
const checkPrice = () => {
|
const checkPrice = () => {
|
||||||
if (!currentCard) { return false }
|
if (!currentCard) return false
|
||||||
if (currentCard.price.length === 0) { return false }
|
if (currentCard.price.length === 0) return false
|
||||||
|
|
||||||
const price = +replaceComma(currentCard.price)
|
const price = +replaceComma(currentCard.price)
|
||||||
return !isNaN(price)
|
return !isNaN(price)
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkIfInteger = (toBeNumber : string) => {
|
const checkIfInteger = (toBeNumber: string) => {
|
||||||
if (toBeNumber.length === 0) { return false }
|
if (toBeNumber.length === 0) return false
|
||||||
if (toBeNumber.includes(',') || toBeNumber.includes('.')) { return false }
|
if (toBeNumber.includes(',') || toBeNumber.includes(',')) return false
|
||||||
return !isNaN(+toBeNumber)
|
return !isNaN(+toBeNumber)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -103,7 +141,7 @@ const validFields = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
const validate = () => {
|
const validate = () => {
|
||||||
if (!currentCard) { return }
|
if (!currentCard) return
|
||||||
|
|
||||||
validFields.name = currentCard.name.length > 0
|
validFields.name = currentCard.name.length > 0
|
||||||
validFields.price = checkPrice()
|
validFields.price = checkPrice()
|
||||||
|
@ -111,7 +149,7 @@ const validate = () => {
|
||||||
validFields.sheets = checkIfInteger(currentCard.sheets)
|
validFields.sheets = checkIfInteger(currentCard.sheets)
|
||||||
validFields.layers = checkIfInteger(currentCard.layers)
|
validFields.layers = checkIfInteger(currentCard.layers)
|
||||||
|
|
||||||
if (Object.values(validFields).every(value => value)) {
|
if (Object.values(validFields).every((value) => value)) {
|
||||||
emit('update')
|
emit('update')
|
||||||
dialog.value?.close()
|
dialog.value?.close()
|
||||||
}
|
}
|
||||||
|
@ -124,6 +162,9 @@ onMounted(() => {
|
||||||
validFields.roles = true
|
validFields.roles = true
|
||||||
validFields.sheets = true
|
validFields.sheets = true
|
||||||
validFields.layers = true
|
validFields.layers = true
|
||||||
|
randomName.value = generateRandomName()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
onClickOutside(wrapper, () => dialog.value?.close())
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
51
app/components/Pp/TimelineCard.vue
Normal file
51
app/components/Pp/TimelineCard.vue
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
<template>
|
||||||
|
<article class="TimelineCard">
|
||||||
|
<Icon class="icon" :name="icon" mode="svg" />
|
||||||
|
<div class="text">
|
||||||
|
<strong>{{ title }}</strong>
|
||||||
|
<p>{{ description }}</p>
|
||||||
|
<div class="state" :style="{
|
||||||
|
'--color': stateColor,
|
||||||
|
}">
|
||||||
|
<Icon :name="stateIcon" mode="svg" />
|
||||||
|
<span>{{ stateMessage }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { TimelineCard, TimelineState } from '../../../shared/TimelineCard'
|
||||||
|
|
||||||
|
|
||||||
|
const { state } = defineProps<TimelineCard>()
|
||||||
|
|
||||||
|
const icons : Record<TimelineState, string> = {
|
||||||
|
planned: 'uil:clock',
|
||||||
|
inProgress: 'uil:cog',
|
||||||
|
done: 'uil:check-circle'
|
||||||
|
}
|
||||||
|
|
||||||
|
const colors : Record<TimelineState, string> = {
|
||||||
|
planned: 'var(--color-darkest)',
|
||||||
|
inProgress: 'var(--color-main-dark)',
|
||||||
|
done: 'var(--color-accent-darkest)',
|
||||||
|
}
|
||||||
|
|
||||||
|
const stateColor = computed(() => colors[state.value])
|
||||||
|
const stateIcon = computed(() => icons[state.value])
|
||||||
|
const stateMessage = computed(() => {
|
||||||
|
switch (state.value) {
|
||||||
|
case 'planned':
|
||||||
|
let planned = 'Geplant'
|
||||||
|
if (state.message) planned += ` für ${state.message}`
|
||||||
|
return planned
|
||||||
|
case 'inProgress':
|
||||||
|
return 'In Bearbeitung'
|
||||||
|
case 'done':
|
||||||
|
let done = 'Abgeschlossen'
|
||||||
|
if (state.message) done += ` am ${state.message}`
|
||||||
|
return done
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
24
app/composables/navigation.ts
Normal file
24
app/composables/navigation.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const isNavigationVisible = ref(false)
|
||||||
|
|
||||||
|
export const useNavigation = () => {
|
||||||
|
const toggleNavigation = () => {
|
||||||
|
isNavigationVisible.value = !isNavigationVisible.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const showNavigation = () => {
|
||||||
|
isNavigationVisible.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const hideNavigation = () => {
|
||||||
|
isNavigationVisible.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
isNavigationVisible,
|
||||||
|
toggleNavigation,
|
||||||
|
showNavigation,
|
||||||
|
hideNavigation
|
||||||
|
}
|
||||||
|
}
|
11
app/layouts/default.vue
Executable file
11
app/layouts/default.vue
Executable file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<PpHeader />
|
||||||
|
<div class="page">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
<PpFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
11
app/layouts/landingpage.vue
Normal file
11
app/layouts/landingpage.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div class="page-wrapper">
|
||||||
|
<PpHeader type="lp" />
|
||||||
|
<div class="page">
|
||||||
|
<NuxtPage />
|
||||||
|
</div>
|
||||||
|
<PpFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
</script>
|
|
@ -1,6 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<section class="Imp flex-col gap-default content full">
|
<section class="Legal flex-col gap-default content full">
|
||||||
|
<h1>Impressum</h1>
|
||||||
<div>
|
<div>
|
||||||
<p>
|
<p>
|
||||||
Fiona Lena Urban<br/>
|
Fiona Lena Urban<br/>
|
||||||
|
|
248
app/pages/index.vue
Executable file → Normal file
248
app/pages/index.vue
Executable file → Normal file
|
@ -1,185 +1,97 @@
|
||||||
<template>
|
<template>
|
||||||
<PpDeleteDialog
|
<section class="Home flex-col content full">
|
||||||
ref="deleteModal"
|
<div class="home-hero">
|
||||||
:current-card-index="currentCardIndex"
|
<div class="text">
|
||||||
@delete="removeCard(currentCardIndex)"
|
<h1>
|
||||||
/>
|
Du zahlst zuviel für's Papier?
|
||||||
<PpPriceCardDialog
|
</h1>
|
||||||
ref="modal"
|
<NuxtLink to="/rechner">
|
||||||
:current-card="currentCard"
|
<PpButton class="cta">Preise vergleichen</PpButton>
|
||||||
:current-card-index="currentCardIndex"
|
</NuxtLink>
|
||||||
@update="updateCard()"
|
<h2>
|
||||||
/>
|
Mit ProPapier Preise vergleichen und sparen.
|
||||||
<section class="content flex-col">
|
</h2>
|
||||||
<aside class="filter-bar">
|
</div>
|
||||||
<PpButtonGroup
|
</div>
|
||||||
:buttons="filterButtons"
|
<div class="home-text padding">
|
||||||
@click="sort"
|
<p>
|
||||||
/>
|
Mit <strong>ProPapier</strong> vergleichst du schnell & unkompliziert Preise für Klopapier und sparst so bares Geld.
|
||||||
</aside>
|
</p>
|
||||||
<div class="pc-wrapper flex-col" role="list">
|
</div>
|
||||||
<PpPriceCard
|
<div class="home-text padding">
|
||||||
v-for="(card, index) in cards"
|
<h3>
|
||||||
:key="card.uuid"
|
Wir haben noch viel vor!
|
||||||
:deletable="cards.length > 1"
|
</h3>
|
||||||
:card="card"
|
<p class="padding">
|
||||||
@update="openModal(false, index)"
|
Für ProPapier sind über die nächste Zeit noch einige weitere Features geplant.
|
||||||
@remove="openDeleteModal()"
|
</p>
|
||||||
|
<div class="timeline">
|
||||||
|
<PpTimelineCard
|
||||||
|
v-for="card in timeline"
|
||||||
|
v-bind="card"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<PpToolbar>
|
|
||||||
<PpButton class="mini-button text-white transparent" @click="sort(currentSort)">
|
|
||||||
<Icon class="icon" name="uil:refresh" mode="svg" />
|
|
||||||
<span>Neu sortieren</span>
|
|
||||||
<span
|
|
||||||
class="dot"
|
|
||||||
:class="{ visible : isDirty}"
|
|
||||||
aria-hidden="true"
|
|
||||||
/>
|
|
||||||
</PpButton>
|
|
||||||
<PpButton class="mini-button text-white transparent" @click="openModal(true, -1)">
|
|
||||||
<Icon class="icon" name="uil:plus" mode="svg" />
|
|
||||||
<span>Hinzufügen</span>
|
|
||||||
</PpButton>
|
|
||||||
</PpToolbar>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import type { Card } from '../../shared/Card'
|
import type { TimelineCard } from '../../shared/TimelineCard'
|
||||||
import type { Button } from '../../shared/ButtonGroup'
|
|
||||||
import { PpPriceCardDialog, PpDeleteDialog } from '#components'
|
|
||||||
|
|
||||||
const currentSort = ref(0)
|
definePageMeta({
|
||||||
const isDirty = ref(false)
|
layout: 'landingpage'
|
||||||
const currentCard = ref<Card>()
|
|
||||||
const currentCardIndex = ref<number>(-1)
|
|
||||||
const modal = useTemplateRef<typeof PpPriceCardDialog>('modal')
|
|
||||||
const deleteModal = useTemplateRef<typeof PpDeleteDialog>('deleteModal')
|
|
||||||
|
|
||||||
const createCard = (uuid : string) : Card => ({
|
|
||||||
uuid,
|
|
||||||
name: '',
|
|
||||||
price: '',
|
|
||||||
roles: '',
|
|
||||||
sheets: '',
|
|
||||||
layers: '',
|
|
||||||
ppr: 0,
|
|
||||||
pps: 0,
|
|
||||||
ppl: 0,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const cards = useState<Card[]>('cards', () => [
|
const timeline : TimelineCard[] = [
|
||||||
createCard(crypto.randomUUID()),
|
|
||||||
])
|
|
||||||
|
|
||||||
const addCard = (card : Card) => {
|
|
||||||
const price = calculate(card)
|
|
||||||
cards.value.unshift({ ...card, ...price })
|
|
||||||
isDirty.value = true
|
|
||||||
updateLocalStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeCard = (index : number) => {
|
|
||||||
cards.value.splice(index, 1)
|
|
||||||
isDirty.value = true
|
|
||||||
updateLocalStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateCard = () => {
|
|
||||||
if (currentCardIndex.value === -1) {
|
|
||||||
addCard(currentCard.value!)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const price = calculate(currentCard.value!)
|
|
||||||
const newCard = { ...currentCard.value!, ...price }
|
|
||||||
cards.value.splice(currentCardIndex.value, 1, newCard)
|
|
||||||
isDirty.value = true
|
|
||||||
updateLocalStorage()
|
|
||||||
}
|
|
||||||
|
|
||||||
const openModal = (createNew : boolean, index : number) => {
|
|
||||||
if (createNew) {
|
|
||||||
currentCardIndex.value = -1
|
|
||||||
currentCard.value = createCard(crypto.randomUUID())
|
|
||||||
|
|
||||||
modal.value?.$el.showModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
currentCardIndex.value = index
|
|
||||||
currentCard.value = { ...cards.value[index]! }
|
|
||||||
|
|
||||||
modal.value?.$el.showModal()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const openDeleteModal = () => {
|
|
||||||
deleteModal.value?.$el.showModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const updateLocalStorage = () => {
|
|
||||||
localStorage.setItem('cards', JSON.stringify(cards.value.map(card => {
|
|
||||||
const { uuid, name, price, roles, sheets, layers } = card
|
|
||||||
return { uuid, name, price, roles, sheets, layers }
|
|
||||||
})))
|
|
||||||
localStorage.setItem('sort', JSON.stringify(currentSort.value))
|
|
||||||
}
|
|
||||||
|
|
||||||
const filterButtons = ref<Button[]>([
|
|
||||||
{
|
{
|
||||||
label: 'Rollen',
|
icon: 'uil:chart-bar',
|
||||||
icon: 'uil:toilet-paper',
|
title: 'Mehr Vergleiche',
|
||||||
|
description: 'Zusätzliche Kategorien für Taschentücher und Küchenrolle',
|
||||||
|
state: {
|
||||||
|
value: 'inProgress',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Blatt',
|
icon: 'uil:cloud-database-tree',
|
||||||
icon: 'uil:file-landscape',
|
title: 'Datenbank',
|
||||||
|
description: 'Eine von der Community gestützte Datenbank mit Preisen für alle Produkte',
|
||||||
|
state: {
|
||||||
|
value: 'planned',
|
||||||
|
message: '2025',
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: 'Lagen',
|
icon: 'uil:qrcode-scan',
|
||||||
icon: 'uil:layer-group',
|
title: 'Barcode Scan',
|
||||||
},
|
description: 'Ganz einfach Barcode Scannen und Produkt direkt zum Rechner hinzufügen',
|
||||||
])
|
state: {
|
||||||
|
value: 'planned',
|
||||||
const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
|
message: '2025',
|
||||||
cards.value.sort((a : Card, b : Card) => a[key] - b[key])
|
|
||||||
}
|
|
||||||
|
|
||||||
const sort = (index : number) => {
|
|
||||||
currentSort.value = index
|
|
||||||
filterButtons.value.forEach(button => { button.active = false })
|
|
||||||
filterButtons.value[index]!.active = true
|
|
||||||
|
|
||||||
switch (index) {
|
|
||||||
case 0:
|
|
||||||
sortBy('ppr')
|
|
||||||
break
|
|
||||||
case 1:
|
|
||||||
sortBy('pps')
|
|
||||||
break
|
|
||||||
case 2:
|
|
||||||
sortBy('ppl')
|
|
||||||
break
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
updateLocalStorage()
|
{
|
||||||
isDirty.value = false
|
icon: 'uil:user',
|
||||||
}
|
title: 'Optionale Accounts',
|
||||||
|
description: 'Zur Synchronisierung auf mehreren Geräten',
|
||||||
const calculate = (card : Card) => {
|
state: {
|
||||||
const ppr = +replaceComma(card.price) / +card.roles
|
value: 'planned',
|
||||||
const pps = (ppr / +card.sheets) * 100
|
}
|
||||||
const ppl = (pps / +card.layers)
|
},
|
||||||
|
{
|
||||||
return { ppr, pps, ppl }
|
icon: 'uil:cog',
|
||||||
}
|
title: 'Personalisierung',
|
||||||
|
description: 'Persönliche Präferenzen zur Wortwahl, Standardsortierung und mehr',
|
||||||
onMounted(() => {
|
state: {
|
||||||
const cardsFromStorage = JSON.parse(localStorage.getItem('cards') ?? '[]').map((card : Card) => ({ ...card, ...calculate(card) }))
|
value: 'planned',
|
||||||
cards.value = cardsFromStorage.length !== 0 ? cardsFromStorage : cards.value
|
}
|
||||||
const sortFromStorage = +JSON.parse(localStorage.getItem('sort') ?? '0')
|
},
|
||||||
sort(sortFromStorage)
|
{
|
||||||
filterButtons.value[sortFromStorage]!.active = true
|
icon: 'uil:vector-square',
|
||||||
})
|
title: 'm² Preise',
|
||||||
|
description: 'Quadratmeterpreise für noch genauere Vergleiche',
|
||||||
|
state: {
|
||||||
|
value: 'planned',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
]
|
||||||
</script>
|
</script>
|
|
@ -1,17 +1,53 @@
|
||||||
<template>
|
<template>
|
||||||
<section class="flex-col gap-default content full">
|
<section class="Legal flex-col gap-default content full">
|
||||||
<h3>
|
<h2>1) Einleitung und Kontaktdaten des Verantwortlichen</h2>
|
||||||
Datenschutzerklärung
|
<p><b>1.1</b> Wir freuen uns, dass du unsere Website besuchst und bedanken uns für dein Interesse. Im Folgenden informieren wir dich über den Umgang mit deinen personenbezogenen Daten bei der Nutzung unserer Website. Personenbezogene Daten sind hierbei alle Daten, mit denen du persönlich identifiziert werden kannst.</p>
|
||||||
</h3>
|
<p><b>1.2</b> Verantwortlicher für die Datenverarbeitung auf dieser Website im Sinne der Datenschutz-Grundverordnung (DSGVO) ist Fiona Lena Urban, Teichäckerweg 39, 76297 Stutensee , Tel.: 017631640961, E-Mail: fiona@webfussel.de. Der für die Verarbeitung von personenbezogenen Daten Verantwortliche ist diejenige natürliche oder juristische Person, die allein oder gemeinsam mit anderen über die Zwecke und Mittel der Verarbeitung von personenbezogenen Daten entscheidet.</p>
|
||||||
<p>
|
<h2>2) Datenerfassung beim Besuch unserer Website</h2>
|
||||||
Wir senden keinerlei Daten an Drittanbieter.
|
<p><b>2.1</b> Bei der bloß informatorischen Nutzung unserer Website, also wenn du dich nicht registrierst oder uns anderweitig Informationen übermittelst, erheben wir nur solche Daten, die dein Browser an den Seitenserver übermittelt (sog. „Server-Logfiles“). Wenn du unsere Website aufrufst, erheben wir die folgenden Daten, die für uns technisch erforderlich sind, um dir die Website anzuzeigen:</p>
|
||||||
</p>
|
<ul>
|
||||||
<p>
|
<li>Unsere besuchte Website</li>
|
||||||
Wir verwerten keinerlei Daten.
|
<li>Datum und Uhrzeit zum Zeitpunkt des Zugriffs</li>
|
||||||
</p>
|
<li>Menge der gesendeten Daten in Byte</li>
|
||||||
<p>
|
<li>Quelle/Verweis, von welchem du auf die Seite gelangtest</li>
|
||||||
Alle persistierten Daten befinden sich ausschließlich auf Ihrem Endgerät im sogenannten "localStorage" und werden ausschließlich auf Ihrem Gerät verarbeitet.
|
<li>Verwendeter Browser</li>
|
||||||
</p>
|
<li>Verwendetes Betriebssystem</li>
|
||||||
|
<li>Verwendete IP-Adresse (ggf.: in anonymisierter Form)</li>
|
||||||
|
</ul>
|
||||||
|
<p>Die Verarbeitung erfolgt gemäß Art. 6 Abs. 1 lit. f DSGVO auf Basis unseres berechtigten Interesses an der Verbesserung der Stabilität und Funktionalität unserer Website. Eine Weitergabe oder anderweitige Verwendung der Daten findet nicht statt. Wir behalten uns allerdings vor, die Server-Logfiles nachträglich zu überprüfen, sollten konkrete Anhaltspunkte auf eine rechtswidrige Nutzung hinweisen.</p>
|
||||||
|
<p><b>2.2</b> Diese Website nutzt aus Sicherheitsgründen und zum Schutz der Übertragung personenbezogener Daten und anderer vertraulicher Inhalte (z.B. Bestellungen oder Anfragen an uns) eine SSL-bzw. TLS-Verschlüsselung. Du kannst eine verschlüsselte Verbindung an der Zeichenfolge „https://“ und dem Schloss-Symbol in deiner Browserzeile erkennen.</p>
|
||||||
|
<h2>3) Kontaktaufnahme</h2>
|
||||||
|
<p>Im Rahmen der Kontaktaufnahme mit uns (z.B. per Kontaktformular oder E-Mail) werden – ausschließlich zum Zweck der Bearbeitung und Beantwortung deines Anliegens und nur im dafür erforderlichen Umfang – personenbezogene Daten verarbeitet.</p>
|
||||||
|
<p>Rechtsgrundlage für die Verarbeitung dieser Daten ist unser berechtigtes Interesse an der Beantwortung deines Anliegens gemäß Art. 6 Abs. 1 lit. f DSGVO. Zielt deine Kontaktierung auf einen Vertrag ab, so ist zusätzliche Rechtsgrundlage für die Verarbeitung Art. 6 Abs. 1 lit. b DSGVO. Deine Daten werden gelöscht, wenn sich aus den Umständen entnehmen lässt, dass der betroffene Sachverhalt abschließend geklärt ist und sofern keine gesetzlichen Aufbewahrungspflichten entgegenstehen.</p>
|
||||||
|
<h2>4) Webanalysedienste</h2>
|
||||||
|
<p>Plausible</p>
|
||||||
|
<p>Diese Website nutzt „Plausible“, ein Webanalyse-Tool der Firma Plausible Insights OÜ Västriku tn 2, 50403, Tartu, Estland.</p>
|
||||||
|
<p>Es werden damit Interaktionen von zufällig ausgewählten, einzelnen Besuchern mit der Internetseite anonymisiert aufgezeichnet. So entsteht ein Protokoll von z.B. Mausbewegungen und -Klicks mit dem Ziel, Verbesserungsmöglichkeiten der jeweiligen Internetseite aufzuzeigen. Zu keinem Zeitpunkt werden personenbezogene Daten erhoben oder verarbeitet. Plausible erhebt bei der Nutzung dieser Internetseite ausschließlich nicht personenbezogene Daten wie Informationen zum Browser und zum User Agent. Diese werden in nicht personenbeziehbarer Form gespeichert und zu statistischen Zwecken ausgewertet. Eine Löschung findet statt, sobald die Daten für unsere Auswertungszwecke nicht mehr benötigt werden.</p>
|
||||||
|
<p>Sofern im Einzelfall doch personenbezogene Daten verarbeitet werden, erfolgt die Verarbeitung auf Basis unseres berechtigten Interesses an der statistischen Auswertung des Nutzungsverhaltens zu Optimierungszwecken gemäß Art. 6 Abs. 1 lit. f DSGVO.</p>
|
||||||
|
<h2>5) Rechte des Betroffenen</h2>
|
||||||
|
<p><b>5.1</b> Das geltende Datenschutzrecht gewährt dir gegenüber uns als Verantwortlichen hinsichtlich der Verarbeitung deiner personenbezogenen Daten die nachstehenden Betroffenenrechte (Auskunfts- und Interventionsrechte), wobei für die jeweiligen Ausübungsvoraussetzungen auf die angeführte Rechtsgrundlage verwiesen wird:</p>
|
||||||
|
<ul>
|
||||||
|
<li>Auskunftsrecht gemäß Art. 15 DSGVO;</li>
|
||||||
|
<li>Recht auf Berichtigung gemäß Art. 16 DSGVO;</li>
|
||||||
|
<li>Recht auf Löschung gemäß Art. 17 DSGVO;</li>
|
||||||
|
<li>Recht auf Einschränkung der Verarbeitung gemäß Art. 18 DSGVO;</li>
|
||||||
|
<li>Recht auf Unterrichtung gemäß Art. 19 DSGVO;</li>
|
||||||
|
<li>Recht auf Datenübertragbarkeit gemäß Art. 20 DSGVO;</li>
|
||||||
|
<li>Recht auf Widerruf erteilter Einwilligungen gemäß Art. 7 Abs. 3 DSGVO;</li>
|
||||||
|
<li>Recht auf Beschwerde gemäß Art. 77 DSGVO.</li>
|
||||||
|
</ul>
|
||||||
|
<p><b>5.2</b> WIDERSPRUCHSRECHT</p>
|
||||||
|
<p>WENN WIR IM RAHMEN EINER INTERESSENABWÄGUNG DEINE PERSONENBEZOGENEN DATEN AUFGRUND UNSERES ÜBERWIEGENDEN BERECHTIGTEN INTERESSES VERARBEITEN, HAST DU DAS JEDERZEITIGE RECHT, AUS GRÜNDEN, DIE SICH AUS DEINER BESONDEREN SITUATION ERGEBEN, GEGEN DIESE VERARBEITUNG WIDERSPRUCH MIT WIRKUNG FÜR DIE ZUKUNFT EINZULEGEN.</p>
|
||||||
|
<p>MACHST DU VON DEINEM WIDERSPRUCHSRECHT GEBRAUCH, BEENDEN WIR DIE VERARBEITUNG DER BETROFFENEN DATEN. EINE WEITERVERARBEITUNG BLEIBT ABER VORBEHALTEN, WENN WIR ZWINGENDE SCHUTZWÜRDIGE GRÜNDE FÜR DIE VERARBEITUNG NACHWEISEN KÖNNEN, DIE DEINE INTERESSEN, GRUNDRECHTE UND GRUNDFREIHEITEN ÜBERWIEGEN, ODER WENN DIE VERARBEITUNG DER GELTENDMACHUNG, AUSÜBUNG ODER VERTEIDIGUNG VON RECHTSANSPRÜCHEN DIENT.</p>
|
||||||
|
<p>WERDEN DEINE PERSONENBEZOGENEN DATEN VON UNS VERARBEITET, UM DIREKTWERBUNG ZU BETREIBEN, HAST DU DAS RECHT, JEDERZEIT WIDERSPRUCH GEGEN DIE VERARBEITUNG DIR BETREFFENDER PERSONENBEZOGENER DATEN ZUM ZWECKE DERARTIGER WERBUNG EINZULEGEN. DU KANNST DEN WIDERSPRUCH WIE OBEN BESCHRIEBEN AUSÜBEN.</p>
|
||||||
|
<p>MACHST DU VON DEINEM WIDERSPRUCHSRECHT GEBRAUCH, BEENDEN WIR DIE VERARBEITUNG DER BETROFFENEN DATEN ZU DIREKTWERBEZWECKEN.</p>
|
||||||
|
<h2>6) Dauer der Speicherung personenbezogener Daten</h2>
|
||||||
|
<p>Die Dauer der Speicherung von personenbezogenen Daten bemisst sich anhand der jeweiligen Rechtsgrundlage, am Verarbeitungszweck und – sofern einschlägig – zusätzlich anhand der jeweiligen gesetzlichen Aufbewahrungsfrist (z.B. handels- und steuerrechtliche Aufbewahrungsfristen).</p>
|
||||||
|
<p>Bei der Verarbeitung von personenbezogenen Daten auf Grundlage einer ausdrücklichen Einwilligung gemäß Art. 6 Abs. 1 lit. a DSGVO werden die betroffenen Daten so lange gespeichert, bis du deine Einwilligung widerrufst.</p>
|
||||||
|
<p>Existieren gesetzliche Aufbewahrungsfristen für Daten, die im Rahmen rechtsgeschäftlicher bzw. rechtsgeschäftsähnlicher Verpflichtungen auf der Grundlage von Art. 6 Abs. 1 lit. b DSGVO verarbeitet werden, werden diese Daten nach Ablauf der Aufbewahrungsfristen routinemäßig gelöscht, sofern sie nicht mehr zur Vertragserfüllung oder Vertragsanbahnung erforderlich sind und/oder unsererseits kein berechtigtes Interesse an der Weiterspeicherung fortbesteht.</p>
|
||||||
|
<p>Bei der Verarbeitung von personenbezogenen Daten auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO werden diese Daten so lange gespeichert, bis du dein Widerspruchsrecht nach Art. 21 Abs. 1 DSGVO ausübst, es sei denn, wir können zwingende schutzwürdige Gründe für die Verarbeitung nachweisen, die deine Interessen, Rechte und Freiheiten überwiegen, oder die Verarbeitung dient der Geltendmachung, Ausübung oder Verteidigung von Rechtsansprüchen.</p>
|
||||||
|
<p>Bei der Verarbeitung von personenbezogenen Daten zum Zwecke der Direktwerbung auf Grundlage von Art. 6 Abs. 1 lit. f DSGVO werden diese Daten so lange gespeichert, bis du dein Widerspruchsrecht nach Art. 21 Abs. 2 DSGVO ausübst.</p>
|
||||||
|
<p>Sofern sich aus den sonstigen Informationen dieser Erklärung über spezifische Verarbeitungssituationen nichts anderes ergibt, werden gespeicherte personenbezogene Daten im Übrigen dann gelöscht, wenn sie für die Zwecke, für die sie erhoben oder auf sonstige Weise verarbeitet wurden, nicht mehr notwendig sind.</p>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
|
151
app/pages/rechner.vue
Executable file
151
app/pages/rechner.vue
Executable file
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div class="nuxt-page-wrapper flex-col">
|
||||||
|
<ClientOnly>
|
||||||
|
<PpDeleteDialog
|
||||||
|
ref="deleteModal"
|
||||||
|
:current-card-index="currentCardIndex"
|
||||||
|
@delete="removeCard(currentCardIndex)"
|
||||||
|
/>
|
||||||
|
<PpPriceCardDialog
|
||||||
|
ref="modal"
|
||||||
|
:current-card="currentCard"
|
||||||
|
:current-card-index="currentCardIndex"
|
||||||
|
@update="updateCard()"
|
||||||
|
/>
|
||||||
|
<section class="content flex-col">
|
||||||
|
<aside class="filter-bar">
|
||||||
|
<button v-for="(button, index) in filterButtons" @click="() => sort(index)" :class="{ 'active': button.active }">
|
||||||
|
{{ button.label }}
|
||||||
|
</button>
|
||||||
|
</aside>
|
||||||
|
<div class="flex-col" role="list" v-if="cards.length">
|
||||||
|
<PpPriceCard
|
||||||
|
ref="priceCard"
|
||||||
|
v-for="(card, index) in cards"
|
||||||
|
:key="card.uuid"
|
||||||
|
:deletable="cards.length > 1"
|
||||||
|
:card="card"
|
||||||
|
@update="openModal(false, index)"
|
||||||
|
@remove="openDeleteModal()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p class="info-text grow" v-else>
|
||||||
|
Du hast noch keinerlei Einträge angelegt.
|
||||||
|
<br />Aber das ist gar nicht schlimm!
|
||||||
|
<br />Tippe einfach unten auf "+ Hinzufügen" und leg los.
|
||||||
|
</p>
|
||||||
|
</section>
|
||||||
|
<PpToolbar>
|
||||||
|
<PpButton class="mini-button text-white transparent" @click="openModal(true, -1)">
|
||||||
|
<Icon class="icon" name="uil:plus" mode="svg" />
|
||||||
|
<span>Hinzufügen</span>
|
||||||
|
</PpButton>
|
||||||
|
</PpToolbar>
|
||||||
|
</ClientOnly>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { PriceCard } from '../../shared/PriceCard'
|
||||||
|
import type { Button } from '../../shared/ButtonGroup'
|
||||||
|
import { PpPriceCardDialog, PpDeleteDialog, PpPriceCard } from '#components'
|
||||||
|
|
||||||
|
const cards = useLocalStorage<PriceCard[]>('cards', [])
|
||||||
|
const currentSort = useLocalStorage<number>('sort', 0)
|
||||||
|
const currentCard = ref<PriceCard>()
|
||||||
|
const currentCardIndex = ref<number>(-1)
|
||||||
|
const modal = useTemplateRef<typeof PpPriceCardDialog>('modal')
|
||||||
|
const deleteModal = useTemplateRef<typeof PpDeleteDialog>('deleteModal')
|
||||||
|
const priceCards = useTemplateRef<(typeof PpPriceCard)[]>('priceCard')
|
||||||
|
|
||||||
|
const createCard = (uuid : string) : PriceCard => ({
|
||||||
|
uuid,
|
||||||
|
name: '',
|
||||||
|
price: '',
|
||||||
|
roles: '',
|
||||||
|
sheets: '',
|
||||||
|
layers: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
const addCard = (card : PriceCard) => {
|
||||||
|
cards.value.unshift({ ...card })
|
||||||
|
sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const removeCard = (index : number) => {
|
||||||
|
cards.value.splice(index, 1)
|
||||||
|
sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateCard = () => {
|
||||||
|
if (currentCardIndex.value === -1) {
|
||||||
|
addCard(currentCard.value!)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const newCard = { ...currentCard.value! }
|
||||||
|
cards.value.splice(currentCardIndex.value, 1, newCard)
|
||||||
|
sort()
|
||||||
|
}
|
||||||
|
|
||||||
|
const openModal = (createNew : boolean, index : number) => {
|
||||||
|
if (createNew) {
|
||||||
|
currentCardIndex.value = -1
|
||||||
|
currentCard.value = createCard(randomUUID())
|
||||||
|
modal.value?.$el.showModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
currentCardIndex.value = index
|
||||||
|
currentCard.value = { ...cards.value[index]! }
|
||||||
|
|
||||||
|
modal.value?.$el.showModal()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const openDeleteModal = () => {
|
||||||
|
deleteModal.value?.$el.showModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const filterButtons = ref<Button[]>([
|
||||||
|
{
|
||||||
|
label: 'Rollen',
|
||||||
|
icon: 'uil:toilet-paper',
|
||||||
|
active: currentSort.value === 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Blatt',
|
||||||
|
icon: 'uil:file-landscape',
|
||||||
|
active: currentSort.value === 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Lagen',
|
||||||
|
icon: 'uil:layer-group',
|
||||||
|
active: currentSort.value === 2,
|
||||||
|
},
|
||||||
|
])
|
||||||
|
|
||||||
|
const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
|
||||||
|
cards.value.sort((a, b) => {
|
||||||
|
const aCard = priceCards.value?.find(card => card.uuid === a.uuid) || null
|
||||||
|
const bCard = priceCards.value?.find(card => card.uuid === b.uuid) || null
|
||||||
|
if (!aCard || !bCard) return 0
|
||||||
|
return aCard[key] - bCard[key]
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const sort = async (index : number = currentSort.value) => {
|
||||||
|
currentSort.value = index
|
||||||
|
filterButtons.value.forEach(button => { button.active = false })
|
||||||
|
filterButtons.value[index]!.active = true
|
||||||
|
|
||||||
|
await nextTick()
|
||||||
|
|
||||||
|
switch (index) {
|
||||||
|
case 0: return sortBy('ppr')
|
||||||
|
case 1: return sortBy('pps')
|
||||||
|
case 2: return sortBy('ppl')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,3 +1,5 @@
|
||||||
export const replaceComma = (value: string | number) => `${value}`.replace(',', '.')
|
export const replaceComma = (value: string | number) => `${value}`.replace(',', '.')
|
||||||
|
|
||||||
export const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
|
export const clamp = (value: number, min: number, max: number) => Math.min(Math.max(value, min), max)
|
||||||
|
|
||||||
|
export const between = (value: number, min: number, max: number) => value >= min && value <= max
|
7
app/utils/uuid.ts
Normal file
7
app/utils/uuid.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
export const randomUUID = (): string => {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
||||||
|
const r = Math.random() * 16 | 0;
|
||||||
|
const v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
})
|
||||||
|
}
|
47
docker-compose.yaml
Normal file
47
docker-compose.yaml
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
version: '3'
|
||||||
|
|
||||||
|
services:
|
||||||
|
propapier:
|
||||||
|
image: oven/bun:latest
|
||||||
|
container_name: propapier
|
||||||
|
working_dir: /app
|
||||||
|
ports:
|
||||||
|
- "1338:3000"
|
||||||
|
volumes:
|
||||||
|
- propapier_data:/app
|
||||||
|
environment:
|
||||||
|
- NODE_ENV=production
|
||||||
|
command: >
|
||||||
|
sh -c "
|
||||||
|
# Install git and curl if not already installed
|
||||||
|
if ! command -v git &> /dev/null || ! command -v curl &> /dev/null; then
|
||||||
|
echo 'Installing required packages...'
|
||||||
|
apt-get update && apt-get install -y git curl
|
||||||
|
fi &&
|
||||||
|
|
||||||
|
# Clone repository if not already cloned
|
||||||
|
if [ ! -d /app/.git ]; then
|
||||||
|
echo 'Cloning repository...'
|
||||||
|
git clone https://git.webfussel.de/webfussel/propapier /tmp/propapier &&
|
||||||
|
cp -r /tmp/propapier/. /app/ &&
|
||||||
|
rm -rf /tmp/propapier
|
||||||
|
fi &&
|
||||||
|
|
||||||
|
# Install dependencies and start application
|
||||||
|
echo 'Installing dependencies...' &&
|
||||||
|
bun install &&
|
||||||
|
echo 'Building application...' &&
|
||||||
|
bun run build &&
|
||||||
|
echo 'Starting application...' &&
|
||||||
|
bun .output/server/index.mjs
|
||||||
|
"
|
||||||
|
restart: unless-stopped
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
|
||||||
|
interval: 30s
|
||||||
|
timeout: 10s
|
||||||
|
retries: 3
|
||||||
|
start_period: 40s
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
propapier_data:
|
|
@ -1,4 +1,5 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
const description = 'Du zahlst zuviel für\'s Papier? Vergleiche schnell und unkompliziert die Preise für Toiletten-, Küchen- und andere Papier hier.'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-11-01',
|
compatibilityDate: '2024-11-01',
|
||||||
devtools: { enabled: false },
|
devtools: { enabled: false },
|
||||||
|
@ -16,22 +17,95 @@ export default defineNuxtConfig({
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
app: {
|
||||||
|
pageTransition: {
|
||||||
|
name: 'page',
|
||||||
|
mode: 'out-in',
|
||||||
|
},
|
||||||
|
head: {
|
||||||
|
htmlAttrs: { lang: 'de' },
|
||||||
|
link: [
|
||||||
|
{ rel: 'preload', as: 'image', href: '/img/propapier.svg', type: 'image/svg+xml' },
|
||||||
|
{ rel: 'icon', type: 'image/x-icon', href: '/favicon.svg' },
|
||||||
|
],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
routeRules: {
|
routeRules: {
|
||||||
'/': { prerender: true },
|
'/': { prerender: true },
|
||||||
'/imprint': { prerender: true },
|
'/imprint': { prerender: true },
|
||||||
'/privacy': { prerender: true },
|
'/privacy': { prerender: true },
|
||||||
},
|
},
|
||||||
|
|
||||||
modules: ['@nuxt/icon', '@vueuse/nuxt', '@nuxtjs/device'],
|
modules: [
|
||||||
|
'@nuxt/icon',
|
||||||
|
'@vueuse/nuxt',
|
||||||
|
'@nuxtjs/device',
|
||||||
|
'@nuxt/fonts',
|
||||||
|
'nuxt-seo-utils',
|
||||||
|
'nuxt-ripple',
|
||||||
|
'@nuxtjs/sitemap',
|
||||||
|
'@nuxtjs/robots',
|
||||||
|
'@nuxtjs/plausible'
|
||||||
|
],
|
||||||
|
|
||||||
css : [
|
css : [
|
||||||
'./app/assets/styles/general.css',
|
'./app/assets/styles/general.css',
|
||||||
'./app/assets/styles/header.css',
|
'./app/assets/styles/header.css',
|
||||||
|
'./app/assets/styles/navigation.css',
|
||||||
'./app/assets/styles/footer.css',
|
'./app/assets/styles/footer.css',
|
||||||
'./app/assets/styles/button.css',
|
'./app/assets/styles/button.css',
|
||||||
'./app/assets/styles/buttonGroup.css',
|
'./app/assets/styles/buttonGroup.css',
|
||||||
'./app/assets/styles/priceCard.css',
|
'./app/assets/styles/priceCard.css',
|
||||||
'./app/assets/styles/formInput.css',
|
'./app/assets/styles/timelineCard.css',
|
||||||
|
'./app/assets/styles/form/textfield.css',
|
||||||
|
'./app/assets/styles/form/search.css',
|
||||||
'./app/assets/styles/toolbar.css',
|
'./app/assets/styles/toolbar.css',
|
||||||
]
|
'./app/assets/styles/page.css',
|
||||||
|
'./app/assets/styles/dialog.css',
|
||||||
|
'./app/assets/styles/landingpage.css',
|
||||||
|
],
|
||||||
|
|
||||||
|
site: {
|
||||||
|
url: 'https://pro-papier.de',
|
||||||
|
name: 'ProPapier',
|
||||||
|
},
|
||||||
|
|
||||||
|
seo: {
|
||||||
|
meta: {
|
||||||
|
title: 'ProPapier',
|
||||||
|
description,
|
||||||
|
themeColor: [
|
||||||
|
{ content: '#18181b', media: '(prefers-color-scheme: dark)' },
|
||||||
|
{ content: 'white', media: '(prefers-color-scheme: light)' },
|
||||||
|
],
|
||||||
|
twitterCreator: '@webfussel',
|
||||||
|
twitterSite: '@propapier',
|
||||||
|
author: 'webfussel',
|
||||||
|
colorScheme: 'dark light',
|
||||||
|
applicationName: 'ProPapier',
|
||||||
|
|
||||||
|
// Nuxt SEO Utils already sets the below tags for you
|
||||||
|
ogSiteName: 'ProPapier',
|
||||||
|
ogLocale: 'de_DE',
|
||||||
|
ogType: 'website',
|
||||||
|
ogUrl: 'https://pro-papier.de',
|
||||||
|
ogTitle: 'ProPapier',
|
||||||
|
ogDescription: description,
|
||||||
|
|
||||||
|
// Other Nuxt SEO modules handles these
|
||||||
|
ogImage: '/img/og.png',
|
||||||
|
robots: 'index, follow',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
sitemap: {
|
||||||
|
// exclude all URLs that start with /secret
|
||||||
|
exclude: ['/other/**'],
|
||||||
|
},
|
||||||
|
|
||||||
|
plausible: {
|
||||||
|
// Prevent tracking on localhost
|
||||||
|
ignoredHostnames: ['localhost'],
|
||||||
|
},
|
||||||
})
|
})
|
1519
package-lock.json
generated
1519
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -7,17 +7,23 @@
|
||||||
"dev": "nuxt dev",
|
"dev": "nuxt dev",
|
||||||
"dev:expose": "nuxt dev --host",
|
"dev:expose": "nuxt dev --host",
|
||||||
"generate": "nuxt generate",
|
"generate": "nuxt generate",
|
||||||
"preview": "nuxt preview",
|
"preview": "npx serve .output/public",
|
||||||
"prepare": "nuxt prepare",
|
"prepare": "nuxt prepare",
|
||||||
"postinstall": "nuxt prepare"
|
"postinstall": "nuxt prepare"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@iconify-json/simple-icons": "^1.2.32",
|
"@iconify-json/simple-icons": "^1.2.32",
|
||||||
"@iconify-json/uil": "^1.2.3",
|
"@iconify-json/uil": "^1.2.3",
|
||||||
|
"@nuxt/fonts": "^0.11.3",
|
||||||
"@nuxt/icon": "^1.10.3",
|
"@nuxt/icon": "^1.10.3",
|
||||||
"@nuxtjs/device": "^3.2.4",
|
"@nuxtjs/device": "^3.2.4",
|
||||||
|
"@nuxtjs/plausible": "^1.2.0",
|
||||||
|
"@nuxtjs/robots": "^5.2.10",
|
||||||
|
"@nuxtjs/sitemap": "^7.3.0",
|
||||||
"@vueuse/nuxt": "^13.1.0",
|
"@vueuse/nuxt": "^13.1.0",
|
||||||
"nuxt": "^3.16.2",
|
"nuxt": "^3.16.2",
|
||||||
|
"nuxt-ripple": "^0.0.8",
|
||||||
|
"nuxt-seo-utils": "^7.0.11",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
"vue-router": "latest"
|
"vue-router": "latest"
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Before Width: 32px | Height: 32px | Size: 4.2 KiB |
1
public/favicon.svg
Executable file
1
public/favicon.svg
Executable file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1835 1957" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><path d="M1567.66,18.116c145.891,-0 264.336,225.821 264.336,503.97c-0,278.149 -118.445,503.971 -264.336,503.971c-145.891,-0 -264.336,-225.822 -264.336,-503.971c0,-278.149 118.445,-503.97 264.336,-503.97Zm-0,224.151c-68.571,-0 -124.242,125.383 -124.242,279.819c0,154.437 55.671,279.82 124.242,279.82c68.571,0 124.241,-125.383 124.241,-279.82c0,-154.436 -55.67,-279.819 -124.241,-279.819Z" style="fill:#fff;"/><path d="M1303.43,755.105c13.811,85.466 89.537,243.344 171.549,272.195l-167.42,-1.213l-4.129,-270.982Z" style="fill:#fff;"/><path d="M9.052,1835.28l1.152,-1300.71c4.727,-203.082 60.44,-510.777 379.038,-525.353l1070.29,5.76c-106.16,85.364 -183.364,230.766 -179.726,543.787l0,1309.93c0,94.917 -24.936,111.898 -116.361,20.738c-67.285,-67.09 -128.514,-68.942 -195.856,-2.304c-80.33,79.49 -145.126,81.518 -216.593,2.304c-64.824,-71.852 -138.451,-64.497 -203.92,-0c-74.453,73.348 -133.367,79.706 -208.528,-4.609c-59.394,-66.627 -136.107,-49.149 -208.528,9.217c-95.361,76.853 -120.97,91.489 -120.97,-58.757Zm265.074,-1341.6c-0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.181,2.321 -5.181,5.18c0,2.859 2.322,5.181 5.181,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Zm138.149,624.213l0,79.687l-84.961,0l0,72.657l85.037,-0c0.756,52.26 7.176,98.257 19.26,137.988c12.891,42.383 31.25,77.734 55.078,106.055c23.828,28.32 51.953,49.609 84.375,63.867c32.422,14.258 68.164,21.386 107.227,21.386c20.703,0 41.211,-1.562 61.523,-4.687c20.313,-3.125 39.844,-7.422 58.594,-12.891l-11.719,-93.75c-16.406,6.25 -33.691,11.133 -51.855,14.649c-18.164,3.515 -36.621,5.273 -55.371,5.273c-24.219,0 -46.094,-5.078 -65.625,-15.234c-19.532,-10.156 -36.328,-25.391 -50.391,-45.703c-14.062,-20.313 -24.805,-46.094 -32.227,-77.344c-6.796,-28.617 -10.481,-61.82 -11.053,-99.609l182.147,-0l0,-72.657l-182.226,0l-0,-79.687l182.226,-0l0,-73.242l-181.972,-0c0.952,-34.41 4.578,-64.879 10.878,-91.407c7.422,-31.25 17.969,-57.031 31.641,-77.343c13.672,-20.313 30.274,-35.547 49.805,-45.704c19.531,-10.156 41.601,-15.234 66.211,-15.234c19.14,0 37.793,1.856 55.957,5.567c18.164,3.71 35.449,8.691 51.855,14.941l11.719,-94.336c-20.313,-5.469 -40.234,-9.863 -59.766,-13.184c-19.531,-3.32 -39.843,-4.98 -60.937,-4.98c-39.063,-0 -74.903,7.129 -107.52,21.387c-32.617,14.257 -60.644,35.644 -84.082,64.16c-23.437,28.515 -41.601,64.062 -54.492,106.64c-11.367,37.545 -17.722,80.709 -19.066,129.493l-85.231,-0l0,73.242l84.961,-0Zm403.587,-624.213c0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.18,2.321 -5.18,5.18c0,2.859 2.321,5.181 5.18,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Zm-272.284,0c0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.18,2.321 -5.18,5.18c0,2.859 2.321,5.181 5.18,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Zm541.737,0c0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.18,2.321 -5.18,5.18c-0,2.859 2.321,5.181 5.18,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Z" style="fill:#fff;"/></g></svg>
|
After (image error) Size: 3.3 KiB |
1
public/img/desktop-background.svg
Normal file
1
public/img/desktop-background.svg
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<svg id="visual" viewBox="0 0 3840 2160" width="3840" height="2160" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1"><rect x="0" y="0" width="3840" height="2160" fill="#292929"></rect><defs><linearGradient id="grad1_0" x1="43.8%" y1="100%" x2="100%" y2="0%"><stop offset="14.444444444444446%" stop-color="#013174" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#013174" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad1_1" x1="43.8%" y1="100%" x2="100%" y2="0%"><stop offset="14.444444444444446%" stop-color="#013174" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#202e5a" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad1_2" x1="43.8%" y1="100%" x2="100%" y2="0%"><stop offset="14.444444444444446%" stop-color="#282b41" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#202e5a" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad1_3" x1="43.8%" y1="100%" x2="100%" y2="0%"><stop offset="14.444444444444446%" stop-color="#282b41" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#292929" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad2_0" x1="0%" y1="100%" x2="56.3%" y2="0%"><stop offset="14.444444444444446%" stop-color="#013174" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#013174" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad2_1" x1="0%" y1="100%" x2="56.3%" y2="0%"><stop offset="14.444444444444446%" stop-color="#202e5a" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#013174" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad2_2" x1="0%" y1="100%" x2="56.3%" y2="0%"><stop offset="14.444444444444446%" stop-color="#202e5a" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#282b41" stop-opacity="1"></stop></linearGradient></defs><defs><linearGradient id="grad2_3" x1="0%" y1="100%" x2="56.3%" y2="0%"><stop offset="14.444444444444446%" stop-color="#292929" stop-opacity="1"></stop><stop offset="85.55555555555554%" stop-color="#282b41" stop-opacity="1"></stop></linearGradient></defs><g transform="translate(3840, 2160)"><path d="M-1956 0C-1926.7 -253.3 -1897.4 -506.6 -1820 -753.9C-1742.7 -1001.2 -1617.2 -1242.4 -1438.3 -1438.3C-1259.3 -1634.1 -1027 -1784.5 -778.4 -1879.2C-529.8 -1973.9 -264.9 -2012.9 0 -2052L0 0Z" fill="#292a35"></path><path d="M-1467 0C-1445 -190 -1423.1 -380 -1365 -565.4C-1307 -750.9 -1212.9 -931.8 -1078.7 -1078.7C-944.5 -1225.6 -770.2 -1338.3 -583.8 -1409.4C-397.3 -1480.4 -198.7 -1509.7 0 -1539L0 0Z" fill="#252d4d"></path><path d="M-978 0C-963.4 -126.7 -948.7 -253.3 -910 -376.9C-871.3 -500.6 -808.6 -621.2 -719.1 -719.1C-629.7 -817 -513.5 -892.2 -389.2 -939.6C-264.9 -986.9 -132.4 -1006.5 0 -1026L0 0Z" fill="#172f67"></path><path d="M-489 0C-481.7 -63.3 -474.4 -126.7 -455 -188.5C-435.7 -250.3 -404.3 -310.6 -359.6 -359.6C-314.8 -408.5 -256.7 -446.1 -194.6 -469.8C-132.4 -493.5 -66.2 -503.2 0 -513L0 0Z" fill="#013174"></path></g><g transform="translate(0, 0)"><path d="M1962 0C1948.3 263.6 1934.6 527.1 1846.8 765C1759 1002.9 1597.1 1215.1 1413.5 1413.5C1229.9 1612 1024.5 1796.7 785.3 1895.8C546.1 1994.9 273 2008.5 0 2022L0 0Z" fill="#292a35"></path><path d="M1471.5 0C1461.2 197.7 1451 395.3 1385.1 573.7C1319.3 752.1 1197.9 911.3 1060.1 1060.1C922.4 1209 768.4 1347.5 588.9 1421.9C409.5 1496.2 204.8 1506.3 0 1516.5L0 0Z" fill="#252d4d"></path><path d="M981 0C974.2 131.8 967.3 263.6 923.4 382.5C879.5 501.4 798.6 607.5 706.8 706.8C614.9 806 512.2 898.3 392.6 947.9C273 997.5 136.5 1004.2 0 1011L0 0Z" fill="#172f67"></path><path d="M490.5 0C487.1 65.9 483.7 131.8 461.7 191.2C439.8 250.7 399.3 303.8 353.4 353.4C307.5 403 256.1 449.2 196.3 474C136.5 498.7 68.3 502.1 0 505.5L0 0Z" fill="#013174"></path></g></svg>
|
After (image error) Size: 3.8 KiB |
BIN
public/img/hero-image.webp
Normal file
BIN
public/img/hero-image.webp
Normal file
Binary file not shown.
After ![]() (image error) Size: 26 KiB |
BIN
public/img/og.png
Executable file
BIN
public/img/og.png
Executable file
Binary file not shown.
After ![]() (image error) Size: 384 KiB |
1
public/img/propapier.svg
Executable file
1
public/img/propapier.svg
Executable file
|
@ -0,0 +1 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg width="100%" height="100%" viewBox="0 0 1835 1957" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;"><g><path d="M1567.66,18.116c145.891,-0 264.336,225.821 264.336,503.97c-0,278.149 -118.445,503.971 -264.336,503.971c-145.891,-0 -264.336,-225.822 -264.336,-503.971c0,-278.149 118.445,-503.97 264.336,-503.97Zm-0,224.151c-68.571,-0 -124.242,125.383 -124.242,279.819c0,154.437 55.671,279.82 124.242,279.82c68.571,0 124.241,-125.383 124.241,-279.82c0,-154.436 -55.67,-279.819 -124.241,-279.819Z" style="fill:#fff;"/><path d="M1303.43,755.105c13.811,85.466 89.537,243.344 171.549,272.195l-167.42,-1.213l-4.129,-270.982Z" style="fill:#fff;"/><path d="M9.052,1835.28l1.152,-1300.71c4.727,-203.082 60.44,-510.777 379.038,-525.353l1070.29,5.76c-106.16,85.364 -183.364,230.766 -179.726,543.787l0,1309.93c0,94.917 -24.936,111.898 -116.361,20.738c-67.285,-67.09 -128.514,-68.942 -195.856,-2.304c-80.33,79.49 -145.126,81.518 -216.593,2.304c-64.824,-71.852 -138.451,-64.497 -203.92,-0c-74.453,73.348 -133.367,79.706 -208.528,-4.609c-59.394,-66.627 -136.107,-49.149 -208.528,9.217c-95.361,76.853 -120.97,91.489 -120.97,-58.757Zm265.074,-1341.6c-0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.181,2.321 -5.181,5.18c0,2.859 2.322,5.181 5.181,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Zm138.149,624.213l0,79.687l-84.961,0l0,72.657l85.037,-0c0.756,52.26 7.176,98.257 19.26,137.988c12.891,42.383 31.25,77.734 55.078,106.055c23.828,28.32 51.953,49.609 84.375,63.867c32.422,14.258 68.164,21.386 107.227,21.386c20.703,0 41.211,-1.562 61.523,-4.687c20.313,-3.125 39.844,-7.422 58.594,-12.891l-11.719,-93.75c-16.406,6.25 -33.691,11.133 -51.855,14.649c-18.164,3.515 -36.621,5.273 -55.371,5.273c-24.219,0 -46.094,-5.078 -65.625,-15.234c-19.532,-10.156 -36.328,-25.391 -50.391,-45.703c-14.062,-20.313 -24.805,-46.094 -32.227,-77.344c-6.796,-28.617 -10.481,-61.82 -11.053,-99.609l182.147,-0l0,-72.657l-182.226,0l-0,-79.687l182.226,-0l0,-73.242l-181.972,-0c0.952,-34.41 4.578,-64.879 10.878,-91.407c7.422,-31.25 17.969,-57.031 31.641,-77.343c13.672,-20.313 30.274,-35.547 49.805,-45.704c19.531,-10.156 41.601,-15.234 66.211,-15.234c19.14,0 37.793,1.856 55.957,5.567c18.164,3.71 35.449,8.691 51.855,14.941l11.719,-94.336c-20.313,-5.469 -40.234,-9.863 -59.766,-13.184c-19.531,-3.32 -39.843,-4.98 -60.937,-4.98c-39.063,-0 -74.903,7.129 -107.52,21.387c-32.617,14.257 -60.644,35.644 -84.082,64.16c-23.437,28.515 -41.601,64.062 -54.492,106.64c-11.367,37.545 -17.722,80.709 -19.066,129.493l-85.231,-0l0,73.242l84.961,-0Zm403.587,-624.213c0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.18,2.321 -5.18,5.18c0,2.859 2.321,5.181 5.18,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Zm-272.284,0c0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.18,2.321 -5.18,5.18c0,2.859 2.321,5.181 5.18,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Zm541.737,0c0,-2.859 -2.321,-5.18 -5.18,-5.18l-132.364,0c-2.859,0 -5.18,2.321 -5.18,5.18c-0,2.859 2.321,5.181 5.18,5.181l132.364,-0c2.859,-0 5.18,-2.322 5.18,-5.181Z" style="fill:#fff;"/></g></svg>
|
After (image error) Size: 3.3 KiB |
1
server/api/health.ts
Normal file
1
server/api/health.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export default defineEventHandler((): string => 'ok' )
|
|
@ -1,11 +0,0 @@
|
||||||
export type Card = {
|
|
||||||
ppr: number
|
|
||||||
pps: number
|
|
||||||
ppl: number
|
|
||||||
uuid: string
|
|
||||||
name: string
|
|
||||||
price: string
|
|
||||||
roles: string
|
|
||||||
sheets: string
|
|
||||||
layers: string
|
|
||||||
}
|
|
8
shared/PriceCard.ts
Normal file
8
shared/PriceCard.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export type PriceCard = {
|
||||||
|
uuid : string
|
||||||
|
name : string
|
||||||
|
price : string
|
||||||
|
roles : string
|
||||||
|
sheets : string
|
||||||
|
layers : string
|
||||||
|
}
|
11
shared/TimelineCard.ts
Normal file
11
shared/TimelineCard.ts
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
export type TimelineState = 'planned' | 'inProgress' | 'done'
|
||||||
|
|
||||||
|
export type TimelineCard = {
|
||||||
|
icon: string
|
||||||
|
title: string
|
||||||
|
description: string
|
||||||
|
state: {
|
||||||
|
value: TimelineState
|
||||||
|
message ?: string
|
||||||
|
}
|
||||||
|
}
|
Loading…
Add table
Add a link
Reference in a new issue