Compare commits

..

2 commits

Author SHA1 Message Date
220d4b1f30 add: new design
New design
2025-05-10 13:01:21 +02:00
ae0f6cb620 add: data from MongoDB
Retrieve Data from MongoDB
2025-05-10 12:56:57 +02:00
52 changed files with 5725 additions and 3398 deletions

19
app/app.vue Normal file → Executable file
View file

@ -1,18 +1,5 @@
<template> <template>
<PpNavigation /> <PpHeader />
<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>

View file

@ -1,6 +1,6 @@
.Button { .Button {
--padding: var(--padding-xs); --padding: .2rem;
--background: var(--color-main-dark); --background: var(--color-gradient-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: var(--padding-default); gap: 1rem;
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: var(--padding-s) var(--padding-l); padding: .5em 1.5em;
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: var(--padding-s) var(--padding-l); padding: .5em 1.5em;
border-radius: var(--radius-default); border-radius: var(--radius-default);
&.danger { &.danger {
--background: var(--color-error); --background: var(--color-gradient-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: var(--padding-s) var(--padding-l); padding: .5em 1.5em;
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: var(--padding-s); padding: .5rem;
} }
&.cta { &.cta {
background: var(--background); background: var(--background);
color: var(--color); color: var(--color);
padding: var(--padding-s) var(--padding-l); padding: .5rem 1.5rem;
border-radius: var(--radius-default); border-radius: var(--radius-default);
box-shadow: var(--box-shadow-z2); box-shadow: var(--box-shadow-z2);
@ -93,30 +93,18 @@
} }
} }
&.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: var(--padding-s) var(--padding-l); padding: .5rem 1.5rem;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: var(--padding-xs); gap: .5rem;
& > .icon { & > .icon {
font-size: var(--font-size-xl); font-size: 1.5rem;
} }
& > span { & > span {
font-size: var(--font-size-s); font-size: .8rem;
} }
} }
} }

View file

@ -1,6 +1,9 @@
.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);
@ -9,8 +12,8 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: var(--padding-xs); gap: .5rem;
padding: var(--padding-s); padding: .5rem;
flex-grow: 1; flex-grow: 1;
background: var(--background); background: var(--background);
color: var(--color); color: var(--color);
@ -21,5 +24,13 @@
--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;
}
} }
} }

View file

@ -1,71 +0,0 @@
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%);
}
}
}

View file

@ -1,49 +1,38 @@
.Footer { .Footer {
position: relative; position: relative;
background: var(--color-darkest); background: var(--color-darkest);
padding: var(--padding-default); padding: 1rem;
z-index: 100; z-index: 100;
& h4 { & h4 {
color: var(--color-lightest); color: var(--color-lightest);
text-align: center; text-align: center;
margin-bottom: var(--padding-default); margin-bottom: 1rem;
} }
& .bottom { & .bottom {
display: flex; display: flex;
justify-content: center; gap: 1rem;
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: var(--font-size-xl); font-size: 1.5rem;
justify-content: center; justify-content: center;
margin-bottom: var(--padding-xl); margin-bottom: 2rem;
} }
& .data-links { & .data-links {
justify-content: flex-end; justify-content: flex-end;
font-size: var(--font-size-s); font-size: .8rem;
} }
& ul { & ul {
list-style: none; list-style: none;
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--padding-default); gap: 1rem;
& a { & a {
color: var(--color-lightest); color: var(--color-lightest);

View file

@ -1,15 +0,0 @@
.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;
}
}

View file

@ -1,72 +0,0 @@
.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);
}
}

52
app/assets/styles/formInput.css Executable file
View file

@ -0,0 +1,52 @@
.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);
}
}
}
}

View file

