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

Merge branch 'filters_ui' into 'develop'

Filters Feature

See merge request !5
parents c335538e 4b01336c
No related branches found
No related tags found
2 merge requests!7Merge develop to main,!5Filters Feature
import classNames from "classnames";
import React from "react";
export default function Button({ children, className, onClick }) {
return (
<button
className={classNames("py-2 px-5 rounded-xl", className)}
onClick={onClick}
>
{children}
</button>
);
}
import { ArrowRightIcon } from "@heroicons/react/outline";
import classNames from "classnames";
import React, { useState, useEffect } from "react";
import Button from "../button/Button";
import { RadioInputForm } from "../radio/RadioForm";
import { CSSTransition } from "react-transition-group";
export function FilterDropdown({ filterOptions, setOpen, addFilter }) {
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
);
}
}, [filterOptions, selectedFilter]);
const onRadioChange = ({ target: { value } }) => {
setRadioValue(value);
};
const onInputChange = ({ target: { value } }) => {
setInputValue(value);
};
const onSelectedFilterChange = (filterId) => {
setSelectedFilter(filterId);
};
const onFilterAdd = () => {
addFilter({
subject: filterOptions[selectedFilter].name,
condition: radioValue,
value: inputValue,
});
setOpen(false);
};
return (
<div className="absolute flex flex-row z-50">
<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>
);
})}
</div>
<CSSTransition
in={selectedFilter}
classNames="secondary-dropdown"
timeout={300}
unmountOnExit
>
{selectedFilter ? (
<span className="ml-3">
<SecondaryFilterDropdown
open={true}
onSelectedFilterChange={onSelectedFilterChange}
radioOptions={filterOptions[selectedFilter]?.options}
radioValue={radioValue}
onRadioChange={onRadioChange}
inputValue={inputValue}
onInputChange={onInputChange}
onFilterAdd={onFilterAdd}
/>
</span>
) : (
<div></div>
)}
</CSSTransition>
</div>
);
}
export function SecondaryFilterDropdown({
open,
onSelectedFilterChange,
radioOptions,
radioValue,
onRadioChange,
inputValue,
onInputChange,
onFilterAdd,
}) {
return (
<div
className={classNames(
"dark:bg-dark-600 w-72 max-h-72 rounded-xl mt-2 transition-all duration-100 px-5 py-2 overflow-y-scroll shadow-lg shadow-dark-600",
{
block: open,
hidden: !open,
}
)}
>
<RadioInputForm
options={radioOptions}
radioValue={radioValue}
onRadioChange={onRadioChange}
inputValue={inputValue}
onInputChange={onInputChange}
/>
<div className="flex flex-row space-x-2 mb-2">
<Button
className="bg-neutral-100 text-gray-700 font-semibold"
onClick={() => onSelectedFilterChange(null)}
>
Cancel
</Button>
<Button
className="bg-indigo-600 text-white font-semibold w-full"
onClick={onFilterAdd}
>
Filter
</Button>
</div>
</div>
);
}
// selected: boolean
function FilterDropdownItem({ children, selected, onClick, id }) {
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",
{
"border-transparent": !selected,
"bg-dark-800 border-indigo-600 font-semibold": selected,
}
)}
onClick={() => onClick(id)}
>
<div className="flex flex-row items-center">
{children}
{selected && (
<span className="ml-auto">
<ArrowRightIcon className="w-4 h-4" />
</span>
)}
</div>
</div>
);
}
import React from "react";
export function Filter({
subject,
condition,
value,
buttonIcon,
onButtonClick,
}) {
return (
<div className="flex flex-wrap justify-start items-center mx-1 mt-2">
<div className="py-5 pl-5 pr-1 font-sans bg-dark-600 rounded-l-lg dark:text-white inline-flex h-10 justify-center items-center whitespace-nowrap font-normal">
<span>{subject}&nbsp;</span>
<span className="font-light dark:text-neutral-300">
{condition}&nbsp;
</span>
<span>{value}</span>
</div>
{/* Button */}
<button
onClick={onButtonClick}
className="mr-2 p-2 bg-dark-600 hover:dark:bg-dark-800 rounded-r-lg justify-center items-center h-10 w-10 inline-flex font-thin text-white"
>
<span>{buttonIcon}</span>
</button>
</div>
);
}
export function FilterButton({ icon, onClick }) {
return (
<button
className="mx-1 mt-2 font-sans font-normal bg-dark-600 rounded-lg dark:text-neutral-200 dark:hover:text-indigo-600 inline-flex h-10 w-10 justify-center items-center whitespace-nowrap"
onClick={onClick}
>
{icon}
</button>
);
}
export function Filters({ children }) {
return (
<div className="flex flex-wrap justify-start items-center">{children}</div>
);
}
import React from "react";
function Input({ placeholder, type, onChange, initialValue }) {
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>
);
}
export default Input;
import classNames from "classnames";
import React from "react";
import Input from "../inputs/Input";
export function RadioInputForm({
options,
radioValue,
onRadioChange,
inputValue,
onInputChange,
}) {
return (
<div>
<div className="flex flex-col py-2 space-y-2">
{Object.keys(options).map((key) => {
return (
<div key={"radio_option_" + options[key].id}>
<Radio
selected={radioValue}
radioValue={options[key].name}
id={options[key].id}
onChange={onRadioChange}
/>
<div className="my-1">
{options[key].name === radioValue && (
<Input
type="text"
placeholder="Enter a value here..."
initialValue={inputValue}
onChange={onInputChange}
/>
)}
</div>
</div>
);
})}
</div>
</div>
);
}
export function Radio({ selected, radioValue, onChange }) {
return (
<label className="inline-flex items-center w-full">
<input
type="radio"
className="form-radio h-4 w-4 accent-indigo-600 hover:accent-indigo-700"
value={radioValue}
checked={selected === radioValue}
onChange={onChange}
/>{" "}
<span
className={classNames("font-poppins ml-2 text-base", {
"dark:text-white font-extralight": selected !== radioValue,
"dark:text-indigo-500": selected === radioValue,
})}
>
{radioValue}
</span>
</label>
);
}
......@@ -176,3 +176,56 @@ export const cryptocurrencies = [
name: "Cardano",
},
];
export const filterOptions = {
price: {
id: "price",
name: "Price",
options: {
equals: {
id: "equals",
name: "is",
},
less_than: {
id: "less_than",
name: "is less than",
},
greater_than: {
id: "greater_than",
name: "is greater than",
},
},
},
name: {
id: "name",
name: "Name",
options: {
equals: {
id: "equals",
name: "is",
},
less_than: {
id: "contains",
name: "contains",
},
},
},
price_change_percentage: {
id: "price_change_percentage",
name: "Price Change in %",
options: {
equals: {
id: "equals",
name: "is",
},
less_than: {
id: "less_than",
name: "is less than",
},
greater_than: {
id: "greater_than",
name: "is greater than",
},
},
},
};
import React, { useState } from "react";
/**
* Format of each filter should be:
* {
* subject: "",
* condition: "",
* value: ""
* }
*/
export default function useFilters(initialArray) {
const [filters, setFilters] = useState(initialArray);
const checkIfFilterExists = (filter) => {
for (let i = 0; i < filters.length; i++) {
if (
filters[i].subject === filter.subject &&
filters[i].condition === filter.condition &&
filters[i].value === filter.value
) {
return true;
}
}
return false;
};
const addFilter = (filter) => {
// Check if filter already exists
if (checkIfFilterExists(filter)) {
return;
}
setFilters([...filters, filter]);
};
const removeFilter = (filter) => {
setFilters(
filters.filter(({ subject, condition, value }) => {
return (
subject !== filter.subject ||
condition !== filter.condition ||
value !== filter.value
);
})
);
};
return [filters, addFilter, removeFilter];
}
......@@ -22,6 +22,7 @@
"react-apexcharts": "^1.3.9",
"react-dom": "17.0.2",
"react-query": "^3.34.15",
"react-transition-group": "^4.4.2",
"sharp": "^0.30.2"
},
"devDependencies": {
......
......@@ -11,7 +11,16 @@ import numeral from "numeral";
import Wrapper from "../components/content/Wrapper";
import Sidebar from "../components/sidebar/Sidebar";
import { Tabs, Tab } from "../components/tabs/Tab";
import { CollectionIcon, TableIcon } from "@heroicons/react/outline";
import {
CollectionIcon,
PlusIcon,
TableIcon,
XIcon,
} from "@heroicons/react/outline";
import useFilters from "../hooks/useFilters";
import { Filter, FilterButton, Filters } from "../components/filters/Filter";
import { FilterDropdown } from "../components/dropdown/FilterDropdown";
import { filterOptions } from "../constants";
import { Table, TableCell, TableRow } from "../components/table/Table";
import Image from "next/image";
import CryptoRowLineChart from "../components/charts/CryptoRowLineChart";
......@@ -19,6 +28,8 @@ import classNames from "classnames";
import Link from "next/link";
export default function Home() {
const [filters, addFilter, removeFilter] = useFilters([]);
const [dropdownOpen, setDropdownOpen] = useState(false);
const [searchText, setSearchText] = useState("");
const listOfCoins = useCryptoList("usd", 21, false);
const filteredCoins = listOfCoins.data?.filter((coin) => {
......@@ -43,6 +54,38 @@ export default function Home() {
{/* Search Field */}
<SearchInput onChange={(e) => setSearchText(e?.target.value)} />
</div>
<div className="mb-2">
<Filters>
{filters.map((filter, idx) => {
return (
<Filter
key={"filter_" + idx}
subject={filter.subject}
condition={filter.condition}
value={filter.value}
buttonIcon={<XIcon className="w-5 h-5" />}
onButtonClick={() => removeFilter(filter)}
/>
);
})}
<div className="relative">
<FilterButton
icon={<PlusIcon className="w-6 h-6" />}
onClick={() =>
setDropdownOpen(!dropdownOpen) && addFilter({})
}
/>
{dropdownOpen && (
<FilterDropdown
setOpen={setDropdownOpen}
addFilter={addFilter}
filterOptions={filterOptions}
/>
)}
</div>
</Filters>
</div>
{/* Main Content */}
<Tabs>
......@@ -91,7 +134,7 @@ export default function Home() {
content={<TableIcon className="w-6 h-6 dark:text-white" />}
>
<Table>
<TableRow className="h-14 items-center sticky top-0 z-50">
<TableRow className="h-14 items-center sticky top-0 z-40">
<TableCell className="w-6 h-10"></TableCell>
<TableCell className="w-24 h-10">
<p className="font-medium text-base text-center">Name</p>
......
......@@ -25,6 +25,24 @@
box-shadow: 0 0 10px rgb(37 99 235), 0 0 5px rgb(37 99 235) !important;
}
.secondary-dropdown-enter {
opacity: 0;
transform: translateX(-50px);
}
.secondary-dropdown-enter-active {
opacity: 1;
transform: translateX(0);
transition: opacity 300ms, transform 300ms;
}
.secondary-dropdown-exit {
opacity: 1;
}
.secondary-dropdown-exit-active {
opacity: 0;
/* transform: scale(0.9); */
transform: translateX(-50px);
transition: opacity 300ms, transform 300ms;
}
.coin-app {
display: flex;
flex-direction: column;
......
......@@ -20,10 +20,15 @@ module.exports = {
},
colors: {
dark: {
100: "#4b5364",
200: "#3f4654",
300: "#343a45",
400: "#282d36",
500: "#1d2026",
600: "#121418",
700: "#111317",
800: "#080808",
900: "#000",
900: "#060608",
},
},
},
......
......@@ -31,7 +31,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.7":
version "7.17.2"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
......@@ -633,6 +633,11 @@ cssesc@^3.0.0:
resolved "https://registry.yarnpkg.com/cssesc/-/cssesc-3.0.0.tgz#37741919903b868565e1c09ea747445cd18983ee"
integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
csstype@^3.0.2:
version "3.0.11"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.0.11.tgz#d66700c5eacfac1940deb4e3ee5642792d85cd33"
integrity sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==
damerau-levenshtein@^1.0.7:
version "1.0.8"
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
......@@ -743,6 +748,14 @@ doctrine@^3.0.0:
dependencies:
esutils "^2.0.2"
dom-helpers@^5.0.1:
version "5.2.1"
resolved "https://registry.yarnpkg.com/dom-helpers/-/dom-helpers-5.2.1.tgz#d9400536b2bf8225ad98fe052e029451ac40e902"
integrity sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==
dependencies:
"@babel/runtime" "^7.8.7"
csstype "^3.0.2"
echarts-for-react@^3.0.2:
version "3.0.2"
resolved "https://registry.yarnpkg.com/echarts-for-react/-/echarts-for-react-3.0.2.tgz#ac5859157048a1066d4553e34b328abb24f2b7c1"
......@@ -1977,12 +1990,13 @@ prelude-ls@^1.2.1:
resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396"
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
process-nextick-args@~2.0.0:
version "2.0.1"
resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
prop-types@^15.5.7, prop-types@^15.7.2:
prop-types@^15.5.7, prop-types@^15.6.2, prop-types@^15.7.2:
version "15.8.1"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
......@@ -2054,6 +2068,16 @@ react-query@^3.34.15:
broadcast-channel "^3.4.1"
match-sorter "^6.0.2"
react-transition-group@^4.4.2:
version "4.4.2"
resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.2.tgz#8b59a56f09ced7b55cbd53c36768b922890d5470"
integrity sha512-/RNYfRAMlZwDSr6z4zNKV6xu53/e2BuaBbGhbyYIXTrmgu/bGHzmqOs7mJSJBHy9Ud+ApHx3QjrkKSp1pxvlFg==
dependencies:
"@babel/runtime" "^7.5.5"
dom-helpers "^5.0.1"
loose-envify "^1.4.0"
prop-types "^15.6.2"
react@17.0.2:
version "17.0.2"
resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"
......
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