updates nothing special

This commit is contained in:
Polaris 2024-03-15 01:10:08 -04:00
commit 2fd992b8f1
74 changed files with 15458 additions and 0 deletions

135
.gitignore vendored Normal file
View File

@ -0,0 +1,135 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.env
start.sh
/public

100
README.md Normal file
View File

@ -0,0 +1,100 @@
**CozyNet Setup:**
### REQUIRES NODE 21 OR HIGHER
1. Download CozyNet from [here](https://gitea.tendokyu.moe/PolarisPyra/cozynet).
2. Navigate to the CozyNet directory: `cd CozyNet`
3. Install Python prerequisites:
```bash
pip install tqdm pillow
```
4. Run Python scripts in `CozyNet/pythonscripts`:
- For image grabbing:
```bash
python imagegrabber.py "C:\Users\polaris\Documents\Chunithm SUN (SDHD 2.10.01)\App\data" "C:\Users\<name>\Documents\Chunithm SUN (SDHD 2.10.01)\App\bin\option" "C:\Users\<name>\Desktop\output"
```
5. Move the files from `output/` into `CozyNet/public`.
6. Create a `.env` file in the CozyNet directory with the following content:
```
NEXT_PUBLIC_ARTEMIS_API_URL=http://localhost:4000
NEXT_PUBLIC_CDN=/
NEXT_PUBLIC_COOKIE_SECURE_HTTPS= true || false (set to true if your running on a production env with https and set to false if youre running on localhost)
```
7. Install dependencies and start CozyNet:
```bash
npm install
npm run dev
```
8. CozyNet should be accessible at `localhost:3000`.
**Artemis API Setup:**
1. Download the Artemis API from [here](https://gitea.tendokyu.moe/PolarisPyra/artemisapi).
2. Navigate to the Artemis API directory: `cd ArtemisApi`
3. Install Node.js dependencies: `npm install`
4. Create a new table in the database for CozyNet Rivals WebUI:
```
python .\database_builder.py --base_path "C:\Users\polaris\Documents\CHUNITHM SUN PLUS" --mysql_host --mysql_port 3306 --mysql_user --mysql_password --mysql_db
```
the above script will make the new following new tables
cozy_static_accessory, cozy_static_mapicon, cozy_static_nameplate, cozy_static_systemvoice, and cozy_static_trophy
4a. Create the rivals table use sql script in dbeaver or cmd
```sql
CREATE TABLE IF NOT EXISTS cozynet_rival_codes(
id INTEGER PRIMARY KEY,
rival_code INTEGER NOT NULL,
FOREIGN KEY (id) REFERENCES aime_user(id) ON DELETE CASCADE
);
```
1. Configure the Artemis API env `.env.development` or `.env.production` the env belong in the root directory:
```
JWT_SECRET=mystrongsecret
host=aime
user=aime
password=dbpassword
database=aime
port=3306
```
prerequisites:
nodemon
```
npm install -g nodemon
```
1. Start Artemis API:
```bash
npm run start:dev # For development
npm run start:prod # For production
```
2. Credits:
SQL tables - Beerpsi
README.zh-TW.md - Raymonf
BUG:
Update:
I have found the problem it is with NEXT_PUBLIC_COOKIE_SECURE_HTTPS=true being set on localhost machine i will try to fix it. Just know you should be using false if you're on localhost.
Initial Bug:
If you're hosting on a local machine the first time you login you will encounter a bug where the username and password get sent to the url bar as queries in plain text and you will then find yourself unable to login. If you clear the url bar and refresh the page you should be able to enter your information and login correctly. I do not currently know what causes this bug and if someone could help me find the cause I would be very greatful.

62
README.zh_tw.md Normal file
View File

@ -0,0 +1,62 @@
**CozyNet設定**
1. 從[這裡](https://gitea.tendokyu.moe/PolarisPyra/cozynet)下載CozyNet。
2. 切換到CozyNet目錄`cd CozyNet`
3. 安裝Python相依性
```bash
pip install tqdm pillow
```
4. 執行在`CozyNet/pythonscripts`中的Python script腳本
- 用於抓取圖片:
```bash
python imagegrabber.py "C:\Users\polaris\Documents\Chunithm SUN (SDHD 2.10.01)\App\data" "C:\Users\<name>\Documents\Chunithm SUN (SDHD 2.10.01)\App\bin\option" "C:\Users\<name>\Desktop\output"
```
- 用於抓取資料:
```bash
python datagrabber.py "C:\Users\polaris\Documents\Chunithm SUN (SDHD 2.10.01)" "C:\Users\<name>\Desktop\output"
```
5. 在CozyNet目錄中建立一個`.env`檔案,內容如下:
```
NEXT_PUBLIC_ARTEMIS_API_URL=http://localhost:4000
NEXT_PUBLIC_CDN=/
NEXT_PUBLIC_COOKIE_SECURE_HTTPS= true || false如果在具有https的生產環境中運行請設為true。如果在localhost上運行請設為false
```
6. 將`output/`的檔案移動到`CozyNet/public`。
7. 將`data/`的檔案移動到`CozyNet/src/lib`(包括稱為 `accessory, mapicon, nameplate, systemvoice, trophy` 的 TypeScript (ts) 檔案)。
8. 安裝相依性並啟動CozyNet
```bash
npm install
npm run dev
```
9. CozyNet應該可在`localhost:3000`訪問。
**Artemis API設定**
1. 從[這裡](https://gitea.tendokyu.moe/PolarisPyra/artemisapi)下載Artemis API。
2. 切換到Artemis API目錄`cd ArtemisApi`
3. 安裝Node.js相依性`npm install`
4. 在資料庫中為CozyNet Rivals WebUI建立一個新表
```sql
CREATE TABLE IF NOT EXISTS cozynet_rival_codes(
id INTEGER PRIMARY KEY,
rival_code INTEGER NOT NULL,
FOREIGN KEY (id) REFERENCES aime_user(id) ON DELETE CASCADE
);
```
5. 於根目錄建立稱為`.env.development`或`.env.production`的檔案並配置Artemis API環境
```
JWT_SECRET=mystrongsecret
host=aime
user=aime
password=dbpassword
database=aime
port=3306
```
6. 啟動Artemis API
```bash
npm run start:dev # 開發用 (development)
npm run start:prod # 生產用 (production)
```

17
components.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "default",
"rsc": true,
"tsx": true,
"tailwind": {
"config": "tailwind.config.ts",
"css": "src/app/globals.css",
"baseColor": "slate",
"cssVariables": true,
"prefix": ""
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils"
}
}

5
next-env.d.ts vendored Normal file
View File

@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

30
next.config.js Normal file
View File

@ -0,0 +1,30 @@
/** @type {import('next').NextConfig} */
const nextConfig = {};
module.exports = {
eslint: {
// Warning: This allows production builds to successfully complete even if
// your project has ESLint errors.
ignoreDuringBuilds: true,
reactStrictMode: true,
},
async headers() {
return [
{
source: '/:all*(svg|jpg|png)',
locale: false,
headers: [{ key: "Cache-Control", value: "public, max-age=36000, must-revalidate" }],
}
]
},
images: {
remotePatterns: [
{
protocol: "https",
hostname: "**",
},
],
unoptimized: true,
},
};

7170
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

69
package.json Normal file
View File

@ -0,0 +1,69 @@
{
"name": "artemisfrontend",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev -p 3030",
"build": "next build",
"start": "next start -p 3030",
"lint": "next lint"
},
"dependencies": {
"@hookform/resolvers": "^3.3.4",
"@radix-ui/react-dropdown-menu": "^2.0.6",
"@radix-ui/react-icons": "^1.3.0",
"@radix-ui/react-label": "^2.0.2",
"@radix-ui/react-scroll-area": "^1.0.5",
"@radix-ui/react-select": "^2.0.0",
"@radix-ui/react-separator": "^1.0.3",
"@radix-ui/react-slot": "^1.0.2",
"@radix-ui/react-switch": "^1.0.3",
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toast": "^1.1.5",
"@react-spring/web": "^9.7.3",
"@types/bcrypt": "^5.0.2",
"@types/cookie": "^0.5.4",
"@types/js-cookie": "^3.0.5",
"@types/jsonwebtoken": "^9.0.5",
"@vercel/analytics": "^1.1.1",
"axios": "^1.6.0",
"bcrypt": "^5.1.1",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"cookie": "^0.5.0",
"framer-motion": "^10.16.16",
"isbot": "^3.8.0",
"js-cookie": "^3.0.5",
"jsonwebtoken": "^9.0.2",
"lucide-react": "^0.292.0",
"next": "14.0.1",
"next-client-cookies": "^1.0.6",
"next-themes": "^0.2.1",
"pnpm": "^8.11.0",
"react-circular-progressbar": "^2.1.0",
"react-hook-form": "^7.49.2",
"react-icons": "^4.12.0",
"react-select": "^5.8.0",
"react-switch": "^7.0.0",
"react-toastify": "^9.1.3",
"rg-stats": "^0.5.4",
"tailwind-merge": "^2.2.0",
"tailwindcss-animate": "^1.0.7",
"zod": "^3.22.4"
},
"devDependencies": {
"@types/express": "^4.17.21",
"@types/node": "^20.11.5",
"@types/react": "^18.2.38",
"@types/react-dom": "^18",
"autoprefixer": "^10.4.16",
"daisyui": "^4.5.0",
"eslint": "^8",
"eslint-config-next": "14.0.1",
"postcss": "^8",
"prettier": "^3.1.1",
"prettier-plugin-tailwindcss": "^0.5.9",
"tailwindcss": "^3.3.0",
"typescript": "^5.3.2"
}
}

6
postcss.config.js Normal file
View File

@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

View File

@ -0,0 +1,165 @@
import os
import xml.etree.ElementTree as ET
from sqlalchemy import create_engine, Column, String, MetaData, Table, text
from sqlalchemy.orm import sessionmaker
from pathlib import Path
import argparse
from sqlalchemy.dialects.mysql import insert
# Define the SQLAlchemy model
metadata = MetaData()
def define_table(data_type):
existing_table = metadata.tables.get(data_type)
if existing_table is not None:
return existing_table
return Table(
data_type,
metadata,
Column('id', String(length=255), primary_key=True),
Column('str', String(length=255, collation='utf8mb4_unicode_ci')),
Column('imagePath', String(
length=255, collation='utf8mb4_unicode_ci'), nullable=True),
Column('sortName', String(
length=255, collation='utf8mb4_unicode_ci'), nullable=True),
Column('category', String(
length=255, collation='utf8mb4_unicode_ci'), nullable=True),
Column('netOpenName', String(
length=255, collation='utf8mb4_unicode_ci'), nullable=True),
Column('rareType', String(
length=255, collation='utf8mb4_unicode_ci'), nullable=True),
mysql_charset='utf8mb4',
mysql_collate='utf8mb4_unicode_ci'
)
def parse_xml(file_path):
tree = ET.parse(file_path)
return tree.getroot()
def find_data(root, data_type):
id_element = root.find('.//name/id')
str_element = root.find('.//name/str')
data = {'id': id_element.text, 'str': str_element.text}
if data_type == "cozynet_chuni_static_accessory":
data.update({
'imagePath': root.find('.//image/path').text or "",
'sortName': root.find('.//sortName').text or "",
'category': root.find('.//category').text or "",
'netOpenName': root.find('.//netOpenName/str').text or ""
})
elif data_type in ["cozynet_chuni_static_nameplate"]:
data.update({
'imagePath': root.find('.//image/path').text or "",
'sortName': root.find('.//sortName').text or "",
'netOpenName': root.find('.//netOpenName/str').text or ""
})
elif data_type in ["cozynet_chuni_static_systemvoice"]:
data.update({
'imagePath': root.find('.//image/path').text or "",
'sortName': root.find('.//sortName').text or ""
})
elif data_type == "cozynet_chuni_static_trophies":
data.update({
'rareType': root.find('.//rareType').text or "",
'netOpenName': root.find('.//netOpenName/str').text or ""
})
elif data_type == "cozynet_chuni_static_mapicon":
data.update({
'imagePath': root.find('.//image/path').text or "",
'sortName': root.find('.//sortName').text or ""
})
return data
def insert_data_into_database(session, data_type, data):
table = define_table(data_type)
stmt = insert(table).values(data).on_duplicate_key_update(data)
session.execute(stmt)
def process_directories(base_path, session, data_types, xml_file_names):
for data_type, xml_file_name in zip(data_types, xml_file_names):
base_directories = [base_path]
for directory in base_directories:
for xml_path in find_xml_files(directory, xml_file_name):
root = parse_xml(xml_path)
data = find_data(root, data_type)
if data:
insert_data_into_database(session, data_type, data)
def find_xml_files(directory, xml_file_name):
for dirpath, _, filenames in os.walk(directory):
if xml_file_name in filenames:
yield Path(dirpath) / xml_file_name
def main():
parser = argparse.ArgumentParser(
description='Process XML files and write data to MySQL database.')
parser.add_argument('--base_path', type=str,
help='Base path to the Chunithm data directory', required=True)
parser.add_argument('--mysql_host', type=str,
help='MySQL host address', required=True)
parser.add_argument('--mysql_port', type=int,
help='MySQL port number', required=True)
parser.add_argument('--mysql_user', type=str,
help='MySQL username', required=True)
parser.add_argument('--mysql_password', type=str,
help='MySQL password', required=True)
parser.add_argument('--mysql_db', type=str,
help='MySQL database name', required=True)
args = parser.parse_args()
base_path = args.base_path
mysql_host = args.mysql_host
mysql_port = args.mysql_port
mysql_user = args.mysql_user
mysql_password = args.mysql_password
mysql_db = args.mysql_db
data_types = ['cozynet_chuni_static_accessory', 'cozynet_chuni_static_nameplate',
'cozynet_chuni_static_systemvoice', 'cozynet_chuni_static_trophies', 'cozynet_chuni_static_mapicon']
xml_file_names = ['AvatarAccessory.xml', 'NamePlate.xml',
'SystemVoice.xml', 'Trophy.xml', 'MapIcon.xml']
# Create MySQL database connection URL
database_url = f"mysql://{mysql_user}:{mysql_password}@{mysql_host}:{mysql_port}/{mysql_db}?charset=utf8mb4"
# Create an engine and session for the MySQL database
engine = create_engine(database_url)
Session = sessionmaker(bind=engine)
session = Session()
# Drop existing tables
for data_type in data_types:
table = define_table(data_type)
table.drop(engine, checkfirst=True)
# Create tables in the MySQL database
for data_type in data_types:
define_table(data_type).create(engine, checkfirst=True)
process_directories(base_path, session, data_types, xml_file_names)
# Commit changes and close the session
session.commit()
session.close()
if __name__ == "__main__":
main()

View File

@ -0,0 +1,159 @@
import os
import re
from pathlib import Path
from PIL import Image
from tqdm import tqdm
import argparse
def find_dds_files(source_folders, file_pattern=None):
dds_files = []
for source_folder in source_folders:
for root, _, files in os.walk(source_folder):
for file in files:
if file.endswith('.dds') and (file_pattern is None or file_pattern.match(file)):
dds_files.append(os.path.join(root, file))
return dds_files
def convert_dds_to_png(dds_file_path, png_file_path, scale_percent=50):
with Image.open(dds_file_path) as img:
new_width = int(img.width * scale_percent / 100)
new_height = int(img.height * scale_percent / 100)
img = img.resize((new_width, new_height), Image.Resampling.LANCZOS)
img.save(png_file_path, 'PNG')
def process_files(files, destination_folder, progress_prefix, scale_percent=100):
files_to_convert = []
for file_path in files:
file_name = os.path.splitext(os.path.basename(file_path))[0] + '.png'
png_file_path = os.path.join(destination_folder, file_name)
if not os.path.exists(png_file_path):
files_to_convert.append(file_path)
if not files_to_convert:
print(f"{progress_prefix}: Already converted!")
return
for file_path in tqdm(files_to_convert, desc=progress_prefix, unit='file'):
file_name = os.path.splitext(os.path.basename(file_path))[0] + '.png'
png_file_path = os.path.join(destination_folder, file_name)
convert_dds_to_png(file_path, png_file_path, scale_percent)
def process_avatarAccessory(source_folders, destination_folder, progress_prefix):
avatar_accessory_files = find_dds_files(source_folders)
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
process_files(avatar_accessory_files, destination_folder,
progress_prefix, scale_percent=50)
def process_nameplates(source_folders, destination_folder, progress_prefix):
nameplate_files = find_dds_files(source_folders)
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
process_files(nameplate_files, destination_folder, progress_prefix)
def process_systemVoiceImages(source_folders, destination_folder, progress_prefix):
nameplate_files = find_dds_files(source_folders)
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
process_files(nameplate_files, destination_folder, progress_prefix)
def process_JacketArt(source_folders, destination_folder, progress_prefix):
nameplate_files = find_dds_files(source_folders)
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
process_files(nameplate_files, destination_folder, progress_prefix)
def process_mapIcon(source_folders, destination_folder, progress_prefix):
nameplate_files = find_dds_files(source_folders)
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)
process_files(nameplate_files, destination_folder, progress_prefix)
def process_partners(source_folders, destination_chunithm, destination_chusan):
pattern_chunithm = re.compile(
r'CHU_UI_Character_0([0-9]{3})_(00|01)_[0-9]{2}\.dds$')
pattern_chusan = re.compile(
r'CHU_UI_Character_([1-9]\d{3,})_(00|01)_[0-9]{2}\.dds$')
chunithm_files = find_dds_files(source_folders, pattern_chunithm)
chusan_files = find_dds_files(source_folders, pattern_chusan)
if not os.path.exists(destination_chunithm):
os.makedirs(destination_chunithm)
process_files(chunithm_files, destination_chunithm,
"Chunithm Partners")
if not os.path.exists(destination_chusan):
os.makedirs(destination_chusan)
process_files(chusan_files, destination_chusan,
"Chusan Partners")
def find_subdirectories(base_directories, subdirectory_names):
subdirectory_paths = {name: [] for name in subdirectory_names}
for base_directory in base_directories:
for subdir in Path(base_directory).rglob('*'):
if subdir.is_dir() and subdir.name in subdirectory_names:
subdirectory_paths[subdir.name].append(str(subdir))
return subdirectory_paths
def main(data_folder, option_folder, output_folder):
relevant_subdirs = ['music', 'avatarAccessory',
'mapIcon', 'namePlate', 'ddsImage', 'systemVoice']
subdirectories = find_subdirectories(
[data_folder, option_folder], relevant_subdirs)
output_path = output_folder
if not os.path.exists(output_path):
os.makedirs(output_path)
process_partners(subdirectories['ddsImage'], os.path.join(
output_path, 'public', 'chunithm_partners'), os.path.join(output_path, 'public', 'chusan_partners'))
process_nameplates(subdirectories['namePlate'], os.path.join(
output_path, 'public', 'namePlates'), 'namePlate')
process_systemVoiceImages(subdirectories['systemVoice'], os.path.join(
output_path, 'public', 'systemVoiceThumbnails'), 'systemVoice thumbnail')
process_JacketArt(subdirectories['music'], os.path.join(
output_path, 'public', 'JacketArt'), 'jacket art')
process_mapIcon(subdirectories['mapIcon'], os.path.join(
output_path, 'public', 'mapIcon'), 'map icon')
process_avatarAccessory(subdirectories['avatarAccessory'], os.path.join(
output_path, 'public', 'avatarAccessory'), 'avatarAccessory Progress')
print("All conversions and transfers complete.")
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description='Converts Chunithm dds files to pngs for CozyNet webUI.')
parser.add_argument('data_folder', type=str, help='Path to data folder')
parser.add_argument('option_folder', type=str,
help='Path to option folder')
parser.add_argument('output_folder', type=str,
help='Path to the output folder')
args = parser.parse_args()
main(args.data_folder, args.option_folder, args.output_folder)

View File