@ -1,191 +1,195 @@
:root { :root {
--color-success: #328104; --padding-default: 1rem;
--color-error: #a20606; --padding-small: .5rem;
--color-blue-light: #d7e1f1; --radius-default: 3px;
--color-blue: #05b0ff; --transition-default: 150ms;
--color-blue-dark: #0266f2;
--color-blue-darkest: #013174;
--color-darkest: #292929; --color-success: #328104;
--color-dark: #404040; --color-error: #A20606;
--color-middle: #707070; --color-blue-light: #0DDCE7;
--color-light: #e0e0e6; --color-blue: #05B0FF;
--color-lightest: #fafaff; --color-blue-dark: #0266F2;
--color-blue-darkest: #013174;
--color-green-light: #05ffc5; --color-darkest: #292929;
--color-green: #02f276; --color-dark: #404040;
--color-green-dark: #09dc33; --color-middle: #707070;
--color-green-darkest: #07b029; --color-light: #E0E0E6;
--color-green-darkest-most: #157c2a; --color-lightest: #FAFAFF;
--color-main: var(--color-blue); --color-green-light: #05FFC5;
--color-main-lightest: var(--color-blue-light); --color-green: #02F276;
--color-main-light: var(--color-blue-light); --color-green-dark: #09DC33;
--color-main-dark: var(--color-blue-dark); --color-green-darkest: #07B029;
--color-main-darkest: var(--color-blue-darkest); --color-green-darkest-most: #157C2A;
--color-accent: var(--color-green);
--color-accent-light: var(--color-green-light);
--color-accent-dark: var(--color-green-dark);
--color-accent-darkest: var(--color-green-darkest);
--color-text: var(--color-darkest); --color-main: var(--color-blue);
--color-text-invert: var(--color-lightest); --color-main-light: var(--color-blue-light);
--color-main-dark: var(--color-blue-dark);
--color-main-darkest: var(--color-blue-darkest);
--color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light)); --color-accent: var(--color-green);
--color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark)); --color-accent-light: var(--color-green-light);
--color-gradient-accent: linear-gradient(to bottom right, var(--color-accent), var(--color-accent-light)); --color-accent-dark: var(--color-green-dark);
--color-gradient-accent-dark: linear-gradient(to bottom right, var(--color-accent-darkest), var(--color-accent-dark)); --color-accent-darkest: var(--color-green-darkest);
--color-gradient-error: linear-gradient(to bottom right, #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); --color-gradient-main: linear-gradient(to bottom right, var(--color-main), var(--color-main-light));
--box-shadow-z2: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23); --color-gradient-main-dark: linear-gradient(to bottom right, var(--color-main-darkest), var(--color-main-dark));
--box-shadow-inset: inset 0 3px 6px rgba(0, 0, 0, 0.16), inset 0 3px 6px rgba(0, 0, 0, 0.23); --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-error: linear-gradient(to bottom right, #B00707, #DC0909);
--color-gradient-error-reverse: linear-gradient(to top left, #B00707, #DC0909);
/* Font Sizes & Scaling Factor*/ --box-shadow-upper: 0 -3px 6px rgba(0,0,0,0.16), 0 -3px 6px rgba(0,0,0,0.23);
--scaling-factor: 1.25; --box-shadow-z2: 0 3px 6px rgba(0,0,0,0.16), 0 3px 6px rgba(0,0,0,0.23);
--font-size-xs: calc(var(--font-size-s) / var(--scaling-factor)); --box-shadow-inset: inset 0 3px 6px rgba(0,0,0,0.16), inset 0 3px 6px rgba(0,0,0,0.23);
--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;
} }
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
} }
html, html,
body { body {
height: 100%; height: 100%;
overflow-x: hidden; overflow-x: hidden;
font-family: sans-serif; font-family: sans-serif;
color: var(--color-text); color: var(--color-darkest);
font-size: var(--font-size-default);
max-width: var(--page-max-width);
margin: 0 auto;
background-image: url("/img/desktop-background.svg");
background-position: center;
box-shadow: var(--box-shadow-z2);
} }
h1, h2, h3 { .dot {
margin: var(--padding-xl) 0 var(--padding-default); --size: 10px;
text-wrap: balance; width: var(--size);
hyphens: auto; height: var(--size);
font-size: var(--font-size-xl); 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);
scale: 0;
transition: var(--transition-default);
&.visible {
scale: 1;
animation: pulse 1s infinite;
}
} }
h2 { @keyframes pulse {
margin: var(--padding-l) 0 var(--padding-default); 0% {
font-size: var(--font-size-l); transform: scale(0.95);
} box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
}
70% {
transform: scale(1);
box-shadow: 0 0 0 10px rgba(255, 255, 255, 0);
}
h3 { 100% {
margin: var(--padding-default) 0 var(--padding-default); transform: scale(0.95);
font-size: var(--font-size-default); box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
} }
a:has(button) {
text-decoration: none;
} }
.card { .card {
overflow: hidden; overflow: hidden;
border-radius: var(--radius-default); border-radius: var(--radius-default);
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;
} }
.flex-row { .flex-row {
display: flex; display: flex;
flex-direction: row; flex-direction: row;
} }
.text-white { .text-white {
color: var(--color-text-invert); color: var(--color-lightest);
} }
.bg-main { .bg-main {
background-color: var(--color-main); background-color: var(--color-main);
} }
.bg-main-hover:hover { .bg-main-hover:hover {
background-color: var(--color-main); background-color: var(--color-main);
} }
.bg-main-dark { .bg-main-dark {
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: var(--padding-default); gap: 1rem;
} }
.bg-blue { .bg-blue {
background: var(--color-blue); background: var(--color-blue);
} }
.bg-white { .bg-white {
background: var(--color-lightest); background: var(--color-lightest);
} }
.padding { .padding {
gap: var(--padding-default); gap: 1rem;
padding: var(--padding-default); padding: var(--padding-default);
} }
.padding-small { .padding-small {
gap: var(--padding-default); gap: 1rem;
padding: var(--padding-s); padding: var(--padding-small);
} }
.roboto-condensed { dialog {
font-family: "Roboto Condensed", sans-serif; top: 50%;
} left: 50%;
width: 100vw;
transform: translate(-50%, -50%);
border: none;
border-radius: var(--radius-default);
.roboto { font-size: 1rem;
font-family: "Roboto", sans-serif;
}
.open-sans { & header {
font-family: "Open Sans", sans-serif; justify-content: space-between;
} align-items: center;
}
.grow { & footer {
flex-grow: 1; justify-content: space-between;
height: var(--padding-s); }
&::backdrop {
background: rgba(0, 0, 0, 0.5);
}
} }

