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 (
-
- );
- 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 (
+
+ );
+ 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
-
-
- {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
+
+
+ {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 */}
-
-
- {/* Search Results */}
- {searchResults.length > 0 && (
-
-
Search Results
-
- {searchResults.map(card => (
-
- {card.image_uris?.normal && (
-
- )}
-
-
{card.name}
-
addToCollection(card)}
- className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2"
- >
-
- Add to Collection
-
-
-
- ))}
-
-
- )}
-
- {/* Collection */}
-
-
My Cards
-
- {collection.map(({ card, quantity }) => (
-
- {card.image_uris?.normal && (
-
- )}
-
-
-
{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 */}
+
+
+ {/* Search Results */}
+ {searchResults.length > 0 && (
+
+
Search Results
+
+ {searchResults.map(card => (
+
+ {card.image_uris?.normal && (
+
+ )}
+
+
{card.name}
+
addToCollection(card)}
+ className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2"
+ >
+
+ Add to Collection
+
+
+
+ ))}
+
+
+ )}
+
+ {/* Collection */}
+
+
My Cards
+
+ {collection.map(({ card, quantity }) => (
+
+ {card.image_uris?.normal && (
+
+ )}
+
+
+
{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)}
- >
-
-
-
-
-
-
-
-
{deck.name}
- {validation.isValid ? (
-
-
- Legal
-
- ) : (
-
- )}
-
-
-
- {deck.format}
- {deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards
-
-
- {commander && (
-
- Commander: {commander.name}
-
- )}
-
-
{
- e.stopPropagation();
- onEdit?.(deck.id);
- }}
- className="mt-4 w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2 text-white"
- >
-
- Edit Deck
-
-
-
- );
+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)}
+ >
+
+
+
+
+
+
+
+
{deck.name}
+ {validation.isValid ? (
+
+
+ Legal
+
+ ) : (
+
+ )}
+
+
+
+ {deck.format}
+ {deck.cards.reduce((acc, curr) => acc + curr.quantity, 0)} cards
+
+
+ {commander && (
+
+ Commander: {commander.name}
+
+ )}
+
+
{
+ e.stopPropagation();
+ onEdit?.(deck.id);
+ }}
+ className="mt-4 w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2 text-white"
+ >
+
+ Edit Deck
+
+
+
+ );
}
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 */}
-
-
-
-
- {searchResults.map(card => (
-
-
-
-
{card.name}
-
addCardToDeck(card)}
- className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2"
- >
-
- Add to Deck
-
-
-
- ))}
-
-
-
- {/* 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"
- />
-
-
setDeckFormat(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"
- >
- Standard
- Modern
- Commander
- Legacy
- Vintage
- Pauper
-
-
- {deckFormat === 'commander' && (
-
{
- const card =
- selectedCards.find(c => c.card.id === e.target.value)?.card ||
- null;
- setCommander(card);
- }}
- 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"
- >
- Select Commander
- {selectedCards
- .filter(c =>
- c.card.type_line?.toLowerCase().includes('legendary')
- )
- .map(({ card }) => (
-
- {card.name}
-
- ))}
-
- )}
-
-
-
- {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.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"
- />
-
removeCardFromDeck(card.id)}
- className="text-red-500 hover:text-red-400"
- >
-
-
-
- ))}
-
-
-
- Total Price: ${totalPrice.toFixed(2)}
-
-
- {deckSize > 0 && (
-
- Suggested Land Count: {suggestedLandCountValue}
- {Object.entries(suggestedLands).map(([landType, count]) => (
-
- {landType}: {count}
-
- ))}
-
- )}
-
- {deckSize > 0 && (
-
-
- Add Suggested Lands
-
- )}
-
-
- {isSaving ? (
- <>
-
- Save Deck
- >
- ) : (
- <>
-
- {initialDeck ? 'Update Deck' : 'Save Deck'}
- >
- )}
-
-
-
-
-
- {snackbar && (
-
-
-
- {snackbar.type === 'success' ? (
-
- ) : (
-
- )}
- {snackbar.message}
-
-
setSnackbar(null)} className="ml-4 text-gray-200 hover:text-white focus:outline-none">
-
-
-
-
- )}
-
- );
+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 */}
+
+
+
+
+ {searchResults.map(card => (
+
+
+
+
{card.name}
+
addCardToDeck(card)}
+ className="w-full px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2"
+ >
+
+ Add to Deck
+
+
+
+ ))}
+
+
+
+ {/* 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"
+ />
+
+
setDeckFormat(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"
+ >
+ Standard
+ Modern
+ Commander
+ Legacy
+ Vintage
+ Pauper
+
+
+ {deckFormat === 'commander' && (
+
{
+ const card =
+ selectedCards.find(c => c.card.id === e.target.value)?.card ||
+ null;
+ setCommander(card);
+ }}
+ 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"
+ >
+ Select Commander
+ {selectedCards
+ .filter(c =>
+ c.card.type_line?.toLowerCase().includes('legendary')
+ )
+ .map(({ card }) => (
+
+ {card.name}
+
+ ))}
+
+ )}
+
+
+
+ {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.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"
+ />
+
removeCardFromDeck(card.id)}
+ className="text-red-500 hover:text-red-400"
+ >
+
+
+
+ ))}
+
+
+
+ Total Price: ${totalPrice.toFixed(2)}
+
+
+ {deckSize > 0 && (
+
+ Suggested Land Count: {suggestedLandCountValue}
+ {Object.entries(suggestedLands).map(([landType, count]) => (
+
+ {landType}: {count}
+
+ ))}
+
+ )}
+
+ {deckSize > 0 && (
+
+
+ Add Suggested Lands
+
+ )}
+
+
+ {isSaving ? (
+ <>
+
+ Save Deck
+ >
+ ) : (
+ <>
+
+ {initialDeck ? 'Update Deck' : 'Save Deck'}
+ >
+ )}
+
+
+
+
+
+ {snackbar && (
+
+
+
+ {snackbar.type === 'success' ? (
+
+ ) : (
+
+ )}
+ {snackbar.message}
+
+
setSnackbar(null)} className="ml-4 text-gray-200 hover:text-white focus:outline-none">
+
+
+
+
+ )}
+
+ );
}
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 = () => (
-
- );
-
- 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}
-
-
updateLife(player.id, 1)}
- className="bg-green-600 hover:bg-green-700 rounded-full p-2"
- >
-
-
-
updateLife(player.id, -1)}
- className="bg-red-600 hover:bg-red-700 rounded-full p-2"
- >
-
-
-
-
-
- );
- })}
-
-
- );
-
- 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 = () => (
+
+ );
+
+ 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}
+
+
updateLife(player.id, 1)}
+ className="bg-green-600 hover:bg-green-700 rounded-full p-2"
+ >
+
+
+
updateLife(player.id, -1)}
+ className="bg-red-600 hover:bg-red-700 rounded-full p-2"
+ >
+
+
+
+
+
+ );
+ })}
+
+
+ );
+
+ 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}
-
- )}
-
-
-
-
- setIsSignUp(!isSignUp)}
- className="text-blue-400 hover:text-blue-300"
- >
- {isSignUp ? 'Already have an account? Sign In' : 'Need an account? Sign Up'}
-
-
-
-
- );
+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}
+
+ )}
+
+
+
+
+ setIsSignUp(!isSignUp)}
+ className="text-blue-400 hover:text-blue-300"
+ >
+ {isSignUp ? 'Already have an account? Sign In' : 'Need an account? Sign Up'}
+
+
+
+
+ );
}
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 ? (
-
- ) : (
-
- 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 ? (
+
+ ) : (
+
+ 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 */}
-
-
-
-
- Deckerr
- {navItems.map((item) => (
- setCurrentPage(item.id)}
- className={`flex items-center space-x-2 px-3 py-2 rounded-md text-sm font-medium transition-colors
- ${currentPage === item.id
- ? 'text-white bg-gray-900'
- : 'text-gray-300 hover:text-white hover:bg-gray-700'
- }`}
- >
-
- {item.label}
-
- ))}
-
-
- {user && (
-
-
-
setShowDropdown(!showDropdown)}
- className="flex items-center space-x-3 px-3 py-2 rounded-md hover:bg-gray-700"
- >
-
- {username || user.email}
-
-
-
- {showDropdown && (
-
- {
- setCurrentPage('profile');
- setShowDropdown(false);
- }}
- className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
- >
-
- Profile Settings
-
-
-
- Sign Out
-
-
- )}
-
-
- )}
-
-
-
-
- {/* Mobile Navigation - Bottom */}
-
-
-
Deckerr
-
-
setShowMobileMenu(!showMobileMenu)}
- className="text-gray-300 hover:text-white"
- >
-
-
- {showMobileMenu && (
-
- {navItems.map((item) => (
- {
- setCurrentPage(item.id);
- setShowMobileMenu(false);
- }}
- className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
- >
-
- {item.label}
-
- ))}
- {user && (
- <>
- {
- setCurrentPage('profile');
- setShowMobileMenu(false);
- }}
- className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
- >
-
- Profile Settings
-
-
-
- Sign Out
-
- >
- )}
-
- )}
-
-
-
-
- {/* 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 */}
+
+
+
+
+ Deckerr
+ {navItems.map((item) => (
+ setCurrentPage(item.id)}
+ className={`flex items-center space-x-2 px-3 py-2 rounded-md text-sm font-medium transition-colors
+ ${currentPage === item.id
+ ? 'text-white bg-gray-900'
+ : 'text-gray-300 hover:text-white hover:bg-gray-700'
+ }`}
+ >
+
+ {item.label}
+
+ ))}
+
+
+ {user && (
+
+
+
setShowDropdown(!showDropdown)}
+ className="flex items-center space-x-3 px-3 py-2 rounded-md hover:bg-gray-700"
+ >
+
+ {username || user.email}
+
+
+
+ {showDropdown && (
+
+ {
+ setCurrentPage('profile');
+ setShowDropdown(false);
+ }}
+ className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
+ >
+
+ Profile Settings
+
+
+
+ Sign Out
+
+
+ )}
+
+
+ )}
+
+
+
+
+ {/* Mobile Navigation - Bottom */}
+
+
+
Deckerr
+
+
setShowMobileMenu(!showMobileMenu)}
+ className="text-gray-300 hover:text-white"
+ >
+
+
+ {showMobileMenu && (
+
+ {navItems.map((item) => (
+ {
+ setCurrentPage(item.id);
+ setShowMobileMenu(false);
+ }}
+ className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
+ >
+
+ {item.label}
+
+ ))}
+ {user && (
+ <>
+ {
+ setCurrentPage('profile');
+ setShowMobileMenu(false);
+ }}
+ className="flex items-center space-x-2 w-full px-4 py-2 text-sm text-gray-300 hover:bg-gray-700"
+ >
+
+ Profile Settings
+
+
+
+ Sign Out
+
+ >
+ )}
+
+ )}
+
+
+
+
+ {/* 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 (
-
- );
+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 (
+
+ );
}
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'],
+ },
});