API for list and delete comments, components to list

This commit is contained in:
Brandon Thomas committed 2023-03-18 07:04:18 -04:00
1 parent f1789ffda5
commit 8b5cafe33b
5 files changed
+210 -1

No files matched your search

@@ -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<DeleteCommentResponse>
{
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 };
});
}
@@ -0,0 +1,64 @@
import { ApiConfig } from "@storyteller/components";
export interface ListCommentsSuccessResponse {
success: boolean,
comments: Array<Comment>
}
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<ListCommentsResponse>
{
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 };
});
}
@@ -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<Array<Comment>>([]);
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<JSX.Element> = [];
reversedComments.forEach((comment) => {
const createTime = new Date(comment.created_at);
const relativeCreateTime = formatDistance(createTime, now, {
addSuffix: true,
});
rows.push(
<tr key={comment.token}>
<td>
{/* It's okay to set "dangerous" html here as the server safely created it. */}
<div
className="mt-3 text-center text-lg-start"
dangerouslySetInnerHTML={{
__html: comment.comment_rendered_html || "",
}}
/>
</td>
<td>{relativeCreateTime}</td>
<td>{comment.user_display_name} link</td>
</tr>
);
});
return (
<div>
<table className="table">
<Fade cascade bottom duration="200" distance="10px">
<tbody>{rows}</tbody>
</Fade>
</table>
</div>
);
}
export { CommentList };
@@ -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<HTMLTextAreaElement>
@@ -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) {
</motion.div>
</div>
{/*
TODO - this isn't where it should go
<CommentList entity_type="user" entity_token={userData?.user_token} />
<CreateCommentComponent entity_type="user" entity_token={userData?.user_token} />
*/}
{profileButtonsMobile}
<motion.div className="container-panel mt-5" variants={panel}>