@ -0,0 +1,944 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { Dispatch, SetStateAction, useEffect, useState } from "react"
import axios from "axios"
import { ApiFetch } from "@/lib/api"
type AvatarItemList = {
id: number
user: number
itemId: number
type: number
itemKind: number
stock: number
isValid: number
}
interface UseAvatarItemListParams {
setAvatarItemList: Dispatch<SetStateAction<AvatarItemList[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
axios.defaults.withCredentials = true;
export const UseAvatarItemList = ({
setAvatarItemList,
setLoading,
setError,
}: UseAvatarItemListParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/items`;
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setAvatarItemList(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setAvatarItemList, setLoading, setError])
}
type bestTop = {
maxCombo: number
isAllJustice: number
isFullCombo: number
userPlayDate: string
score: number
isNewRecord: number
judgeHeaven: number
judgeGuilty: number
judgeJustice: number
judgeAttack: number
judgeCritical: number
isClear: number
skillId: number
chartId: number
title: string
level: number
genre: string
jacketPath: string
artist: string
score_change: string
rating_change: string
rating: number
}
interface UseBestTopScoresParams {
setBestTop: Dispatch<SetStateAction<bestTop[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const useBestTop = ({
setBestTop,
setLoading,
setError,
}: UseBestTopScoresParams) => {
useEffect(() => {
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-best-and-top`
const source = axios.CancelToken.source()
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setBestTop(response.data)
setLoading(false)
})
.catch((err) => {
if (!axios.isCancel(err)) {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setBestTop, setLoading, setError])
}
type playerRecentRatingTable = {
score: number
chartId: number
title: string
level: number
genre: string
jacketPath: string
artist: string
rating: number
}
interface UsePlayerRecentRatingTableParams {
setPlayerRecentRatingTable: Dispatch<
SetStateAction<playerRecentRatingTable[]>
>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const usePlayerRecentRatingTable = ({
setPlayerRecentRatingTable,
setLoading,
setError,
}: UsePlayerRecentRatingTableParams) => {
useEffect(() => {
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-recent`
const source = axios.CancelToken.source()
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerRecentRatingTable(response.data)
setLoading(false)
})
.catch((err) => {
if (!axios.isCancel(err)) {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerRecentRatingTable, setLoading, setError])
}
type playerAvatarSelection = {
avatarBack: string
avatarItem: string
avatarWear: string
avatarFront: string
avatarSkin: string
avatarHead: string
avatar_skinfoot_l: string
avatar_skinfoot_r: string
avatarFace: string
}
interface UseCharacterDataParams {
setPlayerInfo: Dispatch<SetStateAction<playerAvatarSelection[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerInfo = ({
setPlayerInfo,
setLoading,
setError,
}: UseCharacterDataParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/profile-data`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerInfo(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
// Handle cancellation if needed
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerInfo, setLoading, setError])
}
type playerVoiceIdSelection = {
voiceId: string
}
interface UsePlayerVoiceIdParams {
setPlayerVoiceId: Dispatch<SetStateAction<playerVoiceIdSelection[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerVoiceId = ({
setPlayerVoiceId,
setLoading,
setError,
}: UsePlayerVoiceIdParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/profile-data`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerVoiceId(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
// Handle cancellation if needed
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerVoiceId, setLoading, setError])
}
type playerMapIconSelection = {
mapIconId: string
}
interface UsePlayerMapIconParams {
setPlayerMapIcon: Dispatch<SetStateAction<playerMapIconSelection[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerMapIcon = ({
setPlayerMapIcon,
setLoading,
setError,
}: UsePlayerMapIconParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/profile-data`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerMapIcon(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
// Handle cancellation if needed
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerMapIcon, setLoading, setError])
}
type playerNamePlateIconSelection = {
nameplateId: string
}
interface UsePlayerNameplateParams {
setPlayerNameplate: Dispatch<SetStateAction<playerNamePlateIconSelection[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerNamePlates = ({
setPlayerNameplate,
setLoading,
setError,
}: UsePlayerNameplateParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/profile-data`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerNameplate(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
// Handle cancellation if needed
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerNameplate, setLoading, setError])
}
type PlayerScoreLogType = any
interface UseFetchPlayerScoreLogParams {
setPlayerScoreLog: Dispatch<SetStateAction<PlayerScoreLogType>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const useFetchPlayerScoreLog = ({
setPlayerScoreLog,
setLoading,
setError,
}: UseFetchPlayerScoreLogParams) => {
useEffect(() => {
let isMounted = true
const source = axios.CancelToken.source()
async function fetchProfileData() {
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-score-playlog`
try {
const user = await ApiFetch<{ body: { version: number; } }>("/SDHD/user");
// Check if the version in the response is the expected version
if (user.body.body.version !== 15) {
setError("Please update to the latest version")
setLoading(false)
return
}
const response = await axios.get(apiUrl, { cancelToken: source.token })
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
if (isMounted) {
setPlayerScoreLog(response.data)
setLoading(false)
}
} catch (error) {
if (axios.isCancel(error)) {
// Request canceled
} else if (error instanceof Error) {
setError(error.message)
setLoading(false)
} else {
setError("An unknown error occurred")
setLoading(false)
}
}
}
fetchProfileData()
return () => {
isMounted = false
source.cancel("Operation canceled by the user.")
}
}, [setError, setLoading, setPlayerScoreLog])
}
type playerStaticMusic = any
interface UsePlayerStaticMusicParam {
setPlayerStaticMusic: Dispatch<SetStateAction<playerStaticMusic>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const useFetchStaticMusic = ({
setPlayerStaticMusic,
setLoading,
setError,
}: UsePlayerStaticMusicParam) => {
useEffect(() => {
let isMounted = true
const source = axios.CancelToken.source()
async function fetchProfileData() {
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-static-music`
try {
const user = await ApiFetch<{ body: { version: number; } }>("/SDHD/user");
// Check if the version in the response is the expected version
if (user.body.body.version !== 15) {
setError("Please update to the latest version")
setLoading(false)
return
}
const response = await axios.get(apiUrl, { cancelToken: source.token })
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
if (isMounted) {
setPlayerStaticMusic(response.data)
setLoading(false)
}
} catch (error) {
if (axios.isCancel(error)) {
// Request canceled
} else if (error instanceof Error) {
setError(error.message)
setLoading(false)
} else {
setError("An unknown error occurred")
setLoading(false)
}
}
}
fetchProfileData()
return () => {
isMounted = false
source.cancel("Operation canceled by the user.")
}
}, [setError, setLoading, setPlayerStaticMusic])
}
type PlayerTeam = {
id: number
teamName: string
teamPoint: number
}
interface usePlayerTeamsParams {
setPlayerTeams: Dispatch<SetStateAction<PlayerTeam[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerTeams = ({
setPlayerTeams,
setLoading,
setError,
}: usePlayerTeamsParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-player-team`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerTeams(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerTeams, setLoading, setError])
}
type PlayerRivalList = any
interface UsePlayerRivalListParams {
setPlayerRivalList: React.Dispatch<React.SetStateAction<PlayerRivalList[]>>
setLoading: React.Dispatch<React.SetStateAction<boolean>>
setError: React.Dispatch<React.SetStateAction<string | null>>
refreshRivalList: boolean // Add the new dependency
}
export const usePlayerRivalList = ({
setPlayerRivalList,
setLoading,
setError,
refreshRivalList, // Include it in the parameters
}: UsePlayerRivalListParams) => {
useEffect(() => {
const fetchData = async () => {
const source = axios.CancelToken.source()
try {
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/listRivals`
const response = await axios.get(apiUrl, { cancelToken: source.token })
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerRivalList(response.data)
setLoading(false)
} catch (error) {
if (!axios.isCancel(error)) {
setError("error")
setLoading(false)
}
}
return () => {
source.cancel("Operation canceled by the user.")
}
}
fetchData()
}, [
setPlayerRivalList,
setLoading,
setError,
refreshRivalList,
]) // Include refreshRivalList as a dependency
}
type PlayerFavoriteSong = {
songId: number
// Add other properties as needed
}
export type PlayerFavoriteSongsList = PlayerFavoriteSong[]
interface UsePlayerFavoriteSongListResult {
playerFavoriteSongs: PlayerFavoriteSongsList
loading: boolean
error: string | null
refetchPlayerFavoriteSongs: () => void // Add refetch function
}
export const usePlayerFavoriteSongsList =
(): UsePlayerFavoriteSongListResult => {
const [playerFavoriteSongs, setPlayerFavoriteSongs] =
useState<PlayerFavoriteSongsList>([])
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const fetchData = async () => {
const source = axios.CancelToken.source()
try {
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/list-favorite-songs`
const response = await axios.get(apiUrl, { cancelToken: source.token })
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerFavoriteSongs(response.data)
setLoading(false)
} catch (error) {
if (!axios.isCancel(error)) {
setError("An error occurred")
setLoading(false)
}
} finally {
source.cancel("Operation canceled by the user.")
}
}
useEffect(() => {
fetchData()
}, [])
const refetchPlayerFavoriteSongs = () => {
setLoading(true)
setError(null)
fetchData()
}
return { playerFavoriteSongs, loading, error, refetchPlayerFavoriteSongs }
}
type PlayerAimeCard = {
accessCode: number
}
interface usePlayerAimeCardParams {
setPlayAimeCard: Dispatch<SetStateAction<PlayerAimeCard[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerAimeCard = ({
setPlayAimeCard,
setLoading,
setError,
}: usePlayerAimeCardParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/access-code`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayAimeCard(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [])
}
type PlayerProfileTrophies = {
name: string
trophyId: number
itemId: string
}
interface usePlayerProfileTrophiesParam {
setPlayerProfileTrophies: Dispatch<SetStateAction<PlayerProfileTrophies[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerProfileTrophies = ({
setPlayerProfileTrophies,
setLoading,
setError,
}: usePlayerProfileTrophiesParam) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/profile-data`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerProfileTrophies(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerProfileTrophies, setLoading, setError])
}
type PlayerTrophies = {
name: string
trophyId: string
itemId: string
rareType: number
isClear: number
isAllJustice: number
}
interface usePlayerTrophiesParam {
setPlayerTrophies: Dispatch<SetStateAction<PlayerTrophies[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerTrophies = ({
setPlayerTrophies,
setLoading,
setError,
}: usePlayerTrophiesParam) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/get-trophies`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerTrophies(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerTrophies, setLoading, setError])
}
type PlayerAvatarItems = {
avatarBack: string
avatarItem: string
avatarWear: string
avatarFront: string
avatarSkin: string
avatarHead: string
avatar_skinfoot_l: string
avatar_skinfoot_r: string
avatarFace: string
}
interface usePlayerAvatarItemsParam {
setPlayerAvatarItems: Dispatch<SetStateAction<PlayerAvatarItems[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
}
export const UsePlayerAvatarItems = ({
setPlayerAvatarItems,
setLoading,
setError,
}: usePlayerAvatarItemsParam) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-avatar-items`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerAvatarItems(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerAvatarItems, setLoading, setError])
}
type PlayerDuelCompletions = {
duelId: string
str: string
}
interface UsePlayerDuelCompletionsParams {
setPlayerDuelCompletions: Dispatch<SetStateAction<PlayerDuelCompletions[]>>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
onSuccess?: (data: PlayerDuelCompletions[]) => void
}
export const UsePlayerDuelCompletions = ({
setPlayerDuelCompletions,
setLoading,
setError,
onSuccess,
}: UsePlayerDuelCompletionsParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-duel-items`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerDuelCompletions(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerDuelCompletions, setLoading, setError])
}
type playerAvailableNameplates = {
nameplateId: string
str: string
imagePath: string
}
interface UsePlayerAvailableNameplatesParams {
setPlayerAvailableNameplates: Dispatch<
SetStateAction<playerAvailableNameplates[]>
>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
onSuccess?: (data: PlayerDuelCompletions[]) => void
}
export const UsePlayerAvailableNameplates = ({
setPlayerAvailableNameplates,
setLoading,
setError,
onSuccess,
}: UsePlayerAvailableNameplatesParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-nameplate-items`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerAvailableNameplates(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerAvailableNameplates, setLoading, setError])
}
type playerAvailableMapIcons = {
itemId: string
str: string
imagePath: string
}
interface UsePlayerAvailableMapIconsParams {
setPlayerAvailableMapIcons: Dispatch<
SetStateAction<playerAvailableMapIcons[]>
>
setLoading: Dispatch<SetStateAction<boolean>>
setError: Dispatch<SetStateAction<string | null>>
onSuccess?: (data: PlayerDuelCompletions[]) => void
}
export const UsePlayerAvailableMapIcons = ({
setPlayerAvailableMapIcons,
setLoading,
setError,
onSuccess,
}: UsePlayerAvailableMapIconsParams) => {
useEffect(() => {
const source = axios.CancelToken.source()
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/chuni-map-items`
axios
.get(apiUrl, { cancelToken: source.token })
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
setPlayerAvailableMapIcons(response.data)
setLoading(false)
})
.catch((err) => {
if (axios.isCancel(err)) {
} else {
setError(err.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}, [setPlayerAvailableMapIcons, setLoading, setError])
}

View File

@ -0,0 +1,528 @@
import axios from "axios"
type AvatarData = any
interface UsePostAvatarItemListParams {
onPostSuccess: (data: AvatarData) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
axios.defaults.withCredentials = true;
export const UsePostAvatarItemList = ({
onPostSuccess,
setLoading,
setError,
}: UsePostAvatarItemListParams) => {
const postAvatarData = (avatarData: AvatarData) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/updateAvatarCustomization`
axios
.post(apiUrl, avatarData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postAvatarData }
}
type KeychipCreation = any
interface UsePostNewKeychipParams {
onPostSuccess: (data: KeychipCreation) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostNewKeychip = ({
onPostSuccess,
setLoading,
setError,
}: UsePostNewKeychipParams) => {
const createNewKeychip = (postTeamData: KeychipCreation) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/create_keychip`
axios
.post(apiUrl, postTeamData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { createNewKeychip }
}
type MapIcon = any
interface UsePostMapIconParams {
onPostSuccess: (data: MapIcon) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostMapIcon = ({
onPostSuccess,
setLoading,
setError,
}: UsePostMapIconParams) => {
const postMapIcon = (avatarData: MapIcon) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_chuni_item_mapchar`
axios
.post(apiUrl, avatarData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postMapIcon }
}
type NameplateId = any
interface UsePostNameplateIdParams {
onPostSuccess: (data: NameplateId) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostNamePlate = ({
onPostSuccess,
setLoading,
setError,
}: UsePostNameplateIdParams) => {
const postNamePlate = (avatarData: NameplateId) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_nameplateId`
axios
.post(apiUrl, avatarData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postNamePlate }
}
type PlayerTeam = any
interface UsePostPlayerTeamParams {
onPostSuccess: (data: PlayerTeam) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostPlayerTeam = ({
onPostSuccess,
setLoading,
setError,
}: UsePostPlayerTeamParams) => {
const postTeamName = (PostTeamData: PlayerTeam) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_chuni_player_team`
axios
.post(apiUrl, PostTeamData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postTeamName }
}
type PlayerTeamProfile = any
interface UsePostPlayerTeamProfileParams {
onPostSuccess: (data: PlayerTeamProfile) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostPlayerTeamProfile = ({
onPostSuccess,
setLoading,
setError,
}: UsePostPlayerTeamProfileParams) => {
const postTeamNameProfile = (
postPlayerTeamProfileData: PlayerTeamProfile,
) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_chuni_profile_player_team`
axios
.post(apiUrl, postPlayerTeamProfileData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postTeamNameProfile }
}
type SystemVoice = any
interface UsePostSystemVoiceParams {
onPostSuccess: (data: SystemVoice) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostSystemVoice = ({
onPostSuccess,
setLoading,
setError,
}: UsePostSystemVoiceParams) => {
const postVoiceID = (avatarData: SystemVoice) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_chuni_item_duel`
axios
.post(apiUrl, avatarData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postVoiceID }
}
type TrophyName = { trophyId: number }
interface UsePostTrophyParams {
onPostSuccess: (data: TrophyName) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostTrophy = ({
onPostSuccess,
setLoading,
setError,
}: UsePostTrophyParams) => {
const postTrophy = (avatarData: TrophyName) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_trophyid`
axios
.post(apiUrl, avatarData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postTrophy }
}
type playerRivals = any
interface UsePostPlayerRivalParams {
onPostSuccess: (data: playerRivals) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostPlayerRival = ({
onPostSuccess,
setLoading,
setError,
}: UsePostPlayerRivalParams) => {
const postRival = (avatarData: playerRivals) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_chuni_player_rivals`
axios
.post(apiUrl, avatarData, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postRival }
}
type playerFavoriteSongs = any
interface UsePostPlayerFavoriteSongsParams {
onPostSuccess: (data: playerFavoriteSongs) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostFavoriteSongs = ({
onPostSuccess,
setLoading,
setError,
}: UsePostPlayerFavoriteSongsParams) => {
const postFavoriteSong = (FavoriteSong: playerFavoriteSongs) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/update_chuni_favorite_music`;
axios
.post(apiUrl, FavoriteSong, {
cancelToken: source.token,
})
.then((response) => {
if (response.status !== 200) {
throw new Error("Network response was not ok")
}
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postFavoriteSong }
}
type NewPlayerCard = any
interface UsePostNewAccessCodeParam {
onPostSuccess: (data: NewPlayerCard) => void
setLoading: (loading: boolean) => void
setError: (error: string | null) => void
}
export const UsePostNewCard = ({
onPostSuccess,
setLoading,
setError,
}: UsePostNewAccessCodeParam) => {
const postAccessCode = (accesscodeData: NewPlayerCard) => {
setLoading(true)
const source = axios.CancelToken.source()
// Include the user ID in the request URL
const apiUrl = `${process.env.NEXT_PUBLIC_ARTEMIS_API_URL}/SDHD/updateAccessCode`
axios
.post(
apiUrl,
{ access_code: accesscodeData.access_code },
{ cancelToken: source.token },
)
.then((response) => {
onPostSuccess(response.data)
setLoading(false)
})
.catch((error) => {
if (axios.isCancel(error)) {
console.log(error.message)
} else {
setError(error.message)
setLoading(false)
}
})
return () => {
source.cancel("Operation canceled by the user.")
}
}
return { postAccessCode }
}

View File

@ -0,0 +1,35 @@
"use client"
import axios from "axios"
import { useState, useEffect } from "react"
import { AxiosError } from "axios"
import { useRouter } from "next/navigation"
import { IsLoggedIn } from "@/lib/auth"
interface UserResponse {
user: string | null
error: AxiosError | null
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const [isSuccess, setIsSuccess] = useState<boolean>(false)
const router = useRouter()
useEffect(() => {
;(async () => {
if (!await IsLoggedIn()) {
router.push("/")
return
}
setIsSuccess(true)
})()
}, [router])
if (!isSuccess) {
}
return <main>{children}</main>
}

20
src/app/chunithm/page.tsx Normal file
View File

@ -0,0 +1,20 @@
import CharacterCard from "@/components/CharacterCard"
import NavigationBar from "@/components/shared/NavigationBar/NavigationBar"
import ScoreCardList from "@/components/PlayerScoreGrid"
import PlayerBestTopScores from "@/components/PlayerTopBestScores"
export default function Chunithm() {
return (
<div className="min-h-screen bg-background overflow-hidden">
<NavigationBar />
<div className="flex w-full flex-col lg:w-3/4 lg:flex-row xl:w-4/5">
<div className="pl-2">
<PlayerBestTopScores />
</div>
<div className="p-4">
<CharacterCard />
<ScoreCardList />
</div>
</div>
</div>
)
}

View File

@ -0,0 +1,53 @@
"use client"
import React, { createContext, useContext, useState, ReactNode } from "react"
interface SettingsContextProps {
isAverageEnabled: boolean
isBestPossibleEnabled: boolean // Added line
toggleAverage: () => void
toggleBestPossible: () => void // Added line
}
const SettingsContext = createContext<SettingsContextProps | undefined>(
undefined,
)
interface SettingsProviderProps {
children: ReactNode
}
export const SettingsProvider: React.FC<SettingsProviderProps> = ({
children,
}) => {
const [isAverageEnabled, setTextEnabled] = useState(false)
const [isBestPossibleEnabled, setBestPossibleEnabled] = useState(false)
const toggleAverage = () => {
setTextEnabled((prev) => !prev)
}
const toggleBestPossible = () => {
setBestPossibleEnabled((prev) => !prev)
}
return (
<SettingsContext.Provider
value={{
isAverageEnabled,
isBestPossibleEnabled,
toggleAverage,
toggleBestPossible,
}}
>
{children}
</SettingsContext.Provider>
)
}
export const useSettings = () => {
const context = useContext(SettingsContext)
if (!context) {
throw new Error("useSettings must be used within a SettingsProvider")
}
return context
}

BIN
src/app/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

View File

@ -0,0 +1,35 @@
"use client"
import axios from "axios"
import { useState, useEffect } from "react"
import { AxiosError } from "axios"
import { useRouter } from "next/navigation"
import { IsLoggedIn } from "@/lib/auth"
interface UserResponse {
user: string | null
error: AxiosError | null
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const [isSuccess, setIsSuccess] = useState<boolean>(false)
const router = useRouter()
useEffect(() => {
;(async () => {
if (!await IsLoggedIn()) {
router.push("/")
return
}
setIsSuccess(true)
})()
}, [router])
if (!isSuccess) {
}
return <main>{children}</main>
}

View File

@ -0,0 +1,13 @@
import SongCardList from "@/components/PlayerFavoritesList"
import NavigationBar from "@/components/shared/NavigationBar/NavigationBar"
const Ranking = () => {
return (
<div className="min-h-screen bg-background p-4 lg:p-2">
<NavigationBar />
<SongCardList />
</div>
)
}
export default Ranking

494
src/app/globals.css Normal file
View File

@ -0,0 +1,494 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.5rem;
}
.dark {
--buttontexthovercolor: 0, 0%, 100%;
--buttonhovercolor: 0, 0%, 12%;
--typographydropdown: 0, 0%, 68%;
--buttonbackgroundcolor: 0, 0%, 2%;
--background: 0, 0%, 0%;
--subsectionbackgroundcolor: 0, 0%, 8%;
--cardsectionbackgroundcolor: 0, 0%, 8%;
--characterbadgetypography: 0, 0%, 68%;
--scorecardbadge: 0, 0%, 12%;
--scorecardbadgetypography: 0, 0%, 68%;
--playertrophybadge: 0, 0%, 12%;
--playerteambadge: 0, 0%, 12%;
--playertrophytext: 0, 0%, 68%;
--heartcolorfave: 40, 70%, 78%;
--heartcolornormal: 3, 70%, 50%;
--segmentedtabactivetypography: 0, 0%, 100%;
--typography: 0, 0%, 68%;
--foreground: 0, 0%, 68%;
--ratingincrease: 135, 29%, 52%;
--paginationtypographyhover: 0, 0%, 100%;
--ratingsame: 163, 29%, 60%;
--ratingdecrease: 360, 75%, 40%;
--songjacketborder: 0, 0%, 2%;
--segmentedtabnotactive: 0, 0%, 12%;
--segmentedtabactive: 0, 0%, 7%;
--segmentedtabtext: 0, 0%, 68%;
--segmentedtabtexthover: 0, 0%, 100%;
--bestrecentratingcolor: 0, 55%, 45%;
--texthover: 220, 83%, 75%;
--basicbadgecolor: 0, 0%, 0%;
--advancedbadgecolor: 0, 0%, 0%;
--expertbadgecolor: 0, 55%, 45%;
--masterbadgecolor: 251, 55%, 45%;
--worldsendbadgecolor: 0, 0%, 0%;
--newbadgecolor: 323, 55%, 45%;
--paginationbackground: 0, 0%, 8%;
--scoretext: 48, 75%, 57%;
--justicecriticaltext: 48, 75%, 57%;
--justicetext: 30, 83%, 47%;
--attacktext: 135, 29%, 52%;
--misstext: 0, 0%, 68%;
}
.light {
--buttontexthovercolor: 0, 0%, 0%;
--buttonhovercolor: 210, 25%, 50%;
--typographydropdown: 210, 15%, 35%;
--buttonbackgroundcolor: 0, 0%, 98%;
--background: 0, 0%, 100%;
--paginationbackground: 0, 0%, 0%;
--subsectionbackgroundcolor: 0, 0%, 96%;
--cardsectionbackgroundcolor: 0, 0%, 96%;
--characterbadgetypography: 0, 0%, 0%;
--scorecardbadge: 210, 25%, 50%;
--scorecardbadgetypography: 0, 0%, 0%;
--playertrophybadge: 210, 25%, 50%;
--playerteambadge: 210, 25%, 50%;
--playertrophytext: 0, 0%, 0%;
--typography: 0, 0%, 0%;
--foreground: 210, 15%, 35%;
--ratingincrease: 135, 29%, 52%;
--paginationtypography: 0, 0%, 100%;
--paginationhover: 210, 25%, 50%;
--paginationtypographyhover: 210, 25%, 50%;
--ratingsame: 163, 29%, 60%;
--ratingdecrease: 360, 75%, 40%;
--songjacketborder: 233, 80%, 65%;
--heartcolorfave: 40, 70%, 78%;
--heartcolornormal: 3, 70%, 50%;
--segmentedtabnotactive: 210, 25%, 50%;
--segmentedtabactive: 210, 25%, 80%;
--segmentedtabtext: 0, 0%, 10%;
--segmentedtabtexthover: 0, 0%, 100%;
--texthover: 220, 83%, 75%;
--segmentedtabactivetypography: 0, 0%, 0%;
--bestrecentratingcolor: 343, 81%, 75%;
--basicbadgecolor: 135, 29%, 52%;
--advancedbadgecolor: 48, 75%, 57%;
--expertbadgecolor: 360, 50%, 70%;
--masterbadgecolor: 315, 22%, 51%;
--worldsendbadgecolor: 135, 29%, 52%;
--newbadgecolor: 342, 48%, 61%;
--scoretext: 48, 75%, 50%;
--justicecriticaltext: 48, 75%, 50%;
--justicetext: 30, 83%, 50%;
--attacktext: 135, 29%, 50%;
--misstext: 33, 15%, 50%;
}
.gruvbox {
--buttontexthovercolor: 30, 7%, 12%;
--buttonhovercolor: 48, 75%, 57%;
--typographydropdown: 48, 67%, 88%;
--buttonbackgroundcolor: 30, 5%, 20%;
--background: 30, 7%, 12%;
--subsectionbackgroundcolor: 30, 11%, 37%;
--cardsectionbackgroundcolor: 30, 11%, 37%;
--characterbadgetypography: 270, 5%, 18%;
--scorecardbadge: 48, 75%, 57%;
--scorecardbadgetypography: 270, 5%, 18%;
--paginationbackground: 0, 0%, 0%;
--playertrophybadge: 48, 75%, 57%;
--playerteambadge: 48, 75%, 57%;
--playertrophytext: 233, 23%, 15%;
--heartcolorfave: 40, 70%, 78%;
--heartcolornormal: 3, 70%, 50%;
--typography: 48, 67%, 88%;
--foreground: 48, 67%, 88%;
--ratingincrease: 135, 29%, 52%;
--ratingsame: 163, 29%, 60%;
--ratingdecrease: 360, 75%, 40%;
--songjacketborder: 30, 5%, 20%;
--paginationtypographyhover: 0, 0%, 100%;
--segmentedtabnotactive: 48, 75%, 57%;
--segmentedtabactive: 42, 76%, 44%;
--segmentedtabtext: 0, 0%, 0%;
--segmentedtabtexthover: 0, 0%, 100%;
--texthover: 220, 83%, 75%;
--segmentedtabactivetypography: 0, 0%, 100%;
--bestrecentratingcolor: 342, 48%, 61%;
--basicbadgecolor: 135, 29%, 52%;
--advancedbadgecolor: 48, 75%, 57%;
--expertbadgecolor: 360, 75%, 40%;
--masterbadgecolor: 315, 22%, 51%;
--worldsendbadgecolor: 135, 29%, 52%;
--newbadgecolor: 342, 48%, 61%;
--scoretext: 48, 75%, 57%;
--justicecriticaltext: 48, 75%, 57%;
--justicetext: 30, 83%, 47%;
--attacktext: 135, 29%, 52%;
--misstext: 33, 15%, 60%;
}
.mocha {
--buttonhovercolor: 40, 70%, 78%;
--buttontexthovercolor: 0, 0%, 0%;
--typographydropdown: 48, 67%, 88%;
--buttonbackgroundcolor: 30, 5%, 20%;
--background: 232, 23%, 18%;
--subsectionbackgroundcolor: 236, 23%, 12%;
--cardsectionbackgroundcolor: 236, 23%, 12%;
--characterbadgetypography: 227, 68%, 88%;
--scorecardbadge: 40, 70%, 78%;
--scorecardbadgetypography: 236, 23%, 12%;
--paginationbackground: 236, 23%, 12%;
--playertrophybadge: 40, 70%, 78%;
--playerteambadge: 233, 23%, 15%;
--playertrophytext: 233, 23%, 15%;
--heartcolorfave: 40, 70%, 78%;
--heartcolornormal: 3, 70%, 50%;
--typography: 227, 68%, 88%;
--foreground: 48, 67%, 88%;
--ratingincrease: 115, 54%, 76%;
--ratingsame: 220, 83%, 75%;
--ratingdecrease: 343, 81%, 75%;
--songjacketborder: 233, 23%, 15%;
--paginationtypographyhover: 0, 0%, 100%;
--paginationtypography: 227, 68%, 88%;
--segmentedtabnotactive: 236, 23%, 12%;
--segmentedtabactivetypography: 0, 0%, 0%;
--segmentedtabactive: 40, 70%, 78%;
--segmentedtabtext: 227, 68%, 88%;
--segmentedtabtexthover: 0, 0%, 0%;
--texthover: 220, 83%, 75%;
--bestrecentratingcolor: 343, 81%, 75%;
--basicbadgecolor: 115, 54%, 76%;
--advancedbadgecolor: 48, 75%, 57%;
--expertbadgecolor: 343, 81%, 75%;
--masterbadgecolor: 267, 83%, 80%;
--worldsendbadgecolor: 170, 57%, 73%;
--newbadgecolor: 343, 81%, 75%;
--scoretext: 40, 70%, 78%;
--justicecriticaltext: 40, 70%, 78%;
--justicetext: 11, 70%, 56%;
--attacktext: 105, 48%, 72%;
--misstext: 227, 68%, 88%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}
.icon-color {
fill: #a6da95;
}
img,
video {
max-width: revert;
height: revert;
}
.rainbow-text {
background: linear-gradient(
to left,
violet,
indigo,
blue,
green,
yellow,
orange,
red
);
-webkit-background-clip: text;
color: transparent;
background-clip: text;
color: transparent;
}
/*
* CHUSAN AVATAR
*/
.avatar_group {
width: 420px;
height: 330px;
margin: 0px auto 0px auto;
/* background-color: red; */
position: relative;
}
/* Mobile Styles */
@media screen and (max-width: 480px) {
.avatar_group {
width: 100%; /* Adjust width for mobile */
height: auto; /* Adjust height as needed */
/* You can also adjust other properties as needed */
}
}
/* アバター土台 */
.avatar_base {
width: 272px;
height: 330px;
padding: 10px 0 0 0;
position: relative;
margin: 0px auto 0px auto;
overflow: hidden;
}
/* 背景 */
.avatar_back {
display: block;
width: 272px;
height: 330px;
position: absolute;
top: 25px;
z-index: 100;
}
/* 右足 */
.avatar_skinfoot_r {
display: block;
width: 42px;
height: 52px;
position: absolute;
overflow: hidden;
z-index: 101;
top: 280px;
left: 84px;
}
.avatar_skinfoot_r img {
transform: translate(0px, -204px);
}
/* 左足 */
.avatar_skinfoot_l {
display: block;
width: 42px;
height: 52px;
position: absolute;
overflow: hidden;
z-index: 102;
top: 280px;
left: 147px;
}
.avatar_skinfoot_l img {
transform: translate(-42px, -204px);
}
/* デフォルト髪の毛 */
.avatar_hair {
display: block;
width: 46px;
height: 78px;
position: absolute;
overflow: hidden;
z-index: 103;
top: 21px;
left: 110px;
}
.avatar_hair img {
transform: translate(0px, 0px);
}
/* 体色 */
.avatar_skin {
display: block;
width: 128px;
height: 204px;
position: absolute;
overflow: hidden;
z-index: 104;
top: 93px;
left: 72px;
}
.avatar_skin img {
transform: translate(0px, 0px);
}
/* 衣装 */
.avatar_wear {
display: block;
width: 258px;
height: 218px;
position: absolute;
overflow: hidden;
z-index: 105;
top: 106px;
left: 7px;
}
.avatar_wear img {
transform: translate(0px, 0px);
}
/* 表情 */
.avatar_face {
display: block;
width: 58px;
height: 64px;
position: absolute;
overflow: hidden;
z-index: 106;
top: 100px;
left: 107px;
}
.avatar_face img {
transform: translate(0px, 0px);
}
/* 表情 */
.avatar_face_static {
display: block;
width: 58px;
height: 64px;
position: absolute;
overflow: hidden;
z-index: 106;
top: 100px;
left: 107px;
}
.avatar_face img {
transform: translate(0px, 0px);
}
/* フェイスカバー */
.avatar_face {
display: block;
width: 116px;
height: 104px;
position: absolute;
overflow: hidden;
z-index: 107;
top: 96px;
left: 78px;
}
.avatar_faceCover img {
transform: translate(0px, 0px);
}
/* 頭 */
.avatar_head {
display: block;
width: 200px;
height: 150px;
position: absolute;
overflow: hidden;
z-index: 108;
top: 28px;
left: 37px;
}
.avatar_head img {
transform: translate(0px, 0px);
}
/* 右手 */
.avatar_hand_r {
display: block;
width: 36px;
height: 72px;
position: absolute;
overflow: hidden;
z-index: 108;
top: 178px;
left: 52px;
}
.avatar_hand_r img {
transform: translate(0px, 0px);
}
/* 左手 */
.avatar_hand_l {
display: block;
width: 36px;
height: 72px;
position: absolute;
overflow: hidden;
z-index: 109;
top: 178px;
left: 184px;
}
.avatar_hand_l img {
transform: translate(0px, 0px);
}
/* 右アイテム */
.avatar_item_r {
display: block;
width: 100px;
height: 272px;
position: absolute;
overflow: hidden;
z-index: 109;
transform: rotate(-5deg);
top: 50px;
left: 9px;
}
.avatar_item_r img {
transform: translate(0px, 0px);
}
/* 左アイテム */
.avatar_item_l {
display: block;
width: 100px;
height: 272px;
position: absolute;
overflow: hidden;
z-index: 110;
transform: rotate(5deg);
top: 50px;
left: 163px;
}
.avatar_item_l img {
transform: translate(-100px, 0px);
}

38
src/app/layout.tsx Normal file
View File

@ -0,0 +1,38 @@
import type { Metadata } from "next"
import { Inter } from "next/font/google"
import "./globals.css"
import { ClientCookiesProvider } from "@/components/shared/cookieProvider/Page"
import { cookies } from "next/headers"
const inter = Inter({ subsets: ["latin"] })
import { SettingsProvider } from "./context/SettingsContext"
import { ThemeProvider } from "@/components/theme-provider"
export const metadata: Metadata = {
title: "Cozy Net",
description: "A Cozy ALL.net Emulation Frontend",
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
themes={["gruvbox", "light", "dark", "mocha"]}
disableTransitionOnChange
>
<SettingsProvider>
<ClientCookiesProvider value={cookies().getAll()}>
{children}
</ClientCookiesProvider>
</SettingsProvider>
</ThemeProvider>
</body>
</html>
)
}

View File

@ -0,0 +1,35 @@
"use client"
import axios from "axios"
import { useState, useEffect } from "react"
import { AxiosError } from "axios"
import { useRouter } from "next/navigation"
import { IsLoggedIn } from "@/lib/auth"
interface UserResponse {
user: string | null
error: AxiosError | null
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const [isSuccess, setIsSuccess] = useState<boolean>(false)
const router = useRouter()
useEffect(() => {
;(async () => {
if (!await IsLoggedIn()) {
router.push("/")
return
} // if error duid not happen everything is alright
setIsSuccess(true)
})()
}, [router])
if (!isSuccess) {
}
return <main>{children}</main>
}

View File

@ -0,0 +1,20 @@
import AimeCard from "@/components/PlayerViewCurrentAimeCard"
import UpdateCard from "@/components/PlayerAimeCardUpdater"
import NavigationBar from "@/components/shared/NavigationBar/NavigationBar"
import NewKeychipComponent from "@/components/PlayerWhiteListKeychips"
export default function Dashboard() {
return (
<div className="bg-back min-h-screen p-4 lg:p-2">
<NavigationBar />
<div className="flex flex-col items-center pt-4">
<UpdateCard />
<div className="pt-4">
<AimeCard />
</div>
<div className="pt-4">
<NewKeychipComponent />
</div>
</div>
</div>
)
}

201
src/app/page.tsx Normal file
View File

@ -0,0 +1,201 @@
"use client"
import { useRouter } from "next/navigation"
import { useState } from "react"
import axios from "axios"
import { toast, ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
import { FaSpinner } from "react-icons/fa"
import { z, ZodError } from "zod" // Import Zod for input validation
import Link from "next/link"
import { Input } from "@/components/ui/input"
import { ApiFetch } from "@/lib/api"
const loginSchema = z.object({
username: z.string().min(3).max(20),
password: z.string().min(6).max(30),
})
export default function Home() {
const { push } = useRouter()
const [loading, setLoading] = useState(false)
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
try {
const formValues = {
username: event.currentTarget.username.value,
password: event.currentTarget.password.value,
}
// Validate form data using Zod
loginSchema.parse(formValues)
setLoading(true)
const response = await ApiFetch(
"/SDHD/verifyUser",
{
method: "POST",
body: JSON.stringify(formValues),
headers: {
"Content-Type": "application/json",
},
},
false,
true,
)
if (response.statusCode !== 200) {
return;
}
toast.success("Login successful!", {
className: "success-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
})
push("/chunithm")
} catch (error) {
handleLoginError(error)
} finally {
setLoading(false)
}
}
function handleLoginError(error: any) {
if (error instanceof ZodError) {
// Zod validation error
toast.error("Invalid credentials, please try again.", {
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
})
} else if (axios.isAxiosError(error)) {
// Other Axios errors
if (error.response && error.response.status === 404) {
toast.error("Invalid credentials, please try again.", {
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
})
}
}
}
return (
<main>
<div className="flex min-h-screen items-center justify-center bg-background px-6">
<div className="w-full max-w-md">
<div className="overflow-hidden rounded-sm bg-subsectionbackgroundcolor">
<div className="px-10 py-8">
<div className="mb-8 text-center">
<h1 className="text-2xl font-semibold text-typography">
USER LOGIN
</h1>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<div>
<label className="mb-2 block text-sm font-medium text-typography">
Username
<Input
type="text"
id="username"
name="username"
placeholder="Username"
required
className="mt-2 w-full rounded-md border px-4 py-2 "
/>
</label>
</div>
<div>
<label className="mb-2 block text-sm font-medium text-typography ">
Password
<Input
type="password"
id="password"
name="password"
placeholder="********"
required
className="mt-2 w-full rounded-md border px-4 py-2 "
/>
</label>
</div>
<div className="flex items-center justify-end">
<Link
href="/"
className="start cursor-pointer text-sm font-bold uppercase text-typography hover:underline"
>
Forgot Password?
</Link>
</div>
<div className="px-10 py-4 text-center">
<button
type="submit"
className="relative cursor-pointer text-sm font-bold uppercase text-typography hover:underline"
disabled={loading}
>
{loading && (
<span className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 transform">
<FaSpinner className="animate-spin" />
</span>
)}
{!loading && "Login"}
</button>
</div>
</form>
</div>
<div className="px-10 py-4 text-center">
<Link
href="/signup"
className="cursor-pointer text-sm font-bold uppercase text-typography hover:underline"
>
Create account
</Link>
</div>
<ToastContainer
position="top-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
limit={3}
toastClassName="toast-class"
/>
</div>
</div>
</div>
<style jsx>{`
.toast-class {
background-color: #2a2a2a;
color: white;
border-radius: 8px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
.success-toast {
background-color: #4caf50;
}
.error-toast {
background-color: #f44336;
}
.toast-body {
font-size: 0.9rem;
padding: 15px;
}
`}</style>
</main>
)
}

View File

@ -0,0 +1,35 @@
"use client"
import axios from "axios"
import { useState, useEffect } from "react"
import { AxiosError } from "axios"
import { useRouter } from "next/navigation"
import { IsLoggedIn } from "@/lib/auth"
interface UserResponse {
user: string | null
error: AxiosError | null
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const [isSuccess, setIsSuccess] = useState<boolean>(false)
const router = useRouter()
useEffect(() => {
;(async () => {
if (!await IsLoggedIn()) {
router.push("/")
return
} // if error duid not happen everything is alright
setIsSuccess(true)
})()
}, [router])
if (!isSuccess) {
}
return <main>{children}</main>
}

14
src/app/settings/page.tsx Normal file
View File

@ -0,0 +1,14 @@
import SettingsPage from "@/components/PlayerSettings"
import NavigationBar from "@/components/shared/NavigationBar/NavigationBar"
export default function Settings() {
return (
<div className="min-h-screen bg-background p-2">
<NavigationBar />
<div className="mt-2 flex flex-col gap-4 md:gap-6 lg:flex-row lg:gap-8">
<div className="w-full lg:w-1/4 xl:w-1/5">
<SettingsPage />
</div>
</div>
</div>
)
}

251
src/app/signup/page.tsx Normal file
View File

@ -0,0 +1,251 @@
"use client"
import { useRouter } from "next/navigation"
import axios from "axios"
import Link from "next/link"
import { toast, ToastContainer } from "react-toastify"
import "react-toastify/dist/ReactToastify.css"
import { z, ZodError } from "zod" // Import Zod for input validation
import { Input } from "@/components/ui/input"
import { ApiFetch } from "@/lib/api"
// Define the Zod schema for signup form data
const signupSchema = z.object({
email: z.string().email(),
accessCode: z.string().min(20),
username: z.string().min(3).max(20),
password: z.string().min(6).max(30),
})
// InputField component
interface InputFieldProps {
id: string
type: string
name: string
labelText: string
placeholder: string
required: boolean
className?: string
}
const InputField: React.FC<InputFieldProps> = ({
id,
type,
name,
labelText,
placeholder,
required,
className,
}) => (
<div>
<label htmlFor={id} className="text-text mb-2 block text-sm font-medium">
{labelText}
</label>
<input
type={type}
id={id}
name={name}
placeholder={placeholder}
required={required}
className={`w-full rounded-md border px-4 py-2 ${className}`}
/>
</div>
)
// Signup component
export default function Signup() {
const { push } = useRouter() // Use push instead of replace
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault()
try {
const formValues = {
email: event.currentTarget.email.value,
accessCode: event.currentTarget.accessCode.value,
username: event.currentTarget.username.value,
password: event.currentTarget.password.value,
}
// Validate form data using Zod
signupSchema.parse(formValues);
const response = await ApiFetch(
"/SDHD/registerUser",
{
method: "POST",
body: JSON.stringify(formValues),
headers: {
"Content-Type": "application/json",
},
},
false,
true,
);
if (response.statusCode !== 200) {
return;
}
// Display success toast and redirect
toast.success("Signup successful!", {
className: "success-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
});
push("/chunithm")
} catch (error) {
handleSignUpError(error)
}
}
// Handle signup errors
function handleSignUpError(error: any) {
if (error instanceof ZodError) {
// Zod validation error
toast.error("Invalid signup data, please check the form.", {
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
})
} else if (axios.isAxiosError(error)) {
// Other Axios errors
if (error.response && error.response.status === 404) {
toast.error(
"Please play at least one credit before signing in to the web UI",
{
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
},
)
}
if (error.response && error.response.status === 500) {
toast.error("Username, email, or password already exists", {
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
})
}
if (error.response && error.response.status === 409) {
toast.error(
"Cannot change username, email, or password during sign-up or rival_code is already taken",
{
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
},
)
}
}
}
// Render the Signup form
return (
<main>
<div className="flex min-h-screen items-center justify-center bg-background px-6">
<div className="w-full max-w-md">
<div className="overflow-hidden rounded-sm bg-subsectionbackgroundcolor">
<div className="px-10 py-8">
<div className="mb-8 text-center">
<h1 className="text-2xl font-semibold text-typography">
USER SIGNUP
</h1>
</div>
<form onSubmit={handleSubmit} className="space-y-6">
<Input
id="email"
type="email"
name="email"
placeholder="Enter your email"
required={true}
className=""
/>
<Input
id="accessCode"
type="text"
name="accessCode"
placeholder="Enter your access code"
required={true}
className=""
/>
<Input
id="username"
type="text"
name="username"
placeholder="Enter your username"
required={true}
className=""
/>
<Input
id="password"
type="password"
name="password"
placeholder="Enter your password"
required={true}
className=""
/>
<div className="px-10 py-4 text-center">
<button
type="submit"
className="text-text cursor-pointer text-sm font-bold uppercase hover:underline"
>
Sign up
</button>
</div>
</form>
</div>
<div className="px-10 py-4 text-center">
<Link
href="/"
className="text-text start cursor-pointer text-sm font-bold uppercase hover:underline"
>
Already have an account? Login
</Link>
</div>
<ToastContainer
position="top-center"
autoClose={5000}
hideProgressBar={false}
newestOnTop={false}
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
pauseOnHover
limit={3}
toastClassName="toast-class"
/>
</div>
</div>
</div>
<style jsx>{`
.toast-class {
background-color: #2a2a2a;
color: white;
border-radius: 8px;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.1);
}
.success-toast {
background-color: #4caf50;
}
.error-toast {
background-color: #f44336;
}
.toast-body {
font-size: 0.9rem;
padding: 15px;
}
`}</style>
</main>
)
}

View File

@ -0,0 +1,35 @@
"use client"
import axios from "axios"
import { useState, useEffect } from "react"
import { AxiosError } from "axios"
import { useRouter } from "next/navigation"
import { IsLoggedIn } from "@/lib/auth"
interface UserResponse {
user: string | null
error: AxiosError | null
}
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
const [isSuccess, setIsSuccess] = useState<boolean>(false)
const router = useRouter()
useEffect(() => {
;(async () => {
if (!await IsLoggedIn()) {
router.push("/")
return
}
setIsSuccess(true)
})()
}, [router])
if (!isSuccess) {
}
return <main>{children}</main>
}

58
src/app/userbox/page.tsx Normal file
View File

@ -0,0 +1,58 @@
import AvatarCustomizationData from "@/components/CharacterCustomization"
import PlayerMapIconComponent from "@/components/PlayerMapIcon"
import SystemVoiceComponent from "@/components/PlayerSystemVoice"
import PlayerTeamComponent from "@/components/PlayerTeams"
import NavigationBar from "@/components/shared/NavigationBar/NavigationBar"
import PlayerTrophyComponent from "@/components/PlayerTrophy"
import PlayerNamePlateComponent from "@/components/PlayerNameplate"
import RivalComponent from "@/components/PlayerRivals"
import { FaHeart } from "react-icons/fa" // Import heart icon from react-icons library
import Link from "next/link" // Import Link from next/link
import CharacterCard from "@/components/CharacterCard"
const UserBox = () => {
return (
<div className="min-h-screen bg-background p-4 lg:p-2">
<NavigationBar />
<div className="dark:bg-base bg-lightbackgroundmin-h-screen flex flex-col items-center ">
<div className="">
<div className="mt-4 w-full">
<AvatarCustomizationData />
</div>
<div className="mt-4 w-full">
<PlayerNamePlateComponent />
</div>
<div className="mt-4 w-full">
<PlayerTrophyComponent />
</div>
<div className="mt-4 w-full">
<SystemVoiceComponent />
</div>
<div className="mt-4 w-full">
<PlayerMapIconComponent />
</div>
<div className="mt-4 w-full">
<PlayerTeamComponent />
</div>
<div className="mt-4 w-full">
<RivalComponent />
</div>
<Link href="/favorites">
{" "}
{/* Replace '/favorites' with the actual path of your favorites page */}
<div className="hover:bg-color13 dark:hover:bg-mantle mt-4 flex h-[120px] w-full cursor-pointer flex-col items-center justify-center rounded-sm bg-subsectionbackgroundcolor p-4">
<FaHeart
className="mr-2 text-heartcolornormal"
style={{ fontSize: "5em" }}
/>
<span className="mt-2 text-sm font-bold text-typography ">
FAVORITES
</span>
</div>
</Link>
</div>
</div>
</div>
)
}
export default UserBox

View File

@ -0,0 +1,168 @@
// Import necessary libraries and components
"use client"
import React, { useState } from "react"
import Image from "next/image"
import { UsePlayerInfo } from "@/app/axios/useFetchApi"
interface CharacterImageProps {
characterId: number
}
const CharacterImage = ({ characterId }: CharacterImageProps) => {
let calculatedId = Math.floor(characterId / 10)
const calculatedIdStr = calculatedId.toString()
if (calculatedIdStr.length > 4) {
calculatedId = parseInt(calculatedIdStr.slice(0, -1))
}
const paddedId = calculatedId.toString().padStart(4, "0")
const baseDirectory = `${process.env.NEXT_PUBLIC_CDN}chusan_partners/CHU_UI_Character_${paddedId}_00_02.png`
const fallbackDirectory = `${process.env.NEXT_PUBLIC_CDN}chunithm_partners/CHU_UI_Character_${paddedId}_00_02.png`
const [imageUrl, setImageUrl] = useState(baseDirectory)
const [attemptedFallback, setAttemptedFallback] = useState(false)
const handleError = () => {
if (!attemptedFallback) {
setImageUrl(fallbackDirectory)
setAttemptedFallback(true)
}
}
return (
<Image
src={imageUrl}
onError={handleError}
alt="Character Image"
width={64}
height={64}
className="absolute left-2 top-2 h-10 w-10 border-4 border-songjacketborder md:left-6 md:top-4 md:h-20 md:w-20"
/>
)
}
// TeamInfo component
type TeamInfoProps = {
teamName?: string
}
const TeamInfo = ({ teamName }: TeamInfoProps) => (
<div className="font-semi absolute left-24 top-2 mt-1 rounded-sm bg-playerteambadge p-1 text-xs font-semibold uppercase text-characterbadgetypography md:left-28 md:top-6 md:mt-2 md:p-3">
{teamName ? `Belongs to team ${teamName}` : "You do not belong to a team."}
</div>
)
// CharacterCard component
const CharacterCard = () => {
// State and effect hooks for managing player info
const [profileData, setPlayerInfo] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
// Fetch player info using custom hook
UsePlayerInfo({ setPlayerInfo, setLoading, setError })
const isVersion15 = profileData.some(
(user) => user.version === 15 || user.version === 13,
)
if (!Array.isArray(profileData) || profileData.length === 0 || !isVersion15) {
return null // Don't render the card if version is not 15
}
// get the highest version of a users profile
const highestVersion = Math.max(...profileData.map((user) => user.version))
// Filter the profileData to include only users with the highest version
const versionUserData = profileData.filter(
(user) => user.version === highestVersion,
)
return (
<div className="md:mx-auto md:px-0">
{versionUserData.map((user, index) => {
// Destructure user data
const {
trophyname,
level,
userName,
playerRating,
highestRating,
overPowerRate,
lastPlayDate,
characterId,
reincarnationNum,
teamName,
} = user
return (
<div
key={index}
className="relative mb-2 w-[395px] rounded-sm bg-cardsectionbackgroundcolor p-2 sm:w-[640px] md:w-[840px] md:p-6 lg:w-[690px] xl:w-[790px] 2xl:w-[1390px] "
>
<div className="flex items-center">
<CharacterImage characterId={characterId} />
</div>
<TeamInfo teamName={teamName} />
<div className="pt-15 md:pt-20">
<div className="mb-2 ml-1 mt-2 text-xs uppercase md:-ml-1 md:text-sm">
<span className="rounded-sm bg-playertrophybadge px-2 py-1 font-semibold text-playertrophytext">
{trophyname || "New comer"}
</span>
</div>
<div className="mb-2 md:mb-2">
<span className="text-xs uppercase text-typography">
Lv.
{reincarnationNum > 0 && (
<span className="text-xs uppercase text-typography">
<span className="text-lg font-bold">
{" "}
{reincarnationNum}
{level}
</span>
</span>
)}
{reincarnationNum < 1 && (
<span className="text-lg font-bold"> {level}</span>
)}
</span>
<span className="ml-2 font-bold text-typography md:ml-2 md:text-lg">
{userName}
</span>
</div>
<div className="mb-2 md:mb-2">
<span className="text-xs uppercase text-typography">
Rating
</span>
<div className="flex items-center">
<span className="mr-1 font-bold text-typography">
{(playerRating / 100).toFixed(2)}
</span>
<span className="ml-1 text-xs font-bold text-typography">
(max): {(highestRating / 100).toFixed(2)}
</span>
</div>
</div>
<div className="mb-2 md:mb-2">
<span className="text-xs uppercase text-typography">
Over Power
</span>
<div className="flex items-center text-typography">
<span className="mr-1 font-bold text-typography">
{(overPowerRate / 100).toFixed(2)} %
</span>
</div>
</div>
<div className="text-xs text-typography">
Last play date{" "}
<span className="font-bold">
{lastPlayDate || "Not available"}
</span>
</div>
</div>
</div>
)
})}
</div>
)
}
export default CharacterCard

View File

@ -0,0 +1,229 @@
/* eslint-disable @next/next/no-img-element */
"use client"
import { useState } from "react"
import { UsePlayerAvatarItems, UsePlayerInfo } from "@/app/axios/useFetchApi"
import { UsePostAvatarItemList } from "@/app/axios/usePostApi"
type PlayerData = {
avatarBack: string
avatarItem: string
avatarWear: string
avatarFront: string
avatarSkin: string
avatarHead: string
avatar_skinfoot_l: string
avatar_skinfoot_r: string
avatarFace: string
}
type AvatarParts = Record<string, string>
type DropdownOption = {
label: string
value: string
category: string
sortName: string
}
type DropdownProps = {
options: DropdownOption[]
onChange: (value: string) => void
label: string
value: string
}
const Dropdown: React.FC<DropdownProps & { className?: string }> = ({
options,
onChange,
label,
value,
className,
}) => {
return (
<div className={`dropdown-container relative ${className}`}>
<label className="block text-sm font-medium text-typography pb-1 pt-2 ">
{label}
</label>
<div className="mt-1 relative">
<select
value={value}
onChange={(e) => onChange(e.target.value)}
className=" w-[230px] sm:w-[280px] md:w-[280px] lg:w-[280px] text-typography px-3 py-2 border border-transparent font-medium rounded-sm bg-background focus:outline-none transition duration-150 ease-in-out whitespace-nowrap"
>
{options.map((option, index) => (
<option
key={`${option.value}-${index}`}
value={option.value}
className="text-gray-900"
>
{option.label}
</option>
))}
</select>
</div>
</div>
)
}
const AvatarCustomizationData: React.FC = () => {
const [avatarCustomizationData, setPlayerInfo] = useState<PlayerData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [avatarItems, setAvatarItems] = useState<PlayerData[]>([])
UsePlayerAvatarItems({
setPlayerAvatarItems: setAvatarItems,
setLoading,
setError,
})
UsePlayerInfo({ setPlayerInfo, setLoading, setError })
const { postAvatarData } = UsePostAvatarItemList({
onPostSuccess: (data) => {
console.log("Data posted successfully:", data)
},
setLoading,
setError,
})
const getAvatarParts = (playerData: PlayerData): AvatarParts => ({
avatar_back: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarBack}.png`,
avatar_face_static: `${process.env.NEXT_PUBLIC_CDN}avatarStatic/CHU_UI_Avatar_Tex_Face.png`,
avatar_face: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarFace}.png`,
avatar_item_r: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarItem}.png`,
avatar_item_l: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarItem}.png`,
avatar_wear: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarWear}.png`,
avatar_Front: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarFront}.png`,
avatar_skin: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarSkin}.png`,
avatar_head: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarHead}.png`,
avatar_skinfoot_l: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarSkin}.png`,
avatar_skinfoot_r: `${process.env.NEXT_PUBLIC_CDN}avatarAccessory/CHU_UI_Avatar_Tex_0${playerData.avatarSkin}.png`,
avatar_hand_r: `${process.env.NEXT_PUBLIC_CDN}avatarStatic/CHU_UI_Avatar_Tex_RightHand.png`,
avatar_hand_l: `${process.env.NEXT_PUBLIC_CDN}avatarStatic/CHU_UI_Avatar_Tex_LeftHand.png`,
})
const handleSelectionChange = (part: keyof PlayerData, value: string) => {
console.log(`Setting ${part} to ${value}`)
setPlayerInfo(
avatarCustomizationData.map((data, index) =>
index === 0 ? { ...data, [part]: value } : data,
),
)
}
const handlePostData = () => {
if (avatarCustomizationData.length > 0) {
const { avatarItem, avatarBack, avatarFace, avatarWear, avatarHead } =
avatarCustomizationData[0]
const dataToPost = {
avatarItem,
avatarBack,
avatarFace,
avatarWear,
avatarHead,
}
postAvatarData(dataToPost)
console.log("Attempting to post data", dataToPost)
}
}
if (loading) return <div></div>
if (error) return <div>Error: {error}</div>
if (
!Array.isArray(avatarCustomizationData) ||
avatarCustomizationData.length === 0
) {
return <div>No player data found.</div>
}
const playerCustomization = getAvatarParts(avatarCustomizationData[0])
const dropdownOptions: DropdownOption[] = avatarItems.map((item: any) => ({
label: item.str,
value: item.item_id.toString(), // Use item_id as the value
category: item.category,
sortName: item.sortName,
}))
const avatarHeadOptions = dropdownOptions.filter(
(option) => option.category === "2",
)
const avatarFaceCoverOptions = dropdownOptions.filter(
(option) => option.category === "3",
)
const avatarItemOptions = dropdownOptions.filter(
(option) => option.category === "5",
)
const avatarBackOptions = dropdownOptions.filter(
(option) => option.category === "7",
)
const avatarWearOptions = dropdownOptions.filter(
(option) => option.category === "1",
)
return (
<div className="bg-subsectionbackgroundcolor rounded-sm mx-auto">
<div className="flex flex-col md:flex-row justify-center items-center">
<div className="avatar_group w-full md:w-1/2 flex justify-center items-center">
<div className="avatar_base">
{Object.entries(playerCustomization).map(([className, src]) => (
<div className={className} key={className}>
<img src={src} alt={className} />
</div>
))}
</div>
</div>
<div className="avatar_customization_dropdowns w-full md:w-1/2 flex flex-col justify-center items-center">
<Dropdown
options={avatarHeadOptions}
onChange={(value) => handleSelectionChange("avatarHead", value)}
label="Head Options: "
value={avatarCustomizationData[0].avatarHead}
/>
<Dropdown
options={avatarFaceCoverOptions}
onChange={(value) => handleSelectionChange("avatarFace", value)}
label="Face Cover Options: "
value={avatarCustomizationData[0].avatarFace}
/>
<Dropdown
options={avatarItemOptions}
onChange={(value) => handleSelectionChange("avatarItem", value)}
label="Item Options: "
value={avatarCustomizationData[0].avatarItem}
/>
<Dropdown
options={avatarBackOptions}
onChange={(value) => handleSelectionChange("avatarBack", value)}
label="Back Options: "
value={avatarCustomizationData[0].avatarBack}
/>
<Dropdown
options={avatarWearOptions}
onChange={(value) => handleSelectionChange("avatarWear", value)}
label="Outfit Options: "
value={avatarCustomizationData[0].avatarWear}
/>
<div className="pt-4">
<button
onClick={handlePostData}
className="text-typography px-3 py-2 mb-3 border border-transparent font-medium rounded-sm bg-buttonbackgroundcolor hover:text-buttontexthovercolor hover:bg-buttonhovercolor focus:outline-none transition duration-150 ease-in-out whitespace-nowrap"
>
Update Avatar
</button>
</div>
</div>
</div>
</div>
)
}
export default AvatarCustomizationData

View File

@ -0,0 +1,92 @@
"use client"
import { useState, ChangeEvent } from "react"
import { UsePostNewCard } from "@/app/axios/usePostApi"
import { Input } from "./ui/input"
const UpdateCard = () => {
const [newAccessCode, setNewAccessCode] = useState("")
const [error, setError] = useState("")
const { postAccessCode } = UsePostNewCard({
onPostSuccess: (data) => {},
setLoading: (loading) => {},
setError: (err) => {
setError("Failed to update access code. Please try again.")
},
})
const handleUpdateCard = async () => {
if (!newAccessCode) {
setError("Please enter a new access code.")
return
}
if (newAccessCode.length !== 20) {
setError("The access code must have exactly 20 digits.")
return
}
setError("")
try {
console.log("Attempting to update access code:", newAccessCode)
await postAccessCode({ access_code: newAccessCode })
setNewAccessCode("")
} catch (error) {}
}
const handleGenerateAccessCode = () => {
const randomCode = Array.from({ length: 20 }, () =>
Math.floor(Math.random() * 10),
)
setNewAccessCode(randomCode.join(""))
}
const handleInputChange = (e: ChangeEvent<HTMLInputElement>) => {
let numericValue = e.target.value.replace(/\D/g, "")
numericValue = numericValue.slice(0, 20)
setNewAccessCode(numericValue)
}
return (
<div className="flex items-center justify-center">
<div className="relative flex w-[390px] flex-col rounded-sm bg-subsectionbackgroundcolor p-2 pb-4">
<label className="mb-2 block pb-1 pt-1 text-sm font-bold text-typography">
Change Aime Card - Must be 20 digits long
</label>
<div className=" flex-grow">
<Input
type="text"
name="newAccessCode"
placeholder="New Aime Card"
value={newAccessCode}
onChange={handleInputChange}
maxLength={20}
className="w-[370px] rounded-md border border-gray-300 p-2 font-mono focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
{error && <p className="px-2 py-1 text-red-500">{error}</p>}
<div className="mt-2 flex justify-between">
<div className="flex">
<button
onClick={handleGenerateAccessCode}
className="hover:bg-red mb-3 mr-2 whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Generate Card
</button>
<button
onClick={handleUpdateCard}
className="hover:bg-red mb-3 whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Change Card
</button>
</div>
</div>
</div>
</div>
)
}
export default UpdateCard

View File

@ -0,0 +1,228 @@
"use client"
import React, { useEffect, useState } from "react"
import Image from "next/image"
import {
useFetchStaticMusic,
usePlayerFavoriteSongsList,
} from "@/app/axios/useFetchApi"
import { FaHeart } from "react-icons/fa" // Import heart icon from react-icons library
import { UsePostFavoriteSongs } from "@/app/axios/usePostApi"
import { Input } from "./ui/input"
import { JacketImageProps, songCardProps } from "@/lib/types"
const JacketImage: React.FC<JacketImageProps> = ({ jacketPath }) => (
<Image
src={jacketPath}
alt="Character Image"
width={90}
height={90}
className=" border-sm border-4 border-songjacketborder"
/>
)
const SongCard: React.FC<songCardProps> = ({
title,
artist,
songId,
jacketPath,
}) => {
const { playerFavoriteSongs, refetchPlayerFavoriteSongs } =
usePlayerFavoriteSongsList()
const [loading, setLoading] = useState<boolean>(false)
const [error, setError] = useState<string | null>(null)
const isFavorite = playerFavoriteSongs.some(
(favSong) => favSong.songId === songId
)
const { postFavoriteSong } = UsePostFavoriteSongs({
onPostSuccess: (data) => {
console.log("Favorite song posted successfully:", data)
// Refetch player's favorite songs after a successful post
refetchPlayerFavoriteSongs()
},
setLoading,
setError,
})
const handleFavoriteClick = () => {
const favoriteSongData = {
songId,
}
postFavoriteSong(favoriteSongData)
}
const formattedJacketPath = `${process.env.NEXT_PUBLIC_CDN
}JacketArt/${jacketPath.slice(0, -3)}png`
return (
<div className="flex items-center">
<div className="flex-shrink-0">
<JacketImage jacketPath={formattedJacketPath} />
</div>
<div className="ml-4">
<span className="block font-semibold text-typography dark:text-typography">
{title}
</span>
<span className="block text-typography dark:text-typography">
{artist}
</span>
</div>
<button className="ml-auto p-2" onClick={handleFavoriteClick}>
<FaHeart
className={`${isFavorite ? "text-heartcolornormal" : "text-heartcolorfave"
}`}
style={{ fontSize: "2em" }}
/>
</button>
</div>
)
}
interface NavigationButtonsProps {
currentPage: number
totalPages: number
onPageChange: (page: number) => void
onNext: () => void
onPrevious: () => void
}
// NavigationButtons Component
const NavigationButtons: React.FC<NavigationButtonsProps> = ({
currentPage,
totalPages,
onPageChange,
onNext,
onPrevious,
}) => {
// Only show a maximum of 5 page buttons around the current page
const rangeStart = 5 * Math.floor((currentPage - 1) / 5) + 1
const rangeEnd = Math.min(rangeStart + 4, totalPages)
const pages = Array.from(
{ length: rangeEnd - rangeStart + 1 },
(_, index) => rangeStart + index
)
return (
<div className="flex items-center justify-center space-x-1 ">
<button
onClick={onPrevious}
disabled={currentPage === 1}
className="rounded bg-paginationbackground px-4 font-bold text-paginationtypography hover:text-paginationtypographyhover disabled:opacity-40"
>
Previous
</button>
{pages.map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
disabled={currentPage === page}
className={`rounded bg-paginationbackground px-2 font-bold text-paginationtypography hover:text-paginationtypographyhover ${currentPage === page ? " " : ""
}`}
>
{page}
</button>
))}
<button
onClick={onNext}
disabled={currentPage === totalPages}
className="rounded bg-paginationbackground px-4 font-bold text-paginationtypography hover:text-paginationtypographyhover disabled:opacity-40"
>
Next
</button>
</div>
)
}
const SongCardList = () => {
const [profileData, setPlayerStaticMusic] = useState<any[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const [itemsPerPage] = useState<number>(7)
const [currentPage, setCurrentPage] = useState<number>(1)
const [searchQuery, setSearchQuery] = useState("")
const [filteredData, setFilteredData] = useState<any[]>([])
const { playerFavoriteSongs } = usePlayerFavoriteSongsList() // Use the hook
useFetchStaticMusic({ setPlayerStaticMusic, setLoading, setError })
useEffect(() => {
const filtered = profileData.filter((item) => {
const queryLower = searchQuery.toLowerCase()
return item.title && item.title.toLowerCase().includes(queryLower)
})
setFilteredData(filtered)
}, [searchQuery, profileData])
const totalPages = Math.ceil(filteredData.length / itemsPerPage)
const indexLastItem = currentPage * itemsPerPage
const indexFirstItem = indexLastItem - itemsPerPage
const currentItems = filteredData.slice(indexFirstItem, indexLastItem)
const handleNext = () => {
setCurrentPage((prevCurrentPage) =>
Math.min(prevCurrentPage + 1, totalPages)
)
}
const handlePrevious = () => {
setCurrentPage((prevCurrentPage) => Math.max(prevCurrentPage - 1, 1))
}
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
if (loading) {
return <div>Loading...</div>
}
if (error) {
return <div>Error: {error}</div>
}
return (
<div className="mx-auto w-full sm:w-[650px] md:w-[850px] lg:w-[700px] xl:w-[800px] 2xl:w-[1400px]">
<div className="w my-2">
<Input
type="text"
placeholder="Search by title"
className="w-full border-b border-gray-300 bg-transparent text-typography transition duration-300 ease-in-out focus:outline-none dark:text-typography"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="sm:w-[650px] md:w-[850px] lg:w-[700px] xl:w-[800px] 2xl:w-[1400px]">
{currentItems.map((songCardInfo, index) => (
<div
key={index}
className="mb-2 rounded-sm bg-subsectionbackgroundcolor p-2 shadow-lg"
>
<SongCard
{...songCardInfo}
isFavorite={playerFavoriteSongs.some(
(favSong) => favSong.songId === songCardInfo.songId
)}
/>
</div>
))}
</div>
<div className="flex justify-around ">
<div className="rounded-md py-2">
<NavigationButtons
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
onNext={handleNext}
onPrevious={handlePrevious}
/>
</div>
</div>
</div>
)
}
export default SongCardList

View File

@ -0,0 +1,182 @@
/* eslint-disable @next/next/no-img-element */
"use client"
import React, { useEffect, useState } from "react"
import {
UsePlayerAvailableMapIcons,
UsePlayerMapIcon,
} from "@/app/axios/useFetchApi"
import { UsePostMapIcon } from "@/app/axios/usePostApi"
type PlayerData = {
mapIconId: string
}
type playerMapIcon = {
itemId: string
str: string
imagePath: string
}
type DropdownOption = {
str: string
itemId: string
imagePath: string
}
type DropdownProps = {
options: DropdownOption[]
onChange: (value: string) => void
str: string
id: string
}
const DEFAULT_IMAGE = `${process.env.NEXT_PUBLIC_CDN}mapIcon/CHU_UI_MapIcon_00000001.png`
const Dropdown: React.FC<DropdownProps & { className?: string }> = ({
options,
onChange,
str,
id,
className,
}) => {
return (
<div className={`dropdown-container relative ${className}`}>
<label className="block pb-1 pt-2 text-sm font-medium text-typography ">
{str}
</label>
<div className="relative mt-1">
<select
value={id}
onChange={(e) => {
// console.log("Dropdown value changed:", e.target.value);
onChange(e.target.value)
}}
className=" w-[230px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out focus:outline-none sm:w-[280px] md:w-[280px] lg:w-[280px]"
>
{options.map((option, index) => (
<option
key={`${option.itemId}-${index}`}
value={option.itemId}
className=" dark:text-white"
>
{option.str}
</option>
))}
</select>
</div>
</div>
)
}
const PlayerMapIconComponent: React.FC = () => {
const [playerData, setPlayerData] = useState<PlayerData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [mapIconData, setMapIconData] = useState<playerMapIcon[]>([])
const [selectedNameplateId, setSelectedItemId] = useState<string>("")
const [playerInfoLoaded, setPlayerInfoLoaded] = useState(false)
UsePlayerMapIcon({
setPlayerMapIcon: setPlayerData,
setLoading,
setError,
})
UsePlayerAvailableMapIcons({
setPlayerAvailableMapIcons: setMapIconData,
setLoading,
setError,
onSuccess: (data) => {
// Handle success if needed
},
})
useEffect(() => {
if (playerData.length > 0 && !playerInfoLoaded) {
const initialMapIconId = playerData[0].mapIconId
setSelectedItemId(initialMapIconId)
setPlayerInfoLoaded(true)
}
}, [playerData, playerInfoLoaded])
const { postMapIcon } = UsePostMapIcon({
onPostSuccess: (data) => {
console.log("Data posted successfully:", data)
},
setLoading,
setError,
})
const handleSelectionChange = (part: keyof PlayerData, mapIconId: string) => {
if (part === "mapIconId") {
setSelectedItemId(mapIconId)
console.log("Selected itemId:", mapIconId)
}
setPlayerData(
playerData.map((data, index) =>
index === 0 ? { ...data, [part]: mapIconId } : data,
),
)
}
const handlePostData = () => {
if (playerData.length > 0) {
const { mapIconId } = playerData[0]
const dataToPost = {
mapIconId: selectedNameplateId,
}
postMapIcon(dataToPost)
console.log("Attempting to post data", dataToPost)
}
}
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!Array.isArray(playerData) || playerData.length === 0) {
return <div>No player data found.</div>
}
return (
<div className="items-center justify-center rounded-sm bg-subsectionbackgroundcolor pl-4 pt-4 md:flex-row">
{selectedNameplateId && (
<img
className="h-[140px] w-[140px]"
src={`${
process.env.NEXT_PUBLIC_CDN
}mapIcon/CHU_UI_MapIcon_${selectedNameplateId
.toString()
.padStart(8, "0")}.png`}
alt="itemID"
onError={(e) => {
console.error("Error loading image:", e)
e.currentTarget.src = DEFAULT_IMAGE
}}
onLoad={() => {}}
/>
)}
<div className="flex w-full flex-col items-start md:w-auto">
<Dropdown
options={mapIconData.map((data) => ({
itemId: data.itemId,
str: data.str,
imagePath: data.imagePath,
}))}
onChange={(value) => handleSelectionChange("mapIconId", value)}
str="Map Icon Options: "
id={selectedNameplateId}
/>
<div className="mt-4 flex items-center">
<button
onClick={handlePostData}
className="mb-3 whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Update Map Icon
</button>
</div>
</div>
</div>
)
}
export default PlayerMapIconComponent

View File

@ -0,0 +1,160 @@
/* eslint-disable @next/next/no-img-element */
"use client"
import React, { useEffect, useState } from "react"
import { UsePostNamePlate } from "@/app/axios/usePostApi"
import { UsePlayerAvailableNameplates } from "@/app/axios/useFetchApi"
import { UsePlayerNamePlates } from "@/app/axios/useFetchApi"
import { DropdowNameplateProps, DropdownNameplateOption, Nameplate, PlayerNameplateData } from "@/lib/types"
const DEFAULT_IMAGE = `${process.env.NEXT_PUBLIC_CDN}namePlates/CHU_UI_NamePlate_00000001.png`
const Dropdown: React.FC<DropdowNameplateProps & { className?: string }> = ({
options,
onChange,
str,
id,
className,
}) => {
return (
<div className={`dropdown-container relative ${className}`}>
<label className="block pb-1 pt-2 text-sm font-medium text-typography ">
{str}
</label>
<div className="relative mt-1">
<select
value={id}
onChange={(e) => {
onChange(e.target.value)
}}
className=" w-[230px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out focus:outline-none sm:w-[280px] md:w-[280px] lg:w-[280px]"
>
{options.map((option, index) => (
<option
key={`${option.nameplateId}-${index}`}
value={option.nameplateId}
className=" dark:text-white"
>
{option.str}
</option>
))}
</select>
</div>
</div>
)
}
const PlayerNamePlateComponent: React.FC = () => {
const [playerData, setPlayerData] = useState<PlayerNameplateData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [namePlateData, setNamePlateData] = useState<Nameplate[]>([])
const [selectedNameplateId, setSelectedNameplateId] = useState<string>("")
const [playerInfoLoaded, setPlayerInfoLoaded] = useState(false)
UsePlayerNamePlates({
setPlayerNameplate: setPlayerData,
setLoading,
setError,
})
UsePlayerAvailableNameplates({
setPlayerAvailableNameplates: setNamePlateData,
setLoading,
setError,
onSuccess: (data) => {
},
})
useEffect(() => {
if (playerData.length > 0 && !playerInfoLoaded) {
const initialNameplateId = playerData[0].nameplateId
setSelectedNameplateId(initialNameplateId)
setPlayerInfoLoaded(true)
}
}, [playerData, playerInfoLoaded])
const { postNamePlate } = UsePostNamePlate({
onPostSuccess: (data) => {
console.log("Data posted successfully:", data)
},
setLoading,
setError,
})
const handleSelectionChange = (
part: keyof PlayerNameplateData,
nameplateId: string,
) => {
if (part === "nameplateId") {
setSelectedNameplateId(nameplateId)
console.log("Selected nameplateId:", nameplateId)
}
setPlayerData(
playerData.map((data, index) =>
index === 0 ? { ...data, [part]: nameplateId } : data,
),
)
}
const handlePostData = () => {
if (playerData.length > 0) {
const dataToPost = {
nameplateId: selectedNameplateId,
}
postNamePlate(dataToPost)
console.log("Attempting to post data", dataToPost)
}
}
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!Array.isArray(playerData) || playerData.length === 0) {
return <div>No player data found.</div>
}
return (
<div className="items-center justify-center rounded-sm bg-subsectionbackgroundcolor pl-4 pt-4 md:flex-row">
{selectedNameplateId && (
<img
className="w-[300px]"
src={`${process.env.NEXT_PUBLIC_CDN
}namePlates/CHU_UI_NamePlate_${selectedNameplateId
.toString()
.padStart(8, "0")}.png`}
alt="Nameplate"
onError={(e) => {
console.error("Error loading image:", e)
e.currentTarget.src = DEFAULT_IMAGE
}}
onLoad={() => { }}
/>
)}
<div className="flex w-full flex-col items-start md:w-auto">
<Dropdown
options={namePlateData.map((data) => ({
str: data.str,
nameplateId: data.nameplateId,
imagePath: data.imagePath,
}))}
onChange={(value) => handleSelectionChange("nameplateId", value)}
str="Nameplate Options: "
id={selectedNameplateId}
/>
<div className="mt-4 flex items-center">
<button
onClick={handlePostData}
className="mb-3 whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Update Nameplate
</button>
</div>
</div>
</div>
)
}
export default PlayerNamePlateComponent

View File

@ -0,0 +1,80 @@
"use client"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
import * as z from "zod"
import { Button } from "@/components/ui/button"
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form"
import { Input } from "@/components/ui/input"
import { toast } from "@/components/ui/use-toast"
const FormSchema = z.object({
password: z.string().min(8, {
message: "Username must be at least 2 characters.",
}),
})
export function PasswordResetForm() {
const form = useForm<z.infer<typeof FormSchema>>({
resolver: zodResolver(FormSchema),
defaultValues: {
password: "",
},
})
function onSubmit(data: z.infer<typeof FormSchema>) {
toast({
title: "You submitted the following values:",
description: (
<pre className="mt-2 w-[340px] rounded-md bg-slate-950 p-4">
<code className="text-white">{JSON.stringify(data, null, 2)}</code>
</pre>
),
})
}
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="w-2/3 space-y-4 pl-4"
>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Reset password</FormLabel>
<FormDescription className="dark:text-text">
Enter a new password
</FormDescription>
<FormControl>
<Input
placeholder="New Password..."
value={field.value.replace(/./g, "*")} // Replace each character with *
onChange={(e) => form.setValue("password", e.target.value)} // Update the form value
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="dark:text-text bg-red dark:hover: dark:bg-mantle hover:bg-yellow dark:hover:bg-yellow hover: w-[180px] whitespace-nowrap rounded-sm border-transparent font-medium transition duration-150 ease-in-out focus:outline-none "
>
Submit
</Button>
</form>
</Form>
)
}

View File

@ -0,0 +1,134 @@
"use client"
import React, { useEffect, useState } from "react"
import { UsePostPlayerRival } from "@/app/axios/usePostApi"
import { usePlayerRivalList } from "@/app/axios/useFetchApi"
import { Input } from "./ui/input"
import { RivalData } from "@/lib/types"
import { ApiFetch } from "@/lib/api"
const RivalComponent = () => {
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [successMessage, setSuccessMessage] = useState<string | null>(null)
const [showAccessCode, setShowAccessCode] = useState(false)
const [ownRivalCode, setOwnRivalCode] = useState(-1)
const [targetRivalCode, setTargetRivalCode] = useState(0)
const [rivals, setRivals] = useState<{ username: string }[]>([])
const [refreshRivalList, setRefreshRivalList] = useState(false)
const { postRival } = UsePostPlayerRival({
onPostSuccess: (data) => {
setSuccessMessage("Rival added successfully!")
setError(null)
// Set refreshRivalList to true after posting a rival
setRefreshRivalList(true)
},
setLoading,
setError,
})
const handlePostRival = () => {
if (targetRivalCode) {
postRival({
rivalCode: targetRivalCode,
})
}
}
usePlayerRivalList({
setPlayerRivalList: setRivals,
setLoading,
setError,
refreshRivalList,
})
useEffect(() => {
async function fetchData() {
const user = (await ApiFetch<UserDataResponse>("/SDHD/user")).body.body;
setOwnRivalCode(user.rivalCode);
}
fetchData();
}, []);
useEffect(() => {
const successTimeout = setTimeout(() => {
setSuccessMessage(null)
}, 3000)
return () => {
clearTimeout(successTimeout)
}
}, [successMessage])
return (
<div className="items-center justify-center rounded-sm bg-subsectionbackgroundcolor pl-4 pt-4 md:flex-row">
<div className="flex w-full flex-col items-start md:w-auto">
<div className="relative mt-1">
<label className="block pb-1 pt-2 text-sm font-medium text-typography">
Enter Rival Aime Card:
</label>
<Input
type="number"
placeholder="Enter Rival Code:"
onChange={(e) => setTargetRivalCode(e.target.valueAsNumber)}
className="w-[300px] rounded-md border-gray-300 py-2 pl-3 pr-10 shadow-sm focus:outline-none sm:w-[200px] sm:text-sm md:w-[200px] lg:w-[300px]"
/>
</div>
{successMessage && (
<div className="text-green mt-2">{successMessage}</div>
)}
<div className="mt-4 flex items-center space-x-2">
<button
onClick={handlePostRival}
className="mb-3 whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Add Rival
</button>
<button
onClick={() => setShowAccessCode(!showAccessCode)}
className="mb-3 whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
{showAccessCode ? "Hide Rival Code" : "Show Rival Code"}
</button>
</div>
{showAccessCode && (
<div>
<label className="text-sm font-medium text-typography">
Your Rival code:
</label>
<div className="text-lg font-bold text-typography">
{ownRivalCode}
</div>
</div>
)}
{rivals.length > 0 && (
<div>
<label className="text-xs font-medium text-typography">
Rivals need to have played once on the most recent version that
your server supports to show up here.
</label>
<div className="mb-1">
{" "}
<label className="text-sm font-medium text-typography">
Your Rivals:
</label>
</div>
<div className="pb-2">
<ul>
{rivals.map((rival, index) => (
<li key={index} className="text-md font-bold ">
Rival {index + 1}: {rival.username}
</li>
))}
</ul>
</div>
</div>
)}
</div>
</div>
)
}
export default RivalComponent

View File

@ -0,0 +1,289 @@
"use client"
import React, { useEffect, useState } from "react"
import Image from "next/image"
import { useFetchPlayerScoreLog } from "@/app/axios/useFetchApi"
import { useSettings } from "@/app/context/SettingsContext"
import { Input } from "./ui/input"
import { JacketImageProps, NavigationButtonsProps, getDifficultyClass, getDifficultyText, getGrade, getRatingChangeColor } from "@/lib/utils"
import { ScoreCardProps } from "@/lib/types"
// JacketImage Component
const JacketImage: React.FC<JacketImageProps> = ({ jacketPath }) => (
<Image
src={jacketPath}
alt="Character Image"
width={90}
height={90}
className="border-sm ml-1 mt-4 border-4 border-songjacketborder dark:border-gray-400"
/>
)
const ScoreCard: React.FC<ScoreCardProps> = ({
userPlayDate,
title,
rating_change,
genre,
score,
isNewRecord,
isAllJustice,
judgeJustice,
judgeAttack,
chartId,
judgeGuilty,
artist,
judgeHeaven,
judgeCritical,
level,
isClear,
isFullCombo,
maxCombo,
jacketPath,
rating,
}) => {
const formattedJacketPath = `${process.env.NEXT_PUBLIC_CDN
}JacketArt/${jacketPath.slice(0, -3)}png`
const realJusticeCritical = judgeCritical + judgeHeaven
const { isBestPossibleEnabled } = useSettings()
return (
<div className="h-50 rounded-sm bg-cardsectionbackgroundcolor p-2 text-typography ">
<div className="flex items-center justify-between">
<span className="text-lg text-typography">{userPlayDate}</span>
<div className="flex justify-end">
<div className="flex flex-col items-end justify-center">
<div className="flex items-center">
<span
className={`justify-center rounded-sm p-1 px-2 text-center text-sm font-bold ${getDifficultyClass(
chartId,
)} text-scorecardbadgetypography`}
>
{getDifficultyText(chartId)}{" "}
{level !== undefined ? level.toString() : "N/A"}{" "}
</span>
{isNewRecord !== 0 && isNewRecord ? (
<span className="bg-color13 ml-2 rounded-sm bg-newbadgecolor p-1 px-2 text-sm font-bold text-scorecardbadgetypography">
NEW!!
</span>
) : (
<span className=""></span>
)}
</div>
</div>
</div>
</div>
<div className={`text-xs ${getRatingChangeColor(rating_change)}`}>
Rating: {rating_change}&nbsp;
{isBestPossibleEnabled && (
<span className={`text-xs ${getRatingChangeColor(rating_change)}`}>
{(rating / 100).toFixed(2)}
</span>
)}
</div>
<div className="flex">
<div className="flex items-start space-x-4">
<JacketImage jacketPath={formattedJacketPath} />
<div className="flex flex-col justify-center pt-3">
<h2 className="w-[200px] truncate pb-2 text-xs text-typography lg:w-[310px]">
{title}
</h2>
<div className="h-[1] w-[180px] overflow-hidden truncate pb-2 text-xs text-typography">
{artist}
</div>
<div className="text-xs text-typography">{genre}</div>
</div>
</div>
</div>
<div className="flex pl-1 pt-4">
<div
className={` mr-2 rounded-sm bg-scorecardbadge px-2 py-1 text-sm font-bold text-scorecardbadgetypography ${isFullCombo
? "bg-scorecardbadge"
: isAllJustice
? "dark:bg-green-500"
: ""
}`}
>
{isAllJustice
? "All Justice"
: isFullCombo
? "Full Combo"
: `Combo: ${maxCombo}`}
</div>
<div
className={`mr-2 rounded-sm bg-scorecardbadge px-4 py-1 text-sm font-bold text-scorecardbadgetypography ${isClear === 0 ? "bg-scorecardbadge" : ""
}`}
>
{isClear === 1 ? "CLEAR" : "UNCLEARED"}
</div>
<div className="bg-yellow rounded-sm bg-scorecardbadge px-4 py-1 text-sm font-bold text-scorecardbadgetypography ">
{getGrade(score)}
</div>
</div>
<div className="flex items-center justify-start pt-2">
<div className="pr-1 text-xs text-scoretext md:pr-2">
Score: {score.toLocaleString()}
</div>
<div className="flex items-center justify-start">
<div className="pr-1 text-xs text-justicecriticaltext md:pr-2 ">
Justice Critical: {realJusticeCritical}
</div>
<div className="pr-1 text-xs text-justicetext md:pr-2">
Justice: {judgeJustice}
</div>
<div className="pr-1 text-xs text-attacktext md:pr-2">
Attack: {judgeAttack}
</div>
<div className="text-xs text-misstext">Miss: {judgeGuilty}</div>
</div>
</div>
</div>
)
}
// NavigationButtons Component
const NavigationButtons: React.FC<NavigationButtonsProps> = ({
currentPage,
totalPages,
onPageChange,
onNext,
onPrevious,
}) => {
// Only show a maximum of 5 page buttons around the current page
const rangeStart = 5 * Math.floor((currentPage - 1) / 5) + 1
const rangeEnd = Math.min(rangeStart + 4, totalPages)
const pages = Array.from(
{ length: rangeEnd - rangeStart + 1 },
(_, index) => rangeStart + index,
)
return (
<div className="flex items-center justify-center space-x-1 ">
<button
onClick={onPrevious}
disabled={currentPage === 1}
className="rounded bg-paginationbackground px-4 font-bold text-paginationtypography hover:text-paginationtypographyhover disabled:opacity-40"
>
Previous
</button>
{pages.map((page) => (
<button
key={page}
onClick={() => onPageChange(page)}
disabled={currentPage === page}
className={`rounded bg-paginationbackground px-2 font-bold text-paginationtypography hover:text-paginationtypographyhover ${currentPage === page ? " " : ""
}`}
>
{page}
</button>
))}
<button
onClick={onNext}
disabled={currentPage === totalPages}
className="rounded bg-paginationbackground px-4 font-bold text-paginationtypography hover:text-paginationtypographyhover disabled:opacity-40"
>
Next
</button>
</div>
)
}
// ScoreCardList Component
const ScoreCardList = () => {
const [profileData, setPlayerScoreLog] = useState<any[]>([])
const [loading, setLoading] = useState<boolean>(true)
const [error, setError] = useState<string | null>(null)
const [itemsPerPage] = useState<number>(9)
const [currentPage, setCurrentPage] = useState<number>(1)
const [searchQuery, setSearchQuery] = useState("")
const [filteredData, setFilteredData] = useState<any[]>([])
useFetchPlayerScoreLog({ setPlayerScoreLog, setLoading, setError })
useEffect(() => {
const queryLower = searchQuery.toLowerCase()
const filtered = profileData.filter((item) => {
// Check if the query is present in the title, artist, level, or grade
const titleMatches = item.title.toLowerCase().includes(queryLower)
const artistMatches = item.artist.toLowerCase().includes(queryLower)
const levelMatches = item.level
? item.level.toString().toLowerCase().includes(queryLower)
: false
const gradeMatches = getGrade(item.score)
.toLowerCase()
.includes(queryLower)
return titleMatches || artistMatches || levelMatches || gradeMatches
})
setFilteredData(filtered)
}, [searchQuery, profileData])
const totalPages = Math.ceil(filteredData.length / itemsPerPage)
// Get current items for the current page
const indexLastItem = currentPage * itemsPerPage
const indexFirstItem = indexLastItem - itemsPerPage
const currentItems = filteredData.slice(indexFirstItem, indexLastItem)
// Go to the next page
const handleNext = () => {
setCurrentPage((prevCurrentPage) =>
Math.min(prevCurrentPage + 1, totalPages),
)
}
// Go to the previous page
const handlePrevious = () => {
setCurrentPage((prevCurrentPage) => Math.max(prevCurrentPage - 1, 1))
}
// Go to the specific page
const handlePageChange = (page: number) => {
setCurrentPage(page)
}
if (loading) {
return <div></div> // Display loading state
}
if (error) {
return <div>Error: {error}</div> // Display error state
}
return (
<div className=" w-[405px] sm:w-[640px] md:w-[840px] lg:w-[690px] xl:w-[790px] 2xl:w-[1390px]">
<div className="">
<Input
type="text"
placeholder="Search by artist, level, song title, or grade. "
className={` focus-visible: w-[395px] bg-transparent text-typography transition duration-300 ease-in-out focus:outline-none dark:border-gray-300 dark:bg-transparent sm:w-[640px] md:w-[840px] lg:w-[690px] xl:w-[790px] 2xl:w-[1390px]`}
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
/>
</div>
<div className="grid w-[405px] grid-cols-1 sm:w-[640px] sm:grid-cols-1 md:w-[840px] md:grid-cols-1 lg:w-[700px] lg:grid-cols-1 xl:w-[790px] xl:grid-cols-1 2xl:w-[1400px] 2xl:grid-cols-3">
{currentItems.map((scoreCardInfo, index) => (
<div key={index} className="dark:bg-crust mb-2 mr-2 mt-2 rounded-sm">
<ScoreCard {...scoreCardInfo} />
</div>
))}
</div>
<div className="mt-2 flex justify-around">
<div className="rounded-md">
<NavigationButtons
currentPage={currentPage}
totalPages={totalPages}
onPageChange={handlePageChange}
onNext={handleNext}
onPrevious={handlePrevious}
/>
</div>
</div>
</div>
)
}
export default ScoreCardList

View File

@ -0,0 +1,47 @@
"use client"
import React from "react"
import { useSettings } from "@/app/context/SettingsContext"
import { Switch } from "@/components/ui/switch"
import { ModeToggle } from "./darkmodetoggle"
import { Label } from "@/components/ui/label"
import { PasswordResetForm } from "./PlayerPasswordReset"
const SettingsPage: React.FC = () => {
const { isAverageEnabled, toggleAverage } = useSettings()
const { isBestPossibleEnabled, toggleBestPossible } = useSettings()
return (
<div className="absolute pl-1 lg:pl-10 xl:pl-10">
<div className="rounded-sm bg-subsectionbackgroundcolor p-4">
<h1 className="mb-4 text-xl font-bold text-typography">UI Settings</h1>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2">
<Switch
id="airplane-mode"
checked={isAverageEnabled}
onCheckedChange={toggleAverage}
/>
<Label htmlFor="airplane-mode" className="text-typography">
Average Rating
</Label>
</div>
</div>
<div className="pt-2"></div>
<div className="flex items-center space-x-2">
<Switch
id="airplane-mode"
checked={isBestPossibleEnabled}
onCheckedChange={toggleBestPossible}
/>
<Label htmlFor="airplane-mode" className="text-typography">
Show best rating formula rating on score card
</Label>
</div>
<div className="mt-2 flex items-center">
<ModeToggle />
<span className="ml-2 text-lg text-typography">Select Theme</span>
</div>
</div>
</div>
)
}
export default SettingsPage

View File

@ -0,0 +1,164 @@
/* eslint-disable @next/next/no-img-element */
"use client"
import React, { useState, useEffect } from "react"
import { UsePostSystemVoice } from "@/app/axios/usePostApi"
import { UsePlayerVoiceId } from "@/app/axios/useFetchApi"
import { UsePlayerDuelCompletions } from "@/app/axios/useFetchApi"
import AudioPlayer from "./shared/audioPlayer/page"
import { DropdownVoiceProps, PlayerVoiceData } from "@/lib/types"
const Dropdown: React.FC<DropdownVoiceProps & { className?: string }> = ({
options,
onChange,
str,
id,
className,
}) => {
return (
<div className={`dropdown-container relative ${className}`}>
<label className="block pb-1 pt-2 text-sm font-medium text-typography">
{str}
</label>
<div className="relative mt-1">
<select
value={id}
onChange={(e) => onChange(e.target.value)}
className=" w-[230px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out focus:outline-none sm:w-[280px] md:w-[280px] lg:w-[280px]"
>
{options.map((option, index) => (
<option
key={`${option.id}-${index}`}
value={option.id}
className="text-typography"
>
{option.str}
</option>
))}
</select>
</div>
</div>
)
}
const SystemVoiceComponent = () => {
const [systemVoiceData, setSystemVoiceData] = useState<PlayerVoiceData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [duelData, setDuelData] = useState<{ duelId: string; str: string }[]>(
[],
)
const [selectedDuelId, setSelectedDuelId] = useState<string>("")
const [playerInfoLoaded, setPlayerInfoLoaded] = useState(false)
UsePlayerVoiceId({
setPlayerVoiceId: setSystemVoiceData,
setLoading,
setError,
})
UsePlayerDuelCompletions({
setPlayerDuelCompletions: setDuelData,
setLoading,
setError,
onSuccess: (data) => { },
})
useEffect(() => {
if (systemVoiceData.length > 0 && !playerInfoLoaded) {
const initialVoiceId = systemVoiceData[0].voiceId
setSelectedDuelId(initialVoiceId)
setPlayerInfoLoaded(true)
}
}, [systemVoiceData, playerInfoLoaded])
const { postVoiceID } = UsePostSystemVoice({
onPostSuccess: (data) => {
console.log("Data posted successfully:", data)
},
setLoading,
setError,
})
const handleSelectionChange = (part: keyof PlayerVoiceData, id: string) => {
if (part === "voiceId") {
setSelectedDuelId(id)
}
setSystemVoiceData(
systemVoiceData.map((data, index) =>
index === 0 ? { ...data, [part]: id } : data,
),
)
}
const handlePostData = () => {
if (systemVoiceData.length > 0) {
const { voiceId } = systemVoiceData[0]
const dataToPost = {
voiceId: selectedDuelId,
}
postVoiceID(dataToPost)
console.log("Attempting to post data", dataToPost)
}
}
if (loading) return <div></div>
if (error) return <div>Error: {error}</div>
if (!Array.isArray(systemVoiceData) || systemVoiceData.length === 0) {
return <div>No player data found.</div>
}
const DEFAULT_IMAGE = `${process.env.NEXT_PUBLIC_CDN}systemVoice/CHU_UI_SystemVoice_00000001.png`
return (
<div className="items-center justify-center rounded-sm bg-subsectionbackgroundcolor pl-4 pt-4 md:flex-row">
{selectedDuelId && (
<img
className="h-[128px] w-[200px]"
src={`${process.env.NEXT_PUBLIC_CDN
}systemVoiceThumbnails/CHU_UI_SystemVoice_${selectedDuelId
.toString()
.padStart(8, "0")}.png`}
alt="System Voice"
onError={(e) => {
e.currentTarget.src = DEFAULT_IMAGE
}}
/>
)}
<div className="flex w-full flex-col items-start md:w-auto">
<Dropdown
options={duelData.map((data) => ({
str: data.str,
id: data.duelId,
sortName: "",
}))}
onChange={(value) => handleSelectionChange("voiceId", value)}
str="Duel Options: "
id={selectedDuelId}
/>
<div className="mt-4 flex items-center">
<button
onClick={handlePostData}
className="mb-3 whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Update System Voice
</button>
<div>
<AudioPlayer
src={`${process.env.NEXT_PUBLIC_CDN
}systemVoice/systemvoice0${selectedDuelId
.toString()
.padStart(3, "0")}/00001.wav`}
/>
</div>
</div>
</div>
</div>
)
}
export default SystemVoiceComponent

View File

@ -0,0 +1,143 @@
/* eslint-disable @next/next/no-img-element */
"use client"
import React, { useState, useEffect } from "react"
import { UsePlayerTeams } from "@/app/axios/useFetchApi"
import { UsePostPlayerTeamProfile } from "@/app/axios/usePostApi"
import { UsePostPlayerTeam } from "@/app/axios/usePostApi"
import { Input } from "./ui/input"
import { PlayerTeamData } from "@/lib/types"
const PlayerTeamComponent = () => {
const [playerTeamData, setPlayerTeams] = useState<PlayerTeamData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [newTeamName, setNewTeamName] = useState<string>("")
const [selectedTeam, setSelectedTeam] = useState<string>("")
const [successMessage, setSuccessMessage] = useState<string | null>(null)
const [refreshTeams, setRefreshTeams] = useState(false)
UsePlayerTeams({ setPlayerTeams, setLoading, setError })
const { postTeamName } = UsePostPlayerTeam({
onPostSuccess: (data) => {
console.log("Data posted successfully:", data)
setSuccessMessage("Team created successfully!")
setTimeout(() => {
setSuccessMessage(null)
}, 3000)
setNewTeamName("")
setPlayerTeams(data.teams)
},
setLoading,
setError,
})
const { postTeamNameProfile } = UsePostPlayerTeamProfile({
onPostSuccess: (data) => {
console.log("Player team profile updated successfully:", data)
},
setLoading,
setError,
})
const handlePostData = () => {
if (newTeamName.trim() !== "") {
const isTeamNameExists = playerTeamData.some(
(team) => team.teamName.toLowerCase() === newTeamName.toLowerCase(),
)
if (isTeamNameExists) {
setError("Team name already exists. Please choose a different name.")
setSuccessMessage(null)
setTimeout(() => {
setError(null)
}, 3000)
return
}
const dataToPost = { teamName: newTeamName }
postTeamName(dataToPost)
console.log("Attempting to post data", dataToPost)
}
}
const handleSelectTeam = (e: React.ChangeEvent<HTMLSelectElement>) => {
setSelectedTeam(e.target.value)
}
const handlePostPlayerTeamProfile = () => {
if (selectedTeam) {
const dataToPost = { teamId: selectedTeam }
postTeamNameProfile(dataToPost)
console.log("Posting player team profile data", dataToPost)
} else {
console.error("No team selected")
}
}
useEffect(() => {
if (refreshTeams) {
setRefreshTeams(false)
}
}, [refreshTeams])
if (loading) return <div>Loading...</div>
return (
<div className="items-center justify-center rounded-sm bg-subsectionbackgroundcolor pl-4 pt-4 md:flex-row">
<div className="flex w-full flex-col">
<div className="mb-4 flex items-center">
<Input
type="text"
placeholder="Enter Team Name"
value={newTeamName}
onChange={(e) => setNewTeamName(e.target.value)}
className=" hover:bg-yellow w-[230px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium transition duration-150 ease-in-out focus:outline-none sm:w-[280px] md:w-[280px] lg:w-[280px]"
/>
<div className="pl-1">
<button
onClick={handlePostData}
className="mr-2 w-[180px] whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Create Team
</button>
</div>
</div>
<div className="mb-4 flex items-center">
<select
value={selectedTeam}
onChange={handleSelectTeam}
className=" w-[230px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out focus:outline-none sm:w-[280px] md:w-[280px] lg:w-[280px]"
>
<option value="">Select a Team</option>
{playerTeamData.map((team) => (
<option key={team.id} value={team.id}>
{team.teamName}
</option>
))}
</select>
<div className="pl-1">
<button
onClick={handlePostPlayerTeamProfile}
className="w-[180px] whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Set Player Team
</button>
</div>
</div>
{successMessage && (
<div className="text-green mb-2">{successMessage}</div>
)}
{error && <div className="text-red mb-2">{error}</div>}
</div>
</div>
)
}
export default PlayerTeamComponent

View File

@ -0,0 +1,177 @@
"use client"
import React, { useEffect, useState } from "react"
import Image from "next/image"
import { useBestTop } from "@/app/axios/useFetchApi"
import { usePlayerRecentRatingTable } from "@/app/axios/useFetchApi"
import { useSettings } from "@/app/context/SettingsContext"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "./ui/separator"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
const PlayerBestTopScores = () => {
const [bestTop, setBestTop] = useState<any[]>([])
const [recentRatingTable, setPlayerRecentRatingTable] = useState<any[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [selectedSegment, setSelectedSegment] = useState("Top30") // Set "Top30" as the default
const [displayedData, setDisplayedData] = useState<any[]>([])
const { isAverageEnabled } = useSettings()
const [averageTop30Rating, setAverageTop30Rating] = useState<number>(0) // New state for average rating
const [averageRecentRating, setAverageRecentRating] = useState<number>(0) // New state for average recent rating
useBestTop({ setBestTop, setLoading, setError })
usePlayerRecentRatingTable({
setPlayerRecentRatingTable,
setLoading,
setError,
})
useEffect(() => {
const sortedData =
selectedSegment === "Top30"
? [...bestTop].sort(
(a, b) => (parseFloat(b.rating) || 0) - (parseFloat(a.rating) || 0),
)
: [...recentRatingTable].sort(
(a, b) => (parseFloat(b.rating) || 0) - (parseFloat(a.rating) || 0),
)
// Filter out entries with undefined or null ratings
const filteredData = sortedData.filter(
(entry) => entry.rating !== undefined && entry.rating !== null,
)
if (selectedSegment === "Recent10") {
setDisplayedData(filteredData.slice(0, 10))
// Calculate average of recent 10 ratings from displayed data
const recent10Ratings = filteredData
.slice(0, 10)
.map((entry) => parseFloat(entry.rating))
const averageRecentRatingValue =
recent10Ratings.length > 0
? recent10Ratings.reduce((sum, rating) => sum + rating, 0) /
recent10Ratings.length
: 0
setAverageRecentRating(averageRecentRatingValue)
} else {
const increasedRatingData = filteredData.filter(
(entry) =>
entry.rating_change === "Increase" ||
entry.score_change === "Increase",
)
const uniqueData = increasedRatingData.filter(
(entry, index, self) =>
index === self.findIndex((e) => e.title === entry.title),
)
setDisplayedData(uniqueData.slice(0, 30))
const top30Ratings = uniqueData
.slice(0, 30)
.map((entry) => parseFloat(entry.rating))
console.log(top30Ratings)
const averageRating =
top30Ratings.length > 0
? top30Ratings.reduce((sum, rating) => sum + rating, 0) /
top30Ratings.length
: 0
setAverageTop30Rating(averageRating)
}
}, [bestTop, recentRatingTable, selectedSegment])
return (
<div className="justify-center md:mx-auto md:px-0 ">
<div className="flex justify-start pb-2">
<div className="inline-flex rounded-md shadow-sm " role="group">
<Tabs defaultValue="top30">
<TabsList>
<TabsTrigger
value="top30"
onClick={() => setSelectedSegment("Top30")}
>
Top 30
</TabsTrigger>
<TabsTrigger
value="recent10"
onClick={() => setSelectedSegment("Recent10")}
>
Recent 10
</TabsTrigger>
</TabsList>
<TabsContent value="top30">
{/* Content for Top 30 */}
{selectedSegment === "Top30" && <div></div>}
</TabsContent>
<TabsContent value="recent10">
{/* Content for Recent 10 */}
{selectedSegment === "Recent10" && <div></div>}
</TabsContent>
</Tabs>
</div>
</div>
<div>
{loading && <div></div>}
{error && <div>Error: {error}</div>}
<div className=""></div>
<ScrollArea className="h-[950px] w-[340px] rounded-md border">
<div className="p-4">
<div className=" pb-2 text-typography ">
{isAverageEnabled && selectedSegment === "Top30" && (
<p>Average Rating {(averageTop30Rating / 100).toFixed(2)}</p>
)}
{isAverageEnabled && selectedSegment === "Recent10" && (
<p>Average Rating {(averageRecentRating / 100).toFixed(2)}</p>
)}
</div>
{displayedData.map((user, index) => {
const formattedJacketPath = `${process.env.NEXT_PUBLIC_CDN
}JacketArt/${user.jacketPath.slice(0, -3)}png`
return (
<>
<div key={index} className="flex items-start ">
<div className="flex-none pb-2">
<Image
src={formattedJacketPath}
alt={`${user.title} Jacket Art`}
width={90}
height={90}
className="border-sm border-4 border-songjacketborder"
/>
</div>
<div className="flex-grow pl-2">
<div className="text-typography mb-1">
<span className=" text-typography text-xl">
{" "}
{index + 1}.
</span>{" "}
{user.title}
</div>
<div className="text-typography">
{user.level}
<span className="text-bestrecentratingcolor">
{" "}
{(user.rating / 100).toFixed(2)}
</span>
</div>
<span className="text-bestrecentratingcolor">
{parseInt(user.score).toLocaleString()}
</span>
</div>
</div>
<div>
<Separator className="my-2" />
</div>
</>
)
})}
</div>
</ScrollArea>
</div>
</div>
)
}
export default PlayerBestTopScores

View File

@ -0,0 +1,108 @@
"use client"
import { UsePostTrophy } from "@/app/axios/usePostApi"
import React, { useState } from "react"
import {
UsePlayerProfileTrophies,
UsePlayerTrophies,
} from "@/app/axios/useFetchApi"
import { DropdownProps, PlayerData, PlayerTrophiesData } from "@/lib/types"
const Dropdown: React.FC<DropdownProps & { className?: string }> = ({
options,
onChange,
label,
trophyId,
className,
}) => {
return (
<div className={`dropdown-container relative ${className}`}>
<label className="dark:text-blue block pb-1 pt-2 text-sm font-medium ">
{label}
</label>
<div className="relative mt-1">
<select
value={trophyId}
onChange={(e) => onChange(e.target.value)}
className=" w-[230px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out focus:outline-none sm:w-[280px] md:w-[280px] lg:w-[280px]"
>
{options.map((option, index) => (
<option
key={`${option.trophyId}-${index}`}
value={option.trophyId}
className=" dark:text-white"
>
{option.name}{" "}
{option.rareType === 6
? "(MASTER ALL JUSTICE)"
: option.rareType === 4
? "(EXPERT ALL JUSTICE)"
: ""}
</option>
))}
</select>
</div>
</div>
)
}
const PlayerTropyComponent = () => {
const [playerData, setPlayerProfileTrophies] = useState<PlayerData[]>([])
const [PlayerTrophies, setPlayerTrophies] = useState<PlayerTrophiesData[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
UsePlayerTrophies({ setPlayerTrophies, setLoading, setError })
UsePlayerProfileTrophies({ setPlayerProfileTrophies, setLoading, setError })
const { postTrophy } = UsePostTrophy({
onPostSuccess: (data) => {
console.log(data)
},
setLoading,
setError,
})
const handlePostData = () => {
if (playerData.length > 0) {
const { trophyId } = playerData[0]
const dataToPost = { trophyId }
console.log("Data to be posted:", dataToPost)
postTrophy(dataToPost)
}
}
if (loading) return <div></div>
if (error) return <div>Error: {error}</div>
if (!Array.isArray(playerData) || playerData.length === 0) {
return <div>No player data found.</div>
}
return (
<div className="items-center justify-center rounded-sm bg-subsectionbackgroundcolor pl-4 pt-4 md:flex-row">
<div className="items- flex w-full flex-col md:w-auto">
<Dropdown
options={PlayerTrophies}
onChange={(value) =>
setPlayerProfileTrophies([
{ ...playerData[0], trophyId: Number(value) },
])
}
label="System Trophy: "
className="text-typography"
trophyId={playerData[0].trophyId}
/>
<div className=" mt-4">
<button
onClick={handlePostData}
className="mb-3 whitespace-nowrap rounded-sm border border-transparent bg-buttonbackgroundcolor px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Update Trophy
</button>
</div>
</div>
</div>
)
}
export default PlayerTropyComponent

View File

@ -0,0 +1,59 @@
"use client"
import { useState } from "react"
import { UsePlayerAimeCard } from "@/app/axios/useFetchApi"
import { Input } from "./ui/input"
interface AimeCardProps { }
const AimeCard: React.FC<AimeCardProps> = () => {
const [playAimeCard, setPlayAimeCard] = useState<any>({})
const [showNumbers, setShowNumbers] = useState(false)
UsePlayerAimeCard({
setPlayAimeCard,
setLoading: () => { },
setError: () => { },
})
const toggleShowNumbers = () => {
setShowNumbers(!showNumbers)
}
const formatCardNumber = () => {
const cardNumber = playAimeCard?.accessCode || ""
if (showNumbers) {
return cardNumber
} else {
const visiblePart = cardNumber.slice(0, 4)
const hiddenPart = cardNumber.slice(4, -4).replace(/[0-9]/g, "*")
const lastFourDigits = cardNumber.slice(-4)
return `${visiblePart}${hiddenPart}${lastFourDigits}`
}
}
return (
<div className="flex items-center justify-center">
<div className="w-[390px] max-w-md rounded-sm bg-subsectionbackgroundcolor p-2">
<div className="mb-2">
<label className="mb-2 block pb-1 pt-1 text-sm font-bold text-typography">
Aime Card Number
</label>
<div className="relative">
<Input
type="text"
value={formatCardNumber()}
readOnly
className="w-full rounded-md border border-gray-300 px-4 py-2 font-mono text-typography focus:border-transparent focus:outline-none"
/>
<button
onClick={toggleShowNumbers}
className="absolute right-4 top-1/2 -translate-y-1/2 transform rounded-md px-2 py-1 text-typography"
>
{showNumbers ? "Hide" : "View"}
</button>
</div>
</div>
</div>
</div>
)
}
export default AimeCard

View File

@ -0,0 +1,164 @@
"use client"
import React, { useEffect, useState } from "react"
import { UsePostNewKeychip } from "@/app/axios/usePostApi"
import { Input } from "./ui/input"
import { ApiFetch } from "@/lib/api"
const NewKeychipComponent = () => {
const initialKeychipData = {
arcade_nickname: "",
name: "",
serial: "",
namcopcbid: "",
}
const [userId, setUserId] = useState(0)
const [keychipData, setKeychipData] = useState(initialKeychipData)
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [accessCodeError, setAccessCodeError] = useState<string | null>(null)
const [successMessage, setSuccessMessage] = useState<string>("")
useEffect(() => {
async function fetchData() {
const user = (await ApiFetch<UserDataResponse>("/SDHD/user")).body.body;
setUserId(user.userId);
}
fetchData();
}, []);
const { createNewKeychip } = UsePostNewKeychip({
onPostSuccess: (data) => {
setSuccessMessage("Keychip created successfully!")
setKeychipData(initialKeychipData)
setAccessCodeError(null)
},
setLoading,
setError,
})
const handleChange = (
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
) => {
setKeychipData({ ...keychipData, [e.target.name]: e.target.value })
}
const allowedUserIds = [10000, 10011, 10044];
const handleSubmit = () => {
// Check if any of the input fields are empty
if (
keychipData.name.trim() === "" ||
keychipData.arcade_nickname.trim() === "" ||
keychipData.serial.trim() === ""
) {
setAccessCodeError("Please fill in all the required fields.")
// Set red color and vanish after 3 seconds
setTimeout(() => {
setAccessCodeError(null)
}, 3000)
return
}
if (allowedUserIds.includes(userId)) {
createNewKeychip(keychipData)
console.log("Submitting keychip data", keychipData)
} else {
setAccessCodeError("Invalid access code. Please check and try again.")
}
}
useEffect(() => {
let timeoutId: NodeJS.Timeout
if (successMessage) {
timeoutId = setTimeout(() => {
setSuccessMessage("")
}, 3000)
}
return () => {
clearTimeout(timeoutId)
}
}, [successMessage])
const generateRandomSerial = () => {
let uniqueNumbers = ""
while (uniqueNumbers.length < 4) {
const digit = Math.floor(Math.random() * 10)
if (!uniqueNumbers.includes(digit.toString())) {
uniqueNumbers += digit
}
}
const randomNumbers = Math.floor(1000 + Math.random() * 9000)
const randomSerial = `A69E01A${uniqueNumbers}${randomNumbers}`
setKeychipData({ ...keychipData, serial: randomSerial })
}
if (loading) return <div>Loading...</div>
if (error) return <div>Error: {error}</div>
if (!allowedUserIds.includes(userId)) {
return <div></div>
}
return (
<div className="mx-auto w-[390px] rounded-sm bg-subsectionbackgroundcolor p-2">
<label className="mb-2 block pb-1 pt-1 text-sm font-bold text-black text-typography">
Create New Keychip
</label>
{successMessage && (
<div className="text-green mb-4">{successMessage}</div>
)}
{accessCodeError && (
<div className="text-red mb-4">{accessCodeError}</div>
)}
<div>
<div className="mb-4">
<Input
type="text"
name="name"
value={keychipData.name}
onChange={handleChange}
placeholder="Arcade Name"
className="w-full rounded-md border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="mb-4">
<Input
type="text"
name="arcade_nickname"
value={keychipData.arcade_nickname}
onChange={handleChange}
placeholder="Arcade Nickname"
className="w-full rounded-md border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="">
<Input
type="text"
name="serial"
value={keychipData.serial}
onChange={handleChange}
placeholder="Serial Number"
className="w-full rounded-md border border-gray-300 p-2 focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="flex flex-col items-center pb-2 pt-4">
<button
onClick={generateRandomSerial}
className="hover:bg-red mb-3 w-[220px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Generate Random Serial
</button>
<button
onClick={handleSubmit}
className="hover:bg-red mb-3 w-[220px] whitespace-nowrap rounded-sm border border-transparent bg-background px-3 py-2 font-medium text-typography transition duration-150 ease-in-out hover:bg-buttonhovercolor hover:text-buttontexthovercolor focus:outline-none"
>
Create New Keychip
</button>
</div>
</div>
</div>
)
}
export default NewKeychipComponent

View File

@ -0,0 +1,46 @@
"use client"
import * as React from "react"
import { MoonIcon, SunIcon } from "@radix-ui/react-icons"
import { useTheme } from "next-themes"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu"
export function ModeToggle() {
const { setTheme } = useTheme()
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="outline" size="icon">
<SunIcon className="h-[1.2rem] w-[1.2rem] rotate-0 scale-100 text-typography transition-all dark:-rotate-90 dark:scale-0" />
<MoonIcon className="absolute h-[1.2rem] w-[1.2rem] rotate-90 scale-0 text-foreground transition-all dark:rotate-0 dark:scale-100" />
<span className="sr-only">Toggle theme</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="ml-16 bg-subsectionbackgroundcolor text-typography"
>
<DropdownMenuItem onClick={() => setTheme("light")}>
Light
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("dark")}>
Dark
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("gruvbox")}>
Gruvbox
</DropdownMenuItem>
<DropdownMenuItem onClick={() => setTheme("mocha")}>
Catppuccin Mocha
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}

View File

@ -0,0 +1,157 @@
"use client"
import { useState } from "react"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { serialize } from "cookie"
import { AxiosError } from "axios"
import { ApiFetch } from "@/lib/api"
const removeCookie = (cookieName: string): string =>
serialize(cookieName, "", {
httpOnly: true, // Change to true for better security
secure: process.env.NODE_ENV === "production",
sameSite: "strict",
maxAge: -1,
path: "/",
})
const removeLoginInfoCookie = async (): Promise<void> => {
const serializedToken = removeCookie("BlackForestAPI_SESSION");
document.cookie = serializedToken;
await ApiFetch("/SDHD/logoutUser", { method: "POST" }, true, true);
}
const NavigationBar = () => {
const [isMenuOpen, setIsMenuOpen] = useState(false)
const { push } = useRouter()
const handleLogout = async () => {
try {
console.log("Logging out...")
await removeLoginInfoCookie()
console.log("Logout successful.")
push("/")
} catch (error) {
console.error("Logout failed:", error)
}
}
const handleMenuClick = () => {
setIsMenuOpen(!isMenuOpen)
}
return (
<div>
<nav className="z-50 mt-1 flex items-center justify-between rounded-sm p-5">
{/* Hamburger Icon */}
<div className="z-50 md:hidden" onClick={handleMenuClick}>
<svg
className="h-6 w-6"
fill="none"
stroke="#CAD3F5"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M4 6h16M4 12h16m-7 6h7"
/>
</svg>
</div>
<div className="hidden flex-1 justify-end md:flex">
<Link
href="/chunithm"
className="pl-2 font-bold uppercase text-typography hover:text-texthover "
>
SCORES
</Link>
<Link
href="/userbox"
className="pl-2 font-bold uppercase text-typography hover:text-texthover "
>
USERBOX
</Link>
<Link
href="/management"
className="pl-2 font-bold uppercase text-typography hover:text-texthover "
>
MANAGEMENT
</Link>
<Link
href="/settings"
className="pl-2 font-bold uppercase text-typography hover:text-texthover "
>
SETTINGS
</Link>
<div className="pl-2 font-bold uppercase ">
<button
onClick={handleLogout}
className="text-bg-typography font-bold uppercase text-typography hover:text-texthover "
>
Logout
</button>
</div>
</div>
</nav>
{/* Side Menu */}
<div
className={`fixed left-0 top-0 z-50 h-full w-64 transform bg-subsectionbackgroundcolor transition-all duration-300 ease-in-out ${
isMenuOpen ? "translate-x-0 " : "-translate-x-full"
}`}
>
<div className="mt-10 flex flex-col">
<Link
href="/chunithm"
className="pb-2 pl-2 font-bold uppercase text-typography hover:text-texthover "
>
SCORES
</Link>
<Link
href="/userbox"
className="pb-2 pl-2 font-bold uppercase text-typography hover:text-texthover "
>
USERBOX
</Link>
<Link
href="/management"
className="pb-2 pl-2 font-bold uppercase text-typography hover:text-texthover "
>
MANAGEMENT
</Link>
<Link
href="/settings"
className="pb-2 pl-2 font-bold uppercase text-typography hover:text-texthover "
>
SETTINGS
</Link>
<div className="text-bg-typography pb-2 pl-2 font-bold uppercase hover:text-texthover ">
<button
onClick={handleLogout}
className="text-bg-typography font-bold uppercase text-typography hover:text-texthover "
>
Logout
</button>
</div>
</div>
<button
className="pl-2 font-bold uppercase text-typography hover:text-texthover "
onClick={handleMenuClick}
>
CLOSE MENU
</button>
</div>
{/* Overlay */}
{isMenuOpen && (
<div
className="z-60 fixed inset-0 bg-black bg-opacity-50"
onClick={handleMenuClick}
></div>
)}
</div>
)
}
export default NavigationBar

View File

@ -0,0 +1,103 @@
"use client"
import React, { useState, useEffect, useRef } from "react"
interface AudioPlayerProps {
src: string // Define the type for src here
}
const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
const audioRef = useRef(new Audio(src))
const [isPlaying, setIsPlaying] = useState(false)
const [progress, setProgress] = useState(0) // Progress bar state
useEffect(() => {
audioRef.current.src = src
}, [src])
const togglePlayPause = () => {
const audio = audioRef.current
if (isPlaying) {
audio.pause()
console.log(`Paused: ${src}`)
} else {
audio.play()
console.log(`Playing: ${src}`)
}
setIsPlaying(!isPlaying)
}
// Update the progress as the audio plays
const onPlaying = () => {
const audio = audioRef.current
setProgress((audio.currentTime / audio.duration) * 100)
}
// Seek the audio when the progress bar is clicked
const onProgressClick = (e: any) => {
const audio = audioRef.current
const clickX = e.nativeEvent.offsetX
const width = e.target.clientWidth
const duration = audio.duration
audio.currentTime = (clickX / width) * duration
}
useEffect(() => {
const audio = audioRef.current
audio.addEventListener("play", () => {
setIsPlaying(true)
console.log(`Playing: ${src}`)
})
audio.addEventListener("pause", () => {
setIsPlaying(false)
console.log(`Paused: ${src}`)
})
audio.addEventListener("timeupdate", onPlaying)
return () => {
audio.removeEventListener("play", () => {
setIsPlaying(true)
console.log(`Playing: ${src}`)
})
audio.removeEventListener("pause", () => {
setIsPlaying(false)
console.log(`Paused: ${src}`)
})
audio.removeEventListener("timeupdate", onPlaying)
}
}, [src])
return (
<div className="flex flex-col items-end">
<div className="flex pb-3 pl-2 ">
<button onClick={togglePlayPause} className="text focus:outline-none">
{isPlaying ? (
<svg
xmlns="http://www.w3.org/2000/svg"
height="20"
viewBox="0 0 320 512"
className="icon-color h-6 w-6"
>
<path d="M48 64C21.5 64 0 85.5 0 112V400c0 26.5 21.5 48 48 48H80c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H48zm192 0c-26.5 0-48 21.5-48 48V400c0 26.5 21.5 48 48 48h32c26.5 0 48-21.5 48-48V112c0-26.5-21.5-48-48-48H240z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
height="20"
viewBox="0 0 384 512"
className="icon-color h-6 w-6"
>
<path d="M73 39c-14.8-9.1-33.4-9.4-48.5-.9S0 62.6 0 80V432c0 17.4 9.4 33.4 24.5 41.9s33.7 8.1 48.5-.9L361 297c14.3-8.7 23-24.2 23-41s-8.7-32.2-23-41L73 39z" />
</svg>
)}
</button>
{/* <div
onClick={onProgressClick}
className="w-full bg-gray-300 rounded-full h-1.5 cursor-pointer"
>
<div
className="bg-yellow h-1.5 rounded-full"
style={{ width: `${progress}%` }}
></div>
</div> */}
</div>
</div>
)
}
export default AudioPlayer

View File

@ -0,0 +1,7 @@
"use client"
import { CookiesProvider } from "next-client-cookies"
export const ClientCookiesProvider: typeof CookiesProvider = (props) => (
<CookiesProvider {...props} />
)

View File

@ -0,0 +1,9 @@
"use client"
import * as React from "react"
import { ThemeProvider as NextThemesProvider } from "next-themes"
import { type ThemeProviderProps } from "next-themes/dist/types"
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
}

View File

@ -0,0 +1,36 @@
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const badgeVariants = cva(
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
default:
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
secondary:
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
destructive:
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
outline: "text-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
VariantProps<typeof badgeVariants> {}
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
)
}
export { Badge, badgeVariants }

View File

@ -0,0 +1,56 @@
import * as React from "react"
import { Slot } from "@radix-ui/react-slot"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
destructive:
"bg-destructive text-destructive-foreground hover:bg-destructive/90",
outline:
"border border-input bg-background hover:bg-accent hover:text-accent-foreground",
secondary:
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
ghost: "hover:bg-accent hover:text-accent-foreground",
link: "text-primary underline-offset-4 hover:underline",
},
size: {
default: "h-10 px-4 py-2",
sm: "h-9 rounded-md px-3",
lg: "h-11 rounded-md px-8",
icon: "h-10 w-10",
},
},
defaultVariants: {
variant: "default",
size: "default",
},
}
)
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
asChild?: boolean
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
const Comp = asChild ? Slot : "button"
return (
<Comp
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
)
}
)
Button.displayName = "Button"
export { Button, buttonVariants }

