A Pen by Jason Davis on CodePen.
Created
May 5, 2025 10:18
-
-
Save jasondavis/f568989dda8c1b712a3e001aae41793c to your computer and use it in GitHub Desktop.
Collection Manager Web App
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<div class="search-bar"> | |
<input type="text" id="searchInput" placeholder="Search..."> | |
</div> | |
<div id="itemModal" class="modal" style="display:none;"> | |
<div class="modal-content"> | |
<h3 id="formTitle">Add New Item</h3> | |
<input id="newTitle" placeholder="Title"> | |
<input id="newDesc" placeholder="Description"> | |
<input id="newIcon" placeholder="Icon URL"> | |
<select id="newType"> | |
<option value="">Type (optional)</option> | |
<option>iOS App</option> | |
<option>Desktop Software</option> | |
<option>AR Filter</option> | |
<option>AI App</option> | |
<option>Bookmark</option> | |
<option>Other</option> | |
</select> | |
<input id="newCollection" placeholder="Collection (e.g., AI Tools)"> | |
<div style="margin-top: 10px;"> | |
<button onclick="submitItem()">Save</button> | |
<button onclick="cancelEdit()">Cancel</button> | |
</div> | |
</div> | |
</div> | |
<div class="controls"> | |
<button onclick="showModal()">Add New Item</button> | |
<button onclick="downloadJSON()">Export JSON</button> | |
<input type="file" id="importFile" accept=".json" onchange="importJSON()" /> | |
<button onclick="toggleView()">Toggle View</button> | |
<button onclick="toggleTheme()">Toggle Theme</button> | |
</div> | |
<div class="controls"> | |
<label for="collectionFilter">Filter by Collection:</label> | |
<select id="collectionFilter" onchange="renderCollection(collection)"> | |
<option value="All">All</option> | |
</select> | |
</div> | |
<div id="collectionContainer"></div> | |
<div class="letter-nav" id="letterNav"></div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
let collection = []; | |
const STORAGE_KEY = "appCollectionData"; | |
let selectedCollection = "All"; | |
let currentView = "list"; | |
let isGrid = false; | |
//let editingIndex = null; | |
loadFromLocalStorage(); | |
if (collection.length === 0) { | |
// fallback demo data | |
collection = [ | |
{ | |
title: "Obsidian", | |
description: "A knowledge base app", | |
collection: "Desktop Software", | |
icon: "https://obsidian.md/images/og/og-image.png" | |
}, | |
{ | |
title: "Apex AI", | |
description: "AI Tool", | |
icon: "https://via.placeholder.com/40", | |
type: "AI App" | |
}, | |
{ | |
title: "Bounce Back", | |
description: "Music Track", | |
icon: "https://via.placeholder.com/40", | |
type: "Other" | |
}, | |
{ | |
title: "ChatGPT", | |
description: "Chat Assistant", | |
icon: "https://via.placeholder.com/40", | |
type: "AI App" | |
}, | |
{ | |
title: "DevLogger", | |
description: "iOS Utility", | |
icon: "https://via.placeholder.com/40", | |
type: "iOS App" | |
}, | |
{ | |
title: "Echo", | |
description: "Sound Tool", | |
icon: "https://via.placeholder.com/40", | |
type: "Desktop Software" | |
} | |
]; | |
} | |
//let isGrid = localStorage.getItem("viewMode") === "grid"; | |
let editingIndex = null; | |
function groupByLetter(data) { | |
const result = {}; | |
for (let item of data) { | |
const letter = item.title[0].toUpperCase(); | |
if (!result[letter]) result[letter] = []; | |
result[letter].push(item); | |
} | |
return result; | |
} | |
function renderCollection(data) { | |
const container = document.getElementById("collectionContainer"); | |
const nav = document.getElementById("letterNav"); | |
container.innerHTML = ""; | |
nav.innerHTML = ""; | |
const selectedCollection = document.getElementById("collectionFilter").value; | |
if (selectedCollection !== "All") { | |
data = data.filter((item) => item.collection === selectedCollection); | |
} | |
const grouped = groupByLetter(data); | |
const letters = Object.keys(grouped).sort(); | |
for (let letter of letters) { | |
if (!isGrid) { | |
const navLink = document.createElement("a"); | |
navLink.href = `#letter-${letter}`; | |
navLink.textContent = letter; | |
nav.appendChild(navLink); | |
} | |
const section = document.createElement("div"); | |
section.className = "item-group"; | |
section.id = `letter-${letter}`; | |
if (!isGrid) section.innerHTML = `<h2>${letter}</h2>`; | |
for (let item of grouped[letter]) { | |
const itemDiv = document.createElement("div"); | |
if (editingIndex !== null && collection[editingIndex] === item) { | |
itemDiv.style.border = "2px solid #007AFF"; | |
} | |
itemDiv.className = "item"; | |
itemDiv.innerHTML = ` | |
<img src="${item.icon}" alt=""> | |
<div class="info"> | |
<div class="title">${item.title}</div> | |
<div class="desc">${item.description}</div> | |
${item.type ? `<div class="type">${item.type}</div>` : ""} | |
</div><div style="text-align:right;"> | |
<button onclick='editItem(${JSON.stringify(item).replace( | |
/'/g, | |
"\\'" | |
)})'>Edit</button> | |
</div>`; | |
section.appendChild(itemDiv); | |
} | |
container.appendChild(section); | |
} | |
} | |
function addNewItem() { | |
const title = document.getElementById("newTitle").value.trim(); | |
const desc = document.getElementById("newDesc").value.trim(); | |
const icon = | |
document.getElementById("newIcon").value.trim() || | |
"https://via.placeholder.com/40"; | |
const type = document.getElementById("newType").value; | |
const collectionName = | |
document.getElementById("newCollection").value.trim() || "General"; | |
if (!title || !desc) return alert("Title and Description required."); | |
collection.push({ | |
title, | |
description: desc, | |
icon, | |
type, | |
collection: collectionName | |
}); | |
updateCollectionsDropdown(); | |
renderCollection(collection); | |
document | |
.querySelectorAll(".form input, .form select") | |
.forEach((el) => (el.value = "")); | |
} | |
function submitItem() { | |
const title = document.getElementById("newTitle").value.trim(); | |
const desc = document.getElementById("newDesc").value.trim(); | |
const icon = | |
document.getElementById("newIcon").value.trim() || | |
"https://via.placeholder.com/40"; | |
const type = document.getElementById("newType").value; | |
const collectionName = | |
document.getElementById("newCollection").value.trim() || "General"; | |
if (!title || !desc) return alert("Title and Description required."); | |
// Prevent duplicate title if adding new | |
const duplicate = collection.some( | |
(item, i) => | |
item.title === title && (editingIndex === null || i !== editingIndex) | |
); | |
if (duplicate) return alert("An item with that title already exists."); | |
const newItem = { | |
title, | |
description: desc, | |
icon, | |
type, | |
collection: collectionName | |
}; | |
if (editingIndex !== null) { | |
collection[editingIndex] = newItem; | |
saveToLocalStorage(); | |
} else { | |
collection.push(newItem); | |
saveToLocalStorage(); | |
} | |
closeModal(); | |
updateCollectionsDropdown(); | |
renderCollection(collection); | |
} | |
function editItemOLD(item) { | |
document.getElementById("newTitle").value = item.title; | |
document.getElementById("newDesc").value = item.description; | |
document.getElementById("newIcon").value = item.icon; | |
document.getElementById("newType").value = item.type || ""; | |
document.getElementById("newCollection").value = item.collection || "General"; | |
collection = collection.filter((i) => i.title !== item.title); | |
updateCollectionsDropdown(); | |
renderCollection(collection); | |
} | |
function editItem(item) { | |
editingIndex = collection.findIndex( | |
(i) => | |
i.title === item.title && | |
i.description === item.description && | |
i.icon === item.icon | |
); | |
if (editingIndex === -1) return alert("Item not found."); | |
document.getElementById("formTitle").textContent = "Edit Item"; | |
document.getElementById("newTitle").value = item.title; | |
document.getElementById("newDesc").value = item.description; | |
document.getElementById("newIcon").value = item.icon; | |
document.getElementById("newType").value = item.type || ""; | |
document.getElementById("newCollection").value = item.collection || "General"; | |
showModal(); | |
} | |
function updateCollectionsDropdown() { | |
const select = document.getElementById("collectionSelect"); | |
if (!select) return; | |
const collections = [ | |
"All", | |
...new Set(collection.map((item) => item.collection || "General")) | |
]; | |
select.innerHTML = ""; | |
collections.forEach((name) => { | |
const option = document.createElement("option"); | |
option.value = name; | |
option.textContent = name; | |
select.appendChild(option); | |
}); | |
} | |
function saveToLocalStorage() { | |
localStorage.setItem(STORAGE_KEY, JSON.stringify(collection)); | |
} | |
function loadFromLocalStorage() { | |
const stored = localStorage.getItem(STORAGE_KEY); | |
if (stored) { | |
try { | |
collection = JSON.parse(stored); | |
} catch (e) { | |
console.error("Invalid saved data:", e); | |
} | |
} | |
} | |
function downloadJSON() { | |
const blob = new Blob([JSON.stringify(collection, null, 2)], { | |
type: "application/json" | |
}); | |
const a = document.createElement("a"); | |
a.href = URL.createObjectURL(blob); | |
a.download = "collection.json"; | |
a.click(); | |
} | |
function importJSON() { | |
const file = document.getElementById("importFile").files[0]; | |
if (!file) return; | |
const reader = new FileReader(); | |
reader.onload = (e) => { | |
try { | |
const imported = JSON.parse(e.target.result); | |
if (Array.isArray(imported)) { | |
collection = imported; | |
renderCollection(collection); | |
} else { | |
alert("Invalid JSON structure."); | |
} | |
} catch (err) { | |
alert("Error reading JSON: " + err.message); | |
} | |
}; | |
reader.readAsText(file); | |
} | |
document.getElementById("searchInput").addEventListener("input", (e) => { | |
const value = e.target.value.toLowerCase(); | |
const filtered = collection.filter((item) => | |
item.title.toLowerCase().includes(value) | |
); | |
renderCollection(filtered); | |
}); | |
function showModal() { | |
document.getElementById("itemModal").style.display = "flex"; | |
} | |
function closeModal() { | |
document.getElementById("itemModal").style.display = "none"; | |
editingIndex = null; | |
document.getElementById("formTitle").textContent = "Add New Item"; | |
document | |
.querySelectorAll(".modal-content input, .modal-content select") | |
.forEach((el) => (el.value = "")); | |
} | |
function cancelEdit() { | |
closeModal(); | |
} | |
function toggleView() { | |
isGrid = !isGrid; | |
localStorage.setItem("viewMode", isGrid ? "grid" : "list"); | |
document | |
.getElementById("collectionContainer") | |
.classList.toggle("grid-view", isGrid); | |
renderCollection(collection); | |
} | |
function toggleTheme() { | |
const isDark = document.body.classList.toggle("dark-mode"); | |
localStorage.setItem("theme", isDark ? "dark" : "light"); | |
} | |
//let selectedCollection = "All"; | |
//let currentView = "list"; | |
//let isGrid = false; | |
if (isGrid) { | |
document.getElementById("collectionContainer").classList.add("grid-view"); | |
} | |
if (localStorage.getItem("theme") === "dark") { | |
document.body.classList.add("dark-mode"); | |
} | |
updateCollectionsDropdown(); | |
renderCollection(collection); | |
console.log("Initial Collection:", collection); | |
console.log("Selected:", selectedCollection); | |
console.log("View:", currentView); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
body { | |
font-family: -apple-system, sans-serif; | |
margin: 0; | |
padding: 0; | |
background: #f9f9f9; | |
} | |
.search-bar, | |
.form, | |
.controls { | |
padding: 10px 15px; | |
background: white; | |
border-bottom: 1px solid #ddd; | |
} | |
.form input, | |
.form select { | |
width: 100%; | |
padding: 8px; | |
margin-bottom: 8px; | |
font-size: 14px; | |
border: 1px solid #ccc; | |
border-radius: 4px; | |
} | |
.form button, | |
.controls button { | |
padding: 8px 12px; | |
font-size: 14px; | |
margin-right: 10px; | |
border-radius: 4px; | |
cursor: pointer; | |
border: none; | |
background: #007aff; | |
color: white; | |
} | |
.letter-nav { | |
position: fixed; | |
right: 10px; | |
top: 60px; | |
line-height: 1.4; | |
} | |
.letter-nav a { | |
display: block; | |
color: #d00; | |
text-decoration: none; | |
font-weight: bold; | |
} | |
.item-group { | |
padding: 10px 15px; | |
} | |
.item-group h2 { | |
margin: 10px 0; | |
font-size: 18px; | |
border-bottom: 1px solid #ccc; | |
} | |
.item { | |
display: flex; | |
align-items: center; | |
padding: 6px 0; | |
} | |
.item img { | |
width: 40px; | |
height: 40px; | |
border-radius: 4px; | |
margin-right: 10px; | |
object-fit: cover; | |
} | |
.item .info .title { | |
font-weight: bold; | |
} | |
.item .info .desc, | |
.item .info .type { | |
font-size: 12px; | |
color: #666; | |
} | |
.grid-view .item-group h2 { | |
display: none; | |
} | |
.grid-view .item-group { | |
display: flex; | |
flex-wrap: wrap; | |
gap: 10px; | |
} | |
.grid-view .item { | |
flex: 1 1 calc(50% - 10px); | |
background: #fff; | |
border: 1px solid #ddd; | |
border-radius: 6px; | |
padding: 10px; | |
box-sizing: border-box; | |
display: block; | |
} | |
.grid-view .item img { | |
width: 100%; | |
height: 120px; | |
object-fit: cover; | |
border-radius: 4px; | |
margin-bottom: 8px; | |
} | |
.grid-view .item .info { | |
text-align: center; | |
} | |
.grid-view .item .info .type { | |
font-size: 11px; | |
background: #eee; | |
padding: 2px 5px; | |
display: inline-block; | |
margin-top: 4px; | |
border-radius: 4px; | |
} | |
body.dark-mode { | |
background-color: #111; | |
color: #f5f5f5; | |
} | |
body.dark-mode .search-bar, | |
body.dark-mode .form, | |
body.dark-mode .controls { | |
background: #1a1a1a; | |
border-color: #333; | |
} | |
body.dark-mode input, | |
body.dark-mode select { | |
background: #222; | |
color: #f5f5f5; | |
border-color: #444; | |
} | |
body.dark-mode .item { | |
background: #1e1e1e; | |
border-color: #333; | |
} | |
body.dark-mode .item .desc, | |
body.dark-mode .item .type { | |
color: #aaa; | |
} | |
body.dark-mode .letter-nav a { | |
color: #0f0; | |
} | |
body.dark-mode button { | |
background: #333; | |
color: #f5f5f5; | |
} | |
.modal { | |
position: fixed; | |
top: 0; | |
left: 0; | |
width: 100vw; | |
height: 100vh; | |
background: rgba(0, 0, 0, 0.6); | |
display: flex; | |
align-items: center; | |
justify-content: center; | |
z-index: 999; | |
} | |
.modal-content { | |
background: white; | |
padding: 20px; | |
border-radius: 10px; | |
max-width: 400px; | |
width: 90%; | |
} | |
body.dark-mode .modal-content { | |
background: #1c1c1c; | |
color: #f5f5f5; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment