diff --git a/projects/stopwatch/index.html b/projects/stopwatch/index.html index f786147..8371e51 100644 --- a/projects/stopwatch/index.html +++ b/projects/stopwatch/index.html @@ -1,18 +1,41 @@ - + - - - Stopwatch | Vanilla Verse - + + + Stopwatch + -

Stopwatch

-
- - - -
- +
+

⏱️ Stopwatch

+ +
+ 00:00.000 +
+ +
+ + + +
+ + + +
+ Space Start/Pause • L Lap • R Reset +
+
+ + diff --git a/projects/stopwatch/main.js b/projects/stopwatch/main.js index 0ee5d09..6f84a90 100644 --- a/projects/stopwatch/main.js +++ b/projects/stopwatch/main.js @@ -7,9 +7,4 @@ * Implementation notes: * - Use performance.now() for precise deltas * - requestAnimationFrame for smooth display updates - */ - -document.addEventListener('DOMContentLoaded', () => { - console.log('Stopwatch app ready'); - // TODO: Wire up buttons and timing loop -}); + */ \ No newline at end of file diff --git a/projects/stopwatch/styles.css b/projects/stopwatch/styles.css index 0885a41..f5af81f 100644 --- a/projects/stopwatch/styles.css +++ b/projects/stopwatch/styles.css @@ -1,3 +1,244 @@ +<<<<<<< unit +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif; + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; +} + +.container { + background: rgba(255, 255, 255, 0.95); + border-radius: 24px; + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); + padding: 40px; + max-width: 500px; + width: 100%; +} + +h1 { + text-align: center; + color: #333; + font-size: 28px; + margin-bottom: 30px; + font-weight: 600; +} + +.display { + background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%); + color: #fff; + font-size: 56px; + font-weight: 300; + text-align: center; + padding: 40px 20px; + border-radius: 16px; + margin-bottom: 30px; + font-variant-numeric: tabular-nums; + letter-spacing: 2px; + box-shadow: inset 0 2px 8px rgba(0, 0, 0, 0.2); +} + +.controls { + display: flex; + gap: 12px; + margin-bottom: 30px; + justify-content: center; +} + +button { + flex: 1; + padding: 16px 24px; + font-size: 16px; + font-weight: 600; + border: none; + border-radius: 12px; + cursor: pointer; + transition: all 0.2s ease; + text-transform: uppercase; + letter-spacing: 0.5px; + position: relative; + overflow: hidden; +} + +button:focus { + outline: 3px solid rgba(102, 126, 234, 0.5); + outline-offset: 2px; +} + +button:active { + transform: translateY(1px); +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +#startPauseBtn { + background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); + color: white; +} + +#startPauseBtn:hover:not(:disabled) { + box-shadow: 0 8px 20px rgba(56, 239, 125, 0.4); + transform: translateY(-2px); +} + +#startPauseBtn.paused { + background: linear-gradient(135deg, #ee0979 0%, #ff6a00 100%); +} + +#startPauseBtn.paused:hover:not(:disabled) { + box-shadow: 0 8px 20px rgba(238, 9, 121, 0.4); +} + +#lapBtn { + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; +} + +#lapBtn:hover:not(:disabled) { + box-shadow: 0 8px 20px rgba(118, 75, 162, 0.4); + transform: translateY(-2px); +} + +#resetBtn { + background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%); + color: white; +} + +#resetBtn:hover:not(:disabled) { + box-shadow: 0 8px 20px rgba(245, 87, 108, 0.4); + transform: translateY(-2px); +} + +.laps-container { + max-height: 300px; + overflow-y: auto; + margin-top: 20px; +} + +.laps-container::-webkit-scrollbar { + width: 8px; +} + +.laps-container::-webkit-scrollbar-track { + background: #f1f1f1; + border-radius: 10px; +} + +.laps-container::-webkit-scrollbar-thumb { + background: #888; + border-radius: 10px; +} + +.laps-container::-webkit-scrollbar-thumb:hover { + background: #555; +} + +.laps-header { + font-weight: 600; + color: #333; + margin-bottom: 12px; + font-size: 18px; +} + +.lap-item { + display: flex; + justify-content: space-between; + align-items: center; + padding: 14px 16px; + background: #f8f9fa; + margin-bottom: 8px; + border-radius: 10px; + border-left: 4px solid #667eea; + transition: all 0.2s ease; +} + +.lap-item:hover { + background: #e9ecef; + transform: translateX(4px); +} + +.lap-number { + font-weight: 600; + color: #667eea; + font-size: 16px; +} + +.lap-times { + display: flex; + gap: 20px; + align-items: center; +} + +.lap-time { + font-variant-numeric: tabular-nums; + color: #333; + font-size: 15px; +} + +.lap-delta { + font-variant-numeric: tabular-nums; + color: #666; + font-size: 14px; + font-style: italic; +} + +.keyboard-hint { + text-align: center; + color: #666; + font-size: 13px; + margin-top: 20px; + padding-top: 20px; + border-top: 1px solid #e0e0e0; +} + +.keyboard-hint kbd { + background: #f5f5f5; + padding: 3px 8px; + border-radius: 4px; + border: 1px solid #ccc; + font-family: monospace; + font-size: 12px; +} + +@media (max-width: 500px) { + .container { + padding: 24px; + } + + .display { + font-size: 42px; + padding: 30px 15px; + } + + button { + padding: 14px 18px; + font-size: 14px; + } + + .controls { + flex-wrap: wrap; + } + + button { + flex-basis: calc(50% - 6px); + } + + #resetBtn { + flex-basis: 100%; + } +} +======= /* TODO: Style stopwatch display and controls */ :root { --bg: #0b1220; @@ -15,3 +256,4 @@ body { place-items: center; } #app { width: min(92vw, 600px); } +>>>>>>> main diff --git a/projects/unit-converter/index.html b/projects/unit-converter/index.html index cfb4f47..6154d99 100644 --- a/projects/unit-converter/index.html +++ b/projects/unit-converter/index.html @@ -1,18 +1,50 @@ - + - - - Unit Converter | Vanilla Verse - + + + Unit Converter + + + + -

Unit Converter

-
- - - -
- +
+
+

Unit Converter

+ +
+ +
+ + + +
+ +
+
+ +
+ + +
+
+ +
+ +
+ +
+ +
+ + +
+
+
+
+ + diff --git a/projects/unit-converter/main.js b/projects/unit-converter/main.js index 6a883ab..4365ad5 100644 --- a/projects/unit-converter/main.js +++ b/projects/unit-converter/main.js @@ -1,16 +1,201 @@ -/** - * TODO: Unit converter logic - * Categories: - * - Length: m, km, cm, mm, mi, yd, ft, in - * - Temperature: C, F, K - * - Weight: g, kg, lb, oz - * Implementation: - * - Define conversion maps/functions - * - Convert on input/change events; show precise and rounded values - * Optional: Persist last category/units to localStorage - */ - -document.addEventListener('DOMContentLoaded', () => { - console.log('Unit converter ready'); - // TODO: Build conversion functions and wire to form +// Unit conversion definitions +const units = { + length: { + m: { name: 'Meters', toBase: (v) => v, fromBase: (v) => v }, + km: { name: 'Kilometers', toBase: (v) => v * 1000, fromBase: (v) => v / 1000 }, + mi: { name: 'Miles', toBase: (v) => v * 1609.344, fromBase: (v) => v / 1609.344 }, + ft: { name: 'Feet', toBase: (v) => v * 0.3048, fromBase: (v) => v / 0.3048 } + }, + temperature: { + C: { + name: 'Celsius', + toBase: (v) => v, + fromBase: (v) => v + }, + F: { + name: 'Fahrenheit', + toBase: (v) => (v - 32) * 5/9, + fromBase: (v) => (v * 9/5) + 32 + }, + K: { + name: 'Kelvin', + toBase: (v) => v - 273.15, + fromBase: (v) => v + 273.15 + } + }, + weight: { + g: { name: 'Grams', toBase: (v) => v, fromBase: (v) => v }, + kg: { name: 'Kilograms', toBase: (v) => v * 1000, fromBase: (v) => v / 1000 }, + lb: { name: 'Pounds', toBase: (v) => v * 453.592, fromBase: (v) => v / 453.592 }, + oz: { name: 'Ounces', toBase: (v) => v * 28.3495, fromBase: (v) => v / 28.3495 } + } +}; + +// State management +let currentCategory = 'length'; +let isConverting = false; + +// DOM elements +const input1 = document.getElementById('input1'); +const input2 = document.getElementById('input2'); +const unit1 = document.getElementById('unit1'); +const unit2 = document.getElementById('unit2'); +const swapBtn = document.getElementById('swapBtn'); +const categoryBtns = document.querySelectorAll('.category-btn'); +const themeToggle = document.getElementById('themeToggle'); + +// Load state from localStorage +function loadState() { + const saved = localStorage.getItem('unitConverterState'); + if (saved) { + try { + const state = JSON.parse(saved); + currentCategory = state.category || 'length'; + return state; + } catch (e) { + return null; + } + } + return null; +} + +// Save state to localStorage +function saveState() { + const state = { + category: currentCategory, + unit1: unit1.value, + unit2: unit2.value + }; + localStorage.setItem('unitConverterState', JSON.stringify(state)); +} + +// Load theme from localStorage +function loadTheme() { + const theme = localStorage.getItem('unitConverterTheme') || 'light'; + document.documentElement.setAttribute('data-theme', theme); + themeToggle.textContent = theme === 'dark' ? '☀️' : '🌙'; +} + +// Toggle between light and dark theme +function toggleTheme() { + const currentTheme = document.documentElement.getAttribute('data-theme'); + const newTheme = currentTheme === 'dark' ? 'light' : 'dark'; + document.documentElement.setAttribute('data-theme', newTheme); + localStorage.setItem('unitConverterTheme', newTheme); + themeToggle.textContent = newTheme === 'dark' ? '☀️' : '🌙'; +} + +// Populate unit dropdowns based on current category +function populateUnits() { + const categoryUnits = units[currentCategory]; + const unitKeys = Object.keys(categoryUnits); + + unit1.innerHTML = ''; + unit2.innerHTML = ''; + + unitKeys.forEach(key => { + const option1 = document.createElement('option'); + option1.value = key; + option1.textContent = categoryUnits[key].name; + unit1.appendChild(option1); + + const option2 = document.createElement('option'); + option2.value = key; + option2.textContent = categoryUnits[key].name; + unit2.appendChild(option2); + }); + + const saved = loadState(); + if (saved && saved.category === currentCategory) { + if (unitKeys.includes(saved.unit1)) unit1.value = saved.unit1; + if (unitKeys.includes(saved.unit2)) unit2.value = saved.unit2; + } else { + unit1.value = unitKeys[0]; + unit2.value = unitKeys[1] || unitKeys[0]; + } +} + +// Convert units bi-directionally +function convert(fromInput, toInput) { + if (isConverting) return; + + const value = parseFloat(fromInput.value); + if (isNaN(value) || fromInput.value === '') { + toInput.value = ''; + return; + } + + const fromUnit = fromInput === input1 ? unit1.value : unit2.value; + const toUnit = fromInput === input1 ? unit2.value : unit1.value; + + const categoryUnits = units[currentCategory]; + + // Convert to base unit, then to target unit + const baseValue = categoryUnits[fromUnit].toBase(value); + const result = categoryUnits[toUnit].fromBase(baseValue); + + isConverting = true; + toInput.value = parseFloat(result.toFixed(6)).toString(); + isConverting = false; +} + +// Switch between categories (Length, Temperature, Weight) +function switchCategory(category) { + currentCategory = category; + + categoryBtns.forEach(btn => { + btn.classList.toggle('active', btn.dataset.category === category); + }); + + input1.value = ''; + input2.value = ''; + + populateUnits(); + saveState(); +} + +// Swap units and values +function swapUnits() { + const tempValue = input1.value; + const tempUnit = unit1.value; + + input1.value = input2.value; + unit1.value = unit2.value; + + input2.value = tempValue; + unit2.value = tempUnit; + + saveState(); +} + +// Event listeners +categoryBtns.forEach(btn => { + btn.addEventListener('click', () => { + switchCategory(btn.dataset.category); + }); }); + +input1.addEventListener('input', () => convert(input1, input2)); +input2.addEventListener('input', () => convert(input2, input1)); + +unit1.addEventListener('change', () => { + convert(input1, input2); + saveState(); +}); + +unit2.addEventListener('change', () => { + convert(input1, input2); + saveState(); +}); + +swapBtn.addEventListener('click', swapUnits); +themeToggle.addEventListener('click', toggleTheme); + +// Initialize app +loadTheme(); +const saved = loadState(); +if (saved) { + switchCategory(saved.category); +} else { + populateUnits(); +} diff --git a/projects/unit-converter/styles.css b/projects/unit-converter/styles.css index af845c5..2c90834 100644 --- a/projects/unit-converter/styles.css +++ b/projects/unit-converter/styles.css @@ -1,17 +1,260 @@ -/* TODO: Style the unit converter form and result area */ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + :root { - --bg: #0a0f1d; - --fg: #e2e8f0; - --accent: #60a5fa; + --bg-gradient-1: #667eea; + --bg-gradient-2: #764ba2; + --card-bg: #ffffff; + --text-primary: #1a1a2e; + --text-secondary: #6b7280; + --input-bg: #f3f4f6; + --input-border: #e5e7eb; + --input-focus: #667eea; + --accent-primary: #667eea; + --accent-hover: #5568d3; + --shadow: rgba(0, 0, 0, 0.1); + --shadow-lg: rgba(0, 0, 0, 0.2); +} + +[data-theme="dark"] { + --bg-gradient-1: #1a1a2e; + --bg-gradient-2: #16213e; + --card-bg: #0f172a; + --text-primary: #f1f5f9; + --text-secondary: #94a3b8; + --input-bg: #1e293b; + --input-border: #334155; + --input-focus: #818cf8; + --accent-primary: #818cf8; + --accent-hover: #6366f1; + --shadow: rgba(0, 0, 0, 0.3); + --shadow-lg: rgba(0, 0, 0, 0.5); } -* { box-sizing: border-box; } + body { - margin: 0; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif; - background: var(--bg); - color: var(--fg); - min-height: 100vh; - display: grid; - place-items: center; -} -#app { width: min(92vw, 720px); } + font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + background: linear-gradient(135deg, var(--bg-gradient-1) 0%, var(--bg-gradient-2) 100%); + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + transition: background 0.3s ease; +} + +.container { + background: var(--card-bg); + border-radius: 24px; + box-shadow: 0 20px 60px var(--shadow-lg); + padding: 40px; + max-width: 520px; + width: 100%; + transition: all 0.3s ease; +} + +.header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 30px; +} + +h1 { + color: var(--text-primary); + font-size: 32px; + font-weight: 700; + letter-spacing: -0.5px; +} + +.theme-toggle { + background: var(--input-bg); + border: 2px solid var(--input-border); + width: 50px; + height: 50px; + border-radius: 50%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 22px; + transition: all 0.3s ease; +} + +.theme-toggle:hover { + background: var(--accent-primary); + border-color: var(--accent-primary); + transform: rotate(180deg); +} + +.category-selector { + display: flex; + gap: 12px; + margin-bottom: 32px; + justify-content: center; + flex-wrap: wrap; +} + +.category-btn { + padding: 12px 24px; + border: 2px solid var(--accent-primary); + background: transparent; + color: var(--accent-primary); + border-radius: 30px; + cursor: pointer; + font-size: 15px; + font-weight: 600; + transition: all 0.3s ease; + font-family: 'Inter', sans-serif; +} + +.category-btn:hover { + background: var(--accent-primary); + color: white; + transform: translateY(-2px); + box-shadow: 0 4px 12px var(--shadow); +} + +.category-btn.active { + background: var(--accent-primary); + color: white; + box-shadow: 0 4px 12px var(--shadow); +} + +.converter-box { + background: var(--input-bg); + border-radius: 20px; + padding: 30px; + margin-bottom: 20px; + border: 1px solid var(--input-border); +} + +.input-group { + margin-bottom: 16px; +} + +.input-group label { + display: block; + margin-bottom: 10px; + color: var(--text-secondary); + font-weight: 600; + font-size: 14px; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.input-wrapper { + display: flex; + gap: 12px; +} + +input[type="number"] { + flex: 1; + padding: 14px 16px; + border: 2px solid var(--input-border); + border-radius: 12px; + font-size: 16px; + font-family: 'Inter', sans-serif; + background: var(--card-bg); + color: var(--text-primary); + transition: all 0.3s ease; +} + +input[type="number"]:focus { + outline: none; + border-color: var(--input-focus); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +select { + padding: 14px 16px; + border: 2px solid var(--input-border); + border-radius: 12px; + font-size: 16px; + font-family: 'Inter', sans-serif; + background: var(--card-bg); + color: var(--text-primary); + cursor: pointer; + min-width: 110px; + transition: all 0.3s ease; + font-weight: 500; +} + +select:focus { + outline: none; + border-color: var(--input-focus); + box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); +} + +.swap-container { + display: flex; + justify-content: center; + margin: 24px 0; +} + +.swap-btn { + background: linear-gradient(135deg, var(--accent-primary), var(--accent-hover)); + color: white; + border: none; + width: 56px; + height: 56px; + border-radius: 50%; + cursor: pointer; + font-size: 26px; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.4s cubic-bezier(0.68, -0.55, 0.265, 1.55); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.swap-btn:hover { + transform: rotate(180deg) scale(1.1); + box-shadow: 0 8px 25px rgba(102, 126, 234, 0.5); +} + +.swap-btn:active { + transform: rotate(180deg) scale(0.95); +} + +@media (max-width: 600px) { + .container { + padding: 28px; + } + + h1 { + font-size: 26px; + } + + .theme-toggle { + width: 44px; + height: 44px; + font-size: 20px; + } + + .category-btn { + padding: 10px 20px; + font-size: 14px; + } + + .input-wrapper { + flex-direction: column; + } + + select { + width: 100%; + } +} + +/* Remove spinner from number input */ +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + -webkit-appearance: none; + margin: 0; +} + +input[type="number"] { + -moz-appearance : textfield; +}