This commit is contained in:
muhammadeko 2022-04-30 13:31:57 +07:00
parent 23d6ba1a11
commit 8c8e00b687
No known key found for this signature in database
GPG Key ID: 51366716C10E98B1
24 changed files with 10600 additions and 21 deletions

2
.gitignore vendored
View File

@ -11,6 +11,8 @@
# production
/build
.idea
# misc
.DS_Store
.env.local

5
.idea/.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/

7
.idea/discord.xml Normal file
View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DiscordProjectSettings">
<option name="show" value="PROJECT_FILES" />
<option name="description" value="" />
</component>
</project>

12
.idea/gc-tools.iml Normal file
View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/temp" />
<excludeFolder url="file://$MODULE_DIR$/.tmp" />
<excludeFolder url="file://$MODULE_DIR$/tmp" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
</profile>
</component>

8
.idea/modules.xml Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/gc-tools.iml" filepath="$PROJECT_DIR$/.idea/gc-tools.iml" />
</modules>
</component>
</project>

6
.idea/vcs.xml Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

9
Dockerfile Normal file
View File

@ -0,0 +1,9 @@
FROM node:16-slim
ADD . /booklist-react
WORKDIR /booklist-react
RUN npm install
RUN npm install -g serve
RUN npm run build
CMD ["npm","install","-g","serve"]
CMD ["serve", "-s", "build"]
EXPOSE 3000

18
docker-compose.yml Normal file
View File

@ -0,0 +1,18 @@
version: '3.8'
services:
exzork_booklist_react:
container_name: exzork_booklist_react
build:
context: .
dockerfile: Dockerfile
volumes:
- .:/booklist-react
- /booklist-react/node_modules
- /booklist-react/build
environment:
- VIRTUAL_HOST=booklist-react.exzork.me
- LETSENCRYPT_HOST=booklist-react.exzork.me
- LETSENCRYPT_EMAIL=muhammadeko.if@gmail.com
networks:
default :
name: nginx-proxy

888
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,10 @@
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.9.0",
"@emotion/styled": "^11.8.1",
"@mui/material": "^5.6.3",
"@tailwindcss/forms": "^0.5.0",
"@testing-library/jest-dom": "^5.16.4",
"@testing-library/react": "^13.1.1",
"@testing-library/user-event": "^13.5.0",
@ -10,9 +14,17 @@
"@types/node": "^16.11.32",
"@types/react": "^18.0.8",
"@types/react-dom": "^18.0.3",
"autoprefixer": "^10.4.5",
"axios": "^0.27.2",
"fs": "^0.0.1-security",
"lodash": "^4.17.21",
"postcss": "^8.4.12",
"react": "^18.1.0",
"react-dom": "^18.1.0",
"react-router-dom": "^6.3.0",
"react-scripts": "5.0.1",
"react-select-search": "^3.0.9",
"tailwindcss": "^3.0.24",
"typescript": "^4.6.4",
"web-vitals": "^2.1.4"
},
@ -39,5 +51,8 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"@types/lodash": "^4.14.182"
}
}

6
postcss.config.js Normal file
View File

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

View File

