[WIP] move to typescript

This commit is contained in:
Niko Lockenvitz 2020-06-26 03:12:06 +02:00
parent 7c59e4ec49
commit 9a58d66eff
No known key found for this signature in database
GPG key ID: 9403BD1956FFF190
30 changed files with 2436 additions and 5593 deletions

3
.browserslistrc Normal file
View file

@ -0,0 +1,3 @@
> 1%
last 2 versions
not dead

25
.eslintrc.js Normal file
View file

@ -0,0 +1,25 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"@typescript-eslint/no-use-before-define": ["error", { functions: false }],
"@typescript-eslint/camelcase": "off",
"@typescript-eslint/class-name-casing": "off",
"@typescript-eslint/no-explicit-any": "off",
"prettier/prettier": 0,
},
};

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ node_modules
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-debug.log*
# Editor directories and files # Editor directories and files
.idea .idea

View file

@ -1,5 +1,3 @@
module.exports = { module.exports = {
presets: [ presets: ["@vue/cli-plugin-babel/preset"]
'@vue/cli-plugin-babel/preset' };
]
}

7103
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,8 @@
{ {
"name": "ffc", "name": "ffc",
"version": "0.1.0", "version": "0.1.0",
"author": "Niko Lockenvitz & Rene-Pascal Fischer",
"license": "MIT",
"private": true, "private": true,
"scripts": { "scripts": {
"serve": "vue-cli-service serve", "serve": "vue-cli-service serve",
@ -9,44 +11,33 @@
"start": "vue-cli-service serve" "start": "vue-cli-service serve"
}, },
"dependencies": { "dependencies": {
"core-js": "^3.6.4", "core-js": "^3.6.5",
"register-service-worker": "^1.7.1", "register-service-worker": "^1.7.1",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue-router": "^3.1.6", "vue-class-component": "^7.2.3",
"vue-router": "^3.2.0",
"vuetify": "^2.2.11" "vuetify": "^2.2.11"
}, },
"devDependencies": { "devDependencies": {
"@vue/cli-plugin-babel": "~4.3.0", "@typescript-eslint/eslint-plugin": "^2.33.0",
"@vue/cli-plugin-eslint": "~4.3.0", "@typescript-eslint/parser": "^2.33.0",
"@vue/cli-plugin-pwa": "^4.4.1", "@vue/cli-plugin-babel": "^4.4.0",
"@vue/cli-plugin-router": "^4.3.1", "@vue/cli-plugin-eslint": "^4.4.0",
"@vue/cli-service": "~4.3.0", "@vue/cli-plugin-pwa": "^4.4.0",
"babel-eslint": "^10.1.0", "@vue/cli-plugin-router": "^4.4.0",
"@vue/cli-plugin-typescript": "^4.4.0",
"@vue/cli-service": "^4.4.0",
"@vue/eslint-config-prettier": "^6.0.0",
"@vue/eslint-config-typescript": "^5.0.2",
"eslint": "^6.7.2", "eslint": "^6.7.2",
"eslint-plugin-prettier": "^3.1.3",
"eslint-plugin-vue": "^6.2.2", "eslint-plugin-vue": "^6.2.2",
"prettier": "^1.19.1",
"sass": "^1.19.0", "sass": "^1.19.0",
"sass-loader": "^8.0.0", "sass-loader": "^8.0.0",
"vue-cli-plugin-vuetify": "~2.0.5", "typescript": "~3.9.3",
"vue-cli-plugin-vuetify": "^2.0.6",
"vue-template-compiler": "^2.6.11", "vue-template-compiler": "^2.6.11",
"vuetify-loader": "^1.3.0" "vuetify-loader": "^1.3.0"
}, }
"eslintConfig": {
"root": true,
"env": {
"node": true
},
"extends": [
"plugin:vue/essential",
"eslint:recommended"
],
"parserOptions": {
"parser": "babel-eslint"
},
"rules": {}
},
"browserslist": [
"> 1%",
"last 2 versions",
"not dead"
]
} }

View file

@ -4,7 +4,7 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="icon" href="<%= BASE_URL %>favicon.ico"> <link rel="icon" href="<%= BASE_URL %>favicon.ico">
<title><%= htmlWebpackPlugin.options.title %></title> <title><%= htmlWebpackPlugin.options.title %></title>
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900"> <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900">

View file

@ -26,48 +26,88 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
import { Deck, CustomDialogOptions, FFCFile } from "./types";
import NavigationBar from "./components/layout/NavigationBar.vue"; import NavigationBar from "./components/layout/NavigationBar.vue";
import CustomDialog from "./components/customdialog/CustomDialog.vue"; import CustomDialog from "./components/customdialog/CustomDialog.vue";
import { import {
readFromLocalStorage, readFromLocalStorage,
saveToLocalStorage, saveToLocalStorage,
clearLocalStorage clearLocalStorage
} from "./helpers/localStorageHelper.js"; } from "./helpers/localStorageHelper";
import { import { addDecksFromFile, addDecksFromJSON } from "./helpers/addDecksHelper";
addDecksFromFile,
addDecksFromJSON
} from "./helpers/addDecksHelper.js";
const DEFAULT_SNACKBAR_TIMEOUT = 2000; const DEFAULT_SNACKBAR_TIMEOUT = 2000;
export default { const AppProps = Vue.extend({
props: { props: {
title: String title: String
}, }
});
@Component({
components: { components: {
NavigationBar, NavigationBar,
CustomDialog CustomDialog
}, }
})
export default class App extends AppProps {
propertiesToSyncWithLocalStorage = [{ key: "decks", defaultValue: [] }];
decks = [] as Deck[];
navBarList = [
{
to: "/",
icon: "mdi-home",
title: "Home"
},
{
to: "/add",
icon: "mdi-plus",
title: "Add Deck"
},
{
to: "/settings",
icon: "mdi-cog",
title: "Settings"
},
{
to: "/about",
icon: "mdi-information",
title: "About"
}
];
snackbar = {
text: "",
snackbar: false,
timeout: DEFAULT_SNACKBAR_TIMEOUT
};
$refs!: {
navbar: NavigationBar;
customDialog: CustomDialog;
};
created() { created() {
this.$eventHub.$on("deleteSelectedDecks", () => { this.$eventHub.$on("deleteSelectedDecks", () => {
this.decks = this.decks.filter( this.decks = this.decks.filter(deck => !deck.selected);
deck => !deck.selected
);
}); });
this.$eventHub.$on("addDecksFromFile", fileContent => { this.$eventHub.$on("addDecksFromFile", (fileContent: string) => {
addDecksFromFile(this, fileContent); addDecksFromFile(this, fileContent);
}); });
this.$eventHub.$on("addDecksFromJSON", fileContent => { this.$eventHub.$on("addDecksFromJSON", (fileContent: FFCFile) => {
addDecksFromJSON(this, fileContent); addDecksFromJSON(this, fileContent);
}); });
this.$eventHub.$on("snackbarEvent", output => { this.$eventHub.$on("snackbarEvent", (message: string) => {
this.showSnackbar(output); this.showSnackbar(message);
}); });
this.$eventHub.$on("clearLocalStorage", () => { this.$eventHub.$on("clearLocalStorage", () => {
clearLocalStorage(this); clearLocalStorage(this);
}); });
this.$eventHub.$on("showCustomDialog", options => { this.$eventHub.$on("showCustomDialog", (options: CustomDialogOptions) => {
this.showCustomDialog(options); this.showCustomDialog(options);
}); });
@ -80,74 +120,43 @@ export default {
{ deep: true } { deep: true }
); );
} }
}, }
data() {
return {
propertiesToSyncWithLocalStorage: [{ key: "decks", defaultValue: [] }],
decks: [],
navBarList: [
{
to: "/",
icon: "mdi-home",
title: "Home"
},
{
to: "/add",
icon: "mdi-plus",
title: "Add Deck"
},
{
to: "/settings",
icon: "mdi-cog",
title: "Settings"
},
{
to: "/about",
icon: "mdi-information",
title: "About"
}
],
snackbar: {
text: "",
snackbar: false,
timeout: DEFAULT_SNACKBAR_TIMEOUT
}
};
},
mounted() { mounted() {
readFromLocalStorage(this); readFromLocalStorage(this);
}, this.decks.forEach(deck => {
computed: { deck.selected = false;
numberOfSelectedDecks() { });
return this.decks.filter(deck => deck.selected).length;
}
},
methods: {
swipeLeft() {
if (this.$route.name === "Learn") {
return;
}
this.$refs.navbar.hideDrawer();
},
swipeRight() {
if (this.$route.name === "Learn") {
return;
}
this.$refs.navbar.showDrawer();
},
showSnackbar(text, timeout) {
// timeout: {value?: number, factor?: number}
this.snackbar.text = text;
this.snackbar.timeout = timeout
? timeout.value || (timeout.factor || 1) * DEFAULT_SNACKBAR_TIMEOUT
: DEFAULT_SNACKBAR_TIMEOUT;
this.snackbar.snackbar = true;
},
showCustomDialog(options) {
this.$refs.customDialog.show(options);
}
} }
};
get numberOfSelectedDecks() {
return this.decks.filter(deck => deck.selected).length;
}
swipeLeft() {
if (this.$route.name === "Learn") {
return;
}
this.$refs.navbar.hideDrawer();
}
swipeRight() {
if (this.$route.name === "Learn") {
return;
}
this.$refs.navbar.showDrawer();
}
showSnackbar(text: string, timeout?: { value?: number; factor?: number }) {
// timeout: {value?: number, factor?: number}
this.snackbar.text = text;
this.snackbar.timeout = timeout
? timeout.value || (timeout.factor || 1) * DEFAULT_SNACKBAR_TIMEOUT
: DEFAULT_SNACKBAR_TIMEOUT;
this.snackbar.snackbar = true;
}
showCustomDialog(options: CustomDialogOptions) {
this.$refs.customDialog.show(options);
}
}
</script> </script>
<style> <style>
@ -173,10 +182,13 @@ body {
} }
/* fix highlighting problems in vuetify */ /* fix highlighting problems in vuetify */
.theme--dark.v-list-item:hover::before, .theme--dark.v-btn:hover::before, .v-btn:not(.v-btn--text):not(.v-btn--outlined):hover:before{ .theme--dark.v-list-item:hover::before,
.theme--dark.v-btn:hover::before,
.v-btn:not(.v-btn--text):not(.v-btn--outlined):hover:before {
opacity: 0; opacity: 0;
} }
.theme--dark.v-list-item--active:hover::before, .theme--dark.v-list-item--active::before { .theme--dark.v-list-item--active:hover::before,
.theme--dark.v-list-item--active::before {
opacity: 0.24; opacity: 0.24;
} }
</style> </style>

