updates nothing special
This commit is contained in:
commit
2fd992b8f1
135
.gitignore
vendored
Normal file
135
.gitignore
vendored
Normal 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
100
README.md
Normal 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
62
README.zh_tw.md
Normal 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
17
components.json
Normal 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
5
next-env.d.ts
vendored
Normal 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
30
next.config.js
Normal 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
7170
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
69
package.json
Normal file
69
package.json
Normal 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
6
postcss.config.js
Normal file
|
@ -0,0 +1,6 @@
|
|||
module.exports = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
autoprefixer: {},
|
||||
},
|
||||
}
|
165
pythonscripts/database_builder.py
Normal file
165
pythonscripts/database_builder.py
Normal 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()
|
159
pythonscripts/imagegrabber.py
Normal file
159
pythonscripts/imagegrabber.py
Normal 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)
|
944
src/app/axios/useFetchApi.tsx
Normal file
944
src/app/axios/useFetchApi.tsx
Normal 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])
|
||||
}
|
528
src/app/axios/usePostApi.tsx
Normal file
528
src/app/axios/usePostApi.tsx
Normal 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 }
|
||||
}
|
35
src/app/chunithm/layout.tsx
Normal file
35
src/app/chunithm/layout.tsx
Normal 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
20
src/app/chunithm/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
53
src/app/context/SettingsContext.tsx
Normal file
53
src/app/context/SettingsContext.tsx
Normal 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
BIN
src/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 25 KiB |
35
src/app/favorites/layout.tsx
Normal file
35
src/app/favorites/layout.tsx
Normal 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>
|
||||
}
|
13
src/app/favorites/page.tsx
Normal file
13
src/app/favorites/page.tsx
Normal 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
494
src/app/globals.css
Normal 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
38
src/app/layout.tsx
Normal 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>
|
||||
)
|
||||
}
|
35
src/app/management/layout.tsx
Normal file
35
src/app/management/layout.tsx
Normal 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>
|
||||
}
|
20
src/app/management/page.tsx
Normal file
20
src/app/management/page.tsx
Normal 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
201
src/app/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
35
src/app/settings/layout.tsx
Normal file
35
src/app/settings/layout.tsx
Normal 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
14
src/app/settings/page.tsx
Normal 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
251
src/app/signup/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
35
src/app/userbox/layout.tsx
Normal file
35
src/app/userbox/layout.tsx
Normal 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
58
src/app/userbox/page.tsx
Normal 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
|
168
src/components/CharacterCard.tsx
Normal file
168
src/components/CharacterCard.tsx
Normal 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
|
229
src/components/CharacterCustomization.tsx
Normal file
229
src/components/CharacterCustomization.tsx
Normal 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
|
92
src/components/PlayerAimeCardUpdater.tsx
Normal file
92
src/components/PlayerAimeCardUpdater.tsx
Normal 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
|
228
src/components/PlayerFavoritesList.tsx
Normal file
228
src/components/PlayerFavoritesList.tsx
Normal 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
|
182
src/components/PlayerMapIcon.tsx
Normal file
182
src/components/PlayerMapIcon.tsx
Normal 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
|
160
src/components/PlayerNameplate.tsx
Normal file
160
src/components/PlayerNameplate.tsx
Normal 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
|
80
src/components/PlayerPasswordReset.tsx
Normal file
80
src/components/PlayerPasswordReset.tsx
Normal 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>
|
||||
)
|
||||
}
|
134
src/components/PlayerRivals.tsx
Normal file
134
src/components/PlayerRivals.tsx
Normal 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
|
289
src/components/PlayerScoreGrid.tsx
Normal file
289
src/components/PlayerScoreGrid.tsx
Normal 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}
|
||||
{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
|
47
src/components/PlayerSettings.tsx
Normal file
47
src/components/PlayerSettings.tsx
Normal 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
|
164
src/components/PlayerSystemVoice.tsx
Normal file
164
src/components/PlayerSystemVoice.tsx
Normal 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
|
143
src/components/PlayerTeams.tsx
Normal file
143
src/components/PlayerTeams.tsx
Normal 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
|
177
src/components/PlayerTopBestScores.tsx
Normal file
177
src/components/PlayerTopBestScores.tsx
Normal 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
|
108
src/components/PlayerTrophy.tsx
Normal file
108
src/components/PlayerTrophy.tsx
Normal 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
|
59
src/components/PlayerViewCurrentAimeCard.tsx
Normal file
59
src/components/PlayerViewCurrentAimeCard.tsx
Normal 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
|
164
src/components/PlayerWhiteListKeychips.tsx
Normal file
164
src/components/PlayerWhiteListKeychips.tsx
Normal 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
|
46
src/components/darkmodetoggle.tsx
Normal file
46
src/components/darkmodetoggle.tsx
Normal 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>
|
||||
)
|
||||
}
|
157
src/components/shared/NavigationBar/NavigationBar.tsx
Normal file
157
src/components/shared/NavigationBar/NavigationBar.tsx
Normal 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
|
103
src/components/shared/audioPlayer/page.tsx
Normal file
103
src/components/shared/audioPlayer/page.tsx
Normal 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
|
7
src/components/shared/cookieProvider/Page.tsx
Normal file
7
src/components/shared/cookieProvider/Page.tsx
Normal file
|
@ -0,0 +1,7 @@
|
|||
"use client"
|
||||
|
||||
import { CookiesProvider } from "next-client-cookies"
|
||||
|
||||
export const ClientCookiesProvider: typeof CookiesProvider = (props) => (
|
||||
<CookiesProvider {...props} />
|
||||
)
|
9
src/components/theme-provider.tsx
Normal file
9
src/components/theme-provider.tsx
Normal 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>
|
||||
}
|
36
src/components/ui/badge.tsx
Normal file
36
src/components/ui/badge.tsx
Normal 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 }
|
56
src/components/ui/button.tsx
Normal file
56
src/components/ui/button.tsx
Normal 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 }
|
79
src/components/ui/card.tsx
Normal file
79
src/components/ui/card.tsx
Normal 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 }
|
200
src/components/ui/dropdown-menu.tsx
Normal file
200
src/components/ui/dropdown-menu.tsx
Normal 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
176
src/components/ui/form.tsx
Normal 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,
|
||||
}
|
25
src/components/ui/input.tsx
Normal file
25
src/components/ui/input.tsx
Normal 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 }
|
26
src/components/ui/label.tsx
Normal file
26
src/components/ui/label.tsx
Normal 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 }
|
116
src/components/ui/pagination.tsx
Normal file
116
src/components/ui/pagination.tsx
Normal 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,
|
||||
}
|
48
src/components/ui/scroll-area.tsx
Normal file
48
src/components/ui/scroll-area.tsx
Normal 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 }
|
160
src/components/ui/select.tsx
Normal file
160
src/components/ui/select.tsx
Normal 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,
|
||||
}
|
31
src/components/ui/separator.tsx
Normal file
31
src/components/ui/separator.tsx
Normal 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 }
|
29
src/components/ui/switch.tsx
Normal file
29
src/components/ui/switch.tsx
Normal 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 }
|
55
src/components/ui/tabs.tsx
Normal file
55
src/components/ui/tabs.tsx
Normal 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 }
|
24
src/components/ui/textarea.tsx
Normal file
24
src/components/ui/textarea.tsx
Normal 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
127
src/components/ui/toast.tsx
Normal 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,
|
||||
}
|
35
src/components/ui/toaster.tsx
Normal file
35
src/components/ui/toaster.tsx
Normal 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>
|
||||
)
|
||||
}
|
192
src/components/ui/use-toast.ts
Normal file
192
src/components/ui/use-toast.ts
Normal 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
71
src/lib/api.ts
Normal 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
7
src/lib/auth.ts
Normal 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
113
src/lib/types.ts
Normal 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
|
||||
}
|
9
src/lib/types/user-data.ts
Normal file
9
src/lib/types/user-data.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
interface UserDataResponse {
|
||||
body: UserData;
|
||||
}
|
||||
|
||||
interface UserData {
|
||||
userId: number;
|
||||
version: number;
|
||||
rivalCode: number;
|
||||
}
|
92
src/lib/utils.ts
Normal file
92
src/lib/utils.ts
Normal 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
245
tailwind.config.ts
Normal 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
33
tsconfig.json
Normal 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"]
|
||||
}
|
Reference in New Issue
Block a user