Compare commits

..

3 commits

Author SHA1 Message Date
0a73fcbcf5 add: (WIP) category view
Removed h1 from calculator, add preparation for category view
2025-05-22 07:47:44 +02:00
50ef15c1a6 add: new filtering layout
Correct icon for sorting ascending/descending
2025-05-15 07:36:28 +02:00
3fd26b4e66 add: (wip) new filtering layout
DropDown component, ToggleButton component,
2025-05-13 20:43:15 +02:00
44 changed files with 661 additions and 1250 deletions

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

@ -1,18 +1,11 @@
<template>
<PpNavigation />
<NuxtLayout>
<NuxtPage />
</NuxtLayout>
<div class="page-wrapper">
<PpHeader />
<div class="page">
<NuxtPage />
</div>
<PpFooter />
</div>
</template>
<style>
.page-enter-active,
.page-leave-active {
transition: all 200ms;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(.5rem);
}
</style>
<script setup lang="ts">
</script>

11
app/assets/icons/sort_asc.svg Executable file
View file

@ -0,0 +1,11 @@
<?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 100 100"
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: currentColor">
<path d="M26.208,59.542l-1.208,1.25l-0,-31.625c-0,-2.286 -1.881,-4.167 -4.167,-4.167c-2.285,0 -4.166,1.881 -4.166,4.167l-0,31.625l-1.209,-1.25c-0.784,-0.785 -1.849,-1.226 -2.958,-1.226c-2.295,0 -4.184,1.889 -4.184,4.184c0,1.109 0.441,2.174 1.226,2.958l8.333,8.334c0.396,0.379 0.864,0.676 1.375,0.875c1.008,0.445 2.159,0.445 3.167,-0c0.511,-0.199 0.978,-0.496 1.375,-0.875l8.333,-8.334c0.784,-0.784 1.225,-1.849 1.225,-2.958c0,-2.295 -1.888,-4.184 -4.183,-4.184c-1.109,0 -2.174,0.441 -2.959,1.226m19.625,-26.209l12.5,0c2.286,0 4.167,-1.881 4.167,-4.166c-0,-2.286 -1.881,-4.167 -4.167,-4.167l-12.5,0c-2.285,0 -4.166,1.881 -4.166,4.167c-0,2.285 1.881,4.166 4.166,4.166m41.667,12.5l-41.667,0c-2.285,0 -4.166,1.881 -4.166,4.167c-0,2.286 1.881,4.167 4.166,4.167l25,-0c2.286,-0 4.167,-1.881 4.167,-4.167c-0,-2.286 -1.881,-4.167 -4.167,-4.167m16.667,20.834l-41.667,-0c-2.285,-0 -4.166,1.881 -4.166,4.166c-0,2.286 1.881,4.167 4.166,4.167l41.667,0c2.286,0 4.167,-1.881 4.167,-4.167c-0,-2.285 -1.881,-4.166 -4.167,-4.166"
style="fill: currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

4
app/assets/icons/sort_desc.svg Executable file
View file

