From 6efbbdac489fbe4bb4d22c47f9157d5f9d6ee52a Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Jan 2026 13:33:01 +0000 Subject: [PATCH] Add editable user journey feature with steps, inputs and results This commit introduces a new User Journey page that allows users to: - Create and manage custom typography journeys - Add/edit/delete steps with inputs and results - Reorder steps using move up/down controls - Export/import journeys as JSON for sharing - Auto-save journeys to localStorage The feature includes: - journey.php: New page with complete journey editor interface - css/journey.css: Comprehensive styling matching site design - js/journey.js: Full interactive functionality with localStorage support - Updated navigation in index.php and toc.php to include journey link Each step captures a title, input description, and result outcome, making it easy to document and track typography design decisions throughout a project workflow. https://claude.ai/code/session_01RTPyL13uNHQxxx57TmyB6a --- css/journey.css | 241 ++++++++++++++++++++++++++++++++ index.php | 1 + journey.php | 64 +++++++++ js/journey.js | 359 ++++++++++++++++++++++++++++++++++++++++++++++++ toc.php | 1 + 5 files changed, 666 insertions(+) create mode 100644 css/journey.css create mode 100644 journey.php create mode 100644 js/journey.js diff --git a/css/journey.css b/css/journey.css new file mode 100644 index 0000000..97a918f --- /dev/null +++ b/css/journey.css @@ -0,0 +1,241 @@ +/* Journey Page Styles */ + +#journey-controls { + padding: 0 11.8%; + margin: 2em 0; + display: flex; + gap: 0.5em; + flex-wrap: wrap; +} + +.btn-primary, +.btn-secondary { + font-family: 'premiera book', georgia, serif; + font-size: 1em; + padding: 0.5em 1em; + border: 1px solid #8b7355; + background: #fff; + color: #000; + cursor: pointer; + transition: all 0.2s ease; +} + +.btn-primary { + background: #8b7355; + color: #fff; +} + +.btn-primary:hover { + background: #6d5940; +} + +.btn-secondary:hover { + background: #f5f5f0; +} + +#journey-container { + padding: 0; + margin: 2em 0; +} + +.empty-state { + padding: 0 11.8%; + color: #666; + font-style: italic; + margin: 2em 0; +} + +.journey-step { + padding: 1.5em 11.8%; + margin: 1em 0; + border-top: 1px solid #d4c4a8; + position: relative; +} + +.journey-step:first-child { + border-top: 2px solid #8b7355; +} + +.step-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1em; +} + +.step-number { + font-family: 'premiera bold', georgia, serif; + font-weight: bold; + font-size: 1.2em; + color: #8b7355; +} + +.step-controls { + display: flex; + gap: 0.5em; +} + +.btn-icon { + background: none; + border: none; + cursor: pointer; + font-size: 1.2em; + padding: 0.25em 0.5em; + color: #8b7355; + transition: color 0.2s ease; +} + +.btn-icon:hover { + color: #6d5940; +} + +.step-field { + margin: 1em 0; +} + +.step-field label { + display: block; + font-family: 'premiera bold', georgia, serif; + font-weight: bold; + margin-bottom: 0.25em; + font-size: 0.9em; + color: #666; +} + +.step-field input, +.step-field textarea { + width: 100%; + font-family: 'premiera book', georgia, serif; + font-size: 1em; + line-height: 1.375em; + padding: 0.5em; + border: 1px solid #d4c4a8; + background: #fff; + box-sizing: border-box; + transition: border-color 0.2s ease; +} + +.step-field input:focus, +.step-field textarea:focus { + outline: none; + border-color: #8b7355; +} + +.step-field textarea { + min-height: 4em; + resize: vertical; +} + +.step-title-input { + font-family: 'premiera bold', georgia, serif; + font-weight: bold; + font-size: 1.1em; +} + +/* Move up/down buttons */ +.btn-move { + background: none; + border: 1px solid #d4c4a8; + cursor: pointer; + padding: 0.25em 0.5em; + font-size: 0.9em; + color: #8b7355; + transition: all 0.2s ease; +} + +.btn-move:hover { + background: #f5f5f0; + border-color: #8b7355; +} + +/* Import/Export Modal */ +.modal { + display: none; + position: fixed; + z-index: 1000; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.5); + align-items: center; + justify-content: center; +} + +.modal.active { + display: flex; +} + +.modal-content { + background-color: #fff8e5; + padding: 2em; + border: 2px solid #8b7355; + max-width: 600px; + width: 90%; + max-height: 80vh; + overflow-y: auto; +} + +.modal-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1em; + padding-bottom: 0.5em; + border-bottom: 1px solid #d4c4a8; +} + +.modal-header h2 { + margin: 0; + font-size: 1.5em; +} + +.modal-close { + background: none; + border: none; + font-size: 1.5em; + cursor: pointer; + color: #8b7355; + padding: 0; + width: 1.5em; + height: 1.5em; + line-height: 1; +} + +.modal-close:hover { + color: #6d5940; +} + +.modal-body textarea { + width: 100%; + min-height: 200px; + font-family: monospace; + font-size: 0.9em; + padding: 0.5em; + border: 1px solid #d4c4a8; + background: #fff; + box-sizing: border-box; +} + +.modal-footer { + margin-top: 1em; + display: flex; + gap: 0.5em; + justify-content: flex-end; +} + +/* Responsive adjustments */ +@media screen and (max-width: 768px) { + #journey-controls { + flex-direction: column; + } + + .btn-primary, + .btn-secondary { + width: 100%; + } + + .step-controls { + flex-wrap: wrap; + } +} diff --git a/index.php b/index.php index c31cce4..33e9b99 100644 --- a/index.php +++ b/index.php @@ -101,6 +101,7 @@ diff --git a/journey.php b/journey.php new file mode 100644 index 0000000..71e73a4 --- /dev/null +++ b/journey.php @@ -0,0 +1,64 @@ + + + + +User Journey | The Elements of Typographic Style Applied to the Web + + + + + + + +
+

