├── src
├── react-app-env.d.ts
├── input.css
├── config.ts
├── types
│ ├── image.ts
│ ├── button.ts
│ ├── item.ts
│ ├── ranking.ts
│ ├── votingResult.ts
│ └── filter.ts
├── setupTests.ts
├── axiosConfig.ts
├── App.test.tsx
├── utils
│ ├── SendVoting.ts
│ ├── GetMyRank.ts
│ ├── GetItems.ts
│ ├── GetRanking.ts
│ ├── GetItemRank.ts
│ └── PVote.ts
├── index.css
├── components
│ ├── Error.tsx
│ ├── BackTo.tsx
│ ├── Title.tsx
│ ├── UI
│ │ ├── Box.tsx
│ │ ├── Button.tsx
│ │ └── Image.tsx
│ ├── Attention.tsx
│ ├── RankingVS.tsx
│ ├── Rule.tsx
│ ├── ChooseButton.tsx
│ ├── MyData.tsx
│ ├── Rank.tsx
│ ├── Filter.tsx
│ ├── ItemRank.tsx
│ └── PreciseVote.tsx
├── reportWebVitals.ts
├── index.tsx
├── App.css
├── logo.svg
├── App.tsx
└── output.css
├── public
├── ys.webp
├── robots.txt
├── images
│ ├── bg.webp
│ ├── db.webp
│ ├── md.webp
│ ├── vs.webp
│ ├── wl.webp
│ ├── lhh.webp
│ ├── title.webp
│ └── lanren.webp
├── manifest.json
└── index.html
├── .env.development
├── .env.production
├── README.md
├── .gitignore
├── tsconfig.json
├── tailwind.config.js
└── package.json
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/public/ys.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/ys.webp
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/.env.development:
--------------------------------------------------------------------------------
1 | REACT_APP_BACKEND_URL=http://localhost:8080
2 | REACT_APP_FRONTEND_URL=http://localhost:3000
--------------------------------------------------------------------------------
/public/images/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/bg.webp
--------------------------------------------------------------------------------
/public/images/db.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/db.webp
--------------------------------------------------------------------------------
/public/images/md.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/md.webp
--------------------------------------------------------------------------------
/public/images/vs.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/vs.webp
--------------------------------------------------------------------------------
/public/images/wl.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/wl.webp
--------------------------------------------------------------------------------
/src/input.css:
--------------------------------------------------------------------------------
1 | /* src/index.css */
2 | @tailwind base;
3 | @tailwind components;
4 | @tailwind utilities;
5 |
--------------------------------------------------------------------------------
/.env.production:
--------------------------------------------------------------------------------
1 | REACT_APP_BACKEND_URL=https://vote.qiuy.cloud
2 | REACT_APP_FRONTEND_URL=https://vote.qiuy.cloud
--------------------------------------------------------------------------------
/public/images/lhh.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/lhh.webp
--------------------------------------------------------------------------------
/public/images/title.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/title.webp
--------------------------------------------------------------------------------
/public/images/lanren.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Qiuarctica/ys-voting-frontend/HEAD/public/images/lanren.webp
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # 以撒的结合道具投票箱
2 |
3 | 前端部分:React + Typescript + TailwindCSS
4 |
5 |
6 | # TODO
7 |
8 | + 道具池筛选机制
9 |
10 | + 回到顶部
11 |
12 | + 加一个看板(娘?)评价选择,太变态了考虑召唤马手抓走你的鼠标
--------------------------------------------------------------------------------
/src/config.ts:
--------------------------------------------------------------------------------
1 | const BACKEN_URL = process.env.REACT_APP_BACKEND_URL || "http://localhost:8080";
2 | const FRONTEND_URL = process.env.REACT_APP_FRONTEND_URL || "http://localhost:3000";
3 |
4 | export { BACKEN_URL, FRONTEND_URL };
--------------------------------------------------------------------------------
/src/types/image.ts:
--------------------------------------------------------------------------------
1 | export interface image {
2 | url: string; // 图片的url
3 | alt?: string; // 图片的alt
4 | width?: number; // 图片的宽度
5 | height?: number; // 图片的高度
6 | name?: string; // 图片的名字(可选)
7 | description: string; // 图片的描述(可选)
8 | }
--------------------------------------------------------------------------------
/src/setupTests.ts:
--------------------------------------------------------------------------------
1 | // jest-dom adds custom jest matchers for asserting on DOM nodes.
2 | // allows you to do things like:
3 | // expect(element).toHaveTextContent(/react/i)
4 | // learn more: https://github.com/testing-library/jest-dom
5 | import '@testing-library/jest-dom';
6 |
--------------------------------------------------------------------------------
/src/types/button.ts:
--------------------------------------------------------------------------------
1 | // 按钮的props
2 |
3 | export interface button{
4 | text : string; // 按钮上的文字
5 | onClick : (() => void); // 点击按钮的回调
6 | className? : string; // 按钮的额外类名(可选)
7 | backGround? : string; // 按钮的背景颜色(可选)
8 | disabled? : boolean; // 按钮是否不可用(可选)
9 | }
--------------------------------------------------------------------------------
/src/types/item.ts:
--------------------------------------------------------------------------------
1 | // 道具的类型
2 |
3 | export interface item {
4 | id: number; // 道具的id
5 | name: string; // 道具的名字
6 | url: string; // 道具的图片url(可选)
7 | quality: number; // 道具的品质
8 | description: string; // 道具的描述
9 | filternum: number; // 筛选后的道具数量
10 | }
11 |
12 |
--------------------------------------------------------------------------------
/src/axiosConfig.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 | import { BACKEN_URL } from './config';
3 |
4 | // 创建一个 axios 实例
5 | const axiosInstance = axios.create({
6 | withCredentials: true, // 确保每次请求都发送 cookie
7 | baseURL: BACKEN_URL, // 替换为你的 API 基础 URL
8 | });
9 |
10 | export default axiosInstance;
--------------------------------------------------------------------------------
/src/App.test.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { render, screen } from '@testing-library/react';
3 | import App from './App';
4 |
5 | test('renders learn react link', () => {
6 | render();
7 | const linkElement = screen.getByText(/learn react/i);
8 | expect(linkElement).toBeInTheDocument();
9 | });
10 |
--------------------------------------------------------------------------------
/src/types/ranking.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface ranking {
3 | rank: number; // 排名
4 | name: string; // 名字
5 | score: number; // 分数
6 | winpercent: number; // 胜率
7 | totals: number; // 总数
8 | }
9 |
10 | export interface itemRank {
11 | id: number;
12 | name: string;
13 | total: number;
14 | wincount: number;
15 | winrate: number;
16 | }
--------------------------------------------------------------------------------
/src/utils/SendVoting.ts:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../axiosConfig";
2 | import { vote } from "../types/votingResult";
3 | import { BACKEN_URL } from "../config";
4 |
5 | export async function SendVoting(result: vote): Promise {
6 |
7 | const response = await axiosInstance.post(`${BACKEN_URL}/api/vote/sendVoting`, result);
8 | return response.data;
9 |
10 | }
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
5 | sans-serif;
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | }
9 |
10 | code {
11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
12 | monospace;
13 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 |
25 | deploy.sh
--------------------------------------------------------------------------------
/src/types/votingResult.ts:
--------------------------------------------------------------------------------
1 | // export interface votingResult {
2 | // type: string; // 投票的类型 (道具/人物)
3 | // winner: number; // 胜利者的id
4 | // loser: number; // 失败者的id
5 | // filterNum: number; // 过滤后道具的数量
6 | // }
7 |
8 | // resulte枚举
9 |
10 | export enum VOTEResult {
11 | LEFT = 1,
12 | RIGHT = 2,
13 | NOBODY = 3,
14 | SKIP = 4
15 | }
16 | export interface vote {
17 | result: VOTEResult // 投票的类型 1为左边 2为右边 3为全输 4为跳过
18 | }
--------------------------------------------------------------------------------
/src/components/Error.tsx:
--------------------------------------------------------------------------------
1 | import Box from "./UI/Box";
2 |
3 | export default function Error(props: { error: string, onClick: () => void }) {
4 | const { error, onClick } = props;
5 | return (
6 |
7 |
11 |
12 |
13 | );
14 | }
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from 'web-vitals';
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/components/BackTo.tsx:
--------------------------------------------------------------------------------
1 | export interface BackToProps {
2 | to: string;
3 | toClick: () => void;
4 | }
5 |
6 | export default function BackTo(props: { backs: BackToProps[] }) {
7 | return (
8 |
9 | {props.backs.map((back, index) => (
10 |
13 | ))}
14 |
15 | );
16 | }
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "ys.webp",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/webp"
9 | },
10 | {
11 | "src": "ys.webp",
12 | "type": "image/webp",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "ys.webp",
17 | "type": "image/webp",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
--------------------------------------------------------------------------------
/src/components/Title.tsx:
--------------------------------------------------------------------------------
1 | // import { useState } from "react";
2 | // import Button from "./UI/Button";
3 |
4 | import Box from "./UI/Box";
5 |
6 | const Title = () => {
7 | // const [titleIndex, setTitleIndex] = useState(0);
8 | // const TitleList: string[] = ["道具", "角色"];
9 | // const onClick = () => {
10 | // const TitleSize = TitleList.length;
11 | // setTitleIndex((titleIndex + 1) % TitleSize);
12 | // }
13 | return (
14 |
15 |

16 |
17 |
18 | )
19 | }
20 |
21 | export default Title;
--------------------------------------------------------------------------------
/src/components/UI/Box.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 |
3 | interface BoxProps {
4 | children: React.ReactNode;
5 | className?: string;
6 | }
7 |
8 | const Box: React.FC = ({ children, className }) => {
9 | return (
10 |
11 |
12 | {children}
13 |
14 |
15 |
16 | );
17 | }
18 |
19 | export default Box;
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "lib": [
5 | "dom",
6 | "dom.iterable",
7 | "esnext"
8 | ],
9 | "allowJs": true,
10 | "skipLibCheck": true,
11 | "esModuleInterop": true,
12 | "allowSyntheticDefaultImports": true,
13 | "strict": true,
14 | "forceConsistentCasingInFileNames": true,
15 | "noFallthroughCasesInSwitch": true,
16 | "module": "esnext",
17 | "moduleResolution": "node",
18 | "resolveJsonModule": true,
19 | "isolatedModules": true,
20 | "noEmit": true,
21 | "jsx": "react-jsx"
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom/client';
3 | import './index.css';
4 | import App from './App';
5 | import reportWebVitals from './reportWebVitals';
6 | import './input.css'
7 |
8 | const root = ReactDOM.createRoot(
9 | document.getElementById('root') as HTMLElement
10 | );
11 | root.render(
12 | //
13 |
14 | //
15 | );
16 |
17 | // If you want to start measuring performance in your app, pass a function
18 | // to log results (for example: reportWebVitals(console.log))
19 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
20 | reportWebVitals();
21 |
--------------------------------------------------------------------------------
/src/components/UI/Button.tsx:
--------------------------------------------------------------------------------
1 | // 基本按钮样式
2 |
3 | import { button } from '../../types/button';
4 |
5 | const Button = (props: { bt: button }) => {
6 | const bt: button = props.bt;
7 | return (
8 |
15 | );
16 | }
17 |
18 | export default Button;
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | module.exports = {
3 | content: [
4 | "./src/**/*.{js,jsx,ts,tsx}",
5 | ],
6 | theme: {
7 | extend: {
8 | animation: {
9 | 'slide-fade': 'slide-fade 0.75s ease-out forwards',
10 | },
11 | keyframes: {
12 | 'slide-fade': {
13 | '0%': {
14 | transform: 'translateY(30px)', // 开始时文字位于下方
15 | opacity: '0', // 文字完全透明
16 | },
17 | '100%': {
18 | transform: 'translateY(0)', // 文字最终位置
19 | opacity: '1', // 文字完全不透明
20 | },
21 | },
22 | },
23 | },
24 | },
25 | plugins: [],
26 | }
--------------------------------------------------------------------------------
/src/App.css:
--------------------------------------------------------------------------------
1 | .App {
2 | text-align: center;
3 | }
4 |
5 | .App-logo {
6 | height: 40vmin;
7 | pointer-events: none;
8 | }
9 |
10 | @media (prefers-reduced-motion: no-preference) {
11 | .App-logo {
12 | animation: App-logo-spin infinite 20s linear;
13 | }
14 | }
15 |
16 | .App-header {
17 | background-color: #282c34;
18 | min-height: 100vh;
19 | display: flex;
20 | flex-direction: column;
21 | align-items: center;
22 | justify-content: center;
23 | font-size: calc(10px + 2vmin);
24 | color: white;
25 | }
26 |
27 | .App-link {
28 | color: #61dafb;
29 | }
30 |
31 | @keyframes App-logo-spin {
32 | from {
33 | transform: rotate(0deg);
34 | }
35 | to {
36 | transform: rotate(360deg);
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/src/utils/GetMyRank.ts:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../axiosConfig";
2 | import { BACKEN_URL } from "../config";
3 |
4 |
5 | interface myvote {
6 | winner: number;
7 | loser: number;
8 | weight: number;
9 | CreatedAt: string;
10 | }
11 |
12 | export interface MyRankData {
13 | total_votes: number; // 自己总共投票的次数
14 | most_voted_times: number; // 自己最多投票的次数
15 | most_voted_item: number; //自己最多投票的道具id
16 | matching_rate: number; // 自己投票和总榜投票的匹配率
17 | max_difference: number; // 自己投票和总榜投票的最大差异
18 | max_diff_vote: myvote; // 自己投票和总榜投票的最大差异的投票
19 | }
20 |
21 | export async function GetMyRank(): Promise {
22 | const res = await axiosInstance.get(`${BACKEN_URL}/api/rank/getMyRank`);
23 | return res.data;
24 | }
--------------------------------------------------------------------------------
/src/types/filter.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface filter {
3 | startQuality: number; // 开始的品质
4 | endQuality: number; // 结束的品质
5 | canBeLost: boolean; // 是否可以被Lost获取
6 | itemPools: string[]; // 道具池
7 | isActive: number; //是否是主动道具 0:不限 1:是 2:否
8 | }
9 |
10 | export const itemPools: string[] = [
11 | "treasure",
12 | "shop",
13 | "boss",
14 | "devil",
15 | "angel",
16 | "secret",
17 | "goldenChest",
18 | "redChest",
19 | "curse",
20 | ]
21 |
22 | export const itemPoolsMap: Record = {
23 | "treasure": "宝箱房",
24 | "shop": "商店",
25 | "boss": "Boss房",
26 | "devil": "恶魔房",
27 | "angel": "天使房",
28 | "secret": "隐藏房",
29 | "goldenChest": "金箱",
30 | "redChest": "红箱",
31 | "curse": "刺房",
32 | }
--------------------------------------------------------------------------------
/src/utils/GetItems.ts:
--------------------------------------------------------------------------------
1 | // 调用后端的restfulAPI,获得两个道具数据
2 | import axiosInstance from "../axiosConfig";
3 | import { item } from "../types/item";
4 | import { BACKEN_URL } from "../config";
5 | import { filter } from "../types/filter";
6 |
7 | // get /api/item/getItems&num=2&startQuality=1&endQuality=2&canBeLost=true
8 | // &itemPools=A,B,C...
9 | export async function GetItems(num: number, filter: filter): Promise- {
10 | const res = await axiosInstance.get(`${BACKEN_URL}/api/item/getItems`, {
11 | params: {
12 | num: num,
13 | startQuality: filter.startQuality,
14 | endQuality: filter.endQuality,
15 | canBeLost: filter.canBeLost,
16 | itemPools: filter.itemPools.join(','),
17 | isActive: filter.isActive
18 | }
19 | });
20 | return res.data;
21 | }
--------------------------------------------------------------------------------
/src/utils/GetRanking.ts:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../axiosConfig";
2 | import { ranking } from "../types/ranking";
3 | import { BACKEN_URL } from "../config";
4 | import { filter, itemPools } from "../types/filter";
5 |
6 | // get /api/rank/getRanking?type=XXX&itemPools=A,B,C...&startQuality=1&endQuality=2&canBeLost=true
7 |
8 | export async function GetRanking(type: string, filter: filter): Promise {
9 | const res = await axiosInstance.get(`${BACKEN_URL}/api/rank/getRanking`, {
10 | params: {
11 | type: type,
12 | startQuality: filter.startQuality,
13 | endQuality: filter.endQuality,
14 | canBeLost: filter.canBeLost,
15 | itemPools: filter.itemPools.join(','),
16 | isActive: filter.isActive
17 | }
18 | });
19 | return res.data;
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/utils/GetItemRank.ts:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../axiosConfig";
2 | import { BACKEN_URL } from "../config";
3 | import { filter } from "../types/filter";
4 | import { itemRank, ranking } from "../types/ranking";
5 |
6 | // get /api/rank/getItemRank?itemID=XXX&itemPools=A,B,C...&startQuality=1&endQuality=2&canBeLost=true
7 |
8 | export async function GetItemRank(itemID: number, filter: filter): Promise {
9 | const res = await axiosInstance.get(`${BACKEN_URL}/api/rank/getItemRank`, {
10 | params: {
11 | itemID: itemID,
12 | startQuality: filter.startQuality,
13 | endQuality: filter.endQuality,
14 | canBeLost: filter.canBeLost,
15 | itemPools: filter.itemPools.join(','),
16 | isActive: filter.isActive
17 | }
18 | });
19 | return res.data;
20 | }
21 |
22 |
--------------------------------------------------------------------------------
/src/utils/PVote.ts:
--------------------------------------------------------------------------------
1 | import axiosInstance from "../axiosConfig";
2 | import { BACKEN_URL } from "../config";
3 |
4 | export interface Pvote {
5 | winner: number; //定向投票的胜者
6 | loser: number; //定向投票的失败者
7 | description: string; // 定向投票的理由
8 | }
9 |
10 | export async function SendPvote(result: Pvote): Promise {
11 |
12 | const response = await axiosInstance.post(`${BACKEN_URL}/api/vote/Pvote`, result);
13 | return response.data;
14 |
15 | }
16 |
17 | interface PvoteNum {
18 | pvoteNum: number;
19 | }
20 |
21 | export async function GetPvoteNum(): Promise {
22 | const response = await axiosInstance.get(`${BACKEN_URL}/api/vote/GetPvoteNum`);
23 | return response.data;
24 | }
25 |
26 | interface PvoteRecords {
27 | pvoteRecords: Pvote[];
28 |
29 | }
30 |
31 | export async function GetPvoteRecords(): Promise {
32 | const response = await axiosInstance.get(`${BACKEN_URL}/api/vote/GetPvoteRecords`);
33 | return response.data;
34 | }
--------------------------------------------------------------------------------
/src/components/Attention.tsx:
--------------------------------------------------------------------------------
1 | import Box from "./UI/Box"
2 |
3 | export default function Attention() {
4 | return (
5 |
6 |
7 | 注意:本投票箱仿照
8 | 明日方舟投票箱
9 | 进行设计,同时作为某大二学生的练手项目,存在诸多瑕疵望大家谅解。另外,此项目正处于测试期,数据库是测试用数据,不具有任何意义。
10 |
11 |
12 | 本投票箱仅供娱乐,不具有任何商业用途,如有侵权请联系我删除。同时,本投票箱开源在
13 | Github
14 | 上,非常希望有志同道合的同学可以加入这个项目,或者是提一些建议!若是想进行交流,或是提出建议,亦或是发现bug,欢迎加入QQ群:903619222。
15 |
16 |
17 | 本投票箱还缺少很多很多数据,希望大家踊跃参与,多多宣传,谢谢大家!!并且,制作不易,如果喜欢本投票箱,还请去Github上给我点一个Star,真的很需要,谢谢!
18 |
19 |
20 |
21 | )
22 | }
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "ys-voting-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@testing-library/dom": "^10.4.0",
7 | "@testing-library/jest-dom": "^6.6.3",
8 | "@testing-library/react": "^16.2.0",
9 | "@testing-library/user-event": "^13.5.0",
10 | "@types/jest": "^27.5.2",
11 | "@types/node": "^16.18.126",
12 | "@types/react": "^19.0.10",
13 | "@types/react-dom": "^19.0.4",
14 | "axios": "^1.7.9",
15 | "react": "^19.0.0",
16 | "react-dom": "^19.0.0",
17 | "react-scripts": "5.0.1",
18 | "typescript": "^4.9.5",
19 | "web-vitals": "^2.1.4"
20 | },
21 | "scripts": {
22 | "start": "react-scripts start",
23 | "build": "react-scripts build",
24 | "test": "react-scripts test",
25 | "eject": "react-scripts eject"
26 | },
27 | "eslintConfig": {
28 | "extends": [
29 | "react-app",
30 | "react-app/jest"
31 | ]
32 | },
33 | "browserslist": {
34 | "production": [
35 | ">0.2%",
36 | "not dead",
37 | "not op_mini all"
38 | ],
39 | "development": [
40 | "last 1 chrome version",
41 | "last 1 firefox version",
42 | "last 1 safari version"
43 | ]
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/components/UI/Image.tsx:
--------------------------------------------------------------------------------
1 | // 图片的容器
2 | import { image } from "../../types/image";
3 | import { BACKEN_URL } from "../../config";
4 |
5 | const Image = (props: { image_prop: image }) => {
6 | const image_prop: image = props.image_prop;
7 |
8 | return (
9 |
10 |
11 |

14 | {image_prop.name ? (
15 |
16 | {image_prop.name}
17 |
18 | ) : null}
19 | {image_prop.description ? (
20 |
21 | {image_prop.description}
22 |
23 | ) : null}
24 |
25 | );
26 | }
27 |
28 | export default Image;
--------------------------------------------------------------------------------
/src/components/RankingVS.tsx:
--------------------------------------------------------------------------------
1 | // 用于展示两个道具/角色的图片,中间标注VS
2 |
3 | import { image } from '../types/image';
4 | import Image from './UI/Image';
5 | import ChooseButton, { ChooseButtonProps } from "./ChooseButton";
6 | import Title from './Title';
7 | import Box from './UI/Box';
8 |
9 | const RankingVS = (props: { left: image, right: image, ChooseProps: ChooseButtonProps, LastVote: string }) => {
10 | return (
11 |
12 | {/* 标题 */}
13 |
14 |
15 | {/* 中间层:道具VS */}
16 |
17 |
20 |
21 |

22 |
23 |
{props.LastVote}
24 |
25 |
26 |
29 |
30 |
31 | {/* 下方层:选择按钮 */}
32 |
33 |
34 |
35 |
36 |
37 | );
38 | }
39 |
40 | export default RankingVS;
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
25 | 以撒投票箱
26 |
27 |
28 |
29 |
30 |
31 |
41 |
42 |
43 |
--------------------------------------------------------------------------------
/src/components/Rule.tsx:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from "react";
2 | import Box from "./UI/Box";
3 |
4 | export default function Rule() {
5 | const [visible, setVisible] = useState(false);
6 |
7 | useEffect(() => {
8 | setVisible(true);
9 | }, []);
10 |
11 | return (
12 |
13 |
14 | 3.5日更新:1. 更换了香港服务器并且获得了一个域名,vote.qiuy.cloud,可以使用域名访问啦!
15 | 2. 添加了定向投票功能,可以进行定向投票啦!但是有以下限制:1. 每个IP每小时最多投票三次 2. 不能投票给相同的道具
16 | 3. 只能让当前排名更低的道具获胜,并且对于前50名的道具,排名差不能超过20
17 | 4. ELO机制的本意是为了在样本量较低时,道具之间能快速拉开差分,但是现在样本量已经足够,ELO机制反而拖累了道具之间的差距,所以我们将会逐步减少ELO机制的影响,最终取消ELO机制,让道具的排名更加稳定。
18 |
19 |
20 | 3.4日更新: 1. 修改了排行榜样式,并且没有一个鼠标滚轮受到伤害!
21 | 2. 修改了后端API,你们的投票更快更准更难刷票了!(大概)
22 | 3. 请大家不要乱投票不要乱投票不要乱投票,务必做出理性的选择!!!!让每个道具落在他该在的地方上!
23 |
24 |
25 |
26 | 规则:假设你拿到了一个只会生成两个道具的伊甸长子权,请你在出现的道具中,结合各种实战场景选择你最可能选择的道具。投票采取ELO机制,每个道具的ELO分数会根据你的选择和其他人的选择动态进行调整。最后根据分数和胜率进行排名。
27 |
28 |
29 | 可以点击道具图片,或是下方的四个按钮进行投票,其中"还有人类吗?"会使得两个道具总场次++,胜率降低,但是不改变分数;
30 | 而"你问我?我怎么知道?"选项则是会跳过这一轮选择,不会对道具产生影响。
31 |
32 |
33 | 由于存在筛选器,小样本的投票可能会出现误差,例如筛选后只有四级道具,那么由于对手过于强,可能会导致某些四级道具评分下降。
34 | 于是我们对于筛选过的投票会进行降权处理以减少这种误差(例如筛选后道具只有50个,那么由这次投票产生的所有影响会减少至50/705)。如果您希望得到更准确的排名,请尽量少使用筛选器(虽然可能会出现狗粮比狗粮的情况)(为了解决这种情况可以适当勾选里罗筛选器)。使用筛选器后若出现
35 | 道具少于2个,会自动将筛选器初始化为默认值,并且弹出一个需要点击关闭的错误提示。
36 |
37 |
38 |
39 | );
40 | }
--------------------------------------------------------------------------------
/src/logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/components/ChooseButton.tsx:
--------------------------------------------------------------------------------
1 | // 选择按钮,用于做出选择
2 |
3 | export interface ChooseButtonProps {
4 | OnClick_1: () => void,//左赢
5 | OnClick_2: () => void,//右赢
6 | OnClick_3: () => void,//都输
7 | OnClick_4: () => void,//无事发生
8 | }
9 |
10 | const ChooseButton = (props: { prop: ChooseButtonProps }) => {
11 |
12 | return (
13 |
14 |
22 |
23 |
31 |
39 |
40 |
41 |
42 |
50 |
51 | );
52 |
53 | }
54 |
55 | export default ChooseButton;
56 |
--------------------------------------------------------------------------------
/src/components/MyData.tsx:
--------------------------------------------------------------------------------
1 | import { MyRankData, GetMyRank } from "../utils/GetMyRank";
2 | import { useState } from "react";
3 | import Box from "./UI/Box";
4 | import { item } from "../types/item";
5 | import { ranking } from "../types/ranking";
6 | import { BACKEN_URL } from "../config";
7 |
8 | export default function MyData(props: { allItems: item[], totalrank: ranking[] }) {
9 | const [myRankData, setMyRankData] = useState(null);
10 | const [loading, setLoading] = useState(false);
11 | const [showReport, setShowReport] = useState(false); // 添加状态变量
12 |
13 | const handleGetMyRank = async () => {
14 | if (showReport) {
15 | setShowReport(false);
16 | return;
17 | }
18 |
19 | setLoading(true);
20 | try {
21 | const data = await GetMyRank();
22 | setMyRankData(data);
23 | setShowReport(true); // 显示报告
24 | } catch (error) {
25 | console.error("Error fetching my rank data:", error);
26 | } finally {
27 | setLoading(false);
28 | }
29 | };
30 |
31 | const formatDate = (dateString: string) => {
32 | const date = new Date(dateString);
33 | return date.toLocaleString();
34 | };
35 |
36 | const MostVotedItem = props.allItems.find((item) => {
37 | return item.id === myRankData?.most_voted_item
38 | });
39 |
40 | const MostDiffItemWinner = props.allItems.find((item) => {
41 | return item.id === myRankData?.max_diff_vote.winner
42 | });
43 |
44 | const MostDiffItemWinnerRank = props.totalrank.find((item) => item.name === MostDiffItemWinner?.name)?.rank;
45 |
46 | const MostDiffItemLoser = props.allItems.find((item) => {
47 | return item.id === myRankData?.max_diff_vote.loser
48 | });
49 |
50 | const MostDiffItemLoserRank = props.totalrank.find((item) => item.name === MostDiffItemLoser?.name)?.rank;
51 |
52 | return (
53 |
54 |
55 |
62 | {showReport && myRankData && (
63 |
64 |
65 |
在您 {myRankData.total_votes} 次的投票记录中,您成功为
66 |
67 |

68 |
{MostVotedItem?.name}
69 |
70 |
投出了 {myRankData.most_voted_times} 次有效投票,看来他和你的相性很好呢
71 |
72 |
73 |
哇塞!您的投票和总榜的投票匹配率达到了惊人的 {(100 * myRankData.matching_rate).toFixed(2)}% !!!!
74 |
其中最大差异为 {myRankData.max_difference} 名,
75 |
这是多么小众惊人独到的理解啊!让我们回味一下当时的情景吧:
76 |
您在 {formatDate(myRankData.max_diff_vote.CreatedAt)} 时,以 {(myRankData.max_diff_vote.weight * 100).toFixed(0)}% 的权重投票给了
77 |
78 |

79 |
{MostDiffItemWinner?.name}({MostDiffItemWinnerRank} 名)
80 |
81 |
而
82 |
83 |

84 |
{MostDiffItemLoser?.name}({MostDiffItemLoserRank} 名)
85 |
86 |
却败下阵来,这是一个多么令人难以置信的选择啊!
87 |
88 |
89 | )}
90 |
91 |
92 | );
93 | }
--------------------------------------------------------------------------------
/src/components/Rank.tsx:
--------------------------------------------------------------------------------
1 | import { ranking } from "../types/ranking";
2 | import Box from "./UI/Box";
3 | import { useState } from "react";
4 |
5 | export default function Rank(props: { rank: ranking[], title: string, onRefresh: () => void }) {
6 | const [isRefreshing, setIsRefreshing] = useState(false);
7 | const [isExpanded, setIsExpanded] = useState(false);
8 | const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
9 | const [sortBy, setSortBy] = useState<"total" | "score" | "winpercent">("score");
10 | const toggleSortOrder = (criteria: "score" | "winpercent" | "total") => {
11 | if (sortBy === criteria) {
12 | setSortOrder(prevOrder => (prevOrder === "asc" ? "desc" : "asc"));
13 | } else {
14 | setSortBy(criteria);
15 | setSortOrder("desc");
16 | }
17 | };
18 | const sortedRank = [...props.rank].sort((a, b) => {
19 | if (sortBy === "score") {
20 | return sortOrder === "asc" ? a.score - b.score : b.score - a.score;
21 | } else if (sortBy === "winpercent") {
22 | return sortOrder === "asc" ? a.winpercent - b.winpercent : b.winpercent - a.winpercent;
23 | } else {
24 | return sortOrder === "asc" ? a.totals - b.totals : b.totals - a.totals;
25 | }
26 | });
27 |
28 | const NotExpandSize = 30;
29 |
30 | const handleRefresh = async () => {
31 | setIsRefreshing(true);
32 | await props.onRefresh();
33 | setIsRefreshing(false);
34 | };
35 |
36 | const toggleExpand = () => {
37 | setIsExpanded(!isExpanded);
38 | };
39 |
40 | const getBackgroundColor = (index: number) => {
41 | const colors = [
42 | "bg-red-400", "bg-red-300", "bg-red-200",
43 | "bg-yellow-400", "bg-yellow-300", "bg-yellow-200",
44 | "bg-green-400", "bg-green-300", "bg-green-200"
45 | ];
46 | const sectionSize = Math.floor(props.rank.length / 9);
47 | const idx = (Math.floor(index / sectionSize) > 8) ? 8 : Math.floor(index / sectionSize);
48 | return colors[idx];
49 | };
50 |
51 | const getCupLevel = (index: number) => {
52 | const levels = [
53 | "超大杯上", "超大杯中", "超大杯下",
54 | "大杯上", "大杯中", "大杯下",
55 | "中杯上", "中杯中", "中杯下"
56 | ];
57 | const sectionSize = Math.floor(props.rank.length / 9);
58 | const idx = (Math.floor(index / sectionSize) > 8) ? 8 : Math.floor(index / sectionSize);
59 | return levels[idx];
60 | };
61 |
62 | return (
63 |
64 |
65 |
66 |
68 |
69 |
76 |
77 |
78 |
杯级
79 |
排名
80 |
名字
81 |
87 |
93 |
99 |
100 |
101 | {sortedRank.map((item, index) => {
102 | if (!isExpanded && index >= NotExpandSize) {
103 | return null;
104 | }
105 | return (
106 |
107 |
{getCupLevel(index)}
108 |
{item.rank}
109 |
{item.name}
110 |
{item.score.toFixed(1)}
111 |
{(item.winpercent * 100).toFixed(1)}%
112 |
{(item.totals).toFixed(1)}
113 |
114 | );
115 | })}
116 |
117 |
125 |
126 | );
127 | }
--------------------------------------------------------------------------------
/src/components/Filter.tsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { filter, itemPoolsMap } from "../types/filter";
3 | import Box from "./UI/Box";
4 |
5 | interface FilterProps {
6 | Filter: filter;
7 | setFilter: React.Dispatch>;
8 | filterNum: number;
9 | onFilterChange: (Filter: filter) => void;
10 | }
11 |
12 | export default function Filter({ setFilter, Filter, onFilterChange, filterNum }: FilterProps) {
13 |
14 | const handleStartQualityChange = (event: React.ChangeEvent) => {
15 | const startQuality = parseInt(event.target.value);
16 | setFilter((prevFilter) => ({
17 | ...prevFilter,
18 | startQuality,
19 | }));
20 | onFilterChange({
21 | ...Filter,
22 | startQuality,
23 | });
24 | };
25 |
26 | const StartValueSelect = () => {
27 | return (
28 |
29 |
30 |
40 |
41 | )
42 | }
43 |
44 | const handleEndQualityChange = (event: React.ChangeEvent) => {
45 | const endQuality = parseInt(event.target.value);
46 | setFilter((prevFilter) => ({
47 | ...prevFilter,
48 | endQuality,
49 | }));
50 | onFilterChange({
51 | ...Filter,
52 | endQuality,
53 | });
54 | };
55 |
56 | const EndValueSelect = () => {
57 | return (
58 |
59 |
60 |
70 |
71 | )
72 | }
73 |
74 | const handleCanBeLostChange = (event: React.ChangeEvent) => {
75 | const canBeLost = event.target.value === "true";
76 | setFilter((prevFilter) => ({
77 | ...prevFilter,
78 | canBeLost,
79 | }));
80 | onFilterChange({
81 | ...Filter,
82 | canBeLost,
83 | });
84 | };
85 |
86 | const CanBeLostSelect = () => {
87 | return (
88 |
89 |
90 |
99 |
100 | )
101 | }
102 |
103 | const handleIsActiveChange = (event: React.ChangeEvent) => {
104 | const isActive = parseInt(event.target.value);
105 | setFilter((prevFilter) => ({
106 | ...prevFilter,
107 | isActive,
108 | }));
109 | onFilterChange({
110 | ...Filter,
111 | isActive,
112 | });
113 | };
114 |
115 | const IsActiveSelect = () => {
116 | return (
117 |
118 |
119 |
129 |
130 | )
131 | }
132 |
133 | const handleItemPoolsChange = (event: React.ChangeEvent) => {
134 | const { value, checked } = event.target;
135 | setFilter((prevFilter) => {
136 | const newItemPools = checked
137 | ? [...prevFilter.itemPools, value]
138 | : prevFilter.itemPools.filter((pool) => pool !== value);
139 | onFilterChange({
140 | ...Filter,
141 | itemPools: newItemPools,
142 | });
143 | return {
144 | ...prevFilter,
145 | itemPools: newItemPools,
146 | };
147 | });
148 | };
149 |
150 | const clearItemPools = () => {
151 | setFilter((prevFilter) => ({
152 | ...prevFilter,
153 | itemPools: [],
154 | }));
155 | onFilterChange({
156 | ...Filter,
157 | itemPools: [],
158 | });
159 | };
160 |
161 | const ItemPoolsSelect = () => {
162 | return (
163 |
164 |
165 |
166 | {Object.entries(itemPoolsMap).map(([key, value]) => (
167 |
177 | ))}
178 |
179 |
180 |
181 | )
182 | }
183 |
184 | return (
185 |
186 | 还剩下
{filterNum}
个道具
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 | );
200 | }
--------------------------------------------------------------------------------
/src/components/ItemRank.tsx:
--------------------------------------------------------------------------------
1 | import { itemRank } from "../types/ranking";
2 | import { item } from "../types/item";
3 | import { useState } from "react";
4 | import { BACKEN_URL } from "../config";
5 | import Box from "./UI/Box";
6 |
7 | export default function ItemRank(props: {
8 | onSelectRankItemChange: (item: item | undefined) => void,
9 | selectRank: itemRank[],
10 | allItems: item[],
11 | selectRankItem: item | undefined
12 | }) {
13 | // 搜索部分等保持不变……
14 | const [searchTerm, setSearchTerm] = useState("");
15 | const [currentPage, setCurrentPage] = useState(1);
16 | const [sortOrder, setSortOrder] = useState<"asc" | "desc">("desc");
17 |
18 | const itemsPerPage = 4;
19 | const filteredItems = props.allItems.filter(item => item.name.includes(searchTerm));
20 | const totalPages = Math.ceil(filteredItems.length / itemsPerPage);
21 |
22 | const handleItemClick = (item: item) => {
23 | setSearchTerm(item.name);
24 | props.onSelectRankItemChange(item);
25 | };
26 |
27 | const handleInputChange = (e: React.ChangeEvent) => {
28 | const value = e.target.value;
29 | setSearchTerm(value.trim());
30 | setCurrentPage(1); // 搜索变更时重置页码
31 | if (value === "") {
32 | props.onSelectRankItemChange(undefined);
33 | } else if (filteredItems.length === 1 && filteredItems[0].name === value) {
34 | props.onSelectRankItemChange(filteredItems[0]);
35 | }
36 | };
37 |
38 | // 排序部分
39 | props.selectRank.sort((a, b) => {
40 | if (sortOrder === "asc") {
41 | return a.winrate === b.winrate ? a.total - b.total : a.winrate - b.winrate;
42 | } else {
43 | return a.winrate === b.winrate ? b.total - a.total : b.winrate - a.winrate;
44 | }
45 | });
46 |
47 | const toggleSortOrder = () => {
48 | setSortOrder(prev => (prev === "asc" ? "desc" : "asc"));
49 | };
50 |
51 | const handlePageChange = (direction: "prev" | "next" | "first" | "end") => {
52 | setCurrentPage(prev => {
53 | if (direction === "prev") return Math.max(prev - 1, 1);
54 | else if (direction === "next") return Math.min(prev + 1, totalPages);
55 | else if (direction === "first") return 1;
56 | else if (direction === "end") return totalPages;
57 | return prev;
58 | });
59 | };
60 |
61 | const paginatedItems = filteredItems.slice((currentPage - 1) * itemsPerPage, currentPage * itemsPerPage);
62 |
63 | // 针对详情列表采用懒加载:初始只显示一部分,滚动到底自动加载更多
64 | const [rankingPage, setRankingPage] = useState(1);
65 | const rankingPageSize = 100; // 每次加载10条
66 | const paginatedRankDetail = props.selectRank.slice(0, rankingPage * rankingPageSize);
67 |
68 | const handleDetailsScroll = (e: React.UIEvent) => {
69 | const { scrollTop, scrollHeight, clientHeight } = e.currentTarget;
70 | // 当滚动到底部10px以内时加载更多
71 | if (scrollTop + clientHeight >= scrollHeight - 10) {
72 | if (rankingPage * rankingPageSize < props.selectRank.length) {
73 | setRankingPage(prev => prev + 1);
74 | }
75 | }
76 | };
77 |
78 | return (
79 |
80 | 道具对位查询
81 |
88 | {paginatedItems.length > 0 && (
89 |
90 | {paginatedItems.map(item => (
91 | - handleItemClick(item)}
94 | className="cursor-pointer p-2 hover:bg-gray-200 transition-colors duration-300"
95 | >
96 |
97 | {item.name}
98 |
99 | ))}
100 |
101 | )}
102 | {totalPages > 1 && (
103 |
104 |
111 |
118 |
125 |
132 |
133 | )}
134 | {props.selectRankItem && (
135 |
136 |
137 |
138 | {props.selectRankItem.name}对位榜单
139 |
140 |
146 |
147 |
148 |
排名
149 |
名字
150 |
胜场
151 |
154 |
总场
155 |
156 | {/* 修改详情列表为固定高度、overflow-y-auto,并监听滚动事件实现懒加载 */}
157 |
158 | {paginatedRankDetail.map((item, index) => {
159 | const bgColor = item.winrate > 0.5 ? "bg-red-400" : "bg-green-400";
160 | return (
161 |
162 |
{index + 1}
163 |
{item.name}
164 |
{item.wincount.toFixed(1)}
165 |
{(item.winrate * 100).toFixed(1)}%
166 |
{item.total.toFixed(1)}
167 |
168 | );
169 | })}
170 |
171 |
172 | )}
173 |
174 | );
175 | }
--------------------------------------------------------------------------------
/src/App.tsx:
--------------------------------------------------------------------------------
1 | import RankingVS from "./components/RankingVS";
2 | import { ChooseButtonProps } from "./components/ChooseButton";
3 | import { useState, useEffect, useRef } from "react";
4 | import { GetItems } from "./utils/GetItems";
5 | import { SendVoting } from "./utils/SendVoting";
6 | import { item } from "./types/item";
7 | import Rule from "./components/Rule";
8 | import Attention from "./components/Attention";
9 | import Rank from "./components/Rank";
10 | import { itemRank, ranking } from "./types/ranking";
11 | import { GetRanking } from "./utils/GetRanking";
12 | import Filter from "./components/Filter";
13 | import { filter } from "./types/filter";
14 | import Error from "./components/Error";
15 | import { GetItemRank } from "./utils/GetItemRank";
16 | import ItemRank from "./components/ItemRank";
17 | import BackTo, { BackToProps } from "./components/BackTo";
18 | import { VOTEResult } from "./types/votingResult";
19 | import MyData from "./components/MyData";
20 | import PreciseVote from "./components/PreciseVote";
21 | // import MovingGif from "./components/MovingGif";
22 |
23 | function App() {
24 | const defaultFilter: filter = {
25 | startQuality: 0,
26 | endQuality: 4,
27 | canBeLost: false,
28 | itemPools: [],
29 | isActive: 0,
30 | }
31 | const [ItemList, setItemList] = useState- ([]);
32 | const [error, setError] = useState(null);
33 | const [lastVote, setLastVote] = useState(null);
34 | const [rank, setRank] = useState([]);
35 | const [filter, setFilter] = useState(defaultFilter);
36 | const [showBackToTop, setShowBackToTop] = useState(false);
37 |
38 | // 单独某个道具的排行榜
39 | const [selectRankItem, setSelectRankItem] = useState
- ();
40 | const [selectRank, setSelectRank] = useState();
41 |
42 | const [allItems, setAllItems] = useState
- ([]);
43 | const fn = ItemList.length > 0 ? ItemList[0].filternum : 0;
44 |
45 | const GetTwoItem = (filter_p: filter) => {
46 | GetItems(2, filter_p)
47 | .then((res: item[]) => {
48 | setItemList(res);
49 | })
50 | .catch((error) => {
51 | console.error("Error fetching items:", error);
52 | if (error.response) {
53 | const errorMessage = error.response.data.error
54 | setError(errorMessage);
55 | if (errorMessage.includes("道具数量不足"))
56 | OnFilterChange(defaultFilter);
57 | } else {
58 | setError("无法获取道具,请稍后再试");
59 | }
60 | });
61 | };
62 |
63 | const GetRank = (filter_p: filter) => {
64 | GetRanking("item", filter_p)
65 | .then((res: ranking[]) => {
66 | setRank(res);
67 | })
68 | .catch((error) => {
69 | if (error.response) {
70 | const errorMessage = error.response.data.error
71 | setError(errorMessage);
72 | } else {
73 | setError("无法获取排行榜,请稍后再试");
74 | }
75 |
76 | });
77 | };
78 |
79 | const GetItmRank = (item: item, filter_p: filter) => {
80 | GetItemRank(item.id, filter_p)
81 | .then((res: itemRank[]) => {
82 | setSelectRank(res);
83 | }
84 | )
85 | .catch((error) => {
86 | console.error("Error fetching ranking:", error);
87 | setError("无法获得道具对位数据,请稍后尝试");
88 | }
89 | );
90 | }
91 |
92 | const GetAllItems = () => {
93 | GetItems(0, defaultFilter)
94 | .then((res: item[]) => {
95 | setAllItems(res);
96 | })
97 | .catch((error) => {
98 | console.error("Error fetching items:", error);
99 | setError("无法获取道具信息,请稍后再试");
100 | });
101 | }
102 |
103 | const OnSelectItemChange = (item: item | undefined) => {
104 | setSelectRankItem(item);
105 | if (item)
106 | GetItmRank(item, filter);
107 | }
108 |
109 | const OnFilterChange = (filter_p: filter) => {
110 | if (filter_p.startQuality > filter_p.endQuality) {
111 | filter_p.endQuality = filter_p.startQuality;
112 | }
113 | setFilter(filter_p);
114 | GetRank(filter_p);
115 | GetTwoItem(filter_p);
116 | if (selectRankItem) {
117 | GetItmRank(selectRankItem, filter_p);
118 | }
119 | };
120 |
121 |
122 | useEffect(() => {
123 | GetTwoItem(defaultFilter);
124 | GetRank(defaultFilter);
125 | GetAllItems();
126 | }, []);
127 |
128 | useEffect(() => {
129 | const handleScroll = () => {
130 | if (window.scrollY > 200) {
131 | setShowBackToTop(true);
132 | } else {
133 | setShowBackToTop(false);
134 | }
135 | };
136 |
137 | window.addEventListener("scroll", handleScroll);
138 | return () => {
139 | window.removeEventListener("scroll", handleScroll);
140 | };
141 | }, []);
142 |
143 | const vote = async (VoteResult: VOTEResult) => {
144 | SendVoting({ result: VoteResult }).then(() => {
145 | if (VoteResult === VOTEResult.NOBODY) {
146 | setLastVote(`哈哈,${ItemList[0].name}(${rank.find((item) => item.name === ItemList[0].name)?.rank}名),${ItemList[1].name}(${rank.find((item) => item.name === ItemList[1].name)?.rank}名)没一个是人`);
147 | } else {
148 | const winnerItem = VoteResult === VOTEResult.LEFT ? ItemList[0] : ItemList[1];
149 | const loserItem = VoteResult === VOTEResult.LEFT ? ItemList[1] : ItemList[0];
150 | setLastVote(`成功投票给${winnerItem?.name}(${rank.find((item) => item.name === winnerItem?.name)?.rank}名),与此同时${loserItem?.name}(${rank.find((item) => item.name === loserItem?.name)?.rank}名)`);
151 | }
152 | GetTwoItem(filter);
153 | }).catch((error) => {
154 | console.error("Error post vote:", error);
155 | if (error.response) {
156 | const errorMessage = error.response.data.error
157 | setError(errorMessage);
158 | } else {
159 | setError("无法投票,请刷新或稍后再试");
160 | }
161 | });
162 | };
163 | const ChoosenButtonP: ChooseButtonProps = {
164 | OnClick_1: () => vote(VOTEResult.LEFT),
165 | OnClick_2: () => vote(VOTEResult.RIGHT),
166 | OnClick_3: () => vote(VOTEResult.NOBODY),
167 | OnClick_4: () => GetTwoItem(filter),
168 | }
169 | const filterRef = useRef(null);
170 | const itemRankRef = useRef(null);
171 | const ruleRef = useRef(null);
172 | const attentionRef = useRef(null);
173 | const rankRef = useRef(null);
174 | const myDataRef = useRef(null);
175 | const scrollToComponent = (ref: React.RefObject) => {
176 | if (ref) {
177 | ref.current?.scrollIntoView({ behavior: "smooth" });
178 | }
179 | };
180 |
181 | const basebackList: BackToProps[] = [
182 | { to: "筛选", toClick: () => scrollToComponent(filterRef) },
183 | { to: "个性化报告", toClick: () => scrollToComponent(myDataRef) },
184 | { to: "对位榜单", toClick: () => scrollToComponent(itemRankRef) },
185 | { to: "道具排行榜", toClick: () => scrollToComponent(rankRef) },
186 | ];
187 |
188 | const backLists = !showBackToTop ? basebackList : [
189 | { to: "回到顶部", toClick: () => { window.scrollTo({ top: 0, behavior: 'smooth' }) } },
190 | ...basebackList
191 | ];
192 |
193 |
194 | return (
195 |
196 |
197 |
198 |
199 |
200 | {error &&
{ setError(null) }} />}
201 | {ItemList.length > 0 && (
202 |
220 | )}
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 |
230 |
233 |
234 |
235 |
236 |
239 |
240 | {
241 | GetRank(filter);
242 | }} />
243 |
244 | {/* */}
245 |
246 |
247 |
248 |
249 |
250 | );
251 | }
252 |
253 |
254 | export default App;
255 |
--------------------------------------------------------------------------------
/src/components/PreciseVote.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState } from "react";
2 | import { item } from "../types/item";
3 | import { Pvote, GetPvoteNum, SendPvote as pvoteFunc, GetPvoteRecords } from "../utils/PVote";
4 | import { BACKEN_URL } from "../config";
5 | import Box from "./UI/Box";
6 |
7 |
8 |
9 |
10 | export default function PreciseVote(props: { allItems: item[] }) {
11 | const allItems = props.allItems;
12 | const [ipVoteCount, setIpVoteCount] = useState(0);
13 |
14 | const [winnerSearch, setWinnerSearch] = useState("");
15 | const [loserSearch, setLoserSearch] = useState("");
16 | const [winnerSelected, setWinnerSelected] = useState- (null);
17 | const [loserSelected, setLoserSelected] = useState
- (null);
18 | const [description, setDescription] = useState("");
19 | const [loading, setLoading] = useState(false);
20 | const [message, setMessage] = useState("");
21 |
22 | const [PvoteRecords, setPvoteRecords] = useState([]);
23 | const [enablePvoteRecords, setEnablePvoteRecords] = useState(false);
24 |
25 | const maxVoteCount = 100;
26 |
27 | // Filter items by search term
28 | const filteredWinnerItems = allItems.filter((it) =>
29 | it.name.toLowerCase().includes(winnerSearch.toLowerCase())
30 | );
31 | const filteredLoserItems = allItems.filter((it) =>
32 | it.name.toLowerCase().includes(loserSearch.toLowerCase())
33 | );
34 |
35 | // Load the full item list and current IP vote count on component mount
36 | useEffect(() => {
37 | GetPvoteNum()
38 | .then((num) => {
39 | setIpVoteCount(num.pvoteNum);
40 | })
41 | .catch((error) => {
42 | console.error("Error fetching precise vote number:", error);
43 | });
44 | }, []);
45 |
46 | const handleSubmitVote = async () => {
47 | if (!winnerSelected || !loserSelected) {
48 | setMessage("请选择胜者和败者的道具!");
49 | return;
50 | }
51 | if (winnerSelected.id === loserSelected.id) {
52 | setMessage("胜者和败者不能为同一个道具");
53 | return;
54 | }
55 | if (!description) {
56 | setMessage("请输入您的投票理由!");
57 | return;
58 | }
59 | setLoading(true);
60 | setMessage("");
61 | try {
62 | const result = await pvoteFunc({
63 | winner: winnerSelected.id,
64 | loser: loserSelected.id,
65 | description,
66 | });
67 | setMessage("申诉成功,谢谢你的支持!");
68 | // Increment the IP vote count
69 | setIpVoteCount((prev) => prev + 1);
70 | // Clear selections and description
71 | setWinnerSelected(null);
72 | setLoserSelected(null);
73 | setWinnerSearch("");
74 | setLoserSearch("");
75 | setDescription("");
76 | } catch (error: any) {
77 | console.error("Error performing precise vote:", error);
78 | if (error.response) {
79 | const errorMessage = error.response.data.error
80 | setMessage(errorMessage);
81 | } else
82 | setMessage("投票失败,请刷新或稍后再试!");
83 | } finally {
84 | setLoading(false);
85 | }
86 | };
87 |
88 | const handleGetPvoteRecords = async () => {
89 | if (!enablePvoteRecords) {
90 | setEnablePvoteRecords(true);
91 | const records = await GetPvoteRecords();
92 | console.log(records)
93 | setPvoteRecords(records.pvoteRecords);
94 | } else {
95 | setEnablePvoteRecords(false);
96 | setPvoteRecords([]);
97 | }
98 | }
99 |
100 | return (
101 |
102 |
为什么为什么为什么为什么这个不如那个
103 | {ipVoteCount >= maxVoteCount ? (
104 |
105 | 您已经达到定向投票次数(每个IP每小时最多3次)。
106 |
107 | ) : (
108 | <>
109 |
110 |
113 | {winnerSelected && (
114 |
120 | )}
121 | {!winnerSelected && (
122 |
{
126 | setWinnerSearch(e.target.value);
127 | setWinnerSelected(null);
128 | }}
129 | placeholder="请输入道具名称..."
130 | className={"border p-2 rounded-xl items-center text-center"}
131 | />
132 |
133 | )}
134 | {winnerSearch && !winnerSelected && (
135 |
136 | {filteredWinnerItems.map((item) => (
137 | - {
140 | setWinnerSelected(item);
141 | setWinnerSearch(item.name);
142 | }}
143 | className="cursor-pointer p-2 hover:bg-gray-200 transition-colors duration-300"
144 | >
145 |
146 | {item.name}
147 |
148 | ))}
149 |
150 | )}
151 |
152 |
153 |
154 |
157 | {loserSelected && (
158 |
164 | )}
165 | {!loserSelected && (
166 |
{
170 | setLoserSearch(e.target.value);
171 | setLoserSelected(null);
172 | }}
173 | placeholder="请输入道具名称..."
174 | className={"border p-2 rounded-xl items-center text-center"}
175 | />
176 | )}
177 | {loserSearch && !loserSelected && (
178 |
179 | {filteredLoserItems.map((item) => (
180 | - {
183 | setLoserSelected(item);
184 | setLoserSearch(item.name);
185 | }}
186 | className="cursor-pointer p-2 hover:bg-gray-200 transition-colors duration-300"
187 | >
188 |
189 | {item.name}
190 |
191 | ))}
192 |
193 | )}
194 |
195 |
196 |
197 |
200 |
206 |
207 |
208 |
215 | {message && {message}
}
216 |
217 | 您当前已申诉: {ipVoteCount} 次 (每个IP最多3次)
218 |
219 | >
220 | )}
221 |
222 |
225 |
226 | {PvoteRecords.map((record, index) => {
227 | const winner = allItems.find((item) => item.id === record.winner);
228 | const loser = allItems.find((item) => item.id === record.loser);
229 | return (
230 | -
231 | 我寻思
应该大于
,因为:{record.description}
232 |
233 | )
234 |
235 | }
236 | )}
237 |
238 |
239 |
240 | );
241 | }
--------------------------------------------------------------------------------
/src/output.css:
--------------------------------------------------------------------------------
1 | /* src/index.css */
2 |
3 | *, ::before, ::after {
4 | --tw-border-spacing-x: 0;
5 | --tw-border-spacing-y: 0;
6 | --tw-translate-x: 0;
7 | --tw-translate-y: 0;
8 | --tw-rotate: 0;
9 | --tw-skew-x: 0;
10 | --tw-skew-y: 0;
11 | --tw-scale-x: 1;
12 | --tw-scale-y: 1;
13 | --tw-pan-x: ;
14 | --tw-pan-y: ;
15 | --tw-pinch-zoom: ;
16 | --tw-scroll-snap-strictness: proximity;
17 | --tw-gradient-from-position: ;
18 | --tw-gradient-via-position: ;
19 | --tw-gradient-to-position: ;
20 | --tw-ordinal: ;
21 | --tw-slashed-zero: ;
22 | --tw-numeric-figure: ;
23 | --tw-numeric-spacing: ;
24 | --tw-numeric-fraction: ;
25 | --tw-ring-inset: ;
26 | --tw-ring-offset-width: 0px;
27 | --tw-ring-offset-color: #fff;
28 | --tw-ring-color: rgb(59 130 246 / 0.5);
29 | --tw-ring-offset-shadow: 0 0 #0000;
30 | --tw-ring-shadow: 0 0 #0000;
31 | --tw-shadow: 0 0 #0000;
32 | --tw-shadow-colored: 0 0 #0000;
33 | --tw-blur: ;
34 | --tw-brightness: ;
35 | --tw-contrast: ;
36 | --tw-grayscale: ;
37 | --tw-hue-rotate: ;
38 | --tw-invert: ;
39 | --tw-saturate: ;
40 | --tw-sepia: ;
41 | --tw-drop-shadow: ;
42 | --tw-backdrop-blur: ;
43 | --tw-backdrop-brightness: ;
44 | --tw-backdrop-contrast: ;
45 | --tw-backdrop-grayscale: ;
46 | --tw-backdrop-hue-rotate: ;
47 | --tw-backdrop-invert: ;
48 | --tw-backdrop-opacity: ;
49 | --tw-backdrop-saturate: ;
50 | --tw-backdrop-sepia: ;
51 | --tw-contain-size: ;
52 | --tw-contain-layout: ;
53 | --tw-contain-paint: ;
54 | --tw-contain-style: ;
55 | }
56 |
57 | ::backdrop {
58 | --tw-border-spacing-x: 0;
59 | --tw-border-spacing-y: 0;
60 | --tw-translate-x: 0;
61 | --tw-translate-y: 0;
62 | --tw-rotate: 0;
63 | --tw-skew-x: 0;
64 | --tw-skew-y: 0;
65 | --tw-scale-x: 1;
66 | --tw-scale-y: 1;
67 | --tw-pan-x: ;
68 | --tw-pan-y: ;
69 | --tw-pinch-zoom: ;
70 | --tw-scroll-snap-strictness: proximity;
71 | --tw-gradient-from-position: ;
72 | --tw-gradient-via-position: ;
73 | --tw-gradient-to-position: ;
74 | --tw-ordinal: ;
75 | --tw-slashed-zero: ;
76 | --tw-numeric-figure: ;
77 | --tw-numeric-spacing: ;
78 | --tw-numeric-fraction: ;
79 | --tw-ring-inset: ;
80 | --tw-ring-offset-width: 0px;
81 | --tw-ring-offset-color: #fff;
82 | --tw-ring-color: rgb(59 130 246 / 0.5);
83 | --tw-ring-offset-shadow: 0 0 #0000;
84 | --tw-ring-shadow: 0 0 #0000;
85 | --tw-shadow: 0 0 #0000;
86 | --tw-shadow-colored: 0 0 #0000;
87 | --tw-blur: ;
88 | --tw-brightness: ;
89 | --tw-contrast: ;
90 | --tw-grayscale: ;
91 | --tw-hue-rotate: ;
92 | --tw-invert: ;
93 | --tw-saturate: ;
94 | --tw-sepia: ;
95 | --tw-drop-shadow: ;
96 | --tw-backdrop-blur: ;
97 | --tw-backdrop-brightness: ;
98 | --tw-backdrop-contrast: ;
99 | --tw-backdrop-grayscale: ;
100 | --tw-backdrop-hue-rotate: ;
101 | --tw-backdrop-invert: ;
102 | --tw-backdrop-opacity: ;
103 | --tw-backdrop-saturate: ;
104 | --tw-backdrop-sepia: ;
105 | --tw-contain-size: ;
106 | --tw-contain-layout: ;
107 | --tw-contain-paint: ;
108 | --tw-contain-style: ;
109 | }
110 |
111 | /* ! tailwindcss v3.4.17 | MIT License | https://tailwindcss.com */
112 |
113 | /*
114 | 1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
115 | 2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
116 | */
117 |
118 | *,
119 | ::before,
120 | ::after {
121 | box-sizing: border-box;
122 | /* 1 */
123 | border-width: 0;
124 | /* 2 */
125 | border-style: solid;
126 | /* 2 */
127 | border-color: #e5e7eb;
128 | /* 2 */
129 | }
130 |
131 | ::before,
132 | ::after {
133 | --tw-content: '';
134 | }
135 |
136 | /*
137 | 1. Use a consistent sensible line-height in all browsers.
138 | 2. Prevent adjustments of font size after orientation changes in iOS.
139 | 3. Use a more readable tab size.
140 | 4. Use the user's configured `sans` font-family by default.
141 | 5. Use the user's configured `sans` font-feature-settings by default.
142 | 6. Use the user's configured `sans` font-variation-settings by default.
143 | 7. Disable tap highlights on iOS
144 | */
145 |
146 | html,
147 | :host {
148 | line-height: 1.5;
149 | /* 1 */
150 | -webkit-text-size-adjust: 100%;
151 | /* 2 */
152 | /* 3 */
153 | tab-size: 4;
154 | /* 3 */
155 | font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
156 | /* 4 */
157 | font-feature-settings: normal;
158 | /* 5 */
159 | font-variation-settings: normal;
160 | /* 6 */
161 | -webkit-tap-highlight-color: transparent;
162 | /* 7 */
163 | }
164 |
165 | /*
166 | 1. Remove the margin in all browsers.
167 | 2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
168 | */
169 |
170 | body {
171 | margin: 0;
172 | /* 1 */
173 | line-height: inherit;
174 | /* 2 */
175 | }
176 |
177 | /*
178 | 1. Add the correct height in Firefox.
179 | 2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
180 | 3. Ensure horizontal rules are visible by default.
181 | */
182 |
183 | hr {
184 | height: 0;
185 | /* 1 */
186 | color: inherit;
187 | /* 2 */
188 | border-top-width: 1px;
189 | /* 3 */
190 | }
191 |
192 | /*
193 | Add the correct text decoration in Chrome, Edge, and Safari.
194 | */
195 |
196 | abbr:where([title]) {
197 | -webkit-text-decoration: underline dotted;
198 | text-decoration: underline dotted;
199 | }
200 |
201 | /*
202 | Remove the default font size and weight for headings.
203 | */
204 |
205 | h1,
206 | h2,
207 | h3,
208 | h4,
209 | h5,
210 | h6 {
211 | font-size: inherit;
212 | font-weight: inherit;
213 | }
214 |
215 | /*
216 | Reset links to optimize for opt-in styling instead of opt-out.
217 | */
218 |
219 | a {
220 | color: inherit;
221 | text-decoration: inherit;
222 | }
223 |
224 | /*
225 | Add the correct font weight in Edge and Safari.
226 | */
227 |
228 | b,
229 | strong {
230 | font-weight: bolder;
231 | }
232 |
233 | /*
234 | 1. Use the user's configured `mono` font-family by default.
235 | 2. Use the user's configured `mono` font-feature-settings by default.
236 | 3. Use the user's configured `mono` font-variation-settings by default.
237 | 4. Correct the odd `em` font sizing in all browsers.
238 | */
239 |
240 | code,
241 | kbd,
242 | samp,
243 | pre {
244 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
245 | /* 1 */
246 | font-feature-settings: normal;
247 | /* 2 */
248 | font-variation-settings: normal;
249 | /* 3 */
250 | font-size: 1em;
251 | /* 4 */
252 | }
253 |
254 | /*
255 | Add the correct font size in all browsers.
256 | */
257 |
258 | small {
259 | font-size: 80%;
260 | }
261 |
262 | /*
263 | Prevent `sub` and `sup` elements from affecting the line height in all browsers.
264 | */
265 |
266 | sub,
267 | sup {
268 | font-size: 75%;
269 | line-height: 0;
270 | position: relative;
271 | vertical-align: baseline;
272 | }
273 |
274 | sub {
275 | bottom: -0.25em;
276 | }
277 |
278 | sup {
279 | top: -0.5em;
280 | }
281 |
282 | /*
283 | 1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
284 | 2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
285 | 3. Remove gaps between table borders by default.
286 | */
287 |
288 | table {
289 | text-indent: 0;
290 | /* 1 */
291 | border-color: inherit;
292 | /* 2 */
293 | border-collapse: collapse;
294 | /* 3 */
295 | }
296 |
297 | /*
298 | 1. Change the font styles in all browsers.
299 | 2. Remove the margin in Firefox and Safari.
300 | 3. Remove default padding in all browsers.
301 | */
302 |
303 | button,
304 | input,
305 | optgroup,
306 | select,
307 | textarea {
308 | font-family: inherit;
309 | /* 1 */
310 | font-feature-settings: inherit;
311 | /* 1 */
312 | font-variation-settings: inherit;
313 | /* 1 */
314 | font-size: 100%;
315 | /* 1 */
316 | font-weight: inherit;
317 | /* 1 */
318 | line-height: inherit;
319 | /* 1 */
320 | letter-spacing: inherit;
321 | /* 1 */
322 | color: inherit;
323 | /* 1 */
324 | margin: 0;
325 | /* 2 */
326 | padding: 0;
327 | /* 3 */
328 | }
329 |
330 | /*
331 | Remove the inheritance of text transform in Edge and Firefox.
332 | */
333 |
334 | button,
335 | select {
336 | text-transform: none;
337 | }
338 |
339 | /*
340 | 1. Correct the inability to style clickable types in iOS and Safari.
341 | 2. Remove default button styles.
342 | */
343 |
344 | button,
345 | input:where([type='button']),
346 | input:where([type='reset']),
347 | input:where([type='submit']) {
348 | -webkit-appearance: button;
349 | /* 1 */
350 | background-color: transparent;
351 | /* 2 */
352 | background-image: none;
353 | /* 2 */
354 | }
355 |
356 | /*
357 | Use the modern Firefox focus style for all focusable elements.
358 | */
359 |
360 | :-moz-focusring {
361 | outline: auto;
362 | }
363 |
364 | /*
365 | Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
366 | */
367 |
368 | :-moz-ui-invalid {
369 | box-shadow: none;
370 | }
371 |
372 | /*
373 | Add the correct vertical alignment in Chrome and Firefox.
374 | */
375 |
376 | progress {
377 | vertical-align: baseline;
378 | }
379 |
380 | /*
381 | Correct the cursor style of increment and decrement buttons in Safari.
382 | */
383 |
384 | ::-webkit-inner-spin-button,
385 | ::-webkit-outer-spin-button {
386 | height: auto;
387 | }
388 |
389 | /*
390 | 1. Correct the odd appearance in Chrome and Safari.
391 | 2. Correct the outline style in Safari.
392 | */
393 |
394 | [type='search'] {
395 | -webkit-appearance: textfield;
396 | /* 1 */
397 | outline-offset: -2px;
398 | /* 2 */
399 | }
400 |
401 | /*
402 | Remove the inner padding in Chrome and Safari on macOS.
403 | */
404 |
405 | ::-webkit-search-decoration {
406 | -webkit-appearance: none;
407 | }
408 |
409 | /*
410 | 1. Correct the inability to style clickable types in iOS and Safari.
411 | 2. Change font properties to `inherit` in Safari.
412 | */
413 |
414 | ::-webkit-file-upload-button {
415 | -webkit-appearance: button;
416 | /* 1 */
417 | font: inherit;
418 | /* 2 */
419 | }
420 |
421 | /*
422 | Add the correct display in Chrome and Safari.
423 | */
424 |
425 | summary {
426 | display: list-item;
427 | }
428 |
429 | /*
430 | Removes the default spacing and border for appropriate elements.
431 | */
432 |
433 | blockquote,
434 | dl,
435 | dd,
436 | h1,
437 | h2,
438 | h3,
439 | h4,
440 | h5,
441 | h6,
442 | hr,
443 | figure,
444 | p,
445 | pre {
446 | margin: 0;
447 | }
448 |
449 | fieldset {
450 | margin: 0;
451 | padding: 0;
452 | }
453 |
454 | legend {
455 | padding: 0;
456 | }
457 |
458 | ol,
459 | ul,
460 | menu {
461 | list-style: none;
462 | margin: 0;
463 | padding: 0;
464 | }
465 |
466 | /*
467 | Reset default styling for dialogs.
468 | */
469 |
470 | dialog {
471 | padding: 0;
472 | }
473 |
474 | /*
475 | Prevent resizing textareas horizontally by default.
476 | */
477 |
478 | textarea {
479 | resize: vertical;
480 | }
481 |
482 | /*
483 | 1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
484 | 2. Set the default placeholder color to the user's configured gray 400 color.
485 | */
486 |
487 | input::placeholder,
488 | textarea::placeholder {
489 | opacity: 1;
490 | /* 1 */
491 | color: #9ca3af;
492 | /* 2 */
493 | }
494 |
495 | /*
496 | Set the default cursor for buttons.
497 | */
498 |
499 | button,
500 | [role="button"] {
501 | cursor: pointer;
502 | }
503 |
504 | /*
505 | Make sure disabled buttons don't get the pointer cursor.
506 | */
507 |
508 | :disabled {
509 | cursor: default;
510 | }
511 |
512 | /*
513 | 1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
514 | 2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
515 | This can trigger a poorly considered lint error in some tools but is included by design.
516 | */
517 |
518 | img,
519 | svg,
520 | video,
521 | canvas,
522 | audio,
523 | iframe,
524 | embed,
525 | object {
526 | display: block;
527 | /* 1 */
528 | vertical-align: middle;
529 | /* 2 */
530 | }
531 |
532 | /*
533 | Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
534 | */
535 |
536 | img,
537 | video {
538 | max-width: 100%;
539 | height: auto;
540 | }
541 |
542 | /* Make elements with the HTML hidden attribute stay hidden by default */
543 |
544 | [hidden]:where(:not([hidden="until-found"])) {
545 | display: none;
546 | }
547 |
548 | .visible {
549 | visibility: visible;
550 | }
551 |
552 | .fixed {
553 | position: fixed;
554 | }
555 |
556 | .m-0 {
557 | margin: 0px;
558 | }
559 |
560 | .mx-1 {
561 | margin-left: 0.25rem;
562 | margin-right: 0.25rem;
563 | }
564 |
565 | .mx-auto {
566 | margin-left: auto;
567 | margin-right: auto;
568 | }
569 |
570 | .ml-10 {
571 | margin-left: 2.5rem;
572 | }
573 |
574 | .ml-4 {
575 | margin-left: 1rem;
576 | }
577 |
578 | .mt-2 {
579 | margin-top: 0.5rem;
580 | }
581 |
582 | .flex {
583 | display: flex;
584 | }
585 |
586 | .inline-flex {
587 | display: inline-flex;
588 | }
589 |
590 | .grid {
591 | display: grid;
592 | }
593 |
594 | .h-16 {
595 | height: 4rem;
596 | }
597 |
598 | .h-5 {
599 | height: 1.25rem;
600 | }
601 |
602 | .h-\[24rem\] {
603 | height: 24rem;
604 | }
605 |
606 | .h-\[48rem\] {
607 | height: 48rem;
608 | }
609 |
610 | .h-auto {
611 | height: auto;
612 | }
613 |
614 | .w-1\/3 {
615 | width: 33.333333%;
616 | }
617 |
618 | .w-1\/6 {
619 | width: 16.666667%;
620 | }
621 |
622 | .w-16 {
623 | width: 4rem;
624 | }
625 |
626 | .w-5 {
627 | width: 1.25rem;
628 | }
629 |
630 | .w-\[42rem\] {
631 | width: 42rem;
632 | }
633 |
634 | .w-\[44rem\] {
635 | width: 44rem;
636 | }
637 |
638 | .w-full {
639 | width: 100%;
640 | }
641 |
642 | .min-w-\[54rem\] {
643 | min-width: 54rem;
644 | }
645 |
646 | .max-w-\[44rem\] {
647 | max-width: 44rem;
648 | }
649 |
650 | .flex-1 {
651 | flex: 1 1 0%;
652 | }
653 |
654 | .-rotate-90 {
655 | --tw-rotate: -90deg;
656 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
657 | }
658 |
659 | .rotate-90 {
660 | --tw-rotate: 90deg;
661 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
662 | }
663 |
664 | .transform {
665 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
666 | }
667 |
668 | @keyframes slide-fade {
669 | 0% {
670 | transform: translateY(30px);
671 | opacity: 0;
672 | }
673 |
674 | 100% {
675 | transform: translateY(0);
676 | opacity: 1;
677 | }
678 | }
679 |
680 | .animate-slide-fade {
681 | animation: slide-fade 0.75s ease-out forwards;
682 | }
683 |
684 | .cursor-not-allowed {
685 | cursor: not-allowed;
686 | }
687 |
688 | .cursor-pointer {
689 | cursor: pointer;
690 | }
691 |
692 | .grid-cols-3 {
693 | grid-template-columns: repeat(3, minmax(0, 1fr));
694 | }
695 |
696 | .flex-col {
697 | flex-direction: column;
698 | }
699 |
700 | .flex-wrap {
701 | flex-wrap: wrap;
702 | }
703 |
704 | .items-center {
705 | align-items: center;
706 | }
707 |
708 | .justify-center {
709 | justify-content: center;
710 | }
711 |
712 | .justify-between {
713 | justify-content: space-between;
714 | }
715 |
716 | .gap-2 {
717 | gap: 0.5rem;
718 | }
719 |
720 | .space-x-1 > :not([hidden]) ~ :not([hidden]) {
721 | --tw-space-x-reverse: 0;
722 | margin-right: calc(0.25rem * var(--tw-space-x-reverse));
723 | margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
724 | }
725 |
726 | .space-x-10 > :not([hidden]) ~ :not([hidden]) {
727 | --tw-space-x-reverse: 0;
728 | margin-right: calc(2.5rem * var(--tw-space-x-reverse));
729 | margin-left: calc(2.5rem * calc(1 - var(--tw-space-x-reverse)));
730 | }
731 |
732 | .space-x-2 > :not([hidden]) ~ :not([hidden]) {
733 | --tw-space-x-reverse: 0;
734 | margin-right: calc(0.5rem * var(--tw-space-x-reverse));
735 | margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
736 | }
737 |
738 | .space-x-4 > :not([hidden]) ~ :not([hidden]) {
739 | --tw-space-x-reverse: 0;
740 | margin-right: calc(1rem * var(--tw-space-x-reverse));
741 | margin-left: calc(1rem * calc(1 - var(--tw-space-x-reverse)));
742 | }
743 |
744 | .space-x-6 > :not([hidden]) ~ :not([hidden]) {
745 | --tw-space-x-reverse: 0;
746 | margin-right: calc(1.5rem * var(--tw-space-x-reverse));
747 | margin-left: calc(1.5rem * calc(1 - var(--tw-space-x-reverse)));
748 | }
749 |
750 | .space-y-10 > :not([hidden]) ~ :not([hidden]) {
751 | --tw-space-y-reverse: 0;
752 | margin-top: calc(2.5rem * calc(1 - var(--tw-space-y-reverse)));
753 | margin-bottom: calc(2.5rem * var(--tw-space-y-reverse));
754 | }
755 |
756 | .space-y-16 > :not([hidden]) ~ :not([hidden]) {
757 | --tw-space-y-reverse: 0;
758 | margin-top: calc(4rem * calc(1 - var(--tw-space-y-reverse)));
759 | margin-bottom: calc(4rem * var(--tw-space-y-reverse));
760 | }
761 |
762 | .space-y-3 > :not([hidden]) ~ :not([hidden]) {
763 | --tw-space-y-reverse: 0;
764 | margin-top: calc(0.75rem * calc(1 - var(--tw-space-y-reverse)));
765 | margin-bottom: calc(0.75rem * var(--tw-space-y-reverse));
766 | }
767 |
768 | .space-y-4 > :not([hidden]) ~ :not([hidden]) {
769 | --tw-space-y-reverse: 0;
770 | margin-top: calc(1rem * calc(1 - var(--tw-space-y-reverse)));
771 | margin-bottom: calc(1rem * var(--tw-space-y-reverse));
772 | }
773 |
774 | .space-y-5 > :not([hidden]) ~ :not([hidden]) {
775 | --tw-space-y-reverse: 0;
776 | margin-top: calc(1.25rem * calc(1 - var(--tw-space-y-reverse)));
777 | margin-bottom: calc(1.25rem * var(--tw-space-y-reverse));
778 | }
779 |
780 | .space-y-6 > :not([hidden]) ~ :not([hidden]) {
781 | --tw-space-y-reverse: 0;
782 | margin-top: calc(1.5rem * calc(1 - var(--tw-space-y-reverse)));
783 | margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
784 | }
785 |
786 | .rounded {
787 | border-radius: 0.25rem;
788 | }
789 |
790 | .rounded-2xl {
791 | border-radius: 1rem;
792 | }
793 |
794 | .rounded-md {
795 | border-radius: 0.375rem;
796 | }
797 |
798 | .rounded-xl {
799 | border-radius: 0.75rem;
800 | }
801 |
802 | .border {
803 | border-width: 1px;
804 | }
805 |
806 | .border-t-2 {
807 | border-top-width: 2px;
808 | }
809 |
810 | .border-gray-200 {
811 | --tw-border-opacity: 1;
812 | border-color: rgb(229 231 235 / var(--tw-border-opacity, 1));
813 | }
814 |
815 | .bg-blue-400 {
816 | --tw-bg-opacity: 1;
817 | background-color: rgb(96 165 250 / var(--tw-bg-opacity, 1));
818 | }
819 |
820 | .bg-blue-500 {
821 | --tw-bg-opacity: 1;
822 | background-color: rgb(59 130 246 / var(--tw-bg-opacity, 1));
823 | }
824 |
825 | .bg-gray-100 {
826 | --tw-bg-opacity: 1;
827 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
828 | }
829 |
830 | .bg-gray-50 {
831 | --tw-bg-opacity: 1;
832 | background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
833 | }
834 |
835 | .bg-green-200 {
836 | --tw-bg-opacity: 1;
837 | background-color: rgb(187 247 208 / var(--tw-bg-opacity, 1));
838 | }
839 |
840 | .bg-green-300 {
841 | --tw-bg-opacity: 1;
842 | background-color: rgb(134 239 172 / var(--tw-bg-opacity, 1));
843 | }
844 |
845 | .bg-green-400 {
846 | --tw-bg-opacity: 1;
847 | background-color: rgb(74 222 128 / var(--tw-bg-opacity, 1));
848 | }
849 |
850 | .bg-green-600 {
851 | --tw-bg-opacity: 1;
852 | background-color: rgb(22 163 74 / var(--tw-bg-opacity, 1));
853 | }
854 |
855 | .bg-orange-700 {
856 | --tw-bg-opacity: 1;
857 | background-color: rgb(194 65 12 / var(--tw-bg-opacity, 1));
858 | }
859 |
860 | .bg-red-200 {
861 | --tw-bg-opacity: 1;
862 | background-color: rgb(254 202 202 / var(--tw-bg-opacity, 1));
863 | }
864 |
865 | .bg-red-300 {
866 | --tw-bg-opacity: 1;
867 | background-color: rgb(252 165 165 / var(--tw-bg-opacity, 1));
868 | }
869 |
870 | .bg-red-400 {
871 | --tw-bg-opacity: 1;
872 | background-color: rgb(248 113 113 / var(--tw-bg-opacity, 1));
873 | }
874 |
875 | .bg-red-500 {
876 | --tw-bg-opacity: 1;
877 | background-color: rgb(239 68 68 / var(--tw-bg-opacity, 1));
878 | }
879 |
880 | .bg-red-800 {
881 | --tw-bg-opacity: 1;
882 | background-color: rgb(153 27 27 / var(--tw-bg-opacity, 1));
883 | }
884 |
885 | .bg-yellow-200 {
886 | --tw-bg-opacity: 1;
887 | background-color: rgb(254 240 138 / var(--tw-bg-opacity, 1));
888 | }
889 |
890 | .bg-yellow-300 {
891 | --tw-bg-opacity: 1;
892 | background-color: rgb(253 224 71 / var(--tw-bg-opacity, 1));
893 | }
894 |
895 | .bg-yellow-400 {
896 | --tw-bg-opacity: 1;
897 | background-color: rgb(250 204 21 / var(--tw-bg-opacity, 1));
898 | }
899 |
900 | .bg-opacity-100 {
901 | --tw-bg-opacity: 1;
902 | }
903 |
904 | .bg-opacity-50 {
905 | --tw-bg-opacity: 0.5;
906 | }
907 |
908 | .bg-opacity-\[93\%\] {
909 | --tw-bg-opacity: 93%;
910 | }
911 |
912 | .bg-\[url\(\'\.\.\/public\/images\/bg\.webp\'\)\] {
913 | background-image: url('../public/images/bg.webp');
914 | }
915 |
916 | .bg-\[length\:100\%\] {
917 | background-size: 100%;
918 | }
919 |
920 | .bg-no-repeat {
921 | background-repeat: no-repeat;
922 | }
923 |
924 | .object-contain {
925 | object-fit: contain;
926 | }
927 |
928 | .object-cover {
929 | object-fit: cover;
930 | }
931 |
932 | .p-2 {
933 | padding: 0.5rem;
934 | }
935 |
936 | .px-1 {
937 | padding-left: 0.25rem;
938 | padding-right: 0.25rem;
939 | }
940 |
941 | .px-10 {
942 | padding-left: 2.5rem;
943 | padding-right: 2.5rem;
944 | }
945 |
946 | .px-2 {
947 | padding-left: 0.5rem;
948 | padding-right: 0.5rem;
949 | }
950 |
951 | .px-4 {
952 | padding-left: 1rem;
953 | padding-right: 1rem;
954 | }
955 |
956 | .px-5 {
957 | padding-left: 1.25rem;
958 | padding-right: 1.25rem;
959 | }
960 |
961 | .py-0\.5 {
962 | padding-top: 0.125rem;
963 | padding-bottom: 0.125rem;
964 | }
965 |
966 | .py-1 {
967 | padding-top: 0.25rem;
968 | padding-bottom: 0.25rem;
969 | }
970 |
971 | .py-2 {
972 | padding-top: 0.5rem;
973 | padding-bottom: 0.5rem;
974 | }
975 |
976 | .py-3 {
977 | padding-top: 0.75rem;
978 | padding-bottom: 0.75rem;
979 | }
980 |
981 | .py-4 {
982 | padding-top: 1rem;
983 | padding-bottom: 1rem;
984 | }
985 |
986 | .py-5 {
987 | padding-top: 1.25rem;
988 | padding-bottom: 1.25rem;
989 | }
990 |
991 | .pt-5 {
992 | padding-top: 1.25rem;
993 | }
994 |
995 | .text-center {
996 | text-align: center;
997 | }
998 |
999 | .text-2xl {
1000 | font-size: 1.5rem;
1001 | line-height: 2rem;
1002 | }
1003 |
1004 | .text-4xl {
1005 | font-size: 2.25rem;
1006 | line-height: 2.5rem;
1007 | }
1008 |
1009 | .text-base {
1010 | font-size: 1rem;
1011 | line-height: 1.5rem;
1012 | }
1013 |
1014 | .text-lg {
1015 | font-size: 1.125rem;
1016 | line-height: 1.75rem;
1017 | }
1018 |
1019 | .text-xl {
1020 | font-size: 1.25rem;
1021 | line-height: 1.75rem;
1022 | }
1023 |
1024 | .font-bold {
1025 | font-weight: 700;
1026 | }
1027 |
1028 | .font-semibold {
1029 | font-weight: 600;
1030 | }
1031 |
1032 | .text-black {
1033 | --tw-text-opacity: 1;
1034 | color: rgb(0 0 0 / var(--tw-text-opacity, 1));
1035 | }
1036 |
1037 | .text-blue-600 {
1038 | --tw-text-opacity: 1;
1039 | color: rgb(37 99 235 / var(--tw-text-opacity, 1));
1040 | }
1041 |
1042 | .text-blue-700 {
1043 | --tw-text-opacity: 1;
1044 | color: rgb(29 78 216 / var(--tw-text-opacity, 1));
1045 | }
1046 |
1047 | .text-red-500 {
1048 | --tw-text-opacity: 1;
1049 | color: rgb(239 68 68 / var(--tw-text-opacity, 1));
1050 | }
1051 |
1052 | .text-white {
1053 | --tw-text-opacity: 1;
1054 | color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1055 | }
1056 |
1057 | .shadow-lg {
1058 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1059 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1060 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1061 | }
1062 |
1063 | .shadow-md {
1064 | --tw-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
1065 | --tw-shadow-colored: 0 4px 6px -1px var(--tw-shadow-color), 0 2px 4px -2px var(--tw-shadow-color);
1066 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1067 | }
1068 |
1069 | .filter {
1070 | filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
1071 | }
1072 |
1073 | .transition-all {
1074 | transition-property: all;
1075 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1076 | transition-duration: 150ms;
1077 | }
1078 |
1079 | .transition-colors {
1080 | transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
1081 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1082 | transition-duration: 150ms;
1083 | }
1084 |
1085 | .transition-shadow {
1086 | transition-property: box-shadow;
1087 | transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
1088 | transition-duration: 150ms;
1089 | }
1090 |
1091 | .duration-300 {
1092 | transition-duration: 300ms;
1093 | }
1094 |
1095 | .hover\:bg-blue-400:hover {
1096 | --tw-bg-opacity: 1;
1097 | background-color: rgb(96 165 250 / var(--tw-bg-opacity, 1));
1098 | }
1099 |
1100 | .hover\:bg-blue-600:hover {
1101 | --tw-bg-opacity: 1;
1102 | background-color: rgb(37 99 235 / var(--tw-bg-opacity, 1));
1103 | }
1104 |
1105 | .hover\:bg-gray-100:hover {
1106 | --tw-bg-opacity: 1;
1107 | background-color: rgb(243 244 246 / var(--tw-bg-opacity, 1));
1108 | }
1109 |
1110 | .hover\:bg-gray-300:hover {
1111 | --tw-bg-opacity: 1;
1112 | background-color: rgb(209 213 219 / var(--tw-bg-opacity, 1));
1113 | }
1114 |
1115 | .hover\:bg-gray-400:hover {
1116 | --tw-bg-opacity: 1;
1117 | background-color: rgb(156 163 175 / var(--tw-bg-opacity, 1));
1118 | }
1119 |
1120 | .hover\:bg-gray-50:hover {
1121 | --tw-bg-opacity: 1;
1122 | background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
1123 | }
1124 |
1125 | .hover\:bg-red-400:hover {
1126 | --tw-bg-opacity: 1;
1127 | background-color: rgb(248 113 113 / var(--tw-bg-opacity, 1));
1128 | }
1129 |
1130 | .hover\:bg-red-600:hover {
1131 | --tw-bg-opacity: 1;
1132 | background-color: rgb(220 38 38 / var(--tw-bg-opacity, 1));
1133 | }
1134 |
1135 | .hover\:bg-yellow-400:hover {
1136 | --tw-bg-opacity: 1;
1137 | background-color: rgb(250 204 21 / var(--tw-bg-opacity, 1));
1138 | }
1139 |
1140 | .hover\:bg-opacity-\[10\%\]:hover {
1141 | --tw-bg-opacity: 10%;
1142 | }
1143 |
1144 | .hover\:text-black:hover {
1145 | --tw-text-opacity: 1;
1146 | color: rgb(0 0 0 / var(--tw-text-opacity, 1));
1147 | }
1148 |
1149 | .hover\:text-red-600:hover {
1150 | --tw-text-opacity: 1;
1151 | color: rgb(220 38 38 / var(--tw-text-opacity, 1));
1152 | }
1153 |
1154 | .hover\:text-white:hover {
1155 | --tw-text-opacity: 1;
1156 | color: rgb(255 255 255 / var(--tw-text-opacity, 1));
1157 | }
1158 |
1159 | .hover\:shadow-2xl:hover {
1160 | --tw-shadow: 0 25px 50px -12px rgb(0 0 0 / 0.25);
1161 | --tw-shadow-colored: 0 25px 50px -12px var(--tw-shadow-color);
1162 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1163 | }
1164 |
1165 | .hover\:shadow-lg:hover {
1166 | --tw-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1);
1167 | --tw-shadow-colored: 0 10px 15px -3px var(--tw-shadow-color), 0 4px 6px -4px var(--tw-shadow-color);
1168 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1169 | }
1170 |
1171 | .hover\:shadow-xl:hover {
1172 | --tw-shadow: 0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1);
1173 | --tw-shadow-colored: 0 20px 25px -5px var(--tw-shadow-color), 0 8px 10px -6px var(--tw-shadow-color);
1174 | box-shadow: var(--tw-ring-offset-shadow, 0 0 #0000), var(--tw-ring-shadow, 0 0 #0000), var(--tw-shadow);
1175 | }
1176 |
1177 | .group:hover .group-hover\:rotate-0 {
1178 | --tw-rotate: 0deg;
1179 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1180 | }
1181 |
1182 | .group:hover .group-hover\:rotate-180 {
1183 | --tw-rotate: 180deg;
1184 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1185 | }
1186 |
1187 | .group:hover .group-hover\:scale-\[400\%\] {
1188 | --tw-scale-x: 400%;
1189 | --tw-scale-y: 400%;
1190 | transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
1191 | }
1192 |
--------------------------------------------------------------------------------