From 8d421eacca0fc5b88d926ab106e6a22691b7b5f7 Mon Sep 17 00:00:00 2001 From: fill-the-fill Date: Fri, 28 Jan 2022 14:44:32 +0200 Subject: [PATCH 1/7] Add search bar into tools page & Modify filtering design --- .../ShowcaseCheckbox/styles.module.css | 3 +- .../showcase/ShowcaseFilterToggle/index.js | 69 ++++++ .../ShowcaseFilterToggle/styles.module.css | 70 ++++++ .../showcase/ShowcaseTagSelect/index.js | 89 ++++++++ .../ShowcaseTagSelect/styles.module.css | 51 +++++ .../showcase/ShowcaseTooltip/index.js | 139 ++++++++++++ .../ShowcaseTooltip/styles.module.css | 45 ++++ src/data/builder-tools.js | 9 + src/pages/tools/index.js | 212 +++++++++++++----- src/pages/tools/styles.module.css | 112 +++++++++ 10 files changed, 736 insertions(+), 63 deletions(-) create mode 100644 src/components/showcase/ShowcaseFilterToggle/index.js create mode 100644 src/components/showcase/ShowcaseFilterToggle/styles.module.css create mode 100644 src/components/showcase/ShowcaseTagSelect/index.js create mode 100644 src/components/showcase/ShowcaseTagSelect/styles.module.css create mode 100644 src/components/showcase/ShowcaseTooltip/index.js create mode 100644 src/components/showcase/ShowcaseTooltip/styles.module.css diff --git a/src/components/showcase/ShowcaseCheckbox/styles.module.css b/src/components/showcase/ShowcaseCheckbox/styles.module.css index 42214cd71f..1bb21cc737 100644 --- a/src/components/showcase/ShowcaseCheckbox/styles.module.css +++ b/src/components/showcase/ShowcaseCheckbox/styles.module.css @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -.checkboxContainer { + .checkboxContainer { display: inline; padding: 5px; user-select: none; @@ -20,3 +20,4 @@ margin-left: 0.5rem; text-overflow: ellipsis; } + diff --git a/src/components/showcase/ShowcaseFilterToggle/index.js b/src/components/showcase/ShowcaseFilterToggle/index.js new file mode 100644 index 0000000000..d493318721 --- /dev/null +++ b/src/components/showcase/ShowcaseFilterToggle/index.js @@ -0,0 +1,69 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + import React, {useState, useEffect, useCallback} from 'react'; + import {useHistory, useLocation} from '@docusaurus/router'; + + // import {prepareUserState} from '../../index'; + + import styles from './styles.module.css'; + import clsx from 'clsx'; + + export const Operator = 'OR' | 'AND'; + + export const OperatorQueryKey = 'operator'; + + export function readOperator(search) { + return (new URLSearchParams(search).get(OperatorQueryKey) ?? + 'OR'); + } + + export default function ShowcaseFilterToggle() { + const id = 'showcase_filter_toggle'; + const location = useLocation(); + const history = useHistory(); + const [operator, setOperator] = useState(false); + useEffect(() => { + setOperator(readOperator(location.search) === 'AND'); + }, [location]); + const toggleOperator = useCallback(() => { + setOperator((o) => !o); + const searchParams = new URLSearchParams(location.search); + searchParams.delete(OperatorQueryKey); + if (!operator) { + searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND'); + } + history.push({ + ...location, + search: searchParams.toString(), + state: prepareUserState(), + }); + }, [operator, location, history]); + + return ( +
+ { + if (e.key === 'Enter') { + toggleOperator(); + } + }} + checked={operator} + /> + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
+ ); + } \ No newline at end of file diff --git a/src/components/showcase/ShowcaseFilterToggle/styles.module.css b/src/components/showcase/ShowcaseFilterToggle/styles.module.css new file mode 100644 index 0000000000..e147ae5b26 --- /dev/null +++ b/src/components/showcase/ShowcaseFilterToggle/styles.module.css @@ -0,0 +1,70 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + .screenReader { + border: 0; + clip: rect(0 0 0 0); + clip-path: polygon(0 0, 0 0, 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 10px; + white-space: nowrap; + } + + .checkboxLabel { + --height: 25px; + --width: 80px; + --border: 2px; + display: flex; + width: var(--width); + height: var(--height); + position: relative; + border-radius: var(--height); + border: var(--border) solid var(--ifm-color-primary-darkest); + cursor: pointer; + justify-content: space-around; + opacity: 0.75; + transition: opacity var(--ifm-transition-fast) + var(--ifm-transition-timing-default); + box-shadow: var(--ifm-global-shadow-md); + } + + .checkboxLabel:hover { + opacity: 1; + box-shadow: var(--ifm-global-shadow-md), + 0 0 2px 1px var(--ifm-color-primary-dark); + } + + .checkboxLabel::after { + position: absolute; + content: ''; + inset: 0; + width: calc(var(--width) / 2); + height: 100%; + border-radius: var(--height); + background-color: var(--ifm-color-primary-darkest); + transition: transform var(--ifm-transition-fast) + var(--ifm-transition-timing-default); + transform: translateX(calc(var(--width) / 2 - var(--border))); + } + + input:focus-visible ~ .checkboxLabel::after { + outline: 2px solid currentColor; + } + + .checkboxLabel > * { + font-size: 0.8rem; + color: inherit; + transition: opacity 150ms ease-in 50ms; + } + + input:checked ~ .checkboxLabel::after { + transform: translateX(calc(-1 * var(--border))); + } \ No newline at end of file diff --git a/src/components/showcase/ShowcaseTagSelect/index.js b/src/components/showcase/ShowcaseTagSelect/index.js new file mode 100644 index 0000000000..b15af972a6 --- /dev/null +++ b/src/components/showcase/ShowcaseTagSelect/index.js @@ -0,0 +1,89 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + import React, { + useCallback, + useState, + useEffect, + forwardRef +} from 'react'; +import {useHistory, useLocation} from '@docusaurus/router'; +import {toggleListItem} from '@site/src/utils/jsUtils'; +// import {prepareUserState} from '../../../pages/showcase/index'; +import Tags from '@site/src/data/showcases'; + +import styles from './styles.module.css'; + +const TagQueryStringKey = 'tags'; + +export function readSearchTags(search) { + return new URLSearchParams(search).getAll(TagQueryStringKey); +} + +function replaceSearchTags(search, newTags) { + const searchParams = new URLSearchParams(search); + searchParams.delete(TagQueryStringKey); + newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag)); + return searchParams.toString(); +} + +const ShowcaseTagSelect = forwardRef( + ({id, icon, label, tag, ...rest}, ref) => { + const location = useLocation(); + const history = useHistory(); + const [selected, setSelected] = useState(false); + useEffect(() => { + const tags = readSearchTags(location.search); + setSelected(tags.includes(tag)); + }, [tag, location]); + const toggleTag = useCallback(() => { + const tags = readSearchTags(location.search); + const newTags = toggleListItem(tags, tag); + const newSearch = replaceSearchTags(location.search, newTags); + history.push({ + ...location, + search: newSearch, + // state: prepareUserState(), + }); + }, [tag, location, history]); + return ( + <> + { + if (e.key === 'Enter') { + toggleTag(); + } + }} + onFocus={(e) => { + if (e.relatedTarget) { + e.target.nextElementSibling?.dispatchEvent( + new KeyboardEvent('focus'), + ); + } + }} + onBlur={(e) => { + e.target.nextElementSibling?.dispatchEvent( + new KeyboardEvent('blur'), + ); + }} + onChange={toggleTag} + checked={selected} + {...rest} + /> + + + ); + }, +); + +export default ShowcaseTagSelect; \ No newline at end of file diff --git a/src/components/showcase/ShowcaseTagSelect/styles.module.css b/src/components/showcase/ShowcaseTagSelect/styles.module.css new file mode 100644 index 0000000000..5d01e1049e --- /dev/null +++ b/src/components/showcase/ShowcaseTagSelect/styles.module.css @@ -0,0 +1,51 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + .checkboxLabel:hover { + opacity: 1; + box-shadow: 0 0 2px 1px var(--ifm-color-secondary-darkest); + } + + .screenReader { + border: 0; + clip: rect(0 0 0 0); + clip-path: polygon(0 0, 0 0, 0 0); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 10px; + white-space: nowrap; + } + + input[type='checkbox'] + .checkboxLabel { + display: flex; + align-items: center; + cursor: pointer; + line-height: 1.5; + border-radius: 4px; + padding: 0.275rem 0.8rem; + opacity: 0.85; + transition: opacity 200ms ease-out; + border: 2px solid var(--ifm-color-secondary-darkest); + } + + input:focus-visible + .checkboxLabel { + outline: 2px solid currentColor; + } + + input:checked + .checkboxLabel { + opacity: 0.9; + background-color: var(--site-color-checkbox-checked-bg); + border: 2px solid var(--ifm-color-primary-darkest); + } + + input:checked + .checkboxLabel:hover { + opacity: 0.75; + box-shadow: 0 0 2px 1px var(--ifm-color-primary-dark); + } \ No newline at end of file diff --git a/src/components/showcase/ShowcaseTooltip/index.js b/src/components/showcase/ShowcaseTooltip/index.js new file mode 100644 index 0000000000..0b4f9e737b --- /dev/null +++ b/src/components/showcase/ShowcaseTooltip/index.js @@ -0,0 +1,139 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + import React, {useEffect, useState, useRef} from 'react'; + import ReactDOM from 'react-dom'; + import {usePopper} from 'react-popper'; + import styles from './styles.module.css'; + + export default function Tooltip({ + children, + id, + anchorEl, + text, + delay, + }){ + const [open, setOpen] = useState(false); + const [referenceElement, setReferenceElement] = useState( + null, + ); + const [popperElement, setPopperElement] = useState(null); + const [arrowElement, setArrowElement] = useState(null); + const [container, setContainer] = useState(null); + const {styles: popperStyles, attributes} = usePopper( + referenceElement, + popperElement, + { + modifiers: [ + { + name: 'arrow', + options: { + element: arrowElement, + }, + }, + { + name: 'offset', + options: { + offset: [0, 8], + }, + }, + ], + }, + ); + + const timeout = useRef(null); + const tooltipId = `${id}_tooltip`; + + useEffect(() => { + if (anchorEl) { + if (typeof anchorEl === 'string') { + setContainer(document.querySelector(anchorEl)); + } else { + setContainer(anchorEl); + } + } else { + setContainer(document.body); + } + }, [container, anchorEl]); + + useEffect(() => { + const showEvents = ['mouseenter', 'focus']; + const hideEvents = ['mouseleave', 'blur']; + + const handleOpen = () => { + // There is no point in displaying an empty tooltip. + if (text === '') { + return; + } + + // Remove the title ahead of time to avoid displaying + // two tooltips at the same time (native + this one). + referenceElement?.removeAttribute('title'); + + timeout.current = window.setTimeout(() => { + setOpen(true); + }, delay || 400); + }; + + const handleClose = () => { + clearInterval(!timeout.current); + setOpen(false); + }; + + if (referenceElement) { + showEvents.forEach((event) => { + referenceElement.addEventListener(event, handleOpen); + }); + + hideEvents.forEach((event) => { + referenceElement.addEventListener(event, handleClose); + }); + } + + return () => { + if (referenceElement) { + showEvents.forEach((event) => { + referenceElement.removeEventListener(event, handleOpen); + }); + + hideEvents.forEach((event) => { + referenceElement.removeEventListener(event, handleClose); + }); + } + }; + }, [referenceElement, text, delay]); + + return ( + <> + {React.cloneElement(children, { + ref: setReferenceElement, + 'aria-describedby': open ? tooltipId : undefined, + })} + {container + ? ReactDOM.createPortal( + open && ( + + ), + container, + ) + : container} + + ); + } \ No newline at end of file diff --git a/src/components/showcase/ShowcaseTooltip/styles.module.css b/src/components/showcase/ShowcaseTooltip/styles.module.css new file mode 100644 index 0000000000..b280c6c35a --- /dev/null +++ b/src/components/showcase/ShowcaseTooltip/styles.module.css @@ -0,0 +1,45 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + + .tooltip { + border-radius: 4px; + padding: 4px 8px; + color: var(--site-color-tooltip); + background: var(--site-color-tooltip-background); + font-size: 0.8rem; + z-index: 500; + line-height: 1.4; + font-weight: 500; + max-width: 300px; + opacity: 0.92; +} + +.tooltipArrow { + visibility: hidden; +} + +.tooltipArrow, +.tooltipArrow::before { + position: absolute; + width: 8px; + height: 8px; + background: inherit; +} + +.tooltipArrow::before { + visibility: visible; + content: ''; + transform: rotate(45deg); +} + +.tooltip[data-popper-placement^='top'] > .tooltipArrow { + bottom: -4px; +} + +.tooltip[data-popper-placement^='bottom'] > .tooltipArrow { + top: -4px; +} \ No newline at end of file diff --git a/src/data/builder-tools.js b/src/data/builder-tools.js index 30ded21db8..ab0ed05301 100644 --- a/src/data/builder-tools.js +++ b/src/data/builder-tools.js @@ -25,6 +25,7 @@ export const Tags = { description: "Our favorite Cardano builder tools that you must absolutely check-out.", icon: <>⭐️, + color: '#e9669e', }, // API @@ -32,6 +33,7 @@ export const Tags = { label: "APIs", description: "Cardano APIs.", icon: null, + color: '#39ca30', }, // For builder tools with a get started tag, a link to the get started page is required. @@ -39,6 +41,7 @@ export const Tags = { label: "Get Started", description: "This builder tool has a get started page in the developer portal.", icon: null, + color: '#dfd545', }, // Library @@ -47,6 +50,7 @@ export const Tags = { description: "Cardano libraries.", icon: null, + color: '#a44fb7', }, // Marlowe @@ -55,6 +59,7 @@ export const Tags = { description: "Marlowe", icon: null, + color: '#127f82' }, // NFT Tools @@ -62,6 +67,7 @@ export const Tags = { label: "NFT", description: "Non-Fungible Token (NFT)", icon: null, + color: '#fe6829', }, // Plutus @@ -70,6 +76,7 @@ export const Tags = { description: "Plutus", icon: null, + color: '#8c2f00', }, // Stake Pool Operator Tools @@ -78,6 +85,7 @@ export const Tags = { description: "Stake pool operator tools.", icon: null, + color: '#4267b2', }, // Oracle Tools @@ -86,6 +94,7 @@ export const Tags = { description: "Oracle tools.", icon: null, + color: '#14cfc3', }, }; diff --git a/src/pages/tools/index.js b/src/pages/tools/index.js index 7e029f7ac3..edacd428a5 100644 --- a/src/pages/tools/index.js +++ b/src/pages/tools/index.js @@ -1,42 +1,38 @@ import React, { useState, useMemo, useCallback, useEffect } from "react"; import Layout from "@theme/Layout"; -import ShowcaseCheckbox from "@site/src/components/showcase/ShowcaseCheckbox"; +import ShowcaseTooltip from "@site/src/components/showcase/ShowcaseTooltip"; +import ShowcaseTagSelect from "@site/src/components/showcase/ShowcaseTagSelect"; import ShowcaseCard from "@site/src/components/showcase/ShowcaseCard"; +import ShowcaseFilterToggle, { + Operator, + readOperator +} from "@site/src/components/showcase/ShowcaseFilterToggle"; import clsx from "clsx"; import PortalHero from "../portalhero"; import { toggleListItem } from "../../utils/jsUtils"; import { SortedShowcases, Tags, TagList } from "../../data/builder-tools"; import { useHistory, useLocation } from "@docusaurus/router"; +import styles from "./styles.module.css"; + +import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; + const TITLE = "Builder Tools"; const DESCRIPTION = "Tools to help you build on Cardano"; const CTA = "Add your tool"; const FILENAME = "builder-tools.js"; -function filterProjects(projects, selectedTags, operator) { - if (selectedTags.length === 0) { - return projects; +export function prepareUserState() { + if (ExecutionEnvironment.canUseDOM) { + return { + scrollTopPosition: window.scrollY, + focusedElementId: document.activeElement?.id, + }; } - return projects.filter((showcase) => { - if (showcase.tags.length === 0) { - return false; - } - if (operator === "AND") { // no operator selection for the time being, we use OR - return selectedTags.every((tag) => showcase.tags.includes(tag)); - } else { - return selectedTags.some((tag) => showcase.tags.includes(tag)); - } - }); -} -function useFilteredProjects(projects, selectedTags, operator) { - return useMemo(() => filterProjects(projects, selectedTags, operator), [ - projects, - selectedTags, - operator, - ]); + return undefined; } const TagQueryStringKey = "tags"; @@ -52,6 +48,53 @@ function replaceSearchTags(search, newTags) { return searchParams.toString(); } +function filterProjects( + projects, + selectedTags, + operator, + searchName, +) { + if (searchName) { + // eslint-disable-next-line no-param-reassign + projects = projects.filter((project) => + project.title.toLowerCase().includes(searchName.toLowerCase()), + ); + } + if (selectedTags.length === 0) { + return projects; + } + return projects.filter((project) => { + if (project.tags.length === 0) { + return false; + } + if (operator === 'AND') { + return selectedTags.every((tag) => project.tags.includes(tag)); + } else { + return selectedTags.some((tag) => project.tags.includes(tag)); + } + }) +} + + +function useFilteredUsers() { + const location = useLocation(); + const [operator, setOperator] = useState('OR'); + // On SSR / first mount (hydration) no tag is selected + const [selectedTags, setSelectedTags] = useState([]); + const [searchName, setSearchName] = useState(null); + // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) + useEffect(() => { + setSelectedTags(readSearchTags(location.search)); + setOperator(readOperator(location.search)); + setSearchName(readSearchName(location.search)); + }, [location]); + + return useMemo( + () => filterProjects(SortedShowcases, selectedTags, operator, searchName), + [selectedTags, operator, searchName], + ); +} + function useSelectedTags() { // The search query-string is the source of truth! const location = useLocation(); @@ -59,13 +102,7 @@ function useSelectedTags() { // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); - - // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) - useEffect(() => { - const tags = readSearchTags(location.search); - setSelectedTags(tags); - }, [location, setSelectedTags]); - + // Update the QS value const toggleTag = useCallback( (tag) => { @@ -73,7 +110,6 @@ function useSelectedTags() { const newTags = toggleListItem(tags, tag); const newSearch = replaceSearchTags(location.search, newTags); push({ ...location, search: newSearch }); - // no need to call setSelectedTags, useEffect will do the sync }, [location, push] ); @@ -92,32 +128,50 @@ function ShowcaseHeader() { ); } -function ShowcaseFilters({ selectedTags, toggleTag, operator, setOperator }) { +function ShowcaseFilters() { return (
+
+
+

Filters

+
+ +
{TagList.map((tag) => { - const { label, description, icon } = Tags[tag]; + const { label, description, color, icon } = Tags[tag]; + const id = `showcase_checkbox_id_${tag}`; return ( -
- - {icon} {label} - - ) : ( - label - ) - } - onChange={() => toggleTag(tag)} - checked={selectedTags.includes(tag)} - /> -
+ <> +
+ + + ) : ( + + ) + } + /> + +
+ ); })}
@@ -125,12 +179,13 @@ function ShowcaseFilters({ selectedTags, toggleTag, operator, setOperator }) { ); } -function ShowcaseCards({ filteredProjects }) { +function ShowcaseCards() { + const filteredProjects = useFilteredUsers(); return (

{filteredProjects.length} project - {filteredProjects.length > 1 ? "s" : ""} + {filteredProjects.length > 1 ? "s" : ""}

{filteredProjects.length > 0 ? ( @@ -151,15 +206,50 @@ function ShowcaseCards({ filteredProjects }) {
); } +const SearchNameQueryKey = 'name'; + +function readSearchName(search) { + return new URLSearchParams(search).get(SearchNameQueryKey); +} + +function SearchBar() { + const history = useHistory(); + const location = useLocation(); + const [value, setValue] = useState(null); + useEffect(() => { + setValue(readSearchName(location.search)); + }, [location]); + return ( +
+ { + setValue(e.currentTarget.value); + const newSearch = new URLSearchParams(location.search); + newSearch.delete(SearchNameQueryKey); + if (e.currentTarget.value) { + newSearch.set(SearchNameQueryKey, e.currentTarget.value); + } + history.push({ + ...location, + search: newSearch.toString(), + state: prepareUserState(), + }); + setTimeout(() => { + document.getElementById('searchbar')?.focus(); + }, 0); + }} + /> +
+ ); +} function Showcase() { const { selectedTags, toggleTag } = useSelectedTags(); - const [operator, setOperator] = useState("OR"); - const filteredProjects = useFilteredProjects( - SortedShowcases, - selectedTags, - operator - ); + const filteredProjects = useFilteredUsers(); + return ( @@ -167,14 +257,12 @@ function Showcase() { + ); } -export default Showcase; - +export default Showcase; \ No newline at end of file diff --git a/src/pages/tools/styles.module.css b/src/pages/tools/styles.module.css index 04371e4834..b33a96df59 100644 --- a/src/pages/tools/styles.module.css +++ b/src/pages/tools/styles.module.css @@ -5,6 +5,19 @@ * LICENSE file in the root directory of this source tree. */ + .checkboxListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.checkboxListItem:last-child { + margin-right: 0; +} + .heroBanner { padding: 4rem 0; text-align: center; @@ -28,3 +41,102 @@ max-height: 175px; overflow: hidden; } + + +.filterCheckbox { + justify-content: space-between; +} + +.filterCheckbox, +.checkboxList { + display: flex; + align-items: center; +} + +.filterCheckbox > div:first-child { + display: flex; + flex: 1 1 auto; + align-items: center; +} + +.filterCheckbox > div > * { + margin-bottom: 0; + margin-right: 8px; +} + +.checkboxList { + flex-wrap: wrap; +} + +.checkboxList, +.showcaseList { + padding: 0; + list-style: none; +} + +.checkboxListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.checkboxListItem:last-child { + margin-right: 0; +} + +.searchContainer { + margin-left: auto; + display: block; + text-align: right; + padding: 0 var(--ifm-spacing-horizontal); +} + +.searchContainer input { + height: 30px; + border-radius: 15px; + padding: 10px; + border: 1px solid gray; +} + +.showcaseList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.showcaseFavorite { + padding-top: 2rem; + padding-bottom: 2rem; + background-color: var(--site-color-favorite-background); +} + +.showcaseFavoriteHeader { + display: flex; + align-items: center; +} + +.showcaseFavoriteHeader > h2 { + margin-bottom: 0; +} + +.showcaseFavoriteHeader > svg { + width: 30px; + height: 30px; +} + +.svgIconFavoriteXs, +.svgIconFavorite { + color: var(--site-color-svg-icon-favorite); +} + +.svgIconFavoriteXs { + margin-left: 0.625rem; + font-size: 1rem; +} + +.svgIconFavorite { + margin-left: 1rem; +} \ No newline at end of file From a6603abdfdd3175462769c2bd8a46e602c4bd769 Mon Sep 17 00:00:00 2001 From: fill-the-fill Date: Fri, 28 Jan 2022 14:59:55 +0200 Subject: [PATCH 2/7] Modify "Featured" tool to display as a star instead of color --- src/pages/tools/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/tools/index.js b/src/pages/tools/index.js index edacd428a5..e8ad368ca3 100644 --- a/src/pages/tools/index.js +++ b/src/pages/tools/index.js @@ -154,8 +154,10 @@ function ShowcaseFilters() { id={id} label={label} icon={ - tag === "favorite" ? ( - + label === "Featured" ? ( + {icon} ) : ( Date: Wed, 2 Feb 2022 13:16:56 +0200 Subject: [PATCH 3/7] Modify tools showcase --- src/components/showcase/ShowcaseCard/index.js | 87 +++++++++--- .../showcase/ShowcaseCard/styles.module.css | 96 ++++++++++--- .../showcase/ShowcaseFilterToggle/index.js | 125 ++++++++-------- .../showcase/ShowcaseTagSelect/index.js | 35 +++-- src/data/builder-tools.js | 2 +- src/pages/tools/index.js | 134 ++++++++++-------- src/pages/tools/styles.module.css | 4 +- src/svg/fav.svg | 8 ++ 8 files changed, 314 insertions(+), 177 deletions(-) create mode 100644 src/svg/fav.svg diff --git a/src/components/showcase/ShowcaseCard/index.js b/src/components/showcase/ShowcaseCard/index.js index b43933ed24..a9a3dc6ff2 100644 --- a/src/components/showcase/ShowcaseCard/index.js +++ b/src/components/showcase/ShowcaseCard/index.js @@ -5,29 +5,82 @@ * LICENSE file in the root directory of this source tree. */ -import React, { memo } from "react"; +import React, {memo, forwardRef} from 'react'; +import clsx from 'clsx'; +import Image from '@theme/IdealImage'; +import Link from '@docusaurus/Link'; -import styles from "./styles.module.css"; -import clsx from "clsx"; -import Image from "@theme/IdealImage"; -import ShowcaseFooter from './components/ShowcaseFooter' -import ShowcaseBody from "./components/ShowcaseBody"; +import styles from './styles.module.css'; +import Tooltip from '../ShowcaseTooltip/index'; +import { + Tags, +} from '../../../data/builder-tools'; +import Fav from '../../../svg/fav.svg' +const TagComp = forwardRef( + ({label, color, description}, ref) => ( +
  • + {label.toLowerCase()} + +
  • + ), +); +function ShowcaseCardTag({tags}) { + const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]})); -const ShowcaseCard = memo(function ({ showcase }) { - const showFooter = showcase.getstarted || showcase.website || showcase.source return ( -
    -
    -
    - {showcase.title} -
    - - {showFooter && } + <> + {tagObjects.map((tagObject, index) => { + const id = `showcase_card_tag_${tagObject.tag}`; + + return ( + + + + ); + })} + + ); +} + +const ShowcaseCard = memo((user) => ( +
  • +
    + {user.showcase.title} +
    +
    +
    +

    + + {user.showcase.title} + +

    + {user.showcase.tags.includes('featured') && ( + + )} + {user.showcase.website && ( + + Source + + )}
    +

    {user.showcase.description}

    - ); -}); +
      + +
    +
  • +)); export default ShowcaseCard; diff --git a/src/components/showcase/ShowcaseCard/styles.module.css b/src/components/showcase/ShowcaseCard/styles.module.css index 2495278b25..40fedd189c 100644 --- a/src/components/showcase/ShowcaseCard/styles.module.css +++ b/src/components/showcase/ShowcaseCard/styles.module.css @@ -5,31 +5,95 @@ * LICENSE file in the root directory of this source tree. */ -.showcaseCard { - height: 100%; -} - .showcaseCardImage { - max-height: 175px; overflow: hidden; + height: 150px; + border-bottom: 2px solid var(--ifm-color-emphasis-200); +} + +.showcaseCardHeader { + display: flex; + align-items: center; + margin-bottom: 12px; +} + +.showcaseCardTitle { + margin-bottom: 0; + flex: 1 1 auto; +} + +.showcaseCardTitle a { + text-decoration: none; + background: linear-gradient( + var(--ifm-color-primary), + var(--ifm-color-primary) + ) + 0% 100% / 0% 1px no-repeat; + transition: background-size ease-out 200ms; +} + +.showcaseCardTitle a:not(:focus):hover { + background-size: 100% 1px; +} + +.showcaseCardTitle, +.showcaseCardHeader .svgIconFavorite { + margin-right: 0.25rem; +} + +.showcaseCardHeader .svgIconFavorite { + color: var(--site-color-svg-icon-favorite); +} + +.showcaseCardSrcBtn { + margin-left: 6px; + padding-left: 12px; + padding-right: 12px; + border: none !important; +} + +.showcaseCardSrcBtn:focus-visible { + background-color: var(--ifm-color-secondary-dark); +} + +html[data-theme='dark'] .showcaseCardSrcBtn { + background-color: var(--ifm-color-emphasis-200) !important; + color: inherit; +} + +html[data-theme='dark'] .showcaseCardSrcBtn:hover { + background-color: var(--ifm-color-emphasis-300) !important; +} + +.showcaseCardBody { + font-size: smaller; + line-height: 1.66; } -.titleIconsRow { +.cardFooter { display: flex; - flex-direction: row; - flex-wrap: nowrap; + flex-wrap: wrap; } -.titleIconsRowTitle { - flex: 1; +.tag { + font-size: 0.675rem; + border: 1px solid var(--ifm-color-secondary-darkest); + cursor: default; + margin-right: 6px; + margin-bottom: 6px !important; + border-radius: 12px; + display: inline-flex; + align-items: center; } -.titleIconsRowIcons { - flex-grow: 0; - flex-shrink: 0; +.tag .textLabel { + margin-left: 8px; } -.tagIcon { - margin: 0.2rem; - user-select: none; +.tag .colorLabel { + width: 7px; + height: 7px; + border-radius: 50%; + margin-left: 6px; + margin-right: 6px; } diff --git a/src/components/showcase/ShowcaseFilterToggle/index.js b/src/components/showcase/ShowcaseFilterToggle/index.js index d493318721..3db11b6d8e 100644 --- a/src/components/showcase/ShowcaseFilterToggle/index.js +++ b/src/components/showcase/ShowcaseFilterToggle/index.js @@ -5,65 +5,66 @@ * LICENSE file in the root directory of this source tree. */ - import React, {useState, useEffect, useCallback} from 'react'; - import {useHistory, useLocation} from '@docusaurus/router'; - - // import {prepareUserState} from '../../index'; - - import styles from './styles.module.css'; - import clsx from 'clsx'; - - export const Operator = 'OR' | 'AND'; - - export const OperatorQueryKey = 'operator'; - - export function readOperator(search) { - return (new URLSearchParams(search).get(OperatorQueryKey) ?? - 'OR'); - } - - export default function ShowcaseFilterToggle() { - const id = 'showcase_filter_toggle'; - const location = useLocation(); - const history = useHistory(); - const [operator, setOperator] = useState(false); - useEffect(() => { - setOperator(readOperator(location.search) === 'AND'); - }, [location]); - const toggleOperator = useCallback(() => { - setOperator((o) => !o); - const searchParams = new URLSearchParams(location.search); - searchParams.delete(OperatorQueryKey); - if (!operator) { - searchParams.append(OperatorQueryKey, operator ? 'OR' : 'AND'); - } - history.push({ - ...location, - search: searchParams.toString(), - state: prepareUserState(), - }); - }, [operator, location, history]); - - return ( -
    - { - if (e.key === 'Enter') { - toggleOperator(); - } - }} - checked={operator} - /> - {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} - -
    - ); - } \ No newline at end of file +import React, { useState, useEffect, useCallback } from "react"; +import { useHistory, useLocation } from "@docusaurus/router"; + +import {prepareUserState} from '../../../pages/tools/index'; + +import styles from "./styles.module.css"; +import clsx from "clsx"; + +export const Operator = "OR" | "AND"; + +export const OperatorQueryKey = "operator"; + +export function readOperator(search) { + return new URLSearchParams(search).get(OperatorQueryKey) ?? "OR"; +} + +export default function ShowcaseFilterToggle() { + const id = "showcase_filter_toggle"; + const location = useLocation(); + const history = useHistory(); + const [operator, setOperator] = useState(false); + + useEffect(() => { + setOperator(readOperator(location.search) === "AND"); + }, [location]); + + const toggleOperator = useCallback(() => { + setOperator((o) => !o); + const searchParams = new URLSearchParams(location.search); + searchParams.delete(OperatorQueryKey); + if (!operator) { + searchParams.append(OperatorQueryKey, operator ? "OR" : "AND"); + } + history.push({ + ...location, + search: searchParams.toString(), + state: prepareUserState(), + }); + }, [operator, location, history]); + + return ( +
    + { + if (e.key === "Enter") { + toggleOperator(); + } + }} + checked={operator} + /> + {/* eslint-disable-next-line jsx-a11y/label-has-associated-control */} + +
    + ); +} diff --git a/src/components/showcase/ShowcaseTagSelect/index.js b/src/components/showcase/ShowcaseTagSelect/index.js index b15af972a6..5dea2e58e7 100644 --- a/src/components/showcase/ShowcaseTagSelect/index.js +++ b/src/components/showcase/ShowcaseTagSelect/index.js @@ -5,20 +5,15 @@ * LICENSE file in the root directory of this source tree. */ - import React, { - useCallback, - useState, - useEffect, - forwardRef -} from 'react'; -import {useHistory, useLocation} from '@docusaurus/router'; -import {toggleListItem} from '@site/src/utils/jsUtils'; -// import {prepareUserState} from '../../../pages/showcase/index'; -import Tags from '@site/src/data/showcases'; +import React, { useCallback, useState, useEffect, forwardRef } from "react"; +import { useHistory, useLocation } from "@docusaurus/router"; +import { toggleListItem } from "@site/src/utils/jsUtils"; +import {prepareUserState} from '../../../pages/tools/index'; +import Tags from "@site/src/data/showcases"; -import styles from './styles.module.css'; +import styles from "./styles.module.css"; -const TagQueryStringKey = 'tags'; +const TagQueryStringKey = "tags"; export function readSearchTags(search) { return new URLSearchParams(search).getAll(TagQueryStringKey); @@ -32,14 +27,16 @@ function replaceSearchTags(search, newTags) { } const ShowcaseTagSelect = forwardRef( - ({id, icon, label, tag, ...rest}, ref) => { + ({ id, icon, label, tag, ...rest }, ref) => { const location = useLocation(); const history = useHistory(); const [selected, setSelected] = useState(false); + useEffect(() => { const tags = readSearchTags(location.search); setSelected(tags.includes(tag)); }, [tag, location]); + const toggleTag = useCallback(() => { const tags = readSearchTags(location.search); const newTags = toggleListItem(tags, tag); @@ -47,7 +44,7 @@ const ShowcaseTagSelect = forwardRef( history.push({ ...location, search: newSearch, - // state: prepareUserState(), + state: prepareUserState(), }); }, [tag, location, history]); return ( @@ -57,20 +54,20 @@ const ShowcaseTagSelect = forwardRef( id={id} className={styles.screenReader} onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { toggleTag(); } }} onFocus={(e) => { if (e.relatedTarget) { e.target.nextElementSibling?.dispatchEvent( - new KeyboardEvent('focus'), + new KeyboardEvent("focus") ); } }} onBlur={(e) => { e.target.nextElementSibling?.dispatchEvent( - new KeyboardEvent('blur'), + new KeyboardEvent("blur") ); }} onChange={toggleTag} @@ -83,7 +80,7 @@ const ShowcaseTagSelect = forwardRef( ); - }, + } ); -export default ShowcaseTagSelect; \ No newline at end of file +export default ShowcaseTagSelect; diff --git a/src/data/builder-tools.js b/src/data/builder-tools.js index ab0ed05301..286cd93a1b 100644 --- a/src/data/builder-tools.js +++ b/src/data/builder-tools.js @@ -16,6 +16,7 @@ import React from "react"; import { sortBy, difference } from "../utils/jsUtils"; +import { Fav } from '../svg/fav.svg' // List of available tags. The tag should be singular and the label in plural. (PLEASE DO NOT ADD NEW TAGS) export const Tags = { @@ -24,7 +25,6 @@ export const Tags = { label: "Featured", description: "Our favorite Cardano builder tools that you must absolutely check-out.", - icon: <>⭐️, color: '#e9669e', }, diff --git a/src/pages/tools/index.js b/src/pages/tools/index.js index e8ad368ca3..bcc97785b4 100644 --- a/src/pages/tools/index.js +++ b/src/pages/tools/index.js @@ -3,10 +3,9 @@ import React, { useState, useMemo, useCallback, useEffect } from "react"; import Layout from "@theme/Layout"; import ShowcaseTooltip from "@site/src/components/showcase/ShowcaseTooltip"; import ShowcaseTagSelect from "@site/src/components/showcase/ShowcaseTagSelect"; -import ShowcaseCard from "@site/src/components/showcase/ShowcaseCard"; +import ShowcaseCard from "@site/src/components/showcase/ShowcaseCard/"; import ShowcaseFilterToggle, { - Operator, - readOperator + readOperator, } from "@site/src/components/showcase/ShowcaseFilterToggle"; import clsx from "clsx"; @@ -16,8 +15,8 @@ import { SortedShowcases, Tags, TagList } from "../../data/builder-tools"; import { useHistory, useLocation } from "@docusaurus/router"; import styles from "./styles.module.css"; -import ExecutionEnvironment from '@docusaurus/ExecutionEnvironment'; - +import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment"; +import Fav from "../../svg/fav.svg" const TITLE = "Builder Tools"; const DESCRIPTION = "Tools to help you build on Cardano"; @@ -35,6 +34,16 @@ export function prepareUserState() { return undefined; } +function restoreUserState(userState) { + const { scrollTopPosition, focusedElementId } = userState ?? { + scrollTopPosition: 0, + focusedElementId: undefined, + }; + // @ts-expect-error: if focusedElementId is undefined it returns null + document.getElementById(focusedElementId)?.focus(); + window.scrollTo({ top: scrollTopPosition }); +} + const TagQueryStringKey = "tags"; function readSearchTags(search) { @@ -48,16 +57,10 @@ function replaceSearchTags(search, newTags) { return searchParams.toString(); } -function filterProjects( - projects, - selectedTags, - operator, - searchName, -) { +function filterProjects(projects, selectedTags, operator, searchName) { if (searchName) { - // eslint-disable-next-line no-param-reassign projects = projects.filter((project) => - project.title.toLowerCase().includes(searchName.toLowerCase()), + project.title.toLowerCase().includes(searchName.toLowerCase()) ); } if (selectedTags.length === 0) { @@ -67,31 +70,33 @@ function filterProjects( if (project.tags.length === 0) { return false; } - if (operator === 'AND') { + if (operator === "AND") { return selectedTags.every((tag) => project.tags.includes(tag)); } else { return selectedTags.some((tag) => project.tags.includes(tag)); } - }) + }); } - -function useFilteredUsers() { +function useFilteredProjects() { const location = useLocation(); - const [operator, setOperator] = useState('OR'); + const [operator, setOperator] = useState("OR"); + // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); const [searchName, setSearchName] = useState(null); + // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) useEffect(() => { setSelectedTags(readSearchTags(location.search)); setOperator(readOperator(location.search)); setSearchName(readSearchName(location.search)); + restoreUserState(location.state); }, [location]); return useMemo( () => filterProjects(SortedShowcases, selectedTags, operator, searchName), - [selectedTags, operator, searchName], + [selectedTags, operator, searchName] ); } @@ -102,7 +107,7 @@ function useSelectedTags() { // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); - + // Update the QS value const toggleTag = useCallback( (tag) => { @@ -131,13 +136,13 @@ function ShowcaseHeader() { function ShowcaseFilters() { return (
    -
    -
    -

    Filters

    -
    - +
    +
    +

    Filters

    -
    + +
    +
    {TagList.map((tag) => { const { label, description, color, icon } = Tags[tag]; const id = `showcase_checkbox_id_${tag}`; @@ -155,9 +160,13 @@ function ShowcaseFilters() { label={label} icon={ label === "Featured" ? ( - {icon} + + + ) : ( +
    +

    No result

    + +
    + + ); + } + return (

    @@ -190,25 +211,24 @@ function ShowcaseCards() { {filteredProjects.length > 1 ? "s" : ""}

    - {filteredProjects.length > 0 ? ( -
    - {filteredProjects.map((showcase) => ( - - ))} -
    - ) : ( -
    -

    No result

    -
    - )} +
    + +
    +
      + {filteredProjects.map((showcase) => ( + + ))} +
    ); } -const SearchNameQueryKey = 'name'; +const SearchNameQueryKey = "name"; function readSearchName(search) { return new URLSearchParams(search).get(SearchNameQueryKey); @@ -218,14 +238,16 @@ function SearchBar() { const history = useHistory(); const location = useLocation(); const [value, setValue] = useState(null); + useEffect(() => { setValue(readSearchName(location.search)); }, [location]); + return (
    { setValue(e.currentTarget.value); @@ -237,11 +259,11 @@ function SearchBar() { history.push({ ...location, search: newSearch.toString(), - state: prepareUserState(), + // state: prepareUserState(), }); setTimeout(() => { - document.getElementById('searchbar')?.focus(); - }, 0); + document.getElementById("searchbar")?.focus(); + }, 1); }} />
    @@ -250,21 +272,15 @@ function SearchBar() { function Showcase() { const { selectedTags, toggleTag } = useSelectedTags(); - const filteredProjects = useFilteredUsers(); + const filteredProjects = useFilteredProjects(); return ( -
    - - - -
    + +
    ); } -export default Showcase; \ No newline at end of file +export default Showcase; diff --git a/src/pages/tools/styles.module.css b/src/pages/tools/styles.module.css index b33a96df59..18f2bf6a58 100644 --- a/src/pages/tools/styles.module.css +++ b/src/pages/tools/styles.module.css @@ -44,6 +44,7 @@ .filterCheckbox { + margin-top: 20px; justify-content: space-between; } @@ -89,9 +90,6 @@ .searchContainer { margin-left: auto; - display: block; - text-align: right; - padding: 0 var(--ifm-spacing-horizontal); } .searchContainer input { diff --git a/src/svg/fav.svg b/src/svg/fav.svg new file mode 100644 index 0000000000..3799fc5748 --- /dev/null +++ b/src/svg/fav.svg @@ -0,0 +1,8 @@ + + + + + + From 800c5290234e806f47fe7c8349ccc22f3c455adf Mon Sep 17 00:00:00 2001 From: fill-the-fill Date: Wed, 2 Feb 2022 13:18:01 +0200 Subject: [PATCH 4/7] Remove old design --- .../ShowcaseCard/components/Button.js | 14 -------- .../ShowcaseCard/components/ShowcaseBody.js | 24 ------------- .../ShowcaseCard/components/ShowcaseFooter.js | 18 ---------- .../ShowcaseCard/components/TagIcons.js | 34 ------------------- 4 files changed, 90 deletions(-) delete mode 100644 src/components/showcase/ShowcaseCard/components/Button.js delete mode 100644 src/components/showcase/ShowcaseCard/components/ShowcaseBody.js delete mode 100644 src/components/showcase/ShowcaseCard/components/ShowcaseFooter.js delete mode 100644 src/components/showcase/ShowcaseCard/components/TagIcons.js diff --git a/src/components/showcase/ShowcaseCard/components/Button.js b/src/components/showcase/ShowcaseCard/components/Button.js deleted file mode 100644 index 4f83994763..0000000000 --- a/src/components/showcase/ShowcaseCard/components/Button.js +++ /dev/null @@ -1,14 +0,0 @@ -import React from 'react' - -const Button = ({text,hrefValue}) => ( - - {text} - -) - -export default Button \ No newline at end of file diff --git a/src/components/showcase/ShowcaseCard/components/ShowcaseBody.js b/src/components/showcase/ShowcaseCard/components/ShowcaseBody.js deleted file mode 100644 index c51fc503c8..0000000000 --- a/src/components/showcase/ShowcaseCard/components/ShowcaseBody.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react' -import styles from "../styles.module.css"; -import ShowcaseCardTagIcons from './TagIcons'; - - -const ShowcaseBody = ({tags,title,description}) => ( -
    -
    -
    -
    -
    -

    {title}

    -
    -
    - -
    -
    - {description} -
    -
    -
    -) - -export default ShowcaseBody; \ No newline at end of file diff --git a/src/components/showcase/ShowcaseCard/components/ShowcaseFooter.js b/src/components/showcase/ShowcaseCard/components/ShowcaseFooter.js deleted file mode 100644 index 9b9fd25e5e..0000000000 --- a/src/components/showcase/ShowcaseCard/components/ShowcaseFooter.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react' -import Button from './Button'; - -const ShowcaseFooter = ({getstarted,website,source}) => { - return ( -
    -
    - {getstarted &&
    -
    - ) -} - - - -export default ShowcaseFooter; \ No newline at end of file diff --git a/src/components/showcase/ShowcaseCard/components/TagIcons.js b/src/components/showcase/ShowcaseCard/components/TagIcons.js deleted file mode 100644 index 09e299b544..0000000000 --- a/src/components/showcase/ShowcaseCard/components/TagIcons.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react' -import { Tags, TagList } from "../../../../data/showcases"; -import { sortBy } from "../../../../utils/jsUtils"; -import styles from "../styles.module.css"; - -function TagIcon({ label, description, icon }) { - return ( - - {icon} - - ); -} - -function ShowcaseCardTagIcons({ tags }) { - const tagObjects = tags - .map((tag) => ({ tag, ...Tags[tag] })) - .filter((tagObject) => !!tagObject.icon); - - // Keep same order of icons for all tags - const tagObjectsSorted = sortBy(tagObjects, (tagObject) => - TagList.indexOf(tagObject.tag) - ); - - return tagObjectsSorted.map((tagObject, index) => ( - - )); -} - -export default ShowcaseCardTagIcons; \ No newline at end of file From e64d25bb05702a5c5527e6b58964afc45425cf6e Mon Sep 17 00:00:00 2001 From: fill-the-fill Date: Wed, 2 Feb 2022 15:36:01 +0200 Subject: [PATCH 5/7] Modify showcase design --- src/components/showcase/ShowcaseCard/index.js | 76 ++--- src/data/showcases.js | 14 +- src/pages/showcase/index.js | 269 ++++++++++++------ src/pages/showcase/styles.module.css | 110 +++++++ 4 files changed, 353 insertions(+), 116 deletions(-) diff --git a/src/components/showcase/ShowcaseCard/index.js b/src/components/showcase/ShowcaseCard/index.js index a9a3dc6ff2..8a27208a61 100644 --- a/src/components/showcase/ShowcaseCard/index.js +++ b/src/components/showcase/ShowcaseCard/index.js @@ -5,29 +5,27 @@ * LICENSE file in the root directory of this source tree. */ -import React, {memo, forwardRef} from 'react'; -import clsx from 'clsx'; -import Image from '@theme/IdealImage'; -import Link from '@docusaurus/Link'; +import React, { memo, forwardRef } from "react"; +import clsx from "clsx"; +import Image from "@theme/IdealImage"; +import Link from "@docusaurus/Link"; +import styles from "./styles.module.css"; +import Tooltip from "../ShowcaseTooltip/index"; +import { Tags as ToolsTags} from "../../../data/builder-tools"; +import { Tags as ShowcaseTags} from "../../../data/showcases"; +import Fav from "../../../svg/fav.svg"; -import styles from './styles.module.css'; -import Tooltip from '../ShowcaseTooltip/index'; -import { - Tags, -} from '../../../data/builder-tools'; -import Fav from '../../../svg/fav.svg' - -const TagComp = forwardRef( - ({label, color, description}, ref) => ( +const TagComp = forwardRef(({ label, color, description }, ref) =>
  • {label.toLowerCase()} - +
  • - ), ); -function ShowcaseCardTag({tags}) { - const tagObjects = tags.map((tag) => ({tag, ...Tags[tag]})); +function ShowcaseCardTag({ tags }) { + + const selectedTags = window.location.href.includes('tools') ? ToolsTags : ShowcaseTags + const tagObjects = tags.map((tag) => ({ tag, ...selectedTags[tag] })); return ( <> @@ -39,7 +37,8 @@ function ShowcaseCardTag({tags}) { key={index} text={tagObject.description} anchorEl="#__docusaurus" - id={id}> + id={id} + > ); @@ -48,37 +47,46 @@ function ShowcaseCardTag({tags}) { ); } -const ShowcaseCard = memo((user) => ( -
  • -
    - {user.showcase.title} +const ShowcaseCard = memo((card) => ( +
  • +
    + {card.showcase.title}

    - - {user.showcase.title} - + {card.showcase.title}

    - {user.showcase.tags.includes('featured') && ( + {card.showcase.tags.includes("featured") && ( )} - {user.showcase.website && ( + {card.showcase.website && ( + + Website + + )} + {card.showcase.source && ( + > Source )}
    -

    {user.showcase.description}

    +

    {card.showcase.description}

    -
      - +
        +
      )); diff --git a/src/data/showcases.js b/src/data/showcases.js index 8db6d2c325..fd60af1142 100644 --- a/src/data/showcases.js +++ b/src/data/showcases.js @@ -36,7 +36,7 @@ export const Tags = { label: "Featured", description: "Our favorite Cardano projects that you must absolutely check-out.", - icon: <>⭐️, + color: '#e9669e', }, // Analytics @@ -44,6 +44,7 @@ export const Tags = { label: "Analytics", description: "Tools that provide special insights related to Cardano.", icon: null, + color: '#39ca30', }, // Cardano Block Explorers @@ -52,6 +53,7 @@ export const Tags = { description: "Block explorers are browsers for the Cardano blockchain. They can display the contents of individual blocks and transactions.", icon: null, + color: '#293133', }, // Educational @@ -60,6 +62,7 @@ export const Tags = { description: "Educational projects that will help you onboarding to Cardano.", icon: null, + color: '#a44fb7', }, // Games @@ -67,6 +70,7 @@ export const Tags = { label: "Games", description: "Games on the Cardano blockchain.", icon: null, + color: '#127f82' }, // Gateways @@ -74,6 +78,7 @@ export const Tags = { label: "Gateways", description: "Payment Gateway Providers.", icon: null, + color: '#fe6829', }, // For open-source sites, a link to the source code is required @@ -81,6 +86,7 @@ export const Tags = { label: "Open-Source", description: "Open-Source sites can be useful for inspiration.", icon: null, + color: '#8c2f00', }, // Pool Tools @@ -89,6 +95,7 @@ export const Tags = { description: "Pool tools provide delegates with the necessary tools to find a good pool.", icon: null, + color: '#4267b2', }, // Meta data projects @@ -96,6 +103,7 @@ export const Tags = { label: "Metadata", description: "Transaction metadata", icon: null, + color: '#14cfc3', }, // Native tokens related projects @@ -103,6 +111,7 @@ export const Tags = { label: "Native Tokens", description: "Native Tokens", icon: null, + color: '#E63244' }, // NFT projects @@ -110,6 +119,7 @@ export const Tags = { label: "NFT", description: "Non-Fungible Token (NFT)", icon: null, + color: '#F5D033' }, // Wallets @@ -118,6 +128,7 @@ export const Tags = { description: "Cardano wallets store the public and/or private keys to access and manage your funds.", icon: null, + color: '#7BC8A6' }, // DEX @@ -125,6 +136,7 @@ export const Tags = { label: "DEX", description: "DEX allows direct peer-to-peer cryptocurrency transactions to take place online securely.", icon: null, + color: '#1B32F0' }, }; diff --git a/src/pages/showcase/index.js b/src/pages/showcase/index.js index 5aa3b5f57b..e44a1dd4a6 100644 --- a/src/pages/showcase/index.js +++ b/src/pages/showcase/index.js @@ -1,42 +1,47 @@ import React, { useState, useMemo, useCallback, useEffect } from "react"; import Layout from "@theme/Layout"; -import ShowcaseCheckbox from "@site/src/components/showcase/ShowcaseCheckbox"; -import ShowcaseCard from "@site/src/components/showcase/ShowcaseCard"; +import ShowcaseTooltip from "@site/src/components/showcase/ShowcaseTooltip"; +import ShowcaseTagSelect from "@site/src/components/showcase/ShowcaseTagSelect"; +import ShowcaseCard from "@site/src/components/showcase/ShowcaseCard/"; +import ShowcaseFilterToggle, { + readOperator, +} from "@site/src/components/showcase/ShowcaseFilterToggle"; import clsx from "clsx"; import PortalHero from "../portalhero"; import { toggleListItem } from "../../utils/jsUtils"; import { SortedShowcases, Tags, TagList } from "../../data/showcases"; import { useHistory, useLocation } from "@docusaurus/router"; +import styles from "./styles.module.css"; + +import ExecutionEnvironment from "@docusaurus/ExecutionEnvironment"; +import Fav from "../../svg/fav.svg" const TITLE = "Showcase"; const DESCRIPTION = "See the awesome projects people are building with Cardano"; const CTA = "Add your project"; const FILENAME = "showcases.js"; -function filterProjects(projects, selectedTags, operator) { - if (selectedTags.length === 0) { - return projects; +export function prepareUserState() { + if (ExecutionEnvironment.canUseDOM) { + return { + scrollTopPosition: window.scrollY, + focusedElementId: document.activeElement?.id, + }; } - return projects.filter((showcase) => { - if (showcase.tags.length === 0) { - return false; - } - if (operator === "AND") { // no operator selection for the time being, we use OR - return selectedTags.every((tag) => showcase.tags.includes(tag)); - } else { - return selectedTags.some((tag) => showcase.tags.includes(tag)); - } - }); + + return undefined; } -function useFilteredProjects(projects, selectedTags, operator) { - return useMemo(() => filterProjects(projects, selectedTags, operator), [ - projects, - selectedTags, - operator, - ]); +function restoreUserState(userState) { + const { scrollTopPosition, focusedElementId } = userState ?? { + scrollTopPosition: 0, + focusedElementId: undefined, + }; + // @ts-expect-error: if focusedElementId is undefined it returns null + document.getElementById(focusedElementId)?.focus(); + window.scrollTo({ top: scrollTopPosition }); } const TagQueryStringKey = "tags"; @@ -52,19 +57,56 @@ function replaceSearchTags(search, newTags) { return searchParams.toString(); } -function useSelectedTags() { - // The search query-string is the source of truth! +function filterProjects(projects, selectedTags, operator, searchName) { + if (searchName) { + projects = projects.filter((project) => + project.title.toLowerCase().includes(searchName.toLowerCase()) + ); + } + if (selectedTags.length === 0) { + return projects; + } + return projects.filter((project) => { + if (project.tags.length === 0) { + return false; + } + if (operator === "AND") { + return selectedTags.every((tag) => project.tags.includes(tag)); + } else { + return selectedTags.some((tag) => project.tags.includes(tag)); + } + }); +} + +function useFilteredProjects() { const location = useLocation(); - const { push } = useHistory(); + const [operator, setOperator] = useState("OR"); // On SSR / first mount (hydration) no tag is selected const [selectedTags, setSelectedTags] = useState([]); + const [searchName, setSearchName] = useState(null); // Sync tags from QS to state (delayed on purpose to avoid SSR/Client hydration mismatch) useEffect(() => { - const tags = readSearchTags(location.search); - setSelectedTags(tags); - }, [location, setSelectedTags]); + setSelectedTags(readSearchTags(location.search)); + setOperator(readOperator(location.search)); + setSearchName(readSearchName(location.search)); + restoreUserState(location.state); + }, [location]); + + return useMemo( + () => filterProjects(SortedShowcases, selectedTags, operator, searchName), + [selectedTags, operator, searchName] + ); +} + +function useSelectedTags() { + // The search query-string is the source of truth! + const location = useLocation(); + const { push } = useHistory(); + + // On SSR / first mount (hydration) no tag is selected + const [selectedTags, setSelectedTags] = useState([]); // Update the QS value const toggleTag = useCallback( @@ -73,7 +115,6 @@ function useSelectedTags() { const newTags = toggleListItem(tags, tag); const newSearch = replaceSearchTags(location.search, newTags); push({ ...location, search: newSearch }); - // no need to call setSelectedTags, useEffect will do the sync }, [location, push] ); @@ -92,32 +133,56 @@ function ShowcaseHeader() { ); } -function ShowcaseFilters({ selectedTags, toggleTag, operator, setOperator }) { +function ShowcaseFilters() { return (
      -
      +
      +
      +

      Filters

      +
      + +
      +
      {TagList.map((tag) => { - const { label, description, icon } = Tags[tag]; + const { label, description, color, icon } = Tags[tag]; + const id = `showcase_checkbox_id_${tag}`; return ( -
      - - {icon} {label} - - ) : ( - label - ) - } - onChange={() => toggleTag(tag)} - checked={selectedTags.includes(tag)} - /> -
      + <> +
      + + + + + ) : ( + + ) + } + /> + +
      + ); })}
      @@ -125,53 +190,95 @@ function ShowcaseFilters({ selectedTags, toggleTag, operator, setOperator }) { ); } -function ShowcaseCards({ filteredProjects }) { +function ShowcaseCards() { + const filteredProjects = useFilteredProjects(); + + if (filteredProjects.length === 0) { + return ( +
      +
      +

      No result

      + +
      +
      + ); + } + return (

      {filteredProjects.length} project - {filteredProjects.length > 1 ? "s" : ""} + {filteredProjects.length > 1 ? "s" : ""}

      - {filteredProjects.length > 0 ? ( -
      - {filteredProjects.map((showcase) => ( - - ))} -
      - ) : ( -
      -

      No result

      -
      - )} +
      + +
      +
        + {filteredProjects.map((showcase) => ( + + ))} +
      ); } +const SearchNameQueryKey = "name"; + +function readSearchName(search) { + return new URLSearchParams(search).get(SearchNameQueryKey); +} + +function SearchBar() { + const history = useHistory(); + const location = useLocation(); + const [value, setValue] = useState(null); + + useEffect(() => { + setValue(readSearchName(location.search)); + }, [location]); + + return ( +
      + { + setValue(e.currentTarget.value); + const newSearch = new URLSearchParams(location.search); + newSearch.delete(SearchNameQueryKey); + if (e.currentTarget.value) { + newSearch.set(SearchNameQueryKey, e.currentTarget.value); + } + history.push({ + ...location, + search: newSearch.toString(), + // state: prepareUserState(), + }); + setTimeout(() => { + document.getElementById("searchbar")?.focus(); + }, 1); + }} + /> +
      + ); +} function Showcase() { const { selectedTags, toggleTag } = useSelectedTags(); - const [operator, setOperator] = useState("OR"); - const filteredProjects = useFilteredProjects( - SortedShowcases, - selectedTags, - operator - ); + const filteredProjects = useFilteredProjects(); + return ( -
      - - -
      + +
      ); } diff --git a/src/pages/showcase/styles.module.css b/src/pages/showcase/styles.module.css index 04371e4834..18f2bf6a58 100644 --- a/src/pages/showcase/styles.module.css +++ b/src/pages/showcase/styles.module.css @@ -5,6 +5,19 @@ * LICENSE file in the root directory of this source tree. */ + .checkboxListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.checkboxListItem:last-child { + margin-right: 0; +} + .heroBanner { padding: 4rem 0; text-align: center; @@ -28,3 +41,100 @@ max-height: 175px; overflow: hidden; } + + +.filterCheckbox { + margin-top: 20px; + justify-content: space-between; +} + +.filterCheckbox, +.checkboxList { + display: flex; + align-items: center; +} + +.filterCheckbox > div:first-child { + display: flex; + flex: 1 1 auto; + align-items: center; +} + +.filterCheckbox > div > * { + margin-bottom: 0; + margin-right: 8px; +} + +.checkboxList { + flex-wrap: wrap; +} + +.checkboxList, +.showcaseList { + padding: 0; + list-style: none; +} + +.checkboxListItem { + user-select: none; + white-space: nowrap; + height: 32px; + font-size: 0.8rem; + margin-top: 0.5rem; + margin-right: 0.5rem; +} + +.checkboxListItem:last-child { + margin-right: 0; +} + +.searchContainer { + margin-left: auto; +} + +.searchContainer input { + height: 30px; + border-radius: 15px; + padding: 10px; + border: 1px solid gray; +} + +.showcaseList { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 24px; +} + +.showcaseFavorite { + padding-top: 2rem; + padding-bottom: 2rem; + background-color: var(--site-color-favorite-background); +} + +.showcaseFavoriteHeader { + display: flex; + align-items: center; +} + +.showcaseFavoriteHeader > h2 { + margin-bottom: 0; +} + +.showcaseFavoriteHeader > svg { + width: 30px; + height: 30px; +} + +.svgIconFavoriteXs, +.svgIconFavorite { + color: var(--site-color-svg-icon-favorite); +} + +.svgIconFavoriteXs { + margin-left: 0.625rem; + font-size: 1rem; +} + +.svgIconFavorite { + margin-left: 1rem; +} \ No newline at end of file From 8cf9c66dd2d6d39628452ca6a7dbcab6b7e04ff1 Mon Sep 17 00:00:00 2001 From: fill-the-fill Date: Wed, 2 Feb 2022 16:08:13 +0200 Subject: [PATCH 6/7] Fix react-popper yarn issue a --- package.json | 2 ++ yarn.lock | 22 +++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 7518f94fb9..eb64cc30d4 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,12 @@ "@docusaurus/preset-classic": "^2.0.0-beta.14", "@docusaurus/theme-search-algolia": "^2.0.0-beta.14", "@mdx-js/react": "^1.6.22", + "@popperjs/core": "^2.11.2", "clsx": "^1.1.1", "node-fetch": "^2.6.1", "react": "^17.0.2", "react-dom": "^17.0.2", + "react-popper": "^2.2.5", "ts-node": "^10.4.0", "typescript": "^4.5.3" }, diff --git a/yarn.lock b/yarn.lock index 31b4da88e1..bc71ae12c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2348,6 +2348,11 @@ resolved "https://registry.yarnpkg.com/@polka/url/-/url-1.0.0-next.21.tgz#5de5a2385a35309427f6011992b544514d559aa1" integrity sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g== +"@popperjs/core@^2.11.2": + version "2.11.2" + resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.2.tgz#830beaec4b4091a9e9398ac50f865ddea52186b9" + integrity sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA== + "@sideway/address@^4.1.3": version "4.1.3" resolved "https://registry.yarnpkg.com/@sideway/address/-/address-4.1.3.tgz#d93cce5d45c5daec92ad76db492cc2ee3c64ab27" @@ -7259,7 +7264,7 @@ react-error-overlay@^6.0.9: resolved "https://registry.yarnpkg.com/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" integrity sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew== -react-fast-compare@^3.1.1: +react-fast-compare@^3.0.1, react-fast-compare@^3.1.1: version "3.2.0" resolved "https://registry.yarnpkg.com/react-fast-compare/-/react-fast-compare-3.2.0.tgz#641a9da81b6a6320f270e89724fb45a0b39e43bb" integrity sha512-rtGImPZ0YyLrscKI9xTpV8psd6I8VAtjKCzQDlzyDvqJA8XOW78TXYQwNRNd8g8JZnDu8q9Fu/1v4HPAVwVdHA== @@ -7306,6 +7311,14 @@ react-loadable-ssr-addon-v5-slorber@^1.0.1: dependencies: "@babel/runtime" "^7.10.3" +react-popper@^2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/react-popper/-/react-popper-2.2.5.tgz#1214ef3cec86330a171671a4fbcbeeb65ee58e96" + integrity sha512-kxGkS80eQGtLl18+uig1UIf9MKixFSyPxglsgLBxlYnyDf65BiY9B3nZSc6C9XUNDgStROB0fMQlTEz1KxGddw== + dependencies: + react-fast-compare "^3.0.1" + warning "^4.0.2" + react-router-config@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/react-router-config/-/react-router-config-5.1.1.tgz#0f4263d1a80c6b2dc7b9c1902c9526478194a988" @@ -8727,6 +8740,13 @@ wait-on@^6.0.0: minimist "^1.2.5" rxjs "^7.1.0" +warning@^4.0.2: + version "4.0.3" + resolved "https://registry.yarnpkg.com/warning/-/warning-4.0.3.tgz#16e9e077eb8a86d6af7d64aa1e05fd85b4678ca3" + integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w== + dependencies: + loose-envify "^1.0.0" + watchpack@^2.3.1: version "2.3.1" resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.3.1.tgz#4200d9447b401156eeca7767ee610f8809bc9d25" From 6c83416d473fa5cae35c27d4cae411ee2239b2a5 Mon Sep 17 00:00:00 2001 From: fill-the-fill Date: Wed, 2 Feb 2022 16:09:51 +0200 Subject: [PATCH 7/7] Fix uselocation tags issue --- src/components/showcase/ShowcaseCard/index.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/showcase/ShowcaseCard/index.js b/src/components/showcase/ShowcaseCard/index.js index 8a27208a61..8602c2c7cf 100644 --- a/src/components/showcase/ShowcaseCard/index.js +++ b/src/components/showcase/ShowcaseCard/index.js @@ -11,6 +11,7 @@ import Image from "@theme/IdealImage"; import Link from "@docusaurus/Link"; import styles from "./styles.module.css"; import Tooltip from "../ShowcaseTooltip/index"; +import { useLocation } from "@docusaurus/router"; import { Tags as ToolsTags} from "../../../data/builder-tools"; import { Tags as ShowcaseTags} from "../../../data/showcases"; import Fav from "../../../svg/fav.svg"; @@ -23,8 +24,9 @@ const TagComp = forwardRef(({ label, color, description }, ref) => ); function ShowcaseCardTag({ tags }) { - - const selectedTags = window.location.href.includes('tools') ? ToolsTags : ShowcaseTags + + const location = useLocation() + const selectedTags = location.pathname.includes('tools') ? ToolsTags : ShowcaseTags const tagObjects = tags.map((tag) => ({ tag, ...selectedTags[tag] })); return (