diff --git a/assets/css/webdesign.css b/assets/css/webdesign.css index 181f54c5..2cfee4d4 100644 --- a/assets/css/webdesign.css +++ b/assets/css/webdesign.css @@ -615,6 +615,13 @@ a.wd-karte:hover, .wd-karte--klickbar:hover { .wd-tag--lila { background: rgb(var(--wd-lila-rgb) / .12); border-color: rgb(var(--wd-lila-rgb) / .34); color: var(--wd-lila); } .wd-tag--gold { background: rgb(var(--wd-gold-rgb) / .12); border-color: rgb(var(--wd-gold-rgb) / .34); color: var(--wd-gold); } .wd-tag--ok { background: rgb(var(--wd-ok-rgb) / .12); border-color: rgb(var(--wd-ok-rgb) / .34); color: var(--wd-ok); } +/* Aqua fuer die Arcade. Als Schriftfarbe unbedenklich: Hyper Aqua liegt + auf allen vier dunklen Flaechen zwischen 12,4:1 und 19,8:1. */ +.wd-tag--aqua { background: rgb(var(--wd-aqua-rgb) / .12); border-color: rgb(var(--wd-aqua-rgb) / .34); color: rgb(var(--wd-aqua-rgb)); } +/* Indigo NUR als Flaeche und Kante, NIE als Schrift: Prism Indigo ist + die einzige Farbe der Palette, die als Text durchfaellt (3,25--4,29:1). + Die Schrift bleibt deshalb bewusst Chrome Silver. */ +.wd-tag--indigo { background: rgb(var(--wd-indigo-rgb) / .16); border-color: rgb(var(--wd-indigo-rgb) / .40); color: rgb(var(--wd-chrome-rgb)); } .wd-tags { display: flex; flex-wrap: wrap; gap: .45rem; } /* --------------------------------------------------------------------- diff --git a/assets/img/wd-projekt-analyse.jpg b/assets/img/wd-projekt-analyse.jpg new file mode 100644 index 00000000..7501198d Binary files /dev/null and b/assets/img/wd-projekt-analyse.jpg differ diff --git a/assets/img/wd-projekt-buchhaltung.jpg b/assets/img/wd-projekt-buchhaltung.jpg new file mode 100644 index 00000000..0ed45622 Binary files /dev/null and b/assets/img/wd-projekt-buchhaltung.jpg differ diff --git a/assets/img/wd-projekt-zockeranstalt.jpg b/assets/img/wd-projekt-zockeranstalt.jpg new file mode 100644 index 00000000..6e93d071 Binary files /dev/null and b/assets/img/wd-projekt-zockeranstalt.jpg differ diff --git a/assets/js/i18n-wd-portfolio.js b/assets/js/i18n-wd-portfolio.js index dabf4f14..3f220804 100644 --- a/assets/js/i18n-wd-portfolio.js +++ b/assets/js/i18n-wd-portfolio.js @@ -4,9 +4,16 @@ Inhaltsregeln, die hier bewusst eingehalten sind: - Keine erfundenen Kundenbewertungen. Auf dieser Seite steht KEIN einziges Zitat, weil es noch keines gibt, das jemand freigegeben hat. - - Keine fremden Projekte ohne Erlaubnis. Beide gezeigten Seiten sind - eigene Projekte; die Vorschaubilder sind echte Screenshots der - laufenden Seiten vom 22.08.2026, keine Mockups. + - Keine fremden Projekte ohne Erlaubnis. Alle fuenf gezeigten Seiten + sind eigene Projekte; die Vorschaubilder sind echte Screenshots der + laufenden Seiten (Projekt 1-2 vom 22.08.2026, Projekt 3-5 vom + 24.08.2026), keine Mockups. + - Zwei Vorschaubilder sind bewusst NICHT unbearbeitet: + Die Buchhaltung zeigt nur die Anmeldung, weil dahinter echte + Geschaeftszahlen liegen. Beim Command Center sind die Gesichter + unkenntlich gemacht -- die Seite gehoert dem Team, und niemand hat + zugestimmt, sein Bild im Portfolio zu zeigen. Beides steht als + Hinweis auch fuer die Besucher auf der Seite, nicht nur hier. - Jede Referenz im gleichen Format: Ausgangslage, Lösung, Ergebnis. ===================================================================== */ window.I18N_WD = { @@ -20,18 +27,18 @@ window.I18N_WD = { pf_eyebrow: { de: "Portfolio", "de-CH": "Portfolio", en: "Portfolio", fr: "Portfolio", pt: "Portfólio" }, pf_h1: { - de: "Zwei Projekte, die man anfassen kann", - "de-CH": "Zwei Projekt, wo mer aalange cha", - en: "Two projects you can actually visit", - fr: "Deux projets que l'on peut réellement visiter", - pt: "Dois projetos que se podem mesmo visitar", + de: "Fünf Projekte, die man anfassen kann", + "de-CH": "Föif Projekt, wo mer aalange cha", + en: "Five projects you can actually visit", + fr: "Cinq projets que l'on peut réellement visiter", + pt: "Cinco projetos que se podem mesmo visitar", }, pf_lead: { - de: "Beide Seiten laufen wirklich, unter ihrer eigenen Adresse, auf einem eigenen Server. Die Vorschaubilder sind echte Aufnahmen der laufenden Seiten — keine gezeichneten Entwürfe.", - "de-CH": "Beidi Site laufed würklich, under ihrer eigene Adress, uf eme eigene Server. D Vorschaubilder sind ächti Ufnahme vo de laufende Site — kei zeichneti Entwürf.", - en: "Both sites are genuinely running, at their own address, on their own server. The preview images are real captures of the live sites — not drawn mockups.", - fr: "Les deux sites fonctionnent réellement, à leur propre adresse, sur un serveur dédié. Les aperçus sont de vraies captures des sites en ligne — pas des maquettes dessinées.", - pt: "Ambos os sites estão mesmo no ar, no seu próprio endereço, em servidor próprio. As pré-visualizações são capturas reais dos sites ativos — não são maquetes desenhadas.", + de: "Jede dieser Seiten läuft wirklich, unter ihrer eigenen Adresse. Die Vorschaubilder sind echte Aufnahmen der laufenden Seiten — keine gezeichneten Entwürfe. Wo eine Aufnahme bearbeitet ist, steht der Grund beim Projekt dabei.", + "de-CH": "Jedi vo dene Site lauft würklich, under ihrer eigene Adress. D Vorschaubilder sind ächti Ufnahme vo de laufende Site — kei zeichneti Entwürf. Wo en Ufnahm bearbeitet isch, staht de Grund bim Projekt debii.", + en: "Every one of these sites is genuinely running, at its own address. The preview images are real captures of the live sites — not drawn mockups. Where a capture has been edited, the reason is given with the project.", + fr: "Chacun de ces sites fonctionne réellement, à sa propre adresse. Les aperçus sont de vraies captures des sites en ligne — pas des maquettes dessinées. Lorsqu'une capture a été retouchée, la raison est indiquée avec le projet.", + pt: "Cada um destes sites está mesmo no ar, no seu próprio endereço. As pré-visualizações são capturas reais dos sites ativos — não são maquetes desenhadas. Sempre que uma captura foi editada, o motivo é indicado junto ao projeto.", }, /* Gemeinsame Beschriftungen — gleiches Format für jede Referenz */ @@ -147,12 +154,165 @@ window.I18N_WD = { pt: "Não há uma única citação de cliente nesta página. Não por falta de clientes satisfeitos, mas porque ainda ninguém escreveu e aprovou um depoimento — e avaliações inventadas não aparecem aqui por princípio. Assim que existirem reais, estarão aqui. Até lá, prefiro um espaço vazio a um inventado.", }, + + /* ---------------- Projekt 3: Dogi's ZockerAnstalt ---------------- */ + p3_h2: { de: "Dogi's ZockerAnstalt", "de-CH": "Dogi's ZockerAnstalt", en: "Dogi's ZockerAnstalt", fr: "Dogi's ZockerAnstalt", pt: "Dogi's ZockerAnstalt" }, + p3_alt: { + de: "Startseite von zockeranstalt.dogfather-universe.com", + "de-CH": "Startsite vo zockeranstalt.dogfather-universe.com", + en: "Home page of zockeranstalt.dogfather-universe.com", + fr: "Page d'accueil de zockeranstalt.dogfather-universe.com", + pt: "Página inicial de zockeranstalt.dogfather-universe.com", + }, + p3_art: { de: "Spieleseite", "de-CH": "Spilsite", en: "Games site", fr: "Site de jeux", pt: "Site de jogos" }, + p3_ausgang: { + de: "Die Runde wollte zusammen spielen, ohne sich bei einem fremden Anbieter anzumelden.", + "de-CH": "D Rundi hät wölle zäme spile, ohni sich bi eme frönde Aabieter aazmelde.", + en: "The group wanted to play together without signing up with an outside provider.", + fr: "Le groupe voulait jouer ensemble sans s'inscrire chez un prestataire externe.", + pt: "O grupo queria jogar em conjunto sem se registar num fornecedor externo.", + }, + p3_loesung: { + de: "Eine eigene Spielhalle mit acht selbst gebauten Spielen, mehrere davon gemeinsam in Echtzeit spielbar.", + "de-CH": "En eigeni Spilhalle mit acht sälber boute Spil, mehreri devo zäme i Echtziit spilbar.", + en: "An arcade of its own with eight hand-built games, several of them playable together in real time.", + fr: "Une salle de jeux maison avec huit jeux créés sur mesure, dont plusieurs jouables ensemble en temps réel.", + pt: "Um salão de jogos próprio com oito jogos feitos de raiz, vários deles jogáveis em conjunto em tempo real.", + }, + p3_ergebnis: { + de: "Läuft unter eigener Adresse auf dem eigenen Server.", + "de-CH": "Lauft under eigener Adress uf em eigene Server.", + en: "Running at its own address on its own server.", + fr: "En ligne à sa propre adresse, sur un serveur dédié.", + pt: "No ar no seu próprio endereço, em servidor próprio.", + }, + p3_f1: { de: "Acht Spiele, jedes einzeln gebaut", "de-CH": "Acht Spil, jedes einzeln boue", en: "Eight games, each built individually", fr: "Huit jeux, chacun développé séparément", pt: "Oito jogos, cada um construído de raiz" }, + p3_f2: { de: "Mehrspieler in Echtzeit, mit Live-Ranglisten", "de-CH": "Mehrspiler i Echtziit, mit Live-Rangliste", en: "Real-time multiplayer with live rankings", fr: "Multijoueur en temps réel, avec classements en direct", pt: "Multijogador em tempo real, com classificações ao vivo" }, + p3_f3: { de: "Konten und Bestenlisten", "de-CH": "Konte und Bestelischte", en: "Accounts and high-score tables", fr: "Comptes et tableaux des meilleurs scores", pt: "Contas e tabelas de melhores pontuações" }, + p3_f4: { de: "Ein Regelwerk zu jedem Spiel", "de-CH": "Es Regelwärch zu jedem Spil", en: "A rulebook for every game", fr: "Un règlement pour chaque jeu", pt: "Um regulamento para cada jogo" }, + p3_f5: { de: "Eigene Fotos hochladbar, für ein Suchbild daraus", "de-CH": "Eigeni Fotti chasch ufelade, für es Suechbild drus", en: "Upload your own photos to turn them into a spot-the-difference picture", fr: "Possibilité d'importer ses propres photos pour en faire un jeu des différences", pt: "Pode carregar fotos próprias para criar um jogo das diferenças" }, + p3_stil: { + de: "Spielhallen-Optik in Neonfarben — kräftig, aber ohne Flackern und Blinken.", + "de-CH": "Spilhalle-Optik i Neonfarbe — kräftig, aber ohni Flackere und Blinke.", + en: "Arcade look in neon colours — bold, but with no flicker or blinking.", + fr: "Esthétique arcade aux couleurs néon — franche, mais sans scintillement ni clignotement.", + pt: "Visual de arcada em cores néon — marcante, mas sem tremeluzir nem piscar.", + }, + + /* ---------------- Projekt 4: Buchhaltung & Steuern ---------------- */ + p4_h2: { de: "Buchhaltung & Steuern", "de-CH": "Buechhaltig & Stüüre", en: "Bookkeeping & Tax", fr: "Comptabilité & impôts", pt: "Contabilidade & Impostos" }, + p4_alt: { + de: "Anmeldeseite von buchhaltung.vans-diy-bastelbedarf.com", + "de-CH": "Aameldesite vo buchhaltung.vans-diy-bastelbedarf.com", + en: "Sign-in page of buchhaltung.vans-diy-bastelbedarf.com", + fr: "Page de connexion de buchhaltung.vans-diy-bastelbedarf.com", + pt: "Página de início de sessão de buchhaltung.vans-diy-bastelbedarf.com", + }, + p4_art: { de: "Fachanwendung", "de-CH": "Fachaawändig", en: "Specialist application", fr: "Application métier", pt: "Aplicação especializada" }, + p4_ausgang: { + de: "Die Zahlen des Shops lagen in Tabellen und Zetteln verstreut, und vor dem Steuertermin musste alles von Hand zusammengesucht werden.", + "de-CH": "D Zahle vom Shop sind i Tabelle und uf Zettel verstreut gsi, und vor em Stüürtermin hät mer alles vo Hand zämesueche müesse.", + en: "The shop's figures were scattered across spreadsheets and slips of paper, and everything had to be gathered by hand before the tax deadline.", + fr: "Les chiffres de la boutique étaient éparpillés entre tableurs et bouts de papier, et il fallait tout rassembler à la main avant l'échéance fiscale.", + pt: "Os números da loja estavam dispersos por folhas de cálculo e papéis, e antes do prazo fiscal era tudo reunido à mão.", + }, + p4_loesung: { + de: "Eine eigene Buchhaltung mit doppelter Buchführung, die Verkäufe automatisch aus dem Shop übernimmt.", + "de-CH": "En eigeni Buechhaltig mit doppleter Buechfüehrig, wo d Verchäuf automatisch us em Shop übernimmt.", + en: "A bookkeeping system of its own using double-entry accounting, which pulls sales in from the shop automatically.", + fr: "Une comptabilité maison en partie double, qui récupère automatiquement les ventes depuis la boutique.", + pt: "Uma contabilidade própria por partidas dobradas, que importa as vendas da loja automaticamente.", + }, + p4_ergebnis: { + de: "Im laufenden Betrieb, mit einer schriftlichen Verfahrensdokumentation an den GoBD entlang.", + "de-CH": "Im laufende Betrib, mit ere schriftliche Verfahrensdokumentation em GoBD naa.", + en: "In daily use, with written process documentation following the German GoBD rules.", + fr: "En service quotidien, avec une documentation écrite des procédures suivant les règles allemandes GoBD.", + pt: "Em uso diário, com documentação escrita dos procedimentos segundo as regras alemãs GoBD.", + }, + p4_f1: { de: "Verkäufe kommen alle fünf Minuten selbst aus dem Shop", "de-CH": "Verchäuf chömed alli föif Minute vo sälber us em Shop", en: "Sales arrive from the shop by themselves every five minutes", fr: "Les ventes remontent d'elles-mêmes de la boutique toutes les cinq minutes", pt: "As vendas chegam sozinhas da loja a cada cinco minutos" }, + p4_f2: { de: "Rechnungen mit fortlaufender Nummer, Korrektur nur per Stornorechnung", "de-CH": "Rächnige mit fortloufender Nummere, Korrektur nur per Stornorächnig", en: "Invoices with consecutive numbering; corrections only via a cancellation invoice", fr: "Factures à numérotation continue ; correction uniquement par facture d'annulation", pt: "Faturas com numeração sequencial; correção apenas por fatura de anulação" }, + p4_f3: { de: "Kontoauszüge einlesen (CAMT.053, MT940 oder CSV)", "de-CH": "Chontouszüg iiläse (CAMT.053, MT940 oder CSV)", en: "Import bank statements (CAMT.053, MT940 or CSV)", fr: "Import des relevés bancaires (CAMT.053, MT940 ou CSV)", pt: "Importação de extratos bancários (CAMT.053, MT940 ou CSV)" }, + p4_f4: { de: "Eigene Konten je Kunde und Lieferant", "de-CH": "Eigeni Chonte pro Chund und Lieferant", en: "Separate accounts for each customer and supplier", fr: "Comptes distincts pour chaque client et fournisseur", pt: "Contas próprias para cada cliente e fornecedor" }, + p4_f5: { de: "Auswertungen entstehen alle aus demselben Buchungsbestand", "de-CH": "D Uuswertige entstöhnd alli us em glyche Buechigsbestand", en: "Every report is generated from one and the same set of entries", fr: "Toutes les analyses proviennent d'un seul et même ensemble d'écritures", pt: "Todas as análises resultam do mesmo conjunto de lançamentos" }, + p4_stil: { + de: "Ruhig und zurückhaltend — bei Zahlen soll nichts vom Lesen ablenken.", + "de-CH": "Ruehig und zrugghaltend — bi Zahle söll nüt vom Läse ablänke.", + en: "Calm and restrained — with figures, nothing should distract from reading.", + fr: "Sobre et discret — face à des chiffres, rien ne doit détourner de la lecture.", + pt: "Calmo e discreto — com números, nada deve distrair da leitura.", + }, + p4_kein_link: { + de: "Ohne öffentlichen Zugang: Dahinter liegen echte Geschäftszahlen.", + "de-CH": "Ohni öffentliche Zuegang: Dehinter ligged ächti Gschäftszahle.", + en: "No public access: real business figures lie behind it.", + fr: "Pas d'accès public : de vrais chiffres d'entreprise se trouvent derrière.", + pt: "Sem acesso público: por trás estão números reais da empresa.", + }, + + /* ---------------- Projekt 5: Command Center ---------------- */ + p5_h2: { de: "Command Center", "de-CH": "Command Center", en: "Command Center", fr: "Command Center", pt: "Command Center" }, + p5_alt: { + de: "Startseite von analyse.dogfather-universe.com, die Gesichter sind zum Schutz der Beteiligten unkenntlich gemacht", + "de-CH": "Startsite vo analyse.dogfather-universe.com, d Gsichter sind zum Schutz vo de Beteiligte unkenntlich gmacht", + en: "Home page of analyse.dogfather-universe.com; faces have been obscured to protect the people shown", + fr: "Page d'accueil de analyse.dogfather-universe.com ; les visages sont floutés pour protéger les personnes concernées", + pt: "Página inicial de analyse.dogfather-universe.com; os rostos foram desfocados para proteger as pessoas visadas", + }, + p5_art: { de: "Auswertung", "de-CH": "Uuswertig", en: "Analytics", fr: "Analyse", pt: "Análise" }, + p5_ausgang: { + de: "Die Zahlen zu Videos und Reichweite standen an verschiedenen Stellen, und niemand im Team sah dasselbe Bild.", + "de-CH": "D Zahle zu Video und Reichwiiti sind a verschidene Ort gstande, und im Team hät niemert s glyche Bild gseh.", + en: "The figures on videos and reach sat in different places, and nobody on the team saw the same picture.", + fr: "Les chiffres sur les vidéos et la portée se trouvaient à divers endroits, et personne dans l'équipe ne voyait la même chose.", + pt: "Os números sobre vídeos e alcance estavam em sítios diferentes, e ninguém na equipa via o mesmo quadro.", + }, + p5_loesung: { + de: "Eine gemeinsame Auswertung, zu der jede und jeder im Team einen eigenen Zugang hat.", + "de-CH": "En gmeinsami Uuswertig, wo jedi und jede im Team en eigene Zuegang hät.", + en: "One shared analytics view, with an individual login for everyone on the team.", + fr: "Une analyse commune, avec un accès personnel pour chaque membre de l'équipe.", + pt: "Uma análise partilhada, com acesso próprio para cada pessoa da equipa.", + }, + p5_ergebnis: { + de: "Im täglichen Einsatz, alle arbeiten mit demselben Stand.", + "de-CH": "Im täglichen Iisatz, alli schaffed mit em glyche Stand.", + en: "In daily use — everyone works from the same set of numbers.", + fr: "Utilisé au quotidien : tout le monde travaille sur les mêmes données.", + pt: "Em uso diário: todos trabalham com os mesmos dados.", + }, + p5_f1: { de: "Videos, Zahlen, Monate und Notizen an einer Stelle", "de-CH": "Video, Zahle, Mönet und Notize a einem Ort", en: "Videos, figures, months and notes in one place", fr: "Vidéos, chiffres, mois et notes au même endroit", pt: "Vídeos, números, meses e notas num só sítio" }, + p5_f2: { de: "Eigener Zugang je Person, mit unterschiedlichen Rechten", "de-CH": "En eigene Zuegang pro Person, mit underschidliche Rächt", en: "A separate login per person, with different permissions", fr: "Un accès distinct par personne, avec des droits différents", pt: "Um acesso próprio por pessoa, com permissões diferentes" }, + p5_f3: { de: "Zugangscodes selbst änderbar, nichts fest eingebaut", "de-CH": "Zuegangscode chasch sälber ändere, nüt fest iiboue", en: "Access codes can be changed by you; nothing is hard-wired", fr: "Les codes d'accès sont modifiables par vous ; rien n'est figé dans le code", pt: "Os códigos de acesso podem ser alterados por si; nada está fixo no código" }, + p5_f4: { de: "Als App installierbar", "de-CH": "Als App installierbar", en: "Installable as an app", fr: "Installable comme une application", pt: "Instalável como aplicação" }, + p5_stil: { + de: "Dunkel und aufgeräumt, mit einer ruhigen Leitfarbe je Bereich.", + "de-CH": "Dunkel und ufgruumt, mit ere ruehige Leitfarb pro Bereich.", + en: "Dark and tidy, with one calm accent colour per area.", + fr: "Sombre et ordonné, avec une couleur dominante sobre par section.", + pt: "Escuro e arrumado, com uma cor dominante discreta por área.", + }, + p5_hinweis_h3: { + de: "Zum Vorschaubild", + "de-CH": "Zum Vorschaubild", + en: "About the preview image", + fr: "À propos de l'aperçu", + pt: "Sobre a pré-visualização", + }, + p5_hinweis: { + de: "Die Gesichter auf der Aufnahme sind unkenntlich gemacht. Die Seite gehört dem Team, nicht mir — und niemand hat zugestimmt, sein Bild hier zu zeigen.", + "de-CH": "D Gsichter uf de Ufnahm sind unkenntlich gmacht. D Site ghört em Team, nöd mir — und niemert hät zuegstimmt, sis Bild do z zeige.", + en: "The faces in the capture have been obscured. The site belongs to the team, not to me — and nobody agreed to have their picture shown here.", + fr: "Les visages de la capture ont été floutés. Le site appartient à l'équipe, pas à moi — et personne n'a consenti à ce que son image figure ici.", + pt: "Os rostos da captura foram desfocados. O site pertence à equipa, não a mim — e ninguém consentiu que a sua imagem fosse mostrada aqui.", + }, + pf_cta_h2: { - de: "Soll deins das dritte sein?", - "de-CH": "Söll dis s dritte si?", - en: "Shall yours be the third?", - fr: "Le tien sera-t-il le troisième ?", - pt: "O teu vai ser o terceiro?", + de: "Soll deins das sechste sein?", + "de-CH": "Söll dis s sächste si?", + en: "Shall yours be the sixth?", + fr: "Le tien sera-t-il le sixième ?", + pt: "O teu vai ser o sexto?", }, pf_cta_p: { de: "Ob dein Projekt hier später auftaucht, entscheidest ausschließlich du — die Aufnahme ins Portfolio ist von der Veröffentlichung deiner Seite getrennt und braucht deine ausdrückliche Erlaubnis.", diff --git a/blickfang-portfolio-handy.png b/blickfang-portfolio-handy.png index 736dd3ae..cabd250f 100644 Binary files a/blickfang-portfolio-handy.png and b/blickfang-portfolio-handy.png differ diff --git a/blickfang-portfolio-pc.png b/blickfang-portfolio-pc.png index eb8a2326..2b460fb6 100644 Binary files a/blickfang-portfolio-pc.png and b/blickfang-portfolio-pc.png differ diff --git a/pruef-portfolio-computer.png b/pruef-portfolio-computer.png new file mode 100644 index 00000000..2d0d22ca Binary files /dev/null and b/pruef-portfolio-computer.png differ diff --git a/pruef-portfolio-handy.png b/pruef-portfolio-handy.png new file mode 100644 index 00000000..910a1805 Binary files /dev/null and b/pruef-portfolio-handy.png differ diff --git a/pruef-portfolio.mjs b/pruef-portfolio.mjs new file mode 100644 index 00000000..2680c694 --- /dev/null +++ b/pruef-portfolio.mjs @@ -0,0 +1,210 @@ +/* ===================================================================== + Prueft die Portfolio-Seite nach den drei neuen Eintraegen. + + Der wichtigste Test hier ist NICHT "sieht huebsch aus", sondern: + Hat jeder data-i18n-Schluessel im HTML auch eine Uebersetzung? Ein + fehlender Schluessel faellt im Deutschen nicht auf -- der Text steht + ja als Rueckfalltext im HTML -- und bricht erst auf, wenn jemand die + Sprache wechselt. Genau deshalb wird hier in ALLEN fuenf Sprachen + geprueft, nicht nur in einer. + ===================================================================== */ +import { chromium } from "playwright"; +import http from "http"; +import fs from "fs"; +import path from "path"; + +const TYP = { + ".html": "text/html", ".css": "text/css", ".js": "text/javascript", + ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", + ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg", +}; + +const fehlt = []; +const srv = http.createServer((q, r) => { + const u = decodeURIComponent(q.url.split("?")[0]); + const p = path.join(process.cwd(), u); + if (fs.existsSync(p) && !p.endsWith("/")) { + r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" }); + return r.end(fs.readFileSync(p)); + } + fehlt.push(u); + r.writeHead(404); + r.end(""); +}); + +let ok = 0, fehl = 0; +const pruefe = (name, gut, info) => { + if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } + else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } +}; + +/* Kontrast nach WCAG 2.2. Halbtransparente Flaechen muessen ueber + ihrem Untergrund verrechnet werden, sonst misst man Unsinn. */ +function leuchtkraft([r, g, b]) { + const f = (v) => { + v /= 255; + return v <= 0.04045 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); + }; + return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b); +} +function verhaeltnis(a, b) { + const l1 = leuchtkraft(a), l2 = leuchtkraft(b); + return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); +} + +srv.listen(4403, async () => { + const browser = await chromium.launch(); + const seite = await browser.newPage({ viewport: { width: 1440, height: 950 } }); + const skriptfehler = []; + seite.on("pageerror", (e) => skriptfehler.push(String(e).slice(0, 120))); + + await seite.goto("http://127.0.0.1:4403/webdesign/portfolio.html"); + await seite.waitForTimeout(1500); + + /* Die Vorschaubilder tragen loading="lazy" -- der Browser laedt sie + erst, wenn sie in Sichtweite kommen. Ohne dieses Vorbeischauen misst + der Test "Bild nicht geladen" und meint damit nur, dass er selbst + nie hingesehen hat. */ + await seite.evaluate(async () => { + for (const el of document.querySelectorAll(".pfo-bild")) { + el.scrollIntoView(); + await new Promise((r) => setTimeout(r, 250)); + } + window.scrollTo(0, 0); + }); + await seite.waitForTimeout(1200); + + console.log("1. SIND ALLE FUENF PROJEKTE DA?"); + const projekte = await seite.evaluate(() => + [...document.querySelectorAll(".pfo-projekt")].map((a) => ({ + titel: a.querySelector("h2")?.textContent.trim(), + bild: a.querySelector("img")?.getAttribute("src")?.replace(/.*\//, ""), + geladen: a.querySelector("img")?.naturalWidth > 0, + link: a.querySelector("a[href^='http']")?.href || null, + }))); + pruefe("fuenf Projekte auf der Seite", projekte.length === 5, String(projekte.length)); + for (const p of projekte) { + pruefe(` ${p.titel}: Vorschaubild geladen`, p.geladen, p.bild); + } + + /* Die Buchhaltung bekommt bewusst KEINEN Live-Knopf: Dahinter liegt + nur eine Anmeldemaske, ein Knopf dorthin waere ein leeres + Versprechen. Das wird hier festgehalten, damit es niemand spaeter + "der Vollstaendigkeit halber" wieder einbaut. */ + const buch = projekte.find((p) => /Buchhaltung/i.test(p.titel || "")); + pruefe("die Buchhaltung hat absichtlich keinen Live-Knopf", buch && !buch.link, + buch ? String(buch.link) : "nicht gefunden"); + + console.log("\n2. UEBERSETZUNGEN — IN ALLEN FUENF SPRACHEN"); + /* Geprueft werden die SEITENEIGENEN Schluessel (pf_, p1_-p5_, + seiten_titel). Navigations- und Markentexte liegen bewusst nicht + hier, sondern in wd-core.js -- sie gelten fuer alle Seiten und + werden von server/pruefe-webdesign-i18n.mjs abgedeckt. Sie hier + mitzuzaehlen hat nur Fehlalarm erzeugt. + + Und geprueft wird das ERGEBNIS, nicht das Woerterbuch: Die Seite + wird wirklich auf jede der fuenf Sprachen umgeschaltet, danach darf + kein seiteneigener Text leer sein. */ + const luecken = []; + for (const sprache of ["de", "de-CH", "en", "fr", "pt"]) { + const kaputt = await seite.evaluate((spr) => { + window.WD.setSprache(spr); + const raus = []; + const eigen = /^(pf_|p[1-5]_|seiten_titel)/; + for (const el of document.querySelectorAll("[data-i18n]")) { + const k = el.getAttribute("data-i18n"); + if (eigen.test(k) && !el.textContent.trim()) raus.push(k + " (leer)"); + } + for (const el of document.querySelectorAll("[data-i18n-attr]")) { + const k = el.getAttribute("data-i18n-attr").split(":")[1]; + const wert = el.getAttribute("alt"); + if (eigen.test(k) && !(wert && wert.trim())) raus.push(k + " (alt leer)"); + } + return raus; + }, sprache); + kaputt.forEach((k) => luecken.push(sprache + ": " + k)); + } + pruefe("in jeder Sprache ist jeder seiteneigene Text gefuellt", + luecken.length === 0, luecken.slice(0, 4).join(", ") || "alle fuenf Sprachen sauber"); + + /* Gegenprobe: Der Test darf nicht deshalb gruen sein, weil die + Umschaltung in Wahrheit gar nichts tut. */ + const wechsel = await seite.evaluate(() => { + const el = document.querySelector('[data-i18n="pf_h1"]'); + window.WD.setSprache("de"); + const deutsch = el.textContent.trim(); + window.WD.setSprache("en"); + const englisch = el.textContent.trim(); + window.WD.setSprache("de"); + return { deutsch, englisch }; + }); + pruefe("die Umschaltung wirkt wirklich", + wechsel.deutsch !== wechsel.englisch, wechsel.englisch); + await seite.waitForTimeout(300); + + console.log("\n3. DIE SEITE SPRICHT NICHT MEHR VON ZWEI PROJEKTEN"); + const zahlwort = await seite.evaluate(() => { + const t = window.I18N_WD; + const treffer = []; + for (const [k, v] of Object.entries(t)) { + for (const [s, txt] of Object.entries(v)) { + if (/\b(Zwei|Two|Deux|Dois|Beide|Both|Ambos)\b/i.test(txt)) treffer.push(k + "/" + s); + } + } + return treffer; + }); + pruefe("keine Stelle behauptet noch 'zwei' oder 'beide'", zahlwort.length === 0, + zahlwort.join(", ") || "sauber"); + + console.log("\n4. LESBARKEIT DER NEUEN SCHILDER"); + const schilder = await seite.evaluate(() => { + const holen = (sel) => { + const el = document.querySelector(sel); + if (!el) return null; + const s = getComputedStyle(el); + /* Untergrund suchen: das erste Elternteil mit deckender Farbe. */ + let p = el.parentElement, unter = null; + while (p && !unter) { + const bg = getComputedStyle(p).backgroundColor; + const m = bg.match(/[\d.]+/g); + if (m && (m.length === 3 || parseFloat(m[3]) === 1)) unter = m.slice(0, 3).map(Number); + p = p.parentElement; + } + return { + text: el.textContent.trim(), + farbe: s.color.match(/[\d.]+/g).slice(0, 3).map(Number), + eigen: s.backgroundColor.match(/[\d.]+/g), + unter: unter || [3, 8, 16], + }; + }; + return { aqua: holen(".wd-tag--aqua"), indigo: holen(".wd-tag--indigo") }; + }); + + for (const [name, d] of Object.entries(schilder)) { + if (!d) { pruefe(`Schild ${name} gefunden`, false); continue; } + /* Eigene halbtransparente Flaeche ueber den Untergrund rechnen. */ + const a = d.eigen.length === 4 ? parseFloat(d.eigen[3]) : 1; + const flaeche = d.unter.map((u, i) => Math.round(Number(d.eigen[i]) * a + u * (1 - a))); + const v = verhaeltnis(d.farbe, flaeche); + pruefe(`Schild "${d.text}" ist gut lesbar (AA = 4,5:1)`, v >= 4.5, v.toFixed(2) + ":1"); + } + + console.log("\n5. KEINE FEHLER, KEINE FEHLENDEN DATEIEN"); + pruefe("kein Skriptfehler", skriptfehler.length === 0, skriptfehler[0] || ""); + const verpasst = fehlt.filter((f) => /\.(jpg|webp|png|css|js|svg)$/.test(f)); + pruefe("keine fehlende Datei", verpasst.length === 0, verpasst.join(", ") || "keine"); + + await seite.screenshot({ path: "pruef-portfolio-computer.png", fullPage: true }); + const handy = await browser.newPage({ viewport: { width: 390, height: 844 } }); + await handy.goto("http://127.0.0.1:4403/webdesign/portfolio.html"); + await handy.waitForTimeout(1500); + const ueberlauf = await handy.evaluate(() => + document.documentElement.scrollWidth - document.documentElement.clientWidth); + pruefe("auf dem Handy laeuft nichts seitlich ueber", ueberlauf <= 1, ueberlauf + "px"); + await handy.screenshot({ path: "pruef-portfolio-handy.png", fullPage: true }); + + console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); + await browser.close(); + srv.close(); + process.exit(fehl ? 1 : 0); +}); diff --git a/webdesign/portfolio.html b/webdesign/portfolio.html index ce1c4031..67567399 100644 --- a/webdesign/portfolio.html +++ b/webdesign/portfolio.html @@ -9,7 +9,7 @@ der Inhalt sonst installiert unter Uhr und Akkuanzeige. --> Portfolio — Dogfather Webdesign - + @@ -65,12 +65,12 @@
Portfolio -

Zwei Projekte, die man anfassen kann

-

Beide Seiten laufen wirklich, unter ihrer eigenen Adresse, auf einem eigenen Server. Die Vorschaubilder sind echte Aufnahmen der laufenden Seiten — keine gezeichneten Entwürfe.

+

Fünf Projekte, die man anfassen kann

+

Jede dieser Seiten läuft wirklich, unter ihrer eigenen Adresse. Die Vorschaubilder sind echte Aufnahmen der laufenden Seiten — keine gezeichneten Entwürfe. Wo eine Aufnahme bearbeitet ist, steht der Grund beim Projekt dabei.

- @@ -184,6 +184,161 @@ + +
+ Startseite von zockeranstalt.dogfather-universe.com +
+
+

Dogi's ZockerAnstalt

+ Spieleseite +
+ +
+

Ausgangslage

+

Die Runde wollte zusammen spielen, ohne sich bei einem fremden Anbieter anzumelden.

+
+
+

Umgesetzte Lösung

+

Eine eigene Spielhalle mit acht selbst gebauten Spielen, mehrere davon gemeinsam in Echtzeit spielbar.

+
+
+

Ergebnis

+

Läuft unter eigener Adresse auf dem eigenen Server.

+
+ +
+ Details ansehen +
+
+

Wichtigste Funktionen

+
    +
  • Acht Spiele, jedes einzeln gebaut
  • +
  • Mehrspieler in Echtzeit, mit Live-Ranglisten
  • +
  • Konten und Bestenlisten
  • +
  • Ein Regelwerk zu jedem Spiel
  • +
  • Eigene Fotos hochladbar, für ein Suchbild daraus
  • +
+
+
+

Stilrichtung

+

Spielhallen-Optik in Neonfarben — kräftig, aber ohne Flackern und Blinken.

+
+
+
+ + +
+
+ + +
+ Anmeldeseite von buchhaltung.vans-diy-bastelbedarf.com +
+
+

Buchhaltung & Steuern

+ Fachanwendung +
+ +
+

Ausgangslage

+

Die Zahlen des Shops lagen in Tabellen und Zetteln verstreut, und vor dem Steuertermin musste alles von Hand zusammengesucht werden.

+
+
+

Umgesetzte Lösung

+

Eine eigene Buchhaltung mit doppelter Buchführung, die Verkäufe automatisch aus dem Shop übernimmt.

+
+
+

Ergebnis

+

Im laufenden Betrieb, mit einer schriftlichen Verfahrensdokumentation an den GoBD entlang.

+
+ +
+ Details ansehen +
+
+

Wichtigste Funktionen

+
    +
  • Verkäufe kommen alle fünf Minuten selbst aus dem Shop
  • +
  • Rechnungen mit fortlaufender Nummer, Korrektur nur per Stornorechnung
  • +
  • Kontoauszüge einlesen (CAMT.053, MT940 oder CSV)
  • +
  • Eigene Konten je Kunde und Lieferant
  • +
  • Auswertungen entstehen alle aus demselben Buchungsbestand
  • +
+
+
+

Stilrichtung

+

Ruhig und zurückhaltend — bei Zahlen soll nichts vom Lesen ablenken.

+
+
+
+ + +

Ohne öffentlichen Zugang: Dahinter liegen echte Geschäftszahlen.

+
+
+ + +
+ Startseite von analyse.dogfather-universe.com, die Gesichter sind zum Schutz der Beteiligten unkenntlich gemacht +
+
+

Command Center

+ Auswertung +
+ +
+

Ausgangslage

+

Die Zahlen zu Videos und Reichweite standen an verschiedenen Stellen, und niemand im Team sah dasselbe Bild.

+
+
+

Umgesetzte Lösung

+

Eine gemeinsame Auswertung, zu der jede und jeder im Team einen eigenen Zugang hat.

+
+
+

Ergebnis

+

Im täglichen Einsatz, alle arbeiten mit demselben Stand.

+
+ +
+ Details ansehen +
+
+

Wichtigste Funktionen

+
    +
  • Videos, Zahlen, Monate und Notizen an einer Stelle
  • +
  • Eigener Zugang je Person, mit unterschiedlichen Rechten
  • +
  • Zugangscodes selbst änderbar, nichts fest eingebaut
  • +
  • Als App installierbar
  • +
+
+
+

Stilrichtung

+

Dunkel und aufgeräumt, mit einer ruhigen Leitfarbe je Bereich.

+
+ +
+

Zum Vorschaubild

+

Die Gesichter auf der Aufnahme sind unkenntlich gemacht. Die Seite gehört dem Team, nicht mir — und niemand hat zugestimmt, sein Bild hier zu zeigen.

+
+
+
+ + +
+
+

Was hier bewusst fehlt
@@ -195,7 +350,7 @@

-

Soll deins das dritte sein?

+

Soll deins das sechste sein?

Ob dein Projekt hier später auftaucht, entscheidest ausschließlich du — die Aufnahme ins Portfolio ist von der Veröffentlichung deiner Seite getrennt und braucht deine ausdrückliche Erlaubnis.

Projekt anfragen diff --git a/webdesign/sw.js b/webdesign/sw.js index d261c0f8..62711eed 100644 --- a/webdesign/sw.js +++ b/webdesign/sw.js @@ -34,7 +34,7 @@ Start alle alten Zwischenspeicher weg. Muss bei jeder Änderung an den Dateien unten hochgezählt werden, sonst hängen Nutzer auf einem alten Stand fest. */ -const CACHE_NAME = "dogfather-webdesign-v33"; +const CACHE_NAME = "dogfather-webdesign-v34"; /* Bausteine, die die Oberfläche zum Anzeigen braucht. Bewusst KEINE HTML-Datei in dieser Liste. */