View File

@ -0,0 +1,79 @@
import * as React from "react"
import { cn } from "@/lib/utils"
const Card = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn(
"rounded-lg border bg-card text-card-foreground shadow-sm",
className
)}
{...props}
/>
))
Card.displayName = "Card"
const CardHeader = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
))
CardHeader.displayName = "CardHeader"
const CardTitle = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLHeadingElement>
>(({ className, ...props }, ref) => (
<h3
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
CardTitle.displayName = "CardTitle"
const CardDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => (
<p
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
CardDescription.displayName = "CardDescription"
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
))
CardContent.displayName = "CardContent"
const CardFooter = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
))
CardFooter.displayName = "CardFooter"
export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }

View File

@ -0,0 +1,200 @@
"use client"
import * as React from "react"
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
import { Check, ChevronRight, Circle } from "lucide-react"
import { cn } from "@/lib/utils"
const DropdownMenu = DropdownMenuPrimitive.Root
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
const DropdownMenuGroup = DropdownMenuPrimitive.Group
const DropdownMenuPortal = DropdownMenuPrimitive.Portal
const DropdownMenuSub = DropdownMenuPrimitive.Sub
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
const DropdownMenuSubTrigger = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubTrigger>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubTrigger> & {
inset?: boolean
}
>(({ className, inset, children, ...props }, ref) => (
<DropdownMenuPrimitive.SubTrigger
ref={ref}
className={cn(
"flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none focus:bg-accent data-[state=open]:bg-accent",
inset && "pl-8",
className
)}
{...props}
>
{children}
<ChevronRight className="ml-auto h-4 w-4" />
</DropdownMenuPrimitive.SubTrigger>
))
DropdownMenuSubTrigger.displayName =
DropdownMenuPrimitive.SubTrigger.displayName
const DropdownMenuSubContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.SubContent>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.SubContent>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.SubContent
ref={ref}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
DropdownMenuSubContent.displayName =
DropdownMenuPrimitive.SubContent.displayName
const DropdownMenuContent = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 min-w-[8rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
))
DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
const DropdownMenuItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
const DropdownMenuCheckboxItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.CheckboxItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.CheckboxItem>
>(({ className, children, checked, ...props }, ref) => (
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
))
DropdownMenuCheckboxItem.displayName =
DropdownMenuPrimitive.CheckboxItem.displayName
const DropdownMenuRadioItem = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.RadioItem>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.RadioItem>
>(({ className, children, ...props }, ref) => (
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none transition-colors focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="h-2 w-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
))
DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
const DropdownMenuLabel = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Label> & {
inset?: boolean
}
>(({ className, inset, ...props }, ref) => (
<DropdownMenuPrimitive.Label
ref={ref}
className={cn(
"px-2 py-1.5 text-sm font-semibold",
inset && "pl-8",
className
)}
{...props}
/>
))
DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
const DropdownMenuSeparator = React.forwardRef<
React.ElementRef<typeof DropdownMenuPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof DropdownMenuPrimitive.Separator>
>(({ className, ...props }, ref) => (
<DropdownMenuPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
const DropdownMenuShortcut = ({
className,
...props
}: React.HTMLAttributes<HTMLSpanElement>) => {
return (
<span
className={cn("ml-auto text-xs tracking-widest opacity-60", className)}
{...props}
/>
)
}
DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
export {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuCheckboxItem,
DropdownMenuRadioItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuGroup,
DropdownMenuPortal,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuRadioGroup,
}

176
src/components/ui/form.tsx Normal file
View File

@ -0,0 +1,176 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { Slot } from "@radix-ui/react-slot"
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
FormProvider,
useFormContext,
} from "react-hook-form"
import { cn } from "@/lib/utils"
import { Label } from "@/components/ui/label"
const Form = FormProvider
type FormFieldContextValue<
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
> = {
name: TName
}
const FormFieldContext = React.createContext<FormFieldContextValue>(
{} as FormFieldContextValue
)
const FormField = <
TFieldValues extends FieldValues = FieldValues,
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>
>({
...props
}: ControllerProps<TFieldValues, TName>) => {
return (
<FormFieldContext.Provider value={{ name: props.name }}>
<Controller {...props} />
</FormFieldContext.Provider>
)
}
const useFormField = () => {
const fieldContext = React.useContext(FormFieldContext)
const itemContext = React.useContext(FormItemContext)
const { getFieldState, formState } = useFormContext()
const fieldState = getFieldState(fieldContext.name, formState)
if (!fieldContext) {
throw new Error("useFormField should be used within <FormField>")
}
const { id } = itemContext
return {
id,
name: fieldContext.name,
formItemId: `${id}-form-item`,
formDescriptionId: `${id}-form-item-description`,
formMessageId: `${id}-form-item-message`,
...fieldState,
}
}
type FormItemContextValue = {
id: string
}
const FormItemContext = React.createContext<FormItemContextValue>(
{} as FormItemContextValue
)
const FormItem = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => {
const id = React.useId()
return (
<FormItemContext.Provider value={{ id }}>
<div ref={ref} className={cn("space-y-2", className)} {...props} />
</FormItemContext.Provider>
)
})
FormItem.displayName = "FormItem"
const FormLabel = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
>(({ className, ...props }, ref) => {
const { error, formItemId } = useFormField()
return (
<Label
ref={ref}
className={cn(error && "text-destructive", className)}
htmlFor={formItemId}
{...props}
/>
)
})
FormLabel.displayName = "FormLabel"
const FormControl = React.forwardRef<
React.ElementRef<typeof Slot>,
React.ComponentPropsWithoutRef<typeof Slot>
>(({ ...props }, ref) => {
const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
return (
<Slot
ref={ref}
id={formItemId}
aria-describedby={
!error
? `${formDescriptionId}`
: `${formDescriptionId} ${formMessageId}`
}
aria-invalid={!!error}
{...props}
/>
)
})
FormControl.displayName = "FormControl"
const FormDescription = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, ...props }, ref) => {
const { formDescriptionId } = useFormField()
return (
<p
ref={ref}
id={formDescriptionId}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
)
})
FormDescription.displayName = "FormDescription"
const FormMessage = React.forwardRef<
HTMLParagraphElement,
React.HTMLAttributes<HTMLParagraphElement>
>(({ className, children, ...props }, ref) => {
const { error, formMessageId } = useFormField()
const body = error ? String(error?.message) : children
if (!body) {
return null
}
return (
<p
ref={ref}
id={formMessageId}
className={cn("text-sm font-medium text-destructive", className)}
{...props}
>
{body}
</p>
)
})
FormMessage.displayName = "FormMessage"
export {
useFormField,
Form,
FormItem,
FormLabel,
FormControl,
FormDescription,
FormMessage,
FormField,
}