User Journey

+ +

Create your own typographic journey by adding steps, inputs, and results. Each step represents a stage in your design process, helping you document and track your typography decisions.

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

No steps yet. Click "Add Step" to begin your journey.

+
+ +
+ +
+

About User Journeys

+

A user journey helps you:

+ + +

How to Use

+

Add Step: Create a new step in your journey

+

Edit: Click on any field to edit it

+

Delete: Remove steps you no longer need

+

Export/Import: Save and share your journey

+ + +
+ + + + + + diff --git a/js/journey.js b/js/journey.js new file mode 100644 index 0000000..57885fc --- /dev/null +++ b/js/journey.js @@ -0,0 +1,359 @@ +// User Journey Management +(function() { + 'use strict'; + + let journey = { + steps: [] + }; + + const STORAGE_KEY = 'webtypography_journey'; + + // DOM Elements + const journeyContainer = document.getElementById('journey-container'); + const emptyState = document.getElementById('journey-empty'); + const addStepBtn = document.getElementById('add-step'); + const clearJourneyBtn = document.getElementById('clear-journey'); + const exportJourneyBtn = document.getElementById('export-journey'); + const importJourneyBtn = document.getElementById('import-journey'); + + // Initialize + function init() { + loadJourney(); + renderJourney(); + attachEventListeners(); + } + + // Load journey from localStorage + function loadJourney() { + try { + const saved = localStorage.getItem(STORAGE_KEY); + if (saved) { + journey = JSON.parse(saved); + if (!Array.isArray(journey.steps)) { + journey.steps = []; + } + } + } catch (e) { + console.error('Error loading journey:', e); + journey.steps = []; + } + } + + // Save journey to localStorage + function saveJourney() { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(journey)); + } catch (e) { + console.error('Error saving journey:', e); + alert('Error saving journey. Your browser storage may be full.'); + } + } + + // Attach event listeners + function attachEventListeners() { + addStepBtn.addEventListener('click', addStep); + clearJourneyBtn.addEventListener('click', clearJourney); + exportJourneyBtn.addEventListener('click', exportJourney); + importJourneyBtn.addEventListener('click', importJourney); + } + + // Add a new step + function addStep() { + const newStep = { + id: Date.now(), + title: 'New Step', + input: '', + result: '' + }; + journey.steps.push(newStep); + saveJourney(); + renderJourney(); + } + + // Remove a step + function removeStep(id) { + if (confirm('Are you sure you want to delete this step?')) { + journey.steps = journey.steps.filter(step => step.id !== id); + saveJourney(); + renderJourney(); + } + } + + // Move step up + function moveStepUp(id) { + const index = journey.steps.findIndex(step => step.id === id); + if (index > 0) { + [journey.steps[index - 1], journey.steps[index]] = + [journey.steps[index], journey.steps[index - 1]]; + saveJourney(); + renderJourney(); + } + } + + // Move step down + function moveStepDown(id) { + const index = journey.steps.findIndex(step => step.id === id); + if (index < journey.steps.length - 1) { + [journey.steps[index], journey.steps[index + 1]] = + [journey.steps[index + 1], journey.steps[index]]; + saveJourney(); + renderJourney(); + } + } + + // Update step field + function updateStep(id, field, value) { + const step = journey.steps.find(step => step.id === id); + if (step) { + step[field] = value; + saveJourney(); + } + } + + // Clear entire journey + function clearJourney() { + if (confirm('Are you sure you want to clear the entire journey? This cannot be undone.')) { + journey.steps = []; + saveJourney(); + renderJourney(); + } + } + + // Export journey + function exportJourney() { + const modal = createModal('Export Journey', + 'Copy this JSON data to save your journey:', + JSON.stringify(journey, null, 2), + true + ); + document.body.appendChild(modal); + modal.classList.add('active'); + + // Select all text in textarea + const textarea = modal.querySelector('textarea'); + textarea.select(); + } + + // Import journey + function importJourney() { + const modal = createModal('Import Journey', + 'Paste your journey JSON data below:', + '', + false, + function(textarea) { + try { + const imported = JSON.parse(textarea.value); + if (imported.steps && Array.isArray(imported.steps)) { + if (confirm('This will replace your current journey. Continue?')) { + journey = imported; + saveJourney(); + renderJourney(); + closeModal(modal); + } + } else { + alert('Invalid journey data. Please check the format.'); + } + } catch (e) { + alert('Error parsing JSON data. Please check the format.'); + } + } + ); + document.body.appendChild(modal); + modal.classList.add('active'); + } + + // Create modal + function createModal(title, message, content, readonly, onConfirm) { + const modal = document.createElement('div'); + modal.className = 'modal'; + + const modalContent = document.createElement('div'); + modalContent.className = 'modal-content'; + + const header = document.createElement('div'); + header.className = 'modal-header'; + header.innerHTML = ` +

