add descriptions to closet lists (formatted! :3)

This commit is contained in:
Emi Matchu 2020-10-28 00:00:14 -07:00
parent 4e00962edc
commit 15f10c615b
4 changed files with 108 additions and 0 deletions

View file

@ -20,6 +20,7 @@
"apollo-server-env": "^2.4.3",
"aws-sdk": "^2.726.0",
"dataloader": "^2.0.0",
"dompurify": "^2.2.0",
"emotion": "^10.0.27",
"graphql": "^15.0.0",
"honeycomb-beeline": "^2.2.0",
@ -36,6 +37,7 @@
"react-router-dom": "^5.1.2",
"react-scripts": "3.4.1",
"react-transition-group": "^4.3.0",
"simple-markdown": "^0.7.2",
"xmlrpc": "^1.3.2"
},
"scripts": {

View file

@ -1,9 +1,12 @@
import React from "react";
import { css } from "emotion";
import { Badge, Box, Center, Wrap, VStack } from "@chakra-ui/core";
import { CheckIcon, EmailIcon, StarIcon } from "@chakra-ui/icons";
import gql from "graphql-tag";
import { useParams } from "react-router-dom";
import { useQuery } from "@apollo/client";
import SimpleMarkdown from "simple-markdown";
import DOMPurify from "dompurify";
import HangerSpinner from "./components/HangerSpinner";
import { Heading1, Heading2, Heading3 } from "./util";
@ -35,6 +38,7 @@ function UserItemsPage() {
closetLists {
id
name
description
ownsOrWantsItems
isDefaultList
items {
@ -237,6 +241,11 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) {
{closetList.name}
</Heading3>
)}
{closetList.description && (
<Box marginBottom="2">
<MarkdownAndSafeHTML>{closetList.description}</MarkdownAndSafeHTML>
</Box>
)}
{sortedItems.length > 0 ? (
<ItemCardList>
{sortedItems.map((item) => (
@ -264,6 +273,77 @@ function ClosetList({ closetList, isCurrentUser, showHeading }) {
);
}
const unsafeMarkdownRules = {
autolink: SimpleMarkdown.defaultRules.autolink,
br: SimpleMarkdown.defaultRules.br,
em: SimpleMarkdown.defaultRules.em,
escape: SimpleMarkdown.defaultRules.escape,
link: SimpleMarkdown.defaultRules.link,
list: SimpleMarkdown.defaultRules.list,
newline: SimpleMarkdown.defaultRules.newline,
paragraph: SimpleMarkdown.defaultRules.paragraph,
strong: SimpleMarkdown.defaultRules.strong,
u: SimpleMarkdown.defaultRules.u,
// DANGER: We override Markdown's `text` rule to _not_ escape HTML. This is
// intentional, to allow users to embed some limited HTML. DOMPurify is
// responsible for sanitizing the HTML afterward. Do not use these rules
// without sanitizing!!
text: {
...SimpleMarkdown.defaultRules.text,
html: (node) => node.content,
},
};
const markdownParser = SimpleMarkdown.parserFor(unsafeMarkdownRules);
const unsafeMarkdownOutput = SimpleMarkdown.htmlFor(
SimpleMarkdown.ruleOutput(unsafeMarkdownRules, "html")
);
function MarkdownAndSafeHTML({ children }) {
const htmlAndMarkdown = children;
const unsafeHtml = unsafeMarkdownOutput(markdownParser(htmlAndMarkdown));
const sanitizedHtml = DOMPurify.sanitize(unsafeHtml, {
ALLOWED_TAGS: [
"b",
"i",
"u",
"strong",
"em",
"a",
"p",
"div",
"br",
"ol",
"ul",
"li",
],
ALLOWED_ATTR: ["href", "class"],
// URL must either start with an approved host (external link), or with a
// slash or hash (internal link).
ALLOWED_URI_REGEXP: /^https?:\/\/(impress\.openneo\.net|impress-2020\.openneo\.net|www\.neopets\.com|neopets\.com)\/|^[\/#]/,
});
return (
<Box
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
className={css`
.paragraph,
ol,
ul {
margin-bottom: 1em;
}
ol,
ul {
margin-left: 2em;
}
`}
></Box>
);
}
function NeopetsStarIcon(props) {
// Converted from the Neopets favicon with https://www.vectorizer.io/.
return (

View file

@ -21,6 +21,9 @@ const typeDefs = gql`
id: ID!
name: String
# A user-customized description. May contain Markdown and limited HTML.
description: String
# Whether this is a list of items they own, or items they want.
ownsOrWantsItems: OwnsOrWants!
@ -158,6 +161,7 @@ const resolvers = {
.map((closetList) => ({
id: closetList.id,
name: closetList.name,
description: closetList.description,
ownsOrWantsItems: closetList.hangersOwned ? "OWNS" : "WANTS",
isDefaultList: false,
items: allClosetHangers
@ -169,6 +173,7 @@ const resolvers = {
closetListNodes.push({
id: `user-${id}-default-list-OWNS`,
name: "Not in a list",
description: null,
ownsOrWantsItems: "OWNS",
isDefaultList: true,
items: allClosetHangers
@ -181,6 +186,7 @@ const resolvers = {
closetListNodes.push({
id: `user-${id}-default-list-WANTS`,
name: "Not in a list",
description: null,
ownsOrWantsItems: "WANTS",
isDefaultList: true,
items: allClosetHangers

View file

@ -4694,6 +4694,14 @@
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/react@>=16.0.0":
version "16.9.54"
resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.54.tgz#140280c31825287ee74e9da95285b91c5a2e471d"
integrity sha512-GhawhYraQZpGFO2hVMArjPrYbnA/6+DS8SubK8IPhhVClmKqANihsRenOm5E0mvqK0m/BKoqVktA1O1+Xvlz9w==
dependencies:
"@types/prop-types" "*"
csstype "^3.0.2"
"@types/reactcss@*":
version "1.2.3"
resolved "https://registry.yarnpkg.com/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834"
@ -8327,6 +8335,11 @@ domhandler@^2.3.0:
dependencies:
domelementtype "1"
dompurify@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/dompurify/-/dompurify-2.2.0.tgz#51d34e76faa38b5d6b4e83a0678530f27fe3965c"
integrity sha512-bqFOQ7XRmmozp0VsKdIEe8UwZYxj0yttz7l80GBtBqdVRY48cOpXH2J/CVO7AEkV51qY0EBVXfilec18mdmQ/w==
domutils@1.5.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/domutils/-/domutils-1.5.1.tgz#dcd8488a26f563d61079e48c9f7b7e32373682cf"
@ -16302,6 +16315,13 @@ signal-exit@^3.0.0, signal-exit@^3.0.2:
resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-3.0.2.tgz#b5fdc08f1287ea1178628e415e25132b73646c6d"
integrity sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=
simple-markdown@^0.7.2:
version "0.7.2"
resolved "https://registry.yarnpkg.com/simple-markdown/-/simple-markdown-0.7.2.tgz#896cc3e3dd9acd068d30e696bce70b0b97655665"
integrity sha512-XfCvqqzMyzRj4L7eIxJgGaQ2Gaxr20GhTFMB+1yuY8q3xffjzmOg4Q5tC0kcaJPV42NNUHCQDaRK6jzi3/RhrA==
dependencies:
"@types/react" ">=16.0.0"
simple-swizzle@^0.2.2:
version "0.2.2"
resolved "https://registry.yarnpkg.com/simple-swizzle/-/simple-swizzle-0.2.2.tgz#a4da6b635ffcccca33f70d17cb92592de95e557a"