diff --git a/src/components/gameComponent.vue b/src/components/gameComponent.vue index 9ef1bcf7279b7ff7db9a37890bd5c442f7d8b0d6..0f53b219c0e411e7cf5af9cd90e5b580da50e5de 100644 --- a/src/components/gameComponent.vue +++ b/src/components/gameComponent.vue @@ -17,22 +17,20 @@ import type { InstitutionData } from '@/model/InstitutionData' import type { UserData } from '@/model/userData' import type { DocumentData } from '@/model/DocumentData' - onMounted(async () => { settings.SCALE_MODE = SCALE_MODES.NEAREST const app = new Application<HTMLCanvasElement>({ - view: document.getElementById("pixi-canvas") as HTMLCanvasElement, + view: document.getElementById('pixi-canvas') as HTMLCanvasElement, resolution: window.devicePixelRatio || 2, autoDensity: true, backgroundColor: 0x000000, - width: document.getElementById("pixi-content")?.clientWidth, - height: document.getElementById("pixi-content")?.clientHeight - }); + width: document.getElementById('pixi-content')?.clientWidth, + height: document.getElementById('pixi-content')?.clientHeight + }) const userStore = useUserStore() const chatStore = useChatStore() - const socket = io("https://game-service-w6pzbdwekq-ez.a.run.app/") - + const socket = io('https://game-service-w6pzbdwekq-ez.a.run.app/') // create viewport const viewport = new Viewport({ @@ -44,8 +42,8 @@ onMounted(async () => { viewport.scale.set(2, 2) const map = Sprite.from(img) - map.anchor.set(0.5, 0.5); - map.scale.set(2, 2); + map.anchor.set(0.5, 0.5) + map.scale.set(2, 2) map.position.set(496, 496) viewport.addChild(map) @@ -53,9 +51,9 @@ onMounted(async () => { let chatTables: ChatTable[] = [] let institutionSigns: InstitutionSign[] = [] let uid_players: string[] = [] - console.log("user in userStore is equal to : " + userStore.user?.uid) + console.log('user in userStore is equal to : ' + userStore.user?.uid) if (userStore.user !== null) { - console.log("Add Hero to the game !") + console.log('Add Hero to the game !') const hero = new Hero(socket, viewport, userStore.uid) chatStore.setHero(hero) } @@ -65,10 +63,6 @@ onMounted(async () => { new ChatTable("Institution 2", 100, 230, viewport, chatTables) new ChatTable("Institution 3", 300, 490, viewport, chatTables) - new LibraryAction("heia", 550, 440, viewport, chatTables) - new LibraryAction("heds", 350, 440, viewport, chatTables) - new LibraryAction("heg", 550, 180, viewport, chatTables) - const institutionSign_1 = new InstitutionSign({ InstitutionID: "1", InstitutionName: "Icosys", @@ -97,30 +91,43 @@ onMounted(async () => { library: null }, 300, 440, viewport) institutionSigns.push(institutionSign_1) + + const institution_test = { + InstitutionID: 'institution_test', + InstitutionName: 'Icosys', + InstitutionType: 'Technology', + InstitutionUrl: 'https://icosys.ch/', + members: [], + mailAddress: 'icosys@hefr.ch', + library: null + } as InstitutionData + + new LibraryAction(institution_test, 550, 440, viewport, chatTables) + new LibraryAction(institution_test, 350, 440, viewport, chatTables) + new LibraryAction(institution_test, 550, 180, viewport, chatTables) + const arbre = Sprite.from(arbre_sprite) - arbre.anchor.set(0.5, 0.5); - arbre.scale.set(2, 2); + arbre.anchor.set(0.5, 0.5) + arbre.scale.set(2, 2) arbre.position.set(496, 496) - arbre.eventMode = "none" + arbre.eventMode = 'none' viewport.addChild(arbre) app.stage.addChild(viewport) - //================================================ // WEBSOCKET CONNECTIONS ON GAME LOAD //================================================ - /** * Load the existing players on the scene * * */ - socket.on("new player", (msg) => { - console.log("New Player connected, here is the list of all connected players \n", msg.players) - console.log("this is the local list of connected players \n " + uid_players) + socket.on('new player', (msg) => { + console.log('New Player connected, here is the list of all connected players \n', msg.players) + console.log('this is the local list of connected players \n ' + uid_players) if (msg.uid == userStore.user?.uid) { - console.log("USER ID = " + getAuth().currentUser?.uid) + console.log('USER ID = ' + getAuth().currentUser?.uid) const hero = new Hero(socket, viewport, userStore.uid) chatStore.setHero(hero) } @@ -139,7 +146,7 @@ onMounted(async () => { * Move the player that triggers a movement * */ - socket.on("player moving", (msg) => { + socket.on('player moving', (msg) => { for (const player of players) { if (player.uid === msg.uid) { player.movePlayer(msg.direction) @@ -151,35 +158,32 @@ onMounted(async () => { * remove the player that trigger a disconnection * */ - socket.on("player disconnected", (msg) => { - console.log("player has been disconnected :" + msg.uid) + socket.on('player disconnected', (msg) => { + console.log('player has been disconnected :' + msg.uid) const index = uid_players.indexOf(msg.uid) uid_players.splice(index, 1) for (const player of players) { if (player.uid === msg.uid) { player.removePlayerFromScene() } - } - }) - /** * Emit an event to the websocket before the window close to inform * other player that the current player is leaving the game * */ - window.addEventListener('beforeunload', function() { - socket.emit("close connection", { uid: userStore.uid }) - }); + window.addEventListener('beforeunload', function () { + socket.emit('close connection', { uid: userStore.uid }) + }) /** * resize the game * */ - window.addEventListener('resize', function() { - location.reload(); + window.addEventListener('resize', function () { + location.reload() }) }) </script> @@ -198,4 +202,4 @@ onMounted(async () => { margin: 0; overflow: hidden; } -</style> \ No newline at end of file +</style> diff --git a/src/components/libraryComponent.vue b/src/components/libraryComponent.vue index 36c28311c27c84ef688108dad40ca3d64fbfd68b..d915c2f56e7d45f9a3ac50bcb11f8a1418715ee5 100644 --- a/src/components/libraryComponent.vue +++ b/src/components/libraryComponent.vue @@ -1,72 +1,167 @@ <template> - <div class="library-ui"> - <v-col cols="12"> - <v-row> - <v-col cols="10"> - <p class="text-h5 font-weight-bold">{{selectedInstitution?.InstitutionName}}</p> - </v-col> - <v-col cols="2"> - <v-btn class="text-center mb-2" icon="mdi-close" variant="flat" color="tertiary_container" @click="closeLibrary()"> - </v-btn> - </v-col> - </v-row> - - <v-divider></v-divider> - </v-col> - <ul> - <li v-for="file in selectedLibrary?.documents" :key="file.title"> - <a href="#" @click.prevent="openFile(file.url)">{{ file.title }}</a> - </li> - </ul> + <v-sheet class="pa-6 mx-auto rounded-xl d-flex flex-column" width="100%" height="90%" color="surface"> + <v-row justify="end"> + <v-btn icon="mdi-close" variant="flat" color="primary_container" size="large" @click="closeLibrary"></v-btn> + </v-row> + + <p class="text-h5 font-weight-bold mb-6"> + {{ selectedInstitution?.InstitutionName }} + </p> + + <div v-if="!loading && selectedLibrary?.documents?.length"> + <v-list density="compact"> + <v-list-subheader>Documents</v-list-subheader> + <v-list-item v-for="file in selectedLibrary.documents" :key="file.title" class="hoverable list-item-with-delete" + clickable :ripple="false"> + <!-- Delete Button --> + <v-btn icon="mdi-delete" size="x-small" variant="flat" color="red-lighten-2" class="delete-btn" + @click.stop="deleteDocument(file)" /> + + <v-list-item-content @click="openFile(file.url)"> + <v-list-item-title class="text-primary font-weight-medium d-flex align-center"> + <v-icon size="18" class="mr-2 text-primary">mdi-open-in-new</v-icon> + <span>{{ file.title }}</span> + </v-list-item-title> + </v-list-item-content> + </v-list-item> + </v-list> + </div> + + <div v-else-if="!loading"> + <v-alert type="info" color="blue-lighten-3" border="start"> + Aucun document disponible. + </v-alert> </div> - </template> - - <script setup lang="ts"> - import { onMounted } from 'vue'; - import { useLibraryStore } from '@/stores/libraryStore'; - import { useInstitutionStore } from '@/stores/institutionStore'; - import { useUiStore } from '@/stores/uiStore'; - import { storeToRefs } from 'pinia'; - - const libraryStore = useLibraryStore(); - const institutionStore = useInstitutionStore(); - const uiStore = useUiStore(); - - const { libraries, selectedLibrary } = storeToRefs(libraryStore); - const {selectedInstitution} = storeToRefs(institutionStore); - - onMounted(() => { - libraryStore.fetchLibraries(); - const selectedLibrary = libraryStore.libraries.find( - (library) => library.InstitutionID === selectedInstitution.value?.InstitutionID); - -if (selectedLibrary) { - libraryStore.openLibrary(selectedLibrary); + + <div v-else> + <v-progress-linear indeterminate color="primary" /> + </div> + + <div class="add-doc-container d-flex align-center"> + <input ref="fileInput" type="file" style="display: none" @change="handleFileChange" /> + <v-btn icon="mdi-plus" variant="flat" color="blue-lighten-4" class="add-btn-icon" @click="triggerFilePicker" /> + <span class="ml-2 text-caption font-weight-medium">Add a document</span> + </div> + </v-sheet> +</template> + +<script setup lang="ts"> +import { onMounted, ref } from 'vue' +import { useLibraryStore } from '@/stores/libraryStore' +import { useInstitutionStore } from '@/stores/institutionStore' +import { useUiStore } from '@/stores/uiStore' +import { storeToRefs } from 'pinia' +import { useUserStore } from '@/stores/userStore' +import type { DocumentData } from '@/model/DocumentData' + +const deleteDocument = (file: DocumentData) => { + libraryStore.removeDocumentFromLibrary(file) + .then(() => { + console.log('Document deleted successfully') + libraryStore.fetchLibraries() + }) + .catch((err) => { + console.error('Error deleting document:', err) + }) } - }); - - const closeLibrary = () => uiStore.toggleLibrary(); - const openFile = (url: string) => { - window.open(url, '_blank'); - }; - </script> - - <style scoped> - .library-ui { - position: absolute; - top: 20%; - left: 25%; - width: 50%; - background: white; - border: 1px solid #ccc; - padding: 16px; - z-index: 10; - } - - a { - color: blue; - cursor: pointer; - text-decoration: underline; +const fileInput = ref<HTMLInputElement | null>(null) +const userStore = useUserStore() +const { userData } = storeToRefs(userStore) + +const triggerFilePicker = () => { + fileInput.value?.click() +} + +const handleFileChange = (event: Event) => { + const target = event.target as HTMLInputElement + const file = target.files?.[0] + if (file && userData.value) { + libraryStore.addDocumentToLibrary(file, userData.value) + .then(() => { + console.log('File uploaded successfully') + libraryStore.fetchLibraries() + }) + .catch((error) => { + console.error('Error uploading file:', error) + }) } - </style> - \ No newline at end of file +} + +const libraryStore = useLibraryStore() +const institutionStore = useInstitutionStore() +const uiStore = useUiStore() + +const { selectedInstitution } = storeToRefs(institutionStore) +const { libraries, selectedLibrary } = storeToRefs(libraryStore) + +const loading = ref(true) + +onMounted(async () => { + loading.value = true + libraryStore.fetchLibraries().then(() => { + console.log('libraries', libraries.value) + const institutionId = "institution_test" + console.log('institutionId', institutionId) + libraryStore.openLibraryByInstitutionId(institutionId ?? '') + }) + loading.value = false +}) + +const closeLibrary = () => uiStore.toggleLibrary() +const openFile = (url: string) => { + window.open(url, '_blank') +} +</script> + +<style scoped> +.library-ui { + position: absolute; + top: 20%; + left: 25%; + width: 50%; + background: white; + border: 1px solid #ccc; + padding: 16px; + z-index: 10; +} + +a { + color: blue; + cursor: pointer; + text-decoration: underline; +} + +.add-btn { + position: absolute; + bottom: 0px; + left: 0px; + transform: scale(0.65); + z-index: 5; +} + +.hoverable { + cursor: pointer; + transition: background-color 0.2s ease-in-out; + border-radius: 6px; +} + +.hoverable:hover { + background-color: rgba(0, 0, 0, 0.05); + /* subtle gray highlight */ +} + +.list-item-with-delete { + position: relative; + padding-top: 12px; + padding-right: 32px; +} + +.delete-btn { + position: absolute; + top: 4px; + right: 4px; + z-index: 1; + transform: scale(0.8); +} + +</style> diff --git a/src/model/GameElement/LibraryAction.ts b/src/model/GameElement/LibraryAction.ts index bb43a85fa6305b6facd4df55e2739f3819286ed7..56feec6d80db418e4f7b282a08b5ff8f7c63446c 100644 --- a/src/model/GameElement/LibraryAction.ts +++ b/src/model/GameElement/LibraryAction.ts @@ -1,19 +1,21 @@ -import sprite from "../../assets/library.png" -import { Sprite } from "pixi.js" -import { Viewport } from "pixi-viewport" +import sprite from '../../assets/library.png' +import { Sprite } from 'pixi.js' +import { Viewport } from 'pixi-viewport' import { GlowFilter } from 'pixi-filters' import { useUiStore } from '@/stores/uiStore' import { useLibraryStore } from '@/stores/libraryStore' +import type { InstitutionData } from '../InstitutionData' +import { useInstitutionStore } from '@/stores/institutionStore' /** * Class representing a library dialog. */ export class LibraryAction { /** - * The name of the organisation related to the library. + * The name of the library. * @type {string} */ - public libraryName: string + public institution: InstitutionData /** * The viewport in which this table is rendered. @@ -39,6 +41,12 @@ export class LibraryAction { */ private libraryStore: any + /** + * The institution store used for managing institution state. + * @type {any} + */ + private institutionStore: any + /** * Create a new chat table. * @param {string} name - The name of the chat room associated with this table. @@ -47,21 +55,28 @@ export class LibraryAction { * @param {Viewport} viewPort - The viewport in which to render the table. * @param {Array<ChatTable>} chatTables - The array of all chat tables in the game. */ - constructor(name: string, x: number, y: number, viewPort: Viewport, chatTables) { - this.libraryName = name - this.library_sprite = Sprite.from(sprite); - this.library_sprite.anchor.set(0.5, 0.5); - this.library_sprite.scale.set(2, 2); - this.library_sprite.position.set(x, y); - this.library_sprite.eventMode = 'static'; - this.library_sprite.cursor = 'pointer'; - this.library_sprite.on('mouseover', this.glowElement.bind(this)); + constructor( + institution: InstitutionData, + x: number, + y: number, + viewPort: Viewport, + chatTables, + ) { + this.institution = institution + this.library_sprite = Sprite.from(sprite) + this.library_sprite.anchor.set(0.5, 0.5) + this.library_sprite.scale.set(2, 2) + this.library_sprite.position.set(x, y) + this.library_sprite.eventMode = 'static' + this.library_sprite.cursor = 'pointer' + this.library_sprite.on('mouseover', this.glowElement.bind(this)) this.library_sprite.on('mouseleave', this.removeGlow.bind(this)) - this.library_sprite.on('click', this.selectTable.bind(this)) + this.library_sprite.on('click', this.selectLibrary.bind(this)) this.viewPort = viewPort this.viewPort.addChild(this.library_sprite) this.uiStore = useUiStore() this.libraryStore = useLibraryStore() + this.institutionStore = useInstitutionStore() chatTables.push(this) } @@ -77,9 +92,9 @@ export class LibraryAction { * Select this table and join its associated chat room. * This is typically called when the table is clicked. */ - private selectTable() { - //this.libraryStore.setLibraryName(this.libraryName) - this.libraryStore.openLibrary(this.libraryName) + private selectLibrary() { + // find institution with the same name + this.institutionStore.setSelectedInstitution(this.institution) this.uiStore.toggleLibrary() } @@ -90,4 +105,4 @@ export class LibraryAction { private removeGlow() { this.library_sprite.filters = null } -} \ No newline at end of file +} diff --git a/src/stores/libraryStore.ts b/src/stores/libraryStore.ts index 91159c38a4d8def91e9c489bbc331726128a3c6c..40c5bbb454b371114fed9148c71de55d2c9efa99 100644 --- a/src/stores/libraryStore.ts +++ b/src/stores/libraryStore.ts @@ -1,7 +1,18 @@ import type { LibraryData } from '@/model/LibraryData' import { defineStore } from 'pinia' import { db, storage } from '@/stores/utils/firebase' -import { addDoc, collection, deleteDoc, doc, getDoc, getDocs, Timestamp } from '@firebase/firestore' +import { + addDoc, + collection, + collectionGroup, + deleteDoc, + doc, + getDoc, + getDocs, + query, + Timestamp, + where +} from '@firebase/firestore' import { deleteObject, getDownloadURL, ref, uploadBytes } from 'firebase/storage' import type { UserData } from '@/model/userData' import type { DocumentData } from '@/model/DocumentData' @@ -15,10 +26,18 @@ export const useLibraryStore = defineStore('LibraryStore', { return { selectedLibrary: null as LibraryData | null, libraries: [] as LibraryData[], - libraryName: '' as string | undefined, + libraryName: '' as string | undefined } }, actions: { + openLibraryByInstitutionId(institutionId: string) { + const library = this.libraries.find((lib) => lib.InstitutionID === institutionId) + if (library != null) { + this.openLibrary(library) + } else { + console.error('Library not found for institution ID:', institutionId) + } + }, openLibrary(library: LibraryData) { this.selectedLibrary = library }, @@ -28,16 +47,40 @@ export const useLibraryStore = defineStore('LibraryStore', { async fetchLibraries() { const librariesSnapshot = await getDocs(collection(db, 'libraries')) const libraries: LibraryData[] = [] - librariesSnapshot.forEach((doc) => { + + for (const doc of librariesSnapshot.docs) { + // Get collection of documents from this library + const documentsQuery = query( + collectionGroup(db, 'documents'), + where('from_uid', '==', doc.id) + ) + const documentsSnapshot = await getDocs(documentsQuery) + const documents: DocumentData[] = [] + + for (const document of documentsSnapshot.docs) { + const documentData = document.data() + const docItem: DocumentData = { + uid: document.id, + username: documentData.username, + title: documentData.title, + url: documentData.url, + content: documentData.content, + timestamp: documentData.timestamp.seconds + } + documents.push(docItem) + } + + // Create library object const libraryData = doc.data() const library: LibraryData = { LibraryID: doc.id, InstitutionID: libraryData.institution_id, - documents: libraryData.documents + documents: documents } libraries.push(library) - console.log(library) - }) + } + + console.log('libraryStore.fetchLibraries: ', libraries) this.libraries = libraries }, async addDocumentToLibrary(file: File, user: UserData) {