View file

@ -1,88 +1,68 @@
.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;
align-items: center;
gap: var(--padding-default);
padding: var(--padding-default);
&:not(.lp) { & strong {
background: var(--color-main-darkest); font-size: 2em;
& span {
color: var(--color-main-dark);
}
} }
&.lp { & input[type="checkbox"] {
position: absolute; display: none;
background: rgba(0,0,0,.5); }
backdrop-filter: blur(10px);
mask: linear-gradient(to top, transparent, black 30%); & input[type="checkbox"]:checked + nav {
width: 100%; translate: 0;
max-width: var(--page-max-width); }
& nav,
& ul {
gap: 1em;
}
& nav {
position: fixed;
padding: var(--padding-default);
translate: 100% 0;
width: 100vw;
right: 0;
top: 0; top: 0;
padding-bottom: var(--padding-xxl); height: 100dvh;
transition: 150ms ease-in-out;
background: var(--color-lightest);
font-size: 2em;
align-items: end;
z-index: 100;
} }
& .logo { & ul {
height: 40px;
}
& .burger-button {
all: unset;
color: var(--color-lightest);
}
& header {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%; width: 100%;
font-weight: bold; align-items: center;
& li {
list-style: none;
& a { & a {
text-decoration: none; color: var(--color-black);
color: var(--color-lightest); text-decoration: none;
} transition: var(--transition-default);
& .header-text { &.active {
display: flex; color: var(--color-main);
align-items: center; font-weight: bold;
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);
} }
} }
} }

View file

@ -1,40 +0,0 @@
.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);
}

View file

@ -1,69 +0,0 @@
.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);
}
}
}
}
}

View file

@ -1,80 +0,0 @@
.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);
}

View file

