diff --git a/browser/src/components/ContributionCard.tsx b/browser/src/components/ContributionCard.tsx index 85f64e1..3416af2 100644 --- a/browser/src/components/ContributionCard.tsx +++ b/browser/src/components/ContributionCard.tsx @@ -15,11 +15,12 @@ import { OrbitControls } from "@react-three/drei/core/OrbitControls"; import { MdLink } from "react-icons/md"; import { getContributionLink } from "src/helpers/contributions"; -import sanitizeHtml from "sanitize-html"; import parse from 'html-react-parser'; +import sanitizeHtml from "sanitize-html"; +import ContributionsCarousel from "./ContributionsCarousel"; interface Props { - contribution: Contribution; + contribution: ClientContribution; hideHeader?: boolean; isCompact?: boolean; className?: string; @@ -29,7 +30,7 @@ export function getFullContributionResponse({ response, prompt, pattern, -}: Contribution) { +}: ClientContribution) { return ( PromptDescriptions[prompt].replace( `{${Placeholder}}`, @@ -46,14 +47,30 @@ export function ContributionCard({ isCompact = false, className = "", }: Props) { - const { author, response, prompt, pattern, createdAt, id } = contribution; + const { author, response, responseHtml, prompt, pattern, createdAt, id } = contribution; const authorDisplay = getDisplayForAuthor(author, true); const date = dayjs(createdAt, { utc: true }); const dateDisplay = date.format("MMM, YYYY"); const contributionLink = getContributionLink(contribution); - const responseHtml = parse(sanitizeHtml(response)); + + const renderHtml = (resp: string): string | JSX.Element | JSX.Element[] => { + // Remove first p tag to prevent first text going to next line, sanitize html string + // and then convert to JSX element + return parse( + sanitizeHtml( + resp.replace( + /]*>|<\/p[^>]*>/, + "" + ) + ) + ) + }; + + const input = renderHtml( + responseHtml || response + ); return (
{getPatternPlaceholder(pattern, prompt)}, })}{" "} - {responseHtml} + {input}

{id ? ( diff --git a/browser/src/components/ContributionSection.tsx b/browser/src/components/ContributionSection.tsx index 143dee9..594d2a1 100644 --- a/browser/src/components/ContributionSection.tsx +++ b/browser/src/components/ContributionSection.tsx @@ -3,7 +3,7 @@ import { useCallback, useContext, useState } from "react"; import { descriptionText } from "../classNameConstants"; import { Author, - Contribution, + ClientContribution, Pattern, PatternToDisplay, Prompt, @@ -40,6 +40,9 @@ import { AsyncButton } from "./core/AsyncButton"; import dayjs from "dayjs"; import { ArweaveContext } from "src/helpers/contexts/ArweaveContext"; +import { Converter } from "showdown"; + + enum Page { TermsOfUse, Contribute, @@ -125,7 +128,7 @@ function PreviewCard({ prompt: Prompt; pattern: Pattern; }) { - const contribution: Contribution = { + const contribution: ClientContribution = { author, response: response || "...", prompt, @@ -358,10 +361,11 @@ export function ContributionSection() { pattern: selectedPattern, } as any) ); + const toMarkdownConverter = new Converter(); const newContributionId = await addContribution({ prompt: selectedPrompt, pattern: selectedPattern, - response, + response: toMarkdownConverter.makeMarkdown(response), walletId: currentUser!.walletId, }); // TODO: eliminate this and just return th actual contribution data with the response above. diff --git a/browser/src/components/core/Editor.css b/browser/src/components/core/Editor.css index 20de53b..741fcd4 100755 --- a/browser/src/components/core/Editor.css +++ b/browser/src/components/core/Editor.css @@ -78,6 +78,10 @@ margin-top: 24px; } +.invalidLink { + border-color: red; +} + .modal { background-color: hsla(0, 0%, 15%, 1); width:100%; diff --git a/browser/src/components/core/Editor.tsx b/browser/src/components/core/Editor.tsx index 617ac46..131da87 100755 --- a/browser/src/components/core/Editor.tsx +++ b/browser/src/components/core/Editor.tsx @@ -13,6 +13,11 @@ import Placeholder from "@tiptap/extension-placeholder"; import History from "@tiptap/extension-history"; import CharacterCount from "@tiptap/extension-character-count"; +import { Converter } from "showdown"; +import sanitizeHtml from "sanitize-html"; +import isURL from "validator/lib/isURL"; + + import { ButtonClass } from "src/types/styles"; import { ResponseCharacterLimit } from "../ContributionSection"; import "./Editor.css"; @@ -35,20 +40,19 @@ export function Editor({ }: Props) { const [linkInput, setLinkInput] = useState(null); const [displayLinkModal, setDisplayLinkModal] = useState(false); - - const sanitize = (inputHtml: string): string => { - // Remove first p tag to prevent text going to next line - return inputHtml.replace( - /]*>|<\/p[^>]*>/, - "" - ); - }; + const [isInvalidInput, setIsInvalidInput] = useState(false); const openModal = () => { setDisplayLinkModal(true) toggleBackgroundScrollingOnModal(false); } + const closeModal = () => { + setDisplayLinkModal(false); + setLinkInput(null); + toggleBackgroundScrollingOnModal(true) + } + // Set Cmd/Ctrl-k shortcut const CustomLink = Link.extend({ addKeyboardShortcuts() { @@ -76,27 +80,37 @@ export function Editor({ ], onUpdate: ({ editor }) => { onChange( - sanitize( - editor.getHTML() - ) + sanitizeHtml(editor.getHTML()) ); + // Okay so what we want is ContributionCard should render the + // html directly to be fast, but we should store markdown + // so we should keep onChange the same so that contribution card is + // recieve the html, but we need to find a way to persist the + // markdown, but only to the request and not the contribution card + // maybe a set markdown field? or an optional value on the AddContributionRequest setResponseLength(editor.storage.characterCount.characters()); }, }) - const setLink = (cancel: boolean) => { - if (cancel) { - // Previous url - return editor.getAttributes('link').href; + const setLink = (save: boolean) => { + setIsInvalidInput(false); + if (!save) { + closeModal(); } - var url = linkInput; - // TODO: Add Link validation - - if (url === "" || url === null || url === undefined) { - //editor.chain().focus().extendMarkRange('link').unsetLink().run() - editor.chain().focus().unsetLink().run() - return + var url = null; + // If link is not null, check if it's valid and display error message otherwise. + if (!(linkInput === "" || linkInput === null || linkInput === undefined)) { + if (isURL(linkInput)) { + url = linkInput; + } else { + setIsInvalidInput(true); + return; + } + } else { + editor.chain().focus().unsetLink().run(); + closeModal(); + return; } // Add so href doesn't point to pluriverse.world/{url} @@ -107,22 +121,9 @@ export function Editor({ url = "http://" + url; } - // Update link - editor.chain().focus().setLink({ href: url }).run() - }; - - const onClickAddLink = () => { - setLink(false); - setDisplayLinkModal(false); - setLinkInput(null); - toggleBackgroundScrollingOnModal(true) - }; - - const onCloseModal = () => { - setLink(true); - setDisplayLinkModal(false); - setLinkInput(null); - toggleBackgroundScrollingOnModal(true); + // Set link. + editor.chain().focus().setLink({ href: url }).run(); + closeModal(); }; const getPreviousLink = () => { @@ -168,30 +169,30 @@ export function Editor({ }} className={`menuItem linkIcon ${displayLinkModal ? 'shimmer' : 'white'}`} > - 🔗 + 🔗
} - onCloseModal()} shouldCloseOnOverlayClick={true} >

- Add Link + Add Link

- { - setLinkInput(e.target.value) + setLinkInput((e.target.value) || "") }} onKeyPress={handleKeyPress} autoFocus @@ -201,24 +202,24 @@ export function Editor({
- ) diff --git a/browser/src/helpers/api.ts b/browser/src/helpers/api.ts index dca8ea8..719e070 100644 --- a/browser/src/helpers/api.ts +++ b/browser/src/helpers/api.ts @@ -2,6 +2,7 @@ import { AddContributionResponse, AddContributionRequest, GetContributionRequest, + ClientContribution, Contribution, GetContributionsRequest, AddUserRequest, @@ -12,6 +13,8 @@ import { VerifyTwitterRequest, } from "../types/common/server-api"; +import { Converter } from "showdown"; + export function withQueryParams( url: string, params: Record @@ -69,23 +72,28 @@ export async function addContribution( body: request, method: "POST", }); - console.log(`Added ${response} contribution`); return response as AddContributionResponse; } export async function getContribution({ id, -}: GetContributionRequest): Promise { +}: GetContributionRequest): Promise { const response = await makeRequest(`${ApiUrl}/contributions/${id}`, { method: "GET", }); - return response as Contribution; + + const mdToHtmlConverter = new Converter(); + response.responseHtml = mdToHtmlConverter.makeHtml( + response.response + ); + + return response as ClientContribution; } export async function getContributions({ offset, contributionId, -}: GetContributionsRequest): Promise { +}: GetContributionsRequest): Promise { const response = await makeRequest( withQueryParams(`${ApiUrl}/contributions`, { offset: offset ? String(offset) : offset, @@ -95,7 +103,16 @@ export async function getContributions({ method: "GET", } ); - return response as Contribution[]; + + const mdToHtmlConverter = new Converter(); + const responseWithHtml = response.map((res) => { + res.responseHtml = mdToHtmlConverter.makeHtml( + res.response + ); + return res; + }); + + return response as ClientContribution[]; } export async function getUser({ diff --git a/browser/src/types/common/server-api/index.ts b/browser/src/types/common/server-api/index.ts index a276571..b2cccc5 100644 --- a/browser/src/types/common/server-api/index.ts +++ b/browser/src/types/common/server-api/index.ts @@ -61,10 +61,15 @@ export interface Contribution { createdAt: Date; } +export interface ClientContribution extends Contribution { + responseHtml?: string; +} + export interface AddContributionRequest { walletId: string; // This should be the full text response, formatted as markdown. response: string; + // Full text response formatted as markdown as HTML. prompt: Prompt; pattern: Pattern; } diff --git a/browser/yarn.lock b/browser/yarn.lock index ad72ef9..1384c32 100644 --- a/browser/yarn.lock +++ b/browser/yarn.lock @@ -7773,6 +7773,11 @@ entities@^3.0.1: resolved "https://registry.yarnpkg.com/entities/-/entities-3.0.1.tgz#2b887ca62585e96db3903482d336c1006c3001d4" integrity sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q== +entities@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.1.0.tgz#992d3129cf7df6870b96c57858c249a120f8b8b5" + integrity sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w== + err-code@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/err-code/-/err-code-3.0.1.tgz#a444c7b992705f2b120ee320b09972eef331c920" @@ -9769,7 +9774,7 @@ hash.js@1.1.7, hash.js@^1.0.0, hash.js@^1.0.3, hash.js@^1.1.7: inherits "^2.0.3" minimalistic-assert "^1.0.1" -he@1.2.0, he@^1.2.0: +he@^1.2.0: version "1.2.0" resolved "https://registry.npmjs.org/he/-/he-1.2.0.tgz" integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw== @@ -12216,6 +12221,13 @@ loader-runner@^2.4.0: resolved "https://registry.yarnpkg.com/loader-runner/-/loader-runner-2.4.0.tgz#ed47066bfe534d7e84c4c7b9998c2a75607d9357" integrity sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw== +linkify-it@^3.0.1: + version "3.0.3" + resolved "https://registry.yarnpkg.com/linkify-it/-/linkify-it-3.0.3.tgz#a98baf44ce45a550efb4d49c769d07524cc2fa2e" + integrity sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ== + dependencies: + uc.micro "^1.0.1" + linkifyjs@^3.0.5: version "3.0.5" resolved "https://registry.yarnpkg.com/linkifyjs/-/linkifyjs-3.0.5.tgz#99e51a3a0c0e232fcb63ebb89eea3ff923378f34" @@ -12458,6 +12470,17 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +markdown-it@^12.0.0: + version "12.3.2" + resolved "https://registry.yarnpkg.com/markdown-it/-/markdown-it-12.3.2.tgz#bf92ac92283fe983fe4de8ff8abfb5ad72cd0c90" + integrity sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg== + dependencies: + argparse "^2.0.1" + entities "~2.1.0" + linkify-it "^3.0.1" + mdurl "^1.0.1" + uc.micro "^1.0.5" + md5.js@^1.3.4: version "1.3.5" resolved "https://registry.yarnpkg.com/md5.js/-/md5.js-1.3.5.tgz#b5d07b8e3216e3e27cd728d72f70d1e6a342005f" @@ -12477,6 +12500,11 @@ mdn-data@2.0.4: resolved "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz" integrity sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA== +mdurl@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/mdurl/-/mdurl-1.0.1.tgz#fe85b2ec75a59037f2adfec100fd6c601761152e" + integrity sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4= + media-typer@0.3.0: version "0.3.0" resolved "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz" @@ -12940,14 +12968,6 @@ node-gyp-build@^4.2.0: resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.3.0.tgz#9f256b03e5826150be39c764bf51e993946d71a3" integrity sha512-iWjXZvmboq0ja1pUGULQBexmxq8CV4xBhX7VDOTbL7ZR4FOowwY/VOtRxBN/yKxmdGoIp4j5ysNT4u3S2pDQ3Q== -node-html-parser@^5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-5.2.0.tgz#6f29fd00d79f65334e7e20200964644207925607" - integrity sha512-fmiwLfQu+J2A0zjwSEkztSHexAf5qq/WoiL/Hgo1K7JpfEP+OGWY5maG0kGaM+IFVdixF/1QbyXaQ3h4cGfeLw== - dependencies: - css-select "^4.1.3" - he "1.2.0" - node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz" @@ -16776,6 +16796,13 @@ shellwords@^0.1.1: resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww== +showdown@^1.9.1: + version "1.9.1" + resolved "https://registry.yarnpkg.com/showdown/-/showdown-1.9.1.tgz#134e148e75cd4623e09c21b0511977d79b5ad0ef" + integrity sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA== + dependencies: + yargs "^14.2" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz" @@ -18082,6 +18109,11 @@ typescript@^4.4.2: resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz" integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== +uc.micro@^1.0.1, uc.micro@^1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.6.tgz#9c411a802a409a91fc6cf74081baba34b24499ac" + integrity sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA== + uint8arrays@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/uint8arrays/-/uint8arrays-3.0.0.tgz#260869efb8422418b6f04e3fac73a3908175c63b" @@ -18357,6 +18389,11 @@ validate-npm-package-license@^3.0.1: spdx-correct "^3.0.0" spdx-expression-parse "^3.0.0" +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.yarnpkg.com/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== + varint@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/varint/-/varint-6.0.0.tgz#9881eb0ce8feaea6512439d19ddf84bf551661d0"