1
0

Complete React migration improvements and fixes

- Fix button styling inconsistency for 'did' button using proper CSS reset
- Replace Font Awesome spinners with yellow color (#fff700) for consistency
- Implement holographic card effects for special status cards (yui, first, etc.)
- Add cached user data loading from /json/users.json for faster initial load
- Fix loading states to show spinners instead of "User not found" messages
- Clean up debug console logs for production readiness
- Add proper error handling for API calls
- Update CSS imports to use local files from /pkg/ directory

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-18 14:43:38 +09:00
parent e7f39a1894
commit 3ebc0c8aef
2152 changed files with 373417 additions and 21 deletions

View File

@@ -0,0 +1,79 @@
import { Card } from '../../types';
import '../../styles/card-effects.css';
interface SpecialCardProps {
card: Card;
className?: string;
}
export default function SpecialCard({ card, className = '' }: SpecialCardProps) {
const isSpecialStatus = ['yui', 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seven'].includes(card.status);
if (!isSpecialStatus) {
return (
<div className={`text-center ${className}`}>
<div className="relative mb-2">
<div className="card-wrapper">
<div className="card-reflection">
<img
src={`/card/card_${card.card}.webp`}
alt={`Card ${card.card}`}
className="w-full rounded-lg"
/>
</div>
</div>
</div>
<div className="text-sm">
<div className="flex items-center justify-center gap-1 mb-1">
{card.skill === 'critical' && <span className="icon-sandar"></span>}
{card.skill === 'post' && <span className="icon-moon"></span>}
{card.skill === 'luck' && <span className="icon-api"></span>}
{card.skill === 'ten' && <span className="icon-power"></span>}
{card.skill === 'lost' && <span></span>}
{card.skill === 'dragon' && <span className="icon-home"></span>}
{card.skill === 'nyan' && <span></span>}
{card.skill === 'yui' && <span className="icon-ai"></span>}
{card.skill === '3d' && <span></span>}
{card.skill === 'model' && <i className="fa-solid fa-cube"></i>}
{card.skill === 'first' && <span className="icon-moji_a"></span>}
<span>{card.cp}</span>
</div>
</div>
</div>
);
}
return (
<div className={`text-center ${className}`}>
<div className="relative mb-2">
<div className="card-wrapper">
<div className="card-reflection">
<img
src={`/card/card_${card.card}.webp`}
alt={`Card ${card.card}`}
className="w-full rounded-lg"
/>
</div>
<div className={`card-status pattern-${card.status}`}></div>
<div className={`card-status color-${card.status}`}></div>
</div>
</div>
<div className="text-sm">
<div className="flex items-center justify-center gap-1 mb-1">
{card.skill === 'critical' && <span className="icon-sandar"></span>}
{card.skill === 'post' && <span className="icon-moon"></span>}
{card.skill === 'luck' && <span className="icon-api"></span>}
{card.skill === 'ten' && <span className="icon-power"></span>}
{card.skill === 'lost' && <span></span>}
{card.skill === 'dragon' && <span className="icon-home"></span>}
{card.skill === 'nyan' && <span></span>}
{card.skill === 'yui' && <span className="icon-ai"></span>}
{card.skill === '3d' && <span></span>}
{card.skill === 'model' && <i className="fa-solid fa-cube"></i>}
{card.skill === 'first' && <span className="icon-moji_a"></span>}
<span>{card.cp}</span>
</div>
</div>
</div>
);
}

View File

@@ -8,13 +8,15 @@ export default function HomePage() {
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetchUsers(),
queryFn: () => fetchUsers()
});
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-xl">Loading...</div>
<div className="text-center">
<i className="fa-solid fa-spinner fa-spin text-6xl text-yellow-500"></i>
</div>
</div>
);
}
@@ -39,7 +41,7 @@ export default function HomePage() {
<a href="/svn" className="btn ml-2">seven</a>
</div>
{users && (
{users?.data && Array.isArray(users.data) && users.data.length > 0 && (
<div className="bg-white rounded-lg p-6">
<div className="grid gap-4">
{users.data.map((user) => (
@@ -106,6 +108,7 @@ export default function HomePage() {
</div>
</div>
)}
</div>
</div>
);

View File

@@ -15,7 +15,7 @@ export default function OwnerPage() {
<div className="min-h-screen">
<Navigation />
<div className="flex items-center justify-center py-8">
<div className="text-xl">Loading...</div>
<i className="fa-solid fa-spinner fa-spin text-6xl text-yellow-500"></i>
</div>
</div>
);

View File

@@ -8,7 +8,7 @@ import { fetchUsers, fetchUserCards } from '../../utils/api';
export default function UserPage() {
const { username } = useParams<{ username: string }>();
const { data: users } = useQuery({
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetchUsers(),
});
@@ -21,7 +21,7 @@ export default function UserPage() {
enabled: !!user?.id,
});
if (!user) {
if (!user && !isLoading) {
return (
<div className="min-h-screen">
<Navigation />
@@ -32,6 +32,16 @@ export default function UserPage() {
);
}
if (!user) {
return (
<div className="min-h-screen flex items-center justify-center">
<div className="text-center">
<i className="fa-solid fa-spinner fa-spin text-6xl text-yellow-500"></i>
</div>
</div>
);
}
return (
<div className="min-h-screen">
<Navigation username={username} />
@@ -40,7 +50,9 @@ export default function UserPage() {
<UserProfile user={user} cards={cards?.data || []} />
{cardsLoading ? (
<div className="text-center py-8">Loading cards...</div>
<div className="text-center py-8">
<i className="fa-solid fa-spinner fa-spin text-4xl text-yellow-500"></i>
</div>
) : (
<CardGrid cards={cards?.data || []} user={user} />
)}

54
src/hooks/useUserData.ts Normal file
View File

@@ -0,0 +1,54 @@
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
import { fetchUsers } from '../utils/api';
import type { User } from '../types';
// Separate function for cached data
const fetchCachedUsers = async (): Promise<{ data: User[] }> => {
try {
const cachedResponse = await axios.get('/json/user.json');
const userMap = cachedResponse.data;
const cachedUsers: User[] = Object.entries(userMap).map(([username, id]) => ({
id: parseInt(id as string),
username,
did: '',
aiten: 0,
handle: '',
created_at: '',
model: false,
fav: '0'
}));
return { data: cachedUsers };
} catch (error) {
console.error('Failed to fetch cached users:', error);
return { data: [] };
}
};
export const useUserData = () => {
// First query for cached data (fast)
const cachedQuery = useQuery({
queryKey: ['users', 'cached'],
queryFn: fetchCachedUsers,
staleTime: 0,
refetchOnMount: false,
});
// Second query for fresh data (slower but complete)
const freshQuery = useQuery({
queryKey: ['users', 'fresh'],
queryFn: () => fetchUsers(),
enabled: !!cachedQuery.data, // Only start after cached data is loaded
staleTime: 5 * 60 * 1000, // 5 minutes
});
// Return fresh data if available, otherwise cached data
return {
data: freshQuery.data || cachedQuery.data,
isLoading: cachedQuery.isLoading,
isFetching: freshQuery.isFetching,
error: freshQuery.error || cachedQuery.error,
};
};

View File

@@ -28,6 +28,17 @@
@apply px-3 py-1 rounded border border-primary bg-primary text-white hover:border-light hover:bg-secondary transition-colors;
}
button.btn {
font-family: inherit;
font-size: inherit;
line-height: inherit;
vertical-align: baseline;
margin: 0;
border: none;
background: none;
padding: 0;
}
.btn-sm {
@apply px-2 py-1 text-sm rounded border border-primary bg-primary text-white hover:border-light;
}

350
src/styles/card-effects.css Normal file
View File

@@ -0,0 +1,350 @@
/* Card Holographic Effects */
.card-wrapper {
display: grid;
place-items: center;
position: relative;
aspect-ratio: 5/7;
width: 100%;
max-width: 200px;
margin: 0 auto;
}
.card-reflection {
display: block;
position: relative;
overflow: hidden;
border-radius: 7px;
box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.2);
}
.card-reflection img {
width: 100%;
height: auto;
display: block;
}
.card-reflection::after {
content: "";
height: 100%;
width: 30px;
position: absolute;
top: -180px;
left: 0;
background-color: #ffffffa8;
opacity: 0;
transform: rotate(45deg);
animation: reflection 4s ease-in-out infinite;
}
@keyframes reflection {
0% { transform: scale(0) rotate(45deg); opacity: 0; }
80% { transform: scale(0) rotate(45deg); opacity: 0.5; }
81% { transform: scale(4) rotate(45deg); opacity: 1; }
100% { transform: scale(50) rotate(45deg); opacity: 0; }
}
.card-status {
aspect-ratio: 5/7;
border-radius: 7px;
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 10;
}
.card-wrapper {
cursor: pointer;
transition: transform 0.3s ease;
}
.card-wrapper:hover {
transform: scale(1.05);
}
/* Status Effects */
.pattern-yui {
background: repeating-radial-gradient(circle at -150% -25%, rgba(255, 255, 0, 0.3), transparent 3px, rgba(255, 255, 0, 0.2) 6px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: screen;
opacity: 0.7;
animation: shimmer 3s ease-in-out infinite;
}
.color-yui {
background: linear-gradient(115deg,
transparent 0%,
rgba(255, 255, 0, 0.4) 25%,
transparent 50%,
rgba(255, 255, 0, 0.3) 75%,
transparent 100%);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: soft-light;
opacity: 0.8;
animation: gradient-shift 4s ease-in-out infinite;
}
@keyframes shimmer {
0%, 100% { opacity: 0.4; background-size: 120% 120%; }
50% { opacity: 0.6; background-size: 140% 140%; }
}
@keyframes gradient-shift {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
.pattern-first {
background: repeating-radial-gradient(circle at -150% -25%,
rgba(0, 255, 255, 0.3),
transparent 3px,
rgba(64, 224, 208, 0.2) 6px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: screen;
opacity: 0.7;
animation: shimmer 3s ease-in-out infinite;
}
.color-first {
background: linear-gradient(115deg,
transparent 0%,
rgba(0, 255, 255, 0.4) 25%,
transparent 50%,
rgba(64, 224, 208, 0.3) 75%,
transparent 100%);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: soft-light;
opacity: 0.6;
animation: gradient-shift 4s ease-in-out infinite;
}
.pattern-second {
background: repeating-radial-gradient(circle at -150% -25%, #000, #777 3px, #000 3px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
.color-second {
background: linear-gradient(115deg, transparent 20%, #000 30%, transparent 48% 52%, #000 70%, transparent);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: overlay;
}
.pattern-third {
background: repeating-radial-gradient(circle at -150% -25%, #c71585, #777 3px, #ffff00 3px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
.color-third {
background: linear-gradient(115deg, transparent 20%, #c71585 30%, transparent 48% 52%, #c71585 70%, transparent);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: overlay;
}
.pattern-fourth {
background: repeating-radial-gradient(circle at -150% -25%, #fff, #777 3px, #fff 3px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
.color-fourth {
background: linear-gradient(115deg, transparent 20%, #40A4BF 30%, transparent 48% 52%, #404FBF 70%, transparent);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: overlay;
}
.pattern-fifth {
background: repeating-radial-gradient(circle at -150% -25%, #000, #990033 3px, #990033 3px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
.color-fifth {
background: linear-gradient(115deg, transparent 20%, #FF0000 30%, transparent 48% 52%, #990033 70%, transparent);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: overlay;
}
.pattern-sixth {
background: repeating-radial-gradient(circle at center, #f1f1f1, #313131 3px, #fff700 3px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
.color-sixth {
background: linear-gradient(115deg, transparent 20%, #f1f1f1 30%, transparent 48% 52%, #313131 70%, transparent);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: overlay;
}
.pattern-seven {
background: repeating-radial-gradient(circle at center, #fff700, #313131 3px, #000700 3px);
background-position: 50% 50%;
background-size: 120% 120%;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
.color-seven {
background: linear-gradient(115deg, transparent 20%, #fff700 30%, transparent 48% 52%, #fff700 70%, transparent);
background-position: 50% 50%;
background-size: 200% 200%;
mix-blend-mode: overlay;
}
/* Global animation for all pattern effects */
.pattern-second, .pattern-third, .pattern-fourth, .pattern-fifth, .pattern-sixth, .pattern-seven {
animation: shimmer 3s ease-in-out infinite;
mix-blend-mode: screen;
opacity: 0.6;
}
.color-second, .color-third, .color-fourth, .color-fifth, .color-sixth, .color-seven {
animation: gradient-shift 4s ease-in-out infinite;
mix-blend-mode: soft-light;
opacity: 0.5;
}
/* Hover Effects */
.card-wrapper:hover > .pattern-yui,
.card-wrapper:hover > .pattern-first,
.card-wrapper:hover > .pattern-second,
.card-wrapper:hover > .pattern-third,
.card-wrapper:hover > .pattern-fourth,
.card-wrapper:hover > .pattern-fifth,
.card-wrapper:hover > .pattern-sixth,
.card-wrapper:hover > .pattern-seven {
animation-duration: 1.5s;
opacity: 0.7;
}
.card-wrapper:hover > .color-yui,
.card-wrapper:hover > .color-first,
.card-wrapper:hover > .color-second,
.card-wrapper:hover > .color-third,
.card-wrapper:hover > .color-fourth,
.card-wrapper:hover > .color-fifth,
.card-wrapper:hover > .color-sixth,
.card-wrapper:hover > .color-seven {
animation-duration: 2s;
opacity: 0.8;
}
.card-wrapper:hover > .highlight-yui,
.card-wrapper:hover > .highlight-first,
.card-wrapper:hover > .highlight-second,
.card-wrapper:hover > .highlight-third,
.card-wrapper:hover > .highlight-fourth,
.card-wrapper:hover > .highlight-fifth,
.card-wrapper:hover > .highlight-sixth,
.card-wrapper:hover > .highlight-seven {
background-repeat: no-repeat;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 0.3; }
50% { opacity: 0.8; }
}
/* Enhanced holographic effects */
.card-status.pattern-yui::after,
.card-status.pattern-first::after,
.card-status.pattern-second::after,
.card-status.pattern-third::after,
.card-status.pattern-fourth::after,
.card-status.pattern-fifth::after,
.card-status.pattern-sixth::after,
.card-status.pattern-seven::after {
content: "";
position: absolute;
top: -180px;
left: 0;
width: 30px;
height: 100%;
background: linear-gradient(45deg, transparent, rgba(255, 255, 255, 0.8), transparent);
opacity: 0;
transform: rotate(45deg);
animation: holographic-sweep 6s ease-in-out infinite;
}
@keyframes holographic-sweep {
0% { transform: scale(0) rotate(45deg) translateX(-200px); opacity: 0; }
10% { transform: scale(0) rotate(45deg) translateX(-200px); opacity: 0; }
15% { transform: scale(2) rotate(45deg) translateX(-100px); opacity: 0.6; }
20% { transform: scale(4) rotate(45deg) translateX(0px); opacity: 1; }
25% { transform: scale(6) rotate(45deg) translateX(100px); opacity: 0.8; }
30% { transform: scale(8) rotate(45deg) translateX(200px); opacity: 0; }
100% { transform: scale(0) rotate(45deg) translateX(300px); opacity: 0; }
}
/* Rainbow shimmer effect for special cards */
.card-status.color-yui,
.card-status.color-first {
background: linear-gradient(45deg,
#ff0000, #ff7f00, #ffff00, #00ff00,
#0000ff, #4b0082, #9400d3, #ff0000);
background-size: 400% 400%;
animation: rainbow-flow 3s ease-in-out infinite;
mix-blend-mode: color-dodge;
opacity: 0.3;
}
@keyframes rainbow-flow {
0%, 100% { background-position: 0% 50%; }
50% { background-position: 100% 50%; }
}
/* Floating particles effect */
.card-wrapper::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-image:
radial-gradient(2px 2px at 20px 30px, rgba(255, 255, 255, 0.8), transparent),
radial-gradient(2px 2px at 40px 70px, rgba(255, 255, 255, 0.6), transparent),
radial-gradient(1px 1px at 90px 40px, rgba(255, 255, 255, 0.4), transparent),
radial-gradient(1px 1px at 130px 80px, rgba(255, 255, 255, 0.7), transparent);
background-repeat: repeat;
background-size: 200px 200px;
animation: float-particles 8s linear infinite;
opacity: 0;
pointer-events: none;
z-index: 15;
}
.card-wrapper:hover::before {
opacity: 1;
}
@keyframes float-particles {
0% { transform: translateY(0px) translateX(0px); }
33% { transform: translateY(-10px) translateX(5px); }
66% { transform: translateY(-5px) translateX(-5px); }
100% { transform: translateY(0px) translateX(0px); }
}

View File

@@ -24,10 +24,13 @@ export interface Card {
id: number;
card: number;
cp: number;
status: 'normal' | 'yui' | 'first' | 'second' | 'third' | 'fourth' | 'fifth' | 'sixth' | 'seven';
skill: 'critical' | 'post' | 'luck' | 'ten' | 'lost' | 'dragon' | 'nyan' | 'yui' | '3d' | 'model' | 'first';
status: 'normal' | 'yui' | 'first' | 'second' | 'third' | 'fourth' | 'fifth' | 'sixth' | 'seven' | 'super' | 'lost';
skill: 'critical' | 'post' | 'luck' | 'ten' | 'lost' | 'dragon' | 'nyan' | 'yui' | '3d' | 'model' | 'first' | 'normal';
author?: string;
url?: string;
count?: number;
created_at?: string;
user_id?: number;
}
export interface CardOwner {

View File

@@ -13,14 +13,43 @@ export const api = axios.create({
});
// API関数
export const fetchUsers = async (itemsPerPage = 3000): Promise<{ data: User[] }> => {
const response = await api.get(`users?itemsPerPage=${itemsPerPage}`);
return response.data;
export const fetchUsers = async (itemsPerPage = 8000): Promise<{ data: User[] }> => {
try {
// First try to load cached users.json for fast initial loading
const cachedResponse = await axios.get('/json/users.json');
// Background fetch for fresh data
setTimeout(async () => {
try {
await api.get(`users?itemsPerPage=${itemsPerPage}`);
} catch (error) {
console.error('Background fetch failed:', error);
}
}, 100);
return { data: cachedResponse.data };
} catch (error) {
console.error('Failed to fetch cached users:', error);
// Fallback to API if cached JSON fails
try {
const response = await api.get(`users?itemsPerPage=${itemsPerPage}`);
return { data: response.data };
} catch (apiError) {
console.error('API fallback failed:', apiError);
return { data: [] };
}
}
};
export const fetchUserCards = async (userId: number, itemsPerPage = 8000): Promise<{ data: Card[] }> => {
const response = await api.get(`users/${userId}/card?itemsPerPage=${itemsPerPage}`);
return response.data;
try {
const response = await api.get(`users/${userId}/card?itemsPerPage=${itemsPerPage}`);
// API returns array directly, not wrapped in { data: [...] }
return { data: response.data };
} catch (error) {
console.error('Failed to fetch user cards:', error);
return { data: [] };
}
};
export const fetchUser = async (userId: number): Promise<{ data: User }> => {