@ -1,8 +1,11 @@
.PriceCard { .PriceCard {
position: relative; position: relative;
overflow: hidden;
width: 100%; width: 100%;
transition: 150ms;
opacity: 1;
color: var(--color-darkest); color: var(--color-darkest);
border-bottom: 1px dashed var(--color-light); background: black;
.bottom { .bottom {
position: absolute; position: absolute;
@ -15,7 +18,7 @@
& > * { & > * {
flex-grow: 1; flex-grow: 1;
color: var(--color-lightest); color: var(--color-lightest);
font-size: var(--font-size-xl); font-size: 2rem;
display: flex; display: flex;
align-items: center; align-items: center;
@ -25,14 +28,14 @@
} }
& .bg-edit { & .bg-edit {
background: var(--color-main-dark); background: var(--color-gradient-main-dark);
padding: var(--padding-xl); padding: 2rem;
text-align: left; text-align: left;
} }
& .bg-delete { & .bg-delete {
background: var(--color-error); background: var(--color-gradient-error-reverse);
padding: var(--padding-xl); padding: 2rem;
text-align: right; text-align: right;
justify-content: flex-end; justify-content: flex-end;
} }
@ -42,8 +45,9 @@
position: relative; position: relative;
background: var(--color-lightest); background: var(--color-lightest);
z-index: 2; z-index: 2;
gap: var(--padding-default); gap: 1rem;
padding: var(--padding-default); padding: 1rem;
border-radius: var(--radius-default);
&.animated { &.animated {
transition: var(--transition-default); transition: var(--transition-default);
@ -56,14 +60,14 @@
color: var(--color-darkest); color: var(--color-darkest);
& .icon { & .icon {
font-size: var(--font-size-default); font-size: 1rem;
cursor: pointer; cursor: pointer;
} }
} }
& .name-price { & .name-price {
display: flex; display: flex;
gap: var(--padding-xs); gap: .5rem;
& > span:nth-child(1) { & > span:nth-child(1) {
font-weight: bold; font-weight: bold;
@ -75,7 +79,7 @@
& > span:nth-child(2)::before { & > span:nth-child(2)::before {
content: '•'; content: '•';
margin-right: var(--padding-xs); margin-right: .5rem;
color: var(--color-middle); color: var(--color-middle);
} }
} }
@ -84,19 +88,19 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
width: 100%; width: 100%;
gap: var(--padding-default); gap: 1rem;
justify-content: space-between; justify-content: space-between;
& > .info { & > .info {
flex-grow: 0; flex-grow: 0;
align-items: center; align-items: center;
gap: var(--padding-xxs); gap: .25rem;
font-weight: bold; font-weight: bold;
& > .price { & > .price {
display: flex; display: flex;
align-items: center; align-items: center;
gap: var(--padding-xs); gap: .5rem;
& > .icon { & > .icon {
color: var(--color-main-dark); color: var(--color-main-dark);
@ -104,7 +108,7 @@
} }
& > .pro { & > .pro {
font-size: var(--font-size-xs); font-size: .6rem;
color: var(--color-middle); color: var(--color-middle);
font-weight: lighter; font-weight: lighter;
} }

View file

@ -1,28 +0,0 @@
.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);
}
}

View file

@ -4,10 +4,11 @@
background: var(--color-main-darkest); background: var(--color-main-darkest);
position: sticky; position: sticky;
bottom: 0; bottom: 0;
z-index: 1000; z-index: 100;
box-shadow: var(--box-shadow-upper); box-shadow: var(--box-shadow-upper);
& > .Button { & > .Button {
font-size: var(--font-size-default); --padding: 1rem;
font-size: 1rem;
} }
} }

View file

@ -1,24 +1,24 @@
<template> <template>
<dialog <dialog
ref="dialog" ref="dialog"
closedby="none" closedby="any"
> >
<form method="dialog" class="wrapper" ref="wrapper"> <form method="dialog">
<header class="flex-row"> <header class="flex-row padding">
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="flex-col"> <div class="padding 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"> <footer class="flex-row padding">
<PpButton class="text"> <PpButton class="text">
<span>Abbrechen</span> <span>Abbrechen</span>
</PpButton> </PpButton>
<PpButton class="danger raised" @click="$emit('delete')"> <PpButton class="danger raised" @click="$emit('delete')">
<span>Löschen</span> <span>Löschen</span>
</PpButton> </PpButton>
@ -35,11 +35,5 @@ 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>

View file

@ -9,6 +9,7 @@
</li> </li>
</ul> </ul>
<div class="bottom"> <div class="bottom">
<small>&copy; 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">
@ -17,8 +18,6 @@
</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">&copy; 2025 by webfussel, webertoire</p>
</footer> </footer>
</template> </template>

View file

@ -1,25 +0,0 @@
<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>

View file

@ -1,30 +0,0 @@
<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>

43
app/components/Pp/FormInput.vue Executable file
View file

@ -0,0 +1,43 @@
<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>

View file

@ -1,28 +1,56 @@
<template> <template>
<div class="Header" :class="[type]"> <header class="Header">
<header class="roboto-condensed"> <NuxtLink to="/">
<NuxtLink class="header-text" to="/"> <strong><span>Pro</span>Papier</strong>
<img class="logo" src="/img/propapier.svg" alt="ProPapier logo" /> </NuxtLink>
<div> <label for="burger_nav_toggle">
<span class="big">ProPapier</span> <Icon name="solar:hamburger-menu-broken" size="2em" />
<span class="small">Vergleichen. Schnell. Unkompliziert.</span> </label>
</div> <input ref="checkbox" type="checkbox" id="burger_nav_toggle"/>
</NuxtLink> <nav class="flex-col">
<button class="burger-button" @click="open()"> <label for="burger_nav_toggle">
<Icon name="uil:bars" size="2em" mode="svg" /> <Icon name="solar:close-circle-broken" />
</button> </label>
</header> <ul class="flex-col">
<div id="subheader" /> <li v-for="navPoint in navPoints" :key="navPoint.label">
</div> <NuxtLink
:to="navPoint.to"
active-class="active"
@click="closeNav()"
>
{{ navPoint.label }}
</NuxtLink>
</li>
</ul>
</nav>
</header>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
type Props = { type NavPoint = {
type ?: 'lp' label: string
to: string
} }
defineProps<Props>() const navPoints = useState<NavPoint[]>('nav', () => ([
{
label: 'Home',
to: '/',
},
{
label: 'Übersicht',
to: '/overview',
},
]))
const cb = useTemplateRef('checkbox')
const closeNav = () => { cb.value!.checked = false }
const nav = useNavigation()
const open = () => { nav.showNavigation() }
</script> </script>
<style scoped>
header a {
text-decoration: none;
color: var(--color-darkest);
}
</style>

View file

@ -1,41 +0,0 @@
<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>

View file

@ -1,5 +1,5 @@
<template> <template>
<article class="PriceCard roboto-condensed" v-ripple="$device.isMobile ? { color: 'rgba(0, 0, 0, 0.1)' } : { duration: 0, scale: 0 }"> <article class="PriceCard card">
<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,7 +13,6 @@
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">
@ -21,10 +20,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>
@ -34,23 +33,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(ppr) }}</span> <span class="value">{{ intl.format(card.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(pps) }}</span> <span class="value">{{ intl.format(card.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(ppl) }}</span> <span class="value">{{ intl.format(card.ppl) }}</span>
</div> </div>
<span class="pro">Pro 1000 {{ card.layers ? `(${card.layers})` : '' }}</span> <span class="pro">Pro 100 {{ card.layers ? `(${card.layers})` : '' }}</span>
</div> </div>
</main> </main>
</div> </div>
@ -58,17 +57,16 @@
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PriceCard } from '../../../shared/PriceCard' import type { Card } from '../../../shared/Card'
type Props = { type Props = {
deletable: boolean deletable: boolean
card: PriceCard card: Card
} }
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')
@ -77,40 +75,22 @@ const intl = Intl.NumberFormat('de-DE', {
currency: 'EUR', currency: 'EUR',
}) })
const { lengthX, direction, isSwiping } = useSwipe(top, { const { lengthX, isSwiping } = useSwipe(top, {
passive: true, passive: false,
threshold: 20, threshold: 30,
onSwipe() { onSwipe() {
if (['down', 'up'].includes(direction.value)) return if (lengthX.value > 50 || lengthX.value < -50) {
left.value = `${-clamp(lengthX.value, 0, 100)}px` left.value = `${-clamp(lengthX.value, -100, 100)}px`
}
}, },
onSwipeEnd() { onSwipeEnd() {
if (['down', 'up'].includes(direction.value)) return if (lengthX.value < -50) update()
if (lengthX.value > 50) { if (lengthX.value > 50) deleteCard()
vibrate(100)
deleteCard()
}
left.value = '0' left.value = '0'
}, },
}) })
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 update = () => emit('update')
const deleteCard = () => emit('remove') const deleteCard = () => emit('remove')
defineExpose({
ppr, pps, ppl, uuid : card.uuid,
})
</script> </script>

View file

@ -1,134 +1,96 @@
<template> <template>
<dialog ref="dialog" closedby="none"> <dialog
<div class="wrapper" ref="wrapper"> ref="dialog"
<form method="dialog"> closedby="any"
<header class="flex-row"> >
{{ currentCardIndex > -1 ? 'Bearbeiten' : 'Neues hinzufügen' }} <form method="dialog">
<PpButton class="round text"> <header class="flex-row padding">
<Icon name="uil:times" mode="svg" /> {{ cardLabel }}
</PpButton> <PpButton class="round text">
</header> <Icon name="uil:times" mode="svg" />
</form> </PpButton>
</header>
</form>
<main v-if="currentCard"> <main v-if="currentCard">
<div class="flex-col gap-default"> <div class="padding flex-col">
<div class="flex-row gap-default"> <div class="flex-row gap-default">
<PpFormTextField <PpFormInput
v-model="currentCard.name" v-model="currentCard.name"
id="card_name" id="card_name"
label="Name" label="Name"
icon="uil:pricetag-alt" :class="{'error': !validFields.name }"
: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"
/> />
<PpFormTextField <PpFormInput
v-model="currentCard.price" v-model="currentCard.price"
id="card_price" id="card_price"
label="Preis" label="Preis"
placeholder="2,49" :class="{'error': !validFields.price }"
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">
<PpFormTextField <PpFormInput
v-model="currentCard.roles" v-model="currentCard.roles"
id="card_roles" id="card_roles"
label="Rollen" label="Rollen"
placeholder="8" :class="{'error': !validFields.roles }"
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"
/> />
<PpFormTextField <PpFormInput
v-model="currentCard.sheets" v-model="currentCard.sheets"
id="card_sheets" id="card_sheets"
label="Blatt" label="Blätter"
placeholder="150" :class="{'error': !validFields.sheets }"
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"
/> />
<PpFormTextField <PpFormInput
v-model="currentCard.layers" v-model="currentCard.layers"
id="card_layers" id="card_layers"
label="Lagen" label="Lagen"
placeholder="3" :class="{'error': !validFields.layers }"
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"> <footer class="flex-row padding">
<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>{{ currentCardIndex > -1 ? 'Übernehmen' : 'Hinzufügen' }}</span> <span>{{ cardLabel }}</span>
</PpButton> </PpButton>
</footer> </footer>
</div>
</dialog> </dialog>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import type { PriceCard } from '../../../shared/PriceCard' import type { Card } from '../../../shared/Card'
type Props = { type Props = {
currentCardIndex: number currentCardIndex : number
currentCard?: PriceCard currentCard ?: Card
} }
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 wrapper = useTemplateRef<HTMLElement>('wrapper') const cardLabel = computed(() => currentCardIndex > -1 ? 'Bearbeiten' : 'Hinzufügen')
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)
} }
@ -141,7 +103,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()
@ -149,7 +111,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()
} }
@ -162,9 +124,6 @@ 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>

