Compare commits

...

6 Commits

@ -14,27 +14,27 @@
"vue-router": "^3.1.5" "vue-router": "^3.1.5"
}, },
"devDependencies": { "devDependencies": {
"@editorjs/editorjs": "^2.17.0", "@editorjs/editorjs": "^2.18.0",
"@editorjs/list": "^1.4.0", "@editorjs/list": "^1.5.0",
"@typescript-eslint/eslint-plugin": "^2.18.0", "@typescript-eslint/eslint-plugin": "^3.2.0",
"@typescript-eslint/parser": "^2.18.0", "@typescript-eslint/parser": "^3.2.0",
"@vue/cli-plugin-babel": "^4.2.0", "@vue/cli-plugin-babel": "^4.2.0",
"@vue/cli-plugin-eslint": "^4.2.0", "@vue/cli-plugin-eslint": "^4.2.0",
"@vue/cli-plugin-pwa": "^4.2.0", "@vue/cli-plugin-pwa": "^4.2.0",
"@vue/cli-plugin-typescript": "^4.2.0", "@vue/cli-plugin-typescript": "^4.2.0",
"@vue/cli-service": "^4.2.0", "@vue/cli-service": "^4.2.0",
"@vue/eslint-config-standard": "^5.1.0", "@vue/eslint-config-standard": "^5.1.2",
"@vue/eslint-config-typescript": "^5.0.1", "@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^6.7.2", "eslint": "^7.2.0",
"eslint-plugin-import": "^2.20.1", "eslint-plugin-import": "^2.21.2",
"eslint-plugin-node": "^11.0.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-promise": "^4.2.1", "eslint-plugin-promise": "^4.2.1",
"eslint-plugin-standard": "^4.0.0", "eslint-plugin-standard": "^4.0.1",
"eslint-plugin-vue": "^6.1.2", "eslint-plugin-vue": "^6.2.2",
"lint-staged": "^9.5.0", "lint-staged": "^9.5.0",
"raw-loader": "^4.0.0", "raw-loader": "^4.0.0",
"typescript": "~3.7.5", "typescript": "~3.9.5",
"vue-property-decorator": "^8.4.0", "vue-property-decorator": "^8.5.0",
"vue-template-compiler": "^2.6.11" "vue-template-compiler": "^2.6.11"
}, },
"eslintConfig": { "eslintConfig": {

@ -20,5 +20,15 @@
</noscript> </noscript>
<div id="app"></div> <div id="app"></div>
<!-- built files will be auto injected --> <!-- built files will be auto injected -->
<script>
window.goatcounter = {no_onload: true}
window.addEventListener('hashchange', function(e) {
window.goatcounter.count({
path: location.pathname + location.search + location.hash
})
})
</script>
<script data-goatcounter="https://rpg-cards-ng.goatcounter.com/count"
async src="//gc.zgo.at/count.js"></script>
</body> </body>
</html> </html>

@ -54,6 +54,10 @@ header {
header > p { header > p {
opacity: .6; opacity: .6;
} }
footer {
display: block;
margin-top: 1.5em;
}
section[name=notifications] { section[name=notifications] {
display: block; display: block;
max-width: 70rem; max-width: 70rem;
@ -63,18 +67,20 @@ section[name=notifications] {
} }
#popup { #popup {
display: flex;
justify-content: center;
align-items: center;
position: fixed; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
display: block;
background-color: #0008; background-color: #0008;
overflow-y: auto;
} }
#popup > .popup-content { #popup > .popup-content {
max-width: calc(80rem); width: 75rem;
height: calc(100vh - 20rem); max-width: 100vw;
margin: 10rem auto;
} }
main.popup > :not(#popup) { main.popup > :not(#popup) {
@ -133,6 +139,44 @@ button.action-close {
cursor: pointer; cursor: pointer;
} }
.options-form {
display: flex;
flex-flow: row nowrap;
justify-content: space-evenly;
}
.deck-form-fields {
display: flex;
flex-flow: column nowrap;
justify-content: center;
max-width: 25rem;
width: 50%;
margin-right: -15%;
text-shadow: 0 0 3px black;
z-index: 1;
}
.deck-form-fields label {
display: flex;
justify-content: space-between;
align-items: center;
}
.deck-form-fields label,
.deck-form-fields select,
.deck-form-fields input,
.deck-form-fields button {
margin: .5em 0;
}
.deck-form-fields input[type=color] {
margin-left: .5em;
padding: 0;
vertical-align: middle;
}
.codex-editor--narrow .codex-editor__redactor { .codex-editor--narrow .codex-editor__redactor {
margin-right: 0; margin-right: 0;
} }
.centered {
margin: auto inherit;
text-align: center;
}

