diff --git a/.claude/settings.local.json b/.claude/settings.local.json
new file mode 100644
index 0000000..a7e3ae7
--- /dev/null
+++ b/.claude/settings.local.json
@@ -0,0 +1,19 @@
+{
+ "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:*)"
+ ],
+ "deny": []
+ }
+}
\ No newline at end of file
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 25c9f88..e835948 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -4,6 +4,7 @@ on:
push:
branches:
- main
+ - react-migration
jobs:
build-deploy:
@@ -12,19 +13,17 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
- node-version: 16
- ref: main
- submodules: true
+ node-version: 23
+ ref: ${{ github.ref }}
fetch-depth: 0
- run: |
- npm install -g yarn@1.22.19 # ← yarn 1系を使う!
- yarn install --frozen-lockfile --ignore-engines
+ npm install
- name: Build
env:
TZ: "Asia/Tokyo"
run: |
- yarn build
+ npm run build
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
diff --git a/.nvmrc b/.nvmrc
new file mode 100644
index 0000000..b393560
--- /dev/null
+++ b/.nvmrc
@@ -0,0 +1 @@
+23
\ No newline at end of file
diff --git a/index.html b/index.html
new file mode 100644
index 0000000..6824ddd
--- /dev/null
+++ b/index.html
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+ card.syui.ai
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/package.json b/package.json
index 1bd91d4..aa85083 100644
--- a/package.json
+++ b/package.json
@@ -1,28 +1,38 @@
{
"name": "card",
- "version": "0.0.1",
+ "version": "0.1.0",
"private": true,
+ "type": "module",
"scripts": {
- "dev": "vue-cli-service serve",
- "build": "vue-cli-service build",
- "lint": "vue-cli-service lint"
+ "dev": "vite",
+ "build": "tsc && vite build",
+ "preview": "vite preview",
+ "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@google/model-viewer": "^3.4.0",
- "@nuxtjs/proxy": "^2.1.0",
+ "@tanstack/react-query": "^5.17.19",
"axios": "^1.6.8",
- "core-js": "^3.6.4",
"moment": "^2.29.4",
- "three": "^0.162.0",
- "vue": "^2.6.11",
- "vue-loading-template": "^1.3.2",
- "vue-meta": "^2.4.0",
- "vue-template-compiler": "^2.6.14"
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.20.1",
+ "three": "^0.163.0",
+ "zustand": "^4.4.7"
},
"devDependencies": {
- "@vue/cli-service": "~4.5.15"
- },
- "resolutions": {
- "minimatch": "^3.1.2"
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@typescript-eslint/eslint-plugin": "^6.14.0",
+ "@typescript-eslint/parser": "^6.14.0",
+ "@vitejs/plugin-react": "^4.2.1",
+ "autoprefixer": "^10.4.16",
+ "eslint": "^8.55.0",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "postcss": "^8.4.32",
+ "tailwindcss": "^3.3.6",
+ "typescript": "^5.2.2",
+ "vite": "^5.0.8"
}
-}
+}
\ No newline at end of file
diff --git a/postcss.config.js b/postcss.config.js
new file mode 100644
index 0000000..e99ebc2
--- /dev/null
+++ b/postcss.config.js
@@ -0,0 +1,6 @@
+export default {
+ plugins: {
+ tailwindcss: {},
+ autoprefixer: {},
+ },
+}
\ No newline at end of file
diff --git a/src/App.tsx b/src/App.tsx
new file mode 100644
index 0000000..9399d08
--- /dev/null
+++ b/src/App.tsx
@@ -0,0 +1,37 @@
+import { Routes, Route } from 'react-router-dom';
+import { useEffect } from 'react';
+import HomePage from './components/pages/HomePage';
+import DocsPage from './components/pages/DocsPage';
+import OwnerPage from './components/pages/OwnerPage';
+import UserPage from './components/pages/UserPage';
+
+function App() {
+ useEffect(() => {
+ // HTTPS リダイレクト
+ if (location.protocol !== "https:" && window.location.host !== "localhost:8080") {
+ location.replace("https:" + location.href.substring(location.protocol.length));
+ }
+ }, []);
+
+ return (
+
+
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+
+
+ );
+}
+
+export default App;
\ No newline at end of file
diff --git a/src/components/card/CardGrid.tsx b/src/components/card/CardGrid.tsx
new file mode 100644
index 0000000..ffcda1d
--- /dev/null
+++ b/src/components/card/CardGrid.tsx
@@ -0,0 +1,140 @@
+import { useState } from 'react';
+import type { Card, User } from '../../types';
+import { SKILL_ICONS, CARD_STATUS_COLORS } from '../../utils/constants';
+
+interface CardGridProps {
+ cards: Card[];
+ user?: User;
+}
+
+export default function CardGrid({ cards }: CardGridProps) {
+ const [showInfo, setShowInfo] = useState(false);
+ const [sortBy, setSortBy] = useState<'time' | 'cp' | 'card'>('time');
+
+ const sortedCards = [...cards].sort((a, b) => {
+ switch (sortBy) {
+ case 'cp':
+ return b.cp - a.cp;
+ case 'card':
+ return b.card - a.card;
+ default:
+ return b.id - a.id; // newest first
+ }
+ });
+
+ const getSkillIcon = (skill: Card['skill']) => {
+ const iconClass = SKILL_ICONS[skill];
+ if (skill === 'model') {
+ return ;
+ }
+ return ;
+ };
+
+ const getCardImage = (card: Card) => {
+ if (card.author === 'yui') {
+ return `/card/card_origin2_${card.card}.webp`;
+ } else if (card.author) {
+ return `/card/card_origin_${card.card}.webp`;
+ }
+ return `/card/card_${card.card}.webp`;
+ };
+
+ const isSpecialStatus = (status: Card['status']) => {
+ return status !== 'normal';
+ };
+
+ return (
+
+ {/* Control Buttons */}
+
+ setSortBy('time')}
+ className={`btn ${sortBy === 'time' ? 'bg-secondary' : ''}`}
+ >
+ new
+
+ setSortBy('cp')}
+ className={`btn ${sortBy === 'cp' ? 'bg-secondary' : ''}`}
+ >
+ cp
+
+ setSortBy('card')}
+ className={`btn ${sortBy === 'card' ? 'bg-secondary' : ''}`}
+ >
+ card
+
+ setShowInfo(!showInfo)}
+ className={`btn ${showInfo ? 'bg-secondary' : ''}`}
+ >
+ info
+
+
+
+ {/* Cards Grid */}
+
+ {sortedCards.map((card) => (
+
+ {isSpecialStatus(card.status) ? (
+
+
+
+
+
+
+
+
+ ) : (
+
+ {card.card === 43 ? (
+
+
+
+ ) : (
+
+
+ {card.author && (
+
+ @{card.author}
+
+ )}
+
+ )}
+
+ )}
+
+
+
+ {getSkillIcon(card.skill)}
+ {card.cp}
+
+
+ {showInfo && (
+
+
ID {card.card}
+
CID {card.id}
+
{card.skill}
+
✧ {card.status}
+
+ )}
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/common/Navigation.tsx b/src/components/common/Navigation.tsx
new file mode 100644
index 0000000..98d0354
--- /dev/null
+++ b/src/components/common/Navigation.tsx
@@ -0,0 +1,77 @@
+import { useLocation } from 'react-router-dom';
+
+interface NavigationProps {
+ username?: string;
+}
+
+export default function Navigation({ username: _username }: NavigationProps) {
+ const location = useLocation();
+ const loc = location.pathname.split('/').slice(-1)[0];
+
+ const getDisplayText = () => {
+ switch (loc) {
+ case 'te':
+ return '@yui.syui.ai /ten';
+ case 'pr':
+ return '@yui.syui.ai /fav 1234567';
+ case 'docs':
+ case 'en':
+ return '@yui.syui.ai /help';
+ case 'di':
+ return '@yui.syui.ai /did';
+ case 'svn':
+ return '@yui.syui.ai /ten pay 7';
+ default:
+ return '@yui.syui.ai /card';
+ }
+ };
+
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/DocsPage.tsx b/src/components/pages/DocsPage.tsx
new file mode 100644
index 0000000..2afa9c9
--- /dev/null
+++ b/src/components/pages/DocsPage.tsx
@@ -0,0 +1,115 @@
+import { useQuery } from '@tanstack/react-query';
+import Navigation from '../common/Navigation';
+import { fetchFanarts } from '../../utils/api';
+
+interface DocsPageProps {
+ isEnglish?: boolean;
+ page?: 'did' | 'vr' | 'aiten' | 'cards' | 'seven' | 'fanart' | 'photo' | 'favorite';
+}
+
+export default function DocsPage({ isEnglish = false, page }: DocsPageProps) {
+ const { data: fanarts } = useQuery({
+ queryKey: ['fanarts'],
+ queryFn: fetchFanarts,
+ enabled: page === 'fanart',
+ });
+
+ const renderContent = () => {
+ if (page === 'did') {
+ return (
+
+
DID
+
+
+ Decentralized identifiers (DIDs) are a new type of identifier that enables verifiable,
+ decentralized digital identity. A DID refers to any subject (e.g., a person, organization,
+ thing, data model, abstract entity, etc.) as determined by the controller of the DID.
+
+
+
+
+ https://www.w3.org/TR/did-core/
+
+
+
+ );
+ }
+
+ if (page === 'fanart' && fanarts) {
+ return (
+
+
/fa <share-url> <img-url>
+
+ {fanarts.data.filter(item => !item.delete).map((item, index) => (
+
+ ))}
+
+
+ );
+ }
+
+ // Default documentation content
+ return (
+
+
{isEnglish ? 'Cards can be drawn once a day' : 'カードは1日に1回、引くことができます'}
+
{isEnglish ? 'Card emission rates are as follows' : 'カードの排出率は以下のとおりです'}
+
+
+
+
+ status
+
+
+
+
+ normal
+ rare
+ super
+
+
+ 90%
+ 9%
+ 1%
+
+
+
+
+
{isEnglish ? 'Battle' : '対戦について'}
+
@yui.syui.ai /card -b
+
{isEnglish ? 'Random match, one of the top 3 cards on hand will be chosen at random' : 'ランダムマッチ、手持ちの上位3枚のうち1枚がランダムで選ばれます'}
+
+
Mastodon
+
+
+
+ @yui@syui.ai
+ /card
+
+
+
+ );
+ };
+
+ return (
+
+
+
+
+
+ {renderContent()}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/HomePage.tsx b/src/components/pages/HomePage.tsx
new file mode 100644
index 0000000..dd8d631
--- /dev/null
+++ b/src/components/pages/HomePage.tsx
@@ -0,0 +1,112 @@
+import { useState } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import Navigation from '../common/Navigation';
+import { fetchUsers } from '../../utils/api';
+
+export default function HomePage() {
+ const [didEnable, setDidEnable] = useState(false);
+
+ const { data: users, isLoading } = useQuery({
+ queryKey: ['users'],
+ queryFn: () => fetchUsers(),
+ });
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
help
+
en
+
fav
+
ten
+
setDidEnable(!didEnable)}
+ className="btn ml-2"
+ >
+ did
+
+
all
+
seven
+
+
+ {users && (
+
+
+ {users.data.map((user) => (
+
+
+
+ {user.model && (
+
+
+
+ )}
+ {user.fav !== '0' && (
+
✧
+ )}
+ {user.username === 'ai' && (
+
+
+
+ )}
+
ID {user.id}
+
+
+
+
+
+ {didEnable && user.did && (
+
+ )}
+
+ ))}
+
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/OwnerPage.tsx b/src/components/pages/OwnerPage.tsx
new file mode 100644
index 0000000..9853879
--- /dev/null
+++ b/src/components/pages/OwnerPage.tsx
@@ -0,0 +1,73 @@
+import { useQuery } from '@tanstack/react-query';
+import Navigation from '../common/Navigation';
+import { fetchCardData } from '../../utils/api';
+
+export default function OwnerPage() {
+ const { data: cardData, isLoading } = useQuery({
+ queryKey: ['cardData'],
+ queryFn: fetchCardData,
+ });
+
+ const specialCards = [22, 25, 26, 27, 29, 33, 36, 39, 40, 41, 44, 45];
+
+ if (isLoading) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+ {cardData?.data.map((card) => {
+ const shouldShow = card.id < 15 || specialCards.includes(card.id);
+
+ if (!shouldShow) return null;
+
+ const isBlackCard = [22, 27, 29, 33, 36].includes(card.id) && card.owner === 'none';
+
+ return (
+
+
+ card : {card.id} / {card.h}
+
+
+
+
+
+
+
+ {card.owner ? (
+ <>
+ owner :
+ {card.owner}
+
+ >
+ ) : (
+ <>owner : none>
+ )}
+
+
+
+ );
+ })}
+
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/pages/UserPage.tsx b/src/components/pages/UserPage.tsx
new file mode 100644
index 0000000..be4c92e
--- /dev/null
+++ b/src/components/pages/UserPage.tsx
@@ -0,0 +1,50 @@
+import { useParams } from 'react-router-dom';
+import { useQuery } from '@tanstack/react-query';
+import Navigation from '../common/Navigation';
+import CardGrid from '../card/CardGrid';
+import UserProfile from '../user/UserProfile';
+import { fetchUsers, fetchUserCards } from '../../utils/api';
+
+export default function UserPage() {
+ const { username } = useParams<{ username: string }>();
+
+ const { data: users } = useQuery({
+ queryKey: ['users'],
+ queryFn: () => fetchUsers(),
+ });
+
+ const user = users?.data.find(u => u.username === username);
+
+ const { data: cards, isLoading: cardsLoading } = useQuery({
+ queryKey: ['userCards', user?.id],
+ queryFn: () => fetchUserCards(user!.id),
+ enabled: !!user?.id,
+ });
+
+ if (!user) {
+ return (
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+ {cardsLoading ? (
+
Loading cards...
+ ) : (
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/components/user/UserProfile.tsx b/src/components/user/UserProfile.tsx
new file mode 100644
index 0000000..5cd58a7
--- /dev/null
+++ b/src/components/user/UserProfile.tsx
@@ -0,0 +1,79 @@
+import type { User, Card } from '../../types';
+
+interface UserProfileProps {
+ user: User;
+ cards: Card[];
+}
+
+export default function UserProfile({ user, cards }: UserProfileProps) {
+ return (
+
+
+ {user.username}
+
+ {/* Badges */}
+
+
+
+
+ {cards.find(c => c.card === 65) && (
+
+
+
+ )}
+
+ {user.aiten >= 70000000 && (
+
+
+
+ )}
+
+
+
+
+ ID: {user.id}
+
+
+
+ {user.aiten?.toLocaleString()}
+
+
+
+ {cards.filter(c => c.skill === 'lost').length}
+
+
+
+ {cards.filter(c => c.card >= 96 && c.card <= 121).length}
+
+ {user.planet && (
+
+
+ {user.planet}
+
+ )}
+
+
+ {/* Badge Images */}
+
+ {cards.find(c => c.card === 18) && (
+
+ )}
+ {cards.find(c => c.card === 41) && (
+
+ )}
+ {cards.find(c => c.card === 45) && (
+
+ )}
+ {cards.find(c => c.card === 75) && (
+
+ )}
+ {cards.find(c => c.card === 94) && (
+
+ )}
+ {cards.filter(c => c.card >= 96 && c.card <= 121).length === 26 && (
+
+ )}
+
+
+ );
+}
\ No newline at end of file
diff --git a/src/index.css b/src/index.css
new file mode 100644
index 0000000..53ae34d
--- /dev/null
+++ b/src/index.css
@@ -0,0 +1,60 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+@layer base {
+ html {
+ @apply text-lg;
+ text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+ }
+
+ body {
+ @apply bg-light font-sans;
+ text-size-adjust: 100%;
+ -webkit-text-size-adjust: 100%;
+ overflow-wrap: anywhere;
+ word-break: normal;
+ line-break: strict;
+ }
+
+ a {
+ @apply text-primary no-underline hover:text-secondary;
+ }
+}
+
+@layer components {
+ .btn {
+ @apply px-3 py-1 rounded border border-primary bg-primary text-white hover:border-light hover:bg-secondary transition-colors;
+ }
+
+ .btn-sm {
+ @apply px-2 py-1 text-sm rounded border border-primary bg-primary text-white hover:border-light;
+ }
+
+ .card-reflection {
+ @apply block relative overflow-hidden;
+ }
+
+ .card-reflection::after {
+ content: "";
+ @apply absolute top-0 left-0 w-8 h-full bg-white/40 opacity-0;
+ transform: rotate(45deg) translateX(-180px);
+ animation: reflection 4s ease-in-out infinite;
+ }
+
+ .card-wrapper {
+ @apply grid place-items-center relative;
+ }
+
+ .card-status {
+ @apply aspect-[5/7] rounded-lg shadow-lg absolute w-full h-full;
+ }
+}
+
+@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; }
+}
\ No newline at end of file
diff --git a/src/main.tsx b/src/main.tsx
new file mode 100644
index 0000000..da6bbb4
--- /dev/null
+++ b/src/main.tsx
@@ -0,0 +1,18 @@
+import React from 'react'
+import ReactDOM from 'react-dom/client'
+import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
+import { BrowserRouter } from 'react-router-dom'
+import App from './App.tsx'
+import './index.css'
+
+const queryClient = new QueryClient()
+
+ReactDOM.createRoot(document.getElementById('root')!).render(
+
+
+
+
+
+
+ ,
+)
\ No newline at end of file
diff --git a/src/types/index.ts b/src/types/index.ts
new file mode 100644
index 0000000..4a623d5
--- /dev/null
+++ b/src/types/index.ts
@@ -0,0 +1,56 @@
+export interface User {
+ id: number;
+ username: string;
+ did: string;
+ aiten: number;
+ handle: string;
+ delete?: boolean;
+ created_at: string;
+ planet?: number;
+ model?: boolean;
+ bsky?: boolean;
+ fav?: string;
+ game?: boolean;
+ game_lv?: number;
+ model_attack?: number;
+ model_critical?: number;
+ model_critical_d?: number;
+ room?: boolean;
+ login?: boolean;
+ game_exp?: number;
+}
+
+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';
+ author?: string;
+ url?: string;
+}
+
+export interface CardOwner {
+ id: number;
+ h: string;
+ owner: string | null;
+ ten?: number;
+ ten_skill?: boolean;
+ first_skill?: boolean;
+}
+
+export interface Fanart {
+ img: string;
+ link: string;
+ author: string;
+ delete?: boolean;
+}
+
+export interface Seven {
+ card: number;
+ count: number;
+ handle: string;
+ cp: number;
+}
+
+export type LocationPath = 'di' | 'docs' | 'en' | 'vr' | 'te' | 'c' | 'svn' | 'fa' | 'ph' | 'pr' | 'owner' | string;
\ No newline at end of file
diff --git a/src/utils/api.ts b/src/utils/api.ts
new file mode 100644
index 0000000..70667b4
--- /dev/null
+++ b/src/utils/api.ts
@@ -0,0 +1,49 @@
+import axios from 'axios';
+import type { User, Card, CardOwner, Fanart, Seven } from '../types';
+
+const getApiUrl = () => {
+ if (window.location.host === "localhost:8080" || window.location.host === "192.168.11.12:8080") {
+ return "/api/";
+ }
+ return "https://api.syui.ai/";
+};
+
+export const api = axios.create({
+ baseURL: getApiUrl(),
+});
+
+// API関数
+export const fetchUsers = async (itemsPerPage = 3000): Promise<{ data: User[] }> => {
+ const response = await api.get(`users?itemsPerPage=${itemsPerPage}`);
+ return response.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;
+};
+
+export const fetchUser = async (userId: number): Promise<{ data: User }> => {
+ const response = await api.get(`users/${userId}`);
+ return response.data;
+};
+
+export const fetchSevens = async (itemsPerPage = 8000): Promise<{ data: Seven[] }> => {
+ const response = await api.get(`sevs?itemsPerPage=${itemsPerPage}`);
+ return response.data;
+};
+
+export const fetchCardData = async (): Promise<{ data: CardOwner[] }> => {
+ const response = await axios.get("/json/card.json");
+ return response.data;
+};
+
+export const fetchFanarts = async (): Promise<{ data: Fanart[] }> => {
+ const response = await axios.get("/json/fanart.json");
+ return response.data;
+};
+
+export const fetchPhotos = async (): Promise<{ data: Fanart[] }> => {
+ const response = await axios.get("/json/photo.json");
+ return response.data;
+};
\ No newline at end of file
diff --git a/src/utils/constants.ts b/src/utils/constants.ts
new file mode 100644
index 0000000..2605bcb
--- /dev/null
+++ b/src/utils/constants.ts
@@ -0,0 +1,45 @@
+export const CARD_STATUS_COLORS = {
+ yui: 'bg-gradient-to-br from-yellow-400 to-yellow-600',
+ first: 'bg-gradient-to-br from-cyan-400 to-blue-500',
+ second: 'bg-gradient-to-br from-gray-600 to-black',
+ third: 'bg-gradient-to-br from-pink-500 to-yellow-400',
+ fourth: 'bg-gradient-to-br from-blue-400 to-blue-600',
+ fifth: 'bg-gradient-to-br from-red-500 to-red-800',
+ sixth: 'bg-gradient-to-br from-gray-100 to-gray-800',
+ seven: 'bg-gradient-to-br from-yellow-400 to-yellow-700',
+ normal: 'bg-white'
+} as const;
+
+export const SKILL_ICONS = {
+ critical: 'icon-sandar',
+ post: 'icon-moon',
+ luck: 'icon-api',
+ ten: 'icon-power',
+ lost: '●',
+ dragon: 'icon-home',
+ nyan: '▲',
+ yui: 'icon-ai',
+ '3d': '■',
+ model: 'fa-solid fa-cube',
+ first: 'icon-moji_a'
+} as const;
+
+export const PLANET_THRESHOLDS = {
+ GALAXY: 1000000,
+ NEUTRON: 466666,
+ SUN: 333000,
+ EARTH: 1.0,
+ MOON: 0
+} as const;
+
+export const CARD_EMISSION_RATES = {
+ normal: 90,
+ rare: 9,
+ super: 1
+} as const;
+
+export const CP_RANGES = {
+ normal: { min: 0, max: 200 },
+ rare: { min: 0, max: 450 },
+ super: { min: 0, max: 800 }
+} as const;
\ No newline at end of file
diff --git a/tailwind.config.js b/tailwind.config.js
new file mode 100644
index 0000000..7504a99
--- /dev/null
+++ b/tailwind.config.js
@@ -0,0 +1,22 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: [
+ "./index.html",
+ "./src/**/*.{js,ts,jsx,tsx}",
+ ],
+ theme: {
+ extend: {
+ colors: {
+ primary: '#847e00',
+ secondary: '#008CCF',
+ accent: '#fff700',
+ dark: '#343434',
+ light: '#f1f1f1',
+ },
+ fontFamily: {
+ sans: ['"Helvetica Neue"', 'Helvetica', 'Arial', 'sans-serif'],
+ },
+ },
+ },
+ plugins: [],
+}
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
new file mode 100644
index 0000000..7a7611e
--- /dev/null
+++ b/tsconfig.json
@@ -0,0 +1,25 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "resolveJsonModule": true,
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
+}
\ No newline at end of file
diff --git a/tsconfig.node.json b/tsconfig.node.json
new file mode 100644
index 0000000..099658c
--- /dev/null
+++ b/tsconfig.node.json
@@ -0,0 +1,10 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true
+ },
+ "include": ["vite.config.ts"]
+}
\ No newline at end of file
diff --git a/vite.config.ts b/vite.config.ts
new file mode 100644
index 0000000..3dbc7bb
--- /dev/null
+++ b/vite.config.ts
@@ -0,0 +1,24 @@
+import { defineConfig } from 'vite'
+import react from '@vitejs/plugin-react'
+
+export default defineConfig({
+ plugins: [react()],
+ build: {
+ outDir: 'dist',
+ },
+ server: {
+ port: 8080,
+ proxy: {
+ '/api': {
+ target: 'https://api.syui.ai',
+ changeOrigin: true,
+ rewrite: (path) => path.replace(/^\/api/, '')
+ }
+ }
+ },
+ preview: {
+ port: 4173,
+ host: true,
+ https: false
+ }
+})
\ No newline at end of file