1
src/assets/logo.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 87.5 100"><defs><style>.cls-1{fill:#1697f6;}.cls-2{fill:#7bc6ff;}.cls-3{fill:#1867c0;}.cls-4{fill:#aeddff;}</style></defs><title>Artboard 46</title><polyline class="cls-1" points="43.75 0 23.31 0 43.75 48.32"/><polygon class="cls-2" points="43.75 62.5 43.75 100 0 14.58 22.92 14.58 43.75 62.5"/><polyline class="cls-3" points="43.75 0 64.19 0 43.75 48.32"/><polygon class="cls-4" points="64.58 14.58 87.5 14.58 43.75 100 43.75 62.5 64.58 14.58"/></svg>

After

Width:  |  Height:  |  Size: 539 B

View file

@ -34,65 +34,63 @@
</v-dialog> </v-dialog>
</template> </template>
<script> <script lang="ts">
export default { import Vue from "vue";
name: "Dialog", import Component from "vue-class-component";
props: {}, import { CustomDialogOptions, CustomDialogOptionsButton } from "../../types";
data() {
return { @Component
showDialog: false, export default class Dialog extends Vue {
options: { showDialog = false;
title: "", options = {
format: "", title: "",
message: "", format: "",
tableHead: { message: "",
name: "", tableHead: {
value: "" name: "",
}, value: ""
table: [
{
name: "",
value: ""
}
],
buttons: [
{
name: "Close",
color: "indigo",
callback: null
}
]
}
};
},
methods: {
hide() {
this.showDialog = false;
}, },
show(options) { table: [
this.showDialog = true; {
this.options = options; name: "",
if ( value: ""
!this.options.buttons ||
(this.options.buttons && this.options.buttons.length === 0)
) {
this.options.buttons = [
{
name: "Close",
color: "indigo",
callback: null
}
];
} }
}, ],
close(btn) { buttons: [
this.showDialog = false; {
if (btn && btn.callback) { name: "Close",
btn.callback(); color: "indigo",
callback: undefined,
} }
}, ]
} as CustomDialogOptions;
hide() {
this.showDialog = false;
} }
}; show(options: CustomDialogOptions) {
this.showDialog = true;
this.options = options;
if (
!this.options.buttons ||
(this.options.buttons && this.options.buttons.length === 0)
) {
this.options.buttons = [
{
name: "Close",
color: "indigo",
callback: undefined,
}
];
}
}
close(btn?: CustomDialogOptionsButton) {
this.showDialog = false;
if (btn && btn.callback) {
btn.callback();
}
}
}
</script> </script>
<style scoped> <style scoped>

