mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-04 17:38:10 +00:00
API for list and delete comments, components to list
This commit is contained in:
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}>
|
||||
|
||||
Reference in new issue
Block a user