@ -0,0 +1,160 @@
.card-front, .card-back {
display: flex;
flex-flow: column nowrap;
}
.card-front {
justify-content: flex-start;
}
.card-front > header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
align-items: center;
height: 3.6rem;
color: white;
font-size: 1.2rem;
font-weight: normal;
font-variant: small-caps;
padding: 0 1em;
text-align: left;
}
.card-front > header > h1 {
margin: 0;
line-height: .9em;
font-size: 2rem;
}
.card-front > header > img {
height: 3rem;
}
.card-front > main {
position: relative;
display: flex;
flex-flow: column nowrap;
flex: 1;
height: 100%;
margin: .1rem .4rem .5rem;
padding: .2rem 1rem;
background: white;
border-radius: 1rem;
font-size: 1.2rem;
color: black;
overflow: hidden;
}
.card-back {
justify-content: center;
}
.card-back > .icon-wrapper {
margin: 3em;
}
.card-content .cdx-block {
padding: 0;
}
.card-content .ce-paragraph, .card-content p {
margin: 0;
line-height: 1.3;
}
.card-content ul {
list-style-position: inside;
margin: 0;
padding-left: .5em;
}
.card-content li > p {
display: inline;
}
.card-content h2 {
font-size: 1.4rem;
color: var(--highlight-color);
margin: 0;
font-weight: normal;
}
.card-content h3 {
font-size: 1.4rem;
color: var(--highlight-color);
margin: 0 0 .2em 0;
font-weight: normal;
font-variant: small-caps;
line-height: .9em;
border-bottom: 1px solid var(--highlight-color);
}
.card-content .card-delimiter {
height: 0;
margin: .2em 0;
padding: 0;
border: 2px solid var(--highlight-color);
}
.card-content .card-delimiter.pointing-right {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 0 2px 220px;
border-color: transparent transparent transparent var(--highlight-color);
}
.card-content .card-delimiter.pointing-left {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 220px 2px 0;
border-color: transparent var(--highlight-color) transparent transparent;
}
.card-content .cdx-list__item {
padding: 0;
line-height: 1.3;
}
.card-content .card-charges-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
min-height: 1em;
}
.card-content .card-charges-wrapper.card-charges-stretch { justify-content: space-around; }
.card-content .card-charges-wrapper > .card-charge {
width: 1.0em;
height: 1.0em;
border: 2px solid var(--highlight-color);
margin: .5em .2em;
}
.card-content .card-charges-wrapper > .card-charge-circle { border-radius: 100%; }
.card-content .card-charges-wrapper > .card-charge-size-1 { width: 1.0em; height: 1.0em; }
.card-content .card-charges-wrapper > .card-charge-size-2 { width: 1.2em; height: 1.2em; }
.card-content .card-charges-wrapper > .card-charge-size-3 { width: 1.4em; height: 1.4em; }
.card-content .card-charges-wrapper > .card-charge-size-4 { width: 1.6em; height: 1.6em; }
.card-content .card-charges-wrapper > .card-charge-size-5 { width: 1.8em; height: 1.8em; }
.card-content .card-dnd-stats {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
align-items: center;
color: var(--highlight-color);
}
.card-content .dnd-stat-block {
flex: 1 1 auto;
display: flex;
flex-flow: row wrap;
font-size: .8em;
}
.card-content .dnd-stat-block > .dnd-stat-title {
width: 100%;
font-weight: bold;
text-align: center;
}
.card-content .dnd-stat-block > input {
width: 50%;
background: white;
color: var(--highlight-color);
border: none;
padding: 0;
margin: 0;
font-size: 1em;
text-align: center;
}
.card-content .dnd-stat-block {
}
[contenteditable="true"] { outline: none; }