View file

@ -63,79 +63,83 @@
</div> </div>
</template> </template>
<script> <script lang="ts">
import * as selectedDeckDialogHelper from "../../helpers/selectedDeckDialogHelper.js"; import Vue from "vue";
import Component from "vue-class-component";
export default { import { Deck } from '../../types';
name: "NavigationBar",
import * as selectedDeckDialogHelper from "../../helpers/selectedDeckDialogHelper";
const NavigationBarProps = Vue.extend({
props: { props: {
title: String, title: String,
decks: Array, decks: {type: Array as () => Deck[]},
numberOfSelectedDecks: Number, numberOfSelectedDecks: Number,
navBarList: Array navBarList: Array
},
data: () => ({
primaryDrawer: {
model: false,
type: "temporary",
clipped: true,
floating: false,
mini: false
}
}),
computed: {
isInDeckSelection() {
return this.$route.name === "DeckSelection";
},
isInLearning() {
return this.$route.name === "Learn";
},
colorAppBar() {
if (this.isInDeckSelection && this.numberOfSelectedDecks > 0) {
return "indigo";
}
return "";
},
toolbarTitle() {
if (this.isInDeckSelection && this.numberOfSelectedDecks > 0) {
return `${this.numberOfSelectedDecks} deck${
this.numberOfSelectedDecks === 1 ? "" : "s"
} selected`;
}
return this.title;
}
},
methods: {
deselectAll() {
this.decks.forEach(deck => {
deck.selected = false;
});
},
selectAll() {
this.decks.forEach(deck => {
deck.selected = true;
});
},
deleteSelected() {
selectedDeckDialogHelper.deleteSelected(this);
},
showInfoForSelectedDeck() {
selectedDeckDialogHelper.showInfoForSelectedDeck(this);
},
showDrawer() {
this.primaryDrawer.model = true;
},
hideDrawer() {
this.primaryDrawer.model = false;
},
quitLearning() {
selectedDeckDialogHelper.quitLearning(this);
},
togglePrimaryDrawer() {
this.primaryDrawer.model = !this.primaryDrawer.model;
}
} }
}; });
@Component
export default class NavigationBar extends NavigationBarProps {
primaryDrawer = {
model: false,
type: "temporary",
clipped: true,
floating: false,
mini: false
};
get isInDeckSelection() {
return this.$route.name === "DeckSelection";
}
get isInLearning() {
return this.$route.name === "Learn";
}
get colorAppBar() {
if (this.isInDeckSelection && this.numberOfSelectedDecks > 0) {
return "indigo";
}
return "";
}
get toolbarTitle() {
if (this.isInDeckSelection && this.numberOfSelectedDecks > 0) {
return `${this.numberOfSelectedDecks} deck${
this.numberOfSelectedDecks === 1 ? "" : "s"
} selected`;
}
return this.title;
}
deselectAll() {
this.decks.forEach(deck => {
deck.selected = false;
});
}
selectAll() {
this.decks.forEach(deck => {
deck.selected = true;
});
}
deleteSelected() {
selectedDeckDialogHelper.deleteSelected(this);
}
showInfoForSelectedDeck() {
selectedDeckDialogHelper.showInfoForSelectedDeck(this);
}
showDrawer() {
this.primaryDrawer.model = true;
}
hideDrawer() {
this.primaryDrawer.model = false;
}
quitLearning() {
selectedDeckDialogHelper.quitLearning(this);
}
togglePrimaryDrawer() {
this.primaryDrawer.model = !this.primaryDrawer.model;
}
}
</script> </script>
<!-- Add "scoped" attribute to limit CSS to this component only --> <!-- Add "scoped" attribute to limit CSS to this component only -->

