switch to typescript
This commit is contained in:
parent
031ca31c7e
commit
23559b1a3b
63 changed files with 7943 additions and 1397 deletions
37
src/frontend/components/ConnectionOverlay.vue
Normal file
37
src/frontend/components/ConnectionOverlay.vue
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
<template>
|
||||
<div class="overlay connection-lost" v-if="show">
|
||||
<div class="overlay-content" v-if="lostConnection">
|
||||
<div>⁉️ LOST CONNECTION ⁉️</div>
|
||||
<span class="btn" @click="$emit('reconnect')">Reconnect</span>
|
||||
</div>
|
||||
<div class="overlay-content" v-if="connecting">
|
||||
<div>Connecting...</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import Communication from './../Communication'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'connection-overlay',
|
||||
emits: {
|
||||
reconnect: null,
|
||||
},
|
||||
props: {
|
||||
connectionState: Number,
|
||||
},
|
||||
computed: {
|
||||
lostConnection (): boolean {
|
||||
return this.connectionState === Communication.CONN_STATE_DISCONNECTED
|
||||
},
|
||||
connecting (): boolean {
|
||||
return this.connectionState === Communication.CONN_STATE_CONNECTING
|
||||
},
|
||||
show (): boolean {
|
||||
return !!(this.lostConnection || this.connecting)
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
45
src/frontend/components/GameTeaser.vue
Normal file
45
src/frontend/components/GameTeaser.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="game-teaser" :style="style">
|
||||
<router-link class="game-info" :to="{ name: 'game', params: { id: game.id } }">
|
||||
<span class="game-info-text">
|
||||
🧩 {{game.tilesFinished}}/{{game.tilesTotal}}<br />
|
||||
👥 {{game.players}}<br />
|
||||
{{time(game.started, game.finished)}}<br />
|
||||
</span>
|
||||
</router-link>
|
||||
<router-link v-if="false && game.hasReplay" class="game-replay" :to="{ name: 'replay', params: { id: game.id } }">
|
||||
↪️ Watch replay
|
||||
</router-link>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Time from './../../common/Time'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'game-teaser',
|
||||
props: {
|
||||
game: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style (): object {
|
||||
const url = this.game.imageUrl.replace('uploads/', 'uploads/r/') + '-375x210.webp'
|
||||
return {
|
||||
'background-image': `url("${url}")`,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
time(start: number, end: number) {
|
||||
const icon = end ? '🏁' : '⏳'
|
||||
const from = start;
|
||||
const to = end || Time.timestamp()
|
||||
const timeDiffStr = Time.timeDiffStr(from, to)
|
||||
return `${icon} ${timeDiffStr}`
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
27
src/frontend/components/HelpOverlay.vue
Normal file
27
src/frontend/components/HelpOverlay.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||
<table class="overlay-content help" @click.stop="">
|
||||
<tr><td>⬆️ Move up:</td><td><div><kbd>W</kbd>/<kbd>↑</kbd>/🖱️</div></td></tr>
|
||||
<tr><td>⬇️ Move down:</td><td><div><kbd>S</kbd>/<kbd>↓</kbd>/🖱️</div></td></tr>
|
||||
<tr><td>⬅️ Move left:</td><td><div><kbd>A</kbd>/<kbd>←</kbd>/🖱️</div></td></tr>
|
||||
<tr><td>➡️ Move right:</td><td><div><kbd>D</kbd>/<kbd>→</kbd>/🖱️</div></td></tr>
|
||||
<tr><td></td><td><div>Move faster by holding <kbd>Shift</kbd></div></td></tr>
|
||||
|
||||
<tr><td>🔍+ Zoom in:</td><td><div><kbd>E</kbd>/🖱️-Wheel</div></td></tr>
|
||||
<tr><td>🔍- Zoom out:</td><td><div><kbd>Q</kbd>/🖱️-Wheel</div></td></tr>
|
||||
<tr><td>🖼️ Toggle preview:</td><td><div><kbd>Space</kbd></div></td></tr>
|
||||
<tr><td>🧩✔️ Toggle fixed pieces:</td><td><div><kbd>F</kbd></div></td></tr>
|
||||
<tr><td>🧩❓ Toggle loose pieces:</td><td><div><kbd>G</kbd></div></td></tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'help-overlay',
|
||||
emits: {
|
||||
bgclick: null,
|
||||
},
|
||||
})
|
||||
</script>
|
||||
29
src/frontend/components/ImageTeaser.vue
Normal file
29
src/frontend/components/ImageTeaser.vue
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
<template>
|
||||
<div class="imageteaser" :style="style" @click="onClick"></div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'image-teaser',
|
||||
props: {
|
||||
image: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
style (): object {
|
||||
const url = this.image.url.replace('uploads/', 'uploads/r/') + '-150x100.webp'
|
||||
return {
|
||||
'backgroundImage': `url("${url}")`,
|
||||
}
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
onClick() {
|
||||
this.$emit('click')
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
98
src/frontend/components/NewGameDialog.vue
Normal file
98
src/frontend/components/NewGameDialog.vue
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
<template>
|
||||
<div>
|
||||
<h1>New game</h1>
|
||||
<table>
|
||||
<tr>
|
||||
<td><label>Pieces: </label></td>
|
||||
<td><input type="text" v-model="tiles" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Scoring: </label></td>
|
||||
<td>
|
||||
<label><input type="radio" v-model="scoreMode" value="1" /> Any (Score when pieces are connected to each other or on final location)</label>
|
||||
<br />
|
||||
<label><input type="radio" v-model="scoreMode" value="0" /> Final (Score when pieces are put to their final location)</label>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Image: </label></td>
|
||||
<td>
|
||||
<span v-if="image">
|
||||
<img :src="image.url" style="width: 150px;" />
|
||||
or
|
||||
<upload @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" />
|
||||
</span>
|
||||
<span v-else>
|
||||
<upload @uploaded="mediaImgUploaded($event)" accept="image/*" label="Upload an image" />
|
||||
(or select from below)
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<button class="btn" :disabled="!canStartNewGame" @click="onNewGameClick">Start new game</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<h1>Image lib</h1>
|
||||
<div>
|
||||
<image-teaser v-for="(i,idx) in images" :image="i" @click="image = i" :key="idx" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
import GameCommon from './../../common/GameCommon'
|
||||
import Upload from './../components/Upload.vue'
|
||||
import ImageTeaser from './../components/ImageTeaser.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'new-game-dialog',
|
||||
components: {
|
||||
Upload,
|
||||
ImageTeaser,
|
||||
},
|
||||
props: {
|
||||
images: Array,
|
||||
},
|
||||
emits: {
|
||||
newGame: null,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
tiles: 1000,
|
||||
image: '',
|
||||
scoreMode: GameCommon.SCORE_MODE_ANY,
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
// TODO: ts type UploadedImage
|
||||
mediaImgUploaded(data: any) {
|
||||
this.image = data.image
|
||||
},
|
||||
canStartNewGame () {
|
||||
if (!this.tilesInt || !this.image || ![0, 1].includes(this.scoreModeInt)) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
},
|
||||
onNewGameClick() {
|
||||
this.$emit('newGame', {
|
||||
tiles: this.tilesInt,
|
||||
image: this.image,
|
||||
scoreMode: this.scoreModeInt,
|
||||
})
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
scoreModeInt (): number {
|
||||
return parseInt(`${this.scoreMode}`, 10)
|
||||
},
|
||||
tilesInt (): number {
|
||||
return parseInt(`${this.tiles}`, 10)
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
27
src/frontend/components/PreviewOverlay.vue
Normal file
27
src/frontend/components/PreviewOverlay.vue
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<template>
|
||||
<div class="overlay" @click="$emit('bgclick')">
|
||||
<div class="preview">
|
||||
<div class="img" :style="previewStyle"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'preview-overlay',
|
||||
props: {
|
||||
img: String,
|
||||
},
|
||||
emits: {
|
||||
bgclick: null,
|
||||
},
|
||||
computed: {
|
||||
previewStyle (): object {
|
||||
return {
|
||||
backgroundImage: `url('${this.img}')`,
|
||||
}
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
45
src/frontend/components/PuzzleStatus.vue
Normal file
45
src/frontend/components/PuzzleStatus.vue
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="timer">
|
||||
<div>
|
||||
🧩 {{piecesDone}}/{{piecesTotal}}
|
||||
</div>
|
||||
<div>
|
||||
{{icon}} {{durationStr}}
|
||||
</div>
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Time from './../../common/Time'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'puzzle-status',
|
||||
props: {
|
||||
finished: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
piecesDone: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
piecesTotal: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
icon (): string {
|
||||
return this.finished ? '🏁' : '⏳'
|
||||
},
|
||||
durationStr (): string {
|
||||
return Time.durationStr(this.duration)
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
46
src/frontend/components/Scores.vue
Normal file
46
src/frontend/components/Scores.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<template>
|
||||
<div class="scores">
|
||||
<div>Scores</div>
|
||||
<table>
|
||||
<tr v-for="(p, idx) in actives" :key="idx" :style="{color: p.color}">
|
||||
<td>⚡</td>
|
||||
<td>{{p.name}}</td>
|
||||
<td>{{p.points}}</td>
|
||||
</tr>
|
||||
<tr v-for="(p, idx) in idles" :key="idx" :style="{color: p.color}">
|
||||
<td>💤</td>
|
||||
<td>{{p.name}}</td>
|
||||
<td>{{p.points}}</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: "scores",
|
||||
props: {
|
||||
activePlayers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
idlePlayers: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
actives (): Array<any> {
|
||||
// TODO: dont sort in place
|
||||
this.activePlayers.sort((a: any, b: any) => b.points - a.points)
|
||||
return this.activePlayers
|
||||
},
|
||||
idles (): Array<any> {
|
||||
// TODO: dont sort in place
|
||||
this.idlePlayers.sort((a: any, b: any) => b.points - a.points)
|
||||
return this.idlePlayers
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
38
src/frontend/components/SettingsOverlay.vue
Normal file
38
src/frontend/components/SettingsOverlay.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
<template>
|
||||
<div class="overlay transparent" @click="$emit('bgclick')">
|
||||
<table class="overlay-content settings" @click.stop="">
|
||||
<tr>
|
||||
<td><label>Background: </label></td>
|
||||
<td><input type="color" v-model="modelValue.background" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Color: </label></td>
|
||||
<td><input type="color" v-model="modelValue.color" /></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><label>Name: </label></td>
|
||||
<td><input type="text" maxLength="16" v-model="modelValue.name" /></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'settings-overlay',
|
||||
emits: {
|
||||
bgclick: null,
|
||||
'update:modelValue': null,
|
||||
},
|
||||
props: {
|
||||
modelValue: Object,
|
||||
},
|
||||
created () {
|
||||
// TODO: ts type PlayerSettings
|
||||
this.$watch('modelValue', (val: any) => {
|
||||
this.$emit('update:modelValue', val)
|
||||
}, { deep: true })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
33
src/frontend/components/Upload.vue
Normal file
33
src/frontend/components/Upload.vue
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
<template>
|
||||
<label>
|
||||
<input type="file" style="display: none" @change="upload" :accept="accept" />
|
||||
<span class="btn">{{label || 'Upload File'}}</span>
|
||||
</label>
|
||||
</template>
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'upload',
|
||||
props: {
|
||||
accept: String,
|
||||
label: String,
|
||||
},
|
||||
methods: {
|
||||
async upload(evt: Event) {
|
||||
const target = (evt.target as HTMLInputElement)
|
||||
if (!target.files) return;
|
||||
const file = target.files[0]
|
||||
if (!file) return;
|
||||
const formData = new FormData();
|
||||
formData.append('file', file, file.name);
|
||||
const res = await fetch('/upload', {
|
||||
method: 'post',
|
||||
body: formData,
|
||||
})
|
||||
const j = await res.json()
|
||||
this.$emit('uploaded', j)
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
Loading…
Add table
Add a link
Reference in a new issue