diff --git a/docker-compose.yaml b/docker-compose.yaml index 9b367a2..6921f43 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -9,6 +9,9 @@ services: volumes: - ${PWD}/config/nginx/nginx_dev.conf:/etc/nginx/nginx.conf - ${PWD}/proxy-cache:/var/cache/nginx + depends_on: + - webapp + - api # The webapp dev server on port 3000 webapp: diff --git a/package.json b/package.json index 1b5c8de..e570c81 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,7 @@ "react-dom": "^16.8.5", "react-infinite-scroller": "^1.2.4", "react-router-dom": "^5.0.0", + "react-transition-group": "^4.0.0", "resolve": "^1.10.0", "style-loader": "^0.23.1", "sw-precache-webpack-plugin": "^0.11.5", diff --git a/src/CriteriumField.css b/src/CriteriumField.css new file mode 100644 index 0000000..1a72f2f --- /dev/null +++ b/src/CriteriumField.css @@ -0,0 +1,24 @@ +.CriteriumField.bad, .CriteriumField.bad .CriteriumField-title > a { + color: #ae4b53; + font-size: 1rem; +} +.CriteriumField.mediocre, .CriteriumField.mediocre .CriteriumField-title > a { + color: #c49863; + font-size: 1rem; +} +.CriteriumField.good { + color: #46962b; + font-size: 1rem; +} + +.CriteriumField-details { + color: rgb(33, 37, 41); + font-size: 14px; +} +.CriteriumField-details p { + margin-bottom: 0.4rem +} + +.CriteriumField-title > a { + text-decoration: underline; +} diff --git a/src/CriteriumField.js b/src/CriteriumField.js new file mode 100644 index 0000000..a5ea544 --- /dev/null +++ b/src/CriteriumField.js @@ -0,0 +1,99 @@ +import PropTypes from 'prop-types'; +import React, { Component } from 'react'; +import { Transition } from 'react-transition-group'; + +import './CriteriumField.css'; + +const transitionDuration = 300; + +const defaultTransitionStyle = { + transition: `max-height ${transitionDuration}ms ease-in-out`, + maxHeight: 0, + overflowY: 'hidden', +}; + +const transitionStyles = { + entering: { maxHeight: 500 }, + entered: { maxHeight: 500, overflowY: 'auto' }, + exiting: { maxHeight: 0 }, + exited: { maxHeight: 0 }, +}; + + +class CriteriumField extends Component { + state = {expanded: false}; + + showHide = (evt) => { + evt.preventDefault(); + this.setState({expanded: !this.state.expanded}); + }; + + render() { + if (this.props.type === 'positive') { + return ( +
+ + {this.props.title} +
+ ); + } else if (this.props.type === 'mediocre') { + return ( +
+ { this.props.icon ? this.props.icon : } + {this.props.title} +
+ ); + } else { + if (this.props.children === null || typeof this.props.children === 'undefined') { + return ( +
+
{this.props.title}
+
+ ); + } + + return ( +
+
{this.props.title}
+ + {state => ( +
+ {this.props.children} +
+ )} +
+
+ ); + } + } +} + +CriteriumField.propTypes = { + type: PropTypes.oneOf(['positive', 'mediocre', 'negative']), + title: PropTypes.string.isRequired, + keyProp: PropTypes.string.isRequired, + icon: PropTypes.instanceOf(Component), +}; + +class IconGood extends Component { + render() { + return ; + } +} + +class IconBad extends Component { + render() { + return ; + } +} + +class IconOptimize extends Component { + render() { + return ; + } +} + +export default CriteriumField; diff --git a/src/SiteDetailsPage.css b/src/SiteDetailsPage.css index 094f442..4777e1c 100644 --- a/src/SiteDetailsPage.css +++ b/src/SiteDetailsPage.css @@ -1,15 +1,3 @@ -.SiteDetailsPage .bad { - color: #ae4b53; - font-size: 1rem; -} -.SiteDetailsPage .mediocre { - color: #c49863; - font-size: 1rem; -} -.SiteDetailsPage .good { - color: #46962b; - font-size: 1rem; -} .SiteDetailsPage h1 { margin-top: 2rem; font-family: 'Arvo', sans-serif; diff --git a/src/SiteDetailsPage.js b/src/SiteDetailsPage.js index fde4fdf..6d69a77 100644 --- a/src/SiteDetailsPage.js +++ b/src/SiteDetailsPage.js @@ -1,5 +1,6 @@ import React, { Component } from 'react'; import { TypeField, StateField } from './LocationLabel'; +import CriteriumField from './CriteriumField'; import FavouriteAddRemove from './FavouriteAddRemove'; import LocationLabel from './LocationLabel'; import ScoreField from './ScoreField'; @@ -9,6 +10,25 @@ import axios from 'axios'; import _ from 'underscore'; +/** + * A cheap hash function for hashing strings + * + * @param String The string to be hashed + */ +function hashCode(str) { + var hash = 0; + if (str.length === 0) { + return hash; + } + for (let i=0; i < str.length; i++) { + var char = str.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; // Convert to 32bit integer + } + return hash.toString(); +} + + class SiteDetailsPage extends Component { _isMounted = false; @@ -71,7 +91,7 @@ class SiteDetailsPage extends Component { }, { criterium: 'CANONICAL_URL', - component: , + component: , data: this.state.site.rating.CANONICAL_URL, }, { @@ -116,17 +136,17 @@ class SiteDetailsPage extends Component { }, { criterium: 'NO_THIRD_PARTY_COOKIES', - component: , + component: , data: this.state.site.rating.NO_THIRD_PARTY_COOKIES, }, { criterium: 'NO_SCRIPT_ERRORS', - component: , + component: , data: this.state.site.rating.NO_SCRIPT_ERRORS, }, { criterium: 'NO_NETWORK_ERRORS', - component: , + component: , data: this.state.site.rating.NO_NETWORK_ERRORS, }, { @@ -257,42 +277,23 @@ class SiteDetailsPage extends Component { } } -class IconGood extends Component { - render() { - return ; - } -} - -class IconBad extends Component { - render() { - return ; - } -} - -class IconOptimize extends Component { - render() { - return ; - } -} - -class CriteriumField extends Component { - render() { - if (this.props.type === 'positive') { - return
{this.props.title}
; - } else if (this.props.type === 'mediocre') { - return
{ this.props.icon ? this.props.icon : }{this.props.title}
; - } else { - return
{this.props.title}
; - } - } -} - class CanonicalURLField extends Component { render() { if (this.props.data.value) { return ; } - return ; + + return +

Die Site ist unter den folgenden URLs erreichbar:

+
    + { + this.props.details.map((url) => { + return
  • {url}
  • ; + }) + } +
+

Es sollte eine URL ausgewählt werden, auf die von allen anderen Varianten weiter geleitet wird.

+
; } } @@ -347,7 +348,12 @@ class DNSResolvableField extends Component { if (this.props.data.value) { return } - return + + return +

Das bedeutet in der Regel, dass eine genutzte Domain beim Registrar nicht verlängert wurde. Falls der Hostname + der Site nicht identisch mit der Domain ist, und stattdessen beispielsweise mit `www.` beginnt, könnte es sich + auch um eine fehlende Konfiguration beim DNS-Provider handeln.

+
; } } @@ -356,7 +362,12 @@ class FaviconField extends Component { if (this.props.data.value) { return ; } - return ; + + return +

Ein Icon hilft Nutzer*innen, ein Browser-Tab oder ein Bookmark der Site besser wieder zu erkennen.

+

Anleitung: How + to Add a Favicon to your Site

+
; } } @@ -365,16 +376,93 @@ class FeedField extends Component { if (this.props.data.value) { return ; } - return ; + + return +

Feeds helfen Suchmaschinen dabei, aktuelle Meldungen zeitnah nach Veröffentlichung in ihren Suchindex + aufzunehmen, was die Site besser in Suchergebnissen platziert. Außerdem helfen sie auch versierten + Nutzer*innen dabei, über neue Inhalte auf dem laufenden zu bleiben.

+

Die meistgenutzten CMSe unterstützen die Veröffentlichung von Feeds ohne zusätzlichen Aufwand.

+

Anleitung: RSS Feeds aktivieren in GCMS

+
; } } class CookiesField extends Component { + state = {thirdPartyCookies: null}; + + componentDidMount() { + let url = Object.keys(this.props.details)[0]; + let cookies = this.props.details[url].cookies; + if (typeof cookies !== 'undefined') { + let parsedURL = new URL(url); + let thirdPartyCookies = cookies.filter(cookie => { + return parsedURL.hostname.indexOf(cookie.host_key); + }); + if (thirdPartyCookies.length > 0) { + this.setState({thirdPartyCookies: thirdPartyCookies}); + } + } + } + + expiryString(duration) { + if (duration < 60 * 3) { + return Math.floor(duration).toString() + " Sekunden"; + } + duration = duration / 60.0; + if (duration < 100) { + return Math.floor(duration).toString() + " Minuten"; + } + duration = duration / 60.0; + if (duration < 48) { + return Math.floor(duration).toString() + " Stunden"; + } + duration = duration / 24.0; + if (duration < 100) { + return Math.floor(duration).toString() + " Tage"; + } + duration = duration / 30.0; + if (duration < 15) { + return Math.floor(duration).toString() + " Monate"; + } + duration = duration * 30.0 / 365; + return Math.floor(duration).toString() + " Jahre"; + } + render() { if (this.props.data.value) { return ; } - return ; + + return ( + +

Cookies von Dritten, auch Third Party Cookies genannt, erlauben das Verfolgen von Nutzer*innen über + die Grenzen der Seite, auf der die Cookies gesetzt wurden, hinweg. Damit stellen sie einen Eingriff in die + Informationelle Selbstbestimmung dar, insbesondere dann, wenn sie ohne Einwilligung gesetzt werden.

+

Da Green Spider keine Einwilligung in das Setzen von Cookies gibt, werden alle nachstehenden Cookies + ohne explizite Einwilligung gesetzt.

+ + + + + + + + + + { + this.state.thirdPartyCookies !== null ? this.state.thirdPartyCookies.map((cookie) => { + return + + + + ; + }) : null + } + +
DomainNameLebensdauer
{cookie.host_key}{cookie.name}{this.expiryString(Math.abs(cookie.expires_utc - cookie.creation_utc) / 1000000)}
+
+ ); } } @@ -385,13 +473,23 @@ class FontField extends Component { font = 'Titillium'; } - if (typeof this.props.data !== 'undefined') { - if (this.props.data.value) { - return ; - } - return ; + if (typeof this.props.data === 'undefined') { + return
; } - return
; + + if (this.props.data.value) { + return ; + } + + return +

Die Schriftart Arvo bzw. der Variante für Überschriften, Arvo Gruen, ist ein markanter Bestandteil der + Corporate-Design-Richtlinien von BÜNDNIS 90/DIE GRÜNEN. Die Verwendung der Schrift hilft dabei, den Absender + kenntlich zu machen, so wie es auch der Einsatz der richtigen Farben und die Verwendung des Logos tun.

+

Die empfohlenen Schriften stehen unter{' '} + github.com/netzbegruenung/webfonts für die + einfache Verwendung auf Webseiten zur Verfügung.

+
; } } @@ -400,7 +498,17 @@ class HTTPSField extends Component { if (this.props.data.value) { return ; } - return ; + + return +

Per TLS verschlüsselte HTTP-Verbindungen schützen Nutzer*innen vor der Preisgabe privater Informationen. + Entsprechend gehört HTTPS für immer mehr Nutzer*innen bei einem vertrauenswürdigen Webangebot zu den + Pflicht-Kriterien. Auch viele Unternehmen, darunter beispielsweise Google, haben inzwischen die HTTPS-Verbindung + zum Standard erklärt. Seiten, die nicht per HTTPS erreichbar sind, werden entsprechend von Google im + Suchergebnis schlechter platziert.

+

Inzwischen gibt es TLS-Zertifikate für Verschlüsselte Server-Kommunikation auch kostenlos, z. B. von + Let's Encrypt.

+

Lesetipp: HTTPS as a ranking signal

+
; } } @@ -432,7 +540,16 @@ class ResponsiveField extends Component { } else if (this.props.data.score > 0) { return ; } - return ; + + return +

