1
0

Compare commits

...

21 Commits

Author SHA1 Message Date
2339aa5071 fix cards id 2025-07-23 02:15:57 +09:00
2767842aae fix planet 2025-07-20 13:18:25 +09:00
054846f8e5 Add planet display to user profile
- Display planet value with earth icon
- Format large numbers as M (millions) or K (thousands)
- Example: 2000336.062109 displays as 2.00M
- Show only when planet value exists
2025-07-20 13:00:35 +09:00
60076d0e83 fix 2025-07-18 16:45:46 +09:00
8fa9747847 fix gh-actions 2025-07-18 16:30:00 +09:00
syui
2a698fa9af Merge pull request #3 from aisyui/react-migration-merge
Add pull request merge support to GitHub Actions
2025-07-18 16:07:08 +09:00
8d6137ce67 Add pull request merge support to GitHub Actions
- Add pull_request trigger with closed type
- Add condition to run only when PR is merged or on push to main
- Enable deployment on both direct push and PR merge to main

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 16:05:46 +09:00
syui
2b954ec582 Merge pull request #2 from aisyui/react-migration
React migration
2025-07-18 15:55:13 +09:00
94062298ab Merge main branch into react-migration
- Resolve merge conflict in Navigation.tsx
- Keep user search functionality removed (react-migration version)
- Maintain mobile responsive design improvements
- Ready for merge back to main

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:53:28 +09:00
14873a9589 fix gh-actions 2025-07-18 15:50:22 +09:00
e829de6878 Remove user search functionality from navigation
- Delete both user and id search forms completely
- Simplify navigation to only show AI icon and Bluesky link
- Clean up UI by removing unused search features

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:46:24 +09:00
3302ceb852 Hide navigation elements on mobile devices
- Add 'hidden md:block' to Bluesky link to hide on mobile
- Add 'hidden md:flex' to form inputs to hide on mobile
- Keep only the AI icon visible on mobile for cleaner UI
- Desktop users still see full navigation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:46:05 +09:00
815dfb26e0 Hide navigation elements on mobile devices
- Add 'hidden md:block' to Bluesky link to hide on mobile
- Add 'hidden md:flex' to form inputs to hide on mobile
- Keep only the AI icon visible on mobile for cleaner UI
- Desktop users still see full navigation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:45:30 +09:00
f8c37a1f73 rm claude 2025-07-18 15:38:09 +09:00
3e3244c852 fix gh-actions 2025-07-18 15:36:48 +09:00
syui
416a71d3f0 Merge pull request #1 from aisyui/react-migration
React migration
2025-07-18 15:34:17 +09:00
c050a639af Fix GitHub Actions warning for setup-node inputs
- Move 'ref' and 'fetch-depth' parameters from setup-node to checkout action
- These parameters belong to actions/checkout, not actions/setup-node
- Resolves "Unexpected input(s)" warnings in workflow

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:28:56 +09:00
2cd1a62e5d Fix SPA routing for GitHub Pages deployment
- Update 404.html with React content instead of Vue
- Add automatic 404.html copy to build script
- Fix URL typo in og:url meta tag
- Ensure client-side routing works for user pages

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:26:05 +09:00
510899a0dd Update README.md for React migration
- Update tech stack information (Vue2 → React 18)
- Add comprehensive project overview
- Include new features list
- Update development commands (yarn → npm)
- Add build instructions
- Document Node.js 20+ recommendation

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:20:28 +09:00
56fa9c3ab6 Fix TypeScript errors for GitHub Actions build
- Add 'normal' skill type to SKILL_ICONS constant
- Fix parseInt undefined error in UserPage by providing default value
- Ensure type safety for Card skill property
- API URL correctly configured for production (https://api.syui.ai/)

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:19:26 +09:00
8b27c8e829 Remove Vue-related files and empty directories
- Delete vue.config.js
- Delete src/App.vue
- Delete src/main.js (old Vue entry point)
- Remove empty directories: src/bin, src/context
- Complete cleanup after React migration

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-07-18 15:18:08 +09:00
19 changed files with 216 additions and 2043 deletions

View File

@@ -1,21 +0,0 @@
{
"permissions": {
"allow": [
"Bash(git commit:*)",
"Bash(git checkout:*)",
"Bash(mkdir:*)",
"Bash(npm install)",
"Bash(nvm use:*)",
"Bash(npm run dev:*)",
"Bash(npm run build:*)",
"Bash(npm run preview:*)",
"Bash(pkill:*)",
"Bash(npx serve:*)",
"Bash(python3:*)",
"Bash(git add:*)",
"Bash(open:*)",
"WebFetch(domain:api.syui.ai)"
],
"deny": []
}
}

View File

@@ -4,18 +4,18 @@ on:
push:
branches:
- main
- react-migration
jobs:
build-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.ref }}
fetch-depth: 0
- uses: actions/setup-node@v4
with:
node-version: 23
ref: ${{ github.ref }}
fetch-depth: 0
- run: |
npm install
@@ -26,9 +26,10 @@ jobs:
npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
force_orphan: true
user_name: 'ai[bot]'
user_email: '138105980+yui-syui-ai[bot]@users.noreply.github.com'

