UI updates

This commit is contained in:
gamebrary 2024-09-12 15:58:11 -07:00
parent 2ba26242aa
commit 74f484ec27
30 changed files with 873 additions and 391 deletions

View file

@ -189,7 +189,7 @@ export default {
mutation: 'SET_PLATFORMS',
});
} catch (e) {
this.$bvToast.toast('There was an error loading platforms', { variant: 'error' });
this.$bvToast.toast('There was an error loading platforms');
}
},

View file

@ -122,8 +122,9 @@ export default {
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
this.$bvToast.toast(`There was an error removing "${this.game.name}"`);
} catch (e) {
// this.$bvToast.toast(`There was an error removing "${this.game.name}"`, { title: list.name, variant: 'danger' });
this.$bvToast.toast(`There was an error removing "${this.game.name}"`);
}
},
},

View file

@ -0,0 +1,276 @@
<template lang="html">
<div class="standard-board pb-5">
GRID!@@
<!-- TODO: clean this up, remove all isGrid references -->
<!-- TODO: update isGrid to use board?.grid -->
<div class="standard-list" :class="{ 'grid': isGrid }">
<p v-if="isEmpty">
This board is empty.
</p>
<draggable
class="games"
:class="{ 'game-grid': isGrid }"
handle=".card"
ghost-class="card-placeholder"
drag-class="border-success"
chosen-class="border-primary"
filter=".drag-filter"
delay="50"
animation="500"
:list="list.games"
:move="validateMove"
:disabled="draggingDisabled"
:group="{ name: 'games' }"
@end="dragEnd"
@start="dragStart"
>
<GameCard
v-for="(game, index) in listGames"
:key="index"
:list="list"
:ref="game.id"
:game-id="game.id"
:ranked="board.ranked"
:rank="index + 1"
:vertical="isGrid"
:hide-platforms="isGrid"
:class="isGrid ? null: 'mb-3'"
@click.native="openGame(game.id, list)"
/>
<template v-if="isBoardOwner">
<b-card
v-if="isGrid"
body-class="align-content-center text-center"
:bg-variant="darkTheme ? 'dark' : 'light'"
:text-variant="darkTheme ? 'light' : 'dark'"
@click="openGameSelectorSidebar"
>
Expand your collection!
<b-button
class="mt-2"
:variant="darkTheme ? 'success' : 'primary'"
>
Add games
</b-button>
</b-card>
<b-button
v-else
class="py-3"
block
:variant="darkTheme ? 'success' : 'primary'"
@click="openGameSelectorSidebar"
>
Add games
</b-button>
</template>
</draggable>
</div>
</div>
</template>
<script>
import { HIGHLIGHTED_GAME_TIMEOUT, BOARD_TYPE_GRID } from '@/constants';
import draggable from 'vuedraggable';
import slugify from 'slugify'
import { mapState, mapGetters } from 'vuex';
import GameCard from '@/components/GameCard';
export default {
components: {
draggable,
GameCard,
},
computed: {
...mapState(['cachedGames', 'dragging', 'progresses', 'board', 'user', 'settings', 'highlightedGame']),
...mapGetters(['isBoardOwner', 'darkTheme']),
list() {
const [firstList] = this.board?.lists;
return firstList || [];
},
needsFlattening() {
return this.board?.lists?.length > 0;
},
isGrid() {
return this.board?.type === BOARD_TYPE_GRID;
},
filter() {
return this.listGames || [];
},
draggingDisabled() {
return !this.user || !this.isBoardOwner;
},
listGames() {
return this.list?.games
?.map((id) => this.cachedGames?.[id])
?.filter(({ id }) => Boolean(id));
},
isEmpty() {
return this.listGames.length === 0;
},
gameSelectorEventName() {
return `SELECT_GAME_LIST_${this.listIndex}`;
},
},
mounted() {
if (this.needsFlattening) this.flattenAndSaveBoard();
if (this.highlightedGame) this.highlightGame();
this.$bus.$on(this.gameSelectorEventName, this.selectGame);
},
destroyed() {
this.$bus.$off(this.gameSelectorEventName, this.selectGame);
},
methods: {
// TODO: update this to work
highlightGame() {
const lists = Object.values(this.$refs);
console.log('lists', lists)
lists.forEach(([list], index) => {
console.log('list.$refs', list.$refs);
const [gameRef] = list.$refs[this.highlightedGame];
console.log()
if (gameRef) {
console.log('gameRef', gameRef);
setTimeout(() => {
gameRef?.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, index * 1000);
}
});
// setTimeout(() => {
// this.$store.commit('SET_HIGHLIGHTED_GAME', null);
// }, HIGHLIGHTED_GAME_TIMEOUT);
},
async flattenAndSaveBoard() {
const mergedGamesList = [...new Set(this.board?.lists?.map(({ games }) => games)?.flat())];
const payload = {
...this.board,
lastUpdated: Date.now(),
lists: [{ name: '', games: mergedGamesList }],
}
this.$store.commit('SET_GAME_BOARD', payload);
await this.$store.dispatch('SAVE_BOARD');
},
openGameSelectorSidebar() {
this.$store.commit('SET_GAME_SELECTOR_DATA', {
title: `Add games to ${this.board.name}`,
filter: this.filter,
eventName: this.gameSelectorEventName,
})
},
selectGame(gameId) {
if (this.list.games.includes(gameId)) {
this.removeGame(gameId);
} else {
this.addGame(gameId);
}
},
async addGame(gameId) {
const board = JSON.parse(JSON.stringify(this.board));
board?.lists?.[0]?.games.push(gameId);
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board?.id);
await this.$store.dispatch('LOAD_IGDB_GAMES', [gameId]);
} catch (e) {
// this.$bvToast.toast(`There was an error adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
async removeGame(gameId) {
const { boardId, listIndex } = this.$route?.query;
const boardIndex = this.boards.findIndex(({ id }) => id === boardId);
const board = this.boards[boardIndex];
const gameIndex = board?.lists?.[listIndex]?.games?.indexOf(gameId);
board.lists[listIndex].games.splice(gameIndex, 1);
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board?.id)
} catch (e) {
// this.$bvToast.toast(`There was an error removing "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
openGame(id, list) {
const slug = slugify(this.cachedGames[id].slug, { lower: true });
this.$router.push({
name: 'game',
params: {
id,
slug,
boardId: this.board?.id,
},
});
},
validateMove({ from, to }) {
const sameList = from.id === to.id;
const notInList = !this.board?.lists?.[to.id]?.games?.includes(Number(this.draggingId));
return sameList || notInList && !sameList;
},
dragStart({ item }) {
this.$store.commit('SET_DRAGGING_STATUS', true);
this.draggingId = item.id;
},
dragEnd() {
this.$store.commit('SET_DRAGGING_STATUS', false);
this.saveBoard();
},
async saveBoard() {
await this.$store.dispatch('SAVE_BOARD')
.catch(() => {
this.$store.commit('SET_SESSION_EXPIRED', true);
});
},
}
};
</script>
<style lang="scss" scoped>
.standard-board {
display: flex;
justify-content: center;
align-items: center;
overflow-y: auto;
flex-direction: column;
}
</style>

View file

@ -43,10 +43,11 @@
</template>
<script>
import { BOARD_TYPE_STANDARD, BOARD_TYPE_TIER, IMAGE_SIZE_THUMB } from '@/constants';
import { BOARD_TYPE_STANDARD, BOARD_TYPE_TIER, BOARD_TYPE_GRID, IMAGE_SIZE_THUMB } from '@/constants';
import { mapGetters, mapState } from 'vuex';
import { getImageUrl } from '@/utils';
import StandardMiniBoard from '@/components/MiniBoards/StandardMiniBoard';
import GridMiniBoard from '@/components/MiniBoards/GridMiniBoard';
import KanbanMiniBoard from '@/components/MiniBoards/KanbanMiniBoard';
import TierMiniBoard from '@/components/MiniBoards/TierMiniBoard';
@ -59,6 +60,7 @@ export default {
components: {
StandardMiniBoard,
GridMiniBoard,
KanbanMiniBoard,
TierMiniBoard,
},
@ -82,6 +84,7 @@ export default {
miniBoardComponent() {
if (this.board?.type === BOARD_TYPE_TIER) return 'TierMiniBoard';
if (this.board?.type === BOARD_TYPE_STANDARD) return 'StandardMiniBoard';
if (this.board?.type === BOARD_TYPE_GRID) return 'GridMiniBoard';
return 'KanbanMiniBoard';
},

View file

@ -1,26 +1,66 @@
<template lang="html">
<div class="standard-board pb-5">
<StandardList
v-for="(list, listIndex) in board.lists"
:ref="`list-${listIndex}`"
:key="list.id"
:listIndex="listIndex"
:list="list"
/>
<div class="standard-board pb-5 d-flex align-items-center flex-column">
<p v-if="isEmpty">
This board is empty.
</p>
<draggable
class="games"
handle=".game-card"
ghost-class="card-placeholder"
drag-class="border-success"
chosen-class="border-primary"
filter=".drag-filter"
delay="50"
animation="500"
:list="list.games"
:move="validateMove"
:disabled="draggingDisabled"
:group="{ name: 'games' }"
@end="dragEnd"
@start="dragStart"
>
<GameCard
v-for="(gameId, index) in listGames"
:key="index"
:list="list"
:ref="gameId"
:game-id="gameId"
:ranked="board.ranked"
:rank="index + 1"
class="mb-3"
@click.native="openGame(gameId, list)"
/>
<b-button
v-if="isBoardOwner"
class="py-3"
block
:variant="darkTheme ? 'success' : 'primary'"
@click="openGameSelectorSidebar"
>
Add games
</b-button>
</draggable>
</div>
</template>
<script>
import { mapState } from 'vuex';
import StandardList from '@/components/Lists/StandardList';
import { HIGHLIGHTED_GAME_TIMEOUT } from '@/constants';
import draggable from 'vuedraggable';
import slugify from 'slugify'
import { mapState, mapGetters } from 'vuex';
import GameCard from '@/components/GameCard';
export default {
components: {
StandardList,
draggable,
GameCard,
},
computed: {
...mapState(['board', 'highlightedGame']),
...mapState(['cachedGames', 'dragging', 'progresses', 'board', 'user', 'settings', 'highlightedGame']),
...mapGetters(['isBoardOwner', 'darkTheme']),
list() {
const [firstList] = this.board?.lists;
@ -29,34 +69,59 @@ export default {
},
needsFlattening() {
return this.board?.lists?.length && this.board.type === 'standard';
return this.board?.lists?.length > 0;
},
hasLists() {
return this.board?.lists?.length > 0;
filter() {
return this.listGames || [];
},
draggingDisabled() {
return !this.user || !this.isBoardOwner;
},
listGames() {
return this.list?.games;
},
isEmpty() {
return this.listGames.length === 0;
},
gameSelectorEventName() {
return `SELECT_GAME_LIST_${this.listIndex}`;
},
},
mounted() {
if (this.needsFlattening) this.flattenAndSaveBoard();
if (this.highlightedGame) this.highlightGame();
this.$bus.$on(this.gameSelectorEventName, this.selectGame);
},
destroyed() {
this.$bus.$off(this.gameSelectorEventName, this.selectGame);
},
methods: {
highlightGame() {
const lists = Object.values(this.$refs);
// TODO: update this to work
// const lists = Object.values(this.$refs);
lists.forEach(([list], index) => {
const [gameRef] = list.$refs[`${index}-${this.highlightedGame}`];
// lists.forEach(([list], index) => {
// console.log('list.$refs', list.$refs);
if (gameRef) {
console.log('gameRef', gameRef);
// const [gameRef] = list.$refs[this.highlightedGame];
setTimeout(() => {
gameRef?.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
}, index * 1000);
}
});
// if (gameRef) {
// console.log('gameRef', gameRef);
// setTimeout(() => {
// gameRef?.$el.scrollIntoView({ behavior: 'smooth', block: 'center' });
// }, index * 1000);
// }
// });
setTimeout(() => {
this.$store.commit('SET_HIGHLIGHTED_GAME', null);
@ -77,19 +142,88 @@ export default {
await this.$store.dispatch('SAVE_BOARD');
},
flattenBoard() {
this.$store.commit('FLATTEN_BOARD');
openGameSelectorSidebar() {
this.$store.commit('SET_GAME_SELECTOR_DATA', {
title: `Add games to ${this.board.name}`,
filter: this.filter,
eventName: this.gameSelectorEventName,
})
},
selectGame(gameId) {
if (this.list.games.includes(gameId)) {
this.removeGame(gameId);
} else {
this.addGame(gameId);
}
},
async addGame(gameId) {
const board = JSON.parse(JSON.stringify(this.board));
board?.lists?.[0]?.games.push(gameId);
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board?.id);
await this.$store.dispatch('LOAD_IGDB_GAMES', [gameId]);
} catch (e) {
// this.$bvToast.toast(`There was an error adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
async removeGame(gameId) {
const { boardId, listIndex } = this.$route?.query;
const boardIndex = this.boards.findIndex(({ id }) => id === boardId);
const board = this.boards[boardIndex];
const gameIndex = board?.lists?.[listIndex]?.games?.indexOf(gameId);
board.lists[listIndex].games.splice(gameIndex, 1);
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board?.id)
} catch (e) {
// this.$bvToast.toast(`There was an error removing "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
openGame(id, list) {
const slug = slugify(this.cachedGames[id].slug, { lower: true });
this.$router.push({
name: 'game',
params: {
id,
slug,
boardId: this.board?.id,
},
});
},
validateMove({ from, to }) {
const sameList = from.id === to.id;
const notInList = !this.board?.lists?.[to.id]?.games?.includes(Number(this.draggingId));
return sameList || notInList && !sameList;
},
dragStart({ item }) {
this.$store.commit('SET_DRAGGING_STATUS', true);
this.draggingId = item.id;
},
dragEnd() {
this.$store.commit('SET_DRAGGING_STATUS', false);
this.saveBoard();
},
async saveBoard() {
await this.$store.dispatch('SAVE_BOARD')
.catch(() => {
this.$store.commit('SET_SESSION_EXPIRED', true);
});
},
}
};
</script>
<style lang="scss" scoped>
.standard-board {
display: flex;
justify-content: center;
align-items: center;
overflow-y: auto;
flex-direction: column;
}
</style>

View file

@ -0,0 +1,119 @@
<template lang="html">
<b-sidebar
id="clone-board-sidebar"
v-bind="sidebarRightProps"
@shown="setBoardName"
>
<template #default="{ hide }">
<SidebarHeader @hide="hide" title="Clone board" />
<form @submit.prevent="cloneBoard" class="px-3">
<b-form-group label="Board name:" label-for="boardName">
<b-form-input
id="boardName"
v-model.trim="boardName"
autofocus
required
/>
</b-form-group>
<MiniBoard
class="mb-2"
:board="board"
no-link
/>
<b-button
:variant="darkTheme ? 'secondary' : 'primary'"
:disabled="saving"
type="submit"
>
<b-spinner small v-if="saving" />
<span v-else>Clone board</span>
</b-button>
</form>
</template>
</b-sidebar>
</template>
<script>
import SidebarHeader from '@/components/SidebarHeader';
import MiniBoard from '@/components/Board/MiniBoard';
import { mapState, mapGetters } from 'vuex';
import { BOARD_TYPE_KANBAN } from '@/constants';
export default {
data() {
return {
boardName: '',
saving: false,
}
},
components: {
SidebarHeader,
MiniBoard,
},
computed: {
...mapState(['board', 'user']),
...mapGetters(['sidebarRightProps', 'darkTheme']),
payload() {
const dateCreated = Date.now();
const isBoardOwner = this.board.owner === this.user.uid;
const backgroundUrl = isBoardOwner
? this.board.backgroundUrl
: null;
console.log('isBoardOwner', isBoardOwner);
// TODO: clone wallpaper when cloning board?
return {
type: this.board.type || BOARD_TYPE_KANBAN,
lists: this.board.lists,
ranked: this.board.ranked || false,
backgroundUrl: backgroundUrl || null,
backgroundColor: this.board?.backgroundColor || null,
lastUpdated: this.board.lastUpdated || dateCreated,
darkTheme: this.board.darkTheme || false,
dateCreated: this.board.dateCreated || dateCreated,
originalOwnerId: this.board.owner,
isPublic: false,
dateCreated,
lastUpdated: dateCreated,
originalBoardId: this.board.id,
originalDateCreated: this.board.dateCreated || dateCreated,
owner: this.user.uid,
name: this.boardName,
}
},
},
methods: {
setBoardName() {
this.boardName = this.board.name || '';
},
async cloneBoard() {
try {
this.saving = true;
console.log(this.payload);
debugger;
const { id } = await this.$store.dispatch('CREATE_BOARD', this.payload);
this.saving = false;
this.$router.push({ name: 'board', params: { id } });
} catch (e) {
console.log(e);
}
},
},
};
</script>

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
id="create-board-sidebar"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
@hidden="saving = false"
>
<template #default="{ hide }">
@ -112,7 +111,7 @@ export default {
computed: {
...mapState(['user']),
...mapGetters(['sidebarProps']),
...mapGetters(['sidebarRightProps']),
sampleBoard() {
if (this.board.type === BOARD_TYPE_KANBAN) return DEFAULT_BOARD_KANBAN;
@ -153,6 +152,8 @@ export default {
owner: this.user.uid,
}
console.log('payload', payload)
const { id } = await this.$store.dispatch('CREATE_BOARD', payload);
this.$router.push({ name: 'board', params: { id } });
@ -163,3 +164,19 @@ export default {
},
};
</script>
<style lang="scss" rel="stylesheet/scss">
.game-cover {
width: 120px;
}
.standard-list {
width: 100%;
max-width: 600px;
overflow-x: hidden;
&.grid {
max-width: 1280px;
}
}
</style>

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
id="create-tag-sidebar"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
@hidden="saving = false"
>
<template #default="{ hide }">
@ -75,7 +74,7 @@ export default {
computed: {
...mapState(['tags']),
...mapGetters(['sidebarProps', 'swatchesProps', 'darkTheme']),
...mapGetters(['sidebarRightProps', 'swatchesProps', 'darkTheme']),
},
methods: {

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
id="edit-board-sidebar"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
>
<template #default="{ hide }">
<SidebarHeader @hide="hide" title="Edit board" />
@ -10,8 +9,7 @@
<form @submit.stop.prevent="saveBoard" class="p-3">
<b-sidebar
id="select-board-wallpaper"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
>
<template #default="{ hide }">
<SidebarHeader @hide="hide" title="Select board background" />
@ -65,15 +63,6 @@
Ranked
</b-form-checkbox>
<b-form-checkbox
v-if="board.type === $options.BOARD_TYPE_STANDARD"
v-model="board.grid"
class="mb-3"
switch
>
Grid
</b-form-checkbox>
<b-form-checkbox
v-model="board.darkTheme"
class="mb-3"
@ -185,7 +174,7 @@ export default {
computed: {
...mapState(['user']),
...mapGetters(['darkTheme', 'sidebarProps', 'swatchesProps']),
...mapGetters(['darkTheme', 'sidebarRightProps', 'swatchesProps']),
boardId() {
return this.$route?.params?.id;

View file

@ -2,8 +2,7 @@
<b-sidebar
id="profile-sidebar"
:visible="editProfileSidebarOpen"
right
v-bind="sidebarProps"
v-bind="sidebarRightProps"
@shown="loadProfile"
@hidden="$store.commit('SET_PROFILE_SIDEBAR_OPEN', false)"
>
@ -198,7 +197,7 @@
<b-sidebar
id="boardWallpaper"
v-bind="sidebarProps"
v-bind="sidebarRightProps"
right
>
<template #default="{ hide }">
@ -271,7 +270,7 @@ export default {
computed: {
...mapState(['user', 'editProfileSidebarOpen']),
...mapGetters(['sidebarProps', 'darkTheme']),
...mapGetters(['sidebarRightProps', 'darkTheme']),
style() {
return this.wallpaperImage

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
:visible="activeTagIndex !== null"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
@shown="load"
@hidden="closeSidebar"
>
@ -110,7 +109,7 @@ export default {
computed: {
...mapState(['tags', 'cachedGames', 'activeTagIndex']),
...mapGetters(['sidebarProps', 'swatchesProps', 'darkTheme']),
...mapGetters(['sidebarRightProps', 'swatchesProps', 'darkTheme']),
isEmpty() {
return this.tag?.games?.length === 0;

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
id="gameTagsSidebar"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
>
<template #default="{ hide }">
<SidebarHeader @hide="hide" :title="sidebarTitle" />
@ -68,7 +67,7 @@ export default {
computed: {
...mapState(['tags', 'game']),
...mapGetters(['sidebarProps']),
...mapGetters(['sidebarRightProps']),
isEmpty() {
return this.tags.length === 0 || !this.game;

View file

@ -65,11 +65,12 @@
<small v-else>{{ gameProgress }}%</small>
</b-badge>
<h2
v-if="!hideTitle || hideCover"
:class="['text-wrap',
{
'text-success' : gameCompleted, 'mb-1': !board.grid,
'text-success' : gameCompleted, 'mb-1': board.type !== $options.BOARD_TYPE_GRID,
'mt-2': vertical,
}
]"
@ -130,12 +131,13 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { getImageUrl } from '@/utils';
import { IMAGE_SIZE_COVER_SMALL, PLATFORMS } from '@/constants';
import { IMAGE_SIZE_COVER_SMALL, PLATFORMS, BOARD_TYPE_GRID } from '@/constants';
import GameRibbon from '@/components/GameRibbon';
import slugify from 'slugify';
export default {
IMAGE_SIZE_COVER_SMALL,
BOARD_TYPE_GRID,
getImageUrl,
props: {

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
:visible="visible"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
@hidden="closeSidebar"
>
<template #default="{ hide }">
@ -77,7 +76,7 @@ export default {
computed: {
...mapState(['board', 'gameSelectorData']),
...mapGetters(['isBoardOwner', 'sidebarProps', 'darkTheme']),
...mapGetters(['isBoardOwner', 'sidebarRightProps', 'darkTheme']),
title() {
return this.gameSelectorData?.title || 'Select a game';

View file

@ -1,7 +1,7 @@
<template lang="html">
<b-sidebar
id="keyboard-shortcuts-sidebar"
v-bind="sidebarProps"
v-bind="sidebarLeftProps"
z-index="2001"
>
<template #default="{ hide }">
@ -57,7 +57,7 @@ export default {
computed: {
...mapState(['user']),
...mapGetters(['darkTheme', 'sidebarProps']),
...mapGetters(['darkTheme', 'sidebarLeftProps']),
},
mounted() {

View file

@ -1,8 +1,7 @@
<template>
<b-sidebar
id="edit-list-modal"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
:visible="activeBoardListIndex !== null"
@shown="openEditListSidebar"
@hidden="closeSidebar"
@ -193,7 +192,7 @@ export default {
computed: {
...mapState(['board', 'activeBoardListIndex']),
...mapGetters(['darkTheme', 'sidebarProps', 'swatchesProps']),
...mapGetters(['darkTheme', 'sidebarRightProps', 'swatchesProps']),
},
methods: {

View file

@ -1,240 +0,0 @@
<template lang="html">
<div class="standard-list" :class="{ 'grid': isGrid }" >
<p v-if="isEmpty">
This board is empty.
</p>
<draggable
class="games"
:class="{ 'game-grid': isGrid }"
handle=".card"
ghost-class="card-placeholder"
drag-class="border-success"
chosen-class="border-primary"
filter=".drag-filter"
delay="50"
animation="500"
:list="list.games"
:move="validateMove"
:disabled="draggingDisabled"
:group="{ name: 'games' }"
@end="dragEnd"
@start="dragStart"
>
<GameCard
v-for="(game, index) in listGames"
:key="index"
:list="list"
:ref="`${listIndex}-${game.id}`"
:game-id="game.id"
:ranked="board.ranked"
:rank="index + 1"
:vertical="isGrid"
:hide-platforms="isGrid"
:class="isGrid ? null: 'mb-3'"
@click.native="openGame(game.id, list)"
/>
<template v-if="isBoardOwner">
<b-card
v-if="isGrid"
body-class="align-content-center text-center"
:bg-variant="darkTheme ? 'dark' : 'light'"
:text-variant="darkTheme ? 'light' : 'dark'"
@click="openGameSelectorSidebar"
>
Expand your collection!
<b-button
class="mt-2"
:variant="darkTheme ? 'success' : 'primary'"
>
Add games
</b-button>
</b-card>
<b-button
v-else
class="py-3"
block
:variant="darkTheme ? 'success' : 'primary'"
@click="openGameSelectorSidebar"
>
Add games
</b-button>
</template>
</draggable>
</div>
</template>
<script>
import draggable from 'vuedraggable';
import slugify from 'slugify'
import { mapState, mapGetters } from 'vuex';
import GameCard from '@/components/GameCard';
export default {
components: {
draggable,
GameCard,
},
props: {
listIndex: Number,
list: {
type: Object,
default: () => {},
},
},
data() {
return {
draggingId: null,
editing: false,
};
},
mounted() {
this.$bus.$on(this.gameSelectorEventName, this.selectGame);
},
destroyed() {
this.$bus.$off(this.gameSelectorEventName, this.selectGame);
},
computed: {
...mapState(['cachedGames', 'dragging', 'progresses', 'board', 'user', 'settings']),
...mapGetters(['isBoardOwner', 'darkTheme']),
isGrid() {
return this.board?.grid;
},
filter() {
return this.list?.games || [];
},
draggingDisabled() {
return !this.user || !this.isBoardOwner;
},
autoSortEnabled() {
return ['sortByName', 'sortByRating', 'sortByReleaseDate', 'sortByProgress'].includes(this.list?.sortOrder);
},
listGames() {
return this.list?.games?.map((id) => this.cachedGames?.[id]) || []
.filter(({ id }) => Boolean(id));
},
isEmpty() {
return this.list.games.length === 0;
},
gameSelectorEventName() {
return `SELECT_GAME_LIST_${this.listIndex}`;
},
},
methods: {
openGameSelectorSidebar() {
this.$store.commit('SET_GAME_SELECTOR_DATA', {
title: `Add games to ${this.board.name}`,
filter: this.filter,
eventName: this.gameSelectorEventName,
})
},
selectGame(gameId) {
if (this.list.games.includes(gameId)) {
this.removeGame(gameId);
} else {
this.addGame(gameId);
}
},
async addGame(gameId) {
const board = JSON.parse(JSON.stringify(this.board));
board?.lists?.[0]?.games.push(gameId);
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board?.id);
await this.$store.dispatch('LOAD_IGDB_GAMES', [gameId]);
} catch (e) {
// this.$bvToast.toast(`There was an error adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
async removeGame(gameId) {
const { boardId, listIndex } = this.$route?.query;
const boardIndex = this.boards.findIndex(({ id }) => id === boardId);
const board = this.boards[boardIndex];
const gameIndex = board?.lists?.[listIndex]?.games?.indexOf(gameId);
board.lists[listIndex].games.splice(gameIndex, 1);
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board?.id)
} catch (e) {
// this.$bvToast.toast(`There was an error removing "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
openGame(id, list) {
const slug = slugify(this.cachedGames[id].slug, { lower: true });
this.$router.push({
name: 'game',
params: {
id,
slug,
boardId: this.board?.id,
},
});
},
validateMove({ from, to }) {
const sameList = from.id === to.id;
const notInList = !this.board?.lists?.[to.id]?.games?.includes(Number(this.draggingId));
return sameList || notInList && !sameList;
},
dragStart({ item }) {
this.$store.commit('SET_DRAGGING_STATUS', true);
this.draggingId = item.id;
},
dragEnd() {
this.$store.commit('SET_DRAGGING_STATUS', false);
this.saveBoard();
},
async saveBoard() {
await this.$store.dispatch('SAVE_BOARD')
.catch(() => {
this.$store.commit('SET_SESSION_EXPIRED', true);
});
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss">
.game-cover {
width: 120px;
}
.standard-list {
width: 100%;
max-width: 600px;
overflow-x: hidden;
&.grid {
max-width: 1280px;
}
}
</style>

View file

@ -1,8 +1,8 @@
<template lang="html">
<template lang="html">
<b-sidebar
id="mainMenu"
:visible="menuOpen"
v-bind="sidebarProps"
v-bind="sidebarLeftProps"
@hidden="hideSidebar"
>
<template #default="{ hide }">
@ -22,58 +22,84 @@
<div class="p-3">
<b-button
:variant="routeName === 'boards' ? 'primary' : darkTheme ? 'dark' : 'light'"
:variant="routeName === 'boards' ? activeVariant : variant"
block
class="text-left align-items-center d-flex"
:to="{ name: 'boards' }"
>
<i class="fa-regular fa-rectangle-list" />
<span class="ml-2">Boards</span>
<b-badge v-if="boards.length" class="ml-auto" variant="light">
{{ boards.length }}
</b-badge>
</b-button>
<b-button
:variant="routeName === 'games' ? 'primary' : darkTheme ? 'dark' : 'light'"
:variant="routeName === 'games' ? activeVariant : variant"
:to="{ name: 'games' }"
block
class="text-left align-items-center d-flex"
>
<i class="fa-regular fa-gamepad fa-fw" />
Games
<i class="fa-regular fa-gamepad fa-fw" />
<span class="ml-2">Games</span>
<b-badge v-if="gameCount" class="ml-auto" variant="light">
{{ gameCount }}
</b-badge>
</b-button>
<b-button
block
:variant="routeName === 'tags' ? 'primary' : darkTheme ? 'dark' : 'light'"
:to="{ name: 'tags' }"
:variant="routeName === 'tags' ? activeVariant : variant"
class="text-left align-items-center d-flex"
block
>
<i class="fa-light fa-tags fa-fw" />
<span class="ml-2">Tags</span>
<b-badge v-if="tags.length" class="ml-auto" variant="light">
{{ tags.length }}
</b-badge>
</b-button>
<b-button
:to="{ name: 'notes' }"
:variant="routeName === 'notes' ? 'primary' : darkTheme ? 'dark' : 'light'"
:variant="routeName === 'notes' ? activeVariant : variant"
class="text-left align-items-center d-flex"
block
>
<i class="fa-regular fa-notes"></i>
<i class="fa-regular fa-notes fa-fw" />
<span class="ml-2">Notes</span>
<b-badge v-if="notesCount" class="ml-auto" variant="light">
{{ notesCount }}
</b-badge>
</b-button>
<b-button
:variant="routeName === 'wallpapers' ? 'primary' : darkTheme ? 'dark' : 'light'"
:variant="routeName === 'wallpapers' ? activeVariant : variant"
:to="{ name: 'wallpapers' }"
class="text-left align-items-center d-flex"
block
>
<i class="fa-solid fa-images"></i>
<i class="fa-solid fa-images fa-fw" />
<span class="ml-2">Wallpapers</span>
<b-badge v-if="wallpaperCount" class="ml-auto" variant="light">
{{ wallpaperCount }}
</b-badge>
</b-button>
<b-button
block
:variant="routeName === 'settings' ? 'primary' : darkTheme ? 'dark' : 'light'"
class="text-left"
:variant="routeName === 'settings' ? activeVariant : variant"
v-b-toggle.settingsSidebar
>
<i class="fa-regular fa-gear fa-fw" />
Settings
<span class="ml-2">Settings</span>
</b-button>
</div>
</template>
@ -86,6 +112,7 @@
<script>
import { mapState, mapGetters } from 'vuex';
import { THUMBNAIL_PREFIX } from '@/constants';
import ProfileDockMenu from '@/components/Dock/ProfileDockMenu';
import SidebarHeader from '@/components/SidebarHeader';
import MainSidebarFooter from '@/components/MainSidebarFooter';
@ -101,11 +128,33 @@ export default {
computed: {
...mapState(['user', 'board', 'boards', 'settings', 'user', 'games', 'notes', 'tags', 'wallpapers', 'menuOpen']),
...mapGetters(['navPosition', 'latestRelease', 'darkTheme', 'transparencyEnabled', 'sidebarProps']),
...mapGetters(['navPosition', 'latestRelease', 'darkTheme', 'transparencyEnabled', 'sidebarLeftProps']),
routeName() {
return this.$route?.name;
},
variant() {
return this.darkTheme ? 'dark' : 'light';
},
activeVariant() {
return this.darkTheme ? 'success' : 'primary';
},
gameCount() {
return Object.keys(this.games).length;
},
notesCount() {
return Object.keys(this.notes).length;
},
wallpaperCount() {
const wallpapers = this.wallpapers?.filter((wallpaper) => !wallpaper?.fullPath?.includes(THUMBNAIL_PREFIX));
return wallpapers.length;
},
},
methods: {

View file

@ -1,13 +1,6 @@
<template>
<div class="p-3">
<div class="d-flex justify-content-end">
<!-- <b-button disabled>
<i class="fa-solid fa-language" />
<span class="ml-2">Change language</span>
</b-button> -->
</div>
<div class="mt-1 text-center d-flex justify-content-between small">
<div class="mt-1 text-center d-flex justify-content-between">
<div class="d-flex justify-content-between align-items-center">
<img
src="/logo.png"
@ -26,21 +19,17 @@
target="_blank"
title="GitHub"
v-b-tooltip.hover
size="sm"
class="ml-1"
>
<i class="fa-brands fa-github fa-fw" />
<i class="fa-brands fa-github" />
</b-button>
<b-button
v-b-toggle.keyboard-shortcuts-sidebar
:variant="darkTheme ? 'dark' : 'light'"
title="Keyboard Shortcuts"
size="sm"
v-b-tooltip.hover
class="ml-1"
>
<i class="fa-solid fa-command fa-fw" />
<i class="fa-solid fa-command" />
</b-button>
<b-button
@ -48,23 +37,19 @@
id="help"
title="Help"
:variant="darkTheme ? 'dark' : 'light'"
size="sm"
v-b-tooltip.hover
class="ml-1"
>
<i class="fa-regular fa-circle-info fa-fw" />
<i class="fa-regular fa-circle-info" />
</b-button>
<b-button
@click="toggleTheme"
:variant="darkTheme ? 'dark' : 'light'"
size="sm"
v-b-tooltip.hover
class="ml-1"
title="Toggle theme"
>
<i v-if="darkTheme" class="fa-solid fa-sun fa-fw" />
<i v-else class="fa-solid fa-moon fa-fw" />
<i v-if="darkTheme" class="fa-solid fa-sun" />
<i v-else class="fa-solid fa-moon" />
</b-button>
</div>
</div>

View file

@ -0,0 +1,112 @@
<template>
<div v-if="isGrid" class="grid">
<b-avatar
v-for="(game, index) in firstList.games"
v-b-tooltip.hover
:key="index"
:style="`border-radius: 4px !important;`"
:variant="darkTheme ? 'black' : 'light'"
:title="game.name"
:src="showGameThumbnails ? game.src : null"
text=" "
/>
</div>
<div
v-else
class="board d-flex rounded overflow-hidden justify-content-center"
>
<b-card
body-class="p-0"
:bg-variant="darkTheme ? 'black' : 'transparent'"
:text-variant="darkTheme ? 'light' : 'dark'"
style="width: 80px"
class="overflow-hidden align-self-start"
>
<template v-if="firstList.games.length">
<div
v-for="(game, index) in firstList.games"
:key="index"
:class="[
currentGameId === game.id
? 'border bg-danger border-danger'
: darkTheme
? 'border-black bg-dark'
: 'border-light bg-white',
{ 'border-bottom': index !== firstList.games.length - 1 },
]"
class=""
>
<b-avatar
:style="`border-radius: 4px !important;`"
text=" "
:variant="darkTheme ? 'black' : 'light'"
class="m-1"
v-b-tooltip.hover
:title="game.name"
:src="showGameThumbnails ? game.src : null"
size="20"
/>
<small v-if="board.ranked">{{ index + 1 }}</small>
</div>
</template>
<div
v-else
class="rounded overflow-hidden"
style="height: 22px; width: 60px;"
/>
</b-card>
</div>
</template>
<script>
import { mapGetters, mapState } from 'vuex';
export default {
props: {
board: {
type: Object,
required: true,
},
},
computed: {
...mapGetters(['darkTheme', 'showGameThumbnails']),
...mapState(['routeName', 'game']),
currentGameId() {
return this.routeName === 'game'
? this.game?.id
: null;
},
isGrid() {
return this.board?.grid;
},
firstList() {
return this.board?.lists?.[0] || {};
},
},
};
</script>
<style scoped>
.grid {
grid-gap: .5rem;
display: grid;
max-width: 296px;
padding: .5rem;
margin: 0 auto;
grid-template-columns: repeat(6, 1fr);
@media(max-width: 768px) {
justify-content: start;
grid-template-columns: repeat(3, 1fr);
width: 152px;
margin: 0;
}
}
</style>

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
id="filtersSidebar"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
>
<template #default="{ hide }">
<SidebarHeader @hide="hide" title="Filter search results" />
@ -151,7 +150,7 @@ export default {
computed: {
...mapState(['platforms']),
...mapGetters(['darkTheme', 'sidebarProps']),
...mapGetters(['darkTheme', 'sidebarRightProps']),
sortedPlatforms() {
return orderby(this.platforms, [platform => platform.name]);

View file

@ -1,7 +1,7 @@
<template lang="html">
<b-sidebar
id="settingsSidebar"
v-bind="sidebarProps"
v-bind="sidebarLeftProps"
z-index="2001"
>
<template #default="{ hide }">
@ -115,7 +115,7 @@ export default {
computed: {
...mapState(['settings']),
...mapGetters(['darkTheme', 'showGameThumbnails', 'transparencyEnabled', 'ageRating', 'navPosition', 'sidebarProps']),
...mapGetters(['darkTheme', 'showGameThumbnails', 'transparencyEnabled', 'ageRating', 'navPosition', 'sidebarLeftProps']),
ageRatingOptions() {
return AGE_RATINGS.map((rating) => ({

View file

@ -1,8 +1,7 @@
<template lang="html">
<b-sidebar
id="wallpaper-details-sidebar"
v-bind="sidebarProps"
right
v-bind="sidebarRightProps"
:visible="visible"
@hidden="closeSidebar"
>
@ -126,7 +125,7 @@ export default {
computed: {
...mapState(['boards', 'wallpapers', 'activeWallpaper']),
...mapGetters(['darkTheme', 'sidebarProps']),
...mapGetters(['darkTheme', 'sidebarRightProps']),
formattedBoards() {
return this.boards.map((board) => ({ ...board, backgroundUrl: this.getWallpaperUrl(board.backgroundUrl) }));

View file

@ -22,6 +22,7 @@ export const NO_IMAGE_PATH = '/placeholder.gif';
export const BOARD_TYPE_STANDARD = 'standard';
export const BOARD_TYPE_KANBAN = 'kanban';
export const BOARD_TYPE_TIER = 'tier';
export const BOARD_TYPE_GRID = 'grid';
export const SORT_TYPE_ALPHABETICALLY = 'alphabetically';
export const SORT_TYPE_RATING = 'rating';
@ -32,7 +33,8 @@ export const HIGHLIGHTED_GAME_TIMEOUT = 5000;
export const BOARD_TYPES = [
{ text: 'Kanban', value: BOARD_TYPE_KANBAN },
{ text: 'Standard', value: BOARD_TYPE_STANDARD },
{ text: 'List', value: BOARD_TYPE_STANDARD },
{ text: 'Grid', value: BOARD_TYPE_GRID },
{ text: 'Tier', value: BOARD_TYPE_TIER },
];
@ -47,8 +49,9 @@ export const DEFAULT_BOARD_BASE = {
name: '',
ranked: false,
isPublic: false,
grid: false,
darkTheme: false,
backgroundColor: null,
backgroundUrl: null,
}
export const DEFAULT_PROFILE = {

View file

@ -38,6 +38,9 @@ Vue.use(BootstrapVue, {
BButton: { variant: 'secondary' },
BAvatar: { variant: 'muted' },
BDropdown: { variant: 'primary' },
BToast: {
noCloseButton: true,
},
});
Vue.component('ModalHeader', ModalHeader);

View file

@ -3,6 +3,7 @@
<div v-else-if="hasAccess">
<EditListSidebar v-if="isBoardOwner && board.type !== $options.BOARD_TYPE_STANDARD" />
<CloneBoardSidebar v-if="user" />
<portal to="pageTitle">
<div class="d-flex flex-column">
@ -31,17 +32,28 @@
</portal>
<portal to="headerActions">
<b-button
v-if="canEdit"
:variant="darkTheme ? 'success' : 'primary'"
v-b-toggle.edit-board-sidebar
<b-dropdown
:variant="darkTheme ? 'black' : 'light'"
right
no-caret
>
<i class="fa-solid fa-pen" />
</b-button>
<template #button-content>
<i class="fa-solid fa-ellipsis-vertical px-1" />
</template>
<b-dropdown-item v-if="canEdit" v-b-toggle.edit-board-sidebar>
<i class="fa-solid fa-pen fa-fw" /> Edit board
</b-dropdown-item>
<b-dropdown-item v-b-toggle.clone-board-sidebar>
<i class="fa-regular fa-clone fa-fw" /> Clone board
</b-dropdown-item>
</b-dropdown>
</portal>
<StandardBoard v-if="board.type === $options.BOARD_TYPE_STANDARD" />
<TierBoard v-else-if="board.type === $options.BOARD_TYPE_TIER" />
<GridBoard v-else-if="board.type === $options.BOARD_TYPE_GRID" />
<KanbanBoard v-else />
</div>
@ -57,24 +69,34 @@
<script>
import BoardPlaceholder from '@/components/Board/BoardPlaceholder';
import KanbanBoard from '@/components/Board/KanbanBoard';
import GridBoard from '@/components/Board/GridBoard';
import TierBoard from '@/components/Board/TierBoard';
import StandardBoard from '@/components/Board/StandardBoard';
import EditListSidebar from '@/components/Lists/EditListSidebar';
import CloneBoardSidebar from '@/components/CloneBoardSidebar';
import chunk from 'lodash.chunk';
import { getImageThumbnail } from '@/utils';
import { BOARD_TYPE_STANDARD, BOARD_TYPE_TIER, MAX_QUERY_LIMIT } from '@/constants';
import {
BOARD_TYPE_STANDARD,
BOARD_TYPE_TIER,
BOARD_TYPE_GRID,
MAX_QUERY_LIMIT
} from '@/constants';
import { mapState, mapGetters } from 'vuex';
export default {
BOARD_TYPE_STANDARD,
BOARD_TYPE_TIER,
BOARD_TYPE_GRID,
components: {
BoardPlaceholder,
KanbanBoard,
TierBoard,
CloneBoardSidebar,
EditListSidebar,
GridBoard,
KanbanBoard,
StandardBoard,
TierBoard,
},
data() {

View file

@ -274,7 +274,6 @@
</article>
<aside>
<AmazonLink class="mr-3" />
<GameInBoards class="d-none d-lg-inline" />
<template v-if="highlightedAchievements">

View file

@ -32,14 +32,30 @@ export default {
return game?.websites?.map(({ url, category }) => ({ url, ...LINKS_CATEGORIES[category] })) || [];
},
sidebarProps(state, getters) {
sidebarRightProps(state, getters) {
return {
scrollable: true,
shadow: true,
backdrop: true,
'no-header': true,
'bg-variant': getters?.darkTheme ? 'dark' : 'light',
'text-variant': getters?.darkTheme ? 'light' : 'dark',
right: true,
noHeader: true,
bgVariant: getters?.darkTheme ? 'dark' : 'light',
textVariant: getters?.darkTheme ? 'light' : 'dark',
sidebarClass: ['rounded-left border border-right-0', getters?.darkTheme ? 'border-black' : 'border-white'],
bodyClass: 'rounded-left',
}
},
sidebarLeftProps(state, getters) {
return {
scrollable: true,
shadow: true,
backdrop: true,
noHeader: true,
bgVariant: getters?.darkTheme ? 'dark' : 'light',
textVariant: getters?.darkTheme ? 'light' : 'dark',
sidebarClass: ['rounded-right border border-left-0', getters?.darkTheme ? 'border-black' : 'border-white'],
bodyClass: 'rounded-right',
}
},

View file

@ -58,11 +58,11 @@ body.dark {
height: 10px;
width: 10px;
}
*::-webkit-scrollbar-track { background-color: var(--dark); }
*::-webkit-scrollbar-track:hover { background-color: var(--dark); }
*::-webkit-scrollbar-track:active { background-color: var(--dark); }
*::-webkit-scrollbar-thumb { background-color: var(--info); }
*::-webkit-scrollbar-thumb:hover { background-color: var(--info); }
*::-webkit-scrollbar-thumb:active { background-color: darken($info, 5%); }
}
}

View file

@ -854,15 +854,15 @@ $modal-scale-transform: scale(1.5) !default;
// Toasts
// $toast-max-width: 200px !default;
// $toast-padding-x: .5rem !default;
// $toast-padding-y: .5rem !default;
// $toast-font-size: $font-size-base !default;
// $toast-color: $white !default;
// $toast-background-color: $black;
// $toast-border-width: 0 !default;
// // $toast-border-color: rgba(0, 0, 0, .1) !default;
// $toast-border-radius: $border-radius !default;
// $toast-box-shadow: none !default;
$toast-padding-x: .5rem !default;
$toast-padding-y: .5rem !default;
$toast-font-size: $font-size-base !default;
$toast-color: $light !default;
$toast-background-color: $black;
$toast-border-width: 0 !default;
// $toast-border-color: rgba(0, 0, 0, .1) !default;
$toast-border-radius: $border-radius !default;
$toast-box-shadow: none !default;
// $toast-header-color: $white !default;
// $toast-header-background-color: $black !default;
// $toast-header-border-color: transparent !default;