@ -4,6 +4,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator' import { Component, Prop, Vue } from 'vue-property-decorator'
import { Card } from '@/types'
import Editor from '@editorjs/editorjs' import Editor from '@editorjs/editorjs'
import List from '@editorjs/list' import List from '@editorjs/list'
@ -26,7 +27,6 @@ export default class DeckCardEditor extends Vue {
holder: this.$refs.cardEl as HTMLElement, holder: this.$refs.cardEl as HTMLElement,
autofocus: false, autofocus: false,
tools: { tools: {
// header: Heading,
list: { class: List, inlineToolbar: true }, list: { class: List, inlineToolbar: true },
heading: { class: Heading, inlineToolbar: true }, heading: { class: Heading, inlineToolbar: true },
delimiter: { class: Delimiter, inlineToolbar: false }, delimiter: { class: Delimiter, inlineToolbar: false },
@ -47,117 +47,3 @@ export default class DeckCardEditor extends Vue {
} }
} }
</script> </script>
<style>
.card-content .cdx-block {
padding: 0;
}
.card-content .ce-paragraph, .card-content p {
margin: 0;
line-height: 1.3;
}
.card-content ul {
list-style-position: inside;
margin: 0;
padding-left: .5em;
}
.card-content li > p {
display: inline;
}
.card-content h2 {
font-size: 1.4rem;
color: var(--highlight-color);
margin: 0;
font-weight: normal;
}
.card-content h3 {
font-size: 1.4rem;
color: var(--highlight-color);
margin: 0 0 .2em 0;
font-weight: normal;
font-variant: small-caps;
line-height: .9em;
border-bottom: 1px solid var(--highlight-color);
}
.card-content .card-delimiter {
height: 0;
margin: .2em 0;
padding: 0;
border: 2px solid var(--highlight-color);
}
.card-content .card-delimiter.pointing-right {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 0 2px 220px;
border-color: transparent transparent transparent var(--highlight-color);
}
.card-content .card-delimiter.pointing-left {
height: 0;
margin: .2em 0;
border-style: solid;
border-width: 2px 220px 2px 0;
border-color: transparent var(--highlight-color) transparent transparent;
}
.card-content .cdx-list__item {
padding: 0;
line-height: 1.3;
}
.card-content .card-charges-wrapper {
display: flex;
justify-content: flex-start;
align-items: center;
min-height: 1em;
}
.card-content .card-charges-wrapper.card-charges-stretch { justify-content: space-around; }
.card-content .card-charges-wrapper > .card-charge {
width: 1.0em;
height: 1.0em;
border: 2px solid var(--highlight-color);
margin: .5em .2em;
}
.card-content .card-charges-wrapper > .card-charge-circle { border-radius: 100%; }
.card-content .card-charges-wrapper > .card-charge-size-1 { width: 1.0em; height: 1.0em; }
.card-content .card-charges-wrapper > .card-charge-size-2 { width: 1.2em; height: 1.2em; }
.card-content .card-charges-wrapper > .card-charge-size-3 { width: 1.4em; height: 1.4em; }
.card-content .card-charges-wrapper > .card-charge-size-4 { width: 1.6em; height: 1.6em; }
.card-content .card-charges-wrapper > .card-charge-size-5 { width: 1.8em; height: 1.8em; }
.card-content .card-dnd-stats {
display: flex;
flex-flow: row nowrap;
justify-content: space-around;
align-items: center;
color: var(--highlight-color);
}
.card-content .dnd-stat-block {
flex: 1 1 auto;
display: flex;
flex-flow: row wrap;
font-size: .8em;
}
.card-content .dnd-stat-block > .dnd-stat-title {
width: 100%;
font-weight: bold;
text-align: center;
}
.card-content .dnd-stat-block > input {
width: 50%;
background: white;
color: var(--highlight-color);
border: none;
padding: 0;
margin: 0;
font-size: 1em;
text-align: center;
}
.card-content .dnd-stat-block {
}
[contenteditable="true"] { outline: none; }
</style>

