initial commit

This commit is contained in:
Andre Schaf 2023-09-27 17:49:51 +02:00
parent 1790106c30
commit 03b0dd8422
33 changed files with 1006 additions and 513 deletions

View file

@ -0,0 +1,47 @@
import { TestBed } from '@angular/core/testing';
import { MODULES } from './mocks/modules';
import { ApiService } from './api.service';
describe('ApiService', () => {
let service: ApiService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(ApiService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should return all modules', () => {
service.getModules().subscribe(modules => {
expect(modules.length).toBe(MODULES.length);
});
});
it('should return module with id 1', () => {
service.getModule(1).subscribe(module => {
expect(module).toBeTruthy();
expect(module?.id).toBe(1);
});
});
it('should return undefined when module with id 100 does not exist', () => {
service.getModule(100).subscribe(module => {
expect(module).toBeFalsy();
});
});
it('should return ordered modules', () => {
const mod1 = MODULES[0];
const mod2 = MODULES[1];
const orderedModules = service.orderModules([mod1, mod2]);
expect(orderedModules.length).toBe(2);
expect(orderedModules[0]).toBe(mod1);
expect(orderedModules[1]).toBe(mod2);
});
});

26
src/app/api.service.ts Normal file
View file

@ -0,0 +1,26 @@
import { Injectable } from '@angular/core';
import { Observable, of } from 'rxjs';
import { Module } from './interfaces/module';
import { MODULES } from './mocks/modules';
@Injectable({
providedIn: 'root'
})
export class ApiService {
constructor() { }
getModules(): Observable<Module[]> {
return of(MODULES);
}
getModule(id: number): Observable<Module|undefined> {
return of(MODULES.find(module => module.id === id));
}
orderModules(modules: Module[]): Module[] {
return modules;
}
}

View file