2
.gitignore vendored
View File

@@ -6,3 +6,5 @@ yarn-error.log
package-lock.json
yarn.lock
**DS_Store
.claude
repos

View File

@@ -4,6 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ai.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>card.syui.ai</title>
<link rel="stylesheet" href="/pkg/icomoon/style.css" />
<link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css" />
@@ -12,4 +13,4 @@
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
</html>

View File

@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"build": "tsc && vite build && cp dist/index.html dist/404.html",
"preview": "vite preview",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},

View File

@@ -1,12 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1"><title>card.syui.ai</title><link href="app.css" rel="preload" as="style"><link href="app.js" rel="preload" as="script"><link href="chunk-vendors.js" rel="preload" as="script"><link href="app.css" rel="stylesheet">
<meta name="twitter:card" content="summary">
<meta property="og:url" content="https://card.syui.aiL">
<meta property="og:title" content="card.syui.ai">
<meta property="og:description" content="@yui.bsky.social /card">
<meta property="og:image" content="https://card.syui.ai/card/card_0.png">
</head>
<body><div id="app"></div><script src="chunk-vendors.js"></script><script src="app.js"></script></body>
<html lang="ja">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/ai.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>card.syui.ai</title>
<link rel="stylesheet" href="/pkg/icomoon/style.css" />
<link rel="stylesheet" href="/pkg/font-awesome/css/all.min.css" />
<script type="module" crossorigin src="/index.js"></script>
<link rel="stylesheet" crossorigin href="/index.css">
<meta name="twitter:card" content="summary">
<meta property="og:url" content="https://card.syui.ai">
<meta property="og:title" content="card.syui.ai">
<meta property="og:description" content="@yui.bsky.social /card">
<meta property="og:image" content="https://card.syui.ai/card/card_0.png">
</head>
<body>
<div id="root"></div>
</body>
</html>

View File

