Add PWA support and update app metadata for Deckerr

This commit is contained in:
Matthieu
2025-11-21 16:41:47 +01:00
parent 57f0e7efe7
commit 73b7735074
19 changed files with 9454 additions and 186 deletions

View File

@@ -192,9 +192,9 @@ const CardSearch = () => {
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="min-h-screen bg-gray-900 text-white p-3 sm:p-6 md:pt-16 pb-16 md:pb-0">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-6">Card Search</h1>
<h1 className="text-2xl md:text-3xl font-bold mb-4 md:mb-6">Card Search</h1>
<form onSubmit={handleSearch} className="mb-8 space-y-4">
{/* Card Details */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -295,17 +295,17 @@ const CardSearch = () => {
</div>
{/* Mana Cost */}
<div className="grid grid-cols-3 md:grid-cols-6 gap-2">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
{Object.entries(manaCost).map(([color, count]) => (
<div key={color} className="flex items-center space-x-2">
<span style={{ fontSize: '1.5em' }}>
<span style={{ fontSize: '1.2em' }} className="md:text-[1.5em]">
{color === 'W' ? '⚪' : color === 'U' ? '🔵' : color === 'B' ? '⚫' : color === 'R' ? '🔴' : color === 'G' ? '🟢' : '🟤'}
</span>
<input
type="number"
value={count}
onChange={(e) => setManaCost({ ...manaCost, [color]: parseInt(e.target.value) })}
className="w-16 px-2 py-1 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white"
className="w-14 sm:w-16 px-2 py-2 bg-gray-800 border border-gray-700 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent text-white"
min="0"
/>
</div>
@@ -586,7 +586,7 @@ const CardSearch = () => {
<button
type="submit"
className="mt-4 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg"
className="mt-4 w-full sm:w-auto min-h-[44px] px-6 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg font-medium text-base"
>
Search
</button>
@@ -605,7 +605,7 @@ const CardSearch = () => {
)}
{searchResults && searchResults.length > 0 && (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3 sm:gap-4">
{searchResults.map((card) => {
const currentFaceIndex = getCurrentFaceIndex(card.id);
const isMultiFaced = isDoubleFaced(card);

View File

@@ -189,9 +189,9 @@ export default function Collection() {
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="min-h-screen bg-gray-900 text-white p-3 sm:p-6 md:pt-16 pb-16 md:pb-0">
<div className="max-w-7xl mx-auto">
<h1 className="text-3xl font-bold mb-6">My Collection</h1>
<h1 className="text-2xl md:text-3xl font-bold mb-4 md:mb-6">My Collection</h1>
{/* Search within collection */}
<div className="mb-8">
@@ -228,7 +228,7 @@ export default function Collection() {
<p className="text-sm">Try a different search term</p>
</div>
) : (
<div className="grid grid-cols-3 sm:grid-cols-4 md:grid-cols-6 lg:grid-cols-8 xl:grid-cols-10 gap-3">
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 lg:grid-cols-7 xl:grid-cols-9 gap-2 sm:gap-3">
{filteredCollection.map(({ card, quantity }) => {
const currentFaceIndex = getCurrentFaceIndex(card.id);
const isMultiFaced = isDoubleFaced(card);
@@ -252,7 +252,7 @@ export default function Collection() {
className="w-full h-auto"
/>
{/* Quantity badge */}
<div className="absolute top-1 right-1 bg-blue-600 text-white text-xs font-bold px-2 py-1 rounded-full shadow-lg">
<div className="absolute top-1 right-1 bg-blue-600 text-white text-xs sm:text-sm font-bold px-2 py-1 rounded-full shadow-lg">
x{quantity}
</div>
{/* Flip button for double-faced cards */}
@@ -295,7 +295,7 @@ export default function Collection() {
const displayOracleText = currentFace?.oracle_text || hoveredCard.oracle_text;
return (
<div className="fixed top-1/2 right-8 transform -translate-y-1/2 z-40 pointer-events-none">
<div className="hidden lg:block fixed top-1/2 right-8 transform -translate-y-1/2 z-40 pointer-events-none">
<div className="bg-gray-800 rounded-lg shadow-2xl p-4 max-w-md">
<div className="relative">
<img
@@ -350,14 +350,16 @@ export default function Collection() {
{/* Sliding Panel */}
<div className="fixed top-0 right-0 h-full w-full md:w-96 bg-gray-800 shadow-2xl z-50 overflow-y-auto animate-slide-in-right">
<div className="p-6">
{/* Close button */}
<button
onClick={() => setSelectedCard(null)}
className="absolute top-4 right-4 text-gray-400 hover:text-white transition-colors"
>
<X size={24} />
</button>
{/* Close button - fixed position, stays visible when scrolling */}
<button
onClick={() => setSelectedCard(null)}
className="fixed top-4 right-4 bg-gray-700 hover:bg-gray-600 text-white p-2 md:p-1.5 rounded-full transition-colors z-[60] shadow-lg"
aria-label="Close"
>
<X size={24} className="md:w-5 md:h-5" />
</button>
<div className="p-4 sm:p-6">
{/* Card Image */}
<div className="relative mb-4">
@@ -385,8 +387,8 @@ export default function Collection() {
{/* Card Info */}
<div className="space-y-4">
<div>
<h2 className="text-2xl font-bold text-white mb-2">{displayName}</h2>
<p className="text-sm text-gray-400">{displayTypeLine}</p>
<h2 className="text-xl md:text-2xl font-bold text-white mb-2">{displayName}</h2>
<p className="text-xs sm:text-sm text-gray-400">{displayTypeLine}</p>
</div>
{displayOracleText && (
@@ -442,7 +444,7 @@ export default function Collection() {
});
}}
disabled={isUpdating}
className="w-full mt-3 px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg flex items-center justify-center gap-2 transition-colors"
className="w-full mt-3 min-h-[44px] px-4 py-2 bg-red-600 hover:bg-red-700 disabled:bg-gray-600 disabled:cursor-not-allowed text-white rounded-lg flex items-center justify-center gap-2 transition-colors"
>
<Trash2 size={20} />
Remove from Collection

View File

@@ -25,7 +25,7 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
className="bg-gray-800 rounded-xl overflow-hidden shadow-lg card-hover cursor-pointer animate-scale-in"
onClick={() => onEdit?.(deck.id)}
>
<div className="relative h-48 overflow-hidden">
<div className="relative h-32 sm:h-40 md:h-48 overflow-hidden">
<img
src={commander?.image_uris?.normal || deck.cards[0]?.card.image_uris?.normal}
alt={commander?.name || deck.cards[0]?.card.name}
@@ -66,7 +66,7 @@ export default function DeckCard({ deck, onEdit }: DeckCardProps) {
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 btn-ripple transition-smooth glow-on-hover"
className="mt-4 w-full min-h-[44px] px-4 py-3 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center justify-center gap-2 text-white btn-ripple transition-smooth glow-on-hover"
>
<Edit size={20} />
Edit Deck

View File

@@ -490,9 +490,9 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
};
return (
<div className="min-h-screen bg-gray-900 text-white p-6">
<div className="min-h-screen bg-gray-900 text-white p-3 sm:p-6 md:pt-16 pb-16 md:pb-0">
<div className="max-w-7xl mx-auto">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4 sm:gap-6">
{/* Card Search Section */}
<div className="lg:col-span-2 space-y-6">
<form onSubmit={handleSearch} className="flex gap-2">
@@ -511,10 +511,10 @@ export default function DeckManager({ initialDeck, onSave }: DeckManagerProps) {
</div>
<button
type="submit"
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center gap-2"
className="min-h-[44px] px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded-lg flex items-center gap-2"
>
<Search size={20} />
Search
<span className="hidden sm:inline">Search</span>
</button>
</form>

View File

@@ -0,0 +1,128 @@
import React, { useEffect, useState } from 'react';
import { Download, X } from 'lucide-react';
interface BeforeInstallPromptEvent extends Event {
prompt: () => Promise<void>;
userChoice: Promise<{ outcome: 'accepted' | 'dismissed' }>;
}
export default function PWAInstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<BeforeInstallPromptEvent | null>(null);
const [showPrompt, setShowPrompt] = useState(false);
const [isInstalled, setIsInstalled] = useState(false);
useEffect(() => {
// Check if already installed
const isStandalone = window.matchMedia('(display-mode: standalone)').matches;
if (isStandalone || (window.navigator as any).standalone) {
setIsInstalled(true);
return;
}
// Check if user dismissed the prompt before
const dismissed = localStorage.getItem('pwa-install-dismissed');
const dismissedTime = dismissed ? parseInt(dismissed) : 0;
const daysSinceDismissed = (Date.now() - dismissedTime) / (1000 * 60 * 60 * 24);
// Show prompt again after 7 days
if (daysSinceDismissed > 7) {
localStorage.removeItem('pwa-install-dismissed');
}
const handler = (e: Event) => {
e.preventDefault();
const promptEvent = e as BeforeInstallPromptEvent;
setDeferredPrompt(promptEvent);
// Show prompt if not dismissed recently
if (!dismissed || daysSinceDismissed > 7) {
setShowPrompt(true);
}
};
window.addEventListener('beforeinstallprompt', handler);
// Detect if app was installed
window.addEventListener('appinstalled', () => {
setIsInstalled(true);
setShowPrompt(false);
setDeferredPrompt(null);
});
return () => {
window.removeEventListener('beforeinstallprompt', handler);
};
}, []);
const handleInstallClick = async () => {
if (!deferredPrompt) return;
try {
await deferredPrompt.prompt();
const choiceResult = await deferredPrompt.userChoice;
if (choiceResult.outcome === 'accepted') {
console.log('User accepted the install prompt');
setShowPrompt(false);
} else {
console.log('User dismissed the install prompt');
handleDismiss();
}
} catch (error) {
console.error('Error during installation:', error);
} finally {
setDeferredPrompt(null);
}
};
const handleDismiss = () => {
setShowPrompt(false);
localStorage.setItem('pwa-install-dismissed', Date.now().toString());
};
if (isInstalled || !showPrompt) {
return null;
}
return (
<div className="fixed bottom-20 md:bottom-4 left-4 right-4 md:left-auto md:right-4 md:max-w-sm z-50 animate-slide-in-bottom">
<div className="bg-gradient-to-r from-blue-600 to-purple-600 rounded-lg shadow-2xl p-4 text-white">
<button
onClick={handleDismiss}
className="absolute top-2 right-2 text-white/80 hover:text-white transition-colors"
aria-label="Dismiss"
>
<X size={20} />
</button>
<div className="flex items-start gap-3 pr-6">
<div className="bg-white/20 rounded-lg p-2 flex-shrink-0">
<Download size={24} />
</div>
<div className="flex-1">
<h3 className="font-bold text-lg mb-1">Install Deckerr</h3>
<p className="text-sm text-white/90 mb-3">
Install our app for quick access and offline support!
</p>
<div className="flex gap-2">
<button
onClick={handleInstallClick}
className="flex-1 bg-white text-blue-600 font-semibold py-2 px-4 rounded-lg hover:bg-blue-50 transition-colors min-h-[44px]"
>
Install
</button>
<button
onClick={handleDismiss}
className="flex-1 bg-white/20 font-semibold py-2 px-4 rounded-lg hover:bg-white/30 transition-colors min-h-[44px]"
>
Not Now
</button>
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -88,13 +88,13 @@ export default function Profile() {
<label className="block text-sm font-medium text-gray-300 mb-2">
Theme Color
</label>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 sm:gap-4">
{THEME_COLORS.map((color) => (
<button
key={color}
type="button"
onClick={() => setThemeColor(color)}
className={`h-12 rounded-lg border-2 transition-all capitalize
className={`h-12 sm:h-14 rounded-lg border-2 transition-all capitalize text-sm sm:text-base
${themeColor === color
? 'border-white scale-105'
: 'border-transparent hover:border-gray-600'
@@ -110,7 +110,7 @@ export default function Profile() {
<button
type="submit"
disabled={saving}
className="w-full flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-semibold py-2 px-4 rounded-lg transition duration-200"
className="w-full min-h-[44px] flex items-center justify-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-600 text-white font-semibold py-3 px-4 rounded-lg transition duration-200"
>
{saving ? (
<div className="animate-spin rounded-full h-5 w-5 border-t-2 border-b-2 border-white"></div>
@@ -125,4 +125,4 @@ export default function Profile() {
</div>
</div>
);
}
}