View file

@ -1,51 +0,0 @@
<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>

View file

@ -1,24 +0,0 @@
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
}
}

View file

@ -1,11 +0,0 @@
<template>
<div class="page-wrapper">
<PpHeader />
<div class="page">
<NuxtPage />
</div>
<PpFooter />
</div>
</template>
<script setup lang="ts">
</script>

View file

@ -1,11 +0,0 @@
<template>
<div class="page-wrapper">
<PpHeader type="lp" />
<div class="page">
<NuxtPage />
</div>
<PpFooter />
</div>
</template>
<script setup lang="ts">
</script>

View file

@ -1,7 +1,6 @@
<template> <template>
<div> <div>
<section class="Legal flex-col gap-default content full"> <section class="Imp flex-col gap-default content full">
<h1>Impressum</h1>
<div> <div>
<p> <p>
Fiona Lena Urban<br/> Fiona Lena Urban<br/>

252
app/pages/index.vue Normal file → Executable file
View file

@ -1,97 +1,185 @@
<template> <template>
<section class="Home flex-col content full"> <PpDeleteDialog
<div class="home-hero"> ref="deleteModal"
<div class="text"> :current-card-index="currentCardIndex"
<h1> @delete="removeCard(currentCardIndex)"
Du zahlst zuviel für's Papier? />
</h1> <PpPriceCardDialog
<NuxtLink to="/rechner"> ref="modal"
<PpButton class="cta">Preise vergleichen</PpButton> :current-card="currentCard"
</NuxtLink> :current-card-index="currentCardIndex"
<h2> @update="updateCard()"
Mit ProPapier Preise vergleichen und sparen. />
</h2> <section class="content flex-col">
</div> <aside class="filter-bar">
</div> <PpButtonGroup
<div class="home-text padding"> :buttons="filterButtons"
<p> @click="sort"
Mit <strong>ProPapier</strong> vergleichst du schnell & unkompliziert Preise für Klopapier und sparst so bares Geld. />
</p> </aside>
</div> <div class="pc-wrapper flex-col" role="list">
<div class="home-text padding"> <PpPriceCard
<h3> v-for="(card, index) in cards"
Wir haben noch viel vor! :key="card.uuid"
</h3> :deletable="cards.length > 1"
<p class="padding"> :card="card"
Für ProPapier sind über die nächste Zeit noch einige weitere Features geplant. @update="openModal(false, index)"
</p> @remove="openDeleteModal()"
<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 { TimelineCard } from '../../shared/TimelineCard' import type { Card } from '../../shared/Card'
import type { Button } from '../../shared/ButtonGroup'
import { PpPriceCardDialog, PpDeleteDialog } from '#components'
definePageMeta({ const currentSort = ref(0)
layout: 'landingpage' const isDirty = ref(false)
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 timeline : TimelineCard[] = [ const cards = useState<Card[]>('cards', () => [
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[]>([
{ {
icon: 'uil:chart-bar', label: 'Rollen',
title: 'Mehr Vergleiche', icon: 'uil:toilet-paper',
description: 'Zusätzliche Kategorien für Taschentücher und Küchenrolle',
state: {
value: 'inProgress',
}
}, },
{ {
icon: 'uil:cloud-database-tree', label: 'Blatt',
title: 'Datenbank', icon: 'uil:file-landscape',
description: 'Eine von der Community gestützte Datenbank mit Preisen für alle Produkte',
state: {
value: 'planned',
message: '2025',
}
}, },
{ {
icon: 'uil:qrcode-scan', label: 'Lagen',
title: 'Barcode Scan', icon: 'uil:layer-group',
description: 'Ganz einfach Barcode Scannen und Produkt direkt zum Rechner hinzufügen',
state: {
value: 'planned',
message: '2025',
}
}, },
{ ])
icon: 'uil:user',
title: 'Optionale Accounts', const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
description: 'Zur Synchronisierung auf mehreren Geräten', cards.value.sort((a : Card, b : Card) => a[key] - b[key])
state: { }
value: 'planned',
} const sort = (index : number) => {
}, currentSort.value = index
{ filterButtons.value.forEach(button => { button.active = false })
icon: 'uil:cog', filterButtons.value[index]!.active = true
title: 'Personalisierung',
description: 'Persönliche Präferenzen zur Wortwahl, Standardsortierung und mehr', switch (index) {
state: { case 0:
value: 'planned', sortBy('ppr')
} break
}, case 1:
{ sortBy('pps')
icon: 'uil:vector-square', break
title: 'm² Preise', case 2:
description: 'Quadratmeterpreise für noch genauere Vergleiche', sortBy('ppl')
state: { break
value: 'planned', }
}
}, updateLocalStorage()
] isDirty.value = false
</script> }
const calculate = (card : Card) => {
const ppr = +replaceComma(card.price) / +card.roles
const pps = (ppr / +card.sheets) * 100
const ppl = (pps / +card.layers)
return { ppr, pps, ppl }
}
onMounted(() => {
const cardsFromStorage = JSON.parse(localStorage.getItem('cards') ?? '[]').map((card : Card) => ({ ...card, ...calculate(card) }))
cards.value = cardsFromStorage.length !== 0 ? cardsFromStorage : cards.value
const sortFromStorage = +JSON.parse(localStorage.getItem('sort') ?? '0')
sort(sortFromStorage)
filterButtons.value[sortFromStorage]!.active = true
})
</script>

81
app/pages/overview.vue Executable file
View file

@ -0,0 +1,81 @@
<template>
<section class="content flex-col">
<aside class="filter-bar">
<PpButtonGroup
:buttons="filterButtons"
@click="sort"
/>
</aside>
<div class="pc-wrapper flex-col" role="list">
<p
v-for="product in products"
:key="product.sku"
>
{{ product.name }} - {{ product.price }}
</p>
</div>
</section>
</template>
<script setup lang="ts">
import type { Card } from '../../shared/Card'
import type { Button } from '../../shared/ButtonGroup'
import type { Product } from '../../shared/db/Product'
const currentSort = ref(0)
const createCard = (uuid : string) : Card => ({
uuid,
name: '',
price: 0,
roles: 0,
sheets: 0,
layers: 0,
ppr: 0,
pps: 0,
ppl: 0,
})
const { data : products } = useFetch<Product[]>('/api/products')
const cards = useState('cards', () => [
createCard(crypto.randomUUID()),
])
const filterButtons = ref<Button[]>([
{
label: 'Rollen',
icon: 'uil:toilet-paper',
},
{
label: 'Blatt',
icon: 'uil:file-landscape',
},
{
label: 'Lagen',
icon: 'uil:layer-group',
},
])
const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
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
}
}
</script>

View file

@ -1,10 +1,13 @@
<template> <template>
<section class="Legal flex-col gap-default content full"> <section class="flex-col gap-default content full">
<h1> <h3>
Datenschutzerklärung Datenschutzerklärung
</h1> </h3>
<p> <p>
Wir sammeln anonyme Daten zum Erstellen von Statistiken über die Anzahl der Besuche auf unserer Seite, um herauszufinden, wie viel Pflegeaufwand und Rechenleistung benötigt wird. Wir senden keinerlei Daten an Drittanbieter.
</p>
<p>
Wir verwerten keinerlei Daten.
</p> </p>
<p> <p>
Alle persistierten Daten befinden sich ausschließlich auf Ihrem Endgerät im sogenannten "localStorage" und werden ausschließlich auf Ihrem Gerät verarbeitet. Alle persistierten Daten befinden sich ausschließlich auf Ihrem Endgerät im sogenannten "localStorage" und werden ausschließlich auf Ihrem Gerät verarbeitet.

View file

@ -1,151 +0,0 @@
<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>

View file

@ -1,5 +1,3 @@
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

View file

@ -1,7 +0,0 @@
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);
})
}

