From a077c40c5a4b9a4eeb119eac70cdb89e3c0515b8 Mon Sep 17 00:00:00 2001 From: Reynier Matthieu Date: Thu, 6 Mar 2025 15:49:44 +0100 Subject: [PATCH] add chunk to get cards collection from scryfall --- .bolt/config.json | 4 +- .bolt/prompt | 12 +- .env | 2 +- .gitignore | 46 +- LICENSE | 40 +- eslint.config.js | 54 +- index.html | 24 +- postcss.config.js | 10 +- src/App.tsx | 180 +-- src/components/CardCarousel.tsx | 92 +- src/components/CardSearch.tsx | 1084 +++++++-------- src/components/Collection.tsx | 240 ++-- src/components/DeckCard.tsx | 152 +-- src/components/DeckEditor.tsx | 168 +-- src/components/DeckList.tsx | 196 +-- src/components/DeckManager.tsx | 1206 ++++++++--------- src/components/LifeCounter.tsx | 332 ++--- src/components/LoginForm.tsx | 292 ++-- src/components/MagicCard.tsx | 60 +- src/components/ManaIcons.tsx | 212 +-- src/components/Navigation.tsx | 396 +++--- src/components/Profile.tsx | 254 ++-- src/contexts/AuthContext.tsx | 212 +-- src/index.css | 30 +- src/lib/supabase.ts | 18 +- src/main.tsx | 20 +- src/services/api.ts | 43 +- src/types/index.ts | 72 +- src/utils/deckValidation.ts | 160 +-- src/utils/theme.ts | 82 +- .../migrations/20250131132458_black_frost.sql | 284 ++-- tailwind.config.js | 26 +- tsconfig.app.json | 48 +- tsconfig.json | 12 +- tsconfig.node.json | 42 +- vite.config.ts | 18 +- 36 files changed, 3068 insertions(+), 3055 deletions(-) diff --git a/.bolt/config.json b/.bolt/config.json index 6b6787d..4745085 100644 --- a/.bolt/config.json +++ b/.bolt/config.json @@ -1,3 +1,3 @@ -{ - "template": "bolt-vite-react-ts" +{ + "template": "bolt-vite-react-ts" } diff --git a/.bolt/prompt b/.bolt/prompt index 3e7687f..a01f8fe 100644 --- a/.bolt/prompt +++ b/.bolt/prompt @@ -1,7 +1,7 @@ -For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production. - -By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them. - -Use icons from lucide-react for logos. - +For all designs I ask you to make, have them be beautiful, not cookie cutter. Make webpages that are fully featured and worthy for production. + +By default, this template supports JSX syntax with Tailwind CSS classes, React hooks, and Lucide React for icons. Do not install other packages for UI themes, icons, etc unless absolutely necessary or I request them. + +Use icons from lucide-react for logos. + Use stock photos from unsplash where appropriate, only valid URLs you know exist. Do not download the images, only link to them in image tags. diff --git a/.env b/.env index 4ce15c5..60cdee0 100644 --- a/.env +++ b/.env @@ -1,2 +1,2 @@ -VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InllZGdoanJweXhoeGVzbmJ0YmlwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzgzMjk2OTEsImV4cCI6MjA1MzkwNTY5MX0.pKO-axLvkHtTOMEtEkCPz2yY2khm9RKzkqcFkl_VM_U +VITE_SUPABASE_ANON_KEY=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InllZGdoanJweXhoeGVzbmJ0YmlwIiwicm9sZSI6ImFub24iLCJpYXQiOjE3MzgzMjk2OTEsImV4cCI6MjA1MzkwNTY5MX0.pKO-axLvkHtTOMEtEkCPz2yY2khm9RKzkqcFkl_VM_U VITE_SUPABASE_URL=https://yedghjrpyxhxesnbtbip.supabase.co diff --git a/.gitignore b/.gitignore index a547bf3..98c0612 100644 --- a/.gitignore +++ b/.gitignore @@ -1,24 +1,24 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln *.sw? diff --git a/LICENSE b/LICENSE index bea5a63..997dd93 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,21 @@ -MIT License - -Copyright (c) 2025 Reynier Matthieu - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +MIT License + +Copyright (c) 2025 Reynier Matthieu + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/eslint.config.js b/eslint.config.js index 82c2e20..4cf9e7e 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,28 +1,28 @@ -import js from '@eslint/js'; -import globals from 'globals'; -import reactHooks from 'eslint-plugin-react-hooks'; -import reactRefresh from 'eslint-plugin-react-refresh'; -import tseslint from 'typescript-eslint'; - -export default tseslint.config( - { ignores: ['dist'] }, - { - extends: [js.configs.recommended, ...tseslint.configs.recommended], - files: ['**/*.{ts,tsx}'], - languageOptions: { - ecmaVersion: 2020, - globals: globals.browser, - }, - plugins: { - 'react-hooks': reactHooks, - 'react-refresh': reactRefresh, - }, - rules: { - ...reactHooks.configs.recommended.rules, - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, - } +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist'] }, + { + extends: [js.configs.recommended, ...tseslint.configs.recommended], + files: ['**/*.{ts,tsx}'], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser, + }, + plugins: { + 'react-hooks': reactHooks, + 'react-refresh': reactRefresh, + }, + rules: { + ...reactHooks.configs.recommended.rules, + 'react-refresh/only-export-components': [ + 'warn', + { allowConstantExport: true }, + ], + }, + } ); diff --git a/index.html b/index.html index e4b78ea..c390ea3 100644 --- a/index.html +++ b/index.html @@ -1,13 +1,13 @@ - - - - - - - Vite + React + TS - - -
- - + + + + + + + Vite + React + TS + + +
+ + diff --git a/postcss.config.js b/postcss.config.js index 2aa7205..4d7e202 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,6 +1,6 @@ -export default { - plugins: { - tailwindcss: {}, - autoprefixer: {}, - }, +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, }; diff --git a/src/App.tsx b/src/App.tsx index 3c69e3a..c009edc 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,91 +1,91 @@ -import React, { useState } from 'react'; - import DeckManager from './components/DeckManager'; - import DeckList from './components/DeckList'; - import LoginForm from './components/LoginForm'; - import Navigation from './components/Navigation'; - import Collection from './components/Collection'; - import DeckEditor from './components/DeckEditor'; - import Profile from './components/Profile'; - import CardSearch from './components/CardSearch'; - import LifeCounter from './components/LifeCounter'; - import { AuthProvider, useAuth } from './contexts/AuthContext'; - - type Page = 'home' | 'deck' | 'login' | 'collection' | 'edit-deck' | 'profile' | 'search' | 'life-counter'; - - function AppContent() { - const [currentPage, setCurrentPage] = useState('home'); - const [selectedDeckId, setSelectedDeckId] = useState(null); - const { user, loading } = useAuth(); - - if (loading) { - return ( -
-
-
- ); - } - - if (!user && currentPage !== 'login') { - return ; - } - - const handleDeckEdit = (deckId: string) => { - setSelectedDeckId(deckId); - setCurrentPage('edit-deck'); - }; - - const renderPage = () => { - switch (currentPage) { - case 'home': - return ( -
-
-

My Decks

- -
-
- ); - case 'deck': - return ; - case 'edit-deck': - return selectedDeckId ? ( - { - setSelectedDeckId(null); - setCurrentPage('home'); - }} - /> - ) : null; - case 'collection': - return ; - case 'profile': - return ; - case 'search': - return ; - case 'life-counter': - return ; - case 'login': - return ; - default: - return null; - } - }; - - return ( -
- - {renderPage()} -
- ); - } - - function App() { - return ( - - - - ); - } - +import React, { useState } from 'react'; + import DeckManager from './components/DeckManager'; + import DeckList from './components/DeckList'; + import LoginForm from './components/LoginForm'; + import Navigation from './components/Navigation'; + import Collection from './components/Collection'; + import DeckEditor from './components/DeckEditor'; + import Profile from './components/Profile'; + import CardSearch from './components/CardSearch'; + import LifeCounter from './components/LifeCounter'; + import { AuthProvider, useAuth } from './contexts/AuthContext'; + + type Page = 'home' | 'deck' | 'login' | 'collection' | 'edit-deck' | 'profile' | 'search' | 'life-counter'; + + function AppContent() { + const [currentPage, setCurrentPage] = useState('home'); + const [selectedDeckId, setSelectedDeckId] = useState(null); + const { user, loading } = useAuth(); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!user && currentPage !== 'login') { + return ; + } + + const handleDeckEdit = (deckId: string) => { + setSelectedDeckId(deckId); + setCurrentPage('edit-deck'); + }; + + const renderPage = () => { + switch (currentPage) { + case 'home': + return ( +
+
+

My Decks

+ +
+
+ ); + case 'deck': + return ; + case 'edit-deck': + return selectedDeckId ? ( + { + setSelectedDeckId(null); + setCurrentPage('home'); + }} + /> + ) : null; + case 'collection': + return ; + case 'profile': + return ; + case 'search': + return ; + case 'life-counter': + return ; + case 'login': + return ; + default: + return null; + } + }; + + return ( +
+ + {renderPage()} +
+ ); + } + + function App() { + return ( + + + + ); + } + export default App; diff --git a/src/components/CardCarousel.tsx b/src/components/CardCarousel.tsx index d0c580c..60d06dd 100644 --- a/src/components/CardCarousel.tsx +++ b/src/components/CardCarousel.tsx @@ -1,47 +1,47 @@ -import { useEffect, useState } from 'react'; -import { Card } from '../types'; -import { getRandomCards } from '../services/api'; - -export default function CardCarousel() { - const [cards, setCards] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const loadCards = async () => { - try { - const randomCards = await getRandomCards(6); - setCards(randomCards); - } catch (error) { - console.error('Failed to load cards:', error); - } finally { - setLoading(false); - } - }; - - loadCards(); - }, []); - - if (loading) { - return
; - } - - return ( -
-
- {cards.map((card, index) => ( -
- ))} -
-
- ); +import { useEffect, useState } from 'react'; +import { Card } from '../types'; +import { getRandomCards } from '../services/api'; + +export default function CardCarousel() { + const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadCards = async () => { + try { + const randomCards = await getRandomCards(6); + setCards(randomCards); + } catch (error) { + console.error('Failed to load cards:', error); + } finally { + setLoading(false); + } + }; + + loadCards(); + }, []); + + if (loading) { + return
; + } + + return ( +
+
+ {cards.map((card, index) => ( +
+ ))} +
+
+ ); } diff --git a/src/components/CardSearch.tsx b/src/components/CardSearch.tsx index fd8c3a4..567ee3c 100644 --- a/src/components/CardSearch.tsx +++ b/src/components/CardSearch.tsx @@ -1,543 +1,543 @@ -import React, { useState } from 'react'; -import { searchCards } from '../services/api'; -import { Card } from '../types'; -import MagicCard from './MagicCard'; - -const CardSearch = () => { - const [cardName, setCardName] = useState(''); - const [text, setText] = useState(''); - const [rulesText, setRulesText] = useState(''); - const [typeLine, setTypeLine] = useState(''); - const [typeMatch, setTypeMatch] = useState('partial'); - const [typeInclude, setTypeInclude] = useState(true); - const [colors, setColors] = useState({ W: false, U: false, B: false, R: false, G: false, C: false }); - const [colorMode, setColorMode] = useState('exactly'); - const [commanderColors, setCommanderColors] = useState({ W: false, U: false, B: false, R: false, G: false, C: false }); - const [manaCost, setManaCost] = useState({ W: 0, U: 0, B: 0, R: 0, G: 0, C: 0 }); - const [manaValue, setManaValue] = useState(''); - const [manaValueComparison, setManaValueComparison] = useState('='); - const [games, setGames] = useState({ paper: false, arena: false, mtgo: false }); - const [format, setFormat] = useState(''); - const [formatStatus, setFormatStatus] = useState(''); - const [set, setSet] = useState(''); - const [block, setBlock] = useState(''); - const [rarity, setRarity] = useState({ common: false, uncommon: false, rare: false, mythic: false }); - const [criteria, setCriteria] = useState(''); - const [criteriaMatch, setCriteriaMatch] = useState('partial'); - const [criteriaInclude, setCriteriaInclude] = useState(true); - const [price, setPrice] = useState(''); - const [currency, setCurrency] = useState('usd'); - const [priceComparison, setPriceComparison] = useState('='); - const [artist, setArtist] = useState(''); - const [flavorText, setFlavorText] = useState(''); - const [loreFinder, setLoreFinder] = useState(''); - const [language, setLanguage] = useState('en'); - const [displayImages, setDisplayImages] = useState(false); - const [order, setOrder] = useState('name'); - const [showAllPrints, setShowAllPrints] = useState(false); - const [includeExtras, setIncludeExtras] = useState(false); - const [searchResults, setSearchResults] = useState([]); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - setLoading(true); - setError(null); - - let query = ''; - - if (cardName) query += `name:${cardName} `; - if (text) query += `o:${text} `; - if (rulesText) query += `o:"${rulesText.replace('~', cardName)}" `; - if (typeLine) { - const typeQuery = typeMatch === 'partial' ? typeLine : `"${typeLine}"`; - query += `${typeInclude ? '' : '-'}t:${typeQuery} `; - } - if (Object.values(colors).some(Boolean)) { - const activeColors = Object.keys(colors).filter((key) => colors[key as keyof typeof colors]).join(''); - const colorQuery = colorMode === 'exactly' ? `c:${activeColors}` : `color<=${activeColors}`; - query += `${colorQuery} `; - } - if (Object.values(commanderColors).some(Boolean)) { - const activeColors = Object.keys(commanderColors).filter((key) => commanderColors[key as keyof typeof commanderColors]).join(''); - query += `id:${activeColors} `; - } - - const manaCostString = Object.entries(manaCost) - .filter(([, count]) => count > 0) - .map(([color, count]) => `{${color}}`.repeat(count)) - .join(''); - - if (manaCostString) query += `m:${manaCostString} `; - - if (manaValue) query += `mv${manaValueComparison}${manaValue} `; - if (Object.values(games).some(Boolean)) { - const activeGames = Object.keys(games).filter((key) => games[key as keyof typeof games]).join(','); - query += `game:${activeGames} `; - } - if (format) query += `f:${format} `; - if (formatStatus) query += `${formatStatus}:${format} `; - if (set) query += `e:${set} `; - if (block) query += `b:${block} `; - if (Object.values(rarity).some(Boolean)) { - const activeRarities = Object.keys(rarity).filter((key) => rarity[key as keyof typeof rarity]).join(','); - query += `r:${activeRarities} `; - } - if (criteria) { - const criteriaQuery = criteriaMatch === 'partial' ? criteria : `"${criteria}"`; - query += `${criteriaInclude ? '' : '-'}o:${criteriaQuery} `; - } - if (price) query += `${currency}${priceComparison}${price} `; - if (artist) query += `a:${artist} `; - if (flavorText) query += `ft:${flavorText} `; - if (loreFinder) query += `${loreFinder} `; - if (language) query += `lang:${language} `; - if (displayImages) query += `display:grid `; - if (order) query += `order:${order} `; - if (showAllPrints) query += `unique:prints `; - if (includeExtras) query += `include:extras `; - - try { - const cards = await searchCards(query.trim()); - setSearchResults(cards || []); - } catch (err) { - setError('Failed to fetch cards.'); - console.error('Error fetching cards:', err); - } finally { - setLoading(false); - } - }; - - return ( -
-
-

Card Search

-
- {/* Card Details */} -
- setCardName(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Card Name" - /> - setText(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Text" - /> - setRulesText(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Rules Text (~ for card name)" - /> -
- setTypeLine(e.target.value)} - className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Type Line" - /> - - -
-
- - {/* Colors */} -
-
-

Card Colors

-
- {Object.entries(colors).map(([color, active]) => ( - - ))} -
- -
-
-

Commander Colors

-
- {Object.entries(commanderColors).map(([color, active]) => ( - - ))} -
-
-
- - {/* Mana Cost */} -
- {Object.entries(manaCost).map(([color, count]) => ( -
- - {color === 'W' ? 'βšͺ' : color === 'U' ? 'πŸ”΅' : color === 'B' ? '⚫' : color === 'R' ? 'πŸ”΄' : color === 'G' ? '🟒' : '🟀'} - - setManaCost({ ...manaCost, [color]: parseInt(e.target.value) })} - className="w-16 px-2 py-1 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - min="0" - /> -
- ))} -
- - {/* Stats */} -
- - setManaValue(e.target.value)} - className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Mana Value" - /> -
- - {/* Games */} -
-