View file

@ -63,7 +63,7 @@ Morbi tempor quis justo vitae imperdiet.`,
} }
}, },
updateVerticalCentering() { updateVerticalCentering() {
for (let el of document.getElementsByClassName("max-height")) { for (const el of document.getElementsByClassName("max-height")) {
if (el.scrollHeight > el.clientHeight) { if (el.scrollHeight > el.clientHeight) {
el.classList.remove("flex-center"); el.classList.remove("flex-center");
} else { } else {

View file

@ -1,4 +1,14 @@
export function addDecksFromFile(context, fileContent) { import { FFCFile, Deck, CustomDialogOptions } from '@/types';
import router from '@/router';
interface Context {
showSnackbar: Function,
decks: Deck[],
showCustomDialog: Function,
$router: typeof router,
}
export function addDecksFromFile(context: Context, fileContent: string) {
try { try {
addDecksFromJSON(context, JSON.parse(fileContent)); addDecksFromJSON(context, JSON.parse(fileContent));
} catch (e) { } catch (e) {
@ -6,9 +16,13 @@ export function addDecksFromFile(context, fileContent) {
} }
} }
export function addDecksFromJSON(context, fileContent) { interface addedDeckAndCards {
// Following decks have been added: d0 (5 cards), d1 (76 cards)... name: string,
const addedDecksAndCards = []; numberOfCards: number,
}
export function addDecksFromJSON(context: Context, fileContent: FFCFile) {
const addedDecksAndCards = [] as addedDeckAndCards[];
try { try {
for (const deckShortName in fileContent.decks) { for (const deckShortName in fileContent.decks) {
const cards = []; const cards = [];
@ -45,7 +59,7 @@ export function addDecksFromJSON(context, fileContent) {
} }
} }
function showAddedDecksConfirmation(context, addedDecksAndCards) { function showAddedDecksConfirmation(context: Context, addedDecksAndCards: addedDeckAndCards[]) {
const numberOfAddedCards = addedDecksAndCards.reduce( const numberOfAddedCards = addedDecksAndCards.reduce(
(total, deck) => total + deck.numberOfCards, (total, deck) => total + deck.numberOfCards,
0 0
@ -61,7 +75,7 @@ function showAddedDecksConfirmation(context, addedDecksAndCards) {
table: addedDecksAndCards.map((deck) => { table: addedDecksAndCards.map((deck) => {
return { return {
name: deck.name, name: deck.name,
value: deck.numberOfCards, value: String(deck.numberOfCards),
}; };
}), }),
buttons: [ buttons: [
@ -73,6 +87,6 @@ function showAddedDecksConfirmation(context, addedDecksAndCards) {
}, },
}, },
], ],
}; } as CustomDialogOptions;
context.showCustomDialog(options); context.showCustomDialog(options);
} }

View file

@ -1,6 +0,0 @@
export function copyToClipboard() {
let ffcURL = document.getElementById("ffc-url");
ffcURL.select();
ffcURL.setSelectionRange(0, 99999);
document.execCommand("copy");
}

View file

@ -0,0 +1,8 @@
export function copyToClipboard() {
const ffcURL = document.getElementById("ffc-url") as HTMLInputElement;
if (!ffcURL) return;
ffcURL.select();
ffcURL.setSelectionRange(0, ffcURL.value.length);
document.execCommand("copy");
ffcURL.blur();
}

View file

@ -1,27 +1,33 @@
const LOCAL_STORAGE_APP_CONTEXT = "ffc_"; const LOCAL_STORAGE_APP_CONTEXT = "ffc_";
function get(key) { interface SyncItem {
return localStorage.getItem(LOCAL_STORAGE_APP_CONTEXT + key); key: string;
defaultValue: any;
} }
function set(key, value) { interface Context {
propertiesToSyncWithLocalStorage: SyncItem[];
[x: string]: any;
}
function get(key: string): string {
return localStorage.getItem(LOCAL_STORAGE_APP_CONTEXT + key) || "";
}
function set(key: string, value: string): void {
localStorage.setItem(LOCAL_STORAGE_APP_CONTEXT + key, value); localStorage.setItem(LOCAL_STORAGE_APP_CONTEXT + key, value);
} }
// function remove(key) { function clearAppData(): void {
// localStorage.removeItem(LOCAL_STORAGE_APP_CONTEXT + key);
// }
function clearAppData() {
for (let i = 0; i < localStorage.length; i++) { for (let i = 0; i < localStorage.length; i++) {
const key = localStorage.key(i); const key = localStorage.key(i) || "";
if (key.startsWith(LOCAL_STORAGE_APP_CONTEXT)) { if (key.startsWith(LOCAL_STORAGE_APP_CONTEXT)) {
localStorage.removeItem(key); localStorage.removeItem(key);
} }
} }
} }
export function readFromLocalStorage(context) { export function readFromLocalStorage(context: Context) {
context.propertiesToSyncWithLocalStorage.forEach((item) => { context.propertiesToSyncWithLocalStorage.forEach((item) => {
try { try {
context[item.key] = JSON.parse(get(item.key)); context[item.key] = JSON.parse(get(item.key));
@ -33,10 +39,10 @@ export function readFromLocalStorage(context) {
} }
}); });
} }
export function saveToLocalStorage(context, item) { export function saveToLocalStorage(context: Context, item: SyncItem) {
set(item.key, JSON.stringify(context[item.key])); set(item.key, JSON.stringify(context[item.key]));
} }
export function clearLocalStorage(context) { export function clearLocalStorage(context: Context) {
clearAppData(); clearAppData();
context.propertiesToSyncWithLocalStorage.forEach((item) => { context.propertiesToSyncWithLocalStorage.forEach((item) => {
context[item.key] = item.defaultValue; context[item.key] = item.defaultValue;

View file

@ -1,4 +1,16 @@
export function deleteSelected(context) { import Vue from 'vue';
import { Deck, CustomDialogOptions } from '@/types';
import router from '@/router';
interface Context {
numberOfSelectedDecks: number,
$eventHub: typeof Vue,
decks: Deck[],
deselectAll: Function,
$router: typeof router,
}
export function deleteSelected(context: Context) {
const options = { const options = {
title: `Delete Deck${context.numberOfSelectedDecks > 1 ? "s" : ""}?`, title: `Delete Deck${context.numberOfSelectedDecks > 1 ? "s" : ""}?`,
message: `Do you really want to delete the ${ message: `Do you really want to delete the ${
@ -24,10 +36,10 @@ export function deleteSelected(context) {
context.$eventHub.$emit("showCustomDialog", options); context.$eventHub.$emit("showCustomDialog", options);
} }
export function showInfoForSelectedDeck(context) { export function showInfoForSelectedDeck(context: Context) {
const selectedDeck = context.decks.find((deck) => deck.selected); const selectedDeck = context.decks.find((deck) => deck.selected);
const options = { const options = {
title: selectedDeck.name, title: selectedDeck?.name,
table: [], table: [],
buttons: [ buttons: [
{ {
@ -35,7 +47,7 @@ export function showInfoForSelectedDeck(context) {
color: "indigo", color: "indigo",
}, },
], ],
}; } as CustomDialogOptions;
const infos = [ const infos = [
{ {
meta: "file", meta: "file",
@ -58,20 +70,28 @@ export function showInfoForSelectedDeck(context) {
]; ];
for (const info of infos) { for (const info of infos) {
for (const content of info.content) { for (const content of info.content) {
options.table.push({ if (info.meta === "file") {
name: content.name, options.table?.push({
value: selectedDeck.meta[info.meta][content.key] || "-", name: content.name,
}); value: selectedDeck?.meta.file[content.key] || "-",
});
} else if (info.meta === "deck") {
options.table?.push({
name: content.name,
value: selectedDeck?.meta.deck[content.key] || "-",
});
}
} }
} }
options.table.push({ options.table?.push({
name: "Number of Cards", name: "Number of Cards",
value: selectedDeck.cards.length, value: String(selectedDeck?.cards.length || 0),
}); });
context.$eventHub.$emit("showCustomDialog", options); context.$eventHub.$emit("showCustomDialog", options);
} }
export function quitLearning(context) {
export function quitLearning(context: Context) {
context.$eventHub.$emit("showCustomDialog", { context.$eventHub.$emit("showCustomDialog", {
title: "Quit Learning?", title: "Quit Learning?",
message: message:
@ -91,4 +111,4 @@ export function quitLearning(context) {
} }
] ]
}); });
} }

View file

@ -1,15 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import vuetify from './plugins/vuetify';
import './registerServiceWorker'
Vue.config.productionTip = false
Vue.prototype.$eventHub = new Vue()
new Vue({
router,
vuetify,
render: h => h(App)
}).$mount('#app')

20
src/main.ts Normal file
View file

@ -0,0 +1,20 @@
import Vue from "vue";
import App from "./App.vue";
import "./registerServiceWorker";
import router from "./router";
import vuetify from "./plugins/vuetify";
Vue.config.productionTip = false;
declare module "vue/types/vue" {
interface Vue {
$eventHub: Vue;
}
}
Vue.prototype.$eventHub = new Vue();
new Vue({
router,
vuetify,
render: (h) => h(App),
}).$mount("#app");

View file

@ -1,10 +0,0 @@
import Vue from 'vue';
import Vuetify from 'vuetify/lib';
Vue.use(Vuetify);
export default new Vuetify({
theme: {
dark: true,
},
});

10
src/plugins/vuetify.ts Normal file
View file

@ -0,0 +1,10 @@
import Vue from "vue";
import Vuetify from "vuetify/lib";
Vue.use(Vuetify);
export default new Vuetify({
theme: {
dark: true,
},
});

View file

@ -1,32 +0,0 @@
/* eslint-disable no-console */
import { register } from 'register-service-worker'
if (process.env.NODE_ENV === 'production') {
register(`${process.env.BASE_URL}service-worker.js`, {
ready () {
console.log(
'App is being served from cache by a service worker.\n' +
'For more details, visit https://goo.gl/AFskqB'
)
},
registered () {
console.log('Service worker has been registered.')
},
cached () {
console.log('Content has been cached for offline use.')
},
updatefound () {
console.log('New content is downloading.')
},
updated () {
console.log('New content is available; please refresh.')
},
offline () {
console.log('No internet connection found. App is running in offline mode.')
},
error (error) {
console.error('Error during service worker registration:', error)
}
})
}

View file

@ -0,0 +1,34 @@
/* eslint-disable no-console */
import { register } from "register-service-worker";
if (process.env.NODE_ENV === "production") {
register(`${process.env.BASE_URL}service-worker.js`, {
ready() {
console.log(
"App is being served from cache by a service worker.\n" +
"For more details, visit https://goo.gl/AFskqB"
);
},
registered() {
console.log("Service worker has been registered.");
},
cached() {
console.log("Content has been cached for offline use.");
},
updatefound() {
console.log("New content is downloading.");
},
updated() {
console.log("New content is available; please refresh.");
},
offline() {
console.log(
"No internet connection found. App is running in offline mode."
);
},
error(error) {
console.error("Error during service worker registration:", error);
}
});
}

View file

@ -1,9 +1,9 @@
import Vue from 'vue' import Vue from "vue";
import VueRouter from 'vue-router' import VueRouter, { RouteConfig } from "vue-router";
Vue.use(VueRouter) Vue.use(VueRouter);
const routes = [ const routes: Array<RouteConfig> = [
{ {
path: '/', path: '/',
name: 'DeckSelection', name: 'DeckSelection',
@ -32,11 +32,11 @@ const routes = [
name: 'About', name: 'About',
component: () => import('../views/About.vue'), component: () => import('../views/About.vue'),
} }
] ];
const router = new VueRouter({ const router = new VueRouter({
base: process.env.BASE_URL, base: process.env.BASE_URL,
routes routes
}) });
export default router export default router;

13
src/shims-tsx.d.ts vendored Normal file
View file

@ -0,0 +1,13 @@
import Vue, { VNode } from "vue";
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any;
}
}
}

4
src/shims-vue.d.ts vendored Normal file
View file

@ -0,0 +1,4 @@
declare module "*.vue" {
import Vue from "vue";
export default Vue;
}

View file

@ -1,22 +1,72 @@
export interface FFCFile {
meta: {
author: string;
[x: string]: any;
};
decks: {
[deck_short_name: string]: {
meta: {
deck_name: string;
description: string;
next_card_id: number;
[x: string]: any;
};
cards: {
[cardId: string]: {
q: string;
a: string;
};
};
};
};
}
export interface Deck { export interface Deck {
id: number, id: number;
selected: boolean, selected: boolean;
name: string, name: string;
meta: { meta: {
file: object, file: {
deck: object [x: string]: any;
}, };
cards: Card[], deck: {
[x: string]: any;
};
};
cards: Card[];
} }
export interface Card { export interface Card {
id: number, id: number;
q: string, q: string;
a: string, a: string;
r?: Rating[], r?: Rating[];
} }
export interface Rating { export interface Rating {
t: number, t: number;
r: number, r: number;
} }
export interface CustomDialogOptions {
title: string;
format?: string;
message?: string;
tableHead?: {
name: string;
value: string;
};
table?: CustomDialogOptionsTableRow[];
buttons?: CustomDialogOptionsButton[];
}
export interface CustomDialogOptionsTableRow {
name: string;
value: string;
}
export interface CustomDialogOptionsButton {
name: string;
color: string;
callback?: Function;
}

View file

@ -23,7 +23,7 @@
</template> </template>
<script> <script>
import { copyToClipboard } from "../helpers/copyToClipboardHelper.js"; import { copyToClipboard } from "../helpers/copyToClipboardHelper";
export default { export default {
name: "About", name: "About",

47
tsconfig.json Normal file
View file

@ -0,0 +1,47 @@
{
"compilerOptions": {
"target": "esnext",
"module": "esnext",
"noImplicitAny": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"strict": true,
"experimentalDecorators": true,
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env",
"vuetify"
],
"typeRoots": [
"./node_modules/@types",
"./node_modules/vuetify/types"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}

View file

@ -1,14 +1,12 @@
module.exports = { module.exports = {
"publicPath": "/ffc/", publicPath: "/ffc/",
"transpileDependencies": [ transpileDependencies: ["vuetify"],
"vuetify"
],
pwa: { pwa: {
themeColor: "#363636", themeColor: "#363636",
msTileColor: "#3F51B5", msTileColor: "#3F51B5",
manifestOptions: { manifestOptions: {
name: "Fancy Flashcard", name: "Fancy Flashcard",
short_name: "FFC", short_name: "FFC",
} },
} },
} };