diff --git a/public/assets/fonts/gdrico.eot b/public/assets/fonts/gdrico.eot new file mode 100644 index 0000000..9e91735 Binary files /dev/null and b/public/assets/fonts/gdrico.eot differ diff --git a/public/assets/fonts/gdrico.svg b/public/assets/fonts/gdrico.svg new file mode 100644 index 0000000..7544752 --- /dev/null +++ b/public/assets/fonts/gdrico.svg @@ -0,0 +1,26 @@ + + + +Generated by IcoMoon + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/fonts/gdrico.ttf b/public/assets/fonts/gdrico.ttf new file mode 100644 index 0000000..2f4f4ef Binary files /dev/null and b/public/assets/fonts/gdrico.ttf differ diff --git a/public/assets/fonts/gdrico.woff b/public/assets/fonts/gdrico.woff new file mode 100644 index 0000000..8af4ae1 Binary files /dev/null and b/public/assets/fonts/gdrico.woff differ diff --git a/src/App.vue b/src/App.vue index 58763c1..239a592 100644 --- a/src/App.vue +++ b/src/App.vue @@ -18,7 +18,7 @@ main { margin: 5rem; align-content: center; padding-top: 4.5rem !important; - padding-bottom: 7rem !important; + padding-bottom: 10rem !important; } header { diff --git a/src/assets/_fonts.less b/src/assets/_fonts.less new file mode 100644 index 0000000..0a3b944 --- /dev/null +++ b/src/assets/_fonts.less @@ -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'; +} diff --git a/src/assets/_layouts.less b/src/assets/_layouts.less index a3d050c..60943e3 100644 --- a/src/assets/_layouts.less +++ b/src/assets/_layouts.less @@ -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; + } +} diff --git a/src/assets/_mixins.less b/src/assets/_mixins.less index 687b484..bfccb07 100644 --- a/src/assets/_mixins.less +++ b/src/assets/_mixins.less @@ -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; +} diff --git a/src/assets/components/cards.less b/src/assets/components/cards.less index 96a51db..b2f99d2 100644 --- a/src/assets/components/cards.less +++ b/src/assets/components/cards.less @@ -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 { diff --git a/src/assets/components/titles.less b/src/assets/components/titles.less index 4cb492b..0861a01 100644 --- a/src/assets/components/titles.less +++ b/src/assets/components/titles.less @@ -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 { diff --git a/src/assets/forms.less b/src/assets/forms.less new file mode 100644 index 0000000..e69de29 diff --git a/src/assets/main.less b/src/assets/main.less index 23c485b..a4f373a 100644 --- a/src/assets/main.less +++ b/src/assets/main.less @@ -2,6 +2,7 @@ @import '@fontsource/roboto'; @import '@fontsource/poppins'; @import '_theme'; +@import '_fonts'; @import '_mixins'; @import '_layouts'; diff --git a/src/components/AppFooter.vue b/src/components/AppFooter.vue index 2b90f1d..edfc86c 100644 --- a/src/components/AppFooter.vue +++ b/src/components/AppFooter.vue @@ -24,6 +24,7 @@ const version = __APP_VERSION__; footer { margin: 2rem; + margin-top: 2rem; position: absolute; left: 0; right: 0; diff --git a/src/components/AppHeader.vue b/src/components/AppHeader.vue index 481e0ef..1b938ac 100644 --- a/src/components/AppHeader.vue +++ b/src/components/AppHeader.vue @@ -2,9 +2,18 @@
{{ appTitle }}
- + + - Compte + +
+ +
Compte
+
+
@@ -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; +} diff --git a/src/components/LoggedOutHome.vue b/src/components/LoggedOutHome.vue index 4eb4412..2956775 100644 --- a/src/components/LoggedOutHome.vue +++ b/src/components/LoggedOutHome.vue @@ -6,13 +6,33 @@

L'application web pour vous accompagner pour votre JDR avec Gégé, le bot discord.

-
+

Pourquoi ?

