Merge pull request #9 from dennisschoepf/timeline

This commit is contained in:
Dennis Schoepf 2021-08-07 01:13:37 +02:00 committed by GitHub
commit 5e96f1892c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 4250 additions and 74 deletions

1
.gitignore vendored
View file

@ -3,3 +3,4 @@ node_modules
dist
.yarn
pnp.js
sourceproject

View file

@ -7,6 +7,11 @@
</head>
<body>
<div class="ui">
<div id="score">
<div id="score-found">0</div>
<div id="score-divider">/</div>
<div id="score-total">6</div>
</div>
<div id="companion">
<svg xmlns="http://www.w3.org/2000/svg" id="companion-shape" viewBox="0 0 80 80">
<polygon fill="#A60303" points="40,0 68,12 80,40 68,68 40,80 12,68 0,40 12,12"></polygon>
@ -24,11 +29,20 @@
</div>
<div id="info-message">
<div id="info-message-header">
<h2 id="info-message-headline">Test</h2>
<img id="info-message-img" src="" />
<div id="info-message-header-text">
<h4 id="info-message-subheadline"></h4>
<h2 id="info-message-headline">Test</h2>
</div>
<div id="info-message-contents">Test</div>
<a id="info-message-link" href="" target="_blank">Check this out on Github</a>
</div>
<div id="info-message-contents">
<div id="info-message-contents-text"></div>
<div id="info-message-contents-version"></div>
<div id="info-message-contents-legacy"></div>
<h4 id="info-message-content-commits-headline">Last Commits</h4>
<div id="info-message-contents-commits"></div>
</div>
<a id="info-message-link" href="" target="_blank">Check this out in detail</a>
<div id="info-message-close">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path
@ -38,8 +52,263 @@
</svg>
</div>
</div>
<div id="intro">
<div id="intro-step1">
<h1>Hello there 👋,</h1>
<p>
thanks for participating in an interactive study for this research project. If there are
any problems with this study do not hesitate to contact me via mail:
<a href="mailto:me@dnsc.io">me@dnsc.io</a>
</p>
<p>
You are participating in a study on Playful Experiences within the Onboarding Process of
Software Development Projects. In the following you are going to be guided through an
interactive visualization of a software project. Within that visualization you are going
to be able to reveal certain parts of the project, that might be of value in an
onboarding process. The visualizations are going to be accompanied by some questions on
the visualization itself but also on the broader aspect of using game mechanics in
software development. There is additional information on the study background and the
study procedure in the next step.
</p>
<div id="intro-disclaimer">
<h3>Disclaimer</h3>
<p>
Do not expect a real game experience within this study. This study aims to gather
initial feedback on certain aspects of which information could be interesting to
include within software development onboarding and how its presentation changes how it
is perceived. The visualization of the in-game elements is therefore very abstract.
The arguably more important part of this study are the answers to the questions you
will be asked in the end, so please answer them carefully. Thanks in advance, let's
start now!
</p>
</div>
</div>
<div id="intro-step2">
<h1>Information about data protection and declaration of consent</h1>
<p>Dear participant,</p>
<p>
thank you for your interest in participating in this study on ”Playful Experiences in
the Onboarding Process of Software Development Projects”. This study takes place as a
part of my master thesis on “Onboarding in Software Development Projects as a Space of
Play”. The thesis is written as a part of the new Joint Master "Human-Computer
Interaction" of the FH Salzburg and the University of Salzburg. In order to use your
personal data, we would need your written declaration of consent. Please, carefully read
the following information.
</p>
<h3>General Study Information</h3>
<p>
The goal of this study is to research how software developers experience playful
approaches on the onboarding process into new projects. The results of this study are
going to contribute to the research body of both the software development field but also
the field of “Play”. From the study results conclusions are going to be drawn about: The
openness towards playful experiences in the software development field, possible hurdles
on implementing such experiences, the general attitude of developers towards play in the
field and the selection of what information to include in onboarding processes.
</p>
<h3>Overall study procedure</h3>
<p>
The participant will take part in a hybrid-study setting. Part of the study consists of
going through an interactive visualization of an open-source software project and
revealing parts of information within some parts of the project. The other part of the
study consists of questions on the visualization and general questions on the
intersection of Play and software development. The study can be accessed online and can
be finished online without additional work on the participants side. Overall the
procedure should take between 15 and 25 minutes.
</p>
<h3>Rights</h3>
<p>
The participation in this study is voluntary. You may withdraw or stop your
participation at any time without incurring any consequences. Moreover, you can request
information about the processed personal data at any time, even after the termination of
the study (for further information see also Confidentiality and data processing). If you
have any questions please contact the study leader.
</p>
<h3>Risks</h3>
<p>
You do not incur any risks by participating in this study. If you feel uncomfortable
during the study you can cancel at any time without giving reasons and without incurring
any consequences.
</p>
<h3>Confidentiality and Data Processing</h3>
<p>
Your data will only be processed within the aforementioned project if you give your
written consent. The data is going to be processed via the Firebase platform
(https://firebase.google.com/) on european servers. For additional information on their
privacy and data protection guidelines visit their documentation at
https://policies.google.com/u/0/privacy. The following data is going to be processed
within the study:
</p>
<ul>
<li>
Demographic Information (Name, Age, Professional Background, Years of Experience,
Gender)
</li>
<li>
Your actions within the visualization (Timestamps of Clicks and Mouse moves together
with the timestamps of certain events)
</li>
<li>Your answers to the questions that are part of the study</li>
</ul>
<p>
After the end of the study, the data is going to be exported from Firebase and the
underlying app instance is going to be deleted. This data is going to be kept strictly
confidential and is not going to be passed on to further third parties. The data is
going to be analyzed and then used within my master thesis. If you only want to be
mentioned anonymously in my thesis please indicate so at the bottom. If you are doing
that your name and age are going to be hidden in my thesis and your data is only going
to be connected to a random ID.
</p>
<h3>Renumeration</h3>
<p>You will not receive an expense allowance for participation.</p>
<h3>Consent</h3>
<p>
I have carefully read and understood the declaration of consent. Any additional
questions were answered to my satisfaction. My participation is voluntary, and I know
that I can withdraw at any time, even without giving reasons, without incurring any
disadvantages. By clicking on “Agree” below I agree to the processing of my personal
data collected for this study and I am willing to participate.
</p>
<label for="intro-anonymous">
<input name="intro-anonymous" type="checkbox" id="intro-anonymous" />
I want to participate anonymously (the data is not going to be connected to your name)
</label>
</div>
<div id="intro-step3">
<h1>General Questions</h1>
<p>Please provide some additional information about you below:</p>
<label id="name-label" for="name">
Your name
<input type="text" id="intro-name" name="name" placeholder="e.g. Max" />
</label>
<label for="age">
Your age
<input type="number" id="intro-age" name="age" placeholder="e.g. 28" />
</label>
<label for="background">
Your Professional Background
<input
type="text"
id="intro-background"
name="background"
placeholder="e.g. Web Developer"
/>
</label>
<label for="experience">
Your Professional Experience
<select
type="text"
id="intro-experience"
name="experience"
placeholder="e.g. Web Developer"
>
<option selected="true" default disabled>Choose an option ...</option>
<option value="0-1">0-1 years</option>
<option value="1-3">1-3 years</option>
<option value="3-5">3-5 years</option>
<option value="5-10">5-10 years</option>
<option value="10+">10 or more years</option>
</select>
</label>
<div id="intro-error">Please provide a value for all inputs</div>
</div>
<div id="intro-step4">
<h1>The study itself</h1>
<p>
Now we're ready to dive into this study. After clicking on the button below you are
dropped into a visualization of a software development project. This project is a
monorepo, more specifically the
<a href="https://github.com/ethereumjs/ethereumjs-monorepo">ethereumjs monorepo</a>. You
are controlling a little player character with your mouse. The character consists of a
head and tail. As soon as its head touches elements on the screen you are able to
interact with them. That should be all you need for now, just drop in now and take a
look at what is there.
</p>
<p>Have fun! 🤟</p>
</div>
<div id="intro-step5">
<h1>Questions about this visualization sepcifically</h1>
<p>
After the project-specific "knowledge" questions in the last step I want to get your
valuable input as a developer for the following questions:
</p>
<label for="k-1">
1) Do you remember any of the contributors of this project? Please name them below
<textarea id="k-1" name="k-1"></textarea>
</label>
</div>
<div id="intro-step6">
<h1>Questions about Play in Software Development</h1>
<p>
After the project-specific "knowledge" questions in the last step I want to get your
valuable input as a developer for the following questions:
</p>
<label for="fb-1">
1) Would you say, that you have learned something about the underlying project from
going through this interactive visualization? If yes, what have you learned? If no, what
was missing from the visualization in your opinion?
<textarea id="fb-1" name="fb-1"></textarea>
</label>
<label for="fb-2">
2) What was your overall experience going through this visualization? What did you like
or did not like? Is there anything that stood out for you?
<textarea id="fb-2" name="fb-2"></textarea>
</label>
<label for="fb-3">
3) How did you experience the companion (Lower right)? Was it helpful or rather
annoying?
<textarea id="fb-3" name="fb-3"></textarea>
</label>
<label for="fb-4">
4) Could you imagine yourself using a visualization or something similar on different
projects to learn about them? If so, on which projects would you want to try it out? If
not, what would you prefer instead, just going throught the files?
<textarea id="fb-4" name="fb-4"></textarea>
</label>
<label for="fb-5">
5) Have you felt like any information was missing on the things that were shown within
the visualization?
<textarea id="fb-5" name="fb-5"></textarea>
</label>
<label for="fb-6">
6) Would you have liked to see additional information on the underlying project? If so,
what kind of information and how would you have liked?
<textarea id="fb-6" name="fb-6"></textarea>
</label>
<label for="fb-7">
7) Do you have any additional ideas on how playful elements or game mechanics could be
used within the onboarding phase of software development projects?
<textarea id="fb-7" name="fb-7"></textarea>
</label>
<label for="fb-8">
8) What is your general stance on using games/game mechanics or playful elements within
software development?
<textarea id="fb-8" name="fb-8"></textarea>
</label>
<label for="fb-9">
9) Do you have any additional ideas on how playful elements or game mechanics could be
used within the onboarding phase of software development projects? Any elements from
games you play that you think could be reused when making yourself familiar with new
projects?
<textarea id="fb-9" name="fb-9"></textarea>
</label>
<label for="fb-10">
10) Anything else you want to mention?
<textarea id="fb-10" name="fb-10"></textarea>
</label>
</div>
<div id="intro-step7">
<h2>Thank you so much! 🙏</h2>
<p>
Thank you for your participation. If you have additional feedback please contact me:
<a href="mailto:me@dnsc.io">me@dnsc.io</a>. You can now safely close the browser window.
</p>
</div>
<button id="intro-button">Let's start!</button>
</div>
</div>
<div id="backdrop"></div>
<div id="comp-backdrop"></div>
<div id="intro-backdrop"></div>
<script src="./main.ts"></script>
</body>
</html>