@ -1,7 +1,13 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router'; import { RouterModule, Routes } from '@angular/router';
const routes: Routes = []; import { HomeComponent } from './home/home.component';
import { ModuleDetailComponent } from './module-detail/module-detail.component';
const routes: Routes = [
{ path: '', component: HomeComponent },
{ path: 'module/:id', component: ModuleDetailComponent },
];
@NgModule({ @NgModule({
imports: [RouterModule.forRoot(routes)], imports: [RouterModule.forRoot(routes)],

View file

@ -0,0 +1,23 @@
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
color: #333;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.content {
display: flex;
margin: 82px auto 32px;
padding: 0 16px 200px;
max-width: 960px;
flex-direction: column;
align-items: center;
}
.content-inner {
width: 100%;
display: flex;
}

View file

@ -1,484 +1,12 @@
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content below * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * Delete the template below * * * * * * * * * * -->
<!-- * * * * * * * to get started with your project! * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<style>
:host {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 14px;
color: #333;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1,
h2,
h3,
h4,
h5,
h6 {
margin: 8px 0;
}
p {
margin: 0;
}
.spacer {
flex: 1;
}
.toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
height: 60px;
display: flex;
align-items: center;
background-color: #1976d2;
color: white;
font-weight: 600;
}
.toolbar img {
margin: 0 16px;
}
.toolbar #twitter-logo {
height: 40px;
margin: 0 8px;
}
.toolbar #youtube-logo {
height: 40px;
margin: 0 16px;
}
.toolbar #twitter-logo:hover,
.toolbar #youtube-logo:hover {
opacity: 0.8;
}
.content {
display: flex;
margin: 82px auto 32px;
padding: 0 16px;
max-width: 960px;
flex-direction: column;
align-items: center;
}
svg.material-icons {
height: 24px;
width: auto;
}
svg.material-icons:not(:last-child) {
margin-right: 8px;
}
.card svg.material-icons path {
fill: #888;
}
.card-container {
display: flex;
flex-wrap: wrap;
justify-content: center;
margin-top: 16px;
}
.card {
all: unset;
border-radius: 4px;
border: 1px solid #eee;
background-color: #fafafa;
height: 40px;
width: 200px;
margin: 0 8px 16px;
padding: 16px;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
transition: all 0.2s ease-in-out;
line-height: 24px;
}
.card-container .card:not(:last-child) {
margin-right: 0;
}
.card.card-small {
height: 16px;
width: 168px;
}
.card-container .card:not(.highlight-card) {
cursor: pointer;
}
.card-container .card:not(.highlight-card):hover {
transform: translateY(-3px);
box-shadow: 0 4px 17px rgba(0, 0, 0, 0.35);
}
.card-container .card:not(.highlight-card):hover .material-icons path {
fill: rgb(105, 103, 103);
}
.card.highlight-card {
background-color: #1976d2;
color: white;
font-weight: 600;
border: none;
width: auto;
min-width: 30%;
position: relative;
}
.card.card.highlight-card span {
margin-left: 60px;
}
svg#rocket {
width: 80px;
position: absolute;
left: -10px;
top: -24px;
}
svg#rocket-smoke {
height: calc(100vh - 95px);
position: absolute;
top: 10px;
right: 180px;
z-index: -10;
}
a,
a:visited,
a:hover {
color: #1976d2;
text-decoration: none;
}
a:hover {
color: #125699;
}
.terminal {
position: relative;
width: 80%;
max-width: 600px;
border-radius: 6px;
padding-top: 45px;
margin-top: 8px;
overflow: hidden;
background-color: rgb(15, 15, 16);
}
.terminal::before {
content: "\2022 \2022 \2022";
position: absolute;
top: 0;
left: 0;
height: 4px;
background: rgb(58, 58, 58);
color: #c2c3c4;
width: 100%;
font-size: 2rem;
line-height: 0;
padding: 14px 0;
text-indent: 4px;
}
.terminal pre {
font-family: SFMono-Regular,Consolas,Liberation Mono,Menlo,monospace;
color: white;
padding: 0 1rem 1rem;
margin: 0;
}
.circle-link {
height: 40px;
width: 40px;
border-radius: 40px;
margin: 8px;
background-color: white;
border: 1px solid #eeeeee;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.12), 0 1px 2px rgba(0, 0, 0, 0.24);
transition: 1s ease-out;
}
.circle-link:hover {
transform: translateY(-0.25rem);
box-shadow: 0px 3px 15px rgba(0, 0, 0, 0.2);
}
footer {
margin-top: 8px;
display: flex;
align-items: center;
line-height: 20px;
}
footer a {
display: flex;
align-items: center;
}
.github-star-badge {
color: #24292e;
display: flex;
align-items: center;
font-size: 12px;
padding: 3px 10px;
border: 1px solid rgba(27,31,35,.2);
border-radius: 3px;
background-image: linear-gradient(-180deg,#fafbfc,#eff3f6 90%);
margin-left: 4px;
font-weight: 600;
}
.github-star-badge:hover {
background-image: linear-gradient(-180deg,#f0f3f6,#e6ebf1 90%);
border-color: rgba(27,31,35,.35);
background-position: -.5em;
}
.github-star-badge .material-icons {
height: 16px;
width: 16px;
margin-right: 4px;
}
svg#clouds {
position: fixed;
bottom: -160px;
left: -230px;
z-index: -10;
width: 1920px;
}
/* Responsive Styles */
@media screen and (max-width: 767px) {
.card-container > *:not(.circle-link) ,
.terminal {
width: 100%;
}
.card:not(.highlight-card) {
height: 16px;
margin: 8px 0;
}
.card.highlight-card span {
margin-left: 72px;
}
svg#rocket-smoke {
right: 120px;
transform: rotate(-5deg);
}
}
@media screen and (max-width: 575px) {
svg#rocket-smoke {
display: none;
visibility: hidden;
}
}
</style>
<!-- Toolbar -->
<div class="toolbar" role="banner">
<img
width="40"
alt="Angular Logo"
src="data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNTAgMjUwIj4KICAgIDxwYXRoIGZpbGw9IiNERDAwMzEiIGQ9Ik0xMjUgMzBMMzEuOSA2My4ybDE0LjIgMTIzLjFMMTI1IDIzMGw3OC45LTQzLjcgMTQuMi0xMjMuMXoiIC8+CiAgICA8cGF0aCBmaWxsPSIjQzMwMDJGIiBkPSJNMTI1IDMwdjIyLjItLjFWMjMwbDc4LjktNDMuNyAxNC4yLTEyMy4xTDEyNSAzMHoiIC8+CiAgICA8cGF0aCAgZmlsbD0iI0ZGRkZGRiIgZD0iTTEyNSA1Mi4xTDY2LjggMTgyLjZoMjEuN2wxMS43LTI5LjJoNDkuNGwxMS43IDI5LjJIMTgzTDEyNSA1Mi4xem0xNyA4My4zaC0zNGwxNy00MC45IDE3IDQwLjl6IiAvPgogIDwvc3ZnPg=="
/>
<span>Welcome</span>
<div class="spacer"></div>
<a aria-label="Angular on twitter" target="_blank" rel="noopener" href="https://twitter.com/angular" title="Twitter">
<svg id="twitter-logo" height="24" data-name="Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 400 400">
<rect width="400" height="400" fill="none"/>
<path d="M153.62,301.59c94.34,0,145.94-78.16,145.94-145.94,0-2.22,0-4.43-.15-6.63A104.36,104.36,0,0,0,325,122.47a102.38,102.38,0,0,1-29.46,8.07,51.47,51.47,0,0,0,22.55-28.37,102.79,102.79,0,0,1-32.57,12.45,51.34,51.34,0,0,0-87.41,46.78A145.62,145.62,0,0,1,92.4,107.81a51.33,51.33,0,0,0,15.88,68.47A50.91,50.91,0,0,1,85,169.86c0,.21,0,.43,0,.65a51.31,51.31,0,0,0,41.15,50.28,51.21,51.21,0,0,1-23.16.88,51.35,51.35,0,0,0,47.92,35.62,102.92,102.92,0,0,1-63.7,22A104.41,104.41,0,0,1,75,278.55a145.21,145.21,0,0,0,78.62,23" fill="#fff"/>
</svg>
</a>
<a aria-label="Angular on YouTube" target="_blank" rel="noopener" href="https://youtube.com/angular" title="YouTube">
<svg id="youtube-logo" height="24" width="24" data-name="Logo" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#fff">
<path d="M0 0h24v24H0V0z" fill="none"/>
<path d="M21.58 7.19c-.23-.86-.91-1.54-1.77-1.77C18.25 5 12 5 12 5s-6.25 0-7.81.42c-.86.23-1.54.91-1.77 1.77C2 8.75 2 12 2 12s0 3.25.42 4.81c.23.86.91 1.54 1.77 1.77C5.75 19 12 19 12 19s6.25 0 7.81-.42c.86-.23 1.54-.91 1.77-1.77C22 15.25 22 12 22 12s0-3.25-.42-4.81zM10 15V9l5.2 3-5.2 3z"/>
</svg>
</a>
</div>
<div class="content" role="main"> <div class="content" role="main">
<h1>{{title}}</h1>
<!-- Highlight Card --> <div class="content-inner">
<div class="card highlight-card card-small"> <app-modules [style]="'all'"></app-modules>
<router-outlet></router-outlet>
<svg id="rocket" xmlns="http://www.w3.org/2000/svg" width="101.678" height="101.678" viewBox="0 0 101.678 101.678"> <app-modules [style]="'cluster'"></app-modules>
<title>Rocket Ship</title>
<g id="Group_83" data-name="Group 83" transform="translate(-141 -696)">
<circle id="Ellipse_8" data-name="Ellipse 8" cx="50.839" cy="50.839" r="50.839" transform="translate(141 696)" fill="#dd0031"/>
<g id="Group_47" data-name="Group 47" transform="translate(165.185 720.185)">
<path id="Path_33" data-name="Path 33" d="M3.4,42.615a3.084,3.084,0,0,0,3.553,3.553,21.419,21.419,0,0,0,12.215-6.107L9.511,30.4A21.419,21.419,0,0,0,3.4,42.615Z" transform="translate(0.371 3.363)" fill="#fff"/>
<path id="Path_34" data-name="Path 34" d="M53.3,3.221A3.09,3.09,0,0,0,50.081,0,48.227,48.227,0,0,0,18.322,13.437c-6-1.666-14.991-1.221-18.322,7.218A33.892,33.892,0,0,1,9.439,25.1l-.333.666a3.013,3.013,0,0,0,.555,3.553L23.985,43.641a2.9,2.9,0,0,0,3.553.555l.666-.333A33.892,33.892,0,0,1,32.647,53.3c8.55-3.664,8.884-12.326,7.218-18.322A48.227,48.227,0,0,0,53.3,3.221ZM34.424,9.772a6.439,6.439,0,1,1,9.106,9.106,6.368,6.368,0,0,1-9.106,0A6.467,6.467,0,0,1,34.424,9.772Z" transform="translate(0 0.005)" fill="#fff"/>
</g>
</g>
</svg>
<span>{{ title }} app is running!</span>
<svg id="rocket-smoke" xmlns="http://www.w3.org/2000/svg" width="516.119" height="1083.632" viewBox="0 0 516.119 1083.632">
<title>Rocket Ship Smoke</title>
<path id="Path_40" data-name="Path 40" d="M644.6,141S143.02,215.537,147.049,870.207s342.774,201.755,342.774,201.755S404.659,847.213,388.815,762.2c-27.116-145.51-11.551-384.124,271.9-609.1C671.15,139.365,644.6,141,644.6,141Z" transform="translate(-147.025 -140.939)" fill="#f5f5f5"/>
</svg>
</div> </div>
<!-- Resources --> <app-cart></app-cart>
<h2>Resources</h2>
<p>Here are some links to help you get started:</p>
<div class="card-container">
<a class="card" target="_blank" rel="noopener" href="https://angular.io/tutorial">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M5 13.18v4L12 21l7-3.82v-4L12 17l-7-3.82zM12 3L1 9l11 6 9-4.91V17h2V9L12 3z"/></svg>
<span>Learn Angular</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg> </a>
<a class="card" target="_blank" rel="noopener" href="https://angular.io/cli">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M9.4 16.6L4.8 12l4.6-4.6L8 6l-6 6 6 6 1.4-1.4zm5.2 0l4.6-4.6-4.6-4.6L16 6l6 6-6 6-1.4-1.4z"/></svg>
<span>CLI Documentation</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://material.angular.io">
<svg xmlns="http://www.w3.org/2000/svg" style="margin-right: 8px" width="21.813" height="23.453" viewBox="0 0 179.2 192.7"><path fill="#ffa726" d="M89.4 0 0 32l13.5 118.4 75.9 42.3 76-42.3L179.2 32 89.4 0z"/><path fill="#fb8c00" d="M89.4 0v192.7l76-42.3L179.2 32 89.4 0z"/><path fill="#ffe0b2" d="m102.9 146.3-63.3-30.5 36.3-22.4 63.7 30.6-36.7 22.3z"/><path fill="#fff3e0" d="M102.9 122.8 39.6 92.2l36.3-22.3 63.7 30.6-36.7 22.3z"/><path fill="#fff" d="M102.9 99.3 39.6 68.7l36.3-22.4 63.7 30.6-36.7 22.4z"/></svg>
<span>Angular Material</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://blog.angular.io/">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M13.5.67s.74 2.65.74 4.8c0 2.06-1.35 3.73-3.41 3.73-2.07 0-3.63-1.67-3.63-3.73l.03-.36C5.21 7.51 4 10.62 4 14c0 4.42 3.58 8 8 8s8-3.58 8-8C20 8.61 17.41 3.8 13.5.67zM11.71 19c-1.78 0-3.22-1.4-3.22-3.14 0-1.62 1.05-2.76 2.81-3.12 1.77-.36 3.6-1.21 4.62-2.58.39 1.29.59 2.65.59 4.04 0 2.65-2.15 4.8-4.8 4.8z"/></svg>
<span>Angular Blog</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</a>
<a class="card" target="_blank" rel="noopener" href="https://angular.io/devtools/">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><g><rect fill="none" height="24" width="24"/></g><g><g><path d="M14.73,13.31C15.52,12.24,16,10.93,16,9.5C16,5.91,13.09,3,9.5,3S3,5.91,3,9.5C3,13.09,5.91,16,9.5,16 c1.43,0,2.74-0.48,3.81-1.27L19.59,21L21,19.59L14.73,13.31z M9.5,14C7.01,14,5,11.99,5,9.5S7.01,5,9.5,5S14,7.01,14,9.5 S11.99,14,9.5,14z"/><polygon points="10.29,8.44 9.5,6 8.71,8.44 6.25,8.44 8.26,10.03 7.49,12.5 9.5,10.97 11.51,12.5 10.74,10.03 12.75,8.44"/></g></g></svg>
<span>Angular DevTools</span>
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z"/></svg>
</a>
</div>
<!-- Next Steps -->
<h2>Next Steps</h2>
<p>What do you want to do next with your app?</p>
<input type="hidden" #selection>
<div class="card-container">
<button class="card card-small" (click)="selection.value = 'component'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>New Component</span>
</button>
<button class="card card-small" (click)="selection.value = 'material'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Angular Material</span>
</button>
<button class="card card-small" (click)="selection.value = 'pwa'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Add PWA Support</span>
</button>
<button class="card card-small" (click)="selection.value = 'dependency'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Add Dependency</span>
</button>
<button class="card card-small" (click)="selection.value = 'test'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Run and Watch Tests</span>
</button>
<button class="card card-small" (click)="selection.value = 'build'" tabindex="0">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z"/></svg>
<span>Build for Production</span>
</button>
</div>
<!-- Terminal -->
<div class="terminal" [ngSwitch]="selection.value">
<pre *ngSwitchDefault>ng generate component xyz</pre>
<pre *ngSwitchCase="'material'">ng add @angular/material</pre>
<pre *ngSwitchCase="'pwa'">ng add @angular/pwa</pre>
<pre *ngSwitchCase="'dependency'">ng add _____</pre>
<pre *ngSwitchCase="'test'">ng test</pre>
<pre *ngSwitchCase="'build'">ng build</pre>
</div>
<!-- Links -->
<div class="card-container">
<a class="circle-link" title="Find a Local Meetup" href="https://www.meetup.com/find/?keywords=angular" target="_blank" rel="noopener">
<svg xmlns="http://www.w3.org/2000/svg" width="24.607" height="23.447" viewBox="0 0 24.607 23.447">
<title>Meetup Logo</title>
<path id="logo--mSwarm" d="M21.221,14.95A4.393,4.393,0,0,1,17.6,19.281a4.452,4.452,0,0,1-.8.069c-.09,0-.125.035-.154.117a2.939,2.939,0,0,1-2.506,2.091,2.868,2.868,0,0,1-2.248-.624.168.168,0,0,0-.245-.005,3.926,3.926,0,0,1-2.589.741,4.015,4.015,0,0,1-3.7-3.347,2.7,2.7,0,0,1-.043-.38c0-.106-.042-.146-.143-.166a3.524,3.524,0,0,1-1.516-.69A3.623,3.623,0,0,1,2.23,14.557a3.66,3.66,0,0,1,1.077-3.085.138.138,0,0,0,.026-.2,3.348,3.348,0,0,1-.451-1.821,3.46,3.46,0,0,1,2.749-3.28.44.44,0,0,0,.355-.281,5.072,5.072,0,0,1,3.863-3,5.028,5.028,0,0,1,3.555.666.31.31,0,0,0,.271.03A4.5,4.5,0,0,1,18.3,4.7a4.4,4.4,0,0,1,1.334,2.751,3.658,3.658,0,0,1,.022.706.131.131,0,0,0,.1.157,2.432,2.432,0,0,1,1.574,1.645,2.464,2.464,0,0,1-.7,2.616c-.065.064-.051.1-.014.166A4.321,4.321,0,0,1,21.221,14.95ZM13.4,14.607a2.09,2.09,0,0,0,1.409,1.982,4.7,4.7,0,0,0,1.275.221,1.807,1.807,0,0,0,.9-.151.542.542,0,0,0,.321-.545.558.558,0,0,0-.359-.534,1.2,1.2,0,0,0-.254-.078c-.262-.047-.526-.086-.787-.138a.674.674,0,0,1-.617-.75,3.394,3.394,0,0,1,.218-1.109c.217-.658.509-1.286.79-1.918a15.609,15.609,0,0,0,.745-1.86,1.95,1.95,0,0,0,.06-1.073,1.286,1.286,0,0,0-1.051-1.033,1.977,1.977,0,0,0-1.521.2.339.339,0,0,1-.446-.042c-.1-.092-.2-.189-.307-.284a1.214,1.214,0,0,0-1.643-.061,7.563,7.563,0,0,1-.614.512A.588.588,0,0,1,10.883,8c-.215-.115-.437-.215-.659-.316a2.153,2.153,0,0,0-.695-.248A2.091,2.091,0,0,0,7.541,8.562a9.915,9.915,0,0,0-.405.986c-.559,1.545-1.015,3.123-1.487,4.7a1.528,1.528,0,0,0,.634,1.777,1.755,1.755,0,0,0,1.5.211,1.35,1.35,0,0,0,.824-.858c.543-1.281,1.032-2.584,1.55-3.875.142-.355.28-.712.432-1.064a.548.548,0,0,1,.851-.24.622.622,0,0,1,.185.539,2.161,2.161,0,0,1-.181.621c-.337.852-.68,1.7-1.018,2.552a2.564,2.564,0,0,0-.173.528.624.624,0,0,0,.333.71,1.073,1.073,0,0,0,.814.034,1.22,1.22,0,0,0,.657-.655q.758-1.488,1.511-2.978.35-.687.709-1.37a1.073,1.073,0,0,1,.357-.434.43.43,0,0,1,.463-.016.373.373,0,0,1,.153.387.7.7,0,0,1-.057.236c-.065.157-.127.316-.2.469-.42.883-.846,1.763-1.262,2.648A2.463,2.463,0,0,0,13.4,14.607Zm5.888,6.508a1.09,1.09,0,0,0-2.179.006,1.09,1.09,0,0,0,2.179-.006ZM1.028,12.139a1.038,1.038,0,1,0,.01-2.075,1.038,1.038,0,0,0-.01,2.075ZM13.782.528a1.027,1.027,0,1,0-.011,2.055A1.027,1.027,0,0,0,13.782.528ZM22.21,6.95a.882.882,0,0,0-1.763.011A.882.882,0,0,0,22.21,6.95ZM4.153,4.439a.785.785,0,1,0,.787-.78A.766.766,0,0,0,4.153,4.439Zm8.221,18.22a.676.676,0,1,0-.677.666A.671.671,0,0,0,12.374,22.658ZM22.872,12.2a.674.674,0,0,0-.665.665.656.656,0,0,0,.655.643.634.634,0,0,0,.655-.644A.654.654,0,0,0,22.872,12.2ZM7.171-.123A.546.546,0,0,0,6.613.43a.553.553,0,1,0,1.106,0A.539.539,0,0,0,7.171-.123ZM24.119,9.234a.507.507,0,0,0-.493.488.494.494,0,0,0,.494.494.48.48,0,0,0,.487-.483A.491.491,0,0,0,24.119,9.234Zm-19.454,9.7a.5.5,0,0,0-.488-.488.491.491,0,0,0-.487.5.483.483,0,0,0,.491.479A.49.49,0,0,0,4.665,18.936Z" transform="translate(0 0.123)" fill="#f64060"/>
</svg>
</a>
<a class="circle-link" title="Join the Conversation on Discord" href="https://discord.gg/angular" target="_blank" rel="noopener">
<svg xmlns="http://www.w3.org/2000/svg" width="26" height="26" viewBox="0 0 245 240">
<title>Discord Logo</title>
<path d="M104.4 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1.1-6.1-4.5-11.1-10.2-11.1zM140.9 103.9c-5.7 0-10.2 5-10.2 11.1s4.6 11.1 10.2 11.1c5.7 0 10.2-5 10.2-11.1s-4.5-11.1-10.2-11.1z"/>
<path d="M189.5 20h-134C44.2 20 35 29.2 35 40.6v135.2c0 11.4 9.2 20.6 20.5 20.6h113.4l-5.3-18.5 12.8 11.9 12.1 11.2 21.5 19V40.6c0-11.4-9.2-20.6-20.5-20.6zm-38.6 130.6s-3.6-4.3-6.6-8.1c13.1-3.7 18.1-11.9 18.1-11.9-4.1 2.7-8 4.6-11.5 5.9-5 2.1-9.8 3.5-14.5 4.3-9.6 1.8-18.4 1.3-25.9-.1-5.7-1.1-10.6-2.7-14.7-4.3-2.3-.9-4.8-2-7.3-3.4-.3-.2-.6-.3-.9-.5-.2-.1-.3-.2-.4-.3-1.8-1-2.8-1.7-2.8-1.7s4.8 8 17.5 11.8c-3 3.8-6.7 8.3-6.7 8.3-22.1-.7-30.5-15.2-30.5-15.2 0-32.2 14.4-58.3 14.4-58.3 14.4-10.8 28.1-10.5 28.1-10.5l1 1.2c-18 5.2-26.3 13.1-26.3 13.1s2.2-1.2 5.9-2.9c10.7-4.7 19.2-6 22.7-6.3.6-.1 1.1-.2 1.7-.2 6.1-.8 13-1 20.2-.2 9.5 1.1 19.7 3.9 30.1 9.6 0 0-7.9-7.5-24.9-12.7l1.4-1.6s13.7-.3 28.1 10.5c0 0 14.4 26.1 14.4 58.3 0 0-8.5 14.5-30.6 15.2z"/>
</svg>
</a>
</div>
<!-- Footer -->
<footer>
Love Angular?&nbsp;
<a href="https://github.com/angular/angular" target="_blank" rel="noopener"> Give our repo a star.
<div class="github-star-badge">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M0 0h24v24H0z" fill="none"/><path d="M12 17.27L18.18 21l-1.64-7.03L22 9.24l-7.19-.61L12 2 9.19 8.63 2 9.24l5.46 4.73L5.82 21z"/></svg>
Star
</div>
</a>
<a href="https://github.com/angular/angular" target="_blank" rel="noopener">
<svg class="material-icons" xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" fill="#1976d2"/><path d="M0 0h24v24H0z" fill="none"/></svg>
</a>
</footer>
<svg id="clouds" xmlns="http://www.w3.org/2000/svg" width="2611.084" height="485.677" viewBox="0 0 2611.084 485.677">
<title>Gray Clouds Background</title>
<path id="Path_39" data-name="Path 39" d="M2379.709,863.793c10-93-77-171-168-149-52-114-225-105-264,15-75,3-140,59-152,133-30,2.83-66.725,9.829-93.5,26.25-26.771-16.421-63.5-23.42-93.5-26.25-12-74-77-130-152-133-39-120-212-129-264-15-54.084-13.075-106.753,9.173-138.488,48.9-31.734-39.726-84.4-61.974-138.487-48.9-52-114-225-105-264,15a162.027,162.027,0,0,0-103.147,43.044c-30.633-45.365-87.1-72.091-145.206-58.044-52-114-225-105-264,15-75,3-140,59-152,133-53,5-127,23-130,83-2,42,35,72,70,86,49,20,106,18,157,5a165.625,165.625,0,0,0,120,0c47,94,178,113,251,33,61.112,8.015,113.854-5.72,150.492-29.764a165.62,165.62,0,0,0,110.861-3.236c47,94,178,113,251,33,31.385,4.116,60.563,2.495,86.487-3.311,25.924,5.806,55.1,7.427,86.488,3.311,73,80,204,61,251-33a165.625,165.625,0,0,0,120,0c51,13,108,15,157-5a147.188,147.188,0,0,0,33.5-18.694,147.217,147.217,0,0,0,33.5,18.694c49,20,106,18,157,5a165.625,165.625,0,0,0,120,0c47,94,178,113,251,33C2446.709,1093.793,2554.709,922.793,2379.709,863.793Z" transform="translate(142.69 -634.312)" fill="#eee"/>
</svg>
</div> </div>
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * * The content above * * * * * * * * * * * -->
<!-- * * * * * * * * * * is only a placeholder * * * * * * * * * * -->
<!-- * * * * * * * * * * and can be replaced. * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<!-- * * * * * * * * * * End of Placeholder * * * * * * * * * * * -->
<!-- * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * -->
<router-outlet></router-outlet>

