From 908fb34c3f53d4c81449c01fa411b69e6f976b6f Mon Sep 17 00:00:00 2001 From: Ewan Lyon Date: Wed, 24 Jul 2024 21:24:58 +1000 Subject: [PATCH] add showcase page --- packages/speedrun-theme/src/css/custom.css | 8 +- website/docusaurus.config.ts | 5 + .../FavouriteIcon/favourite-icon.tsx | 34 ++ .../FavouriteIcon/styles.module.css | 54 ++++ .../_components/ShowcaseCard/index.tsx | 81 +++++ .../ShowcaseCard/styles.module.css | 99 ++++++ .../ShowcaseFilterToggle/index.tsx | 85 +++++ .../ShowcaseFilterToggle/styles.module.css | 57 ++++ .../_components/ShowcaseTagSelect/index.tsx | 93 ++++++ .../ShowcaseTagSelect/styles.module.css | 38 +++ .../_components/ShowcaseTooltip/index.tsx | 131 ++++++++ .../ShowcaseTooltip/styles.module.css | 45 +++ website/src/pages/showcase/data.tsx | 106 +++++++ .../pages/showcase/images/heavenly-bodies.png | Bin 0 -> 27571 bytes website/src/pages/showcase/index.tsx | 291 ++++++++++++++++++ website/src/pages/showcase/styles.module.css | 95 ++++++ 16 files changed, 1220 insertions(+), 2 deletions(-) create mode 100644 website/src/pages/showcase/_components/FavouriteIcon/favourite-icon.tsx create mode 100644 website/src/pages/showcase/_components/FavouriteIcon/styles.module.css create mode 100644 website/src/pages/showcase/_components/ShowcaseCard/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseCard/styles.module.css create mode 100644 website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseFilterToggle/styles.module.css create mode 100644 website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseTagSelect/styles.module.css create mode 100644 website/src/pages/showcase/_components/ShowcaseTooltip/index.tsx create mode 100644 website/src/pages/showcase/_components/ShowcaseTooltip/styles.module.css create mode 100644 website/src/pages/showcase/data.tsx create mode 100644 website/src/pages/showcase/images/heavenly-bodies.png create mode 100644 website/src/pages/showcase/index.tsx create mode 100644 website/src/pages/showcase/styles.module.css diff --git a/packages/speedrun-theme/src/css/custom.css b/packages/speedrun-theme/src/css/custom.css index 3af995a..281aa16 100644 --- a/packages/speedrun-theme/src/css/custom.css +++ b/packages/speedrun-theme/src/css/custom.css @@ -33,14 +33,17 @@ --ifm-table-border-width: 0px; --speeddocs-primary: #030c20; + --speeddocs-primary-rgb-values: 3, 12, 32; --speeddocs-secondary: #14213d; + --speeddocs-secondary-rgb-values: 20, 33, 61; --speeddocs-accent: #fca311; + --speeddocs-accent-rgb-values: 252, 163, 17; --logo-filter: invert(1); - --ifm-navbar-background-color: var(--speeddocs-secondary); + --ifm-navbar-background-color: rgba(var(--speeddocs-secondary-rgb-values), 0.5); --ifm-background-color: var(--speeddocs-primary) !important; --ifm-background-surface-color: var(--speeddocs-secondary) !important; - --ifm-background-surface-color-split: 3, 12, 32; /* Used for the gradients on top of images */ + /* --ifm-background-surface-color-split: 3, 12, 32; Used for the gradients on top of images */ --ifm-navbar-search-input-background-color: var(--speeddocs-primary); --ifm-navbar-search-input-icon: url('data:image/svg+xml;utf8,'); --ifm-footer-background-color: var(--speeddocs-secondary); @@ -89,6 +92,7 @@ .navbar { border-bottom: thin solid var(--speeddocs-accent); + backdrop-filter: saturate(180%) blur(10px); } footer { diff --git a/website/docusaurus.config.ts b/website/docusaurus.config.ts index acd106c..1e20f4d 100644 --- a/website/docusaurus.config.ts +++ b/website/docusaurus.config.ts @@ -49,6 +49,11 @@ const config: Config = { position: "left", label: "Resources", }, + { + position: "left", + label: "Showcase", + to: "showcase" + }, { href: `https://github.com/${speedrunDocsConfig.github.username}/${speedrunDocsConfig.github.repository}`, position: "right", diff --git a/website/src/pages/showcase/_components/FavouriteIcon/favourite-icon.tsx b/website/src/pages/showcase/_components/FavouriteIcon/favourite-icon.tsx new file mode 100644 index 0000000..f3aa6ea --- /dev/null +++ b/website/src/pages/showcase/_components/FavouriteIcon/favourite-icon.tsx @@ -0,0 +1,34 @@ +/** + * 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, { type ReactNode, type ComponentProps } from "react"; +import clsx from "clsx"; +import styles from "./styles.module.css"; + +export type SvgIconProps = ComponentProps<"svg"> & { + viewBox?: string; + size?: "inherit" | "small" | "medium" | "large"; + color?: "inherit" | "primary" | "secondary" | "success" | "error" | "warning"; + svgClass?: string; // Class attribute on the child + colorAttr?: string; // Applies a color attribute to the SVG element. +}; + +export default function FavouriteIcon(props: SvgIconProps): JSX.Element { + const { svgClass, colorAttr, children, color = "inherit", size = "medium", viewBox = "0 0 24 24", ...rest } = props; + + return ( + + + + ); +} diff --git a/website/src/pages/showcase/_components/FavouriteIcon/styles.module.css b/website/src/pages/showcase/_components/FavouriteIcon/styles.module.css new file mode 100644 index 0000000..cf39090 --- /dev/null +++ b/website/src/pages/showcase/_components/FavouriteIcon/styles.module.css @@ -0,0 +1,54 @@ +/** + * 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. + */ + +.svgIcon { + user-select: none; + width: 1em; + height: 1em; + display: inline-block; + fill: currentColor; + flex-shrink: 0; + color: inherit; +} + +/* font-size */ +.small { + font-size: 1.25rem; +} + +.medium { + font-size: 1.5rem; +} + +.large { + font-size: 2.185rem; +} + +/* colours */ +.primary { + color: var(--ifm-color-primary); +} + +.secondary { + color: var(--ifm-color-secondary); +} + +.success { + color: var(--ifm-color-success); +} + +.error { + color: var(--ifm-color-error); +} + +.warning { + color: var(--ifm-color-warning); +} + +.inherit { + color: inherit; +} diff --git a/website/src/pages/showcase/_components/ShowcaseCard/index.tsx b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx new file mode 100644 index 0000000..a522ffc --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseCard/index.tsx @@ -0,0 +1,81 @@ +/** + * 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 from "react"; +import clsx from "clsx"; +import Link from "@docusaurus/Link"; +import Translate from "@docusaurus/Translate"; +// import Image from '@theme/IdealImage'; +import FavouriteIcon from "../FavouriteIcon/favourite-icon"; +import { Tags, TagList, type TagType, type Website, type Tag } from "../../data"; +import Heading from "@theme/Heading"; +import Tooltip from "../ShowcaseTooltip"; +import styles from "./styles.module.css"; + +const TagComp = React.forwardRef(({ label, color, description }, ref) => ( +
  • + {label.toLowerCase()} + +
  • +)); + +function ShowcaseCardTag({ tags }: { tags: TagType[] }) { + const tagObjects = tags.map((tag) => ({ tag, ...Tags[tag] })); + + // Keep same order for all tags + const tagObjectsSorted = tagObjects.sort((a, b) => TagList.indexOf(a.tag) - TagList.indexOf(b.tag)); + + return ( + <> + {tagObjectsSorted.map((tagObject, index) => { + const id = `showcase_card_tag_${tagObject.tag}`; + + return ( + + + + ); + })} + + ); +} + +function getCardImage(user: Website): string { + return user.preview ?? `https://slorber-api-screenshot.netlify.app/${encodeURIComponent(user.website)}/showcase`; +} + +function ShowcaseCard({ user }: { user: Website }) { + const image = getCardImage(user); + return ( +
  • +
    + {user.title} +
    +
    +
    + + + {user.title} + + + {user.tags.includes("favourite") && } + {user.source && ( + + source + + )} +
    +

    {user.description}

    +
    +
      + +
    +
  • + ); +} + +export default React.memo(ShowcaseCard); diff --git a/website/src/pages/showcase/_components/ShowcaseCard/styles.module.css b/website/src/pages/showcase/_components/ShowcaseCard/styles.module.css new file mode 100644 index 0000000..4d45056 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseCard/styles.module.css @@ -0,0 +1,99 @@ +/** + * 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. + */ + +.showcaseCardImage { + 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; +} + +.showcaseCardSrcBtn:focus-visible { + background-color: var(--ifm-color-secondary-dark); +} + +[data-theme='dark'] .showcaseCardSrcBtn { + background-color: var(--ifm-color-emphasis-200) !important; + color: inherit; +} + +[data-theme='dark'] .showcaseCardSrcBtn:hover { + background-color: var(--ifm-color-emphasis-300) !important; +} + +.showcaseCardBody { + font-size: smaller; + line-height: 1.66; +} + +.cardFooter { + display: flex; + flex-wrap: wrap; +} + +.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; +} + +.tag .textLabel { + margin-left: 8px; +} + +.tag .colorLabel { + width: 7px; + height: 7px; + border-radius: 50%; + margin-left: 6px; + margin-right: 6px; +} diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx new file mode 100644 index 0000000..78c1126 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseFilterToggle/index.tsx @@ -0,0 +1,85 @@ +/** + * 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 clsx from 'clsx'; +import {useHistory, useLocation} from '@docusaurus/router'; + +import {prepareUserState} from '../../index'; + +import styles from './styles.module.css'; + +export type Operator = 'OR' | 'AND'; + +export const OperatorQueryKey = 'operator'; + +export function readOperator(search: string): Operator { + return (new URLSearchParams(search).get(OperatorQueryKey) ?? + 'OR') as Operator; +} + +export default function ShowcaseFilterToggle(): JSX.Element { + 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, 'AND'); + } + history.push({ + ...location, + search: searchParams.toString(), + state: prepareUserState(), + }); + }, [operator, location, history]); + + const ClearTag = () => { + history.push({ + ...location, + search: '', + state: prepareUserState(), + }); + }; + + return ( +
    + { + if (e.key === 'Enter') { + toggleOperator(); + } + }} + checked={operator} + /> + + + +
    + ); +} diff --git a/website/src/pages/showcase/_components/ShowcaseFilterToggle/styles.module.css b/website/src/pages/showcase/_components/ShowcaseFilterToggle/styles.module.css new file mode 100644 index 0000000..4fde44d --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseFilterToggle/styles.module.css @@ -0,0 +1,57 @@ +/** + * 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 { + --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))); +} diff --git a/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx new file mode 100644 index 0000000..88101b6 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseTagSelect/index.tsx @@ -0,0 +1,93 @@ +/** + * 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, type ComponentProps, type ReactNode, type ReactElement } from "react"; +import { useHistory, useLocation } from "@docusaurus/router"; +import type { TagType } from "../../data"; + +import { prepareUserState } from "../../index"; +import styles from "./styles.module.css"; + +interface Props extends ComponentProps<"input"> { + icon: ReactElement>; + label: ReactNode; + tag: TagType; +} + +const TagQueryStringKey = "tags"; + +export function readSearchTags(search: string): TagType[] { + return new URLSearchParams(search).getAll(TagQueryStringKey) as TagType[]; +} + +function replaceSearchTags(search: string, newTags: TagType[]) { + const searchParams = new URLSearchParams(search); + searchParams.delete(TagQueryStringKey); + newTags.forEach((tag) => searchParams.append(TagQueryStringKey, tag)); + return searchParams.toString(); +} + +function toggleListItem(list: T[], item: T): T[] { + const itemIndex = list.indexOf(item); + if (itemIndex === -1) { + return list.concat(item); + } + const newList = [...list]; + newList.splice(itemIndex, 1); + return newList; +} + +function ShowcaseTagSelect({ id, icon, label, tag, ...rest }: Props, ref: React.ForwardedRef) { + 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 React.forwardRef(ShowcaseTagSelect); diff --git a/website/src/pages/showcase/_components/ShowcaseTagSelect/styles.module.css b/website/src/pages/showcase/_components/ShowcaseTagSelect/styles.module.css new file mode 100644 index 0000000..7367147 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseTagSelect/styles.module.css @@ -0,0 +1,38 @@ +/** + * 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); +} + +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); +} diff --git a/website/src/pages/showcase/_components/ShowcaseTooltip/index.tsx b/website/src/pages/showcase/_components/ShowcaseTooltip/index.tsx new file mode 100644 index 0000000..6ea7df5 --- /dev/null +++ b/website/src/pages/showcase/_components/ShowcaseTooltip/index.tsx @@ -0,0 +1,131 @@ +/** + * 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"; + +interface Props { + anchorEl?: HTMLElement | string; + id: string; + text: string; + children: React.ReactElement; +} + +export default function Tooltip({ children, id, anchorEl, text }: Props): JSX.Element { + 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); + }, 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]); + + return ( + <> + {React.cloneElement(children, { + ref: setReferenceElement, + "aria-describedby": open ? tooltipId : undefined, + })} + {container + ? ReactDOM.createPortal( + open && ( + + ), + container + ) + : container} + + ); +} diff --git a/website/src/pages/showcase/_components/ShowcaseTooltip/styles.module.css b/website/src/pages/showcase/_components/ShowcaseTooltip/styles.module.css new file mode 100644 index 0000000..5500c81 --- /dev/null +++ b/website/src/pages/showcase/_components/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; +} diff --git a/website/src/pages/showcase/data.tsx b/website/src/pages/showcase/data.tsx new file mode 100644 index 0000000..e7ade27 --- /dev/null +++ b/website/src/pages/showcase/data.tsx @@ -0,0 +1,106 @@ +/* + * ADD YOUR SITE TO THE DOCUSAURUS SHOWCASE + * + * Please don't submit a PR yourself: message Clubwho + * + * Instructions for maintainers: + * - Add the site in the json array below + * - `title` is the project's name (no need for the "Docs" suffix) + * - Use relevant tags to categorize the site (read the tag descriptions on the + * https://docusaurus.io/showcase page and some further clarifications below) + * - Add a local image preview (decent screenshot of the Docusaurus site) + * - The image MUST be added to the GitHub repository, and use `require("img")` + * - The image has to have minimum width 640 and an aspect of no wider than 2:1 + * - If a website is open-source, add a source link. The link should open + * to a directory containing the `docusaurus.config.js` file + * - Resize images: node admin/scripts/resizeImage.js + * - Run optimizt manually (see resize image script comment) + * - Open a PR and check for reported CI errors + * + */ + +// LIST OF AVAILABLE TAGS +// Available tags to assign to a showcase site +// Please choose all tags that you think might apply. +// We'll remove inappropriate tags, but it's less likely that we add tags. +export type TagType = + | "favourite" + // Feel free to add the 'design' tag as long as there's _some_ level of + // CSS/swizzling. + | "design" + // Site must have more than one locale. + | "i18n" + // Large sites are defined as those with > 200 pages, excluding versions. + | "large" + | "personal"; + +// Add sites to this list +// prettier-ignore +const Websites: Website[] = [ + { + title: 'Heavenly Bodies', + description: 'Heavenly Bodies', + preview: require('./images/heavenly-bodies.png'), + website: 'https://ewanlyon.github.io/heavenly-bodies-speedrun-docs/', + source: 'https://github.com/ewanlyon/heavenly-bodies-speedrun-docs', + tags: ['design', 'favourite'], + }, + + /* + Pro Tip: add your site in alphabetical order. + Appending your site here (at the end) is more likely to produce Git conflicts. + */ +]; + +export type Website = { + title: string; + description: string; + preview: string | null; // null = use our serverless screenshot service + website: string; + source: string | null; + tags: TagType[]; +}; + +export type Tag = { + label: string; + description: string; + color: string; +}; + +export const Tags: { [type in TagType]: Tag } = { + favourite: { + label:"Favourite", + description: "Our favourite Speedrun-Docs sites that you must absolutely check out!", + color: '#e9669e', + }, + design: { + label: "Design", + description: "Beautiful Speedrun-Docs sites, polished and standing out from the initial template!", + color: "#a44fb7", + }, + i18n: { + label: "I18n", + description: "Translated Speedrun-Docs sites using the internationalization support with more than 1 locale.", + color: "#127f82", + }, + large: { + label: "Large", + description: "Very large Speedrun-Docs sites, including many more pages than the average!", + color: "#8c2f00", + }, + personal: { + label: "Personal", + description: "Personal websites, blogs and digital gardens built with Speedrun-Docs", + color: "#14cfc3", + }, +}; + +export const TagList = Object.keys(Tags) as TagType[]; +function sortWebsites() { + let result = Websites; + // Sort by site name + result = result.sort((a, b) => a.title.toLowerCase().localeCompare(b.title.toLowerCase())); + return result; +} + +export const sortedWebsites = sortWebsites(); diff --git a/website/src/pages/showcase/images/heavenly-bodies.png b/website/src/pages/showcase/images/heavenly-bodies.png new file mode 100644 index 0000000000000000000000000000000000000000..06ff3c276fed23402cca08cc1773f57c6f6eb9ff GIT binary patch literal 27571 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 user.title.toLowerCase().includes(searchName.toLowerCase())); + } + if (selectedTags.length === 0) { + return websites; + } + return websites.filter((user) => { + if (user.tags.length === 0) { + return false; + } + if (operator === "AND") { + return selectedTags.every((tag) => user.tags.includes(tag)); + } + return selectedTags.some((tag) => user.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)); + restoreUserState(location.state); + }, [location]); + + return useMemo( + () => filterUsers(sortedWebsites, selectedTags, operator, searchName), + [selectedTags, operator, searchName] + ); +} + +const TITLE = "Docusaurus Site Showcase"; +const DESCRIPTION = "List of websites people are building with Speedrun-Docs"; + +function ShowcaseHeader() { + return ( +
    + {TITLE} +

    {DESCRIPTION}

    +

    Message Clubwho on Discord to have your site added.

    +
    + ); +} + +function useSiteCountPlural() { + const { selectMessage } = usePluralForm(); + return (sitesCount: number) => + selectMessage( + sitesCount, + translate( + { + id: "showcase.filters.resultCount", + description: + 'Pluralized label for the number of sites found on the showcase. Use as much plural forms (separated by "|") as your language support (see https://www.unicode.org/cldr/cldr-aux/charts/34/supplemental/language_plural_rules.html)', + message: "1 site|{sitesCount} sites", + }, + { sitesCount } + ) + ); +} + +function ShowcaseFilters() { + const filteredUsers = useFilteredUsers(); + const siteCountPlural = useSiteCountPlural(); + return ( +
    +
    +
    + + Filters + + {siteCountPlural(filteredUsers.length)} +
    + +
    +
      + {TagList.map((tag, i) => { + const { label, description, color } = Tags[tag]; + const id = `showcase_checkbox_id_${tag}`; + + return ( +
    • + + + ) : ( + + ) + } + /> + +
    • + ); + })} +
    +
    + ); +} + +const favouriteWebsites = sortedWebsites.filter((user) => user.tags.includes("favourite")); +const otherUsers = sortedWebsites.filter((user) => !user.tags.includes("favourite")); + +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 ShowcaseCards() { + const filteredUsers = useFilteredUsers(); + + if (filteredUsers.length === 0) { + return ( +
    +
    + + No result + +
    +
    + ); + } + + return ( +
    + {filteredUsers.length === sortedWebsites.length ? ( + <> +
    +
    +
    + + Our favorites + + +
    +
      + {favouriteWebsites.map((user) => ( + + ))} +
    +
    +
    +
    + + All sites + +
      + {otherUsers.map((user) => ( + + ))} +
    +
    + + ) : ( +
    +
    +
      + {filteredUsers.map((user) => ( + + ))} +
    +
    + )} +
    + ); +} + +export default function Showcase(): JSX.Element { + return ( + +
    + + +
    + +
    + +
    +
    + ); +} diff --git a/website/src/pages/showcase/styles.module.css b/website/src/pages/showcase/styles.module.css new file mode 100644 index 0000000..d9cfd94 --- /dev/null +++ b/website/src/pages/showcase/styles.module.css @@ -0,0 +1,95 @@ +/** + * 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. + */ + +.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; +} + +.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; +}