Example #1
0
 ({
   id,
   contentId,
   contentType,
   rewardAmount,
   rewardType,
   rewarderId,
   rewarderUsername,
   timeStamp
 }) => (
   <li
     style={{ background: '#fff' }}
     className={notiFeedListItem}
     key={id}
   >
     <div>
       <UsernameText
         user={{ id: rewarderId, username: rewarderUsername }}
         color={Color.blue()}
       />{' '}
       <span
         style={{
           color:
             rewardAmount === 25
               ? Color.gold()
               : rewardAmount >= 10
               ? Color.rose()
               : rewardAmount >= 5
               ? Color.orange()
               : rewardAmount >= 3
               ? Color.pink()
               : Color.lightBlue(),
           fontWeight: 'bold'
         }}
       >
         rewarded you {rewardAmount === 1 ? 'a' : rewardAmount}{' '}
         {rewardType}
         {rewardAmount > 1 ? 's' : ''}
       </span>{' '}
       for your{' '}
       <ContentLink
         style={{ color: Color.green() }}
         content={{
           id: contentId,
           title: contentType
         }}
         type={contentType}
       />
     </div>
     <small>{timeSince(timeStamp)}</small>
   </li>
 )
Example #2
0
export default function Bar({ index, bar, barPercentage }) {
  return (
    <section
      style={{
        display: 'flex',
        alignItems: 'center',
        width: '100%',
        marginTop: index === 0 ? 0 : '1rem'
      }}
    >
      <div
        style={{
          fontSize: '1.3rem',
          width: '2.5rem'
        }}
      >
        {bar.label}
      </div>
      <div
        style={{
          display: 'inline-block',
          whiteSpace: 'nowrap',
          minWidth: `${barPercentage}%`,
          padding: '0.5rem',
          marginLeft: '1rem',
          background: index === 0 ? Color.orange() : Color.logoBlue(),
          fontSize: '1.5rem',
          color: '#fff',
          textAlign: 'center',
          fontWeight: 'bold'
        }}
      >
        {`${addCommasToNumber(bar.value)} XP`}
      </div>
      {barPercentage < 100 && (
        <div style={{ width: `${100 - barPercentage}%` }} />
      )}
    </section>
  );
}
Example #3
0
function ProfilePanel({
  history,
  profile,
  profile: { id, numMessages },
  userId,
  expandable,
  isCreator,
  openDirectMessageChannel,
  removeStatusMsg,
  updateStatusMsg,
  uploadBio,
  uploadProfilePic,
  username
}) {
  const [bioEditModalShown, setBioEditModalShown] = useState(false);
  const [comments, setComments] = useState([]);
  const [commentsShown, setCommentsShown] = useState(false);
  const [commentsLoadMoreButton, setCommentsLoadMoreButton] = useState(false);
  const [imageUri, setImageUri] = useState();
  const [processing, setProcessing] = useState(false);
  const [imageEditModalShown, setImageEditModalShown] = useState(false);
  const [mouseEnteredProfile, setMouseEnteredProfile] = useState(false);
  const [alertModalShown, setAlertModalShown] = useState(false);
  const CommentInputAreaRef = useRef(null);
  const FileInputRef = useRef(null);
  const mounted = useRef(true);

  useEffect(() => {
    mounted.current = true;
    handleLoadComments();
    async function handleLoadComments() {
      try {
        const { comments } = await loadComments({
          id: profile.id,
          type: 'user',
          limit: 1
        });
        if (mounted.current) {
          setComments(comments);
        }
      } catch (error) {
        console.error(error);
      }
    }
    return function cleanUp() {
      mounted.current = false;
    };
  }, [profile.id]);
  const canEdit = userId === profile.id || isCreator;
  const { profileFirstRow, profileSecondRow, profileThirdRow } = profile;
  const noProfile = !profileFirstRow && !profileSecondRow && !profileThirdRow;

  return (
    <div
      key={profile.id}
      className={css`
        width: 100%;
        margin-bottom: 1rem;
        line-height: 2.3rem;
        font-size: 1.5rem;
        position: relative;
      `}
    >
      <div
        className={css`
          background: ${Color[profile.profileTheme || 'logoBlue']()};
          min-height: 2.5rem;
          border-top-right-radius: ${borderRadius};
          border-top-left-radius: ${borderRadius};
          border-bottom: none;
          display: flex;
          align-items: center;
          justify-content: center;
          @media (max-width: ${mobileMaxWidth}) {
            border-radius: 0;
            border-left: none;
            border-right: none;
          }
        `}
        style={{ padding: profile.userType ? '0.5rem' : undefined }}
      >
        {profile.userType && (
          <div
            style={{
              fontSize: '2.2rem',
              color: '#fff'
            }}
          >
            {profile.userType.includes('teacher')
              ? 'teacher'
              : profile.userType}
          </div>
        )}
      </div>
      <div
        className={css`
          background: #fff;
          display: flex;
          flex-direction: column;
          padding: 1rem;
          border: ${Color.borderGray()} 1px solid;
          ${profile.twinkleXP
            ? 'border-bottom: none;'
            : `
                border-bottom-left-radius: ${borderRadius};
                border-bottom-right-radius: ${borderRadius};
              `};
          border-top: none;
          @media (max-width: ${mobileMaxWidth}) {
            border-radius: 0;
            border-left: none;
            border-right: none;
          }
        `}
      >
        <div style={{ display: 'flex', height: '100%', marginTop: '1rem' }}>
          <div
            style={{
              width: '20rem',
              display: 'flex',
              flexDirection: 'column'
            }}
          >
            <div
              onMouseEnter={() => setMouseEnteredProfile(true)}
              onMouseLeave={() => setMouseEnteredProfile(false)}
            >
              <Link to={`/users/${profile.username}`}>
                <ProfilePic
                  style={{
                    width: '18rem',
                    height: '18rem',
                    cursor: 'pointer'
                  }}
                  userId={profile.id}
                  profilePicId={profile.profilePicId}
                  online={userId === profile.id || !!profile.online}
                  large
                />
              </Link>
            </div>
            <div
              style={{
                display: 'flex',
                justifyContent: 'center',
                alignItems: 'center',
                marginTop: '1.5rem'
              }}
            >
              <Button
                color="orange"
                transparent
                style={{
                  color: mouseEnteredProfile && Color.orange(),
                  padding: '0.5rem'
                }}
                onClick={() => history.push(`/users/${profile.username}`)}
              >
                View Profile
              </Button>
            </div>
            {profile.youtubeUrl && (
              <Button
                color="red"
                transparent
                style={{ padding: '0.5rem' }}
                onClick={() => window.open(profile.youtubeUrl)}
              >
                Visit YouTube
              </Button>
            )}
            {profile.website && (
              <Button
                color="blue"
                transparent
                style={{ padding: '0.5rem' }}
                onClick={() => window.open(profile.website)}
              >
                Visit Website
              </Button>
            )}
          </div>
          <div
            style={{
              marginLeft: '2rem',
              display: 'flex',
              flexDirection: 'column',
              justifyContent: 'space-between',
              position: 'relative',
              width: 'CALC(100% - 19rem)'
            }}
          >
            <UserDetails
              profile={profile}
              removeStatusMsg={removeStatusMsg}
              updateStatusMsg={updateStatusMsg}
              uploadBio={uploadBio}
              userId={userId}
            />
            {canEdit && (
              <div
                style={{
                  zIndex: 1,
                  display: 'flex',
                  flexDirection: 'column'
                }}
              >
                <div style={{ display: 'flex' }}>
                  <Button transparent onClick={onChangeProfilePictureClick}>
                    Change Pic
                  </Button>
                  <Button
                    transparent
                    onClick={() => setBioEditModalShown(true)}
                    style={{ marginLeft: '0.5rem' }}
                  >
                    Edit Bio
                  </Button>
                  {profile.id === userId &&
                    comments.length > 0 &&
                    renderMessagesButton({
                      style: { marginLeft: '0.5rem' }
                    })}
                </div>
              </div>
            )}
            {expandable && userId !== profile.id && (
              <div
                style={{
                  marginTop: noProfile ? '2rem' : '1rem',
                  display: 'flex'
                }}
              >
                {renderMessagesButton()}
                <Button
                  style={{ marginLeft: '0.5rem' }}
                  color="green"
                  onClick={() =>
                    openDirectMessageChannel(
                      { id: userId, username },
                      { id: profile.id, username: profile.username },
                      false
                    )
                  }
                >
                  <Icon icon="comments" />
                  <span style={{ marginLeft: '0.7rem' }}>Talk</span>
                </Button>
              </div>
            )}
            {profile.lastActive && !profile.online && profile.id !== userId && (
              <div
                style={{
                  marginTop: '1rem',
                  fontSize: '1.5rem',
                  color: Color.gray()
                }}
              >
                <p>last online {timeSince(profile.lastActive)}</p>
              </div>
            )}
          </div>
          <input
            ref={FileInputRef}
            style={{ display: 'none' }}
            type="file"
            onChange={handlePicture}
            accept="image/*"
          />
          {bioEditModalShown && (
            <BioEditModal
              firstLine={profileFirstRow}
              secondLine={profileSecondRow}
              thirdLine={profileThirdRow}
              onSubmit={handleUploadBio}
              onHide={() => setBioEditModalShown(false)}
            />
          )}
          {imageEditModalShown && (
            <ImageEditModal
              imageUri={imageUri}
              onHide={() => {
                setImageUri(undefined);
                setImageEditModalShown(false);
                setProcessing(false);
              }}
              processing={processing}
              onConfirm={uploadImage}
            />
          )}
        </div>
        <Comments
          comments={comments}
          commentsLoadLimit={20}
          commentsShown={commentsShown}
          contentId={profile.id}
          inputAreaInnerRef={CommentInputAreaRef}
          inputTypeLabel={`message to ${profile.username}`}
          loadMoreButton={commentsLoadMoreButton}
          noInput={profile.id === userId}
          numPreviews={1}
          onAttachStar={onAttachStar}
          onCommentSubmit={onCommentSubmit}
          onDelete={onDeleteComment}
          onEditDone={onEditComment}
          onLikeClick={onLikeComment}
          onLoadMoreComments={onLoadMoreComments}
          onLoadMoreReplies={onLoadMoreReplies}
          onPreviewClick={onExpandComments}
          onReplySubmit={onReplySubmit}
          onRewardCommentEdit={onEditRewardComment}
          parent={{ ...profile, type: 'user' }}
          style={{ marginTop: '1rem' }}
          userId={userId}
        />
      </div>
      {!!profile.twinkleXP && <RankBar profile={profile} />}
      {alertModalShown && (
        <AlertModal
          title="Image is too large (limit: 5mb)"
          content="Please select a smaller image"
          onHide={() => setAlertModalShown(false)}
        />
      )}
    </div>
  );

  function handlePicture(event) {
    const reader = new FileReader();
    const maxSize = 5000;
    const file = event.target.files[0];
    if (file.size / 1000 > maxSize) {
      return setAlertModalShown(true);
    }
    reader.onload = upload => {
      setImageEditModalShown(true);
      setImageUri(upload.target.result);
    };
    reader.readAsDataURL(file);
    event.target.value = null;
  }

  function onAttachStar(star) {
    setComments(
      comments.map(comment => {
        return {
          ...comment,
          stars:
            comment.id === star.contentId
              ? (comment.stars || []).concat(star)
              : comment.stars || [],
          replies: comment.replies.map(reply => ({
            ...reply,
            stars:
              reply.id === star.contentId
                ? (reply.stars || []).concat(star)
                : reply.stars || []
          }))
        };
      })
    );
  }

  function onCommentSubmit(comment) {
    setComments([comment].concat(comments));
  }

  function onChangeProfilePictureClick() {
    FileInputRef.current.click();
  }

  function onDeleteComment(commentId) {
    setComments(
      comments
        .filter(comment => comment.id !== commentId)
        .map(comment => ({
          ...comment,
          replies: (comment.replies || []).filter(
            reply => reply.id !== commentId
          )
        }))
    );
  }

  function onEditComment({ editedComment, commentId }) {
    setComments(
      comments.map(comment => ({
        ...comment,
        content: comment.id === commentId ? editedComment : comment.content,
        replies: comment.replies
          ? comment.replies.map(reply =>
              reply.id === commentId
                ? {
                    ...reply,
                    content: editedComment
                  }
                : reply
            )
          : []
      }))
    );
  }

  function onEditRewardComment({ id, text }) {
    setComments(
      comments.map(comment => ({
        ...comment,
        stars: comment.stars
          ? comment.stars.map(star => ({
              ...star,
              rewardComment: star.id === id ? text : star.rewardComment
            }))
          : [],
        replies: comment.replies.map(reply => ({
          ...reply,
          stars: reply.stars
            ? reply.stars.map(star => ({
                ...star,
                rewardComment: star.id === id ? text : star.rewardComment
              }))
            : []
        }))
      }))
    );
  }

  async function onExpandComments() {
    if (!commentsShown) {
      const { comments, loadMoreButton } = await loadComments({
        id: profile.id,
        type: 'user',
        limit: 5
      });
      setComments(comments);
      setCommentsShown(true);
      setCommentsLoadMoreButton(loadMoreButton);
    }
  }

  function onLikeComment({ commentId, likes }) {
    setComments(
      comments.map(comment => ({
        ...comment,
        likes: comment.id === commentId ? likes : comment.likes,
        replies: comment.replies
          ? comment.replies.map(reply =>
              reply.id === commentId
                ? {
                    ...reply,
                    likes
                  }
                : reply
            )
          : []
      }))
    );
  }

  function onLoadMoreComments({ comments: newComments, loadMoreButton }) {
    setComments(comments.concat(newComments));
    setCommentsLoadMoreButton(loadMoreButton);
  }

  function onLoadMoreReplies({ commentId, replies, loadMoreButton }) {
    setComments(
      comments.map(comment => ({
        ...comment,
        replies:
          comment.id === commentId
            ? replies.concat(comment.replies)
            : comment.replies,
        loadMoreButton:
          comment.id === commentId ? loadMoreButton : comment.loadMoreButton
      }))
    );
  }

  async function onMessagesButtonClick() {
    await onExpandComments();
    if (profile.id !== userId) CommentInputAreaRef.current.focus();
  }

  function onReplySubmit(data) {
    setComments(
      comments.map(comment => {
        let match = false;
        let commentId = data.replyId || data.commentId;
        if (comment.id === commentId) {
          match = true;
        } else {
          for (let reply of comment.replies || []) {
            if (reply.id === commentId) {
              match = true;
              break;
            }
          }
        }
        return {
          ...comment,
          replies: match ? comment.replies.concat([data]) : comment.replies
        };
      })
    );
  }

  function renderMessagesButton(props = {}) {
    return (
      <Button
        {...props}
        disabled={commentsShown && id === userId}
        color="logoBlue"
        onClick={onMessagesButtonClick}
      >
        <Icon icon="comment-alt" />
        <span style={{ marginLeft: '0.7rem' }}>
          {id === userId ? '' : 'Leave '}
          Message
          {id === userId && Number(numMessages) > 0 && !commentsShown
            ? `${numMessages > 1 ? 's' : ''}`
            : ''}
          {Number(numMessages) > 0 && !commentsShown ? ` (${numMessages})` : ''}
        </span>
      </Button>
    );
  }

  async function handleUploadBio(params) {
    await uploadBio({ ...params, profileId: profile.id });
    setBioEditModalShown(false);
  }

  async function uploadImage(image) {
    setProcessing(true);
    await uploadProfilePic(image);
    setImageUri(undefined);
    setProcessing(false);
    setImageEditModalShown(false);
  }
}
Example #4
0
export default function AlreadyPosted({
  changingPage,
  contentId,
  style,
  type,
  uploaderId,
  url,
  videoCode
}) {
  const [existingContent, setExistingContent] = useState({});
  const [loading, setLoading] = useState(false);
  const mounted = useRef(true);
  useEffect(() => {
    mounted.current = true;
    checkExists();
    async function checkExists() {
      setLoading(true);
      const { content } = await checkIfContentExists({
        type,
        url,
        videoCode
      });
      if (mounted.current) {
        setExistingContent(content);
        setLoading(false);
      }
    }
    return function cleanUp() {
      mounted.current = false;
    };
  }, [url]);
  return !changingPage &&
    existingContent.id &&
    !loading &&
    existingContent.id !== contentId ? (
    <div
      style={{
        fontSize: '1.6rem',
        padding: '1rem',
        color: uploaderId !== existingContent.uploader ? '#000' : '#fff',
        backgroundColor:
          uploaderId !== existingContent.uploader
            ? Color.orange()
            : Color.blue(),
        ...style
      }}
      className={css`
        > a {
          color: ${uploaderId !== existingContent.uploader ? '#000' : '#fff'};
          font-weight: bold;
        }
      `}
    >
      This content has{' '}
      <Link
        style={{ fontWeight: 'bold' }}
        to={`/${type === 'url' ? 'link' : 'video'}s/${existingContent.id}`}
      >
        already been posted before
        {uploaderId !== existingContent.uploader ? ' by someone else' : ''}
      </Link>
    </div>
  ) : null;
}
Example #5
0
function VideoThumbImage({
  difficulty,
  height = '55%',
  playIcon,
  src,
  userId,
  videoId
}) {
  const [xpEarned, setXpEarned] = useState(false);
  const mounted = useRef(true);
  useEffect(() => {
    mounted.current = true;
    checkXpStatus();
    async function checkXpStatus() {
      const authorization = auth();
      const authExists = !!authorization.headers.authorization;
      if (authExists) {
        try {
          const {
            data: { xpEarned }
          } = await request.get(
            `${API_URL}/xpEarned?videoId=${videoId}`,
            auth()
          );
          if (mounted.current) setXpEarned(xpEarned);
        } catch (error) {
          console.error(error.response || error);
        }
      } else {
        setXpEarned(false);
      }
    }
    return function cleanUp() {
      mounted.current = false;
    };
  }, [videoId, difficulty, userId]);

  return (
    <div
      style={{
        display: 'block',
        width: '100%',
        height: 'auto',
        overFlow: 'hidden',
        paddingBottom: height,
        position: 'relative'
      }}
    >
      <img
        alt="Thumbnail"
        src={src}
        style={{
          display: 'block',
          width: '100%',
          height: '100%',
          position: 'absolute',
          top: 0,
          left: 0,
          bottom: 0,
          right: 0,
          margin: 'auto',
          borderBottom: !!xpEarned && `0.8rem solid ${Color.green()}`
        }}
      />
      {playIcon && (
        <a
          className={css`
            position: absolute;
            display: block;
            background: url('/img/play-button-image.png');
            background-size: contain;
            height: 3rem;
            width: 3rem;
            top: 50%;
            left: 50%;
            margin: -1.5rem 0 0 -1.5rem;
          `}
        />
      )}
      {!!difficulty && (
        <div
          style={{
            position: 'absolute',
            padding: '0.1rem 0.5rem',
            background:
              difficulty === 5
                ? Color.gold()
                : difficulty === 4
                ? Color.brownOrange()
                : difficulty === 3
                ? Color.orange()
                : difficulty === 2
                ? Color.pink()
                : Color.logoBlue(),
            fontSize: '1.5rem',
            fontWeight: 'bold',
            color: '#fff'
          }}
        >
          {addCommasToNumber(difficulty * xp)} XP
        </div>
      )}
    </div>
  );
}
Example #6
0
function renderNotificationMessage(notification, myId) {
  const {
    contentId,
    discussionId,
    type,
    reward = {},
    rootType,
    rootTitle,
    rootId,
    userId,
    username,
    commentContent,
    rootCommentUploader,
    discussionTitle,
    discussionUploader
  } = notification
  let action = ''
  let isReplyNotification =
    commentContent && Number(rootCommentUploader) === myId
  let isDiscussionAnswerNotification =
    discussionTitle && discussionUploader === myId
  if (isReplyNotification) {
    action = returnCommentActionText('reply')
  } else if (isDiscussionAnswerNotification) {
    action = returnCommentActionText('comment')
  } else {
    switch (type) {
      case 'reward':
        action = (
          <Fragment>
            <span
              style={{
                fontWeight: 'bold',
                color: reward.rewardAmount > 1 ? Color.gold() : Color.orange()
              }}
            >
              rewarded you{' '}
              {reward.rewardAmount === 1 ? 'a' : reward.rewardAmount}{' '}
              {reward.rewardType}
              {reward.rewardAmount > 1 ? 's' : ''}
            </span>{' '}
            for
          </Fragment>
        )
        break
      case 'like':
        action = 'likes'
        break
      case 'comment':
        action =
          rootType === 'question'
            ? returnCommentActionText('answer')
            : returnCommentActionText('comment')
        break
      case 'discussion':
        action = 'added a discussion to'
        break
      default:
        break
    }
  }
  const target = `your ${
    isReplyNotification
      ? 'comment'
      : isDiscussionAnswerNotification
        ? 'discussion topic'
        : rootType
  }: `
  let contentTitle = isReplyNotification
    ? commentContent
    : (isDiscussionAnswerNotification ? discussionTitle : rootTitle) || ''
  let title =
    contentTitle.length > 50 ? contentTitle.substr(0, 50) + '...' : contentTitle
  if (isReplyNotification) title = `"${title}"`
  const content = {
    title,
    id: isReplyNotification
      ? contentId
      : type === 'discussion'
        ? contentId
        : isDiscussionAnswerNotification
          ? discussionId
          : rootId
  }
  return (
    <div>
      <UsernameText
        user={{ id: userId, name: username }}
        color={Color.blue()}
      />
      &nbsp;{action} {target}
      <ContentLink
        content={content}
        type={
          isReplyNotification
            ? 'comment'
            : type === 'discussion' || isDiscussionAnswerNotification
              ? 'discussion'
              : rootType
        }
      />
    </div>
  )

  function returnCommentActionText(type) {
    const title =
      type === 'comment'
        ? 'commented on'
        : type === 'reply'
          ? 'replied to'
          : 'answered'
    return (
      <ContentLink
        style={{ color: Color.green() }}
        content={{ id: contentId, title }}
        type="comment"
      />
    )
  }
}
Example #7
0
export default function RankBar({ profile }) {
  const rankColor =
    profile.rank === 1
      ? Color.gold()
      : profile.rank === 2
        ? Color.silver()
        : profile.rank === 3
          ? '#fff'
          : undefined
  return (
    <div
      className={css`
          padding: 1.5rem 0;
          font-size: 2rem;
          color: ${rankColor};
          font-weight: bold;
          text-align: center;
          border-bottom-left-radius: ${borderRadius};
          border-bottom-right-radius: ${borderRadius};
          ${profile.rank > 3 ? `border: 1px solid #e7e7e7;` : ''}
          background: ${
            profile.rank < 3
              ? Color.black(1 - (profile.rank - 1) / 10)
              : profile.rank === 3
                ? Color.orange()
                : Color.whiteGray()
          };
          @media (max-width: ${mobileMaxWidth}) {
            border-radius: 0;
            border-left: none;
            border-right: none;
          }
        `}
    >
      <span>
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoBlue() : Color.buttonGray())
          }}
        >
          Rank
        </span>{' '}
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoBlue() : Color.buttonGray())
          }}
        >
          #{profile.rank}
        </span>{' '}
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoBlue() : Color.buttonGray())
          }}
        >
          with
        </span>
      </span>{' '}
      <span>
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoGreen() : Color.buttonGray())
          }}
        >
          {addCommasToNumber(profile.twinkleXP)}
        </span>{' '}
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.gold() : Color.buttonGray())
          }}
        >
          XP
        </span>
      </span>
    </div>
  )
}