Some more stuff
Sorry, I’m in a rush, can’t be bothered to properly commit
This commit is contained in:
@@ -18,7 +18,7 @@ main {
|
||||
margin: 5rem;
|
||||
align-content: center;
|
||||
padding-top: 4.5rem !important;
|
||||
padding-bottom: 7rem !important;
|
||||
padding-bottom: 10rem !important;
|
||||
}
|
||||
|
||||
header {
|
||||
|
||||
76
src/assets/_fonts.less
Normal file
76
src/assets/_fonts.less
Normal 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';
|
||||
}
|
||||
@@ -17,8 +17,14 @@
|
||||
figure,
|
||||
img {
|
||||
grid-area: figure;
|
||||
max-width: 100%;
|
||||
max-height: 10rem;
|
||||
max-width: min(10rem, 25%);
|
||||
max-height: 100%;
|
||||
border-radius: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media all and (max-width: 400px) {
|
||||
.two-col-img-text {
|
||||
gap: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -46,7 +46,6 @@
|
||||
.flex-size-even {
|
||||
* {
|
||||
flex: 1;
|
||||
flex-basis: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,3 +90,11 @@
|
||||
.text-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.ul-no-style {
|
||||
list-style: none;
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@
|
||||
border-radius: 2rem;
|
||||
padding: 2rem;
|
||||
.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 {
|
||||
.themed(background-color, @light-background-200, @dark-background-200);
|
||||
@@ -17,6 +22,16 @@
|
||||
&.primary {
|
||||
.themed(background-color, @light-primary, @dark-primary);
|
||||
.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 {
|
||||
|
||||
@@ -17,39 +17,35 @@ h5 {
|
||||
.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 {
|
||||
font-size: 100%;
|
||||
}
|
||||
|
||||
@h1-size: 4.21rem;
|
||||
.h1,
|
||||
h1 {
|
||||
font-size: @h1-size;
|
||||
}
|
||||
|
||||
@h2-size: 3.158rem;
|
||||
.h2,
|
||||
h2 {
|
||||
font-size: @h2-size;
|
||||
}
|
||||
|
||||
@h3-size: 2.369rem;
|
||||
.h3,
|
||||
h3 {
|
||||
font-size: @h3-size;
|
||||
}
|
||||
|
||||
@h4-size: 1.777rem;
|
||||
.h4,
|
||||
h4 {
|
||||
font-size: @h4-size;
|
||||
}
|
||||
|
||||
@h5-size: 1.333rem;
|
||||
.h5,
|
||||
h5 {
|
||||
font-size: @h5-size;
|
||||
}
|
||||
|
||||
.tag-font-size(h1, @h1-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 {
|
||||
|
||||
0
src/assets/forms.less
Normal file
0
src/assets/forms.less
Normal file
@@ -2,6 +2,7 @@
|
||||
@import '@fontsource/roboto';
|
||||
@import '@fontsource/poppins';
|
||||
@import '_theme';
|
||||
@import '_fonts';
|
||||
@import '_mixins';
|
||||
@import '_layouts';
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ const version = __APP_VERSION__;
|
||||
|
||||
footer {
|
||||
margin: 2rem;
|
||||
margin-top: 2rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
|
||||
@@ -2,9 +2,18 @@
|
||||
<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 ? '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>
|
||||
<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>
|
||||
</header>
|
||||
</template>
|
||||
@@ -20,7 +29,6 @@ const appTitle = import.meta.env.VITE_NAME;
|
||||
|
||||
const isDark = useDark();
|
||||
const toggleDark = useToggle(isDark);
|
||||
|
||||
const pbStore = usePocketbaseStore();
|
||||
const loggedIn = ref(pbStore.auth.loggedIn);
|
||||
|
||||
@@ -45,4 +53,8 @@ header {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
#account {
|
||||
gap: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,13 +6,33 @@
|
||||
<h2 class="card">
|
||||
L'application web pour vous accompagner pour votre JDR avec Gégé, le bot discord.
|
||||
</h2>
|
||||
<div class="flex-col-center flex-size-even gap-1rem">
|
||||
<div>
|
||||
<h3>Pourquoi ?</h3>
|
||||
<ul class="flex-row gap-1rem no-style">
|
||||
<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 class="features">
|
||||
<li class="feature">Créer ses personnages</li>
|
||||
<li class="feature">Gérer ses fiche personnage</li>
|
||||
<li class="feature">Faire évoluer ses personnage</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button class="h4 raise margin-3rem">Se connecter avec Discord</button>
|
||||
</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>
|
||||
|
||||
@@ -1,22 +1,30 @@
|
||||
<template>
|
||||
<div class="card primary flex-col gap-1rem">
|
||||
<div class="h4" id="name">
|
||||
{{ props.campaign.name }}
|
||||
<RouterLink :to="{ name: 'edit-campaign', params: { campaignId: props.campaign.id } }">
|
||||
<div class="campaign">
|
||||
<div class="campaign-title">
|
||||
{{ props.campaign.name }}
|
||||
</div>
|
||||
<div class="flex-col gamemaster">
|
||||
<div>Maître jeu</div>
|
||||
<div class="user">
|
||||
<UserAvatarAndName :user="props.campaign.expand!.game_master!" :align="'right'" />
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex-col gap-1rem players">
|
||||
<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>
|
||||
</ul>
|
||||
</div>
|
||||
</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>
|
||||
</RouterLink>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import UserAvatarAndName from './UserAvatarAndName.vue';
|
||||
import type { Campaign } from '@/models/Campaign';
|
||||
import { displayName } from '@/models/User';
|
||||
|
||||
const props = defineProps<{
|
||||
campaign: Campaign;
|
||||
@@ -24,7 +32,48 @@ const props = defineProps<{
|
||||
</script>
|
||||
|
||||
<style scoped lang="less">
|
||||
.card {
|
||||
@import '@/assets/main';
|
||||
|
||||
.campaign {
|
||||
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>
|
||||
|
||||
41
src/components/UserAvatar.vue
Normal file
41
src/components/UserAvatar.vue
Normal 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>
|
||||
33
src/components/UserAvatarAndName.vue
Normal file
33
src/components/UserAvatarAndName.vue
Normal 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
18
src/models/Base.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,49 @@
|
||||
import { type RecordModel } from 'pocketbase';
|
||||
import { type User } from './User';
|
||||
import { User, type IUser } from './User';
|
||||
import { CRecordModel } from './Base';
|
||||
|
||||
interface CampaignDetails {
|
||||
game_master?: User;
|
||||
players?: User[];
|
||||
interface ICampaignDetails {
|
||||
game_master?: IUser;
|
||||
players?: IUser[];
|
||||
}
|
||||
|
||||
export interface Campaign extends RecordModel {
|
||||
export interface ICampaign extends RecordModel {
|
||||
expand?: CampaignDetails;
|
||||
game_master: string;
|
||||
name: 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 {
|
||||
game_master: string | null;
|
||||
name: string | null;
|
||||
|
||||
@@ -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;
|
||||
name?: string;
|
||||
avatar?: string;
|
||||
expand?: { [key: string]: any };
|
||||
avatarLink: (pbStore: any) => Observable<string | null>;
|
||||
}
|
||||
|
||||
export interface User extends SimpleUser {
|
||||
avatar?: string;
|
||||
export interface IUser extends SimpleUser {
|
||||
email?: string;
|
||||
emailVisibility: boolean;
|
||||
verified: boolean;
|
||||
}
|
||||
|
||||
export function displayName(user: SimpleUser): string {
|
||||
if (user.name && user.name.trim() !== '') {
|
||||
return user.name;
|
||||
export class SimpleUser extends CRecordModel implements ISimpleUser {
|
||||
avatar?: string;
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -16,13 +16,18 @@ const router = createRouter({
|
||||
{
|
||||
path: '/campaigns',
|
||||
name: 'campaigns',
|
||||
component: () => import('@/views/CampaignsView.vue'),
|
||||
component: () => import('@/views/ListCampaignsView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/new-campaign',
|
||||
name: 'new-campaign',
|
||||
component: () => import('@/views/CreateCampaignView.vue'),
|
||||
},
|
||||
{
|
||||
path: '/campaign/:campaignId',
|
||||
name: 'edit-campaign',
|
||||
component: () => import('@/views/CampaignView.vue'),
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@ import { defineStore } from 'pinia';
|
||||
import { from, map, Observable, tap } from 'rxjs';
|
||||
import { computed, ref } from 'vue';
|
||||
|
||||
import type { Campaign, NewCampaign } from '@/models/Campaign';
|
||||
import type { SimpleUser } from '@/models/User';
|
||||
import { Campaign, type ICampaign, type NewCampaign } from '@/models/Campaign';
|
||||
import { SimpleUser, type IUser } from '@/models/User';
|
||||
|
||||
export const usePocketbaseStore = defineStore('pocketbase', () => {
|
||||
const pb = new PocketBase(import.meta.env.VITE_PB_URL);
|
||||
pb.autoCancellation(false);
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////
|
||||
// Authentication //
|
||||
@@ -50,15 +51,23 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
|
||||
|
||||
function listCampaigns(): Observable<Campaign[]> {
|
||||
return from(
|
||||
pb.collection('campaigns_simple_view').getFullList<Campaign>({
|
||||
pb.collection('campaigns_simple_view').getFullList<ICampaign>({
|
||||
sort: 'name',
|
||||
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> {
|
||||
return from(pb.collection('campaign').create(campaign));
|
||||
function getCampaignById(id: string): Observable<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>({
|
||||
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 {
|
||||
pb,
|
||||
auth: {
|
||||
authStore,
|
||||
loggedIn,
|
||||
@@ -84,12 +104,14 @@ export const usePocketbaseStore = defineStore('pocketbase', () => {
|
||||
logout,
|
||||
deleteAccount,
|
||||
},
|
||||
campaign: {
|
||||
listCampaigns,
|
||||
createCampaign,
|
||||
campaigns: {
|
||||
list: listCampaigns,
|
||||
create: createCampaign,
|
||||
get: getCampaignById,
|
||||
},
|
||||
users: {
|
||||
allUsersSimple,
|
||||
listSimple: allUsersSimple,
|
||||
avatar: userAvatar,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<h1>Hello {{ username }} !</h1>
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
<ul class="no-style flex-col-center gap-1rem">
|
||||
<ul class="ul-no-style flex-col-center gap-1rem">
|
||||
<li>
|
||||
<button @click="logout()">Logout</button>
|
||||
</li>
|
||||
|
||||
23
src/views/CampaignView.vue
Normal file
23
src/views/CampaignView.vue
Normal 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>
|
||||
@@ -11,7 +11,7 @@
|
||||
<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) }}
|
||||
{{ user.displayName() }}
|
||||
</option>
|
||||
</select>
|
||||
|
||||
@@ -27,7 +27,7 @@ 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';
|
||||
import { type SimpleUser } from '@/models/User';
|
||||
|
||||
const pbStore = usePocketbaseStore();
|
||||
const users = ref<SimpleUser[]>([]);
|
||||
@@ -39,7 +39,7 @@ const campaign = ref<NewCampaign>({
|
||||
});
|
||||
|
||||
const createCampaign = () => {
|
||||
pbStore.campaign.createCampaign(campaign.value).subscribe({
|
||||
pbStore.campaigns.create(campaign.value).subscribe({
|
||||
next: () => {
|
||||
router.push({ name: 'home' });
|
||||
},
|
||||
@@ -47,8 +47,10 @@ const createCampaign = () => {
|
||||
};
|
||||
|
||||
onMounted(() => {
|
||||
pbStore.users.allUsersSimple().subscribe({
|
||||
next: (results) => (users.value = results.filter((user) => user.id !== pbStore.auth.userId)),
|
||||
pbStore.users.listSimple().subscribe({
|
||||
next: (results) => {
|
||||
users.value = results.filter((user) => user.id !== pbStore.auth.userId);
|
||||
},
|
||||
error: (err) => console.warn('Failed to fetch all users', err),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,26 @@
|
||||
<template>
|
||||
<h1>Campagnes</h1>
|
||||
<div class="flex-col flex-center">
|
||||
<RouterLink :to="{ name: 'new-campaign' }" class="button">Créer une campagne</RouterLink>
|
||||
</div>
|
||||
<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>
|
||||
<ul v-if="campaignsGameMaster.length > 0" class="campaign-list">
|
||||
<li v-for="campaign in campaignsGameMaster" :key="campaign.id" class="campaign">
|
||||
<SmallCampaignCard :campaign="campaign" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<div v-else>Pas de campagne pour l’instant</div>
|
||||
</div>
|
||||
<div v-else>Pas de campagne pour l’instant</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>
|
||||
<ul v-if="campaignsPlayer.length > 0" class="campaign-list">
|
||||
<li v-for="campaign in campaignsPlayer" :key="campaign.id">
|
||||
<SmallCampaignCard :campaign="campaign" />
|
||||
</li>
|
||||
</ul>
|
||||
<div v-else class="card">Pas de campagne pour l’instant</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,10 +44,26 @@ const campaignsPlayer = computed<Campaign[]>(() =>
|
||||
);
|
||||
|
||||
onMounted(() => {
|
||||
pbStore.campaign.listCampaigns().subscribe({
|
||||
pbStore.campaigns.list().subscribe({
|
||||
next: (result) => (campaigns.value = result),
|
||||
error: (err) => console.warn(err),
|
||||
complete: () => console.log('List campaigns completed'),
|
||||
});
|
||||
});
|
||||
</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>
|
||||
Reference in New Issue
Block a user