diff --git a/packages/components/src/api/comments/DeleteComment.ts b/packages/components/src/api/comments/DeleteComment.ts new file mode 100644 index 0000000000..1f355083d4 --- /dev/null +++ b/packages/components/src/api/comments/DeleteComment.ts @@ -0,0 +1,57 @@ +import { ApiConfig } from "@storyteller/components"; + +export interface DeleteCommentRequest { + // If a user is both an *author* and *mod*, this controls how to delete the comment. + // Entirely optional! So we'll ignore it for now. + as_mod: boolean | null | undefined, +} + +export interface DeleteCommentSuccessResponse { + success: boolean, +} + +export interface DeleteCommentErrorResponse { + success: boolean, +} + +type DeleteCommentResponse = DeleteCommentSuccessResponse | DeleteCommentErrorResponse; + +export function DeleteCommentIsOk(response: DeleteCommentResponse): response is DeleteCommentSuccessResponse { + return response?.success === true; +} + +export function DeleteCommentIsError(response: DeleteCommentResponse): response is DeleteCommentErrorResponse { + return response?.success === false; +} + +export async function DeleteComment(commentToken: string) : Promise +{ + const endpoint = new ApiConfig().commentDelete(commentToken); + + const request = {}; // TODO: Control moderator scope. (Entirely optional for now.) + + return await fetch(endpoint, { + method: 'POST', + headers: { + 'Accept': 'application/json', + 'Content-Type': 'application/json', + }, + credentials: 'include', + body: JSON.stringify(request), + }) + .then(res => res.json()) + .then(res => { + if (!res) { + return { success : false }; + } + + if (res && 'success' in res) { + return res; + } else { + return { success : false }; + } + }) + .catch(e => { + return { success : false }; + }); +} diff --git a/packages/components/src/api/comments/ListComments.ts b/packages/components/src/api/comments/ListComments.ts new file mode 100644 index 0000000000..defb7f79b8 --- /dev/null +++ b/packages/components/src/api/comments/ListComments.ts @@ -0,0 +1,64 @@ +import { ApiConfig } from "@storyteller/components"; + +export interface ListCommentsSuccessResponse { + success: boolean, + comments: Array +} + +export interface Comment { + token: string, + + user_token: string, + username: string, + user_display_name: string, + user_gravatar_hash: string, + + comment_markdown: string, + comment_rendered_html: string, + + created_at: Date, + updated_at: Date, + maybe_edited_at?: Date, +} + +export interface ListCommentsErrorResponse { + success: boolean, +} + +type ListCommentsResponse = ListCommentsSuccessResponse | ListCommentsErrorResponse; + +export function ListCommentsIsOk(response: ListCommentsResponse): response is ListCommentsSuccessResponse { + return response?.success === true; +} + +export function ListCommentsIsError(response: ListCommentsResponse): response is ListCommentsErrorResponse { + return response?.success === false; +} + +export async function ListComments(entityType: string, entityToken: string) : Promise +{ + const endpoint = new ApiConfig().commentList(entityType, entityToken); + + return await fetch(endpoint, { + method: 'GET', + headers: { + 'Accept': 'application/json', + }, + credentials: 'include', + }) + .then(res => res.json()) + .then(res => { + if (!res) { + return { success : false }; // TODO: This loses error semantics and is deprecated + } + + if (res && 'success' in res) { + return res; + } else { + return { success : false }; + } + }) + .catch(e => { + return { success : false }; + }); +} diff --git a/packages/fakeyou.com/src/v2/view/_common/comments/CommentList.tsx b/packages/fakeyou.com/src/v2/view/_common/comments/CommentList.tsx new file mode 100644 index 0000000000..c755d30944 --- /dev/null +++ b/packages/fakeyou.com/src/v2/view/_common/comments/CommentList.tsx @@ -0,0 +1,78 @@ +import React, { useState, useEffect, useCallback } from "react"; +import { formatDistance } from "date-fns"; +import { + Comment, + ListComments, + ListCommentsIsError, + ListCommentsIsOk, +} from "@storyteller/components/src/api/comments/ListComments"; + +const Fade = require("react-reveal/Fade"); + +interface Props { + entity_type: string; + entity_token: string; +} + + +function CommentList(props: Props) { + const [comments, setComments] = useState>([]); + + const getComments = useCallback(async () => { + const response = await ListComments(props.entity_type, props.entity_token); + + if (ListCommentsIsOk(response)) { + setComments(response.comments); + } else if (ListCommentsIsError(response)) { + // TODO + } + }, [props.entity_token, props.entity_type]); + + useEffect(() => { + getComments(); + }, [getComments]); + + + // NB: It's more convenient to show recent data first + var reversedComments = comments.slice().reverse(); + + const now = new Date(); + + let rows: Array = []; + + reversedComments.forEach((comment) => { + + const createTime = new Date(comment.created_at); + const relativeCreateTime = formatDistance(createTime, now, { + addSuffix: true, + }); + + rows.push( + + + {/* It's okay to set "dangerous" html here as the server safely created it. */} +
+ + {relativeCreateTime} + {comment.user_display_name} link + + ); + }); + + return ( +
+ + + {rows} + +
+
+ ); +} + +export { CommentList }; diff --git a/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentComponent.tsx b/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentComponent.tsx index cb2557b58b..6e69558854 100644 --- a/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentComponent.tsx +++ b/packages/fakeyou.com/src/v2/view/_common/comments/CreateCommentComponent.tsx @@ -34,7 +34,7 @@ function CreateCommentComponent(props: Props) { //let ratingValue = rating.maybe_rating_value || undefined; //setUserRatingValue(ratingValue); } - }, [props.entity_type, props.entity_token]); + }, [props.entity_type, props.entity_token, uuidIdempotencyToken, commentMarkdown]); const handleCommentChange = ( ev: React.FormEvent diff --git a/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/ProfilePage.tsx b/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/ProfilePage.tsx index 1b2b60057d..a2c8021b86 100644 --- a/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/ProfilePage.tsx +++ b/packages/fakeyou.com/src/v2/view/pages/profile/profile_view/ProfilePage.tsx @@ -39,6 +39,8 @@ import "tippy.js/dist/tippy.css"; import { motion } from "framer-motion"; import { usePrefixedDocumentTitle } from "../../../../../common/UsePrefixedDocumentTitle"; import { faCalendarAlt } from "@fortawesome/pro-solid-svg-icons"; +//import { CreateCommentComponent } from "../../../_common/comments/CreateCommentComponent"; +//import { CommentList } from "../../../_common/comments/CommentList"; interface Props { sessionWrapper: SessionWrapper; @@ -393,6 +395,14 @@ function ProfilePage(props: Props) {
+ {/* + + TODO - this isn't where it should go + + + */} + + {profileButtonsMobile}