This commit is contained in:
Jan Naahs 2020-09-20 20:07:15 +02:00
parent 7075de7d44
commit 9f0686a09e
12 changed files with 164 additions and 66 deletions

View File

@ -56,6 +56,7 @@
"@fortawesome/free-solid-svg-icons": "^5.14.0",
"@fortawesome/react-fontawesome": "^0.1.11",
"axios": "^0.19.2",
"fuse.js": "^6.4.1",
"react-hook-form": "^5.7.2",
"regenerator-runtime": "^0.13.7",
"semver": "^6.1.1",

View File

@ -1,19 +1,19 @@
import React from "react";
const Button = ({ children, type, onClick, isSubmit, className, size }) => {
const Button = ({ children, type, onClick, isSubmit, className, size, isDisabled = false }) => {
let color = '';
let padding = '';
switch (type) {
case 'success':
color = 'bg-green hover:glow-green hover:bg-green-light';
color = `bg-green ${isDisabled ? null : "hover:glow-green hover:bg-green-light" }`;
break;
case 'danger':
color = 'bg-red hover:glow-red hover:bg-red-light';
color = `bg-red ${isDisabled ? null : "hover:glow-red hover:bg-red-light"}`;
break;
default:
color = 'bg-gray-light hover:glow-orange hover:bg-orange'
color = `bg-gray-light ${isDisabled ? null : "hover:glow-orange hover:bg-orange"}`
}
switch (size) {
@ -25,7 +25,7 @@ const Button = ({ children, type, onClick, isSubmit, className, size }) => {
}
return (
<button onClick={onClick} className={`${className ? className: null} ${padding} ${color} inline-block accentuated text-black font-bold`}
<button onClick={onClick} disabled={isDisabled} className={`${className ? className: null} ${isDisabled ? "bg-opacity-50 cursor-not-allowed" : null} ${padding} ${color} inline-block accentuated text-black font-bold`}
type={isSubmit ? 'submit' : 'button'}>
{children}
</button>

View File

@ -1,6 +1,6 @@
import React from "react";
const Input = ({name, inputRef, placeholder = null, type="text", defaultValue=null}) => {
const Input = ({name, inputRef, placeholder = null, type="text", defaultValue=null, hasAutoComplete=true}) => {
return (
<input
className="shadow appearance-none border w-full py-2 px-3 text-black"
@ -9,6 +9,7 @@ const Input = ({name, inputRef, placeholder = null, type="text", defaultValue=nu
name={name}
id={name}
type={type}
autoComplete={hasAutoComplete ? "on" : "off"}
defaultValue={defaultValue}
/>
)

View File

@ -1,8 +1,8 @@
import React from "react";
const Select = ({name, inputRef, children}) => {
const Select = ({name, inputRef, children, className}) => {
return (
<div className="relative">
<div className={`${className} relative`}>
<select
className="shadow appearance-none border w-full py-2 px-3 text-black"
name={name}

View File

@ -9,12 +9,13 @@ import Tab from "../../components/Tabs/Tab";
import AddMod from "./components/AddMod/AddMod";
import UploadMod from "./components/UploadMod";
import LoadMods from "./components/LoadMods";
import Fuse from "fuse.js";
const Mods = () => {
const [installedMods, setInstalledMods] = useState([]);
const [factorioVersion, setFactorioVersion] = useState(null);
const [fuse, setFuse] = useState(undefined);
const fetchInstalledMods = () => {
modsResource.installed()
@ -28,25 +29,42 @@ const Mods = () => {
useEffect(() => {
server.factorioVersion()
.then(res => {
if (res) {
setFactorioVersion(res.base_mod_version)
}
.then(data => {
setFactorioVersion(data.base_mod_version)
fetchInstalledMods();
})
// fetch list of mods
modsResource.portal.list()
.then(res => {
setFuse(new Fuse(res.results, {
keys: [
{
"name": "name",
weight: 2
},
{
"name": "title",
weight: 1
}
],
minMatchCharLength: 3
}));
});
}, []);
return (
<div>
<TabControl>
<Tab title="Install Mod">
<AddMod refetchInstalledMods={fetchInstalledMods}/>
<AddMod refetchInstalledMods={fetchInstalledMods} fuse={fuse}/>
</Tab>
<Tab title="Upload Mod">
<UploadMod/>
</Tab>
<Tab title="Load Mod from Save">
<LoadMods/>
<LoadMods refreshMods={fetchInstalledMods}/>
</Tab>
</TabControl>
@ -82,7 +100,7 @@ const Mods = () => {
title="Mod packs"
className="mb-6"
content={
"Test"
"Test"
}
/>
</div>

View File

@ -5,7 +5,7 @@ import modResource from "../../../../../api/resources/mods";
const AddMod = ({refetchInstalledMods}) => {
const AddMod = ({refetchInstalledMods, fuse}) => {
const [isFactorioAuthenticated, setIsFactorioAuthenticated] = useState(false);
@ -16,7 +16,7 @@ const AddMod = ({refetchInstalledMods}) => {
}, []);
return isFactorioAuthenticated
? <AddModForm setIsFactorioAuthenticated={setIsFactorioAuthenticated} refetchInstalledMods={refetchInstalledMods}/>
? <AddModForm fuse={fuse} setIsFactorioAuthenticated={setIsFactorioAuthenticated} refetchInstalledMods={refetchInstalledMods}/>
: <FactorioLogin setIsFactorioAuthenticated={setIsFactorioAuthenticated}/>
}

View File

@ -1,33 +1,83 @@
import React from "react";
import React, {useCallback, useEffect, useState} from "react";
import modsResource from "../../../../../../api/resources/mods";
import Button from "../../../../../components/Button";
import Label from "../../../../../components/Label";
import {useForm} from "react-hook-form";
import Input from "../../../../../components/Input";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faExternalLinkAlt} from "@fortawesome/free-solid-svg-icons/faExternalLinkAlt";
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
const LinkModPortal = () => {
return <a href="https://mods.factorio.com" target="_blank" className="px-2 text-blue hover:text-blue-light">Mod
Portal <FontAwesomeIcon icon={faExternalLinkAlt}/></a>
}
const AddModForm = ({setIsFactorioAuthenticated}) => {
const AddModForm = ({setIsFactorioAuthenticated, fuse}) => {
const {register, handleSubmit} = useForm();
const {register, watch, setValue} = useForm();
const [suggestedMods, setSuggestedMods] = useState([]);
const [selectedMod, setSelectedMod] = useState(null)
const [autocomplete, setAutocomplete] = useState(NaN);
const mod = watch('mod');
const logout = () => {
modsResource.portal.logout()
.then(res => {
setIsFactorioAuthenticated(false);
});
.then(() => setIsFactorioAuthenticated(false));
}
const updateSuggestedMods = () => {
if (typeof fuse != "undefined") {
clearTimeout(autocomplete)
setAutocomplete(setTimeout(() => setSuggestedMods(fuse.search(mod || '')), 200));
}
};
const selectMod = mod => {
clearTimeout(autocomplete);
setValue('mod', mod.item.title); // triggers effect for mod
setSuggestedMods([]);
setSelectedMod(mod);
}
// get triggered if mod changed
useEffect(() => {
if (selectedMod === null) {
updateSuggestedMods();
} else if (selectedMod.item.title !== mod) {
setSelectedMod(null);
updateSuggestedMods();
}
}, [mod]);
const addMod = data => {
// todo install selected mod
console.log(data);
// todo update list of installed mods
}
return (
<form >
<div className="mb-4">
<form onSubmit={addMod}>
<div className="mb-4 relative">
<Label text="Mod" htmlFor="mod"/>
<Input inputRef={register({required: true})} name="mod"/>
{ typeof fuse !== "undefined"
? <Input inputRef={register({required: true})} hasAutoComplete={false} name="mod"/>
: <div className="border border-gray-medium w-full py-2 px-3 text-white">
<FontAwesomeIcon icon={faSpinner} spin={true}/> Loading List of Mods from <LinkModPortal/>
</div>
}
{suggestedMods.length > 0 &&
<ul className="bg-white text-black h-64 overflow-y-scroll absolute bottom-0 left-0 w-full -mb-64">
{suggestedMods.map((mod, index) => <li className="px-2 py-1 cursor-pointer hover:bg-blue-light" onClick={() => selectMod(mod)} key={index}>{mod.item.title}</li>)}
</ul>
}
</div>
<Button isSubmit={true}>Install</Button>
<Button onClick={logout} type="danger">Logout</Button>
<Button isDisabled={selectedMod === null} isSubmit={true} className="mr-2">Install</Button>
<Button onClick={logout} type="danger" className="mr-2">Logout</Button>
<LinkModPortal/>
</form>
)
)
}
export default AddModForm;

View File

@ -4,28 +4,33 @@ import Select from "../../../components/Select";
import Label from "../../../components/Label";
import {useForm} from "react-hook-form";
import Button from "../../../components/Button";
import modsResource from "../../../../api/resources/mods";
const LoadMods = () => {
const LoadMods = ({refreshMods}) => {
const [saves, setSaves] = useState([]);
const {register, handleSubmit} = useForm();
useEffect(() => {
(async () => {
const res = await savesResource.list()
if (res.success) {
setSaves(res.data);
}
setSaves(await savesResource.list());
})();
}, [])
}, []);
const loadMods = data => {
savesResource.mods(data.save)
.then(({mods}) => {
modsResource.portal.installMultiple(mods).then(refreshMods)
})
}
return (
<form >
<form onSubmit={handleSubmit(loadMods)}>
<Label text="Save" htmlFor="save"/>
<Select name="save" inputRef={register}>
<Select name="save" inputRef={register} className="mb-4">
{saves?.map(save => <option value={save.name} key={save.name}>{save.name}</option>)}
</Select>
<Button isSubmit={true}>Save</Button>
<Button isSubmit={true}>Load</Button>
</form>
)
}

View File

@ -1,18 +1,24 @@
import React, {useState} from "react";
import Button from "../../../components/Button";
import Label from "../../../components/Label";
const UploadMod = () => {
const [fileName, setFileName] = useState('Select File ...');
return (
<div className="relative bg-white shadow text-black h-full w-full">
<input
className="absolute left-0 top-0 opacity-0 cursor-pointer w-full h-full"
onChange={e => setFileName(e.currentTarget.files[0].name)}
name="savefile"
id="savefile" type="file"/>
<div className="px-2 py-3">{fileName}</div>
</div>
<form>
<Label text="Save" htmlFor="savefile"/>
<div className="relative bg-white shadow text-black h-full w-full mb-4">
<input
className="absolute left-0 top-0 opacity-0 cursor-pointer w-full h-full"
onChange={e => setFileName(e.currentTarget.files[0].name)}
name="savefile"
id="savefile" type="file"/>
<div className="px-2 py-2">{fileName}</div>
</div>
<Button isSubmit={true}>Upload</Button>
</form>
)
}

View File

@ -1,31 +1,38 @@
import {useForm} from "react-hook-form";
import Button from "../../../components/Button";
import React from "react";
import React, {useState} from "react";
import saves from "../../../../api/resources/saves";
import Label from "../../../components/Label";
import Input from "../../../components/Input";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
import {faSpinner} from "@fortawesome/free-solid-svg-icons";
const CreateSaveForm = ({onSuccess}) => {
const {register, handleSubmit, errors} = useForm();
const [isLoading, setIsLoading] = useState(false);
const onSubmit = async (data, e) => {
const res = await saves.create(data.savefile);
if (res) {
e.target.reset();
onSuccess();
}
setIsLoading(true)
saves.create(data.savefile)
.then(() => {
e.target.reset();
onSuccess();
})
.finally(() => setIsLoading(false))
};
return (
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-6">
<Label text="Savefile Name" htmlFor="savefile"/>
<Input inputRef={register({required: true})} name="savefile"/>
{errors.savefile && <span className="block text-red">Savefile Name is required</span>}
</div>
<Button type="success" isSubmit={true}>Create Save</Button>
</form>
<div className="mb-6">
<Label text="Savefile Name" htmlFor="savefile"/>
<Input inputRef={register({required: true})} name="savefile"/>
{errors.savefile && <span className="block text-red">Savefile Name is required</span>}
</div>
{ isLoading
? <Button type="success" isSubmit={true}>Create Save <FontAwesomeIcon icon={faSpinner} spin={true}/></Button>
: <Button type="success" isSubmit={true}>Create Save</Button>
}
</form>
)
}

View File

@ -36,10 +36,6 @@ const mods = {
},
portal: {
login: async (username, password) => {
// const data = new FormData();
// data.set('username', username);
// data.set('password', password);
const response = await client.post('/api/mods/portal/login', {
username,
password
@ -53,6 +49,14 @@ const mods = {
logout: async () => {
const response = await client.get('/api/mods/portal/logout');
return response.data
},
installMultiple: async mods => {
const response = await client.post('/api/mods/portal/install/multiple', mods);
return response.data
},
list: async () => {
const response = await client.get('/api/mods/portal/list');
return response.data
}
}
}

View File

@ -23,5 +23,11 @@ export default {
}
});
return response.data;
},
mods: async save => {
const response = await client.post("/api/saves/mods", {
saveFile: save
});
return response.data;
}
}