Skip to content

Commit

Permalink
Add market comparison settings
Browse files Browse the repository at this point in the history
  • Loading branch information
GODrums committed Feb 24, 2025
1 parent 75fec94 commit a6a068f
Show file tree
Hide file tree
Showing 5 changed files with 116 additions and 14 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "betterfloat",
"displayName": "BetterFloat",
"version": "3.1.1",
"version": "3.1.2",
"description": "Enhance your website experience on 9+ CS2 skin markets!",
"author": "Rums",
"license": "CC BY NC SA 4.0",
Expand Down
4 changes: 4 additions & 0 deletions src/lib/@typings/ExtensionTypes.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ export namespace Extension {
hash: string;
};

export interface APIMarketResponse {
[market: string]: Partial<MarketEntry>;
}

export interface AbstractPriceMapping {
[name: string]: any;
}
Expand Down
16 changes: 16 additions & 0 deletions src/lib/handlers/networkhandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,22 @@ import { cacheRealCurrencyRates } from './mappinghandler';
let isCurrencyFetched = false;
let isCurrencyFetchDone = false;

export async function fetchMarketComparisonData(buff_name: string, steamId?: string): Promise<Extension.APIMarketResponse> {
const response = await fetch(`${process.env.PLASMO_PUBLIC_BETTERFLOATAPI}/v1/price/${buff_name}`, {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'x-via': `BetterFloat/${chrome.runtime.getManifest().version}`,
'x-steamid': steamId || '',
},
});
const data = (await response.json()) as Extension.APIMarketResponse;
if (!response.ok) {
throw new Error(`Error fetching market comparison data: ${data.message || 'Unknown error'}`);
}
return data;
}

