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"