diff --git a/app/composables/useDatabase.ts b/app/composables/useDatabase.ts index 622aa6a..d4fb931 100644 --- a/app/composables/useDatabase.ts +++ b/app/composables/useDatabase.ts @@ -1,6 +1,7 @@ import { Database } from "bun:sqlite"; import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite"; import * as schema from '../db/schema'; +import { eq, or, sql } from "drizzle-orm"; let instance: BunSQLiteDatabase & { $client: Database; diff --git a/app/types/campaign.d.ts b/app/types/campaign.d.ts new file mode 100644 index 0000000..683894b --- /dev/null +++ b/app/types/campaign.d.ts @@ -0,0 +1,25 @@ +import type { User } from "./auth"; +import type { Character, ItemState } from "./character"; +import type { Serialize } from 'nitropack'; + +export type CampaignVariables = { + money: number; + inventory: ItemState[]; +}; +export type Campaign = { + id: number; + name: string; + link: string; + owner: { id: number, username: string }; + members: Array<{ member: { id: number, username: string } }>; + characters: Array>; + public_notes: string; + dm_notes: string; + logs: CampaignLog[]; +} & CampaignVariables; +export type CampaignLog = { + from: number; + timestamp: Serialize; + type: 'ITEM' | 'CHARACTER' | 'PLACE' | 'EVENT' | 'FIGHT' | 'TEXT'; + details: string; +}; \ No newline at end of file diff --git a/db.sqlite b/db.sqlite index 9b536c3..76ab3e7 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/server/api/campaign/[id].get.ts b/server/api/campaign/[id].get.ts index 4cfc53e..42674c4 100644 --- a/server/api/campaign/[id].get.ts +++ b/server/api/campaign/[id].get.ts @@ -19,20 +19,17 @@ export default defineEventHandler(async (e) => { const db = useDatabase(); const data = db.query.campaignTable.findFirst({ + columns: { owner: false }, with: { - members: { with: { member: { columns: { username: true, id: true } } }, columns: { id: false, rights: false, user: false }, extras: { 'characters': sql`NULL`.as('characters') } }, - characters: { with: { character: { columns: { id: true, name: true, owner: true } } } }, + members: { with: { member: { columns: { username: true, id: true } } }, columns: { id: false, user: false } }, + characters: { with: { character: { columns: { id: true, name: true, owner: true } } }, columns: { character: false } }, owner: { columns: { username: true, id: true } }, - logs: { columns: { details: true, from: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp } + logs: { columns: { details: true, from: true, timestamp: true, type: true }, orderBy: ({ timestamp }) => timestamp }, }, where: ({ id: _id }) => eq(_id, parseInt(id, 10)), }).sync(); - if(data && (data.owner.id === session.user.id || data.members.find(e => e.member?.id === session.user!.id))) - { - data.members.forEach(e => e.characters = data.characters.filter(_e => _e.character?.owner === e.member?.id)); - return data as Campaign; - } + if(data && (data.owner.id === session.user.id || data.members.find(e => e.member?.id === session.user!.id))) return data as Campaign; else if(!data) return setResponseStatus(e, 404); else return setResponseStatus(e, 403); }); \ No newline at end of file diff --git a/server/api/character/[id].get.ts b/server/api/character/[id].get.ts index 3e2c989..a242622 100644 --- a/server/api/character/[id].get.ts +++ b/server/api/character/[id].get.ts @@ -1,7 +1,8 @@ import useDatabase from '~/composables/useDatabase'; -import { characterTable } from '~/db/schema'; +import { campaignCharactersTable, campaignMembersTable, campaignTable, characterAbilitiesTable, characterChoicesTable, characterLevelingTable, characterTable, characterTrainingTable, usersTable } from '~/db/schema'; import { group } from '#shared/general.util'; -import type { Character, CharacterVariables, Level, MainStat, TrainingLevel } from '~/types/character'; +import type { Character, MainStat, TrainingLevel } from '~/types/character'; +import { and, eq, exists, getTableColumns, isNotNull, or, sql } from 'drizzle-orm'; export default defineEventHandler(async (e) => { const id = getRouterParam(e, "id"); @@ -21,6 +22,7 @@ export default defineEventHandler(async (e) => { } const db = useDatabase(); + const character = db.query.characterTable.findFirst({ with: { abilities: true, @@ -29,9 +31,23 @@ export default defineEventHandler(async (e) => { choices: true, user: { columns: { username: true } + }, + campaign: { + columns: { character: false, id: false, }, + with: { + campaign: { + columns: { owner: true, }, + with: { + members: { columns: { user: true } } + } + } + } } }, - where: (character, { eq, and }) => and(eq(character.id, parseInt(id, 10)), eq(characterTable.owner, session.user!.id)), + where: and(eq(characterTable.id, parseInt(id, 10)), or(eq(characterTable.visibility, 'public'), eq(characterTable.owner, session.user!.id), exists(db.select({ id: sql`NULL` }).from(campaignCharactersTable) + .leftJoin(campaignTable, eq(campaignCharactersTable.id, campaignTable.id)) + .leftJoin(campaignMembersTable, and(eq(campaignMembersTable.id, campaignTable.id), eq(campaignMembersTable.user, session.user.id))) + .where(and(eq(campaignCharactersTable.character, parseInt(id, 10)), or(eq(campaignTable.owner, session.user.id), isNotNull(campaignMembersTable.user))))))), }).sync(); if(character !== undefined) diff --git a/shared/campaign.util.ts b/shared/campaign.util.ts index 1cd4265..da64185 100644 --- a/shared/campaign.util.ts +++ b/shared/campaign.util.ts @@ -5,6 +5,7 @@ import { div, dom, icon, span, text } from "#shared/dom.util"; import { button, loading } from "#shared/components.util"; import { CharacterCompiler } from "#shared/character.util"; import { tooltip } from "./floating.util"; +import type { Character } from "~/types/character"; export const CampaignValidation = z.object({ id: z.number(), @@ -27,13 +28,33 @@ async function copyLink() */ +class CharacterPrinter +{ + private compiler?: CharacterCompiler; + container: HTMLElement = div('flex flex-col gap-2'); + constructor(character: number, name: string) + { + this.container.replaceChildren(div('flex flex-row gap-1', [span(undefined, 'joue'), span('font-bold', name)]), loading('small')); + useRequestFetch()(`/api/character/${character}`).then((character) => { + if(character) + { + this.compiler = new CharacterCompiler(character); + this.container.replaceChildren(div('flex flex-row gap-1', [span(undefined, 'joue'), span('font-bold', name)]), ); + } + else throw new Error(); + }).catch((e) => { + console.error(e); + this.container.replaceChildren(span('text-sm italic text-light-red dark:text-dark-red', 'Données indisponible')); + }) + } +} type PlayerState = { status: boolean; statusDOM: HTMLElement; statusTooltip: Text; - user: User; + user: { id: number, username: string }; }; -function defaultPlayerState(user: User): PlayerState +function defaultPlayerState(user: { id: number, username: string }): PlayerState { const statusTooltip = text('Absent'); return { @@ -49,16 +70,21 @@ export class CampaignSheet campaign?: Campaign; container: HTMLElement = div('flex flex-1 h-full w-full items-start justify-center'); dm!: PlayerState; + players!: Array constructor(id: string, user: ComputedRef) { this.user = user; const load = div("flex justify-center items-center w-full h-full", [ loading('large') ]); this.container.replaceChildren(load); - useRequestFetch()(`/api/campaign/${id}`).then(campaign => { + useRequestFetch()(`/api/campaign/${id}`).then((campaign) => { if(campaign) { this.campaign = campaign; this.dm = defaultPlayerState(campaign.owner); + this.players = campaign.members.map(e => ({ + ...defaultPlayerState(e.member), + characters: campaign.characters.filter(_e => _e.character?.owner === e.member.id).map(_e => new CharacterPrinter(_e.character!.id, _e.character!.name)), + })); document.title = `d[any] - Campagne ${campaign.name}`; this.render(); @@ -77,14 +103,19 @@ export class CampaignSheet ])); }); } - render() + private render() { + const campaign = this.campaign; + + if(!campaign) + return; + this.container.replaceChildren(div('flex flex-col w-full h-full items-center gap-4', [ div('flex flex-row gap-8 items-center', [ - div('text-2xl font-semibold', [text(this.campaign.name)]), + div('text-2xl font-semibold', [text(campaign.name)]), div('border border-light-35 dark:border-dark-35 p-1 flex flex-row items-center gap-2', [ - dom('pre', { class: 'ps-1 w-[400px] truncate' }, [ text(`https://d-any.com/campaign/join/${ encodeURIComponent(this.campaign.link) }`) ]), - button(icon('radix-icons:clipboard', { width: 16, height: 16 })), + dom('pre', { class: 'ps-1 w-[400px] truncate' }, [ text(`https://d-any.com/campaign/join/${ encodeURIComponent(campaign.link) }`) ]), + button(icon('radix-icons:clipboard', { width: 16, height: 16 }), () => {}, 'p-1'), ]) ]), div('flex flex-row gap-4 flex-1', [ @@ -93,73 +124,29 @@ export class CampaignSheet div('flex flex-col divide-y divide-light-25 dark:divide-dark-25', [ div('flex flex-col py-1 my-1', [ div('flex flex-row items-center justify-between', [ - span(undefined, this.campaign.owner.username), + span(undefined, this.dm.user.username), this.dm.statusDOM, ]) ]) ]), div('flex flex-row items-center gap-4', [ span('text-lg font-bold tracking-tight', 'Joueurs'), span('border-b border-dashed border-light-35 dark:border-dark-35 flex-1') ]), div('flex flex-col divide-y divide-light-25 dark:divide-dark-25', - this.campaign.members.length > 0 ? this.campaign.members.map((player: any) => div('flex flex-col py-1 my-1', [ + this.players.length > 0 ? this.players.map((player) => div('flex flex-col py-1 my-1', [ div('flex flex-row items-center justify-between', [ - span(undefined, player.member.username), - undefined, + span(undefined, player.user.username), + player.statusDOM, ]), - player.characters.length > 0 ? div('flex flex-col', - player.characters.map((character: any) => div('flex flex-row items-center justify-between', [ - span(undefined, 'joue'), span('text-bold', character.name) + player.characters && player.characters.length > 0 ? div('flex flex-col', + player.characters.map((character) => div('flex flex-row items-center justify-between', [ + character.container ])) ) : span('text-sm italic text-light-70 dark:text-dark-70', 'Pas de personnages'), - ])) : span('text-sm italic py-2 text-center', 'Invitez des joueurs via le lien'), + ])) : [span('text-sm italic py-2 text-center', 'Invitez des joueurs via le lien')], ) ]), div('border-l border-light-35 dark:border-dark-35'), div('flex flex-col divide-y divide-light-25 dark:divide-dark-25 w-[800px]') ]) ])); - /* -
-
-
-
Maitre de jeu
-
-
-
- {{ campaign.owner.username }} - -
-
-
-
Joueurs
-
- - -
-
-
-
-
-
-
- */ } } \ No newline at end of file diff --git a/shared/character.util.ts b/shared/character.util.ts index 40f9223..e40e520 100644 --- a/shared/character.util.ts +++ b/shared/character.util.ts @@ -1359,7 +1359,7 @@ export class CharacterSheet readonly: dom("span", { class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35", text: `${character.health - character.variables.health}`, - listeners: { click: () => health.readonly.replaceWith(health.edit) }, + listeners: { click: () => { health.readonly.replaceWith(health.edit); health.edit.select(); health.edit.focus(); } }, }), edit: input('text', { defaultValue: (character.health - character.variables.health).toString(), input: (v) => { return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10)); @@ -1370,7 +1370,7 @@ export class CharacterSheet readonly: dom("span", { class: "font-bold px-2 border-transparent border cursor-pointer hover:border-light-35 dark:hover:border-dark-35", text: `${character.mana - character.variables.mana}`, - listeners: { click: () => mana.readonly.replaceWith(mana.edit) }, + listeners: { click: () => { mana.readonly.replaceWith(mana.edit); mana.edit.select(); mana.edit.focus(); } }, }), edit: input('text', { defaultValue: (character.mana - character.variables.mana).toString(), input: (v) => { return v.startsWith('-') || v.startsWith('+') ? v.length === 1 || !isNaN(parseInt(v.substring(1), 10)) : v.length === 0 || !isNaN(parseInt(v, 10));