View File

@ -0,0 +1,25 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> { }
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:transparent focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Input.displayName = "Input"
export { Input }

View File

@ -0,0 +1,26 @@
"use client"
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/lib/utils"
const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName
export { Label }

View File

@ -0,0 +1,116 @@
import * as React from "react"
import { ChevronLeft, ChevronRight, MoreHorizontal } from "lucide-react"
import { cn } from "@/lib/utils"
import { ButtonProps, buttonVariants } from "@/components/ui/button"
const Pagination = ({ className, ...props }: React.ComponentProps<"nav">) => (
<nav
role="navigation"
aria-label="pagination"
className={cn("mx-auto flex w-full justify-center", className)}
{...props}
/>
)
const PaginationContent = React.forwardRef<
HTMLUListElement,
React.ComponentProps<"ul">
>(({ className, ...props }, ref) => (
<ul
ref={ref}
className={cn("flex flex-row items-center gap-1", className)}
{...props}
/>
))
PaginationContent.displayName = "PaginationContent"
const PaginationItem = React.forwardRef<
HTMLLIElement,
React.ComponentProps<"li">
>(({ className, ...props }, ref) => (
<li ref={ref} className={cn("", className)} {...props} />
))
PaginationItem.displayName = "PaginationItem"
type PaginationLinkProps = {
isActive?: boolean
} & Pick<ButtonProps, "size"> &
React.ComponentProps<"a">
const PaginationLink = ({
className,
isActive,
size = "icon",
...props
}: PaginationLinkProps) => (
<PaginationItem>
<a
aria-current={isActive ? "page" : undefined}
className={cn(
buttonVariants({
variant: isActive ? "outline" : "ghost",
size,
}),
className
)}
{...props}
/>
</PaginationItem>
)
PaginationLink.displayName = "PaginationLink"
const PaginationPrevious = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to previous page"
size="default"
className={cn("gap-1 pl-2.5", className)}
{...props}
>
<ChevronLeft className="h-4 w-4" />
<span>Previous</span>
</PaginationLink>
)
PaginationPrevious.displayName = "PaginationPrevious"
const PaginationNext = ({
className,
...props
}: React.ComponentProps<typeof PaginationLink>) => (
<PaginationLink
aria-label="Go to next page"
size="default"
className={cn("gap-1 pr-2.5", className)}
{...props}
>
<span>Next</span>
<ChevronRight className="h-4 w-4" />
</PaginationLink>
)
const PaginationEllipsis = ({
className,
...props
}: React.ComponentProps<"span">) => (
<span
aria-hidden
className={cn("flex h-9 w-9 items-center justify-center", className)}
{...props}
>
<MoreHorizontal className="h-4 w-4" />
<span className="sr-only">More pages</span>
</span>
)
export {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
}