@ -1,26 +1,46 @@
import React from 'react';
import React, {Component} from 'react';
import logo from './logo.svg';
import './App.css';
import Main from "./components/Main";
import {BrowserRouter} from "react-router-dom";
import Navbar from "./components/Navbar";
import Header from "./components/Header";
function App() {
return (
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.tsx</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
interface IState{
headerTitle: string;
}
export default App;
export default class App extends Component<{},IState>{
constructor(props:{}) {
super(props);
this.state = {
headerTitle: 'Hello World'
}
}
handleHeaderTitleChange = (headerTitle:string) =>{
this.setState({
headerTitle: headerTitle
})
}
render() {
const NavbarProps = {
headerTitle: this.state.headerTitle,
handleHeaderTitleChange: this.handleHeaderTitleChange
}
const HeaderProps = {
name: this.state.headerTitle,
}
const MainProps = {
handleHeaderTitleChange: this.handleHeaderTitleChange
}
return(
<BrowserRouter>
<Navbar {...NavbarProps}/>
<Header {...HeaderProps}/>
<Main {...MainProps}/>
</BrowserRouter>
)
}
}

11
src/components/Header.tsx Normal file
View File

@ -0,0 +1,11 @@
import React from "react";
export default function Header(props:{name:string}){
return (
<header className="bg-white shadow">
<div className="max-w-7xl mx-auto py-6 px-4 sm:px-6 lg:px-8">
<div className="text-xl md:text-3xl font-bold text-gray-900">{props.name}</div>
</div>
</header>
);
}

19
src/components/Main.tsx Normal file
View File

@ -0,0 +1,19 @@
import React from "react";
import RouteMenu from "./RouteMenu";
import {RouteProps} from "react-router-dom";
interface Props extends RouteProps{
handleHeaderTitleChange: (title: string) => void;
}
export default function Main(props: Props) {
const RouteMenuProps = {
handleHeaderTitleChange: props.handleHeaderTitleChange
};
return (
<main className="bg-gray-100 min-h-full">
<div className="max-w-7xl mx-auto py-6 sm:px-6 lg:px-8 ">
<RouteMenu {...RouteMenuProps}/>
</div>
</main>
);
}

View File

@ -0,0 +1,25 @@
import React from "react";
import {useLocation, Link, useNavigate} from "react-router-dom"
export default function MenuDesktop(props:{handleHeaderTitleChange: (title:string) => void}) {
const location = useLocation();
const navigate = useNavigate();
const {handleHeaderTitleChange} = props;
/*
{location.pathname === "/" ? (
<Link to="/" className="menu-item bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium">Home</Link>
) : (
<Link to="/" onClick={() => handleHeaderTitleChange("Home")} className="menu-item text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Home</Link>
)}
*/
return (
<>
{location.pathname === "/artifact" ? (
<Link to="/artifact" className="menu-item bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium">Artifact</Link>
) : (
<Link to="/artifact" onClick={() => handleHeaderTitleChange("Artifact Command Generator")} className="menu-item text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Artifact</Link>
)}
</>
)
}

View File

@ -0,0 +1,25 @@
import React from "react";
import {useLocation, Link} from "react-router-dom";
export default function MenuMobile(props: any) {
const location = useLocation();
const {handleHeaderTitleChange} = props;
/*
{location.pathname === "/" ? (
<Link to="/" className="menu-item block bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium">Home</Link>
) : (
<Link to="/" onClick={() => handleHeaderTitleChange("Home")} className="menu-item block text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Home</Link>
)}
*/
return (
<>
{location.pathname === "/artifact" ? (
<Link to="/artifact" className="menu-item block bg-gray-900 text-white px-3 py-2 rounded-md text-sm font-medium">Artifact</Link>
) : (
<Link to="/artifact" onClick={() => handleHeaderTitleChange("Artifact Command Generator")} className="menu-item block text-gray-300 hover:bg-gray-700 hover:text-white px-3 py-2 rounded-md text-sm font-medium">Artifact</Link>
)}
</>
)
}

77
src/components/Navbar.tsx Normal file
View File

@ -0,0 +1,77 @@
import React from "react";
import MenuDesktop from "./MenuDesktop";
import MenuMobile from "./MenuMobile";
class Navbar extends React.Component {
handleHeaderTitleChange;
toggleMobileMenu = () => {
const menuShow = document.querySelector(".menu-show");
const menuHide = document.querySelector(".menu-hide");
const mobileMenu = document.querySelector("#mobile-menu");
if (mobileMenu && menuHide && menuShow){
if (menuShow.classList.contains("block")) {
menuShow.classList.remove("block");
menuShow.classList.add("hidden");
menuHide.classList.remove("hidden");
menuHide.classList.add("block");
mobileMenu.classList.remove("hidden");
} else {
menuShow.classList.remove("hidden");
menuShow.classList.add("block");
menuHide.classList.remove("block");
menuHide.classList.add("hidden");
mobileMenu.classList.add("hidden");
}
}
}
constructor(props:{handleHeaderTitleChange: (title:string) => void}) {
super(props);
const {handleHeaderTitleChange} = props;
this.handleHeaderTitleChange = handleHeaderTitleChange;
const pathname =window.location.pathname;
if(pathname==="/") handleHeaderTitleChange("Home")
else if(pathname==="/artifact") handleHeaderTitleChange("Artifact Command Generator")
}
render() {
return(
<nav className="bg-gray-800">
<div className="max-w-7xl mx-auto px-2 sm:px-6 lg:px-8">
<div className="relative flex items-center justify-between h-16">
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
{/* Mobile menu button */}
<button onClick={this.toggleMobileMenu} className="inline-flex items-center justify-center p-2 rounded-md text-gray-400 hover:text-white hover:bg-gray-700 focus:outline-none focus:bg-gray-700 focus:text-white transition duration-150 ease-in-out">
<svg className="menu-show block h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6h16M4 12h16M4 18h16"/>
</svg>
<svg className="menu-hide hidden h-6 w-6" stroke="currentColor" fill="none" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12"/>
</svg>
</button>
</div>
<div className="flex-1 flex items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex-shrink-0 flex items-center text-white text-lg">
Grasscutter Tools
</div>
<div className="hidden sm:block sm:ml-6 w-full">
<div className="flex w-full">
<MenuDesktop handleHeaderTitleChange={this.handleHeaderTitleChange}/>
</div>
</div>
</div>
</div>
</div>
<div className="hidden sm:hidden" id="mobile-menu">
<div className="px-2 pt2 pb-3 space-y-1">
<MenuMobile handleHeaderTitleChange={this.handleHeaderTitleChange}/>
</div>
</div>
</nav>
)
}
}
export default Navbar;

View File

@ -0,0 +1,22 @@
import {Routes, Route, RouteProps} from "react-router-dom";
import React from "react";
import Artifacts from "./pages/Artifacts";
interface RouteMenuProps extends RouteProps{
handleHeaderTitleChange: (title: string) => void;
}
export default function RouteMenu(props: RouteMenuProps) {
const defaultProps = {
handleHeaderTitleChange: props.handleHeaderTitleChange
};
return (
<Routes>
<Route path="/" element={
<div>Nothing Here</div>
}/>
<Route path="/artifact" element={
<Artifacts/>
}/>
</Routes>
);
}

View File

@ -0,0 +1,189 @@
import {ChangeEvent, useEffect, useState} from "react";
import ReliquaryDataProvider, {IReliquaryAffix, IReliquaryMain} from "../providers/ReliquaryDataProvider";
import {Autocomplete, Chip, createFilterOptions, TextField} from "@mui/material";
//@ts-ignore
import _ from "lodash";
interface IArtifact {
id: number;
name: string;
}
export default function Artifacts() {
const [reliquaryMains, setReliquaryMains] = useState<IReliquaryMain[]>([]);
const [reliquaryAffixes, setReliquaryAffixes] = useState<IReliquaryAffix[]>([]);
const [uid, setUid] = useState(0);
const [selectedArtifact, setSelectedArtifact] = useState(0);
const [selectedMainStat, setSelectedMainStat] = useState(0);
const [selectedAffixes, setSelectedAffixes] = useState<number[]>([]);
const [selectedAffixesAmount, setSelectedAffixesAmount] = useState<Record<number, number>>({});
const [artifactData, setArtifactData] = useState<IArtifact[]>([]);
const [generatedArtifact, setGeneratedArtifact] = useState("/giveart ");
useEffect(() => {
const dataArtifact: IArtifact[] = [];
const initReliquaryData = async () => {
await ReliquaryDataProvider.init();
setReliquaryMains(ReliquaryDataProvider.getReliquaryMains());
setReliquaryAffixes(ReliquaryDataProvider.getReliquaryAffixes());
};
fetch("https://gist.githubusercontent.com/exzork/0c5ab10f35f6aa718735380d8ce585db/raw/bc14a3f87f3a49aa053a43db999d04a8f09dcd71/GM%2520Handbook.txt")
.then(res => res.text())
.then(text => {
const lines = text.split("\n");
lines.forEach(line => {
const lineSplit = line.split(" : ");
if (lineSplit.length === 2) {
const id = parseInt(lineSplit[0]);
const name = lineSplit[1];
if ((id >= 23341 && id <= 30000) || (id >= 51110 && id <= 99554)) {
if (dataArtifact.filter((x) => x.name === lineSplit[1]).length === 0) {
dataArtifact.push({id: id, name: name});
}
}
}
});
setArtifactData(dataArtifact);
initReliquaryData();
});
}, []);
useEffect(() => {
handleGeneratedArtifact()
}, [uid, selectedArtifact, selectedMainStat, selectedAffixes, selectedAffixesAmount]);
const handleArtifactChange = (event: any, value: any) => {
if (value !== null) {
setSelectedArtifact(value.id);
}
};
const handleMainStatChange = (event: any, value: any) => {
if (value !== null) {
setSelectedMainStat(value.Id);
}
};
const handleAffixSelected = (event: ChangeEvent<HTMLInputElement>, amount = false, affixId=0) => {
let newSelectedAffixes = [...selectedAffixes];
let newSelectedAffixesAmount = {...selectedAffixesAmount};
if (amount) {
newSelectedAffixesAmount[affixId]=Number(event.target.value);
} else {
if (newSelectedAffixes.indexOf(Number(event.currentTarget.value)) === -1) {
newSelectedAffixes.push(Number(event.currentTarget.value));
newSelectedAffixesAmount[Number(event.currentTarget.value)] = 1;
} else {
newSelectedAffixes.splice(newSelectedAffixes.indexOf(Number(event.currentTarget.value)), 1);
newSelectedAffixesAmount[Number(event.currentTarget.value)] = 0;
}
}
setSelectedAffixes(newSelectedAffixes);
setSelectedAffixesAmount(newSelectedAffixesAmount);
};
const getPercent = (affix:IReliquaryAffix)=>{
if (affix.PropType.indexOf("PERCENT") !== -1 || affix.PropType.indexOf("CRITICAL") !== -1 || affix.PropType.indexOf("EFFICIENCY") !== -1 || affix.PropType.indexOf("HURT") !== -1) {
return parseFloat(String(affix.PropValue*100)).toPrecision(3) + "%";
}
return parseInt(String(affix.PropValue));
};
const handleGeneratedArtifact = () => {
let selectedAffixesCombine: (string | number)[] = [];
if (selectedAffixes.length > 0) {
selectedAffixesCombine = selectedAffixes.map(x => {
if (selectedAffixesAmount[x] > 1) {
return x + "," + selectedAffixesAmount[x];
}
return x;
});
}
const generated = "/giveart "+uid+" "+selectedArtifact+" "+selectedMainStat+" "+selectedAffixesCombine.join(" ")+" 21";
setGeneratedArtifact(generated);
};
return (
<form method="POST" className="space-y-8 divide-y divide-gray-200 bg-white p-10">
<div className="space-y-8 divide-y divide-gray-200 sm:space-y-5">
<div>
<div>
<p className="mt-1 max-w-2xl text-sm text-gray-500">Please fill artifact details</p>
</div>
<div className="mt-6 sm:mt-5 space-y-6 sm:space-y-5">
<div
className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label htmlFor="name"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2">
UID
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<input type="text" aria-label="UID" name="uid" id="uid" className="block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" onChange={(event) => setUid(parseInt(event.target.value))}/>
</div>
</div>
<div
className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label htmlFor="description"
className="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
Artifact Name
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<Autocomplete
aria-label="Artifact Name" id="ArtifactName"
className="block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"
options={artifactData}
getOptionLabel={(option) => option.name}
onChange={handleArtifactChange}
renderInput={(params) => <TextField {...params} label="Artifact Name" variant="outlined"/>}
/>
</div>
</div>
<div
className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label htmlFor="image"
className="block text-sm font-medium text-gray-700 sm:mt-px sm:pt-2">
Main Stats
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2">
<div
className="relative h-10 rounded-lg flex justify-center items-center">
<Autocomplete
aria-label="Artifact Main Stats" id="ArtifactMainStats"
className="block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md"
options={reliquaryMains}
getOptionLabel={(option) => option.PropType}
onChange={handleMainStatChange}
renderInput={(params) => <TextField {...params} label="Main Stats" variant="outlined"/>}
/>
</div>
</div>
</div>
<div
className="sm:grid sm:grid-cols-3 sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<label htmlFor="start_date"
className="block text-sm font-medium leading-5 text-gray-700 sm:mt-px sm:pt-2">
Artifact Sub Stats
</label>
<div className="mt-1 sm:mt-0 sm:col-span-2 h-48 overflow-auto">
{reliquaryAffixes.map((affix, index) => {
return (
<div key={index} className="flex">
<input type="checkbox" className="mr-4 ml-4 focus:ring-indigo-500 h-4 w-4 mt-1 text-indigo-600 border-gray-300 rounded" value={affix.Id} id={"select-"+affix.Id} onChange={(e)=>handleAffixSelected(e,false,0)}/>
<label className="flex-grow" htmlFor={"select-"+affix.Id}>{affix.PropType +" - "+getPercent(affix)}</label>
<input type="number" defaultValue="1" min="1" className="flex-none block shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" onChange={(e)=>handleAffixSelected(e,true, affix.Id)}/>
</div>
);
})}
</div>
</div>
<div className="block sm:gap-4 sm:items-start sm:border-t sm:border-gray-200 sm:pt-5">
<input type="text" onClick={(e)=>{navigator.clipboard.writeText(e.currentTarget.value)}} className="block w-full shadow-sm focus:ring-indigo-500 focus:border-indigo-500 sm:text-sm border-gray-300 rounded-md" value={generatedArtifact} readOnly/>
</div>
</div>
</div>
</div>
</form>
);
}

View File

@ -0,0 +1,46 @@
export interface IReliquaryMain {
Id: number;
PropDepotId: number;
PropType:string;
AffixName:string;
}
export interface IReliquaryAffix {
Id: number;
DepotId: number;
GroupId: number;
PropType:string;
PropValue:number;
}
export default class ReliquaryDataProvider {
private static reliquaryMains:IReliquaryMain[] = [];
private static reliquaryAffixes:IReliquaryAffix[] = [];
public static async init(){
await this.loadReliquaryMain();
await this.loadReliquaryAffixes();
}
private static async loadReliquaryMain(){
let data = await fetch("https://raw.githubusercontent.com/Dimbreath/GenshinData/master/ExcelBinOutput/ReliquaryMainPropExcelConfigData.json");
let json:IReliquaryMain[] = await data.json();
this.reliquaryMains = [];
json.forEach(element => {
if (this.reliquaryMains.filter(x => x.PropType === element.PropType).length === 0)
this.reliquaryMains.push(element)
});
}
private static async loadReliquaryAffixes(){
let data = await fetch("https://raw.githubusercontent.com/Dimbreath/GenshinData/master/ExcelBinOutput/ReliquaryAffixExcelConfigData.json");
let json = await data.json();
this.reliquaryAffixes = json;
}
public static getReliquaryMains():IReliquaryMain[]{
return this.reliquaryMains;
}
public static getReliquaryAffixes():IReliquaryAffix[]{
return this.reliquaryAffixes;
}
}

View File

@ -11,3 +11,6 @@ code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@tailwind base;
@tailwind components;
@tailwind utilities;

11
tailwind.config.js Normal file
View File

@ -0,0 +1,11 @@
module.exports = {
content: [
"./src/**/*.{js,jsx,ts,tsx}",
],
theme: {
extend: {},
},
plugins: [
require('@tailwindcss/forms')
],
}

9149
yarn.lock Normal file

File diff suppressed because it is too large Load Diff