${title}

+ + `; + + const body = document.createElement('div'); + body.className = 'modal-body'; + body.innerHTML = ` +

${message}

+ + `; + + const footer = document.createElement('div'); + footer.className = 'modal-footer'; + + if (onConfirm) { + const confirmBtn = document.createElement('button'); + confirmBtn.className = 'btn-primary'; + confirmBtn.textContent = 'Import'; + confirmBtn.addEventListener('click', function() { + onConfirm(body.querySelector('textarea')); + }); + footer.appendChild(confirmBtn); + } + + const closeBtn = document.createElement('button'); + closeBtn.className = 'btn-secondary'; + closeBtn.textContent = 'Close'; + closeBtn.addEventListener('click', function() { + closeModal(modal); + }); + footer.appendChild(closeBtn); + + modalContent.appendChild(header); + modalContent.appendChild(body); + modalContent.appendChild(footer); + modal.appendChild(modalContent); + + // Close on background click + modal.addEventListener('click', function(e) { + if (e.target === modal) { + closeModal(modal); + } + }); + + // Close button + header.querySelector('.modal-close').addEventListener('click', function() { + closeModal(modal); + }); + + return modal; + } + + // Close modal + function closeModal(modal) { + modal.classList.remove('active'); + setTimeout(() => modal.remove(), 300); + } + + // Render journey + function renderJourney() { + if (journey.steps.length === 0) { + journeyContainer.innerHTML = ''; + emptyState.style.display = 'block'; + return; + } + + emptyState.style.display = 'none'; + journeyContainer.innerHTML = ''; + + journey.steps.forEach((step, index) => { + const stepElement = createStepElement(step, index); + journeyContainer.appendChild(stepElement); + }); + } + + // Create step element + function createStepElement(step, index) { + const stepDiv = document.createElement('div'); + stepDiv.className = 'journey-step'; + stepDiv.setAttribute('data-step-id', step.id); + + const header = document.createElement('div'); + header.className = 'step-header'; + + const stepNumber = document.createElement('div'); + stepNumber.className = 'step-number'; + stepNumber.textContent = `Step ${index + 1}`; + + const controls = document.createElement('div'); + controls.className = 'step-controls'; + + // Move up button + if (index > 0) { + const moveUpBtn = document.createElement('button'); + moveUpBtn.className = 'btn-move'; + moveUpBtn.textContent = '↑'; + moveUpBtn.title = 'Move up'; + moveUpBtn.addEventListener('click', () => moveStepUp(step.id)); + controls.appendChild(moveUpBtn); + } + + // Move down button + if (index < journey.steps.length - 1) { + const moveDownBtn = document.createElement('button'); + moveDownBtn.className = 'btn-move'; + moveDownBtn.textContent = '↓'; + moveDownBtn.title = 'Move down'; + moveDownBtn.addEventListener('click', () => moveStepDown(step.id)); + controls.appendChild(moveDownBtn); + } + + // Delete button + const deleteBtn = document.createElement('button'); + deleteBtn.className = 'btn-icon'; + deleteBtn.innerHTML = '×'; + deleteBtn.title = 'Delete step'; + deleteBtn.addEventListener('click', () => removeStep(step.id)); + controls.appendChild(deleteBtn); + + header.appendChild(stepNumber); + header.appendChild(controls); + + // Title field + const titleField = createField('Title', step.title, 'text', 'step-title-input'); + titleField.querySelector('input').addEventListener('input', (e) => { + updateStep(step.id, 'title', e.target.value); + }); + + // Input field + const inputField = createField('Input', step.input, 'textarea'); + inputField.querySelector('textarea').addEventListener('input', (e) => { + updateStep(step.id, 'input', e.target.value); + }); + + // Result field + const resultField = createField('Result', step.result, 'textarea'); + resultField.querySelector('textarea').addEventListener('input', (e) => { + updateStep(step.id, 'result', e.target.value); + }); + + stepDiv.appendChild(header); + stepDiv.appendChild(titleField); + stepDiv.appendChild(inputField); + stepDiv.appendChild(resultField); + + return stepDiv; + } + + // Create field element + function createField(label, value, type, className) { + const field = document.createElement('div'); + field.className = 'step-field'; + + const labelElement = document.createElement('label'); + labelElement.textContent = label; + field.appendChild(labelElement); + + if (type === 'textarea') { + const textarea = document.createElement('textarea'); + textarea.value = value; + textarea.placeholder = `Enter ${label.toLowerCase()}...`; + field.appendChild(textarea); + } else { + const input = document.createElement('input'); + input.type = type; + input.value = value; + input.placeholder = `Enter ${label.toLowerCase()}...`; + if (className) { + input.className = className; + } + field.appendChild(input); + } + + return field; + } + + // Initialize on DOM ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + +})(); diff --git a/toc.php b/toc.php index c35ffcd..fc4d9dc 100644 --- a/toc.php +++ b/toc.php @@ -56,6 +56,7 @@
  • Reference

    1. Bibliography
    2. +
    3. User Journey