diff --git a/package.json b/package.json index 95f6dc66..d9a9f8fe 100644 --- a/package.json +++ b/package.json @@ -48,6 +48,7 @@ "fs-extra": "^10.0.0", "klaw": "^3.0.0", "lottie-react": "^2.4.0", + "papaparse": "^5.4.1", "sirv-cli": "^1.0.14", "vite": "^3.0.5" }, @@ -92,6 +93,7 @@ "@types/lodash.defaultsdeep": "^4.6.6", "@types/lodash.isequal": "^4.5.5", "@types/node": "^15.14.9", + "@types/papaparse": "^5.3.15", "@types/preact-i18n": "^2.3.0", "@types/prismjs": "^1.26.0", "@types/react-beautiful-dnd": "^13", diff --git a/src/pages/home/Home.tsx b/src/pages/home/Home.tsx index 179e4dcd..2efab42a 100644 --- a/src/pages/home/Home.tsx +++ b/src/pages/home/Home.tsx @@ -5,6 +5,8 @@ import { Lock, } from "@styled-icons/boxicons-solid"; import { observer } from "mobx-react-lite"; +import Papa from "papaparse"; +import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import styled from "styled-components/macro"; @@ -39,240 +41,116 @@ const DisabledButtonWrapper = styled.div` pointer-events: none; `; -export default observer(() => { +interface Server { + id: string; + name: string; + description: string; + inviteCode: string; + disabled: boolean; +} + +interface CachedData { + timestamp: number; + data: Server[]; +} + +const CACHE_KEY = "server_list_cache"; +const CACHE_DURATION = 15 * 60 * 1000; // 15 minutes in milliseconds + +// Safe localStorage wrapper +const safeStorage = { + getItem: (key: string): string | null => { + try { + return localStorage.getItem(key); + } catch (e) { + console.warn("Failed to read from localStorage:", e); + return null; + } + }, + setItem: (key: string, value: string): void => { + try { + localStorage.setItem(key, value); + } catch (e) { + console.warn("Failed to write to localStorage:", e); + } + }, +}; + +const Home: React.FC = () => { const client = useClient(); + const [servers, setServers] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); - const servers = [ - { - id: "01J544PT4T3WQBVBSDK3TBFZW7", - name: "PepChat Official", - description: - "Get your questions answered and stay up-to-date with the state of the project.", - inviteCode: "pepchatdiscover", - disabled: false, - }, - { - id: "01J5ZQMJSQ5AFZJJ3S204JK5Q4", - name: "Elite Group Buy (EGB)", - description: "Group buy peptides, amino blends & more.", - inviteCode: "elitegroupbuydiscover", - disabled: false, - }, - { - id: "01J545CBXQRWZZAASZQ6THKE96", - name: "Qingdao Sigma Chemical (QSC)", - description: - "China wholesale bioactive compounds. (International, US, EU, Canada and Australia domestic)", - inviteCode: "qscdiscover", - disabled: false, - }, - { - id: "01J63A8HQ8S10MM4B3K85VMYBW", - name: "Wonderland", - description: "Peptide life social group.", - inviteCode: "wonderlanddiscover", - disabled: false, - }, - { - id: "01J5VPXSS0EK69QD69RX6SKZHW", - name: "Kimmes Korner", - description: "Peptide group buys.", - inviteCode: "kimmeskornerdiscover", - disabled: false, - }, - { - id: "01J5Z5QBQWREPZZPMVKJNCBDP2", - name: "Joyous", - description: "Peptide group buys.", - inviteCode: "joyousdiscover", - disabled: false, - }, - { - id: "01J6FNC5667A6RWV1SK4FMP19S", - name: "Rabbit Hole Research", - description: - "A peptide research collective focused on community, education, and facilitating group buys.", - inviteCode: "rabbitholediscover", - disabled: false, - }, - { - id: "01J6DDFWNT3SFKVQHK8J29RPXE", - name: "Johnny 5", - description: - "Amazing community of helpful people. Focus on weight loss group buys.", - inviteCode: "johnny5discover", - disabled: false, - }, - { - id: "01J64CC6710N7CCWBBT625VXQ3", - name: "The Raven Nest", - description: - "Group buys, protocols, social, and all things peptides.", - inviteCode: "ravennestdiscover", - disabled: false, - }, - { - id: "01J72VR94J6722AHF1MD33DB4F", - name: "New Beginnings Research", - description: - "Peptide community focused on education, research, and organized group buys.", - inviteCode: "newbeginningsdiscover", - disabled: false, - }, - { - id: "01J6ZRS52BA42BJFVT0M4WY0Q6", - name: "Deb's PepTalk", - description: "Peptide GB's, education & ramblings.", - inviteCode: "debspeptalkdiscover", - disabled: false, - }, - { - id: "01J7E2NW9WXSHWJR7B75CDB2AC", - name: "AOB", - description: "Handmade organic beauty products", - inviteCode: "aobdiscover", - disabled: false, - }, - { - id: "01J6DHAK4RH0H6QK35CZ4G3ZSW", - name: "Cousin Eddie's Corner", - description: "Peptides with a dose of humour!", - inviteCode: "cousineddiescornerdiscover", - disabled: false, - }, - { - id: "01J6RS5RR3YKPMW09M7D71BTD2", - name: "HYB", - description: "China wholesale direct.", - inviteCode: "hybdiscover", - disabled: false, - }, - { - id: "01J740MT75NC05F6VB9EJ4Y115", - name: "Royal Peptides", - description: - "USA domestic wholesale vendor with 3rd party tested kits.", - inviteCode: "royalpeptidesdiscover", - disabled: false, - }, - { - id: "01J78Z1C1XW209S5YSQZMPS0E4", - name: "The Pep Planner", - description: - "Planner to keep track of daily pins, peptide information, orders & more.", - inviteCode: "thepepplannerdiscover", - disabled: false, - }, - { - id: "01J74BC8PFE9XBDX05J3Y9R9PV", - name: "Monkey Peps", - description: - "A Peptide Community for support, sourcing and group testing.", - inviteCode: "monkeypepsdiscover", - disabled: false, - }, - { - id: "01J7EGW77XE2GSJGPR87MQXZW4", - name: "SRY-LAB", - description: - "Peptide factory in China. Wholesale, retail and customization.", - inviteCode: "srylabdiscover", - disabled: false, - }, - { - id: "01J7NZR6KTG9BTRMNPCQQJ1VES", - name: "Shanghai Nexa Pharma", - description: - "Ship from domestic USA 3-5 business days. Custom batch manufacture MOQ 300 vials.", - inviteCode: "snpdiscover", - disabled: false, - }, - { - id: "01J72F71TZWQFEBNSSFBMSDZK1", - name: "Angel Shanghai Chem (ASC)", - description: "Manufacturer of Peptides", - inviteCode: "ascdiscover", - disabled: false, - }, - { - id: "01J72C64KX97MP5K6ABDRP62P4", - name: "The Hood", - description: - "Welcome to the neighbour-hood. A magical place full of potions and peps.", - inviteCode: "thehooddiscover", - disabled: false, - }, - { - id: "01J7RF37VXVMTS55K1C18PQ2HY", - name: "Peppy Princess", - description: "Beauty and skin-care experts.", - inviteCode: "peppyprincessdiscover", - disabled: false, - }, - { - id: "01J71Z3FVMJVCVCD8X4WGVR1SF", - name: "JoLynn's World", - description: "Pep talk and group buys.", - inviteCode: "jolynnsworlddiscover", - disabled: false, - }, - { - id: "01J84NMVTR2NQVHV9FQ1VR6YBN", - name: "Henan Tirzepa Peptides", - description: - "Factory direct wholesale of peptide products, door-to-door delivery.", - inviteCode: "henantirzepadiscover", - disabled: false, - }, - { - id: "01J8CQBJRR8EYVQFM7ARD1P11P", - name: "Peptopia", - description: - "Discussions about safe use, sourcing, testing, & more. GBs for Tirz, Reta, Sema, Cagri & more.", - inviteCode: "peptopiadiscover", - disabled: false, - }, - { - id: "01J8GZYC66E5T7PZNYVHD4DC6V", - name: "Nantong Guangyuan Chemical (GYC)", - description: - "High quality peptides with 99% purity from manufacturers.", - inviteCode: "gycdiscover", - disabled: false, - }, - { - id: "01J9QDPBRHTCBV4DJ15G28393H", - name: "Uther Pharmaceutical Peptide", - description: "Chemistry changes the world.", - inviteCode: "uppdiscover", - disabled: false, - }, - { - id: "01J9R4AP31FG4VX4FTZTSMWHFF", - name: "Tianjin Cangtu", - description: - "Direct factory supply with 99%+ purity and safe delivery.", - inviteCode: "tianjincangtudiscover", - disabled: false, - }, - { - id: "01JAJBYY4N7ZATDG446M4XGTMA", - name: "Shanghai Sigma Audley", - description: - "China peptides, steroid tablets, oil, and APIs supplier.", - inviteCode: "shanghaisigmaaudleydiscover", - disabled: false, - }, - { - id: "01J5TQYA639STTEX7SH5KXC96M", - name: "Joe Lu's Hideout", - description: "Peptide group buys.", - inviteCode: "placeholder", - disabled: true, - }, - ]; + const fetchAndCacheData = async () => { + try { + const csvUrl = + "https://docs.google.com/spreadsheets/d/e/2PACX-1vRY41D-NgTE6bC3kTN3dRpisI-DoeHG8Eg7n31xb1CdydWjOLaphqYckkTiaG9oIQSWP92h3NE-7cpF/pub?gid=0&single=true&output=csv"; - const renderServerButton = (server) => { + Papa.parse(csvUrl, { + download: true, + header: true, + dynamicTyping: true, + complete: (result) => { + if (result.errors.length > 0) { + console.error("CSV parsing errors:", result.errors); + setError("Error parsing server data"); + setLoading(false); + return; + } + + const cacheData: CachedData = { + timestamp: Date.now(), + data: result.data, + }; + + safeStorage.setItem(CACHE_KEY, JSON.stringify(cacheData)); + setServers(result.data); + setLoading(false); + }, + error: (err) => { + console.error("Error fetching CSV:", err); + setError( + "Failed to load server data. Please try again later.", + ); + setLoading(false); + }, + }); + } catch (err) { + console.error("Unexpected error:", err); + setError("An unexpected error occurred. Please try again later."); + setLoading(false); + } + }; + + useEffect(() => { + const getCachedOrFetchData = async () => { + try { + const cachedData = safeStorage.getItem(CACHE_KEY); + + if (cachedData) { + const parsed: CachedData = JSON.parse(cachedData); + const isExpired = + Date.now() - parsed.timestamp > CACHE_DURATION; + + if (!isExpired && Array.isArray(parsed.data)) { + setServers(parsed.data); + setLoading(false); + return; + } + } + } catch (err) { + console.warn("Error reading cache:", err); + // Continue to fetch fresh data if cache read fails + } + + await fetchAndCacheData(); + }; + + getCachedOrFetchData(); + }, []); + + const renderServerButton = (server: Server) => { const isServerJoined = client.servers.get(server.id); const linkTo = isServerJoined ? `/server/${server.id}` @@ -298,7 +176,9 @@ export default observer(() => { if (server.disabled) { return ( - {buttonContent} + + {buttonContent} + ); } else { return ( @@ -309,6 +189,14 @@ export default observer(() => { } }; + if (loading) { + return
Loading...
; + } + + if (error) { + return
{error}
; + } + return (
@@ -323,7 +211,9 @@ export default observer(() => {
- {" "} + ); -}); +}; + +export default observer(Home); diff --git a/yarn.lock b/yarn.lock index 185600d8..1e454485 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2731,6 +2731,15 @@ __metadata: languageName: node linkType: hard +"@types/papaparse@npm:^5.3.15": + version: 5.3.15 + resolution: "@types/papaparse@npm:5.3.15" + dependencies: + "@types/node": "*" + checksum: 265cc2fd7e36514568398491a9b1d47ad4408f3946fd19f69202ef7af313c8d094ef1562172a36bec6e34ae8e9daf80fc1c5a36327858c236c50c6ebaac3e221 + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.0 resolution: "@types/parse-json@npm:4.0.0" @@ -3724,6 +3733,7 @@ __metadata: "@types/lodash.defaultsdeep": ^4.6.6 "@types/lodash.isequal": ^4.5.5 "@types/node": ^15.14.9 + "@types/papaparse": ^5.3.15 "@types/preact-i18n": ^2.3.0 "@types/prismjs": ^1.26.0 "@types/react-beautiful-dnd": ^13 @@ -3759,6 +3769,7 @@ __metadata: mediasoup-client: "npm:@insertish/mediasoup-client@3.6.36-esnext" mobx: ^6.6.0 mobx-react-lite: 3.4.0 + papaparse: ^5.4.1 preact: ^10.5.14 preact-context-menu: 0.4.1 preact-i18n: ^2.4.0-preactx @@ -7324,6 +7335,13 @@ __metadata: languageName: node linkType: hard +"papaparse@npm:^5.4.1": + version: 5.4.1 + resolution: "papaparse@npm:5.4.1" + checksum: fc9e52f7158dca3517c229e3309065b1ab5da6c7194572fba4f31ff138bc43e3c91182cc40365cc828f97fe10d0aca416068fd731661058bea0f69ddb84a411a + languageName: node + linkType: hard + "parent-module@npm:^1.0.0": version: 1.0.1 resolution: "parent-module@npm:1.0.1"