Add pattern templates for projects

This commit is contained in:
chris 2025-12-13 18:18:44 -05:00
parent 7865d8e772
commit a1ee84967d
5 changed files with 113 additions and 16 deletions

View File

@ -1,5 +1,7 @@
// --- Data Init & Colors ---
let projects = JSON.parse(localStorage.getItem('crochetCounters')) || [];
let patterns = JSON.parse(localStorage.getItem('crochetPatterns')) || [];
if (!Array.isArray(patterns)) patterns = [];
// New Earthy/Woodland Palette extracted from image vibes
const colors = [
'#a17d63', // Soft oak
@ -41,6 +43,9 @@ const colorGrid = document.getElementById('colorGrid');
const customColorInput = document.getElementById('customColorInput');
const saveOverlay = document.getElementById('saveOverlay');
const saveList = document.getElementById('saveList');
const patternPicker = document.getElementById('patternPicker');
const patternSelect = document.getElementById('patternSelect');
if (patternPicker && patternSelect) populatePatternSelect();
let pendingSaveSelection = [];
let lastCountPulse = null;
let lastFinishedId = null;
@ -230,11 +235,23 @@ function closeColorPicker() {
colorOverlay.dataset.partId = '';
}
function savePatterns() {
localStorage.setItem('crochetPatterns', JSON.stringify(patterns));
}
function populatePatternSelect() {
if (!patternPicker || !patternSelect) return;
const hasPatterns = patterns.length > 0;
patternPicker.style.display = hasPatterns ? 'block' : 'none';
patternSelect.innerHTML = '<option value=\"\">No pattern</option>' + patterns.map(p => `<option value="${p.id}">${p.name}</option>`).join('');
}
function exportData(selectedProjects = projects) {
const payload = {
projects: selectedProjects,
isDarkMode,
animationsEnabled
animationsEnabled,
patterns
};
const names = selectedProjects.map(p => p.name || 'Project').join('_').replace(/\s+/g, '-').slice(0, 50) || 'projects';
const filename = `toadstool_${names}.json`;
@ -271,6 +288,10 @@ async function handleImport(event) {
animationsEnabled = data.animationsEnabled;
localStorage.setItem('crochetAnimations', animationsEnabled);
}
if (Array.isArray(data.patterns)) {
patterns = data.patterns;
localStorage.setItem('crochetPatterns', JSON.stringify(patterns));
}
localStorage.setItem('crochetCounters', JSON.stringify(projects));
applyTheme();
render();
@ -618,7 +639,7 @@ document.addEventListener('visibilitychange', async () => {
lastCountPulse = { partId, dir: change > 0 ? 'up' : 'down' };
save();
}
async function resetCount(pId, partId) {
async function resetCount(pId, partId) {
const project = projects.find(p => p.id === pId);
const part = project.parts.find(pt => pt.id === partId);
if (part.locked || part.finished) return;
@ -627,7 +648,13 @@ document.addEventListener('visibilitychange', async () => {
part.count = 0;
save();
}
}
}
function saveProjectAsPattern(pId) {
const project = projects.find(p => p.id === pId);
if (!project) return;
openModal('savePattern', pId);
}
// --- Modal Logic ---
function openModal(type, pId = null, partId = null) {
@ -637,6 +664,10 @@ function openModal(type, pId = null, partId = null) {
if (type === 'addProject') {
modalTitle.innerText = "New Project Name";
modalInput.type = "text"; modalInput.placeholder = "e.g., Amigurumi Bear";
if (patternPicker && patternSelect) {
populatePatternSelect();
patternSelect.value = '';
}
} else if (type === 'addPart') {
modalTitle.innerText = "New Part Name";
modalInput.type = "text"; modalInput.placeholder = "e.g., Head";
@ -651,6 +682,11 @@ function openModal(type, pId = null, partId = null) {
if(part.locked || part.finished) return;
modalTitle.innerText = "Rename Part";
modalInput.value = part.name; modalInput.type = "text";
} else if (type === 'savePattern') {
modalTitle.innerText = "Save as Pattern";
const project = projects.find(p => p.id === pId);
modalInput.value = project ? `${project.name} pattern` : '';
modalInput.type = "text"; modalInput.placeholder = "Pattern name";
} else if (type === 'manualCount') {
const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId);
if(part.locked || part.finished) return;
@ -661,6 +697,9 @@ function openModal(type, pId = null, partId = null) {
const part = projects.find(p => p.id === pId).parts.find(pt => pt.id === partId);
if (part.locked) return;
}
if (type !== 'addProject' && patternPicker) {
patternPicker.style.display = 'none';
}
modal.classList.add('active');
setTimeout(() => modalInput.focus(), 100);
}
@ -674,12 +713,31 @@ function closeModal() {
if (modalState.type === 'addProject') {
const nextColor = colors[projects.length % colors.length];
projects.push({ id: Date.now(), name: val, color: nextColor, collapsed: false, parts: [] });
projects[projects.length-1].parts.push({ id: Date.now() + 1, name: 'Part 1', count: 0, locked: false, finished: false, minimized: false, max: null, color: nextColor });
const newProject = { id: Date.now(), name: val, color: nextColor, collapsed: false, note: '', parts: [] };
const selectedPatternId = patternSelect ? patternSelect.value : '';
const chosenPattern = selectedPatternId ? patterns.find(p => String(p.id) === selectedPatternId) : null;
if (chosenPattern && Array.isArray(chosenPattern.parts) && chosenPattern.parts.length) {
chosenPattern.parts.forEach((pt, idx) => {
newProject.parts.push({
id: Date.now() + idx + 1,
name: pt.name || `Part ${idx + 1}`,
count: 0,
locked: false,
finished: false,
minimized: false,
max: pt.max ?? null,
color: pt.color || newProject.color,
note: ''
});
});
} else {
newProject.parts.push({ id: Date.now() + 1, name: 'Part 1', count: 0, locked: false, finished: false, minimized: false, max: null, color: nextColor, note: '' });
}
projects.push(newProject);
}
else if (modalState.type === 'addPart') {
const project = projects.find(p => p.id === modalState.pId);
project.parts.push({ id: Date.now(), name: val, count: 0, locked: false, finished: false, minimized: false, max: null, color: project.color });
project.parts.push({ id: Date.now(), name: val, count: 0, locked: false, finished: false, minimized: false, max: null, color: project.color, note: '' });
project.collapsed = false;
}
else if (modalState.type === 'renamePart') {
@ -710,6 +768,15 @@ function closeModal() {
}
}
}
else if (modalState.type === 'savePattern') {
const project = projects.find(p => p.id === modalState.pId);
if (project) {
const template = project.parts.map(pt => ({ name: pt.name, color: pt.color, max: pt.max }));
patterns.push({ id: Date.now(), name: val || project.name, color: project.color, parts: template });
savePatterns();
populatePatternSelect();
}
}
save();
closeModal();
}
@ -824,6 +891,7 @@ function render() {
</div>
<div class="project-actions">
<button class="btn-add-part" onclick="openModal('addPart', ${project.id})">+ Part</button>
<button class="btn-save-pattern" onclick="saveProjectAsPattern(${project.id})" title="Save as pattern"><i class="fa-solid fa-swatchbook"></i></button>
<button class="btn-delete-project" onclick="deleteProject(${project.id})">×</button>
</div>
</div>

1
assets/app.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -297,6 +297,11 @@ h1 {
cursor: pointer; box-shadow: 0 3px 8px rgba(0,0,0,0.1); transition: transform 0.1s;
}
.btn-add-part:active { transform: scale(0.95); }
.btn-save-pattern {
background: none; border: 1px dashed var(--border); color: var(--text-muted);
border-radius: 14px; padding: 6px 10px; font-size: 0.9rem; cursor: pointer;
}
.btn-save-pattern:hover { color: var(--project-color); border-color: var(--project-color); }
.btn-delete-project {
background: none; border: none; color: var(--text-muted); font-size: 1.2rem; cursor: pointer; padding: 5px;
@ -520,6 +525,22 @@ button:active { transform: scale(0.97); box-shadow: none; }
.modal-btn { padding: 12px 24px; border: none; border-radius: 10px; font-size: 1rem; font-weight: 600; cursor: pointer; }
.btn-cancel { background: var(--lock-btn-bg); color: var(--text-muted); }
.btn-save { background: var(--text); color: var(--bg); }
.pattern-picker { display: none; margin: 12px 0 6px; }
.pattern-picker label {
display: block;
font-size: 0.9rem;
color: var(--text-muted);
margin-bottom: 4px;
}
.pattern-picker select {
width: 100%;
padding: 10px 12px;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--input-bg);
color: var(--text);
font-family: inherit;
}
.empty-state { text-align: center; color: var(--text-muted); margin-top: 80px; font-size: 1.2rem; font-style: italic;}

1
assets/style.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -40,6 +40,12 @@
<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>