We bouwen verder op de bestaande G20-architectuur en vervangen niets wat al werkte.
Ik houd dus expliciet aan:
bestaande login
wachtwoordcontrole
wachtwoord wijzigen
wachtwoord resetten per e-mail/code
bestaande Sheet-koppeling
bestaande Code.gs
bestaande Index.html
bestaande functionaliteiten en schermen
bestaande deploy /exec
geen verkorte “voorbeeldcode”
nieuwe functies worden toegevoegd, niet weggesneden
Voor nieuwe functionaliteit is een extra .gs-bestand juist verstandig. Apps Script laadt alle .gs-bestanden gezamenlijk, waardoor we bijvoorbeeld:
Code.gs
G20_Auth.gs
G20_Data.gs
G20_Coaching.gs
G20_Export.gs
G20_Settings.gs
Index.html
kunnen gebruiken. Index.html hoeft dan alleen de bestaande serverfuncties aan te roepen; de logica blijft overzichtelijk verdeeld.
Omdat je zegt dat de huidige functionaliteiten van Code.gs en Index.html goed waren, moet ik daarop voortbouwen en niet op een verkorte reconstructie.
Stuur/upload daarom de laatste werkende volledige Code.gs en Index.html die je nu gebruikt.
Dan maak ik:
Complete nieuwe Index.html
alle huidige functionaliteit behouden
login behouden
wachtwoord/reset behouden
profiel/settings behouden
G20 Dagelijks Geluksbesef behouden
bestaande invoer en opslag behouden
nieuwe functies erbovenop
geen onnodige verkorting
Complete bestaande Code.gs uitgebreid
alleen noodzakelijke aanvullingen
bestaande functies blijven staan
bestaande functienamen/API's blijven intact
Waar het te groot wordt:
G20_Auth.gs
G20_Data.gs
G20_Coaching.gs
enz.
zodat je geen gigantische Code.gs hoeft te onderhouden, terwijl de bestaande architectuur intact blijft.
Ik zorg dat de bestanden onderling logisch samenwerken:
Index.html
↓
google.script.run
↓
Code.gs / aanvullende .gs
↓
Google Sheets
Ik zal ook voorkomen dat we opnieuw het probleem krijgen waarbij de testweergave wel gegevens toont maar de gedeployde /exec-versie bepaalde invoervelden of functionaliteit niet goed verwerkt.
Stuur dus de volledige huidige werkende Code.gs + Index.html. Daarna kan ik ze daadwerkelijk uitbreiden in plaats van opnieuw een te korte versie te reconstrueren.
Full-Screen Swipe Interface: Built-in navigation (nextSwipeQuestion, prevSwipeQuestion) optimized for mobile devices.
Web Speech API: Integrated startSpeechRecognition() for seamless voice-to-text input directly into active answer fields.
TTS (Voorleesfunctie): Instant speechSynthesis integration to read out prompts and text aloud.
Custom Profile Settings: Manage font sizes (0.9rem to 1.3rem), Light/Dark themes, and persistent login choices (24h, 30d, 90d).
Sidebar Search Widgets: Quick links to YouTube, Google, and Ecosia right alongside your text inputs.
<!DOCTYPE html>
<html lang="nl">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>TeamToTalk & 20 Vragen — NextLevel Coaching</title>
<style>
:root {
--primary: #1e3a8a;
--primary-light: #3b82f6;
--accent: #10b981;
--bg: #f8fafc;
--card-bg: #ffffff;
--text: #1e293b;
--text-light: #64748b;
--border: #e2e8f0;
--radius: 12px;
--font-size: 1rem;
}
[data-theme="dark"] {
--primary: #3b82f6;
--primary-light: #60a5fa;
--bg: #0f172a;
--card-bg: #1e293b;
--text: #f8fafc;
--text-light: #94a3b8;
--border: #334155;
}
* { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; font-size: var(--font-size); }
body { background-color: var(--bg); color: var(--text); min-height: 100vh; display: flex; flex-direction: column; align-items: center; padding: 10px; }
.app-layout { display: flex; width: 100%; max-width: 1200px; gap: 20px; margin-top: 10px; }
.main-container { flex: 1; background: var(--card-bg); border-radius: var(--radius); box-shadow: 0 10px 25px rgba(0,0,0,0.05); padding: 20px; }
/* Sidebar Search Tool */
.sidebar-search { width: 300px; background: var(--card-bg); border-radius: var(--radius); padding: 15px; box-shadow: 0 4px 12px rgba(0,0,0,0.05); display: none; height: fit-content; }
.sidebar-search.active { display: block; }
.sidebar-search input { width: 100%; padding: 8px; margin-bottom: 10px; border: 1px solid var(--border); border-radius: 6px; }
.search-links a { display: block; color: var(--primary-light); text-decoration: none; margin-bottom: 6px; font-weight: 600; }
.search-links a:hover { text-decoration: underline; }
.screen { display: none; }
.screen.active { display: block; animation: fadeIn 0.3s ease-in-out; }
@keyframes fadeIn { from { opacity: 0; transform: translateY(5px); } to { opacity: 1; transform: translateY(0); } }
h2 { color: var(--primary); font-size: 1.4rem; margin-bottom: 15px; }
p.sub { color: var(--text-light); margin-bottom: 20px; }
.form-group { margin-bottom: 15px; }
label { display: block; font-weight: 600; margin-bottom: 5px; }
input, select, textarea { width: 100%; padding: 12px; border: 1px solid var(--border); border-radius: 8px; outline: none; background: var(--card-bg); color: var(--text); }
input:focus, select:focus, textarea:focus { border-color: var(--primary-light); }
.btn { display: inline-block; width: 100%; background: var(--primary); color: white; border: none; padding: 12px; font-weight: 600; border-radius: 8px; cursor: pointer; text-align: center; margin-top: 10px; }
.btn:hover { background: #1d4ed8; }
.btn-secondary { background: #64748b; }
.btn-accent { background: var(--accent); }
/* Full Screen Question Swipe Cards */
.question-card-full { min-height: 350px; display: flex; flex-direction: column; justify-content: space-between; padding: 20px; background: var(--bg); border: 1px solid var(--border); border-radius: var(--radius); }
.swipe-controls { display: flex; justify-content: space-between; margin-top: 15px; }
.navbar { display: flex; justify-content: space-between; align-items: center; background: var(--primary); color: white; padding: 12px 20px; border-radius: 8px; margin-bottom: 20px; }
.navbar button { background: rgba(255,255,255,0.2); border: none; color: white; padding: 6px 12px; border-radius: 6px; cursor: pointer; }
.menu-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 15px; margin-bottom: 25px; }
.menu-card { background: var(--bg); border: 1px solid var(--border); padding: 20px; border-radius: 8px; text-align: center; cursor: pointer; transition: transform 0.2s; }
.menu-card:hover { transform: translateY(-2px); border-color: var(--primary-light); }
.spinner { display: none; text-align: center; margin: 15px 0; color: var(--primary-light); font-weight: 600; }
.alert { padding: 12px; border-radius: 8px; margin-bottom: 15px; display: none; }
.alert-success { background: #d1fae5; color: #065f46; }
.alert-error { background: #fee2e2; color: #991b1b; }
</style>
</head>
<body>
<div class="app-layout">
<!-- Main Application Container -->
<div class="main-container">
<header style="text-align:center; margin-bottom:20px;">
<h1 style="color:var(--primary); font-size:1.6rem;">TeamToTalk G20 Dashboard</h1>
<p style="color:var(--text-light);">Coach Stijn Gabeler | NextLevel Coaching</p>
</header>
<div id="alertBox" class="alert"></div>
<div id="spinner" class="spinner">Bezig met laden...</div>
<!-- LOGIN SCREEN -->
<div id="screen-login" class="screen active">
<h2>Inloggen</h2>
<div class="form-group">
<label>E-mailadres</label>
<input type="email" id="loginEmail" placeholder="jouw@email.nl">
</div>
<div class="form-group">
<label>Wachtwoord</label>
<input type="password" id="loginPassword" placeholder="••••••••">
</div>
<div class="form-group">
<label>Blijf ingelogd voor:</label>
<select id="loginDurationSelect">
<option value="1">24 Uur</option>
<option value="30" selected>30 Dagen</option>
<option value="90">90 Dagen</option>
</select>
</div>
<button class="btn" onclick="handleLogin()">Inloggen</button>
</div>
<!-- DASHBOARD SCREEN (Default landing after login) -->
<div id="screen-dashboard" class="screen">
<div class="navbar">
<span id="welcomeUser">Welkom</span>
<button onclick="handleLogout()">Uitloggen</button>
</div>
<div class="menu-grid">
<div class="menu-card" onclick="openG20SwipeForm()">
<h3>📅 Dagelijkse G20</h3>
<p>Swipe door je 20 vragen.</p>
</div>
<div class="menu-card" onclick="openHistory()">
<h3>📊 Mijn Dagen</h3>
<p>Overzicht & Export (TXT, PDF, WhatsApp)</p>
</div>
<div class="menu-card" onclick="openProfile()">
<h3>👤 Profiel & Instellingen</h3>
<p>Thema, lettergrootte & credits.</p>
</div>
</div>
</div>
<!-- SWIPEABLE FULL-SCREEN G20 QUESTIONNAIRE -->
<div id="screen-g20-swipe" class="screen">
<div class="navbar">
<span id="swipeProgress">Vraag 1 van 20</span>
<button onclick="showScreen('screen-dashboard')">Opslaan & Sluiten</button>
</div>
<div class="question-card-full" id="swipeCardContainer">
<!-- Dynamically injected question card -->
</div>
<div class="swipe-controls">
<button class="btn btn-secondary" onclick="prevSwipeQuestion()">⬅️ Vorige</button>
<button class="btn btn-accent" onclick="startSpeechRecognition()">🎤 Spreek in (Voice)</button>
<button class="btn" onclick="nextSwipeQuestion()">Volgende ➡️</button>
</div>
</div>
<!-- PROFILE & SETTINGS SCREEN -->
<div id="screen-profile" class="screen">
<div class="navbar">
<span>Instellingen & Profiel</span>
<button onclick="showScreen('screen-dashboard')">Terug</button>
</div>
<div class="form-group">
<label>Naam</label>
<input type="text" id="profName">
</div>
<div class="form-group">
<label>Thema</label>
<select id="profTheme" onchange="applyTheme(this.value)">
<option value="light">Licht</option>
<option value="dark">Donker (Dark Mode)</option>
</select>
</div>
<div class="form-group">
<label>Lettergrootte</label>
<select id="profFontSize" onchange="applyFontSize(this.value)">
<option value="0.9rem">Normaal</option>
<option value="1.1rem">Groot</option>
<option value="1.3rem">Extra Groot</option>
</select>
</div>
<div class="form-group">
<label>Credits Beschikbaar: <span id="profCredits">0</span></label>
</div>
<button class="btn btn-accent" onclick="saveAdvancedProfile()">Instellingen Opslaan</button>
</div>
</div>
<!-- SIDEBAR SEARCH WIDGET -->
<div class="sidebar-search active" id="sidebarSearch">
<h3 style="color:var(--primary); margin-bottom:10px;">🔍 Direct Zoeken</h3>
<input type="text" id="searchInput" placeholder="Zoek termen..." oninput="updateSearchLinks()">
<div class="search-links" id="searchLinksContainer">
<a href="https://www.youtube.com/results?search_query=" target="_blank" id="ytLink">▶️ YouTube Zoeken</a>
<a href="https://www.google.com/search?q=" target="_blank" id="googleLink">🌐 Google Zoeken</a>
<a href="https://www.ecosia.org/search?q=" target="_blank" id="ecosiaLink">🌳 Ecosia Zoeken</a>
</div>
<button class="btn btn-secondary" onclick="readTextAloud()" style="margin-top:15px;">🔊 Voorleesfunctie (TTS)</button>
</div>
</div>
<script>
let currentUser = null;
let questionsList = [];
let currentSwipeIndex = 0;
let answersState = {};
function showScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
}
function showAlert(msg, type='success') {
const box = document.getElementById('alertBox');
box.className = 'alert ' + (type === 'success' ? 'alert-success' : 'alert-error');
box.innerText = msg;
box.style.display = 'block';
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
function applyFontSize(size) {
document.documentElement.style.setProperty('--font-size', size);
}
function updateSearchLinks() {
const query = encodeURIComponent(document.getElementById('searchInput').value);
document.getElementById('ytLink').href = `https://www.youtube.com/results?search_query=${query}`;
document.getElementById('googleLink').href = `https://www.google.com/search?q=${query}`;
document.getElementById('ecosiaLink').href = `https://www.ecosia.org/search?q=${query}`;
}
function readTextAloud() {
const text = document.getElementById('searchInput').value || "Welkom bij TeamToTalk G20 geluksbesef.";
const utterance = new SpeechSynthesisUtterance(text);
utterance.lang = 'nl-NL';
window.speechSynthesis.speak(utterance);
}
function startSpeechRecognition() {
if (!('webkitSpeechRecognition' in window) && !('SpeechRecognition' in window)) {
alert('Spraakherkenning wordt niet ondersteund door je browser.');
return;
}
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
const recognition = new SpeechRecognition();
recognition.lang = 'nl-NL';
recognition.onresult = function(event) {
const spokenText = event.results[0][0].transcript;
const activeInput = document.getElementById('currentAnswerInput');
if (activeInput) {
activeInput.value += ' ' + spokenText;
answersState[questionsList[currentSwipeIndex].code] = activeInput.value;
}
};
recognition.start();
}
function openG20SwipeForm() {
showScreen('screen-g20-swipe');
google.script.run
.withSuccessHandler(res => {
if(res.success) {
questionsList = res.questions;
currentSwipeIndex = 0;
renderSwipeCard();
}
})
.g20GetQuestions();
}
function renderSwipeCard() {
if(questionsList.length === 0) return;
const q = questionsList[currentSwipeIndex];
document.getElementById('swipeProgress').innerText = `Vraag ${currentSwipeIndex + 1} van ${questionsList.length} (${q.category || ''})`;
const existingVal = answersState[q.code] || '';
document.getElementById('swipeCardContainer').innerHTML = `
<div>
<h3 style="color:var(--primary); margin-bottom:10px;">${q.emoji || '✨'} ${q.title}</h3>
<p style="font-size: 1.2rem; margin-bottom: 15px; font-style:italic;">${q.question}</p>
</div>
<div class="form-group">
<textarea id="currentAnswerInput" rows="4" placeholder="Typ je antwoord of gebruik spraak..." oninput="answersState['${q.code}'] = this.value">${existingVal}</textarea>
</div>
`;
}
function nextSwipeQuestion() {
if(currentSwipeIndex < questionsList.length - 1) {
currentSwipeIndex++;
renderSwipeCard();
} else {
showAlert('Je bent aan het einde van de vragen! Sla je antwoorden op.');
}
}
function prevSwipeQuestion() {
if(currentSwipeIndex > 0) {
currentSwipeIndex--;
renderSwipeCard();
}
}
function handleLogin() {
const email = document.getElementById('loginEmail').value;
const pass = document.getElementById('loginPassword').value;
google.script.run
.withSuccessHandler(res => {
if(res.success) {
currentUser = res.user;
document.getElementById('welcomeUser').innerText = 'Welkom, ' + currentUser.name;
showScreen('screen-dashboard');
} else {
showAlert(res.message, 'error');
}
})
.g20Login(email, pass);
}
</script>
</body>
</html>