View file

@ -1,5 +1,4 @@
const description = 'Du zahlst zuviel für\'s Papier? Vergleiche schnell und unkompliziert die Preise für Toiletten-, Küchen- und andere Papier hier.' // https://nuxt.com/docs/api/configuration/nuxt-config
export default defineNuxtConfig({ export default defineNuxtConfig({
compatibilityDate: '2024-11-01', compatibilityDate: '2024-11-01',
devtools: { enabled: false }, devtools: { enabled: false },
@ -17,20 +16,6 @@ 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 },
@ -41,71 +26,24 @@ export default defineNuxtConfig({
'@nuxt/icon', '@nuxt/icon',
'@vueuse/nuxt', '@vueuse/nuxt',
'@nuxtjs/device', '@nuxtjs/device',
'@nuxt/fonts', 'nuxt-mongoose',
'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/timelineCard.css', './app/assets/styles/formInput.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: { mongoose: {
url: 'https://pro-papier.de', uri: `${process.env.MONGODB_URI}`,
name: 'ProPapier', modelsDir: 'models',
devtools: true,
}, },
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'],
},
}) })

6983
package-lock.json generated Executable file → Normal file

File diff suppressed because it is too large Load diff

View file

@ -3,27 +3,22 @@
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"prepare": "nuxt prepare",
"build": "nuxt build", "build": "nuxt build",
"dev": "nuxt dev", "dev": "nuxt dev",
"dev:expose": "nuxt dev --host", "dev:expose": "nuxt dev --host",
"generate": "nuxt generate", "generate": "nuxt generate",
"preview": "npx serve .output/public", "preview": "nuxt preview",
"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-mongoose": "^1.0.6",
"nuxt-seo-utils": "^7.0.11",
"vue": "latest", "vue": "latest",
"vue-router": "latest" "vue-router": "latest"
} }

