feat: initiële commit — Karby PWA koolhydratenteller met NEVO database, maaltijden, dagboek, e2e tests
- NEVO dataset (2328 voedingsmiddelen) met zoekfunctie - Dagboek met eetmomenten, koolhydratenlimiet en progress bar - Maaltijden opslaan + nesten in dagboek + eenmalig/permanent bewerken - Portie-systeem met steppers per NEVO-categorie - Vorteq amber/goud branding met Karby logo - Voedingscentrum links per product - 8 Playwright e2e tests (100% groen) - PWA met offline support Kanban board: koolhydraat-app
This commit is contained in:
commit
c2b9074ef9
15 changed files with 52159 additions and 0 deletions
7
.gitignore
vendored
Normal file
7
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
node_modules/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.cache/
|
||||||
|
*.log
|
||||||
|
.env
|
||||||
|
.hermes/
|
||||||
553
data/nevo-online.zip
Normal file
553
data/nevo-online.zip
Normal file
|
|
@ -0,0 +1,553 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="nl" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<link rel="apple-touch-icon" sizes="180x180" href="/themes/custom/sdv_theme/favicon_180x180.png" />
|
||||||
|
<meta name="twitter:card" content="summary_large_image" />
|
||||||
|
<meta name="msapplication-tileimage" content="/themes/custom/sdv_theme/favicon.png" />
|
||||||
|
<meta name="MobileOptimized" content="width" />
|
||||||
|
<meta name="HandheldFriendly" content="true" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script type="application/ld+json">{
|
||||||
|
"@context": "https://schema.org",
|
||||||
|
"@graph": [
|
||||||
|
{
|
||||||
|
"@type": "Organization",
|
||||||
|
"additionalType": "GovernmentOrganization",
|
||||||
|
"@id": "https://www.rivm.nl/",
|
||||||
|
"description": "Het Rijksinstituut voor Volksgezondheid en Milieu zet zich in voor een gezonde bevolking en een duurzame, veilige en gezonde leefomgeving voor huidige en toekomstige generaties. Dat doen we door wetenschap te verbinden aan praktijk en beleid. We adviseren op basis van wetenschappelijke kennis en onderzoek, coördineren preventieprogramma’s en staan klaar in tijden van crisis. We werken binnen en buiten het RIVM samen met organisaties en mensen verschillende expertises en achtergronden.",
|
||||||
|
"legalName": "Rijksinstituut voor Volksgezondheid en Milieu",
|
||||||
|
"name": "RIVM",
|
||||||
|
"parentOrganization": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"name": "Ministerie van Volksgezondheid, Welzijn en Sport"
|
||||||
|
},
|
||||||
|
"sameAs": [
|
||||||
|
"https://social.overheid.nl/@rivm",
|
||||||
|
"https://nl.linkedin.com/company/rivm",
|
||||||
|
"https://www.youtube.com/user/RIVMnl",
|
||||||
|
"https://www.instagram.com/rivmnl",
|
||||||
|
"https://www.facebook.com/RIVMnl",
|
||||||
|
"https://twitter.com/rivm"
|
||||||
|
],
|
||||||
|
"subOrganization": {
|
||||||
|
"@type": "Organization",
|
||||||
|
"url": [
|
||||||
|
"https://rijksvaccinatieprogramma.nl/",
|
||||||
|
"https://lci.rivm.nl/",
|
||||||
|
"https://www.atlasleefomgeving.nl/",
|
||||||
|
"https://www.vzinfo.nl/",
|
||||||
|
"https://www.pns.nl",
|
||||||
|
"https://www.sportenbewegenincijfers.nl/",
|
||||||
|
"https://rvs.rivm.nl/",
|
||||||
|
"https://www.loketgezondleven.nl"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"url": "https://www.rivm.nl/",
|
||||||
|
"telephone": "088 689 8989",
|
||||||
|
"contactPoint": {
|
||||||
|
"@type": "ContactPoint",
|
||||||
|
"telephone": "088 689 8989",
|
||||||
|
"email": "info@rivm.nl",
|
||||||
|
"url": "https://www.rivm.nl/",
|
||||||
|
"areaServed": {
|
||||||
|
"@type": "Country",
|
||||||
|
"name": "Netherlands"
|
||||||
|
},
|
||||||
|
"hoursAvailable": {
|
||||||
|
"@type": "OpeningHoursSpecification",
|
||||||
|
"dayOfWeek": [
|
||||||
|
"monday",
|
||||||
|
"tuesday",
|
||||||
|
"wednesday",
|
||||||
|
"thursday",
|
||||||
|
"friday"
|
||||||
|
],
|
||||||
|
"opens": "08:00",
|
||||||
|
"closes": "17:00"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"address": {
|
||||||
|
"@type": "PostalAddress",
|
||||||
|
"streetAddress": "Antonie van Leeuwenhoeklaan 9",
|
||||||
|
"addressLocality": "Bilthoven",
|
||||||
|
"postalCode": "3721 MA",
|
||||||
|
"addressCountry": "NL"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "WebPage",
|
||||||
|
"@id": "https://www.rivm.nl/documenten/nevo-online-versie",
|
||||||
|
"breadcrumb": {
|
||||||
|
"@type": "BreadcrumbList",
|
||||||
|
"itemListElement": [
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 1,
|
||||||
|
"name": "Home",
|
||||||
|
"item": "https://www.rivm.nl/"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "ListItem",
|
||||||
|
"position": 2,
|
||||||
|
"name": "NEVO-online versie 2025 9.0",
|
||||||
|
"item": "https://www.rivm.nl/documenten/nevo-online-versie"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"publisher": {
|
||||||
|
"@type": "GovernmentOrganization",
|
||||||
|
"@id": "https://www.rivm.nl/",
|
||||||
|
"name": "RIVM",
|
||||||
|
"url": "https://www.rivm.nl/",
|
||||||
|
"sameAs": [
|
||||||
|
"https://social.overheid.nl/@rivm",
|
||||||
|
"https://nl.linkedin.com/company/rivm",
|
||||||
|
"https://www.youtube.com/user/RIVMnl",
|
||||||
|
"https://www.instagram.com/rivmnl",
|
||||||
|
"https://www.facebook.com/RIVMnl",
|
||||||
|
"https://twitter.com/rivm"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"isAccessibleForFree": "True"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"@type": "WebSite",
|
||||||
|
"@id": "https://www.rivm.nl/",
|
||||||
|
"name": "RIVM",
|
||||||
|
"url": "https://www.rivm.nl/"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}</script>
|
||||||
|
<meta http-equiv="x-ua-compatible" content="ie=edge" />
|
||||||
|
<link rel="icon" href="/themes/custom/sdv_theme/favicon.ico" type="image/vnd.microsoft.icon" />
|
||||||
|
<link rel="alternate" hreflang="nl" href="https://www.rivm.nl/documenten/nevo-online-versie" />
|
||||||
|
<link rel="canonical" href="https://www.rivm.nl/documenten/nevo-online-versie" />
|
||||||
|
<link rel="shortlink" href="https://www.rivm.nl/media/142221" />
|
||||||
|
|
||||||
|
<title>NEVO-online versie 2025 9.0 | RIVM</title>
|
||||||
|
<link rel="stylesheet" media="all" href="/sites/default/files/css/css_Z_8Yj8xwgVd9EgLk1D9QtbEnlpw-tZNmPQGUP5T8Efs.css?delta=0&language=nl&theme=sdv_theme&include=eJxNi1sKg0AMAC-0uEcKUQNmjWabR-vevqVU6NcwAzOrhodhB-O2-5bsHtykDhLRV1nUqK6WHWXChlehK4TP_W4_Le2RZAOSATN00aMLBdV_Kb4-4d4_JDtR4Hv78KCjzuj0Bq9-OkQ" />
|
||||||
|
<link rel="stylesheet" media="all" href="/sites/default/files/css/css_uRmUlQqtPcYi55oq0a5RzbVKCGx70z5yhLisErtQEi4.css?delta=1&language=nl&theme=sdv_theme&include=eJxNi1sKg0AMAC-0uEcKUQNmjWabR-vevqVU6NcwAzOrhodhB-O2-5bsHtykDhLRV1nUqK6WHWXChlehK4TP_W4_Le2RZAOSATN00aMLBdV_Kb4-4d4_JDtR4Hv78KCjzuj0Bq9-OkQ" />
|
||||||
|
<link rel="stylesheet" media="print" href="/sites/default/files/css/css_uCkJK9CuQvszaudM5lWHYA0K2hntavvubKzbevb3tJY.css?delta=2&language=nl&theme=sdv_theme&include=eJxNi1sKg0AMAC-0uEcKUQNmjWabR-vevqVU6NcwAzOrhodhB-O2-5bsHtykDhLRV1nUqK6WHWXChlehK4TP_W4_Le2RZAOSATN00aMLBdV_Kb4-4d4_JDtR4Hv78KCjzuj0Bq9-OkQ" />
|
||||||
|
|
||||||
|
<script src="/themes/custom/bootstrap_rijkshuisstijl/js/jquery.min.js?v=3.7.1"></script>
|
||||||
|
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<a id="back-to-top-focus" tabindex="-1"></a>
|
||||||
|
<nav id="skipnav" class="skipnav list-group">
|
||||||
|
<a href="#main-title" class="sr-only sr-only-focusable list-group-item-info">Overslaan en naar de inhoud gaan</a>
|
||||||
|
<a href="#navbar-main" class="sr-only sr-only-focusable list-group-item-info">Direct naar de hoofdnavigatie</a>
|
||||||
|
</nav>
|
||||||
|
<script type="text/javascript">
|
||||||
|
// <![CDATA[
|
||||||
|
(function(window, document, dataLayerName, id) {
|
||||||
|
window[dataLayerName]=window[dataLayerName]||[],window[dataLayerName].push({start:(new Date).getTime(),event:"stg.start"});var scripts=document.getElementsByTagName('script')[0],tags=document.createElement('script');
|
||||||
|
function stgCreateCookie(a,b,c){var d="";if(c){var e=new Date;e.setTime(e.getTime()+24*c*60*60*1e3),d="; expires="+e.toUTCString();f=""}document.cookie=a+"="+b+d+f+"; path=/"}
|
||||||
|
var qP=[];
|
||||||
|
|
||||||
|
dataLayerName!=="dataLayer"&&qP.push("data_layer_name="+dataLayerName);var qPString=qP.length>0?("?"+qP.join("&")):"";
|
||||||
|
tags.async=!0,tags.src="https://statistiek.rijksoverheid.nl/containers/"+id+".js"+qPString,scripts.parentNode.insertBefore(tags,scripts);
|
||||||
|
!function(a,n,i){a[n]=a[n]||{};for(var c=0;c<i.length;c++)!function(i){a[n][i]=a[n][i]||{},a[n][i].api=a[n][i].api||function(){var a=[].slice.call(arguments,0);"string"==typeof a[0]&&window[dataLayerName].push({event:n+"."+i+":"+a[0],parameters:[].slice.call(arguments,1)})}}(i[c])}(window,"ppms",["tm","cm"])
|
||||||
|
})(window, document, 'dataLayer', 'c4aa3d73-954f-43c9-a9db-1d0e67ee1135');
|
||||||
|
// ]]>
|
||||||
|
</script>
|
||||||
|
<div class="dialog-off-canvas-main-canvas" data-off-canvas-main-canvas>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="layout-container">
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<header id="header" class="header ">
|
||||||
|
<div class="logo">
|
||||||
|
<a href="/"> <img src="/themes/custom/sdv_theme/logo.svg" width="44" height="77" alt="Rijksoverheid logo | naar de homepage van RIVM" class="logo-image" />
|
||||||
|
</a> <span class="wordmark">
|
||||||
|
<span class="sender ">Rijksinstituut voor Volksgezondheid<br>en Milieu</span>
|
||||||
|
<span class="secondary-sender ">Ministerie van Volksgezondheid,<br>Welzijn en Sport</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="navbar-topnav">
|
||||||
|
<div class="container">
|
||||||
|
<div class="topnavwrapper">
|
||||||
|
|
||||||
|
<ul class="menu top-navigation navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl#1" class="nav-link">Nederlands</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl/en#2" class="nav-link">English</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="navbar-titlebar">
|
||||||
|
<div class="container">
|
||||||
|
<strong>RIVM </strong><em>De zorg voor morgen</em> begint vandaag
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="parentnav">
|
||||||
|
<div class="navspan">
|
||||||
|
<nav class="navbar-nav navbar-expand-lg navbar-full navbar-branded navbar-border affix-top" id="navbar-main">
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="navbar-toggler icon-menu" id="menuButton" type="button" data-toggle="collapse" data-target="#navbarResponsive" aria-controls="navbarResponsive" aria-expanded="false">Menu
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse d-lg-flex clearfix" id="navbarResponsive">
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="navbar-nav use-dropdown">
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/home" class="icon-home nav-item nav-link" data-drupal-visually-hidden-label="1" data-drupal-link-system-path="node/11">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/onderwerpen" class="nav-item nav-link" data-drupal-link-system-path="onderwerpen">Onderwerpen</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/over-het-rivm" class="nav-item nav-link" data-drupal-link-system-path="node/173661">Over RIVM</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/publicatieoverzicht" class="nav-item nav-link" data-drupal-link-system-path="node/160981">Publicaties</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl/en/international" class="nav-item nav-link">International</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/contact" class="nav-item nav-link" data-drupal-link-system-path="node/86191">Contact</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/agenda" class="nav-item nav-link" data-drupal-link-system-path="agenda">Agenda</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://mijn.rivm.nl/" title="Mijn RIVM" class="nav-item nav-link">MijnRIVM</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="menu top-navigation navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl#1" class="nav-link">Nederlands</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl/en#2" class="nav-link">English</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="navbar-toggler navbar-toggler-right auto-sm icon-zoek collapsed" id="searchButton" type="button" data-toggle="collapse" data-target="#searchformResponsive" aria-controls="searchformResponsive" aria-expanded="false"><span class="sr-only">Zoekveld</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse d-lg-flex clearfix" id="searchformResponsive">
|
||||||
|
<div class="nav-search">
|
||||||
|
<form action="/zoeken" method="get" id="views-exposed-form-zoekresultaten-page-search-dummy" accept-charset="UTF-8" novalidate>
|
||||||
|
<div class="form-inline">
|
||||||
|
|
||||||
|
<div class="js-form-item js-form-type-search form-type-search js-form-item-search form-item-search">
|
||||||
|
<label for="edit-search">Zoeken</label>
|
||||||
|
<div class="search-wrapper">
|
||||||
|
<input data-drupal-selector="edit-search" type="search" id="edit-search" name="search" value="" size="30" maxlength="128" class="form-search form-control">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div data-drupal-selector="edit-actions" class="form-actions js-form-wrapper form-group" id="edit-actions">
|
||||||
|
<button data-drupal-selector="edit-submit-zoekresultaten" type="submit" id="edit-submit-zoekresultaten" class="button js-form-submit form-submit btn btn-primary">Zoeken</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<nav class="navbar-nav navbar-branded-light affix-top">
|
||||||
|
<div class="container">
|
||||||
|
<ul class="breadcrumb ">
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
<a href="/">Home</a>
|
||||||
|
</li>
|
||||||
|
<li class="breadcrumb-item">
|
||||||
|
NEVO-online versie 2025 9.0
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<a id="main-title" tabindex="-1"></a> <h1>NEVO-online versie 2025 9.0</h1>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
|
||||||
|
<div data-drupal-messages-fallback class="hidden"></div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<main role="main" class="container">
|
||||||
|
|
||||||
|
|
||||||
|
<div class="layout-local row">
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="layout-content">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-lg-12">
|
||||||
|
<div class="downloads-wrapper media-document container">
|
||||||
|
|
||||||
|
<div class="content-dates">
|
||||||
|
<span class="content-date-created">Publicatiedatum 18-11-2019 | 11:03</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="description">
|
||||||
|
<p>NEVO-online versie 2025 9.0</p>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="card-columns">
|
||||||
|
<li class="card card-nobg">
|
||||||
|
<hr class="top">
|
||||||
|
<div class="document">
|
||||||
|
|
||||||
|
<a href="https://www.rivm.nl/sites/default/files/2026-06/NEVO2025_v9.0.zip" class="file file--mime-application-zip file--package-x-generic" download="NEVO2025_v9.0.zip">
|
||||||
|
<span class="file-name">NEVO-online versie 2025 9.0</span>
|
||||||
|
<span class="file-subtitle">ZIP | 26,44 MB </span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<hr class="bottom">
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<div class="subdocument mt-3">
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="subjectswrapper mt-3">
|
||||||
|
<h3 class="h6">Gerelateerde onderwerpen</h3>
|
||||||
|
|
||||||
|
<div class="subjects-wrapper">
|
||||||
|
<ul class="field field--name-field-onderwerpen-media field--type-entity-reference field--label-hidden field__items card-columns">
|
||||||
|
<li class="field__item card card-nobg">
|
||||||
|
<hr class="top" />
|
||||||
|
<div class="document">
|
||||||
|
|
||||||
|
<article class="sdv-onderwerp teaser">
|
||||||
|
<a href="/nederlands-voedingsstoffenbestand" rel="bookmark">
|
||||||
|
|
||||||
|
<h4 class="h6 subjecttitle" >
|
||||||
|
Nederlands Voedingsstoffenbestand (NEVO)
|
||||||
|
</h4>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
<p class="lead">Het RIVM beheert het Nederlands Voedingsstoffenbestand (NEVO). Dit bestand bevat informatie over de voedingswaarde van voedingsmiddelen. Het gaat hierbij om energie en voedingsstoffen zoals eiwitten, koolhydraten, vetten en vetzuren, vitamines en mineralen. NEVO is het officiële voedingsstoffenbestand van Nederland, met gegevens van hoge kwaliteit en waarvan voor elk getal bekend is wat de herkomst is.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<hr class="bottom" />
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer bg-brand">
|
||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div aria-labelledby="block-voet-menu" id="block-voet" class="block block-menu navigation menu--footer">
|
||||||
|
|
||||||
|
<h2 id="block-voet-menu">Service</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/contact" class="nav-link" data-drupal-link-system-path="node/86191">Contact</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/contact/persinformatie" class="nav-link" data-drupal-link-system-path="node/91011">Persinformatie</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/werken-bij-rivm" title="Werken bij het RIVM" class="nav-link" data-drupal-link-system-path="node/233851">Werken bij het RIVM</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/klachten" class="nav-link" data-drupal-link-system-path="node/132841">Klachten</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/woo-verzoeken-bij-het-rivm" class="nav-link">Woo-verzoeken bij het RIVM</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/contact/zakendoen-met-rivm" class="nav-link" data-drupal-link-system-path="node/82561">Zakendoen met het RIVM</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/huisregels-sociale-media" title="Huisregels sociale media" class="nav-link" data-drupal-link-system-path="node/249581">Huisregels sociale media</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div aria-labelledby="block-footer2-menu" id="block-footer2" class="block block-menu navigation menu--footer-2">
|
||||||
|
|
||||||
|
<h2 id="block-footer2-menu">Over deze site</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/cookies" class="nav-link" data-drupal-link-system-path="node/132301">Cookies</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/privacy" class="nav-link" data-drupal-link-system-path="node/133641">Privacy</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/toegankelijkheid" class="nav-link" data-drupal-link-system-path="node/246661">Toegankelijkheid</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/disclaimer-copyright" class="nav-link" data-drupal-link-system-path="node/133671">Disclaimer en copyright</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/en/contact/coordinated-vulnerability-disclosure" class="nav-link">Coordinated Vulnerability Disclosure</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/archief-rivmnl" class="nav-link" title="archiefweb.eu" data-drupal-link-system-path="node/135291">Website archief</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div aria-labelledby="block-footer3-menu" id="block-footer3" class="block block-menu navigation menu--footer-3">
|
||||||
|
|
||||||
|
<h2 id="block-footer3-menu">Talen</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl/en#4" class="nav-link">English</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.rivm.nl#3" class="nav-link">Nederlands</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-3">
|
||||||
|
<div aria-labelledby="block-footer4-menu" id="block-footer4" class="block block-menu navigation menu--footer-4">
|
||||||
|
|
||||||
|
<h2 id="block-footer4-menu">Volg ons</h2>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/abonneren" class="nav-link" data-drupal-link-system-path="node/137751">Abonneren nieuwsbrieven</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="/rss" class="nav-link" data-drupal-link-system-path="node/134371">RSS feed</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://twitter.com/rivm" class="nav-link">X Twitter</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.facebook.com/RIVMnl" class="nav-link">Facebook</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://nl.linkedin.com/company/rivm" class="nav-link">LinkedIn</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.youtube.com/user/RIVMnl" class="nav-link">Youtube</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.instagram.com/rivmnl" class="nav-link">Instagram </a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://social.overheid.nl/@rivm" rel="me" class="nav-link">Mastodon</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<script type="application/json" data-drupal-selector="drupal-settings-json">{"path":{"baseUrl":"\/","pathPrefix":"","currentPath":"media\/142221","currentPathIsAdmin":false,"isFront":false,"currentLanguage":"nl"},"pluralDelimiter":"\u0003","suppressDeprecationErrors":true,"ajaxPageState":{"libraries":"eJx1jEEOgzAMBD8UNU-KbLCowY6j2GnL74sQ9NbT7mg1i2bh0aGVzuvmz8HuwavkRQxBEv7bdxKxd6JPCNctz300kMeFyedXQcaJg8o0PEzzhed0W0dSryDlJ6k1rmw13yU5axMqSgso1ZER_DjZPUjP_gX43E3i","theme":"sdv_theme","theme_token":null},"ajaxTrustedUrl":{"\/zoeken":true},"sdv_ckeditor":{"headings":[{"model":"htmlDivBgBrandLightest","view":{"name":"div","classes":"bg-brand-lightest blockPadding"},"priority":"normal"},{"model":"spanCTA","view":{"name":"span","classes":"cta-button"},"priority":"low"},{"model":"spanCTAAlternative","view":{"name":"span","classes":"cta-button cta-button-alt"},"priority":"high"},{"model":"spanCTAAlternative2","view":{"name":"span","classes":"cta-button cta-button-alt-2"},"priority":"high"},{"model":"htmlPre","view":{"name":"pre","classes":""},"priority":"normal"},{"model":"spanAlignCenter","view":{"name":"span","classes":"text-align-center w100"},"priority":"normal"},{"model":"spanAlignRight","view":{"name":"span","classes":"text-align-right w100"},"priority":"normal"}]},"mopinion":{"id":"Y0rTmSpMeW3aCdOIyn63QDS4j6sKgJjKiWeaRwFd"},"data":{"extlink":{"extTarget":false,"extTargetAppendNewWindowDisplay":true,"extTargetAppendNewWindowLabel":"(opent in een nieuw venster)","extTargetNoOverride":false,"extNofollow":false,"extTitleNoOverride":false,"extNoreferrer":false,"extFollowNoOverride":false,"extClass":"ext","extLabel":"(externe link)","extImgClass":false,"extSubdomains":false,"extExclude":"(test\\.sdv\\.rivm\\.nl)|(sdv\\.docker\\.localhost)|(rivmweb\\.lndo\\.site)","extInclude":"mailing\\?cid=[0-9]+\\\u0026mid=[0-9]+","extCssExclude":"","extCssInclude":"","extCssExplicit":"","extAlert":false,"extAlertText":"This link will take you to an external web site. We are not responsible for their content.","extHideIcons":false,"mailtoClass":"0","telClass":"tel","mailtoLabel":"(link stuurt een e-mail)","telLabel":"(link is een telefoonnummer)","extUseFontAwesome":true,"extIconPlacement":"append","extPreventOrphan":false,"extFaLinkClasses":"ext","extFaMailtoClasses":"fa fa-envelope-o","extAdditionalLinkClasses":"","extAdditionalMailtoClasses":"","extAdditionalTelClasses":"","extFaTelClasses":"fa fa-phone","allowedDomains":[],"extExcludeNoreferrer":""}},"user":{"uid":0,"permissionsHash":"92b44a05bc44831597b563d01b801cb319e4f322316c870b58eb7a5e58650699"}}</script>
|
||||||
|
<script src="/sites/default/files/js/js_yPnYEv_8Um7O1EnOXizW2pxKP2pxxurlPmqNauhTa1Q.js?scope=footer&delta=0&language=nl&theme=sdv_theme&include=eJw9i1sOgCAMBC9k5EgNr2ihUEKL8fgaA37tTibjmFW02wYdU5ZzoIhiInMQO0tbvJWwZhP6aJb2iZuECxw6jxrBD1EuZuKnVvVu7NUS_FHhhhW5mnUeOFkz4w"></script>
|
||||||
|
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20954
data/voedingsmiddelen.json
Normal file
20954
data/voedingsmiddelen.json
Normal file
File diff suppressed because it is too large
Load diff
5
docker/Dockerfile
Normal file
5
docker/Dockerfile
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
FROM nginx:alpine
|
||||||
|
COPY index.html /usr/share/nginx/html/
|
||||||
|
COPY data/voedingsmiddelen.json /usr/share/nginx/html/data/
|
||||||
|
RUN echo 'server { listen 80; root /usr/share/nginx/html; index index.html; location / { try_files $uri $uri/ /index.html; } }' > /etc/nginx/conf.d/default.conf
|
||||||
|
EXPOSE 80
|
||||||
20954
docker/data/voedingsmiddelen.json
Normal file
20954
docker/data/voedingsmiddelen.json
Normal file
File diff suppressed because it is too large
Load diff
21
docker/docker-compose.yml
Normal file
21
docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
services:
|
||||||
|
eetdagboek:
|
||||||
|
build: .
|
||||||
|
container_name: eetdagboek
|
||||||
|
restart: unless-stopped
|
||||||
|
networks:
|
||||||
|
- proxy
|
||||||
|
labels:
|
||||||
|
- "traefik.enable=true"
|
||||||
|
- "traefik.http.routers.eetdagboek.entrypoints=websecure"
|
||||||
|
- "traefik.http.routers.eetdagboek.rule=Host(`eetdagboek.vantwout.dev`)"
|
||||||
|
- "traefik.http.routers.eetdagboek.tls=true"
|
||||||
|
- "traefik.http.routers.eetdagboek.tls.certresolver=le"
|
||||||
|
- "traefik.http.services.eetdagboek.loadbalancer.server.port=80"
|
||||||
|
- "traefik.http.routers.eetdagboek-http.rule=Host(`eetdagboek.vantwout.dev`)"
|
||||||
|
- "traefik.http.routers.eetdagboek-http.entrypoints=web"
|
||||||
|
- "traefik.http.routers.eetdagboek-http.middlewares=redirect-https@internal"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
proxy:
|
||||||
|
external: true
|
||||||
4560
docker/index.html
Normal file
4560
docker/index.html
Normal file
File diff suppressed because it is too large
Load diff
70
hermes-verify-contrast.py
Normal file
70
hermes-verify-contrast.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Ad-hoc verification: contrast audit fix checker for koolhydraat-app."""
|
||||||
|
import re, sys
|
||||||
|
|
||||||
|
FILES = [
|
||||||
|
"/Users/cas/projects/koolhydraat-app/index.html",
|
||||||
|
"/Users/cas/projects/koolhydraat-app/docker/index.html",
|
||||||
|
]
|
||||||
|
|
||||||
|
REQUIRED_SELECTORS = [
|
||||||
|
".food-item-kh",
|
||||||
|
".eetmoment-kh",
|
||||||
|
".eetmoment-item-kh",
|
||||||
|
".total-carb-value",
|
||||||
|
".detail-kh-number",
|
||||||
|
".portion-preview-value",
|
||||||
|
".detail-info-link",
|
||||||
|
]
|
||||||
|
|
||||||
|
errors = []
|
||||||
|
files_checked = 0
|
||||||
|
fixes_found = 0
|
||||||
|
|
||||||
|
for fp in FILES:
|
||||||
|
try:
|
||||||
|
text = open(fp, "r", encoding="utf-8").read()
|
||||||
|
except FileNotFoundError:
|
||||||
|
errors.append(f"MISSING: {fp}")
|
||||||
|
continue
|
||||||
|
files_checked += 1
|
||||||
|
|
||||||
|
lines = text.split("\n")
|
||||||
|
for i, line in enumerate(lines, 1):
|
||||||
|
stripped = line.strip()
|
||||||
|
if re.search(r"(?<!border-)color:\s*var\(--accent\)", stripped):
|
||||||
|
errors.append(f"BANNED in {fp}:{i}: {stripped}")
|
||||||
|
if re.search(r"color:\s*var\(--primary\)$", stripped):
|
||||||
|
errors.append(f"BANNED in {fp}:{i}: {stripped}")
|
||||||
|
|
||||||
|
for sel in REQUIRED_SELECTORS:
|
||||||
|
idx = text.find(sel)
|
||||||
|
if idx >= 0:
|
||||||
|
chunk = text[idx:idx+300]
|
||||||
|
if "color: var(--primary-dark)" in chunk:
|
||||||
|
fixes_found += 1
|
||||||
|
else:
|
||||||
|
errors.append(f"MISSING FIX: {sel} in {fp}")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
print("VERIFICATION FAILED")
|
||||||
|
for e in errors:
|
||||||
|
print(f" \u2717 {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
print("VERIFICATION PASSED")
|
||||||
|
print(f" Files checked: {files_checked}")
|
||||||
|
print(f" Fixes confirmed: {fixes_found}")
|
||||||
|
expected = len(REQUIRED_SELECTORS) * files_checked
|
||||||
|
print(f" Expected fixes: {expected}")
|
||||||
|
print(f" All contrast fixes applied to both files \u2713")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("--- CSS Variables ---")
|
||||||
|
for fp in FILES:
|
||||||
|
text = open(fp, "r", encoding="utf-8").read()
|
||||||
|
for var in ["--primary-dark", "--primary", "--accent"]:
|
||||||
|
m = re.search(rf"{var}:\s*(#[0-9A-Fa-f]+)", text)
|
||||||
|
if m:
|
||||||
|
label = fp.split("/")[-1]
|
||||||
|
print(f" {label}: {var} = {m.group(1)}")
|
||||||
4560
index.html
Normal file
4560
index.html
Normal file
File diff suppressed because it is too large
Load diff
75
package-lock.json
generated
Normal file
75
package-lock.json
generated
Normal file
|
|
@ -0,0 +1,75 @@
|
||||||
|
{
|
||||||
|
"name": "koolhydraat-app",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@playwright/test": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-8nKv6+0RJSL9FE4jYOEGXnPeM/Hg12qZpmqzZjRh3qM0Y7c3z1mrOTfFLids72RDQYVh9WpLEfR5WdpNX4fkig==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/fsevents": {
|
||||||
|
"version": "2.3.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||||
|
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"dependencies": {
|
||||||
|
"playwright-core": "1.61.1"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"playwright": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"fsevents": "2.3.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/playwright-core": {
|
||||||
|
"version": "1.61.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz",
|
||||||
|
"integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"playwright-core": "cli.js"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
5
package.json
Normal file
5
package.json
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
{
|
||||||
|
"devDependencies": {
|
||||||
|
"@playwright/test": "^1.61.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
playwright.config.js
Normal file
14
playwright.config.js
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
const { defineConfig } = require('@playwright/test');
|
||||||
|
|
||||||
|
module.exports = defineConfig({
|
||||||
|
testDir: './tests',
|
||||||
|
timeout: 30000,
|
||||||
|
expect: { timeout: 10000 },
|
||||||
|
retries: 0,
|
||||||
|
use: {
|
||||||
|
baseURL: 'https://eetdagboek.vantwout.dev',
|
||||||
|
headless: true,
|
||||||
|
viewport: { width: 390, height: 844 }, // iPhone 14 size
|
||||||
|
ignoreHTTPSErrors: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
4
test-results/.last-run.json
Normal file
4
test-results/.last-run.json
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"status": "passed",
|
||||||
|
"failedTests": []
|
||||||
|
}
|
||||||
291
test-suggest.js
Normal file
291
test-suggest.js
Normal file
|
|
@ -0,0 +1,291 @@
|
||||||
|
// Test script for the new suggestMealName and shortItemName functions
|
||||||
|
|
||||||
|
const NEVO_DESC = new Set([
|
||||||
|
'rauw','rauwe','gekookt','gekookte','onbereid','onbereide','bereid','bereide',
|
||||||
|
'vers','verse','diepvries','diepgevroren','gedroogd','gedroogde',
|
||||||
|
'gem','gemixt','gesneden','gebakken','gebraden','gegrild','gestoofd','gefrituurd',
|
||||||
|
'ongezouten','gezouten','gerookt','gepekeld',
|
||||||
|
'gekonserveerd','geconserveerd','gezeefd','uitgesmolten',
|
||||||
|
'gepaneerd','gevuld','voorgekookt','koelvers',
|
||||||
|
'halfvolle','magere','volle','halfvol',
|
||||||
|
'scharrel','biologisch','biologische','mais',
|
||||||
|
'witte','bruine','zwarte','rode','groene','groen','gele','oranje',
|
||||||
|
'zoete','zure','bittere','zoute',
|
||||||
|
'schil','vliesje','vel','niet','panklaar',
|
||||||
|
'kruimig','vastkokend','eigen','mnd','ongekookt','gt','kippen','naturel',
|
||||||
|
]);
|
||||||
|
|
||||||
|
function shortItemName(naam) {
|
||||||
|
if (!naam) return '';
|
||||||
|
let name = naam.replace(/^\d+\s+/, '');
|
||||||
|
const dsi = name.indexOf(' ');
|
||||||
|
if (dsi > 0) name = name.substring(dsi).trim();
|
||||||
|
name = name.split('/')[0].trim();
|
||||||
|
name = name.replace(/\([^)]*\)/g, '').trim();
|
||||||
|
name = name.split(',')[0].trim();
|
||||||
|
name = name.replace(/\b\d+[%+]?\b/g, '').trim();
|
||||||
|
name = name.replace(/>/g, '').trim();
|
||||||
|
name = name.replace(/\s+/g, ' ').trim();
|
||||||
|
if (!name) return '';
|
||||||
|
const words = name.split(/\s+/);
|
||||||
|
let keep = words.slice(0, 2);
|
||||||
|
if (keep.length >= 2 && keep[1].endsWith('-') && keep[0].toLowerCase() !== 'ei') {
|
||||||
|
const mod = keep[1].slice(0, -1);
|
||||||
|
const base = keep[0];
|
||||||
|
const comp = mod.toLowerCase() + base.toLowerCase();
|
||||||
|
keep = [comp.charAt(0).toUpperCase() + comp.slice(1)];
|
||||||
|
}
|
||||||
|
keep = keep.map(w => w.replace(/-$/, ''));
|
||||||
|
const result = [];
|
||||||
|
const seen = new Set();
|
||||||
|
for (const w of keep) {
|
||||||
|
if (w.length <= 1) continue;
|
||||||
|
const wl = w.toLowerCase();
|
||||||
|
if (NEVO_DESC.has(wl)) continue;
|
||||||
|
if (seen.has(wl)) continue;
|
||||||
|
seen.add(wl);
|
||||||
|
result.push(w);
|
||||||
|
}
|
||||||
|
return result.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function nevoDinerType(nevoCat) {
|
||||||
|
const map = {
|
||||||
|
'Aardappelen en knolgewassen': 'aardappel',
|
||||||
|
'Groente': 'groente',
|
||||||
|
'Vlees en gevogelte': 'vlees',
|
||||||
|
'Vis, schaal- en schelpdieren': 'vlees',
|
||||||
|
'Vleeswaren': 'vlees',
|
||||||
|
'Eieren': 'vlees',
|
||||||
|
'Brood': 'brood',
|
||||||
|
'Kaas': 'kaas',
|
||||||
|
'Melk en melkproducten': 'zuivel',
|
||||||
|
'Fruit': 'fruit',
|
||||||
|
};
|
||||||
|
return map[nevoCat] || 'overig';
|
||||||
|
}
|
||||||
|
|
||||||
|
const CATEGORY_MAP = {
|
||||||
|
'Brood': 'Brood',
|
||||||
|
'Melk en melkproducten': 'Zuivel',
|
||||||
|
'Kaas': 'Zuivel',
|
||||||
|
'Fruit': 'Fruit',
|
||||||
|
'Groente': 'Groente',
|
||||||
|
'Aardappelen en knolgewassen': 'Groente',
|
||||||
|
'Peulvruchten': 'Groente',
|
||||||
|
'Vlees en gevogelte': 'Vlees/Vis',
|
||||||
|
'Vis, schaal- en schelpdieren': 'Vlees/Vis',
|
||||||
|
'Vleeswaren': 'Vlees/Vis',
|
||||||
|
'Eieren': 'Vlees/Vis',
|
||||||
|
'Alcoholische dranken': 'Dranken',
|
||||||
|
'Niet-alcoholische dranken': 'Dranken',
|
||||||
|
'Hartige snacks en zoutjes': 'Tussendoortjes',
|
||||||
|
'Noten en zaden': 'Tussendoortjes',
|
||||||
|
'Gebak en koek': 'Snoep/Koek',
|
||||||
|
'Suiker, snoep, zoet beleg en zoete sauzen': 'Snoep/Koek',
|
||||||
|
'Graanproducten en meelsoorten': 'Overig',
|
||||||
|
'Vleesvervangers en zuivelvervangers': 'Overig',
|
||||||
|
'Samengestelde gerechten': 'Overig',
|
||||||
|
'Soepen': 'Overig',
|
||||||
|
'Hartige sauzen': 'Overig',
|
||||||
|
'Kruiden en specerijen': 'Overig',
|
||||||
|
'Vetten en oliën': 'Overig',
|
||||||
|
'Diversen': 'Overig',
|
||||||
|
'Flesvoeding en preparaten': 'Overig',
|
||||||
|
'Hartig broodbeleg': 'Overig'
|
||||||
|
};
|
||||||
|
|
||||||
|
function mapCategory(itemCat) {
|
||||||
|
return CATEGORY_MAP[itemCat] || 'Overig';
|
||||||
|
}
|
||||||
|
|
||||||
|
function suggestMealName(items) {
|
||||||
|
if (!items || items.length === 0) return '';
|
||||||
|
if (items.length === 1) return shortItemName(items[0].item.naam);
|
||||||
|
|
||||||
|
const enriched = items.map(e => ({
|
||||||
|
name: shortItemName(e.item.naam),
|
||||||
|
cat: mapCategory(e.item.cat || 'Overig'),
|
||||||
|
nevoCat: e.item.cat || 'Overig',
|
||||||
|
type: nevoDinerType(e.item.cat || 'Overig'),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const byCat = {};
|
||||||
|
enriched.forEach(info => {
|
||||||
|
if (!byCat[info.cat]) byCat[info.cat] = [];
|
||||||
|
byCat[info.cat].push(info);
|
||||||
|
});
|
||||||
|
const cats = Object.keys(byCat);
|
||||||
|
|
||||||
|
const hasAardappel = enriched.some(e => e.type === 'aardappel');
|
||||||
|
const hasGroente = enriched.some(e => e.type === 'groente');
|
||||||
|
const hasVlees = enriched.some(e => e.type === 'vlees');
|
||||||
|
|
||||||
|
if (hasAardappel && (hasGroente || hasVlees)) {
|
||||||
|
const groenteNamen = enriched
|
||||||
|
.filter(e => e.type === 'groente')
|
||||||
|
.map(e => e.name);
|
||||||
|
const vleesNamen = enriched
|
||||||
|
.filter(e => e.type === 'vlees')
|
||||||
|
.map(e => e.name);
|
||||||
|
const groente = groenteNamen[0] || '';
|
||||||
|
const vlees = vleesNamen[0] || '';
|
||||||
|
|
||||||
|
if (hasGroente && hasVlees && groente && vlees) {
|
||||||
|
return 'AVG ' + groente + ' en ' + vlees;
|
||||||
|
}
|
||||||
|
if (hasGroente && groente) {
|
||||||
|
return 'Prakkie ' + groente + (vlees ? ' met ' + vlees : '');
|
||||||
|
}
|
||||||
|
const other = enriched.find(e => e.type !== 'aardappel');
|
||||||
|
if (other) return 'Aardappel met ' + other.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasGroente && hasVlees) {
|
||||||
|
const gr = enriched.filter(e => e.type === 'groente').map(e => e.name)[0] || '';
|
||||||
|
const vl = enriched.filter(e => e.type === 'vlees').map(e => e.name)[0] || '';
|
||||||
|
if (gr && vl) return gr + ' met ' + vl;
|
||||||
|
if (gr) return 'Groente met ' + vl;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cats.includes('Brood')) {
|
||||||
|
const beleg = enriched.filter(e => e.cat !== 'Brood').map(e => e.name);
|
||||||
|
if (beleg.length === 1) return 'Broodje ' + beleg[0];
|
||||||
|
if (beleg.length === 2) return 'Broodje ' + beleg[0] + ' en ' + beleg[1];
|
||||||
|
if (beleg.length <= 4) return 'Brood met ' + beleg.slice(0, -1).join(', ') + ' en ' + beleg[beleg.length - 1];
|
||||||
|
return 'Brood met beleg';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cats.length === 1) {
|
||||||
|
const cat = cats[0];
|
||||||
|
const catNamen = byCat[cat].map(i => i.name);
|
||||||
|
if (catNamen.length <= 3) {
|
||||||
|
if (catNamen.length === 2) return catNamen[0] + ' en ' + catNamen[1];
|
||||||
|
if (catNamen.length === 3) return catNamen[0] + ', ' + catNamen[1] + ' en ' + catNamen[2];
|
||||||
|
return catNamen[0];
|
||||||
|
}
|
||||||
|
return cat + ' (' + catNamen.length + ' stuks)';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cats.length === 2) {
|
||||||
|
const [a, b] = cats;
|
||||||
|
const na = byCat[a].map(i => i.name);
|
||||||
|
const nb = byCat[b].map(i => i.name);
|
||||||
|
if (na.length === 1 && nb.length === 1) return na[0] + ' & ' + nb[0];
|
||||||
|
const parts = cats.map(c => {
|
||||||
|
const n = byCat[c].map(i => i.name);
|
||||||
|
if (n.length <= 2) return n.join(' & ');
|
||||||
|
return n[0] + ' (+' + (n.length - 1) + ')';
|
||||||
|
});
|
||||||
|
return parts.join(' & ');
|
||||||
|
}
|
||||||
|
|
||||||
|
const sorted = cats.sort((a, b) => byCat[b].length - byCat[a].length);
|
||||||
|
const [top1, top2] = sorted;
|
||||||
|
const rest = enriched.length - byCat[top1].length - byCat[top2].length;
|
||||||
|
let r = top1 + ' & ' + top2;
|
||||||
|
if (rest > 0) r += ' +' + rest + ' meer';
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
// === TEST SCENARIOS ===
|
||||||
|
|
||||||
|
// Bug report: AVG dinner
|
||||||
|
const bugItems = [
|
||||||
|
{ item: { naam: '2747 Aardappelen kruimig z schil gekookt gem', cat: 'Aardappelen en knolgewassen', kh: '19' } },
|
||||||
|
{ item: { naam: '1146 Spinazie gesneden diepvries onbereid', cat: 'Groente', kh: '1' } },
|
||||||
|
{ item: { naam: '2769 Ei kippen- scharrel rauw Scharrelei/scharrelkippeneieren rauw', cat: 'Eieren', kh: '13' } },
|
||||||
|
{ item: { naam: '3026 Speklap bereid in eigen vet', cat: 'Vlees en gevogelte', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== BUG REPORT: AVG dinner ===');
|
||||||
|
console.log('Items:', bugItems.map(i => shortItemName(i.item.naam)).join(', '));
|
||||||
|
console.log('Suggestion:', suggestMealName(bugItems));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Broodje kaas
|
||||||
|
const broodjeKaas = [
|
||||||
|
{ item: { naam: '533 Brood bruin volkoren', cat: 'Brood', kh: '40' } },
|
||||||
|
{ item: { naam: '511 Kaas Edammer 40+', cat: 'Kaas', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== Broodje kaas ===');
|
||||||
|
console.log('Items:', broodjeKaas.map(i => i.item.naam).join(', '));
|
||||||
|
console.log('Suggestion:', suggestMealName(broodjeKaas));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Prakkie: aardappel + groente
|
||||||
|
const prakkie = [
|
||||||
|
{ item: { naam: '1 Aardappelen rauw', cat: 'Aardappelen en knolgewassen', kh: '19' } },
|
||||||
|
{ item: { naam: '8 Andijvie gekookt', cat: 'Groente', kh: '1' } },
|
||||||
|
];
|
||||||
|
console.log('=== Prakkie: aardappel + groente ===');
|
||||||
|
console.log('Items:', prakkie.map(i => shortItemName(i.item.naam)).join(', '));
|
||||||
|
console.log('Suggestion:', suggestMealName(prakkie));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Prakkie met vlees
|
||||||
|
const prakkieVlees = [
|
||||||
|
{ item: { naam: '1 Aardappelen rauw', cat: 'Aardappelen en knolgewassen', kh: '19' } },
|
||||||
|
{ item: { naam: '8 Andijvie gekookt', cat: 'Groente', kh: '1' } },
|
||||||
|
{ item: { naam: '1432 Speklap rauw', cat: 'Vlees en gevogelte', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== Prakkie met vlees ===');
|
||||||
|
console.log('Items:', prakkieVlees.map(i => shortItemName(i.item.naam)).join(', '));
|
||||||
|
console.log('Suggestion:', suggestMealName(prakkieVlees));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Brood met ham
|
||||||
|
const broodHam = [
|
||||||
|
{ item: { naam: '533 Brood bruin volkoren', cat: 'Brood', kh: '40' } },
|
||||||
|
{ item: { naam: '328 Ham rauwe', cat: 'Vleeswaren', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== Broodje ham ===');
|
||||||
|
console.log('Suggestion:', suggestMealName(broodHam));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Yoghurt + fruit (ontbijt)
|
||||||
|
const ontbijt = [
|
||||||
|
{ item: { naam: '270 Melk rauwe', cat: 'Melk en melkproducten', kh: '5' } },
|
||||||
|
{ item: { naam: '148 Aardbeien', cat: 'Fruit', kh: '5' } },
|
||||||
|
{ item: { naam: '209 Ontbijtproduct Cornflakes', cat: 'Graanproducten en meelsoorten', kh: '75' } },
|
||||||
|
];
|
||||||
|
console.log('=== Ontbijt (generic) ===');
|
||||||
|
console.log('Items:', ontbijt.map(i => shortItemName(i.item.naam)).join(', '));
|
||||||
|
console.log('Suggestion:', suggestMealName(ontbijt));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Groente + vlees dinner
|
||||||
|
const groenteVlees = [
|
||||||
|
{ item: { naam: '51 Spinazie rauw', cat: 'Groente', kh: '1' } },
|
||||||
|
{ item: { naam: '1432 Speklap rauw', cat: 'Vlees en gevogelte', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== Groente + vlees ===');
|
||||||
|
console.log('Suggestion:', suggestMealName(groenteVlees));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Brood met 2 beleg
|
||||||
|
const broodDubbel = [
|
||||||
|
{ item: { naam: '533 Brood bruin volkoren', cat: 'Brood', kh: '40' } },
|
||||||
|
{ item: { naam: '511 Kaas Edammer 40+', cat: 'Kaas', kh: '0' } },
|
||||||
|
{ item: { naam: '328 Ham rauwe', cat: 'Vleeswaren', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== Broodje kaas en ham ===');
|
||||||
|
console.log('Suggestion:', suggestMealName(broodDubbel));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Single category, multiple items (Fruit)
|
||||||
|
const fruitMand = [
|
||||||
|
{ item: { naam: '147 Appel z schil gem', cat: 'Fruit', kh: '13' } },
|
||||||
|
{ item: { naam: '148 Aardbeien', cat: 'Fruit', kh: '5' } },
|
||||||
|
{ item: { naam: '33 Rozijnen gedroogd', cat: 'Fruit', kh: '67' } },
|
||||||
|
];
|
||||||
|
console.log('=== Fruitmand (3 items) ===');
|
||||||
|
console.log('Suggestion:', suggestMealName(fruitMand));
|
||||||
|
console.log();
|
||||||
|
|
||||||
|
// Kip + groente
|
||||||
|
const kipGroente = [
|
||||||
|
{ item: { naam: '51 Spinazie rauw', cat: 'Groente', kh: '1' } },
|
||||||
|
{ item: { naam: '95 Paardenvlees rauw', cat: 'Vlees en gevogelte', kh: '0' } },
|
||||||
|
];
|
||||||
|
console.log('=== Kip + groente (vlees + groente) ===');
|
||||||
|
console.log('Suggestion:', suggestMealName(kipGroente));
|
||||||
86
tests/e2e.spec.js
Normal file
86
tests/e2e.spec.js
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
const { test, expect } = require('@playwright/test');
|
||||||
|
|
||||||
|
const BASE = 'https://eetdagboek.vantwout.dev';
|
||||||
|
|
||||||
|
test.describe('Karby Eetdagboek', () => {
|
||||||
|
|
||||||
|
test('page loads with correct title and elements', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
await expect(page).toHaveTitle('Karby');
|
||||||
|
await expect(page.locator('.search-input')).toBeVisible();
|
||||||
|
await expect(page.locator('.cat-btn').first()).toBeVisible();
|
||||||
|
await expect(page.locator('#navZoeken')).toBeVisible();
|
||||||
|
await expect(page.locator('#navDagboek')).toBeVisible();
|
||||||
|
await expect(page.locator('#navMaaltijden')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('search returns results and opens detail modal', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
await page.locator('.search-input').fill('aardappel');
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
const results = page.locator('.food-item');
|
||||||
|
await expect(results.first()).toBeVisible({ timeout: 5000 });
|
||||||
|
await results.first().click();
|
||||||
|
await expect(page.locator('#detailModal')).not.toHaveClass(/hidden/, { timeout: 5000 });
|
||||||
|
await page.locator('#detailModal .modal-close').click();
|
||||||
|
await expect(page.locator('#detailModal')).toHaveClass(/hidden/, { timeout: 3000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('category filters work', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
await page.locator('.cat-btn').filter({ hasText: 'Brood' }).click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
const activeBtn = page.locator('.cat-btn.active');
|
||||||
|
await expect(activeBtn).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('add item to diary via eetmoment flow', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
await page.locator('.search-input').fill('brood');
|
||||||
|
await page.waitForTimeout(800);
|
||||||
|
await page.locator('.food-item').first().click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
// Detail modal with Add button
|
||||||
|
await expect(page.locator('#detailAddBtn')).toBeVisible({ timeout: 3000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('diary tab shows date navigation', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
await page.locator('#navDagboek').click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(page.locator('.day-navigator')).toBeVisible({ timeout: 3000 });
|
||||||
|
await expect(page.locator('.day-nav-btn').first()).toBeVisible();
|
||||||
|
await expect(page.locator('.day-nav-today')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('settings modal opens', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
// Click settings gear icon
|
||||||
|
await page.locator('#settingsBtn').click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(page.locator('#settingsModal')).not.toHaveClass(/hidden/, { timeout: 3000 });
|
||||||
|
// Close
|
||||||
|
await page.locator('#settingsModal .modal-close').click();
|
||||||
|
await expect(page.locator('#settingsModal')).toHaveClass(/hidden/, { timeout: 2000 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maaltijden tab shows saved meals section', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
await page.locator('#navMaaltijden').click();
|
||||||
|
await page.waitForTimeout(500);
|
||||||
|
await expect(page.locator('#tabMaaltijden')).toBeVisible();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('contrast: key elements have visible colors', async ({ page }) => {
|
||||||
|
await page.goto(BASE);
|
||||||
|
// Nav buttons should have visible text color (not transparent)
|
||||||
|
const navBtns = page.locator('.nav-btn');
|
||||||
|
const count = await navBtns.count();
|
||||||
|
expect(count).toBeGreaterThan(0);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const color = await navBtns.nth(i).evaluate(el => getComputedStyle(el).color);
|
||||||
|
expect(color).not.toBe('rgba(0, 0, 0, 0)');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
Loading…
Add table
Reference in a new issue