This is an automated email from the ASF dual-hosted git repository.

linkinstar pushed a commit to branch dev
in repository https://gitbox.apache.org/repos/asf/incubator-answer.git

commit 80bd8cd001c9382d809f5f18c55ccf38b7566631
Author: hgaol <[email protected]>
AuthorDate: Tue May 7 10:57:44 2024 +0800

    finish backend api and frontend
---
 internal/controller/meta_controller.go             |  24 ++-
 internal/router/answer_api_router.go               |   3 +
 internal/schema/meta_schema.go                     |   6 +-
 internal/service/meta/meta_service.go              |   8 +-
 ui/src/components/Comment/index.tsx                | 219 +++++++++++----------
 .../Questions/Detail/components/Answer/index.tsx   |   3 -
 .../Questions/Detail/components/Question/index.tsx |   3 -
 .../Detail/components/Reactions/index.tsx          | 155 ++++++++++-----
 ui/src/pages/Questions/Detail/index.scss           |  23 ++-
 ui/src/services/common.ts                          |  10 +
 10 files changed, 282 insertions(+), 172 deletions(-)

diff --git a/internal/controller/meta_controller.go 
b/internal/controller/meta_controller.go
index de7d5ddc..fe598b90 100644
--- a/internal/controller/meta_controller.go
+++ b/internal/controller/meta_controller.go
@@ -46,11 +46,11 @@ func NewMetaController(
 // @Accept json
 // @Produce json
 // @Security ApiKeyAuth
-// @Param data body schema.ReactionReq true "reaction"
+// @Param data body schema.UpdateReactionReq true "reaction"
 // @Success 200 {object} handler.RespBody
 // @Router /answer/api/v1/meta/reaction [put]
 func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) {
-       req := &schema.ReactionReq{}
+       req := &schema.UpdateReactionReq{}
        if handler.BindAndCheck(ctx, req) {
                return
        }
@@ -59,3 +59,23 @@ func (mc *MetaController) AddOrUpdateReaction(ctx 
*gin.Context) {
        resp, err := mc.metaService.AddOrUpdateReaction(ctx, req)
        handler.HandleResponse(ctx, err, resp)
 }
+
+// GetReaction get reaction
+// @Summary get reaction
+// @Description get reaction for an object
+// @Tags Meta
+// @Accept json
+// @Produce json
+// @Security ApiKeyAuth
+// @Param data body schema.UpdateReactionReq true "reaction"
+// @Success 200 {object} handler.RespBody
+// @Router /answer/api/v1/meta/reaction [put]
+func (mc *MetaController) GetReaction(ctx *gin.Context) {
+       req := &schema.GetReactionReq{}
+       if handler.BindAndCheck(ctx, req) {
+               return
+       }
+
+       resp, err := mc.metaService.GetReactionByObjectId(ctx, req.ObjectID)
+       handler.HandleResponse(ctx, err, resp)
+}
diff --git a/internal/router/answer_api_router.go 
b/internal/router/answer_api_router.go
index a23ddfbf..90acc755 100644
--- a/internal/router/answer_api_router.go
+++ b/internal/router/answer_api_router.go
@@ -184,6 +184,9 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r 
*gin.RouterGroup) {
 
        // rank
        r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage)
