Switch to pnpm, add home page and navigation menu
This commit is contained in:
parent
2ae875cb45
commit
e86f299580
21 changed files with 3209 additions and 2173 deletions
|
@ -1,4 +1,7 @@
|
||||||
{
|
{
|
||||||
"root": true,
|
"root": true,
|
||||||
"extends": "awzzm-vue/v3"
|
"extends": "awzzm-vue/v3",
|
||||||
|
"rules": {
|
||||||
|
"vue/no-static-inline-styles": "off"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
2
.nvmrc
2
.nvmrc
|
@ -1 +1 @@
|
||||||
15
|
16
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
# moritzruth.de
|
# moritzruth.de
|
||||||
> My personal website
|
|
||||||
|
|
||||||
:fire: [**moritzruth.de**](https://moritzruth.de)
|
🔥 [**moritzruth.de**](https://moritzruth.de)
|
||||||
|
|
|
@ -4,12 +4,14 @@
|
||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
|
||||||
<title>Moritz Ruth</title>
|
<title>Moritz Ruth</title>
|
||||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css">
|
<link rel="stylesheet" href="/node_modules/@fontsource/plus-jakarta-sans/400.css">
|
||||||
<meta name="description" content="I do web and print things, sometimes photography.">
|
<link rel="stylesheet" href="/node_modules/@fontsource/plus-jakarta-sans/800.css">
|
||||||
|
<link rel="stylesheet" href="/node_modules/@fontsource/syne/800.css">
|
||||||
|
<meta name="description" content="web development and graphic design.">
|
||||||
<meta name="keywords" content="web, dev, development, coding, moritz, ruth, development, design">
|
<meta name="keywords" content="web, dev, development, coding, moritz, ruth, development, design">
|
||||||
<link rel="shortcut icon" type="image/png" href="/icon.png">
|
<link rel="shortcut icon" type="image/png" href="/icon.png">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body class="bg-[#fefefe]">
|
||||||
<div id="app"></div>
|
<div id="app"></div>
|
||||||
<script type="module" src="/src/main.js"></script>
|
<script type="module" src="/src/main.js"></script>
|
||||||
<script
|
<script
|
||||||
|
|
24
package.json
24
package.json
|
@ -2,22 +2,28 @@
|
||||||
"name": "moritzruth.de",
|
"name": "moritzruth.de",
|
||||||
"author": "Moritz Ruth <dev@moritzruth.de>",
|
"author": "Moritz Ruth <dev@moritzruth.de>",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite --host",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
"lint": "eslint . --fix"
|
"lint": "eslint . --fix"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@zhuowenli/vue-feather-icons": "^5.0.2",
|
"@fontsource/plus-jakarta-sans": "^4.4.5",
|
||||||
"vue": "^3.0.11"
|
"@fontsource/syne": "^4.4.5",
|
||||||
|
"@vueuse/core": "^5.0.3",
|
||||||
|
"blobs": "^2.2.1-beta.1",
|
||||||
|
"kute.js": "^2.1.2",
|
||||||
|
"vue": "^3.1.1",
|
||||||
|
"vue-router": "4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-vue": "^1.2.2",
|
"@vitejs/plugin-vue": "^1.2.3",
|
||||||
"@vue/compiler-sfc": "^3.0.11",
|
"@vue/compiler-sfc": "^3.1.1",
|
||||||
"eslint": "^7.25.0",
|
"eslint": "^7.28.0",
|
||||||
"eslint-config-awzzm-vue": "^1.6.0",
|
"eslint-config-awzzm-vue": "^1.6.0",
|
||||||
"vite": "^2.2.3",
|
"vite": "^2.3.7",
|
||||||
"vite-plugin-windicss": "^0.15.10",
|
"vite-plugin-pages": "^0.13.1",
|
||||||
"windicss": "^2.5.14"
|
"vite-plugin-windicss": "^1.0.4",
|
||||||
|
"windicss": "^3.1.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
2705
pnpm-lock.yaml
generated
Normal file
2705
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,2 +1 @@
|
||||||
https://moritz-ruth.de/* https://moritzruth.de/:splat 301!
|
https://moritz-ruth.de/* https://moritzruth.de/:splat 301!
|
||||||
/impressum /impressum.txt 200
|
|
||||||
|
|
|
@ -1,3 +1,2 @@
|
||||||
User-agent: *
|
User-agent: *
|
||||||
Disallow: /impressum
|
Disallow: /impressum
|
||||||
Disallow: /impressum.txt
|
|
||||||
|
|
52
src/App.vue
52
src/App.vue
|
@ -1,59 +1,23 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="bg-black w-screen h-screen text-white overflow-hidden flex justify-center items-center">
|
<NavigationMenu/>
|
||||||
<div class="px-8 mx-auto max-w-4xl w-full h-96 font-extrabold relative">
|
<div class="overflow-x-hidden">
|
||||||
<div class="app__circle w-52 h-52 rounded-full bg-gradient-to-bl from-yellow-300 to-yellow-800 mr-15"/>
|
<div class="bg-white text-black max-w-1200px mx-auto px-6 sm:px-10">
|
||||||
<div class="app__box w-96 h-44 bg-gradient-to-bl from-yellow-300 to-yellow-800 rotate-12 transform"/>
|
<router-view/>
|
||||||
<div class="text-6xl sm:text-8xl font-black relative -left-1">
|
|
||||||
Moritz Ruth
|
|
||||||
</div>
|
|
||||||
<div class="flex mt-8 justify-items-stretch">
|
|
||||||
<div class="w-1 bg-gradient-to-b from-yellow-300 to-yellow-500"/>
|
|
||||||
<div class="text-2xl sm:text-4xl ml-5 sm:ml-8 space-y-2 pb-1">
|
|
||||||
<p>I do web and print things.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="fixed flex bottom-15 sm:bottom-8 space-x-5">
|
|
||||||
<a class="app__social-link" href="https://twitter.com/moritz_ruth" aria-label="Twitter"><TwitterIcon size="1x"/></a>
|
|
||||||
<a class="app__social-link" href="https://github.com/moritzruth" aria-label="GitHub"><GithubIcon size="1x"/></a>
|
|
||||||
<a class="app__social-link" href="mailto:hey@m0.is" aria-label="Email"><MailIcon size="1x"/></a>
|
|
||||||
</div>
|
|
||||||
<div class="fixed bottom-0 right-0 p-2 sm:p-8 sm:text-gray-400 font-bold">
|
|
||||||
<a href="/impressum" target="_blank">Impressum</a>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style>
|
||||||
body {
|
body {
|
||||||
background: rgba(255, 255, 255, 0.2);
|
font-size: 20px;
|
||||||
}
|
|
||||||
|
|
||||||
.app__circle {
|
|
||||||
position: absolute;
|
|
||||||
top: -10em;
|
|
||||||
right: -15vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app__box {
|
|
||||||
position: absolute;
|
|
||||||
top: 25em;
|
|
||||||
left: -25vw;
|
|
||||||
}
|
|
||||||
|
|
||||||
.app__social-link {
|
|
||||||
@apply text-black text-3xl p-4 bg-white bg-opacity-50 from-yellow-300 to-yellow-500 -sm:rounded-full flex items-center justify-center
|
|
||||||
transition duration-200 sm:bg-opacity-0 sm:text-white sm:hover:bg-opacity-30;
|
|
||||||
|
|
||||||
backdrop-filter: blur(5px);
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { TwitterIcon, MailIcon, GithubIcon } from "@zhuowenli/vue-feather-icons"
|
import NavigationMenu from "./components/NavigationMenu.vue"
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "App",
|
name: "App",
|
||||||
components: { TwitterIcon, GithubIcon, MailIcon }
|
components: { NavigationMenu }
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
132
src/components/BlurredBlobCanvas.vue
Normal file
132
src/components/BlurredBlobCanvas.vue
Normal file
|
@ -0,0 +1,132 @@
|
||||||
|
<template>
|
||||||
|
<svg :style="{ width: size, height: size, filter: `blur(${blur}px)` }">
|
||||||
|
<path
|
||||||
|
v-for="(blob, index) in blobs"
|
||||||
|
:key="index"
|
||||||
|
:ref="blob"
|
||||||
|
style="transition: 2000ms; transition-property: opacity, transform; transform-origin: center"
|
||||||
|
d=""
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as blobs2 from "blobs/v2"
|
||||||
|
import KUTE from "kute.js"
|
||||||
|
import { ref } from "vue"
|
||||||
|
import { getListOfIndexes } from "../getListOfIndexes.js"
|
||||||
|
|
||||||
|
const BLOB_OPTIONS = {
|
||||||
|
extraPoints: 6,
|
||||||
|
randomness: 30
|
||||||
|
}
|
||||||
|
|
||||||
|
// let lastMouseEvent = null
|
||||||
|
// let currentMouseEvent = null
|
||||||
|
// let mouseSpeed = 0
|
||||||
|
//
|
||||||
|
// window.addEventListener("mousemove", event => {
|
||||||
|
// currentMouseEvent = event
|
||||||
|
// })
|
||||||
|
//
|
||||||
|
// setInterval(() => {
|
||||||
|
// if (currentMouseEvent !== null && lastMouseEvent !== null && currentMouseEvent !== lastMouseEvent) {
|
||||||
|
// const deltaX = currentMouseEvent.screenX - lastMouseEvent.screenX
|
||||||
|
// const deltaY = currentMouseEvent.screenY - lastMouseEvent.screenY
|
||||||
|
// mouseSpeed = Math.sqrt((deltaX * deltaX) + (deltaY * deltaY))
|
||||||
|
// } else {
|
||||||
|
// mouseSpeed = 0
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// lastMouseEvent = currentMouseEvent
|
||||||
|
// console.log(mouseSpeed)
|
||||||
|
// }, 100)
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "BlurredBlobCanvas",
|
||||||
|
props: {
|
||||||
|
colors: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
blur: {
|
||||||
|
type: Number,
|
||||||
|
default: 30
|
||||||
|
},
|
||||||
|
minimumDuration: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
durationVariation: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
},
|
||||||
|
minimumOpacity: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
opacityVariation: {
|
||||||
|
type: Number,
|
||||||
|
default: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
// eslint-disable-next-line unicorn/no-new-array
|
||||||
|
blobs: getListOfIndexes(this.colors.length).map(() => ref(null))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
const animate = index => {
|
||||||
|
const blob = this.blobs[index]
|
||||||
|
const duration = (Math.random() * this.durationVariation) + this.minimumDuration
|
||||||
|
|
||||||
|
if (blob.value) {
|
||||||
|
KUTE.to(
|
||||||
|
blob.value,
|
||||||
|
{
|
||||||
|
path: blobs2.svgPath(this.getRandomBlobOptions())
|
||||||
|
},
|
||||||
|
{
|
||||||
|
duration,
|
||||||
|
morphPrecision: 5,
|
||||||
|
easing: "easingSinusoidalInOut",
|
||||||
|
onComplete() {
|
||||||
|
animate(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
).start()
|
||||||
|
|
||||||
|
this.setOpacity(blob.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
this.blobs.forEach((blob, index) => {
|
||||||
|
blob.value.setAttribute("d", blobs2.svgPath(this.getRandomBlobOptions()))
|
||||||
|
|
||||||
|
blob.value.setAttribute("fill", this.colors[index])
|
||||||
|
this.setOpacity(blob.value)
|
||||||
|
|
||||||
|
animate(index)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getRandomBlobOptions() {
|
||||||
|
return {
|
||||||
|
...BLOB_OPTIONS,
|
||||||
|
seed: Math.random(),
|
||||||
|
size: this.size
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setOpacity(element) {
|
||||||
|
element.style.opacity = (Math.random() * this.opacityVariation) + this.minimumOpacity
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
55
src/components/ClickToBounce.vue
Normal file
55
src/components/ClickToBounce.vue
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div ref="wrapper" class="select-none">
|
||||||
|
<slot :setTrigger="setTrigger"/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { ref, watchEffect } from "vue"
|
||||||
|
import KUTE from "kute.js"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "ClickToBounce",
|
||||||
|
setup() {
|
||||||
|
const trigger = ref(null)
|
||||||
|
const wrapper = ref(null)
|
||||||
|
|
||||||
|
let current = null
|
||||||
|
|
||||||
|
watchEffect(onInvalidate => {
|
||||||
|
if (trigger.value !== null) {
|
||||||
|
const listener = () => {
|
||||||
|
current?.stop()
|
||||||
|
|
||||||
|
current = KUTE.to(wrapper.value, { scale: 0.9 }, {
|
||||||
|
duration: 100,
|
||||||
|
easing: "easingElasticInOut",
|
||||||
|
onComplete() {
|
||||||
|
current = KUTE.to(wrapper.value, { scale: 1 }, {
|
||||||
|
duration: 100,
|
||||||
|
easing: "easingElasticInOut",
|
||||||
|
onComplete() {
|
||||||
|
current = null
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
}).start()
|
||||||
|
}
|
||||||
|
|
||||||
|
trigger.value.addEventListener("click", listener, { passive: true })
|
||||||
|
|
||||||
|
onInvalidate(() => {
|
||||||
|
trigger.value.removeEventListener("click", listener)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
setTrigger(element) {
|
||||||
|
trigger.value = element
|
||||||
|
},
|
||||||
|
wrapper
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
104
src/components/NavigationMenu.vue
Normal file
104
src/components/NavigationMenu.vue
Normal file
|
@ -0,0 +1,104 @@
|
||||||
|
<template>
|
||||||
|
<div class="sm:hidden fixed z-101 bottom-3 right-3 rounded-full backdrop-filter backdrop-blur-20 bg-white bg-opacity-20 w-20 h-20 flex justify-center items-center shadow-lg">
|
||||||
|
<div role="button" class="flex flex-col justify-evenly items-center h-10" @click="active = !active">
|
||||||
|
<div class="w-10 h-2px bg-black transition duration-200 transform" :style="`transform: ${active ? 'translateY(350%)' : ''} rotate(${active ? 45 : 0}deg)`"/>
|
||||||
|
<div class="w-10 h-2px bg-black transition duration-200 transform" :style="`transform: ${active ? 'translateY(-350%)' : ''} rotate(${active ? -45 : 0}deg)`"/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<nav
|
||||||
|
class="fixed sm:sticky top-0 z-100 w-screen h-screen sm:h-20 backdrop-filter backdrop-blur-40 bg-white bg-opacity-40 transition duration-200"
|
||||||
|
:class="[scrolled && 'shadow-lg', active ? 'opacity-100' : '-sm:opacity-0 -sm:pointer-events-none']"
|
||||||
|
>
|
||||||
|
<div class="flex items-center justify-between h-full max-w-1200px mx-auto flex-grow -sm:flex-col px-6 sm:px-10">
|
||||||
|
<div class="fixed transition-all duration-500" :style="{ left: blobState.x + 'px', top: blobState.y + 'px', opacity: blobState.show ? 1 : 0 }">
|
||||||
|
<BlurredBlobCanvas
|
||||||
|
:colors="['#eb34cf', '#79faff']"
|
||||||
|
:opacity-variation="0"
|
||||||
|
:minimum-opacity="0.9"
|
||||||
|
:minimum-duration="1000"
|
||||||
|
:duration-variation="500"
|
||||||
|
:blur="10"
|
||||||
|
:size="100"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<router-link class="uppercase font-special relative top-1 -sm:mt-20" to="/" @click="active = false">
|
||||||
|
Moritz Ruth
|
||||||
|
</router-link>
|
||||||
|
<div class="flex -sm:flex-col -sm:mb-30vh justify-end items-center sm:space-x-20 -sm:space-y-10 relative">
|
||||||
|
<router-link
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.to"
|
||||||
|
:ref="item.element"
|
||||||
|
class="lowercase text-2xl -sm:text-4xl"
|
||||||
|
:to="item.to"
|
||||||
|
@click="active = false"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { useWindowScroll, useWindowSize } from "@vueuse/core"
|
||||||
|
import { computed, ref, watch, reactive } from "vue"
|
||||||
|
import { useRoute } from "vue-router"
|
||||||
|
import { useWindowScrollLock } from "../utils/useWindowScrollLock.js"
|
||||||
|
import BlurredBlobCanvas from "./BlurredBlobCanvas.vue"
|
||||||
|
|
||||||
|
const ITEMS = [
|
||||||
|
{
|
||||||
|
label: "Projects",
|
||||||
|
to: "/projects"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "Contact",
|
||||||
|
to: "/contact"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "NavigationMenu",
|
||||||
|
components: { BlurredBlobCanvas },
|
||||||
|
setup() {
|
||||||
|
const { y: windowScroll } = useWindowScroll()
|
||||||
|
const { width: windowWidth } = useWindowSize()
|
||||||
|
const route = useRoute()
|
||||||
|
const active = ref(false)
|
||||||
|
|
||||||
|
useWindowScrollLock(active)
|
||||||
|
|
||||||
|
const items = ITEMS.map(item => ({
|
||||||
|
...item,
|
||||||
|
element: ref(null)
|
||||||
|
}))
|
||||||
|
|
||||||
|
const activeItem = computed(() => items.find(item => item.to === route.path) ?? null)
|
||||||
|
const blobState = reactive({
|
||||||
|
x: windowWidth / 2,
|
||||||
|
y: 20,
|
||||||
|
show: false
|
||||||
|
})
|
||||||
|
|
||||||
|
watch([windowWidth, activeItem], () => {
|
||||||
|
if (activeItem.value === null) {
|
||||||
|
blobState.show = false
|
||||||
|
} else {
|
||||||
|
const { x, width, y } = activeItem.value.element.value.$el.getBoundingClientRect()
|
||||||
|
|
||||||
|
blobState.x = x + (width / 2) - 20
|
||||||
|
blobState.y = y - 20
|
||||||
|
blobState.show = true
|
||||||
|
}
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
return {
|
||||||
|
scrolled: computed(() => windowScroll.value > 0),
|
||||||
|
blobState,
|
||||||
|
items,
|
||||||
|
active
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
2
src/getListOfIndexes.js
Normal file
2
src/getListOfIndexes.js
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
// eslint-disable-next-line unicorn/no-new-array
|
||||||
|
export const getListOfIndexes = size => [...new Array(size).keys()]
|
11
src/main.js
11
src/main.js
|
@ -1,7 +1,14 @@
|
||||||
import "virtual:windi.css"
|
import "virtual:windi.css"
|
||||||
|
import routes from "virtual:generated-pages"
|
||||||
import { createApp } from "vue"
|
import { createApp } from "vue"
|
||||||
|
import { createRouter, createWebHistory } from "vue-router"
|
||||||
import App from "./App.vue"
|
import App from "./App.vue"
|
||||||
|
|
||||||
createApp(App).mount("#app")
|
const router = createRouter({
|
||||||
|
history: createWebHistory(),
|
||||||
|
routes
|
||||||
|
})
|
||||||
|
|
||||||
if (window.location.path !== "/") history.replaceState({}, "", "/")
|
createApp(App)
|
||||||
|
.use(router)
|
||||||
|
.mount("#app")
|
||||||
|
|
11
src/pages/contact.vue
Normal file
11
src/pages/contact.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
contact
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ContactPage"
|
||||||
|
}
|
||||||
|
</script>
|
113
src/pages/index.vue
Normal file
113
src/pages/index.vue
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
<template>
|
||||||
|
<main class="flex -md:flex-col justify-between items-start md:items-center min-h-80vh">
|
||||||
|
<div class="font-bold relative -md:pt-20vh -md:pb-20 transform skew-x-7 -skew-y-7 rotate-7">
|
||||||
|
<div class="_fade-2">
|
||||||
|
<div class="_pattern transform rotate-179 absolute w-full h-40 -left-4 md:-left-10 top-20vh -md:-mt-10 md:-top-10 opacity-3 md:opacity-5"/>
|
||||||
|
</div>
|
||||||
|
<div class="_slide">
|
||||||
|
<ClickToBounce v-slot="{ setTrigger }" class="text-6xl md:text-7xl -md:-mt-2 mb-30 font-special" style="--delay: 0">
|
||||||
|
Hey <span :ref="setTrigger" class="_clap">👋</span>
|
||||||
|
</ClickToBounce>
|
||||||
|
</div>
|
||||||
|
<div class="font-special text-3xl sm:text-5xl mb-5 _slide" style="--delay: 200">
|
||||||
|
I’m<br>
|
||||||
|
<span class="text-1.1em text-transparent bg-clip-text bg-gradient-to-b from-blue-500 to-blue-900">Moritz Ruth</span>
|
||||||
|
</div>
|
||||||
|
<div class="text-gray-800 text-2xl sm:text-3xl">
|
||||||
|
<ul class="_list">
|
||||||
|
<li class="_slide" style="--delay: 400">
|
||||||
|
software developer
|
||||||
|
</li>
|
||||||
|
<li class="_slide" style="--delay: 600">
|
||||||
|
graphic designer
|
||||||
|
</li>
|
||||||
|
<li class="_slide" style="--delay: 800">
|
||||||
|
typography enthusiast
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="self-center">
|
||||||
|
<BlurredBlobCanvas
|
||||||
|
class="_fade-1"
|
||||||
|
:colors="['#6577fc', '#eb34cf', '#6577fc', '#eb34cf']"
|
||||||
|
:size="300"
|
||||||
|
:blur="30"
|
||||||
|
:minimum-duration="2000"
|
||||||
|
:duration-variation="500"
|
||||||
|
:minimum-opacity="0.2"
|
||||||
|
:opacity-variation="0.4"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
._pattern {
|
||||||
|
background-image: url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='72' viewBox='0 0 36 72'%3E%3Cg fill-rule='evenodd'%3E%3Cg \
|
||||||
|
fill='%23000000' fill-opacity='1'%3E%3Cpath d='M2 6h12L8 18 2 6zm18 36h12l-6 12-6-12z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
|
||||||
|
}
|
||||||
|
|
||||||
|
._list > li::before {
|
||||||
|
@apply text-blue-900;
|
||||||
|
content: "*";
|
||||||
|
}
|
||||||
|
|
||||||
|
/* created using emojicursor.app */
|
||||||
|
._clap {
|
||||||
|
cursor: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='50' height='90' viewport='0 0 100 100' style='fill:black;font-size:50px;'><text y='50%'>✋\
|
||||||
|
</text></svg>") 16 0,auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
._fade-1 {
|
||||||
|
animation: fade 2s ease-in both;
|
||||||
|
}
|
||||||
|
|
||||||
|
._fade-2 {
|
||||||
|
animation: fade 2s 2s ease-out both;
|
||||||
|
}
|
||||||
|
|
||||||
|
._slide {
|
||||||
|
--delay: 0;
|
||||||
|
animation: slide-down 1s ease both;
|
||||||
|
animation-delay: calc(var(--delay) * 1ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@screen md {
|
||||||
|
._slide {
|
||||||
|
animation-delay: calc(var(--delay) * 1.4ms + 1.5s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes fade {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slide-down {
|
||||||
|
from {
|
||||||
|
transform: translateY(-50%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import BlurredBlobCanvas from "../components/BlurredBlobCanvas.vue"
|
||||||
|
import ClickToBounce from "../components/ClickToBounce.vue"
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: "IndexPage",
|
||||||
|
components: { ClickToBounce, BlurredBlobCanvas }
|
||||||
|
}
|
||||||
|
</script>
|
11
src/pages/projects.vue
Normal file
11
src/pages/projects.vue
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
projects
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: "ProjectsPage"
|
||||||
|
}
|
||||||
|
</script>
|
22
src/utils/useWindowScrollLock.js
Normal file
22
src/utils/useWindowScrollLock.js
Normal file
|
@ -0,0 +1,22 @@
|
||||||
|
import { watchEffect, onUnmounted, getCurrentInstance } from "vue"
|
||||||
|
|
||||||
|
const lockingInstances = new Set()
|
||||||
|
|
||||||
|
const update = () => {
|
||||||
|
document.body.style.overflowY = lockingInstances.size === 0 ? null : "hidden"
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useWindowScrollLock(locked) {
|
||||||
|
const instance = getCurrentInstance()
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
if (locked.value) lockingInstances.add(instance)
|
||||||
|
else lockingInstances.delete(instance)
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
lockingInstances.delete(instance)
|
||||||
|
update()
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,5 +1,6 @@
|
||||||
import vuePlugin from "@vitejs/plugin-vue"
|
import vuePlugin from "@vitejs/plugin-vue"
|
||||||
import windicssPlugin from "vite-plugin-windicss"
|
import windicssPlugin from "vite-plugin-windicss"
|
||||||
|
import pagesPlugin from "vite-plugin-pages"
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* https://vitejs.dev/config/
|
* https://vitejs.dev/config/
|
||||||
|
@ -8,6 +9,7 @@ import windicssPlugin from "vite-plugin-windicss"
|
||||||
export default {
|
export default {
|
||||||
plugins: [
|
plugins: [
|
||||||
vuePlugin(),
|
vuePlugin(),
|
||||||
|
pagesPlugin(),
|
||||||
windicssPlugin()
|
windicssPlugin()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,15 @@ import { defineConfig } from "vite-plugin-windicss"
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
theme: {
|
theme: {
|
||||||
fontFamily: {
|
fontFamily: {
|
||||||
sans: ["Inter var", "sans-serif"]
|
sans: ["Plus Jakarta Sans", "sans-serif"],
|
||||||
|
special: ["Syne", "monospace"]
|
||||||
|
},
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
blue: {
|
||||||
|
900: "#0041ff"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue