Sharepoint - Balkendiagramm

Diagramm aus Sharepointliste

Mit Hilfe von Sharepointplus & highcharts.js

jQuery(document).ready(function(){ jQuery('#system-message-container').hide(); });

1. Einführung

In diesem kurzen Guide führe ich euch durch den Source Code eines kleinen Einstieg Projektes zum Thema Diagramm Erstellung auf Basis von Sharepointisten. Die hier besprochene Lösung wurde mit dem Sharepointplus, jQuery und highcharts.js Frameworks realisiert.

Die hier gezeigte Lösung funktioniert (natürlich) nur in Kombination mit einem MS Sharepoint Server. Da mir kein solcher öffentlich zur Verfügung steht muss ich hier leider auf Screenshots zurückgreifen.

2. Die Liste (Sharepoint)

Unsere Sharepointliste beinhaltet alle relevanten Daten für das Diagramm. Die Spalten definieren die Themen, die Zeilen die Zeitachse (hier Monate). Die folgenden Spalten (Typen) wurden von mir verwendet.

2.1 Demoliste - der Aufbau

beinhaltet in unserem Besipiel den Namen des jeweiligen Monats.

Hier werden wir die Zahlenwerte für das jeweilige Thema im jeweiligen Monat hinterlegen.

Hier werden wir die Zahlenwerte für das jeweilige Thema im jeweiligen Monat hinterlegen.

Hier werden wir die Zahlenwerte für das jeweilige Thema im jeweiligen Monat hinterlegen.

Weitere Spalten für jedes Thema gem. den Beispielen bei Bedarf...

Rechts seht Ihr einen Screenshot meiner Sharepointliste.An dieser Stelle noch ein Wichtiger Hinweis:

Spaltennamen und Titel bleiben, auch nach einer Änderung im Hintergrund auf dem alten Wert. Wenn Ihr nun also die Spalte Projekt A erstellt und dann merkt das es eigentlich Projekt AB heissen sollte könnt Ihr diese Natürlich umbenennen, im Hintergrund bleibt jedoch der Spaltenname Projekt A aktiv und wir müssen mit diesem arbeiten.

An dieser stelle noch der folgende Hinweis. Ich habe neben der Standardansicht noch die View "dataview" erstellt. Diese beinhaltet alle Spalten ausser der Titel Spalte. Die Titel werden wir nachher per Array im JS nachreichen.

3. Get into the Code

3.1 getColumntitles

Nun da wir die Liste erstellt und befüllt haben starten wir auch schon durch. Wie schon erwähnt gehen wir nicht zu sehr ins Detail, den kompletten Quelltext könnt Ihr euch hier herunterladen: Link zum HTML Dokument. An dieser Stelle nochmals der Hinweis, das Dokument so ist nicht lauffähig - Die Scripts und Ressopurcen verweisen auf Lokale Quellen eines internen Sharepoint Pfads. Mit Hilfe des Dokumentierten Codes könnt Ihr aber Eure Lösung vielleicht besser umsetzen.

Im ersten Schritt holen wir uns zunächst mit der Funktion "getColumntitles" mit Hilfe von jQuery Deferred die Spaltentitel der Liste "Demoliste" aus der View "dataview".

            function getColumntitles(){
                $.when(
                  (function() {
                    var deferred = jQuery.Deferred();
                    $SP().list("Demoliste").view('dataview',function(data) {
                    var array = [];
                    for(var i=0; i<data.fields.length;i++)array.push(data.fields[i]);
                        //console.log(array);
                    
                        deferred.resolve(array);
                    })
                    return deferred;
                  }())
                ).done(function(spaltentitel) {
                    var columns = [];
                    // Wir schreiben die Spaltentitel Displaykonform in ein neues Array und entfernen Sharepoint Sonderzeichen in der Bezeichnung
                    // Das originale Array bentigen wir jedoch noch fr die sptere Abfrage
                    for(var i = 0; i<spaltentitel.length; i++){
                        var temptitle = "";
                        temptitle = spaltentitel[i].replace(/_x0020_/g," ").replace(/_/g," ");
                        columns.push(temptitle);
                    }
                    getElements(spaltentitel, columns);
                }).fail(function(err) {
                  alert("[ERROR] "+err)
                })
            }