-
+ + diff --git a/src/components/SmallCampaignCard.vue b/src/components/SmallCampaignCard.vue index 6935257..42d5fc3 100644 --- a/src/components/SmallCampaignCard.vue +++ b/src/components/SmallCampaignCard.vue @@ -1,22 +1,30 @@ diff --git a/src/components/UserAvatar.vue b/src/components/UserAvatar.vue new file mode 100644 index 0000000..99c4246 --- /dev/null +++ b/src/components/UserAvatar.vue @@ -0,0 +1,41 @@ + + + + + diff --git a/src/components/UserAvatarAndName.vue b/src/components/UserAvatarAndName.vue new file mode 100644 index 0000000..a30ebd7 --- /dev/null +++ b/src/components/UserAvatarAndName.vue @@ -0,0 +1,33 @@ + + + + + diff --git a/src/models/Base.ts b/src/models/Base.ts new file mode 100644 index 0000000..07cf24d --- /dev/null +++ b/src/models/Base.ts @@ -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; + } +} diff --git a/src/models/Campaign.ts b/src/models/Campaign.ts index 49bca5d..d9f89f0 100644 --- a/src/models/Campaign.ts +++ b/src/models/Campaign.ts @@ -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; diff --git a/src/models/User.ts b/src/models/User.ts index dd46889..5af1089 100644 --- a/src/models/User.ts +++ b/src/models/User.ts @@ -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; } -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 { + return this.avatar + ? (pbStore.users.avatar(this.id, thumbSize) as Observable) + : 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; } diff --git a/src/router/index.ts b/src/router/index.ts index 7c0dcdb..dd12e5f 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -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'), + }, ], }); diff --git a/src/stores/pocketbase.ts b/src/stores/pocketbase.ts index f7ffa3e..918e346 100644 --- a/src/stores/pocketbase.ts +++ b/src/stores/pocketbase.ts @@ -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 { return from( - pb.collection('campaigns_simple_view').getFullList({ + pb.collection('campaigns_simple_view').getFullList({ sort: 'name', expand: 'players,game_master', }) + ).pipe(map((campaigns) => campaigns.map((campaign) => new Campaign(campaign)))); + } + + function createCampaign(campaign: NewCampaign): Observable { + return from(pb.collection('campaign').create(campaign)).pipe( + map((campaign) => new Campaign(campaign)) ); } - function createCampaign(campaign: NewCampaign): Observable { - return from(pb.collection('campaign').create(campaign)); + function getCampaignById(id: string): Observable { + return from(pb.collection('campaign').getOne(id)).pipe( + map((campaign) => new Campaign(campaign)) + ); } ///////////////////////////////////////////////////////////////////////////// @@ -70,10 +79,21 @@ export const usePocketbaseStore = defineStore('pocketbase', () => { pb.collection('public_users').getFullList({ sort: 'username', }) + ).pipe(map((users) => users.map((user) => new SimpleUser(user)))); + } + + function userAvatar(userId: string, thumbSize: number = 100): Observable { + return from(pb.collection('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, }, }; }); diff --git a/src/views/AccountView.vue b/src/views/AccountView.vue index b8a066f..8ea4293 100644 --- a/src/views/AccountView.vue +++ b/src/views/AccountView.vue @@ -3,7 +3,7 @@

Hello {{ username }} !

Actions

-
    +
    • diff --git a/src/views/CampaignView.vue b/src/views/CampaignView.vue new file mode 100644 index 0000000..7fafd32 --- /dev/null +++ b/src/views/CampaignView.vue @@ -0,0 +1,23 @@ + + + diff --git a/src/views/CreateCampaignView.vue b/src/views/CreateCampaignView.vue index 7b6b3f2..49ac516 100644 --- a/src/views/CreateCampaignView.vue +++ b/src/views/CreateCampaignView.vue @@ -11,7 +11,7 @@ @@ -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([]); @@ -39,7 +39,7 @@ const campaign = ref({ }); 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), }); }); diff --git a/src/views/CampaignsView.vue b/src/views/ListCampaignsView.vue similarity index 52% rename from src/views/CampaignsView.vue rename to src/views/ListCampaignsView.vue index d945496..2d97ea4 100644 --- a/src/views/CampaignsView.vue +++ b/src/views/ListCampaignsView.vue @@ -1,34 +1,26 @@ @@ -52,10 +44,26 @@ const campaignsPlayer = computed(() => ); 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'), }); }); + +