+
+       // reaction
+       r.GET("/meta/reaction", a.metaController.GetReaction)
 }
 
 func (a *AnswerAPIRouter) RegisterAuthUserWithAnyStatusAnswerAPIRouter(r 
*gin.RouterGroup) {
diff --git a/internal/schema/meta_schema.go b/internal/schema/meta_schema.go
index 55fc66e7..cf213813 100644
--- a/internal/schema/meta_schema.go
+++ b/internal/schema/meta_schema.go
@@ -19,13 +19,17 @@
 
 package schema
 
-type ReactionReq struct {
+type UpdateReactionReq struct {
        ObjectID string `validate:"required" form:"object_id" json:"object_id"` 
// object id
        Emoji    string `validate:"required" form:"emoji" json:"emoji"`         
// emoji
        Type     string `validate:"required" form:"type" json:"type"`           
// type
        UserID   string `json:"-"`
 }
 
+type GetReactionReq struct {
+       ObjectID string `validate:"required" form:"object_id" json:"object_id"` 
// object id
+}
+
 type ReactSummaryMeta map[string][]string
 
 type ReactionResp ReactSummaryMeta
diff --git a/internal/service/meta/meta_service.go 
b/internal/service/meta/meta_service.go
index 24e852cb..cd7ca4f7 100644
--- a/internal/service/meta/meta_service.go
+++ b/internal/service/meta/meta_service.go
@@ -122,7 +122,7 @@ func (ms *MetaService) GetReactionByObjectId(ctx 
context.Context, objectID strin
 }
 
 // AddOrUpdateReaction add or update reaction
-func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req 
*schema.ReactionReq) (resp schema.ReactionResp, err error) {
+func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req 
*schema.UpdateReactionReq) (resp schema.ReactionResp, err error) {
        // get reaction for this object
        reactionMeta, err := ms.GetMetaByObjectIdAndKey(ctx, req.ObjectID, 
entity.ObjectReactSummaryKey)
 
@@ -171,7 +171,7 @@ func (ms *MetaService) AddOrUpdateReaction(ctx 
context.Context, req *schema.Reac
 }
 
 // updateReaction update reaction
-func (ms *MetaService) updateReaction(req *schema.ReactionReq, reaction 
schema.ReactSummaryMeta) {
+func (ms *MetaService) updateReaction(req *schema.UpdateReactionReq, reaction 
schema.ReactSummaryMeta) {
        emojiUserIds, ok := reaction[req.Emoji]
 
        if !ok {
@@ -215,14 +215,14 @@ func (ms *MetaService) updateReaction(req 
*schema.ReactionReq, reaction schema.R
 
 func (ms *MetaService) convertToReactionResp(ctx context.Context, reaction 
schema.ReactSummaryMeta) (schema.ReactionResp, error) {
        resp := schema.ReactionResp{}
-       // traverse map and convert to user name
+       // traverse map and convert to username
        for emoji, userIds := range reaction {
                userNames := make([]string, 0)
                userBasicInfos, err := 
ms.userCommon.BatchUserBasicInfoByID(ctx, userIds)
                if err != nil {
                        return nil, err
                }
-               // get user name
+               // get username
                for _, userBasicInfo := range userBasicInfos {
                        userNames = append(userNames, userBasicInfo.Username)
                }
diff --git a/ui/src/components/Comment/index.tsx 
b/ui/src/components/Comment/index.tsx
index 69c705b8..88d29de8 100644
--- a/ui/src/components/Comment/index.tsx
+++ b/ui/src/components/Comment/index.tsx
@@ -44,6 +44,7 @@ import {
   postVote,
 } from '@/services';
 import { commentReplyStore } from '@/stores';
+import Reactions from '@/pages/Questions/Detail/components/Reactions';
 
 import { Form, ActionBar, Reply } from './components';
 
@@ -361,118 +362,130 @@ const Comment = ({ objectId, mode, commentId }) => {
       }),
     );
   };
+
+  const handleAddComment = () => {
+    if (tryNormalLogged(true)) {
+      setVisibleComment(!visibleComment);
+    }
+  };
+
   return (
-    <div
-      className={classNames(
-        'comments-wrap',
-        comments.length > 0 && 'bg-light px-3 py-2 rounded',
-      )}>
-      {comments.map((item) => {
-        return (
-          <div
-            key={item.comment_id}
-            id={item.comment_id}
-            className="py-2 comment-item">
-            {item.showEdit ? (
-              <Form
-                className="mt-2"
-                value={item.original_text}
-                type="edit"
-                mode={mode}
-                onSendReply={(value) =>
-                  handleSendReply({ ...item, value, type: 'edit' })
-                }
-                onCancel={() => handleCancel(item.comment_id)}
-              />
-            ) : (
-              <div className="d-block">
-                {item.reply_user_display_name && (
-                  <Link to="." className="small me-1 text-nowrap">
-                    @{item.reply_user_display_name}
-                  </Link>
-                )}
-
-                <div
-                  className="fmt small text-break text-wrap"
-                  dangerouslySetInnerHTML={{ __html: item.parsed_text }}
+    <>
+      <Reactions
+        objectId={objectId}
+        showAddCommentBtn={comments.length === 0}
+        handleClickComment={handleAddComment}
+      />
+      <div
+        className={classNames(
+          'comments-wrap',
+          comments.length > 0 && 'bg-light px-3 py-2 rounded',
+        )}>
+        {comments.map((item) => {
+          return (
+            <div
+              key={item.comment_id}
+              id={item.comment_id}
+              className="py-2 comment-item">
+              {item.showEdit ? (
+                <Form
+                  className="mt-2"
+                  value={item.original_text}
+                  type="edit"
+                  mode={mode}
+                  onSendReply={(value) =>
+                    handleSendReply({ ...item, value, type: 'edit' })
+                  }
+                  onCancel={() => handleCancel(item.comment_id)}
                 />
-              </div>
-            )}
+              ) : (
+                <div className="d-block">
+                  {item.reply_user_display_name && (
+                    <Link to="." className="small me-1 text-nowrap">
+                      @{item.reply_user_display_name}
+                    </Link>
+                  )}
+
+                  <div
+                    className="fmt small text-break text-wrap"
+                    dangerouslySetInnerHTML={{ __html: item.parsed_text }}
+                  />
+                </div>
+              )}
+
+              {currentReplyId === item.comment_id ? (
+                <Reply
+                  userName={item.user_display_name}
+                  mode={mode}
+                  onSendReply={(value) =>
+                    handleSendReply({ ...item, value, type: 'reply' })
+                  }
+                  onCancel={() => handleCancel(item.comment_id)}
+                />
+              ) : null}
+              {item.showEdit || currentReplyId === item.comment_id ? null : (
+                <ActionBar
+                  nickName={item.user_display_name}
+                  username={item.username}
+                  createdAt={item.created_at}
+                  voteCount={item.vote_count}
+                  isVote={item.is_vote}
+                  memberActions={item.member_actions}
+                  userStatus={item.user_status}
+                  onReply={() => {
+                    handleReply(item.comment_id);
+                  }}
+                  onAction={(action) => handleAction(action, item)}
+                  onVote={(e) => {
+                    e.preventDefault();
+                    handleVote(item.comment_id, item.is_vote);
+                  }}
+                />
+              )}
+            </div>
+          );
+        })}
 
-            {currentReplyId === item.comment_id ? (
-              <Reply
-                userName={item.user_display_name}
-                mode={mode}
-                onSendReply={(value) =>
-                  handleSendReply({ ...item, value, type: 'reply' })
-                }
-                onCancel={() => handleCancel(item.comment_id)}
-              />
-            ) : null}
-            {item.showEdit || currentReplyId === item.comment_id ? null : (
-              <ActionBar
-                nickName={item.user_display_name}
-                username={item.username}
-                createdAt={item.created_at}
-                voteCount={item.vote_count}
-                isVote={item.is_vote}
-                memberActions={item.member_actions}
-                userStatus={item.user_status}
-                onReply={() => {
-                  handleReply(item.comment_id);
-                }}
-                onAction={(action) => handleAction(action, item)}
-                onVote={(e) => {
-                  e.preventDefault();
-                  handleVote(item.comment_id, item.is_vote);
-                }}
-              />
-            )}
-          </div>
-        );
-      })}
-
-      <div className={classNames(comments.length > 0 && 'py-2')}>
-        <Button
-          variant="link"
-          className="p-0 btn-no-border"
-          size="sm"
-          onClick={() => {
-            if (tryNormalLogged(true)) {
-              setVisibleComment(!visibleComment);
-            }
-          }}>
-          {t('btn_add_comment')}
-        </Button>
-        {data &&
-          (pageIndex || 1) < Math.ceil((data?.count || 0) / pageSize) && (
+        <div className={classNames(comments.length > 0 && 'py-2')}>
+          {comments.length > 0 && (
             <Button
               variant="link"
+              className="p-0 btn-no-border"
               size="sm"
-              className="p-0 ms-3 btn-no-border"
-              onClick={() => {
-                setPageIndex(pageIndex + 1);
-              }}>
-              {t('show_more', {
-                count:
-                  data.count - (pageIndex === 0 ? 3 : pageIndex * pageSize),
-              })}
+              onClick={handleAddComment}>
+              {t('btn_add_comment')}
             </Button>
           )}
+          {data &&
+            (pageIndex || 1) < Math.ceil((data?.count || 0) / pageSize) && (
+              <Button
+                variant="link"
+                size="sm"
+                className="p-0 ms-3 btn-no-border"
+                onClick={() => {
+                  setPageIndex(pageIndex + 1);
+                }}>
+                {t('show_more', {
+                  count:
+                    data.count - (pageIndex === 0 ? 3 : pageIndex * pageSize),
+                })}
+              </Button>
+            )}
+        </div>
+
+        {visibleComment && (
+          <Form
+            mode={mode}
+            className={classNames(
+              'mt-2',
+              comments.length <= 0 && 'bg-light p-3 rounded',
+            )}
+            onSendReply={(value) => handleSendReply({ value, type: 'comment' 
})}
+            onCancel={() => setVisibleComment(!visibleComment)}
+          />
+        )}
       </div>
-
-      {visibleComment && (
-        <Form
-          mode={mode}
-          className={classNames(
-            'mt-2',
-            comments.length <= 0 && 'bg-light p-3 rounded',
-          )}
-          onSendReply={(value) => handleSendReply({ value, type: 'comment' })}
-          onCancel={() => setVisibleComment(!visibleComment)}
-        />
-      )}
-    </div>
+    </>
   );
 };
 
diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx 
b/ui/src/pages/Questions/Detail/components/Answer/index.tsx
index 6b382dba..474c83d6 100644
--- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx
@@ -36,7 +36,6 @@ import { scrollToElementTop, bgFadeOut } from '@/utils';
 import { AnswerItem } from '@/common/interface';
 import { acceptanceAnswer } from '@/services';
 import { useRenderHtmlPlugin } from '@/utils/pluginKit';
-import Reactions from '../Reactions';
 
 interface Props {
   data: AnswerItem;
@@ -199,8 +198,6 @@ const Index: FC<Props> = ({
         </div>
       </div>
 
-      <Reactions />
-
       <Comment
         objectId={data.id}
         mode="answer"
diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx 
b/ui/src/pages/Questions/Detail/components/Question/index.tsx
index 1252098a..0a76c2f9 100644
--- a/ui/src/pages/Questions/Detail/components/Question/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx
@@ -37,7 +37,6 @@ import { useRenderHtmlPlugin } from '@/utils/pluginKit';
 import { formatCount, guard } from '@/utils';
 import { following } from '@/services';
 import { pathFactory } from '@/router/pathFactory';
-import Reactions from '../Reactions';
 
 interface Props {
   data: any;
@@ -213,8 +212,6 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, 
isLogged }) => {
         </div>
       </div>
 
-      <Reactions />
-
       <Comment
         objectId={data?.id}
         mode="question"
diff --git a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx 
b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx
index 6236c684..5668682a 100644
--- a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx
+++ b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx
@@ -1,32 +1,85 @@
-import { FC, memo } from 'react';
+import { FC, memo, useEffect, useState } from 'react';
 import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
+import { useTranslation } from 'react-i18next';
 
 import { Icon } from '@/components';
+import { queryReactions, updateReaction } from '@/services';
+import { tryNormalLogged } from '@/utils/guard';
 
-interface Props {}
-
-interface Data {
-  title: string;
-  icon: string;
-  className: string;
+interface Props {
+  objectId: string;
+  showAddCommentBtn?: boolean;
+  handleClickComment: () => void;
 }
 
-const data: Data[] = [
-  { title: 'hello', icon: 'heart-fill', className: 'text-danger' },
-  { title: 'hello', icon: 'emoji-laughing-fill', className: 'text-warning' },
-  { title: 'hello', icon: 'emoji-frown-fill', className: 'text-warning' },
+const emojiMap = [
+  {
+    name: 'heart',
+    icon: 'heart-fill',
+    className: 'text-danger',
+  },
+  {
+    name: 'smile',
+    icon: 'emoji-laughing-fill',
+    className: 'text-warning',
+  },
+  {
+    name: 'frown',
+    icon: 'emoji-frown-fill',
+    className: 'text-warning',
+  },
 ];
 
-const Index: FC<Props> = () => {
+const Index: FC<Props> = ({
+  objectId,
+  showAddCommentBtn,
+  handleClickComment,
+}) => {
+  const [reactions, setReactions] = useState<Record<string, string[]>>();
+  const { t } = useTranslation('translation', { keyPrefix: 'comment' });
+
+  useEffect(() => {
+    queryReactions(objectId).then((res) => {
+      setReactions(res);
+    });
+  }, []);
+
+  const handleSubmit = (params: { object_id: string; emoji: string }) => {
+    if (!tryNormalLogged(true)) {
+      return;
+    }
+    updateReaction({ ...params, type: 'toggle' }).then((res) => {
+      setReactions(res);
+    });
+  };
+
+  const convertToTooltip = (names: string[]) => {
+    const n: number = Math.min(5, names.length);
+    let ret = '';
+    for (let i = 0; i < n; i += 1) {
+      if (i === n - 1) {
+        ret += names[i];
+      } else {
+        ret += `${names[i]}, `;
+      }
+    }
+    if (names.length > 5) {
+      ret += ` and ${names.length - 5} more...`;
+    }
+    return ret;
+  };
+
   const renderTooltip = (props) => (
     <Tooltip id="reaction-button-tooltip" {...props} bsPrefix="tooltip">
       <div className="d-block d-md-flex flex-wrap m-0 p-0">
-        {data.map((d) => (
+        {emojiMap.map((d) => (
           <Button
             key={d.icon}
             variant="light"
             size="sm"
-            onClick={() => alert('hellob')}>
+            onClick={() =>
+              handleSubmit({ object_id: objectId, emoji: d.name })
+            }>
             <Icon name={d.icon} className={d.className} />
           </Button>
         ))}
@@ -36,49 +89,55 @@ const Index: FC<Props> = () => {
 
   return (
     <div className="d-block d-md-flex flex-wrap mt-4 mb-3">
-      <Button
-        variant="outline-secondary"
-        className="rounded-pill answer-reaction-btn"
-        size="sm">
-        <Icon name="chat-text-fill" />
-        <span className="ms-1">{6} comments</span>
-      </Button>
+      {showAddCommentBtn && (
+        <Button
+          className="rounded-pill btn-no-border answer-reaction-btn bg-light"
+          size="sm"
+          onClick={handleClickComment}>
+          <Icon name="chat-text-fill" />
+          <span className="ms-1">{t('btn_add_comment')}</span>
+        </Button>
+      )}
 
       <OverlayTrigger trigger="click" placement="top" overlay={renderTooltip}>
         <Button
-          variant="outline-secondary"
           size="sm"
-          className="rounded-pill ms-2 answer-reaction-btn">
+          className="rounded-pill ms-2 answer-reaction-btn bg-light 
btn-no-border">
           <Icon name="emoji-smile-fill" />
           <span className="ms-1">+</span>
         </Button>
       </OverlayTrigger>
 
-      {/* <div className="arrow" /> */}
-
-      {data.map((d) => (
-        <OverlayTrigger
-          placement="top"
-          // trigger="hover"
-          trigger="click"
-          overlay={
-            <Tooltip>
-              <div className="text-start">
-                <b>heart</b> <br /> fenbox, joyqi, robin, andrus, jackathon and
-                7 more...
-              </div>
-            </Tooltip>
-          }>
-          <Button
-            title="hahah"
-            variant="outline-secondary"
-            className="rounded-pill ms-2 answer-reaction-btn"
-            size="sm">
-            <Icon name={d.icon} className={d.className} />
-            <span className="ms-1">{3}</span>
-          </Button>
-        </OverlayTrigger>
-      ))}
+      {reactions &&
+        emojiMap.map((emoji) => {
+          if (!reactions[emoji.name] || reactions[emoji.name].length === 0) {
+            return null;
+          }
+          return (
+            <OverlayTrigger
+              key={emoji.name}
+              placement="top"
+              overlay={
+                <Tooltip>
+                  <div className="text-start">
+                    <b>{emoji.name}</b> <br />{' '}
+                    {convertToTooltip(reactions[emoji.name])}
+                  </div>
+                </Tooltip>
+              }>
+              <Button
+                title="hahah"
+                className="rounded-pill ms-2 answer-reaction-btn bg-light 
btn-no-border"
+                size="sm"
+                onClick={() =>
+                  handleSubmit({ object_id: objectId, emoji: emoji.name })
+                }>
+                <Icon name={emoji.icon} className={emoji.className} />
+                <span className="ms-1">{reactions[emoji.name].length}</span>
+              </Button>
+            </OverlayTrigger>
+          );
+        })}
     </div>
   );
 };
diff --git a/ui/src/pages/Questions/Detail/index.scss 
b/ui/src/pages/Questions/Detail/index.scss
index cf5e910d..72cb02e8 100644
--- a/ui/src/pages/Questions/Detail/index.scss
+++ b/ui/src/pages/Questions/Detail/index.scss
@@ -22,19 +22,30 @@
 .answer-reaction-btn {
   padding-top: 0.2rem;
   padding-bottom: 0.2rem;
-  border: 1px solid $gray-300;
+  color: $secondary;
+  &:hover {
+    background-color: $gray-400 !important;
+    color: $secondary;
+  }
+  &:active {
+    background-color: $gray-500 !important;
+    color: $secondary !important;
+  }
 }
 
 #reaction-button-tooltip {
   .tooltip-inner {
     padding: 0.2rem;
     background-color: $white;
-    border: 1px solid $gray-300;
+    border: 1px solid $gray-500;
     .btn {
       background-color: $white;
       border: none;
       &:hover {
-        background-color: $gray-400;
+        background-color: $gray-100;
+      }
+      &:active {
+        background-color: $gray-300;
       }
     }
   }
@@ -42,15 +53,11 @@
   .tooltip-arrow {
     overflow: hidden;
     bottom: calc(-1 * $tooltip-arrow-height + 1px);
-    // width: calc($tooltip-arrow-width + 4px);
-    // height: calc($tooltip-arrow-height + 2px);
-    // box-sizing: content-box;
     &::before {
-      border: 1px solid $gray-300;
+      border: 1px solid $gray-500;
       width: 50%;
       height: 100%;
       transform: translate(-50%, -50%) rotate(-45deg);
-      // transform: rotate(-45deg);
       box-sizing: content-box;
       left: 50%;
       bottom: 50%;
diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts
index 0676cc71..a783fa49 100644
--- a/ui/src/services/common.ts
+++ b/ui/src/services/common.ts
@@ -88,6 +88,16 @@ export const addComment = (params) => {
   return request.post('/answer/api/v1/comment', params);
 };
 
+export const updateReaction = (params) => {
+  return request.put('/answer/api/v1/meta/reaction', params);
+};
+
+export const queryReactions = (object_id: string) => {
+  return request.get<Record<string, string[]>>(
+    `/answer/api/v1/meta/reaction?object_id=${object_id}`,
+  );
+};
+
 export const queryTags = (tag: string) => {
   return request.get(
     `/answer/api/v1/question/tags?tag=${encodeURIComponent(tag)}`,

Reply via email to