BIN
public/favicon.ico Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View file

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -1 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 384 KiB

View file

@ -1 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 3.3 KiB

View file

@ -0,0 +1,9 @@
export default defineEventHandler(async () => {
try {
return await ProductSchema.find()
} catch (error) {
console.error(error)
return []
}
})

View file

@ -0,0 +1,49 @@
import { defineMongooseModel } from '#nuxt/mongoose'
export const ProductSchema = defineMongooseModel({
name: 'products',
schema: {
name: {
type: String,
required: true,
},
brand: {
type: String,
required: true,
},
image: {
type: String,
required: true,
unique: true,
},
market: {
type: String,
required: true,
},
category: {
type: String,
required: true,
},
sku: {
type: String,
required: true,
unique: true,
},
price: {
type: Number,
required: true,
},
pieces: {
type: Number,
required: true,
},
sheets: {
type: Number,
required: true,
},
layers: {
type: Number,
required: true,
},
}
})

11
shared/Card.ts Normal file
View file

@ -0,0 +1,11 @@
export type Card = {
ppr: number
pps: number
ppl: number
uuid: string
name: string
price: string
roles: string
sheets: string
layers: string
}

View file

@ -1,8 +0,0 @@
export type PriceCard = {
uuid : string
name : string
price : string
roles : string
sheets : string
layers : string
}

View file

@ -1,11 +0,0 @@
export type TimelineState = 'planned' | 'inProgress' | 'done'
export type TimelineCard = {
icon: string
title: string
description: string
state: {
value: TimelineState
message ?: string
}
}

12
shared/db/Product.ts Normal file
View file

@ -0,0 +1,12 @@
export type Product = {
name: string
brand: string
image: string
market: string
category: string
sku: string
price: number
pieces: number
sheets: number
layers: number
}