main
Norman Köhring 11 months ago
parent dbbb060720
commit 46c42d46e4

@ -12,7 +12,8 @@
"format": "prettier --write src/"
},
"dependencies": {
"vue": "^3.3.4"
"vue": "^3.3.4",
"vue-confetti": "^2.3.0"
},
"devDependencies": {
"@rushstack/eslint-patch": "^1.2.0",

@ -8,6 +8,9 @@ dependencies:
vue:
specifier: ^3.3.4
version: 3.3.4
vue-confetti:
specifier: ^2.3.0
version: 2.3.0
devDependencies:
'@rushstack/eslint-patch':
@ -2201,6 +2204,10 @@ packages:
fsevents: 2.3.2
dev: true
/vue-confetti@2.3.0:
resolution: {integrity: sha512-zmPniVzBKv0ie/BEXBR6Isi08hYSd6lS18b8VduG5BzZ2tv6bO/rlwISg+IpGY2XsqAFTXFdTC28YR+UPocnAw==}
dev: false
/vue-eslint-parser@9.3.1(eslint@8.39.0):
resolution: {integrity: sha512-Clr85iD2XFZ3lJ52/ppmUDG/spxQu6+MAeHXjjyI4I1NUYZ9xmenQp4N0oaHJhrA8OOxltCVxMRfANGa70vU0g==}
engines: {node: ^14.17.0 || >=16.0.0}

@ -1,12 +1,61 @@
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue'
import { app } from './main'
import Wordle from './components/Wordle.vue'
import Solution from './components/Solution.vue'
import Present from './components/Present.vue'
const solution = 'gutschein'
const video = ref<null | HTMLVideoElement>(null)
onMounted(() => {
const nextTimeout = (delta: number, video: HTMLVideoElement) => {
const nextDelta = Math.round(2000 + Math.random() * 10000)
setTimeout(() => {
video.play()
nextTimeout(nextDelta, video)
}, delta)
}
nextTimeout(20000, video.value)
})
const level = ref(0)
watch(level, newLevel => {
if (newLevel === 0) return
const confetti = app.config.globalProperties.$confetti
if (newLevel === 1) {
confetti.update({
particles: [{ type: 'heart', size: 15 }, { type: 'circle', size: 5 }],
particlesPerFrame: 1,
})
confetti.start()
} else if (newLevel === 2) {
confetti.update({
particles: [{ type: 'heart', size: 20 }],
particlesPerFrame: .1,
windSpeedMax: 0,
})
}
})
</script>
<template>
<header>
<video autoplay playsinline src="./happybirthday.mp4" width="852" height="480" />
<transition>
<video v-if="level < 2"
ref="video" autoplay playsinline
src="./assets/happybirthday.mp4" width="852" height="480"
/>
<img v-else
src="./assets/present.png"
/>
</transition>
</header>
<main>
Happy Birthday
<Wordle :solution="solution" @success="level++" v-if="level === 0" />
<Solution :solution="solution" @success="level++" v-else-if="level === 1" />
<Present v-else />
</main>
</template>

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

@ -0,0 +1,22 @@
<script setup lang="ts">
</script>
<template>
<p class="center">
Ein Gutschein für ein Fotoshooting mit
<a href="https://martin-neuhof.com/" target="_blank">Martin Neuhof</a>
im Studio mit allem drum und dran!
</p>
<div class="image-box">
<img src="../assets/mn06.jpg" />
<img src="../assets/mn05.jpg" />
<img src="../assets/mn04.jpg" />
<img src="../assets/mn02.jpg" />
<img src="../assets/mn03.jpg" />
<img src="../assets/mn01.jpg" />
<img src="../assets/mn07.jpg" />
</div>
<p class="center footer">
Du kannst den Gutschein jederzeit einlösen, wenn wir mal in Leipzig sind.
</p>
</template>

@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue'
interface Props {
solution: string
}
defineProps<Props>()
const messages = [
'Yeah! Gratuladingsda!',
'Ja, jetzt genieß doch erstmal ein bisschen deinen Triumpf!',
'Ist dir denn das tolle Konfetti gar nichts wert?! Guck doch mal!',
'Das Konfetti zu basteln, war echt gar nicht mal so einfach, okay?',
'Na gut, na gut... hier bitte, dein Geschenk. Alles Gute zum Geburtstag!',
]
const buttons = [
'Wattn jetze nu mein Geschenk?',
'Okay, habe ich. Jetzt her mit dem Geschenk!',
'Doch doch, ist alles total toll. Wo ist nochmal das Geschenk?',
'Ja ja, du bist richtig toll, hui, Konfetti! Geschenk?!',
'',
]
const index = ref(1)
const emit = defineEmits<{
(e: 'success'): void
}>()
const success = ref(false)
function raiseIndex() {
index.value++
if (index.value > 4) {
success.value = true
setTimeout(() => emit('success'), 3500)
}
}
</script>
<template>
<div class="solution" :class="{ 'solved': success }">
<div class="history">
<div v-for="letter in solution" class="letter green">{{ letter }}</div>
</div>
<p v-for="i in index">{{ messages[i - 1] }}</p>
<button @click="raiseIndex">{{ buttons[index - 1] }}</button>
</div>
</template>

@ -0,0 +1,80 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
export interface Props {
solution: string
}
type Status = 'red' | 'yellow' | 'green'
const props = defineProps<Props>()
const emit = defineEmits<{
(e: 'success'): void
}>()
const scrollElement = ref<null | HTMLDivElement>(null)
const history = ref<{ status: Status, letter: string }[][]>([])
const input = ref<string>('')
let success = false
function addInput(key: string) {
if (input.value.length < props.solution.length) {
input.value = `${input.value}${event.key}`
}
}
function removeInput() {
if (input.value.length === 0) return
input.value = input.value.slice(0, -1)
}
function submit() {
if (input.value.length < props.solution.length) return
if (input.value.toLowerCase() === props.solution) {
success = true
emit('success')
return
}
const letters = input.value.split('')
const newHistoryEntry = letters.map((letter, i) => {
letter = letter.toLowerCase()
const entry = { status: 'red', letter }
if (props.solution.indexOf(letter) === i) entry.status = 'green'
else if (props.solution.indexOf(letter) >= 0) entry.status = 'yellow'
return entry
})
history.value.push(newHistoryEntry)
input.value = ''
if (scrollElement.value) scrollElement.value.scrollIntoView()
}
function keyHandler ({ key }) {
if (success) return
if (key === 'Enter') submit()
else if (key === 'Backspace') removeInput()
else if (key.toLowerCase().match(/^[a-z0-9]$/)) addInput(key)
}
document.removeEventListener('keydown', keyHandler)
document.addEventListener('keydown', keyHandler)
</script>
<template>
<div class="history" v-for="word in history">
<div v-for="{ letter, status } in word" :class="['letter', status]">
{{ letter }}
</div>
</div>
<div class="input">
<div v-for="(_, i) in solution.length" class="letter">
{{ input[i] ?? '' }}
</div>
</div>
<div ref="scrollElement">&nbsp;</div>
</template>

@ -1,9 +1,91 @@
body {
:root {
scroll-behavior: smooth;
}
body, #app {
width: 100vw;
height: 100vh;
margin: 0;
padding: 0;
background: #444;
color: #EEE;
overflow: hidden;
display: flex;
flex-flow: column no-wrap;
flex-flow: column nowrap;
align-items: center;
font: 20px/1.35 monospace;
}
#app > header {
margin: 2em 0;
}
.letter {
width: 2em;
height: 2em;
border: 2px solid #444;
background: #222;
font-size: 2rem;
text-align: center;
line-height: 2;
color: #EEE;
font-weight: bold;
text-transform: uppercase;
}
.letter.red { background: #222; }
.letter.yellow { background: #EE23; }
.letter.green { background: #2E23; }
.history, .input { display: flex; }
.input { margin-bottom: 3em; }
.solution {
display: flex;
flex-flow: column nowrap;
justify-content: center;
align-items: center;
transition: opacity 3s ease-in;
opacity: 1.0;
}
.solution.solved {
opacity: 0.0;
}
button {
border: 2px solid black;
border-radius: 4px;
padding: .5em 2em;
font-size: 1em;
}
p { margin: .5em 0; }
p:first-of-type { margin-top: 1em; }
p:last-of-type { margin-bottom: 1em; }
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
a {
color: white;
font-weight: bold;
}
.image-box {
text-align: center;
margin: auto;
}
.center {
text-align: center;
margin: 2em 0;
}
.footer {
display: block;
height: 5em;
}

@ -1,6 +1,11 @@
import './assets/main.css'
import './main.css'
import { createApp } from 'vue'
import App from './App.vue'
import Confetti from 'vue-confetti'
export const app = createApp(App)
app.use(Confetti)
app.mount('#app')
createApp(App).mount('#app')

Loading…
Cancel
Save