View file

@ -1,29 +0,0 @@
import { TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [RouterTestingModule],
declarations: [AppComponent]
}));
it('should create the app', () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it(`should have as title 'module-configurator'`, () => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.componentInstance;
expect(app.title).toEqual('module-configurator');
});
it('should render title', () => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('.content span')?.textContent).toContain('module-configurator app is running!');
});
});

View file

@ -5,6 +5,7 @@ import { Component } from '@angular/core';
templateUrl: './app.component.html', templateUrl: './app.component.html',
styleUrls: ['./app.component.css'] styleUrls: ['./app.component.css']
}) })
export class AppComponent { export class AppComponent {
title = 'module-configurator'; title = 'Modul Konfigurator';
} }

View file

@ -1,18 +1,32 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { CartModule } from './cart/cart.module';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { ModulesComponent } from './modules/modules.component';
import { ModulesItemComponent } from './modules-item/modules-item.component';
import { HomeComponent } from './home/home.component';
import { ModuleDetailComponent } from './module-detail/module-detail.component';
@NgModule({ @NgModule({
declarations: [ declarations: [
AppComponent AppComponent,
ModulesComponent,
HomeComponent,
ModuleDetailComponent,
ModulesItemComponent
], ],
imports: [ imports: [
BrowserModule, BrowserModule,
AppRoutingModule AppRoutingModule,
CartModule,
], ],
providers: [], providers: [],
bootstrap: [AppComponent] bootstrap: [AppComponent]
}) })
export class AppModule { } export class AppModule { }

