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:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,28 +1,21 @@
|
||||
<template>
|
||||
<div class="h3">Bonjour {{ pbStore.auth.username }} !</div>
|
||||
<div class="h2">Bonjour {{ pbStore.auth.username }} !</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 l’instant</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>
|
||||
|
||||
@@ -6,12 +6,12 @@
|
||||
<h2 class="card">
|
||||
L'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 ?</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">Gé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>
|
||||
|
||||
30
src/components/SmallCampaignCard.vue
Normal file
30
src/components/SmallCampaignCard.vue
Normal 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>
|
||||
@@ -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
20
src/models/Campaign.ts
Normal 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
48
src/models/Character.ts
Normal 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
20
src/models/User.ts
Normal 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;
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
61
src/views/CampaignsView.vue
Normal file
61
src/views/CampaignsView.vue
Normal 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 l’instant</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Campagnes où 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 l’instant</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>
|
||||
@@ -1,9 +1,6 @@
|
||||
<template>
|
||||
<h1>Création d’une 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>
|
||||
|
||||
Reference in New Issue
Block a user