FIX: mpa, nuxt4 future, improvements in intro and services

Added mpa support, nuxt4 future compatibility, improvements in intro and services
This commit is contained in:
webfussel 2025-02-12 13:18:55 +01:00
parent 078d4bfd82
commit 9642496e5a
35 changed files with 324 additions and 172 deletions

View file

@ -25,3 +25,15 @@ useSeoMeta({
twitterUrl: 'https://webfussel.de',
})
</script>
<style>
.page-enter-active,
.page-leave-active {
transition: all 0.4s;
}
.page-enter-from,
.page-leave-to {
opacity: 0;
filter: blur(.5rem);
}
</style>

75
app/assets/css/button.css Normal file
View file

@ -0,0 +1,75 @@
.Button {
all: unset;
transition: 250ms;
cursor: pointer;
padding: 1rem 1.5rem;
outline: 3px solid transparent;
box-shadow: 0 0 0 0 var(--color-orange);
border-radius: 99999px;
display: flex;
align-items: center;
justify-content: center;
gap: 1rem;
&.default {
background: var(--color-orange);
color: var(--color-black);
}
&.white {
background: var(--color-white);
color: var(--color-black);
}
&:hover {
outline-color: var(--color-black);
box-shadow: 0 0 0 6px var(--color-orange);
}
&.cta {
font-size: clamp(1rem, 2vw, 1.5rem);
}
}
.DualButton {
--size: 2.2rem;
display: flex;
width: 100%;
& .divider {
display: flex;
align-items: center;
justify-content: center;
background-color: var(--color-black);
color: var(--color-white);
border-radius: 9999px;
width: var(--size);
height: var(--size);
padding: var(--size);
font-size: 1.2rem;
z-index: 1;
margin-left: calc(var(--size) * -1 - 25px);
border: 2px solid var(--color-black);
}
.Button {
border: 2px solid currentColor;
}
& .Button:hover {
outline: none;
box-shadow: none;
background-color: var(--color-black);
color: var(--color-white);
border-color: var(--color-orange);
}
& .Button:first-child {
padding-right: calc(var(--size) * 2);
}
& .Button:last-child {
padding-left: calc(var(--size) * 2);
margin-left: calc(var(--size) * -1 - 25px);
}
}

View file

@ -73,6 +73,7 @@ body {
background: var(--color-black);
}
.h1, .h2, .h3, .h4, .h5, .h6,
h1, h2, h3, h4, h5, h6 {
text-align: left;
font-family: 'Roboto Condensed', sans-serif;
@ -82,8 +83,11 @@ h1 {
font-size: 4rem;
}
.h2,
h2,
h3 {
font-size: 1.5rem;
font-weight: bold;
}
a {
@ -198,6 +202,43 @@ span.chip {
box-shadow: 0 19px 38px rgba(0, 0, 0, 0.30), 0 15px 12px rgba(0, 0, 0, 0.22);
}
.tip-container {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
.tip-container .tip {
scale: 0;
position: absolute;
top: -3rem;
width: max-content;
border: 1px solid var(--color-white);
border-radius: 999px;
background-color: var(--color-black);
padding: .5em 1.5rem;
transition: 150ms;
}
.tip-container:hover .tip {
scale: 1;
}
.animate-up-down {
animation: up-down 1.5s ease-in-out alternate-reverse infinite;
}
@keyframes up-down {
0% {
translate: 0 -0.1rem;
}
100% {
translate: 0 0.4em;
}
}
@media (width <= 780px) {
h1, h2, h3, h4, h5, h6, p {
text-align: center;

View file

@ -10,10 +10,6 @@
position: fixed;
z-index: 1000;
& .socials {
gap: 1rem;
}
& .logo {
fill-rule: evenodd;
clip-rule: evenodd;
@ -51,19 +47,23 @@
}
& nav {
flex: 3;
position: relative;
font-weight: normal;
font-size: 1.2rem;
display: flex;
justify-content: space-between;
align-items: center;
& .active {
color: var(--color-orange);
}
}
& > .wrapper {
display: flex;
align-items: center;
padding: 15px 30px;
justify-content: space-between;
padding: 15px 22px;
transition: 750ms;
backdrop-filter: blur(10px);
border-radius: 0;
@ -174,13 +174,6 @@
color: var(--color-white);
flex-direction: column;
& .socials {
flex-direction: row;
height: max-content;
gap: 3rem;
padding-bottom: 2rem;
}
& ul {
flex-direction: column;
justify-content: center;
@ -188,7 +181,7 @@
gap: 8vh;
& li {
font-size: 10vw;
font-size: clamp(1rem, 10vw, 3rem);
}
}
}

View file

@ -0,0 +1,16 @@
.Technology {
position: relative;
align-items: center;
&.s img {
height: 15px;
}
&.m img {
height: 30px;
}
&.l img {
height: 50px;
}
}

View file

@ -1,6 +1,6 @@
<template>
<component :is="type" v-bind="actualProps()" class="Button">
{{ label }}
<component :is="type" v-bind="actualProps()" class="Button" :class="[design]">
<slot />
</component>
</template>
@ -9,13 +9,13 @@
type Props = {
type ?: 'a' | 'button'
href ?: string
label : string
design ?: string
}
const {
type = 'a',
href,
label,
href = '#',
design = 'default',
} = defineProps<Props>()
const actualProps = () => {

View file

@ -38,7 +38,7 @@
</template>
<script setup lang="ts">
import { css, pcss, html, js, njs, nuxt, scss, ts, tw, vue, flutter, dart, android } from '~/Skills'
import { android, css, dart, flutter, html, js, njs, nuxt, pcss, scss, ts, tw, vue } from '../utils/skills'
const customers = [
{

84
app/components/Footer.vue Normal file
View file

@ -0,0 +1,84 @@
<template>
<footer class="Footer flex-col default-gap">
<ul class="sitemap default-gap">
<li v-for="{ label, ...rest} in nav" :key="label">
<NuxtLink v-bind="rest">{{label}}</NuxtLink>
</li>
</ul>
<ul class="sitemap default-gap">
<li v-for="({icon, ...rest}) in socials" :key="rest.href">
<a v-bind="rest" target="_blank">
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
</a>
</li>
</ul>
<ul class="sitemap default-gap">
<li class="tip-container">
<Icon name="material-symbols:cookie-off-outline" size="1.5rem" mode="svg" />
<span class="tip">Ohne Cookies</span>
</li>
<li class="tip-container">
<Icon name="material-symbols:fingerprint-off" size="1.5rem" mode="svg" />
<span class="tip">Ohne Tracker</span>
</li>
</ul>
<p>&copy; 2024 by <a href="https://webfussel.de">webfussel</a></p>
</footer>
</template>
<script lang="ts" setup>
const nav = [
{
to: `/#intro`,
label: 'Über mich',
'aria-label': 'Link dieser Seite: Über mich'
}, {
to: `/#skills`,
label: 'Meine Expertise',
'aria-label': 'Link dieser Seite: Meine Expertise'
}, {
to: `/#customers`,
label: 'Kunden',
'aria-label': 'Link dieser Seite: Kunden'
}, {
to: `/#services`,
label: 'Services',
'aria-label': 'Link dieser Seite: Services'
}, {
to: `/#network`,
label: 'Mein Netzwerk',
'aria-label': 'Link dieser Seite: Mein Netzwerk'
}, {
to: '/imp',
label: 'Impressum',
'aria-label': 'Link dieser Seite: Impressum'
}
]
const socials = [
{
href: 'https://www.linkedin.com/in/webfussel/',
icon: 'ri:linkedin-box-line',
'aria-label': 'Externer Link: LinkedIn Profil'
},
{
href: 'https://mastodontech.de/@webfussel',
icon: 'ri:mastodon-line',
rel: 'me',
'aria-label': 'Externer Link: Mastodon Profil'
},
{
href: 'https://bsky.app/profile/webfussel.de',
icon: 'ri:bluesky-line',
'aria-label': 'Externer Link: Bluesky Profil'
},
{
href: 'https://ko-fi.com/webfussel',
icon: 'wf:kofi',
'aria-label': 'Externer Link: KoFi Profil'
},
]
</script>

View file

@ -28,15 +28,8 @@
</label>
<nav>
<ul class="main-nav">
<li v-for="({label, ...rest}) in nav" :key="label" @click="isBurgerOpen = false">
<NuxtLink v-bind="rest">{{ label }}</NuxtLink>
</li>
</ul>
<ul class="socials">
<li v-for="({icon, ...rest}) in socials" :key="rest.href" @click="isBurgerOpen = false">
<a v-bind="rest" target="_blank">
<Icon :name="icon" :alt="rest['aria-label']" size="1.5em" mode="svg" />
</a>
<li v-for="({label, to, aria}) in nav" :key="label" @click="isBurgerOpen = false">
<NuxtLink :to="to" :aria-label="aria" active-class="active">{{ label }}</NuxtLink>
</li>
</ul>
</nav>
@ -57,46 +50,30 @@ const burgerLabel = computed(() => isBurgerOpen.value ? burgerCloseLabel : burge
const nav = [
{
to: `/#intro`,
label: 'Über mich',
'aria-label': 'Link dieser Seite: Über mich'
}, {
to: `/#customers`,
label: 'Kunden',
'aria-label': 'Link dieser Seite: Kunden'
}, {
to: `/#services`,
label: 'Services',
'aria-label': 'Link dieser Seite: Services'
}
]
const socials = [
{
href: 'https://www.linkedin.com/in/webfussel/',
icon: 'ri:linkedin-box-line',
'aria-label': 'Externer Link: LinkedIn Profil'
to: `/`,
label: 'home',
'aria-label': 'Link dieser Seite: Startseite'
},
{
href: 'https://mastodontech.de/@webfussel',
icon: 'ri:mastodon-line',
rel: 'me',
'aria-label': 'Externer Link: Mastodon Profil'
to: `/services`,
label: 'leistungen',
aria: 'Link dieser Seite: Leistungen'
},
{
href: 'https://bsky.app/profile/webfussel.de',
icon: 'ri:bluesky-line',
'aria-label': 'Externer Link: Bluesky Profil'
to: `/references`,
label: 'referenzen',
aria: 'Link dieser Seite: Referenzen'
},
{
href: 'https://ko-fi.com/webfussel',
icon: 'wf:kofi',
'aria-label': 'Externer Link: KoFi Profil'
to: `/contact`,
label: 'kontakt',
aria: 'Link dieser Seite: Kontakt'
},
]
onMounted(() => {
observer = new IntersectionObserver(([entry]) => {
if (!entry) return
const { isIntersecting } = entry
header.value?.classList.toggle('sticks', !isIntersecting)
headerWrapper.value?.classList.toggle('z-4', !isIntersecting)

View file

@ -9,17 +9,21 @@
Component <span class="highlight">&</span> API Entwicklerin
</h2>
<p class="fulltext">
Ich unterstütze Unternehmen dabei, ihre Daten so richtig nice zusammen zu sammeln
und in wunderschöne Komponenten zu gießen.
Ich unterstütze Unternehmen dabei, ihre Daten von verschiedenen Endpunkten sauber aufzubereiten
und anschließend in einer Webapplication schön zu verpacken.
</p>
<p class="fulltext">
Mit über 20 Jahren Erfahrung in der Webentwicklung habe ich
inzwischen so ziemlich jeden Stuff miterlebt.
</p>
<p class="fulltext">
Du brauchst großartige Komponenten und saubere Schnittstellen?
Egal, ob Komponenten, Schnittstellen oder Anbindung an Headless CMS.
Ich biete dir genau das, was du brauchst, um eine individuelle WebApp in Fahrt zu bringen, deren Inhalte einfach zu verändern sind.
</p>
<Button href="#services" class="cta" label="Lass mal reden" />
<Button class="cta" href="#skills">
<Icon name="ph:lightbulb-duotone" size="1.5em" mode="svg" />
Fussel erklärt's dir
</Button>
</div>
<div class="intro-img">
<picture>
@ -29,3 +33,5 @@
</div>
</section>
</template>
<script setup lang="ts">
</script>

View file

@ -13,7 +13,9 @@
<span v-for="tag in tags">{{tag}}</span>
</p>
<p class="flavour">{{flavour}}</p>
<Button :href="link" class="button" target="_blank" rel="noreferrer noopener" label="Zur Homepage" :aria-label="`Externer Link zur Homepage von ${name}`" />
<Button :href="link" class="button" target="_blank" rel="noreferrer noopener" :aria-label="`Externer Link zur Homepage von ${name}`">
Zur Homepage
</Button>
</article>
</template>

View file

@ -12,10 +12,13 @@
</h3>
<span class="chip">{{service.availability}}</span>
<p>{{service.smallClaim}}</p>
<Button :href="service.link" class="cta" :label="service.button" aria-label="Zur externen Seite von zur Terminbuchung" />
<Button :href="service.link" class="cta" aria-label="Zur externen Seite von zur Terminbuchung">
{{ service.button }}
<Icon :name="`ph:${service.icon}-duotone`" size="1.5em" mode="svg"></Icon>
</Button>
<ul class="flex-col">
<li v-for="point in service.list">
<Icon class="color-icon" name="ri:check-double-line" aria-hidden="true" alt="checkmark icon" size="1.5em" mode="svg" />
<Icon class="color-icon" name="ph:caret-circle-double-right-duotone" aria-hidden="true" alt="checkmark icon" size="1.5em" mode="svg" />
<span>{{point}}</span>
</li>
</ul>
@ -59,6 +62,7 @@ const services = [
price: '149 € / Einmalig',
availability: 'Frei',
smallClaim: 'Du hast eine Homepage und willst mal drüber schauen lassen?',
icon: 'magnifying-glass',
button: 'Jetzt untersuchen',
link: 'https://tidycal.com/webfussel/quick-check',
list: [
@ -73,6 +77,7 @@ const services = [
price: 'ab 999 € je nach Umfang',
availability: slotsLabel,
smallClaim: 'Umsetzung deiner Vision. Von einzelnen Tickets bis hin zu kompletten Anwendungen.',
icon: 'trend-up',
button: 'Jetzt durchstarten',
link: 'https://tidycal.com/webfussel/project-booking',
list: [
@ -86,6 +91,7 @@ const services = [
availability: `Frei ab ${readableDate}`,
price: '105 € / Stunde',
smallClaim: 'Du brauchst einfach Unterstützung im Team, bis sich der Trubel legt?',
icon: 'timer',
button: 'Jetzt buchen',
link: 'https://tidycal.com/webfussel/hourly-booking',
list: [

View file

@ -20,17 +20,22 @@
</ul>
</article>
<div class="bottom flex-col margin-top default-gap">
<h3>Du brauchst was davon? Kein Ding.</h3>
<Button href="#services" class="cta" label="Lass mal reden" />
<h3>Manche von euch haben hier sicher kein Wort verstanden.</h3>
<Button href="#services" class="cta">
<span class="animate-up-down">
<Icon name="ph:caret-double-down-duotone" size="1.5em" mode="svg" />
</span>
Für normale Menschen
</Button>
</div>
</section>
</template>
<script setup lang="ts">
import { gl, webstorm, njs, nuxt, pcss, react, rust, ts, vitest, vue } from '~/Skills'
import { gl, njs, nuxt, pcss, react, rust, ts, vitest, vue, webstorm } from '../utils/skills'
const technologies = [nuxt, ts, pcss, vue, react, njs, vitest, gl, webstorm]
const technologies = [nuxt, ts, pcss, vue, react, njs, vitest, gl, webstorm, rust]
const skills = [
{

View file

@ -1,5 +1,5 @@
<template>
<div class="Technology flex-col" :class="[size]">
<div class="Technology flex-col tip-container" :class="[size]">
<a v-if="link" :href="link" target="_blank" rel="noopener noreferrer">
<img loading="lazy" :src="img" :alt="altText()" :height="getPixelForSize()" :width="getWidth()" />
</a>

6
app/pages/index.vue Normal file
View file

@ -0,0 +1,6 @@
<template>
<div>
<Intro />
<Skills />
</div>
</template>

5
app/pages/references.vue Normal file
View file

@ -0,0 +1,5 @@
<template>
<div>
<Customers />
</div>
</template>

5
app/pages/services.vue Normal file
View file

@ -0,0 +1,5 @@
<template>
<div>
<Services />
</div>
</template>

View file

@ -31,7 +31,6 @@ export const flutter: ISkill = {name: 'Flutter', img: flutterImg, link: 'https:/
export const gl: ISkill = {name: 'GitLab', img: glImg, link: 'https://gitlab.com', width: 55}
export const html: ISkill = {name: 'HTML', img: htmlImg, width: 44}
export const js: ISkill = {name: 'JavaScript', img: jsImg}
export const webstorm: ISkill = {name: 'JetBrains IDEs', img: webstormImg, link: 'https://www.jetbrains.com/webstorm'}
export const njs: ISkill = {name: 'Nodejs', img: njsImg, link: 'https://nodejs.org/en', width: 46}
export const nuxt: ISkill = {name: 'Nuxt', img: nuxtImg, link: 'https://nuxt.com', width: 75}
export const pcss: ISkill = {name: 'PostCSS', img: postCssImg, link: 'https://postcss.org'}
@ -42,3 +41,4 @@ export const tw: ISkill = {name: 'Tailwind', img: twImg, width: 84}
export const ts: ISkill = {name: 'TypeScript', img: tsImg, link: 'https://www.typescriptlang.org'}
export const vitest: ISkill = {name: 'Vitest', img: vitestImg, link: 'https://vitest.dev', width: 55}
export const vue: ISkill = {name: 'Vue', img: vueImg, link: 'https://vuejs.org', width: 58}
export const webstorm: ISkill = {name: 'JetBrains IDEs', img: webstormImg, link: 'https://www.jetbrains.com/webstorm'}

View file

@ -1,24 +0,0 @@
.Button {
all: unset;
transition: 250ms;
background: var(--color-orange);
color: var(--color-black);
cursor: pointer;
padding: 1rem 1.5rem;
outline: 3px solid transparent;
box-shadow: 0 0 0 0 var(--color-orange);
border-radius: 99999px;
text-align: center;
display: flex;
align-items: center;
justify-content: center;
&:hover {
outline-color: var(--color-black);
box-shadow: 0 0 0 6px var(--color-orange);
}
&.cta {
font-size: clamp(1rem, 2vw, 1.5rem);
}
}

View file

@ -1,32 +0,0 @@
.Technology {
position: relative;
align-items: center;
&.s img {
height: 15px;
}
&.m img {
height: 30px;
}
&.l img {
height: 50px;
}
& span {
scale: 0;
position: absolute;
top: -3rem;
width: max-content;
border: 1px solid var(--color-white);
border-radius: 999px;
background-color: var(--color-black);
padding: .5em 1.5rem;
transition: 150ms;
}
&:hover span {
scale: 1;
}
}

View file

@ -1,44 +0,0 @@
<template>
<footer class="Footer flex-col default-gap">
<ul class="sitemap default-gap">
<li v-for="{ label, ...rest} in nav" :key="label">
<NuxtLink v-bind="rest">{{label}}</NuxtLink>
</li>
</ul>
<div class="notes">
<p>Natürlich ohne Cookies und Tracker.</p>
<p>Made with nuxt, typescript & postcss.</p>
</div>
<p>&copy; 2024 by <a href="https://webfussel.de">webfussel</a></p>
</footer>
</template>
<script lang="ts" setup>
const nav = [
{
to: `/#intro`,
label: 'Über mich',
'aria-label': 'Link dieser Seite: Über mich'
}, {
to: `/#skills`,
label: 'Meine Expertise',
'aria-label': 'Link dieser Seite: Meine Expertise'
}, {
to: `/#customers`,
label: 'Kunden',
'aria-label': 'Link dieser Seite: Kunden'
}, {
to: `/#services`,
label: 'Services',
'aria-label': 'Link dieser Seite: Services'
}, {
to: `/#network`,
label: 'Mein Netzwerk',
'aria-label': 'Link dieser Seite: Mein Netzwerk'
}, {
to: '/imp',
label: 'Impressum',
'aria-label': 'Link dieser Seite: Impressum'
}
]
</script>

View file

@ -1,5 +1,8 @@
export default defineNuxtConfig({
ssr: true,
future: {
compatibilityVersion: 4,
},
nitro: {
prerender: {
@ -36,6 +39,10 @@ export default defineNuxtConfig({
},
app: {
pageTransition: {
name: 'page',
mode: 'out-in',
},
head: {
htmlAttrs: { lang: 'de' },
link: [

11
package-lock.json generated
View file

@ -7,6 +7,7 @@
"name": "nuxt-app",
"hasInstallScript": true,
"devDependencies": {
"@iconify-json/material-symbols": "^1.2.14",
"@iconify-json/ri": "^1.2.5",
"@nuxt/icon": "^1.10.3",
"nuxt": "^3.15.3",
@ -1037,6 +1038,16 @@
"node": ">=18"
}
},
"node_modules/@iconify-json/material-symbols": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@iconify-json/material-symbols/-/material-symbols-1.2.14.tgz",
"integrity": "sha512-S0AAFFQPVr8Dkrprspz/otNjxdD3rJRXDGZjbO8a8zn8ZR5mO8jAF81lVoTfUWxPH6SCtH2lK1JQGXHGPxld7g==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@iconify/types": "*"
}
},
"node_modules/@iconify-json/ri": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/@iconify-json/ri/-/ri-1.2.5.tgz",

View file

@ -14,6 +14,7 @@
"build:deploy": "nuxt build && firebase deploy --only hosting"
},
"devDependencies": {
"@iconify-json/material-symbols": "^1.2.14",
"@iconify-json/ri": "^1.2.5",
"@nuxt/icon": "^1.10.3",
"nuxt": "^3.15.3",