View file

@ -0,0 +1,29 @@
.cluster {
margin-left: 10px;
color: #222;
font-size: 0.8rem;
}
.item-name {
margin: 0.5rem 0;
}
.item-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-left: 0.5rem;
}
.item-actions button {
height: 1.5rem;
width: 1.5rem;
margin-left: 0.15rem;
}
.item {
display: flex;
justify-content: space-between;
align-items: center;
}

View file

@ -0,0 +1,18 @@
<div *ngIf="item" class="item">
<div class="item-name">
<div>{{item.name}}</div>
<div class="cluster">{{item.cluster}}</div>
</div>
<div class="item-actions">
<button
[disabled]="isFirst(item)"
(click)="moveUp(item)">🡅</button>
<button
[disabled]="isLast(item)"
(click)="moveDown(item)">🡇</button>
<button
(click)="removeFromCart(item)"
>✖</button>
</div>
</div>

View file

@ -0,0 +1,37 @@
import { Component, Input } from '@angular/core';
import { Module } from '../interfaces/module';
import { CartService } from '../cart/cart.service';
@Component({
selector: 'app-cart-item',
templateUrl: './cart-item.component.html',
styleUrls: ['./cart-item.component.css']
})
export class CartItemComponent {
@Input() item?: Module;
constructor(private cart: CartService) { }
removeFromCart(module: Module): void {
this.cart.remove(module);
}
isFirst(module: Module): boolean {
return this.cart.isFirst(module);
}
isLast(module: Module): boolean {
return this.cart.isLast(module);
}
moveUp(module: Module): void {
this.cart.moveUp(module);
}
moveDown(module: Module): void {
this.cart.moveDown(module);
}
}