@@ -1,10 +1,45 @@
# card
- host : [card.syui.ai](https://card.syuiai)
A React-based card collection viewer with holographic effects.
## Overview
- Production URL: [card.syui.ai](https://card.syui.ai)
- API: https://api.syui.ai
## Tech Stack
- React 18
- TypeScript
- Vite
- Tailwind CSS
- React Query (TanStack Query)
- React Router v6
## Features
- User card collections display
- Holographic effects for special status cards
- Favorite card highlighting
- Responsive grid layout
- Fast initial loading with JSON caching
## Development
```sh
$ nvm use 16
$ yarn install
$ yarn dev
# Node.js 20+ recommended
$ npm install
$ npm run dev
```
## Build
```sh
$ npm run build
$ npm run preview
```
## Environment
The app runs on localhost:8080 by default and uses proxy for API calls during development.

View File

@@ -4,6 +4,7 @@ import HomePage from './components/pages/HomePage';
import DocsPage from './components/pages/DocsPage';
import OwnerPage from './components/pages/OwnerPage';
import UserPage from './components/pages/UserPage';
import CardDisplayPage from './components/pages/CardDisplayPage';
function App() {
useEffect(() => {
@@ -28,6 +29,7 @@ function App() {
<Route path="/fa" element={<DocsPage page="fanart" />} />
<Route path="/ph" element={<DocsPage page="photo" />} />
<Route path="/pr" element={<DocsPage page="favorite" />} />
<Route path="/cards/:id" element={<CardDisplayPage />} />
<Route path="/:username" element={<UserPage />} />
</Routes>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -33,7 +33,7 @@ export default function Navigation({ username: _username }: NavigationProps) {
<a href="/" className="text-accent text-2xl px-4">
<span className="icon-ai"></span>
</a>
<code className="bg-dark p-0">
<code className="bg-dark p-0 hidden md:block">
<a
href="https://bsky.app/profile/yui.syui.ai"
target="_blank"
@@ -45,32 +45,6 @@ export default function Navigation({ username: _username }: NavigationProps) {
</code>
</div>
<div className="flex gap-2">
<form className="flex gap-2">
<input
type="text"
placeholder="user"
className="px-2 py-1 text-black rounded"
/>
<input
type="submit"
value="Go"
className="px-3 py-1 bg-primary text-white rounded hover:bg-secondary cursor-pointer"
/>
</form>
<form className="flex gap-2">
<input
type="text"
placeholder="id"
className="px-2 py-1 text-black rounded"
/>
<input
type="submit"
value="Go"
className="px-3 py-1 bg-primary text-white rounded hover:bg-secondary cursor-pointer"
/>
</form>
</div>
</div>
</div>
);

View File

@@ -0,0 +1,62 @@
import { useParams } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { fetchCardById } from '../../utils/api';
import '../../styles/card-effects.css';
import '../../styles/card-fullscreen.css';
export default function CardDisplayPage() {
const { id } = useParams<{ id: string }>();
const cardId = parseInt(id || '0');
const { data: card, isLoading } = useQuery({
queryKey: ['card', cardId],
queryFn: () => fetchCardById(cardId),
enabled: cardId > 0,
});
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">
<i className="fa-solid fa-spinner fa-spin text-6xl text-yellow-500"></i>
</div>
);
}
if (!card) {
return null;
}
const isSpecialStatus = ['yui', 'first', 'second', 'third', 'fourth', 'fifth', 'sixth', 'seven'].includes(card.status);
return (
<div
className="min-h-screen flex items-center justify-center"
style={{
background: 'linear-gradient(to bottom, #1a1a1a, #2d2d2d, #1a1a1a)',
backgroundColor: '#1a1a1a'
}}
>
<div className="w-full h-full flex items-center justify-center">
<div className="relative" style={{ maxHeight: '100vh', height: '100vh', width: 'auto' }}>
<div className="h-full flex flex-col items-center justify-center">
<div className="card-wrapper-fullscreen" style={{ height: '100vh', width: '100%', maxWidth: 'none', aspectRatio: '5/7' }}>
<div className="card-reflection">
<img
src={`/card/card_${card.card}.webp`}
alt={`Card ${card.card}`}
className="w-full rounded-lg"
/>
</div>
{isSpecialStatus && (
<>
<div className={`card-status pattern-${card.status}`}></div>
<div className={`card-status color-${card.status}`}></div>
</>
)}
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,14 +1,35 @@
import { useQuery } from '@tanstack/react-query';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useEffect } from 'react';
import Navigation from '../common/Navigation';
import { fetchUsers } from '../../utils/api';
import { fetchUsersWithCache } from '../../utils/api';
import { api } from '../../utils/api';
export default function HomePage() {
const queryClient = useQueryClient();
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetchUsers()
queryFn: () => fetchUsersWithCache(),
staleTime: 0, // Always consider data stale
});
// Refresh with API data after cache load
useEffect(() => {
if (users?.isFromCache) {
// Fetch fresh data from API in background
api.get('users?itemsPerPage=8000').then(response => {
console.log('Background API fetch successful, got', response.data.length, 'users with planet data');
// Update the query cache with fresh data
queryClient.setQueryData(['users'], {
data: response.data,
isFromCache: false
});
}).catch(error => {
console.error('Background API fetch failed:', error);
});
}
}, [users?.isFromCache, queryClient]);
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center">

View File

@@ -4,17 +4,27 @@ import Navigation from '../common/Navigation';
import CardGrid from '../card/CardGrid';
import UserProfile from '../user/UserProfile';
import SpecialCard from '../card/SpecialCard';
import { fetchUsers, fetchUserCards } from '../../utils/api';
import { fetchUsers, fetchUserCards, fetchUser } from '../../utils/api';
export default function UserPage() {
const { username } = useParams<{ username: string }>();
// First get users list to find the user ID
const { data: users, isLoading } = useQuery({
queryKey: ['users'],
queryFn: () => fetchUsers(),
});
const user = users?.data.find(u => u.username === username);
const userId = users?.data.find(u => u.username === username)?.id;
// Then fetch full user data from API to get planet value
const { data: userResponse } = useQuery({
queryKey: ['user', userId],
queryFn: () => fetchUser(userId!),
enabled: !!userId,
});
const user = userResponse?.data || users?.data.find(u => u.username === username);
const { data: cards, isLoading: cardsLoading } = useQuery({
queryKey: ['userCards', user?.id],
@@ -59,7 +69,7 @@ export default function UserPage() {
{/* Favorite Card Section */}
{user.fav && user.fav !== '0' && cards?.data && (
(() => {
const favCard = cards.data.find(card => card.id === parseInt(user.fav));
const favCard = cards.data.find(card => card.id === parseInt(user.fav || '0'));
if (favCard) {
return (
<div className="mb-8">

View File

@@ -6,6 +6,7 @@ interface UserProfileProps {
}
export default function UserProfile({ user, cards }: UserProfileProps) {
return (
<div className="bg-white rounded-lg p-6 mb-8">
<h3 className="text-2xl font-bold mb-4 flex items-center gap-2">
@@ -29,7 +30,7 @@ export default function UserProfile({ user, cards }: UserProfileProps) {
)}
</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 text-sm">
<div>
<strong>ID:</strong> {user.id}
</div>
@@ -45,12 +46,10 @@ export default function UserProfile({ user, cards }: UserProfileProps) {
<span className="icon-ai"></span>
{cards.filter(c => c.card >= 96 && c.card <= 121).length}
</div>
{user.planet && (
<div className="flex items-center gap-1">
<i className="fa-solid fa-earth-americas"></i>
{user.planet}
</div>
)}
<div className="flex items-center gap-1">
<i className="fa-solid fa-earth-americas"></i>
{user.planet?.toLocaleString() || '0'}
</div>
</div>
{/* Badge Images */}

View File

@@ -1,9 +0,0 @@
import Vue from 'vue'
import App from './App.vue'
import VueMeta from 'vue-meta'
Vue.use(VueMeta)
Vue.config.productionTip = false
new Vue({
render: h => h(App)
}).$mount('#app')

View File

@@ -0,0 +1,7 @@
/* Fullscreen Card Display Styles */
.card-wrapper-fullscreen {
display: grid;
place-items: center;
position: relative;
overflow: hidden;
}

View File

@@ -15,32 +15,44 @@ export const api = axios.create({
// API関数
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 };
// Directly fetch from API to get complete user data including planet
const response = await api.get(`users?itemsPerPage=${itemsPerPage}`);
return { data: response.data };
} catch (error) {
console.error('Failed to fetch cached users:', error);
// Fallback to API if cached JSON fails
console.error('Failed to fetch users from API:', error);
// Fallback to cached data (but it doesn't have planet field)
try {
const response = await api.get(`users?itemsPerPage=${itemsPerPage}`);
return { data: response.data };
} catch (apiError) {
console.error('API fallback failed:', apiError);
const cachedResponse = await axios.get('/json/users.json');
return { data: cachedResponse.data };
} catch (cacheError) {
console.error('Cache fallback failed:', cacheError);
return { data: [] };
}
}
};
// Fetch users with cache-first strategy for home page
export const fetchUsersWithCache = async (): Promise<{ data: User[], isFromCache?: boolean }> => {
try {
// First, try to get cached data
console.log('Attempting to load cached users.json');
const cachedResponse = await axios.get('/json/users.json');
console.log('Successfully loaded cached data:', cachedResponse.data.length, 'users');
return { data: cachedResponse.data, isFromCache: true };
} catch (error) {
console.error('Cache read failed, fetching from API:', error);
// If cache fails, fetch from API
try {
const response = await api.get('users?itemsPerPage=8000');
console.log('Successfully loaded from API:', response.data.length, 'users');
return { data: response.data, isFromCache: false };
} catch (apiError) {
console.error('API fetch also failed:', apiError);
return { data: [], isFromCache: false };
}
}
};
export const fetchUserCards = async (userId: number, itemsPerPage = 8000): Promise<{ data: Card[] }> => {
try {
const response = await api.get(`users/${userId}/card?itemsPerPage=${itemsPerPage}`);

View File

@@ -9,7 +9,8 @@ export const SKILL_ICONS = {
yui: 'icon-ai',
'3d': '■',
model: 'fa-solid fa-cube',
first: 'icon-moji_a'
first: 'icon-moji_a',
normal: ''
} as const;
export const PLANET_THRESHOLDS = {

View File

@@ -1,23 +0,0 @@
module.exports = {
devServer: {
proxy: {
"^/api*": {
target: "https://api.syui.ai",
pathRewrite: { "^/api": "" },
}
}
},
publicPath: "/",
configureWebpack: {
output: {
filename: '[name].js',
chunkFilename: '[name].js'
}
},
css: {
extract: {
filename: '[name].css',
chunkFilename: '[name].css'
},
},
}