@ -0,0 +1,4 @@
<?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 100 100" 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;"><path d="M26.208,59.542l-1.208,1.25l-0,-31.625c-0,-2.286 -1.881,-4.167 -4.167,-4.167c-2.285,0 -4.166,1.881 -4.166,4.167l-0,31.625l-1.209,-1.25c-0.784,-0.785 -1.849,-1.226 -2.958,-1.226c-2.295,0 -4.184,1.889 -4.184,4.184c0,1.109 0.441,2.174 1.226,2.958l8.333,8.334c0.396,0.379 0.864,0.676 1.375,0.875c1.008,0.445 2.159,0.445 3.167,-0c0.511,-0.199 0.978,-0.496 1.375,-0.875l8.333,-8.334c0.784,-0.784 1.225,-1.849 1.225,-2.958c0,-2.295 -1.888,-4.184 -4.183,-4.184c-1.109,0 -2.174,0.441 -2.959,1.226m19.625,-26.209l41.667,0c2.286,0 4.167,-1.881 4.167,-4.166c-0,-2.286 -1.881,-4.167 -4.167,-4.167l-41.667,0c-2.285,0 -4.166,1.881 -4.166,4.167c-0,2.285 1.881,4.166 4.166,4.166m41.667,12.5l-41.667,0c-2.285,0 -4.166,1.881 -4.166,4.167c-0,2.286 1.881,4.167 4.166,4.167l25,-0c2.286,-0 4.167,-1.881 4.167,-4.167c-0,-2.286 -1.881,-4.167 -4.167,-4.167m16.667,20.834l-41.667,-0c-2.285,-0 -4.166,1.881 -4.166,4.166c-0,2.286 1.881,4.167 4.166,4.167l12.5,0c2.286,0 4.167,-1.881 4.167,-4.167c-0,-2.285 -1.881,-4.166 -4.167,-4.166" style="fill: currentColor"/></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -1,5 +1,5 @@
.Button {
--padding: var(--padding-xs);
--padding: .2rem;
--background: var(--color-main-dark);
--color: var(--color-lightest);
--background-hover: var(--color-main-dark);
@ -8,7 +8,7 @@
display: flex;
justify-content: center;
align-items: center;
gap: var(--padding-default);
gap: 1rem;
cursor: pointer;
transition: var(--transition-default);
outline: none;
@ -19,13 +19,13 @@
&.transparent {
--background: transparent;
box-shadow: none;
padding: var(--padding-s) var(--padding-l);
padding: .5em 1.5em;
border-radius: var(--radius-default);
}
&.raised {
box-shadow: var(--box-shadow-z2);
padding: var(--padding-s) var(--padding-l);
padding: .5em 1.5em;
border-radius: var(--radius-default);
&.danger {
@ -37,7 +37,7 @@
&.text {
--background: transparent;
--color: var(--color-darkest);
padding: var(--padding-s) var(--padding-l);
padding: .5em 1.5em;
border-radius: var(--radius-default);
&:hover {
@ -62,13 +62,13 @@
justify-content: center;
align-items: center;
border-radius: 100%;
padding: var(--padding-s);
padding: .5rem;
}
&.cta {
background: var(--background);
color: var(--color);
padding: var(--padding-s) var(--padding-l);
padding: .5rem 1.5rem;
border-radius: var(--radius-default);
box-shadow: var(--box-shadow-z2);
@ -98,7 +98,7 @@
--color: var(--color-main-darkest);
border-radius: 100%;
padding: var(--padding);
font-size: var(--font-size-l);
font-size: 1.1em;
&:hover {
scale: 1.2;
@ -106,17 +106,17 @@
}
&.mini-button {
padding: var(--padding-s) var(--padding-l);
padding: .5rem 1.5rem;
display: flex;
flex-direction: column;
gap: var(--padding-xs);
gap: .5rem;
& > .icon {
font-size: var(--font-size-xl);
font-size: 1.5rem;
}
& > span {
font-size: var(--font-size-s);
font-size: .8rem;
}
}
}

View file

@ -9,8 +9,8 @@
display: flex;
align-items: center;
justify-content: center;
gap: var(--padding-xs);
padding: var(--padding-s);
gap: .5rem;
padding: .5rem;
flex-grow: 1;
background: var(--background);
color: var(--color);

View file

@ -2,12 +2,12 @@
dialog {
top: 50%;
left: 50%;
width: clamp(400px, 100vw, calc(var(--page-max-width) - var(--padding-xxl) * 2));
width: 100vw;
translate: -50% -50%;
border: none;
border-radius: var(--radius-default);
background: var(--color-lightest);
font-size: var(--font-size-default);
font-size: 1rem;
color: var(--color-darkest);
position: relative;
@ -40,7 +40,7 @@ dialog {
& > .wrapper {
display: flex;
flex-direction: column;
gap: var(--padding-default);
gap: 1rem;
}
& header {

View file

@ -12,38 +12,27 @@
& .bottom {
display: flex;
justify-content: center;
gap: 1rem;
justify-content: space-between;
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 {
font-size: var(--font-size-xl);
font-size: 1.5rem;
justify-content: center;
margin-bottom: var(--padding-xl);
margin-bottom: 2rem;
}
& .data-links {
justify-content: flex-end;
font-size: var(--font-size-s);
font-size: .8rem;
}
& ul {
list-style: none;
display: flex;
align-items: center;
gap: var(--padding-default);
gap: 1rem;
& a {
color: var(--color-lightest);

View file

@ -0,0 +1,64 @@
.DropDown {
--border-color: var(--color-light);
--label-color: var(--color-darkest);
--background-color: var(--color-lightest);
--arrow-color: var(--color-main-dark);
position: relative;
&.active {
--background-color: var(--color-main-dark);
--label-color: var(--color-lightest);
--arrow-color: var(--color-lightest);
--border-color: var(--color-main-dark);
& ul {
scale: 1 1;
}
}
& button {
all: unset;
cursor: pointer;
color: var(--label-color);
border: 1px solid var(--border-color);
border-radius: var(--radius-default);
background: var(--background-color);
padding: .5rem 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: .5rem;
transition: var(--transition-default);
width: 70px;
& .icon {
color: var(--arrow-color);
}
}
& ul {
position: absolute;
list-style: none;
transform-origin: top;
scale: 1 0;
transition: var(--transition-default);
color: var(--color-darkest);
border: 1px solid var(--color-main-dark);
border-radius: var(--radius-default);
background: var(--color-lightest);
box-shadow: var(--box-shadow-z2);
width: 100%;
z-index: 2000;
top: 0;
& li {
padding: .5rem 1rem;
&.selected {
background: var(--color-main-dark);
color: var(--color-lightest);
}
}
}
}

View file

@ -1,15 +1,15 @@
.Search {
border-radius: 9999px;
background: var(--color-lightest);
padding: 0 var(--padding-default);
padding: 0 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--padding-default);
gap: 1rem;
& > input {
all: unset;
flex-grow: 1;
padding: var(--padding-s) 0;
padding: .5rem 0;
}
}

View file

@ -1,9 +1,9 @@
.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);
--label-position-top: 0.4em;
--label-font-size: 1rem;
--icon-color: var(--color-main-dark);
--message-color: var(--color-middle);
position: relative;
@ -15,11 +15,15 @@
&:focus-within,
&:has(input:not(:placeholder-shown)) {
--icon-color: var(--color-main-dark);
--label-color: var(--color-main-dark);
--label-position-top: -11px;
--label-font-size: 0.8rem;
}
&.error {
--label-color: var(--color-error) !important;
--label-color: var(--color-error);
--icon-color: var(--color-error);
--border-color: var(--color-error);
--message-color: var(--color-error);
}
@ -27,46 +31,37 @@
& .wrapper {
display: flex;
align-items: center;
padding: var(--padding-xxs) var(--padding-xs);
padding: 0.1rem 0.5rem;
border: 1px solid var(--border-color);
border-radius: var(--radius-default);
}
& label {
position: absolute;
display: flex;
align-items: center;
background: var(--color-lightest);
padding: 0.2rem;
left: 1.8rem;
color: var(--label-color);
gap: 2px;
top: -6px;
left: calc(var(--padding-xs) - 2px);
font-size: var(--font-size-s);
top: var(--label-position-top);
font-size: var(--label-font-size);
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;
color: var(--icon-color);
}
& input {
all: unset;
padding: var(--padding-xxs) var(--padding-s) var(--padding-xxs) 0;
font-size: var(--font-size-s);
padding: var(--padding-small);
font-size: 1rem;
width: 100%;
flex: 25% 1 0;
color: var(--color-darkest);
}
& > span {
& span {
color: var(--message-color);
font-size: var(--font-size-xs);
font-size: 0.6rem;
}
}

View file

@ -0,0 +1,14 @@
.ToggleButton {
all: unset;
color: var(--color-lightest);
background: var(--color-main-dark);
padding: .5rem .8rem;
border-radius: var(--radius-default);
display: flex;
align-items: center;
justify-content: center;
& .icon {
font-size: 1.2rem;
}
}

View file

@ -1,7 +1,14 @@
:root {
--padding-default: 1rem;
--padding-small: 0.5rem;
--padding-large: 2rem;
--radius-default: 3px;
--radius-border: 15px;
--transition-default: 150ms;
--color-success: #328104;
--color-error: #a20606;
--color-blue-light: #d7e1f1;
--color-blue-light: #0ddce7;
--color-blue: #05b0ff;
--color-blue-dark: #0266f2;
--color-blue-darkest: #013174;
@ -19,7 +26,6 @@
--color-green-darkest-most: #157c2a;
--color-main: var(--color-blue);
--color-main-lightest: var(--color-blue-light);
--color-main-light: var(--color-blue-light);
--color-main-dark: var(--color-blue-dark);
--color-main-darkest: var(--color-blue-darkest);
@ -29,9 +35,6 @@
--color-accent-dark: var(--color-green-dark);
--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-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));
@ -42,31 +45,6 @@
--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-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;
}
* {
@ -80,35 +58,44 @@ body {
height: 100%;
overflow-x: hidden;
font-family: sans-serif;
color: var(--color-text);
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;
background: var(--color-main-darkest);
}
.dot {
--size: 10px;
width: var(--size);
height: var(--size);
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;
}
}
h1, h2, h3 {
margin: var(--padding-xl) 0 var(--padding-default);
text-wrap: balance;
hyphens: auto;
font-size: var(--font-size-xl);
}
@keyframes pulse {
0% {
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);
}
h2 {
margin: var(--padding-l) 0 var(--padding-default);
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;
100% {
transform: scale(0.95);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
.card {
@ -128,7 +115,7 @@ a:has(button) {
}
.text-white {
color: var(--color-text-invert);
color: var(--color-lightest);
}
.bg-main {
@ -143,16 +130,12 @@ a:has(button) {
background-color: var(--color-main-dark);
}
.bg-main-darkest {
background-color: var(--color-main-darkest);
}
.bg-main-dark-hover:hover {
background-color: var(--color-main-dark);
}
.gap-default {
gap: var(--padding-default);
gap: 1rem;
}
.bg-blue {
@ -164,13 +147,13 @@ a:has(button) {
}
.padding {
gap: var(--padding-default);
gap: 1rem;
padding: var(--padding-default);
}
.padding-small {
gap: var(--padding-default);
padding: var(--padding-s);
gap: 1rem;
padding: var(--padding-small);
}
.roboto-condensed {
@ -187,5 +170,5 @@ a:has(button) {
.grow {
flex-grow: 1;
height: var(--padding-s);
height: 10px;
}

View file

@ -2,40 +2,20 @@
position: sticky;
top: 0;
z-index: 100;
background: var(--color-main-darkest);
display: flex;
align-items: center;
gap: var(--padding-default);
gap: 1rem;
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 {
@ -46,7 +26,7 @@
& .header-text {
display: flex;
align-items: center;
gap: var(--padding-default);
gap: 1rem;
& > div {
display: flex;
@ -54,35 +34,69 @@
}
& .big {
font-size: var(--font-size-xl);
font-size: 1.5em;
}
& .small {
font-size: var(--font-size-s);
font-size: .8em;
font-weight: 100;
}
}
& 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;
& li {
list-style: none;
}
}
}
&: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, &: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);
}
&: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);
}
&: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

@ -14,24 +14,25 @@
}
.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;
& .box {
display: flex;
flex-direction: column;
gap: .5rem;
&.active {
color: var(--color-main-darkest);
& strong {
color: var(--color-darkest);
font-size: .8rem;
}
&:not(.active) {
opacity: .5;
& > div {
background: var(--color-lightest);
display: flex;
justify-content: flex-end;
gap: .5rem;
}
}
}
@ -49,18 +50,10 @@
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;
padding: var(--padding-large) var(--padding-default) var(--padding-default);
color: var(--color-darkest);
text-align: center;
}
@ -75,6 +68,6 @@
}
.Legal {
padding: var(--padding-l) var(--padding-default);
padding: var(--padding-large) var(--padding-default);
color: var(--color-darkest);
}

View file

@ -15,7 +15,7 @@
& > * {
flex-grow: 1;
color: var(--color-lightest);
font-size: var(--font-size-xl);
font-size: 2rem;
display: flex;
align-items: center;
@ -26,13 +26,13 @@
& .bg-edit {
background: var(--color-main-dark);
padding: var(--padding-xl);
padding: 2rem;
text-align: left;
}
& .bg-delete {
background: var(--color-error);
padding: var(--padding-xl);
padding: 2rem;
text-align: right;
justify-content: flex-end;
}
@ -42,7 +42,7 @@
position: relative;
background: var(--color-lightest);
z-index: 2;
gap: var(--padding-default);
gap: 1rem;
padding: var(--padding-default);
&.animated {
@ -56,14 +56,14 @@
color: var(--color-darkest);
& .icon {
font-size: var(--font-size-default);
font-size: 1rem;
cursor: pointer;
}
}
& .name-price {
display: flex;
gap: var(--padding-xs);
gap: .5rem;
& > span:nth-child(1) {
font-weight: bold;
@ -75,7 +75,7 @@
& > span:nth-child(2)::before {
content: '•';
margin-right: var(--padding-xs);
margin-right: .5rem;
color: var(--color-middle);
}
}
@ -84,19 +84,19 @@
display: flex;
flex-direction: row;
width: 100%;
gap: var(--padding-default);
gap: 1rem;
justify-content: space-between;
& > .info {
flex-grow: 0;
align-items: center;
gap: var(--padding-xxs);
gap: .25rem;
font-weight: bold;
& > .price {
display: flex;
align-items: center;
gap: var(--padding-xs);
gap: .5rem;
& > .icon {
color: var(--color-main-dark);
@ -104,7 +104,7 @@
}
& > .pro {
font-size: var(--font-size-xs);
font-size: .6rem;
color: var(--color-middle);
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

@ -8,6 +8,7 @@
box-shadow: var(--box-shadow-upper);
& > .Button {
font-size: var(--font-size-default);
--padding: 1rem;
font-size: 1rem;
}
}

View file

@ -9,6 +9,7 @@
</li>
</ul>
<div class="bottom">
<small>&copy; 2025 by webfussel</small>
<ul class="data-links">
<li v-for="dataLink in dataLinks">
<NuxtLink :to="dataLink.to">
@ -17,8 +18,6 @@
</li>
</ul>
</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>
</template>

View file

@ -0,0 +1,45 @@
<template>
<div
class="DropDown"
:class="{ active }"
ref="dropdown"
>
<button @click="active = !active">
<span>{{ current.label }}</span>
<Icon class="icon" name="uil:angle-down" mode="svg" />
</button>
<ul>
<li
v-for="element in elements"
@click="click(element)"
:class="{ selected : element.value === current.value }"
>
{{ element.label }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import type { DropDownElement } from '../../../../shared/DropDown'
type Props = {
elements : DropDownElement[]
current : DropDownElement
}
const { elements = [] } = defineProps<Props>()
const emit = defineEmits(['click'])
const active = ref(false)
const dropdown = useTemplateRef<HTMLDivElement>('dropdown')
const click = (element : DropDownElement) => {
emit('click', element)
active.value = false
}
onMounted(() => {
onClickOutside(dropdown, () => active.value = false)
})
</script>

View file

@ -1,11 +1,9 @@
<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>
<Icon v-if="icon" class="icon" :name="icon" mode="svg" />
<input v-model="text" :type="type" :id="id" placeholder=" " @blur="emit('blur')" @input="emit('input')" :inputmode="mode" />
<label :for="id">{{ label }}</label>
</div>
<span v-if="message">{{ message }}</span>
</div>
@ -13,13 +11,12 @@
<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'
type?: 'text' | 'number';
message?: string;
icon?: string;
label: string;
id: string;
mode?: 'text' | 'email' | 'search' | 'tel' | 'url' | 'none' | 'numeric' | 'decimal';
};
const { type = "text", mode = "text" } = defineProps<Props>();

View file

@ -0,0 +1,21 @@
<template>
<button class="ToggleButton" @click="click">
<Icon class="icon" :name="icons[current]!" mode="svg" />
</button>
</template>
<script setup lang="ts">
type Props = {
icons : string[]
current: number
}
defineProps<Props>()
const emit = defineEmits(['click'])
const click = () => emit('click')
</script>
<style scoped>
</style>

View file

@ -1,28 +1,33 @@
<template>
<div class="Header" :class="[type]">
<header class="roboto-condensed">
<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>
<button class="burger-button" @click="open()">
<Icon name="uil:bars" size="2em" mode="svg" />
</button>
</header>
<div id="subheader" />
<div class="Header">
<div>
<header class="roboto-condensed">
<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>
<label for="burger_nav_toggle" v-if="available">
<Icon name="solar:hamburger-menu-broken" size="2em" />
</label>
<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>
<div id="subheader" />
</div>
</div>
</template>
<script setup lang="ts">
type Props = {
type ?: 'lp'
}
defineProps<Props>()
const nav = useNavigation()
const open = () => { nav.showNavigation() }
const available = false
</script>

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>
<article class="PriceCard roboto-condensed" v-ripple="$device.isMobile ? { color: 'rgba(0, 0, 0, 0.1)' } : { duration: 0, scale: 0 }">
<article class="PriceCard roboto-condensed" v-ripple="{ color: 'rgba(0, 0, 0, 0.1)' }">
<div class="bottom">
<div class="bg-edit">
<Icon class="icon" name="uil:pen" mode="svg" />
@ -13,7 +13,7 @@
class="top flex-col"
:class="{ 'animated' : !isSwiping }"
:style="{ left }"
@click="cardClick"
@click="update"
>
<header>
<div class="name-price">
@ -58,11 +58,11 @@
</template>
<script setup lang="ts">
import type { PriceCard } from '../../../shared/PriceCard'
import type { Card } from '../../../shared/Card'
type Props = {
deletable: boolean
card: PriceCard
card: Card
}
const { card } = defineProps<Props>()
@ -86,10 +86,8 @@ const { lengthX, direction, isSwiping } = useSwipe(top, {
},
onSwipeEnd() {
if (['down', 'up'].includes(direction.value)) return
if (lengthX.value > 50) {
vibrate(100)
deleteCard()
}
if (lengthX.value > 50) deleteCard()
vibrate(100)
left.value = '0'
},
})
@ -99,13 +97,6 @@ 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')

View file

@ -16,8 +16,7 @@
v-model="currentCard.name"
id="card_name"
label="Name"
icon="uil:pricetag-alt"
:placeholder="randomName"
icon="uil:user"
:class="{ error: !validFields.name }"
:message="!validFields.name ? 'Feld darf nicht leer sein.' : ''"
@input="validFields.name = true"
@ -26,7 +25,6 @@
v-model="currentCard.price"
id="card_price"
label="Preis"
placeholder="2,49"
icon="uil:euro"
mode="decimal"
:class="{ error: !validFields.price }"
@ -39,7 +37,6 @@
v-model="currentCard.roles"
id="card_roles"
label="Rollen"
placeholder="8"
icon="uil:toilet-paper"
mode="decimal"
:class="{ error: !validFields.roles }"
@ -50,7 +47,6 @@
v-model="currentCard.sheets"
id="card_sheets"
label="Blatt"
placeholder="150"
icon="uil:file-landscape"
mode="decimal"
:class="{ error: !validFields.sheets }"
@ -61,7 +57,6 @@
v-model="currentCard.layers"
id="card_layers"
label="Lagen"
placeholder="3"
icon="uil:layer-group"
mode="decimal"
:class="{ error: !validFields.layers }"
@ -86,11 +81,11 @@
</template>
<script setup lang="ts">
import type { PriceCard } from '../../../shared/PriceCard'
import type { Card } from '../../../shared/Card'
type Props = {
currentCardIndex: number
currentCard?: PriceCard
currentCard?: Card
}
const { currentCardIndex, currentCard } = defineProps<Props>()
@ -99,25 +94,6 @@ const emit = defineEmits(['update'])
const dialog = useTemplateRef<HTMLDialogElement>('dialog')
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 = () => {
if (!currentCard) return false
if (currentCard.price.length === 0) return false
@ -162,7 +138,6 @@ onMounted(() => {
validFields.roles = true
validFields.sheets = true
validFields.layers = true
randomName.value = generateRandomName()
})
onClickOutside(wrapper, () => dialog.value?.close())

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>

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

@ -1,97 +1,188 @@
<template>
<section class="Home flex-col content full">
<div class="home-hero">
<div class="text">
<h1>
Du zahlst zuviel für's Papier?
</h1>
<NuxtLink to="/rechner">
<PpButton class="cta">Preise vergleichen</PpButton>
</NuxtLink>
<h2>
Mit ProPapier Preise vergleichen und sparen.
</h2>
</div>
</div>
<div class="home-text padding">
<p>
Mit <strong>ProPapier</strong> vergleichst du schnell & unkompliziert Preise für Klopapier und sparst so bares Geld.
</p>
</div>
<div class="home-text padding">
<h3>
Wir haben noch viel vor!
</h3>
<p class="padding">
Für ProPapier sind über die nächste Zeit noch einige weitere Features geplant.
</p>
<div class="timeline">
<PpTimelineCard
v-for="card in timeline"
v-bind="card"
/>
</div>
</div>
</section>
<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">
<div class="box">
<strong>Kategorie:</strong>
<div>
<PpFormDropDown
:elements="sortElements"
:current="currentSortElement!"
@click="setSortElement"
/>
</div>
</div>
<div class="box">
<strong>Preise Sortieren nach:</strong>
<div>
<PpFormDropDown
:elements="sortElements"
:current="currentSortElement!"
@click="setSortElement"
/>
<PpFormToggleButton
:icons="['pp:sort-desc', 'pp:sort-asc']"
:current="currentSortDirection"
@click="countSortDirection"
/>
</div>
</div>
</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 { TimelineCard } from '../../shared/TimelineCard'
import type { Card } from '../../shared/Card'
import type { Button } from '../../shared/ButtonGroup'
import { PpPriceCardDialog, PpDeleteDialog, PpPriceCard } from '#components'
import type { DropDownElement } from '../../shared/DropDown'
definePageMeta({
layout: 'landingpage'
const cards = useLocalStorage<Card[]>('cards', [])
const currentSort = useLocalStorage<number>('sort', 0)
const currentSortDirection = useLocalStorage<number>('sortDirection', 0)
const currentCard = ref<Card>()
const currentCardIndex = ref<number>(-1)
const modal = useTemplateRef<typeof PpPriceCardDialog>('modal')
const deleteModal = useTemplateRef<typeof PpDeleteDialog>('deleteModal')
const priceCards = useTemplateRef<(typeof PpPriceCard)[]>('priceCard')
const sortElements : DropDownElement[] = [
{
label: 'Rollen',
value: 0,
},
{
label: 'Blatt',
value: 1,
},
{
label: 'Lagen',
value: 2,
}
]
const currentSortElement = ref<DropDownElement>(sortElements[0]!)
const setSortElement = (element : DropDownElement) => {
sort(element.value as number)
}
const createCard = (uuid : string) : Card => ({
uuid,
name: '',
price: '',
roles: '',
sheets: '',
layers: '',
})
const timeline : TimelineCard[] = [
{
icon: 'uil:chart-bar',
title: 'Mehr Vergleiche',
description: 'Zusätzliche Kategorien für Taschentücher und Küchenrolle',
state: {
value: 'inProgress',
const addCard = (card : Card) => {
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 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
if (currentSortDirection.value === 0) {
return bCard[key] - aCard[key]
}
},
{
icon: 'uil:cloud-database-tree',
title: 'Datenbank',
description: 'Eine von der Community gestützte Datenbank mit Preisen für alle Produkte',
state: {
value: 'planned',
message: '2025',
}
},
{
icon: 'uil:qrcode-scan',
title: 'Barcode Scan',
description: 'Ganz einfach Barcode Scannen und Produkt direkt zum Rechner hinzufügen',
state: {
value: 'planned',
message: '2025',
}
},
{
icon: 'uil:user',
title: 'Optionale Accounts',
description: 'Zur Synchronisierung auf mehreren Geräten',
state: {
value: 'planned',
}
},
{
icon: 'uil:cog',
title: 'Personalisierung',
description: 'Persönliche Präferenzen zur Wortwahl, Standardsortierung und mehr',
state: {
value: 'planned',
}
},
{
icon: 'uil:vector-square',
title: 'm² Preise',
description: 'Quadratmeterpreise für noch genauere Vergleiche',
state: {
value: 'planned',
}
},
]
</script>
return aCard[key] - bCard[key]
})
}
const sort = async (index : number = currentSort.value) => {
currentSort.value = index
currentSortElement.value = sortElements[index]!
await nextTick()
switch (index) {
case 0: return sortBy('ppr')
case 1: return sortBy('pps')
case 2: return sortBy('ppl')
}
}
const countSortDirection = () => {
let newSortDirection = currentSortDirection.value + 1
if (newSortDirection > 1) newSortDirection = 0
currentSortDirection.value = newSortDirection
sort()
}
</script>

68
app/pages/rechner.vue → app/pages/other.vue Executable file → Normal file
View file

@ -1,10 +1,10 @@
<template>
<div class="nuxt-page-wrapper flex-col">
<div>
<ClientOnly>
<PpDeleteDialog
ref="deleteModal"
:current-card-index="currentCardIndex"
@delete="removeCard(currentCardIndex)"
ref="deleteModal"
:current-card-index="currentCardIndex"
@delete="removeCard(currentCardIndex)"
/>
<PpPriceCardDialog
ref="modal"
@ -12,30 +12,41 @@
:current-card-index="currentCardIndex"
@update="updateCard()"
/>
<div class="search-bar">
<PpFormSearch
v-model="search"
label="Suche nach Klopapier!"
id="search_field"
/>
</div>
<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">
<div class="flex-col" role="list">
<PpPriceCard
ref="priceCard"
v-for="(card, index) in cards"
:key="card.uuid"
:deletable="cards.length > 1"
:card="card"
@update="openModal(false, index)"
@remove="openDeleteModal()"
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="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>
@ -46,19 +57,22 @@
</template>
<script setup lang="ts">
import type { PriceCard } from '../../shared/PriceCard'
import type { Card } from '../../shared/Card'
import type { Button } from '../../shared/ButtonGroup'
import { PpPriceCardDialog, PpDeleteDialog, PpPriceCard } from '#components'
const cards = useLocalStorage<PriceCard[]>('cards', [])
const cards = useLocalStorage<Card[]>('cards', [])
const currentSort = useLocalStorage<number>('sort', 0)
const currentCard = ref<PriceCard>()
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 priceCards = useTemplateRef<(typeof PpPriceCard)[]>('priceCard')
const createCard = (uuid : string) : PriceCard => ({
const search = ref('')
const createCard = (uuid : string) : Card => ({
uuid,
name: '',
price: '',
@ -68,14 +82,14 @@ const createCard = (uuid : string) : PriceCard => ({
})
const addCard = (card : PriceCard) => {
const addCard = (card : Card) => {
cards.value.unshift({ ...card })
sort()
isDirty.value = true
}
const removeCard = (index : number) => {
cards.value.splice(index, 1)
sort()
isDirty.value = true
}
const updateCard = () => {
@ -86,7 +100,7 @@ const updateCard = () => {
const newCard = { ...currentCard.value! }
cards.value.splice(currentCardIndex.value, 1, newCard)
sort()
isDirty.value = true
}
const openModal = (createNew : boolean, index : number) => {
@ -135,12 +149,12 @@ const sortBy = (key : 'ppr' | 'pps' | 'ppl') => {
})
}
const sort = async (index : number = currentSort.value) => {
const sort = (index : number) => {
currentSort.value = index
filterButtons.value.forEach(button => { button.active = false })
filterButtons.value[index]!.active = true
await nextTick()
isDirty.value = false
switch (index) {
case 0: return sortBy('ppr')

View file

@ -4,7 +4,10 @@
Datenschutzerklärung
</h1>
<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>
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,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({
compatibilityDate: '2024-11-01',
devtools: { enabled: false },
@ -44,28 +43,38 @@ export default defineNuxtConfig({
'@nuxt/fonts',
'nuxt-seo-utils',
'nuxt-ripple',
'@nuxtjs/sitemap',
'@nuxtjs/robots',
'@nuxtjs/plausible'
],
css : [
'./app/assets/styles/general.css',
'./app/assets/styles/header.css',
'./app/assets/styles/navigation.css',
'./app/assets/styles/footer.css',
'./app/assets/styles/button.css',
'./app/assets/styles/buttonGroup.css',
'./app/assets/styles/priceCard.css',
'./app/assets/styles/timelineCard.css',
'./app/assets/styles/form/textfield.css',
'./app/assets/styles/form/search.css',
'./app/assets/styles/form/dropdown.css',
'./app/assets/styles/form/togglebutton.css',
'./app/assets/styles/toolbar.css',
'./app/assets/styles/page.css',
'./app/assets/styles/dialog.css',
'./app/assets/styles/landingpage.css',
],
icon: {
customCollections: [
{
prefix: 'pp',
dir: './app/assets/icons'
}
],
provider: 'iconify',
serverBundle: 'local',
clientBundle: {
scan: true,
},
},
site: {
url: 'https://pro-papier.de',
name: 'ProPapier',
@ -74,7 +83,7 @@ export default defineNuxtConfig({
seo: {
meta: {
title: 'ProPapier',
description,
description: '"Bezahlt du zuviel fürs Papier? Vergleiche schnell und unkompliziert die Preise für Toiletten-, Küchen- und andere Papier hier."',
themeColor: [
{ content: '#18181b', media: '(prefers-color-scheme: dark)' },
{ content: 'white', media: '(prefers-color-scheme: light)' },
@ -86,26 +95,15 @@ export default defineNuxtConfig({
applicationName: 'ProPapier',
// Nuxt SEO Utils already sets the below tags for you
ogSiteName: 'ProPapier',
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',
ogImage: 'https://example.com/my-og-image.png',
robots: 'index, follow',
}
},
sitemap: {
// exclude all URLs that start with /secret
exclude: ['/other/**'],
},
plausible: {
// Prevent tracking on localhost
ignoredHostnames: ['localhost'],
},
})

543
package-lock.json generated
View file

@ -12,9 +12,6 @@
"@nuxt/fonts": "^0.11.3",
"@nuxt/icon": "^1.10.3",
"@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",
"nuxt": "^3.16.2",
"nuxt-ripple": "^0.0.8",
@ -446,12 +443,6 @@
"node": ">=6.9.0"
}
},
"node_modules/@barbapapazes/plausible-tracker": {
"version": "0.5.6",
"resolved": "https://registry.npmjs.org/@barbapapazes/plausible-tracker/-/plausible-tracker-0.5.6.tgz",
"integrity": "sha512-GRZxn3ZngYQ1+QbdP8d66D/lQg+T2oEevG8kBGfNwVbt9VZB67sgMx/gkRo/Ww2lH7QelgjUNzvOeG+DsJX2HQ==",
"license": "MIT"
},
"node_modules/@capsizecss/metrics": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/@capsizecss/metrics/-/metrics-3.5.0.tgz",
@ -2935,282 +2926,6 @@
"defu": "^6.1.4"
}
},
"node_modules/@nuxtjs/plausible": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@nuxtjs/plausible/-/plausible-1.2.0.tgz",
"integrity": "sha512-pjfps32fFN77BhjqHmq2Jx4XCNso9TcYnB+S4IR2qH/c26WDfYB5mQxN5pOEiWRlMkiKq+Y45mBBFtSOVKClCA==",
"license": "MIT",
"dependencies": {
"@barbapapazes/plausible-tracker": "^0.5.6",
"@nuxt/kit": "^3.14.1592",
"defu": "^6.1.4",
"ufo": "^1.5.4"
}
},
"node_modules/@nuxtjs/robots": {
"version": "5.2.10",
"resolved": "https://registry.npmjs.org/@nuxtjs/robots/-/robots-5.2.10.tgz",
"integrity": "sha512-WiO+VA8UwDgVLy7JCrGTrAmSNNw397OuNseKOG051ixswEDd0QhNw4cUtgNd2RkSIL7WlkfwkizdJFCd9y9iXw==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.16.2",
"consola": "^3.4.2",
"defu": "^6.1.4",
"nuxt-site-config": "^3.1.9",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"sirv": "^3.0.1",
"std-env": "^3.9.0",
"ufo": "^1.6.1"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@nuxtjs/robots/node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/@nuxtjs/robots/node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
"integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
"license": "MIT",
"dependencies": {
"confbox": "^0.2.1",
"exsolve": "^1.0.1",
"pathe": "^2.0.3"
}
},
"node_modules/@nuxtjs/sitemap": {
"version": "7.3.0",
"resolved": "https://registry.npmjs.org/@nuxtjs/sitemap/-/sitemap-7.3.0.tgz",
"integrity": "sha512-R3X5hqhWoMXIB4XWQmUuVSGmPFrKTFYFho7pk990si8iayKjSMbBCtTPtHdEscglZukKy4cL7V0WYdDFrUvV5w==",
"license": "MIT",
"dependencies": {
"@nuxt/devtools-kit": "^2.4.1",
"@nuxt/kit": "^3.17.3",
"chalk": "^5.4.1",
"defu": "^6.1.4",
"h3-compression": "^0.3.2",
"nuxt-site-config": "^3.2.0",
"ofetch": "^1.4.1",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"radix3": "^1.1.2",
"semver": "^7.7.2",
"sirv": "^3.0.1",
"ufo": "^1.6.1"
},
"engines": {
"node": ">=18.0.0"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@nuxt/devtools-kit": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/@nuxt/devtools-kit/-/devtools-kit-2.4.1.tgz",
"integrity": "sha512-taA2Nm03JiV3I+SEYS/u1AfjvLm3V9PO8lh0xLsUk/2mlUnL6GZ9xLXrp8VRg11HHt7EPXERGQh8h4iSPU2bSQ==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.17.3",
"@nuxt/schema": "^3.17.3",
"execa": "^8.0.1"
},
"peerDependencies": {
"vite": ">=6.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@nuxt/kit": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.17.3.tgz",
"integrity": "sha512-aw6u6mT3TnM/MmcCRDMv3i9Sbm5/ZMSJgDl+N+WsrWNDIQ2sWmsqdDkjb/HyXF20SNwc2891hRBkaQr3hG2mhA==",
"license": "MIT",
"dependencies": {
"c12": "^3.0.3",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.5",
"ignore": "^7.0.4",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"semver": "^7.7.1",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.13",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.0.1",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/@nuxt/schema": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/@nuxt/schema/-/schema-3.17.3.tgz",
"integrity": "sha512-z4hbeTtg8B2/2I8zqnCAQQ9JmIQA/BfFy/8cRkGKRIMNjOaTOdmAqMnNriSpyp9xfzWGpnvxPFgab/5uSjsAgA==",
"license": "MIT",
"dependencies": {
"@vue/shared": "^3.5.13",
"consola": "^3.4.2",
"defu": "^6.1.4",
"pathe": "^2.0.3",
"std-env": "^3.9.0"
},
"engines": {
"node": "^14.18.0 || >=16.10.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/@nuxtjs/sitemap/node_modules/execa": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz",
"integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==",
"license": "MIT",
"dependencies": {
"cross-spawn": "^7.0.3",
"get-stream": "^8.0.1",
"human-signals": "^5.0.0",
"is-stream": "^3.0.0",
"merge-stream": "^2.0.0",
"npm-run-path": "^5.1.0",
"onetime": "^6.0.0",
"signal-exit": "^4.1.0",
"strip-final-newline": "^3.0.0"
},
"engines": {
"node": ">=16.17"
},
"funding": {
"url": "https://github.com/sindresorhus/execa?sponsor=1"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/get-stream": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz",
"integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==",
"license": "MIT",
"engines": {
"node": ">=16"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/human-signals": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz",
"integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.17.0"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.0.1",
"quansync": "^0.2.8"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/@nuxtjs/sitemap/node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
"integrity": "sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A==",
"license": "MIT",
"dependencies": {
"confbox": "^0.2.1",
"exsolve": "^1.0.1",
"pathe": "^2.0.3"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"license": "ISC",
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/@nuxtjs/sitemap/node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/@oxc-parser/binding-darwin-arm64": {
"version": "0.56.5",
"resolved": "https://registry.npmjs.org/@oxc-parser/binding-darwin-arm64/-/binding-darwin-arm64-0.56.5.tgz",
@ -5672,18 +5387,6 @@
],
"license": "CC-BY-4.0"
},
"node_modules/chalk": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
"integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/chokidar": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
@ -7791,18 +7494,6 @@
"uncrypto": "^0.1.3"
}
},
"node_modules/h3-compression": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/h3-compression/-/h3-compression-0.3.2.tgz",
"integrity": "sha512-B+yCKyDRnO0BXSfjAP4tCXJgJwmnKp3GyH5Yh66mY9KuOCrrGQSPk/gBFG2TgH7OyB/6mvqNZ1X0XNVuy0qRsw==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/codedredd"
},
"peerDependencies": {
"h3": "^1.6.0"
}
},
"node_modules/h3/node_modules/cookie-es": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz",
@ -9618,101 +9309,45 @@
}
},
"node_modules/nuxt-site-config": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nuxt-site-config/-/nuxt-site-config-3.2.0.tgz",
"integrity": "sha512-o1LDV+eaiP0qgM97RxoX2ost3mzmNmg5D3BmiORXCD9lx9CR5OZKc7nXI0zGsASk3eSVj4iNp0ctyF6afPFTow==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/nuxt-site-config/-/nuxt-site-config-3.1.9.tgz",
"integrity": "sha512-YB69GX0st8drv1d5xypweseiEWeR22tfGdyVH3U4R+mpUSz8paBx48ArKC6MgV22DKItoQm51LVoapF5pl5bEQ==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.17.3",
"nuxt-site-config-kit": "3.2.0",
"@nuxt/kit": "^3.16.2",
"nuxt-site-config-kit": "3.1.9",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"sirv": "^3.0.1",
"site-config-stack": "3.2.0",
"ufo": "^1.6.1"
"site-config-stack": "3.1.9",
"ufo": "^1.5.4"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/nuxt-site-config-kit": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/nuxt-site-config-kit/-/nuxt-site-config-kit-3.2.0.tgz",
"integrity": "sha512-kVBXljR7Py8mz5eL6ZysVMlPRwbVX1Tts66StQRwYSJL/srEL8kr/ZfLW6tQU7pDHihcPH3MDgid2gDTFMY3fg==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/nuxt-site-config-kit/-/nuxt-site-config-kit-3.1.9.tgz",
"integrity": "sha512-bcmpajYJgkNzA0jTq6CmmhKF2wHZUUKeVx/CIGI8lwWuAD81EBUZN0T4iKvVDo54g9UBrUUl8/5GhD65YBBG0A==",
"license": "MIT",
"dependencies": {
"@nuxt/kit": "^3.17.3",
"@nuxt/kit": "^3.16.2",
"pkg-types": "^2.1.0",
"site-config-stack": "3.2.0",
"site-config-stack": "3.1.9",
"std-env": "^3.9.0",
"ufo": "^1.6.1"
"ufo": "^1.5.4"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"
}
},
"node_modules/nuxt-site-config-kit/node_modules/@nuxt/kit": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.17.3.tgz",
"integrity": "sha512-aw6u6mT3TnM/MmcCRDMv3i9Sbm5/ZMSJgDl+N+WsrWNDIQ2sWmsqdDkjb/HyXF20SNwc2891hRBkaQr3hG2mhA==",
"license": "MIT",
"dependencies": {
"c12": "^3.0.3",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.5",
"ignore": "^7.0.4",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"semver": "^7.7.1",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.13",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.0.1",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt-site-config-kit/node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/nuxt-site-config-kit/node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.0.1",
"quansync": "^0.2.8"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/nuxt-site-config-kit/node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/nuxt-site-config-kit/node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
@ -9724,109 +9359,12 @@
"pathe": "^2.0.3"
}
},
"node_modules/nuxt-site-config-kit/node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/nuxt-site-config-kit/node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt-site-config/node_modules/@nuxt/kit": {
"version": "3.17.3",
"resolved": "https://registry.npmjs.org/@nuxt/kit/-/kit-3.17.3.tgz",
"integrity": "sha512-aw6u6mT3TnM/MmcCRDMv3i9Sbm5/ZMSJgDl+N+WsrWNDIQ2sWmsqdDkjb/HyXF20SNwc2891hRBkaQr3hG2mhA==",
"license": "MIT",
"dependencies": {
"c12": "^3.0.3",
"consola": "^3.4.2",
"defu": "^6.1.4",
"destr": "^2.0.5",
"errx": "^0.1.0",
"exsolve": "^1.0.5",
"ignore": "^7.0.4",
"jiti": "^2.4.2",
"klona": "^2.0.6",
"knitwork": "^1.2.0",
"mlly": "^1.7.4",
"ohash": "^2.0.11",
"pathe": "^2.0.3",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"semver": "^7.7.1",
"std-env": "^3.9.0",
"tinyglobby": "^0.2.13",
"ufo": "^1.6.1",
"unctx": "^2.4.1",
"unimport": "^5.0.1",
"untyped": "^2.0.0"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt-site-config/node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
"integrity": "sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==",
"license": "MIT"
},
"node_modules/nuxt-site-config/node_modules/local-pkg": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/local-pkg/-/local-pkg-1.1.1.tgz",
"integrity": "sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg==",
"license": "MIT",
"dependencies": {
"mlly": "^1.7.4",
"pkg-types": "^2.0.1",
"quansync": "^0.2.8"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/nuxt-site-config/node_modules/ohash": {
"version": "2.0.11",
"resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz",
"integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==",
"license": "MIT"
},
"node_modules/nuxt-site-config/node_modules/pkg-types": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.1.0.tgz",
@ -9838,47 +9376,6 @@
"pathe": "^2.0.3"
}
},
"node_modules/nuxt-site-config/node_modules/tinyglobby": {
"version": "0.2.13",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz",
"integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==",
"license": "MIT",
"dependencies": {
"fdir": "^6.4.4",
"picomatch": "^4.0.2"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/nuxt-site-config/node_modules/unimport": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/unimport/-/unimport-5.0.1.tgz",
"integrity": "sha512-1YWzPj6wYhtwHE+9LxRlyqP4DiRrhGfJxdtH475im8ktyZXO3jHj/3PZ97zDdvkYoovFdi0K4SKl3a7l92v3sQ==",
"license": "MIT",
"dependencies": {
"acorn": "^8.14.1",
"escape-string-regexp": "^5.0.0",
"estree-walker": "^3.0.3",
"local-pkg": "^1.1.1",
"magic-string": "^0.30.17",
"mlly": "^1.7.4",
"pathe": "^2.0.3",
"picomatch": "^4.0.2",
"pkg-types": "^2.1.0",
"scule": "^1.3.0",
"strip-literal": "^3.0.0",
"tinyglobby": "^0.2.13",
"unplugin": "^2.3.2",
"unplugin-utils": "^0.2.4"
},
"engines": {
"node": ">=18.12.0"
}
},
"node_modules/nuxt/node_modules/confbox": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz",
@ -11494,9 +10991,9 @@
"license": "MIT"
},
"node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"version": "7.7.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@ -11728,12 +11225,12 @@
"license": "MIT"
},
"node_modules/site-config-stack": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/site-config-stack/-/site-config-stack-3.2.0.tgz",
"integrity": "sha512-YmHQr5nMfec5ZTtuxK52YVG8nj4DwI68rSsbznuurQ96bIh2bHf5mNPAZnAyDBPpTWRwmWAliWMRD6WFewWGdA==",
"version": "3.1.9",
"resolved": "https://registry.npmjs.org/site-config-stack/-/site-config-stack-3.1.9.tgz",
"integrity": "sha512-ed53+wLi+36SGqidU+YUpl7f1OHClPLmvUJ/aYZny1dCBnXvOsuFottrMkXDIK2N5UaMED9mz8KrRZTk94ARCg==",
"license": "MIT",
"dependencies": {
"ufo": "^1.6.1"
"ufo": "^1.5.4"
},
"funding": {
"url": "https://github.com/sponsors/harlan-zw"

View file

@ -7,7 +7,7 @@
"dev": "nuxt dev",
"dev:expose": "nuxt dev --host",
"generate": "nuxt generate",
"preview": "npx serve .output/public",
"preview": "nuxt preview",
"prepare": "nuxt prepare",
"postinstall": "nuxt prepare"
},
@ -17,9 +17,6 @@
"@nuxt/fonts": "^0.11.3",
"@nuxt/icon": "^1.10.3",
"@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",
"nuxt": "^3.16.2",
"nuxt-ripple": "^0.0.8",

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,4 +1,4 @@
export type PriceCard = {
export type Card = {
uuid : string
name : string
price : string

4
shared/DropDown.ts Normal file
View file

@ -0,0 +1,4 @@
export type DropDownElement = {
label: string
value: string | number
}

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