Skip to content

Commit fed61eb

Browse files
committed
add conversion pinning feature
1 parent 110f6ea commit fed61eb

19 files changed

+300
-75
lines changed

src/App.tsx

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,28 @@ import "./assets/fonts/fontin/Fontin-Regular.ttf";
55
import "./assets/fonts/fontin/Fontin-SmallCaps.ttf";
66

77
import { AuthGuard } from "./utils/AuthGuard";
8-
import { Header } from "./components/Header";
8+
import { CurrencyMapProvider } from "./utils/CurrencyMapProvider";
9+
import { StorageProvider } from "./utils/StorageProvider";
10+
11+
import { Header } from "./components/Header/Header";
912
import { CalculationView } from "./components/CalculationView";
1013
import { Footer } from "./components/Footer";
1114
import ErrorBoundary from "./components/ErrorBoundary";
1215

1316
function App() {
1417
return (
1518
<div className='flex flex-col gap-4 min-h-screen'>
16-
<Header />
17-
<div className='flex-1 overflow-x-hidden'>
18-
<ErrorBoundary>
19-
<AuthGuard>
20-
<CalculationView />
21-
</AuthGuard>
22-
</ErrorBoundary>
23-
</div>
24-
<Footer />
19+
<AuthGuard>
20+
<CurrencyMapProvider>
21+
<StorageProvider>
22+
<ErrorBoundary>
23+
<Header />
24+
<CalculationView />
25+
<Footer />
26+
</ErrorBoundary>
27+
</StorageProvider>
28+
</CurrencyMapProvider>
29+
</AuthGuard>
2530
</div>
2631
);
2732
}

src/components/CalculationView/CalculationResults.tsx

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,50 @@
1+
import { useContext } from "react";
2+
import { Pin, PinOff } from "lucide-react";
3+
14
import { AmountDisplay } from "../AmountDisplay";
5+
import { StorageContext } from "../../context/StorageContext";
6+
7+
function PinButton({ primary, secondary }: { primary: CurrencyKey; secondary: CurrencyKey }) {
8+
const {
9+
setPreferences,
10+
preferences: { pinned }
11+
} = useContext(StorageContext);
12+
13+
const isCurrentlyPinned = pinned?.primary === primary && pinned?.secondary === secondary;
14+
15+
return (
16+
<button
17+
title={isCurrentlyPinned ? "Unpin" : "Pin to top"}
18+
type='button'
19+
className='text-primary-dark'
20+
onClick={() => {
21+
setPreferences(
22+
isCurrentlyPinned
23+
? { pinned: null }
24+
: {
25+
pinned: {
26+
primary: primary,
27+
secondary: secondary
28+
}
29+
}
30+
);
31+
}}>
32+
{isCurrentlyPinned ? <PinOff className='w-4 h-4' /> : <Pin className='w-4 h-4' />}
33+
</button>
34+
);
35+
}
36+
37+
type Props = {
38+
primary: CurrencyKey;
39+
results: Array<{ currency: CurrencyKey; calculation: number }>;
40+
};
241

