initial
|
@ -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}
|
||||
|
|
53
src/App.vue
|
@ -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
After Width: | Height: | Size: 74 KiB |
BIN
src/assets/mn02.jpg
Normal file
After Width: | Height: | Size: 49 KiB |
BIN
src/assets/mn03.jpg
Normal file
After Width: | Height: | Size: 86 KiB |
BIN
src/assets/mn04.jpg
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
src/assets/mn05.jpg
Normal file
After Width: | Height: | Size: 23 KiB |
BIN
src/assets/mn06.jpg
Normal file
After Width: | Height: | Size: 83 KiB |
BIN
src/assets/mn07.jpg
Normal file
After Width: | Height: | Size: 47 KiB |
BIN
src/assets/mn08.jpg
Normal file
After Width: | Height: | Size: 50 KiB |
BIN
src/assets/present.png
Normal file
After Width: | Height: | Size: 30 KiB |
22
src/components/Present.vue
Normal 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>
|
49
src/components/Solution.vue
Normal 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
|
@ -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"> </div>
|
||||
</template>
|
90
src/main.css
|
@ -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')
|
||||
|
|