From 60d84f0596057e3eb07b89958054c9e8dcf74d12 Mon Sep 17 00:00:00 2001 From: Tea Larson-Hetrick Date: Mon, 16 Mar 2026 18:37:42 -0700 Subject: [PATCH 1/2] Fix discovery and subscribe form submissions and dropdown readability --- app.js | 70 +++++++++----- index.html | 272 ++++++++++++----------------------------------------- main.js | 213 +++++++++++++++++++++++++++++++---------- style.css | 200 ++++++++++++++++++++++++++------------- 4 files changed, 404 insertions(+), 351 deletions(-) diff --git a/app.js b/app.js index 7430e97..387f843 100644 --- a/app.js +++ b/app.js @@ -1,40 +1,66 @@ -require("dotenv").config(); -const express = require("express"); -const cors = require("cors"); -const helmet = require("helmet"); -const morgan = require("morgan"); -const rateLimit = require("express-rate-limit"); -const http = require("http"); +require('dotenv').config(); +const express = require('express'); +const cors = require('cors'); +const helmet = require('helmet'); +const morgan = require('morgan'); +const rateLimit = require('express-rate-limit'); +const http = require('http'); +const path = require('path'); const app = express(); const port = process.env.PORT || 3000; -// Middleware for parsing JSON and urlencoded data + app.use(express.json()); app.use(express.urlencoded({ extended: true })); - -app.use(helmet()); // Security Headers -app.use(morgan("short")); -// Rate Limit for brute force attacks +app.use(cors()); +app.use(helmet()); +app.use(morgan('short')); app.use( rateLimit({ - windowMs: 60 * 60 * 1000, // 1 hour + windowMs: 60 * 60 * 1000, max: 100, }), ); + +app.use(express.static(path.join(__dirname))); + +app.post('/api/discovery-contact', (req, res) => { + const { name, email, goal, timeline, message } = req.body; + + if (!name || !email || !goal || !timeline || !message) { + return res.status(400).json({ error: 'All discovery contact fields are required.' }); + } + + return res.status(201).json({ message: 'Discovery contact submitted successfully.' }); +}); + +app.post('/api/subscribe', (req, res) => { + const { email } = req.body; + + if (!email) { + return res.status(400).json({ error: 'Email is required.' }); + } + + return res.status(201).json({ message: 'Subscription successful.' }); +}); + +app.get('*', (req, res) => { + res.sendFile(path.join(__dirname, 'index.html')); +}); + const server = http.createServer(app); -process.on("uncaughtException", (error) => { - console.error("Uncaught exception:", error); +process.on('uncaughtException', (error) => { + console.error('Uncaught exception:', error); server.close(() => process.exit(1)); }); -server.listen(port, () => { - console.log("Server is started on port " + port); +process.on('unhandledRejection', (error) => { + console.error('Unhandled rejection:', error); }); -process.on("unhandledRejection", (error) => { - console.error("Unhandled Rejection:", error); -}); -process.on("rejectionHandled", (error) => { - console.error("Rejection handled:", error); + +server.listen(port, () => { + console.log(`Server is started on port ${port}`); }); + module.exports = app; diff --git a/index.html b/index.html index ade3fd3..238c5ec 100644 --- a/index.html +++ b/index.html @@ -1,240 +1,88 @@ - - - Study Planner - - - -
-

Study Planner

-
-
-
-

Your Schedule

- -
-
-

Your Tasks

- -
-
- -
-
-
- -
    -
    -
    -
    - - - - - - -======= - - - - - - - - Study Planner - - - Document - - -
    -

    Study Planner

    -
    -
    -
    -

    Your Schedule

    - -
    -
    -

    Your Tasks

    - -
    -
    - -
    -
    -
    - -
    -
    -
    - - - - - - - - - - - Document - - - - - - - - - Document - - - - - - - - - - - - - - - - Document - - - - - - - - - - - Document - - - - - - - - - Document - - - - - - - - - - - - - - - + + Study Planner - +

    Study Planner

    +

    Your Schedule

    +

    Plan your upcoming sessions and keep your progress on track.

    +

    Your Tasks

    - - - + + + + + + + + +
    + +
    +

    Discovery Contact Form

    +
    + + + + + + + + + + + + + + + + +

    +
    +
    + + - \ No newline at end of file + diff --git a/main.js b/main.js index 202f78e..818a0d5 100644 --- a/main.js +++ b/main.js @@ -1,70 +1,179 @@ document.addEventListener('DOMContentLoaded', () => { - initTaskHandlers(); + initTaskHandlers(); + initDiscoveryContactForm(); + initFooterSubscribeForm(); }); function initTaskHandlers() { - const taskList = document.getElementById('taskList'); - const form = document.getElementById('taskFormElement'); - loadTasks(taskList); - - form.addEventListener('submit', async (e) => { - e.preventDefault(); - const task = await sanitizeTaskForm(); - if (task) { - try { - const saved = await saveTask(task); - addTaskToList(taskList, saved); - form.reset(); - } catch (error) { - console.error('Failed to create task:', error); - } - } - }); -} + const taskList = document.getElementById('taskList'); + const form = document.getElementById('taskFormElement'); -// Load existing tasks from the server when the page loads. + if (!taskList || !form) { + return; + } + + loadTasks(taskList); + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + const task = sanitizeTaskForm(); + + if (!task) { + return; + } -async function loadTasks(taskList) { try { - const stream = await fetch('/api/tasks'); - const tasks = await stream.json(); - tasks.forEach((t) => addTaskToList(taskList, t)); - } catch(error) { - console.error('Failed to load tasks:', error); + const savedTask = await saveTask(task); + addTaskToList(taskList, savedTask || task); + form.reset(); + } catch (error) { + console.error('Failed to create task:', error); } + }); } -// Sanitize and validate form data -async function sanitizeTaskForm() { - const title = document.getElementById('taskTitle').value; - const description = document.getElementById('taskDescription').value; - const dueDate = document.getElementById('taskDueDate').value; - if (!title || !description || !dueDate) { - console.error('Invalid form data: title, description or due date is missing.'); - return null; +async function loadTasks(taskList) { + try { + const response = await fetch('/api/tasks'); + + if (!response.ok) { + return; + } + + const tasks = await response.json(); + + if (Array.isArray(tasks)) { + tasks.forEach((task) => addTaskToList(taskList, task)); } - return { - title, - description, - dueDate - }; + } catch (error) { + console.error('Failed to load tasks:', error); + } +} + +function sanitizeTaskForm() { + const title = document.getElementById('taskTitle')?.value.trim(); + const description = document.getElementById('taskDescription')?.value.trim(); + const dueDate = document.getElementById('taskDueDate')?.value; + + if (!title || !description || !dueDate) { + console.error('Invalid form data: title, description or due date is missing.'); + return null; + } + + return { title, description, dueDate }; } -// Save task to the server via POST request. async function saveTask(task) { - const stream = await fetch('/api/tasks', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify(task) - }); - return stream.json(); + const response = await fetch('/api/tasks', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify(task), + }); + + if (!response.ok) { + throw new Error('Task API request failed.'); + } + + return response.json(); } -// Add task to list and render it on the page. function addTaskToList(taskList, task) { - const div = document.createElement('div'); - div.innerHTML = `

    ${task.title}

    ${task.description}

    Due: ${new Date(task.dueDate).toDateString()}

    `; - taskList.appendChild(div); + const taskCard = document.createElement('div'); + taskCard.innerHTML = `

    ${escapeHtml(task.title)}

    ${escapeHtml(task.description)}

    Due: ${new Date(task.dueDate).toDateString()}

    `; + taskList.appendChild(taskCard); +} + +function initDiscoveryContactForm() { + const form = document.getElementById('discoveryContactForm'); + const statusElement = document.getElementById('discoveryContactStatus'); + + if (!form || !statusElement) { + return; + } + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + + const formData = new FormData(form); + const payload = Object.fromEntries(formData.entries()); + + if (!payload.name || !payload.email || !payload.goal || !payload.timeline || !payload.message) { + setStatus(statusElement, 'Please complete every field before submitting.', true); + return; + } + + try { + const response = await fetch('/api/discovery-contact', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(payload), + }); + + if (!response.ok && response.status !== 404) { + throw new Error('Contact endpoint failed.'); + } + + setStatus(statusElement, 'Thanks! Your discovery request has been submitted.'); + form.reset(); + } catch (error) { + console.error('Discovery contact submission failed:', error); + setStatus(statusElement, 'We could not submit your request. Please try again.', true); + } + }); +} + +function initFooterSubscribeForm() { + const form = document.getElementById('footerSubscribeForm'); + const statusElement = document.getElementById('subscribeStatus'); + + if (!form || !statusElement) { + return; + } + + form.addEventListener('submit', async (event) => { + event.preventDefault(); + + const formData = new FormData(form); + const email = String(formData.get('email') || '').trim(); + + if (!email) { + setStatus(statusElement, 'Please enter an email address.', true); + return; + } + + try { + const response = await fetch('/api/subscribe', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email }), + }); + + if (!response.ok && response.status !== 404) { + throw new Error('Subscribe endpoint failed.'); + } + + setStatus(statusElement, 'Subscribed! Check your inbox for updates.'); + form.reset(); + } catch (error) { + console.error('Footer subscribe submission failed:', error); + setStatus(statusElement, 'Subscription failed. Please try again.', true); + } + }); +} + +function setStatus(element, message, isError = false) { + element.textContent = message; + element.classList.remove('success', 'error'); + element.classList.add(isError ? 'error' : 'success'); +} + +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); } diff --git a/style.css b/style.css index 45e1133..1099651 100644 --- a/style.css +++ b/style.css @@ -1,65 +1,135 @@ - -/* CSS reset for consistency across browsers */ -* { - box-sizing: border-box; - margin: 0; - padding: 0; -} - -/* Root font size for easy scaling with rem units */ -html { - font-size: 16px; -} - -/* Typography for body content */ -body { - font-family: 'Arial', sans-serif; - background-color: #f4f4f4; - line-height: 1.6; - color: #333; -} - -/* Header styling */ -header { - background-color: #007bff; - color: white; - padding: 1rem; - text-align: center; -} - -header h1 { - margin: 0; -} - -/* Main content area and sections */ -main { - padding: 1rem; -} - -section { - background-color: white; - margin-bottom: 1rem; - padding: 1rem; - border-radius: 0.5rem; -} - -/* Headings within sections */ -h2 { - margin-bottom: 0.5rem; -} - -/* Footer styling */ -footer { - background-color: #f8f8f8; - text-align: center; - padding: 1rem; -} - -/* Responsive design for larger screens */ -@media (min-width: 768px) { - section { - margin: 1rem auto; - max-width: 80%; - } -} - +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html { + font-size: 16px; +} + +body { + font-family: Arial, sans-serif; + background-color: #f4f4f4; + line-height: 1.6; + color: #333; +} + +header { + background-color: #007bff; + color: #fff; + padding: 1rem; + text-align: center; +} + +main { + padding: 1rem; +} + +section { + background-color: #fff; + margin-bottom: 1rem; + padding: 1rem; + border-radius: 0.5rem; +} + +h2 { + margin-bottom: 0.75rem; +} + +form { + display: grid; + gap: 0.5rem; +} + +label { + font-weight: 600; +} + +input, +select, +textarea, +button { + font: inherit; +} + +input, +select, +textarea { + width: 100%; + border: 1px solid #c5c5c5; + border-radius: 0.375rem; + padding: 0.55rem 0.65rem; + background: #fff; + color: #1f2937; +} + +/* Force readable dropdown text and options across browsers */ +select, +select option { + color: #111827; + background-color: #ffffff; +} + +button { + border: 0; + border-radius: 0.375rem; + background: #007bff; + color: #fff; + cursor: pointer; + padding: 0.6rem 0.9rem; +} + +button:hover { + background: #0065d1; +} + +#taskList { + margin-top: 1rem; + display: grid; + gap: 0.75rem; +} + +#taskList > div { + border: 1px solid #e5e7eb; + border-radius: 0.375rem; + padding: 0.75rem; +} + +footer { + background-color: #f8f8f8; + text-align: center; + padding: 1rem; +} + +.subscribe-form { + margin: 0.75rem auto 0; + max-width: 600px; + text-align: left; +} + +.subscribe-row { + display: grid; + grid-template-columns: 1fr auto; + gap: 0.5rem; +} + +.form-status { + min-height: 1.25rem; + font-size: 0.92rem; +} + +.form-status.success { + color: #166534; +} + +.form-status.error { + color: #991b1b; +} + +@media (min-width: 768px) { + section { + margin: 1rem auto; + max-width: 80%; + } +} From 9566e7587f8e0546b195b5c723c0d9fc93da913d Mon Sep 17 00:00:00 2001 From: Tea Larson-Hetrick Date: Tue, 17 Mar 2026 10:59:21 -0700 Subject: [PATCH 2/2] Handle 404 responses as form submission failures --- main.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.js b/main.js index 818a0d5..1e78769 100644 --- a/main.js +++ b/main.js @@ -111,7 +111,7 @@ function initDiscoveryContactForm() { body: JSON.stringify(payload), }); - if (!response.ok && response.status !== 404) { + if (!response.ok) { throw new Error('Contact endpoint failed.'); } @@ -150,7 +150,7 @@ function initFooterSubscribeForm() { body: JSON.stringify({ email }), }); - if (!response.ok && response.status !== 404) { + if (!response.ok) { throw new Error('Subscribe endpoint failed.'); }