@ -35,7 +35,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator' import { Component, Prop, Vue } from 'vue-property-decorator'
import { cardWHtoStyle, iconPath } from '@/lib' import { Deck, Card } from '@/types'
import { cardSizeToStyle, iconPath } from '@/lib'
import DeckCardEditor from '@/components/deck-card-editor.vue' import DeckCardEditor from '@/components/deck-card-editor.vue'
@Component({ @Component({
@ -46,9 +47,6 @@ export default class DeckCard extends Vue {
@Prop() public readonly deck!: Deck @Prop() public readonly deck!: Deck
@Prop() public readonly isSelection!: boolean @Prop() public readonly isSelection!: boolean
/// TODO: onEdit
// this.$emit('edit', { field: 'content', value: doc.content })
private editHeadline = false; private editHeadline = false;
private editFieldIndex: number | null = null; private editFieldIndex: number | null = null;
@ -77,7 +75,7 @@ export default class DeckCard extends Vue {
private get containerStyle () { private get containerStyle () {
const style = { const style = {
'--highlight-color': this.card.color || this.deck.color, '--highlight-color': this.card.color || this.deck.color,
...cardWHtoStyle(this.deck.cardWidth, this.deck.cardHeight), ...cardSizeToStyle(this.deck.cardSize),
transform: '' transform: ''
} }
@ -110,6 +108,8 @@ export default class DeckCard extends Vue {
} }
</script> </script>
<style src="@/assets/card.css" />
<style scoped> <style scoped>
.flip-card { .flip-card {
position: relative; position: relative;
@ -148,69 +148,21 @@ export default class DeckCard extends Vue {
.flip-card:not(.active):hover > .card-front { .flip-card:not(.active):hover > .card-front {
transform: rotateX(0) rotateY(179deg); transform: rotateX(0) rotateY(179deg);
} }
.card-back {
z-index: 2;
transform: rotateX(0) rotateY(-179deg);
}
.flip-card:not(.active):hover > .card-back { .flip-card:not(.active):hover > .card-back {
z-index: 2; z-index: 2;
transform: rotateX(0) rotateY(0); transform: rotateX(0) rotateY(0);
} }
.card-front { .card-front {
display: flex;
flex-flow: column nowrap;
justify-content: flex-start;
z-index: 1; z-index: 1;
} }
.card-front > header {
display: flex;
flex-flow: row nowrap;
justify-content: space-between;
height: 3.6rem;
color: white;
font-size: 1.2rem;
font-weight: normal;
font-variant: small-caps;
padding: 0 1em;
text-align: left;
}
.card-front > header > h1 {
margin: .5em 0 0 0;
align-self: center;
line-height: .9em;
font-size: 2rem;
}
.card-front > header > img {
height: 3rem;
align-self: end;
}
.card-front > header > h1[contenteditable="true"] { text-decoration: underline dotted; } .card-front > header > h1[contenteditable="true"] { text-decoration: underline dotted; }
.card-front > header > h1[contenteditable="true"]:focus { text-decoration: none; } .card-front > header > h1[contenteditable="true"]:focus { text-decoration: none; }
.card-front > main {
position: relative;
display: flex;
flex-flow: column nowrap;
flex: 1;
height: 100%;
margin: .7rem .4rem .5rem;
padding: .2rem 1rem;
background: white;
border-radius: 1rem;
font-size: 1.2rem;
color: black;
overflow: hidden;
}
.card-back { .card-back {
display: flex;
flex-flow: column nowrap;
justify-content: center;
cursor: pointer; cursor: pointer;
} z-index: 2;
.card-back > .icon-wrapper { transform: rotateX(0) rotateY(-179deg);
margin: 3em;
} }
.card-back > button { .card-back > button {
width: 80%; width: 80%;

@ -12,7 +12,8 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator' import { Component, Prop, Vue } from 'vue-property-decorator'
import { cardWHtoStyle, iconPath, defaultDeck } from '@/lib' import { Deck } from '@/types'
import { cardSizeToStyle, iconPath, defaultDeck } from '@/lib'
const emptyDeck: Deck = { const emptyDeck: Deck = {
...defaultDeck(), ...defaultDeck(),
@ -34,7 +35,7 @@ export default class DeckCover extends Vue {
private get style () { private get style () {
return { return {
backgroundColor: this.deck.color, backgroundColor: this.deck.color,
...cardWHtoStyle(this.deck.cardWidth, this.deck.cardHeight) ...cardSizeToStyle(this.deck.cardSize)
} }
} }
} }

@ -1,5 +1,5 @@
<template> <template>
<form @submit.prevent="saveDeck"> <form class="options-form" @submit.prevent="saveDeck">
<div class="deck-form-fields"> <div class="deck-form-fields">
<select v-model="icon"> <select v-model="icon">
<option :key="iconName" :value="iconName" v-for="iconName in icons">{{ iconName }}</option> <option :key="iconName" :value="iconName" v-for="iconName in icons">{{ iconName }}</option>
@ -24,8 +24,10 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Emit, Vue } from 'vue-property-decorator' import { Component, Prop, Emit, Vue } from 'vue-property-decorator'
import { Deck, CardSize } from '@/types'
import { cardSizeOptions } from '@/consts'
import DeckCover from '@/components/deck-cover.vue' import DeckCover from '@/components/deck-cover.vue'
import { cardWHFromSize, cardSizeFromWH, iconPath } from '../lib' import { iconPath } from '../lib'
@Component({ @Component({
components: { DeckCover } components: { DeckCover }
@ -34,16 +36,13 @@ export default class DeckForm extends Vue {
@Prop() public readonly deck!: Deck @Prop() public readonly deck!: Deck
private icons = ['mouth-watering', 'robe', 'thorny-triskelion'] private icons = ['mouth-watering', 'robe', 'thorny-triskelion']
private sizes = [ private sizes = cardSizeOptions
{ title: '88x62 (Poker)', value: '88x62' },
{ title: '88x56 (Bridge)', value: '88x56' }
]
private icon: string private icon: string
private name: string private name: string
private description: string private description: string
private color: string private color: string
private cardSize: string private cardSize: CardSize
constructor () { constructor () {
super() super()
@ -51,24 +50,21 @@ export default class DeckForm extends Vue {
this.name = this.deck.name this.name = this.deck.name
this.description = this.deck.description this.description = this.deck.description
this.color = this.deck.color this.color = this.deck.color
this.cardSize = cardSizeFromWH(this.deck.cardWidth, this.deck.cardHeight) this.cardSize = this.deck.cardSize
} }
private get iconPath () { private get iconPath () {
return iconPath(this.icon) return iconPath(this.icon)
} }
private get newDeck () { private get newDeck (): Deck {
const [cardWidth, cardHeight] = cardWHFromSize(this.cardSize)
return { return {
...this.deck, ...this.deck,
name: this.name, name: this.name,
description: this.description, description: this.description,
color: this.color, color: this.color,
icon: this.icon, icon: this.icon,
cardWidth, cardSize: this.cardSize
cardHeight
} }
} }
@ -78,32 +74,3 @@ export default class DeckForm extends Vue {
} }
} }
</script> </script>
<style scoped>
form {
display: flex;
flex-flow: row nowrap;
justify-content: space-evenly;
}
.deck-form-fields {
display: flex;
flex-flow: column nowrap;
justify-content: center;
max-width: 25rem;
width: 50%;
margin-right: -15%;
z-index: 1;
}
.deck-form-fields select,
.deck-form-fields input,
.deck-form-fields button {
margin: .5em 0;
}
.deck-form-fields input[type=color] {
margin-left: .5em;
padding: 0;
vertical-align: middle;
}
</style>

@ -7,6 +7,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Prop, Emit, Vue } from 'vue-property-decorator' import { Component, Prop, Emit, Vue } from 'vue-property-decorator'
import { Deck } from '@/types'
import DeckForm from './deck-form.vue' import DeckForm from './deck-form.vue'
@Component({ @Component({

@ -0,0 +1,63 @@
<template>
<div class="switch">
<input :id="id" class="checkbox" type="checkbox" :checked="value" @change="$emit('input', !value)" />
<label :for="id">
<div class="switch-label-text">{{ label }}</div>
<div class="switch-elements-wrapper">
<div class="switch-elements">
<div class="switch-element off"><slot name="off">NO</slot></div>
<div class="switch-element btn"></div>
<div class="switch-element on"><slot name="on">YES</slot></div>
</div>
</div>
</label>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
@Component
export default class FlipSwitch extends Vue {
@Prop() public readonly id!: string
@Prop() public readonly value!: boolean
@Prop() public readonly label!: string
}
</script>
<style scoped>
.switch > input {
display: none;
}
.switch .switch-elements-wrapper {
height: 2em;
width: 4em;
border: 4px solid black;
border-radius: 2em;
background-color: black;
overflow: hidden;
}
.switch .switch-elements {
display: flex;
flex-flow: row nowrap;
transition: transform .2s ease-in;
}
.switch .switch-element {
height: 1.8em;
width: 1.8em;
margin: .1em;
line-height: 1.8em;
flex: 0 0 auto;
}
.switch .btn {
background-color: gray;
border-radius: 5em;
}
input.checkbox:checked + label .switch-elements-wrapper > .switch-elements {
transform: translate(-2em, 0);
}
input.checkbox:checked + label .switch-elements-wrapper {
box-shadow: 0 0 15px 2px green;
}
</style>

@ -2,13 +2,15 @@
<div id="new-deck-form" class="deck"> <div id="new-deck-form" class="deck">
<header>Create a new deck of cards</header> <header>Create a new deck of cards</header>
<DeckForm :deck="newDeck" @save="saveDeck" @close="$emit('close')" /> <DeckForm :deck="newDeck" @save="saveDeck" @close="$emit('close')" />
<footer class="centered">You can also <button @click="importDeck">import</button> an existing set.</footer>
</div> </div>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Emit, Vue } from 'vue-property-decorator' import { Component, Emit, Vue } from 'vue-property-decorator'
import { Deck } from '@/types'
import DeckForm from './deck-form.vue' import DeckForm from './deck-form.vue'
import { defaultDeck, randomId } from '../lib' import { defaultDeck, randomId, isValidDeck } from '../lib'
@Component({ @Component({
components: { DeckForm } components: { DeckForm }
@ -16,6 +18,37 @@ import { defaultDeck, randomId } from '../lib'
export default class NewDeckForm extends Vue { export default class NewDeckForm extends Vue {
private newDeck: Deck = defaultDeck() private newDeck: Deck = defaultDeck()
private importDeck () {
const newFileSelector = document.createElement('input')
newFileSelector.setAttribute('type', 'file')
newFileSelector.onchange = event => {
if (event === null) return
const fileList = (event.target as HTMLInputElement).files
if (fileList === null || fileList.length < 1) return
const file = fileList[0]
if (!file) return
const seemsToBeJSON = file.type === 'application/json'
// TODO: more checks?
let fileOk = seemsToBeJSON
if (!seemsToBeJSON) {
fileOk = window.confirm(`This seems to be wrong file type (${file.type}). Should be JSON. Import anyway?`)
}
if (!fileOk) return
file.text().then((text: string) => {
const json = JSON.parse(text)
if (!isValidDeck(json)) window.alert('Sorry, that did\'t seem to be a valid deck.')
else this.$emit('save', this.$storage.saveDeck(json))
})
}
newFileSelector.click()
}
@Emit('save') @Emit('save')
private saveDeck (deck: Deck) { private saveDeck (deck: Deck) {
deck.id = randomId() // just to make sure deck.id = randomId() // just to make sure

@ -0,0 +1,96 @@
<template>
<div id="print-options-form">
<header>Print Deck</header>
<form @submit.prevent="printDeck">
<div class="deck-form-fields">
<label for="print-option-page-size">
Page Size
<select class="print-option-select" id="print-option-page-size" v-model="pageSize">
<option :key="size.value" :value="size.value" v-for="size in pageSizes">{{ size.title }}</option>
</select>
</label>
<label for="print-option-card-size">
Card Size
<select class="print-option-select" id="print-option-card-size" v-model="cardSize">
<option :key="size.value" :value="size.value" v-for="size in cardSizes">{{ size.title }}</option>
</select>
</label>
<label for="print-option-arrangement">
Arrangement
<select class="print-option-select" id="print-option-arrangement" v-model="arrangement">
<option :key="arrangement.value" :value="arrangement.value" v-for="arrangement in arrangements">{{ arrangement.title }}</option>
</select>
</label>
<FlipSwitch id="print-option-rounded-corners" label="Rounded Corners" v-model="roundedCorners">
</FlipSwitch>
<button type="submit">Print deck</button>
<button class="cancel" @click.prevent="$emit('close')">cancel</button>
</div>
</form>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import { Deck } from '@/types'
import {
cardSizeOptions,
pageSizeOptions,
arrangementOptions,
defaultCardSize,
defaultPageSize,
defaultArrangement
} from '@/consts'
import FlipSwitch from '@/components/flip-switch.vue'
@Component({
components: { FlipSwitch }
})
export default class EditDeckForm extends Vue {
@Prop() public readonly deck!: Deck
private pageSizes = pageSizeOptions
private cardSizes = cardSizeOptions
private arrangements = arrangementOptions
private pageSize = defaultPageSize
private cardSize = defaultCardSize
private arrangement = defaultArrangement
private roundedCorners = true
private mounted () {
this.cardSize = this.deck.cardSize
this.pageSize = this.deck.pageSize
this.arrangement = this.deck.arrangement
this.roundedCorners = this.deck.roundedCorners
}
private printDeck () {
this.$storage.saveDeck({
...this.deck,
arrangement: this.arrangement,
pageSize: this.pageSize,
cardSize: this.cardSize,
roundedCorners: this.roundedCorners
})
console.log('would print on', this.pageSize, `(${this.arrangement})`, this.deck.cards.length, 'cards of size', this.cardSize, this.roundedCorners ? 'with rounded corners' : '')
window.open(`/print/${this.deck.id}`, '_blank')
}
}
</script>
<style scoped>
.print-option-select {
width: 55%;
}
.deck-form-fields {
width: 100%;
max-width: 20em;
margin: auto;
}
</style>

@ -0,0 +1,89 @@
<template>
<div class="card" :style="containerStyle">
<div class="card-front" v-if="showFront">
<header>
<h1>{{ card.name }}</h1>
<img :src="icon" />
</header>
<main ref="cardEl" class="card-content">
</main>
</div>
<div class="card-back" v-if="showBack">BACK</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator'
import { Deck, Card } from '@/types'
import { iconPath } from '@/lib'
import Editor from '@editorjs/editorjs'
import List from '@editorjs/list'
import { Heading, Delimiter, Charges, DnDStats } from '@/editor'
@Component
export default class StaticCard extends Vue {
@Prop() public readonly card!: Card
@Prop() public readonly deck!: Deck
@Prop({ default: false }) public readonly showFront!: boolean
@Prop({ default: false }) public readonly showBack!: boolean
private editor!: Editor
private mounted () {
this.editor = new Editor({
holder: this.$refs.cardEl as HTMLElement,
autofocus: false,
hideToolbar: true,
tools: {
list: { class: List, inlineToolbar: false },
heading: { class: Heading, inlineToolbar: false },
delimiter: { class: Delimiter, inlineToolbar: false },
charges: { class: Charges, inlineToolbar: false },
dndstats: { class: DnDStats, inlineToolbar: false }
},
data: this.card.content,
onReady: () => {
console.log('editor is ready, what to do?')
}
})
}
private get icon () {
const icon = this.card.icon || this.deck.icon
return iconPath(icon)
}
private get backIcon () {
const icon = this.card.backIcon || this.deck.icon
return iconPath(icon)
}
private get containerStyle () {
const color = (this.deck && this.deck.color) || this.card.color
return {
'--highlight-color': color
}
}
}
</script>
<style src="@/assets/card.css" />
<style scoped>
.card {
height: auto;
width: auto;
background-color: var(--highlight-color);
border: none;
box-shadow: none;
margin: 0;
cursor: default;
pointer-events: none;
}
.card-front, .card-back {
width: var(--card-width);
height: var(--card-height);
}
</style>

@ -0,0 +1,27 @@
import { CardSize, PageSize, Arrangement } from './types'
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

@ -21,7 +21,6 @@ class Charges extends ContentlessBlock {
constructor (args: BlockToolArgs) { constructor (args: BlockToolArgs) {
super(args) super(args)
console.log('new charges', args)
this._settingButtons = [ this._settingButtons = [
{ name: 'box', icon, action: (name: string) => this.setVariant(name) }, { name: 'box', icon, action: (name: string) => this.setVariant(name) },
{ name: 'more', icon: icon, action: () => this.increaseAmount() }, { name: 'more', icon: icon, action: () => this.increaseAmount() },
@ -115,8 +114,6 @@ class Charges extends ContentlessBlock {
el.appendChild(this.createCharge()) el.appendChild(this.createCharge())
} }
console.log('rendered', this._amount, 'charges', el)
return el return el
} }

@ -1,3 +1,5 @@
import { CardSize, PageSize, Arrangement, Deck, Card } from './types'
export function randomId (): string { export function randomId (): string {
const now = Date.now() const now = Date.now()
const rnd = Math.round(10000000 + Math.random() * 10000000).toString(36) const rnd = Math.round(10000000 + Math.random() * 10000000).toString(36)
@ -5,19 +7,16 @@ export function randomId (): string {
return `${now}.${rnd}` return `${now}.${rnd}`
} }
export function cardSizeFromWH (w: number, h: number): string { export function cardWHFromSize (size: CardSize): number[] {
return `${h}x${w}` return size.split('x').map(v => parseFloat(v))
}
export function cardWHFromSize (size: string): number[] {
return size.split('x').map(x => parseInt(x, 10)).reverse()
} }
export function iconPath (icon: string): string { export function iconPath (icon: string): string {
return `/img/${icon}.svg` return `/img/${icon}.svg`
} }
export function cardWHtoStyle (w: number, h: number): object { export function cardSizeToStyle (size: CardSize): object {
const [w, h] = cardWHFromSize(size)
const ratio = w / h const ratio = w / h
return { return {
@ -32,9 +31,11 @@ export function defaultDeck (): Deck {
name: 'the nameless', name: 'the nameless',
description: '', description: '',
color: '#3C1C00', color: '#3C1C00',
cardWidth: 62, cards: [],
cardHeight: 88, cardSize: CardSize.Poker,
cards: [] pageSize: PageSize.A4,
arrangement: Arrangement.DoubleSided,
roundedCorners: true
} }
} }
@ -52,3 +53,14 @@ export function defaultCard (): Card {
} }
} }
} }
export function isValidDeck (deck: any): boolean {
const example = defaultDeck() as { [key: string]: any }
for (const key in example) {
const type = typeof example[key]
return typeof deck[key] === type
}
return true
}

@ -4,26 +4,26 @@ import Home from './views/Home.vue'
Vue.use(VueRouter) Vue.use(VueRouter)
const routes = [ const routes = [{
{ path: '/',
path: '/', name: 'Home',
name: 'Home', component: Home
component: Home }, {
}, path: '/deck/:id',
{ name: 'Deck',
path: '/deck/:id', component: () => import(/* webpackChunkName "deck" */ './views/Deck.vue')
name: 'Deck', }, {
component: () => import(/* webpackChunkName "deck" */ './views/Deck.vue') path: '/print/:id',
}, name: 'Print',
{ component: () => import(/* webpackChunkName "print" */ './views/Print.vue')
path: '/about', }, {
name: 'About', path: '/about',
// route level code-splitting name: 'About',
// this generates a separate chunk (about.[hash].js) for this route // route level code-splitting
// which is lazy-loaded when the route is visited. // this generates a separate chunk (about.[hash].js) for this route
component: () => import(/* webpackChunkName: "about" */ './views/About.vue') // which is lazy-loaded when the route is visited.
} component: () => import(/* webpackChunkName: "about" */ './views/About.vue')
] }]
const router = new VueRouter({ const router = new VueRouter({
mode: 'history', mode: 'history',

@ -1,3 +1,4 @@
import { Deck, StoredStuff } from './types'
const KEY = 'rpg-cards-ng' const KEY = 'rpg-cards-ng'
export default class StorageHandler { export default class StorageHandler {

46
src/types.d.ts vendored

@ -1,46 +0,0 @@
interface KV<V> {
[key: string]: V;
}
interface ContentBlock {
type: string;
data: object;
}
interface CardContent {
time: number;
blocks: ContentBlock[];
version: string;
}
interface Card {
id: string;
name: string;
count: number;
tags: string[];
icon: string;
content: CardContent;
backIcon?: string;
color?: string;
}
interface Deck {
id: string;
name: string;
description: string;
color: string;
icon: string;
cards: Card[];
cardWidth: number;
cardHeight: number;
titleFontSize?: number;
}
interface Settings {
color: string;
}
interface StoredStuff {
decks: Deck[];
defaults: Settings;
}

@ -0,0 +1,71 @@
// 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> {
[key: string]: V;
}
export interface ContentBlock {
type: string;
data: object;
}
export interface CardContent {
time: number;
blocks: ContentBlock[];
version: string;
}
export interface Card {
id: string;
name: string;
count: number;
tags: string[];
icon: string;
content: CardContent;
backIcon?: string;
color?: string;
}
export interface Deck {
id: string;
name: string;
description: string;
color: string;
icon: string;
cards: Card[];
cardSize: CardSize;
arrangement: Arrangement;
pageSize: PageSize;
roundedCorners: boolean;
}
export interface Settings {
color: string;
}
export interface StoredStuff {
decks: Deck[];
defaults: Settings;
}

@ -16,7 +16,8 @@
<header> <header>
<span>{{ deck.name }}</span> <span>{{ deck.name }}</span>
<button class="edit-button" @click="popup = true">edit</button> <button class="edit-button" @click="popup = 'edit'">edit</button>
<button class="print-button" @click="popup = 'print'">print</button>
<p>{{ deck.description }}</p> <p>{{ deck.description }}</p>
</header> </header>
@ -35,7 +36,7 @@
<deck-cover @click="newCard" /> <deck-cover @click="newCard" />
</section> </section>
<div id="popup" v-show="popup"> <div id="popup" v-if="popup === 'edit'">
<div class="popup-content"> <div class="popup-content">
<EditDeckForm <EditDeckForm
:deck="deck" :deck="deck"
@ -44,18 +45,30 @@
/> />
</div> </div>
</div> </div>
<div id="popup" v-else-if="popup === 'print'">
<div class="popup-content">
<PrintDeckForm
:deck="deck"
@save="closeAndReload"
@close="popup = false"
/>
</div>
</div>
</main> </main>
</template> </template>
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import { Deck, Card } from '../types'
import DeckCover from '@/components/deck-cover.vue' import DeckCover from '@/components/deck-cover.vue'
import DeckCard from '@/components/deck-card.vue' import DeckCard from '@/components/deck-card.vue'
import EditDeckForm from '@/components/edit-deck-form.vue' import EditDeckForm from '@/components/edit-deck-form.vue'
import PrintDeckForm from '@/components/print-deck-form.vue'
import { iconPath, defaultCard } from '@/lib' import { iconPath, defaultCard } from '@/lib'
@Component({ @Component({
components: { DeckCover, DeckCard, EditDeckForm } components: { DeckCover, DeckCard, EditDeckForm, PrintDeckForm }
}) })
export default class DeckView extends Vue { export default class DeckView extends Vue {
private popup = false private popup = false
@ -137,13 +150,15 @@ export default class DeckView extends Vue {
</script> </script>
<style scoped> <style scoped>
.edit-button { .edit-button, .print-button {
vertical-align: middle; vertical-align: middle;
margin-left: 1em;
margin-top: -2px; margin-top: -2px;
} }
.edit-button {
margin-left: 1em;
}
.deck-bg { .deck-bg {
position: absolute; position: fixed;
top: 0; top: 0;
left: 0; left: 0;
width: 100vw; width: 100vw;

@ -27,6 +27,7 @@
<script lang="ts"> <script lang="ts">
import { Component, Vue } from 'vue-property-decorator' import { Component, Vue } from 'vue-property-decorator'
import { Deck } from '../types'
import DeckCover from '@/components/deck-cover.vue' import DeckCover from '@/components/deck-cover.vue'
import NewDeckForm from '@/components/new-deck-form.vue' import NewDeckForm from '@/components/new-deck-form.vue'

@ -0,0 +1,122 @@
<template>
<main id="print-view" name="print-view" :class="{ loading, 'not-found': notFound }" :style="pageSizeCSS">
<div class="loading" v-if="loading"> loading </div>
<div class="not-found" v-else-if="notFound">Deck not found :(</div>
<template v-else>
<div class="page">
<Card :key="card.id" v-for="card in deck.cards"
:card="card"
:deck="deck"
:show-front="true"
/>
</div>
<div class="page">
<header>Page 2</header>
<p>foo bar baz</p>
</div>
</template>
</main>
</template>
<script lang="ts">
import { Component, Vue } from 'vue-property-decorator'
import { Deck } from '../types'
import { defaultCardSize, defaultPageSize } from '../consts'
import Card from '../components/static-card.vue'
import { iconPath } from '../lib'
interface Dimensions {
width: number;
height: number;
}
@Component({
components: { Card }
})
export default class PrintDeck extends Vue {
private loading = true
private notFound = false
private deck: Deck | null = null
private landscape = false // TODO: not yet implemented
private mounted () {
const currentDeckId = this.$route.params.id
this.deck = this.$storage.findDeck(currentDeckId)
if (this.deck === null) this.notFound = true
this.loading = false
}
private get pageSize (): Dimensions {
const pageSize = this.deck === null ? defaultPageSize : this.deck.pageSize
const [width, height] = pageSize.split(' ').map(x => parseFloat(x))
return { width, height }
}
private get cardSize (): Dimensions {
const cardSize = this.deck === null ? defaultCardSize : this.deck.cardSize
const [height, width] = cardSize.split('x').map(x => parseFloat(x))
return { width, height }
}
private get cardsPerPage (): number {
if (this.deck === null || this.deck.cards.length === 0) return 0
}
private get deckIcon () {
if (this.deck === null) return ''
return iconPath(this.deck.icon)
}
private get pageSizeCSS () {
const cardHeight = `${this.cardSize.height}mm`
const cardWidth = `${this.cardSize.width}mm`
const pageHeight = `${this.pageSize.height}mm`
const pageWidth = `${this.pageSize.width}mm`
console.log(cardHeight, cardWidth, pageHeight, pageWidth)
return {
'--width': pageWidth,
'--height': pageHeight,
'--card-width': cardWidth,
'--card-height': cardHeight
}
}
}
</script>
<style>
@page {
margin: 0;
size: var(--size);
}
#print-view > .page {
box-sizing: border-box;
display: flex;
flex-flow: row wrap;
justify-content: flex-start;
align-content: flex-start;
page-break-after: always;
width: var(--width);
height: var(--height);
margin: 5mm auto;
padding: 1cm 9mm;
background-color: white;
color: black;
}
@media print {
html,body {
background-color: gray;
}
#app > .home-link {
display: none;
}
#print-view > .page {
margin: 0;
box-shadow: none;
}
}
</style>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save