Some more stuff

Sorry, I’m in a rush, can’t be bothered to properly commit
This commit is contained in:
Lucien Cartier-Tilet 2024-02-16 21:44:36 +01:00
parent dd4ebefedc
commit 2871eec4b5
27 changed files with 533 additions and 103 deletions

Binary file not shown.

View File

@ -0,0 +1,26 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Generated by IcoMoon</metadata>
<defs>
<font id="gdrico" horiz-adv-x="1024">
<font-face units-per-em="1024" ascent="960" descent="-64" />
<missing-glyph horiz-adv-x="1024" />
<glyph unicode="&#x20;" horiz-adv-x="512" d="" />
<glyph unicode="&#xe900;" glyph-name="sun" horiz-adv-x="1022" d="M512.032 704c-141.376 0-256-114.624-256-256s114.624-256 256-256c141.376 0 255.968 114.624 255.968 256s-114.592 256-255.968 256v0zM448 896c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM128 768c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM64 512c35.36 0 64-28.64 64-64 0-35.424-28.64-64-64-64s-64 28.576-64 64c0 35.36 28.64 64 64 64zM128 128c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM448 0c0 35.488 28.64 64 64 64 35.456 0 64-28.512 64-64 0-35.264-28.544-64-64-64-35.36 0-64 28.736-64 64zM768 128c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64zM960 384c-35.328 0-64 28.672-64 64 0 35.424 28.672 64 64 64s64-28.576 64-64c0-35.328-28.672-64-64-64zM768 768c0 35.346 28.654 64 64 64s64-28.654 64-64c0-35.346-28.654-64-64-64s-64 28.654-64 64z" />
<glyph unicode="&#xe901;" glyph-name="moon" d="M788.256 250.112c-262.016 0-474.24 212.384-474.24 474.24 0 86.24 24.736 166.016 64.992 235.616-218.368-62.976-379.008-261.984-379.008-500.608 0-288.992 234.24-523.36 523.264-523.36 238.624 0 437.76 160.736 500.736 379.008-69.76-40.128-149.504-64.896-235.744-64.896z" />
<glyph unicode="&#xe90d;" glyph-name="image" d="M959.884 832c0.040-0.034 0.082-0.076 0.116-0.116v-767.77c-0.034-0.040-0.076-0.082-0.116-0.116h-895.77c-0.040 0.034-0.082 0.076-0.114 0.116v767.772c0.034 0.040 0.076 0.082 0.114 0.114h895.77zM960 896h-896c-35.2 0-64-28.8-64-64v-768c0-35.2 28.8-64 64-64h896c35.2 0 64 28.8 64 64v768c0 35.2-28.8 64-64 64v0zM832 672c0-53.020-42.98-96-96-96s-96 42.98-96 96 42.98 96 96 96 96-42.98 96-96zM896 128h-768v128l224 384 256-320h64l224 192z" />
<glyph unicode="&#xe962;" glyph-name="floppy-disk" d="M896 960h-896v-1024h1024v896l-128 128zM512 832h128v-256h-128v256zM896 64h-768v768h64v-320h576v320h74.978l53.022-53.018v-714.982z" />
<glyph unicode="&#xe971;" glyph-name="user" d="M576 253.388v52.78c70.498 39.728 128 138.772 128 237.832 0 159.058 0 288-192 288s-192-128.942-192-288c0-99.060 57.502-198.104 128-237.832v-52.78c-217.102-17.748-384-124.42-384-253.388h896c0 128.968-166.898 235.64-384 253.388z" />
<glyph unicode="&#xe972;" glyph-name="users" horiz-adv-x="1152" d="M768 189.388v52.78c70.498 39.728 128 138.772 128 237.832 0 159.058 0 288-192 288s-192-128.942-192-288c0-99.060 57.502-198.104 128-237.832v-52.78c-217.102-17.748-384-124.42-384-253.388h896c0 128.968-166.898 235.64-384 253.388zM327.196 164.672c55.31 36.15 124.080 63.636 199.788 80.414-15.054 17.784-28.708 37.622-40.492 59.020-30.414 55.234-46.492 116.058-46.492 175.894 0 86.042 0 167.31 30.6 233.762 29.706 64.504 83.128 104.496 159.222 119.488-16.914 76.48-61.94 126.75-181.822 126.75-192 0-192-128.942-192-288 0-99.060 57.502-198.104 128-237.832v-52.78c-217.102-17.748-384-124.42-384-253.388h279.006c14.518 12.91 30.596 25.172 48.19 36.672z" />
<glyph unicode="&#xe976;" glyph-name="user-tie" d="M320 768c0 106.039 85.961 192 192 192s192-85.961 192-192c0-106.039-85.961-192-192-192s-192 85.961-192 192zM768.078 512h-35.424l-199.104-404.244 74.45 372.244-96 96-96-96 74.45-372.244-199.102 404.244h-35.424c-127.924 0-127.924-85.986-127.924-192v-320h768v320c0 106.014 0 192-127.922 192z" />
<glyph unicode="&#xe992;" glyph-name="settings" d="M448 832v16c0 26.4-21.6 48-48 48h-160c-26.4 0-48-21.6-48-48v-16h-192v-128h192v-16c0-26.4 21.6-48 48-48h160c26.4 0 48 21.6 48 48v16h576v128h-576zM256 704v128h128v-128h-128zM832 528c0 26.4-21.6 48-48 48h-160c-26.4 0-48-21.6-48-48v-16h-576v-128h576v-16c0-26.4 21.6-48 48-48h160c26.4 0 48 21.6 48 48v16h192v128h-192v16zM640 384v128h128v-128h-128zM448 208c0 26.4-21.6 48-48 48h-160c-26.4 0-48-21.6-48-48v-16h-192v-128h192v-16c0-26.4 21.6-48 48-48h160c26.4 0 48 21.6 48 48v16h576v128h-576v16zM256 64v128h128v-128h-128z" />
<glyph unicode="&#xe9ad;" glyph-name="bin" d="M192-64h640l64 704h-768zM640 832v128h-256v-128h-320v-192l64 64h768l64-64v192h-320zM576 832h-128v64h128v-64z" />
<glyph unicode="&#xe9c7;" glyph-name="download" d="M736 512l-256-256-256 256h160v384h192v-384zM480 256h-480v-256h960v256h-480zM896 128h-128v64h128v-64z" />
<glyph unicode="&#xe9c8;" glyph-name="upload" d="M480 256h-480v-256h960v256h-480zM896 128h-128v64h128v-64zM224 640l256 256 256-256h-160v-320h-192v320z" />
<glyph unicode="&#xea07;" glyph-name="warning" d="M512 867.226l429.102-855.226h-858.206l429.104 855.226zM512 960c-22.070 0-44.14-14.882-60.884-44.648l-437.074-871.112c-33.486-59.532-5-108.24 63.304-108.24h869.308c68.3 0 96.792 48.708 63.3 108.24h0.002l-437.074 871.112c-16.742 29.766-38.812 44.648-60.882 44.648v0zM576 128c0-35.346-28.654-64-64-64s-64 28.654-64 64c0 35.346 28.654 64 64 64s64-28.654 64-64zM512 256c-35.346 0-64 28.654-64 64v192c0 35.346 28.654 64 64 64s64-28.654 64-64v-192c0-35.346-28.654-64-64-64z" />
<glyph unicode="&#xea13;" glyph-name="enter" d="M384 448h-320v128h320v128l192-192-192-192zM1024 960v-832l-384-192v192h-384v256h64v-192h320v576l256 128h-576v-256h-64v320z" />
<glyph unicode="&#xea14;" glyph-name="exit" d="M768 320v128h-320v128h320v128l192-192zM704 384v-256h-320v-192l-384 192v832h704v-320h-64v256h-512l256-128v-576h256v192z" />
<glyph unicode="&#xea38;" glyph-name="back" d="M32 448l480-480v288h512v384h-512v288z" />
<glyph unicode="&#xeae7;" glyph-name="git" d="M1004.692 493.606l-447.096 447.080c-25.738 25.754-67.496 25.754-93.268 0l-103.882-103.876 78.17-78.17c12.532 5.996 26.564 9.36 41.384 9.36 53.020 0 96-42.98 96-96 0-14.82-3.364-28.854-9.362-41.386l127.976-127.974c12.532 5.996 26.566 9.36 41.386 9.36 53.020 0 96-42.98 96-96s-42.98-96-96-96-96 42.98-96 96c0 14.82 3.364 28.854 9.362 41.386l-127.976 127.974c-3.042-1.456-6.176-2.742-9.384-3.876v-266.968c37.282-13.182 64-48.718 64-90.516 0-53.020-42.98-96-96-96s-96 42.98-96 96c0 41.796 26.718 77.334 64 90.516v266.968c-37.282 13.18-64 48.72-64 90.516 0 14.82 3.364 28.852 9.36 41.384l-78.17 78.17-295.892-295.876c-25.75-25.776-25.75-67.534 0-93.288l447.12-447.080c25.738-25.75 67.484-25.75 93.268 0l445.006 445.006c25.758 25.762 25.758 67.54-0.002 93.29z" />
</font></defs></svg>

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Binary file not shown.

