From 38cf39491607aa46230257b5833d3dc3cc56892c Mon Sep 17 00:00:00 2001 From: Dan Date: Sat, 21 Feb 2026 18:50:16 +0000 Subject: [PATCH] Separating css and js - getting way too long to manage --- index.html | 734 +---------------------------------------------------- script.js | 254 ++++++++++++++++++ style.css | 470 ++++++++++++++++++++++++++++++++++ sw.js | 2 +- 4 files changed, 727 insertions(+), 733 deletions(-) create mode 100644 script.js create mode 100644 style.css diff --git a/index.html b/index.html index feed244..275581f 100644 --- a/index.html +++ b/index.html @@ -10,480 +10,9 @@ + Status Poster - @@ -612,266 +141,7 @@ - + diff --git a/script.js b/script.js new file mode 100644 index 0000000..61ddd60 --- /dev/null +++ b/script.js @@ -0,0 +1,254 @@ +// ── State ──────────────────────────────────────────────────────────────────── +let emoji = '📝'; +let settings = { username: '', apiKey: '' }; + +// ── DOM ────────────────────────────────────────────────────────────────────── +const $ = id => document.getElementById(id); +const emojiDisplay = $('emoji-display'); +const emojiShow = $('emoji-show'); +const emojiInput = $('emoji-input'); +const emojiSection = emojiDisplay.closest('.emoji-section'); +const emojiHint = $('emoji-hint'); +const statusText = $('status-text'); +const statusUrl = $('status-url'); +const charCount = $('char-count'); +const postBtn = $('post-btn'); +const toast = $('toast'); +const settingsBtn = $('settings-btn'); +const overlay = $('overlay'); +const modalClose = $('modal-close'); +const sUsername = $('s-username'); +const sApikey = $('s-apikey'); +const eyeBtn = $('eye-btn'); +const saveBtn = $('save-btn'); + +// ── Settings ───────────────────────────────────────────────────────────────── +function loadSettings() { + try { + const raw = localStorage.getItem('spo-settings'); + if (raw) settings = { ...settings, ...JSON.parse(raw) }; + } catch {} + updateBadge(); +} + +function saveSettings() { + settings.username = sUsername.value.trim().replace(/^@/, ''); + settings.apiKey = sApikey.value.trim(); + try { + localStorage.setItem('spo-settings', JSON.stringify(settings)); + } catch { + showToast('Could not save — storage unavailable', 'error'); + return; + } + updateBadge(); + closeModal(); + showToast('Settings saved', 'success'); +} + +function updateBadge() { + settingsBtn.classList.toggle('needs-setup', !settings.username || !settings.apiKey); +} + +// ── Modal ──────────────────────────────────────────────────────────────────── +function openModal() { + sUsername.value = settings.username; + sApikey.value = settings.apiKey; + overlay.classList.add('open'); + document.body.style.overflow = 'hidden'; + // Focus first empty field + if (!settings.username) sUsername.focus(); + else if (!settings.apiKey) sApikey.focus(); +} + +function closeModal() { + overlay.classList.remove('open'); + document.body.style.overflow = ''; +} + +settingsBtn.addEventListener('click', openModal); +modalClose.addEventListener('click', closeModal); +overlay.addEventListener('click', e => { if (e.target === overlay) closeModal(); }); +saveBtn.addEventListener('click', saveSettings); + +// Eye toggle for API key +eyeBtn.addEventListener('click', () => { + const show = sApikey.type === 'password'; + sApikey.type = show ? 'text' : 'password'; + eyeBtn.textContent = show ? '🙈' : '👁'; + eyeBtn.setAttribute('aria-label', show ? 'Hide API key' : 'Show API key'); +}); + +// ── Emoji picker ───────────────────────────────────────────────────────────── +function extractFirstEmoji(str) { + if (!str) return null; + if (typeof Intl?.Segmenter === 'function') { + const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' }); + for (const { segment } of seg.segment(str)) { + if (/\p{Emoji}/u.test(segment) && segment.trim() !== '') return segment; + } + return null; + } + const m = str.match(/\p{Emoji_Presentation}[\p{Emoji}\u{FE0F}\u{20E3}]*/u); + return m ? m[0] : null; +} + +// Detect platform for hint text +const isMac = /Mac/.test(navigator.userAgent) && !/iPhone|iPad/.test(navigator.userAgent); +const osHint = isMac ? ' · or ⌃⌘Space' : (navigator.userAgent.includes('Win') ? ' · or Win+.' : ''); + +function openEmojiPicker() { + emojiSection.classList.add('picking'); + emojiInput.value = ''; + emojiInput.focus(); + emojiHint.textContent = `Type, paste, or use your emoji keyboard${osHint}`; +} + +function closeEmojiPicker() { + emojiSection.classList.remove('picking'); + emojiHint.textContent = 'Click to change emoji'; +} + +emojiDisplay.addEventListener('click', openEmojiPicker); + +emojiDisplay.addEventListener('keydown', e => { + if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); openEmojiPicker(); } +}); + +emojiInput.addEventListener('input', () => { + const found = extractFirstEmoji(emojiInput.value); + if (found) { + emoji = found; + emojiShow.textContent = found; + closeEmojiPicker(); + } +}); + +emojiInput.addEventListener('blur', () => { + // Small delay so a click on the emoji-display doesn't flicker + setTimeout(() => { if (document.activeElement !== emojiInput) closeEmojiPicker(); }, 150); +}); + +// ── Keyboard shortcuts ──────────────────────────────────────────────────────── +document.addEventListener('keydown', e => { + if (e.key === 'Escape') { + if (emojiSection.classList.contains('picking')) closeEmojiPicker(); + else if (overlay.classList.contains('open')) closeModal(); + } +}); + +// ── Character count ─────────────────────────────────────────────────────────── +statusText.addEventListener('input', () => { + const n = statusText.value.length; + charCount.textContent = `${n} / 5000`; + charCount.classList.toggle('warn', n > 4800); +}); + +// ── Post status ─────────────────────────────────────────────────────────────── +async function postStatus() { + if (!settings.username || !settings.apiKey) { + showToast('Add your credentials in settings first', 'error'); + openModal(); + return; + } + + const content = statusText.value.trim(); + if (!content) { + showToast('Status text is required', 'error'); + statusText.focus(); + return; + } + + postBtn.classList.add('loading'); + postBtn.disabled = true; + + const body = { emoji, content }; + const url = statusUrl.value.trim(); + if (url) body.external_url = url; + + try { + const res = await fetch( + `https://api.omg.lol/address/${encodeURIComponent(settings.username)}/statuses/`, + { + method: 'POST', + headers: { + 'Authorization': `Bearer ${settings.apiKey}`, + 'Content-Type': 'application/json', + }, + body: JSON.stringify(body), + } + ); + + const data = await res.json().catch(() => ({})); + + if (res.ok || data?.request?.status_code === 200) { + const msg = data?.response?.message ?? 'Status posted!'; + showToast(msg, 'success'); + statusText.value = ''; + statusUrl.value = ''; + charCount.textContent = '0 / 5000'; + charCount.classList.remove('warn'); + emoji = '📝'; + emojiShow.textContent = '📝'; + } else { + const msg = data?.response?.message ?? `Error ${res.status}`; + showToast(msg, 'error'); + } + } catch (err) { + const msg = err?.message === 'Failed to fetch' + ? 'Network error — check your connection' + : (err?.message ?? 'Something went wrong'); + showToast(msg, 'error'); + } finally { + postBtn.classList.remove('loading'); + postBtn.disabled = false; + } +} + +postBtn.addEventListener('click', postStatus); + +// Ctrl/Cmd + Enter to post from textarea +statusText.addEventListener('keydown', e => { + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) postStatus(); +}); + +// ── Toast ──────────────────────────────────────────────────────────────────── +let toastTimer; +function showToast(msg, type = '') { + clearTimeout(toastTimer); + toast.textContent = msg; + toast.className = `show ${type}`.trim(); + toastTimer = setTimeout(() => { toast.className = ''; }, 4000); +} + +// ── Apple touch icon (generated at runtime — iOS doesn't support SVG icons) ── +try { + const c = document.createElement('canvas'); + c.width = c.height = 192; + const ctx = c.getContext('2d'); + ctx.fillStyle = '#7c3aed'; + ctx.beginPath(); + ctx.roundRect(0, 0, 192, 192, 38); + ctx.fill(); + ctx.fillStyle = 'white'; + ctx.font = '90px serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText('✦', 96, 98); + const link = document.createElement('link'); + link.rel = 'apple-touch-icon'; + link.href = c.toDataURL('image/png'); + document.head.appendChild(link); +} catch {} + +// ── Service worker ──────────────────────────────────────────────────────────── +if ('serviceWorker' in navigator) { + navigator.serviceWorker.register('sw.js').catch(() => {}); +} + +// ── Init ────────────────────────────────────────────────────────────────────── +loadSettings(); + +// Auto-open settings if no credentials saved yet +if (!settings.username || !settings.apiKey) { + setTimeout(openModal, 500); +} diff --git a/style.css b/style.css new file mode 100644 index 0000000..f394c4e --- /dev/null +++ b/style.css @@ -0,0 +1,470 @@ +/* ── Design tokens ─────────────────────────────────────────────────────── */ +:root { + --bg: #ffffff; + --surface: #f4f4f7; + --border: #e2e2ea; + --text: #18181b; + --muted: #71717a; + --accent: #7c3aed; + --accent-dim: #ede9fe; + --accent-fg: #ffffff; + --danger: #dc2626; + --success: #16a34a; + --r: 14px; + --r-sm: 8px; + --safe-t: env(safe-area-inset-top, 0px); + --safe-b: env(safe-area-inset-bottom, 0px); +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #0f0f13; + --surface: #17171f; + --border: #2a2a38; + --text: #f0f0f8; + --muted: #8b8ba0; + --accent: #a78bfa; + --accent-dim: #2d1f5e; + --accent-fg: #0f0f13; + --danger: #f87171; + --success: #4ade80; + } +} + +/* ── Reset & base ──────────────────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } + +html { height: 100%; } + +body { + min-height: 100%; + background: var(--bg); + color: var(--text); + font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + font-size: 16px; + line-height: 1.5; + -webkit-font-smoothing: antialiased; +} + +/* ── Layout ────────────────────────────────────────────────────────────── */ +.app { + display: flex; + flex-direction: column; + min-height: 100vh; + max-width: 480px; + margin: 0 auto; +} + +/* ── Header ────────────────────────────────────────────────────────────── */ +header { + display: flex; + align-items: center; + justify-content: space-between; + padding: calc(14px + var(--safe-t)) 20px 14px; + background: var(--bg); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} + +.logo { + display: flex; + align-items: center; + gap: 9px; + font-size: 1.05rem; + font-weight: 700; + letter-spacing: -0.01em; +} + +.logo-mark { + width: 30px; + height: 30px; + background: var(--accent); + color: var(--accent-fg); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.9rem; + flex-shrink: 0; +} + +.settings-btn { + width: 40px; + height: 40px; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--r-sm); + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 1.05rem; + color: var(--muted); + transition: background 0.15s, color 0.15s; + position: relative; + -webkit-tap-highlight-color: transparent; +} + +.settings-btn:active { background: var(--border); } + +/* Red dot when credentials missing */ +.settings-btn.needs-setup::after { + content: ''; + position: absolute; + top: -3px; + right: -3px; + width: 10px; + height: 10px; + background: var(--danger); + border-radius: 50%; + border: 2px solid var(--bg); +} + +/* ── Main content ──────────────────────────────────────────────────────── */ +main { + flex: 1; + padding: 28px 20px; + display: flex; + flex-direction: column; + gap: 22px; +} + +/* ── Emoji picker ──────────────────────────────────────────────────────── */ +.emoji-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 10px; +} + +.emoji-display { + width: 92px; + height: 92px; + border-radius: 26px; + background: var(--surface); + border: 2px solid var(--border); + display: flex; + align-items: center; + justify-content: center; + font-size: 3.25rem; + cursor: pointer; + user-select: none; + transition: transform 0.12s, border-color 0.15s, background 0.15s; + -webkit-tap-highlight-color: transparent; + position: relative; +} + +.emoji-display:active { transform: scale(0.94); } +.emoji-display:hover { border-color: var(--accent); background: var(--accent-dim); } + +/* Visible emoji text input — slides in when picking */ +.emoji-picker-input { + width: 200px; + text-align: center; + font-size: 1.5rem; + /* collapse when hidden */ + max-height: 0; + padding-top: 0; + padding-bottom: 0; + overflow: hidden; + opacity: 0; + pointer-events: none; + border-color: transparent; + transition: max-height 0.2s ease, padding 0.18s ease, + opacity 0.15s ease, border-color 0.15s ease; +} + +.emoji-section.picking .emoji-picker-input { + max-height: 60px; + padding-top: 9px; + padding-bottom: 9px; + opacity: 1; + pointer-events: all; + border-color: var(--accent); +} + +.emoji-hint { + font-size: 0.8rem; + color: var(--muted); + transition: opacity 0.15s; +} + +/* ── Form fields ───────────────────────────────────────────────────────── */ +.field { + display: flex; + flex-direction: column; + gap: 7px; +} + +label { + font-size: 0.78rem; + font-weight: 600; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.06em; +} + +textarea, +input[type="url"], +input[type="text"], +input[type="password"] { + background: var(--surface); + border: 1.5px solid var(--border); + border-radius: var(--r); + color: var(--text); + font-family: inherit; + font-size: 1rem; + padding: 13px 15px; + width: 100%; + outline: none; + transition: border-color 0.15s, background 0.15s; + -webkit-appearance: none; +} + +textarea:focus, +input[type="url"]:focus, +input[type="text"]:focus, +input[type="password"]:focus { + border-color: var(--accent); +} + +textarea { + resize: none; + min-height: 116px; + line-height: 1.6; +} + +textarea::placeholder, +input::placeholder { color: var(--muted); } + +.char-count { + font-size: 0.78rem; + color: var(--muted); + text-align: right; + transition: color 0.15s; +} + +.char-count.warn { color: var(--danger); } + +/* ── Post button ───────────────────────────────────────────────────────── */ +.post-btn { + background: var(--accent); + color: var(--accent-fg); + border: none; + border-radius: var(--r); + font-family: inherit; + font-size: 1rem; + font-weight: 700; + padding: 16px; + width: 100%; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: opacity 0.15s, transform 0.1s; + -webkit-tap-highlight-color: transparent; +} + +.post-btn:active:not(:disabled) { transform: scale(0.98); } +.post-btn:disabled { opacity: 0.5; cursor: not-allowed; } + +.spinner { + width: 18px; + height: 18px; + border: 2.5px solid rgba(255, 255, 255, 0.35); + border-top-color: white; + border-radius: 50%; + animation: spin 0.65s linear infinite; + display: none; + flex-shrink: 0; +} + +.post-btn.loading .spinner { display: block; } +.post-btn.loading .btn-label { display: none; } + +@keyframes spin { to { transform: rotate(360deg); } } + +/* ── Toast ─────────────────────────────────────────────────────────────── */ +#toast { + position: fixed; + bottom: calc(24px + var(--safe-b)); + left: 50%; + transform: translateX(-50%) translateY(80px); + background: var(--text); + color: var(--bg); + padding: 11px 22px; + border-radius: 40px; + font-size: 0.9rem; + font-weight: 500; + max-width: calc(100% - 40px); + text-align: center; + box-shadow: 0 6px 28px rgba(0, 0, 0, 0.22); + transition: transform 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); + z-index: 200; + pointer-events: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#toast.show { transform: translateX(-50%) translateY(0); } +#toast.success { background: var(--success); color: #fff; } +#toast.error { background: var(--danger); color: #fff; } + +/* ── Settings modal ────────────────────────────────────────────────────── */ +.overlay { + position: fixed; + inset: 0; + background: rgba(0, 0, 0, 0.48); + z-index: 50; + display: flex; + align-items: flex-end; + opacity: 0; + pointer-events: none; + transition: opacity 0.25s; +} + +.overlay.open { + opacity: 1; + pointer-events: all; +} + +.modal { + background: var(--bg); + border-radius: 22px 22px 0 0; + width: 100%; + max-width: 480px; + margin: 0 auto; + padding-bottom: calc(20px + var(--safe-b)); + transform: translateY(100%); + transition: transform 0.32s cubic-bezier(0.32, 0.72, 0, 1); + max-height: 92vh; + overflow-y: auto; +} + +.overlay.open .modal { transform: translateY(0); } + +.modal-grip { + width: 40px; + height: 4px; + background: var(--border); + border-radius: 2px; + margin: 10px auto 0; +} + +.modal-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 18px 20px 0; +} + +.modal-title { + font-size: 1.1rem; + font-weight: 700; +} + +.close-btn { + width: 32px; + height: 32px; + background: var(--surface); + border: none; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 0.85rem; + color: var(--muted); + -webkit-tap-highlight-color: transparent; +} + +.close-btn:active { background: var(--border); } + +.modal-body { + padding: 20px; + display: flex; + flex-direction: column; + gap: 20px; +} + +/* Privacy notice */ +.privacy-notice { + background: var(--accent-dim); + border: 1.5px solid color-mix(in srgb, var(--accent) 30%, transparent); + border-radius: var(--r); + padding: 14px 16px; + display: flex; + gap: 11px; + align-items: flex-start; + font-size: 0.875rem; + color: var(--text); + line-height: 1.6; +} + +.privacy-icon { font-size: 1.1rem; flex-shrink: 0; margin-top: 2px; } + +/* API key with eye toggle */ +.input-wrap { + position: relative; + display: flex; +} + +.input-wrap input { padding-right: 48px; } + +.eye-btn { + position: absolute; + right: 13px; + top: 50%; + transform: translateY(-50%); + background: none; + border: none; + cursor: pointer; + color: var(--muted); + font-size: 1rem; + padding: 4px; + line-height: 1; + -webkit-tap-highlight-color: transparent; +} + +.save-btn { + background: var(--accent); + color: var(--accent-fg); + border: none; + border-radius: var(--r); + font-family: inherit; + font-size: 1rem; + font-weight: 700; + padding: 14px; + width: 100%; + cursor: pointer; + transition: opacity 0.15s; + -webkit-tap-highlight-color: transparent; +} + +.save-btn:active { opacity: 0.8; } + +/* ── Tablet / desktop ──────────────────────────────────────────────────── */ +@media (min-width: 520px) { + .overlay { align-items: center; } + + .modal { + border-radius: 22px; + margin: 20px; + width: calc(100% - 40px); + max-height: 80vh; + transform: scale(0.96) translateY(10px); + opacity: 0; + transition: transform 0.25s cubic-bezier(0.34, 1.2, 0.64, 1), opacity 0.2s; + } + + .overlay.open .modal { + transform: scale(1) translateY(0); + opacity: 1; + } + + .modal-grip { display: none; } +} diff --git a/sw.js b/sw.js index 0058da3..e589c90 100644 --- a/sw.js +++ b/sw.js @@ -1,5 +1,5 @@ const CACHE = 'status-poster-v1'; -const PRECACHE = ['.', 'index.html', 'manifest.json', 'sw.js', 'icon.svg']; +const PRECACHE = ['.', 'index.html', 'style.css', 'script.js', 'manifest.json', 'sw.js', 'icon.svg']; self.addEventListener('install', e => { e.waitUntil(