11
main.ts
View file

@ -1,4 +1,5 @@
import p5 from 'p5';
import firebase from 'firebase/app';
import { SCREEN_WIDTH, SCREEN_HEIGHT } from './src/constants/screen';
import { DetailScene } from './src/scenes/DetailScene';
import { OverviewScene } from './src/scenes/OverviewScene';
@ -6,6 +7,8 @@ import { Scenes } from './src/scenes/scenes';
import store from './src/store';
import { Companion, CompanionState } from './src/ui/companion';
import { InfoMessage } from './src/ui/info';
import { Intro } from './src/ui/intro';
import { Score } from './src/ui/score';
const sketch = (s: p5) => {
// Scenes
@ -16,8 +19,10 @@ const sketch = (s: p5) => {
s.createCanvas(SCREEN_WIDTH, SCREEN_HEIGHT);
s.noCursor();
new Intro();
new Companion();
new InfoMessage();
new Score();
overviewScene = new OverviewScene();
detailScene = new DetailScene();
@ -36,7 +41,11 @@ const sketch = (s: p5) => {
s.mousePressed = () => {
const { currentScene, companionState, infoMessageShown } = store.getState();
if (companionState !== CompanionState.ACTIVE || !infoMessageShown) {
if (
companionState !== CompanionState.ACTIVE &&
!infoMessageShown &&
store.getState().currentIntroStep === 0
) {
if (currentScene === Scenes.OVERVIEW) {
overviewScene.onSceneClick();
} else if (currentScene === Scenes.DETAIL) {

File diff suppressed because it is too large Load diff

1610
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -5,15 +5,23 @@
"repository": "git@github.com:dennisschoepf/codewanderer.git",
"author": "Dennis Schoepf <dennis@schoepf.co>",
"license": "MIT",
"type": "module",
"scripts": {
"dev": "parcel index.html",
"build": "parcel build index.html",
"deploy": "./deploy.sh"
"deploy": "./deploy.sh",
"dev:meta": "nodemon --ignore '**/*.json' ./scripts/get-metadata.js",
"generate": "node ./scripts/get-metadata.js"
},
"devDependencies": {
"@octokit/core": "^3.5.1",
"@types/animejs": "^3.1.4",
"@types/lodash": "^4.14.171",
"@types/p5": "^1.3.0",
"get-folder-size": "^3.1.0",
"glob": "^7.1.7",
"node-fetch": "^2.6.1",
"nodemon": "^2.0.12",
"parcel-bundler": "^1.12.5",
"prettier": "^2.3.2",
"sass": "^1.35.2",
@ -21,7 +29,9 @@
},
"dependencies": {
"animejs": "^3.2.1",
"firebase": "^8.8.1",
"lodash": "^4.17.21",
"moment": "^2.29.1",
"p5": "^1.4.0",
"rxjs": "^7.2.0",
"zustand": "^3.5.7"

66
scripts/get-metadata.js Normal file
View file

@ -0,0 +1,66 @@
import { resolve } from 'path';
import { readdir, writeFile } from 'fs/promises';
import getItemSize from 'get-folder-size';
import {
getLegaciesForSubproject,
getLinksForSubproject,
getPackagesForSubproject,
getProjectContributors,
} from './helpers.js';
const __dirname = resolve();
export const PROJECT_PATH = resolve(__dirname, 'sourceproject/ethereumjs-monorepo');
export const SUBPACKAGE_PATH = resolve(PROJECT_PATH, 'packages');
const main = async () => {
const subprojectPaths = await readdir(SUBPACKAGE_PATH);
const subprojectOverviewData = await Promise.all(
subprojectPaths.map(async (subprojectPath) => {
const size = await getItemSize.loose(resolve(PROJECT_PATH, SUBPACKAGE_PATH, subprojectPath));
return {
name: subprojectPath,
path: `packages/${subprojectPath}`,
filePath: resolve(PROJECT_PATH, SUBPACKAGE_PATH, subprojectPath),
size: Math.floor(size * 0.001),
};
})
);
const subprojects = subprojectOverviewData.filter(
(subprojectData) => subprojectData.name !== 'vm' && subprojectData.name !== 'ethereum-tests'
);
const projectContributors = await getProjectContributors();
const subprojectsWithRevealables = await Promise.all(
subprojects.map(async (subproject, i) => {
const packages = await getPackagesForSubproject(subproject);
const links = await getLinksForSubproject(subproject);
const legacies = await getLegaciesForSubproject(subproject);
const contributors = [
projectContributors[0 + i],
projectContributors[10 + i],
projectContributors[20 + i],
];
return {
...subproject,
links,
revealables: [...packages, ...contributors, ...legacies],
};
})
);
const jsonToWrite = JSON.stringify(
{
subprojects: subprojectsWithRevealables,
},
null,
2
);
writeFile(resolve(__dirname, 'metadata/project.json'), jsonToWrite);
};
main();

154
scripts/helpers.js Normal file
View file

@ -0,0 +1,154 @@
import { resolve, basename } from 'path';
import { readFile } from 'fs/promises';
import { promisify } from 'util';
import { Octokit } from '@octokit/core';
import child_process from 'child_process';
import nodeGlob from 'glob';
import { PROJECT_PATH, SUBPACKAGE_PATH } from './get-metadata.js';
import getItemSize from 'get-folder-size';
const exec = promisify(child_process.exec);
const glob = promisify(nodeGlob);
const octokit = new Octokit({
auth: '',
});
export async function getLegaciesForSubproject(subproject) {
// Get all paths to project files
const files = await glob(resolve(SUBPACKAGE_PATH, subproject.name, '**/*.ts'));
const filteredFilePaths = files.filter(
(filePath) =>
!filePath.includes('spec.') && !filePath.includes('/test/') && !filePath.includes('@types')
);
// Get line counts for files
const filesWithWordCounts = await Promise.all(
filteredFilePaths.map(async (filteredFilePath) => {
const { stdout } = await exec(`wc -l < ${filteredFilePath}`);
return {
path: filteredFilePath,
count: Number(stdout.replace('\n', '')),
};
})
);
const largestFiles = filesWithWordCounts
.sort((fwcA, fwcB) => fwcB.count - fwcA.count)
.slice(0, 2);
// Transform to legacy object
const legacies = await Promise.all(
largestFiles.map(async (largeFile) => await createLegacy(largeFile))
);
// Return 2 highest line counts
return legacies;
}
export async function getProjectContributors() {
const contribs = await octokit.request('/repos/ethereumjs/ethereumjs-monorepo/contributors');
const contributors = await Promise.all(
contribs.data.map(async (contrib) => await createContributor(contrib))
);
return contributors;
}
export async function getLinksForSubproject(subproject) {
const subprojectPackageJson = await readFile(
resolve(subproject.filePath, 'package.json'),
'utf8'
);
const { dependencies } = JSON.parse(subprojectPackageJson);
const links = Object.keys(dependencies).filter((dependency) =>
dependency.includes('@ethereumjs')
);
return links;
}
export async function getPackagesForSubproject(subproject) {
const subprojectPackageJson = await readFile(
resolve(subproject.filePath, 'package.json'),
'utf8'
);
const { dependencies } = JSON.parse(subprojectPackageJson);
const relevantDependencies = Object.keys(dependencies)
.filter((dependency) => !dependency.includes('ethereumjs') && !dependency.includes('@types'))
.slice(0, 3)
.map((dependencyKey) => ({
name: dependencyKey,
version: dependencies[dependencyKey],
}));
const formattedDependencies = relevantDependencies.map((dep) => createPackage(dep));
return formattedDependencies;
}
const createContributor = async (contrib) => {
if (!contrib) return;
let commits;
try {
const rawCommits = await octokit.request(`/repos/ethereumjs/ethereumjs-monorepo/commits`, {
author: contrib.login,
});
const lastRawCommits = rawCommits.data.slice(0, 3);
commits = lastRawCommits.map((commit) => ({
url: commit.html_url,
message: commit.commit.message,
time: commit.commit.author.date,
}));
} catch (e) {}
return {
type: 'CONTRIBUTOR',
name: contrib.login,
url: contrib.html_url,
size: contrib.contributions,
imageUrl: contrib.avatar_url,
contents:
'This is one of the main contributors to the overall repository and this part of the repository specifically. Below you can see the contributors latest commits and the button on the right will take you straight to the respective Github profile.',
commits,
};
};
const createPackage = ({ name, version }) => {
const path = 'path.to.package.json';
const size = Math.floor(Math.random() * 250) + 50;
return {
type: 'PACKAGE',
name,
path,
version,
size,
contents: `This package is used throughout this part of the repository. Below you can see the version that is installed currently. If you want to take a look at the package documentation, past versions and much more, click the button in the lower right corner. It'll lead you to the npmjs website of the package.`,
url: `https://www.npmjs.com/package/${name}`,
};
};
const createLegacy = async ({ path, count }) => {
const fileSize = await getItemSize(path);
const rawFileContents = await readFile(path, 'utf-8');
const projectPath = path.replace(PROJECT_PATH, '');
const fileContents = rawFileContents.replace(/\r\n/g, '\n').split('\n').slice(30, 40).join('\n');
return {
type: 'LEGACY',
name: basename(projectPath),
path: projectPath,
size: count,
contents: `This file seems pretty big compared to the other files in this part of the project. It's ${fileSize.size} bytes and has ${count} lines of code. That could make it hard to read for new contributors and people looking at the repository. You might want to look into a refactoring in order to keep it at a more readable size. What you could also do is contact one of the contributors to help in understanding what's going on. They must be hiding somewhere in this package as well, try to reveal them to access their Github profiles.`,
fileContents,
fileSize: fileSize.size,
url: `https://github.com/ethereumjs/ethereumjs-monorepo/blob/master${projectPath}`,
};
};

View file

@ -1,7 +1,7 @@
import { mp5 } from '../main';
import { SCREEN_HEIGHT, SCREEN_WIDTH } from './constants/screen';
import { Edge } from './sketchObjects/Edge';
import { RevealableInterface, RevealableTypes } from './sketchObjects/Revealable';
import { Revealable, RevealableInterface, RevealableTypes } from './sketchObjects/Revealable';
import { Coordinates, JSONSubproject, SubProject } from './types';
export function getEdgeDimensions({ size }: JSONSubproject): number {
@ -11,8 +11,8 @@ export function getEdgeDimensions({ size }: JSONSubproject): number {
export function generateRandomEdgeCoordinates(): Coordinates {
return {
x: mp5.random(150, SCREEN_WIDTH - 150),
y: mp5.random(150, SCREEN_HEIGHT - 150),
x: mp5.random(200, SCREEN_WIDTH - 200),
y: mp5.random(200, SCREEN_HEIGHT - 200),
};
}
@ -90,17 +90,37 @@ export function getRevealablesforSubproject(
}));
}
export function generateRevealableCoords(): Coordinates[] {
const areaWidth = mp5.width / 3;
const rowHeight = mp5.height / 2;
export function generateRevealableCoords(existingRevealables: Revealable[]): Coordinates {
let newCoords: Coordinates;
const existingCoordinates = existingRevealables.map(({ area }) => ({ x: area.x, y: area.y }));
// Max. 6 revealables one in each area
return [
{ x: mp5.random(25, areaWidth), y: mp5.random(25, rowHeight) },
{ x: mp5.random(areaWidth, areaWidth * 2), y: mp5.random(25, rowHeight) },
{ x: mp5.random(areaWidth * 2, areaWidth * 3), y: mp5.random(25, rowHeight) },
{ x: mp5.random(25, areaWidth), y: mp5.random(rowHeight, rowHeight * 2) },
{ x: mp5.random(areaWidth, areaWidth * 2), y: mp5.random(rowHeight, rowHeight * 2) },
{ x: mp5.random(areaWidth * 2, areaWidth * 3), y: mp5.random(rowHeight, rowHeight * 2) },
];
if (existingRevealables.length === 0) {
return generateRandomEdgeCoordinates();
} else {
do {
newCoords = generateRandomEdgeCoordinates();
} while (isColliding(newCoords, existingCoordinates));
}
return newCoords;
}
export function generateRevealables(revealables: RevealableInterface[]): Revealable[] {
let revObjs = [];
revealables.forEach((revealable) => {
const coordinates = generateRevealableCoords(revObjs);
revObjs.push({
revealable,
area: {
x: coordinates.x,
y: coordinates.y,
w: revealable.size < 50 ? 50 : revealable.size > 400 ? 400 : revealable.size,
},
});
});
console.log(revObjs);
return revObjs.map((revObj) => new Revealable(revObj.revealable, revObj.area));
}

89
src/logger.ts Normal file
View file

@ -0,0 +1,89 @@
import firebase from 'firebase/app';
import 'firebase/database';
import store from './store';
const firebaseConfig = {
apiKey: 'AIzaSyCORIIdFkDBagiVf0IeK0UxZL1qv0m90_E',
authDomain: 'codewanderer-d9212.firebaseapp.com',
databaseURL: 'https://codewanderer-d9212-default-rtdb.europe-west1.firebasedatabase.app',
projectId: 'codewanderer-d9212',
storageBucket: 'codewanderer-d9212.appspot.com',
messagingSenderId: '143940874668',
appId: '1:143940874668:web:d2f860b4fc5d4292cf8a09',
};
type LogEventType =
| 'CC'
| 'OC'
| 'RC'
| 'LI'
| 'LR'
| 'LS'
| 'LC'
| 'PI'
| 'PR'
| 'PS'
| 'PC'
| 'NI'
| 'NR'
| 'NS'
| 'NC'
| 'GF'
| 'SF'
| 'OTHER';
export interface LogEvent {
timestamp: number;
type: LogEventType;
message?: string;
additionalData?: any;
}
export class Logger {
database: firebase.database.Database;
constructor() {
firebase.initializeApp(firebaseConfig);
this.database = firebase.database();
}
public log(ev: LogEvent) {
console.log('Logging event');
const uid = store.getState().uid;
if (uid) {
const logEvKey = this.database.ref(uid).child('logs').push().key;
this.database.ref(uid).update({ [`logs/${logEvKey}`]: ev });
}
}
public logPersonalData(
name: string,
age: number,
background: string,
experience: string,
anonymous: boolean
) {
const uid = store.getState().uid;
this.database.ref(uid).set({
name,
age,
background,
experience,
anonymous,
});
}
public logQuestions(answers: string[], isKQ: boolean = false) {
const uid = store.getState().uid;
this.database
.ref(uid)
.child(`${isKQ ? 'knowledge' : 'general'}Questions`)
.set(answers);
}
}
export const logger = new Logger();

View file

@ -1,9 +1,10 @@
import _ from 'lodash';
import { mp5 } from '../../main';
import { colors } from '../constants/colors';
import { generateRevealableCoords } from '../helpers';
import { generateRevealables } from '../helpers';
import { logger } from '../logger';
import { Player } from '../sketchObjects/Player';
import { Revealable, RevealableInterface } from '../sketchObjects/Revealable';
import { Revealable, RevealableInterface, RevealableTypes } from '../sketchObjects/Revealable';
import store from '../store';
import { Coordinates } from '../types';
import { CompanionState } from '../ui/companion';
@ -14,6 +15,9 @@ export class DetailScene {
revealables: RevealableInterface[];
revealableCoords: Coordinates[];
revealableObjects: Revealable[];
startTime?: number = null;
wasInteractedWith: boolean = false;
wasHovered: boolean = false;
constructor() {
this.player = new Player();
@ -21,20 +25,35 @@ export class DetailScene {
store.subscribe((state, prevState) => {
if (!_.isEqual(state.revealables, prevState.revealables)) {
this.revealables = state.revealables;
this.revealableCoords = generateRevealableCoords();
this.revealableObjects = this.revealables.map(
(revealable, i) =>
new Revealable(revealable, {
x: this.revealableCoords[i].x,
y: this.revealableCoords[i].y,
w: this.revealables[i].size,
})
);
this.revealableObjects = generateRevealables(this.revealables);
console.log(this.revealableObjects);
}
});
}
draw() {
if (this.startTime === null) {
this.startTime = mp5.millis();
}
if (mp5.millis() > this.startTime + 6000 && !this.wasInteractedWith) {
this.wasInteractedWith = true;
store.getState().addUserMessage({
inputWanted: false,
text: 'Trouble knowing what to do? You should try clicking somewhere in order to spawn reveal bubbles. Try this in different parts of the canvas to see what you can find',
});
} else if (
mp5.millis() > this.startTime + 16000 &&
!this.wasHovered &&
this.wasInteractedWith
) {
this.wasHovered = true;
store.getState().addUserMessage({
inputWanted: false,
text: "Good job with your reveal bubbles, in order to truly find out what is important in this part of the project, try to catch the revealed objects with your character's head to be able to interact with them.",
});
}
mp5.background(mp5.color(colors.greyLighter));
this.player.drawOnReveal();
@ -44,20 +63,36 @@ export class DetailScene {
revObj.draw();
});
store.setState({
revealablesFinished: this.revealableObjects.filter((revObj) => revObj.wasInteractedWith)
.length,
});
this.player.move();
if (
this.revealableObjects.every((revObj) => revObj.wasInteractedWith) &&
!(store.getState().companionState === CompanionState.ACTIVE)
!(store.getState().companionState === CompanionState.ACTIVE) &&
!store.getState().infoMessageShown
) {
if (!store.getState().finishedSubProjects.includes(store.getState().currentSubproject)) {
store.setState((state) => ({
finishedSubProjects: [...state.finishedSubProjects, state.currentSubproject],
}));
}
store.getState().addUserMessage({
text: "Yaay! You've found all of the important parts of this part of the repository. You will be returned to the subproject overview now. Pick the next subproject you want to take a look at there.",
inputWanted: false,
onNext: () => store.setState({ currentScene: Scenes.OVERVIEW }),
onNext: () => {
logger.log({
type: 'SF',
timestamp: Date.now(),
message: `Finished subprojects: ${JSON.stringify(
store.getState().finishedSubProjects
)}`,
});
store.setState({ showScore: false, currentScene: Scenes.OVERVIEW });
},
showIdle: false,
});
}
@ -66,9 +101,27 @@ export class DetailScene {
onSceneClick() {
this.revealableObjects.forEach((revObj) => {
if (revObj.isHovered) {
logger.log({
type:
revObj.type === RevealableTypes.CONTRIBUTOR
? 'NI'
: revObj.type === RevealableTypes.LEGACY
? 'LI'
: 'PI',
timestamp: Date.now(),
message: `Identified ${revObj.name}`,
});
this.wasHovered = true;
revObj.onClick();
} else {
logger.log({
type: 'RC',
timestamp: Date.now(),
});
this.player.reveal();
this.wasInteractedWith = true;
}
});
}

View file

@ -6,13 +6,14 @@ import store from '../store';
import { generateEdges } from '../helpers';
import { Scenes } from './scenes';
import projectMetadata from '../../metadata/project.json';
import { playerHead$ } from '../area';
import { Area } from '../types';
import { logger } from '../logger';
export class OverviewScene {
player: Player;
playerHead: Area;
edges: Edge[];
sfLogged: boolean;
constructor() {
this.edges = generateEdges(projectMetadata.subprojects);
@ -30,14 +31,47 @@ export class OverviewScene {
public onSceneClick() {
this.edges.forEach((edge, i) => {
const dist = mp5.dist(mp5.mouseX, mp5.mouseY, edge.x, edge.y);
if (dist < edge.r) {
if (dist < edge.currentSize) {
logger.log({
type: 'OC',
timestamp: Date.now(),
message: 'Click inside edge',
});
store.getState().setProjectMetadata(edge.name);
store.setState({ currentSubproject: edge.name, currentScene: Scenes.DETAIL });
store.setState({
showScore: true,
currentSubproject: edge.name,
currentScene: Scenes.DETAIL,
});
} else {
logger.log({
type: 'OC',
timestamp: Date.now(),
message: 'Click outside edge',
});
}
});
}
private drawLocations() {
if (store.getState().finishedSubProjects.length === 3 && !store.getState().finishedGame) {
store.setState({ finishedGame: true });
setTimeout(() => {
logger.log({
timestamp: Date.now(),
type: 'GF',
});
store.getState().addUserMessage({
text: "Nice! 😎 You made it all the way through. Now I would be very thankful if you could take some time to answer the following questions. Don't overthink the answers and write down everything that comes to your mind. The more input you give, the better no matter how well it is formulated!",
inputWanted: false,
onNext: () => store.setState({ currentIntroStep: 5 }),
});
}, 800);
}
this.edges.forEach((edgeShape) => {
if (store.getState().finishedSubProjects.some((fsp) => fsp === edgeShape.name)) {
edgeShape.finished = true;

View file

@ -2,8 +2,10 @@ import { combineLatest } from 'rxjs';
import { mp5 } from '../../main';
import { areasColliding, playerHead$, revealedArea$ } from '../area';
import { colors } from '../constants/colors';
import { logger } from '../logger';
import store from '../store';
import { Area } from '../types';
import { Commit } from '../ui/info';
export enum RevealableTypes {
LEGACY = 'LEGACY',
@ -19,6 +21,9 @@ export interface RevealableInterface {
size: number;
path?: string;
imageUrl?: string;
version?: string;
commits?: Commit[];
fileContents?: string;
}
enum RevealableStates {
@ -38,10 +43,14 @@ export class Revealable {
contents: string;
url: string;
imageUrl: string;
version: string;
commits: Commit[];
fileContents: string;
isHovered: boolean;
isRevealed: boolean;
wasInteractedWith: boolean;
wasRevealed: boolean;
minSize: number = 5;
currentSize: number;
@ -51,12 +60,28 @@ export class Revealable {
pulseOpacity: number = 255;
pulseCountUp: boolean;
constructor({ type, name, path, contents, url, imageUrl }: RevealableInterface, area: Area) {
constructor(
{
type,
name,
path,
contents,
url,
imageUrl,
version,
commits,
fileContents,
}: RevealableInterface,
area: Area
) {
this.type = type;
this.name = name;
this.path = path;
this.contents = contents;
this.url = url;
this.version = version;
this.commits = commits;
this.fileContents = fileContents;
this.imageUrl = imageUrl;
this.area = area;
this.currentSize = this.minSize;
@ -85,6 +110,21 @@ export class Revealable {
this.state = RevealableStates.FOUND;
} else if (isRevealed && !isHovered) {
this.state = RevealableStates.REVEALED;
if (!this.wasRevealed) {
logger.log({
type:
this.type === RevealableTypes.CONTRIBUTOR
? 'NR'
: this.type === RevealableTypes.LEGACY
? 'LR'
: 'PR',
timestamp: Date.now(),
message: `Revealed ${this.name}`,
});
}
this.wasRevealed = true;
} else {
this.state = RevealableStates.HIDDEN;
}
@ -108,10 +148,27 @@ export class Revealable {
mp5.fill(mp5.color(colors.red));
mp5.ellipse(this.area.x, this.area.y, this.currentSize);
} else if (this.state === RevealableStates.INACTIVE) {
this.minSize = 35;
this.reduceSize();
mp5.fill(mp5.color(colors.greyDark));
mp5.fill(mp5.color(colors.redDark));
mp5.ellipse(this.area.x, this.area.y, this.currentSize);
mp5.strokeWeight(4);
mp5.stroke(mp5.color(colors.greyLighter));
mp5.line(
this.area.x + this.currentSize / 5,
this.area.y - this.currentSize / 5,
this.area.x - this.currentSize / 5,
this.area.y + this.currentSize / 5
);
mp5.line(
this.area.x - this.currentSize / 5,
this.area.y - this.currentSize / 5,
this.area.x + this.currentSize / 5,
this.area.y + this.currentSize / 5
);
}
}
@ -119,11 +176,26 @@ export class Revealable {
if (this.isHovered && !this.wasInteractedWith) {
this.wasInteractedWith = true;
logger.log({
type:
this.type === RevealableTypes.CONTRIBUTOR
? 'NS'
: this.type === RevealableTypes.LEGACY
? 'LS'
: 'PS',
timestamp: Date.now(),
message: `Showing info message for ${this.name}`,
});
store.getState().addInfoMessage({
type: this.type,
headline: this.name,
innerHTML: this.contents,
imgUrl: this.imageUrl,
url: this.url,
version: this.version,
commits: this.commits,
fileContents: this.fileContents,
});
}
}

View file

@ -6,9 +6,9 @@ import project from '../metadata/project.json';
import { InfoMessageType } from './ui/info';
import { RevealableInterface, RevealableTypes } from './sketchObjects/Revealable';
import { getRevealablesforSubproject } from './helpers';
import { SubProject } from './types';
export interface State {
currentIntroStep: number;
currentScene: Scenes;
currentSubproject?: string;
companionState: CompanionState;
@ -20,15 +20,26 @@ export interface State {
revealables: RevealableInterface[];
finishedSubProjects: string[];
setProjectMetadata: (projectName: string) => void;
participantAnonymous: boolean;
finishedGame: boolean;
revealablesFinished: number;
showScore: boolean;
uid: string;
}
const store = create<State>(
devtools((set) => ({
uid: null,
showScore: false,
currentIntroStep: 1,
revealablesFinished: 0,
currentScene: Scenes.OVERVIEW,
currentSubproject: null,
participantAnonymous: false,
companionState: CompanionState.IDLE,
infoMessageShown: false,
infoMessages: [],
finishedGame: false,
addInfoMessage: (newMessage) =>
set((state) => ({ ...state, infoMessages: [...state.infoMessages, newMessage] })),
userMessages: [],

View file

@ -1,4 +1,5 @@
import anime from 'animejs/lib/anime.es';
import { logger } from '../logger';
import store from '../store';
export enum CompanionState {
@ -22,6 +23,7 @@ export class Companion {
messageTextRef: HTMLElement;
messageInputRef: HTMLElement;
messageButtonRef: HTMLElement;
backdrop: HTMLElement;
hoverAnimation: any;
message: CompanionMessage;
@ -31,6 +33,7 @@ export class Companion {
this.messageTextRef = document.getElementById('message-text');
this.messageInputRef = document.getElementById('message-input');
this.messageButtonRef = document.getElementById('message-confirm');
this.backdrop = document.getElementById('comp-backdrop');
this.ref.addEventListener('click', () => this.handleClick());
this.ref.addEventListener('mouseover', () => this.handleMouseEnter());
@ -43,11 +46,13 @@ export class Companion {
store.subscribe(
(companionState) => {
if (companionState === CompanionState.ACTIVE) {
this.backdrop.style.display = 'block';
this.stopAwaitAnimation();
this.showActiveShape();
this.scaleUpCompanion();
this.showMessage(this.message);
} else if (companionState === CompanionState.IDLE) {
this.backdrop.style.display = 'none';
this.scaleDownCompanion();
this.showIdleShape();
this.stopAwaitAnimation();
@ -93,6 +98,11 @@ export class Companion {
// Hide Message
this.messageRef.style.display = 'none';
store.setState({ companionState: CompanionState.IDLE });
logger.log({
type: 'CC',
timestamp: Date.now(),
message: 'Close message',
});
if (this.message.onNext) {
this.message.onNext();
@ -185,7 +195,7 @@ export class Companion {
}
handleClick() {
const { companionState } = store.getState();
/*const { companionState } = store.getState();
let newCompanionState: CompanionState;
if (companionState === CompanionState.ACTIVE) {
@ -194,7 +204,11 @@ export class Companion {
newCompanionState = CompanionState.ACTIVE;
}
store.setState({ companionState: newCompanionState });
store.setState({ companionState: newCompanionState });*/
logger.log({
type: 'CC',
timestamp: Date.now(),
});
}
handleMouseEnter() {

View file

@ -1,28 +1,55 @@
import moment from 'moment';
import { logger } from '../logger';
import { RevealableTypes } from '../sketchObjects/Revealable';
import store from '../store';
export interface Commit {
url: string;
message: string;
time: string;
}
export interface InfoMessageType {
type: RevealableTypes;
headline: string;
innerHTML: string;
imgUrl?: string;
url?: string;
commits?: Commit[];
version?: string;
fileContents?: string;
}
export class InfoMessage {
type: RevealableTypes;
name: string;
infoMessage: HTMLElement;
infoMessageSubheadline: HTMLElement;
infoMessageHeadline: HTMLElement;
infoMessageContents: HTMLElement;
infoMessageClose: HTMLElement;
infoMessageImgRef: HTMLImageElement;
infoMessageLinkRef: HTMLAnchorElement;
infoMessageCommitsHeadlineRef: HTMLElement;
infoMessageCommitsRef: HTMLElement;
infoMessageVersionRef: HTMLElement;
infoMessageLegacyRef: HTMLElement;
backdrop: HTMLElement;
constructor() {
this.infoMessage = document.getElementById('info-message');
this.infoMessageHeadline = document.getElementById('info-message-headline');
this.infoMessageContents = document.getElementById('info-message-contents');
this.infoMessageSubheadline = document.getElementById('info-message-subheadline');
this.infoMessageContents = document.getElementById('info-message-contents-text');
this.infoMessageClose = document.getElementById('info-message-close');
this.infoMessageImgRef = document.getElementById('info-message-img') as HTMLImageElement;
this.infoMessageLinkRef = document.getElementById('info-message-link') as HTMLAnchorElement;
this.infoMessageCommitsRef = document.getElementById('info-message-contents-commits');
this.infoMessageCommitsHeadlineRef = document.getElementById(
'info-message-content-commits-headline'
);
this.infoMessageVersionRef = document.getElementById('info-message-contents-version');
this.infoMessageLegacyRef = document.getElementById('info-message-contents-legacy');
this.backdrop = document.getElementById('backdrop');
this.backdrop.addEventListener('click', this.onBackdropClick);
@ -39,6 +66,11 @@ export class InfoMessage {
const newMessage = state.infoMessages[state.infoMessages.length - 1];
this.setContents(newMessage.headline, newMessage.innerHTML);
this.type = newMessage.type;
this.name = newMessage.headline;
this.infoMessageSubheadline.innerHTML = this.getTextForType();
if (newMessage.imgUrl) {
this.setImg(newMessage.imgUrl);
} else {
@ -51,16 +83,77 @@ export class InfoMessage {
this.infoMessageLinkRef.style.display = 'none';
}
if (this.type === RevealableTypes.CONTRIBUTOR) {
this.infoMessageCommitsRef.style.display = 'block';
this.infoMessageCommitsHeadlineRef.style.display = 'block';
this.setCommits(newMessage.commits);
} else {
this.infoMessageCommitsRef.style.display = 'none';
this.infoMessageCommitsHeadlineRef.style.display = 'none';
}
if (this.type === RevealableTypes.PACKAGE) {
this.infoMessageVersionRef.style.display = 'block';
this.setVersion(newMessage.version);
} else {
this.infoMessageVersionRef.style.display = 'none';
}
if (this.type === RevealableTypes.LEGACY) {
this.infoMessageLegacyRef.style.display = 'block';
this.setLegacy(newMessage.fileContents);
} else {
this.infoMessageLegacyRef.style.display = 'none';
}
store.setState({ infoMessageShown: true });
}
});
}
private getTextForType(): string {
if (this.type === RevealableTypes.CONTRIBUTOR) {
return 'Contributor';
} else if (this.type === RevealableTypes.PACKAGE) {
return 'NPM Package';
} else {
return 'Legacy Alert';
}
}
private setContents(headline: string, innerHTML: string) {
this.infoMessageHeadline.innerText = headline;
this.infoMessageContents.innerHTML = innerHTML;
}
private setVersion(version: string) {
this.infoMessageVersionRef.innerHTML = `This package is installed at<br/><h3>${version}</h3>`;
}
private setLegacy(fileContents: string) {
this.infoMessageLegacyRef.innerHTML = `
<p>Excerpt from the file:</p>
<pre>${fileContents}</pre>
`;
}
private setCommits(commits: Commit[]) {
const commitEls = commits.map(
({ message, url, time }) =>
`<div class="info-message-contents-commit">
<p>${message}</p>
<div>
<span>${moment(time).format(
'LLL'
)}</span> <a href="${url}" target="_blank">Take a look on Github</a>
</div>
</div>`
);
console.log(commitEls);
this.infoMessageCommitsRef.innerHTML = commitEls.join('');
}
private setImg(imgUrl: string) {
this.infoMessageImgRef.src = imgUrl;
this.infoMessageImgRef.style.display = 'block';
@ -82,10 +175,30 @@ export class InfoMessage {
}
private onBackdropClick() {
logger.log({
type:
this.type === RevealableTypes.CONTRIBUTOR
? 'NC'
: this.type === RevealableTypes.LEGACY
? 'LC'
: 'PC',
timestamp: Date.now(),
message: `Closing info message for ${this.name}`,
});
store.setState({ infoMessageShown: false });
}
private onCloseClick() {
logger.log({
type:
this.type === RevealableTypes.CONTRIBUTOR
? 'NC'
: this.type === RevealableTypes.LEGACY
? 'LC'
: 'PC',
timestamp: Date.now(),
message: `Closing info message for ${this.name}`,
});
store.setState({ infoMessageShown: false });
}
}

269
src/ui/intro.ts Normal file
View file

@ -0,0 +1,269 @@
import { logger } from '../logger';
import { Scenes } from '../scenes/scenes';
import store from '../store';
export class Intro {
currentStep: number;
anonymous: boolean;
nextButton: HTMLElement;
anonymousCheckbox: HTMLInputElement;
introContainer: HTMLElement;
introBackdrop: HTMLElement;
step1Container: HTMLElement;
step2Container: HTMLElement;
step3Container: HTMLElement;
step4Container: HTMLElement;
step5Container: HTMLElement;
step6Container: HTMLElement;
step7Container: HTMLElement;
nameRef: HTMLInputElement;
ageRef: HTMLInputElement;
backgroundRef: HTMLInputElement;
experienceRef: HTMLSelectElement;
fb1: HTMLTextAreaElement;
fb2: HTMLTextAreaElement;
fb3: HTMLTextAreaElement;
fb4: HTMLTextAreaElement;
fb5: HTMLTextAreaElement;
fb6: HTMLTextAreaElement;
fb7: HTMLTextAreaElement;
fb8: HTMLTextAreaElement;
fb9: HTMLTextAreaElement;
fb10: HTMLTextAreaElement;
k1: HTMLTextAreaElement;
errorRef: HTMLElement;
constructor() {
this.step1Container = document.getElementById('intro-step1');
this.step2Container = document.getElementById('intro-step2');
this.step3Container = document.getElementById('intro-step3');
this.step4Container = document.getElementById('intro-step4');
this.step5Container = document.getElementById('intro-step5');
this.step6Container = document.getElementById('intro-step6');
this.step7Container = document.getElementById('intro-step7');
this.nextButton = document.getElementById('intro-button');
this.anonymousCheckbox = document.querySelector('#intro-anonymous');
this.introContainer = document.querySelector('#intro');
this.introBackdrop = document.querySelector('#intro-backdrop');
this.nameRef = document.querySelector('#intro-name');
this.ageRef = document.querySelector('#intro-age');
this.backgroundRef = document.querySelector('#intro-background');
this.experienceRef = document.querySelector('#intro-experience');
this.fb1 = document.querySelector('#fb-1');
this.fb2 = document.querySelector('#fb-2');
this.fb3 = document.querySelector('#fb-3');
this.fb4 = document.querySelector('#fb-4');
this.fb5 = document.querySelector('#fb-5');
this.fb6 = document.querySelector('#fb-6');
this.fb7 = document.querySelector('#fb-7');
this.fb8 = document.querySelector('#fb-8');
this.fb9 = document.querySelector('#fb-9');
this.fb10 = document.querySelector('#fb-10');
this.k1 = document.querySelector('#k-1');
this.errorRef = document.querySelector('#intro-error');
this.nextButton.addEventListener('click', () => this.onNextClick());
this.currentStep = store.getState().currentIntroStep;
if (this.currentStep === 0) {
this.introContainer.style.display = 'none';
this.introBackdrop.style.display = 'none';
} else {
this.showStep();
}
store.subscribe((state) => {
this.currentStep = state.currentIntroStep;
this.anonymous = state.participantAnonymous;
if (state.currentIntroStep === 2) {
this.nextButton.innerHTML = 'Agree';
} else if (state.currentIntroStep === 3) {
this.nextButton.innerHTML = 'Confirm';
} else if (state.currentIntroStep === 4) {
this.nextButton.innerHTML = 'Start already!';
} else if (state.currentIntroStep === 7) {
this.nextButton.style.display = 'none';
} else if (state.currentIntroStep === 0) {
this.introContainer.style.display = 'none';
this.introBackdrop.style.display = 'none';
} else {
this.nextButton.innerHTML = 'Continue';
}
if (state.currentIntroStep !== 0) {
this.introContainer.style.display = 'block';
this.introBackdrop.style.display = 'block';
}
if (this.anonymous) {
this.hideNameInput();
}
this.showStep();
});
}
private onNextClick() {
const currentStep = store.getState().currentIntroStep;
// Track if particpant wants to be anon
if (currentStep === 2) {
store.setState({ participantAnonymous: this.anonymousCheckbox.checked });
}
// Validate input
if (currentStep === 3) {
const name = this.nameRef.value;
const age = Number(this.ageRef.value);
const background = this.backgroundRef.value;
const experience = this.experienceRef.value;
if (
(this.anonymous ? false : !name) ||
!age ||
!background ||
experience === 'Choose an option...'
) {
this.errorRef.style.display = 'block';
return;
} else {
this.errorRef.style.display = 'none';
store.setState({
uid: `${Date.now()}_${name.replace(/[^a-zA-Z ]/g, '').toLowerCase()}`,
});
this.sendDemographicData(name, age, background, experience);
}
}
if (currentStep === 4) {
store.setState({ currentIntroStep: 0 });
setTimeout(() => {
if (store.getState().currentScene !== Scenes.DETAIL) {
store.getState().addUserMessage({
inputWanted: false,
text: "Hey there! Need help here? You'll have to touch the parts of the project you want to take a look at with you character's head. As soon as the project part (packages/...) is highlighted, you can click it to dive deeper into what lies behind 🔬",
});
}
}, 3000);
return;
}
if (currentStep === 6) {
this.sendGeneralQuestionAnswers();
}
if (currentStep === 5) {
this.sendKnowledgeQuestionAnswers();
}
store.setState((state) => ({ currentIntroStep: state.currentIntroStep + 1 }));
}
private sendDemographicData(name: string, age: number, background: string, experience: string) {
logger.logPersonalData(name, age, background, experience, this.anonymous);
}
private sendGeneralQuestionAnswers() {
const answers = [
this.fb1.value,
this.fb2.value,
this.fb3.value,
this.fb4.value,
this.fb5.value,
this.fb6.value,
this.fb7.value,
this.fb8.value,
this.fb9.value,
this.fb10.value,
];
logger.logQuestions(answers);
}
private sendKnowledgeQuestionAnswers() {
const answers = [this.k1.value];
logger.logQuestions(answers, true);
}
private hideNameInput() {
this.nameRef.style.display = 'none';
const label = document.querySelector('#name-label') as HTMLElement;
label.style.display = 'none';
}
private showStep() {
if (this.currentStep === 1) {
this.step1Container.style.display = 'flex';
this.step2Container.style.display = 'none';
this.step3Container.style.display = 'none';
this.step4Container.style.display = 'none';
this.step5Container.style.display = 'none';
this.step6Container.style.display = 'none';
this.step7Container.style.display = 'none';
} else if (this.currentStep === 2) {
this.step1Container.style.display = 'none';
this.step2Container.style.display = 'flex';
this.step3Container.style.display = 'none';
this.step4Container.style.display = 'none';
this.step5Container.style.display = 'none';
this.step6Container.style.display = 'none';
this.step7Container.style.display = 'none';
} else if (this.currentStep === 3) {
this.step1Container.style.display = 'none';
this.step2Container.style.display = 'none';
this.step3Container.style.display = 'flex';
this.step4Container.style.display = 'none';
this.step5Container.style.display = 'none';
this.step6Container.style.display = 'none';
this.step7Container.style.display = 'none';
} else if (this.currentStep === 4) {
this.step1Container.style.display = 'none';
this.step2Container.style.display = 'none';
this.step3Container.style.display = 'none';
this.step4Container.style.display = 'flex';
this.step5Container.style.display = 'none';
this.step6Container.style.display = 'none';
this.step7Container.style.display = 'none';
} else if (this.currentStep === 5) {
this.step1Container.style.display = 'none';
this.step2Container.style.display = 'none';
this.step3Container.style.display = 'none';
this.step4Container.style.display = 'none';
this.step5Container.style.display = 'flex';
this.step6Container.style.display = 'none';
this.step7Container.style.display = 'none';
} else if (this.currentStep === 6) {
this.step1Container.style.display = 'none';
this.step2Container.style.display = 'none';
this.step3Container.style.display = 'none';
this.step4Container.style.display = 'none';
this.step5Container.style.display = 'none';
this.step6Container.style.display = 'flex';
this.step7Container.style.display = 'none';
} else if (this.currentStep === 7) {
this.step1Container.style.display = 'none';
this.step2Container.style.display = 'none';
this.step3Container.style.display = 'none';
this.step4Container.style.display = 'none';
this.step5Container.style.display = 'none';
this.step6Container.style.display = 'none';
this.step7Container.style.display = 'flex';
}
}
}

42
src/ui/score.ts Normal file
View file

@ -0,0 +1,42 @@
import store from '../store';
export class Score {
showScore: boolean;
scoreFound: number;
scoreTotal: number;
scoreRef: HTMLElement;
scoreFoundRef: HTMLElement;
scoreTotalRef: HTMLElement;
constructor() {
this.scoreRef = document.querySelector('#score');
this.scoreFoundRef = document.querySelector('#score-found');
this.scoreTotalRef = document.querySelector('#score-total');
this.scoreFound = 0;
this.scoreTotal = store.getState().revealables.length;
this.scoreFoundRef.innerHTML = this.scoreFound.toString();
this.scoreTotalRef.innerHTML = this.scoreTotal.toString();
if (store.getState().showScore) {
this.scoreRef.style.display = 'flex';
}
store.subscribe((state) => {
if (state.showScore) {
this.scoreRef.style.display = 'flex';
} else {
this.scoreRef.style.display = 'none';
}
this.scoreTotal = state.revealables.length;
this.scoreFound = state.revealablesFinished;
this.scoreFoundRef.innerHTML = this.scoreFound.toString();
this.scoreTotalRef.innerHTML = this.scoreTotal.toString();
});
}
}

View file

@ -23,6 +23,60 @@ main {
button {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell,
'Open Sans', 'Helvetica Neue', sans-serif;
font-size: 16px;
padding: 6px 15px;
background-color: transparent;
border: 2px solid black;
border-radius: 5px;
font-weight: bold;
max-width: 180px;
&:hover {
cursor: pointer;
background-color: black;
color: white;
}
}
label {
font-weight: bold;
margin-top: 15px;
}
input[type='text'],
input[type='number'] {
display: block;
width: 100%;
margin-top: 10px;
font-size: 18px;
padding: 8px 12px;
outline: 0;
border: 2px solid black;
border-radius: 5px;
}
select {
display: block;
width: 100%;
margin-top: 10px;
font-size: 18px;
padding: 8px 12px;
outline: 0;
border: 2px solid black;
border-radius: 5px;
}
textarea {
display: block;
width: 100%;
margin-top: 25px;
min-height: 8rem;
font-size: 18px;
padding: 8px 12px;
outline: 0;
border: 2px solid black;
border-radius: 5px;
}
.ui {
@ -32,9 +86,11 @@ button {
right: 40px;
display: flex;
justify-content: flex-end;
align-items: center;
#companion {
position: relative;
z-index: 10;
&:hover {
cursor: pointer;
@ -90,6 +146,7 @@ button {
#message {
display: none;
position: absolute;
z-index: 10;
right: 60px;
bottom: 50px;
margin-right: 100px;
@ -143,6 +200,25 @@ button {
}
}
#score {
display: none;
position: absolute;
left: 0;
align-items: flex-end;
color: #b0b7bf;
font-size: 32px;
#score-found {
font-size: 60px;
font-weight: bold;
}
#score-divider {
font-size: 42px;
align-self: flex-end;
}
}
#info-message {
display: none;
position: fixed;
@ -159,10 +235,81 @@ button {
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
#info-message-contents {
#info-message-contents-text {
margin: 1.5rem 0;
}
#info-message-contents-legacy {
p {
margin: 0;
font-size: 14px;
color: #4e4e4e;
font-style: italic;
}
pre {
background-color: black;
color: white;
padding: 0.5rem;
border-radius: 10px;
}
}
h4 {
text-transform: uppercase;
letter-spacing: 1px;
font-weight: bold;
color: #b0b7bf;
margin: 0 0 1rem 0;
}
#info-message-contents-version {
h3 {
display: inline-block;
margin: 1rem 0 0 0;
padding: 0.4rem 0.6rem;
background-color: #4e4e4e;
color: #fff;
border-radius: 10px;
font-size: 30px;
letter-spacing: 2px;
font-weight: bold;
}
}
#info-message-contents-commits {
.info-message-contents-commit {
background-color: #d0d5d9;
border-radius: 10px;
padding: 1rem;
&:not(:last-child) {
margin-bottom: 1.5rem;
}
p {
margin: 0;
}
div {
span {
font-size: 14px;
color: #4e4e4e;
}
a {
color: #0d0d0d;
font-size: 14px;
}
}
}
}
}
#info-message-header {
display: flex;
justify-content: space-between;
align-items: flex-end;
justify-content: flex-start;
align-items: center;
margin-bottom: 25px;
#info-message-img {
@ -170,11 +317,22 @@ button {
height: 100px;
width: 100px;
border-radius: 50%;
margin-right: 1.5rem;
}
#info-message-header-text {
#info-message-subheadline {
text-transform: uppercase;
letter-spacing: 1px;
font-weight: bold;
color: #b0b7bf;
margin: 0;
}
#info-message-headline {
margin: 0;
text-align: center;
padding: 0;
text-align: left;
}
}
}
@ -211,6 +369,97 @@ button {
}
}
}
#intro {
background-color: white;
position: fixed;
z-index: 10;
top: 50%;
left: 50%;
min-width: 60%;
max-width: 90%;
max-height: 90%;
overflow-y: auto;
transform: translate(-50%, -50%);
background-color: #fff;
padding: 2.5rem 4rem;
border-radius: 15px;
--tw-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25);
box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000),
var(--tw-shadow);
#intro-step1 {
display: flex;
flex-direction: column;
h3 {
margin: 0.6rem 0 0rem 0;
}
p {
margin: 0.6rem 0 1rem;
}
#intro-disclaimer {
background-color: #ffedd5;
color: #7c2d12;
padding: 4px 20px;
border-radius: 5px;
border-left: 7px solid #7c2d12;
margin-top: 15px;
}
}
#intro-step2 {
display: none;
flex-direction: column;
}
#intro-step3 {
display: none;
flex-direction: column;
}
#intro-step4 {
display: none;
flex-direction: column;
}
#intro-step5 {
display: none;
flex-direction: column;
}
#intro-step6 {
display: none;
flex-direction: column;
label {
margin-top: 60px;
}
}
#intro-step7 {
display: none;
flex-direction: column;
}
#intro-button {
float: right;
margin-top: 25px;
}
#intro-error {
display: none;
margin-top: 25px;
background-color: #fecaca;
color: #7f1d1d;
padding: 8px 20px;
border-radius: 5px;
border-left: 7px solid #7f1d1d;
margin-top: 15px;
}
}
}
#backdrop {
@ -223,3 +472,25 @@ button {
bottom: 0;
background-color: rgba(0, 0, 0, 0.7);
}
#comp-backdrop {
display: none;
position: fixed;
z-index: 3;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.8);
}
#intro-backdrop {
display: block;
position: fixed;
z-index: 3;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(255, 255, 255, 0.95);
}