fix api web
This commit is contained in:
parent
f337c20096
commit
13723cf3d7
@ -1,10 +1,11 @@
|
||||
"""Card-related API routes"""
|
||||
from typing import List
|
||||
from typing import List, Dict
|
||||
from fastapi import APIRouter, HTTPException, Depends
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.models.card import Card, CardDraw, CardDrawResult
|
||||
from app.services.gacha import GachaService
|
||||
from app.services.card_master import card_master_service
|
||||
from app.repositories.user import UserRepository
|
||||
from app.repositories.card import CardRepository, UniqueCardRepository
|
||||
from app.db.base import get_session
|
||||
@ -157,4 +158,16 @@ async def get_gacha_stats(db: AsyncSession = Depends(get_session)):
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Statistics error: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Statistics error: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/master", response_model=List[Dict])
|
||||
async def get_card_master_data():
|
||||
"""
|
||||
全カードマスターデータを取得(ai.jsonから)
|
||||
"""
|
||||
try:
|
||||
cards = card_master_service.get_all_cards()
|
||||
return cards
|
||||
except Exception as e:
|
||||
raise HTTPException(status_code=500, detail=f"Failed to get card master data: {str(e)}")
|
142
api/app/services/card_master.py
Normal file
142
api/app/services/card_master.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""
|
||||
Card master data fetcher from external ai.json
|
||||
"""
|
||||
import httpx
|
||||
import json
|
||||
from typing import Dict, List, Optional
|
||||
from functools import lru_cache
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
CARD_MASTER_URL = "https://git.syui.ai/ai/ai/raw/branch/main/ai.json"
|
||||
|
||||
# Default CP ranges for cards (matching existing gacha.py values)
|
||||
DEFAULT_CP_RANGES = {
|
||||
0: (10, 100),
|
||||
1: (20, 120),
|
||||
2: (30, 130),
|
||||
3: (40, 140),
|
||||
4: (50, 150),
|
||||
5: (25, 125),
|
||||
6: (15, 115),
|
||||
7: (60, 160),
|
||||
8: (80, 180),
|
||||
9: (70, 170),
|
||||
10: (90, 190),
|
||||
11: (35, 135),
|
||||
12: (65, 165),
|
||||
13: (75, 175),
|
||||
14: (100, 200),
|
||||
15: (85, 185),
|
||||
135: (95, 195), # world card
|
||||
}
|
||||
|
||||
|
||||
class CardMasterService:
|
||||
def __init__(self):
|
||||
self._cache = None
|
||||
self._cache_time = 0
|
||||
self._cache_duration = 3600 # 1 hour cache
|
||||
|
||||
@lru_cache(maxsize=1)
|
||||
def fetch_card_master_data(self) -> Optional[Dict]:
|
||||
"""Fetch card master data from external source"""
|
||||
try:
|
||||
response = httpx.get(CARD_MASTER_URL, timeout=10.0)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
return data
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to fetch card master data: {e}")
|
||||
return None
|
||||
|
||||
def get_card_info(self) -> Dict[int, Dict]:
|
||||
"""Get card information in the format expected by gacha service"""
|
||||
master_data = self.fetch_card_master_data()
|
||||
|
||||
if not master_data:
|
||||
# Fallback to hardcoded data
|
||||
return self._get_fallback_card_info()
|
||||
|
||||
try:
|
||||
cards = master_data.get("ai", {}).get("card", {}).get("cards", [])
|
||||
card_info = {}
|
||||
|
||||
for card in cards:
|
||||
card_id = card.get("id")
|
||||
if card_id is not None:
|
||||
# Use name from JSON, fallback to English name
|
||||
name = card.get("name", f"card_{card_id}")
|
||||
|
||||
# Get CP range from defaults
|
||||
cp_range = DEFAULT_CP_RANGES.get(card_id, (50, 150))
|
||||
|
||||
card_info[card_id] = {
|
||||
"name": name,
|
||||
"base_cp_range": cp_range,
|
||||
"ja_name": card.get("lang", {}).get("ja", {}).get("name", name),
|
||||
"description": card.get("lang", {}).get("ja", {}).get("text", "")
|
||||
}
|
||||
|
||||
return card_info
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to parse card master data: {e}")
|
||||
return self._get_fallback_card_info()
|
||||
|
||||
def _get_fallback_card_info(self) -> Dict[int, Dict]:
|
||||
"""Fallback card info if external source fails"""
|
||||
return {
|
||||
0: {"name": "ai", "base_cp_range": (10, 100)},
|
||||
1: {"name": "dream", "base_cp_range": (20, 120)},
|
||||
2: {"name": "radiance", "base_cp_range": (30, 130)},
|
||||
3: {"name": "neutron", "base_cp_range": (40, 140)},
|
||||
4: {"name": "sun", "base_cp_range": (50, 150)},
|
||||
5: {"name": "night", "base_cp_range": (25, 125)},
|
||||
6: {"name": "snow", "base_cp_range": (15, 115)},
|
||||
7: {"name": "thunder", "base_cp_range": (60, 160)},
|
||||
8: {"name": "ultimate", "base_cp_range": (80, 180)},
|
||||
9: {"name": "sword", "base_cp_range": (70, 170)},
|
||||
10: {"name": "destruction", "base_cp_range": (90, 190)},
|
||||
11: {"name": "earth", "base_cp_range": (35, 135)},
|
||||
12: {"name": "galaxy", "base_cp_range": (65, 165)},
|
||||
13: {"name": "create", "base_cp_range": (75, 175)},
|
||||
14: {"name": "supernova", "base_cp_range": (100, 200)},
|
||||
15: {"name": "world", "base_cp_range": (85, 185)},
|
||||
}
|
||||
|
||||
def get_all_cards(self) -> List[Dict]:
|
||||
"""Get all cards with full information"""
|
||||
master_data = self.fetch_card_master_data()
|
||||
|
||||
if not master_data:
|
||||
return []
|
||||
|
||||
try:
|
||||
cards = master_data.get("ai", {}).get("card", {}).get("cards", [])
|
||||
result = []
|
||||
|
||||
for card in cards:
|
||||
card_id = card.get("id")
|
||||
if card_id is not None:
|
||||
cp_range = DEFAULT_CP_RANGES.get(card_id, (50, 150))
|
||||
|
||||
result.append({
|
||||
"id": card_id,
|
||||
"name": card.get("name", f"card_{card_id}"),
|
||||
"ja_name": card.get("lang", {}).get("ja", {}).get("name", ""),
|
||||
"description": card.get("lang", {}).get("ja", {}).get("text", ""),
|
||||
"base_cp_min": cp_range[0],
|
||||
"base_cp_max": cp_range[1]
|
||||
})
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get all cards: {e}")
|
||||
return []
|
||||
|
||||
|
||||
# Singleton instance
|
||||
card_master_service = CardMasterService()
|
@ -10,36 +10,19 @@ from app.models.card import Card, CardRarity
|
||||
from app.repositories.user import UserRepository
|
||||
from app.repositories.card import CardRepository, UniqueCardRepository
|
||||
from app.db.models import DrawHistory
|
||||
from app.services.card_master import card_master_service
|
||||
|
||||
|
||||
class GachaService:
|
||||
"""ガチャシステムのサービスクラス"""
|
||||
|
||||
# カード基本情報(ai.jsonから)
|
||||
CARD_INFO = {
|
||||
0: {"name": "ai", "base_cp_range": (10, 100)},
|
||||
1: {"name": "dream", "base_cp_range": (20, 120)},
|
||||
2: {"name": "radiance", "base_cp_range": (30, 130)},
|
||||
3: {"name": "neutron", "base_cp_range": (40, 140)},
|
||||
4: {"name": "sun", "base_cp_range": (50, 150)},
|
||||
5: {"name": "night", "base_cp_range": (25, 125)},
|
||||
6: {"name": "snow", "base_cp_range": (15, 115)},
|
||||
7: {"name": "thunder", "base_cp_range": (60, 160)},
|
||||
8: {"name": "ultimate", "base_cp_range": (80, 180)},
|
||||
9: {"name": "sword", "base_cp_range": (70, 170)},
|
||||
10: {"name": "destruction", "base_cp_range": (90, 190)},
|
||||
11: {"name": "earth", "base_cp_range": (35, 135)},
|
||||
12: {"name": "galaxy", "base_cp_range": (65, 165)},
|
||||
13: {"name": "create", "base_cp_range": (75, 175)},
|
||||
14: {"name": "supernova", "base_cp_range": (100, 200)},
|
||||
15: {"name": "world", "base_cp_range": (85, 185)},
|
||||
}
|
||||
|
||||
def __init__(self, session: AsyncSession):
|
||||
self.session = session
|
||||
self.user_repo = UserRepository(session)
|
||||
self.card_repo = CardRepository(session)
|
||||
self.unique_repo = UniqueCardRepository(session)
|
||||
# Load card info from external source
|
||||
self.CARD_INFO = card_master_service.get_card_info()
|
||||
|
||||
async def draw_card(self, user_did: str, is_paid: bool = False) -> Tuple[Card, bool]:
|
||||
"""
|
||||
|
@ -387,7 +387,7 @@ Refresh Token: ${agent.session.refreshJwt?.substring(0, 30) || 'N/A'}...
|
||||
<h2>コレクション</h2>
|
||||
<div className="card-grid">
|
||||
{userCards.map((card, index) => (
|
||||
<Card key={index} card={card} />
|
||||
<Card key={index} card={card} detailed={false} />
|
||||
))}
|
||||
</div>
|
||||
{userCards.length === 0 && (
|
||||
|
@ -6,6 +6,7 @@ import '../styles/Card.css';
|
||||
interface CardProps {
|
||||
card: CardType;
|
||||
isRevealing?: boolean;
|
||||
detailed?: boolean;
|
||||
}
|
||||
|
||||
const CARD_INFO: Record<number, { name: string; color: string }> = {
|
||||
@ -27,8 +28,9 @@ const CARD_INFO: Record<number, { name: string; color: string }> = {
|
||||
15: { name: "世界", color: "#54a0ff" },
|
||||
};
|
||||
|
||||
export const Card: React.FC<CardProps> = ({ card, isRevealing = false }) => {
|
||||
export const Card: React.FC<CardProps> = ({ card, isRevealing = false, detailed = false }) => {
|
||||
const cardInfo = CARD_INFO[card.id] || { name: "Unknown", color: "#666" };
|
||||
const imageUrl = `https://git.syui.ai/ai/card/raw/branch/main/img/${card.id}.webp`;
|
||||
|
||||
const getRarityClass = () => {
|
||||
switch (card.status) {
|
||||
@ -45,6 +47,30 @@ export const Card: React.FC<CardProps> = ({ card, isRevealing = false }) => {
|
||||
}
|
||||
};
|
||||
|
||||
if (!detailed) {
|
||||
// Simple view - only image and frame
|
||||
return (
|
||||
<motion.div
|
||||
className={`card card-simple ${getRarityClass()}`}
|
||||
initial={isRevealing ? { rotateY: 180 } : {}}
|
||||
animate={isRevealing ? { rotateY: 0 } : {}}
|
||||
transition={{ duration: 0.8, type: "spring" }}
|
||||
>
|
||||
<div className="card-frame">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={cardInfo.name}
|
||||
className="card-image-simple"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</motion.div>
|
||||
);
|
||||
}
|
||||
|
||||
// Detailed view - all information
|
||||
return (
|
||||
<motion.div
|
||||
className={`card ${getRarityClass()}`}
|
||||
@ -61,6 +87,17 @@ export const Card: React.FC<CardProps> = ({ card, isRevealing = false }) => {
|
||||
<span className="card-cp">CP: {card.cp}</span>
|
||||
</div>
|
||||
|
||||
<div className="card-image-container">
|
||||
<img
|
||||
src={imageUrl}
|
||||
alt={cardInfo.name}
|
||||
className="card-image"
|
||||
onError={(e) => {
|
||||
(e.target as HTMLImageElement).style.display = 'none';
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="card-content">
|
||||
<h3 className="card-name">{cardInfo.name}</h3>
|
||||
{card.is_unique && (
|
||||
|
@ -12,6 +12,7 @@ export const CardBox: React.FC<CardBoxProps> = ({ userDid }) => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [showJson, setShowJson] = useState(false);
|
||||
const [isDeleting, setIsDeleting] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadBoxData();
|
||||
@ -38,6 +39,26 @@ export const CardBox: React.FC<CardBoxProps> = ({ userDid }) => {
|
||||
alert('カードボックスへの保存機能は親コンポーネントから実行してください');
|
||||
};
|
||||
|
||||
const handleDeleteBox = async () => {
|
||||
if (!window.confirm('カードボックスを削除してもよろしいですか?\nこの操作は取り消せません。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsDeleting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
await atprotoOAuthService.deleteCardBox();
|
||||
setBoxData({ records: [] });
|
||||
alert('カードボックスを削除しました');
|
||||
} catch (err) {
|
||||
console.error('カードボックス削除エラー:', err);
|
||||
setError(err instanceof Error ? err.message : 'カードボックスの削除に失敗しました');
|
||||
} finally {
|
||||
setIsDeleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="card-box-container">
|
||||
@ -75,6 +96,15 @@ export const CardBox: React.FC<CardBoxProps> = ({ userDid }) => {
|
||||
<button onClick={loadBoxData} className="refresh-button">
|
||||
🔄 更新
|
||||
</button>
|
||||
{cards.length > 0 && (
|
||||
<button
|
||||
onClick={handleDeleteBox}
|
||||
className="delete-button"
|
||||
disabled={isDeleting}
|
||||
>
|
||||
{isDeleting ? '削除中...' : '🗑️ 削除'}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
113
web/src/components/CardList.tsx
Normal file
113
web/src/components/CardList.tsx
Normal file
@ -0,0 +1,113 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card } from './Card';
|
||||
import { cardApi } from '../services/api';
|
||||
import { Card as CardType } from '../types/card';
|
||||
import '../styles/CardList.css';
|
||||
|
||||
interface CardMasterData {
|
||||
id: number;
|
||||
name: string;
|
||||
ja_name: string;
|
||||
description: string;
|
||||
base_cp_min: number;
|
||||
base_cp_max: number;
|
||||
}
|
||||
|
||||
export const CardList: React.FC = () => {
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [masterData, setMasterData] = useState<CardMasterData[]>([]);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
loadMasterData();
|
||||
}, []);
|
||||
|
||||
const loadMasterData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const response = await fetch('http://localhost:8000/api/v1/cards/master');
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to fetch card master data');
|
||||
}
|
||||
const data = await response.json();
|
||||
setMasterData(data);
|
||||
} catch (err) {
|
||||
console.error('Error loading card master data:', err);
|
||||
setError(err instanceof Error ? err.message : 'Failed to load card data');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="card-list-container">
|
||||
<div className="loading">Loading card data...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="card-list-container">
|
||||
<div className="error">Error: {error}</div>
|
||||
<button onClick={loadMasterData}>Retry</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// Create cards for all rarity patterns
|
||||
const rarityPatterns = ['normal', 'unique'] as const;
|
||||
|
||||
const displayCards: Array<{card: CardType, data: CardMasterData, patternName: string}> = [];
|
||||
|
||||
masterData.forEach(data => {
|
||||
rarityPatterns.forEach(pattern => {
|
||||
const card: CardType = {
|
||||
id: data.id,
|
||||
cp: Math.floor((data.base_cp_min + data.base_cp_max) / 2),
|
||||
status: pattern,
|
||||
skill: null,
|
||||
owner_did: 'sample',
|
||||
obtained_at: new Date().toISOString(),
|
||||
is_unique: pattern === 'unique',
|
||||
unique_id: pattern === 'unique' ? 'sample-unique-id' : null
|
||||
};
|
||||
displayCards.push({
|
||||
card,
|
||||
data,
|
||||
patternName: `${data.id}-${pattern}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
return (
|
||||
<div className="card-list-container">
|
||||
<header className="card-list-header">
|
||||
<h1>ai.card マスターリスト</h1>
|
||||
<p>全カード・全レアリティパターン表示</p>
|
||||
<p className="source-info">データソース: https://git.syui.ai/ai/ai/raw/branch/main/ai.json</p>
|
||||
</header>
|
||||
|
||||
<div className="card-list-simple-grid">
|
||||
{displayCards.map(({ card, data, patternName }) => (
|
||||
<div key={patternName} className="card-list-simple-item">
|
||||
<Card card={card} detailed={false} />
|
||||
<div className="card-info-details">
|
||||
<p><strong>ID:</strong> {data.id}</p>
|
||||
<p><strong>Name:</strong> {data.name}</p>
|
||||
<p><strong>日本語名:</strong> {data.ja_name}</p>
|
||||
<p><strong>レアリティ:</strong> {card.status}</p>
|
||||
<p><strong>CP:</strong> {card.cp}</p>
|
||||
<p><strong>CP範囲:</strong> {data.base_cp_min}-{data.base_cp_max}</p>
|
||||
{data.description && (
|
||||
<p className="card-description">{data.description}</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -3,6 +3,7 @@ import ReactDOM from 'react-dom/client'
|
||||
import { BrowserRouter, Routes, Route } from 'react-router-dom'
|
||||
import App from './App'
|
||||
import { OAuthCallbackPage } from './components/OAuthCallbackPage'
|
||||
import { CardList } from './components/CardList'
|
||||
import { OAuthEndpointHandler } from './utils/oauth-endpoints'
|
||||
|
||||
// Initialize OAuth endpoint handlers for dynamic client metadata and JWKS
|
||||
@ -14,6 +15,7 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route path="/oauth/callback" element={<OAuthCallbackPage />} />
|
||||
<Route path="/list" element={<CardList />} />
|
||||
<Route path="*" element={<App />} />
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
|
@ -504,8 +504,8 @@ class AtprotoOAuthService {
|
||||
owner_did: card.owner_did,
|
||||
obtained_at: card.obtained_at,
|
||||
is_unique: card.is_unique,
|
||||
unique_id: card.unique_id,
|
||||
img: `https://git.syui.ai/ai/ai/raw/branch/main/img/item/card/${card.id}.webp`
|
||||
unique_id: card.unique_id
|
||||
|
||||
})),
|
||||
total_cards: userCards.length,
|
||||
updated_at: createdAt,
|
||||
@ -584,6 +584,38 @@ class AtprotoOAuthService {
|
||||
}
|
||||
}
|
||||
|
||||
// ai.card.boxのコレクションを削除
|
||||
async deleteCardBox(): Promise<void> {
|
||||
// Ensure we have a valid session
|
||||
const sessionInfo = await this.checkSession();
|
||||
if (!sessionInfo) {
|
||||
throw new Error('認証が必要です。ログインしてください。');
|
||||
}
|
||||
|
||||
const did = sessionInfo.did;
|
||||
|
||||
try {
|
||||
console.log('Deleting card box collection...');
|
||||
console.log('Using DID:', did);
|
||||
|
||||
// Ensure we have a fresh agent
|
||||
if (!this.agent) {
|
||||
throw new Error('Agentが初期化されていません。');
|
||||
}
|
||||
|
||||
const response = await this.agent.com.atproto.repo.deleteRecord({
|
||||
repo: did,
|
||||
collection: 'ai.card.box',
|
||||
rkey: 'self'
|
||||
});
|
||||
|
||||
console.log('Card box deleted successfully:', response);
|
||||
} catch (error) {
|
||||
console.error('カードボックス削除エラー:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
// 手動でトークンを設定(開発・デバッグ用)
|
||||
setManualTokens(accessJwt: string, refreshJwt: string): void {
|
||||
console.warn('Manual token setting is not supported with official BrowserOAuthClient');
|
||||
@ -613,4 +645,4 @@ class AtprotoOAuthService {
|
||||
}
|
||||
|
||||
export const atprotoOAuthService = new AtprotoOAuthService();
|
||||
export type { AtprotoSession };
|
||||
export type { AtprotoSession };
|
||||
|
@ -1,6 +1,6 @@
|
||||
.card {
|
||||
width: 250px;
|
||||
height: 350px;
|
||||
height: 380px;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(135deg, #1a1a1a 0%, #2d2d2d 100%);
|
||||
border: 2px solid #333;
|
||||
@ -87,7 +87,26 @@
|
||||
justify-content: space-between;
|
||||
font-size: 14px;
|
||||
color: #888;
|
||||
margin-bottom: 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.card-image-container {
|
||||
width: 100%;
|
||||
height: 150px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 15px;
|
||||
overflow: hidden;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.card-image {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.card-content {
|
||||
@ -148,4 +167,165 @@
|
||||
0% { box-shadow: 0 0 5px rgba(255, 0, 255, 0.5); }
|
||||
50% { box-shadow: 0 0 20px rgba(255, 0, 255, 0.8); }
|
||||
100% { box-shadow: 0 0 5px rgba(255, 0, 255, 0.5); }
|
||||
}
|
||||
}
|
||||
|
||||
/* Simple Card Styles */
|
||||
.card-simple {
|
||||
width: 240px;
|
||||
height: auto;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.card-frame {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 3/4;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #1a1a1a;
|
||||
padding: 25px 25px 30px 25px;
|
||||
border: 3px solid #666;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Normal card - no effects */
|
||||
.card-simple.card-normal .card-frame {
|
||||
border-color: #666;
|
||||
background: #1a1a1a;
|
||||
}
|
||||
|
||||
/* Unique (rare) card - glowing effects */
|
||||
.card-simple.card-unique .card-frame {
|
||||
border-color: #ffd700;
|
||||
background: linear-gradient(135deg, #2a2a1a 0%, #3a3a2a 50%, #2a2a1a 100%);
|
||||
position: relative;
|
||||
isolation: isolate;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Particle/grainy texture for rare cards */
|
||||
.card-simple.card-unique .card-frame::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-image:
|
||||
repeating-radial-gradient(circle at 1px 1px, rgba(255, 255, 255, 0.1) 0px, transparent 1px, transparent 2px),
|
||||
repeating-radial-gradient(circle at 3px 3px, rgba(255, 215, 0, 0.1) 0px, transparent 2px, transparent 4px);
|
||||
background-size: 20px 20px, 30px 30px;
|
||||
opacity: 0.8;
|
||||
z-index: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* Reflection effect for rare cards */
|
||||
.card-simple.card-unique .card-frame::after {
|
||||
content: "";
|
||||
height: 100%;
|
||||
width: 40px;
|
||||
position: absolute;
|
||||
top: -180px;
|
||||
left: 0;
|
||||
background: linear-gradient(90deg,
|
||||
transparent 0%,
|
||||
rgba(255, 215, 0, 0.8) 20%,
|
||||
rgba(255, 255, 0, 0.9) 40%,
|
||||
rgba(255, 223, 0, 1) 50%,
|
||||
rgba(255, 255, 0, 0.9) 60%,
|
||||
rgba(255, 215, 0, 0.8) 80%,
|
||||
transparent 100%
|
||||
);
|
||||
opacity: 0;
|
||||
transform: rotate(45deg);
|
||||
animation: gold-reflection 6s ease-in-out infinite;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
@keyframes gold-reflection {
|
||||
0% { transform: scale(0) rotate(45deg); opacity: 0; }
|
||||
15% { transform: scale(0) rotate(45deg); opacity: 0; }
|
||||
17% { transform: scale(4) rotate(45deg); opacity: 0.8; }
|
||||
20% { transform: scale(50) rotate(45deg); opacity: 0; }
|
||||
100% { transform: scale(50) rotate(45deg); opacity: 0; }
|
||||
}
|
||||
|
||||
/* Glowing backlight effect */
|
||||
.card-simple.card-unique {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.card-simple.card-unique::after {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: 5px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: -1;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
transform: scale(0.95);
|
||||
filter: blur(15px);
|
||||
background: radial-gradient(ellipse at center, #ffd700 0%, #ffb347 50%, transparent 70%);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
/* Glowing border effect for rare cards */
|
||||
.card-simple.card-unique .card-frame {
|
||||
box-shadow:
|
||||
0 0 10px rgba(255, 215, 0, 0.5),
|
||||
inset 0 0 10px rgba(255, 215, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
|
||||
.card-image-simple {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 4px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.card-cp-bar {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #333;
|
||||
border-radius: 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 12px;
|
||||
margin-bottom: 8px;
|
||||
border: 2px solid #666;
|
||||
position: relative;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-simple.card-unique .card-cp-bar {
|
||||
background: linear-gradient(135deg, #2a2a1a 0%, #3a3a2a 50%, #2a2a1a 100%);
|
||||
border-color: #ffd700;
|
||||
box-shadow:
|
||||
0 0 5px rgba(255, 215, 0, 0.3),
|
||||
inset 0 0 5px rgba(255, 215, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.cp-value {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
text-shadow: 0 0 10px rgba(0, 0, 0, 0.5);
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -51,7 +51,8 @@
|
||||
|
||||
.json-button,
|
||||
.refresh-button,
|
||||
.retry-button {
|
||||
.retry-button,
|
||||
.delete-button {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
@ -91,6 +92,22 @@
|
||||
box-shadow: 0 4px 12px rgba(253, 126, 20, 0.4);
|
||||
}
|
||||
|
||||
.delete-button {
|
||||
background: linear-gradient(135deg, #dc3545 0%, #c82333 100%);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.delete-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(220, 53, 69, 0.4);
|
||||
}
|
||||
|
||||
.delete-button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.json-display {
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #dee2e6;
|
||||
|
170
web/src/styles/CardList.css
Normal file
170
web/src/styles/CardList.css
Normal file
@ -0,0 +1,170 @@
|
||||
.card-list-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #0f0f0f 0%, #1a1a1a 100%);
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-list-header {
|
||||
text-align: center;
|
||||
margin-bottom: 40px;
|
||||
padding: 20px;
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border-radius: 12px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
.card-list-header h1 {
|
||||
color: #fff;
|
||||
margin: 0 0 10px 0;
|
||||
font-size: 2.5rem;
|
||||
}
|
||||
|
||||
.card-list-header p {
|
||||
color: #999;
|
||||
margin: 0;
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.card-list-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 30px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.card-list-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
/* Simple grid layout for user-page style */
|
||||
.card-list-simple-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.card-list-simple-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.info-button {
|
||||
background: linear-gradient(135deg, #333 0%, #555 100%);
|
||||
color: white;
|
||||
border: 2px solid #666;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 0.9rem;
|
||||
transition: all 0.3s ease;
|
||||
width: 100%;
|
||||
max-width: 240px;
|
||||
}
|
||||
|
||||
.info-button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||
background: linear-gradient(135deg, #444 0%, #666 100%);
|
||||
}
|
||||
|
||||
.card-info-details {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
max-width: 240px;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.card-info-details p {
|
||||
margin: 5px 0;
|
||||
color: #ccc;
|
||||
font-size: 0.85rem;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.card-info-details p strong {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-meta {
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
border-radius: 8px;
|
||||
padding: 15px;
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
}
|
||||
|
||||
.card-meta p {
|
||||
margin: 5px 0;
|
||||
color: #ccc;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.card-meta p:first-child {
|
||||
font-weight: bold;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.card-description {
|
||||
font-size: 0.85rem;
|
||||
color: #999;
|
||||
font-style: italic;
|
||||
margin-top: 8px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.source-info {
|
||||
font-size: 0.9rem;
|
||||
color: #666;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
.loading, .error {
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
color: #999;
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #ff4757;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 10px 20px;
|
||||
border-radius: 5px;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.card-list-grid {
|
||||
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.card-list-header h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user