View file

@ -0,0 +1,38 @@
.cart {
position: fixed;
bottom: 0;
right: 0;
background-color: #ccc;
padding: 1rem;
}
.headline {
cursor: pointer;
text-transform: uppercase;
}
.cart-content.hidden {
max-height: 0;
}
.cart-content {
transition: max-height 0.3s ease-out;
max-height: 380px;
overflow: hidden;
}
.cart-items {
overflow: auto;
max-height: 300px;
padding: 0 0.5rem;
}
.cart-actions {
display: flex;
width: 100%;
justify-content: flex-end;
}
.cart-actions button {
margin-left: 0.2rem;
}

View file

@ -0,0 +1,13 @@
<div class="cart">
<h4 class="headline" (click)="toggle()">Konfiguration ({{items.length}}) <span>{{ isOpen ? '🡇' : '🡅' }}</span></h4>
<div class="cart-content" [class]="{hidden: !isOpen}">
<div class="cart-items">
<app-cart-item *ngFor="let item of items" [item]="item"></app-cart-item>
</div>
<div class="cart-actions">
<button
[disabled]="!hasAllClusters()"
(click)="makeOrder()">Konfiguration abschicken</button>
</div>
</div>
</div>

View file

@ -0,0 +1,44 @@
import { Component } from '@angular/core';
import { CartService } from './cart.service';
import { ApiService } from '../api.service';
import { Module } from '../interfaces/module';
@Component({
selector: 'app-cart',
templateUrl: './cart.component.html',
styleUrls: ['./cart.component.css']
})
export class CartComponent {
items = this.cart.items;
availableModules: Module[] = [];
isOpen = true;
constructor(
private cart: CartService,
private api: ApiService
) { }
ngOnInit(): void {
this.api.getModules().subscribe(allModules => {
this.availableModules = allModules;
});
}
toggle(): void {
this.isOpen = !this.isOpen;
}
hasAllClusters(): boolean {
return this.cart.hasAllClusters();
}
makeOrder(): void {
try {
const orderedModules = this.cart.order();
console.log(orderedModules);
} catch (error) {
console.log(error);
}
}
}