View File

@ -0,0 +1,48 @@
"use client"
import * as React from "react"
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
import { cn } from "@/lib/utils"
const ScrollArea = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.Root>
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport className="h-full w-full rounded-[inherit]">
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
))
ScrollArea.displayName = ScrollAreaPrimitive.Root.displayName
const ScrollBar = React.forwardRef<
React.ElementRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>,
React.ComponentPropsWithoutRef<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>
>(({ className, orientation = "vertical", ...props }, ref) => (
<ScrollAreaPrimitive.ScrollAreaScrollbar
ref={ref}
orientation={orientation}
className={cn(
"flex touch-none select-none transition-colors",
orientation === "vertical" &&
"h-full w-2.5 border-l border-l-transparent p-[1px]",
orientation === "horizontal" &&
"h-2.5 flex-col border-t border-t-transparent p-[1px]",
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb className="relative flex-1 rounded-full bg-border" />
</ScrollAreaPrimitive.ScrollAreaScrollbar>
))
ScrollBar.displayName = ScrollAreaPrimitive.ScrollAreaScrollbar.displayName
export { ScrollArea, ScrollBar }

View File

@ -0,0 +1,160 @@
"use client"
import * as React from "react"
import * as SelectPrimitive from "@radix-ui/react-select"
import { Check, ChevronDown, ChevronUp } from "lucide-react"
import { cn } from "@/lib/utils"
const Select = SelectPrimitive.Root
const SelectGroup = SelectPrimitive.Group
const SelectValue = SelectPrimitive.Value
const SelectTrigger = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Trigger>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Trigger
ref={ref}
className={cn(
"flex h-10 w-full items-center justify-between rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1",
className
)}
{...props}
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
const SelectScrollUpButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollUpButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollUpButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
const SelectScrollDownButton = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.ScrollDownButton>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.ScrollDownButton>
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn(
"flex cursor-default items-center justify-center py-1",
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName =
SelectPrimitive.ScrollDownButton.displayName
const SelectContent = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Content>
>(({ className, children, position = "popper", ...props }, ref) => (
<SelectPrimitive.Portal>
<SelectPrimitive.Content
ref={ref}
className={cn(
"relative z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
position === "popper" &&
"data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
className
)}
position={position}
{...props}
>
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
"p-1",
position === "popper" &&
"h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]"
)}
>
{children}
</SelectPrimitive.Viewport>
<SelectScrollDownButton />
</SelectPrimitive.Content>
</SelectPrimitive.Portal>
))
SelectContent.displayName = SelectPrimitive.Content.displayName
const SelectLabel = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Label>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Label
ref={ref}
className={cn("py-1.5 pl-8 pr-2 text-sm font-semibold", className)}
{...props}
/>
))
SelectLabel.displayName = SelectPrimitive.Label.displayName
const SelectItem = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Item>
>(({ className, children, ...props }, ref) => (
<SelectPrimitive.Item
ref={ref}
className={cn(
"relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-sm outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50",
className
)}
{...props}
>
<span className="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectPrimitive.ItemIndicator>
<Check className="h-4 w-4" />
</SelectPrimitive.ItemIndicator>
</span>
<SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
</SelectPrimitive.Item>
))
SelectItem.displayName = SelectPrimitive.Item.displayName
const SelectSeparator = React.forwardRef<
React.ElementRef<typeof SelectPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof SelectPrimitive.Separator>
>(({ className, ...props }, ref) => (
<SelectPrimitive.Separator
ref={ref}
className={cn("-mx-1 my-1 h-px bg-muted", className)}
{...props}
/>
))
SelectSeparator.displayName = SelectPrimitive.Separator.displayName
export {
Select,
SelectGroup,
SelectValue,
SelectTrigger,
SelectContent,
SelectLabel,
SelectItem,
SelectSeparator,
SelectScrollUpButton,
SelectScrollDownButton,
}

