Skip to content

Commit

Permalink
Merge pull request #64 from nushydude/63-store-recently-used-3-tradin…
Browse files Browse the repository at this point in the history
…g-pairs-on-the-single-token-page

add recently searched pairs to UI
  • Loading branch information
nushydude authored Apr 25, 2024
2 parents 0f13b92 + 9cb0484 commit 460da1f
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 9 deletions.
41 changes: 41 additions & 0 deletions src/pages/SingleTokenPage/HistoricalSelectedPairs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React from 'react';

type Props = {
selectedPairs: string[];
onPairClick: (pair: string) => void;
removePair: (pair: string) => void;
};

// Show a pill shaped button for each selected pair with a gap of 4px between each pair,
// and a close button to remove the pair.
const HistoricalSelectedPairs = ({
selectedPairs,
onPairClick,
removePair,
}: Props) => {
return (
<div className="grid grid-cols-3 md:grid-cols-2 gap-4">
{selectedPairs.map((pair) => (
<div
key={pair}
className="bg-gray-200 rounded-full px-4 py-1 text-xs cursor-pointer flex justify-between items-center"
onClick={() => onPairClick(pair)}
>
<span className="flex-grow text-left">{pair}</span>
<button
onClick={(event) => {
event.stopPropagation(); // Prevent onPairClick from being triggered
removePair(pair);
}}
className="text-gray-600 hover:text-red-500"
aria-label={`Remove ${pair}`}
>
</button>
</div>
))}
</div>
);
};

export default HistoricalSelectedPairs;
42 changes: 41 additions & 1 deletion src/pages/SingleTokenPage/SingleTokenPage.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import qs from 'query-string';
import { useLocation, useHistory } from 'react-router-dom';
import { Interval } from '../../types/interval';
Expand All @@ -9,6 +9,8 @@ import getDisplayData from './getDisplayData';
import { getDefaultTokenOptions } from './getDefaultTokenOptions';
import { TokenContent } from './TokenContent';

const LOCAL_STORAGE_KEY = 'SINGLE_TOKEN_PAGE-HISTORICAL_PAIRS';

type FieldValues = {
symbol: string;
interval: Interval;
Expand All @@ -25,6 +27,42 @@ const SingleTokenPage = () => {
getDefaultTokenOptions(qs.parse(location.search)),
]);

// Get the historicalPairs from LocalStorage or initialize it to an empty array.
const [historicalPairs, setHistoricalPairs] = useState<string[]>(() => {
const pairs = localStorage.getItem(LOCAL_STORAGE_KEY);
return pairs ? JSON.parse(pairs) : [];
});

// Remove the pair from the historicalPairs
const removePair = (pair: string) => {
setHistoricalPairs((prev) => prev.filter((item) => item !== pair));
};

// Save the historicalPairs to LocalStorage whenever it changes.
useEffect(() => {
localStorage.setItem(LOCAL_STORAGE_KEY, JSON.stringify(historicalPairs));
}, [historicalPairs]);

// If successful i.e. fetchStatus is FETCH_STATUS.success, add the symbol to the end of the historicalPairs
// and only store the last 4 pairs and then save it to LocalStorage.
useEffect(() => {
if (fetchStatus === FETCH_STATUS.success) {
setHistoricalPairs((prev) => {
const uniquePairs = [
...new Set(
[
data[0]?.symbol,
...prev.filter((item) => item !== data[0]?.symbol),
]
// store only the last 6 pairs
.slice(-6),
),
];
return uniquePairs;
});
}
}, [data, fetchStatus]);

const onSubmit = (fieldValues: FieldValues) => {
refetch([fieldValues]);
};
Expand All @@ -48,6 +86,8 @@ const SingleTokenPage = () => {
<div className="w-full mr-0 md:w-60 md:flex-shrink-0 md:mr-4">
<TokenOptionsForm
defaultValues={getDefaultTokenOptions(qs.parse(location.search))}
historicalPairs={historicalPairs}
removePair={removePair}
allowSubmission={fetchStatus !== FETCH_STATUS.fetching}
onSubmit={onSubmit}
onValueChange={onFormValueChange}
Expand Down
26 changes: 26 additions & 0 deletions src/pages/SingleTokenPage/TokenOptionsForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,51 @@ import { useEffect } from 'react';
import { useForm, Controller } from 'react-hook-form';
import { FieldValues } from './types';
import { TokensSelector } from '../../components/TokensSelector';
import HistoricalSelectedPairs from './HistoricalSelectedPairs';

type Props = {
defaultValues: Partial<FieldValues>;
allowSubmission: boolean;
onSubmit: (fieldValues: FieldValues) => void;
onValueChange: (value: Partial<FieldValues>) => void;
historicalPairs: string[];
removePair: (pair: string) => void;
};

export const TokenOptionsForm = ({
defaultValues,
allowSubmission = true,
onSubmit,
onValueChange,
historicalPairs,
removePair,
}: Props) => {
const {
register,
handleSubmit,
control,
watch,
formState: { errors },
setValue,
getValues,
} = useForm<FieldValues>({ defaultValues });

// Subscribe to form value changes
useEffect(() => {
const subscription = watch(onValueChange);
return () => subscription.unsubscribe();
}, [watch, onValueChange]);

const handleClickOnPair = (symbol: string) => {
const currentValues = getValues();
const nextValues = { ...currentValues, symbol };

// This should call onValueChange as well because of the above subscription.
setValue('symbol', symbol);

onSubmit(nextValues);
};

return (
<form
onSubmit={handleSubmit((data) => onSubmit(data))}
Expand All @@ -52,6 +70,14 @@ export const TokenOptionsForm = ({
)}
</div>

<div className="mb-2 md:mb-4">
<HistoricalSelectedPairs
selectedPairs={historicalPairs}
onPairClick={handleClickOnPair}
removePair={removePair}
/>
</div>

<div className="mb-2 md:mb-4">
<label htmlFor="interval" className="mb-1 block">
Interval
Expand Down
14 changes: 7 additions & 7 deletions src/pages/SingleTokenPage/getDefaultTokenOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { FieldValues } from './types';
const LOCAL_STORAGE_KEY = 'default_token_options';

export const getDefaultTokenOptions = (
parsed: qs.ParsedQuery<string>,
queryStringValues: qs.ParsedQuery<string>,
): FieldValues => {
// Load from local storage
let storedOptions: Partial<FieldValues> = {};
Expand All @@ -24,16 +24,16 @@ export const getDefaultTokenOptions = (
let limit = storedOptions.limit || 100;

// Prioritize query string over local storage and default values
if (typeof parsed.symbol === 'string') {
symbol = parsed.symbol;
if (typeof queryStringValues.symbol === 'string') {
symbol = queryStringValues.symbol;
}

if (typeof parsed.interval === 'string') {
interval = parsed.interval as Interval;
if (typeof queryStringValues.interval === 'string') {
interval = queryStringValues.interval as Interval;
}

if (typeof parsed.limit === 'string') {
limit = parseInt(parsed.limit);
if (typeof queryStringValues.limit === 'string') {
limit = parseInt(queryStringValues.limit);
}

// Save to local storage
Expand Down
4 changes: 4 additions & 0 deletions src/utils/nonNullable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// filter(Boolean) did not work with typescript, so had to use filter(nonNullable) to remove nulls from an array
export function nonNullable<T>(value: T): value is NonNullable<T> {
return value !== null && value !== undefined;
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"compilerOptions": {
"target": "es5",
"target": "es2015",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
Expand Down

0 comments on commit 460da1f

Please sign in to comment.