View file

@ -0,0 +1,20 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { CartComponent } from './cart.component';
import { CartItemComponent } from '../cart-item/cart-item.component';
@NgModule({
imports: [
CommonModule
],
declarations: [
CartComponent,
CartItemComponent,
],
exports: [
CartComponent,
],
})
export class CartModule { }

View file

@ -0,0 +1,190 @@
import { TestBed } from '@angular/core/testing';
import { CartService } from './cart.service';
import { Module, Cluster } from '../interfaces/module';
describe('CartService', () => {
let service: CartService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(CartService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
it('should add module to cart', () => {
const mod: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
service.add(mod);
expect(service.items).toContain(mod);
});
it('should not add the same module to cart twice', () => {
const mod: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
service.add(mod);
service.add(mod);
expect(service.items).toContain(mod);
expect(service.items.length).toBe(1);
});
it('should throw error when trying to order without all clusters', () => {
const mod1: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
const mod2: Module = {
id: 2,
name: 'Module 2',
cluster: 'FMEA',
description: 'Description 2'
};
service.add(mod1);
service.add(mod2);
expect(() => service.order()).toThrowError('Not all clusters are selected');
});
it('should return all ordered modules if all clusters are selected', () => {
const clusters: Cluster[] = ['FMEA', 'Control-Plan', 'Fertigungsprüfung', 'Reklamationsmanagement', 'Maßnahmenmanagement'];
clusters.forEach((cluster, idx) => {
let mod: Module = {
id: idx,
name: `Module ${idx}`,
cluster,
description: 'Description 1'
};
service.add(mod);
});
expect(service.order().length).toBe(5);
});
it('should remove module from cart', () => {
const mod: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
service.add(mod);
service.remove(mod);
expect(service.items).not.toContain(mod);
expect(service.items.length).toBe(0);
});
it('should move module up', () => {
const mod1: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
const mod2: Module = {
id: 2,
name: 'Module 2',
cluster: 'FMEA',
description: 'Description 2'
};
service.add(mod1);
service.add(mod2);
service.moveUp(mod2);
expect(service.items[0]).toBe(mod2);
expect(service.items[1]).toBe(mod1);
});
it('should move module down', () => {
const mod1: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
const mod2: Module = {
id: 2,
name: 'Module 2',
cluster: 'FMEA',
description: 'Description 2'
};
service.add(mod1);
service.add(mod2);
service.moveDown(mod1);
expect(service.items[0]).toBe(mod2);
expect(service.items[1]).toBe(mod1);
});
it('should return true if module is first', () => {
const mod1: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
const mod2: Module = {
id: 2,
name: 'Module 2',
cluster: 'FMEA',
description: 'Description 2'
};
service.add(mod1);
service.add(mod2);
expect(service.isFirst(mod1)).toBe(true);
expect(service.isFirst(mod2)).toBe(false);
});
it('should return true if module is last', () => {
const mod1: Module = {
id: 1,
name: 'Module 1',
cluster: 'FMEA',
description: 'Description 1'
};
const mod2: Module = {
id: 2,
name: 'Module 2',
cluster: 'FMEA',
description: 'Description 2'
};
service.add(mod1);
service.add(mod2);
expect(service.isLast(mod1)).toBe(false);
expect(service.isLast(mod2)).toBe(true);
});
});

View file

@ -0,0 +1,75 @@
import { Injectable } from '@angular/core';
import { Module } from '../interfaces/module';
import { ApiService } from '../api.service';
@Injectable({
providedIn: 'root'
})
export class CartService {
items: Module[] = [];
numberAllClusters: number = 0;
constructor(private api: ApiService) {
this.api.getModules().subscribe(allModules => {
this.numberAllClusters = [...new Set(allModules.map(module => module.cluster))].length;
})
}
add(module: Module): void {
if (!this.isInCart(module)) {
this.items.push(module);
}
}
remove(module: Module): void {
const index = this.items.indexOf(module);
if (index > -1) {
this.items.splice(index, 1);
}
}
isInCart(module: Module): boolean {
return this.items.includes(module);
}
hasAllClusters(): boolean {
let clusters = [...new Set(this.items.map(module => module.cluster))];
return clusters.length === this.numberAllClusters;
}
isFirst(module: Module): boolean {
return this.items.indexOf(module) === 0;
}
isLast(module: Module): boolean {
return this.items.indexOf(module) === this.items.length - 1;
}
moveUp(module: Module): void {
const index = this.items.indexOf(module);
if (index > 0) {
this.items.splice(index, 1);
this.items.splice(index - 1, 0, module);
}
}
moveDown(module: Module): void {
const index = this.items.indexOf(module);
if (index < this.items.length - 1) {
this.items.splice(index, 1);
this.items.splice(index + 1, 0, module);
}
}
order(): Module[] {
if (!this.hasAllClusters()) {
throw new Error('Not all clusters are selected');
}
return this.api.orderModules(this.items);
}
}

View file

@ -0,0 +1,4 @@
div {
padding: 0.5rem 2rem;
}

View file

@ -0,0 +1,8 @@
<div>
<p>
Hier können Sie eine individuelle Modulkonfiguration vornehmen.
</p>
<p>
Sie müssen mindestens ein Modul aus jedem Cluster ausgewählt haben, bevor eine Angebotsanfrage verschickt werden kann.
</p>
<div>

View file

@ -0,0 +1,9 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-home',
templateUrl: './home.component.html',
styleUrls: ['./home.component.css']
})
export class HomeComponent { }

View file

@ -0,0 +1,8 @@
export type Cluster = 'FMEA'|'Control-Plan'|'Fertigungsprüfung'|'Reklamationsmanagement'|'Maßnahmenmanagement'
export type Module = {
id: number
name: string
cluster: Cluster
description: string
}

124
src/app/mocks/modules.ts Normal file
View file

@ -0,0 +1,124 @@
import { Module } from '../interfaces/module';
export const MODULES: Module[] = [
{
id: 1,
name: 'Musterprüfung',
cluster: 'Control-Plan',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 2,
name: 'Erstbemusterung',
cluster: 'Control-Plan',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 3,
name: 'CAD Integration',
cluster: 'Fertigungsprüfung',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 4,
name: 'Vorbeugende Instandhaltung',
cluster: 'Fertigungsprüfung',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 5,
name: 'Wareneingangs- / Warenausgangsprüfung',
cluster: 'Fertigungsprüfung',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 6,
name: 'Prüfmittelmanagement',
cluster: 'Fertigungsprüfung',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 7,
name: 'Prüfmittelfähigkeitsuntersuchung',
cluster: 'Fertigungsprüfung',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 8,
name: 'Warranty Management',
cluster: 'Reklamationsmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 9,
name: 'CAPA Management',
cluster: 'Reklamationsmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 10,
name: 'Quality Cockpit',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 11,
name: 'Schulungsmanagement',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 12,
name: 'Qualifikationsmanagement',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 13,
name: 'Aufgabenmanagement',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 14,
name: 'Checklisten',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 15,
name: 'Lieferanten Cockpit',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 16,
name: 'Auditmanagement',
cluster: 'Maßnahmenmanagement',
description: 'Unternehmen nutzen Audits, um im eigenen Haus oder bei Lieferanten die Wirksamkeit eines Managementsystems zu überprüfen und sicherzustellen. Es wird untersucht, ob die unternehmensinternen Prozesse sowie die Anforderungen und Richtlinien den Standards entsprechen. So kann nicht nur der Ist-Zustand analysiert, sondern Verbesserungspotenzial effektiv aufgedeckt werden.',
},
{
id: 17,
name: 'Prozessmanagement',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 18,
name: 'Dokumentenlenkung',
cluster: 'Maßnahmenmanagement',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
{
id: 19,
name: 'APQP Projektmanagement',
cluster: 'FMEA',
description: 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla euismod, nisl quis tincidunt ultricies, nunc nisl ultricies nunc, quis ultricies ni',
},
]

View file

@ -0,0 +1,7 @@
.module-detail {
padding: 0.5rem 2rem;
}
button {
margin-top: 1rem;
}

View file

@ -0,0 +1,19 @@
<div *ngIf="module" class="module-detail">
<h2>{{module.name}}</h2>
<div>
{{ module.description }}
</div>
<button *ngIf="!isInCart()"
(click)="addToCart()">
Hinzufügen
</button>
<button *ngIf="isInCart()"
(click)="removeFromCart()">
Entfernen
</button>
</div>
<div *ngIf="!module" class="module-detail">
<h2>Kein Modul ausgewählt oder nicht gefunden</h2>
</div>

View file

@ -0,0 +1,69 @@
import { Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Location } from '@angular/common';
import { Module } from '../interfaces/module';
import { ApiService } from '../api.service';
import { CartService } from '../cart/cart.service';
@Component({
selector: 'app-module-detail',
templateUrl: './module-detail.component.html',
styleUrls: ['./module-detail.component.css']
})
export class ModuleDetailComponent implements OnInit {
module: Module | undefined;
constructor(
private route: ActivatedRoute,
private api: ApiService,
private location: Location,
private cart: CartService
) {}
ngOnInit(): void {
this.getModule();
}
getModule(): void {
this.route.params.subscribe((params) => {
if (params?.['id']) {
let id = Number(params['id']);
this.api.getModule(id).subscribe(module => {
if (!module) {
this.location.go('/');
return;
}
this.module = module;
});
}
});
}
isInCart(): boolean {
if (!this.module) {
return false;
}
return this.cart.isInCart(this.module);
}
addToCart(): void {
if (!this.module) {
return;
}
this.cart.add(this.module);
}
removeFromCart(): void {
if (!this.module) {
return;
}
this.cart.remove(this.module);
}
}

View file

@ -0,0 +1,22 @@
.module {
display: flex;
justify-content: space-between;
width: 100%;
margin: 0.1rem 0;
align-items: center;
height: 2rem;
}
button.add span {
display: inline-block;
transform: rotate(45deg);
}
button.remove {
background-color: rgba(255, 0, 0, 0.1);
}
button {
margin-left: 0.5rem;
border: 1px solid #ccc;
}

View file

@ -0,0 +1,19 @@
<div *ngIf="module"
class="module">
<a routerLink="/module/{{module.id}}">
{{module.name}}
</a>
<button *ngIf="!isInCart(module)"
class="add"
(click)="addToCart(module)"
>
<span></span>
</button>
<button *ngIf="isInCart(module)"
class="remove"
(click)="removeFromCart(module)"
>
<span></span>
</button>
</div>

View file

@ -0,0 +1,27 @@
import { Component, Input } from '@angular/core';
import { Module } from '../interfaces/module';
import { CartService } from '../cart/cart.service';
@Component({
selector: 'app-modules-item',
templateUrl: './modules-item.component.html',
styleUrls: ['./modules-item.component.css']
})
export class ModulesItemComponent {
@Input() module?: Module;
constructor(private cart: CartService) { }
addToCart(module: Module): void {
this.cart.add(module);
}
removeFromCart(module: Module): void {
this.cart.remove(module);
}
isInCart(module: Module): boolean {
return this.cart.isInCart(module);
}
}

View file

@ -0,0 +1,36 @@
.module.hidden {
display: none;
}
summary {
cursor: pointer;
}
summary, .summary {
padding: 0.5rem 0.5rem 0.5rem 0;
}
details > summary {
list-style: none;
}
details > summary::-webkit-details-marker {
display: none;
}
summary:after {
content: "►";
}
details[open] summary:after {
content: "▼";
}
summary * {
display: inline;
}
h4 {
margin: 0;
width: 100%;
}

View file

@ -0,0 +1,22 @@
<div *ngIf="style === 'all'">
<div class="summary">
<h4>Alle Module</h4>
</div>
<app-modules-item *ngFor="let module of modules"
[module]="module"
class="module">
</app-modules-item>
</div>
<div *ngIf="style === 'cluster'">
<details *ngFor="let cluster of clusters">
<summary><h4>{{cluster}}</h4></summary>
<app-modules-item
*ngFor="let module of modules"
class="module"
[module]="module"
[class]="{hidden: module.cluster !== cluster}"
>
</app-modules-item>
</details>
</div>

View file

@ -0,0 +1,36 @@
import { Component, Input } from '@angular/core';
import { Module } from '../interfaces/module';
import { ApiService } from '../api.service';
import { CartService } from '../cart/cart.service';
@Component({
selector: 'app-modules',
templateUrl: './modules.component.html',
styleUrls: ['./modules.component.css']
})
export class ModulesComponent {
@Input() style?: string = 'all';
clusters: string[] = [];
modules: Module[] = [];
constructor(
private api: ApiService,
private cart: CartService
) { }
addToCart(module: Module): void {
this.cart.add(module);
}
initializeModules(): void {
this.api.getModules().subscribe(allModules => {
this.modules = allModules.sort((a, b) => a.name.localeCompare(b.name));
this.clusters = [...new Set(allModules.map(module => module.cluster))].sort();
});
}
ngOnInit(): void {
this.initializeModules();
}
}

View file

@ -2,7 +2,7 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>ModuleConfigurator</title> <title>Modul Konfigurator</title>
<base href="/"> <base href="/">
<meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="icon" type="image/x-icon" href="favicon.ico"> <link rel="icon" type="image/x-icon" href="favicon.ico">

View file

@ -1 +1,4 @@
/* You can add global styles to this file, and also import other style files */ /* You can add global styles to this file, and also import other style files */
h1, h2, h3, h4, h5, h6 {
margin: 8px 0;
}