View File

@ -18,7 +18,7 @@ main {
margin: 5rem; margin: 5rem;
align-content: center; align-content: center;
padding-top: 4.5rem !important; padding-top: 4.5rem !important;
padding-bottom: 7rem !important; padding-bottom: 10rem !important;
} }
header { header {

76
src/assets/_fonts.less Normal file
View File

@ -0,0 +1,76 @@
@font-face {
font-family: 'gdrico';
src: url('assets/fonts/gdrico.eot?lbue9x');
src:
url('assets/fonts/gdrico.eot?lbue9x#iefix') format('embedded-opentype'),
url('assets/fonts/gdrico.ttf?lbue9x') format('truetype'),
url('assets/fonts/gdrico.woff?lbue9x') format('woff'),
url('assets/fonts/gdrico.svg?lbue9x#gdrico') format('svg');
font-weight: normal;
font-style: normal;
font-display: block;
}
i {
/* use !important to prevent issues with browser extensions that change fonts */
font-family: 'gdrico' !important;
speak: never;
font-style: normal;
font-weight: normal;
font-variant: normal;
text-transform: none;
line-height: 1;
/* Better Font Rendering =========== */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.gdrico-sun:before {
content: '\e900';
}
.gdrico-moon:before {
content: '\e901';
}
.gdrico-image:before {
content: '\e90d';
}
.gdrico-floppy-disk:before {
content: '\e962';
}
.gdrico-user:before {
content: '\e971';
}
.gdrico-users:before {
content: '\e972';
}
.gdrico-user-tie:before {
content: '\e976';
}
.gdrico-settings:before {
content: '\e992';
}
.gdrico-bin:before {
content: '\e9ad';
}
.gdrico-download:before {
content: '\e9c7';
}
.gdrico-upload:before {
content: '\e9c8';
}
.gdrico-warning:before {
content: '\ea07';
}
.gdrico-enter:before {
content: '\ea13';
}
.gdrico-exit:before {
content: '\ea14';
}
.gdrico-back:before {
content: '\ea38';
}
.gdrico-git:before {
content: '\eae7';
}

View File

@ -17,8 +17,14 @@
figure, figure,
img { img {
grid-area: figure; grid-area: figure;
max-width: 100%; max-width: min(10rem, 25%);
max-height: 10rem; max-height: 100%;
border-radius: 2rem; border-radius: 2rem;
} }
} }
@media all and (max-width: 400px) {
.two-col-img-text {
gap: 2rem;
}
}

View File

@ -46,7 +46,6 @@
.flex-size-even { .flex-size-even {
* { * {
flex: 1; flex: 1;
flex-basis: 100%;
} }
} }
@ -91,3 +90,11 @@
.text-center { .text-center {
text-align: center; text-align: center;
} }
.ul-no-style {
list-style: none;
list-style-type: none;
padding: 0;
margin-top: 0;
margin-bottom: 0;
}

View File

@ -4,6 +4,11 @@
border-radius: 2rem; border-radius: 2rem;
padding: 2rem; padding: 2rem;
.themed(background-color, @light-background-100, @dark-background-100); .themed(background-color, @light-background-100, @dark-background-100);
.themed(color, @light-text, @dark-text);
&.less {
.themed(background-color, @light-background-50, @dark-background-50);
}
&.more { &.more {
.themed(background-color, @light-background-200, @dark-background-200); .themed(background-color, @light-background-200, @dark-background-200);
@ -17,6 +22,16 @@
&.primary { &.primary {
.themed(background-color, @light-primary, @dark-primary); .themed(background-color, @light-primary, @dark-primary);
.themed(color, @light-background, @dark-background); .themed(color, @light-background, @dark-background);
&.less {
.themed(background-color, @light-primary-600, @dark-primary-600);
.themed(color, @light-background-50, @dark-background-50);
}
&.more {
.themed(background-color, @light-primary-800, @dark-primary-800);
.themed(color, @light-background-100, @dark-background-100);
}
} }
&.secondary { &.secondary {

View File

@ -17,39 +17,35 @@ h5 {
.title; .title;
} }
.tag-font-size(@tag, @base-size, @min-size: 1rem) {
.@{tag},
@{tag} {
font-size: (@base-size * 1rem);
}
@media all and (max-width: 400px) {
.@{tag},
@{tag} {
font-size: max((@base-size * 0.75rem), @min-size);
}
}
}
html { html {
font-size: 100%; font-size: 100%;
} }
@h1-size: 4.21rem; @h1-size: 4.21rem;
.h1,
h1 {
font-size: @h1-size;
}
@h2-size: 3.158rem; @h2-size: 3.158rem;
.h2,
h2 {
font-size: @h2-size;
}
@h3-size: 2.369rem; @h3-size: 2.369rem;
.h3,
h3 {
font-size: @h3-size;
}
@h4-size: 1.777rem; @h4-size: 1.777rem;
.h4,
h4 {
font-size: @h4-size;
}
@h5-size: 1.333rem; @h5-size: 1.333rem;
.h5,
h5 { .tag-font-size(h1, @h1-size);
font-size: @h5-size; .tag-font-size(h2, @h2-size);
} .tag-font-size(h3, @h3-size);
.tag-font-size(h4, @h4-size);
.tag-font-size(h5, 1.333rem);
.small, .small,
small { small {

0
src/assets/forms.less Normal file
View File

View File

@ -2,6 +2,7 @@
@import '@fontsource/roboto'; @import '@fontsource/roboto';
@import '@fontsource/poppins'; @import '@fontsource/poppins';
@import '_theme'; @import '_theme';
@import '_fonts';
@import '_mixins'; @import '_mixins';
@import '_layouts'; @import '_layouts';

View File

@ -24,6 +24,7 @@ const version = __APP_VERSION__;
footer { footer {
margin: 2rem; margin: 2rem;
margin-top: 2rem;
position: absolute; position: absolute;
left: 0; left: 0;
right: 0; right: 0;

View File

@ -2,9 +2,18 @@
<header class="flex-row-center flex-spread"> <header class="flex-row-center flex-spread">
<RouterLink class="title h4" :to="{ name: 'home' }">{{ appTitle }}</RouterLink> <RouterLink class="title h4" :to="{ name: 'home' }">{{ appTitle }}</RouterLink>
<div class="buttons gap-1rem"> <div class="buttons gap-1rem">
<button @click="toggleDark()" class="secondary">{{ isDark ? 'Sombre' : 'Clair' }}</button> <button @click="toggleDark()" class="secondary">
<i v-if="isDark" class="gdrico-moon"></i>
<i v-else class="gdrico-sun"></i>
</button>
<button v-if="!loggedIn" @click="login()" class="secondary">Connexion</button> <button v-if="!loggedIn" @click="login()" class="secondary">Connexion</button>
<RouterLink v-else :to="{ name: 'account' }" class="button secondary">Compte</RouterLink> <RouterLink v-else :to="{ name: 'account' }" class="button secondary">
<div id="account" class="flex-row">
<i class="gdrico-user"></i>
<div>Compte</div>
</div>
</RouterLink>
</div> </div>
</header> </header>
</template> </template>
@ -20,7 +29,6 @@ const appTitle = import.meta.env.VITE_NAME;
const isDark = useDark(); const isDark = useDark();
const toggleDark = useToggle(isDark); const toggleDark = useToggle(isDark);
const pbStore = usePocketbaseStore(); const pbStore = usePocketbaseStore();
const loggedIn = ref(pbStore.auth.loggedIn); const loggedIn = ref(pbStore.auth.loggedIn);
@ -45,4 +53,8 @@ header {
color: inherit; color: inherit;
} }
} }
#account {
gap: 0.5rem;
}
</style> </style>

View File

@ -6,13 +6,33 @@
<h2 class="card"> <h2 class="card">
L&apos;application web pour vous accompagner pour votre JDR avec Gégé, le bot discord. L&apos;application web pour vous accompagner pour votre JDR avec Gégé, le bot discord.
</h2> </h2>
<div class="flex-col-center flex-size-even gap-1rem"> <div>
<h3>Pourquoi&nbsp;?</h3> <h3>Pourquoi&nbsp;?</h3>
<ul class="flex-row gap-1rem no-style"> <ul class="features">
<li class="card more text-center">Créer ses personnages</li> <li class="feature">Créer ses personnages</li>
<li class="card more text-center">Gérer ses fiche personnage</li> <li class="feature">Gérer ses fiche personnage</li>
<li class="card more text-center">Faire évoluer ses personnage</li> <li class="feature">Faire évoluer ses personnage</li>
</ul> </ul>
</div> </div>
<button class="h4 raise margin-3rem">Se connecter avec Discord</button> <button class="h4 raise margin-3rem">Se connecter avec Discord</button>
</template> </template>
<style scoped lang="less">
@import '@/assets/main';
.features {
.flex-row-center;
.flex-size-even;
.gap-1rem;
.ul-no-style;
flex-wrap: wrap;
align-items: stretch;
}
.feature {
.card;
.more;
.text-center;
min-width: 15rem;
}
</style>

View File

@ -1,22 +1,30 @@
<template> <template>
<div class="card primary flex-col gap-1rem"> <RouterLink :to="{ name: 'edit-campaign', params: { campaignId: props.campaign.id } }">
<div class="h4" id="name"> <div class="campaign">
<div class="campaign-title">
{{ props.campaign.name }} {{ props.campaign.name }}
</div> </div>
<div id="dm"> <div class="flex-col gamemaster">
{{ displayName(props.campaign.expand!.game_master!) }} <div>Maître jeu</div>
<div class="user">
<UserAvatarAndName :user="props.campaign.expand!.game_master!" :align="'right'" />
</div> </div>
<ul id="players" class="no-style"> </div>
<li v-for="player in props.campaign.expand!.players" :key="player.id"> <div class="flex-col gap-1rem players">
{{ displayName(player) }} <div><i class="gdrico-users"></i> Joueurs</div>
<ul class="player-list">
<li v-for="player in props.campaign.expand!.players" :key="player.id" class="player">
<UserAvatarAndName :user="player" />
</li> </li>
</ul> </ul>
</div> </div>
</div>
</RouterLink>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import UserAvatarAndName from './UserAvatarAndName.vue';
import type { Campaign } from '@/models/Campaign'; import type { Campaign } from '@/models/Campaign';
import { displayName } from '@/models/User';
const props = defineProps<{ const props = defineProps<{
campaign: Campaign; campaign: Campaign;
@ -24,7 +32,48 @@ const props = defineProps<{
</script> </script>
<style scoped lang="less"> <style scoped lang="less">
.card { @import '@/assets/main';
.campaign {
min-width: 20rem; min-width: 20rem;
display: grid;
grid-template-areas:
'title gamemaster'
'players players';
width: 30rem;
.card;
.more;
.gap-1rem;
}
.campaign-title {
grid-area: title;
.h4;
}
.gamemaster {
grid-area: gamemaster;
text-align: right;
gap: 0.5rem;
}
.players {
grid-area: players;
width: 100%;
}
.player-list {
flex-wrap: wrap;
.ul-no-style;
.flex-row;
.gap-1rem;
.flex-size-even;
}
.player {
text-align: center;
min-width: 10rem;
.card;
.primary;
} }
</style> </style>

View File

@ -0,0 +1,41 @@
<template>
<div v-if="avatar" class="img-container">
<img :alt="altText" :src="avatar" :height="size" :width="size" />
</div>
<i v-else :class="`gdrico-${icon}`"></i>
</template>
<script setup lang="ts">
import type { SimpleUser } from '@/models/User';
import { usePocketbaseStore } from '@/stores/pocketbase';
import { computed, onMounted, ref } from 'vue';
const props = defineProps<{
user: SimpleUser;
size?: number; // in px
icon?: string;
}>();
const pbStore = usePocketbaseStore();
const icon = computed(() => props.icon ?? 'user');
const altText = computed(() => 'Avatar de ' + (props.user.name ?? props.user.username));
const size = computed(() => props.size ?? 20);
const avatar = ref<string | null>(null);
onMounted(() => {
props.user.avatarLink(pbStore).subscribe({
next: (link) => (avatar.value = link),
});
});
</script>
<style scoped lang="less">
.img-container {
border-radius: 1rem;
img {
object-fit: cover;
border-radius: 1rem;
}
}
</style>

View File

@ -0,0 +1,33 @@
<template>
<div class="flex-row flex-center user" :style="`justify-content: ${align}`">
<UserAvatar
:user="props.user"
:icon="type === 'gamemaster' ? 'user-tie' : 'user'"
:size="avatarSize"
:style="'width: ${avatarSize}px'" />
<span>{{ props.user.displayName() }}</span>
</div>
</template>
<script setup lang="ts">
import UserAvatar from './UserAvatar.vue';
import type { SimpleUser } from '@/models/User';
import { computed } from 'vue';
const props = defineProps<{
user: SimpleUser;
type?: 'gamemaster' | 'player';
avatarSize?: number;
align?: 'right' | 'center' | 'left';
}>();
const type = computed(() => props.type ?? 'player');
const avatarSize = computed(() => props.avatarSize ?? 20);
const align = computed(() => props.align ?? 'center');
</script>
<style scoped lang="less">
.user {
gap: 0.5rem;
}
</style>

18
src/models/Base.ts Normal file
View File

@ -0,0 +1,18 @@
import type { RecordModel } from "pocketbase"
export class CRecordModel implements RecordModel {
[key: string]: any;
id: string;
created: string;
updated: string;
collectionId: string;
collectionName: string;
constructor(from: RecordModel) {
this.id = from.id;
this.created = from.created;
this.updated = from.updated;
this.collectionId = from.collectionId;
this.collectionName = from.collectionName;
}
}

View File

@ -1,18 +1,49 @@
import { type RecordModel } from 'pocketbase'; import { type RecordModel } from 'pocketbase';
import { type User } from './User'; import { User, type IUser } from './User';
import { CRecordModel } from './Base';
interface CampaignDetails { interface ICampaignDetails {
game_master?: User; game_master?: IUser;
players?: User[]; players?: IUser[];
} }
export interface Campaign extends RecordModel { export interface ICampaign extends RecordModel {
expand?: CampaignDetails; expand?: CampaignDetails;
game_master: string; game_master: string;
name: string; name: string;
players: string[]; players: string[];
} }
class CampaignDetails implements ICampaignDetails {
game_master?: User;
players: User[] = [];
constructor(from: ICampaignDetails) {
if (from.game_master) {
this.game_master = new User(from.game_master);
}
if (from.players) {
from.players.forEach((player) => this.players.push(new User(player)));
}
}
}
export class Campaign extends CRecordModel implements ICampaign {
expand?: CampaignDetails;
game_master: string;
name: string;
players: string[];
constructor(from: ICampaign) {
super(from);
this.expand = undefined;
this.game_master = from.game_master;
this.name = from.name;
this.players = from.players;
this.expand = from.expand ? new CampaignDetails(from.expand) : undefined;
}
}
export interface NewCampaign { export interface NewCampaign {
game_master: string | null; game_master: string | null;
name: string | null; name: string | null;

View File

@ -1,20 +1,58 @@
import type { RecordModel } from 'pocketbase'; import { type RecordModel } from 'pocketbase';
import { CRecordModel } from './Base';
import { of, type Observable } from 'rxjs';
export interface SimpleUser extends RecordModel { export interface ISimpleUser extends RecordModel {
username: string; username: string;
name?: string; name?: string;
avatar?: string;
expand?: { [key: string]: any };
avatarLink: (pbStore: any) => Observable<string | null>;
} }
export interface User extends SimpleUser { export interface IUser extends SimpleUser {
avatar?: string;
email?: string; email?: string;
emailVisibility: boolean; emailVisibility: boolean;
verified: boolean; verified: boolean;
} }
export function displayName(user: SimpleUser): string { export class SimpleUser extends CRecordModel implements ISimpleUser {
if (user.name && user.name.trim() !== '') { avatar?: string;
return user.name; username: string;
name?: string;
expand?: { [key: string]: any };
constructor(from: ISimpleUser) {
super(from);
this.username = from.username;
this.name = from.name;
this.expand = from.expand;
this.avatar = from.avatar;
}
displayName(): string {
if (this.name && this.name.trim() !== '') {
return this.name;
}
return this.username;
}
avatarLink(pbStore: any, thumbSize: number = 100): Observable<string | null> {
return this.avatar
? (pbStore.users.avatar(this.id, thumbSize) as Observable<string | null>)
: of(null);
}
}
export class User extends SimpleUser implements IUser {
email?: string;
emailVisibility: boolean;
verified: boolean;
constructor(from: IUser) {
super(from);
this.email = from.email;
this.emailVisibility = from.emailVisibility;
this.verified = from.verified;
} }
return user.username;
} }

View File

@ -16,13 +16,18 @@ const router = createRouter({
{ {
path: '/campaigns', path: '/campaigns',
name: 'campaigns', name: 'campaigns',
component: () => import('@/views/CampaignsView.vue'), component: () => import('@/views/ListCampaignsView.vue'),
}, },
{ {
path: '/new-campaign', path: '/new-campaign',
name: 'new-campaign', name: 'new-campaign',
component: () => import('@/views/CreateCampaignView.vue'), component: () => import('@/views/CreateCampaignView.vue'),
}, },
{
path: '/campaign/:campaignId',
name: 'edit-campaign',
component: () => import('@/views/CampaignView.vue'),
},
], ],
}); });

View File

@ -3,11 +3,12 @@ import { defineStore } from 'pinia';
import { from, map, Observable, tap } from 'rxjs'; import { from, map, Observable, tap } from 'rxjs';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import type { Campaign, NewCampaign } from '@/models/Campaign'; import { Campaign, type ICampaign, type NewCampaign } from '@/models/Campaign';
import type { SimpleUser } from '@/models/User'; import { SimpleUser, type IUser } from '@/models/User';
export const usePocketbaseStore = defineStore('pocketbase', () => { export const usePocketbaseStore = defineStore('pocketbase', () => {
const pb = new PocketBase(import.meta.env.VITE_PB_URL); const pb = new PocketBase(import.meta.env.VITE_PB_URL);
pb.autoCancellation(false);
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
// Authentication // // Authentication //
@ -50,15 +51,23 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
function listCampaigns(): Observable<Campaign[]> { function listCampaigns(): Observable<Campaign[]> {
return from( return from(
pb.collection('campaigns_simple_view').getFullList<Campaign>({ pb.collection('campaigns_simple_view').getFullList<ICampaign>({
sort: 'name', sort: 'name',
expand: 'players,game_master', expand: 'players,game_master',
}) })
).pipe(map((campaigns) => campaigns.map((campaign) => new Campaign(campaign))));
}
function createCampaign(campaign: NewCampaign): Observable<Campaign> {
return from(pb.collection<ICampaign>('campaign').create(campaign)).pipe(
map((campaign) => new Campaign(campaign))
); );
} }
function createCampaign(campaign: NewCampaign): Observable<RecordModel> { function getCampaignById(id: string): Observable<Campaign> {
return from(pb.collection('campaign').create(campaign)); return from(pb.collection<ICampaign>('campaign').getOne(id)).pipe(
map((campaign) => new Campaign(campaign))
);
} }
///////////////////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////////////////
@ -70,10 +79,21 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
pb.collection('public_users').getFullList<SimpleUser>({ pb.collection('public_users').getFullList<SimpleUser>({
sort: 'username', sort: 'username',
}) })
).pipe(map((users) => users.map((user) => new SimpleUser(user))));
}
function userAvatar(userId: string, thumbSize: number = 100): Observable<string | null> {
return from(pb.collection<IUser>('users').getOne(userId)).pipe(
map((user) => {
return user.avatar
? pb.files.getUrl(user, user.avatar, { thumb: `${thumbSize}x${thumbSize}` })
: null;
})
); );
} }
return { return {
pb,
auth: { auth: {
authStore, authStore,
loggedIn, loggedIn,
@ -84,12 +104,14 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
logout, logout,
deleteAccount, deleteAccount,
}, },
campaign: { campaigns: {
listCampaigns, list: listCampaigns,
createCampaign, create: createCampaign,
get: getCampaignById,
}, },
users: { users: {
allUsersSimple, listSimple: allUsersSimple,
avatar: userAvatar,
}, },
}; };
}); });

View File

@ -3,7 +3,7 @@
<h1>Hello {{ username }}&nbsp;!</h1> <h1>Hello {{ username }}&nbsp;!</h1>
<div> <div>
<h2>Actions</h2> <h2>Actions</h2>
<ul class="no-style flex-col-center gap-1rem"> <ul class="ul-no-style flex-col-center gap-1rem">
<li> <li>
<button @click="logout()">Logout</button> <button @click="logout()">Logout</button>
</li> </li>

View File

@ -0,0 +1,23 @@
<template>
<div id="campaign">
<h1>Gestion de campagne</h1>
<h2>{{ campaign?.name }}</h2>
</div>
</template>
<script setup lang="ts">
import type { Campaign } from '@/models/Campaign';
import { usePocketbaseStore } from '@/stores/pocketbase';
import { onMounted, ref } from 'vue';
import { useRoute } from 'vue-router';
const route = useRoute();
const pbStore = usePocketbaseStore();
const campaign = ref<Campaign>();
onMounted(() => {
pbStore.campaigns.get(route.params.campaignId as string).subscribe({
next: (result) => (campaign.value = result),
});
});
</script>

View File

@ -11,7 +11,7 @@
<label class="flex-col gap-1rem" for="players" autocomplete="off">Joueurs (2 à 10)</label> <label class="flex-col gap-1rem" for="players" autocomplete="off">Joueurs (2 à 10)</label>
<select id="players" name="players" multiple v-model="campaign.players"> <select id="players" name="players" multiple v-model="campaign.players">
<option v-for="user in users" :key="user.id" :value="user.id"> <option v-for="user in users" :key="user.id" :value="user.id">
{{ displayName(user) }} {{ user.displayName() }}
</option> </option>
</select> </select>
@ -27,7 +27,7 @@ import { onMounted, ref } from 'vue';
import router from '@/router'; import router from '@/router';
import { usePocketbaseStore } from '@/stores/pocketbase'; import { usePocketbaseStore } from '@/stores/pocketbase';
import { type NewCampaign } from '@/models/Campaign'; import { type NewCampaign } from '@/models/Campaign';
import { type SimpleUser, displayName } from '@/models/User'; import { type SimpleUser } from '@/models/User';
const pbStore = usePocketbaseStore(); const pbStore = usePocketbaseStore();
const users = ref<SimpleUser[]>([]); const users = ref<SimpleUser[]>([]);
@ -39,7 +39,7 @@ const campaign = ref<NewCampaign>({
}); });
const createCampaign = () => { const createCampaign = () => {
pbStore.campaign.createCampaign(campaign.value).subscribe({ pbStore.campaigns.create(campaign.value).subscribe({
next: () => { next: () => {
router.push({ name: 'home' }); router.push({ name: 'home' });
}, },
@ -47,8 +47,10 @@ const createCampaign = () => {
}; };
onMounted(() => { onMounted(() => {
pbStore.users.allUsersSimple().subscribe({ pbStore.users.listSimple().subscribe({
next: (results) => (users.value = results.filter((user) => user.id !== pbStore.auth.userId)), next: (results) => {
users.value = results.filter((user) => user.id !== pbStore.auth.userId);
},
error: (err) => console.warn('Failed to fetch all users', err), error: (err) => console.warn('Failed to fetch all users', err),
}); });
}); });

View File

@ -1,35 +1,27 @@
<template> <template>
<h1>Campagnes</h1> <h1>Campagnes</h1>
<div class="flex-col gap-2rem">
<div class="flex-col flex-center"> <div class="flex-col flex-center">
<RouterLink :to="{ name: 'new-campaign' }" class="button">Créer une campagne</RouterLink> <RouterLink :to="{ name: 'new-campaign' }" class="button">Créer une campagne</RouterLink>
</div> </div>
<div class="flex-col gap-2rem">
<h2>Campagnes que je gère</h2> <h2>Campagnes que je gère</h2>
<div> <ul v-if="campaignsGameMaster.length > 0" class="campaign-list">
<ul <li v-for="campaign in campaignsGameMaster" :key="campaign.id" class="campaign">
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" /> <SmallCampaignCard :campaign="campaign" />
</li> </li>
</ul> </ul>
<div v-else>Pas de campagne pour linstant</div> <div v-else>Pas de campagne pour linstant</div>
</div> </div>
</div>
<div> <div>
<h2>Campagnes je joue</h2> <h2>Campagnes je joue</h2>
<div> <ul v-if="campaignsPlayer.length > 0" class="campaign-list">
<ul <li v-for="campaign in campaignsPlayer" :key="campaign.id">
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" /> <SmallCampaignCard :campaign="campaign" />
</li> </li>
</ul> </ul>
<div v-else class="card">Pas de campagne pour linstant</div> <div v-else class="card">Pas de campagne pour linstant</div>
</div> </div>
</div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -52,10 +44,26 @@ const campaignsPlayer = computed<Campaign[]>(() =>
); );
onMounted(() => { onMounted(() => {
pbStore.campaign.listCampaigns().subscribe({ pbStore.campaigns.list().subscribe({
next: (result) => (campaigns.value = result), next: (result) => (campaigns.value = result),
error: (err) => console.warn(err), error: (err) => console.warn(err),
complete: () => console.log('List campaigns completed'), complete: () => console.log('List campaigns completed'),
}); });
}); });
</script> </script>
<style scoped lang="less">
@import '@/assets/_mixins';
.campaign-list {
.ul-no-style;
.flex-wrap;
.flex-row;
.gap-2rem;
justify-content: space-around;
}
.campaign {
display: inline-block;
height: 100%;
}
</style>