global loading spinner, refactored card component, styling improvements, starts flip card
This commit is contained in:
parent
08025ba9c6
commit
35743b54e7
12 changed files with 203 additions and 61 deletions
32
src/App.vue
32
src/App.vue
|
@ -5,12 +5,15 @@
|
||||||
|
|
||||||
<Notifications :notifications="notifications" @dismiss="dismissNotification" />
|
<Notifications :notifications="notifications" @dismiss="dismissNotification" />
|
||||||
|
|
||||||
<main>
|
<main :name="routeName">
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<div id="popup" v-show="popupShown">
|
<div id="popup" v-show="popupShown">
|
||||||
<div class="popup-content">
|
<div class="popup-content"></div>
|
||||||
|
</div>
|
||||||
|
<div id="loading-popup" v-show="loading">
|
||||||
|
<div class="popup-content spinner">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -23,9 +26,11 @@ import Notifications from '@/components/Notifications.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
setup () {
|
setup () {
|
||||||
|
const { collection: loading } = useState('loading')
|
||||||
const { collection: popupShown } = useState('popup')
|
const { collection: popupShown } = useState('popup')
|
||||||
const { collection: notifications, actions: notificationActions } = useState('notifications')
|
const { collection: notifications, actions: notificationActions } = useState('notifications')
|
||||||
return {
|
return {
|
||||||
|
loading,
|
||||||
popupShown,
|
popupShown,
|
||||||
notifications,
|
notifications,
|
||||||
addNotification: notificationActions.add,
|
addNotification: notificationActions.add,
|
||||||
|
@ -35,16 +40,29 @@ export default defineComponent({
|
||||||
components: { Notifications, Logo },
|
components: { Notifications, Logo },
|
||||||
data () {
|
data () {
|
||||||
return {
|
return {
|
||||||
showPopup: false
|
routeName: 'home'
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'$route' (newRoute) {
|
// this adds a css class to the body equal to the name of the current root
|
||||||
|
'$route' (newRoute, oldRoute) {
|
||||||
const bodyEl = document.body
|
const bodyEl = document.body
|
||||||
bodyEl.className = "" // TODO: is this really the way to go here?
|
const oldClass = oldRoute.name?.toLowerCase()
|
||||||
|
const newClass = newRoute.name?.toLowerCase()
|
||||||
|
this.routeName = newClass || ''
|
||||||
|
|
||||||
const bodyClass = newRoute.meta.bodyClass
|
if (oldClass) bodyEl.classList.remove(oldClass)
|
||||||
if (bodyClass) bodyEl.classList.add(bodyClass)
|
if (newClass) bodyEl.classList.add(newClass)
|
||||||
|
},
|
||||||
|
loading (isLoading) {
|
||||||
|
const bodyEl = document.body
|
||||||
|
if (isLoading) bodyEl.classList.add('loading')
|
||||||
|
else bodyEl.classList.remove('loading')
|
||||||
|
},
|
||||||
|
popupShown (isShown) {
|
||||||
|
const bodyEl = document.body
|
||||||
|
if (isShown) bodyEl.classList.add('popup')
|
||||||
|
else bodyEl.classList.remove('popup')
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted () {
|
mounted () {
|
||||||
|
|
|
@ -21,7 +21,6 @@ body.print {
|
||||||
max-width: 90rem;
|
max-width: 90rem;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
font-size: 1.6rem;
|
font-size: 1.6rem;
|
||||||
min-height: 100vh;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#app > main {
|
#app > main {
|
||||||
|
@ -36,19 +35,24 @@ body.print {
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo {
|
.popup > #app > :not(#popup) {
|
||||||
|
filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.logo {
|
||||||
transition: transform .3s ease-out;
|
transition: transform .3s ease-out;
|
||||||
}
|
}
|
||||||
#logo path.house {
|
.logo path.house {
|
||||||
fill: #222;
|
fill: #222;
|
||||||
fill-opacity: 0.0;
|
fill-opacity: 0.0;
|
||||||
transition: fill-opacity .3s ease-out .2s;
|
transition: fill-opacity .3s ease-out .2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
#logo:hover {
|
.home-link > .logo:hover {
|
||||||
transform: scale(4) translate(5%, 15%);
|
transform: scale(4) translate(5%, 15%);
|
||||||
}
|
}
|
||||||
#logo:hover path.house {
|
.home-link > .logo:hover path.house {
|
||||||
fill-opacity: 1.0;
|
fill-opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -91,7 +95,7 @@ section.notification-section > .warning {
|
||||||
border-color: red;
|
border-color: red;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popup {
|
#popup, #loading-popup {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -100,16 +104,54 @@ section.notification-section > .warning {
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100vw;
|
width: 100vw;
|
||||||
height: 100vh;
|
height: 100vh;
|
||||||
background-color: #0008;
|
background: radial-gradient(#101010, transparent);
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
#popup > .popup-content {
|
.popup-content {
|
||||||
width: 75rem;
|
width: 75rem;
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
}
|
}
|
||||||
|
|
||||||
main.popup > :not(#popup) {
|
.spinner {
|
||||||
filter: blur(10px);
|
font-size: 10px;
|
||||||
|
margin: 50px auto;
|
||||||
|
text-indent: -9999em;
|
||||||
|
width: 11em;
|
||||||
|
height: 11em;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #8193a2;
|
||||||
|
background: linear-gradient(to right, #8193a2 10%, rgba(129,147,162, 0) 42%);
|
||||||
|
position: relative;
|
||||||
|
animation: spin 1.4s infinite linear;
|
||||||
|
transform: translateZ(0);
|
||||||
|
}
|
||||||
|
.spinner::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 50%;
|
||||||
|
height: 50%;
|
||||||
|
background: #8193a2;
|
||||||
|
border-radius: 100% 0 0 0;
|
||||||
|
}
|
||||||
|
.spinner::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
width: 95%;
|
||||||
|
height: 95%;
|
||||||
|
background: #101010;
|
||||||
|
border-radius: 50%;
|
||||||
|
margin: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
}
|
}
|
||||||
|
|
||||||
input, button, select {
|
input, button, select {
|
||||||
|
@ -133,7 +175,6 @@ select {
|
||||||
section.cards {
|
section.cards {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
justify-content: space-evenly;
|
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
section.cards.centered {
|
section.cards.centered {
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="card card-back" :style="cssVars" v-if="showBackSide">
|
<section name="card-back" class="card card-back" :style="cssVars">
|
||||||
<div class="icon-wrapper">
|
<div class="icon-wrapper">
|
||||||
<img :src="iconPath" alt="card icon" />
|
<img :src="iconPath" alt="card icon" />
|
||||||
</div>
|
</div>
|
||||||
<footer><slot name="back"></slot></footer>
|
<footer><slot></slot></footer>
|
||||||
</div>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from 'vue'
|
import { defineComponent } from 'vue'
|
||||||
import { KV } from '@/types'
|
import { KV, ICard } from '@/types'
|
||||||
import { CardSize, defaultCardSize } from '@/consts'
|
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'
|
||||||
|
@ -23,7 +23,8 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
iconPath (): string {
|
iconPath (): string {
|
||||||
return iconPath(this.icon || 'plus')
|
const icon = this.icon || 'plus'
|
||||||
|
return iconPath(icon)
|
||||||
},
|
},
|
||||||
showBackSide (): boolean {
|
showBackSide (): boolean {
|
||||||
return true
|
return true
|
||||||
|
@ -56,13 +57,13 @@ export default defineComponent({
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
margin: 1rem 0;
|
margin: 1rem 0;
|
||||||
}
|
}
|
||||||
#_add_deck.card-back {
|
#_add.card-back {
|
||||||
height: var(--card-height);
|
height: var(--card-height);
|
||||||
width: 25rem;
|
width: 25rem;
|
||||||
border: none;
|
border: none;
|
||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
#_add_deck.card-back > footer {
|
#_add.card-back > footer {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
.card-back > .icon-wrapper {
|
.card-back > .icon-wrapper {
|
|
@ -1,17 +0,0 @@
|
||||||
<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>
|
|
|
@ -18,7 +18,9 @@
|
||||||
<button class="cancel" @click.prevent="$emit('cancel')">cancel</button>
|
<button class="cancel" @click.prevent="$emit('cancel')">cancel</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<DeckCard :deck="{ icon, name, description, color, cardSize, cards: [] }" />
|
<CardBack :icon="icon" :color="color" :size="cardSize">
|
||||||
|
{{ name }}
|
||||||
|
</CardBack>
|
||||||
</form>
|
</form>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -26,10 +28,10 @@
|
||||||
import { defineComponent, ref } from 'vue'
|
import { defineComponent, ref } from 'vue'
|
||||||
import { useState } from '@/state'
|
import { useState } from '@/state'
|
||||||
import { cardSizeOptions, defaultCardSize } from '@/consts'
|
import { cardSizeOptions, defaultCardSize } from '@/consts'
|
||||||
import DeckCard from '@/components/DeckCard.vue'
|
import CardBack from '@/components/CardBack.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
components: { DeckCard },
|
components: { CardBack },
|
||||||
props: {
|
props: {
|
||||||
deck: Object
|
deck: Object
|
||||||
},
|
},
|
||||||
|
|
13
src/components/FlipCard.vue
Normal file
13
src/components/FlipCard.vue
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
<template>
|
||||||
|
<div :id="card && card.id" class="flip-card card" :style="cssVars">
|
||||||
|
<section name="card-front" class="card-front" v-if="showFrontSide">
|
||||||
|
<span>Front Side</span>
|
||||||
|
</section>
|
||||||
|
<section name="card-back" class="card-back" v-if="showBackSide">
|
||||||
|
<div class="icon-wrapper">
|
||||||
|
<img :src="iconPath" alt="card icon" />
|
||||||
|
</div>
|
||||||
|
<footer><slot name="back"></slot></footer>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<svg id="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
<svg class="logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="outer-shadow" height="300%" width="300%" x="-100%" y="-100%">
|
<filter id="outer-shadow" height="300%" width="300%" x="-100%" y="-100%">
|
||||||
<feFlood flood-color="rgba(201, 201, 201, 1)" result="flood"></feFlood>
|
<feFlood flood-color="rgba(201, 201, 201, 1)" result="flood"></feFlood>
|
||||||
|
|
|
@ -15,6 +15,6 @@ export default createRouter({
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', name: 'Home', component: Home },
|
{ path: '/', name: 'Home', component: Home },
|
||||||
{ path: '/deck/:id', name: 'Deck', component: AsyncDeck },
|
{ path: '/deck/:id', name: 'Deck', component: AsyncDeck },
|
||||||
{ path: '/print/:id', name: 'Print', component: AsyncPrint, meta: { bodyClass: 'print' } },
|
{ path: '/print/:id', name: 'Print', component: AsyncPrint },
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
|
@ -5,12 +5,13 @@ import { defaultDeck } from '../lib/deck'
|
||||||
import { defaultCard } from '../lib/card'
|
import { defaultCard } from '../lib/card'
|
||||||
import stateActions from './actions'
|
import stateActions from './actions'
|
||||||
|
|
||||||
const state: State = {
|
export const state: State = {
|
||||||
settings: ref({}),
|
settings: ref({}),
|
||||||
decks: ref({}),
|
decks: ref({}),
|
||||||
notifications: ref([]),
|
notifications: ref([]),
|
||||||
icons: ref(['mouth-watering', 'robe', 'thorny-triskelion']),
|
icons: ref(['mouth-watering', 'robe', 'thorny-triskelion']),
|
||||||
popup: ref(false)
|
popup: ref(false),
|
||||||
|
loading: ref(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useState (prop: string): { [key: string]: any } {
|
export function useState (prop: string): { [key: string]: any } {
|
||||||
|
@ -44,4 +45,4 @@ deckDB.putDeck(testDeck).then(() => {
|
||||||
})
|
})
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default reactive(state)
|
export default state
|
||||||
|
|
|
@ -61,4 +61,5 @@ export interface State {
|
||||||
notifications: Ref<Notification[]>;
|
notifications: Ref<Notification[]>;
|
||||||
icons: Ref<string[]>;
|
icons: Ref<string[]>;
|
||||||
popup: Ref<boolean>;
|
popup: Ref<boolean>;
|
||||||
|
loading: Ref<boolean>;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,16 +1,97 @@
|
||||||
<template>
|
<template>
|
||||||
<h1>Welcome {{ name }}</h1>
|
<template v-if="errorState">
|
||||||
<p>This is a placeholder view.</p>
|
<header>Cannot find this deck</header>
|
||||||
|
<router-link to="/">« lets go back home</router-link>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else>
|
||||||
|
<div class="deck-bg">
|
||||||
|
<img :src="deckIcon" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<header>
|
||||||
|
<span>{{ deck.name }}</span>
|
||||||
|
<button class="edit-button">edit</button>
|
||||||
|
<button class="print-button">print</button>
|
||||||
|
|
||||||
|
<p>{{ deck.description }}</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section name="deck-cards" class="cards" :class="{ centered: deck.cards.length === 0 }">
|
||||||
|
<CardBack v-for="card in deck.cards"
|
||||||
|
:key="card.id"
|
||||||
|
:id="card.id"
|
||||||
|
:card="card"
|
||||||
|
:icon="deck.icon"
|
||||||
|
:color="deck.color"
|
||||||
|
:size="deck.cardSize"
|
||||||
|
/>
|
||||||
|
<CardBack id="_add" @click="addCard" />
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { defineComponent } from "vue"
|
import { defineComponent, watchEffect, ref } from 'vue'
|
||||||
|
import { useRoute } from 'vue-router'
|
||||||
|
import { IDeck } from '@/types'
|
||||||
|
import state from '@/state'
|
||||||
|
import iconPath from '@/lib/iconPath'
|
||||||
|
import CardBack from '@/components/CardBack.vue'
|
||||||
|
|
||||||
const name = 'Deck'
|
const name = 'Deck'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
|
components: { CardBack },
|
||||||
setup () {
|
setup () {
|
||||||
return { name }
|
const route = useRoute()
|
||||||
|
|
||||||
|
const errorState = ref(false)
|
||||||
|
const deck = ref<IDeck | null>(null)
|
||||||
|
const deckIcon = ref('')
|
||||||
|
|
||||||
|
watchEffect(() => {
|
||||||
|
const deckId = route.params.id as string
|
||||||
|
const existingDecks = Object.keys(state.decks.value)
|
||||||
|
const exists = existingDecks.indexOf(deckId) >= 0
|
||||||
|
errorState.value = !exists
|
||||||
|
|
||||||
|
if (exists) {
|
||||||
|
deck.value = state.decks.value[deckId]
|
||||||
|
deckIcon.value = iconPath(deck.value.icon)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const addCard = () => {}
|
||||||
|
|
||||||
|
state.loading.value = false
|
||||||
|
return { errorState, deck, deckIcon, addCard }
|
||||||
|
},
|
||||||
|
beforeRouteEnter (_to, _from, next) {
|
||||||
|
state.loading.value = true
|
||||||
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.edit-button, .print-button {
|
||||||
|
vertical-align: middle;
|
||||||
|
margin-top: -2px;
|
||||||
|
}
|
||||||
|
.edit-button {
|
||||||
|
margin-left: 1em;
|
||||||
|
}
|
||||||
|
.deck-bg {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100vw;
|
||||||
|
max-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
.deck-bg > img {
|
||||||
|
filter: saturate(0%) blur(5px) opacity(8%);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -1,11 +1,13 @@
|
||||||
<template>
|
<template>
|
||||||
<header>RPG Cards for y'all</header>
|
<header>RPG Cards for y'all</header>
|
||||||
|
|
||||||
<section name="deck-covers" class="cards" :class="{ centered: !decks.length }">
|
<section name="deck-covers" class="cards" :class="{ centered: decks.length === 0 }">
|
||||||
<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">
|
||||||
<DeckCard :deck="deck" />
|
<CardBack :icon="deck.icon" :color="deck.color" :size="deck.cardSize">
|
||||||
|
{{ deck.name }} ({{ deck.cards.length }})
|
||||||
|
</CardBack>
|
||||||
</router-link>
|
</router-link>
|
||||||
<Card id="_add_deck" @click="addDeck" />
|
<CardBack id="_add" @click="addDeck" />
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Popup>
|
<Popup>
|
||||||
|
@ -27,13 +29,12 @@ import { defineComponent, ref, computed } from 'vue'
|
||||||
import { useState } from '@/state'
|
import { useState } from '@/state'
|
||||||
|
|
||||||
import Popup from '@/components/Popup.vue'
|
import Popup from '@/components/Popup.vue'
|
||||||
import Card from '@/components/Card.vue'
|
import CardBack from '@/components/CardBack.vue'
|
||||||
import DeckCard from '@/components/DeckCard.vue'
|
|
||||||
import DeckForm from '@/components/DeckForm.vue'
|
import DeckForm from '@/components/DeckForm.vue'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
components: { Popup, Card, DeckCard, DeckForm },
|
components: { Popup, CardBack, DeckForm },
|
||||||
setup () {
|
setup () {
|
||||||
const { actions: popupActions } = useState('popup')
|
const { actions: popupActions } = useState('popup')
|
||||||
const { collection: decks, actions: deckActions } = useState('decks')
|
const { collection: decks, actions: deckActions } = useState('decks')
|
||||||
|
|
Loading…
Add table
Reference in a new issue