Green Spider testet, wie breit die Startseite der Site auf verschieden breiten Bildschirmen ausfällt. + Ist in einer Breite die Seite breiter als der Bildschirm, so gilt der Test als nicht bestanden. + Nutzer*innen con Smartphones sehen in diesen Fällen häufig einen horizontalen Scrollbalken oder müssen + zum vollständigen Betrachten der Seite die Inhalte horizontal Verschieben.

+ +

Tipp: Zieh den Browserfenster so schmal wie Du kannst, im besten Fall auf 360 Pixel Breite. Damit erhältst + Du einen Eindruck, welche Inhalte über den Rand hinausragen.

+
; } } @@ -441,7 +558,11 @@ class ContactLinkField extends Component { if (this.props.data.value) { return ; } - return ; + + return +

Wenn Nutzer*innen mit dem Betreiber einer Site in Kontakt treten wollen, ist ein gut sichtbarer Link mit der + Beschriftung "Kontakt" eine der einfachsten Möglichkeit.

+
; } } @@ -450,7 +571,12 @@ class SocialMediaLinksField extends Component { if (this.props.data.value) { return ; } - return ; + + return +

Über Social-Media-Profile ist es möglich, häufiger mit Nutzer*innen in Kontakt zu treten. Sofern es Profile + gibt, sollten diese am besten von jeder Seite der Site verlinkt werden. Aktuell werden Links zu Facebook, + Twitter und Instagram gewertet.

+
; } } @@ -600,27 +726,87 @@ class WWWOptionalField extends Component { } } -class ScriptErrorsField extends Component { +class LoggedErrorsField extends Component { render() { - if (typeof this.props.data !== 'undefined') { - if (this.props.data.value) { - return ; - } - return ; + if (this.props.type === 'positive') { + return ; } - return
; + + return ( + + { this.props.logEntries !== null && this.props.logEntries !== [] ? + + + { + this.props.logEntries.map((item) => { + return + + + + ; + }) + } + +
{item.source}{item.level}{item.message}
+ : +

Es können leider keine Details zu den gesammelten Fehlern angezeigt werden.

} +
+ ); } } class NetworkErrorsField extends Component { + state = {logEntries: null}; + + componentDidMount() { + let logEntries = Object.values(this.props.details)[0].logs; + if (typeof logEntries !== 'undefined') { + let filteredEntries = logEntries.filter(item => item.source !== 'javascript'); + if (filteredEntries.length > 0) { + this.setState({logEntries: filteredEntries}); + } + } + } + render() { - if (typeof this.props.data !== 'undefined') { - if (this.props.data.value) { - return ; + if (typeof this.props.data === 'undefined') { + return
; + } + + if (this.props.data.value) { + return ; + } + + return ( + + ); + } +} + + +class ScriptErrorsField extends Component { + state = {logEntries: null}; + + componentDidMount() { + let logEntries = Object.values(this.props.details)[0].logs; + if (typeof logEntries !== 'undefined') { + let filteredEntries = logEntries.filter(item => item.source === 'javascript'); + if (filteredEntries.length > 0) { + this.setState({logEntries: filteredEntries}); } - return ; } - return
; + } + + render() { + if (typeof this.props.data === 'undefined') { + return
; + } + + if (this.props.data.value) { + return ; + } + + return ; } } diff --git a/yarn.lock b/yarn.lock index 576726a..0dd20ec 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2516,6 +2516,13 @@ dom-converter@~0.1: dependencies: utila "~0.3" +dom-helpers@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-3.4.0.tgz#e9b369700f959f62ecde5a6babde4bccd9169af8" + integrity sha512-LnuPJ+dwqKDIyotW1VzmOZ5TONUN7CwkCR5hrgawTUbkBGYdeoNLZo6nNfGkCrjtE1nXXaj7iMMpDa8/d9WoIA== + dependencies: + "@babel/runtime" "^7.1.2" + dom-serializer@0: version "0.1.0" resolved "https://registry.yarnpkg.com/dom-serializer/-/dom-serializer-0.1.0.tgz#073c697546ce0780ce23be4a28e293e40bc30c82" @@ -6959,6 +6966,15 @@ react-router@5.0.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" +react-transition-group@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.0.0.tgz#1d82b20d78aa09eac6268ceef0349307146942c6" + integrity sha512-b+uvkr15Pb80mqcsz5WAB+d53zS8/pTp3wDEsOiqpea93G8BqfsMFcPv2XZR0owqU13BJWoJvd17VjOPEY/9aA== + dependencies: + dom-helpers "^3.4.0" + loose-envify "^1.4.0" + prop-types "^15.6.2" + react@^16.8.5: version "16.8.5" resolved "https://registry.yarnpkg.com/react/-/react-16.8.5.tgz#49be3b655489d74504ad994016407e8a0445de66"