In der Funktion "spaltentitel" innerhalb der .done Kapselung schreiben wir die erhaltenen Spaltentitel noch in eine "FrontEndtaugliche" Version um. Vom Sharepoint erhalten wir nämlich einfach die Spaltenbezeichner als String, hier sind Leerschläge als _x0020_ hinterlegt. Bei meinen Spalten habe ich der einfachheit halber keine Leerschläge für die Spaltentitel sondern Underscores verwendet, auch diese ersetzen wir nun mit Leerschlägen. Anschliessend pushen wir die Spaltentitel (Variable temptitle) in das array columns. Das Array columns sieht dann wie folgt aus: columns = ["Erstes Thema", "Thema Nummer 2", "Thema Nummer drei", "Viertes Thema"] Anschliessend rufen wir die Funktion "getElements" auf und liefern die spaltentitel (unverändertes Array) sowie unser columns Array (Frontend Spaltentitel) mit.

3.2 getElements

3.2.1 Ich dreh noch ne Runde...

Auch in der getElements Funktion arbeiten wir wieder mit dem jQuery Deferred Objekt.Als erstes wird das Array "elementarraytable" erstellt. Darin speichern wir anschliessend den Datensatz dieser Sharepoint Tabelle / Liste. Mittels einer for Schleife gehen wir die Anzahl der spaltentitel durch und erstellen ein neues Objekt im Array. Dieses Objekt beinhaltet den Spaltennamen (Frontend) sowie ein weiteres Array "data" wozu wir gleich kommen werden.

Jetzt wirds etwas komplizierter...

                            for (var i=0; i<data.length; i++){
                                
                                for(var n=0; n<spaltentitel.length; n++){
                                    elementarraytable[n]['data'].push(parseInt(data[i].getAttribute(spaltentitel[n])));    
                                }
                            }
Die äussere for Schleife wandert die ZEILEN der Liste / Tabelle ab.Die Innere for Schleife jeweils die SPALTEN der Liste / Tabelle.Jeder Wert wird dann jeweils ins Array "elementarraytable" in das entpsrechende Objekt ins ['data'] Array gepusht.Das Ergebnis sieht dann wie folgt aus:elementarraytable[1] = {name:"Thema Nummer 2", data:[56, 45, 78, 98, 32, 54, ...]}

3.2.2 Die komplette Funktion

            function getElements(spaltentitel, columns){
                $.when(
                    (function() {
                        var deferred = jQuery.Deferred();
                        var elementarraytable = [];
                        $SP().list("Demoliste").get(function(data,error) {
                            if (error) { alert(error) }
                            for(var n=0; n<spaltentitel.length; n++){
                                elementarraytable.push({name:columns[n],data:[]});                    // fr jede Themenspalte ein Object im Array erstellen und den Titel leserlich einsetzen
                            }
                            for (var i=0; i<data.length; i++){
                                
                                for(var n=0; n<spaltentitel.length; n++){
                                    elementarraytable[n]['data'].push(parseInt(data[i].getAttribute(spaltentitel[n])));    
                                }
                            }
                        if (error) {
                            deferred.reject(error);
                        } else {
                            deferred.resolve(elementarraytable);
                        }
                    })
                    return deferred;
                  }())
                ).done(function(tablearray) {
                    
                    buildChart(tablearray, columns);
                    
                }).fail(function(err) {
                  alert("[ERROR] "+err)
                })
            }

4. Let's build something nice

4.1 Die buildChart Funktion

