feat(home): update and fix home page

feat(campaigns): add campaigns page

chore: add oxlint linter on top of eslint

refactor(pocketbase): rework typing of Pocketbase store
This commit is contained in:
2024-02-14 06:58:17 +01:00
parent 0da40ebf42
commit dd4ebefedc
17 changed files with 442 additions and 126 deletions

View File

@@ -12,6 +12,10 @@
flex-direction: row;
}
.center {
margin: 0 auto;
}
.flex-center {
align-items: center;
}
@@ -30,6 +34,22 @@
justify-content: space-between;
}
.flex-even {
justify-content: space-around;
}
.flex-wrap {
.flex;
flex-wrap: wrap;
}
.flex-size-even {
* {
flex: 1;
flex-basis: 100%;
}
}
.themed(@property, @light, @dark) {
@{property}: @light;
html.dark & {
@@ -67,3 +87,7 @@
.flex-row;
justify-content: end;
}
.text-center {
text-align: center;
}

View File

@@ -51,15 +51,15 @@ button,
}
&.h2 {
padding: (@h2-size / 2) (@h2-size * (2 / 3));
padding: (@h2-size / 2) @h2-size;
}
&.h3 {
padding: (@h3-size / 2) (@h3-size * (2 / 3));
padding: (@h3-size / 2) @h3-size;
}
&.h4 {
padding: (@h4-size / 2) (@h4-size * (2 / 3));
padding: (@h4-size / 2) @h4-size;
}
header & {

View File

@@ -23,7 +23,7 @@ body {
ul.no-style {
list-style: none;
list-style-type: none;
padding-left: 0;
padding: 0;
margin-top: 0;
margin-bottom: 0;
}

View File

@@ -2,9 +2,9 @@
<header class="flex-row-center flex-spread">
<RouterLink class="title h4" :to="{ name: 'home' }">{{ appTitle }}</RouterLink>
<div class="buttons gap-1rem">
<button @click="toggleDark()" class="secondary">{{ isDark ? 'Dark' : 'Light' }}</button>
<button v-if="!loggedIn" @click="login()" class="secondary">Login</button>
<RouterLink v-else :to="{ name: 'account' }" class="button secondary">Account</RouterLink>
<button @click="toggleDark()" class="secondary">{{ isDark ? 'Sombre' : 'Clair' }}</button>
<button v-if="!loggedIn" @click="login()" class="secondary">Connexion</button>
<RouterLink v-else :to="{ name: 'account' }" class="button secondary">Compte</RouterLink>
</div>
</header>
</template>

View File

@@ -1,28 +1,21 @@
<template>
<div class="h3">Bonjour {{ pbStore.auth.username }}&nbsp;!</div>
<div class="h2">Bonjour {{ pbStore.auth.username }}&nbsp;!</div>
<div class="campagnes flex-col-center gap-2rem">
<h1>Campagnes</h1>
<RouterLink :to="{ name: 'new-campaign' }" class="button">Créer une campagne</RouterLink>
<ul v-if="campaigns.length > 0">
<li v-for="campaign in campaigns" :key="campaign.id">{{ campaign }}</li>
</ul>
<div v-else>Pas de campagnes pour linstant</div>
<div class="h4">Actions</div>
<RouterLink :to="{ name: 'campaigns' }" class="button">Mes campagnes</RouterLink>
</div>
</template>
<script setup="" lang="ts">
<script setup lang="ts">
import { usePocketbaseStore } from '@/stores/pocketbase';
import { type RecordModel } from 'pocketbase';
import { onMounted, ref } from 'vue';
import { RouterLink } from 'vue-router';
const pbStore = usePocketbaseStore();
const campaigns = ref<RecordModel[]>([]);
onMounted(() => {
pbStore.campaign.listCampaigns().subscribe({
next: (result) => (campaigns.value = result),
});
});
</script>
<style scoped lang="less">
.campaigns {
max-width: 80%;
}
</style>

View File

@@ -6,12 +6,12 @@
<h2 class="card">
L&apos;application web pour vous accompagner pour votre JDR avec Gégé, le bot discord.
</h2>
<div class="flex-col-center gap-1rem">
<div class="flex-col-center flex-size-even gap-1rem">
<h3>Pourquoi&nbsp;?</h3>
<ul class="flex-row gap-1rem no-style">
<li class="card more">Accéder à ses fiche personnage</li>
<li class="card more">Faire évoluer ses personnage</li>
<li class="card more">Créer ses personnages</li>
<li class="card more text-center">Créer ses personnages</li>
<li class="card more text-center">rer ses fiche personnage</li>
<li class="card more text-center">Faire évoluer ses personnage</li>
</ul>
</div>
<button class="h4 raise margin-3rem">Se connecter avec Discord</button>

View File

@@ -0,0 +1,30 @@
<template>
<div class="card primary flex-col gap-1rem">
<div class="h4" id="name">
{{ props.campaign.name }}
</div>
<div id="dm">
{{ displayName(props.campaign.expand!.game_master!) }}
</div>
<ul id="players" class="no-style">
<li v-for="player in props.campaign.expand!.players" :key="player.id">
{{ displayName(player) }}
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import type { Campaign } from '@/models/Campaign';
import { displayName } from '@/models/User';
const props = defineProps<{
campaign: Campaign;
}>();
</script>
<style scoped lang="less">
.card {
min-width: 20rem;
}
</style>

View File

@@ -14,9 +14,8 @@ router.beforeEach((to, from, next) => {
const pbStore = usePocketbaseStore();
if (!pbStore.auth.loggedIn && ['home', 'login'].every((path) => path != to.name)) {
next({ name: 'home' });
} else {
next();
}
next();
});
app.use(createPinia());

20
src/models/Campaign.ts Normal file
View File

@@ -0,0 +1,20 @@
import { type RecordModel } from 'pocketbase';
import { type User } from './User';
interface CampaignDetails {
game_master?: User;
players?: User[];
}
export interface Campaign extends RecordModel {
expand?: CampaignDetails;
game_master: string;
name: string;
players: string[];
}
export interface NewCampaign {
game_master: string | null;
name: string | null;
players: string[];
}

48
src/models/Character.ts Normal file
View File

@@ -0,0 +1,48 @@
import type { RecordModel } from "pocketbase"
interface Effect {
name: string,
effect: string
}
interface BaseSkill {
name: string,
mastery: number,
}
interface FavourableSkill extends BaseSkill {
favourable: boolean
}
export interface PremadeCharacter extends RecordModel {
first_name: string
last_name: string
age: number
heroic_culture: string
particularities: string
description: string
stamina: number
hope: number
defense: number
valour: number
wisdom: number
rewards: Effect[],
virtues: Effect[],
travelling_equipment: Effect[],
image: string
body: number
heart: number
spirit: number
skills: FavourableSkill[]
combat_skills: BaseSkill[]
}
export interface Character extends PremadeCharacter {
campaign: string
current_stamina: number
current_load: number
current_fatigue: number
current_hope: number
status: string
user: string
}

20
src/models/User.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { RecordModel } from 'pocketbase';
export interface SimpleUser extends RecordModel {
username: string;
name?: string;
}
export interface User extends SimpleUser {
avatar?: string;
email?: string;
emailVisibility: boolean;
verified: boolean;
}
export function displayName(user: SimpleUser): string {
if (user.name && user.name.trim() !== '') {
return user.name;
}
return user.username;
}

View File

@@ -13,6 +13,11 @@ const router = createRouter({
name: 'account',
component: () => import('@/views/AccountView.vue'),
},
{
path: '/campaigns',
name: 'campaigns',
component: () => import('@/views/CampaignsView.vue'),
},
{
path: '/new-campaign',
name: 'new-campaign',

View File

@@ -3,11 +3,8 @@ import { defineStore } from 'pinia';
import { from, map, Observable, tap } from 'rxjs';
import { computed, ref } from 'vue';
export interface NewCampaign {
name: string | null;
game_master: string | null;
players: string[] | null;
}
import type { Campaign, NewCampaign } from '@/models/Campaign';
import type { SimpleUser } from '@/models/User';
export const usePocketbaseStore = defineStore('pocketbase', () => {
const pb = new PocketBase(import.meta.env.VITE_PB_URL);
@@ -47,22 +44,15 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
);
}
function simpleUserList(): Observable<RecordModel[]> {
return from(
pb.collection('public_users').getFullList({
sort: 'username',
})
);
}
/////////////////////////////////////////////////////////////////////////////
// Campaigns //
/////////////////////////////////////////////////////////////////////////////
function listCampaigns(): Observable<RecordModel[]> {
function listCampaigns(): Observable<Campaign[]> {
return from(
pb.collection('campaign').getFullList({
pb.collection('campaigns_simple_view').getFullList<Campaign>({
sort: 'name',
expand: 'players,game_master',
})
);
}
@@ -71,9 +61,20 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
return from(pb.collection('campaign').create(campaign));
}
/////////////////////////////////////////////////////////////////////////////
// Users //
/////////////////////////////////////////////////////////////////////////////
function allUsersSimple(): Observable<SimpleUser[]> {
return from(
pb.collection('public_users').getFullList<SimpleUser>({
sort: 'username',
})
);
}
return {
auth: {
authData,
authStore,
loggedIn,
username,
@@ -88,7 +89,7 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
createCampaign,
},
users: {
simpleUserList,
allUsersSimple,
},
};
});

View File

@@ -0,0 +1,61 @@
<template>
<h1>Campagnes</h1>
<div class="flex-col gap-2rem">
<div class="flex-col flex-center">
<RouterLink :to="{ name: 'new-campaign' }" class="button">Créer une campagne</RouterLink>
</div>
<h2>Campagnes que je gère</h2>
<div>
<ul
v-if="campaignsGameMaster.length > 0"
class="no-style center flex-wrap flex-size-even flex-even gap-1rem">
<li v-for="campaign in campaignsGameMaster" :key="campaign.id">
<SmallCampaignCard :campaign="campaign" />
</li>
</ul>
<div v-else>Pas de campagne pour linstant</div>
</div>
</div>
<div>
<h2>Campagnes je joue</h2>
<div>
<ul
v-if="campaignsPlayer.length > 0"
class="no-style center flex-wrap flex-size-even flex-even gap-1rem">
<li v-for="campaign in campaignsGameMaster" :key="campaign.id">
<SmallCampaignCard :campaign="campaign" />
</li>
</ul>
<div v-else class="card">Pas de campagne pour linstant</div>
</div>
</div>
</template>
<script setup lang="ts">
import { usePocketbaseStore } from '@/stores/pocketbase';
import SmallCampaignCard from '@/components/SmallCampaignCard.vue';
import { computed, onMounted, ref } from 'vue';
import { RouterLink } from 'vue-router';
import type { Campaign } from '@/models/Campaign';
const pbStore = usePocketbaseStore();
const campaigns = ref<Campaign[]>([]);
const campaignsGameMaster = computed<Campaign[]>(() =>
campaigns.value.filter((campaign) => campaign.game_master === pbStore.auth.userId)
);
const campaignsPlayer = computed<Campaign[]>(() =>
campaigns.value.filter((campaign) =>
campaign.players.some((player) => player === pbStore.auth.userId)
)
);
onMounted(() => {
pbStore.campaign.listCampaigns().subscribe({
next: (result) => (campaigns.value = result),
error: (err) => console.warn(err),
complete: () => console.log('List campaigns completed'),
});
});
</script>

View File

@@ -1,9 +1,6 @@
<template>
<h1>Création dune campagne</h1>
<div class="h3 card" v-if="campaign.name">
{{ campaign.name }}
</div>
<form @submit.prevent="createCampaign" class="flex-col gap-2rem card">
<form @submit.prevent="createCampaign" class="flex-col gap-2rem card" autocomplete="off">
<label for="campaign-name" class="flex-col gap-1rem">Nom de la nouvelle campagne</label>
<input
name="campaign-name"
@@ -11,6 +8,13 @@
v-model="campaign.name"
placeholder="Nom de la nouvelle campagne" />
<label class="flex-col gap-1rem" for="players" autocomplete="off">Joueurs (2 à 10)</label>
<select id="players" name="players" multiple v-model="campaign.players">
<option v-for="user in users" :key="user.id" :value="user.id">
{{ displayName(user) }}
</option>
</select>
<div class="buttons gap-1rem">
<RouterLink :to="{ name: 'home' }" class="button faded">Annuler</RouterLink>
<button type="submit">Envoyer</button>
@@ -19,33 +23,33 @@
</template>
<script setup lang="ts">
import router from '@/router';
import { usePocketbaseStore, type NewCampaign } from '@/stores/pocketbase';
import { type RecordModel } from 'pocketbase';
import { onMounted, ref } from 'vue';
import router from '@/router';
import { usePocketbaseStore } from '@/stores/pocketbase';
import { type NewCampaign } from '@/models/Campaign';
import { type SimpleUser, displayName } from '@/models/User';
const pbStore = usePocketbaseStore();
const simpleUsers = ref<RecordModel[]>([]);
const users = ref<SimpleUser[]>([]);
const campaign = ref<NewCampaign>({
name: null,
game_master: pbStore.auth.userId,
players: null,
players: [],
});
const createCampaign = () => {
pbStore.campaign.createCampaign(campaign.value).subscribe({
next: () => {
router.push({ name: 'home' });
router.go(0);
},
});
};
onMounted(() => {
pbStore.users.simpleUserList().subscribe({
next: (result) => (simpleUsers.value = result),
error: (err) => console.error('Failed to create campaign:', err),
pbStore.users.allUsersSimple().subscribe({
next: (results) => (users.value = results.filter((user) => user.id !== pbStore.auth.userId)),
error: (err) => console.warn('Failed to fetch all users', err),
});
});
</script>