├── 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 | {image_prop.alt} 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 | <div className='flex items-center justify-center h-[24rem] space-x-2'> 17 | <button onClick={props.ChooseProps.OnClick_1} className="w-1/3"> 18 | <Image image_prop={props.left} /> 19 | </button> 20 | <div className="flex flex-col items-center space-y-16 w-1/3"> 21 | <img src="/images/vs.webp"></img> 22 | <div className="flex flex-col items-center space-y-6"> 23 | <p className="text-2xl text-red-500 font-bold text-center">{props.LastVote}</p> 24 | </div> 25 | </div> 26 | <button onClick={props.ChooseProps.OnClick_2} className='w-1/3'> 27 | <Image image_prop={props.right} /> 28 | </button> 29 | </div> 30 | 31 | {/* 下方层:选择按钮 */} 32 | <div className="flex items-center justify-center 和-"> 33 | <ChooseButton prop={props.ChooseProps} /> 34 | </div> 35 | 36 | </Box> 37 | ); 38 | } 39 | 40 | export default RankingVS; -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="zh-CN"> 3 | 4 | <head> 5 | <meta charset="utf-8" /> 6 | <link rel="icon" href="%PUBLIC_URL%/ys.webp" /> 7 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 8 | <meta name="theme-color" content="#000000" /> 9 | <meta name="description" content="以撒的结合道具投票箱" /> 10 | <link rel="apple-touch-icon" href="%PUBLIC_URL%/ys.webp" /> 11 | <!-- 12 | manifest.json provides metadata used when your web app is installed on a 13 | user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/ 14 | --> 15 | <link rel="manifest" href="%PUBLIC_URL%/manifest.json" /> 16 | <!-- 17 | Notice the use of %PUBLIC_URL% in the tags above. 18 | It will be replaced with the URL of the `public` folder during the build. 19 | Only files inside the `public` folder can be referenced from the HTML. 20 | 21 | Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will 22 | work correctly both with client-side routing and a non-root public URL. 23 | Learn how to configure a non-root public URL by running `npm run build`. 24 | --> 25 | <title>以撒投票箱 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 | {MostVotedItem?.name} 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 | {MostDiffItemWinner?.name} 79 | {MostDiffItemWinner?.name}({MostDiffItemWinnerRank} 名) 80 |
81 |

82 |
83 | {MostDiffItemLoser?.name} 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 | {item.name} 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 |
231 | 232 |
233 |
234 | 235 |
236 |
237 | 238 |
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 | {item.name} 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 | {item.name} 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 | --------------------------------------------------------------------------------