generalize notifications, popups via teleport, create/edit decks
This commit is contained in:
parent
ed8bcda86d
commit
2062beaefb
12 changed files with 251 additions and 61 deletions
18
src/App.vue
18
src/App.vue
|
@ -8,6 +8,11 @@
|
|||
<main>
|
||||
<router-view />
|
||||
</main>
|
||||
|
||||
<div id="popup" v-show="popupShown">
|
||||
<div class="popup-content">
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang='ts'>
|
||||
|
@ -18,14 +23,21 @@ import Notifications from '@/components/Notifications.vue'
|
|||
|
||||
export default defineComponent({
|
||||
setup () {
|
||||
const { collection: notifications, actions } = useState('notifications')
|
||||
const { collection: popupShown } = useState('popup')
|
||||
const { collection: notifications, actions: notificationActions } = useState('notifications')
|
||||
return {
|
||||
popupShown,
|
||||
notifications,
|
||||
addNotification: actions.add,
|
||||
dismissNotification: actions.dismiss
|
||||
addNotification: notificationActions.add,
|
||||
dismissNotification: notificationActions.dismiss,
|
||||
}
|
||||
},
|
||||
components: { Notifications, Logo },
|
||||
data () {
|
||||
return {
|
||||
showPopup: false
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'$route' (newRoute) {
|
||||
const bodyEl = document.body
|
||||
|
|
|
@ -9,7 +9,8 @@
|
|||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { KV, CardSize } from '@/types'
|
||||
import { KV } from '@/types'
|
||||
import { CardSize, defaultCardSize } from '@/consts'
|
||||
import { cardSizeToStyle } from '@/lib/card'
|
||||
import iconPath from '@/lib/iconPath'
|
||||
|
||||
|
@ -32,7 +33,7 @@ export default defineComponent({
|
|||
},
|
||||
cssVars (): KV<string> {
|
||||
const backgroundColor = this.color || 'transparent'
|
||||
const size = this.size as CardSize || CardSize.Poker
|
||||
const size = this.size as CardSize || defaultCardSize
|
||||
return {
|
||||
backgroundColor,
|
||||
...cardSizeToStyle(size)
|
||||
|
|
17
src/components/DeckCard.vue
Normal file
17
src/components/DeckCard.vue
Normal file
|
@ -0,0 +1,17 @@
|
|||
<template>
|
||||
<Card :icon="deck.icon" :color="deck.color" :size="deck.cardSize">
|
||||
<template #back>{{ deck.name }} ({{ deck.cards.length }})</template>
|
||||
</Card>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import Card from '@/components/Card.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { Card },
|
||||
props: {
|
||||
deck: Object
|
||||
}
|
||||
})
|
||||
</script>
|
65
src/components/DeckForm.vue
Normal file
65
src/components/DeckForm.vue
Normal file
|
@ -0,0 +1,65 @@
|
|||
<template>
|
||||
<form class="options-form" @submit.prevent="saveDeck">
|
||||
<div class="deck-form-fields">
|
||||
<select v-model="icon">
|
||||
<option :key="iconName" :value="iconName" v-for="iconName in icons">{{ iconName }}</option>
|
||||
</select>
|
||||
|
||||
<input v-model="name" title="deck name" placeholder="give it a name" />
|
||||
<input v-model="description" title="deck description" placeholder="the most awesome deck of cards" />
|
||||
|
||||
<p>Pick a colour: <input type="color" v-model="color" /></p>
|
||||
|
||||
<select v-model="cardSize">
|
||||
<option :key="size.value" :value="size.value" v-for="size in sizes">{{ size.title }}</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Save deck</button>
|
||||
<button class="cancel" @click.prevent="$emit('close')">cancel</button>
|
||||
</div>
|
||||
|
||||
<DeckCard :deck="{ icon, name, description, color, cardSize, cards: [] }" />
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent, ref } from 'vue'
|
||||
import { useState } from '@/state'
|
||||
import { cardSizeOptions, defaultCardSize } from '@/consts'
|
||||
import DeckCard from '@/components/DeckCard.vue'
|
||||
|
||||
export default defineComponent({
|
||||
components: { DeckCard },
|
||||
props: {
|
||||
deck: Object
|
||||
},
|
||||
data () {
|
||||
const icons = useState('icons').collection
|
||||
|
||||
return {
|
||||
icons,
|
||||
sizes: cardSizeOptions,
|
||||
icon: icons[0],
|
||||
name: 'no-name deck',
|
||||
description: '',
|
||||
color: '#3C1C00',
|
||||
cardSize: defaultCardSize
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
deck (deck, oldDeck) {
|
||||
this.icon = deck.icon
|
||||
this.name = deck.name
|
||||
this.description = deck.description
|
||||
this.color = deck.color
|
||||
this.cardSize = deck.cardSize
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
saveDeck () {
|
||||
const { icon, name, description, color, cardSize } = this
|
||||
this.$emit('save', { icon, name, description, color, cardSize })
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
50
src/consts.ts
Normal file
50
src/consts.ts
Normal file
|
@ -0,0 +1,50 @@
|
|||
// page width x page height
|
||||
export const enum PageSize {
|
||||
A4 = '210mm 297mm',
|
||||
USLetter = '8.5in 11in',
|
||||
JISB4 = '182mm 257mm',
|
||||
A3 = '297mm 420mm',
|
||||
A5 = '148mm 210mm',
|
||||
USLegal = '8.5in 14in',
|
||||
USLedger = '11in 17in',
|
||||
JISB5 = '257mm 364mm'
|
||||
}
|
||||
|
||||
// card width x card height
|
||||
export const enum CardSize {
|
||||
Poker = '64x89',
|
||||
Bridge = '57x89'
|
||||
}
|
||||
|
||||
export const enum Arrangement {
|
||||
DoubleSided = 'doublesided',
|
||||
FrontOnly = 'frontonly',
|
||||
SideBySide = 'sidebyside'
|
||||
}
|
||||
|
||||
|
||||
export const cardSizeOptions = [
|
||||
{ title: '88x62 (Poker)', value: CardSize.Poker },
|
||||
{ title: '88x56 (Bridge)', value: CardSize.Bridge }
|
||||
]
|
||||
|
||||
export const pageSizeOptions = [
|
||||
{ title: 'A4', value: PageSize.A4 }, // 210mm × 297mm
|
||||
{ title: 'US Letter', value: PageSize.USLetter }, // 8.5in × 11in
|
||||
{ title: 'JIS-B4', value: PageSize.JISB4 }, // 182mm × 257mm
|
||||
{ title: 'A3', value: PageSize.A3 }, // 297mm × 420mm
|
||||
{ title: 'A5', value: PageSize.A5 }, // 148mm × 210mm
|
||||
{ title: 'US Legal', value: PageSize.USLegal }, // 8.5in × 14in
|
||||
{ title: 'US Ledger', value: PageSize.USLedger }, // 11in × 17in
|
||||
{ title: 'JIS-B5', value: PageSize.JISB5 } // 257mm × 364mm
|
||||
]
|
||||
|
||||
export const arrangementOptions = [
|
||||
{ title: 'Double Sided', value: Arrangement.DoubleSided },
|
||||
{ title: 'Only Front Sides', value: Arrangement.FrontOnly },
|
||||
{ title: 'Side by Side', value: Arrangement.SideBySide }
|
||||
]
|
||||
|
||||
export const defaultPageSize = pageSizeOptions[0].value
|
||||
export const defaultCardSize = cardSizeOptions[0].value
|
||||
export const defaultArrangement = arrangementOptions[0].value
|
|
@ -1,4 +1,5 @@
|
|||
import { ICard, CardSize } from '../types'
|
||||
import { CardSize } from '../consts'
|
||||
import { ICard } from '../types'
|
||||
|
||||
export function defaultCard (): ICard {
|
||||
return {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { IDeck, CardSize, PageSize, Arrangement } from '../types'
|
||||
import { CardSize, PageSize, Arrangement } from '../consts'
|
||||
import { IDeck } from '../types'
|
||||
|
||||
export function defaultDeck (): IDeck {
|
||||
return {
|
||||
export const defaultDeckValues: IDeck = {
|
||||
id: 0,
|
||||
icon: 'robe',
|
||||
name: 'the nameless',
|
||||
|
@ -13,14 +13,17 @@ export function defaultDeck (): IDeck {
|
|||
arrangement: Arrangement.DoubleSided,
|
||||
roundedCorners: true
|
||||
}
|
||||
|
||||
export function defaultDeck (): IDeck {
|
||||
return { ...defaultDeckValues }
|
||||
}
|
||||
|
||||
export function isValidDeck (deck: any): boolean {
|
||||
const example = defaultDeck() as { [key: string]: any }
|
||||
const example = defaultDeckValues as { [key: string]: any }
|
||||
|
||||
for (const key in example) {
|
||||
const type = typeof example[key]
|
||||
return typeof deck[key] === type
|
||||
if (typeof deck[key] !== type) return false
|
||||
}
|
||||
|
||||
return true
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Ref } from 'vue'
|
||||
import { Notification, IDeck, KV } from '../types'
|
||||
import { defaultDeck } from '../lib/deck'
|
||||
import { defaultDeck, defaultDeckValues } from '../lib/deck'
|
||||
|
||||
/// actions are called like action['sub/foo'](state.sub, payload)
|
||||
export default {
|
||||
|
@ -22,7 +22,37 @@ export default {
|
|||
},
|
||||
|
||||
// DECK ACTIONS
|
||||
'decks/new' (decks: Ref<IDeck[]>) {
|
||||
decks.value.push(defaultDeck())
|
||||
// returns index of newly created deck
|
||||
'decks/new' (decks: Ref<IDeck[]>): number {
|
||||
const newDeck = defaultDeck()
|
||||
const id = decks.value.push(newDeck) - 1
|
||||
newDeck.id = id
|
||||
|
||||
return id
|
||||
},
|
||||
// updates decks[updatedDeck.id]
|
||||
'decks/update' (decks: Ref<IDeck[]>, updatedDeck: IDeck): boolean {
|
||||
const id = updatedDeck.id
|
||||
if (!id || !decks.value[id]) return false // can't update non-existing deck
|
||||
|
||||
decks.value[id] = {
|
||||
...decks.value[id],
|
||||
...updatedDeck
|
||||
}
|
||||
return true
|
||||
},
|
||||
|
||||
// POPUP ACTIONS
|
||||
'popup/show' (popup: Ref<boolean>): boolean {
|
||||
popup.value = true
|
||||
return popup.value
|
||||
},
|
||||
'popup/hide' (popup: Ref<boolean>): boolean {
|
||||
popup.value = false
|
||||
return popup.value
|
||||
},
|
||||
'popup/toggle' (popup: Ref<boolean>): boolean {
|
||||
popup.value = !popup.value
|
||||
return popup.value
|
||||
},
|
||||
}
|
||||
|
|
|
@ -9,7 +9,8 @@ const state: State = {
|
|||
settings: ref({}),
|
||||
decks: ref([]),
|
||||
notifications: ref([]),
|
||||
initialized: ref(false)
|
||||
icons: ref(['mouth-watering', 'robe', 'thorny-triskelion']),
|
||||
popup: ref(false)
|
||||
}
|
||||
|
||||
export function useState (prop: string): { [key: string]: any } {
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import Dexie from 'dexie'
|
||||
import { IDeck, ICard, CardSize, Arrangement, PageSize } from './types'
|
||||
import { CardSize, Arrangement, PageSize } from './consts'
|
||||
import { IDeck, ICard } from './types'
|
||||
|
||||
interface IDeckTable {
|
||||
id: number;
|
||||
|
|
28
src/types.ts
28
src/types.ts
|
@ -1,28 +1,5 @@
|
|||
import { Ref } from 'vue'
|
||||
|
||||
// page width x page height
|
||||
export const enum PageSize {
|
||||
A4 = '210mm 297mm',
|
||||
USLetter = '8.5in 11in',
|
||||
JISB4 = '182mm 257mm',
|
||||
A3 = '297mm 420mm',
|
||||
A5 = '148mm 210mm',
|
||||
USLegal = '8.5in 14in',
|
||||
USLedger = '11in 17in',
|
||||
JISB5 = '257mm 364mm'
|
||||
}
|
||||
|
||||
// card width x card height
|
||||
export const enum CardSize {
|
||||
Poker = '64x89',
|
||||
Bridge = '57x89'
|
||||
}
|
||||
|
||||
export const enum Arrangement {
|
||||
DoubleSided = 'doublesided',
|
||||
FrontOnly = 'frontonly',
|
||||
SideBySide = 'sidebyside'
|
||||
}
|
||||
import { PageSize, CardSize, Arrangement } from './consts'
|
||||
|
||||
export interface KV<V> {
|
||||
[key: string]: V;
|
||||
|
@ -82,5 +59,6 @@ export interface State {
|
|||
settings: Ref<Settings>;
|
||||
decks: Ref<IDeck[]>;
|
||||
notifications: Ref<Notification[]>;
|
||||
initialized: Ref<boolean>;
|
||||
icons: Ref<string[]>;
|
||||
popup: Ref<boolean>;
|
||||
}
|
||||
|
|
|
@ -3,32 +3,63 @@
|
|||
|
||||
<section name="deck-covers" class="cards" :class="{ centered: !decks.length }">
|
||||
<router-link :to="{ name: 'Deck', params: { id: deck.id } }" :key="deck.id" v-for="deck in decks">
|
||||
<Card :icon="deck.icon" :color="deck.color" :size="deck.cardSize">
|
||||
<template #back>{{ deck.name }} ({{ deck.cards.length }})</template>
|
||||
</Card>
|
||||
<DeckCard :deck="deck" />
|
||||
</router-link>
|
||||
<Card id="_add_deck" @click="newDeck" />
|
||||
<Card id="_add_deck" @click="addDeck" />
|
||||
</section>
|
||||
|
||||
<teleport to="#popup > .popup-content">
|
||||
<div class="deck new-deck-form-wrapper">
|
||||
<header>Create a new deck of cards</header>
|
||||
<DeckForm :deck="newDeck" @save="saveDeck" @close="hidePopup" />
|
||||
<footer class="centered">
|
||||
You can also
|
||||
<button @click="importDeck">import</button>
|
||||
an existing deck.
|
||||
</footer>
|
||||
</div>
|
||||
</teleport>
|
||||
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue'
|
||||
import { defineComponent, ref, computed } from 'vue'
|
||||
import { useState } from '@/state'
|
||||
|
||||
import Card from '@/components/Card.vue'
|
||||
|
||||
import DeckCard from '@/components/DeckCard.vue'
|
||||
import DeckForm from '@/components/DeckForm.vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'Home',
|
||||
components: { Card },
|
||||
components: { Card, DeckCard, DeckForm },
|
||||
setup () {
|
||||
const { actions: popupActions } = useState('popup')
|
||||
const { collection: decks, actions: deckActions } = useState('decks')
|
||||
|
||||
const newDeckIndex = ref(0)
|
||||
const newDeck = computed(() => decks.value[newDeckIndex.value])
|
||||
|
||||
const addDeck = () => {
|
||||
const idx = deckActions.new()
|
||||
newDeckIndex.value = idx
|
||||
popupActions.show()
|
||||
}
|
||||
|
||||
const saveDeck = (updatedDeck) => {
|
||||
console.log('saving deck', updatedDeck)
|
||||
updatedDeck.id = newDeckIndex.value
|
||||
deckActions.update(updatedDeck)
|
||||
popupActions.hide()
|
||||
}
|
||||
|
||||
return {
|
||||
decks,
|
||||
// TODO: open popup with Deck settings after creation
|
||||
newDeck: deckActions.new
|
||||
addDeck,
|
||||
newDeck,
|
||||
saveDeck,
|
||||
hidePopup: popupActions.hide
|
||||
// importDeck: deckActions.import,
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue