global loading spinner, refactored card component, styling improvements, starts flip card

vue3
koehr 4 years ago
parent 08025ba9c6
commit 35743b54e7

@ -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
}, },

@ -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({
setup() { components: { CardBack },
return { name } setup () {
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…
Cancel
Save