diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/components/FFXIVItemPrice.tsx | 119 | ||||
| -rw-r--r-- | src/components/FFXIVWorldSelector.tsx | 61 | ||||
| -rw-r--r-- | src/env.d.ts | 1 | ||||
| -rw-r--r-- | src/layouts/Layout.astro | 97 | ||||
| -rw-r--r-- | src/pages/index.astro | 14 |
5 files changed, 292 insertions, 0 deletions
diff --git a/src/components/FFXIVItemPrice.tsx b/src/components/FFXIVItemPrice.tsx new file mode 100644 index 0000000..d65b3ca --- /dev/null +++ b/src/components/FFXIVItemPrice.tsx @@ -0,0 +1,119 @@ +import React, { useState, useEffect } from 'react'; + +interface FFXIVItemPriceProps { + itemId: number; + itemName: string; + itemImageUrl: string; +} + +const FFXIVItemPrice: React.FC<FFXIVItemPriceProps> = ({ itemId = 5530, itemName="Coke", itemImageUrl="https://xivapi.com/i/021000/021462_hr1.png"}) => { + const [selectedWorld, setSelectedWorld] = useState<string | null>(null); + const [dailySaleVelocity, setDailySaleVelocity] = useState<number | null>(null); + const [averageSalePrice, setAverageSalePrice] = useState<number | null>(null); + const [inputQuantity, setInputQuantity] = useState<number>(0); + const [potentialGil, setPotentialGil] = useState<number>(0); + const [dataSource, setDataSource] = useState<string>('world'); + + const fetchData = (attempt: number = 1) => { + fetch(`https://universalis.app/api/v2/aggregated/${selectedWorld}/${itemId}`) + .then(response => response.json()) + .then(data => { + let result = data.results[0]; + try { + setDailySaleVelocity(Math.round(result.nq.dailySaleVelocity.world.quantity)); + setAverageSalePrice(Math.round(result.nq.averageSalePrice.world.price)); + setDataSource('world'); + } catch (error) { + try { + setDailySaleVelocity(Math.round(result.nq.dailySaleVelocity.dc.quantity)); + setAverageSalePrice(Math.round(result.nq.averageSalePrice.dc.price)); + setDataSource('dc'); + } catch (error) { + try { + setDailySaleVelocity(Math.round(result.nq.dailySaleVelocity.region.quantity)); + setAverageSalePrice(Math.round(result.nq.averageSalePrice.region.price)); + setDataSource('region'); + } catch (error) { + console.error('Error fetching data:', error); + if (attempt < 3) { + setTimeout(() => fetchData(attempt + 1), 5000); + } + } + } + } + }) + .catch(error => { + console.error(`Error fetching data (attempt ${attempt}):`, error); + if (attempt < 3) { + setTimeout(() => fetchData(attempt + 1), 5000); + } + }); + }; + + useEffect(() => { + const savedWorld = localStorage.getItem('selectedWorld'); + if (savedWorld) { + setSelectedWorld(savedWorld); + } else { + setSelectedWorld('Midgardsormr'); + } + + fetchData(); + }, [itemId, selectedWorld]); + + const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const quantity = parseInt(e.target.value, 10); + setInputQuantity(quantity); + setPotentialGil(quantity * (averageSalePrice || 0)); + }; + + const formatNumber = (number: number | null) => { + return number !== null ? new Intl.NumberFormat().format(number) : 'N/A'; + }; + + return ( + <div className="ffxiv-container"> + <div className="ffxiv-header"> + <a href={"https://universalis.app/market/" + itemId} className="no-underline"> + <h1 className="ffxiv-h1">{itemName}</h1> + </a> + <img src={itemImageUrl} alt={itemName} className="ffxiv-item-icon" /> + </div> + <table className="ffxiv-table"> + <tbody> + <tr> + <td className="ffxiv-label">Average Price/Item:</td> + <td className="ffxiv-value">{formatNumber(averageSalePrice)} gil</td> + </tr> + <tr> + <td className="ffxiv-label">Daily Sale Velocity:</td> + <td className="ffxiv-value">{formatNumber(dailySaleVelocity)} items</td> + </tr> + <tr> + <td className="ffxiv-label">Enter Quantity:</td> + <td> + <input + type="number" + value={inputQuantity} + onChange={handleInputChange} + placeholder="Enter quantity" + className="ffxiv-input" + /> + </td> + </tr> + <tr> + <td className="ffxiv-label">Potential Gil Earnings:</td> + <td className="ffxiv-value">{formatNumber(potentialGil)} gil</td> + </tr> + </tbody> + </table> + <footer> + {dataSource === 'dc' ? 'Datacenter Fallback ' : dataSource === 'region' ? 'Regional Fallback ' : ''} + {selectedWorld} Marketboard Data provided by Universalis API. <a className="eorzeadb_link" href="#" onClick={(e) => { e.preventDefault(); fetchData(); }}>Click here to reload data</a> + <br /> + </footer> + </div> + ); +}; + +export default FFXIVItemPrice;
\ No newline at end of file diff --git a/src/components/FFXIVWorldSelector.tsx b/src/components/FFXIVWorldSelector.tsx new file mode 100644 index 0000000..d078640 --- /dev/null +++ b/src/components/FFXIVWorldSelector.tsx @@ -0,0 +1,61 @@ +import React, { useState, useEffect } from 'react'; +interface World { + id: number; + name: string; +} + +interface FFXIVWorldSelectorProps { + message: string; +} + +const FFXIVWorldSelector: React.FC<FFXIVWorldSelectorProps> = ({ message = "Select a World" }) => { + const [worlds, setWorlds] = useState<World[]>([]); + const [selectedWorld, setSelectedWorld] = useState<string | null>(null); + + useEffect(() => { + const fetchWorlds = async () => { + try { + const response = await fetch('https://universalis.app/api/v2/worlds'); + const data = await response.json(); + setWorlds(data); + } catch (error) { + console.error('Error fetching worlds:', error); + } + }; + + fetchWorlds(); + + // Load selected world from localStorage + const savedWorld = localStorage.getItem('selectedWorld'); + if (savedWorld) { + setSelectedWorld(savedWorld); + } + }, []); + + const handleWorldChange = (event: React.ChangeEvent<HTMLSelectElement>) => { + const selectedWorld = event.target.value; + setSelectedWorld(selectedWorld); + localStorage.setItem('selectedWorld', selectedWorld); // Save to localStorage + }; + + const handleApplyClick = () => { + window.location.reload(); // Refresh the page + }; + + return ( + <div className="ffxiv-container"> + <label htmlFor="world-select" className="ffxiv-label">{message}</label> + <select id="world-select" onChange={handleWorldChange} value={selectedWorld || ''} className="ffxiv-select"> + <option value="">--Please choose an option--</option> + {worlds.map((world) => ( + <option key={world.id} value={world.name}> + {world.name} + </option> + ))} + </select> + <button onClick={handleApplyClick} className="ffxiv-button">Apply</button> + </div> + ); +}; + +export default FFXIVWorldSelector;
\ No newline at end of file diff --git a/src/env.d.ts b/src/env.d.ts new file mode 100644 index 0000000..9bc5cb4 --- /dev/null +++ b/src/env.d.ts @@ -0,0 +1 @@ +/// <reference path="../.astro/types.d.ts" />
\ No newline at end of file diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro new file mode 100644 index 0000000..827dc43 --- /dev/null +++ b/src/layouts/Layout.astro @@ -0,0 +1,97 @@ +--- +interface Props { + title: string; +} + +const { title } = Astro.props; +--- + +<!doctype html> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta name="description" content="Astro description" /> + <meta name="viewport" content="width=device-width" /> + <link rel="icon" type="image/svg+xml" href="/favicon.svg" /> + <link rel="stylesheet" href="style.css" /> + <meta name="generator" content={Astro.generator} /> + <title>{title}</title> + </head> + <body> + <slot /> + </body> +</html> +<style is:global> +/* + The CSS in this style tag is based off of Bear Blog's default CSS. + https://github.com/HermanMartinus/bearblog/blob/297026a877bc2ab2b3bdfbd6b9f7961c350917dd/templates/styles/blog/default.css + License MIT: https://github.com/HermanMartinus/bearblog/blob/master/LICENSE.md + */ + body { + font-family: Verdana, sans-serif; + margin: auto; + padding: 20px; + max-width: 85ch; + text-align: left; + background-color: #1A1A1A; + word-wrap: break-word; + overflow-wrap: break-word; + line-height: 1.65; + color: #f0f0f0; +} +h1, +h2, +h3, +h4, +h5, +h6, +strong, +b { + color: #ffffff; +} +a { + color: #4ca6ff; +} +nav a { + margin-right: 10px; +} +textarea { + width: 100%; + font-size: 16px; +} +input { + font-size: 16px; +} +content { + line-height: 1.6; +} +table { + width: 100%; +} +img { + max-width: 100%; + height: auto; +} +code { + padding: 2px 5px; + background-color: #646464; +} +td,th { + border: 1px solid white; + padding: 8px; + } +pre { + padding: 1rem; +} +pre > code { + all: unset; +} +blockquote { + border: 1px solid #666; + color: #ccc; + padding: 10px 20px; + margin: 0; + font-style: italic; + } + +</style> diff --git a/src/pages/index.astro b/src/pages/index.astro new file mode 100644 index 0000000..a8653b3 --- /dev/null +++ b/src/pages/index.astro @@ -0,0 +1,14 @@ +--- +import Layout from '../layouts/Layout.astro'; +import FFXIVItemPrice from '../components/FFXIVItemPrice'; +import FFXIVWorldSelector from '../components/FFXIVWorldSelector'; +--- + +<Layout title="FFXIV-MDX-React-Components"> + <h1>FFXIV-MDX-React-Components</h1> + <p>FFXIV-MDX-React-Components is a collection of components for Final Fantasy XIV related data. It is built with MDX and React.</p> + <FFXIVWorldSelector client:load message="Set your home world to get crowdsourced marketboard information!" /> + <br/> + <FFXIVItemPrice client:load itemId={10393} itemName="Thavnairian Bustier" itemImageUrl="https://www.garlandtools.org/files/icons/item/42441.png" /> +</Layout> + |