Games

-
- {['paper', 'arena', 'mtgo'].map((game) => ( - - ))} -
-
- - {/* Formats */} -
- - -
- - {/* Sets */} -
- setSet(e.target.value)} - className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Set Code" - /> - setBlock(e.target.value)} - className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Block Code" - /> -
- - {/* Rarity */} -
-

Rarity

-
- {['common', 'uncommon', 'rare', 'mythic'].map((r) => ( - - ))} -
-
- - {/* Criteria */} -
- setCriteria(e.target.value)} - className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Criteria" - /> - - -
- - {/* Prices */} -
- - - setPrice(e.target.value)} - className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Price" - /> -
- - {/* Additional Filters */} -
- setArtist(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Artist" - /> - setFlavorText(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Flavor Text" - /> - setLoreFinder(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Lore Finderβ„’" - /> - -
- - {/* Preferences */} -
- - - - -
- - -
- - {loading && ( -
-
-
- )} - - {error && ( -
- {error} -
- )} - - {searchResults && searchResults.length > 0 && ( -
- {searchResults.map((card) => ( -
- -
-

{card.name}

-

{card.type_line}

-
-
- ))} -
- )} -
-
- ); -}; - +import React, { useState } from 'react'; +import { searchCards } from '../services/api'; +import { Card } from '../types'; +import MagicCard from './MagicCard'; + +const CardSearch = () => { + const [cardName, setCardName] = useState(''); + const [text, setText] = useState(''); + const [rulesText, setRulesText] = useState(''); + const [typeLine, setTypeLine] = useState(''); + const [typeMatch, setTypeMatch] = useState('partial'); + const [typeInclude, setTypeInclude] = useState(true); + const [colors, setColors] = useState({ W: false, U: false, B: false, R: false, G: false, C: false }); + const [colorMode, setColorMode] = useState('exactly'); + const [commanderColors, setCommanderColors] = useState({ W: false, U: false, B: false, R: false, G: false, C: false }); + const [manaCost, setManaCost] = useState({ W: 0, U: 0, B: 0, R: 0, G: 0, C: 0 }); + const [manaValue, setManaValue] = useState(''); + const [manaValueComparison, setManaValueComparison] = useState('='); + const [games, setGames] = useState({ paper: false, arena: false, mtgo: false }); + const [format, setFormat] = useState(''); + const [formatStatus, setFormatStatus] = useState(''); + const [set, setSet] = useState(''); + const [block, setBlock] = useState(''); + const [rarity, setRarity] = useState({ common: false, uncommon: false, rare: false, mythic: false }); + const [criteria, setCriteria] = useState(''); + const [criteriaMatch, setCriteriaMatch] = useState('partial'); + const [criteriaInclude, setCriteriaInclude] = useState(true); + const [price, setPrice] = useState(''); + const [currency, setCurrency] = useState('usd'); + const [priceComparison, setPriceComparison] = useState('='); + const [artist, setArtist] = useState(''); + const [flavorText, setFlavorText] = useState(''); + const [loreFinder, setLoreFinder] = useState(''); + const [language, setLanguage] = useState('en'); + const [displayImages, setDisplayImages] = useState(false); + const [order, setOrder] = useState('name'); + const [showAllPrints, setShowAllPrints] = useState(false); + const [includeExtras, setIncludeExtras] = useState(false); + const [searchResults, setSearchResults] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + setLoading(true); + setError(null); + + let query = ''; + + if (cardName) query += `name:${cardName} `; + if (text) query += `o:${text} `; + if (rulesText) query += `o:"${rulesText.replace('~', cardName)}" `; + if (typeLine) { + const typeQuery = typeMatch === 'partial' ? typeLine : `"${typeLine}"`; + query += `${typeInclude ? '' : '-'}t:${typeQuery} `; + } + if (Object.values(colors).some(Boolean)) { + const activeColors = Object.keys(colors).filter((key) => colors[key as keyof typeof colors]).join(''); + const colorQuery = colorMode === 'exactly' ? `c:${activeColors}` : `color<=${activeColors}`; + query += `${colorQuery} `; + } + if (Object.values(commanderColors).some(Boolean)) { + const activeColors = Object.keys(commanderColors).filter((key) => commanderColors[key as keyof typeof commanderColors]).join(''); + query += `id:${activeColors} `; + } + + const manaCostString = Object.entries(manaCost) + .filter(([, count]) => count > 0) + .map(([color, count]) => `{${color}}`.repeat(count)) + .join(''); + + if (manaCostString) query += `m:${manaCostString} `; + + if (manaValue) query += `mv${manaValueComparison}${manaValue} `; + if (Object.values(games).some(Boolean)) { + const activeGames = Object.keys(games).filter((key) => games[key as keyof typeof games]).join(','); + query += `game:${activeGames} `; + } + if (format) query += `f:${format} `; + if (formatStatus) query += `${formatStatus}:${format} `; + if (set) query += `e:${set} `; + if (block) query += `b:${block} `; + if (Object.values(rarity).some(Boolean)) { + const activeRarities = Object.keys(rarity).filter((key) => rarity[key as keyof typeof rarity]).join(','); + query += `r:${activeRarities} `; + } + if (criteria) { + const criteriaQuery = criteriaMatch === 'partial' ? criteria : `"${criteria}"`; + query += `${criteriaInclude ? '' : '-'}o:${criteriaQuery} `; + } + if (price) query += `${currency}${priceComparison}${price} `; + if (artist) query += `a:${artist} `; + if (flavorText) query += `ft:${flavorText} `; + if (loreFinder) query += `${loreFinder} `; + if (language) query += `lang:${language} `; + if (displayImages) query += `display:grid `; + if (order) query += `order:${order} `; + if (showAllPrints) query += `unique:prints `; + if (includeExtras) query += `include:extras `; + + try { + const cards = await searchCards(query.trim()); + setSearchResults(cards || []); + } catch (err) { + setError('Failed to fetch cards.'); + console.error('Error fetching cards:', err); + } finally { + setLoading(false); + } + }; + + return ( +
+
+

Card Search

+
+ {/* Card Details */} +
+ setCardName(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Card Name" + /> + setText(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Text" + /> + setRulesText(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Rules Text (~ for card name)" + /> +
+ setTypeLine(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Type Line" + /> + + +
+
+ + {/* Colors */} +
+
+

Card Colors

+
+ {Object.entries(colors).map(([color, active]) => ( + + ))} +
+ +
+
+

Commander Colors

+
+ {Object.entries(commanderColors).map(([color, active]) => ( + + ))} +
+
+
+ + {/* Mana Cost */} +
+ {Object.entries(manaCost).map(([color, count]) => ( +
+ + {color === 'W' ? 'βšͺ' : color === 'U' ? 'πŸ”΅' : color === 'B' ? '⚫' : color === 'R' ? 'πŸ”΄' : color === 'G' ? '🟒' : '🟀'} + + setManaCost({ ...manaCost, [color]: parseInt(e.target.value) })} + className="w-16 px-2 py-1 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + min="0" + /> +
+ ))} +
+ + {/* Stats */} +
+ + setManaValue(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Mana Value" + /> +
+ + {/* Games */} +
+

Games

+
+ {['paper', 'arena', 'mtgo'].map((game) => ( + + ))} +
+
+ + {/* Formats */} +
+ + +
+ + {/* Sets */} +
+ setSet(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Set Code" + /> + setBlock(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Block Code" + /> +
+ + {/* Rarity */} +
+

Rarity

+
+ {['common', 'uncommon', 'rare', 'mythic'].map((r) => ( + + ))} +
+
+ + {/* Criteria */} +
+ setCriteria(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Criteria" + /> + + +
+ + {/* Prices */} +
+ + + setPrice(e.target.value)} + className="flex-1 px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Price" + /> +
+ + {/* Additional Filters */} +
+ setArtist(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Artist" + /> + setFlavorText(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Flavor Text" + /> + setLoreFinder(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Lore Finderβ„’" + /> + +
+ + {/* Preferences */} +
+ + + + +
+ + +
+ + {loading && ( +
+
+
+ )} + + {error && ( +
+ {error} +
+ )} + + {searchResults && searchResults.length > 0 && ( +
+ {searchResults.map((card) => ( +
+ +
+

{card.name}

+

{card.type_line}

+
+
+ ))} +
+ )} +
+
+ ); +}; + export default CardSearch; diff --git a/src/components/Collection.tsx b/src/components/Collection.tsx index 45f59da..4d24083 100644 --- a/src/components/Collection.tsx +++ b/src/components/Collection.tsx @@ -1,121 +1,121 @@ -import React, { useState } from 'react'; -import { Search, Plus } from 'lucide-react'; -import { Card } from '../types'; -import { searchCards } from '../services/api'; - -export default function Collection() { - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [collection, setCollection] = useState<{ card: Card; quantity: number }[]>([]); - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - if (!searchQuery.trim()) return; - - try { - const cards = await searchCards(searchQuery); - setSearchResults(cards); - } catch (error) { - console.error('Failed to search cards:', error); - } - }; - - const addToCollection = (card: Card) => { - setCollection(prev => { - const existing = prev.find(c => c.card.id === card.id); - if (existing) { - return prev.map(c => - c.card.id === card.id - ? { ...c, quantity: c.quantity + 1 } - : c - ); - } - return [...prev, { card, quantity: 1 }]; - }); - }; - - return ( -
-
-

My Collection

- - {/* Search */} -
-
- - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Search cards to add..." - /> -
- -
- - {/* Search Results */} - {searchResults.length > 0 && ( -
-

Search Results

-
- {searchResults.map(card => ( -
- {card.image_uris?.normal && ( - {card.name} - )} -
-

{card.name}

- -
-
- ))} -
-
- )} - - {/* Collection */} -
-

My Cards

-
- {collection.map(({ card, quantity }) => ( -
- {card.image_uris?.normal && ( - {card.name} - )} -
-
-

{card.name}

- - x{quantity} - -
-
-
- ))} -
-
-
-
- ); +import React, { useState } from 'react'; +import { Search, Plus } from 'lucide-react'; +import { Card } from '../types'; +import { searchCards } from '../services/api'; + +export default function Collection() { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [collection, setCollection] = useState<{ card: Card; quantity: number }[]>([]); + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + if (!searchQuery.trim()) return; + + try { + const cards = await searchCards(searchQuery); + setSearchResults(cards); + } catch (error) { + console.error('Failed to search cards:', error); + } + }; + + const addToCollection = (card: Card) => { + setCollection(prev => { + const existing = prev.find(c => c.card.id === card.id); + if (existing) { + return prev.map(c => + c.card.id === card.id + ? { ...c, quantity: c.quantity + 1 } + : c + ); + } + return [...prev, { card, quantity: 1 }]; + }); + }; + + return ( +
+
+

My Collection

+ + {/* Search */} +
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Search cards to add..." + /> +
+ +
+ + {/* Search Results */} + {searchResults.length > 0 && ( +
+

Search Results

+
+ {searchResults.map(card => ( +
+ {card.image_uris?.normal && ( + {card.name} + )} +
+

{card.name}

+ +
+
+ ))} +
+
+ )} + + {/* Collection */} +
+

My Cards

+
+ {collection.map(({ card, quantity }) => ( +
+ {card.image_uris?.normal && ( + {card.name} + )} +
+
+

{card.name}

+ + x{quantity} + +
+
+
+ ))} +
+
+
+
+ ); } diff --git a/src/components/DeckCard.tsx b/src/components/DeckCard.tsx index 1f18c8a..8632fa8 100644 --- a/src/components/DeckCard.tsx +++ b/src/components/DeckCard.tsx @@ -1,77 +1,77 @@ -import React from 'react'; -import { AlertTriangle, Check, Edit } from 'lucide-react'; -import { Deck } from '../types'; -import { validateDeck } from '../utils/deckValidation'; - -interface DeckCardProps { - deck: Deck; - onEdit?: (deckId: string) => void; -} - -export default function DeckCard({ deck, onEdit }: DeckCardProps) { - - if(deck.id === "410ed539-a8f4-4bc4-91f1-6c113b9b7e25"){ - console.log("deck", deck.name); - console.log("cardEntities", deck.cards); - } - - const validation = validateDeck(deck); - const commander = deck.format === 'commander' ? deck.cards.find(card => - card.is_commander - )?.card : null; - - return ( -
onEdit?.(deck.id)} - > -
- {commander?.name -
-
- -
-
-

{deck.name}

- {validation.isValid ? ( -
- - Legal -
- ) : ( -
- - Issues -
- )} -
- -
- {deck.format} - {deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards -
- - {commander && ( -
- Commander: {commander.name} -
- )} - - -
-
- ); +import React from 'react'; +import { AlertTriangle, Check, Edit } from 'lucide-react'; +import { Deck } from '../types'; +import { validateDeck } from '../utils/deckValidation'; + +interface DeckCardProps { + deck: Deck; + onEdit?: (deckId: string) => void; +} + +export default function DeckCard({ deck, onEdit }: DeckCardProps) { + + if(deck.id === "410ed539-a8f4-4bc4-91f1-6c113b9b7e25"){ + console.log("deck", deck.name); + console.log("cardEntities", deck.cards); + } + + const validation = validateDeck(deck); + const commander = deck.format === 'commander' ? deck.cards.find(card => + card.is_commander + )?.card : null; + + return ( +
onEdit?.(deck.id)} + > +
+ {commander?.name +
+
+ +
+
+

{deck.name}

+ {validation.isValid ? ( +
+ + Legal +
+ ) : ( +
+ + Issues +
+ )} +
+ +
+ {deck.format} + {deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards +
+ + {commander && ( +
+ Commander: {commander.name} +
+ )} + + +
+
+ ); } diff --git a/src/components/DeckEditor.tsx b/src/components/DeckEditor.tsx index 69ec8da..b12ac8a 100644 --- a/src/components/DeckEditor.tsx +++ b/src/components/DeckEditor.tsx @@ -1,85 +1,85 @@ -import React, { useEffect, useState } from 'react'; -import { Card, Deck } from '../types'; -import DeckManager from './DeckManager'; -import { supabase } from '../lib/supabase'; -import { getCardsByIds } from '../services/api'; - -interface DeckEditorProps { - deckId: string; - onClose?: () => void; -} - -export default function DeckEditor({ deckId, onClose }: DeckEditorProps) { - const [deck, setDeck] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const fetchDeck = async () => { - try { - // Fetch deck data - const { data: deckData, error: deckError } = await supabase - .from('decks') - .select('*') - .eq('id', deckId) - .single(); - - if (deckError) throw deckError; - - // Fetch deck cards - const { data: cardEntities, error: cardsError } = await supabase - .from('deck_cards') - .select('*') - .eq('deck_id', deckId); - - if (cardsError) throw cardsError; - - // Fetch card details from Scryfall - const cardIds = cardEntities.map(entity => entity.card_id); - const uniqueCardIds = [...new Set(cardIds)]; - const scryfallCards = await getCardsByIds(uniqueCardIds); - - // Combine deck data with card details - const cards = cardEntities.map(entity => ({ - card: scryfallCards.find(c => c.id === entity.card_id) as Card, - quantity: entity.quantity, - })); - - setDeck({ - ...deckData, - cards, - createdAt: new Date(deckData.created_at), - updatedAt: new Date(deckData.updated_at), - }); - } catch (error) { - console.error('Error fetching deck:', error); - } finally { - setLoading(false); - } - }; - - fetchDeck(); - }, [deckId]); - - if (loading) { - return ( -
-
-
- ); - } - - if (!deck) { - return ( -
-
-
-

Error

-

Failed to load deck

-
-
-
- ); - } - - return ; +import React, { useEffect, useState } from 'react'; +import { Card, Deck } from '../types'; +import DeckManager from './DeckManager'; +import { supabase } from '../lib/supabase'; +import { getCardsByIds } from '../services/api'; + +interface DeckEditorProps { + deckId: string; + onClose?: () => void; +} + +export default function DeckEditor({ deckId, onClose }: DeckEditorProps) { + const [deck, setDeck] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchDeck = async () => { + try { + // Fetch deck data + const { data: deckData, error: deckError } = await supabase + .from('decks') + .select('*') + .eq('id', deckId) + .single(); + + if (deckError) throw deckError; + + // Fetch deck cards + const { data: cardEntities, error: cardsError } = await supabase + .from('deck_cards') + .select('*') + .eq('deck_id', deckId); + + if (cardsError) throw cardsError; + + // Fetch card details from Scryfall + const cardIds = cardEntities.map(entity => entity.card_id); + const uniqueCardIds = [...new Set(cardIds)]; + const scryfallCards = await getCardsByIds(uniqueCardIds); + + // Combine deck data with card details + const cards = cardEntities.map(entity => ({ + card: scryfallCards.find(c => c.id === entity.card_id) as Card, + quantity: entity.quantity, + })); + + setDeck({ + ...deckData, + cards, + createdAt: new Date(deckData.created_at), + updatedAt: new Date(deckData.updated_at), + }); + } catch (error) { + console.error('Error fetching deck:', error); + } finally { + setLoading(false); + } + }; + + fetchDeck(); + }, [deckId]); + + if (loading) { + return ( +
+
+
+ ); + } + + if (!deck) { + return ( +
+
+
+

Error

+

Failed to load deck

+
+
+
+ ); + } + + return ; } diff --git a/src/components/DeckList.tsx b/src/components/DeckList.tsx index ec71656..e1569e6 100644 --- a/src/components/DeckList.tsx +++ b/src/components/DeckList.tsx @@ -1,99 +1,99 @@ -import React, { useEffect, useState } from 'react'; -import { getCardById, getCardsByIds } from '../services/api'; -import { Deck } from '../types'; -import { supabase } from "../lib/supabase"; -import DeckCard from "./DeckCard"; - -interface DeckListProps { - onDeckEdit?: (deckId: string) => void; -} - -const DeckList = ({ onDeckEdit }: DeckListProps) => { - const [decks, setDecks] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const fetchDecks = async () => { - const { data: decksData, error: decksError } = await supabase.from('decks').select('*'); - if (decksError) { - console.error('Error fetching decks:', decksError); - setLoading(false); - return; - } - - const decksWithCards = await Promise.all(decksData.map(async (deck) => { - const { data: cardEntities, error: cardsError } = await supabase - .from('deck_cards') - .select('*') - .eq('deck_id', deck.id); - - - - if (cardsError) { - console.error(`Error fetching cards for deck ${deck.id}:`, cardsError); - return { ...deck, cards: [] }; - } - - const cardIds = cardEntities.map((entity) => entity.card_id); - const uniqueCardIds = [...new Set(cardIds)]; - - if(deck.id === "410ed539-a8f4-4bc4-91f1-6c113b9b7e25"){ - console.log("uniqueCardIds", uniqueCardIds); - } - - - - try { - const scryfallCards = await getCardsByIds(uniqueCardIds); - - if (!scryfallCards) { - console.error("scryfallCards is undefined after getCardsByIds"); - return { ...deck, cards: [] }; - } - - const cards = cardEntities.map((entity) => { - const card = scryfallCards.find((c) => c.id === entity.card_id); - return { - card, - quantity: entity.quantity, - is_commander: entity.is_commander, - }; - }); - - return { - ...deck, - cards, - createdAt: new Date(deck.created_at), - updatedAt: new Date(deck.updated_at), - }; - } catch (error) { - console.error("Error fetching cards from Scryfall:", error); - return { ...deck, cards: [] }; - } - })); - - setDecks(decksWithCards); - setLoading(false); - }; - - fetchDecks(); - }, []); - - if (loading) { - return ( -
-
-
- ); - } - - return ( -
- {decks.map((deck) => ( - - ))} -
- ); -}; - +import React, { useEffect, useState } from 'react'; +import { getCardById, getCardsByIds } from '../services/api'; +import { Deck } from '../types'; +import { supabase } from "../lib/supabase"; +import DeckCard from "./DeckCard"; + +interface DeckListProps { + onDeckEdit?: (deckId: string) => void; +} + +const DeckList = ({ onDeckEdit }: DeckListProps) => { + const [decks, setDecks] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchDecks = async () => { + const { data: decksData, error: decksError } = await supabase.from('decks').select('*'); + if (decksError) { + console.error('Error fetching decks:', decksError); + setLoading(false); + return; + } + + const decksWithCards = await Promise.all(decksData.map(async (deck) => { + const { data: cardEntities, error: cardsError } = await supabase + .from('deck_cards') + .select('*') + .eq('deck_id', deck.id); + + + + if (cardsError) { + console.error(`Error fetching cards for deck ${deck.id}:`, cardsError); + return { ...deck, cards: [] }; + } + + const cardIds = cardEntities.map((entity) => entity.card_id); + const uniqueCardIds = [...new Set(cardIds)]; + + if(deck.id === "410ed539-a8f4-4bc4-91f1-6c113b9b7e25"){ + console.log("uniqueCardIds", uniqueCardIds); + } + + + + try { + const scryfallCards = await getCardsByIds(uniqueCardIds); + + if (!scryfallCards) { + console.error("scryfallCards is undefined after getCardsByIds"); + return { ...deck, cards: [] }; + } + + const cards = cardEntities.map((entity) => { + const card = scryfallCards.find((c) => c.id === entity.card_id); + return { + card, + quantity: entity.quantity, + is_commander: entity.is_commander, + }; + }); + + return { + ...deck, + cards, + createdAt: new Date(deck.created_at), + updatedAt: new Date(deck.updated_at), + }; + } catch (error) { + console.error("Error fetching cards from Scryfall:", error); + return { ...deck, cards: [] }; + } + })); + + setDecks(decksWithCards); + setLoading(false); + }; + + fetchDecks(); + }, []); + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+ {decks.map((deck) => ( + + ))} +
+ ); +}; + export default DeckList; diff --git a/src/components/DeckManager.tsx b/src/components/DeckManager.tsx index cddc79d..ed837ac 100644 --- a/src/components/DeckManager.tsx +++ b/src/components/DeckManager.tsx @@ -1,604 +1,604 @@ -import React, { useState, useEffect } from 'react'; -import { Plus, Search, Save, Trash2, Upload, Loader2, CheckCircle, XCircle } from 'lucide-react'; -import { Card, Deck } from '../types'; -import { searchCards } from '../services/api'; -import { useAuth } from '../contexts/AuthContext'; -import { supabase } from '../lib/supabase'; -import { validateDeck } from '../utils/deckValidation'; -import MagicCard from './MagicCard'; - -interface DeckManagerProps { - initialDeck?: Deck; - onSave?: () => void; -} - -const calculateManaCurve = (cards: { card; quantity: number }[]) => { - const manaValues = cards.map(({ card }) => { - if (!card.mana_cost) return 0; - // Basic heuristic: count mana symbols - return (card.mana_cost.match(/\{WUBRG0-9]\}/g) || []).length; - }); - - const averageManaValue = manaValues.reduce((a, b) => a + b, 0) / manaValues.length; - return averageManaValue; -}; - -const suggestLandCountAndDistribution = ( - cards: { card; quantity: number }[], - format: string -) => { - const formatRules = { - standard: { minCards: 60, targetLands: 24.5 }, - modern: { minCards: 60, targetLands: 24.5 }, - commander: { minCards: 100, targetLands: 36.5 }, - legacy: { minCards: 60, targetLands: 24.5 }, - vintage: { minCards: 60, targetLands: 24.5 }, - pauper: { minCards: 60, targetLands: 24.5 }, - }; - - const { minCards, targetLands } = - formatRules[format as keyof typeof formatRules] || formatRules.standard; - const deckSize = cards.reduce((acc, { quantity }) => acc + quantity, 0); - const nonLandCards = cards.reduce( - (acc, { card, quantity }) => - card.type_line?.toLowerCase().includes('land') ? acc : acc + quantity, - 0 - ); - const landsToAdd = Math.max(0, minCards - deckSize); - - const colorCounts = { W: 0, U: 0, B: 0, R: 0, G: 0 }; - let totalColorSymbols = 0; - - cards.forEach(({ card, quantity }) => { - if (card.mana_cost) { - const wMatches = (card.mana_cost.match(/\{W\}/g) || []).length; - const uMatches = (card.mana_cost.match(/\{U\}/g) || []).length; - const bMatches = (card.mana_cost.match(/\{B\}/g) || []).length; - const rMatches = (card.mana_cost.match(/\{R\}/g) || []).length; - const gMatches = (card.mana_cost.match(/\{G\}/g) || []).length; - - colorCounts.W += wMatches * quantity; - colorCounts.U += uMatches * quantity; - colorCounts.B += bMatches * quantity; - colorCounts.R += rMatches * quantity; - colorCounts.G += gMatches * quantity; - - totalColorSymbols += - (wMatches + uMatches + bMatches + rMatches + gMatches) * quantity; - } - }); - - const landDistribution: { [key: string]: number } = {}; - for (const color in colorCounts) { - const proportion = - totalColorSymbols > 0 - ? colorCounts[color as keyof typeof colorCounts] / totalColorSymbols - : 0; - landDistribution[color] = Math.round(landsToAdd * proportion); - } - - let totalDistributed = Object.values(landDistribution).reduce( - (acc, count) => acc + count, - 0 - ); - - if (totalDistributed > landsToAdd) { - // Find the color with the most lands - let maxColor = ''; - let maxCount = 0; - for (const color in landDistribution) { - if (landDistribution[color] > maxCount) { - maxColor = color; - maxCount = landDistribution[color]; - } - } - - // Reduce the land count of that color - landDistribution[maxColor] = maxCount - 1; - } - - return { landCount: landsToAdd, landDistribution }; -}; - -export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { - const [searchQuery, setSearchQuery] = useState(''); - const [searchResults, setSearchResults] = useState([]); - const [selectedCards, setSelectedCards] = useState<{ - card: Card; - quantity: number; - }[]>(initialDeck?.cards || []); - const [deckName, setDeckName] = useState(initialDeck?.name || ''); - const [deckFormat, setDeckFormat] = useState(initialDeck?.format || 'standard'); - const [commander, setCommander] = useState( - initialDeck?.cards.find(card => - card.is_commander - )?.card || null - ); - - const { user } = useAuth(); - const [isImporting, setIsImporting] = useState(false); - const [isSaving, setIsSaving] = useState(false); - const [snackbar, setSnackbar] = useState<{ message: string; type: 'success' | 'error' } | null>(null); - - const handleSearch = async (e: React.FormEvent) => { - e.preventDefault(); - if (!searchQuery.trim()) return; - - try { - const cards = await searchCards(searchQuery); - setSearchResults(cards); - } catch (error) { - console.error('Failed to search cards:', error); - } - }; - - const addCardToDeck = (card: Card) => { - setSelectedCards(prev => { - const isBasicLand = - card.name === 'Plains' || - card.name === 'Island' || - card.name === 'Swamp' || - card.name === 'Mountain' || - card.name === 'Forest'; - const existing = prev.find(c => c.card.id === card.id); - if (existing) { - return prev.map(c => - c.card.id === card.id - ? { - ...c, - quantity: isBasicLand ? c.quantity + 1 : Math.min(c.quantity + 1, 4), - } - : c - ); - } - return [...prev, { card, quantity: 1 }]; - }); - }; - - const removeCardFromDeck = (cardId: string) => - setSelectedCards(prev => prev.filter(c => c.card.id !== cardId)); - - const updateCardQuantity = (cardId: string, quantity: number) => { - setSelectedCards(prev => { - return prev.map(c => { - if (c.card.id === cardId) { - const isBasicLand = - c.card.name === 'Plains' || - c.card.name === 'Island' || - c.card.name === 'Swamp' || - c.card.name === 'Mountain' || - c.card.name === 'Forest'; - return { ...c, quantity: quantity }; - } - return c; - }); - }); - }; - - const saveDeck = async () => { - if (!deckName.trim() || selectedCards.length === 0 || !user) return; - - setIsSaving(true); - try { - const deckToSave: Deck = { - id: initialDeck?.id || crypto.randomUUID(), - name: deckName, - format: deckFormat, - cards: selectedCards, - userId: user.id, - createdAt: initialDeck?.createdAt || new Date(), - updatedAt: new Date(), - }; - - const validation = validateDeck(deckToSave); - - const deckData = { - id: deckToSave.id, - name: deckToSave.name, - format: deckToSave.format, - user_id: deckToSave.userId, - created_at: deckToSave.createdAt, - updated_at: deckToSave.updatedAt, - }; - - // Save or update the deck - const { error: deckError } = await supabase - .from('decks') - .upsert([deckData]) - .select(); - - if (deckError) throw deckError; - - // Delete existing cards if updating - if (initialDeck) { - await supabase.from('deck_cards').delete().eq('deck_id', initialDeck.id); - } - - // Save the deck cards - const deckCards = selectedCards.map(card => ({ - deck_id: deckToSave.id, - card_id: card.card.id, - quantity: card.quantity, - is_commander: card.card.id === commander?.id, - })); - - const { error: cardsError } = await supabase - .from('deck_cards') - .insert(deckCards); - - if (cardsError) throw cardsError; - - setSnackbar({ message: 'Deck saved successfully!', type: 'success' }); - if (onSave) onSave(); - } catch (error) { - console.error('Error saving deck:', error); - setSnackbar({ message: 'Failed to save deck.', type: 'error' }); - } finally { - setIsSaving(false); - setTimeout(() => setSnackbar(null), 3000); // Clear snackbar after 3 seconds - } - }; - - const currentDeck: Deck = { - id: initialDeck?.id || '', - name: deckName, - format: deckFormat, - cards: selectedCards, - userId: user?.id || '', - createdAt: initialDeck?.createdAt || new Date(), - updatedAt: new Date(), - }; - - const validation = validateDeck(currentDeck); - - const deckSize = selectedCards.reduce((acc, curr) => acc + curr.quantity, 0); - const { - landCount: suggestedLandCountValue, - landDistribution: suggestedLands, - } = suggestLandCountAndDistribution(selectedCards, deckFormat); - - const totalPrice = selectedCards.reduce((acc, { card, quantity }) => { - const isBasicLand = - card.name === 'Plains' || - card.name === 'Island' || - card.name === 'Swamp' || - card.name === 'Mountain' || - card.name === 'Forest'; - const price = isBasicLand ? 0 : card.prices?.usd ? parseFloat(card.prices.usd) : 0; - return acc + price * quantity; - }, 0); - - const addSuggestedLandsToDeck = async () => { - const basicLandCards = { - W: { name: 'Plains', set: 'unh' }, - U: { name: 'Island', set: 'unh' }, - B: { name: 'Swamp', set: 'unh' }, - R: { name: 'Mountain', set: 'unh' }, - G: { name: 'Forest', set: 'unh' }, - }; - - for (const color in suggestedLands) { - const landCount = suggestedLands[color]; - if (landCount > 0) { - const landName = basicLandCards[color]?.name; - const landSet = basicLandCards[color]?.set; - - if (landName && landSet) { - try { - const cards = await searchCards(`${landName} set:${landSet}`); - if (cards && cards.length > 0) { - const landCard = cards[0]; // Take the first matching card - for (let i = 0; i < landCount; i++) { - addCardToDeck(landCard); - } - } - } catch (error) { - console.error(`Failed to add ${landName}:`, error); - } - } - } - } - }; - - const handleFileUpload = async ( - event: React.ChangeEvent - ) => { - const file = event.target.files?.[0]; - if (!file) return; - - setIsImporting(true); - try { - const reader = new FileReader(); - reader.onload = async e => { - const text = e.target?.result as string; - const lines = text.split('\n'); - const cardsToAdd: { card: Card; quantity: number }[] = []; - - for (const line of lines) { - const parts = line.trim().split(' '); - const quantity = parseInt(parts[0]); - const cardName = parts.slice(1).join(' '); - - if (isNaN(quantity) || quantity <= 0 || !cardName) continue; - - try { - const searchResults = await searchCards(cardName); - if (searchResults && searchResults.length > 0) { - const card = searchResults[0]; - cardsToAdd.push({ card, quantity }); - } else { - console.warn(`Card not found: ${cardName}`); - alert(`Card not found: ${cardName}`); - } - } catch (error) { - console.error(`Failed to search card ${cardName}:`, error); - alert(`Failed to search card ${cardName}: ${error}`); - } - } - - setSelectedCards(prev => { - const updatedCards = [...prev]; - for (const { card, quantity } of cardsToAdd) { - const existingCardIndex = updatedCards.findIndex( - c => c.card.id === card.id - ); - if (existingCardIndex !== -1) { - updatedCards[existingCardIndex].quantity = Math.min( - updatedCards[existingCardIndex].quantity + quantity, - 4 - ); - } else { - updatedCards.push({ card, quantity }); - } - } - return updatedCards; - }); - }; - - reader.readAsText(file); - } finally { - setIsImporting(false); - } - }; - - return ( -
-
-
- {/* Card Search Section */} -
-
-
- - setSearchQuery(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Search for cards..." - /> -
- -
- -
- {searchResults.map(card => ( -
- -
-

{card.name}

- -
-
- ))} -
-
- - {/* Deck Builder Section */} -
-
- setDeckName(e.target.value)} - className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Deck Name" - /> - - - - {deckFormat === 'commander' && ( - - )} - -
- - {isImporting && ( -
- -
- )} -
- - {!validation.isValid && ( -
-
    - {validation.errors.map((error, index) => ( -
  • {error}
  • - ))} -
-
- )} - -
-

- Cards ({selectedCards.reduce((acc, curr) => acc + curr.quantity, 0)}) -

- {selectedCards.map(({ card, quantity }) => ( -
- {card.name} -
-

{card.name}

- {card.prices?.usd && ( -
${card.prices.usd}
- )} -
- - updateCardQuantity(card.id, parseInt(e.target.value)) - } - min="1" - className="w-16 px-2 py-1 bg-gray-600 border border-gray-500 rounded text-center" - /> - -
- ))} -
- -
- Total Price: ${totalPrice.toFixed(2)} -
- - {deckSize > 0 && ( -
- Suggested Land Count: {suggestedLandCountValue} - {Object.entries(suggestedLands).map(([landType, count]) => ( -
- {landType}: {count} -
- ))} -
- )} - - {deckSize > 0 && ( - - )} - - -
-
-
-
- {snackbar && ( -
-
-
- {snackbar.type === 'success' ? ( - - ) : ( - - )} - {snackbar.message} -
- -
-
- )} -
- ); +import React, { useState, useEffect } from 'react'; +import { Plus, Search, Save, Trash2, Upload, Loader2, CheckCircle, XCircle } from 'lucide-react'; +import { Card, Deck } from '../types'; +import { searchCards } from '../services/api'; +import { useAuth } from '../contexts/AuthContext'; +import { supabase } from '../lib/supabase'; +import { validateDeck } from '../utils/deckValidation'; +import MagicCard from './MagicCard'; + +interface DeckManagerProps { + initialDeck?: Deck; + onSave?: () => void; +} + +const calculateManaCurve = (cards: { card; quantity: number }[]) => { + const manaValues = cards.map(({ card }) => { + if (!card.mana_cost) return 0; + // Basic heuristic: count mana symbols + return (card.mana_cost.match(/\{WUBRG0-9]\}/g) || []).length; + }); + + const averageManaValue = manaValues.reduce((a, b) => a + b, 0) / manaValues.length; + return averageManaValue; +}; + +const suggestLandCountAndDistribution = ( + cards: { card; quantity: number }[], + format: string +) => { + const formatRules = { + standard: { minCards: 60, targetLands: 24.5 }, + modern: { minCards: 60, targetLands: 24.5 }, + commander: { minCards: 100, targetLands: 36.5 }, + legacy: { minCards: 60, targetLands: 24.5 }, + vintage: { minCards: 60, targetLands: 24.5 }, + pauper: { minCards: 60, targetLands: 24.5 }, + }; + + const { minCards, targetLands } = + formatRules[format as keyof typeof formatRules] || formatRules.standard; + const deckSize = cards.reduce((acc, { quantity }) => acc + quantity, 0); + const nonLandCards = cards.reduce( + (acc, { card, quantity }) => + card.type_line?.toLowerCase().includes('land') ? acc : acc + quantity, + 0 + ); + const landsToAdd = Math.max(0, minCards - deckSize); + + const colorCounts = { W: 0, U: 0, B: 0, R: 0, G: 0 }; + let totalColorSymbols = 0; + + cards.forEach(({ card, quantity }) => { + if (card.mana_cost) { + const wMatches = (card.mana_cost.match(/\{W\}/g) || []).length; + const uMatches = (card.mana_cost.match(/\{U\}/g) || []).length; + const bMatches = (card.mana_cost.match(/\{B\}/g) || []).length; + const rMatches = (card.mana_cost.match(/\{R\}/g) || []).length; + const gMatches = (card.mana_cost.match(/\{G\}/g) || []).length; + + colorCounts.W += wMatches * quantity; + colorCounts.U += uMatches * quantity; + colorCounts.B += bMatches * quantity; + colorCounts.R += rMatches * quantity; + colorCounts.G += gMatches * quantity; + + totalColorSymbols += + (wMatches + uMatches + bMatches + rMatches + gMatches) * quantity; + } + }); + + const landDistribution: { [key: string]: number } = {}; + for (const color in colorCounts) { + const proportion = + totalColorSymbols > 0 + ? colorCounts[color as keyof typeof colorCounts] / totalColorSymbols + : 0; + landDistribution[color] = Math.round(landsToAdd * proportion); + } + + let totalDistributed = Object.values(landDistribution).reduce( + (acc, count) => acc + count, + 0 + ); + + if (totalDistributed > landsToAdd) { + // Find the color with the most lands + let maxColor = ''; + let maxCount = 0; + for (const color in landDistribution) { + if (landDistribution[color] > maxCount) { + maxColor = color; + maxCount = landDistribution[color]; + } + } + + // Reduce the land count of that color + landDistribution[maxColor] = maxCount - 1; + } + + return { landCount: landsToAdd, landDistribution }; +}; + +export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) { + const [searchQuery, setSearchQuery] = useState(''); + const [searchResults, setSearchResults] = useState([]); + const [selectedCards, setSelectedCards] = useState<{ + card: Card; + quantity: number; + }[]>(initialDeck?.cards || []); + const [deckName, setDeckName] = useState(initialDeck?.name || ''); + const [deckFormat, setDeckFormat] = useState(initialDeck?.format || 'standard'); + const [commander, setCommander] = useState( + initialDeck?.cards.find(card => + card.is_commander + )?.card || null + ); + + const { user } = useAuth(); + const [isImporting, setIsImporting] = useState(false); + const [isSaving, setIsSaving] = useState(false); + const [snackbar, setSnackbar] = useState<{ message: string; type: 'success' | 'error' } | null>(null); + + const handleSearch = async (e: React.FormEvent) => { + e.preventDefault(); + if (!searchQuery.trim()) return; + + try { + const cards = await searchCards(searchQuery); + setSearchResults(cards); + } catch (error) { + console.error('Failed to search cards:', error); + } + }; + + const addCardToDeck = (card: Card) => { + setSelectedCards(prev => { + const isBasicLand = + card.name === 'Plains' || + card.name === 'Island' || + card.name === 'Swamp' || + card.name === 'Mountain' || + card.name === 'Forest'; + const existing = prev.find(c => c.card.id === card.id); + if (existing) { + return prev.map(c => + c.card.id === card.id + ? { + ...c, + quantity: isBasicLand ? c.quantity + 1 : Math.min(c.quantity + 1, 4), + } + : c + ); + } + return [...prev, { card, quantity: 1 }]; + }); + }; + + const removeCardFromDeck = (cardId: string) => + setSelectedCards(prev => prev.filter(c => c.card.id !== cardId)); + + const updateCardQuantity = (cardId: string, quantity: number) => { + setSelectedCards(prev => { + return prev.map(c => { + if (c.card.id === cardId) { + const isBasicLand = + c.card.name === 'Plains' || + c.card.name === 'Island' || + c.card.name === 'Swamp' || + c.card.name === 'Mountain' || + c.card.name === 'Forest'; + return { ...c, quantity: quantity }; + } + return c; + }); + }); + }; + + const saveDeck = async () => { + if (!deckName.trim() || selectedCards.length === 0 || !user) return; + + setIsSaving(true); + try { + const deckToSave: Deck = { + id: initialDeck?.id || crypto.randomUUID(), + name: deckName, + format: deckFormat, + cards: selectedCards, + userId: user.id, + createdAt: initialDeck?.createdAt || new Date(), + updatedAt: new Date(), + }; + + const validation = validateDeck(deckToSave); + + const deckData = { + id: deckToSave.id, + name: deckToSave.name, + format: deckToSave.format, + user_id: deckToSave.userId, + created_at: deckToSave.createdAt, + updated_at: deckToSave.updatedAt, + }; + + // Save or update the deck + const { error: deckError } = await supabase + .from('decks') + .upsert([deckData]) + .select(); + + if (deckError) throw deckError; + + // Delete existing cards if updating + if (initialDeck) { + await supabase.from('deck_cards').delete().eq('deck_id', initialDeck.id); + } + + // Save the deck cards + const deckCards = selectedCards.map(card => ({ + deck_id: deckToSave.id, + card_id: card.card.id, + quantity: card.quantity, + is_commander: card.card.id === commander?.id, + })); + + const { error: cardsError } = await supabase + .from('deck_cards') + .insert(deckCards); + + if (cardsError) throw cardsError; + + setSnackbar({ message: 'Deck saved successfully!', type: 'success' }); + if (onSave) onSave(); + } catch (error) { + console.error('Error saving deck:', error); + setSnackbar({ message: 'Failed to save deck.', type: 'error' }); + } finally { + setIsSaving(false); + setTimeout(() => setSnackbar(null), 3000); // Clear snackbar after 3 seconds + } + }; + + const currentDeck: Deck = { + id: initialDeck?.id || '', + name: deckName, + format: deckFormat, + cards: selectedCards, + userId: user?.id || '', + createdAt: initialDeck?.createdAt || new Date(), + updatedAt: new Date(), + }; + + const validation = validateDeck(currentDeck); + + const deckSize = selectedCards.reduce((acc, curr) => acc + curr.quantity, 0); + const { + landCount: suggestedLandCountValue, + landDistribution: suggestedLands, + } = suggestLandCountAndDistribution(selectedCards, deckFormat); + + const totalPrice = selectedCards.reduce((acc, { card, quantity }) => { + const isBasicLand = + card.name === 'Plains' || + card.name === 'Island' || + card.name === 'Swamp' || + card.name === 'Mountain' || + card.name === 'Forest'; + const price = isBasicLand ? 0 : card.prices?.usd ? parseFloat(card.prices.usd) : 0; + return acc + price * quantity; + }, 0); + + const addSuggestedLandsToDeck = async () => { + const basicLandCards = { + W: { name: 'Plains', set: 'unh' }, + U: { name: 'Island', set: 'unh' }, + B: { name: 'Swamp', set: 'unh' }, + R: { name: 'Mountain', set: 'unh' }, + G: { name: 'Forest', set: 'unh' }, + }; + + for (const color in suggestedLands) { + const landCount = suggestedLands[color]; + if (landCount > 0) { + const landName = basicLandCards[color]?.name; + const landSet = basicLandCards[color]?.set; + + if (landName && landSet) { + try { + const cards = await searchCards(`${landName} set:${landSet}`); + if (cards && cards.length > 0) { + const landCard = cards[0]; // Take the first matching card + for (let i = 0; i < landCount; i++) { + addCardToDeck(landCard); + } + } + } catch (error) { + console.error(`Failed to add ${landName}:`, error); + } + } + } + } + }; + + const handleFileUpload = async ( + event: React.ChangeEvent + ) => { + const file = event.target.files?.[0]; + if (!file) return; + + setIsImporting(true); + try { + const reader = new FileReader(); + reader.onload = async e => { + const text = e.target?.result as string; + const lines = text.split('\n'); + const cardsToAdd: { card: Card; quantity: number }[] = []; + + for (const line of lines) { + const parts = line.trim().split(' '); + const quantity = parseInt(parts[0]); + const cardName = parts.slice(1).join(' '); + + if (isNaN(quantity) || quantity <= 0 || !cardName) continue; + + try { + const searchResults = await searchCards(cardName); + if (searchResults && searchResults.length > 0) { + const card = searchResults[0]; + cardsToAdd.push({ card, quantity }); + } else { + console.warn(`Card not found: ${cardName}`); + alert(`Card not found: ${cardName}`); + } + } catch (error) { + console.error(`Failed to search card ${cardName}:`, error); + alert(`Failed to search card ${cardName}: ${error}`); + } + } + + setSelectedCards(prev => { + const updatedCards = [...prev]; + for (const { card, quantity } of cardsToAdd) { + const existingCardIndex = updatedCards.findIndex( + c => c.card.id === card.id + ); + if (existingCardIndex !== -1) { + updatedCards[existingCardIndex].quantity = Math.min( + updatedCards[existingCardIndex].quantity + quantity, + 4 + ); + } else { + updatedCards.push({ card, quantity }); + } + } + return updatedCards; + }); + }; + + reader.readAsText(file); + } finally { + setIsImporting(false); + } + }; + + return ( +
+
+
+ {/* Card Search Section */} +
+
+
+ + setSearchQuery(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Search for cards..." + /> +
+ +
+ +
+ {searchResults.map(card => ( +
+ +
+

{card.name}

+ +
+
+ ))} +
+
+ + {/* Deck Builder Section */} +
+
+ setDeckName(e.target.value)} + className="w-full px-4 py-2 bg-gray-700 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Deck Name" + /> + + + + {deckFormat === 'commander' && ( + + )} + +
+ + {isImporting && ( +
+ +
+ )} +
+ + {!validation.isValid && ( +
+
    + {validation.errors.map((error, index) => ( +
  • {error}
  • + ))} +
+
+ )} + +
+

+ Cards ({selectedCards.reduce((acc, curr) => acc + curr.quantity, 0)}) +

+ {selectedCards.map(({ card, quantity }) => ( +
+ {card.name} +
+

{card.name}

+ {card.prices?.usd && ( +
${card.prices.usd}
+ )} +
+ + updateCardQuantity(card.id, parseInt(e.target.value)) + } + min="1" + className="w-16 px-2 py-1 bg-gray-600 border border-gray-500 rounded text-center" + /> + +
+ ))} +
+ +
+ Total Price: ${totalPrice.toFixed(2)} +
+ + {deckSize > 0 && ( +
+ Suggested Land Count: {suggestedLandCountValue} + {Object.entries(suggestedLands).map(([landType, count]) => ( +
+ {landType}: {count} +
+ ))} +
+ )} + + {deckSize > 0 && ( + + )} + + +
+
+
+
+ {snackbar && ( +
+
+
+ {snackbar.type === 'success' ? ( + + ) : ( + + )} + {snackbar.message} +
+ +
+
+ )} +
+ ); } diff --git a/src/components/LifeCounter.tsx b/src/components/LifeCounter.tsx index a43f833..08a8f94 100644 --- a/src/components/LifeCounter.tsx +++ b/src/components/LifeCounter.tsx @@ -1,167 +1,167 @@ -import React, { useState, useEffect } from 'react'; - import { Plus, Minus } from 'lucide-react'; - - interface Player { - id: number; - name: string; - life: number; - color: string; - } - - const COLORS = ['white', 'blue', 'black', 'red', 'green']; - - export default function LifeCounter() { - const [numPlayers, setNumPlayers] = useState(null); - const [playerNames, setPlayerNames] = useState([]); - const [players, setPlayers] = useState([]); - const [setupComplete, setSetupComplete] = useState(false); - - useEffect(() => { - if (numPlayers !== null) { - setPlayers( - Array.from({ length: numPlayers }, (_, i) => ({ - id: i + 1, - name: playerNames[i] || `Player ${i + 1}`, - life: 20, - color: COLORS[i % COLORS.length], - })) - ); - } - }, [numPlayers, playerNames]); - - const handleNumPlayersChange = (e: React.ChangeEvent) => { - const newNumPlayers = parseInt(e.target.value, 10); - setNumPlayers(newNumPlayers); - setPlayerNames(Array(newNumPlayers).fill('')); - }; - - const handleNameChange = (index: number, newName: string) => { - const updatedNames = [...playerNames]; - updatedNames[index] = newName; - setPlayerNames(updatedNames); - }; - - const updateLife = (playerId: number, change: number) => { - setPlayers((prevPlayers) => - prevPlayers.map((player) => - player.id === playerId ? { ...player, life: player.life + change } : player - ) - ); - }; - - const handleSubmit = (e: React.FormEvent) => { - e.preventDefault(); - setSetupComplete(true); - }; - - const renderSetupForm = () => ( -
-

Setup Players

-
-
- - -
- - {numPlayers !== null && - Array.from({ length: numPlayers }, (_, i) => ( -
- - handleNameChange(i, e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder={`Player ${i + 1} Name`} - /> -
- ))} - - {numPlayers !== null && ( - - )} -
-
- ); - - const renderLifeCounters = () => ( -
-
- {players.map((player, index) => { - const angle = (index / players.length) * 360; - const rotation = 360 - angle; - const x = 50 + 40 * Math.cos((angle - 90) * Math.PI / 180); - const y = 50 + 40 * Math.sin((angle - 90) * Math.PI / 180); - - return ( -
-
-

{player.name}

-
{player.life}
-
- - -
-
-
- ); - })} -
-
- ); - - return ( -
-
-

Life Counter

- {!setupComplete ? renderSetupForm() : renderLifeCounters()} -
-
- ); +import React, { useState, useEffect } from 'react'; + import { Plus, Minus } from 'lucide-react'; + + interface Player { + id: number; + name: string; + life: number; + color: string; + } + + const COLORS = ['white', 'blue', 'black', 'red', 'green']; + + export default function LifeCounter() { + const [numPlayers, setNumPlayers] = useState(null); + const [playerNames, setPlayerNames] = useState([]); + const [players, setPlayers] = useState([]); + const [setupComplete, setSetupComplete] = useState(false); + + useEffect(() => { + if (numPlayers !== null) { + setPlayers( + Array.from({ length: numPlayers }, (_, i) => ({ + id: i + 1, + name: playerNames[i] || `Player ${i + 1}`, + life: 20, + color: COLORS[i % COLORS.length], + })) + ); + } + }, [numPlayers, playerNames]); + + const handleNumPlayersChange = (e: React.ChangeEvent) => { + const newNumPlayers = parseInt(e.target.value, 10); + setNumPlayers(newNumPlayers); + setPlayerNames(Array(newNumPlayers).fill('')); + }; + + const handleNameChange = (index: number, newName: string) => { + const updatedNames = [...playerNames]; + updatedNames[index] = newName; + setPlayerNames(updatedNames); + }; + + const updateLife = (playerId: number, change: number) => { + setPlayers((prevPlayers) => + prevPlayers.map((player) => + player.id === playerId ? { ...player, life: player.life + change } : player + ) + ); + }; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + setSetupComplete(true); + }; + + const renderSetupForm = () => ( +
+

Setup Players

+
+
+ + +
+ + {numPlayers !== null && + Array.from({ length: numPlayers }, (_, i) => ( +
+ + handleNameChange(i, e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder={`Player ${i + 1} Name`} + /> +
+ ))} + + {numPlayers !== null && ( + + )} +
+
+ ); + + const renderLifeCounters = () => ( +
+
+ {players.map((player, index) => { + const angle = (index / players.length) * 360; + const rotation = 360 - angle; + const x = 50 + 40 * Math.cos((angle - 90) * Math.PI / 180); + const y = 50 + 40 * Math.sin((angle - 90) * Math.PI / 180); + + return ( +
+
+

{player.name}

+
{player.life}
+
+ + +
+
+
+ ); + })} +
+
+ ); + + return ( +
+
+

Life Counter

+ {!setupComplete ? renderSetupForm() : renderLifeCounters()} +
+
+ ); } diff --git a/src/components/LoginForm.tsx b/src/components/LoginForm.tsx index 19b5777..fac6c86 100644 --- a/src/components/LoginForm.tsx +++ b/src/components/LoginForm.tsx @@ -1,147 +1,147 @@ -import { useState, useEffect } from 'react'; - import { Mail, Lock, LogIn } from 'lucide-react'; - import { useAuth } from '../contexts/AuthContext'; - import { Card } from '../types'; - import { getRandomCards } from '../services/api'; - - export default function LoginForm() { - const [email, setEmail] = useState(''); - const [password, setPassword] = useState(''); - const [isSignUp, setIsSignUp] = useState(false); - const [error, setError] = useState(null); - const { signIn, signUp } = useAuth(); - const [cards, setCards] = useState([]); - const [loading, setLoading] = useState(true); - - useEffect(() => { - const loadCards = async () => { - try { - const randomCards = await getRandomCards(6); - setCards(randomCards); - } catch (error) { - console.error('Failed to load cards:', error); - } finally { - setLoading(false); - } - }; - - loadCards(); - }, []); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - setError(null); - - try { - if (isSignUp) { - await signUp(email, password); - } else { - await signIn(email, password); - } - window.location.href = '/'; // Redirect to home after successful login - } catch (error) { - setError(error instanceof Error ? error.message : 'An error occurred'); - } - }; - - if (loading) { - return
; - } - - return ( -
- {/* Animated Background */} -
-
- {[...cards, ...cards].map((card, index) => ( -
-
-
- ))} -
-
- - {/* Login Form */} -
-

- Deckerr -

- - {error && ( -
- {error} -
- )} - -
-
- -
- - setEmail(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-gray-800/50 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Enter your email" - required - /> -
-
-
- -
- - setPassword(e.target.value)} - className="w-full pl-10 pr-4 py-2 bg-gray-800/50 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" - placeholder="Enter your password" - required - /> -
-
- -
- -
- -
-
-
- ); +import { useState, useEffect } from 'react'; + import { Mail, Lock, LogIn } from 'lucide-react'; + import { useAuth } from '../contexts/AuthContext'; + import { Card } from '../types'; + import { getRandomCards } from '../services/api'; + + export default function LoginForm() { + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [isSignUp, setIsSignUp] = useState(false); + const [error, setError] = useState(null); + const { signIn, signUp } = useAuth(); + const [cards, setCards] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const loadCards = async () => { + try { + const randomCards = await getRandomCards(6); + setCards(randomCards); + } catch (error) { + console.error('Failed to load cards:', error); + } finally { + setLoading(false); + } + }; + + loadCards(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + setError(null); + + try { + if (isSignUp) { + await signUp(email, password); + } else { + await signIn(email, password); + } + window.location.href = '/'; // Redirect to home after successful login + } catch (error) { + setError(error instanceof Error ? error.message : 'An error occurred'); + } + }; + + if (loading) { + return
; + } + + return ( +
+ {/* Animated Background */} +
+
+ {[...cards, ...cards].map((card, index) => ( +
+
+
+ ))} +
+
+ + {/* Login Form */} +
+

+ Deckerr +

+ + {error && ( +
+ {error} +
+ )} + +
+
+ +
+ + setEmail(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-gray-800/50 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Enter your email" + required + /> +
+
+
+ +
+ + setPassword(e.target.value)} + className="w-full pl-10 pr-4 py-2 bg-gray-800/50 border border-gray-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white" + placeholder="Enter your password" + required + /> +
+
+ +
+ +
+ +
+
+
+ ); } diff --git a/src/components/MagicCard.tsx b/src/components/MagicCard.tsx index ea7bd1c..426cefe 100644 --- a/src/components/MagicCard.tsx +++ b/src/components/MagicCard.tsx @@ -1,31 +1,31 @@ -import React from 'react'; -import { Card } from '../types'; - -interface MagicCardProps { - card: Card; -} - -const MagicCard = ({ card }: MagicCardProps) => { - return ( -
- {card.image_uris?.normal ? ( - {card.name} - ) : ( -
- No Image Available -
- )} - {card.prices?.usd && ( -
- ${card.prices.usd} -
- )} -
- ); -}; - +import React from 'react'; +import { Card } from '../types'; + +interface MagicCardProps { + card: Card; +} + +const MagicCard = ({ card }: MagicCardProps) => { + return ( +
+ {card.image_uris?.normal ? ( + {card.name} + ) : ( +
+ No Image Available +
+ )} + {card.prices?.usd && ( +
+ ${card.prices.usd} +
+ )} +
+ ); +}; + export default MagicCard; diff --git a/src/components/ManaIcons.tsx b/src/components/ManaIcons.tsx index 58311e7..7cc75ba 100644 --- a/src/components/ManaIcons.tsx +++ b/src/components/ManaIcons.tsx @@ -1,107 +1,107 @@ -import React from 'react'; - - export const ManaWhite = ({ size = 20, ...props }: { size?: number }) => ( - - - - ); - - export const ManaBlue = ({ size = 20, ...props }: { size?: number }) => ( - - - - - ); - - export const ManaBlack = ({ size = 20, ...props }: { size?: number }) => ( - - - - - ); - - export const ManaRed = ({ size = 20, ...props }: { size?: number }) => ( - - - - - ); - - export const ManaGreen = ({ size = 20, ...props }: { size?: number }) => ( - - - - - ); - - export const ManaColorless = ({ size = 20, ...props }: { size?: number }) => ( - - - +import React from 'react'; + + export const ManaWhite = ({ size = 20, ...props }: { size?: number }) => ( + + + + ); + + export const ManaBlue = ({ size = 20, ...props }: { size?: number }) => ( + + + + + ); + + export const ManaBlack = ({ size = 20, ...props }: { size?: number }) => ( + + + + + ); + + export const ManaRed = ({ size = 20, ...props }: { size?: number }) => ( + + + + + ); + + export const ManaGreen = ({ size = 20, ...props }: { size?: number }) => ( + + + + + ); + + export const ManaColorless = ({ size = 20, ...props }: { size?: number }) => ( + + + ); diff --git a/src/components/Navigation.tsx b/src/components/Navigation.tsx index 3e53bca..a6a215c 100644 --- a/src/components/Navigation.tsx +++ b/src/components/Navigation.tsx @@ -1,199 +1,199 @@ -import React, { useState, useRef, useEffect } from 'react'; - import { Home, PlusSquare, Library, LogOut, Settings, ChevronDown, Search, Heart, Menu } from 'lucide-react'; - import { useAuth } from '../contexts/AuthContext'; - import { supabase } from '../lib/supabase'; - - type Page = 'home' | 'deck' | 'login' | 'collection' | 'profile' | 'search' | 'life-counter'; - - interface NavigationProps { - currentPage: Page; - setCurrentPage: (page: Page) => void; - } - - export default function Navigation({ currentPage, setCurrentPage }: NavigationProps) { - const { user, signOut } = useAuth(); - const [showDropdown, setShowDropdown] = useState(false); - const [showMobileMenu, setShowMobileMenu] = useState(false); - const dropdownRef = useRef(null); - const mobileMenuRef = useRef(null); - const [username, setUsername] = useState(null); - - useEffect(() => { - const fetchProfile = async () => { - if (user) { - const { data } = await supabase - .from('profiles') - .select('username') - .eq('id', user.id) - .single(); - - if (data) { - setUsername(data.username); - } - } - }; - - fetchProfile(); - }, [user]); - - useEffect(() => { - const handleClickOutside = (event: MouseEvent) => { - if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { - setShowDropdown(false); - } - if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) { - setShowMobileMenu(false); - } - }; - - document.addEventListener('mousedown', handleClickOutside); - return () => document.removeEventListener('mousedown', handleClickOutside); - }, []); - - const navItems = [ - { id: 'home' as const, label: 'Home', icon: Home }, - { id: 'deck' as const, label: 'New Deck', icon: PlusSquare }, - { id: 'collection' as const, label: 'Collection', icon: Library }, - { id: 'search' as const, label: 'Search', icon: Search }, - { id: 'life-counter' as const, label: 'Life Counter', icon: Heart }, - ]; - - const handleSignOut = async () => { - try { - await signOut(); - setCurrentPage('login'); - } catch (error) { - console.error('Error signing out:', error); - } - }; - - const getAvatarUrl = (userId: string) => { - return `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}&backgroundColor=b6e3f4,c0aede,d1d4f9`; - }; - - return ( - <> - {/* Desktop Navigation - Top */} - - - {/* Mobile Navigation - Bottom */} - - - {/* Content Padding */} -
- - ); +import React, { useState, useRef, useEffect } from 'react'; + import { Home, PlusSquare, Library, LogOut, Settings, ChevronDown, Search, Heart, Menu } from 'lucide-react'; + import { useAuth } from '../contexts/AuthContext'; + import { supabase } from '../lib/supabase'; + + type Page = 'home' | 'deck' | 'login' | 'collection' | 'profile' | 'search' | 'life-counter'; + + interface NavigationProps { + currentPage: Page; + setCurrentPage: (page: Page) => void; + } + + export default function Navigation({ currentPage, setCurrentPage }: NavigationProps) { + const { user, signOut } = useAuth(); + const [showDropdown, setShowDropdown] = useState(false); + const [showMobileMenu, setShowMobileMenu] = useState(false); + const dropdownRef = useRef(null); + const mobileMenuRef = useRef(null); + const [username, setUsername] = useState(null); + + useEffect(() => { + const fetchProfile = async () => { + if (user) { + const { data } = await supabase + .from('profiles') + .select('username') + .eq('id', user.id) + .single(); + + if (data) { + setUsername(data.username); + } + } + }; + + fetchProfile(); + }, [user]); + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) { + setShowDropdown(false); + } + if (mobileMenuRef.current && !mobileMenuRef.current.contains(event.target as Node)) { + setShowMobileMenu(false); + } + }; + + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const navItems = [ + { id: 'home' as const, label: 'Home', icon: Home }, + { id: 'deck' as const, label: 'New Deck', icon: PlusSquare }, + { id: 'collection' as const, label: 'Collection', icon: Library }, + { id: 'search' as const, label: 'Search', icon: Search }, + { id: 'life-counter' as const, label: 'Life Counter', icon: Heart }, + ]; + + const handleSignOut = async () => { + try { + await signOut(); + setCurrentPage('login'); + } catch (error) { + console.error('Error signing out:', error); + } + }; + + const getAvatarUrl = (userId: string) => { + return `https://api.dicebear.com/7.x/avataaars/svg?seed=${userId}&backgroundColor=b6e3f4,c0aede,d1d4f9`; + }; + + return ( + <> + {/* Desktop Navigation - Top */} + + + {/* Mobile Navigation - Bottom */} + + + {/* Content Padding */} +
+ + ); } diff --git a/src/components/Profile.tsx b/src/components/Profile.tsx index d13c6ed..adddd53 100644 --- a/src/components/Profile.tsx +++ b/src/components/Profile.tsx @@ -1,128 +1,128 @@ -import React, { useState, useEffect } from 'react'; -import { Save } from 'lucide-react'; -import { useAuth } from '../contexts/AuthContext'; -import { supabase } from '../lib/supabase'; - -const THEME_COLORS = ['red', 'green', 'blue', 'yellow', 'grey', 'purple']; - -export default function Profile() { - const { user } = useAuth(); - const [username, setUsername] = useState(''); - const [themeColor, setThemeColor] = useState('blue'); - const [loading, setLoading] = useState(true); - const [saving, setSaving] = useState(false); - - useEffect(() => { - const loadProfile = async () => { - if (user) { - const { data, error } = await supabase - .from('profiles') - .select('username, theme_color') - .eq('id', user.id) - .single(); - - if (data) { - setUsername(data.username || ''); - setThemeColor(data.theme_color || 'blue'); - } - setLoading(false); - } - }; - - loadProfile(); - }, [user]); - - const handleSubmit = async (e: React.FormEvent) => { - e.preventDefault(); - if (!user) return; - - setSaving(true); - try { - const { error } = await supabase - .from('profiles') - .upsert({ - id: user.id, - username, - theme_color: themeColor, - updated_at: new Date() - }); - - if (error) throw error; - alert('Profile updated successfully!'); - } catch (error) { - console.error('Error updating profile:', error); - alert('Failed to update profile'); - } finally { - setSaving(false); - } - }; - - if (loading) { - return ( -
-
-
- ); - } - - return ( -
-
-

Profile Settings

- -
-
- - setUsername(e.target.value)} - className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" - placeholder="Enter your username" - /> -
- -
- -
- {THEME_COLORS.map((color) => ( - - ))} -
-
- - -
-
-
- ); +import React, { useState, useEffect } from 'react'; +import { Save } from 'lucide-react'; +import { useAuth } from '../contexts/AuthContext'; +import { supabase } from '../lib/supabase'; + +const THEME_COLORS = ['red', 'green', 'blue', 'yellow', 'grey', 'purple']; + +export default function Profile() { + const { user } = useAuth(); + const [username, setUsername] = useState(''); + const [themeColor, setThemeColor] = useState('blue'); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + + useEffect(() => { + const loadProfile = async () => { + if (user) { + const { data, error } = await supabase + .from('profiles') + .select('username, theme_color') + .eq('id', user.id) + .single(); + + if (data) { + setUsername(data.username || ''); + setThemeColor(data.theme_color || 'blue'); + } + setLoading(false); + } + }; + + loadProfile(); + }, [user]); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + if (!user) return; + + setSaving(true); + try { + const { error } = await supabase + .from('profiles') + .upsert({ + id: user.id, + username, + theme_color: themeColor, + updated_at: new Date() + }); + + if (error) throw error; + alert('Profile updated successfully!'); + } catch (error) { + console.error('Error updating profile:', error); + alert('Failed to update profile'); + } finally { + setSaving(false); + } + }; + + if (loading) { + return ( +
+
+
+ ); + } + + return ( +
+
+

Profile Settings

+ +
+
+ + setUsername(e.target.value)} + className="w-full px-4 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent" + placeholder="Enter your username" + /> +
+ +
+ +
+ {THEME_COLORS.map((color) => ( + + ))} +
+
+ + +
+
+
+ ); } diff --git a/src/contexts/AuthContext.tsx b/src/contexts/AuthContext.tsx index b7629e5..c71eb95 100644 --- a/src/contexts/AuthContext.tsx +++ b/src/contexts/AuthContext.tsx @@ -1,107 +1,107 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { User } from '@supabase/supabase-js'; -import { supabase } from '../lib/supabase'; - -interface AuthContextType { - user: User | null; - loading: boolean; - signIn: (email: string, password: string) => Promise; - signUp: (email: string, password: string) => Promise; - signOut: () => Promise; -} - -const AuthContext = createContext(undefined); - -export function AuthProvider({ children }: { children: React.ReactNode }) { - const [user, setUser] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // Check active sessions and sets the user - supabase.auth.getSession().then(({ data: { session } }) => { - setUser(session?.user ?? null); - setLoading(false); - }); - - // Listen for changes on auth state - const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null); - setLoading(false); - - // If this is a new sign up, create a profile using setTimeout to avoid deadlock - if (_event === 'SIGNED_IN' && session) { - setTimeout(async () => { - const { error } = await supabase - .from('profiles') - .upsert( - { - id: session.user.id, - theme_color: 'blue' // Default theme color - }, - { onConflict: 'id' } - ); - - if (error) { - console.error('Error creating profile:', error); - } - }, 0); - } - }); - - return () => subscription.unsubscribe(); - }, []); - - const signUp = async (email: string, password: string) => { - const { error, data } = await supabase.auth.signUp({ - email, - password, - }); - - if (error) throw error; - - // Create a profile for the new user using setTimeout to avoid deadlock - if (data.user) { - setTimeout(async () => { - const { error: profileError } = await supabase - .from('profiles') - .insert({ - id: data.user!.id, - theme_color: 'blue' // Default theme color - }); - - if (profileError) { - console.error('Error creating profile:', profileError); - // Optionally handle the error (e.g., delete the auth user) - throw profileError; - } - }, 0); - } - }; - - const signIn = async (email: string, password: string) => { - const { error } = await supabase.auth.signInWithPassword({ - email, - password, - }); - if (error) throw error; - }; - - const signOut = async () => { - const { error } = await supabase.auth.signOut(); - if (error) throw error; - }; - - return ( - - {children} - - ); -} - -export function useAuth() { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { User } from '@supabase/supabase-js'; +import { supabase } from '../lib/supabase'; + +interface AuthContextType { + user: User | null; + loading: boolean; + signIn: (email: string, password: string) => Promise; + signUp: (email: string, password: string) => Promise; + signOut: () => Promise; +} + +const AuthContext = createContext(undefined); + +export function AuthProvider({ children }: { children: React.ReactNode }) { + const [user, setUser] = useState(null); + const [loading, setLoading] = useState(true); + + useEffect(() => { + // Check active sessions and sets the user + supabase.auth.getSession().then(({ data: { session } }) => { + setUser(session?.user ?? null); + setLoading(false); + }); + + // Listen for changes on auth state + const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { + setUser(session?.user ?? null); + setLoading(false); + + // If this is a new sign up, create a profile using setTimeout to avoid deadlock + if (_event === 'SIGNED_IN' && session) { + setTimeout(async () => { + const { error } = await supabase + .from('profiles') + .upsert( + { + id: session.user.id, + theme_color: 'blue' // Default theme color + }, + { onConflict: 'id' } + ); + + if (error) { + console.error('Error creating profile:', error); + } + }, 0); + } + }); + + return () => subscription.unsubscribe(); + }, []); + + const signUp = async (email: string, password: string) => { + const { error, data } = await supabase.auth.signUp({ + email, + password, + }); + + if (error) throw error; + + // Create a profile for the new user using setTimeout to avoid deadlock + if (data.user) { + setTimeout(async () => { + const { error: profileError } = await supabase + .from('profiles') + .insert({ + id: data.user!.id, + theme_color: 'blue' // Default theme color + }); + + if (profileError) { + console.error('Error creating profile:', profileError); + // Optionally handle the error (e.g., delete the auth user) + throw profileError; + } + }, 0); + } + }; + + const signIn = async (email: string, password: string) => { + const { error } = await supabase.auth.signInWithPassword({ + email, + password, + }); + if (error) throw error; + }; + + const signOut = async () => { + const { error } = await supabase.auth.signOut(); + if (error) throw error; + }; + + return ( + + {children} + + ); +} + +export function useAuth() { + const context = useContext(AuthContext); + if (context === undefined) { + throw new Error('useAuth must be used within an AuthProvider'); + } + return context; } diff --git a/src/index.css b/src/index.css index 448d40d..547fe27 100644 --- a/src/index.css +++ b/src/index.css @@ -1,16 +1,16 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - -@keyframes slide { - 0% { - transform: translateX(0); - } - 100% { - transform: translateX(-50%); - } -} - -.animate-slide { - animation: slide 60s linear infinite; +@tailwind base; +@tailwind components; +@tailwind utilities; + +@keyframes slide { + 0% { + transform: translateX(0); + } + 100% { + transform: translateX(-50%); + } +} + +.animate-slide { + animation: slide 60s linear infinite; } diff --git a/src/lib/supabase.ts b/src/lib/supabase.ts index 95674ba..c85bd01 100644 --- a/src/lib/supabase.ts +++ b/src/lib/supabase.ts @@ -1,10 +1,10 @@ -import {createClient} from "@supabase/supabase-js"; - -const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; -const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; - -if (!supabaseUrl || !supabaseAnonKey) { - throw new Error('Missing Supabase environment variables'); -} - +import {createClient} from "@supabase/supabase-js"; + +const supabaseUrl = import.meta.env.VITE_SUPABASE_URL; +const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY; + +if (!supabaseUrl || !supabaseAnonKey) { + throw new Error('Missing Supabase environment variables'); +} + export const supabase = createClient(supabaseUrl, supabaseAnonKey); diff --git a/src/main.tsx b/src/main.tsx index a206d2d..fb41176 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,11 +1,11 @@ -import { StrictMode } from 'react'; - import { createRoot } from 'react-dom/client'; - import App from './App.tsx'; - import './index.css'; - import './utils/theme.ts'; - - createRoot(document.getElementById('root')!).render( - - - +import { StrictMode } from 'react'; + import { createRoot } from 'react-dom/client'; + import App from './App.tsx'; + import './index.css'; + import './utils/theme.ts'; + + createRoot(document.getElementById('root')!).render( + + + ); diff --git a/src/services/api.ts b/src/services/api.ts index 4ab728a..53bac1d 100644 --- a/src/services/api.ts +++ b/src/services/api.ts @@ -23,19 +23,32 @@ export const getCardById = async (cardId: string): Promise => { return await response.json(); }; -export const getCardsByIds = async (cardIds: string[]): Promise => { - - //75 cards per request max - const response = await fetch(`${SCRYFALL_API}/cards/collection`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ - identifiers: cardIds.map((id) => ({ id })), - }), - }); - - const data = await response.json(); - return data.data; +const chunkArray = (array: string[], size: number): string[][] => { + const chunkedArray: string[][] = []; + for (let i = 0; i < array.length; i += size) { + chunkedArray.push(array.slice(i, i + size)); + } + return chunkedArray; +}; + +export const getCardsByIds = async (cardIds: string[]): Promise => { + const chunkedCardIds = chunkArray(cardIds, 75); + let allCards: Card[] = []; + + for (const chunk of chunkedCardIds) { + const response = await fetch(`${SCRYFALL_API}/cards/collection`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + identifiers: chunk.map((id) => ({ id })), + }), + }); + + const data = await response.json(); + allCards = allCards.concat(data.data); + } + + return allCards; }; diff --git a/src/types/index.ts b/src/types/index.ts index 1169805..8aca375 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,37 +1,37 @@ -export interface User { - id: string; - email: string; - username: string; - themeColor: 'red' | 'green' | 'blue' | 'yellow' | 'grey' | 'purple'; -} - -export interface Card { - id: string; - name: string; - image_uris?: { - normal: string; - art_crop: string; - }; - mana_cost?: string; - type_line?: string; - oracle_text?: string; - colors?: string[]; -} - -export interface Deck { - id: string; - name: string; - format: string; - cards: { card: Card; quantity: number, is_commander: boolean }[]; - userId: string; - createdAt: Date; - updatedAt: Date; -} - -export interface CardEntity { - id: string; - deck_id: string; - card_id: string; - quantity: number; - is_commander: boolean; +export interface User { + id: string; + email: string; + username: string; + themeColor: 'red' | 'green' | 'blue' | 'yellow' | 'grey' | 'purple'; +} + +export interface Card { + id: string; + name: string; + image_uris?: { + normal: string; + art_crop: string; + }; + mana_cost?: string; + type_line?: string; + oracle_text?: string; + colors?: string[]; +} + +export interface Deck { + id: string; + name: string; + format: string; + cards: { card: Card; quantity: number, is_commander: boolean }[]; + userId: string; + createdAt: Date; + updatedAt: Date; +} + +export interface CardEntity { + id: string; + deck_id: string; + card_id: string; + quantity: number; + is_commander: boolean; } diff --git a/src/utils/deckValidation.ts b/src/utils/deckValidation.ts index 50725dc..465a47c 100644 --- a/src/utils/deckValidation.ts +++ b/src/utils/deckValidation.ts @@ -1,81 +1,81 @@ -import { Deck } from '../types'; - -interface DeckValidation { - isValid: boolean; - errors: string[]; -} - -const FORMAT_RULES = { - standard: { - minCards: 60, - maxCards: undefined, - maxCopies: 4, - }, - modern: { - minCards: 60, - maxCards: undefined, - maxCopies: 4, - }, - commander: { - minCards: 100, - maxCards: 100, - maxCopies: 1, - requiresCommander: true, - }, - legacy: { - minCards: 60, - maxCards: undefined, - maxCopies: 4, - }, - vintage: { - minCards: 60, - maxCards: undefined, - maxCopies: 4, - }, - pauper: { - minCards: 60, - maxCards: undefined, - maxCopies: 4, - }, -}; - -export function validateDeck(deck: Deck): DeckValidation { - const rules = FORMAT_RULES[deck.format as keyof typeof FORMAT_RULES]; - const errors: string[] = []; - - // Count total cards - const totalCards = deck.cards.reduce((acc, curr) => acc + curr.quantity, 0); - - // Check minimum cards - if (totalCards < rules.minCards) { - errors.push(`Deck must contain at least ${rules.minCards} cards`); - } - - // Check maximum cards - if (rules.maxCards && totalCards > rules.maxCards) { - errors.push(`Deck must not contain more than ${rules.maxCards} cards`); - } - - // Check card copies - const cardCounts = new Map(); - for (const element of deck.cards) { - const {card, quantity} = element; - //console.log("card", card); - const currentCount = cardCounts.get(card.id) || 0; - cardCounts.set(card.id, currentCount + quantity); - } - - cardCounts.forEach((count, cardName) => { - const card = deck.cards.find(c => c.card.id === cardName)?.card; - const isBasicLand = card?.name === 'Plains' || card?.name === 'Island' || card?.name === 'Swamp' || card?.name === 'Mountain' || card?.name === 'Forest'; - - if (!isBasicLand && count > rules.maxCopies) { - errors.push(`${cardName} has too many copies (max ${rules.maxCopies})`); - } - }); - - return { - isValid: errors.length === 0, - errors, - }; +import { Deck } from '../types'; + +interface DeckValidation { + isValid: boolean; + errors: string[]; +} + +const FORMAT_RULES = { + standard: { + minCards: 60, + maxCards: undefined, + maxCopies: 4, + }, + modern: { + minCards: 60, + maxCards: undefined, + maxCopies: 4, + }, + commander: { + minCards: 100, + maxCards: 100, + maxCopies: 1, + requiresCommander: true, + }, + legacy: { + minCards: 60, + maxCards: undefined, + maxCopies: 4, + }, + vintage: { + minCards: 60, + maxCards: undefined, + maxCopies: 4, + }, + pauper: { + minCards: 60, + maxCards: undefined, + maxCopies: 4, + }, +}; + +export function validateDeck(deck: Deck): DeckValidation { + const rules = FORMAT_RULES[deck.format as keyof typeof FORMAT_RULES]; + const errors: string[] = []; + + // Count total cards + const totalCards = deck.cards.reduce((acc, curr) => acc + curr.quantity, 0); + + // Check minimum cards + if (totalCards < rules.minCards) { + errors.push(`Deck must contain at least ${rules.minCards} cards`); + } + + // Check maximum cards + if (rules.maxCards && totalCards > rules.maxCards) { + errors.push(`Deck must not contain more than ${rules.maxCards} cards`); + } + + // Check card copies + const cardCounts = new Map(); + for (const element of deck.cards) { + const {card, quantity} = element; + //console.log("card", card); + const currentCount = cardCounts.get(card.id) || 0; + cardCounts.set(card.id, currentCount + quantity); + } + + cardCounts.forEach((count, cardName) => { + const card = deck.cards.find(c => c.card.id === cardName)?.card; + const isBasicLand = card?.name === 'Plains' || card?.name === 'Island' || card?.name === 'Swamp' || card?.name === 'Mountain' || card?.name === 'Forest'; + + if (!isBasicLand && count > rules.maxCopies) { + errors.push(`${cardName} has too many copies (max ${rules.maxCopies})`); + } + }); + + return { + isValid: errors.length === 0, + errors, + }; } diff --git a/src/utils/theme.ts b/src/utils/theme.ts index ff3f33a..16e1c3c 100644 --- a/src/utils/theme.ts +++ b/src/utils/theme.ts @@ -1,42 +1,42 @@ -export const themeColors = { - red: { - primary: '#ef4444', - secondary: '#b91c1c', - hover: '#dc2626' - }, - green: { - primary: '#22c55e', - secondary: '#15803d', - hover: '#16a34a' - }, - blue: { - primary: '#3b82f6', - secondary: '#1d4ed8', - hover: '#2563eb' - }, - yellow: { - primary: '#eab308', - secondary: '#a16207', - hover: '#ca8a04' - }, - grey: { - primary: '#6b7280', - secondary: '#374151', - hover: '#4b5563' - }, - purple: { - primary: '#a855f7', - secondary: '#7e22ce', - hover: '#9333ea' - }, - white: { - primary: '#f8fafc', - secondary: '#e2e8f0', - hover: '#f1f5f9', - }, - black: { - primary: '#0f172a', - secondary: '#1e293b', - hover: '#1e293b', - } +export const themeColors = { + red: { + primary: '#ef4444', + secondary: '#b91c1c', + hover: '#dc2626' + }, + green: { + primary: '#22c55e', + secondary: '#15803d', + hover: '#16a34a' + }, + blue: { + primary: '#3b82f6', + secondary: '#1d4ed8', + hover: '#2563eb' + }, + yellow: { + primary: '#eab308', + secondary: '#a16207', + hover: '#ca8a04' + }, + grey: { + primary: '#6b7280', + secondary: '#374151', + hover: '#4b5563' + }, + purple: { + primary: '#a855f7', + secondary: '#7e22ce', + hover: '#9333ea' + }, + white: { + primary: '#f8fafc', + secondary: '#e2e8f0', + hover: '#f1f5f9', + }, + black: { + primary: '#0f172a', + secondary: '#1e293b', + hover: '#1e293b', + } }; diff --git a/supabase/migrations/20250131132458_black_frost.sql b/supabase/migrations/20250131132458_black_frost.sql index c629cfc..15da4f4 100644 --- a/supabase/migrations/20250131132458_black_frost.sql +++ b/supabase/migrations/20250131132458_black_frost.sql @@ -1,143 +1,143 @@ -/* - # Initial Schema Setup - - 1. New Tables - - `profiles` - - `id` (uuid, references auth.users) - - `username` (text, unique) - - `theme_color` (text, enum) - - `created_at` (timestamptz) - - `updated_at` (timestamptz) - - `collections` - - `id` (uuid) - - `user_id` (uuid, references profiles) - - `card_id` (text) - - `quantity` (integer) - - `created_at` (timestamptz) - - `updated_at` (timestamptz) - - `decks` - - `id` (uuid) - - `user_id` (uuid, references profiles) - - `name` (text) - - `format` (text) - - `created_at` (timestamptz) - - `updated_at` (timestamptz) - - `deck_cards` - - `id` (uuid) - - `deck_id` (uuid, references decks) - - `card_id` (text) - - `quantity` (integer) - - `is_commander` (boolean) - - 2. Security - - Enable RLS on all tables - - Add policies for authenticated users to manage their own data -*/ - --- Create profiles table -CREATE TABLE public.profiles ( - id uuid PRIMARY KEY REFERENCES auth.users, - username text UNIQUE, - theme_color text CHECK (theme_color IN ('red', 'green', 'blue', 'yellow', 'grey', 'purple')), - created_at timestamptz DEFAULT now(), - updated_at timestamptz DEFAULT now() -); - --- Create collections table -CREATE TABLE public.collections ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - user_id uuid REFERENCES public.profiles(id) NOT NULL, - card_id text NOT NULL, - quantity integer DEFAULT 1, - created_at timestamptz DEFAULT now(), - updated_at timestamptz DEFAULT now() -); - --- Create decks table -CREATE TABLE public.decks ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - user_id uuid REFERENCES public.profiles(id) NOT NULL, - name text NOT NULL, - format text NOT NULL, - created_at timestamptz DEFAULT now(), - updated_at timestamptz DEFAULT now() -); - --- Create deck_cards table -CREATE TABLE public.deck_cards ( - id uuid PRIMARY KEY DEFAULT gen_random_uuid(), - deck_id uuid REFERENCES public.decks(id) NOT NULL, - card_id text NOT NULL, - quantity integer DEFAULT 1, - is_commander boolean DEFAULT false -); - --- Enable Row Level Security -ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; -ALTER TABLE public.collections ENABLE ROW LEVEL SECURITY; -ALTER TABLE public.decks ENABLE ROW LEVEL SECURITY; -ALTER TABLE public.deck_cards ENABLE ROW LEVEL SECURITY; - --- Profiles policies -CREATE POLICY "Users can view their own profile" - ON public.profiles - FOR SELECT - TO authenticated - USING (auth.uid() = id); - -CREATE POLICY "Users can update their own profile" - ON public.profiles - FOR UPDATE - TO authenticated - USING (auth.uid() = id); - --- Collections policies -CREATE POLICY "Users can view their own collection" - ON public.collections - FOR SELECT - TO authenticated - USING (user_id = auth.uid()); - -CREATE POLICY "Users can manage their own collection" - ON public.collections - FOR ALL - TO authenticated - USING (user_id = auth.uid()); - --- Decks policies -CREATE POLICY "Users can view their own decks" - ON public.decks - FOR SELECT - TO authenticated - USING (user_id = auth.uid()); - -CREATE POLICY "Users can manage their own decks" - ON public.decks - FOR ALL - TO authenticated - USING (user_id = auth.uid()); - --- Deck cards policies -CREATE POLICY "Users can view cards in their decks" - ON public.deck_cards - FOR SELECT - TO authenticated - USING ( - EXISTS ( - SELECT 1 FROM public.decks - WHERE decks.id = deck_cards.deck_id - AND decks.user_id = auth.uid() - ) - ); - -CREATE POLICY "Users can manage cards in their decks" - ON public.deck_cards - FOR ALL - TO authenticated - USING ( - EXISTS ( - SELECT 1 FROM public.decks - WHERE decks.id = deck_cards.deck_id - AND decks.user_id = auth.uid() - ) +/* + # Initial Schema Setup + + 1. New Tables + - `profiles` + - `id` (uuid, references auth.users) + - `username` (text, unique) + - `theme_color` (text, enum) + - `created_at` (timestamptz) + - `updated_at` (timestamptz) + - `collections` + - `id` (uuid) + - `user_id` (uuid, references profiles) + - `card_id` (text) + - `quantity` (integer) + - `created_at` (timestamptz) + - `updated_at` (timestamptz) + - `decks` + - `id` (uuid) + - `user_id` (uuid, references profiles) + - `name` (text) + - `format` (text) + - `created_at` (timestamptz) + - `updated_at` (timestamptz) + - `deck_cards` + - `id` (uuid) + - `deck_id` (uuid, references decks) + - `card_id` (text) + - `quantity` (integer) + - `is_commander` (boolean) + + 2. Security + - Enable RLS on all tables + - Add policies for authenticated users to manage their own data +*/ + +-- Create profiles table +CREATE TABLE public.profiles ( + id uuid PRIMARY KEY REFERENCES auth.users, + username text UNIQUE, + theme_color text CHECK (theme_color IN ('red', 'green', 'blue', 'yellow', 'grey', 'purple')), + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Create collections table +CREATE TABLE public.collections ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid REFERENCES public.profiles(id) NOT NULL, + card_id text NOT NULL, + quantity integer DEFAULT 1, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Create decks table +CREATE TABLE public.decks ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + user_id uuid REFERENCES public.profiles(id) NOT NULL, + name text NOT NULL, + format text NOT NULL, + created_at timestamptz DEFAULT now(), + updated_at timestamptz DEFAULT now() +); + +-- Create deck_cards table +CREATE TABLE public.deck_cards ( + id uuid PRIMARY KEY DEFAULT gen_random_uuid(), + deck_id uuid REFERENCES public.decks(id) NOT NULL, + card_id text NOT NULL, + quantity integer DEFAULT 1, + is_commander boolean DEFAULT false +); + +-- Enable Row Level Security +ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.collections ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.decks ENABLE ROW LEVEL SECURITY; +ALTER TABLE public.deck_cards ENABLE ROW LEVEL SECURITY; + +-- Profiles policies +CREATE POLICY "Users can view their own profile" + ON public.profiles + FOR SELECT + TO authenticated + USING (auth.uid() = id); + +CREATE POLICY "Users can update their own profile" + ON public.profiles + FOR UPDATE + TO authenticated + USING (auth.uid() = id); + +-- Collections policies +CREATE POLICY "Users can view their own collection" + ON public.collections + FOR SELECT + TO authenticated + USING (user_id = auth.uid()); + +CREATE POLICY "Users can manage their own collection" + ON public.collections + FOR ALL + TO authenticated + USING (user_id = auth.uid()); + +-- Decks policies +CREATE POLICY "Users can view their own decks" + ON public.decks + FOR SELECT + TO authenticated + USING (user_id = auth.uid()); + +CREATE POLICY "Users can manage their own decks" + ON public.decks + FOR ALL + TO authenticated + USING (user_id = auth.uid()); + +-- Deck cards policies +CREATE POLICY "Users can view cards in their decks" + ON public.deck_cards + FOR SELECT + TO authenticated + USING ( + EXISTS ( + SELECT 1 FROM public.decks + WHERE decks.id = deck_cards.deck_id + AND decks.user_id = auth.uid() + ) + ); + +CREATE POLICY "Users can manage cards in their decks" + ON public.deck_cards + FOR ALL + TO authenticated + USING ( + EXISTS ( + SELECT 1 FROM public.decks + WHERE decks.id = deck_cards.deck_id + AND decks.user_id = auth.uid() + ) ); diff --git a/tailwind.config.js b/tailwind.config.js index 133c495..e702563 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,14 +1,14 @@ -/** @type {import('tailwindcss').Config} */ -export default { - content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], - theme: { - extend: { - colors: { - background: '#0f172a', - primary: 'var(--color-primary)', - secondary: 'var(--color-secondary)', - }, - }, - }, - plugins: [], +/** @type {import('tailwindcss').Config} */ +export default { + content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'], + theme: { + extend: { + colors: { + background: '#0f172a', + primary: 'var(--color-primary)', + secondary: 'var(--color-secondary)', + }, + }, + }, + plugins: [], }; diff --git a/tsconfig.app.json b/tsconfig.app.json index fd906cc..3bf78c6 100644 --- a/tsconfig.app.json +++ b/tsconfig.app.json @@ -1,25 +1,25 @@ -{ - "compilerOptions": { - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true, - "allowSyntheticDefaultImports": true - }, - "include": ["src"] +{ + "compilerOptions": { + "target": "ES2020", + "useDefineForClassFields": true, + "lib": ["ES2020", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true, + "allowSyntheticDefaultImports": true + }, + "include": ["src"] } diff --git a/tsconfig.json b/tsconfig.json index 1ffef60..c16d0de 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,7 +1,7 @@ -{ - "files": [], - "references": [ - { "path": "./tsconfig.app.json" }, - { "path": "./tsconfig.node.json" } - ] +{ + "files": [], + "references": [ + { "path": "./tsconfig.app.json" }, + { "path": "./tsconfig.node.json" } + ] } diff --git a/tsconfig.node.json b/tsconfig.node.json index 0d3d714..d06bdb4 100644 --- a/tsconfig.node.json +++ b/tsconfig.node.json @@ -1,22 +1,22 @@ -{ - "compilerOptions": { - "target": "ES2022", - "lib": ["ES2023"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - - /* Linting */ - "strict": true, - "noUnusedLocals": true, - "noUnusedParameters": true, - "noFallthroughCasesInSwitch": true - }, - "include": ["vite.config.ts"] +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "isolatedModules": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["vite.config.ts"] } diff --git a/vite.config.ts b/vite.config.ts index 147380a..a5ed6d7 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,10 +1,10 @@ -import { defineConfig } from 'vite'; -import react from '@vitejs/plugin-react'; - -// https://vitejs.dev/config/ -export default defineConfig({ - plugins: [react()], - optimizeDeps: { - exclude: ['lucide-react'], - }, +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + optimizeDeps: { + exclude: ['lucide-react'], + }, });