Unsere Build-Funktion ist eine einfache Angelegenheit - basierend auf den Informationen / Anleitungen von highcharts kreiieren wir nun das Diagramm. Hierzu rufen wir die Funktion highcharts auf und übergeben unsere Parameter. Der Chart Type column erstellt uns ein Balken Diagramm. Auf der x-Axis setzen wir ein Array mit Monatsnamen ein. Abschliessend Setzen wir bei den Series unser tablearray Der Rest ist Beigemüse, Details findet Ihr auf der Offiziellen Webseite von highcharts.  

            function buildChart(array, columns){
                $(function () {
                    // Das Diagramm wird erstellt
                    $('#demodiagramm').highcharts({
                       chart: {
                            type: 'column'
                        },
                        title: {
                            text: 'Stunden pro Thema'
                        },
                        xAxis: {
                            categories: ['Januar', 'Februar', 'Mrz', 'April', 'Mai', 'Juni', 'Juli', 'August', 'September', 'Oktober','November','Dezember']
                        },
                        yAxis: {
                            allowDecimals: false,
                                title: {
                                text: 'Stunden'
                                }
                        },
                        plotOptions: {
                            column: {
                               //stacking: 'normal',
                            }
                        },
                        tooltip: {
                            useHTML: true,
                            pointFormat: '{series.name}: <b>{point.y}</b>',
                            valueSuffix: ' Stunden',
                            shared: false
                        },
                        series: array
                    });
                });

5. Das Diagramm

Und das war es eigentlich auch schon. Dank der Verwendung von Sharepoint Plus konnten wir die Daten relativ intuitiv via jQuery Deferred Objekt aus der Liste laden und für unser Diagramm vorbereiten. Wie Ihr vielleicht im Quellcode entnehmen könnt habe ich zusätzlich das JS / CSS Framework uikit verwendet um mir die arbeit etwas zu erleichtern.

Denkt daran, wenn Ihr Listen oder Spalten umbenennt werden diese auf Programmier-Ebene ihre originalen Namen behalten!

Nutzt in Kombination mit Sharepointplus unbedingt die Deferred Lösung. Der Sharepointserver und der Script arbeiten Asynchron - wenn Deferred nicht verwendet wird kann dies zu ganz komischen Ergebbnissen führen.

PHP - NearHere Script

PHP NEAR HERE.../

script

jQuery(document).ready(function(){ jQuery('#system-message-container').hide(); });

1. Einleitung

Der PHP Near Here Script zeigt auf Basis von zwei Datenbanktabellen den nächsten POI (Point of Interest) nach Eingabe einer Postleitzahl. Hierzu wird eine Datenbanktabelle mit Postleitzahlen & Koordinaten sowie eine weitere mit den POI und dessen Koordinaten verwendet.

Live Demo

2. Datenbanktabellen

In unserer Datenbank (im Beispiel geodata_ch) erstellen wir zwei Tabellen (tbl_locations für die POI's sowie tbl_switzerland als Bibliothek für die Postleitzahlen)

2.1 Tabelle "tbl_locations"

Liste der POI's, beinhaltet Name, Anschrift, Kontaktinformationen sowie Koordinaten

ID als Primary Key

Name des POI

Strasse und Hausnummer des POI

Beinhaltet die Postleitzahl des POI

Beinhaltet den Namen der Ortschaft des POI

Webseiten URL des POI

E-Mail Adresse des POI

Telefonnummer des POI

Mehrere Spalten (Spalte pro Lizenz) / Dinge, welcher dieser POI anbietet

Latitude

Longitude

Weitere Spalten bei Bedarf

2.2 Tabelle "tbl_switzerland"

Liste von Schweizer Postleitzahlen sowie den Koordinaten im Zentrum eines Postleitzahlenbereichs. Tabellenspalten sind wie folgt:

Beinhaltet die Postleitzahl

Beinhaltet den Namen der Ortschaft

Beinhaltet den Kantonsbezeichner in Kurzschreibweise (SG, GR, ...)

Latitude

Longitude

3. Frontpage Vorbereitungen

Zum Start erstellen wir ein neues Dokument "index.php" und erstellen einen Standardmässigen Aufbau nach HTML5 (Link zum Boilerplate). Bei diesem Script habe ich für das Frontend auf das uikit Framework zurückgegriffen, natürlich kann auch Bootstrap oder ein anderes Framework verwendet werden.

3.1 HTML

Dementsprechend finden wir im HEAD Bereich diverse Script Verknüpfungen sowie im BODY eine DIV Struktur, welche für uikit aufgebaut wurde. Das soll uns hier jedoch nicht weiter stören, für die Funktion ist aktuell einzig ein Input Feld mit der ID "userplz" sowie einen Button mit der ID "searchplz" wichtig.

						<form>
						    <fieldset class="uk-fieldset">
						    	<div class="uk-child-width-1-2" uk-grid>
							        <div class="uk-margin">
							            <input id="userplz" class="uk-input" type="number" placeholder="Postleitzahl">
							        </div>
							        <div>
							        	<button id="searchplz" class="uk-width-1-1 uk-button uk-button-default">Suchen</button>
								    </div>
								</div>
						    </fieldset>
						</form>

3.2 Javascript

Auf diese beiden springt nämlich unser jQuery Code im HEAD Bereich an, holt sich den eingetragenen Wert aus dem Postleitzahlenfeld und ruft die Funktion getCoordinates auf:

			$('#searchplz').click(function(){
				$('#partnerlist').fadeOut('slow', function(){
					$(this).empty();
					var plz = $('#userplz').val();
					console.log(plz);
					getCoordinates(plz);
				});
				
				return false;
			});

3.3 Javascript Funktion getCoordinates()

Die Funktion getCoordinates startet einen AJAX Request an die Datenbank und erhält so die Koordinaten und weitere Informationen für diese Postleitzahl im JSON Format. Haben wir in der Response keinerlei Angaben zu "lat" oder "lon" tritt der Fehlerfall ein was zu einem Eintrag in der JS Konsole führt.

			function getCoordinates(plz){
				console.log('Abfrage für '+plz+' gestartet');

				$.ajax({
				    type: "POST",
				    url: 'helper/getCoordinates.php',
				    data: {zipcode: plz},
				    success: function(data){
				    	if(data['lat'] && data['lon']){
				    		getPartners(data['lat'],data['lon']);
				    	}else if (data['error']){
				    		console.log(data['error']);
				    	}else{
				    		console.log('An error occured');
				    	}
				    }
				});


			};

getCoordinates.php auf GitHub ansehen

Sofern alles geklappt hat und der Return Wert eine JSON mit lat & lon Schlüsseln enthält werden diese an die Javsscript Funktion getPartners übergeben.

3.4 Javascript Funktion getPartners()

Auch die Funktion getPartners() startet einen AJAX Request - mit Hilfe der PHP Datei "getPartners.php" im helper Verzeichnis wird die komplette Partnerliste aus der DB gelesen, dies ist leider nötig, denn wir brauchen alle POI um im nächsten Schritt die POI mit der kürzesten Distanz zu unserer Postleitzahl identifizieren zu können.

			function getPartners(lat,lon){
				console.log('Searching next Partner for '+lat+' / '+lon);
				

				$.ajax({
				    type: "POST",
				    url: 'helper/getPartners.php',
				    data: {usr_lat: lat, usr_lon: lon},
				    success: function(data){
				    	buildView(data);
				    }
				});

			}

Bevor wir nun damit beginnen können die View aufzubauen werfen wir noch kurz einen Blick in die Datei getPartners.php im helper Verzeichnis...

4. getPartners PHP Abfrage

Die Datei getPartners.php beinhaltet eine einfache SELECT Abfrage auf der Datenbank, anschliessend jedoch werden die Latitude und Longitude Angaben während des Array fetch an die PHP Funktion distance übergeben - hier wird dann anhand der lat,lon Angaben des POI &amp; der Postleitzahl die Distanz der zwei Koordinatenpunkte (direkte Linie!) berechnet. Für die Berechnung wird der PHP Script von geodatasource.com verwendet.

Als Resultat erhalten wir anschliessend ein Array welches via php multisort nach Distanz (Klein nach Gross) sortiert wird. Wir übergeben anschliessend ein JSON Objekt welches alle Partner, die Informationen und deren Distanz zur gesuchten Postleitzahl enthält.Damit werden wir nun die Javascript Funktion füttern und die Inhalte auf der Seite einblenden.

Die buildView Javascript Funktion bereitet die von PHP erhaltenen Informationen in eine Ansicht und hängt diese mittels Append an das Container Element partnerlist. Auch hier werden uikit relevante Klassen & Strukturen verwendet, die Art der Aufbereitung kann natürlich frei gewählt werden. In unserem Beispiel werden die Inhalte als Dropdown Liste ausgegeben - einem Accordion.

			function buildView(partners){
				console.log(partners);
				for (var i = 0; i < 3; i++) {
					console.log(partners[i]);

					var distance 	= ''+partners[i]['distance'];
    				var shortdist 	= distance.substring(0, 4);

					var partner = '<li><a class="uk-accordion-title uk-text-large" href="#">'+partners[i]['details']['name']+'<br/><span class="uk-text-small">Im Umkreis von ca. '+shortdist+'km</span></a>'
								+ '<div class="uk-accordion-content">'
								+ '<div class="uk-margin-small-left">'
								+ '<h4 class="uk-h6 uk-margin-remove-bottom">Anschrift:</h4>'
			        				+'<ul class="uk-list uk-margin-small-top">'
		        						+'<li><b>'+partners[i]['details']["name"]+'</b></li>'
		        						+'<li>'+partners[i]['details']["street"]+'</li>'
		        						+'<li>'+partners[i]['details']["zip"]+' '+partners[i]['details']["town"]+'</li>'
		        					+'</ul>'
		        					+'<table class="uk-table">';
				        		if(partners[i]['details']["phone"]){
				        			partner += '<tr class="uk-table-small"><td><span class="uk-icon" uk-icon="receiver"</td><td>'+partners[i]['details']["phone"]+'</td></tr>';
				        		}
				        		if(partners[i]['details']["mail"]){
				        			partner += '<tr class="uk-table-small"><td><span class="uk-icon" uk-icon="mail"</td><td>'+partners[i]['details']["mail"]+'</td></tr>';
				        		}
				        		if(partners[i]['details']["web"]){
				        			partner += '<tr class="uk-table-small"><td><span class="uk-icon" uk-icon="world"</td><td>'+partners[i]['details']["web"]+'</td></tr>';
				        		}
		        						
		        				partner +='</table>'
								+ '</div></div></li>';
					$('#partnerlist').append(partner);
					
				}
				$('#partnerlist').fadeIn('slow');
			}

5. Weitere Möglichkeiten

Da wir dank der Postleitzahlen Tabelle sowie der Partner / POI Tabelle die Geocoordinaten der Standorte haben kann in einem nächsten Schritt der Code noch durch eine interaktive (Google) Maps Ansicht mit dynamischer POI Anzeige erweitert werden, doch das ist ein Thema für ein anderes mal....

Über mich

Wer ist nx-designs?

nx-designs ist keine Firma im eigentlichen Sinne. Ich (Marco) erledige als Einzelperson vieles selbst, sollte es mir mal nicht möglich sein etwas nach Ihrem Wunsch zu erstellen kann ich glücklicherweise auf den Fähigkeitenpool von Bekannten und Kollegen zurückgreifen. Sei dies im Bereich Fotografie, Musik oder Equipment.Wir finden die für Sie passende eine Lösung.

Freiberuflicher & Leidenschaftlicher Web-Developer & Webmaster. Angefangen als Elektromonteur arbeite ich heute Vollzeit bei einem grossen schweizer Full Service Provider.In meiner Freizeit entwerfe ich Webseiten und programmiere Erweiterungen auf PHP-Basis für das Joomla! CMS. Linkedin Facebook

Webseite

LinkedIn

Impressum

Marco RenschGlashüttenweg 308887 This email address is being protected from spambots. You need JavaScript enabled to view it.

Cookie Hinweis

Cookies sind Textdateien, die bei dem Besuch auf einer Internetseite auf dem Computer des Benutzers gespeichert werden. diese Website verwendet Cookies, um das Angebot nutzerfreundlich, effektiver und sicherer zu machen. Dank dieser Dateien ist es beispielsweise möglich, dass Sie speziell auf Ihre Interessen abgestimmte Informationen auf der Seite angezeigt bekommen. Der ausschließliche Zweck besteht also darin, unser Angebot Ihren Kundenwünschen bestmöglich anzupassen und Ihnen das Surfen bei uns so komfortabel wie möglich zu gestalten und die Navigation zu erleichtern. Diese Website folgt den gültigen Richtlinien zum Nutzerhinweis auf die Verwendung von Cookies.

Sie können die Speicherung der Cookies durch eine entsprechende Einstellung Ihrer Browser-Software verhindern; wir weisen Sie jedoch darauf hin, dass Sie in diesem Fall gegebenenfalls nicht sämtliche Funktionen dieser Website vollumfänglich werden nutzen können. Sie können darüber hinaus die Erfassung der durch das Cookie erzeugten und auf Ihre Nutzung der Website bezogenen Daten (inkl. Ihrer IP-Adresse) an Google sowie die Verarbeitung dieser Daten durch Google verhindern, indem sie das unter dem folgenden Link verfügbare Browser-Plugin herunterladen und installieren: https://tools.google.com/dlpage/gaoptout

Google Analytics

Diese Website benutzt Google Analytics, einen Webanalysedienst der Google Inc. („Google“). Google Analytics verwendet sog. „Cookies“, Textdateien, die auf Ihrem Computer gespeichert werden und die eine Analyse der Benutzung der Website durch Sie ermöglichen. Die durch den Cookie erzeugten Informationen über Ihre Benutzung dieser Website werden in der Regel an einen Server von Google in den USA übertragen und dort gespeichert. Im Falle der Aktivierung der IP-Anonymisierung auf dieser Webseite, wird Ihre IP-Adresse von Google jedoch innerhalb von Mitgliedstaaten der Europäischen Union oder in anderen Vertragsstaaten des Abkommens über den Europäischen Wirtschaftsraum zuvor gekürzt. Nur in Ausnahmefällen wird die volle IP-Adresse an einen Server von Google in den USA übertragen und dort gekürzt. Im Auftrag des Betreibers dieser Website wird Google diese Informationen benutzen, um Ihre Nutzung der Website auszuwerten, um Reports über die Websiteaktivitäten zusammenzustellen und um weitere mit der Websitenutzung und der Internetnutzung verbundene Dienstleistungen gegenüber dem Websitebetreiber zu erbringen. Die im Rahmen von Google Analytics von Ihrem Browser übermittelte IP-Adresse wird nicht mit anderen Daten von Google zusammengeführt. Sie können die Speicherung der Cookies durch eine entsprechende Einstellung Ihrer Browser-Software verhindern; wir weisen Sie jedoch darauf hin, dass Sie in diesem Fall gegebenenfalls nicht sämtliche Funktionen dieser Website vollumfänglich werden nutzen können. Sie können darüber hinaus die Erfassung der durch das Cookie erzeugten und auf Ihre Nutzung der Website bezogenen Daten (inkl. Ihrer IP-Adresse) an Google sowie die Verarbeitung dieser Daten durch Google verhindern, indem Sie das unter dem folgenden Link verfügbare Browser-Plugin herunterladen und installieren: http://tools.google.com/dlpage/gaoptout

Facebook (Like Buttons)

Auf der Seite sind Like-Buttons des sozialen Netzwerks Facebook (Facebook Inc., 1601 Willow Road, Menlo Park, California, 94025, USA) integriert. Wenn Sie unsere Seiten besuchen, wird über das Plugin eine Verbindung zwischen Ihrem Browser und Facebook hergestellt. Facebook erhält dadurch die Information, dass Sie unsere Seite besucht haben. Wenn Sie den "Like-Button" klicken während Sie in Ihrem Facebook-Account eingeloggt sind, können die Inhalte unserer Seiten mit Ihrem Facebook-Profil in Verbindung gebracht werden. Dadurch kann Facebook den Besuch unserer Seiten Ihrem Benutzerkonto zuordnen. Der Betreiber dieses Online-Angebotes hat keine Kenntnis über den Inhalten übermittelter Daten, sowie deren Nutzung durch Facebook. Wenn Sie nicht wünschen, dass Facebook den Besuch unserer Seiten Ihrem Facebook-Account zuordnen kann, loggen Sie sich bitte aus Ihrem Facebook-Benutzerkonto aus.

nx-videoStage

nx-videoStage

Meet the Worlds first HTML5 Joomla! Player with Ambilight Effect

Easy to set up

The Backend options allows you to set up your video as YOU like. You can define all available HTML5 Video configurations. Aswell as a lot of different settings for the videoframe itself like stage color (rgba), border color (rgba) radius. Full List see below.

Stunning Effects

The Ambilight Video Effect use state of the art Canvas technologies and Javascript to create an Ambilight Effect for your Video. Beside that, there are two additional modes included "no Effect" and "Drop Shadow Player".

Configuration Options

Video URL, Video Poster and Video Ratio can be edited seperately

Enable Autoplay, if you like

A player without interactions? Disable the Controlbar

Configure the Videoframe aka Border Settings for thickness, color (rgba) and radius

WE have also included the Loop Function to restart the playback automatically

Define the stage color (background) in rgba

Define the Volume on Player initialization

Display your text on the left, on the right, above or below your Video

Kontakt

Marco Rensch
Glashüttenweg 30
8887 Mels
This email address is being protected from spambots. You need JavaScript enabled to view it.

Technik

Webseite basierend auf Joomla!
Template erstellt mit uikit
Symbolbilder von unsplash.com