View File

@ -0,0 +1,31 @@
"use client"
import * as React from "react"
import * as SeparatorPrimitive from "@radix-ui/react-separator"
import { cn } from "@/lib/utils"
const Separator = React.forwardRef<
React.ElementRef<typeof SeparatorPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SeparatorPrimitive.Root>
>(
(
{ className, orientation = "horizontal", decorative = true, ...props },
ref
) => (
<SeparatorPrimitive.Root
ref={ref}
decorative={decorative}
orientation={orientation}
className={cn(
"shrink-0 bg-border",
orientation === "horizontal" ? "h-[1px] w-full" : "h-full w-[1px]",
className
)}
{...props}
/>
)
)
Separator.displayName = SeparatorPrimitive.Root.displayName
export { Separator }

View File

@ -0,0 +1,29 @@
"use client"
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"
import { cn } from "@/lib/utils"
const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-6 w-11 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-5 w-5 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-5 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName
export { Switch }

View File

@ -0,0 +1,55 @@
"use client"
import * as React from "react"
import * as TabsPrimitive from "@radix-ui/react-tabs"
import { cn } from "@/lib/utils"
const Tabs = TabsPrimitive.Root
const TabsList = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.List>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
>(({ className, ...props }, ref) => (
<TabsPrimitive.List
ref={ref}
className={cn(
"inline-flex h-10 items-center justify-center rounded-md bg-subsectionbackgroundcolor p-1 text-typography",
className,
)}
{...props}
/>
))
TabsList.displayName = TabsPrimitive.List.displayName
const TabsTrigger = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Trigger>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Trigger
ref={ref}
className={cn(
"inline-flex items-center justify-center text-typograhy whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:tgext-segmentedtabactive data-[state=active]:shadow-sm",
className,
)}
{...props}
/>
))
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
const TabsContent = React.forwardRef<
React.ElementRef<typeof TabsPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
>(({ className, ...props }, ref) => (
<TabsPrimitive.Content
ref={ref}
className={cn(
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
className,
)}
{...props}
/>
))
TabsContent.displayName = TabsPrimitive.Content.displayName
export { Tabs, TabsList, TabsTrigger, TabsContent }

