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>
|
<main>
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
|
<div id="popup" v-show="popupShown">
|
||||||
|
<div class="popup-content">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang='ts'>
|
<script lang='ts'>
|
||||||
|
@ -18,14 +23,21 @@ import Notifications from '@/components/Notifications.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup () {
|
setup () {
|
||||||
const { collection: notifications, actions } = useState('notifications')
|
const { collection: popupShown } = useState('popup')
|
||||||
|
const { collection: notifications, actions: notificationActions } = useState('notifications')
|
||||||
return {
|
return {
|
||||||
|
popupShown,
|
||||||
notifications,
|
notifications,
|
||||||
addNotification: actions.add,
|
addNotification: notificationActions.add,
|
||||||
dismissNotification: actions.dismiss
|
dismissNotification: notificationActions.dismiss,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
components: { Notifications, Logo },
|
components: { Notifications, Logo },
|
||||||
|
data () {
|
||||||
|
return {
|
||||||
|
showPopup: false
|
||||||
|
}
|
||||||
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route' (newRoute) {
|
'$route' (newRoute) {
|
||||||
const bodyEl = document.body
|
const bodyEl = document.body
|
||||||
|
|
|
@ -9,7 +9,8 @@
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { KV, CardSize } from '@/types'
|
import { KV } from '@/types'
|
||||||
|
import { CardSize, defaultCardSize } from '@/consts'
|
||||||
import { cardSizeToStyle } from '@/lib/card'
|
import { cardSizeToStyle } from '@/lib/card'
|
||||||
import iconPath from '@/lib/iconPath'
|
import iconPath from '@/lib/iconPath'
|
||||||
|
|
||||||
|
@ -32,7 +33,7 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
cssVars (): KV<string> {
|
cssVars (): KV<string> {
|
||||||
const backgroundColor = this.color || 'transparent'
|
const backgroundColor = this.color || 'transparent'
|
||||||
const size = this.size as CardSize || CardSize.Poker
|
const size = this.size as CardSize || defaultCardSize
|
||||||
return {
|
return {
|
||||||
backgroundColor,
|
backgroundColor,
|
||||||
...cardSizeToStyle(size)
|
...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 {
|
export function defaultCard (): ICard {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,26 +1,29 @@
|
||||||
import { IDeck, CardSize, PageSize, Arrangement } from '../types'
|
import { CardSize, PageSize, Arrangement } from '../consts'
|
||||||
|
import { IDeck } from '../types'
|
||||||
|
|
||||||
|
export const defaultDeckValues: IDeck = {
|
||||||
|
id: 0,
|
||||||
|
icon: 'robe',
|
||||||
|
name: 'the nameless',
|
||||||
|
description: '',
|
||||||
|
color: '#3C1C00',
|
||||||
|
cards: [],
|
||||||
|
cardSize: CardSize.Poker,
|
||||||
|
pageSize: PageSize.A4,
|
||||||
|
arrangement: Arrangement.DoubleSided,
|
||||||
|
roundedCorners: true
|
||||||
|
}
|
||||||
|
|
||||||
export function defaultDeck (): IDeck {
|
export function defaultDeck (): IDeck {
|
||||||
return {
|
return { ...defaultDeckValues }
|
||||||
id: 0,
|
|
||||||
icon: 'robe',
|
|
||||||
name: 'the nameless',
|
|
||||||
description: '',
|
|
||||||
color: '#3C1C00',
|
|
||||||
cards: [],
|
|
||||||
cardSize: CardSize.Poker,
|
|
||||||
pageSize: PageSize.A4,
|
|
||||||
arrangement: Arrangement.DoubleSided,
|
|
||||||
roundedCorners: true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isValidDeck (deck: any): boolean {
|
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) {
|
for (const key in example) {
|
||||||
const type = typeof example[key]
|
const type = typeof example[key]
|
||||||
return typeof deck[key] === type
|
if (typeof deck[key] !== type) return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Ref } from 'vue'
|
import { Ref } from 'vue'
|
||||||
import { Notification, IDeck, KV } from '../types'
|
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)
|
/// actions are called like action['sub/foo'](state.sub, payload)
|
||||||
export default {
|
export default {
|
||||||
|
@ -22,7 +22,37 @@ export default {
|
||||||
},
|
},
|
||||||
|
|
||||||
// DECK ACTIONS
|
// DECK ACTIONS
|
||||||
'decks/new' (decks: Ref<IDeck[]>) {
|
// returns index of newly created deck
|
||||||
decks.value.push(defaultDeck())
|
'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({}),
|
settings: ref({}),
|
||||||
decks: ref([]),
|
decks: ref([]),
|
||||||
notifications: ref([]),
|
notifications: ref([]),
|
||||||
initialized: ref(false)
|
icons: ref(['mouth-watering', 'robe', 'thorny-triskelion']),
|
||||||
|
popup: ref(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useState (prop: string): { [key: string]: any } {
|
export function useState (prop: string): { [key: string]: any } {
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Dexie from 'dexie'
|
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 {
|
interface IDeckTable {
|
||||||
id: number;
|
id: number;
|
||||||
|
|
28
src/types.ts
28
src/types.ts
|
@ -1,28 +1,5 @@
|
||||||
import { Ref } from 'vue'
|
import { Ref } from 'vue'
|
||||||
|
import { PageSize, CardSize, Arrangement } from './consts'
|
||||||
// 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 interface KV<V> {
|
export interface KV<V> {
|
||||||
[key: string]: V;
|
[key: string]: V;
|
||||||
|
@ -82,5 +59,6 @@ export interface State {
|
||||||
settings: Ref<Settings>;
|
settings: Ref<Settings>;
|
||||||
decks: Ref<IDeck[]>;
|
decks: Ref<IDeck[]>;
|
||||||
notifications: Ref<Notification[]>;
|
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 }">
|
<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">
|
<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">
|
<DeckCard :deck="deck" />
|
||||||
<template #back>{{ deck.name }} ({{ deck.cards.length }})</template>
|
|
||||||
</Card>
|
|
||||||
</router-link>
|
</router-link>
|
||||||
<Card id="_add_deck" @click="newDeck" />
|
<Card id="_add_deck" @click="addDeck" />
|
||||||
</section>
|
</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>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent, ref, computed } from 'vue'
|
||||||
import { useState } from '@/state'
|
import { useState } from '@/state'
|
||||||
|
|
||||||
import Card from '@/components/Card.vue'
|
import Card from '@/components/Card.vue'
|
||||||
|
import DeckCard from '@/components/DeckCard.vue'
|
||||||
|
import DeckForm from '@/components/DeckForm.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
components: { Card },
|
components: { Card, DeckCard, DeckForm },
|
||||||
setup () {
|
setup () {
|
||||||
|
const { actions: popupActions } = useState('popup')
|
||||||
const { collection: decks, actions: deckActions } = useState('decks')
|
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 {
|
return {
|
||||||
decks,
|
decks,
|
||||||
// TODO: open popup with Deck settings after creation
|
addDeck,
|
||||||
newDeck: deckActions.new
|
newDeck,
|
||||||
|
saveDeck,
|
||||||
|
hidePopup: popupActions.hide
|
||||||
|
// importDeck: deckActions.import,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Add table
Reference in a new issue