board updates

This commit is contained in:
Gamebrary 2022-10-28 22:23:03 -07:00
parent 0cf1f9cd93
commit 34a3ea0cc5
8 changed files with 397 additions and 89 deletions

View file

@ -8,28 +8,26 @@
<template v-else-if="hasAccess">
<portal to="pageTitle">
<div :class="{ 'd-flex align-items-baseline': publicProfile && publicProfile.userName }">
<p :class="['mb-0', { 'text-white': backgroundUrl, 'text-outlined': backgroundUrl }]">
<div :class="{ 'd-flex align-items-baseline': publicUserName }">
<p :class="['mb-0', { 'text-outlined': backgroundUrl }]">
{{ board.name }}
<small
v-if="publicProfile && publicProfile.userName"
>
<small v-if="publicUserName">
by
<b-link
class="mr-2"
:to="{ name: 'public.profile', params: { userName: publicProfile.userName }}"
:to="{ name: 'public.profile', params: { userName: publicUserName }}"
>
<b-avatar
rounded
v-if="avatarImage"
:src="avatarImage"
:title="`@${publicUserName}`"
v-b-tooltip.hover
:title="`@${publicProfile.userName}`"
/>
@{{ publicProfile.userName }}
@{{ publicUserName }}
</b-link>
</small>
</p>
@ -49,6 +47,7 @@
</portal>
<standard-board v-if="board.type === $options.BOARD_TYPE_STANDARD" />
<tier-board v-else-if="board.type === $options.BOARD_TYPE_TIER" />
<kanban-board v-else />
</template>
@ -65,18 +64,21 @@
<script>
import BoardPlaceholder from '@/components/Board/BoardPlaceholder';
import KanbanBoard from '@/components/Board/KanbanBoard';
import TierBoard from '@/components/Board/TierBoard';
import StandardBoard from '@/components/Board/StandardBoard';
import chunk from 'lodash.chunk';
import { getImageThumbnail } from '@/utils';
import { BOARD_TYPE_STANDARD } from '@/constants';
import { BOARD_TYPE_STANDARD, BOARD_TYPE_TIER } from '@/constants';
import { mapState, mapGetters } from 'vuex';
export default {
BOARD_TYPE_STANDARD,
BOARD_TYPE_TIER,
components: {
BoardPlaceholder,
KanbanBoard,
TierBoard,
StandardBoard,
},
@ -102,6 +104,10 @@ export default {
return this.user || this.board?.isPublic;
},
publicUserName() {
return this.publicProfile?.userName;
},
boardId() {
return this.$route.params?.id;
},

View file

@ -4,6 +4,7 @@
<portal to="pageTitle">Create board</portal>
<b-form @submit.prevent="createBoard" class="field centered">
<small><pre>{{ payload }}</pre></small>
<b-form-group label="Board name:" label-for="boardName">
<b-form-input
id="boardName"
@ -53,51 +54,60 @@
</template>
<script>
import { BOARD_TYPES, BOARD_TYPE_STANDARD } from '@/constants';
import {
BOARD_TYPES,
BOARD_TYPE_KANBAN,
BOARD_TYPE_STANDARD,
BOARD_TYPE_TIER,
DEFAULT_BOARD_KANBAN,
DEFAULT_BOARD_STANDARD,
DEFAULT_BOARD_TIER,
} from '@/constants';
export default {
BOARD_TYPES,
BOARD_TYPE_TIER,
BOARD_TYPE_STANDARD,
data() {
return {
board: {
name: '',
lists: [],
type: BOARD_TYPE_STANDARD,
ranked: false,
},
board: {},
saving: false,
selectedTemplate: null,
};
},
mounted() {
this.board = {
...this.defaultBoard,
type: BOARD_TYPE_STANDARD,
}
},
computed: {
defaultBoard() {
if (this.board.type === BOARD_TYPE_TIER) return DEFAULT_BOARD_TIER;
if (this.board.type === BOARD_TYPE_KANBAN) return DEFAULT_BOARD_KANBAN;
return DEFAULT_BOARD_STANDARD;
},
payload() {
return {
...this.defaultBoard,
...this.board,
}
},
},
methods: {
async createBoard() {
this.saving = true;
try {
// TODO: put default board in constant
const payload = {
...this.board,
// TODO: set default lists based on board type
games: [],
lastUpdated: Date.now(),
lists: [{
name: '',
games: [],
settings: {
showReleaseDates: false,
sortOrder: 'sortByCustom',
showGameTags: false,
showGameNotes: false,
showGameCount: false,
view: 'single'
},
}],
};
console.log(this.payload);
const { id } = await this.$store.dispatch('CREATE_BOARD', payload);
const { id } = await this.$store.dispatch('CREATE_BOARD', this.payload);
this.$router.push({ name: 'board', params: { id } });
} catch (e) {

View file

@ -1,7 +1,7 @@
<template lang="html">
<div class="board-placeholder">
<b-spinner
v-if="board.type === 'list'"
v-if="useSpinner"
class="spinner-centered mt-5"
/>
@ -10,6 +10,7 @@
</template>
<script>
import { BOARD_TYPE_STANDARD, BOARD_TYPE_TIER } from '@/constants';
import KanbanBoardPlaceholder from '@/components/Board/KanbanBoardPlaceholder'
import { mapState } from 'vuex';
@ -20,6 +21,10 @@ export default {
computed: {
...mapState(['board']),
useSpinner() {
return [BOARD_TYPE_STANDARD, BOARD_TYPE_TIER].includes(this.board?.type)
},
},
};
</script>

View file

@ -0,0 +1,68 @@
<template lang="html">
<div class="board px-3 pb-3 d-flex flex-column">
<tier-list
v-for="(list, listIndex) in board.lists"
:list="list"
:allGames="allGames"
:listIndex="listIndex"
:key="listIndex"
/>
<!-- TODO: create different component for adding tier -->
<!-- <add-list
v-if="isBoardOwner"
:empty="empty"
/> -->
</div>
</template>
<script>
import { mapState, mapGetters } from 'vuex';
// import AddList from '@/components/Board/AddList';
import TierList from '@/components/Lists/TierList';
export default {
components: {
TierList,
// AddList,
},
computed: {
...mapState(['board', 'games']),
...mapGetters(['isBoardOwner']),
empty() {
return this.board?.lists?.length === 0;
},
formattedGames() {
return this.board?.games.map((id) => this.games[id]);
},
allGames() {
return this.board.lists.map((list) => list.games).flat(2);
},
},
methods: {
async selectGame(gameId) {
const board = JSON.parse(JSON.stringify(this.board));
board.games.push(gameId);
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 adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
},
};
</script>
<style lang="scss" rel="stylesheet/scss" scoped>
.board {
min-height: calc(100vh - 62px);
}
</style>

View file

@ -10,50 +10,46 @@
]"
:id="listIndex"
>
<b-card no-body>
<b-card
no-body
:bg-variant="darkTheme ? 'info' : 'light'"
:text-variant="darkTheme ? 'light' : 'dark'"
>
<header class="p-2 pr-0 d-flex justify-content-between">
<b-button
block
size="sm"
variant="transparent"
class="text-dark d-flex justify-content-between align-items-center pl-0"
:variant="darkTheme ? 'info' : 'transparent'"
class="d-flex justify-content-between align-items-center pl-0"
:disabled="preview || !isBoardOwner"
:to="{ name: 'board.list.edit', params: { id: board.id, listIndex } }"
:to="{ name: 'board.edit', params: { id: board.id } }"
>
<span>
<b-badge
v-if="showGameCount"
variant="dark"
v-if="!showGameCount"
:variant="darkTheme ? 'dark' : 'info'"
>
{{ list.games.length }}
</b-badge>
{{ list.name }}
</span>
<b-badge
v-if="autoSortEnabled"
variant="light"
:variant="darkTheme ? 'danger' : 'warning'"
v-b-tooltip.hover
:title="`${$t('board.list.sortedBy')} ${$t(`board.list.${list.settings.sortOrder}`)}`"
:title="`${$t('board.list.sortedBy')} ${$t(`board.list.${sortOrder}`)}`"
>
<i class="fa-solid fa-sort fa-fw" />
</b-badge>
</b-button>
<!-- <b-button
size="sm"
variant="transparent"
:to="{ name: 'search', query: { boardId: board.id, listIndex } }"
>
<i class="fa-solid fa-plus fa-fw" />
</b-button> -->
<game-selector
v-if="isBoardOwner"
class="mb-2"
title="Add games"
variant="transparent"
:variant="darkTheme ? 'info' : 'transparent'"
:filter="list.games"
@select-game="selectGame"
>
@ -93,6 +89,7 @@
<game-selector
v-if="isEmpty && isBoardOwner"
class="mb-2"
:variant="darkTheme ? 'secondary' : 'warning'"
:filter="list.games"
@select-game="selectGame"
/>
@ -150,19 +147,23 @@ export default {
computed: {
...mapState(['games', 'dragging', 'progresses', 'board', 'user', 'settings']),
...mapGetters(['isBoardOwner']),
...mapGetters(['isBoardOwner', 'darkTheme']),
draggingDisabled() {
return !this.user || !this.isBoardOwner;
},
autoSortEnabled() {
return ['sortByName', 'sortByRating', 'sortByReleaseDate', 'sortByProgress'].includes(this.list?.settings?.sortOrder);
return ['sortByName', 'sortByRating', 'sortByReleaseDate', 'sortByProgress'].includes(this.sortOrder);
},
sortOrder() {
return this.list?.settings?.sortOrder;
},
sortedGames() {
const { settings, games } = this.list;
const sortOrder = settings.sortOrder || 'sortByCustom';
const sortOrder = settings?.sortOrder || 'sortByCustom';
switch (sortOrder) {
case 'sortByCustom': return this.list.games;

View file

@ -18,12 +18,15 @@
<h3 v-if="hasMultipleLists">{{ list.name }}</h3>
<b-card
no-body
:bg-variant="darkTheme ? 'info' : 'white'"
:text-variant="darkTheme ? 'white' : 'dark'"
class="mb-2 flex-row align-items-center cursor-pointer"
v-for="(game, index) in listGames"
:key="game.id"
@click="openGame(game.id, list)"
>
<b-img
v-if="game"
:src="$options.getThumbnailUrl(game)"
:alt="game.name"
class="m-2 game-cover"
@ -31,35 +34,22 @@
fluid
/>
<h3 class="d-flex mr-2 w-100 px-3">
<b-badge
<h3 class="d-flex mr-2 w-100 px-3 align-items-center">
<b-avatar
v-if="board.ranked"
variant="light"
class="mr-1"
class="mr-2"
>
{{ index + 1 }}
</b-badge>
</b-avatar>
{{ game.name }}
</h3>
</b-card>
<div v-if="isEmpty && isBoardOwner">
<b-button
variant="light"
block
class="mb-2"
:disabled="!isBoardOwner"
:to="{ name: 'search', query: { boardId: board.id, listIndex: 0 } }"
>
<template v-if="isBoardOwner">Add games</template>
<template v-else>Empty list</template>
</b-button>
</div>
<game-selector
:filter="filter"
:title="`Add games to ${list.name}`"
title="Add games"
size="lg"
@select-game="selectGame"
/>
</draggable>
@ -98,7 +88,7 @@ export default {
computed: {
...mapState(['games', 'dragging', 'progresses', 'board', 'user', 'settings']),
...mapGetters(['isBoardOwner']),
...mapGetters(['isBoardOwner', 'darkTheme']),
hasMultipleLists() {
return this.board?.lists?.length > 1;
@ -117,7 +107,7 @@ export default {
},
listGames() {
return this.list.games.map((id) => this.games?.[id]);
return this.list?.games?.map((id) => this.games?.[id]);
},
isEmpty() {
@ -150,7 +140,7 @@ export default {
try {
await this.$store.dispatch('SAVE_GAME_BOARD', board);
await this.$store.dispatch('LOAD_BOARD', board.id);
await this.$store.dispatch('LOAD_BOARD', board?.id);
} catch (e) {
// this.$bvToast.toast(`There was an error adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
}

View file

@ -0,0 +1,195 @@
<template lang="html">
<div class="d-flex align-items-center">
<game-selector
title="Add games"
class="mr-3"
:filter="allGames"
@select-game="selectGame"
>
<i class="fa fa-plus" aria-hidden="true" />
</game-selector>
<b-avatar
variant="primary"
:text="list.name"
:to="{ name: 'board.edit', params: { id: board.id } }"
square
:style="`background-color: ${list.backgroundColor}`"
size="100"
/>
<draggable
handle=".game"
ghost-class="card-placeholder"
drag-class="border-selected"
chosen-class="border-primary"
class="tier-list d-flex"
filter=".drag-filter"
delay="50"
animation="500"
:list="list.games"
:id="listIndex"
:move="validateMove"
:disabled="draggingDisabled"
:group="{ name: 'games' }"
@end="dragEnd"
@start="dragStart"
>
<b-img
v-for="gameId in list.games"
:key="gameId"
class="game cursor-pointer"
fluid
:src="$options.getThumbnailUrl(games[gameId])"
@click="openGame(gameId)"
/>
</draggable>
</div>
</template>
<script>
import { getThumbnailUrl } from '@/utils';
import draggable from 'vuedraggable';
import slugify from 'slugify'
import orderby from 'lodash.orderby';
import { mapState, mapGetters } from 'vuex';
import gameCardMixin from '@/mixins/gameCardMixin';
import GameSelector from '@/components/GameSelector';
export default {
getThumbnailUrl,
mixins: [gameCardMixin],
components: {
GameSelector,
draggable,
},
props: {
allGames: Array,
list: {
type: Object,
default: () => {},
},
listIndex: Number,
preview: Boolean,
},
data() {
return {
draggingId: null,
editing: false,
};
},
computed: {
...mapState(['games', 'dragging', 'progresses', 'board', 'user', 'settings']),
...mapGetters(['isBoardOwner']),
draggingDisabled() {
return !this.user || !this.isBoardOwner;
},
isEmpty() {
return this.list.games.length === 0;
},
},
methods: {
selectGame(gameId) {
return this.list.games.includes(gameId)
? this.removeGame(gameId)
: this.addGame(gameId);
},
async addGame(gameId) {
console.log(gameId);
const board = JSON.parse(JSON.stringify(this.board));
board?.lists?.[this.listIndex]?.games.push(gameId);
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 adding "${this.game.name}"`, { title: list.name, variant: 'danger' });
}
},
async removeGame() {
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(this.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' });
cursor-pointer}
},
openGame(id) {
const slug = slugify(this.games[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" scoped>
.tier-list {
width: calc(100vw - 180px);
height: 100px;
overflow-y: hidden;
overflow-x: auto;
margin-bottom: 1px;
}
.game {
height: 100px;
width: auto;
}
</style>
<style lang="scss" rel="stylesheet/scss">
.card-placeholder {
background: #c00;
}
</style>

View file

@ -7,13 +7,13 @@ export const STEAM_CATEGORY_ID = 13;
export const GOG_CATEGORY_ID = 17;
export const TWITTER_CATEGORY_ID = 5;
export const BOARD_TYPE_STANDARD = 'standard';
export const BOARD_TYPE_KANBAN = 'kanban';
export const BOARD_TYPE_STANDARD = 'list';
export const BOARD_TYPE_TIER = 'tier';
export const BOARD_TYPES = [
{ text: 'Kanban', value: BOARD_TYPE_KANBAN },
{ text: 'Standard', value: BOARD_TYPE_STANDARD },
{ text: 'Kanban', value: BOARD_TYPE_KANBAN },
{ text: 'Tier', value: BOARD_TYPE_TIER },
];
@ -26,13 +26,46 @@ export const PLATFORM_CATEGORIES = {
6: 'computer',
};
export const DEFAULT_BOARD = {
1: 'console',
2: 'arcade',
3: 'platform',
4: 'operating_system',
5: 'portable_console',
6: 'computer',
export const DEFAULT_BOARD_BASE = {
name: '',
ranked: false,
isPublic: false,
}
export const DEFAULT_BOARD_TIER = {
...DEFAULT_BOARD_BASE,
type: BOARD_TYPE_TIER,
lists: [
{ name: 'S', games: [], backgroundColor: '#C0382B' },
{ name: 'A', games: [], backgroundColor: '#E84B3C'},
{ name: 'B', games: [], backgroundColor: '#F39C19' },
{ name: 'C', games: [], backgroundColor: '#F2C511' },
{ name: 'D', games: [], backgroundColor: '#27AF60' },
],
};
export const DEFAULT_BOARD_STANDARD = {
...DEFAULT_BOARD_BASE,
type: BOARD_TYPE_STANDARD,
lists: [
{
games: [],
}
],
};
export const DEFAULT_BOARD_KANBAN = {
...DEFAULT_BOARD_BASE,
type: BOARD_TYPE_KANBAN,
lists: [{
name: 'Click to edit',
games: [],
sortBy: null,
showTags: false,
showNotes: false,
showCount: false,
view: null,
}],
};
export const AUTH_PROVIDERS = {