Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@Acen
Created August 29, 2022 22:33
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Acen/7dd1febf07c026f3a379d7ba76a9f2f7 to your computer and use it in GitHub Desktop.
Save Acen/7dd1febf07c026f3a379d7ba76a9f2f7 to your computer and use it in GitHub Desktop.
CF Request
import type {Validator} from "remix-validated-form";
import {withZod} from "@remix-validated-form/with-zod";
import {z} from "zod";
import {address, UserAgent} from "~/requests/client/config";
interface IRuntimeForm {
[key: string]: any;
}
export class RunTimeFullScanForm<T extends IRuntimeForm> {
constructor(private form: T) {
}
public formData(): FormData {
const form = new FormData();
Object.keys(this.form).forEach((key) => {
if (this.form[key] !== undefined) {
form.append(key, this.form[key])
}
})
return form;
}
}
export type FullScanFields = {
scan_hours: number
sale_amount: number
roi: number
minimum_stack_size: number
minimum_profit_amount: number
price_per_unit: number
world: string
};
export const validator: Validator<FullScanFields> = withZod(z.object({
scan_hours: z.number().positive().min(1).max(128),
sale_amount: z.number().min(1),
roi: z.number().min(1),
minimum_stack_size: z.number().min(1),
minimum_profit_amount: z.number().min(1),
price_per_unit: z.number().min(1),
world: z.string()
}));
const remappedKeys = (fields: any, setDefaults = true) => {
const map = setDefaults ? new Map(Object.entries(defaults)) : new Map();
Array.from(fields as [string, string | boolean | number][]).map((field) => {
let value = field[1];
// checkboxes whyyyyyy
if (value === 'on') {
value = true;
}
if (!isNaN(parseInt(value as string))) {
value = parseInt(value as string)
}
map.set(keyMap(field[0]), value);
return field;
});
return map;
}
const keyMap: (key: string) => string = (key) => {
switch (key) {
case 'sale_amount':
return 'min_sales';
case 'world':
return 'home_server';
case 'minimum_stack_size':
return 'min_stack_size';
case 'hq_only':
return 'hq';
case 'minimum_profit_amount':
return 'min_profit_amount';
case 'price_per_unit':
return 'min_desired_avg_ppu';
case 'out_of_stock':
return 'show_out_stock';
case 'roi':
return 'preferred_roi';
case 'scan_hours':
return 'hours_ago';
default:
// region_wide
// include_vendor
return key;
}
}
const defaults = {
'preferred_roi': 50,
'min_profit_amount': 10000,
'min_desired_avg_ppu': 10000,
'min_stack_size': 1,
'hours_ago': 24,
"min_sales": 5,
"hq": false,
"home_server": "Midgardsormr",
"filters": "all",
"region_wide": false,
"include_vendor": false,
"show_out_stock": true
}
export type ResponseType = {
"R.O.I": number, "avg_ppu": number, "home_server_price": number, "home_update_time": string, // @todo datetime
"ppu": number, "profit_amount": number, "profit_raw_percent": number, "real_name": string, "sale_rates": number, // @todo decimal
"server": string, // @todo world / datacenter
"stack_size": number, "update_time": string, // @todo datetime
"url": string // @todo URL
}
const FullScanRequest: (args: RunTimeFullScanForm<FullScanFields>) => Promise<Response> = async (args) => {
const data = remappedKeys(args.formData());
return fetch(`${address}/api/scan`, {
'method': 'POST', headers: {
"Content-Type": "application/json", "User-Agent": UserAgent
}, body: JSON.stringify(Object.fromEntries(data.entries()))
});
}
export default FullScanRequest;
import {Form, useActionData} from "@remix-run/react";
import type {ActionFunction} from "@remix-run/cloudflare";
import {getUserSessionData} from "~/sessions";
import type {FullScanFields} from "~/requests/FullScan";
import FullScanRequest, {RunTimeFullScanForm} from "~/requests/FullScan";
import type {ErrorBoundaryComponent} from "@remix-run/cloudflare";
import {classNames} from "~/utils";
import {useState} from "react";
import FullScanResultTable from "~/routes/queries/FullScanResultTable";
export const action: ActionFunction = async ({request, params}) => {
const formData = await request.formData();
const session = await getUserSessionData(request);
formData.append('world', session.getWorld());
const typedFormData = new RunTimeFullScanForm<FullScanFields>(Object.fromEntries(formData) as unknown as FullScanFields)
const scan = FullScanRequest(typedFormData);
return await scan.then((response) => response.json()).then((data) => {
// return null;
// console.log('req result', data);
return Object.entries(data).map((entry: [string, any]) => {
return {
id: parseInt(entry[0]), ...entry[1]
}
})
}).catch((error) => {
return error;
});
}
export const ErrorBoundary: ErrorBoundaryComponent = ({error}) => {
console.error('errorBoundary', error);
return <pre>{JSON.stringify(error.message)}</pre>
}
const FullScan = () => {
const results = useActionData();
const [isRunning, setIsRunning] = useState<boolean>(false);
const onSubmit = (e: MouseEvent) => {
if (isRunning) {
e.preventDefault();
} else {
setIsRunning(true);
}
}
if (results) {
console.log('results, pre table render', results);
return <FullScanResultTable rows={results}/>
}
return <main className="flex-1">
<div className="py-6">
<Form method="post">
<div className="max-w-7xl mx-auto px-4 sm:px-6 md:px-8">
<h1 className="text-2xl font-semibold text-gray-900 py-6">Full Scan</h1>
<div className="mt-5 md:mt-0 md:col-span-2 py-6">
<div className="shadow overflow-hidden sm:rounded-md">
<div className="px-4 py-5 bg-white sm:p-6">
<div className="grid grid-cols-6 gap-6">
<div className="col-span-6 sm:col-span-2">
<label htmlFor="scan-hours" className="block text-sm font-medium text-gray-700">
Scan Hours
</label>
<div className={`mt-1 flex rounded-md shadow-sm`}>
<input
type="number"
id="scan-hours"
defaultValue={24}
name="scan_hours"
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-l-md focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
<span
className={`inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm`}>
hours
</span>
</div>
<p className="mt-2 text-sm text-gray-500">
The time period to search over. ex: <code>24</code> is the past 24 hours.
For more items to sell choose a higher number.
</p>
</div>
<div className="col-span-6 sm:col-span-2">
<label htmlFor="sale-amt" className="block text-sm font-medium text-gray-700">
Sale Amount
</label>
<input
type="number"
id="sale-amt"
defaultValue={5}
name="sale_amount"
className="mt-1 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
<p className="mt-2 text-sm text-gray-500">
Number of sales in that time. ex: `5` is 5 sales in that selected time
period. For more items to sell choose a lower number.
</p>
</div>
<div className="col-span-6 sm:col-span-2">
<label htmlFor="roi" className="block text-sm font-medium text-gray-700">
Return on Investment
</label>
<div className={`mt-1 flex rounded-md shadow-sm`}>
<input
type="number"
name="roi"
defaultValue={50}
id="roi"
className="flex-1 min-w-0 block w-full px-3 py-2 rounded-none rounded-l-md focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
<span
className={`inline-flex items-center px-3 rounded-r-md border border-l-0 border-gray-300 bg-gray-50 text-gray-500 sm:text-sm`}>
%
</span>
</div>
<p className="mt-2 text-sm text-gray-500">
Desired R.O.I (return on investment): ex: `50` means that 50% of the revenue
you get from a sale should be all profit (after tax). For more profit,
choose a higher number from 1 to 100.
</p>
</div>
<div className="col-span-6 sm:col-span-2">
<label htmlFor="minimum_stack_size"
className="block text-sm font-medium text-gray-700">
Minimum Stack Size
</label>
<div className={`mt-1 flex rounded-md shadow-sm`}>
<input
type="number"
name="minimum_stack_size"
defaultValue={1}
id="minimum_stack_size"
className="flex-1 min-w-0 block w-full px-3 py-2 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Desired Min Stack Size. ex: `10` is only show deals you can get in stacks of
10 or greater. For more items to sell choose a lower number.
</p>
</div>
<div className="col-span-6 sm:col-span-2">
<label htmlFor="minimum_profit_amount"
className="block text-sm font-medium text-gray-700">
Minimum Profit Amount
</label>
<div className={`mt-1 flex rounded-md shadow-sm`}>
<input
type="number"
name="minimum_profit_amount"
defaultValue={10000}
id="minimum_profit_amount"
className="flex-1 min-w-0 block w-full px-3 py-2 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Desired Min Profit Amount. ex: `10000` is only show deals that yields 10000
gil profit or greater. For more items to sell choose a lower number.
</p>
</div>
<div className="col-span-6 sm:col-span-2">
<label htmlFor="price_per_unit"
className="block text-sm font-medium text-gray-700">
Average Price Per Unit
</label>
<div className={`mt-1 flex rounded-md shadow-sm`}>
<input
type="number"
name="price_per_unit"
defaultValue={10000}
id="price_per_unit"
className="flex-1 min-w-0 block w-full px-3 py-2 focus:ring-blue-500 focus:border-blue-500 block w-full shadow-sm sm:text-sm border-gray-300 rounded-md"
/>
</div>
<p className="mt-2 text-sm text-gray-500">
Desired Average Price Per Unit. ex: `10000` is only show deals that sell on
average for 10000 gil or greater. For more items to sell choose a lower
number.
</p>
</div>
<div className="col-span-6 sm:col-span-2">
<fieldset className="space-y-5">
<legend className="sr-only">Force HQ only</legend>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
id="hq_only"
aria-describedby="comments-description"
name="hq_only"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="hq_only" className="font-medium text-gray-700">
Enable HQ only
</label>
<p className="mt-2 text-sm text-gray-500">
Only search for hq prices
</p>
</div>
</div>
</fieldset>
</div>
<div className="col-span-6 sm:col-span-2">
<fieldset className="space-y-5">
<legend className="sr-only">Region Wide Search</legend>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
id="region_wide"
aria-describedby="comments-description"
name="region_wide"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="region_wide" className="font-medium text-gray-700">
Region Wide Search
</label>
<p className="mt-2 text-sm text-gray-500">
Search all servers in all DataCenters in your region.
</p>
</div>
</div>
</fieldset>
</div>
<div className="col-span-6 sm:col-span-2">
<fieldset className="space-y-5">
<legend className="sr-only">Include Vendor Prices</legend>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
id="include_vendor"
aria-describedby="comments-description"
name="include_vendor"
type="checkbox"
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="include_vendor"
className="font-medium text-gray-700">
Include Vendor Prices
</label>
<p className="mt-2 text-sm text-gray-500">
Compare market prices vs vendor prices on NQ items that can be
purchased from vendors.
</p>
</div>
</div>
</fieldset>
</div>
<div className="col-span-6 sm:col-span-2">
<fieldset className="space-y-5">
<legend className="sr-only">Include Out of Stock</legend>
<div className="relative flex items-start">
<div className="flex items-center h-5">
<input
id="out_of_stock"
aria-describedby="comments-description"
name="out_of_stock"
type="checkbox"
defaultChecked={true}
className="focus:ring-blue-500 h-4 w-4 text-blue-600 border-gray-300 rounded"
/>
</div>
<div className="ml-3 text-sm">
<label htmlFor="out_of_stock" className="font-medium text-gray-700">
Include Out of Stock
</label>
<p className="mt-2 text-sm text-gray-500">
Include out of stock items from the list (they will show up as
having 100% profit margins and 1 bil gil profit).
</p>
</div>
</div>
</fieldset>
</div>
</div>
</div>
</div>
</div>
<div className="flex justify-end">
<button
type="submit"
onClick={onSubmit}
className={classNames(isRunning ? 'bg-gray-500' : 'bg-blue-600', "cursor-pointer ml-3 inline-flex justify-center py-2 px-4 border border-transparent shadow-sm text-sm font-medium rounded-md text-white hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500")}
>
{isRunning && <svg className="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor"
strokeWidth="4"></circle>
<path className="opacity-75" fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
</svg>}
Search
</button>
</div>
</div>
</Form>
</div>
</main>
}
export default FullScan;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment