From 53c7ce959b5c72002c5444b7d6e5b458331a4275 Mon Sep 17 00:00:00 2001 From: Pinapelz Date: Tue, 27 Aug 2024 01:05:43 -0700 Subject: Initial commit --- src/components/FFXIVItemPrice.tsx | 119 ++++++++++++++++++++++++++++++++++ src/components/FFXIVWorldSelector.tsx | 61 +++++++++++++++++ src/env.d.ts | 1 + src/layouts/Layout.astro | 97 +++++++++++++++++++++++++++ src/pages/index.astro | 14 ++++ 5 files changed, 292 insertions(+) create mode 100644 src/components/FFXIVItemPrice.tsx create mode 100644 src/components/FFXIVWorldSelector.tsx create mode 100644 src/env.d.ts create mode 100644 src/layouts/Layout.astro create mode 100644 src/pages/index.astro (limited to 'src') 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 = ({ itemId = 5530, itemName="Coke", itemImageUrl="https://xivapi.com/i/021000/021462_hr1.png"}) => { + const [selectedWorld, setSelectedWorld] = useState(null); + const [dailySaleVelocity, setDailySaleVelocity] = useState(null); + const [averageSalePrice, setAverageSalePrice] = useState(null); + const [inputQuantity, setInputQuantity] = useState(0); + const [potentialGil, setPotentialGil] = useState(0); + const [dataSource, setDataSource] = useState('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) => { + 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 ( +
+ + + + + + + + + + + + + + + + + + + + +
Average Price/Item:{formatNumber(averageSalePrice)} gil
Daily Sale Velocity:{formatNumber(dailySaleVelocity)} items
Enter Quantity: + +
Potential Gil Earnings:{formatNumber(potentialGil)} gil
+ +
+ ); +}; + +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 = ({ message = "Select a World" }) => { + const [worlds, setWorlds] = useState([]); + const [selectedWorld, setSelectedWorld] = useState(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) => { + const selectedWorld = event.target.value; + setSelectedWorld(selectedWorld); + localStorage.setItem('selectedWorld', selectedWorld); // Save to localStorage + }; + + const handleApplyClick = () => { + window.location.reload(); // Refresh the page + }; + + return ( +
+ + + +
+ ); +}; + +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 @@ +/// \ 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; +--- + + + + + + + + + + + {title} + + + + + + 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'; +--- + + +

FFXIV-MDX-React-Components

+

FFXIV-MDX-React-Components is a collection of components for Final Fantasy XIV related data. It is built with MDX and React.

+ +
+ +
+ -- cgit v1.2.3