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:
parent
078d4bfd82
commit
9642496e5a
35 changed files with 324 additions and 172 deletions
29
app/components/Button.vue
Normal file
29
app/components/Button.vue
Normal file
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<component :is="type" v-bind="actualProps()" class="Button" :class="[design]">
|
||||
<slot />
|
||||
</component>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
|
||||
type Props = {
|
||||
type ?: 'a' | 'button'
|
||||
href ?: string
|
||||
design ?: string
|
||||
}
|
||||
|
||||
const {
|
||||
type = 'a',
|
||||
href = '#',
|
||||
design = 'default',
|
||||
} = defineProps<Props>()
|
||||
|
||||
const actualProps = () => {
|
||||
if (type === 'a') {
|
||||
return {
|
||||
href: href,
|
||||
target: href!.startsWith('https://') ? '_blank' : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
204
app/components/Customers.vue
Normal file
204
app/components/Customers.vue
Normal file
|
@ -0,0 +1,204 @@
|
|||
<template>
|
||||
<section id="customers" class="Customers content">
|
||||
<h2>Kunden <span class="highlight">&</span> Projekte.</h2>
|
||||
<h3>Meine bisherigen Geschäftpartner</h3>
|
||||
<div class="customer-list margin-top default-gap">
|
||||
<a v-for="customer in customers" :href="customer.link" target="_blank" rel="noopener noreferrer">
|
||||
<img
|
||||
loading="lazy"
|
||||
height="50"
|
||||
:width="customer.logo.width"
|
||||
:alt="customer.name"
|
||||
:src="`/img/customers/${customer.logo.src}.svg`"
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<h3 class="margin-top-big">Projektauswahl</h3>
|
||||
<div class="projects-list margin-top">
|
||||
<article v-for="pr in projects">
|
||||
<div class="bg">
|
||||
<img height="350" width="400" loading="lazy" :alt="pr.title" :src="pr.image" aria-hidden="true"/>
|
||||
</div>
|
||||
<div>
|
||||
<main>
|
||||
<small class="customer">{{ pr.customer }}</small>
|
||||
<h3 class="title">{{ pr.title }}</h3>
|
||||
<ul>
|
||||
<li v-for="skill in pr.technologies">
|
||||
<Technology v-bind="skill" link="" />
|
||||
</li>
|
||||
</ul>
|
||||
<p v-for="d in pr.desc">{{ d }}</p>
|
||||
<a v-if="pr.link" :href="pr.link" target="_blank">Zur {{pr.type ?? 'Seite'}}</a>
|
||||
</main>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { android, css, dart, flutter, html, js, njs, nuxt, pcss, scss, ts, tw, vue } from '../utils/skills'
|
||||
|
||||
const customers = [
|
||||
{
|
||||
name: 'Bounce Commerce',
|
||||
link: 'https://bounce-commerce.de',
|
||||
logo: {
|
||||
src: 'bounce',
|
||||
width: 150,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'GMX',
|
||||
link: 'https://gmx.net',
|
||||
logo: {
|
||||
src: 'gmx',
|
||||
width: 148,
|
||||
white: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'WEB.DE',
|
||||
link: 'https://web.de',
|
||||
logo: {
|
||||
src: 'webde',
|
||||
width: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: '1&1',
|
||||
link: 'https://1und1.de',
|
||||
logo: {
|
||||
src: '1u1',
|
||||
width: 50,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Körrie',
|
||||
link: 'https://körrie.de',
|
||||
logo: {
|
||||
src: 'koerrie',
|
||||
width: 50,
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Pembe',
|
||||
link: 'https://pembe.io',
|
||||
logo: {
|
||||
src: 'pembe',
|
||||
width: 48,
|
||||
white: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'SAE Institute Germany',
|
||||
link: 'https://www.sae.edu/deu/en/sae-home/',
|
||||
logo: {
|
||||
src: 'sae',
|
||||
width: 77,
|
||||
white: true,
|
||||
},
|
||||
}
|
||||
]
|
||||
|
||||
const projects = [
|
||||
{
|
||||
title: 'Kauft Körrie! App',
|
||||
customer: 'KVK Berlin',
|
||||
image: '/img/projects/koerrie_app.webp',
|
||||
desc: [
|
||||
'Entwicklung einer Android Info-App für die Gewürzmischungen "Körrie" und passendem Zubehör.',
|
||||
'Zusätzlich die Übertragung des Körrie-O-Mat von der Landingpage in die App mit Ergebnisverlauf.',
|
||||
],
|
||||
technologies: [flutter, dart, android],
|
||||
link: 'https://play.google.com/store/apps/details?id=com.koerrieomat&hl=de',
|
||||
type: 'App',
|
||||
},
|
||||
{
|
||||
title: 'Unterricht',
|
||||
customer: 'SAE Institute Germany',
|
||||
image: '/img/projects/education.webp',
|
||||
desc: [
|
||||
'Vorbereitung und Durchführung von Unterricht in JavaScript und TypeScript.',
|
||||
],
|
||||
technologies: [js, ts]
|
||||
},
|
||||
{
|
||||
title: 'Headless CMS & Cache',
|
||||
customer: 'DEKRA',
|
||||
image: '/img/projects/dekra.webp',
|
||||
desc: [
|
||||
'Anbindung an ein Headless CMS und Entwicklung der dazugehörigen Komponentenbibliothek unter Einsatz von Tailwind, sowie serverseitiges Caching.',
|
||||
],
|
||||
technologies: [ts, nuxt, tw, njs]
|
||||
},
|
||||
{
|
||||
title: 'Bounce Script',
|
||||
customer: 'Bounce Commerce',
|
||||
link: 'https://bounce-commerce.de',
|
||||
image: '/img/projects/bounce.webp',
|
||||
desc: [
|
||||
'Script zum Einbinden in Web Shops für Bounce Management.',
|
||||
'Pures JavaScript, so klein gehalten wie möglich zur einfach Integration.',
|
||||
],
|
||||
technologies: [js]
|
||||
},
|
||||
{
|
||||
title: 'WEB.DE / GMX',
|
||||
customer: '1&1 Mail & Media',
|
||||
link: 'https://web.de',
|
||||
image: '/img/projects/webde.webp',
|
||||
desc: [
|
||||
'Neubau der Seiten web.de und GMX mit einem komponentenbasierten Ansatz unter Verwendung von VueJS.',
|
||||
'Optimiert für moderne Browser, während Internet Explorer in einer Extraversion angefertigt wurde.',
|
||||
],
|
||||
technologies: [js, vue, scss]
|
||||
},
|
||||
{
|
||||
title: 'Körrie! Landingpage',
|
||||
customer: 'KVK Berlin',
|
||||
link: 'https://körrie.de',
|
||||
image: '/img/projects/krrie.webp',
|
||||
desc: [
|
||||
'Neubau der Landingpage für "Kauft Körrie!". Die Prämisse war: Kein Schnickschnack.',
|
||||
'Deshalb aufgebaut mit simplem Js, HTML und CSS',
|
||||
],
|
||||
technologies: [html, css, js],
|
||||
},
|
||||
{
|
||||
title: 'UI Tools',
|
||||
customer: 'webfussel',
|
||||
link: 'https://uitools.webfussel.de',
|
||||
image: '/img/projects/uitools.webp',
|
||||
desc: [
|
||||
'Eine kleine Sammlung an Tools für die Erstellung von UIs.',
|
||||
'Farbpalette, Kontraste und CSS Variablen.',
|
||||
'Ist in aktiver Entwicklung.',
|
||||
],
|
||||
technologies: [nuxt, ts, pcss]
|
||||
},
|
||||
// {
|
||||
// customer: 'webfussel',
|
||||
// title: 'Shnaik - Teh Gaem',
|
||||
// link: 'https://shnaik.webfussel.de',
|
||||
// image: '/img/projects/shnaik.webp',
|
||||
// desc: [
|
||||
// 'Nachbau des bekannten Spiels "Snake" für die damaligen Nokia Handys.',
|
||||
// 'Meine erste Erfahrung mit Gaming Libraries und wurde eher als Experiment und Zeitvertreib angefertigt.',
|
||||
// ],
|
||||
// technologies: [ts, css]
|
||||
// },
|
||||
// {
|
||||
// title: 'PixelPalette',
|
||||
// customer: 'webfussel',
|
||||
// link: 'https://pixelpalette.webfussel.de',
|
||||
// image: '/img/projects/pp.webp',
|
||||
// desc: [
|
||||
// 'Ich hatte einige Tage eine Idee, wie man Grafiken mit 4 Farben - angelehnt den Gameboy - komprimieren und im Speicher unterbringen kann.',
|
||||
// 'Prototypisch zum Spaß erstellt.',
|
||||
// ],
|
||||
// technologies: [js, html, css]
|
||||
// },
|
||||
]
|
||||
</script>
|
84
app/components/Footer.vue
Normal file
84
app/components/Footer.vue
Normal 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>© 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>
|
90
app/components/Header.vue
Normal file
90
app/components/Header.vue
Normal file
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div ref="stickyWatch" />
|
||||
<header ref="header" class="Header">
|
||||
<div ref="headerWrapper" class="wrapper z-0">
|
||||
<strong>
|
||||
<svg aria-label="Logo" class="logo" height="40" viewBox="0 0 2500 2500" width="40">
|
||||
<g id="Logo">
|
||||
<g transform="matrix(2.00744,0,-5.91646e-31,2.00744,-1223.85,-1050.52)">
|
||||
<path class="fussel"
|
||||
d="M1232.34,1444.88L1356.88,1532.06C1356.88,1532.06 1405.5,1504.81 1444.06,1395.07C1464.03,1338.21 1476.18,1339.49 1506.32,1320.35C1579.8,1273.69 1638.29,1212.62 1630.86,1195.81C1560.6,1178.12 1512.77,1137.84 1506.32,1102.15L1618.41,946.736C1618.41,946.736 1514.23,877.412 1406.69,896.922C1407.7,845.817 1413.57,804.009 1481.42,759.931C1417.36,736.758 1260.23,740.351 1182.53,834.653C1115.13,783.067 1068.98,763.931 1008.18,759.931L1045.54,872.014C999.993,865.527 914.886,866.941 858.733,902.888C912.917,941.197 943.173,985.627 958.362,1033.91C883.905,1079.32 844.648,1134.09 808.918,1195.81C875.598,1205.68 938.224,1226.42 970.816,1282.99C1016.82,1362.83 1028.77,1456.11 1107.81,1532.06L1232.34,1444.88"/>
|
||||
</g>
|
||||
<g transform="matrix(1,0,0,1,-422.589,697.589)">
|
||||
<path class="glasses" d="M1747.59,277.411C1695.36,294.131 1645.34,294.246 1597.59,277.411"/>
|
||||
</g>
|
||||
<path class="glasses"
|
||||
d="M1175,975C1189.02,1037.51 1161.76,1216.53 1125,1300C1027.14,1307.22 909.088,1298.04 825,1275C798.072,1183.9 789.715,1050.66 825,950C935.158,934.697 1076.23,935.423 1175,975Z"/>
|
||||
<g transform="matrix(-1,0,0,1,2500,2.20268e-13)">
|
||||
<path class="glasses"
|
||||
d="M1175,975C1189.02,1037.51 1161.76,1216.53 1125,1300C1027.14,1307.22 909.088,1298.04 825,1275C798.072,1183.9 789.715,1050.66 825,950C935.158,934.697 1076.23,935.423 1175,975Z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
webfussel
|
||||
</strong>
|
||||
<input id="navToggle" v-model="isBurgerOpen" type="checkbox">
|
||||
<label :aria-label="burgerLabel" for="navToggle">
|
||||
<span/><span/><span/><span/>
|
||||
</label>
|
||||
<nav>
|
||||
<ul class="main-nav">
|
||||
<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>
|
||||
</div>
|
||||
</header>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
let observer: IntersectionObserver
|
||||
const header = ref<HTMLElement | null>(null)
|
||||
const headerWrapper = ref<HTMLElement | null>(null)
|
||||
const stickyWatch = ref<HTMLElement | null>(null)
|
||||
|
||||
const isBurgerOpen = ref<boolean>(false)
|
||||
const burgerOpenLabel = 'Burgermenü öffnen'
|
||||
const burgerCloseLabel = 'Burgermenü schließen'
|
||||
const burgerLabel = computed(() => isBurgerOpen.value ? burgerCloseLabel : burgerOpenLabel)
|
||||
|
||||
const nav = [
|
||||
{
|
||||
to: `/`,
|
||||
label: 'home',
|
||||
'aria-label': 'Link dieser Seite: Startseite'
|
||||
},
|
||||
{
|
||||
to: `/services`,
|
||||
label: 'leistungen',
|
||||
aria: 'Link dieser Seite: Leistungen'
|
||||
},
|
||||
{
|
||||
to: `/references`,
|
||||
label: 'referenzen',
|
||||
aria: 'Link dieser Seite: Referenzen'
|
||||
},
|
||||
{
|
||||
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)
|
||||
headerWrapper.value?.classList.toggle('z-0', isIntersecting)
|
||||
}, {
|
||||
rootMargin: '3% 0px 0px 0px'
|
||||
})
|
||||
observer.observe(stickyWatch.value!)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
})
|
||||
</script>
|
37
app/components/Intro.vue
Normal file
37
app/components/Intro.vue
Normal file
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<section id="intro" class="Intro content full default-gap">
|
||||
<div class="intro-text flex-col default-gap">
|
||||
<h1 class="flex-col">
|
||||
<span class="greeting">Moin.</span>
|
||||
<span class="my-name-wrapper">Ich bin <span class="nowrap"><span class="highlight">Fiona </span><small>Urban</small><span class="dot">.</span></span></span>
|
||||
</h1>
|
||||
<h2>
|
||||
Component <span class="highlight">&</span> API Entwicklerin
|
||||
</h2>
|
||||
<p class="fulltext">
|
||||
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">
|
||||
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 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>
|
||||
<source width="750" height="866" media="(min-width: 431px)" srcset="/img/profile_big.webp" />
|
||||
<img width="430" height="866" src="/img/profile_small.webp" alt="Bild von Fiona Urban" />
|
||||
</picture>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
</script>
|
34
app/components/Person.vue
Normal file
34
app/components/Person.vue
Normal file
|
@ -0,0 +1,34 @@
|
|||
<template>
|
||||
<article class="Person flex-col">
|
||||
<img
|
||||
loading="lazy"
|
||||
width="150"
|
||||
height="150"
|
||||
:srcset="[userImage('1x', true), userImage('2x', true), userImage('3x', true)].join(', ')"
|
||||
:src="userImage('1x', false)"
|
||||
:alt="`Bild von ${name}`"
|
||||
/>
|
||||
<h3>{{name}}</h3>
|
||||
<p>
|
||||
<span v-for="tag in tags">{{tag}}</span>
|
||||
</p>
|
||||
<p class="flavour">{{flavour}}</p>
|
||||
<Button :href="link" class="button" target="_blank" rel="noreferrer noopener" :aria-label="`Externer Link zur Homepage von ${name}`">
|
||||
Zur Homepage
|
||||
</Button>
|
||||
</article>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
img: string
|
||||
name: string
|
||||
tags: string[]
|
||||
flavour: string
|
||||
link: string
|
||||
}
|
||||
|
||||
const { img } = defineProps<Props>()
|
||||
|
||||
const userImage = getImage('/img/network/', img)
|
||||
</script>
|
157
app/components/Services.vue
Normal file
157
app/components/Services.vue
Normal file
|
@ -0,0 +1,157 @@
|
|||
<template>
|
||||
<section id="services" class="Services content">
|
||||
<h2>Services.</h2>
|
||||
<h3>Du hast also beschlossen, dass du <span class="highlight">meine Hilfe</span> brauchst. Cool!</h3>
|
||||
<p class="margin-top">Hinter meinen Angeboten gibt es <span class="highlight">keinerlei Abos oder versteckte Kosten</span>.
|
||||
Aus Transparenzgründen sei aber gesagt, dass sich alle Preise zzgl. 19 % Umsatzsteuer verstehen.</p>
|
||||
<div class="service-list margin-top default-gap">
|
||||
<article v-for="service in services" class="z-2 card flex-col default-gap">
|
||||
<h3 class="flex-col default-gap">
|
||||
<span>{{service.title}}</span>
|
||||
<span class="highlight">{{service.price}}</span>
|
||||
</h3>
|
||||
<span class="chip">{{service.availability}}</span>
|
||||
<p>{{service.smallClaim}}</p>
|
||||
<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="ph:caret-circle-double-right-duotone" aria-hidden="true" alt="checkmark icon" size="1.5em" mode="svg" />
|
||||
<span>{{point}}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
</div>
|
||||
<article class="z-2 card flex-col default-gap margin-top">
|
||||
<h3>Keinen Bock auf Telen? Understandable.</h3>
|
||||
<p>Dann schreib mir einfach gerne direkt eine E-Mail an
|
||||
<ClientOnly><a class="mail" href="mailto:anfragen@webfussel.de">anfragen@webfussel.de<Icon name="ri:mail-line" aria-hidden="true" alt="mail icon" mode="svg" /></a></ClientOnly>
|
||||
</p>
|
||||
<h3>Keine Kohle? Kommt vor.</h3>
|
||||
<p>Meld dich trotzdem. Eventuell ist dein Projekt ja cool genug, dass ich dir da auch entsprechend entgegenkommen kann. :)</p>
|
||||
</article>
|
||||
<h3 id="network" class="margin-top-big">Mein Netzwerk</h3>
|
||||
<p class="margin-top">Doch auch wenn ich mal voll ausgelastet bin - keine Sorge!
|
||||
Mein <span class="highlight">Netzwerk an Profis</span> kann dir sicher auch weiterhelfen.
|
||||
</p>
|
||||
<ClientOnly>
|
||||
<div class="network-list margin-top">
|
||||
<div class="scroll-container default-gap">
|
||||
<Person ref="persons" v-for="person in network" v-bind="person" />
|
||||
</div>
|
||||
</div>
|
||||
</ClientOnly>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import Person from '~/components/Person.vue'
|
||||
|
||||
const slots : number = 0
|
||||
const slotsLabel = `${slots} ${slots === 1 ? `Slot` : `Slots`} frei`
|
||||
|
||||
const freeFromDate = new Date(2025, 6, 1)
|
||||
const intl = new Intl.DateTimeFormat('de-DE', { day: '2-digit', month: '2-digit', year: 'numeric'})
|
||||
const readableDate = intl.format(freeFromDate)
|
||||
|
||||
const services = [
|
||||
{
|
||||
title: 'Quick Check',
|
||||
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: [
|
||||
'Untersuchung des Quellcodes',
|
||||
'Untersuchung der Performance',
|
||||
'Tipps zu CSS und Best Practices',
|
||||
'Behebung unkompliziert nachbuchen',
|
||||
'Für selbst gebaute Seiten',
|
||||
],
|
||||
}, {
|
||||
title: 'Projektbuchung',
|
||||
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: [
|
||||
'Anforderungsanalyse',
|
||||
'Kontinuierliche Projekt-Updates',
|
||||
'Fixe Kosten und Feature-Sets',
|
||||
'Nur 50 % Projektpreis als Anzahlung',
|
||||
],
|
||||
}, {
|
||||
title: 'Stundenbuchung',
|
||||
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: [
|
||||
'Flexible Aufgabenverteilung',
|
||||
'Arbeiten nach agilen Prinzipien',
|
||||
'Monatliche Abrechnung',
|
||||
'Kündigungsfrist von einer Woche',
|
||||
'Flexible Buchung ab 80 Stunden',
|
||||
],
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
const shuffle = <T>(unshuffled : T[]) => unshuffled
|
||||
.map(value => ({ value, sort: Math.random() }))
|
||||
.sort((a, b) => a.sort - b.sort)
|
||||
.map(({ value }) => value)
|
||||
|
||||
const network = shuffle([
|
||||
{
|
||||
name: 'Robert Janus',
|
||||
img: 'robert',
|
||||
tags: ['Digitalberatung', 'Webentwicklung', 'eCommerce'],
|
||||
flavour: 'Website, SEO und Conversions. Auf einen Klick.',
|
||||
link: 'https://robertjanus.de/webertoire',
|
||||
},
|
||||
// {
|
||||
// name: 'Matthias Lehmann',
|
||||
// img: 'matthias',
|
||||
// tags: ['Onlineportale für Patienten', 'Kunden', 'Mitarbeiter'],
|
||||
// flavour: 'Software die macht, was DU willst!',
|
||||
// link: 'https://mind-deploy.de',
|
||||
// },
|
||||
{
|
||||
name: 'Maximilian Schluer',
|
||||
img: 'max',
|
||||
tags: ['iOS Development', 'Software-QA'],
|
||||
flavour: 'Kann dein iOS-Team unterstützen oder dein Software-Qualitätsproblem lösen – egal welches.',
|
||||
link: 'https://max-schluer.de',
|
||||
},
|
||||
// {
|
||||
// name: 'Maria Salcedo',
|
||||
// img: 'maria',
|
||||
// tags: ['Backend', 'DevOps', 'Architektur'],
|
||||
// flavour: 'Effizient und kommunikativ. "You build it, you run it."',
|
||||
// link: 'https://masagu.dev',
|
||||
// },
|
||||
{
|
||||
name: 'Judith Böhlert',
|
||||
img: 'judith',
|
||||
tags: ['Full-stack', 'Frontend'],
|
||||
flavour: 'MVPs und Prototypen - schnell, schick und ohne Drama.',
|
||||
link: 'https://judithboehlert.com',
|
||||
},
|
||||
{
|
||||
name: 'Kevin Damiani',
|
||||
img: 'kevin',
|
||||
tags: ['Webentwicklung', 'Frontend'],
|
||||
flavour: 'Erfahrener Frontend-Entwickler mit Fokus auf Performance, Barrierefreiheit und moderne Technologien.',
|
||||
link: 'https://kevin-damiani.de',
|
||||
},
|
||||
])
|
||||
</script>
|
64
app/components/Skills.vue
Normal file
64
app/components/Skills.vue
Normal file
|
@ -0,0 +1,64 @@
|
|||
<template>
|
||||
<section id="skills" class="Skills content">
|
||||
<h2>Meine Expertise.</h2>
|
||||
<h3>Dies sind meine <span class="highlight">Spezialgebiete</span> - aber ich bin flexibel!</h3>
|
||||
<div class="skill-list margin-top default-gap">
|
||||
<article class="z-2 card flex-col default-gap" v-for="skill in skills">
|
||||
<h3>{{skill.title}}</h3>
|
||||
<main>
|
||||
<p v-for="(t, i) in skill.text" :class="[i === skills.length - 1 && 'margin-top bold']">{{t}}</p>
|
||||
</main>
|
||||
</article>
|
||||
</div>
|
||||
<article class="tech-list z-2 card flex-col default-gap margin-top">
|
||||
<h3>Technologien</h3>
|
||||
<p>Neben den klassischen Webentwicklungsstandards JavaScript, HTML und CSS biete ich außerdem folgende Technologien.</p>
|
||||
<ul class="default-gap">
|
||||
<li v-for="tech in technologies">
|
||||
<Technology v-bind="tech" size="l"/>
|
||||
</li>
|
||||
</ul>
|
||||
</article>
|
||||
<div class="bottom flex-col margin-top default-gap">
|
||||
<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, njs, nuxt, pcss, react, rust, ts, vitest, vue, webstorm } from '../utils/skills'
|
||||
|
||||
const technologies = [nuxt, ts, pcss, vue, react, njs, vitest, gl, webstorm, rust]
|
||||
|
||||
const skills = [
|
||||
{
|
||||
title: 'Komponenten',
|
||||
text: [
|
||||
'Komponenten sind die Teile in deiner Applikation, die alles ansehnlich machen.',
|
||||
'Mit sauber implementierten, responsiven Bausteinen kannst du deine Seite gut Strukturieren, Daten sauber darstellen und den User einspannen.',
|
||||
'Vom kleinen Button bis hin zur Umfangreichen Tabelle bau ich dir (fast) alles.'
|
||||
],
|
||||
}, {
|
||||
title: 'APIs',
|
||||
text: [
|
||||
'Du hast Daten in einer Datenbank liegen, aber keine Ahnung, wie du da gescheit rankommen sollst?',
|
||||
'Liegen deine Daten eventuell sogar verstreut an mehreren Orten, über Datenbanken, Dateien und anderen Storagemöglichkeiten verteilt?',
|
||||
'Kein Ding. Ich bau dir eine Schnittstelle, die alles easy zusammenträgt.'
|
||||
],
|
||||
}, {
|
||||
title: 'Headless CMS',
|
||||
text: [
|
||||
'Wenn man ein Headless CMS anbinden will, dann verknüpft das Komponenten und APIs.',
|
||||
'Für eine saubere und dynamische Einbindung reicht die Library, die euch vom Hersteller zur Verfügung gestellt wird, oft nicht aus.',
|
||||
'Übersichtliche Projektstruktur und saubere Auflösung der Daten - mit Fusselgarantie.'
|
||||
],
|
||||
}
|
||||
]
|
||||
</script>
|
35
app/components/Technology.vue
Normal file
35
app/components/Technology.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<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>
|
||||
<img v-else loading="lazy" :height="getPixelForSize()" :width="getWidth()" :src="img" :alt="altText()"/>
|
||||
<span class="tip">{{name}}</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
type Props = {
|
||||
img: string
|
||||
name: string
|
||||
link?: string
|
||||
width?: number
|
||||
size?: 's' | 'm' | 'l'
|
||||
}
|
||||
|
||||
const {
|
||||
name,
|
||||
size = 'm',
|
||||
width = 50,
|
||||
} = defineProps<Props>()
|
||||
|
||||
const sizes = {
|
||||
s: 15,
|
||||
m: 30,
|
||||
l: 50,
|
||||
}
|
||||
|
||||
const altText = () => `Icon für ${name}`
|
||||
const getPixelForSize = () => sizes[size]
|
||||
const getWidth = () => width / 50 * getPixelForSize()
|
||||
</script>
|
Loading…
Add table
Add a link
Reference in a new issue