add vue swatches

This commit is contained in:
Gamebrary 2022-08-11 15:00:09 -07:00
parent c1f3626a72
commit 642718d207
34 changed files with 1025 additions and 1067 deletions

View file

@ -42,6 +42,7 @@
"vue-raven": "^1.0.0",
"vue-router": "^3.5.1",
"vue-shortkey": "^3.1.7",
"vue-swatches": "^2.1.1",
"vue-tweet-embed": "^2.4.0",
"vuedraggable": "^2.24.3",
"vuefire": "^1.4.5",

View file

@ -72,7 +72,7 @@ export default {
...mapState(['user', 'settings', 'sessionExpired']),
style() {
const backgroundImage = this.backgroundImageUrl
const backgroundImage = this.$route.name === 'game' && this.backgroundImageUrl
? `background-image: url('${this.backgroundImageUrl}');`
: null;
@ -142,18 +142,18 @@ export default {
}
if (this.user) {
this.load();
this.boot();
} else if (this.$route.name !== 'auth' && !this.$route.params.providerId) {
this.$router.replace({ name: 'auth' });
}
},
load() {
boot() {
this.$store.dispatch('LOAD_BOARDS');
this.$store.dispatch('LOAD_RELEASES');
this.$store.dispatch('LOAD_WALLPAPERS');
this.$store.dispatch('SYNC_LOAD_SETTINGS');
this.$store.dispatch('SYNC_LOAD_TAGS');
this.$store.dispatch('LOAD_TAGS');
this.$store.dispatch('SYNC_LOAD_NOTES');
this.$store.dispatch('SYNC_LOAD_PROGRESSES');
},

View file

@ -26,17 +26,12 @@
:disabled="saving || isDuplicate || !listName"
@click.stop="submit"
>
<b-spinner small v-if="saving" />
<b-spinner v-if="saving" small />
<span v-else>Add</span>
</b-button>
<!-- <b-button variant="outline-success">Button</b-button> -->
<!-- <b-button variant="info">Button</b-button> -->
</b-input-group-append>
</b-input-group>
<b-alert
class="mb-2"
:show="isDuplicate && !saving"

View file

@ -1,17 +1,7 @@
<template lang="html">
<div class="text-center pt-5">
<div class="text-center pt-5 ml-auto mr-auto">
<h2 v-if="title">{{ title }}</h2>
<p v-if="message">{{ message }}</p>
<b-button
v-if="actionText"
variant="primary"
@click="$emit('action')"
>
<b-spinner small v-if="busy" />
<span v-else>{{ actionText }}</span>
</b-button>
<slot />
</div>
</template>

View file

@ -13,11 +13,6 @@
<span class="d-none d-lg-inline">Tags</span>
</b-button>
<b-button variant="light" :to="{ name: 'game.progress', params: { id: game.id, slug: game.slug } }">
<i class="fa-solid fa-bars-progress fa-fw" />
<span class="d-none d-lg-inline">Track progress</span>
</b-button>
<b-button variant="light" :to="{ name: 'game.notes', params: { id: game.id, slug: game.slug } }">
<i class="fa-solid fa-note-sticky fa-fw" />
<span class="d-none d-lg-inline">Notes</span>

View file

@ -1,11 +1,7 @@
<!-- TODO: restore wikipedia -->
<template lang="html">
<div class="game-description">
<template v-if="loading">
<b-skeleton
v-for="n in 3"
:key="n"
/>
</template>
<b-spinner v-if="loading" class="spinner-centered" />
<template v-else>
<div v-html="description" />
@ -51,9 +47,10 @@ export default {
if (this.wikipediaExtract) return 'Wikipedia';
if (this.steamDescription) return 'Steam';
return 'IGDB';
// if (this.game?.steam?.short_description) return 'Steam';
if (this.game?.steam?.short_description) return 'Steam';
return 'IGDB';
//
// return this.wikipediaArticle && this.wikipediaArticle.lead && this.wikipediaArticle.lead[0]
// ? 'Wikipedia'

View file

@ -1,34 +1,34 @@
<template lang="html">
<b-list-group flush>
<div class="mt-3">
<game-genres />
<b-list-group-item v-if="gameModes" class="p-2 small">
<div v-if="gameModes" class="pr-2 pb-3 small">
<strong>{{ $t('board.gameModal.gameModes') }}: </strong>
<span class="text-wrap">{{ gameModes }}</span>
</b-list-group-item>
</div>
<b-list-group-item v-if="gameDevelopers" class="p-2 small">
<div v-if="gameDevelopers" class="pr-2 pb-3 small">
<strong>{{ $t('board.gameModal.developers') }}: </strong>
<span class="text-wrap">{{ gameDevelopers }}</span>
</b-list-group-item>
</div>
<b-list-group-item v-if="gamePublishers" class="p-2 small">
<div v-if="gamePublishers" class="pr-2 pb-3 small">
<strong>{{ $t('board.gameModal.publishers') }}: </strong>
<span class="text-wrap">{{ gamePublishers }}</span>
</b-list-group-item>
</div>
<b-list-group-item v-if="playerPerspectives" class="p-2 small">
<div v-if="playerPerspectives" class="pr-2 pb-3 small">
<strong>{{ $t('board.gameModal.perspective') }}: </strong>
<span class="text-wrap">{{ playerPerspectives }}</span>
</b-list-group-item>
</div>
<b-list-group-item class="p-2 small">
<div class="pr-2 pb-3 small">
<strong>Available for: </strong>
<span class="text-wrap">{{ gamePlatforms || 'N/A' }}</span>
</b-list-group-item>
</div>
<b-list-group-item class="p-2 small">
<div class="pr-2 pb-3 small">
<strong>{{ $t('board.gameModal.releaseDate') }}</strong>
<ol v-if="releaseDates" class="list-unstyled mb-0">
<li
@ -42,8 +42,8 @@
<div v-else>
Not released yet
</div>
</b-list-group-item>
</b-list-group>
</div>
</div>
</template>
<script>

View file

@ -1,19 +1,17 @@
<template lang="html">
<b-list-group-item v-if="gameGenres" class="p-2 small">
<div v-if="gameGenres" class="pr-2 pb-3 small">
<strong>Genres:</strong>
<b-avatar
{{ gameGenres }}
<!-- <span
v-for="genre in gameGenres"
:key="genre.id"
rounded
button
variant="transparent"
class="mr-1"
v-b-tooltip.hover
:title="genre.name"
>
<i v-if="genre.icon" :class="`${genre.icon} p-0`" />
</b-avatar>
</b-list-group-item>
{{ genre.name }}
</span> -->
</div>
</template>
<script>
@ -27,10 +25,7 @@ export default {
gameGenres() {
const gameGenres = this.game?.genres || [];
return gameGenres.map(genre => ({
...genre,
icon: GENRE_ICONS[genre.id] || null,
}));
return gameGenres.map((genre) => genre.name).join(', ');
},
},
};

View file

@ -2,40 +2,42 @@
<b-row v-if="user" class="p-1 boards">
<!-- TODO: allow reorganizing and save -->
<!-- TODO: add sorting -->
<empty-state
v-if="!user || !loading && sortedBoards.length === 0"
title="Boards"
message="Use boards to easily organize your video game collections"
>
<b-button :to="{ name: 'create.board' }">
{{ $t('boards.create') }}
</b-button>
<!-- <b-button :to="{ name: 'public.boards' }">
View public boards
</b-button> -->
</empty-state>
<template v-if="showPlaceholder">
Loading
</template>
<template v-else>
<b-col
v-for="board in sortedBoards"
:key="board.id"
cols="6"
sm="6"
md="4"
lg="3"
class="p-2"
<empty-state
v-if="!user || !loading && sortedBoards.length === 0"
title="Boards"
message="Use boards to easily organize your video game collections"
>
<mini-board
:board="board"
:background-image="getWallpaperUrl(board.backgroundUrl)"
@view-board="viewBoard(board.id)"
/>
</b-col>
<b-button :to="{ name: 'create.board' }">
{{ $t('boards.create') }}
</b-button>
<!-- <b-button :to="{ name: 'public.boards' }">
View public boards
</b-button> -->
</empty-state>
<template v-else>
<b-col
v-for="board in sortedBoards"
:key="board.id"
cols="6"
sm="6"
md="4"
lg="3"
class="p-2"
>
<mini-board
:board="board"
:background-image="getWallpaperUrl(board.backgroundUrl)"
@view-board="viewBoard(board.id)"
/>
</b-col>
</template>
</template>
</b-row>
</template>

View file

@ -1,35 +1,23 @@
<template lang="html">
<b-card no-body class="mb-2">
<b-row no-gutters>
<b-col cols="2">
<b-link :to="{ name: 'game', params: { id: game.id, slug: game.slug }}">
<b-card-img
:src="coverUrl"
alt="Image"
/>
</b-link>
</b-col>
<b-card
no-body
:title="game.name"
:img-src="coverUrl"
:img-alt="game.name"
img-top
class="mb-2"
footer-class="p-0 text-center font-weight-bold bold strong"
@click="$router.push({ name: 'game', params: { id: game.id, slug: game.slug }})"
>
<!-- :to="{ name: 'game', params: { id: game.id, slug: game.slug }}" -->
<b-col cols="10">
<b-card-body :title="game.name" title-tag="h4">
<b-button
v-if="activeList"
@click="$emit('addToActiveList', game.id)"
>
Add
</b-button>
<b-button
v-else
@click="handleGameClick"
variant="primary"
>
<i class="fa fa-plus fa-fw" aria-hidden="true" />
Add to list
</b-button>
</b-card-body>
</b-col>
</b-row>
<template #footer>
<small class="text-muted">
<!-- <pre>{{ selectedBoard }}</pre> -->
<!-- <pre>{{ selectedList }}</pre> -->
<strong>{{ game.name }}</strong>
</small>
</template>
</b-card>
</template>
@ -43,22 +31,71 @@ export default {
type: Object,
required: true,
},
activeList: Boolean,
},
computed: {
...mapState(['user']),
...mapState(['user', 'boards']),
coverUrl() {
return getGameCoverUrl(this.game);
},
selectedBoard() {
const { boardId } = this.$route.query;
return this.boards.find(({ id }) => id === boardId);
},
selectedList() {
const { listIndex } = this.$route.query;
return this.selectedBoard.lists[listIndex];
},
},
methods: {
handleGameClick() {
console.log(this.user);
// handleClick() {
// const { listIndex, boardId } = this.$route.query;
//
// if (listIndex && boardId) return this.addGameToList();
//
// return this.user
// ? this.$bus.$emit('ADD_GAME', this.game.id)
// : this.$router.push({ name: 'game', params: { id: this.game.id, slug: this.game.slug }});
// },
// this.$bus.$emit('ADD_GAME', this.game.id);
addGameToList() {
return this.selectedList.games.includes(this.game.id)
? this.removeGame()
: this.addGame();
},
async addGame() {
const boardIndex = this.boards.findIndex(({ id }) => id === this.selectedBoard.id);
const board = this.boards[boardIndex];
console.log(board);
// board.lists[listIndex].games.push(this.game.id);
// try {
// await this.$store.dispatch('SAVE_GAME_BOARD', board);
// } catch (e) {
// // this.$bvToast.toast(`There was an error adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
// }
},
async removeGame({ listIndex, boardId }) {
// const boardIndex = this.boards.findIndex(({ id }) => id === boardId);
// const board = this.boards[boardIndex];
// const gameIndex = board.lists[listIndex].games.indexOf(this.gameId);
//
// board.lists[listIndex].games.splice(gameIndex, 1);
//
// try {
// await this.$store.dispatch('SAVE_GAME_BOARD', board);
// } catch (e) {
// // this.$bvToast.toast(`There was an error removing "${this.game.name}"`, { title: list.name, variant: 'danger' });
// }
},
},
};

View file

@ -1,102 +0,0 @@
<template lang="html">
<b-card
no-body
:title="game.name"
:img-src="coverUrl"
:img-alt="game.name"
img-top
class="mb-2"
footer-class="p-0 text-center font-weight-bold bold strong"
@click="handleClick"
>
<!-- :to="{ name: 'game', params: { id: game.id, slug: game.slug }}" -->
<template #footer>
<small class="text-muted">
<!-- <pre>{{ selectedBoard }}</pre> -->
<!-- <pre>{{ selectedList }}</pre> -->
<strong>{{ game.name }}</strong>
</small>
</template>
</b-card>
</template>
<script>
import { getGameCoverUrl } from '@/utils';
import { mapState } from 'vuex';
export default {
props: {
game: {
type: Object,
required: true,
},
},
computed: {
...mapState(['user', 'boards']),
coverUrl() {
return getGameCoverUrl(this.game);
},
selectedBoard() {
const { boardId } = this.$route.query;
return this.boards.find(({ id }) => id === boardId);
},
selectedList() {
const { listIndex } = this.$route.query;
return this.selectedBoard.lists[listIndex];
},
},
methods: {
handleClick() {
const { listIndex, boardId } = this.$route.query;
if (listIndex && boardId) return this.addGameToList();
return this.user
? this.$bus.$emit('ADD_GAME', this.game.id)
: this.$router.push({ name: 'game', params: { id: this.game.id, slug: this.game.slug }});
},
addGameToList() {
return this.selectedList.games.includes(this.game.id)
? this.removeGame()
: this.addGame();
},
async addGame() {
const boardIndex = this.boards.findIndex(({ id }) => id === this.selectedBoard.id);
const board = this.boards[boardIndex];
console.log(board);
// board.lists[listIndex].games.push(this.game.id);
// try {
// await this.$store.dispatch('SAVE_GAME_BOARD', board);
// } catch (e) {
// // this.$bvToast.toast(`There was an error adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
// }
},
async removeGame({ listIndex, boardId }) {
// const boardIndex = this.boards.findIndex(({ id }) => id === boardId);
// const board = this.boards[boardIndex];
// const gameIndex = board.lists[listIndex].games.indexOf(this.gameId);
//
// board.lists[listIndex].games.splice(gameIndex, 1);
//
// try {
// await this.$store.dispatch('SAVE_GAME_BOARD', board);
// } catch (e) {
// // this.$bvToast.toast(`There was an error removing "${this.game.name}"`, { title: list.name, variant: 'danger' });
// }
},
},
};
</script>

View file

@ -1,11 +1,13 @@
<template lang="html">
<header class="p-2 d-flex">
<home-button />
<!-- TODO: rename target -->
<portal-target name="headerTitle" slim />
<!-- <boards-dropdown v-if="board.id && isBoardPage" /> -->
<!-- <game-dropdown v-if="isGamePage" /> -->
<div class="global-actions">
<!-- TODO: rename target -->
<portal-target name="headerActions" />
<!-- <b-button v-if="user" class="mr-2" variant="success" :to="{ name: 'upgrade' }">

View file

@ -142,8 +142,4 @@ export default {
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.field {
width: 340px;
margin: 0 auto;
}
</style>

View file

@ -1,72 +1,50 @@
<template lang="html">
<div>
<b-card
v-for="({ games, hex, tagTextColor }, name) in tags"
class="tags-list"
<b-row>
<b-col
v-for="({ hex, tagTextColor, name }, index) in tags"
@click="$router.push({ name: 'tag.edit', params: { id: index } })"
cols="6"
xl="4"
class="mb-3"
:key="name"
>
<div>
<b-dropdown class="float-right" right>
<template v-slot:button-content>
<i class="fas fa-ellipsis-h fa-fw" aria-hidden />
</template>
<b-dropdown-item @click="$emit('edit', name)">
Edit
</b-dropdown-item>
<b-dropdown-item
variant="danger"
@click="$emit('delete', name)"
>
Delete
</b-dropdown-item>
</b-dropdown>
<b-badge
pill
tag="small"
:style="`background-color: ${hex}; color: ${tagTextColor}`"
>
{{ name }}
</b-badge>
<p class="small text-muted">
{{ games.length }} Games
</p>
</div>
<div class="d-flex align-items-center overflow-auto">
<b-img
v-for="gameId in games"
:key="gameId"
:src="getCoverUrl(gameId)"
width="80"
class="rounded cursor-pointer mr-2"
@click.stop="openGame(gameId)"
/>
</div>
</b-card>
</div>
<b-button
rounded
block
variant="outline-light"
:style="`background-color: ${hex}; color: ${tagTextColor}`"
>
{{ name }}
</b-button>
</b-col>
</b-row>
</template>
<script>
import { mapState } from 'vuex';
import { mapState, mapGetters } from 'vuex';
export default {
data() {
return {
loading: true,
}
},
computed: {
...mapState(['tags', 'games']),
...mapState(['tags']),
},
async mounted() {
this.loading = true;
await this.$store.dispatch('LOAD_TAGS').catch(() => {
this.loading = false;
});
this.loading = false;
},
methods: {
getCoverUrl(gameId) {
const game = this.games[gameId];
return game && game.cover && game.cover.image_id
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${game.cover.image_id}.jpg`
: '/no-image.jpg';
},
openGame(gameId) {
const { id, slug } = this.games[gameId];
@ -75,9 +53,3 @@ export default {
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.tags-list {
background: #fc0;
}
</style>

View file

@ -19,6 +19,7 @@ import messages from '@/i18n/';
import store from '@/store/';
import router from '@/router';
import bootstrapSettings from '@/bootstrapSettings';
import 'vue-swatches/dist/vue-swatches.css'
const EventBus = new Vue();

View file

@ -66,20 +66,4 @@ export default {
: '/no-image.jpg';
},
},
methods: {
removeTag(tagName) {
this.$store.commit('REMOVE_GAME_TAG', { tagName, gameId: this.gameId });
this.saveTags();
},
async saveTags() {
await this.$store.dispatch('SAVE_TAGS', this.tags)
.catch(() => {
this.$store.commit('SET_SESSION_EXPIRED', true);
});
this.$bvToast.toast('Tags updated');
},
},
};

View file

@ -1,64 +1,60 @@
<template lang="html">
<b-container fluid class="create-board-page pt-3">
<b-container fluid>
<portal to="headerTitle">Create board</portal>
<b-form @submit.prevent="createBoard" class="field">
<b-form-group label="Board name:" label-for="boardName">
<b-form-input
id="boardName"
v-model.trim="board.name"
placeholder="PS4 collection, Nintendo Switch, etc..."
autofocus
required
/>
</b-form-group>
<b-card class="create-board-form">
<b-form @submit.prevent="createBoard">
<h3>{{ $t('boards.create') }}</h3>
<b-form-group
label="Board description"
label-for="boardDescription"
>
<b-form-textarea
id="boardDescription"
v-model="board.description"
maxlength="280"
rows="2"
/>
</b-form-group>
<b-form-group label="Board name:" label-for="boardName">
<b-form-input
id="boardName"
v-model.trim="board.name"
placeholder="e.g. PS4 collection, Nintendo Switch, Xbox..."
autofocus
required
/>
</b-form-group>
<!-- <b-form-group
label="Board template"
>
<b-form-radio-group
v-model="selectedTemplate"
:options="boardTemplatesOptions"
name="radios-btn-default"
description="Optional"
/>
<b-form-group
label="Board description"
label-for="boardDescription"
>
<b-form-textarea
id="boardDescription"
v-model="board.description"
maxlength="280"
rows="2"
/>
</b-form-group>
<b-row v-if="selectedTemplate" class="mt-3">
<b-col v-for="column in boardTemplates[selectedTemplate]" :key="column">
<b-card
:header="column"
header-tag="header"
header-class="p-1 pl-2"
hide-footer
/>
</b-col>
</b-row>
</b-form-group> -->
<!-- <b-form-group
label="Board template"
>
<b-form-radio-group
v-model="selectedTemplate"
:options="boardTemplatesOptions"
name="radios-btn-default"
description="Optional"
/>
<b-row v-if="selectedTemplate" class="mt-3">
<b-col v-for="column in boardTemplates[selectedTemplate]" :key="column">
<b-card
:header="column"
header-tag="header"
header-class="p-1 pl-2"
hide-footer
/>
</b-col>
</b-row>
</b-form-group> -->
<b-button
variant="primary"
loading
type="submit"
>
<b-spinner small v-if="saving" />
<template v-else>Create board</template>
</b-button>
</b-form>
</b-card>
<b-button
variant="primary"
loading
type="submit"
>
<b-spinner small v-if="saving" />
<template v-else>Create board</template>
</b-button>
</b-form>
</b-container>
</template>
@ -111,14 +107,3 @@ export default {
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.create-board-page {
height: calc(100vh - 56px);
}
.create-board-form {
max-width: 420px;
margin: 0 auto;
}
</style>

118
src/pages/CreateTagPage.vue Normal file
View file

@ -0,0 +1,118 @@
<template lang="html">
<b-container fluid>
<portal to="headerTitle">
Create tag
</portal>
<form
ref="newTagForm"
@submit.stop.prevent="submit"
>
<b-form-row class="mb-3">
<b-col cols="8" md="9">
<b-form-input
maxlength="20"
:placeholder="$t('tags.form.inputPlaceholder')"
required
v-model.trim="tagName"
/>
<b-form-text v-if="tagName" tag="span">
{{ $t('tags.form.preview') }}
<b-badge :style="`background-color: ${hex}; color: ${tagTextColor}`">
{{ tagName }}
</b-badge>
</b-form-text>
</b-col>
<b-col cols="4" md="3">
<b-input-group>
<b-form-input
v-model="hex"
type="color"
required
/>
<b-form-input
v-model="tagTextColor"
type="color"
required
/>
</b-input-group>
</b-col>
</b-form-row>
<b-button
variant="primary"
class="d-flex ml-auto"
:disabled="isDuplicate || saving || !Boolean(tagName)"
@click="submit"
>
<b-spinner small v-if="saving" />
<span v-else>{{ $t('tags.form.addTag')}}</span>
</b-button>
<b-alert
class="mt-3 mb-0"
:show="isDuplicate"
variant="warning"
>
{{ $t('tags.form.duplicateMessage', { tagName }) }}
<strong>{{ tagName }}</strong>
</b-alert>
</form>
</b-container>
</template>
<script>
import { mapState } from 'vuex';
export default {
data() {
return {
colorCombinations: [
['#0d1137', '#e52165'],
['#ffffff', '#000000'],
['#101820', '#FEE715'],
['#F2AA4C', '#101820'],
['#F93822', '#FDD20E'],
],
tagTextColor: '#F4B41A',
tagName: '',
}
},
mounted() {
this.setRandomColors();
},
computed: {
...mapState(['tags', 'platform', 'games']),
// isDuplicate() {
// const { tagName, localTags } = this;
//
// const tagNames = Object.keys(localTags)
// .filter(name => name !== tagName)
// .map(name => name.toLowerCase());
//
// return tagNames.includes(tagName.toLowerCase());
// },
},
methods: {
setRandomColors() {
const { colorCombinations } = this;
const randomNumber = Math.floor(Math.random() * colorCombinations.length);
this.tagTextColor = colorCombinations[randomNumber][0];
this.hex = colorCombinations[randomNumber][1];
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -4,6 +4,10 @@
<!-- TODO: show list preview in full page view -->
<!-- TODO: show search inline, allow to go full screen (search page) -->
<b-container fluid class="p-0">
<portal to="headerTitle">
Edit list
</portal>
<b-row v-if="list" no-gutters>
<b-col sm="12" md="6">
<div :style="boardStyles" class="p-3 list-preview d-flex justify-content-center">

236
src/pages/EditTagPage.vue Normal file
View file

@ -0,0 +1,236 @@
<template lang="html">
<b-container fluid>
<portal to="headerTitle">
<div>
<b-button
variant="light"
class="mr-2"
:to="{ name: 'tags' }"
>
<i class="fa-solid fa-chevron-left" />
</b-button>
Edit tag
</div>
</portal>
<portal to="headerActions">
<b-button
variant="light"
class="mr-2"
@click="promptDeleteTag(tag.name)"
>
<i class="fas fa-trash-alt fa-fw" aria-hidden />
</b-button>
</portal>
<div v-if="loading" class="text-center mt-5 ml-auto">
<b-spinner/>
</div>
<form
v-else
ref="form"
@submit="saveTag"
>
<b-alert
class="mt-3 mb-0"
:show="isEditedNameDuplicate && !saving"
variant="warning"
>
You already have a tag named <strong>{{ tag.name }}</strong>
</b-alert>
<label for="tagName">Tag name:</label>
<b-form-input
id="tagName"
v-model.trim="tag.name"
class="mb-3 field"
maxlength="20"
:placeholder="$t('tags.form.inputPlaceholder')"
required
trim
/>
<p>Background color</p>
<v-swatches
v-model="tag.hex"
show-fallback
popover-x="left"
/>
<p>Text color</p>
<v-swatches
v-model="tag.tagTextColor"
show-fallback
popover-x="left"
/>
<p>Preview</p>
<b-button
v-if="tag.name"
rounded
block
size="sm"
class="mr-2 mb-2 field"
variant="outline-light"
:style="`background-color: ${tag.hex}; color: ${tag.tagTextColor}`"
>
{{ tag.name }}
</b-button>
<hr />
<p>Games tagged</p>
<div class="tagged-games">
<b-img
v-for="game in tag.games"
:key="game"
:src="getCoverUrl(game)"
class="cursor-pointer"
thumbnail
@click="$router.push({ name: 'game', params: { id: games[game].id, slug: games[game].slug }})"
/>
</div>
<hr />
<b-button
variant="primary"
:disabled="isEditedNameDuplicate || saving"
type="submit"
>
<b-spinner small v-if="saving" />
<span v-else>Save</span>
</b-button>
</form>
</b-container>
</template>
<script>
import VSwatches from 'vue-swatches'
import { mapState } from 'vuex';
export default {
data() {
return {
tag: {},
loading: true,
originalTagName: '',
localTags: {},
saving: false,
}
},
components: {
VSwatches,
},
computed: {
...mapState(['tags', 'games']),
tagNames() {
const sanitizedNames = this.tags?.map(({ name }) => name.toLowerCase());
return sanitizedNames.length > 0
? sanitizedNames.filter(name => name?.toLowerCase() !== this.originalTagName?.toLowerCase())
: [];
},
isEditedNameDuplicate() {
return this.tagNames?.includes(this.tag?.name?.toLowerCase());
},
tagIndex() {
return this.$route?.params?.id;
},
},
async mounted() {
this.load();
},
methods: {
getCoverUrl(gameId) {
const game = this.games[gameId];
return game?.cover?.image_id
? `https://images.igdb.com/igdb/image/upload/t_cover_small_2x/${game.cover.image_id}.jpg`
: '/no-image.jpg';
},
async load() {
this.loading = true;
await this.$store.dispatch('LOAD_TAGS');
const { tags, tagIndex } = this;
this.tag = JSON.parse(JSON.stringify(tags[tagIndex]));
this.originalTagName = JSON.parse(JSON.stringify(this.tag.name));
this.loading = false;
},
promptDeleteTag(tagName) {
this.$bvModal.msgBoxConfirm(this.$t('tags.delete.message'), {
title: this.$t('tags.delete.title'),
okVariant: 'danger',
okTitle: this.$t('tags.delete.buttonLabel'),
cancelTitle: this.$t('global.cancel'),
headerClass: 'pb-0 border-0',
footerClass: 'pt-0 border-0',
})
.then((value) => {
if (value) {
this.deleteTag(tagName);
}
});
},
deleteTag(tagName) {
this.$delete(this.localTags, tagName);
this.saveTags(true);
},
removeTag(tagName) {
this.$store.commit('REMOVE_GAME_TAG', { tagName, gameId: this.gameId });
this.saveTags();
},
async saveTag(e) {
e.preventDefault();
if (this.$refs.form.checkValidity()) {
const { tag, tags, originalTagName } = this;
tags[this.tagIndex] = tag;
console.log(tags);
// await this.$store.dispatch('SAVE_TAGS', tags)
// .catch(() => {
// this.$store.commit('SET_SESSION_EXPIRED', true);
// });
}
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.tagged-games {
display: grid;
grid-template-columns: repeat(auto-fill,minmax(80px, 1fr));
grid-auto-flow: column;
grid-auto-columns: minmax(80px, 1fr);
grid-gap: .5rem;
overflow-x: auto;
max-width: calc(100vw - 32px);
}
</style>

View file

@ -8,7 +8,9 @@
<template lang="html">
<b-container fluid>
<b-skeleton v-if="loading" />
<div v-if="loading" class="text-center mt-5 ml-auto">
<b-spinner/>
</div>
<template v-else-if="game">
<game-actions />
@ -73,33 +75,56 @@
sm="8"
xl="9"
>
<div class="bg-white p-4 rounded">
<game-titles />
<article class="bg-white p-4 rounded">
<header class="d-flex align-items-start justify-content-between pb-2">
<game-titles />
<b-progress
v-if="progress"
:value="progress"
variant="success"
height="8px"
v-b-modal.progress
class="my-1 w-25"
@click.native="$router.push({ name: 'game.notes', params: { id: game.id, slug: game.slug } })"
/>
<b-badge variant="success" v-if="game && game.steam && game.steam.metacritic">{{ game.steam.metacritic.score }}</b-badge>
<aside>
<b-button
variant="light"
pill
@click="$router.push({ name: 'game.progress', params: { id: game.id, slug: game.slug } })"
>
{{ progress || 0 }}%
</b-button>
<b-badge
v-for="({ hex, tagTextColor }, name) in gameTags"
<!-- <b-button :href="metacriticScore.url" variant="success" v-if="metacriticScore.url">
{{ metacriticScore.score }}
</b-button> -->
</aside>
</header>
<b-button
v-for="({ hex, tagTextColor, name }) in tags"
:key="name"
pill
tag="small"
class="mr-1 mb-2"
rounded
size="sm"
variant="outline-light"
class="mr-1 my-2"
:disabled="saving"
:style="`background-color: ${hex}; color: ${tagTextColor}`"
@click="$router.push({ name: 'game.tags', params: { id: game.id, slug: game.slug } })"
v-b-modal.tags
>
{{ name }}
</b-badge>
</b-button>
<aside class="bg-white float-right pl-2 pb-2">
<b-link
:to="{ name: 'game.media', params: { id: game.id, slug: game.slug } }"
>
<b-img
:src="gameScrenshot"
thumbnail
width="300"
/>
</b-link>
<game-websites :game="game" />
</aside>
<game-description />
<game-details />
<game-note
v-if="note"
@ -108,35 +133,14 @@
@click.native="$router.push({ name: 'game.notes', params: { id: game.id } })"
/>
<b-card
no-body
>
<b-link :to="{ name: 'game.media', params: { id: game.id, slug: game.slug } }">
<b-card-img :src="gameScrenshot" top />
</b-link>
<b-button
class="m-1"
variant="light"
:to="{ name: 'game.media', params: { id: game.id, slug: game.slug } }"
>
<i class="fa-solid fa-photo-film" />
Videos & Screenshots
</b-button>
<b-card-footer v-if="legalNotice">
<small class="text-muted" v-html="legalNotice" />
</b-card-footer>
<game-details />
<game-websites
:game="game"
/>
<b-card-footer v-if="legalNotice">
<small class="text-muted" v-html="legalNotice" />
</b-card-footer>
<!-- TODO: use speedrun logo -->
<!-- <pre>{{ game}}</pre> -->
<!-- <b-card-img src="https://placekitten.com/480/210" alt="Image" bottom></b-card-img> -->
</b-card>
</div>
<!-- TODO: use speedrun logo -->
<!-- <pre>{{ game}}</pre> -->
<!-- <b-card-img src="https://placekitten.com/480/210" alt="Image" bottom></b-card-img> -->
</article>
</b-col>
<b-col
@ -301,7 +305,7 @@ export default {
},
beforeDestroy() {
this.$bus.$emit('UPDATE_WALLPAPER', null);
// this.$bus.$emit('UPDATE_WALLPAPER', null);
// TODO: only clear board if game being viewed is not in current board
// if (!['game', 'board'].includes(this.$route.name)) {
// this.$store.commit('CLEAR_BOARD');
@ -310,7 +314,10 @@ export default {
computed: {
...mapState(['game', 'progresses', 'tags', 'boards', 'user', 'notes']),
...mapGetters(['gameTags']),
metacriticScore() {
return this.game?.steam?.metacritic || {};
},
note() {
return this.notes[this.game?.id] || null;
@ -444,6 +451,7 @@ export default {
this.loading = true;
this.$store.commit('CLEAR_GAME');
this.$bus.$emit('UPDATE_WALLPAPER', null);
this.$store.dispatch('LOAD_TAGS');
await this.$store.dispatch('LOAD_GAME', this.gameId)
.catch(() => {
@ -513,4 +521,9 @@ export default {
background-size: cover;
background-position: center;
}
article {
background: red;
min-height: 50vh;
}
</style>

View file

@ -2,8 +2,14 @@
<b-container>
<portal to="headerTitle">
<span>
{{ game.name }} |
<span class="text-muted">Track progress</span>
<b-button
:to="{ name: 'game', params: { id: game.id, slug: game.slug }}"
variant="light"
>
{{ game.name }}
</b-button>
Track progress
</span>
</portal>

View file

@ -1,65 +1,97 @@
<!-- TODO: finish layout -->
<template lang="html">
<b-container class="p-2">
<b-container fluid>
<portal to="headerTitle">
<span>
{{ game.name }} |
<span class="text-muted">Tags</span>
</span>
<div>
<b-button
:to="gamePage"
variant="light"
class="mr-2"
>
<i v-if="showBackIcon" class="fa-solid fa-chevron-left" />
<template v-else-if="game">
{{ game.name }}
</template>
</b-button>
Tags
</div>
</portal>
<template v-if="loading">
loading
</template>
<template v-else>
<router-link :to="{ name: 'game', params: { id: game.id, slug: game.slug }}">
<b-img :src="gameCoverUrl" width="200" rounded class="mb-2 mr-2" />
</router-link>
<h3>Tags</h3>
<p>Click on tag to add or remove tag from game</p>
<empty-state
v-if="empty"
class="mb-4"
message="Looks like you don't have any tags yet."
<portal to="headerActions">
<b-button
:to="{ name: 'tags' }"
variant="light"
class="mr-2"
>
<b-button @click="manageTags">Manage tags</b-button>
</empty-state>
Manage tags
</b-button>
</portal>
<b-row v-else>
<!-- TODO: Show current games in tag -->
<!-- TODO: Filter tag option if tags > too many -->
<b-col cols="12" md="auto">
<b-list-group>
<b-list-group-item
v-for="({ games, hex, tagTextColor }, name) in sortedTags"
:key="name"
class="d-flex justify-content-between"
button
:variant="games.includes(game.id) ? 'success' : ''"
@click="games.includes(game.id) ? removeTag(name) : addTag(name)"
<div v-if="loading" class="text-center mt-5 ml-auto">
<b-spinner/>
</div>
<b-row v-else>
<b-col cols="6">
<router-link :to="{ name: 'game', params: { id: game.id, slug: game.slug }}" class="float-right">
<b-img :src="gameCoverUrl" fluid rounded class="mb-2 mr-2 field" />
</router-link>
</b-col>
<b-col>
<empty-state
v-if="empty"
class="mb-4"
message="Looks like you don't have any tags yet."
>
<b-button @click="manageTags">Manage tags</b-button>
</empty-state>
<section class="field">
<section>
<h3 class="mb-3">Tags applied to {{ game.name }}</h3>
<b-alert
v-if="tagsSelected.length === 0"
show
variant="light"
>
<b-badge
pill
tag="small"
class="mr-3"
:style="`background-color: ${hex}; color: ${tagTextColor}`"
>
{{ name }}
</b-badge>
No tags applied
</b-alert>
<b-badge variant="light">{{ games.length }} games</b-badge>
</b-list-group-item>
</b-list-group>
</b-col>
</b-row>
</template>
<b-button
v-for="{ name, hex, tagTextColor } in tags"
:key="name"
rounded
block
variant="outline-light"
:style="`background-color: ${hex}; color: ${tagTextColor}`"
@click="removeTag"
>
{{ name }}
</b-button>
</section>
<b-button :to="{ name: 'team.settings' }">Manage tags</b-button>
<br />
<b-button :to="{ name: 'team.settings' }">Create new tag</b-button>
<hr />
<h3 class="my-3">Tags available</h3>
<pre>{{ tags }}</pre>
<!-- <b-button
v-for="({ name, hex, tagTextColor }, index) in tags"
:key="name"
rounded
block
variant="outline-light"
:disabled="saving"
:style="`background-color: ${hex}; color: ${tagTextColor}`"
@click="addTag(index)"
>
{{ name }}
</b-button> -->
</section>
</b-col>
</b-row>
</b-container>
</template>
@ -76,6 +108,7 @@ export default {
data() {
return {
loading: true,
saving: false,
};
},
@ -86,19 +119,38 @@ export default {
return getGameCoverUrl(this.game);
},
showBackIcon() {
return this.game?.name.length > 25;
},
gamePage() {
return { name: 'game', params: { id: this.game?.id, slug: this.game?.slug }};
},
empty() {
return Object.keys(this.tags).length === 0;
},
sortedTags() {
return Object.keys(this.tags)
.sort()
.reduce((res, key) => (res[key] = this.tags[key], res), {});
tagsSelected() {
return this.tags?.filter(({ games }) => {
return games?.includes(this.game?.id);
})
},
tagsAvailable() {
return Object.entries(this.tags).map((t) => {
const [name, tag] = t;
return { name: name, ...tag };
})
.filter(({ games }) => {
return !games.includes(this.game.id);
})
},
},
mounted() {
if (this.game.id !== this.$route.params.id) {
if (this.game?.id !== this.$route.params.id) {
this.loadGame();
} else {
this.loading = false;
@ -110,7 +162,8 @@ export default {
this.loading = true;
this.$store.commit('CLEAR_GAME');
await this.$store.dispatch('LOAD_GAME', this.$route.params.id)
await this.$store.dispatch('LOAD_GAME', this.$route.params.id);
await this.$store.dispatch('LOAD_TAGS')
.catch(() => {
this.loading = false;
});
@ -118,39 +171,43 @@ export default {
this.loading = false;
},
async addTag(tagName) {
const gameId = this.game.id;
async addTag(index) {
console.log('add this tag');
// TODO: use commit instead?
// const gameId = this.game.id;
//
// if (!gameId) return;
//
// const tags = JSON.parse(JSON.stringify(this.tags)) ;
//
// tags[index].games.push(gameId)
//
// console.log(`game id ${gameId} should be included`, tags[index].games);
this.$store.commit('ADD_GAME_TAG', { tagName, gameId });
await this.saveTags();
// this.saving = true;
this.$bvToast.toast(`Tag "${tagName}" added`, { title: this.game.name, variant: 'success' });
// await this.$store.dispatch('SAVE_TAGS', tags)
// .catch((e) => {
// console.log(e);
// });
// this.saving = false;
// this.$store.commit('ADD_GAME_TAG', { tagName, gameId });
// await this.saveTags();
},
async removeTag(tagName) {
const gameId = this.game.id;
this.$store.commit('REMOVE_GAME_TAG', { tagName, gameId });
await this.saveTags();
this.$bvToast.toast(`Tag "${tagName}" removed`, { title: this.game.name, variant: 'success' });
},
saveTags() {
this.$store.dispatch('SAVE_TAGS', this.tags)
.catch(() => {
this.$store.commit('SET_SESSION_EXPIRED', true);
});
// this.$store.commit('REMOVE_GAME_TAG', { tagName, gameId });
// await this.saveTags();
},
manageTags() {
this.$bvModal.hide('tags');
this.$bvModal.hide('game-modal');
this.$router.push({ name: 'tags' });
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -2,8 +2,19 @@
<!-- TODO: pagination? -->
<template lang="html">
<b-container fluid>
<portal to="headerTitle">Notes</portal>
<portal to="headerTitle">
<div>
<b-button
:to="{ name: 'settings' }"
variant="light"
class="mr-2"
>
<i class="fa-solid fa-chevron-left" />
</b-button>
Notes
</div>
</portal>
<portal to="headerActions">
<b-form-input
v-if="!showEmptyState"

View file

@ -46,37 +46,16 @@
<div v-else-if="searchResults.length > 0">
<header class="my-2 d-flex align-items-center justify-content-between">
<pre>{{ activeBoardList }}</pre>
<!-- <pre>{{ activeBoardList }}</pre> -->
<h3 v-if="activeBoardList.length">
Add games to <strong>{{ activeBoardList.name }}</strong>
</h3>
<!-- <h3>Search results</h3> -->
<b-button-toolbar key-nav aria-label="Toolbar with button groups" class="mr-1">
<b-button-group class="mx-1">
<b-button :variant="listView ? 'primary' : 'light'" @click="listView = true">
<i class="fa-solid fa-list fa-fw" aria-hidden />
</b-button>
<b-button :variant="listView ? 'light' : 'primary'" @click="listView = false">
<i class="fa-solid fa-grip fa-fw" aria-hidden />
</b-button>
</b-button-group>
</b-button-toolbar>
</header>
<template v-if="listView">
<div class="masonry-container">
<game-card-search
v-for="game in searchResults"
:key="game.id"
:game="game"
:active-list="Boolean(activeBoardList)"
@addToActiveList="addToActiveList"
/>
</template>
<div v-else class="masonry-container">
<game-card-search-vertical
v-for="game in searchResults"
class="masonry-item"
:key="game.id"
@ -100,23 +79,20 @@
</template>
<script>
import GameCardSearch from '@/components/GameCards/GameCardSearch';
import SearchBox from '@/components/SearchBox';
import GameCardSearchVertical from '@/components/GameCards/GameCardSearchVertical';
import GameCardSearch from '@/components/GameCards/GameCardSearch';
import { mapState } from 'vuex';
export default {
components: {
GameCardSearch,
SearchBox,
GameCardSearchVertical,
GameCardSearch,
},
data() {
return {
searchResults: [],
loading: false,
listView: true,
};
},

View file

@ -3,108 +3,109 @@
<portal to="headerTitle">Settings</portal>
<b-row>
<b-col cols="12" sm="10" md="8" lg="6">
<b-col>
<div class="field">
<settings-card
title="Wallpapers"
description="Manage your wallpapers"
icon="fa-images"
@click.native="$router.push({ name: 'wallpapers' })"
/>
<settings-card
title="Notes"
description="View all your notes"
icon="fa-note-sticky"
@click.native="$router.push({ name: 'notes' })"
/>
<settings-card
title="Tags"
description="View all your tags"
icon="fa-tags"
@click.native="$router.push({ name: 'tags' })"
/>
<settings-card
title="Account"
description="Manage your Gamebrary account"
icon="fa-user"
@click.native="$router.push({ name: 'account.settings' })"
/>
<!-- TODO: fix and reenable -->
<!-- <b-button
block
variant="secondary"
v-b-modal.keyboard-shortcuts
>
Keyboard shortcuts
</b-button> -->
<!-- <b-button
block
variant="secondary"
:to="{ name: 'dev.tools' }"
>
Dev tools
</b-button> -->
<b-button
href="https://github.com/romancm/gamebrary"
target="_blank"
block
variant="secondary"
>
<i class="fab fa-github fa-fw" />
GitHub
</b-button>
<b-button
block
variant="secondary"
href="https://goo.gl/forms/r0juBCsZaUtJ03qb2"
target="_blank"
>
Submit feedback
</b-button>
<!-- TODO: hide for paid users -->
<b-button
block
variant="outline-primary"
href="https://www.paypal.me/RomanCervantes/5"
target="_blank"
>
Buy me a coffee
</b-button>
<hr />
<!-- <b-list-group-item exact exact-active-class="bg-primary text-white" :to="{ name: 'profile.settings' }">
<i class="mr-2 fa-solid fa-user fa-fw" aria-hidden />
<small>Profile</small>
</b-list-group-item> -->
<!-- <b-list-group-item exact exact-active-class="bg-primary text-white" :to="{ name: 'steam.settings' }">
<i class="mr-2 fab fa-steam fa-fw" aria-hidden></i>
<small>Steam</small>
</b-list-group-item> -->
<!-- <hr /> -->
<!-- <b-list-group-item :to="{ name: 'profiles' }">
<i class="mr-2 fa-solid fa-people-group fa-fw" aria-hidden />
<small>Profiles</small>
</b-list-group-item> -->
<!-- {{ $t('global.donateMessage') }} -->
<!-- <a href="https://www.paypal.me/RomanCervantes/5" target="_blank">
{{ $t('global.donating') }}
</a> -->
<small>&copy; 2022 Gamebrary</small>
</div>
<!-- <language-settings /> -->
<settings-card
title="Wallpapers"
description="Manage your wallpapers"
icon="fa-images"
@click.native="$router.push({ name: 'wallpapers' })"
/>
<settings-card
title="Notes"
description="View all your notes"
icon="fa-note-sticky"
@click.native="$router.push({ name: 'notes' })"
/>
<settings-card
title="Tags"
description="View all your tags"
icon="fa-tags"
@click.native="$router.push({ name: 'tags' })"
/>
<settings-card
title="Account"
description="Manage your Gamebrary account"
icon="fa-user"
@click.native="$router.push({ name: 'account.settings' })"
/>
<!-- TODO: fix -->
<b-button
block
variant="secondary"
v-b-modal.keyboard-shortcuts
>
Keyboard shortcuts
</b-button>
<b-button
block
variant="secondary"
:to="{ name: 'dev.tools' }"
>
Dev tools
</b-button>
<b-button
href="https://github.com/romancm/gamebrary"
target="_blank"
block
variant="secondary"
>
<i class="fab fa-github fa-fw" />
GitHub
</b-button>
<b-button
block
variant="secondary"
href="https://goo.gl/forms/r0juBCsZaUtJ03qb2"
target="_blank"
>
Submit feedback
</b-button>
<!-- TODO: hide for paid users -->
<b-button
block
variant="outline-primary"
href="https://www.paypal.me/RomanCervantes/5"
target="_blank"
>
Buy me a coffee
</b-button>
<hr />
<!-- <b-list-group-item exact exact-active-class="bg-primary text-white" :to="{ name: 'profile.settings' }">
<i class="mr-2 fa-solid fa-user fa-fw" aria-hidden />
<small>Profile</small>
</b-list-group-item> -->
<!-- <b-list-group-item exact exact-active-class="bg-primary text-white" :to="{ name: 'steam.settings' }">
<i class="mr-2 fab fa-steam fa-fw" aria-hidden></i>
<small>Steam</small>
</b-list-group-item> -->
<!-- <hr /> -->
<!-- <b-list-group-item :to="{ name: 'profiles' }">
<i class="mr-2 fa-solid fa-people-group fa-fw" aria-hidden />
<small>Profiles</small>
</b-list-group-item> -->
<!-- {{ $t('global.donateMessage') }} -->
<!-- <a href="https://www.paypal.me/RomanCervantes/5" target="_blank">
{{ $t('global.donating') }}
</a> -->
<small>&copy; 2022 Gamebrary</small>
</b-col>
</b-row>
</b-container>

View file

@ -1,13 +0,0 @@
<template lang="html">
<div>
test
</div>
</template>
<script>
export default {
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
</style>

View file

@ -1,194 +1,35 @@
<!-- TODO: break this up into components -->
<!-- TODO: Move add tag to page -->
<!-- TODO: Move edit tag to page -->
<template lang="html">
<b-container fluid>
<b-row>
<b-spinner v-if="loading" class="spinner-centered" />
<b-row v-else>
<portal to="headerTitle">Tags</portal>
<portal to="headerActions">
<b-button
class="mr-2"
variant="light"
:to="{ name: 'tag.create' }"
>
Add tag
</b-button>
</portal>
<empty-state
v-if="showEmptyState"
:title="$t('tags.title')"
message="Tags are a great way to organize your collection"
>
<b-button
variant="primary"
v-b-modal.addTag
variant="light"
:to="{ name: 'tag.create' }"
>
Add tag
Create a tag
</b-button>
</empty-state>
<b-col v-else>
<portal to="headerTitle">Tags</portal>
<portal to="headerActions">
<b-button
class="mr-2"
variant="light"
@click="$bvModal.show('addTag')"
>
Add tag
</b-button>
</portal>
<tags-list
v-if="gameTags && localTags"
@edit="editTag"
@delete="promptDeleteTag"
/>
<tags-list />
</b-col>
<!-- TODO: move to component -->
<b-modal
id="editTag"
hide-footer
>
<template v-slot:modal-header="{ close }">
<modal-header
:title="$t('tags.edit.title')"
@close="close"
>
<b-button
variant="danger"
@click="promptDeleteTag(editingTagName)"
>
<i class="fas fa-trash-alt fa-fw" aria-hidden />
</b-button>
<b-button
variant="primary"
:disabled="isEditedNameDuplicate || !Boolean(editingTagName) || saving"
@click="saveTag"
>
<b-spinner small v-if="saving" />
<span v-else>Save</span>
</b-button>
</modal-header>
</template>
<form
ref="editTagForm"
@submit.stop.prevent="saveTag"
>
<b-form-row class="mb-3" v-if="editingTag">
<b-col cols="8" md="9">
<b-form-input
label="test"
maxlength="20"
:placeholder="$t('tags.form.inputPlaceholder')"
required
v-model.trim="editingTagName"
/>
</b-col>
<b-col cols="4" md="3">
<b-input-group>
<b-form-input
v-model="editingTag.hex"
type="color"
required
/>
<b-form-input
v-model="editingTag.tagTextColor"
type="color"
required
/>
</b-input-group>
</b-col>
</b-form-row>
<template v-if="editingTagName">
Preview:
<b-badge
:style="`background-color: ${editingTag.hex}; color: ${editingTag.tagTextColor}`"
>
{{ editingTagName }}
</b-badge>
</template>
</form>
<b-alert
class="mt-3 mb-0"
:show="isEditedNameDuplicate && !saving"
variant="warning"
>
You already have a tag named <strong>{{ editingTagName }}</strong>
</b-alert>
</b-modal>
<b-modal
id="addTag"
hide-footer
@show="open"
>
<template v-slot:modal-header="{ close }">
<modal-header
:title="$t('Add tag')"
@close="close"
/>
</template>
<form
ref="newTagForm"
@submit.stop.prevent="submit"
>
<b-form-row class="mb-3">
<b-col cols="8" md="9">
<b-form-input
label="test"
maxlength="20"
:placeholder="$t('tags.form.inputPlaceholder')"
required
v-model.trim="tagName"
/>
<b-form-text v-if="tagName" tag="span">
{{ $t('tags.form.preview') }}
<b-badge :style="`background-color: ${hex}; color: ${tagTextColor}`">
{{ tagName }}
</b-badge>
</b-form-text>
</b-col>
<b-col cols="4" md="3">
<b-input-group>
<b-form-input
v-model="hex"
type="color"
required
/>
<b-form-input
v-model="tagTextColor"
type="color"
required
/>
</b-input-group>
</b-col>
</b-form-row>
<b-button
variant="primary"
class="d-flex ml-auto"
:disabled="isDuplicate || saving || !Boolean(tagName)"
@click="submit"
>
<b-spinner small v-if="saving" />
<span v-else>{{ $t('tags.form.addTag')}}</span>
</b-button>
<b-alert
class="mt-3 mb-0"
:show="isDuplicate"
variant="warning"
>
{{ $t('tags.form.duplicateMessage', { tagName }) }}
<strong>{{ tagName }}</strong>
</b-alert>
</form>
</b-modal>
</b-row>
</b-container>
</template>
@ -206,194 +47,30 @@ export default {
data() {
return {
saving: false,
tagName: '',
hex: '#143D59',
tagTextColor: '#F4B41A',
colorCombinations: [
// [text, bg]
['#0d1137', '#e52165'],
['#ffffff', '#000000'],
['#101820', '#FEE715'],
['#F2AA4C', '#101820'],
['#F93822', '#FDD20E'],
],
exclusive: false,
editingTag: {},
editingTagName: '',
editingOriginalTagName: '',
localTags: {},
loading: false,
};
},
computed: {
...mapState(['tags', 'platform', 'games']),
...mapState(['tags']),
showEmptyState() {
return Object.keys(this.tags).length === 0;
},
isDuplicate() {
const { tagName, localTags } = this;
const tagNames = Object.keys(localTags)
.filter(name => name !== tagName)
.map(name => name.toLowerCase());
return tagNames.includes(tagName.toLowerCase());
},
isEditedNameDuplicate() {
const { editingOriginalTagName, editingTagName, localTags } = this;
const tagNames = Object.keys(localTags)
.filter(name => name !== editingOriginalTagName)
.map(tagName => tagName.toLowerCase());
return tagNames.includes(editingTagName.toLowerCase());
},
gameTags() {
return Object.keys(this.localTags).length > 0;
return !this.loading && this.tags?.length === 0;
},
},
mounted() {
this.localTags = JSON.parse(JSON.stringify(this.tags));
this.load();
},
methods: {
async load() {
const gamesInTags = Object.values(this.localTags)
.map(({ games }) => games.map(gameId => String(gameId)))
.reduce((entireList, games) => entireList.concat(games), []);
this.loading = true;
const cachedGameList = Object.keys(this.games);
const deDupedGameList = [...new Set(gamesInTags)];
const gamesToLoad = deDupedGameList
.filter(gameId => !cachedGameList.includes(gameId))
.toString();
await this.$store.dispatch('LOAD_TAGS')
.catch(() => { this.loading = false; })
if (gamesToLoad.length === 0) return;
await this.$store.dispatch('LOAD_GAMES', gamesToLoad)
.catch(() => {
this.$bvToast.toast('Error loading games', { variant: 'error' });
});
},
open() {
this.setRandomColors();
},
setRandomColors() {
const { colorCombinations } = this;
const randomNumber = Math.floor(Math.random() * colorCombinations.length);
this.tagTextColor = colorCombinations[randomNumber][0];
this.hex = colorCombinations[randomNumber][1];
},
editTag(tagName) {
this.editingTagName = tagName;
this.editingOriginalTagName = tagName;
this.editingTag = JSON.parse(JSON.stringify(this.localTags[tagName]));
this.$bvModal.show('editTag');
},
async saveTag(e) {
e.preventDefault();
if (this.$refs.editTagForm.checkValidity()) {
const { editingTagName, editingOriginalTagName, editingTag } = this;
const renaming = editingTagName.toLowerCase() !== editingOriginalTagName.toLowerCase();
if (renaming) {
this.$delete(this.localTags, editingOriginalTagName);
this.$set(this.localTags, editingTagName, editingTag);
await this.saveTags(true);
} else {
this.localTags[editingOriginalTagName] = JSON.parse(JSON.stringify(editingTag));
this.saveTags();
}
}
},
submit(e) {
e.preventDefault();
if (this.$refs.newTagForm.checkValidity()) {
this.createTag();
}
},
reset() {
this.tagName = '';
this.hex = '#143D59';
this.tagTextColor = '#F4B41A';
},
createTag() {
const { hex, tagTextColor, tagName } = this;
const newTag = {
games: [],
hex,
tagTextColor,
};
this.$set(this.localTags, tagName, newTag);
this.saveTags();
},
promptDeleteTag(tagName) {
this.$bvModal.msgBoxConfirm(this.$t('tags.delete.message'), {
title: this.$t('tags.delete.title'),
okVariant: 'danger',
okTitle: this.$t('tags.delete.buttonLabel'),
cancelTitle: this.$t('global.cancel'),
headerClass: 'pb-0 border-0',
footerClass: 'pt-0 border-0',
})
.then((value) => {
if (value) {
this.deleteTag(tagName);
}
});
},
deleteTag(tagName) {
this.$delete(this.localTags, tagName);
this.saveTags(true);
},
async saveTags(deleting) {
this.saving = true;
const action = deleting
? 'SAVE_TAGS_NO_MERGE'
: 'SAVE_TAGS';
await this.$store.dispatch(action, this.localTags)
.catch(() => {
this.saving = false;
this.$store.commit('SET_SESSION_EXPIRED', true);
});
const message = deleting
? 'Tags saved'
: 'Tag added';
this.$bvModal.hide('editTag');
this.$bvModal.hide('addTag');
this.$bvToast.toast(message, { title: 'Success', variant: 'success' });
this.reset();
this.saving = false;
this.loading = false;
},
},
};

View file

@ -10,7 +10,19 @@
/>
<template v-else>
<portal to="headerTitle">Wallpapers</portal>
<portal to="headerTitle">
<div>
<b-button
:to="{ name: 'settings' }"
variant="light"
class="mr-2"
>
<i class="fa-solid fa-chevron-left" />
</b-button>
Wallpapers
</div>
</portal>
<portal to="headerActions">
<b-button

View file

@ -22,6 +22,7 @@ import NotesPage from '@/pages/NotesPage';
import GameNotesPage from '@/pages/GameNotesPage';
import EditListPage from '@/pages/EditListPage';
import GameTagsPage from '@/pages/GameTagsPage';
import CreateTagPage from '@/pages/CreateTagPage';
import GameProgressPage from '@/pages/GameProgressPage';
import PrivacyPolicyPage from '@/pages/PrivacyPolicyPage';
import ProfilePage from '@/pages/ProfilePage';
@ -33,7 +34,7 @@ import SearchPage from '@/pages/SearchPage';
import SteamSettingsPage from '@/pages/SteamSettingsPage';
// import GeneralSettingsPage from '@/pages/GeneralSettingsPage';
import TagsPage from '@/pages/TagsPage';
import TagEditPage from '@/pages/TagEditPage';
import EditTagPage from '@/pages/EditTagPage';
import TermsPage from '@/pages/TermsPage';
import WallpapersPage from '@/pages/WallpapersPage';
@ -101,10 +102,18 @@ const routes = [
title: 'Tags',
},
},
{
name: 'tag.create',
path: '/tags/create',
component: CreateTagPage,
meta: {
title: 'Edit tag',
},
},
{
name: 'tag.edit',
path: '/tags/:id',
component: TagEditPage,
component: EditTagPage,
meta: {
title: 'Edit tag',
},

View file

@ -76,7 +76,6 @@ export default {
LOAD_STEAM_GAME({ commit }, steamGameId) {
return new Promise((resolve, reject) => {
console.log('action', steamGameId);
axios.get(`${API_BASE}/steam-game?gameId=${steamGameId}`)
.then(({ data }) => {
// TODO: move this logic to cloud function
@ -572,9 +571,11 @@ export default {
const db = firestore();
return new Promise((resolve, reject) => {
console.log(tags);
console.log(state.user.uid);
db.collection('tags')
.doc(state.user.uid)
.set(tags, { merge: true })
.set({ tags }, { merge: false })
.then(() => resolve())
.catch(reject);
});
@ -595,17 +596,17 @@ export default {
});
},
SAVE_TAGS_NO_MERGE({ state }, tags) {
const db = firestore();
return new Promise((resolve, reject) => {
db.collection('tags')
.doc(state.user.uid)
.set(tags, { merge: false })
.then(() => resolve())
.catch(reject);
});
},
// SAVE_TAGS_NO_MERGE({ state }, tags) {
// const db = firestore();
//
// return new Promise((resolve, reject) => {
// db.collection('tags')
// .doc(state.user.uid)
// .set(tags, { merge: false })
// .then(() => resolve())
// .catch(reject);
// });
// },
SAVE_SETTINGS({ commit, state }, settings) {
const db = firestore();
@ -720,37 +721,52 @@ export default {
.doc(state.user.uid)
.get()
.then((doc) => {
if (doc.exists) {
const tags = doc.data();
if (!doc.exists) return reject();
commit('SET_TAGS', tags);
resolve();
const { tags } = doc.data();
if (typeof tags === 'object') {
console.warn('Legacy tag detected');
const formattedTags = Object.entries(tags).map(([ ,tag]) => ({ ...tag }));
commit('SET_TAGS', formattedTags);
resolve(formattedTags);
} else {
commit('SET_SESSION_EXPIRED', true);
reject();
console.log('is type', typeof tags);
}
});
});
},
SYNC_LOAD_TAGS({ commit, state }) {
return new Promise((resolve, reject) => {
const db = firestore();
db.collection('tags')
.doc(state.user.uid)
.onSnapshot((doc) => {
if (doc.exists) {
const tags = doc.data();
commit('SET_TAGS', tags);
resolve();
} else {
reject();
}
});
});
},
// SYNC_LOAD_TAGS({ commit, state }) {
// return new Promise((resolve, reject) => {
// const db = firestore();
//
// db.collection('tags')
// .doc(state.user.uid)
// .onSnapshot((doc) => {
// if (doc.exists) {
// const tags = doc.data();
//
// const sortedTags = Object.keys(tags)
// .sort()
// .reduce((res, key) => (res[key] = tags[key], res), {});
//
// const mappedTags = Object.entries(sortedTags).map((t) => {
// const [name, tag] = t;
//
// return { name, ...tag };
// })
//
// commit('SET_TAGS', mappedTags);
// resolve();
// } else {
// reject();
// }
// });
// });
// },
SYNC_LOAD_NOTES({ commit, state }) {
return new Promise((resolve, reject) => {

View file

@ -1,6 +1,6 @@
export default {
user: null,
tags: {},
tags: [],
notes: {},
profile: {},
profiles: [],

View file

@ -1,29 +1,9 @@
.card-columns {
@media(min-width: 420px) {
column-count: 3;
}
@media(min-width: 680px) {
column-count: 4;
}
@media(min-width: 1024px) {
column-count: 5;
}
@media(min-width: 1280px) {
column-count: 7;
}
.field {
width: 320px;
max-width: 100%;
}
.modal {
padding: 1rem 0 !important;
@media(max-width: 600px) {
padding: 0 !important;
}
}
.toast-header {
display: none;
.spinner-centered {
position: absolute;
left: calc(50% - 16px);
}

View file

@ -10030,6 +10030,11 @@ vue-style-loader@^4.1.0, vue-style-loader@^4.1.3:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
vue-swatches@^2.1.1:
version "2.1.1"
resolved "https://registry.yarnpkg.com/vue-swatches/-/vue-swatches-2.1.1.tgz#26c467fb7648ff4ee0887aea36d1e03b15032b83"
integrity sha512-YugkNbByxMz1dnx1nZyHSL3VSf/TnBH3/NQD+t8JKxPSqUmX87sVGBxjEaqH5IMraOLfVmU0pHCHl2BfXNypQg==
vue-template-compiler@^2.6.14:
version "2.6.14"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.6.14.tgz#a2f0e7d985670d42c9c9ee0d044fed7690f4f763"