Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Save jasondavis/f568989dda8c1b712a3e001aae41793c to your computer and use it in GitHub Desktop.
Save jasondavis/f568989dda8c1b712a3e001aae41793c to your computer and use it in GitHub Desktop.
Collection Manager Web App
<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>
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);
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