aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPinapelz <yukais@pinapelz.com>2024-08-27 01:05:43 -0700
committerPinapelz <yukais@pinapelz.com>2024-08-27 01:05:43 -0700
commit53c7ce959b5c72002c5444b7d6e5b458331a4275 (patch)
tree1226a3694ea2f840bb6c3353a4ee5282efd43bf0 /src
Initial commit
Diffstat (limited to 'src')
-rw-r--r--src/components/FFXIVItemPrice.tsx119
-rw-r--r--src/components/FFXIVWorldSelector.tsx61
-rw-r--r--src/env.d.ts1
-rw-r--r--src/layouts/Layout.astro97
-rw-r--r--src/pages/index.astro14
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>
+
send patches to the email below
yukais@pinapelz.com
include the subject [PATCH repo_name]
pinapelz.com
homepage