This commit is contained in:
Norman Köhring 2023-06-19 10:14:34 +02:00
parent dbbb060720
commit 46c42d46e4
18 changed files with 304 additions and 9 deletions

View file

@ -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",

View file

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

View file

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

BIN
src/assets/mn01.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

BIN
src/assets/mn02.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

BIN
src/assets/mn03.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 86 KiB

BIN
src/assets/mn04.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
src/assets/mn05.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
src/assets/mn06.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

BIN
src/assets/mn07.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

BIN
src/assets/mn08.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

BIN
src/assets/present.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View file

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

View file

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

80
src/components/Wordle.vue Normal file
View file

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

View file

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

View file

@ -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')