3-
export const CalculationResults = ({ results }: { results: Array<{ currency: CurrencyKey; calculation: number }> }) => (
42+
export const CalculationResults = ({ primary, results }: Props) => (
443
<div className='flex flex-col w-max self-start'>
544
{results.map((res) => (
6-
<div className='mt-[-4px]'>
7-
<AmountDisplay key={res.currency} rate={res.calculation} currencyName={res.currency} />
45+
<div key={res.currency} className='mt-[-4px] flex justify-between gap-2 w-full'>
46+
<AmountDisplay rate={res.calculation} currencyName={res.currency} />
47+
<PinButton primary={primary} secondary={res.currency} />
848
</div>
949
))}
1050
</div>

src/components/CalculationView/CalculationView.tsx

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import { useMemo, useState } from "react";
1+
import { useContext, useMemo, useState } from "react";
2+
import clsx from "clsx";
23

3-
import { useCurrencyMapData } from "../../hooks/useCurrencyMap";
44
import { convert } from "../../utils/convert";
5+
import { CurrencyMapContext } from "../../context/CurrencyMapContext";
56

67
import { Gears } from "../Gears";
78
import { CurrencySelection } from "./CurrencySelection";
@@ -14,7 +15,7 @@ import { currencies } from "../../constant";
1415
export function CalculationView() {
1516
const [selected, setSelected] = useState<CurrencyKey | "">("");
1617
const [value, setValue] = useState("");
17-
const currencyMap = useCurrencyMapData();
18+
const currencyMap = useContext(CurrencyMapContext);
1819

1920
const convertedResults = useMemo(() => {
2021
if (!selected || !currencyMap) {
@@ -39,24 +40,30 @@ export function CalculationView() {
3940
}, [selected, value, currencyMap]);
4041

4142
if (currencyMap == null) {
42-
return <Gears />;
43+
return (
44+
<div className='flex-1'>
45+
<Gears />
46+
</div>
47+
);
4348
}
4449

4550
return (
46-
<div className='w-full flex justify-center px-4'>
47-
<div className='grid gap-4 p-4 lg:grid-cols-2 lg:auto-rows-min lg:gap-6'>
51+
<div className='w-full flex flex-1 overflow-x-hidden justify-center px-4'>
52+
<div className={clsx("flex flex-col gap-4", "lg:gap-6 lg:flex-row")}>
4853
<CurrencySelection selected={selected} setSelected={setSelected} />
4954

50-
{selected ? (
51-
<CurrencyInput value={value} setValue={setValue} selected={selected} />
52-
) : (
53-
<p>Please select a currency</p>
54-
)}
55+
<div className='flex flex-col gap-4 min-w-[340px]'>
56+
{selected ? (
57+
<CurrencyInput value={value} setValue={setValue} selected={selected} />
58+
) : (
59+
<p>Please select a currency</p>
60+
)}
5561

56-
<div className='min-h-[202px] flex flex-col justify-between gap-2'>
57-
{selected && <CalculationResults results={convertedResults} />}
62+
<div className='min-h-[230px] flex flex-col justify-between gap-2'>
63+
{selected && <CalculationResults primary={selected} results={convertedResults} />}
5864

59-
{selected && convertedResults.length < currencies.length - 2 && <HiddenDataInfo />}
65+
{selected && convertedResults.length < currencies.length - 2 && <HiddenDataInfo />}
66+
</div>
6067
</div>
6168
</div>
6269
</div>

src/components/CalculationView/CurrencySelection.tsx

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { memo } from "react";
12
import clsx from "clsx";
23

34
import { CurrencyIcon } from "../CurrencyIcon";
@@ -8,8 +9,14 @@ type Props = {
89
setSelected: SetStateFn<Props["selected"]>;
910
};
1011

11-
export const CurrencySelection = ({ selected, setSelected }: Props) => (
12-
<section className='lg:row-span-2 grid grid-cols-10 md:grid-cols-4 lg:grid-cols-2 xl:grid-cols-3 w-max gap-1 h-max border-primary-darker border-b pb-2 lg:border-b-0 lg:pb-0 lg:border-r lg:border-1 lg:pr-2'>
12+
export const Component = ({ selected, setSelected }: Props) => (
13+
<section
14+
className={clsx(
15+
"w-max grid grid-cols-10 gap-y-1 gap-x-0 h-max pb-4 border-primary-darker border-b",
16+
"md:grid-cols-4 md:gap-2",
17+
"lg:grid-cols-2 lg:row-span-2 lg:border-b-0 lg:pb-0 lg:pr-4 lg:border-r lg:border-1 ",
18+
"xl:grid-cols-3"
19+
)}>
1320
{currencies.map((c, index) => (
1421
<button
1522
key={c}
@@ -20,8 +27,10 @@ export const CurrencySelection = ({ selected, setSelected }: Props) => (
2027
)}
2128
onClick={() => setSelected(c)}>
2229
<CurrencyIcon index={index} />
23-
<span className='hidden md:block'>{c}</span>
30+
<span className='hidden text-sm md:block lg:text-md xl:text-lg'>{c}</span>
2431
</button>
2532
))}
2633
</section>
2734
);
35+
36+
export const CurrencySelection = memo(Component);

src/components/CalculationView/HiddenDataInfo.tsx

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import { memo } from "react";
12
import { InfoIcon } from "lucide-react";
3+
24
import {
35
Dialog,
46
DialogClose,
@@ -9,11 +11,10 @@ import {
911
DialogTitle,
1012
DialogTrigger
1113
} from "../shadcn/Dialog";
12-
1314
import { AmountDisplay } from "../AmountDisplay";
1415
import { Button } from "../shadcn/Button";
1516

16-
export const HiddenDataInfo = () => (
17+
const Component = () => (
1718
<Dialog>
1819
<DialogTrigger
1920
className='flex gap-1 items-center text-primary-dark opacity-50 text-sm italic select-none cursor-pointer hover:opacity-75 hover:underline'
@@ -45,3 +46,5 @@ export const HiddenDataInfo = () => (
4546
</DialogContent>
4647
</Dialog>
4748
);
49+
50+
export const HiddenDataInfo = memo(Component);

src/components/CurrencyIcon.tsx

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
1-
const iconSize = 34;
1+
import { memo } from "react";
2+
23
const defaultFileSize = 108;
34
const sheetWidth = 2160;
4-
const relativeSize = (iconSize / defaultFileSize) * sheetWidth;
55

6-
export const CurrencyIcon = ({ index }: { index: number }) => (
6+
function getRelativeSize(size: number) {
7+
return (size / defaultFileSize) * sheetWidth;
8+
}
9+
10+
const Component = ({ index, size = 34 }: { index: number; size?: number }) => (
711
<div
812
style={{
9-
width: iconSize,
10-
height: iconSize,
13+
width: size,
14+
height: size,
1115
backgroundImage: "url(currency/sheet.png)",
12-
backgroundSize: relativeSize,
13-
backgroundPosition: index * iconSize * -1
16+
backgroundSize: getRelativeSize(size),
17+
backgroundPosition: index * size * -1
1418
}}
1519
/>
1620
);
21+
export const CurrencyIcon = memo(Component);

src/components/Footer.tsx

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
export function Footer() {
2-
return (
3-
<footer className='p-2 text-center text-sm text-primary-dark'>
4-
This website is not affiliated with, maintained, endorsed or sponsored by Grinding Gear Games or any of its
5-
affiliates.
6-
</footer>
7-
);
8-
}
1+
import { memo } from "react";
2+
3+
export const Component = () => (
4+
<footer className='p-2 text-center text-sm text-primary-dark'>
5+
This website is not affiliated with, maintained, endorsed or sponsored by Grinding Gear Games or any of its
6+
affiliates.
7+
</footer>
8+
);
9+
10+
export const Footer = memo(Component);

src/components/Header.tsx

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/components/Header/Header.tsx

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { Github } from "lucide-react";
2+
import { PinnedConversion } from "./PinnedConversion";
3+
import { memo } from "react";
4+
5+
export const Component = () => (
6+
<div className='w-screen flex justify-between items-center p-2 border-b border-primary-dark'>
7+
<span className='text-xl font-bold select-none font-[FontinSmallCaps]'>Exalt Rates</span>
8+
<PinnedConversion />
9+
<a href='https://github.com/yethranayeh/ExaltRates' target='_blank' title='Exalt Rates GitHub repository'>
10+
<Github className='h-8 w-8 p-1 rounded-md border border-primary-main' />
11+
</a>
12+
</div>
13+
);
14+
15+
export const Header = memo(Component);
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import { useContext } from "react";
2+
import { PinOff } from "lucide-react";
3+
import clsx from "clsx";
4+
5+
import { getCache } from "../../utils/storage";
6+
import { convert } from "../../utils/convert";
7+
import { StorageContext } from "../../context/StorageContext";
8+
import { CurrencyIcon } from "../CurrencyIcon";
9+
10+
import { currencies } from "../../constant";
11+
12+
export function PinnedConversion() {
13+
const {
14+
setPreferences,
15+
preferences: { pinned }
16+
} = useContext(StorageContext);
17+
const currencyMap = getCache();
18+
19+
if (!pinned || !currencyMap) {
20+
return null;
21+
}
22+
23+
const conversionRate = convert(pinned.primary, pinned.secondary, currencyMap);
24+
25+
return (
26+
<div className='flex select-none items-center text-primary-dark text-sm'>
27+
<div className='flex flex-row items-center'>
28+
<span></span>
29+
<CurrencyIcon size={30} index={currencies.indexOf(pinned.primary)} />
30+
</div>
31+
<span>equals</span>
32+
<div className={clsx("flex flex-row items-center ml-1", conversionRate === null ? "text-red-900" : undefined)}>
33+
<span>{conversionRate ? conversionRate?.toFixed(2) : 0}×</span>
34+
<CurrencyIcon size={30} index={currencies.indexOf(pinned.secondary)} />
35+
</div>
36+
<button
37+
title='Unpin conversion'
38+
type='button'
39+
className='text-primary-dark ml-2'
40+
onClick={() => {
41+
setPreferences({ pinned: null });
42+
}}>
43+
<PinOff className='w-4 h-4' />
44+
</button>
45+
</div>
46+
);
47+
}

0 commit comments

Comments
 (0)