Skip to content
Snippets Groups Projects
Commit fc6445a6 authored by Aggarwal Himanshu's avatar Aggarwal Himanshu
Browse files

Merge branch 'filters_logic' into 'develop'

Add support for filtering cryptocurrencies using the filter components

See merge request !9
parents fced7c26 56e07a9a
No related branches found
No related tags found
2 merge requests!17Merge develop into main,!9Add support for filtering cryptocurrencies using the filter components
Showing
with 242 additions and 62 deletions
......@@ -13,11 +13,12 @@ import classNames from "classnames";
* </Button>
* )
*/
function Button({ children, className, onClick }) {
function Button({ children, className, onClick, type }) {
return (
<button
className={classNames("py-2 px-5 rounded-xl", className)}
onClick={onClick}
type={type ? type : "button"}
>
{children}
</button>
......
......@@ -5,19 +5,19 @@ import Button from "../button/Button";
import { RadioInputForm } from "../radio/RadioForm";
import { CSSTransition } from "react-transition-group";
export function FilterDropdown({ filterOptions, setOpen, addFilter }) {
export function FilterDropdown({
filterOptions,
setOpen,
addFilter,
dropdownRef,
}) {
const [selectedFilter, setSelectedFilter] = useState(null);
const [radioValue, setRadioValue] = useState("is");
const [inputValue, setInputValue] = useState("");
useEffect(() => {
if (selectedFilter) {
setRadioValue(
filterOptions[selectedFilter].options[
Object.keys(filterOptions[selectedFilter].options)[0]
].name
);
setRadioValue(Object.keys(filterOptions[selectedFilter].options)[0]);
}
}, [filterOptions, selectedFilter]);
......@@ -35,7 +35,7 @@ export function FilterDropdown({ filterOptions, setOpen, addFilter }) {
const onFilterAdd = () => {
addFilter({
subject: filterOptions[selectedFilter].name,
subject: selectedFilter,
condition: radioValue,
value: inputValue,
});
......@@ -43,24 +43,28 @@ export function FilterDropdown({ filterOptions, setOpen, addFilter }) {
};
return (
<div className="absolute flex flex-row z-50">
<div className="absolute flex flex-row z-50" ref={dropdownRef}>
<div
className={classNames(
"dark:bg-dark-600 w-56 h-max max-h-72 rounded-xl mt-2 transition-all duration-100 p-1 overflow-y-scroll shadow-lg shadow-dark-600"
)}
>
{Object.keys(filterOptions).map((key) => {
return (
<FilterDropdownItem
key={"primary_option_" + key}
selected={selectedFilter === key}
id={key}
onClick={onSelectedFilterChange}
>
{filterOptions[key].name}
</FilterDropdownItem>
);
})}
{Object.keys(filterOptions).length > 0 ? (
Object.keys(filterOptions).map((key) => {
return (
<FilterDropdownItem
key={"primary_option_" + key}
selected={selectedFilter === key}
id={key}
onClick={onSelectedFilterChange}
>
{filterOptions[key].name}
</FilterDropdownItem>
);
})
) : (
<FilterDropdownItem disabled>No options available</FilterDropdownItem>
)}
</div>
<CSSTransition
in={selectedFilter}
......@@ -79,6 +83,9 @@ export function FilterDropdown({ filterOptions, setOpen, addFilter }) {
inputValue={inputValue}
onInputChange={onInputChange}
onFilterAdd={onFilterAdd}
inputLeftSymbol={filterOptions[selectedFilter]?.symbol_left}
inputRightSymbol={filterOptions[selectedFilter]?.symbol_right}
inputType={filterOptions[selectedFilter]?.input_type}
/>
</span>
) : (
......@@ -98,6 +105,9 @@ export function SecondaryFilterDropdown({
inputValue,
onInputChange,
onFilterAdd,
inputLeftSymbol,
inputRightSymbol,
inputType,
}) {
return (
<div
......@@ -115,6 +125,10 @@ export function SecondaryFilterDropdown({
onRadioChange={onRadioChange}
inputValue={inputValue}
onInputChange={onInputChange}
inputLeftSymbol={inputLeftSymbol}
inputRightSymbol={inputRightSymbol}
inputType={inputType}
onSubmit={onFilterAdd}
/>
<div className="flex flex-row space-x-2 mb-2">
<Button
......@@ -126,6 +140,7 @@ export function SecondaryFilterDropdown({
<Button
className="bg-indigo-600 text-white font-semibold w-full"
onClick={onFilterAdd}
type="submit"
>
Filter
</Button>
......@@ -135,17 +150,24 @@ export function SecondaryFilterDropdown({
}
// selected: boolean
function FilterDropdownItem({ children, selected, onClick, id }) {
function FilterDropdownItem({
children,
selected,
onClick,
id,
disabled = false,
}) {
return (
<div
className={classNames(
"py-3 px-4 rounded-xl dark:text-neutral-100 hover:bg-dark-800 border-1 transition-all duration-100 cursor-pointer",
"py-3 px-4 rounded-xl dark:text-neutral-100 border-1 transition-all duration-100 cursor-pointer",
{
"border-transparent": !selected,
"bg-dark-800 border-indigo-600 font-semibold": selected,
"hover:bg-dark-800": !disabled,
}
)}
onClick={() => onClick(id)}
onClick={disabled || !onClick ? undefined : () => onClick(id)}
>
<div className="flex flex-row items-center">
{children}
......
......@@ -4,6 +4,8 @@ export function Filter({
subject,
condition,
value,
symbolLeft,
symbolRight,
buttonIcon,
onButtonClick,
}) {
......@@ -14,7 +16,11 @@ export function Filter({
<span className="font-light dark:text-neutral-300">
{condition}&nbsp;
</span>
<span>{value}</span>
<span>
{symbolLeft}
{value}
{symbolRight}
</span>
</div>
{/* Button */}
......
import classNames from "classnames";
import React from "react";
function Input({ placeholder, type, onChange, initialValue }) {
function Input({
placeholder,
type,
onChange,
initialValue,
symbolLeft,
symbolRight,
}) {
return (
<input
className="w-full h-12 text-base focus:ring-indigo-500 focus:border-indigo-500 border-1 border-transparent font-semibold outline-none dark:text-white px-3 pr-3 rounded-2xl bg-dark-800"
placeholder={placeholder}
type={type ? type : "text"}
onChange={onChange}
defaultValue={initialValue || ""}
></input>
<span className="relative flex w-full flex-wrap items-stretch mb-3">
{symbolLeft && (
<span className="absolute left-3 w-5 text-center items-center justify-center h-full z-10 py-3">
<p className="dark:text-white align-top font-extralight">
{symbolLeft}
</p>
</span>
)}
<input
className={classNames(
"relative w-full h-12 text-base focus:ring-indigo-500 focus:border-indigo-500 border-1 border-transparent font-semibold outline-none dark:text-white rounded-2xl bg-dark-800",
{
"pr-3": !symbolRight,
"pr-8": symbolRight,
"pl-3": !symbolLeft,
"pl-8": symbolLeft,
}
)}
placeholder={placeholder}
type={type ? type : "text"}
onChange={onChange}
defaultValue={initialValue || ""}
></input>
{symbolRight && (
<span className="absolute right-3 w-5 text-center items-center justify-center h-full z-10 py-3">
<p className="dark:text-white align-top font-extralight">
{symbolRight}
</p>
</span>
)}
</span>
);
}
......
......@@ -3,31 +3,38 @@ import React from "react";
import Input from "../inputs/Input";
export function RadioInputForm({
inputLeftSymbol,
inputRightSymbol,
inputType,
options,
radioValue,
onRadioChange,
inputValue,
onInputChange,
onSubmit,
}) {
return (
<div>
<form onSubmit={onSubmit}>
<div className="flex flex-col py-2 space-y-2">
{Object.keys(options).map((key) => {
return (
<div key={"radio_option_" + options[key].id}>
<div key={"radio_option_" + key}>
<Radio
selected={radioValue}
radioValue={options[key].name}
id={options[key].id}
radioValue={key}
radioLabel={options[key].name}
id={key}
onChange={onRadioChange}
/>
<div className="my-1">
{options[key].name === radioValue && (
{key === radioValue && (
<Input
type="text"
placeholder="Enter a value here..."
initialValue={inputValue}
onChange={onInputChange}
symbolLeft={inputLeftSymbol}
symbolRight={inputRightSymbol}
type={inputType}
/>
)}
</div>
......@@ -35,11 +42,11 @@ export function RadioInputForm({
);
})}
</div>
</div>
</form>
);
}
export function Radio({ selected, radioValue, onChange }) {
export function Radio({ selected, radioValue, radioLabel, onChange }) {
return (
<label className="inline-flex items-center w-full">
<input
......@@ -55,7 +62,7 @@ export function Radio({ selected, radioValue, onChange }) {
"dark:text-indigo-500": selected === radioValue,
})}
>
{radioValue}
{radioLabel}
</span>
</label>
);
......
import classNames from "classnames";
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
export function Tabs({ children }) {
const [tab, setTab] = useState("card-view");
useEffect(() => {
console.log(tab);
}, [tab]);
return (
<div>
......
import {
less_than_number,
greater_than_number,
equals_string,
equals_integer,
contains_string,
} from "../utils";
export const cryptoChartOptions = (
colors = [],
dark = false,
......@@ -178,53 +186,89 @@ export const cryptocurrencies = [
];
export const filterOptions = {
price: {
id: "price",
current_price: {
id: "current_price",
name: "Price",
input_type: "number",
symbol_left: "$",
options: {
equals: {
id: "equals",
name: "is",
name: "is (approximately)",
function: equals_integer,
},
less_than: {
id: "less_than",
name: "is less than",
function: less_than_number,
},
greater_than: {
id: "greater_than",
name: "is greater than",
function: greater_than_number,
},
},
},
name: {
id: "name",
name: "Name",
input_type: "text",
options: {
equals: {
id: "equals",
name: "is",
function: equals_string,
},
less_than: {
id: "contains",
name: "contains",
function: contains_string,
},
},
},
price_change_percentage: {
id: "price_change_percentage",
price_change_percentage_24h: {
id: "price_change_percentage_24h",
name: "Price Change in %",
symbol_right: "%",
input_type: "number",
options: {
equals: {
id: "equals",
name: "is",
name: "is (approximately)",
function: equals_integer,
},
less_than: {
id: "less_than",
name: "is less than",
function: less_than_number,
},
greater_than: {
id: "greater_than",
name: "is greater than",
function: greater_than_number,
},
},
},
price_change_24h: {
id: "price_change_24h",
name: "Price Change in $",
input_type: "number",
symbol_left: "$",
options: {
equals: {
id: "equals",
name: "is (approximately)",
function: equals_integer,
},
less_than: {
id: "less_than",
name: "is less than",
function: less_than_number,
},
greater_than: {
id: "greater_than",
name: "is greater than",
function: greater_than_number,
},
},
},
......
export * from "./useFilters";
export * from "./useOnClickOutside";
import React, { useState } from "react";
import { useState } from "react";
/**
* Format of each filter should be:
......@@ -8,7 +8,7 @@ import React, { useState } from "react";
* value: ""
* }
*/
export default function useFilters(initialArray) {
export function useFilters(initialArray) {
const [filters, setFilters] = useState(initialArray);
const checkIfFilterExists = (filter) => {
......
import { useEffect } from "react";
export function useOnClickOutside(ref, handler) {
useEffect(() => {
const listener = (event) => {
if (ref.current && ref.current?.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);
return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, handler]);
}
import React, { useState } from "react";
import React, { useState, useRef, useCallback } from "react";
import Head from "next/head";
import Container from "../components/content/Container";
import Main from "../components/content/Main";
......@@ -17,7 +17,7 @@ import {
TableIcon,
XIcon,
} from "@heroicons/react/outline";
import useFilters from "../hooks/useFilters";
import { useFilters, useOnClickOutside } from "../hooks";
import { Filter, FilterButton, Filters } from "../components/filters/Filter";
import { FilterDropdown } from "../components/dropdown/FilterDropdown";
import { filterOptions } from "../constants";
......@@ -29,13 +29,29 @@ import Link from "next/link";
export default function Home() {
const [filters, addFilter, removeFilter] = useFilters([]);
const filterDropdownRef = useRef(null);
const [dropdownOpen, setDropdownOpen] = useState(false);
const callbackDropdownClose = useCallback(() => setDropdownOpen(false), []);
useOnClickOutside(filterDropdownRef, callbackDropdownClose);
const [searchText, setSearchText] = useState("");
const listOfCoins = useCryptoList("usd", 21, false);
const filteredCoins = listOfCoins.data?.filter((coin) => {
// Filter coins based on search
let filteredCoins = listOfCoins.data?.filter((coin) => {
return coin.name.toLowerCase().includes(searchText.toLowerCase());
});
// Apply filters
for (let i = 0; i < filters.length; i++) {
const { subject, condition, value } = filters[i];
filteredCoins = filteredCoins.filter((coin) => {
return filterOptions[subject]?.options[condition]?.function(
coin[subject],
value
);
});
}
return (
<div>
<Head>
......@@ -60,9 +76,14 @@ export default function Home() {
return (
<Filter
key={"filter_" + idx}
subject={filter.subject}
condition={filter.condition}
subject={filterOptions[filter.subject]?.name}
condition={
filterOptions[filter.subject]?.options[filter.condition]
?.name
}
value={filter.value}
symbolLeft={filterOptions[filter.subject]?.symbol_left}
symbolRight={filterOptions[filter.subject]?.symbol_right}
buttonIcon={<XIcon className="w-5 h-5" />}
onButtonClick={() => removeFilter(filter)}
/>
......@@ -75,12 +96,22 @@ export default function Home() {
setDropdownOpen(!dropdownOpen) && addFilter({})
}
/>
{dropdownOpen && (
<FilterDropdown
dropdownRef={filterDropdownRef}
setOpen={setDropdownOpen}
addFilter={addFilter}
filterOptions={filterOptions}
filterOptions={Object.keys(filterOptions)
.filter(
(option) =>
filters.filter(
(filter) => filter.subject === option
).length === 0
)
.reduce((obj, key) => {
obj[key] = filterOptions[key];
return obj;
}, {})}
/>
)}
</div>
......
export * from "./utils";
export function less_than_number(a, b) {
return Number(a) < Number(b);
}
export function equals_integer(a, b) {
return parseInt(a) === parseInt(b);
}
export function greater_than_number(a, b) {
return Number(a) > Number(b);
}
export function equals_string(a, b) {
return a.toLowerCase() === b.toLowerCase();
}
export function contains_string(a, b) {
return a.toLowerCase().includes(b.toLowerCase());
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment