Initial commit

This commit is contained in:
undergroundwires
2019-12-31 16:09:39 +01:00
commit 4e7f244190
108 changed files with 17296 additions and 0 deletions

View File

@@ -0,0 +1,62 @@
<template>
<div>
<div v-if="categoryIds != null && categoryIds.length > 0" class="cards">
<CardListItem
class="card"
v-for="categoryId of categoryIds"
v-bind:key="categoryId"
:categoryId="categoryId"
:activeCategoryId="activeCategoryId"
v-on:selected="onSelected(categoryId, $event)">
</CardListItem>
</div>
<div v-else class="error">Something went bad 😢</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from 'vue-property-decorator';
import CardListItem from './CardListItem.vue';
import { StatefulVue, IApplicationState } from '@/presentation/StatefulVue';
import { ICategory } from '@/domain/ICategory';
@Component({
components: {
CardListItem,
},
})
export default class CardList extends StatefulVue {
public categoryIds: number[] = [];
public activeCategoryId?: number = null;
public async mounted() {
const state = await this.getCurrentStateAsync();
this.setCategories(state.categories);
}
public onSelected(categoryId: number, isExpanded: boolean) {
this.activeCategoryId = isExpanded ? categoryId : undefined;
}
private setCategories(categories: ReadonlyArray<ICategory>): void {
this.categoryIds = categories.map((category) => category.id);
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/fonts.scss";
.cards {
display: flex;
flex-flow: row wrap;
.card {
}
}
.error {
width: 100%;
text-align: center;
font-size: 3.5em;
font: $default-font;
}
</style>

View File

@@ -0,0 +1,248 @@
<template>
<div class="card"
v-on:click="onSelected(!isExpanded)"
v-bind:class="{
'is-collapsed': !isExpanded,
'is-inactive': activeCategoryId && activeCategoryId != categoryId,
'is-expanded': isExpanded}">
<div class="card__inner">
<span v-if="cardTitle && cardTitle.length > 0">{{cardTitle}}</span>
<span v-else>Oh no 😢</span>
<font-awesome-icon :icon="['far', isExpanded ? 'folder-open' : 'folder']" class="expand-button" />
</div>
<div class="card__expander" v-on:click.stop>
<font-awesome-icon :icon="['fas', 'times']" class="close-button" v-on:click="onSelected(false)"/>
<CardListItemScripts :categoryId="categoryId"></CardListItemScripts>
</div>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch, Emit } from 'vue-property-decorator';
import CardListItemScripts from './CardListItemScripts.vue';
import { StatefulVue } from '@/presentation/StatefulVue';
@Component({
components: {
CardListItemScripts,
},
})
export default class CardListItem extends StatefulVue {
@Prop() public categoryId!: number;
@Prop() public activeCategoryId!: number;
public cardTitle?: string = '';
public isExpanded: boolean = false;
@Emit('selected')
public onSelected(isExpanded: boolean) {
this.isExpanded = isExpanded;
}
@Watch('activeCategoryId')
public async onActiveCategoryChanged(value: |number) {
this.isExpanded = value === this.categoryId;
}
public async mounted() {
this.cardTitle = this.categoryId ? await this.getCardTitleAsync(this.categoryId) : undefined;
}
@Watch('categoryId')
public async onCategoryIdChanged(value: |number) {
this.cardTitle = value ? await this.getCardTitleAsync(value) : undefined;
}
private async getCardTitleAsync(categoryId: number): Promise<string | undefined> {
const state = await this.getCurrentStateAsync();
const category = state.getCategory(this.categoryId);
return category ? category.name : undefined;
}
}
</script>
<style scoped lang="scss">
@import "@/presentation/styles/colors.scss";
.card {
margin: 15px;
width: calc((100% / 3) - 30px);
transition: all 0.2s ease-in-out;
//media queries for stacking cards
@media screen and (max-width: 991px) {
width: calc((100% / 2) - 30px);
}
@media screen and (max-width: 767px) {
width: 100%;
}
@media screen and (max-width: 380px) {
width: 90%;
}
&:hover {
.card__inner {
background-color: $accent;
transform: scale(1.05);
}
}
&__inner {
width: 100%;
padding: 30px;
position: relative;
cursor: pointer;
background-color: $gray;
color: $light-gray;
font-size: 1.5em;
text-transform: uppercase;
text-align: center;
transition: all 0.2s ease-in-out;
&:after {
transition: all 0.3s ease-in-out;
}
.expand-button {
width: 100%;
margin-top: .25em;
}
}
//Expander
&__expander {
transition: all 0.2s ease-in-out;
background-color: $slate;
width: 100%;
position: relative;
display: flex;
justify-content: center;
align-items: center;
text-transform: uppercase;
color: $light-gray;
font-size: 1.5em;
.close-button {
font-size: 0.75em;
position: absolute;
top: 10px;
right: 10px;
cursor: pointer;
&:hover {
opacity: 0.9;
}
}
}
&.is-collapsed {
.card__inner {
&:after {
content: "";
opacity: 0;
}
}
.card__expander {
max-height: 0;
min-height: 0;
overflow: hidden;
margin-top: 0;
opacity: 0;
}
}
&.is-expanded {
.card__inner {
background-color: $accent;
&:after{
content: "";
opacity: 1;
display: block;
height: 0;
width: 0;
position: absolute;
bottom: -30px;
left: calc(50% - 15px);
border-left: 15px solid transparent;
border-right: 15px solid transparent;
border-bottom: 15px solid #333a45;
}
}
.card__expander {
min-height: 200px;
// max-height: 1000px;
// overflow-y: auto;
margin-top: 30px;
opacity: 1;
}
&:hover {
.card__inner {
transform: scale(1);
}
}
}
&.is-inactive {
.card__inner {
pointer-events: none;
opacity: 0.5;
}
&:hover {
.card__inner {
background-color: $gray;
transform: scale(1);
}
}
}
}
//Expander Widths
//when 3 cards in a row
@media screen and (min-width: 992px) {
.card:nth-of-type(3n+2) .card__expander {
margin-left: calc(-100% - 30px);
}
.card:nth-of-type(3n+3) .card__expander {
margin-left: calc(-200% - 60px);
}
.card:nth-of-type(3n+4) {
clear: left;
}
.card__expander {
width: calc(300% + 60px);
}
}
//when 2 cards in a row
@media screen and (min-width: 768px) and (max-width: 991px) {
.card:nth-of-type(2n+2) .card__expander {
margin-left: calc(-100% - 30px);
}
.card:nth-of-type(2n+3) {
clear: left;
}
.card__expander {
width: calc(200% + 30px);
}
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<span>
<span v-if="nodes != null && nodes.length > 0">
<SelectableTree
:nodes="nodes"
:selectedNodeIds="selectedNodeIds"
:filterPredicate="filterPredicate"
:filterText="filterText"
v-on:nodeSelected="checkNodeAsync($event)">
</SelectableTree>
</span>
<span v-else>Nooo 😢</span>
</span>
</template>
<script lang="ts">
import { Component, Prop, Vue, Watch } from 'vue-property-decorator';
import { StatefulVue } from '@/presentation/StatefulVue';
import { Category } from '@/domain/Category';
import { IRepository } from '@/infrastructure/Repository/IRepository';
import { IScript } from '@/domain/IScript';
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { IFilterMatches } from '@/application/State/Filter/IFilterMatches';
import { ScriptNodeParser } from './ScriptNodeParser';
import SelectableTree, { FilterPredicate } from './../SelectableTree/SelectableTree.vue';
import { INode } from './../SelectableTree/INode';
@Component({
components: {
SelectableTree,
},
})
export default class CardListItemScripts extends StatefulVue {
@Prop() public categoryId!: number;
public nodes?: INode[] = null;
public selectedNodeIds?: string[] = null;
public filterText?: string = null;
private matches?: IFilterMatches;
public async mounted() {
// React to state changes
const state = await this.getCurrentStateAsync();
this.reactToChanges(state);
// Update initial state
await this.updateNodesAsync(this.categoryId);
}
public async checkNodeAsync(node: INode) {
if (node.children != null && node.children.length > 0) {
return; // only interested in script nodes
}
const state = await this.getCurrentStateAsync();
if (node.selected) {
state.selection.addSelectedScript(node.id);
} else {
state.selection.removeSelectedScript(node.id);
}
}
@Watch('categoryId')
public async updateNodesAsync(categoryId: |number) {
this.nodes = categoryId ?
await ScriptNodeParser.parseNodes(categoryId, await this.getCurrentStateAsync())
: undefined;
}
public filterPredicate(node: INode): boolean {
return this.matches.scriptMatches.some((script: IScript) => script.id === node.id);
}
private reactToChanges(state: IApplicationState) {
// Update selection data
const updateNodeSelection = (node: INode, selectedScripts: ReadonlyArray<IScript>): INode => {
return {
id: node.id,
text: node.text,
selected: selectedScripts.some((script) => script.id === node.id),
children: node.children ? node.children.map((child) => updateNodeSelection(child, selectedScripts)) : [],
documentationUrls: node.documentationUrls,
};
};
state.selection.changed.on(
(selectedScripts: ReadonlyArray<IScript>) =>
this.nodes = this.nodes.map((node: INode) => updateNodeSelection(node, selectedScripts)),
);
// Update search / filter data
state.filter.filterRemoved.on(() =>
this.filterText = '');
state.filter.filtered.on((matches: IFilterMatches) => {
this.filterText = matches.query;
this.matches = matches;
});
}
}
</script>
<style scoped lang="scss">
</style>

View File

@@ -0,0 +1,43 @@
import { ICategory } from './../../../domain/ICategory';
import { IApplicationState, IUserSelection } from '@/application/State/IApplicationState';
import { INode } from './../SelectableTree/INode';
export class ScriptNodeParser {
public static parseNodes(categoryId: number, state: IApplicationState): INode[] | undefined {
const category = state.getCategory(categoryId);
if (!category) {
throw new Error(`Category with id ${categoryId} does not exist`);
}
const tree = this.parseNodesRecursively(category, state.selection);
return tree;
}
private static parseNodesRecursively(parentCategory: ICategory, selection: IUserSelection): INode[] {
const nodes = new Array<INode>();
if (parentCategory.subCategories && parentCategory.subCategories.length > 0) {
for (const subCategory of parentCategory.subCategories) {
const subCategoryNodes = this.parseNodesRecursively(subCategory, selection);
nodes.push(
{
id: `cat${subCategory.id}`,
text: subCategory.name,
selected: false,
children: subCategoryNodes,
documentationUrls: subCategory.documentationUrls,
});
}
}
if (parentCategory.scripts && parentCategory.scripts.length > 0) {
for (const script of parentCategory.scripts) {
nodes.push( {
id: script.id,
text: script.name,
selected: selection.isSelected(script),
children: undefined,
documentationUrls: script.documentationUrls,
});
}
}
return nodes;
}
}