View File

@ -0,0 +1,24 @@
import * as React from "react"
import { cn } from "@/lib/utils"
export interface TextareaProps
extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = "Textarea"
export { Textarea }

127
src/components/ui/toast.tsx Normal file
View File

@ -0,0 +1,127 @@
import * as React from "react"
import * as ToastPrimitives from "@radix-ui/react-toast"
import { cva, type VariantProps } from "class-variance-authority"
import { X } from "lucide-react"
import { cn } from "@/lib/utils"
const ToastProvider = ToastPrimitives.Provider
const ToastViewport = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Viewport>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Viewport>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Viewport
ref={ref}
className={cn(
"fixed top-0 z-[100] flex max-h-screen w-full flex-col-reverse p-4 sm:bottom-0 sm:right-0 sm:top-auto sm:flex-col md:max-w-[420px]",
className
)}
{...props}
/>
))
ToastViewport.displayName = ToastPrimitives.Viewport.displayName
const toastVariants = cva(
"group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full",
{
variants: {
variant: {
default: "border bg-background text-foreground",
destructive:
"destructive group border-destructive bg-destructive text-destructive-foreground",
},
},
defaultVariants: {
variant: "default",
},
}
)
const Toast = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Root> &
VariantProps<typeof toastVariants>
>(({ className, variant, ...props }, ref) => {
return (
<ToastPrimitives.Root
ref={ref}
className={cn(toastVariants({ variant }), className)}
{...props}
/>
)
})
Toast.displayName = ToastPrimitives.Root.displayName
const ToastAction = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Action>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Action>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Action
ref={ref}
className={cn(
"inline-flex h-8 shrink-0 items-center justify-center rounded-md border bg-transparent px-3 text-sm font-medium ring-offset-background transition-colors hover:bg-secondary focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 group-[.destructive]:border-muted/40 group-[.destructive]:hover:border-destructive/30 group-[.destructive]:hover:bg-destructive group-[.destructive]:hover:text-destructive-foreground group-[.destructive]:focus:ring-destructive",
className
)}
{...props}
/>
))
ToastAction.displayName = ToastPrimitives.Action.displayName
const ToastClose = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Close>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Close>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Close
ref={ref}
className={cn(
"absolute right-2 top-2 rounded-md p-1 text-foreground/50 opacity-0 transition-opacity hover:text-foreground focus:opacity-100 focus:outline-none focus:ring-2 group-hover:opacity-100 group-[.destructive]:text-red-300 group-[.destructive]:hover:text-red-50 group-[.destructive]:focus:ring-red-400 group-[.destructive]:focus:ring-offset-red-600",
className
)}
toast-close=""
{...props}
>
<X className="h-4 w-4" />
</ToastPrimitives.Close>
))
ToastClose.displayName = ToastPrimitives.Close.displayName
const ToastTitle = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Title>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Title>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Title
ref={ref}
className={cn("text-sm font-semibold", className)}
{...props}
/>
))
ToastTitle.displayName = ToastPrimitives.Title.displayName
const ToastDescription = React.forwardRef<
React.ElementRef<typeof ToastPrimitives.Description>,
React.ComponentPropsWithoutRef<typeof ToastPrimitives.Description>
>(({ className, ...props }, ref) => (
<ToastPrimitives.Description
ref={ref}
className={cn("text-sm opacity-90", className)}
{...props}
/>
))
ToastDescription.displayName = ToastPrimitives.Description.displayName
type ToastProps = React.ComponentPropsWithoutRef<typeof Toast>
type ToastActionElement = React.ReactElement<typeof ToastAction>
export {
type ToastProps,
type ToastActionElement,
ToastProvider,
ToastViewport,
Toast,
ToastTitle,
ToastDescription,
ToastClose,
ToastAction,
}

