SpaceHuddle: Game Assisted Brainstorming. A new approach to brainstorming with multiple people.
Version: 0.2.0 Datum: 29.04.2024
SpaceHuddle uses a REST API for exchanging data between the different parts of the application (backend, moderator, client). The API's code is located in the api directory.
The SpaceHuddle API requires the following setup:
- a web server: tested with Apache 2.4,
- an SQL database instance: tested with MariaDB 10.4,
- PHP 8.2,
- Composer dependency manager.
- Install a local web development environment that meets the requirements above. XAMPP works well, setups using Vagrant, Docker, or other virtual machines will work as well.
- Check out the GitHub project on your webserver (for XAMPP for example in the directory
\xampp\htdocs\SpaceHuddle). - If not included in your setup, install Composer following the instructions for your operating system.
- Open a shell/terminal/command prompt, change to the
apidirectory and install the dependencies by callingcomposer install. - Import
api/resources/schema.sqlinto your database. This will create a database called "spacehuddle". Create a MySQL user with full permissions on the database. Enter your database credentials inapi/config/env.php(see item 7). - Create a public and private key and copy them into the directory
api/resources/keysopenssl genrsa -out private.pem 2048openssl rsa -in private.pem -outform PEM -pubout -out public.pem
- Copy
api/config/env.example.phptoapi/config/env.phpand adjust the properties - Start web server and database
API documentation and testing are done using Swagger. The documentation is located in api/docs/v1.
To run it, point your browser to http://{hostname}/{path}/api/docs/v1, e.g., http://localhost/api/docs/v1 or http://localhost/SpaceHuddle/api/docs/v1.
To test the various API endpoints, select one from the list, adapt the proposed request body if necessary and press "Execute". You will see the server response below.
To test for the moderator or facilitator, use the following steps:
- call
/api/user/register/and enter any details forusername,passwordandpassword_confirmation, - call
/api/user/loginand enter your username and password, - copy the value for
access_token, click the button "Authorize" in the upper right corner and enter the token in the field forbearerAuth, - execute any arbitrary REST call for the moderator tool.
To test for a participant, use the following steps:
- call
api/sessionto create a new session with given details and copy the received value forconnection_key, - call
/api/participant/connect/and enter the key insession_key, - copy the value for
access_token, click the button "Authorize" in the upper right corner and enter the token in the field forbearerAuth, - execute any arbitrary REST call for the client tool.
The SpaceHuddle-API is built on the following technologies. Visit the websites to learn more about their use.
- PHP 8.2: programming language
https://www.php.net/ - Slim: micro framework for PHP
https://www.slimframework.com/ - CakePHP: database query framework
https://cakephp.org/ - Swagger-PHP: REST API documentation
https://zircote.github.io/swagger-php/
User administration
- user: User data for registered moderators and co-moderators.
- participant: Participants taking part in the session.
- session_role: Role in a session as moderators or co-moderators.
Tutorial
- tutorial: Which explanatory text has already been read by the user.
- tutorial_participant: Which explanatory text has already been read by the participant.
Tracking
- task_participant_state: How often was the task performed by the participant? Was the task completed?
- task_participant_iteration: How did the individual run go? Was the round won?
- task_participant_iteration_step: Which steps were completed within the round?
Session
- session: A session defines a bundle of topics on tasks for an event which are to be worked on by a joint group of participants.
Topic
- topic: A session can consist of several topics. Topics organize a session to ensure a better overview.
Task
- task: A topic can consist of several tasks.
- selection: A selection is a special task that makes a choice from existing inputs (ideas).
Module
- module: A task must consist of a main module and any number of add-ons. The main module defines which task is to be performed (brainstorming, categorization, evaluation, selection). The add ons define whether a special visualization or game components are desired.
Idea
- idea: Tasks generate ideas (brainstorming) or use ideas as input for further processing in a (selection, categorization or evaluation).
- selection_idea: Defines a list of existing ideas for a selection.
- hierarchy: Categorizes the existing ideas.
- Vote: Evaluates the existing ideas.
- Adjust the properties in the
frontend/.envfile or create your own .env.local or .env.production version of the file. Create your individual MapTiler-key on https://www.maptiler.com/ and set it in .env file. - Download and install Node
https://nodejs.org/en/download/. Use a Node version that is lower or equal to 19.2.0 - Install dependencies with:
npm install
You can use either npm start or npm run serve.
npm run build
npm run test:unit
npm run lint
SpaceHuddle is built on the following technologies. Visit the websites to learn more about their use.
- Typescript: programming language
https://www.typescriptlang.org/ - Sass: css styling language
https://sass-lang.com/ - ESLint: code linter
https://eslint.org/ - VUE3: JavaScript Framework
https://vuejs.org/guide/introduction.html - Vue Class Component: class-style syntax
https://class-component.vuejs.org/ - Element Plus: component library
https://element-plus.org/en-US/component/button.html - Bulma: responsive web interfaces
https://bulma.io/ - Font Awesome: icon library
https://fontawesome.com/ - i18n: translation module
https://www.npmjs.com/package/i18n - Axios: backend access
https://axios-http.com/ - Chart.js: chart components
https://www.chartjs.org/ - Matter-js: physics engine
https://brm.io/matter-js/ - MapLibre: map engine
https://maplibre.org/ - MapTiler: map styles
https://www.maptiler.com/ - OSRM: open source routing machine
http://project-osrm.org/ - PixiJS: 2d webgl renderer
https://pixijs.com/ - Turf: geospatial analysis
http://turfjs.org/
- Navigate to the folder
frontend/src/modulesand choose one of the following subdirectory depending on the module type to be developed.- information: information phase preceding the brainstorming (e.g. inspirational material, explaining the initial situation, evaluating the initial state (quiz, survey)).
- brainstorming: idea collection
- categorisation: structuring ideas
- selection: restrict ideas for further use
- voting: evaluate ideas
- playing: ice breaker games
- common: General module overlapping visualization components for the public screen.
- Create your own module subdirectory in the desired type folder.
- Configure module
- Create
config.jsonfile within your module folder - Set the properties required for your module in the json file
icon: name of the fontawesome icon to be assigned to the module (https://fontawesome.com/)iconPrefix: optional if the icon category is not fastype: choose one of the two optionsmainoraddOn.mainmodules stand alone.addOns extend anymainmodule of the same type.input: Input indicates whether the module uses other modules as an input source. Choose one of the three optionyes,no,optional.syncPublicParticipant: Indicates whether the flow of the client module can be controlled by the moderator. Choose one of the two optiontrue,false.fallback: optional if a module extends another module
- Create
- Set up multilingualism for module
- Create a
localsfolder within your module folder. - Add a
[language abbreviation].jsonto thelocalesfolder for all available languages. - Structure of the language files
{ "description": { "title": "...", "description": "..." }, "publicScreen": { "...": "...", "...": "..." }, "participant": { "...": "...", "...": "..." }, "moderatorContent": { "...": "...", "...": "..." }, "moderatorConfig": { "...": "...", "...": "..." }, "statistic": { "...": "...", "...": "..." } } - The sections
publicScreen,participant,moderatorContent,moderatorConfig,statisticare optional and should only help to structure the code. - Replace the
"..."information with your own content. - The translation text can be embedded in the vue code as follows.
$t('module.!moduletype!.!modulename!.!outputType!.!translationKey!')!moduletype!: Specifies the name of the module type folder (selection,categorisation,brainstorming,information,voting,playing,common)!modulename!: Specifies the name of the module folder!outputType!: Specifies the view name (publicScreen,participant,moderatorContent,moderatorConfig,statistic)!translationKey!: Specifies the translation key
- Create a
- Develop your module.
- Create a
outputfolder within your module folder. - Create a
ModeratorContent.vuefile in theoutputfolder if you need a moderator view in your module that differs from the default view. In the following example, replace the information between ! and !, and expand the functionality according to individual needs.<template> !html section! </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop } from 'vue-property-decorator'; import { IModeratorContent } from '@/types/ui/IModeratorContent'; @Options({ components: {}, }) export default class ModeratorContent extends Vue implements IModeratorContent { @Prop() readonly taskId!: string; } </script> <style lang="scss" scoped> !scss section! </style> - Create a
ModeratorConfig.vuefile in theoutputfolder if you need individual adjustable configuration parameters for the moderator in your module. In the following example, replace the information between ! and !, and expand the functionality according to individual needs.<template> <el-form-item :label="$t('module.!moduletype!.!modulename!.moderatorConfig.!parametername!')" :prop="`${rulePropPath}.!parametername!`" :rules="[defaultFormRules.ruleRequired]" > <el-input-number v-model="modelValue.!parametername!" :placeholder="$t('module.!moduletype!.!modulename!.moderatorConfig.!parameternameExample!')" /> </el-form-item> </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop, Watch } from 'vue-property-decorator'; import * as moduleService from '@/services/module-service'; import { Module } from '@/types/api/Module'; import { ValidationRuleDefinition, defaultFormRules } from '@/utils/formRules'; import * as cashService from '@/services/cash-service'; @Options({ components: {}, emits: ['update'], }) /* eslint-disable @typescript-eslint/no-explicit-any*/ export default class ModeratorConfig extends Vue { defaultFormRules: ValidationRuleDefinition = defaultFormRules; @Prop() readonly rulePropPath!: string; @Prop() readonly moduleId!: string; @Prop() readonly taskId!: string; @Prop() readonly topicId!: string; @Prop({ default: {} }) modelValue!: any; @Prop({ default: {} }) formData!: any; @Prop({ default: {} }) taskType!: any; module: Module | null = null; @Watch('modelValue', { immediate: true }) async onModelValueChanged(): Promise<void> { if (this.modelValue && !this.modelValue.!parametername!) { this.modelValue.!parametername! = !parameternameDefaultValue!; } } @Watch('moduleId', { immediate: true }) async onModuleIdChanged(): Promise<void> { if (this.moduleId) { moduleService.registerGetModuleById( this.moduleId, this.updateModule, EndpointAuthorisationType.MODERATOR, 60 * 60 ); } } updateModule(module: Module): void { this.module = module; } deregisterAll(): void { cashService.deregisterAllGet(this.updateModule); } unmounted(): void { this.deregisterAll(); } } </script> - Create a
participant.vuefile in theoutputfolder if you need a participant view for your module that differs from the default view. In the following example, replace the information between ! and !, and expand the functionality according to individual needs.<template> <ParticipantModuleDefaultContainer :task-id="taskId" :module="moduleName"> !html section! </ParticipantModuleDefaultContainer> </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop, Watch } from 'vue-property-decorator'; import ParticipantModuleDefaultContainer from '@/components/participant/organisms/layout/ParticipantModuleDefaultContainer.vue'; import * as moduleService from '@/services/module-service'; import { Module } from '@/types/api/Module'; import EndpointAuthorisationType from '@/types/enum/EndpointAuthorisationType'; import * as cashService from '@/services/cash-service'; @Options({ components: { ParticipantModuleDefaultContainer, }, }) export default class Participant extends Vue { @Prop() readonly taskId!: string; @Prop() readonly moduleId!: string; @Prop({ default: false }) readonly useFullSize!: boolean; @Prop({ default: '' }) readonly backgroundClass!: string; module: Module | null = null; get moduleName(): string { if (this.module) return this.module.name; return ''; } @Watch('moduleId', { immediate: true }) onModuleIdChanged(): void { if (this.moduleId) { moduleService.registerGetModuleById( this.moduleId, this.updateModule, EndpointAuthorisationType.PARTICIPANT, 60 * 60 ); } } updateModule(module: Module): void { this.module = module; } deregisterAll(): void { cashService.deregisterAllGet(this.updateModule); } unmounted(): void { this.deregisterAll(); } } </script> <style lang="scss" scoped> !scss section! </style> - Create a
PublicScreen.vuefile in theoutputfolder if you need an individual public screen for your module that differs from the default view. In the following example, replace the information between ! and !, and expand the functionality according to individual needs.<template> !html section! </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop } from 'vue-property-decorator'; import EndpointAuthorisationType from '@/types/enum/EndpointAuthorisationType'; @Options({ components: {}, }) export default class PublicScreen extends Vue { @Prop() readonly taskId!: string; @Prop({ default: EndpointAuthorisationType.MODERATOR }) authHeaderTyp!: EndpointAuthorisationType; } </script> <style lang="scss" scoped> !scss section! </style> - Create a
ModuleStatistic.vuefile in theoutputfolder if you need an individual statistic for your module that differs from the default view. In the following example, replace the information between ! and !, and expand the functionality according to individual needs.<template> <div></div> </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop } from 'vue-property-decorator'; @Options({ components: {}, }) /* eslint-disable @typescript-eslint/no-explicit-any*/ export default class ModuleStatistic extends Vue { @Prop() readonly taskId!: string; } </script> <style lang="scss" scoped> !scss section! </style> - Develop additional components and types required for the module within the module folder. Spacehuddle uses atomic design to structure the files. Therefore, depending on what is required, structure the subdirectories into
types,organisms,moleculesandatoms. All these folders are optional. - The implementation of the access to the backend interfaces can be found in the folder
frontend/src/services. To illustrate their use, here is an example implementation for querying all ideas of a task.<template> !html section! </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop } from 'vue-property-decorator'; import { IModeratorContent } from '@/types/ui/IModeratorContent'; import * as ideaService from '@/services/idea-service'; import { Idea } from '@/types/api/Idea.ts'; import IdeaSortOrder from '@/types/enum/IdeaSortOrder'; import EndpointAuthorisationType from '@/types/enum/EndpointAuthorisationType'; import * as cashService from '@/services/cash-service'; @Options({ components: {}, }) export default class ModeratorContent extends Vue implements IModeratorContent { @Prop() readonly taskId!: string; ideas: Idea[] = []; activeIdea!: Idea; @Watch('taskId', { immediate: true }) onTaskIdChanged(): void { ideaService.registerGetIdeasForTask( this.taskId, IdeaSortOrder.TIMESTAMP, null, this.updateIdeas, EndpointAuthorisationType.MODERATOR, 2 * 60 ); } updateIdeas(ideas: Idea[]): void { this.ideas = ideas; } deregisterAll(): void { cashService.deregisterAllGet(this.updateIdeas); } unmounted(): void { this.deregisterAll(); } async save(): Promise<void> { if (this.activeIdea.id) { await ideaService .putIdea(this.activeIdea, EndpointAuthorisationType.MODERATOR) .then((queryResult) => { //todo }); } else if (this.taskId) { await ideaService .postIdea(this.taskId, this.activeIdea, EndpointAuthorisationType.MODERATOR) .then((queryResult) => { if (queryResult) { this.activeIdea = {}; this.ideas.push(queryResult); } }); } } } </script> <style lang="scss" scoped> !scss section! </style>GETis used to read data. Get calls are implemented by registering them at the client-side cash to prevent multiple loading of data by different components. An update interval can be specified by which the data is reloaded from the backend. If no regular update is to take place, the value is to be set to 24 * 60 * 60 seconds = 1 day. It is important to disable the registration when leaving the page, otherwise the update will continue.POSTis used for the initial insertion of data.PUTfor changing already inserted data.DELETEfor deleting data.
- Under
frontend/src/componentscustom VUE components such as entity cards (e.g.IdeaCard), entity change dialogs (e.g.IdeaSettings) or layout components (e.g.ParticipantModuleDefaultContainer) can be found.If these components are to be used in the module in a way that differs from the predefined implementation, we kindly request that you implement an individual development of the components in the module folder. The components can be copied as a template for this purpose.<template> <ParticipantModuleDefaultContainer :task-id="taskId" :module="moduleName"> ... <div class="media" v-for="idea in ideas" :key="idea.id"> <IdeaCard :idea="idea" :is-editable="false" class="public-idea" :show-state="false" /> </div> ... </ParticipantModuleDefaultContainer> </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop, Watch } from 'vue-property-decorator'; import { Module } from '@/types/api/Module'; import EndpointAuthorisationType from '@/types/enum/EndpointAuthorisationType'; import ParticipantModuleDefaultContainer from '@/components/participant/organisms/layout/ParticipantModuleDefaultContainer.vue'; import IdeaCard from '@/components/moderator/organisms/cards/IdeaCard.vue'; @Options({ components: { ParticipantModuleDefaultContainer, IdeaCard }, }) export default class Participant extends Vue { @Prop() readonly taskId!: string; @Prop() readonly moduleId!: string; @Prop({ default: false }) readonly useFullSize!: boolean; @Prop({ default: '' }) readonly backgroundClass!: string; module: Module | null = null; ideas: Idea[] = []; get moduleName(): string { if (this.module) return this.module.name; return ''; } ... } </script> <style lang="scss" scoped> .public-idea { max-width: 20rem; } ... </style> - If data should be updated automatically from the backend, this can be solved by registering at client side cash. It is important to disable the registration when leaving the page, otherwise the update will continue.
<template> !html section! </template> <script lang="ts"> import { Options, Vue } from 'vue-class-component'; import { Prop } from 'vue-property-decorator'; import EndpointAuthorisationType from '@/types/enum/EndpointAuthorisationType'; import * as cashService from '@/services/cash-service'; @Options({ components: {}, }) export default class PublicScreen extends Vue { @Prop() readonly taskId!: string; @Prop({ default: EndpointAuthorisationType.MODERATOR }) authHeaderTyp!: EndpointAuthorisationType; @Watch('taskId', { immediate: true }) onTaskIdChanged(): void { ideaService.registerGetIdeasForTask( this.taskId, IdeaSortOrder.TIMESTAMP, null, this.updateIdeas, EndpointAuthorisationType.MODERATOR, 2 * 60 ); } updateIdeas(ideas: Idea[]): void { this.ideas = ideas; } deregisterAll(): void { cashService.deregisterAllGet(this.updateIdeas); } unmounted(): void { this.deregisterAll(); } } </script> <style lang="scss" scoped> !scss section! </style>
- Create a