385 lines
22 KiB
HTML
385 lines
22 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||
<title>Toadstool Cottage Counter</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@600;700&family=Nunito:wght@400;600;700&display=swap" rel="stylesheet">
|
||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.2/css/all.min.css" crossorigin="anonymous" referrerpolicy="no-referrer">
|
||
<link rel="icon" type="image/png" sizes="32x32" href="assets/icons/appicon-32x32.png">
|
||
<link rel="icon" type="image/png" sizes="128x128" href="assets/icons/appicon-128x128.png">
|
||
<link rel="apple-touch-icon" href="assets/icons/appicon-256x256.png">
|
||
<link rel="manifest" href="assets/site.webmanifest">
|
||
<meta name="theme-color" content="#e4e8d5">
|
||
<link rel="stylesheet" href="assets/style.css">
|
||
</head>
|
||
<body>
|
||
|
||
<header>
|
||
<div class="brand">
|
||
<img class="brand-icon" src="assets/icons/appicon-128x128.png" alt="Toadstool Cottage Counter icon">
|
||
<h1 id="appTitle">Toadstool Cottage Counter</h1>
|
||
</div>
|
||
<div class="header-controls">
|
||
<button class="sync-banner" id="syncBanner" onclick="openAuthModal()" title="Sign in to sync">Local-only mode</button>
|
||
<button class="header-btn hidden" id="installBtn" title="Install app"><i class="fa-solid fa-download"></i></button>
|
||
<button class="header-btn" id="settingsBtn" onclick="openSettingsModal()" title="Settings"><i class="fa-solid fa-gear"></i></button>
|
||
<button class="header-btn" id="focusBtn" onclick="toggleFocusMode()" title="Focus Mode (Keeps Screen On)"><i class="fa-solid fa-eye"></i></button>
|
||
<button class="header-btn" id="saveLoadBtn" onclick="openSaveModal()" title="Save/Load"><i class="fa-solid fa-floppy-disk"></i></button>
|
||
<button class="header-btn" id="authBtn" onclick="openAuthModal()" title="Sign in to sync"><i class="fa-solid fa-user"></i></button>
|
||
</div>
|
||
</header>
|
||
<input type="file" id="importFile" accept="application/json" class="hidden-input" />
|
||
|
||
<div class="container" id="app"></div>
|
||
|
||
<button class="fab" onclick="openModal('addProject')">+</button>
|
||
<button class="fab fab-pattern" id="patternFab" onclick="openPatternComposer()" title="Open Pattern Composer"><i class="fa-solid fa-swatchbook"></i></button>
|
||
|
||
<div class="modal-overlay" id="modalOverlay">
|
||
<div class="modal-content">
|
||
<h3 class="modal-title" id="modalTitle">Title</h3>
|
||
<input type="text" class="modal-input" id="modalInput" autocomplete="off">
|
||
<div class="pattern-picker" id="patternPicker">
|
||
<label for="patternSelect">Pattern (optional)</label>
|
||
<select id="patternSelect">
|
||
<option value="">No pattern</option>
|
||
</select>
|
||
</div>
|
||
<div class="modal-actions">
|
||
<button class="modal-btn btn-cancel" onclick="closeModal()">Cancel</button>
|
||
<button class="modal-btn btn-save" onclick="saveModal()">Save</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="color-overlay" id="colorOverlay">
|
||
<div class="color-modal">
|
||
<h3 class="color-title">Pick a color</h3>
|
||
<div class="color-grid" id="colorGrid"></div>
|
||
<div class="color-custom">
|
||
<label for="customColorInput">Custom:</label>
|
||
<input type="color" id="customColorInput" />
|
||
</div>
|
||
<button class="modal-btn btn-cancel" onclick="closeColorPicker()">Close</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="save-overlay" id="saveOverlay">
|
||
<div class="save-modal">
|
||
<h3 class="color-title">Save or Load</h3>
|
||
<p class="save-subtext">Choose projects to include:</p>
|
||
<div class="save-list" id="saveList"></div>
|
||
<div class="save-actions">
|
||
<button class="modal-btn btn-cancel" onclick="closeSaveModal()">Cancel</button>
|
||
<button class="modal-btn btn-save" onclick="exportSelected()">Save</button>
|
||
<button class="modal-btn btn-save" onclick="triggerImport()">Load</button>
|
||
</div>
|
||
<div class="import-selection hidden" id="importSelection">
|
||
<p class="save-subtext">Imported file projects:</p>
|
||
<div class="save-list" id="importList"></div>
|
||
<div class="save-actions">
|
||
<button class="modal-btn btn-cancel" onclick="cancelImportSelection()">Cancel</button>
|
||
<button class="modal-btn btn-save" onclick="applyImportSelection()">Add Selected</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="settings-overlay" id="settingsOverlay">
|
||
<div class="settings-modal">
|
||
<div class="settings-head">
|
||
<h3 class="color-title">Settings</h3>
|
||
<button class="pattern-close" onclick="closeSettingsModal()">×</button>
|
||
</div>
|
||
<div class="settings-body">
|
||
<label class="settings-toggle">
|
||
<span>Dark mode</span>
|
||
<input type="checkbox" id="settingDarkMode" onchange="setDarkMode(this.checked)">
|
||
</label>
|
||
<p class="settings-note">Use a darker theme that is easier on the eyes at night.</p>
|
||
<label class="settings-toggle">
|
||
<span>Animations</span>
|
||
<input type="checkbox" id="settingAnimations" onchange="setAnimations(this.checked)">
|
||
</label>
|
||
<p class="settings-note">Toggle ambient motion like fireflies and micro-animations.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="auth-overlay" id="authOverlay">
|
||
<div class="auth-modal">
|
||
<div class="auth-modal-head">
|
||
<div>
|
||
<h3 class="color-title">Account</h3>
|
||
<p class="auth-subtext">Sign in to sync across devices.</p>
|
||
</div>
|
||
<button class="pattern-close" onclick="closeAuthModal()">×</button>
|
||
</div>
|
||
<div class="auth-status">
|
||
<span id="authStatusBadge" class="status-pill">Signed out</span>
|
||
<span id="authLastSync" class="status-subtext">Status: unknown</span>
|
||
</div>
|
||
<div class="auth-tabs">
|
||
<button class="auth-tab active" data-mode="login" onclick="setAuthMode('login')">Login</button>
|
||
<button class="auth-tab" data-mode="signup" onclick="setAuthMode('signup')">Sign up</button>
|
||
<button class="auth-tab" data-mode="reset" onclick="setAuthMode('reset')">Reset</button>
|
||
<button class="auth-tab" id="profileTabBtn" data-mode="profile" onclick="setAuthMode('profile')" style="display:none;">Profile</button>
|
||
<button class="auth-tab" id="adminTabBtn" data-mode="admin" onclick="setAuthMode('admin')" style="display:none;">Admin</button>
|
||
</div>
|
||
<div class="auth-content">
|
||
<div id="loginContent" class="auth-tab-content active">
|
||
<form class="auth-form" onsubmit="return submitAuth(event, 'login')">
|
||
<label class="field-label" for="loginEmail">Email</label>
|
||
<input id="loginEmail" type="email" placeholder="you@example.com" autocomplete="email" required>
|
||
<label class="field-label" for="loginPassword">Password</label>
|
||
<input id="loginPassword" type="password" placeholder="••••••••" autocomplete="current-password" required>
|
||
<div class="auth-actions">
|
||
<button type="button" class="modal-btn btn-secondary" onclick="setAuthMode('reset')">Forgot password?</button>
|
||
<button type="button" class="modal-btn btn-cancel" onclick="closeAuthModal()">Cancel</button>
|
||
<button type="submit" class="modal-btn btn-save">Login</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div id="signupContent" class="auth-tab-content">
|
||
<form class="auth-form" onsubmit="return submitAuth(event, 'signup')">
|
||
<label class="field-label" for="signupEmail">Email</label>
|
||
<input id="signupEmail" type="email" placeholder="you@example.com" autocomplete="email" required>
|
||
<label class="field-label" for="signupPassword">Password</label>
|
||
<input id="signupPassword" type="password" placeholder="••••••••" autocomplete="new-password" required>
|
||
<label class="field-label" for="signupConfirmPassword">Confirm Password</label>
|
||
<input id="signupConfirmPassword" type="password" placeholder="••••••••" autocomplete="new-password" required>
|
||
<div class="auth-actions">
|
||
<button type="button" class="modal-btn btn-cancel" onclick="closeAuthModal()">Cancel</button>
|
||
<button type="submit" class="modal-btn btn-save">Sign Up</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div id="resetContent" class="auth-tab-content">
|
||
<form class="auth-form" onsubmit="return requestPasswordReset(event)">
|
||
<label class="field-label" for="resetEmail">Email</label>
|
||
<input id="resetEmail" type="email" placeholder="you@example.com" autocomplete="email" required>
|
||
<div class="auth-actions">
|
||
<button type="button" class="modal-btn btn-cancel" onclick="setAuthMode('login')">Back to login</button>
|
||
<button type="submit" class="modal-btn btn-save">Send reset</button>
|
||
</div>
|
||
</form>
|
||
<hr class="auth-divider">
|
||
<form id="resetConfirmForm" class="auth-form" onsubmit="return confirmPasswordReset(event)">
|
||
<label class="field-label" for="resetToken">Reset Token</label>
|
||
<input id="resetToken" type="text" placeholder="Paste your reset token" autocomplete="one-time-code" required>
|
||
<label class="field-label" for="resetPassword">New Password</label>
|
||
<input id="resetPassword" type="password" placeholder="••••••••" autocomplete="new-password" required>
|
||
<label class="field-label" for="resetConfirmPassword">Confirm New Password</label>
|
||
<input id="resetConfirmPassword" type="password" placeholder="••••••••" autocomplete="new-password" required>
|
||
<div class="auth-actions">
|
||
<button type="submit" class="modal-btn btn-save">Update password</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
<div id="adminContent" class="auth-tab-content">
|
||
<div id="adminPanel" class="admin-panel">
|
||
<h4>Pending Approvals</h4>
|
||
<div class="admin-actions">
|
||
<button type="button" class="modal-btn btn-secondary" onclick="fetchPendingUsers()">Refresh Pending</button>
|
||
</div>
|
||
<div id="pendingList" class="admin-list"></div>
|
||
|
||
<h4 style="margin-top:16px;">User Management</h4>
|
||
<div class="admin-actions">
|
||
<button type="button" class="modal-btn btn-secondary" onclick="fetchAllUsers()">Load All Users</button>
|
||
</div>
|
||
<div id="allUsersList" class="admin-list"></div>
|
||
|
||
<h4 style="margin-top:16px;">System Data</h4>
|
||
<div class="admin-actions">
|
||
<button type="button" class="modal-btn btn-secondary" onclick="downloadBackup()">Backup Data</button>
|
||
<label class="modal-btn btn-secondary">
|
||
Restore
|
||
<input type="file" id="restoreInput" accept="application/json,application/sql,.sql" style="display:none;" onchange="uploadRestore(event)">
|
||
</label>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<form class="auth-profile" onsubmit="return saveProfile(event)">
|
||
<h4 class="auth-section-title">Profile Settings</h4>
|
||
|
||
<div class="auth-profile-body">
|
||
<label class="field-label" for="authDisplayName">Display Name</label>
|
||
<input id="authDisplayName" type="text" placeholder="Your name">
|
||
|
||
<label class="field-label" for="authNote">Profile Note</label>
|
||
<textarea id="authNote" rows="2" placeholder="Add a note for your patterns..."></textarea>
|
||
</div>
|
||
|
||
<div class="auth-sync-section">
|
||
<p class="auth-hint">Sync your projects to the cloud to access them anywhere.</p>
|
||
<button type="button" class="modal-btn btn-secondary" onclick="autoSync()"><i class="fa-solid fa-rotate"></i> Sync Now</button>
|
||
</div>
|
||
|
||
<div class="auth-actions modal-footer">
|
||
<button type="button" class="modal-btn btn-cancel danger-text" onclick="logoutAuth()">Log Out</button>
|
||
<button type="submit" class="modal-btn btn-save">Save Changes</button>
|
||
</div>
|
||
</form>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pattern-overlay" id="patternOverlay">
|
||
<div class="pattern-sheet">
|
||
<div class="pattern-sheet-header">
|
||
<div class="header-main">
|
||
<h2 class="pattern-sheet-title">Pattern Composer</h2>
|
||
<div class="pattern-modes">
|
||
<button class="pattern-mode" data-mode="crochet" onclick="setPatternMode('crochet')">Crochet</button>
|
||
<button class="pattern-mode" data-mode="knit" onclick="setPatternMode('knit')">Knit</button>
|
||
</div>
|
||
</div>
|
||
<div class="header-actions">
|
||
<span class="pattern-save-indicator" id="patternSaveIndicator">Saved</span>
|
||
<button class="icon-action" onclick="savePatternDraft()" title="Save to Basket"><i class="fa-solid fa-bookmark"></i></button>
|
||
<button class="icon-action" onclick="savePatternAndStartProject()" title="Save & Start Project"><i class="fa-solid fa-play"></i></button>
|
||
<button class="icon-action" onclick="exportPatternPDF()" title="Export PDF"><i class="fa-solid fa-file-pdf"></i></button>
|
||
<button class="icon-action" onclick="sharePattern()" title="Share Link"><i class="fa-solid fa-link"></i></button>
|
||
<button class="icon-action danger" onclick="clearPatternOutput()" title="Clear Draft"><i class="fa-solid fa-eraser"></i></button>
|
||
<button class="pattern-close" onclick="closePatternComposer()">×</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pattern-nav">
|
||
<button class="nav-item active" data-tab="info" onclick="showPatternTab('info')">Specs</button>
|
||
<button class="nav-item" data-tab="steps" onclick="showPatternTab('steps')">Draft</button>
|
||
<button class="nav-item" data-tab="view" onclick="showPatternTab('view')">Read</button>
|
||
<button class="nav-item" data-tab="library" onclick="showPatternTab('library')">Shelf</button>
|
||
</div>
|
||
|
||
<div class="pattern-body">
|
||
<div class="pattern-section active" data-section="info">
|
||
<div class="card-grid">
|
||
<div class="input-card">
|
||
<h4 class="card-head">Basic Details</h4>
|
||
<div class="form-row">
|
||
<label class="field-label" for="patternTitle">Title</label>
|
||
<input id="patternTitle" type="text" placeholder="e.g., Baby Fox Plush">
|
||
</div>
|
||
<div class="form-row">
|
||
<label class="field-label" for="patternDesigner">Designer</label>
|
||
<input id="patternDesigner" type="text" placeholder="Your name or shop">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="input-card">
|
||
<h4 class="card-head">Yarn & Tools</h4>
|
||
<label class="field-label">Yarns</label>
|
||
<div id="yarnList" class="yarn-list-container">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
|
||
<label class="field-label" style="margin-top: 10px;">Hooks / Needles</label>
|
||
<div id="hookList" class="hook-list-container">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
|
||
<div class="field-group-inline" style="margin-top: 10px;">
|
||
<div>
|
||
<label class="field-label" for="patternGaugeSts">Sts / 4in</label>
|
||
<input id="patternGaugeSts" type="text" placeholder="e.g., 16 sc">
|
||
</div>
|
||
<div>
|
||
<label class="field-label" for="patternGaugeRows">Rows / 4in</label>
|
||
<input id="patternGaugeRows" type="text" placeholder="e.g., 18 rows">
|
||
</div>
|
||
</div>
|
||
<label class="field-label" for="patternGauge">Gauge Notes</label>
|
||
<textarea id="patternGauge" rows="2" placeholder="Magic ring start; or any extra gauge notes"></textarea>
|
||
</div>
|
||
|
||
<div class="input-card full-width">
|
||
<h4 class="card-head">Materials & Notes</h4>
|
||
<label class="field-label" for="patternMaterials">Materials (one per line)</label>
|
||
<textarea id="patternMaterials" rows="3" placeholder="• 1 skein Worsted Weight Yarn (Main Color) • 4.0mm Hook • Tapestry Needle • Polyester Fiberfill"></textarea>
|
||
|
||
<label class="field-label" for="patternNotes">Notes / Finishing</label>
|
||
<textarea id="patternNotes" rows="2" placeholder="Assembly, finishing, safety warnings, credits..."></textarea>
|
||
|
||
<label class="field-label" style="margin-top: 10px;">Finished Photos</label>
|
||
<div id="finishedPhotoList" class="finished-photo-list"></div>
|
||
<button class="secondary small" type="button" onclick="addFinishedPhoto()">Add Photo</button>
|
||
</div>
|
||
|
||
<div class="input-card full-width">
|
||
<div class="abbrev-head">
|
||
<h4 class="card-head" style="margin:0;">Abbreviations</h4>
|
||
<button class="secondary small" onclick="openAbbrevModal()">Edit / Add</button>
|
||
</div>
|
||
<div id="abbrevSummary" class="selected-abbrev">
|
||
<!-- Selected pills will appear here -->
|
||
<span class="text-muted" style="font-size: 0.9rem;">No stitches selected.</span>
|
||
</div>
|
||
<textarea id="patternAbbrev" rows="3" placeholder="Selected abbreviations will appear here..." readonly></textarea>
|
||
<label class="field-label" for="patternStitches">Special Stitches / Notes</label>
|
||
<textarea id="patternStitches" rows="2" placeholder="Magic ring: ... Invisible decrease: ..."></textarea>
|
||
</div>
|
||
</div>
|
||
<div class="pattern-footer-actions">
|
||
<button class="secondary" onclick="importPatternJSON()">Import JSON</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="pattern-section" data-section="steps">
|
||
<div class="pattern-steps-head">
|
||
<div>
|
||
<h4>Pattern Steps</h4>
|
||
<p class="muted small">Build steps and rows in order. Collapse steps to stay focused.</p>
|
||
</div>
|
||
</div>
|
||
<div id="patternSteps"></div>
|
||
</div>
|
||
|
||
<div class="pattern-section" data-section="library">
|
||
<div class="pattern-steps-head">
|
||
<div>
|
||
<h4>Saved Patterns</h4>
|
||
<p class="muted small">Search, load, or start a project.</p>
|
||
</div>
|
||
<button class="primary" onclick="savePatternDraft()">Save Current Draft</button>
|
||
</div>
|
||
<div class="pattern-library-controls">
|
||
<input id="patternSearch" type="text" placeholder="Search patterns..." oninput="renderPatternLibrary()">
|
||
<select id="patternSort" onchange="renderPatternLibrary()">
|
||
<option value="recent">Most recent</option>
|
||
<option value="name">Name A–Z</option>
|
||
</select>
|
||
</div>
|
||
<div id="patternLibrary" class="pattern-library"></div>
|
||
</div>
|
||
|
||
<div class="pattern-section" data-section="view">
|
||
<div id="patternView" class="pattern-view"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="modal-overlay" id="abbrevModal">
|
||
<div class="modal-content" style="width: min(800px, 94vw); max-height: 85vh; display: flex; flex-direction: column;">
|
||
<h3 class="modal-title">Select Abbreviations</h3>
|
||
<div class="abbrev-controls" style="margin-bottom: 12px;">
|
||
<input type="text" id="abbrevSearch" placeholder="Search stitches..." oninput="filterAbbrev()">
|
||
<button class="secondary small" onclick="loadDefaultAbbrev()">Reset to Defaults</button>
|
||
</div>
|
||
<div id="patternAbbrevList" class="abbrev-grid" style="overflow-y: auto; flex: 1;"></div>
|
||
<div class="modal-actions" style="margin-top: 12px; border-top: 1px solid var(--border); padding-top: 12px;">
|
||
<button class="modal-btn btn-save" onclick="closeAbbrevModal()">Done</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="assets/app.js?v=3"></script>
|
||
<footer class="footer-bg" aria-hidden="true"></footer>
|
||
</body>
|
||
</html>
|