Add description to list page
Pulled MarkdownAndSafeHTML into a shared component, and use it on the single list page now too! I also simplified some of the logic for the item list, because I figure we'll have to give the trade matching stuff its own pass, y'know?
This commit is contained in:
parent
3cd0ffd764
commit
f20c68ea81
3 changed files with 101 additions and 115 deletions
|
@ -9,13 +9,14 @@ import {
|
||||||
Wrap,
|
Wrap,
|
||||||
WrapItem,
|
WrapItem,
|
||||||
} from "@chakra-ui/react";
|
} from "@chakra-ui/react";
|
||||||
|
import { ChevronRightIcon } from "@chakra-ui/icons";
|
||||||
import { Heading1, MajorErrorMessage } from "./util";
|
import { Heading1, MajorErrorMessage } from "./util";
|
||||||
import { gql, useQuery } from "@apollo/client";
|
import { gql, useQuery } from "@apollo/client";
|
||||||
import { Link, useParams } from "react-router-dom";
|
import { Link, useParams } from "react-router-dom";
|
||||||
import { HashLink } from "react-router-hash-link";
|
import { HashLink } from "react-router-hash-link";
|
||||||
|
|
||||||
import HangerSpinner from "./components/HangerSpinner";
|
import HangerSpinner from "./components/HangerSpinner";
|
||||||
import { ChevronRightIcon } from "@chakra-ui/icons";
|
import MarkdownAndSafeHTML from "./components/MarkdownAndSafeHTML";
|
||||||
import ItemCard from "./components/ItemCard";
|
import ItemCard from "./components/ItemCard";
|
||||||
import WIPCallout from "./components/WIPCallout";
|
import WIPCallout from "./components/WIPCallout";
|
||||||
|
|
||||||
|
@ -28,6 +29,7 @@ function UserItemListPage() {
|
||||||
closetList(id: $listId) {
|
closetList(id: $listId) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
description
|
||||||
ownsOrWantsItems
|
ownsOrWantsItems
|
||||||
creator {
|
creator {
|
||||||
id
|
id
|
||||||
|
@ -43,7 +45,7 @@ function UserItemListPage() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
{ variables: { listId } }
|
{ variables: { listId }, context: { sendAuth: true } }
|
||||||
);
|
);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
@ -102,49 +104,20 @@ function UserItemListPage() {
|
||||||
/>
|
/>
|
||||||
</Flex>
|
</Flex>
|
||||||
<Box height="6" />
|
<Box height="6" />
|
||||||
{/* TODO: Description */}
|
{closetList.description && (
|
||||||
|
<MarkdownAndSafeHTML>{closetList.description}</MarkdownAndSafeHTML>
|
||||||
|
)}
|
||||||
<ClosetListContents closetList={closetList} />
|
<ClosetListContents closetList={closetList} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClosetListContents({ closetList }) {
|
function ClosetListContents({ closetList }) {
|
||||||
const isCurrentUser = false; // TODO
|
const sortedItems = [...closetList.items].sort((a, b) =>
|
||||||
|
a.name.localeCompare(b.name)
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: A lot of this is duplicated from UserItemsPage, find shared
|
const tradeMatchingMode = "hide-all"; // TODO
|
||||||
// abstractions!
|
|
||||||
const hasYouWantThisBadge = (item) =>
|
|
||||||
!isCurrentUser &&
|
|
||||||
closetList.ownsOrWantsItems === "OWNS" &&
|
|
||||||
item.currentUserWantsThis;
|
|
||||||
const hasYouOwnThisBadge = (item) =>
|
|
||||||
!isCurrentUser &&
|
|
||||||
closetList.ownsOrWantsItems === "WANTS" &&
|
|
||||||
item.currentUserOwnsThis;
|
|
||||||
const hasAnyTradeBadge = (item) =>
|
|
||||||
hasYouOwnThisBadge(item) || hasYouWantThisBadge(item);
|
|
||||||
|
|
||||||
const sortedItems = [...closetList.items].sort((a, b) => {
|
|
||||||
// This is a cute sort hack. We sort first by, bringing "You own/want
|
|
||||||
// this!" to the top, and then sorting by name _within_ those two groups.
|
|
||||||
const aName = `${hasAnyTradeBadge(a) ? "000" : "999"} ${a.name}`;
|
|
||||||
const bName = `${hasAnyTradeBadge(b) ? "000" : "999"} ${b.name}`;
|
|
||||||
return aName.localeCompare(bName);
|
|
||||||
});
|
|
||||||
|
|
||||||
let tradeMatchingMode;
|
|
||||||
if (isCurrentUser) {
|
|
||||||
// On your own item list, it's not helpful to show your own trade matches!
|
|
||||||
tradeMatchingMode = "hide-all";
|
|
||||||
} else if (closetList.ownsOrWantsItems === "OWNS") {
|
|
||||||
tradeMatchingMode = "offering";
|
|
||||||
} else if (closetList.ownsOrWantsItems === "WANTS") {
|
|
||||||
tradeMatchingMode = "seeking";
|
|
||||||
} else {
|
|
||||||
throw new Error(
|
|
||||||
`unexpected ownsOrWantsItems value: ${closetList.ownsOrWantsItems}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
|
|
|
@ -34,12 +34,11 @@ import {
|
||||||
import gql from "graphql-tag";
|
import gql from "graphql-tag";
|
||||||
import { Link, useHistory, useParams } from "react-router-dom";
|
import { Link, useHistory, useParams } from "react-router-dom";
|
||||||
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
|
import { useQuery, useLazyQuery, useMutation } from "@apollo/client";
|
||||||
import SimpleMarkdown from "simple-markdown";
|
|
||||||
import DOMPurify from "dompurify";
|
|
||||||
|
|
||||||
import HangerSpinner from "./components/HangerSpinner";
|
import HangerSpinner from "./components/HangerSpinner";
|
||||||
import { Heading1, Heading2, Heading3 } from "./util";
|
import { Heading1, Heading2, Heading3 } from "./util";
|
||||||
import ItemCard from "./components/ItemCard";
|
import ItemCard from "./components/ItemCard";
|
||||||
|
import MarkdownAndSafeHTML from "./components/MarkdownAndSafeHTML";
|
||||||
import SupportOnly from "./WardrobePage/support/SupportOnly";
|
import SupportOnly from "./WardrobePage/support/SupportOnly";
|
||||||
import useSupport from "./WardrobePage/support/useSupport";
|
import useSupport from "./WardrobePage/support/useSupport";
|
||||||
import useCurrentUser from "./components/useCurrentUser";
|
import useCurrentUser from "./components/useCurrentUser";
|
||||||
|
@ -657,81 +656,6 @@ function buildClosetListPath(closetList) {
|
||||||
return `/user/${closetList.creator.id}/lists/${ownsOrWants}/${idString}`;
|
return `/user/${closetList.creator.id}/lists/${ownsOrWants}/${idString}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
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|items\.jellyneo\.net)\/|^[/#]/,
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ClassNames>
|
|
||||||
{({ css }) => (
|
|
||||||
<Box
|
|
||||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
|
||||||
className={css`
|
|
||||||
.paragraph,
|
|
||||||
ol,
|
|
||||||
ul {
|
|
||||||
margin-bottom: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
ol,
|
|
||||||
ul {
|
|
||||||
margin-left: 2em;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ClassNames>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserSupportMenu({ children, user }) {
|
function UserSupportMenu({ children, user }) {
|
||||||
const { supportSecret } = useSupport();
|
const { supportSecret } = useSupport();
|
||||||
const toast = useToast();
|
const toast = useToast();
|
||||||
|
|
89
src/app/components/MarkdownAndSafeHTML.js
Normal file
89
src/app/components/MarkdownAndSafeHTML.js
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
import React from "react";
|
||||||
|
import { ClassNames } from "@emotion/react";
|
||||||
|
import { Box } from "@chakra-ui/react";
|
||||||
|
import SimpleMarkdown from "simple-markdown";
|
||||||
|
import DOMPurify from "dompurify";
|
||||||
|
|
||||||
|
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")
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* MarkdownAndSafeHTML renders its children as a Markdown string, with some
|
||||||
|
* safe inline HTML allowed.
|
||||||
|
*
|
||||||
|
* Rendering this component *should* be XSS-safe, it's designed to strip out
|
||||||
|
* bad things! Still, be careful when using it, and consider what you're doing!
|
||||||
|
*/
|
||||||
|
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|items\.jellyneo\.net)\/|^[/#]/,
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ClassNames>
|
||||||
|
{({ css }) => (
|
||||||
|
<Box
|
||||||
|
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||||
|
className={css`
|
||||||
|
.paragraph,
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
margin-bottom: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ol,
|
||||||
|
ul {
|
||||||
|
margin-left: 2em;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</ClassNames>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MarkdownAndSafeHTML;
|
Loading…
Reference in a new issue