diff --git a/assets/cash-money.mp3 b/assets/cash-money.mp3 new file mode 100644 index 0000000..04abc8e Binary files /dev/null and b/assets/cash-money.mp3 differ diff --git a/assets/icon.png b/assets/icon.png new file mode 100644 index 0000000..ec545cd Binary files /dev/null and b/assets/icon.png differ diff --git a/css/base.css b/css/base.css new file mode 100644 index 0000000..8626bbc --- /dev/null +++ b/css/base.css @@ -0,0 +1,89 @@ +/* base.css */ +:root { + --background-color: #f0f0f0; + --text-color: #333; + --card-background: #ffffff; + --button-background: #007acc; + --button-hover: #005fa3; +} + +body { + font-family: 'Sorts Mill Goudy', serif; + background-color: var(--background-color); + color: var(--text-color); + margin: 0; + padding: 20px; +} + +.container { + max-width: 600px; + margin: 0 auto; + background-color: var(--card-background); + padding: 20px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; +} + +h1 { + text-align: center; + margin-bottom: 20px; +} + +form div { + margin-bottom: 15px; +} + +label { + display: block; + margin-bottom: 5px; +} + +input, select, textarea { + width: 100%; + padding: 8px; + box-sizing: border-box; + border: 1px solid #ccc; + border-radius: 4px; + background-color: var(--card-background); + color: var(--text-color); +} + +button { + display: block; + width: 100%; + padding: 10px; + background-color: var(--button-background); + color: white; + border: none; + border-radius: 4px; + cursor: pointer; + margin-top: 10px; +} + +button:hover { + background-color: var(--button-hover); +} + +#csv-output { + margin-top: 20px; + height: 200px; + background-color: var(--card-background); + color: var(--text-color); +} + +footer { + text-align: center; + font-size: 0.5rem; + color: #666; + margin-top: 0px; + padding: 1px; +} + +footer a { + color: #007acc; + text-decoration: none; +} + +footer a:hover { + text-decoration: underline; +} diff --git a/css/components.css b/css/components.css new file mode 100644 index 0000000..2350708 --- /dev/null +++ b/css/components.css @@ -0,0 +1,106 @@ +/* components.css */ +.mana-icon { + font-family: 'mana', sans-serif; + font-size: 24px; + cursor: pointer; + padding: 5px; + margin-right: 10px; +} + +.mana-icon:hover { + opacity: 0.8; +} + + +.menu { + position: relative; + display: inline-block; +} + +.hamburger-menu { + cursor: pointer; + font-size: 30px; + color: var(--text-color); +} + +.menu-content { + display: none; /* Ensure it's hidden by default */ + position: absolute; + top: 40px; + left: 0; + background-color: var(--background-color); + box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); + padding: 10px; + z-index: 1; + min-width: 150px; + border-radius: 4px; +} + +.menu-content.show { + display: block; /* Display menu when toggled */ +} + +.zoom-modal { + display: none; /* Ensure modal is hidden by default */ + position: fixed; + z-index: 1000; + padding-top: 100px; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgba(0, 0, 0, 0.8); +} + +.zoomed-image { + margin: auto; + display: block; + width: 80%; + max-width: 700px; +} + +/* Dark mode styles */ +/* Dark mode styles */ +body.dark-mode .menu-content { + background-color: #333; +} + +body.dark-mode .hamburger-menu, +body.dark-mode .menu-content label, +body.dark-mode .menu-content input { + color: white; +} + +.card-popup { + position: fixed; + bottom: 20px; + right: 20px; + max-width: 20%; + z-index: 1000; + background-color: rgba(0, 0, 0, 0.7); + padding: 10px; + border-radius: 8px; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2); + opacity: 0; + transition: opacity 0.5s ease-in-out; + display: none; +} + +.card-popup img { + width: 100%; + height: auto; + border-radius: 4px; +} + +@media (max-width: 600px) { + .card-popup { + max-width: 40%; + } +} + +@media (max-width: 400px) { + .card-popup { + max-width: 60%; + } +} diff --git a/css/themes.css b/css/themes.css new file mode 100644 index 0000000..55fca66 --- /dev/null +++ b/css/themes.css @@ -0,0 +1,102 @@ +/* Light and Dark mode base theme */ +:root { + --background-color: #f0f0f0; + --text-color: #333; + --card-background: #ffffff; + --button-background: #007acc; + --button-hover: #005fa3; +} + +@media (prefers-color-scheme: dark) { + :root { + --background-color: #1e1e1e; + --text-color: #ffffff; + --card-background: #2e2e2e; + --button-background: #1f6fb2; + --button-hover: #145b8c; + } +} + +/* Mana Color Themes */ +.theme-white { + --background-color: #f9f8f6; + --text-color: #1e1e1e; + --card-background: #ffffff; + --button-background: #e5e5e5; + --button-hover: #cccccc; +} + +.theme-blue { + --background-color: #d7e7f9; + --text-color: #002d72; + --card-background: #e0f2ff; + --button-background: #007acc; + --button-hover: #005fa3; +} + +.theme-black { + --background-color: #333333; + --text-color: #ffffff; + --card-background: #444444; + --button-background: #1f1f1f; + --button-hover: #000000; +} + +.theme-red { + --background-color: #fbe4e4; + --text-color: #8b0000; + --card-background: #ffe5e5; + --button-background: #d32f2f; + --button-hover: #b71c1c; +} + +.theme-green { + --background-color: #e7f4e7; + --text-color: #004d00; + --card-background: #e0ffe0; + --button-background: #388e3c; + --button-hover: #2e7d32; +} + +/* Dark Mode Variants for Mana Themes */ +@media (prefers-color-scheme: dark) { + .theme-white { + --background-color: #1e1e1e; + --text-color: #ffffff; + --card-background: #333333; + --button-background: #555555; + --button-hover: #777777; + } + + .theme-blue { + --background-color: #001f3f; + --text-color: #ffffff; + --card-background: #002d72; + --button-background: #005fa3; + --button-hover: #007acc; + } + + .theme-black { + --background-color: #000000; + --text-color: #e5e5e5; + --card-background: #1c1c1c; + --button-background: #333333; + --button-hover: #555555; + } + + .theme-red { + --background-color: #330000; + --text-color: #ffaaaa; + --card-background: #8b0000; + --button-background: #b71c1c; + --button-hover: #d32f2f; + } + + .theme-green { + --background-color: #002200; + --text-color: #ccffcc; + --card-background: #004d00; + --button-background: #2e7d32; + --button-hover: #388e3c; + } +} diff --git a/index.html b/index.html index ff0e330..b9bdfd9 100644 --- a/index.html +++ b/index.html @@ -1,28 +1,33 @@ + MTG Card CSV Generator - + + + + + - - + +
- + + + + + + + + + diff --git a/js/api.js b/js/api.js new file mode 100644 index 0000000..5c81406 --- /dev/null +++ b/js/api.js @@ -0,0 +1,37 @@ +// api.js + +export async function fetchCardSuggestions(query) { + try { + const response = await fetch(`https://api.scryfall.com/cards/autocomplete?q=${encodeURIComponent(query)}`); + if (!response.ok) { + throw new Error('Network response was not ok'); + } + const data = await response.json(); + return data.data; + } catch (error) { + console.error('Fetch error:', error); + return []; + } +} + +export async function fetchCardDetails(cardName, setCode) { + const response = await fetch(`https://api.scryfall.com/cards/named?exact=${encodeURIComponent(cardName)}&set=${encodeURIComponent(setCode)}`); + const data = await response.json(); + + if (data.object === 'error') { + return null; + } + + return { + set: data.set.toUpperCase(), + card_id: data.collector_number, + mana_cost: data.mana_cost || 'N/A', + power: data.power || 'N/A', + toughness: data.toughness || 'N/A', + type: data.type_line.split('—')[0].trim(), + rarity: data.rarity || 'N/A', + price: data.prices.usd || '0.00', + imageUrl: data.image_uris ? data.image_uris.small : null, + name: data.name + }; +} diff --git a/js/card.js b/js/card.js new file mode 100644 index 0000000..5ee6d2f --- /dev/null +++ b/js/card.js @@ -0,0 +1,68 @@ +// card.js + +import { updateCardDetails, loadCollection } from './storage.js'; +import { showCardPopup } from './popup.js'; + +export async function addCard(cardList) { + const cardName = document.getElementById('card-name').value; + const setCodeInput = document.getElementById('set-code'); + const cardIdInput = document.getElementById('card-id'); + + let setCode = setCodeInput.value; + let cardId = cardIdInput.value; + + const cardDetails = await fetchCardDetails(cardName, setCode); + + if (cardDetails) { + if (!setCode) { + setCode = cardDetails.set; + setCodeInput.value = setCode; + } + if (!cardId) { + cardId = cardDetails.card_id; + cardIdInput.value = cardId; + } + + const cardData = { + name: cardName, + set: setCode, + card_id: cardId, + quantity: document.getElementById('quantity').value, + foil: document.getElementById('foil').value, + mana_cost: cardDetails.mana_cost, + power: cardDetails.power, + toughness: cardDetails.toughness, + type: cardDetails.type, + rarity: cardDetails.rarity, + price: cardDetails.price + }; + + updateCardDetails(cardData); + cardList.push(cardData); // Add the card to the list + showCardPopup(cardData.name, cardDetails.imageUrl, cardDetails.price); + } else { + alert("Card not found with the given name."); + } +} + +export async function fetchCardDetails(cardName, setCode) { + const response = await fetch(`https://api.scryfall.com/cards/named?exact=${encodeURIComponent(cardName)}&set=${encodeURIComponent(setCode)}`); + const data = await response.json(); + + if (data.object === 'error') { + return null; + } + + return { + set: data.set.toUpperCase(), + card_id: data.collector_number, + mana_cost: data.mana_cost || 'N/A', + power: data.power || 'N/A', + toughness: data.toughness || 'N/A', + type: data.type_line.split('—')[0].trim(), + rarity: data.rarity || 'N/A', + price: data.prices.usd || '0.00', + imageUrl: data.image_uris ? data.image_uris.small : null, + name: data.name + }; +} diff --git a/js/csv.js b/js/csv.js new file mode 100644 index 0000000..314a930 --- /dev/null +++ b/js/csv.js @@ -0,0 +1,44 @@ +// csv.js + +import { getRandomWordsFromFile } from './randomWords.js'; + +export function generateCSVContent(cardList) { + let csvContent = "Card Name,Set,Card ID,Quantity,Foil,Mana Type,Power,Toughness,Type,Rarity\n"; + cardList.forEach(card => { + const escapedValues = [ + card.name, + card.set, + card.card_id, + card.quantity, + card.foil, + card.mana_cost, + card.power, + card.toughness, + card.type, + card.rarity + ].map(value => `"${value.replace(/"/g, '""')}"`); // Escape any existing quotes by doubling them + csvContent += escapedValues.join(",") + "\n"; + }); + return csvContent; +} + +export async function downloadCSV(cardList) { + const csvContent = generateCSVContent(cardList); + const randomFileName = getRandomWordsFromFile(); + + // Create a blob with the CSV content + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const url = URL.createObjectURL(blob); + + // Create a temporary link element + const link = document.createElement("a"); + link.setAttribute("href", url); + link.setAttribute("download", `${randomFileName}.csv`); + document.body.appendChild(link); + + // Trigger the download + link.click(); + + // Clean up and remove the link + document.body.removeChild(link); +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..2d020d3 --- /dev/null +++ b/js/main.js @@ -0,0 +1,93 @@ +// main.js + +import { addCard } from './card.js'; +import { downloadCSV } from './csv.js'; +import { saveCollection, loadCollection, clearCollection } from './storage.js'; +import { setTheme, applySavedTheme } from './theme.js'; +import { fetchCardSuggestions } from './api.js'; + +document.addEventListener('DOMContentLoaded', () => { + applySavedTheme(); + + let cardList = loadCollection(); + + document.getElementById('add-card-button').addEventListener('click', () => { + addCard(cardList); + updateCSVContent(cardList); + }); + + document.getElementById('download-csv-button').addEventListener('click', () => downloadCSV(cardList)); + document.getElementById('save-collection-button').addEventListener('click', () => saveCollection(cardList)); + + document.getElementById('clear-collection-button').addEventListener('click', () => { + if (confirm("Are you sure you want to clear your collection? This action cannot be undone.")) { + cardList = []; // Reset the card list + clearCollection(cardList); + updateCSVContent(cardList); + } + }); + + document.getElementById('card-name').addEventListener('input', async function() { + const query = this.value.trim(); + if (query.length < 2) { + document.getElementById('suggestions').innerHTML = ''; + return; + } + const suggestions = await fetchCardSuggestions(query); + const suggestionsDiv = document.getElementById('suggestions'); + suggestionsDiv.innerHTML = ''; + + suggestions.forEach(suggestion => { + const div = document.createElement('div'); + div.textContent = suggestion; + div.onclick = () => { + document.getElementById('card-name').value = suggestion; + suggestionsDiv.innerHTML = ''; + }; + suggestionsDiv.appendChild(div); + }); + }); + + document.getElementById('sound-threshold').addEventListener('input', (event) => { + const soundThreshold = parseFloat(event.target.value); + localStorage.setItem('soundThreshold', soundThreshold); + }); + + document.getElementById('hamburger-menu').addEventListener('click', () => { + const menuContent = document.getElementById('menu-content'); + menuContent.classList.toggle('show'); + }); + + const isDarkMode = window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches; + if (isDarkMode) { + document.body.classList.add('dark-mode'); + } +}); + +// Update the CSV content in the textarea +function updateCSVContent(cardList) { + // Sort the cardList by set and then by card ID + cardList.sort((a, b) => { + if (a.set < b.set) return -1; + if (a.set > b.set) return 1; + return parseInt(a.card_id) - parseInt(b.card_id); + }); + + let csvContent = "Card Name,Set,Card ID,Quantity,Foil,Mana Type,Power,Toughness,Type,Rarity\n"; + cardList.forEach(card => { + const escapedValues = [ + card.name || '', + card.set || '', + card.card_id || '', + card.quantity || '', + card.foil || '', + card.mana_type || '', + card.power || '', + card.toughness || '', + card.type || '', + card.rarity || '' + ].map(value => `"${(value !== undefined ? value : '').replace(/"/g, '""')}"`); + csvContent += escapedValues.join(",") + "\n"; + }); + document.getElementById('csv-output').value = csvContent; +} diff --git a/js/menu.js b/js/menu.js new file mode 100644 index 0000000..634f111 --- /dev/null +++ b/js/menu.js @@ -0,0 +1,4 @@ +export function toggleMenu() { + const menuContent = document.getElementById('menu-content'); + menuContent.classList.toggle('show'); +} diff --git a/js/popup.js b/js/popup.js new file mode 100644 index 0000000..11e52f7 --- /dev/null +++ b/js/popup.js @@ -0,0 +1,40 @@ +// popup.js + +export async function showCardPopup(cardName, imageUrl, price) { + const soundThreshold = parseFloat(localStorage.getItem('soundThreshold')) || 1.00; + const cardPopup = document.getElementById('card-popup'); + + cardPopup.innerHTML = ` +
+ ${cardName} +
+ ${cardName} +
+ Price: $${price} +
+ `; + cardPopup.style.display = 'block'; + cardPopup.style.opacity = 1; + + // Check if the price exceeds the sound threshold and play sound + if (parseFloat(price) >= soundThreshold) { + playSound(); // Play the sound if the price exceeds the threshold + } + + // Dissolve the popup after a few seconds + setTimeout(() => { + cardPopup.style.opacity = 0; + setTimeout(() => { + cardPopup.style.display = 'none'; + }, 500); + }, 3000); +} + +function playSound() { + const audio = new Audio('assets/cash-money.mp3'); + audio.play().then(() => { + console.log('Sound played successfully'); + }).catch(error => { + console.error('Error playing sound:', error); + }); +} diff --git a/js/randomWords.js b/js/randomWords.js new file mode 100644 index 0000000..db17189 --- /dev/null +++ b/js/randomWords.js @@ -0,0 +1,32 @@ +// randomWords.js + +export const randomWords = [ + "nonstop", "detailed", "aspiring", "shock", "play", "bashful", "long", "quarter", + "six", "charge", "dock", "crabby", "dime", "wry", "story", "wiry", "rampant", + "return", "dare", "open", "shame", "many", "brush", "babies", "stem", "squeeze", + "judge", "heavenly", "defeated", "optimal", "invention", "object", "change", + "sink", "verdant", "jaded", "adjoining", "muddled", "switch", "helpless", + "motionless", "skirt", "shrug", "trip", "haircut", "oven", "lip", "habitual", + "yielding", "bag", "wheel", "attach", "ticket", "visit", "reflect", "suppose", + "present", "wound", "voyage", "real", "aunt", "religion", "redundant", "necessary", + "fail", "flower", "unpack", "join", "gamy", "tired", "welcome", "rightful", "jeans", + "obscene", "spring", "basket", "battle", "utter", "descriptive", "caring", "fry", + "resonant", "supply", "geese", "pets", "impulse", "scintillating", "tame", "release", + "tail", "depend", "lively", "nondescript", "punishment", "meek", "crooked", + "representative", "twist", "manage", "bored", "grotesque", "demonic", "camp", + "temporary", "coil", "passenger", "appliance", "clam", "smoggy", "tasteless", "guess", + "verse", "drab", "peep", "business", "paper", "female", "admire", "way", "moor", + "breezy", "opposite", "comparison", "tank", "suit", "ludicrous", "minister", "stiff", + "whine", "request", "camera", "internal", "improve", "unnatural", "decisive", "exist", + "grip", "electric", "bathe", "scandalous", "steer", "humdrum", "action", "rot", + "roll", "quartz", "amused", "sidewalk", "roll", "curve" +]; + +export function getRandomWordsFromFile() { + let randomWordsSelected = []; + for (let i = 0; i < 3; i++) { + const randomIndex = Math.floor(Math.random() * randomWords.length); + randomWordsSelected.push(randomWords[randomIndex]); + } + return randomWordsSelected.join('-'); +} \ No newline at end of file diff --git a/js/storage.js b/js/storage.js new file mode 100644 index 0000000..3b695d4 --- /dev/null +++ b/js/storage.js @@ -0,0 +1,24 @@ +// storage.js + +export function saveCollection(cardList) { + localStorage.setItem('cardList', JSON.stringify(cardList)); +} + +export function loadCollection() { + const savedCollection = localStorage.getItem('cardList'); + if (savedCollection) { + return JSON.parse(savedCollection); + } + return []; +} + +export function updateCardDetails(cardData) { + const cardList = loadCollection(); + cardList.push(cardData); + saveCollection(cardList); +} + +export function clearCollection(cardList) { + cardList.length = 0; // Clear the cardList array + saveCollection(cardList); // Save the empty list to localStorage +} diff --git a/js/sw.js b/js/sw.js new file mode 100644 index 0000000..27567c9 --- /dev/null +++ b/js/sw.js @@ -0,0 +1,54 @@ +// This is the service worker with the combined offline experience (Offline page + Offline copy of pages) + +const CACHE = "pwabuilder-offline-page"; + +importScripts('https://storage.googleapis.com/workbox-cdn/releases/5.1.2/workbox-sw.js'); + +// TODO: replace the following with the correct offline fallback page i.e.: const offlineFallbackPage = "offline.html"; +const offlineFallbackPage = "ToDo-replace-this-name.html"; + +self.addEventListener("message", (event) => { + if (event.data && event.data.type === "SKIP_WAITING") { + self.skipWaiting(); + } +}); + +self.addEventListener('install', async (event) => { + event.waitUntil( + caches.open(CACHE) + .then((cache) => cache.add(offlineFallbackPage)) + ); +}); + +if (workbox.navigationPreload.isSupported()) { + workbox.navigationPreload.enable(); +} + +workbox.routing.registerRoute( + new RegExp('/*'), + new workbox.strategies.StaleWhileRevalidate({ + cacheName: CACHE + }) +); + +self.addEventListener('fetch', (event) => { + if (event.request.mode === 'navigate') { + event.respondWith((async () => { + try { + const preloadResp = await event.preloadResponse; + + if (preloadResp) { + return preloadResp; + } + + const networkResp = await fetch(event.request); + return networkResp; + } catch (error) { + + const cache = await caches.open(CACHE); + const cachedResp = await cache.match(offlineFallbackPage); + return cachedResp; + } + })()); + } +}); \ No newline at end of file diff --git a/js/theme.js b/js/theme.js new file mode 100644 index 0000000..d57cfa1 --- /dev/null +++ b/js/theme.js @@ -0,0 +1,19 @@ +export function setTheme(theme) { + // Remove all theme classes first + document.body.classList.remove('theme-white', 'theme-blue', 'theme-black', 'theme-red', 'theme-green'); + + // Add the selected theme class if it exists + if (theme) { + document.body.classList.add(theme); + } + + // Save the selected theme to localStorage + localStorage.setItem('selectedTheme', theme); +} + +export function applySavedTheme() { + const savedTheme = localStorage.getItem('selectedTheme'); + if (savedTheme) { + setTheme(savedTheme); + } +} diff --git a/manifest.json b/manifest.json index f45d790..09598b9 100644 --- a/manifest.json +++ b/manifest.json @@ -8,12 +8,12 @@ "description": "A PWA for generating CSV files with Magic: The Gathering card details", "icons": [ { - "src": "icon.png", + "src": "assets/icon.png", "sizes": "192x192", "type": "image/png" }, { - "src": "icon.png", + "src": "assets/icon.png", "sizes": "512x512", "type": "image/png" } diff --git a/offline.html b/offline.html index e0db80a..3e36e2f 100644 --- a/offline.html +++ b/offline.html @@ -8,8 +8,7 @@ - - +
@@ -27,9 +26,5 @@ This app is not affiliated with Wizards of the Coast or Hasbro. All rights and trademarks are owned by their respective owners.

- -