View File

@ -0,0 +1,35 @@
"use client"
import {
Toast,
ToastClose,
ToastDescription,
ToastProvider,
ToastTitle,
ToastViewport,
} from "@/components/ui/toast"
import { useToast } from "@/components/ui/use-toast"
export function Toaster() {
const { toasts } = useToast()
return (
<ToastProvider>
{toasts.map(function ({ id, title, description, action, ...props }) {
return (
<Toast key={id} {...props}>
<div className="grid gap-1">
{title && <ToastTitle>{title}</ToastTitle>}
{description && (
<ToastDescription>{description}</ToastDescription>
)}
</div>
{action}
<ToastClose />
</Toast>
)
})}
<ToastViewport />
</ToastProvider>
)
}

View File

@ -0,0 +1,192 @@
// Inspired by react-hot-toast library
import * as React from "react"
import type {
ToastActionElement,
ToastProps,
} from "@/components/ui/toast"
const TOAST_LIMIT = 1
const TOAST_REMOVE_DELAY = 1000000
type ToasterToast = ToastProps & {
id: string
title?: React.ReactNode
description?: React.ReactNode
action?: ToastActionElement
}
const actionTypes = {
ADD_TOAST: "ADD_TOAST",
UPDATE_TOAST: "UPDATE_TOAST",
DISMISS_TOAST: "DISMISS_TOAST",
REMOVE_TOAST: "REMOVE_TOAST",
} as const
let count = 0
function genId() {
count = (count + 1) % Number.MAX_SAFE_INTEGER
return count.toString()
}
type ActionType = typeof actionTypes
type Action =
| {
type: ActionType["ADD_TOAST"]
toast: ToasterToast
}
| {
type: ActionType["UPDATE_TOAST"]
toast: Partial<ToasterToast>
}
| {
type: ActionType["DISMISS_TOAST"]
toastId?: ToasterToast["id"]
}
| {
type: ActionType["REMOVE_TOAST"]
toastId?: ToasterToast["id"]
}
interface State {
toasts: ToasterToast[]
}
const toastTimeouts = new Map<string, ReturnType<typeof setTimeout>>()
const addToRemoveQueue = (toastId: string) => {
if (toastTimeouts.has(toastId)) {
return
}
const timeout = setTimeout(() => {
toastTimeouts.delete(toastId)
dispatch({
type: "REMOVE_TOAST",
toastId: toastId,
})
}, TOAST_REMOVE_DELAY)
toastTimeouts.set(toastId, timeout)
}
export const reducer = (state: State, action: Action): State => {
switch (action.type) {
case "ADD_TOAST":
return {
...state,
toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
}
case "UPDATE_TOAST":
return {
...state,
toasts: state.toasts.map((t) =>
t.id === action.toast.id ? { ...t, ...action.toast } : t
),
}
case "DISMISS_TOAST": {
const { toastId } = action
// ! Side effects ! - This could be extracted into a dismissToast() action,
// but I'll keep it here for simplicity
if (toastId) {
addToRemoveQueue(toastId)
} else {
state.toasts.forEach((toast) => {
addToRemoveQueue(toast.id)
})
}
return {
...state,
toasts: state.toasts.map((t) =>
t.id === toastId || toastId === undefined
? {
...t,
open: false,
}
: t
),
}
}
case "REMOVE_TOAST":
if (action.toastId === undefined) {
return {
...state,
toasts: [],
}
}
return {
...state,
toasts: state.toasts.filter((t) => t.id !== action.toastId),
}
}
}
const listeners: Array<(state: State) => void> = []
let memoryState: State = { toasts: [] }
function dispatch(action: Action) {
memoryState = reducer(memoryState, action)
listeners.forEach((listener) => {
listener(memoryState)
})
}
type Toast = Omit<ToasterToast, "id">
function toast({ ...props }: Toast) {
const id = genId()
const update = (props: ToasterToast) =>
dispatch({
type: "UPDATE_TOAST",
toast: { ...props, id },
})
const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
dispatch({
type: "ADD_TOAST",
toast: {
...props,
id,
open: true,
onOpenChange: (open) => {
if (!open) dismiss()
},
},
})
return {
id: id,
dismiss,
update,
}
}
function useToast() {
const [state, setState] = React.useState<State>(memoryState)
React.useEffect(() => {
listeners.push(setState)
return () => {
const index = listeners.indexOf(setState)
if (index > -1) {
listeners.splice(index, 1)
}
}
}, [state])
return {
...state,
toast,
dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
}
}
export { useToast, toast }

71
src/lib/api.ts Normal file
View File

@ -0,0 +1,71 @@
import { toast } from "react-toastify";
const BASE_OPTIONS = {
credentials: "include",
};
const BASE_URL = process.env.NEXT_PUBLIC_ARTEMIS_API_URL;
if (!BASE_URL) {
throw new Error("Cannot run -- no NEXT_PUBLIC_ARTEMIS_API_URL provided?");
}
export function ToApiUrl(url: string) {
if (url[0] === "/") {
url = url.substring(1)
}
return `${BASE_URL}/${url}`;
}
export type ApiFetchReturn<T> = {
body: T,
statusCode: number;
}
export async function ApiFetch<T = unknown>(
url: string,
options: RequestInit = {},
displaySuccess = false,
displayFailure = false,
): Promise<ApiFetchReturn<T>> {
const mergedOptions = Object.assign({}, BASE_OPTIONS, options);
try {
const res = await fetch(ToApiUrl(url), mergedOptions);
// Some endpoints don't return JSON.
const rt = await res.text();
const rj = (rt[0] === "{" || rt[0] === "[") ? JSON.parse(rt) : null;
if (!res.ok) {
const errorMessage = rj?.message ?? rj?.error ?? rt;
console.warn(errorMessage);
if (displayFailure) {
toast.error(errorMessage, {
className: "error-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
});
}
}
if (res.ok && displaySuccess && rj?.message) {
toast.success(rj.message, {
className: "success-toast",
bodyClassName: "toast-body",
closeButton: false,
theme: "colored",
})
}
return { body: rj ?? rt, statusCode: res.status };
} catch (err) {
console.error(err);
throw err;
}
}

7
src/lib/auth.ts Normal file
View File

@ -0,0 +1,7 @@
import { ApiFetch } from "./api";
export async function IsLoggedIn() {
const response = await ApiFetch("/SDHD/user");
return response.statusCode == 200;
}

113
src/lib/types.ts Normal file
View File

@ -0,0 +1,113 @@
export interface ScoreCardProps {
userPlayDate: string
title: string
rating_change: string
genre: string
score: number
isNewRecord: number
isAllJustice: number
judgeJustice: number
judgeAttack: number
chartId: number
judgeGuilty: number
artist: string
judgeHeaven: number
judgeCritical: number
level: string
isClear: number
isFullCombo: number
maxCombo: number
rating: number
jacketPath: string
}
export type PlayerData = {
trophyId: number
itemId: string
name: string
}
export type PlayerTrophiesData = {
name: string
itemId: string
rareType: number
trophyId: string
isClear: number
isAllJustice: number
}
export type DropdownOption = {
name: string
itemId: string
rareType: number
trophyId: string
}
export type DropdownProps = {
options: DropdownOption[]
onChange: (id: string) => void
label: string
trophyId: number
}
export type PlayerTeamData = {
teamName: string
id: number
teamPoint: number
}
export type PlayerVoiceData = {
voiceId: string
}
export type DropdownVoiceOption = {
str: string
id: string
}
export type DropdownVoiceProps = {
options: DropdownVoiceOption[]
onChange: (id: string) => void
str: string
id: string
}
export type RivalData = {
user1: string
rivalCode: string
version: number
}
export type PlayerNameplateData = {
nameplateId: string
}
export type Nameplate = {
nameplateId: string
str: string
imagePath: string
}
export type DropdownNameplateOption = {
str: string
nameplateId: string
imagePath: string
}
export type DropdowNameplateProps = {
options: DropdownNameplateOption[]
onChange: (nameplateId: string) => void
str: string
id: string
}
export interface songCardProps {
title: string
songId: number
artist: string
jacketPath: string
}
export interface JacketImageProps {
jacketPath: string
}

View File

@ -0,0 +1,9 @@
interface UserDataResponse {
body: UserData;
}
interface UserData {
userId: number;
version: number;
rivalCode: number;
}

92
src/lib/utils.ts Normal file
View File

@ -0,0 +1,92 @@
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
}
// Helper functions
export const getDifficultyClass = (difficulty: number) => {
if (difficulty === undefined || difficulty === null) {
// Handle undefined or null difficulty
// You can return a default class or handle the error as needed
return "default-class" // Replace with an appropriate default class or error handling
}
switch (difficulty.toString()) {
case "0":
return "bg-basicbadgecolor" // Green for basic
case "1":
return "bg-advancebadgecolor" // Yellow for advance
case "2":
return "bg-expertbadgecolor" // Red for expert
case "3":
return "bg-masterbadgecolor " // Purple for master
case "4":
return "bg-[radial-gradient(ellipse_at_top_left,_var(--tw-gradient-stops))] from-black via-red to-black"
case "5":
return "bg-worldsendbadgecolor" // worlds end
default:
return "default-class" // Replace with an appropriate default class for unknown difficulty
}
}
export const getGrade = (score: number) => {
if (score >= 1009000) return "SSS+"
if (score >= 1007500 && score <= 1008999) return "SSS"
if (score >= 1005000 && score <= 1007499) return "SS+"
if (score >= 1000000 && score <= 1004999) return "SS"
if (score >= 990000 && score <= 999999) return "S+"
if (score >= 975000 && score <= 990000) return "S"
if (score >= 950000 && score <= 974999) return "AAA"
if (score >= 925000 && score <= 949999) return "AA"
if (score >= 900000 && score <= 924999) return "A"
if (score >= 800000 && score <= 899999) return "BBB"
if (score >= 700000 && score <= 799999) return "BB"
if (score >= 600000 && score <= 699999) return "B"
if (score >= 500000 && score <= 599999) return "C"
if (score < 500000) return "D"
return ""
}
export const getDifficultyText = (chartId: number) => {
switch (chartId) {
case 0:
return "EASY" // Text for difficulty
case 1:
return "ADVANCE"
case 2:
return "EXPERT"
case 3:
return "MASTER"
case 4:
return "ULTIMA"
case 5:
return "WORLDS END"
}
}
export const getRatingChangeColor = (ratingChange: string) => {
switch (ratingChange) {
case "Increase":
return "text-ratingincrease" // Green for Increase
case "Same":
return "text-ratingsame" // Blue for Same
case "Decrease":
return "text-ratingdecrease" // Red for Decrease
default:
return "dark:text-gray" // Default color
}
}
export interface JacketImageProps {
jacketPath: string
}
export interface NavigationButtonsProps {
currentPage: number
totalPages: number
onPageChange: (page: number) => void
onNext: () => void
onPrevious: () => void
}

245
tailwind.config.ts Normal file
View File

@ -0,0 +1,245 @@
import type { Config } from "tailwindcss";
const config = {
darkMode: ["class"],
content: [
"./pages/**/*.{ts,tsx}",
"./components/**/*.{ts,tsx}",
"./app/**/*.{ts,tsx}",
"./src/**/*.{ts,tsx}",
],
prefix: "",
theme: {
container: {
center: true,
padding: "2rem",
screens: {
"2xl": "1400px",
},
},
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
popover: {
DEFAULT: "hsl(var(--popover))",
foreground: "hsl(var(--popover-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
typography: {
DEFAULT: "hsl(var(--typography))",
foreground: "hsl(var(--typography-foreground))",
},
scorecardbadge: {
DEFAULT: "hsl(var(--scorecardbadge))",
foreground: "hsl(var(--scorecardbadge-foreground))",
},
playertrophybadge: {
DEFAULT: "hsl(var(--playertrophybadge))",
foreground: "hsl(var(--playertrophybadge-foreground))",
},
characterbadgetypography: {
DEFAULT: "hsl(var(--characterbadgetypography))",
foreground: "hsl(var(--characterbadgetypography-foreground))",
},
ratingsame: {
DEFAULT: "hsl(var(--ratingsame))",
foreground: "hsl(var(--ratingsame-foreground))",
},
ratingincrease: {
DEFAULT: "hsl(var(--ratingincrease))",
foreground: "hsl(var(--ratingincrease-foreground))",
},
ratingdecrease: {
DEFAULT: "hsl(var(--ratingdecrease))",
foreground: "hsl(var(--ratingdecrease-foreground))",
},
expertbadgecolor: {
DEFAULT: "hsl(var(--expertbadgecolor))",
foreground: "hsl(var(--expertbadgecolor-foreground))",
},
masterbadgecolor: {
DEFAULT: "hsl(var(--masterbadgecolor))",
foreground: "hsl(var(--masterbadgecolor-foreground))",
},
basicbadgecolor: {
DEFAULT: "hsl(var(--basicbadgecolor))",
foreground: "hsl(var(--basicbadgecolor-foreground))",
},
advancebadgecolor: {
DEFAULT: "hsl(var(--advancebadgecolor))",
foreground: "hsl(var(--advancebadgecolor-foreground))",
},
worldsendbadgecolor: {
DEFAULT: "hsl(var(--worldsendbadgecolor))",
foreground: "hsl(var(--worldsendbadgecolor-foreground))",
},
newbadgecolor: {
DEFAULT: "hsl(var(--newbadgecolor))",
foreground: "hsl(var(--newbadgecolor-foreground))",
},
scoretext: {
DEFAULT: "hsl(var(--scoretext))",
foreground: "hsl(var(--scoretext-foreground))",
},
justicecriticaltext: {
DEFAULT: "hsl(var(--justicecriticaltext))",
foreground: "hsl(var(--justicecriticaltext-foreground))",
},
justicetext: {
DEFAULT: "hsl(var(--justicetext))",
foreground: "hsl(var(--justicetext-foreground))",
},
attacktext: {
DEFAULT: "hsl(var(--attacktext))",
foreground: "hsl(var(--attacktext-foreground))",
},
misstext: {
DEFAULT: "hsl(var(--misstext))",
foreground: "hsl(var(--misstext-foreground))",
},
segmentedtabnotactive: {
DEFAULT: "hsl(var(--segmentedtabnotactive))",
foreground: "hsl(var(--segmentedtabnotactive-foreground))",
},
segmentedtabactive: {
DEFAULT: "hsl(var(--segmentedtabactive))",
foreground: "hsl(var(--segmentedtabactive-foreground))",
},
segmentedtabtext: {
DEFAULT: "hsl(var(--segmentedtabtext))",
foreground: "hsl(var(--segmentedtabtext-foreground))",
},
songjacketborder: {
DEFAULT: "hsl(var(--songjacketborder))",
foreground: "hsl(var(--songjacketborder-foreground))",
},
buttonbackgroundcolor: {
DEFAULT: "hsl(var(--buttonbackgroundcolor))",
foreground: "hsl(var(--buttonbackgroundcolor-foreground))",
},
subsectionbackgroundcolor: {
DEFAULT: "hsl(var(--subsectionbackgroundcolor))",
foreground: "hsl(var(--subsectionbackgroundcolor-foreground))",
},
typographydropdown: {
DEFAULT: "hsl(var(--typographydropdown))",
foreground: "hsl(var(--typographydropdown-foreground))",
},
buttonhovercolor: {
DEFAULT: "hsl(var(--buttonhovercolor))",
foreground: "hsl(var(--buttonhovercolor-foreground))",
},
cardsectionbackgroundcolor: {
DEFAULT: "hsl(var(--cardsectionbackgroundcolor))",
foreground: "hsl(var(--cardsectionbackgroundcolor-foreground))",
},
scorecardbadgetypography: {
DEFAULT: "hsl(var(--scorecardbadgetypography))",
foreground: "hsl(var(--scorecardbadgetypography-foreground))",
},
paginationtypography: {
DEFAULT: "hsl(var(--paginationtypography))",
foreground: "hsl(var(--paginationtypography-foreground))",
},
paginationhover: {
DEFAULT: "hsl(var(--paginationhover))",
foreground: "hsl(var(--paginationhover-foreground))",
},
paginationbackground: {
DEFAULT: "hsl(var(--paginationbackground))",
foreground: "hsl(var(--paginationbackground-foreground))",
},
paginationtypographyhover: {
DEFAULT: "hsl(var(--paginationtypographyhover))",
foreground: "hsl(var(--paginationtypographyhover-foreground))",
},
segmentedtabactivetypography: {
DEFAULT: "hsl(var(--segmentedtabactivetypography))",
foreground: "hsl(var(--segmentedtabactivetypography-foreground))",
},
bestrecentratingcolor: {
DEFAULT: "hsl(var(--bestrecentratingcolor))",
foreground: "hsl(var(--bestrecentratingcolor-foreground))",
},
heartcolorfave: {
DEFAULT: "hsl(var(--heartcolorfave))",
foreground: "hsl(var(--heartcolorfave-foreground))",
},
heartcolornormal: {
DEFAULT: "hsl(var(--heartcolornormal))",
foreground: "hsl(var(--heartcolornormal-foreground))",
},
playertrophytext: {
DEFAULT: "hsl(var(--playertrophytext))",
foreground: "hsl(var(--playertrophytext-foreground))",
},
segmentedtabtexthover: {
DEFAULT: "hsl(var(--segmentedtabtexthover))",
foreground: "hsl(var(--segmentedtabtexthover-foreground))",
},
texthover: {
DEFAULT: "hsl(var(--texthover))",
foreground: "hsl(var(--texthover-foreground))",
},
playerteambadge: {
DEFAULT: "hsl(var(--playerteambadge))",
foreground: "hsl(var(--playerteambadge-foreground))",
},
buttontexthovercolor: {
DEFAULT: "hsl(var(--buttontexthovercolor))",
foreground: "hsl(var(--buttontexthovercolor-foreground))",
},
},
borderRadius: {
lg: "var(--radius)",
md: "calc(var(--radius) - 2px)",
sm: "calc(var(--radius) - 4px)",
},
keyframes: {
"accordion-down": {
from: { height: "0" },
to: { height: "var(--radix-accordion-content-height)" },
},
"accordion-up": {
from: { height: "var(--radix-accordion-content-height)" },
to: { height: "0" },
},
},
animation: {
"accordion-down": "accordion-down 0.2s ease-out",
"accordion-up": "accordion-up 0.2s ease-out",
},
},
},
plugins: [require("tailwindcss-animate")],
} satisfies Config;
export default config;

33
tsconfig.json Normal file
View File

@ -0,0 +1,33 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"]
}
},
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx",
".next/types/**/*.ts",
"src/components/shared/NavigationBar"
],
"exclude": ["node_modules"]
}