// fetches currency rates from freecurrencyapi through my api to avoid rate limits
export async function fetchCurrencyRates() {
if (isCurrencyFetched) {
Expand Down
106 changes: 94 additions & 12 deletions src/lib/inline/CSFMarketComparison.tsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,22 @@
import { useEffect, useState } from 'react';
import { useEffect, useRef, useState } from 'react';
import type { CSFloat } from '~lib/@typings/FloatTypes';
import { LoadingSpinner } from '~popup/components/LoadingSpinner';
import { ScrollArea } from '~popup/ui/scroll-area';

import betterfloatLogo from 'data-base64:/assets/icon.png';
import { useStorage } from '@plasmohq/storage/hook';
import Decimal from 'decimal.js';
import { AnimatePresence } from 'framer-motion';
import { getMarketID } from '~lib/handlers/mappinghandler';
import { fetchMarketComparisonData } from '~lib/handlers/networkhandler';
import { AvailableMarketSources, MarketSource } from '~lib/util/globals';
import { CurrencyFormatter, getMarketURL } from '~lib/util/helperfunctions';
import type { SettingsUser } from '~lib/util/storage';
import { cn } from '~lib/utils';
import { MaterialSymbolsCloseSmallOutlineRounded } from '~popup/components/Icons';
import { Badge } from '~popup/ui/badge';
import { Button } from '~popup/ui/button';
import { CSFCheckbox } from '~popup/ui/checkbox';

interface MarketEntry {
market: string;
Expand All @@ -22,10 +26,6 @@ interface MarketEntry {
updated: number;
}

interface APIMarketResponse {
[market: string]: Partial<MarketEntry>;
}

const CirclePlus: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15" fill="none" width="19" height="19" {...props}>
<circle cx="7" cy="7.5" r="7" fill="#FD484A" fillOpacity="0.25"></circle>
Expand All @@ -52,6 +52,18 @@ const ShieldCheck: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<path d="m9 12 2 2 4-4" />
</svg>
);
const Settings: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<path d="M12.22 2h-.44a2 2 0 0 0-2 2v.18a2 2 0 0 1-1 1.73l-.43.25a2 2 0 0 1-2 0l-.15-.08a2 2 0 0 0-2.73.73l-.22.38a2 2 0 0 0 .73 2.73l.15.1a2 2 0 0 1 1 1.72v.51a2 2 0 0 1-1 1.74l-.15.09a2 2 0 0 0-.73 2.73l.22.38a2 2 0 0 0 2.73.73l.15-.08a2 2 0 0 1 2 0l.43.25a2 2 0 0 1 1 1.73V20a2 2 0 0 0 2 2h.44a2 2 0 0 0 2-2v-.18a2 2 0 0 1 1-1.73l.43-.25a2 2 0 0 1 2 0l.15.08a2 2 0 0 0 2.73-.73l.22-.39a2 2 0 0 0-.73-2.73l-.15-.08a2 2 0 0 1-1-1.74v-.5a2 2 0 0 1 1-1.74l.15-.09a2 2 0 0 0 .73-2.73l-.22-.38a2 2 0 0 0-2.73-.73l-.15.08a2 2 0 0 1-2 0l-.43-.25a2 2 0 0 1-1-1.73V4a2 2 0 0 0-2-2z" />
<circle cx="12" cy="12" r="3" />
</svg>
);
const BanIcon: React.FC<React.SVGProps<SVGSVGElement>> = (props) => (
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" {...props}>
<circle cx="12" cy="12" r="10" />
<path d="m4.9 4.9 14.2 14.2" />
</svg>
);

const ActivityPing: React.FC<{ activity: number }> = ({ activity }) => {
const activityColor = activity < 10 ? 'bg-red-500' : activity < 20 ? 'bg-yellow-500' : 'bg-green-500';
Expand Down Expand Up @@ -158,15 +170,24 @@ const MarketCard: React.FC<{ listing: CSFloat.ListingData; entry: MarketEntry; c
);
};

const freeMarkets = [MarketSource.Buff, MarketSource.Steam];

const CSFMarketComparison: React.FC = () => {
const [isLoading, setIsLoading] = useState(true);
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
const [listing, setListing] = useState<CSFloat.ListingData | null>(null);
const [marketData, setMarketData] = useState<MarketEntry[]>([]);
const [liquidity, setLiquidity] = useState<number | null>(null);
const [currency, setCurrency] = useState('USD');

const [visibleMarkets, setVisibleMarkets] = useStorage<string[]>(
'csf-visible-markets',
AvailableMarketSources.map((m) => m.source)
);
const [user] = useStorage<SettingsUser>('user');

const ref = useRef(null);

const fetchMarketData = async () => {
const item = listing?.item;
if (!item) {
Expand All @@ -179,8 +200,7 @@ const CSFMarketComparison: React.FC = () => {
buff_name += ` - ${item.phase}`;
}
try {
const response = await fetch(`${process.env.PLASMO_PUBLIC_BETTERFLOATAPI}/v1/price/${buff_name}`);
const data = (await response.json()) as APIMarketResponse;
const data = await fetchMarketComparisonData(buff_name, user?.steam?.steamid);

let convertedData = Object.entries(data)
.map(([market, entry]) => ({
Expand All @@ -194,7 +214,7 @@ const CSFMarketComparison: React.FC = () => {

// Filter markets for free users
if (user?.plan.type !== 'pro') {
convertedData = convertedData.filter((entry) => entry.market === MarketSource.Buff || entry.market === MarketSource.Steam);
convertedData = convertedData.filter((entry) => freeMarkets.includes(entry.market as MarketSource));
}

if (isBannedOnBuff(listing?.item)) {
Expand Down Expand Up @@ -249,6 +269,19 @@ const CSFMarketComparison: React.FC = () => {
}
}, [listing]);

const toggleMarket = (market: string) => {
setVisibleMarkets((prev) => {
if (!prev) return [market];
if (prev.includes(market)) {
return prev.filter((m) => m !== market);
}
return [...prev, market];
});
};

// Filter market data based on visibility settings
const filteredMarketData = marketData.filter((entry) => visibleMarkets.includes(entry.market));

return (
<div className="dark w-[210px] max-h-[90vh]" style={{ fontFamily: 'Roboto, "Helvetica Neue", sans-serif' }}>
{isLoading ? (
Expand All @@ -257,10 +290,51 @@ const CSFMarketComparison: React.FC = () => {
</div>
) : (
<div className="flex flex-col gap-2">
<div className="w-full bg-[--highlight-background-minimal] flex justify-center items-center gap-2 rounded-md py-2">
<img src={betterfloatLogo} alt="BetterFloat" className="h-8 w-8" />
<span className="text-white font-bold">Market Comparison</span>
<div className="w-full bg-[--highlight-background-minimal] rounded-md py-2 flex flex-col items-center gap-1">
<div className="flex justify-center items-center gap-2">
<img src={betterfloatLogo} alt="BetterFloat" className="h-8 w-8" />
<span className="text-white font-bold">Market Comparison</span>
</div>
<div className="flex justify-center items-center gap-2">
<Button className="h-9 gap-2 bg-[--highlight-background-minimal] hover:bg-[--highlight-background-heavy] text-white" onClick={() => setIsSettingsOpen(!isSettingsOpen)}>
<Settings className="h-6 w-6" />
<span className="text-sm">Settings</span>
</Button>
</div>
</div>
<AnimatePresence>
{isSettingsOpen && (
<div ref={ref} className="w-full bg-[--highlight-background-minimal] rounded-md p-4 flex flex-col items-center gap-1">
<div className="w-full flex justify-between items-center gap-2 pb-2">
<div className="font-bold text-lg text-white">Settings</div>
<Button variant="ghost" size="icon" className="w-8 h-8 hover:bg-neutral-500/70" onClick={() => setIsSettingsOpen(false)}>
<MaterialSymbolsCloseSmallOutlineRounded className="size-6" />
</Button>
</div>
<div className="w-full space-y-3 text-[--subtext-color]">
{AvailableMarketSources.map((market) => (
<div key={market.source} className="flex justify-between items-center space-x-2">
<div className="flex items-center space-x-2">
<CSFCheckbox id={market.source} checked={visibleMarkets.includes(market.source)} onCheckedChange={() => toggleMarket(market.source)} />
<div className="flex items-center gap-2">
<img src={market.logo} className="h-6 w-6" style={convertStylesStringToObject(market.style)} />
<label htmlFor={market.source} className="text-sm font-medium leading-none cursor-pointer">
{market.text}
</label>
</div>
</div>
{!freeMarkets.includes(market.source) && (
<Badge variant="purple" className="text-white">
Pro
</Badge>
)}
</div>
))}
</div>
</div>
)}
</AnimatePresence>

<div className="flex flex-col justify-center gap-1 p-4 bg-[--highlight-background-minimal] text-[--subtext-color] text-sm rounded-md">
<div className="flex items-center justify-between">
<span>Total Listings:</span>
Expand All @@ -274,7 +348,15 @@ const CSFMarketComparison: React.FC = () => {
)}
</div>
<ScrollArea className="w-full h-[90vh] [--border:227_100%_88%_/_0.07]">
{listing && marketData.map((dataEntry) => <MarketCard key={dataEntry.market} listing={listing} entry={dataEntry} currency={currency} />)}
{listing && filteredMarketData.map((dataEntry) => <MarketCard key={dataEntry.market} listing={listing} entry={dataEntry} currency={currency} />)}
{filteredMarketData.length === 0 && (
<div className="text-[--subtext-color] mt-2 bg-[--highlight-background-minimal] rounded-md">
<div className="flex flex-col items-center justify-center gap-1 p-4">
<BanIcon className="size-8 text-white" />
<span className="text-base text-center text-white">No listings found</span>
</div>
</div>
)}
{user?.plan.type !== 'pro' && (
<div className="text-[--subtext-color] mt-2 bg-[--highlight-background-minimal] rounded-md">
<div className="flex flex-col items-center justify-center gap-1 p-4">
Expand Down
2 changes: 1 addition & 1 deletion src/popup/ui/checkbox.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const CSFCheckbox = React.forwardRef<React.ElementRef<typeof CheckboxPrimitive.R
<CheckboxPrimitive.Root
ref={ref}
className={cn(
'peer h-6 w-6 shrink-0 rounded-md border-2 border-[#ffffff8a] shadow focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-[#237bff] data-[state=checked]:text-white',
'peer h-6 w-6 shrink-0 rounded-md border-2 border-[#ffffff8a] shadow focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-[#237bff] data-[state=checked]:text-white',
className
)}
{...props}
Expand Down

0 comments on commit a6a068f

Please sign in to comment.