diff --git a/package-lock.json b/package-lock.json index 9e4fa8f..64bc5f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "showcase-temp", - "version": "0.0.0", + "name": "showcase", + "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "showcase-temp", - "version": "0.0.0", + "name": "showcase", + "version": "1.0.0", "dependencies": { "@google/model-viewer": "^4.1.0", "axios": "^1.13.2", diff --git a/src/components/PokemonModal/PokemonModal.scss b/src/components/PokemonModal/PokemonModal.scss index ff543a2..0cb0463 100644 --- a/src/components/PokemonModal/PokemonModal.scss +++ b/src/components/PokemonModal/PokemonModal.scss @@ -135,6 +135,37 @@ } } + &__download { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + gap: $spacing-sm; + padding: $spacing-md; + border: 2px solid var(--pokedex-red); + border-radius: $border-radius-md; + background: var(--pokedex-red); + color: #fff; + font-size: 1rem; + font-weight: 600; + cursor: pointer; + transition: all $transition-normal; + + &:hover:not(:disabled) { + background: var(--pokedex-red-dark); + border-color: var(--pokedex-red-dark); + } + + &:disabled { + opacity: 0.7; + cursor: not-allowed; + } + } + + &__download-icon { + font-size: 1.1rem; + } + &__controls { display: flex; flex-direction: column; diff --git a/src/components/PokemonModal/PokemonModal.tsx b/src/components/PokemonModal/PokemonModal.tsx index a286ea3..2ab2b1b 100644 --- a/src/components/PokemonModal/PokemonModal.tsx +++ b/src/components/PokemonModal/PokemonModal.tsx @@ -1,4 +1,4 @@ -import { useEffect, useRef, useState } from 'react'; +import { useEffect, useRef, useState, useCallback } from 'react'; import '@google/model-viewer'; import type { PokemonWithForm } from '../../types/pokemon'; import type { ModelViewerElement } from '../../types/model-viewer'; @@ -14,6 +14,7 @@ export function PokemonModal({ pokemon, onClose }: PokemonModalProps) { const [animations, setAnimations] = useState([]); const [selectedAnimation, setSelectedAnimation] = useState(''); const [modelError, setModelError] = useState(false); + const [downloading, setDownloading] = useState(false); // Reset state when pokemon changes useEffect(() => { @@ -68,6 +69,28 @@ export function PokemonModal({ pokemon, onClose }: PokemonModalProps) { }; }, [pokemon]); + const handleDownload = useCallback(async () => { + if (!pokemon || downloading) return; + setDownloading(true); + try { + const response = await fetch(pokemon.model); + const blob = await response.blob(); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + const formSuffix = pokemon.formName !== 'Regular' ? `_${pokemon.formName}` : ''; + a.href = url; + a.download = `${pokemon.name}${formSuffix}.glb`; + document.body.appendChild(a); + a.click(); + document.body.removeChild(a); + URL.revokeObjectURL(url); + } catch { + // silently fail - model may not be downloadable + } finally { + setDownloading(false); + } + }, [pokemon, downloading]); + const handleClose = () => { const modelViewer = modelViewerRef.current; if (modelViewer) { @@ -118,6 +141,17 @@ export function PokemonModal({ pokemon, onClose }: PokemonModalProps) { {!modelError && (
+ + {animations.length > 0 ? (