Example #1
0
 render() {
   const {
     addonColor,
     autoFocus,
     className,
     onChange,
     placeholder,
     onFocus,
     style,
     value
   } = this.props
   return (
     <div
       className={`${css`
         display: flex;
         align-items: center;
         width: 100%;
         height: 4.3rem;
         position: relative;
         z-index: 1000;
         .addon {
           border: 1px solid ${addonColor || Color.inputBorderGray()};
           align-self: stretch;
           padding: 0 1rem;
           display: flex;
           align-items: center;
         }
         .glyphicon {
           line-height: 4rem;
           font-size: 1.5rem;
         }
         input {
           height: 100%;
           border: 1px solid ${Color.inputBorderGray()};
           border-left: none;
         }
       `} ${className}`}
       style={style}
     >
       <div
         className="addon"
         style={{ backgroundColor: addonColor || Color.borderGray() }}
       >
         <span className="glyphicon glyphicon-search" />
       </div>
       <Input
         autoFocus={autoFocus}
         onFocus={onFocus && onFocus}
         placeholder={placeholder}
         value={value}
         onChange={text => onChange(text)}
         onKeyDown={this.onKeyDown}
       />
       {this.renderDropdownList()}
     </div>
   )
 }
Example #2
0
export default function RoundList({ children, style = {} }) {
  return (
    <ul
      className={css`
        list-style: none;
        padding: 0;
        margin-top: 0;
        margin-bottom: 0;
        width: 100%;
        font-size: 1.5rem;
        li {
          width: 100%;
          background: #fff;
          padding: 1.5rem;
          border: 1px solid ${Color.borderGray()};
          margin-bottom: -1px;
          @media (max-width: ${mobileMaxWidth}) {
            border-left: 0;
            border-right: 0;
          }
        }
        li:first-child {
          border-top-left-radius: ${borderRadius};
          border-top-right-radius: ${borderRadius};
          @media (max-width: ${mobileMaxWidth}) {
            border-radius: 0;
          }
        }
        li:last-child {
          border-bottom-left-radius: ${borderRadius};
          border-bottom-right-radius: ${borderRadius};
          @media (max-width: ${mobileMaxWidth}) {
            border-radius: 0;
          }
        }
        @media (max-width: ${mobileMaxWidth}) {
          margin-top: 2rem;
        }
      `}
      style={style}
    >
      {children}
    </ul>
  )
}
Example #3
0
import { css } from 'emotion';
import { Color, borderRadius, mobileMaxWidth } from 'constants/css';

export const container = css`
  background: #fff;
  width: 100%;
  border: 1px solid ${Color.borderGray()};
  border-radius: ${borderRadius};
  position: static;
  &:last-child {
    margin-bottom: 0;
  }
  .heading {
    user-select: none;
    padding: 1rem;
    display: flex;
    align-items: center;
    width: 100%;
    justify-content: space-between;
  }
  .body {
    font-size: 1.6rem;
    padding: 0;
    position: static;
    z-index: 10;
    .bottom-interface {
      padding: 0 1rem 0 1rem;
      display: flex;
      flex-direction: column;
      .buttons-bar {
        margin-top: 1rem;
Example #4
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 #5
0
export default function CheckListGroup({
  listItems,
  inputType,
  onSelect,
  style = {}
}) {
  return (
    <div
      className={css`
        display: flex;
        flex-direction: column;
        width: 100%;
        nav {
          border: 1px solid ${Color.borderGray()};
          border-top: none;
        }
        nav:first-of-type {
          border: 1px solid ${Color.borderGray()};
          border-top-left-radius: ${borderRadius};
          border-top-right-radius: ${borderRadius};
          section {
            border-top-left-radius: ${innerBorderRadius};
          }
        }
        nav:last-child {
          border-bottom-left-radius: ${borderRadius};
          border-bottom-right-radius: ${borderRadius};
          section {
            border-bottom-left-radius: ${innerBorderRadius};
          }
        }
      `}
      style={style}
    >
      {listItems.map((listItem, index) => {
        return (
          <nav
            className={css`
              display: flex;
              align-items: center;
              width: 100%;
              cursor: pointer;
              &:hover {
                background: ${Color.highlightGray()};
              }
            `}
            onClick={() => onSelect(index)}
            key={index}
          >
            <section
              className={css`
                height: 4.3rem;
                width: 4.3rem;
                background: ${Color.checkboxAreaGray()};
                display: flex;
                align-items: center;
                justify-content: center;
              `}
            >
              <input
                type={inputType}
                checked={listItem.checked}
                onChange={() => onSelect(index)}
              />
            </section>
            <div
              style={{ padding: '0 2rem' }}
              dangerouslySetInnerHTML={{ __html: listItem.label }}
            />
          </nav>
        );
      })}
    </div>
  );
}
Example #6
0
    @media (max-width: ${mobileMaxWidth}) {
      font-size: 3rem;
      &:hover {
        background: #fff;
        color: #000;
      }
    }
  }

  @media (min-width: ${desktopMinWidth}) {
    top: 0;
  }
  @media (max-width: ${mobileMaxWidth}) {
    bottom: 0;
    height: 9rem;
    border-top: 1px solid ${Color.borderGray()};
  }
`;

export const logo = {
  outer: `desktop ${css`
    cursor: pointer;
    position: relative;
    margin-left: 1rem;
    width: 10rem;
    height: 2rem;
  `}`,
  inner: css`
    font-size: 2rem;
    font-weight: bold;
    font-family: sans-serif, Arial, Helvetica;
Example #7
0
function FilterBar({
  color,
  bordered,
  className,
  children,
  innerRef,
  inverted,
  dropdownButton,
  profileTheme,
  style
}) {
  const themeColor = color || profileTheme || 'logoBlue';
  const selectedOpacity = 1;
  return (
    <div
      style={style}
      ref={innerRef}
      className={`${css`
        background: ${inverted ? Color[themeColor](0.5) : '#fff'};
        height: 6rem;
        margin-bottom: 1rem;
        ${
          !inverted && bordered
            ? `
        border-top: 1px solid ${Color.borderGray()};
        border-left: 1px solid ${Color.borderGray()};
        border-right: 1px solid ${Color.borderGray()};
        border-radius: ${borderRadius};
        `
            : ''
        };
        display: flex;
        font-size: 1.7rem;
        width: 100%;
        align-items: center;
        justify-content: space-between;
        > .filter-section {
          width: 30%;
          height: 100%;
          padding: 0.5rem 1rem;
          display: flex;
          justify-content: flex-end;
          border-bottom: ${inverted ? '' : `1px solid ${Color.borderGray()}`};
        };
        > .nav-section {
          display: flex;
          justify-content: space-between;
          align-items: center;
          height: 100%;
          width: ${!dropdownButton ? '100%' : '70%'};
          > nav {
            font-family: sans-serif, Arial, Helvetica;
            font-weight: bold;
            cursor: pointer;
            display: flex;
            align-items: center;
            justify-content: center;
            height: 100%;
            width: 100%;
            border-bottom: ${inverted ? '' : `1px solid ${Color.borderGray()}`};
            color: ${inverted ? '#fff' : Color.gray()};
            > a {
              color: ${inverted ? '#fff' : Color.gray()};
              text-decoration: none;
            }
            &.alert {
              color: ${Color.gold()}!important;
            }
          }
          > nav.active {
            background: ${inverted ? Color[themeColor](selectedOpacity) : ''};
            border-bottom: ${
              inverted ? '' : `3px solid ${Color[themeColor](selectedOpacity)}`
            };
            color: ${inverted ? '#fff' : Color[themeColor](selectedOpacity)};
            > a {
              color: ${inverted ? '#fff' : Color[themeColor](selectedOpacity)};
            }
            @media (max-width: ${mobileMaxWidth}) {
              border-bottom: ${
                inverted
                  ? ''
                  : `4px solid ${Color[themeColor](selectedOpacity)}`
              };
            }
          }
          > nav.active.alert {
            border-bottom: 3px solid ${Color.gold()}!important;
          }
          > nav:first-of-type {
            ${
              !inverted && bordered ? 'border-bottom-left-radius: 5px;' : ''
            } @media (max-width: ${mobileMaxWidth}) {
              border-bottom-left-radius: 0;
            }
          }
          > nav:last-child {
            @media (max-width: ${mobileMaxWidth}) {
              border-bottom-right-radius: 0;
            }
            ${
              !inverted && bordered && !dropdownButton
                ? 'border-bottom-right-radius: 5px;'
                : ''
            };
          }
          > nav:hover {
            transition: border-bottom 0.5s, background 0.5s;
            background: ${inverted ? Color[themeColor](selectedOpacity) : ''};
            color: ${inverted ? '#fff' : Color[themeColor](selectedOpacity)};
            border-bottom: ${
              inverted ? '' : `3px solid ${Color[themeColor](selectedOpacity)}`
            };
            &.alert {
              color: ${Color.gold()}!important;
              border-bottom: 3px solid ${Color.gold()}!important;
            }
            > a {
              color: ${inverted ? '#fff' : Color[themeColor](selectedOpacity)};
              transition: color 0.5s, font-weight 0.5s;
              font-weight: bold;
            }
          }
        }
        @media (max-width: ${mobileMaxWidth}) {
          height: 5.5rem;
          border-radius: 0;
          border-left: none;
          border-right: none;
        }
      `} ${className}`}
    >
      <div className="nav-section">{children}</div>
      {dropdownButton && <div className="filter-section">{dropdownButton}</div>}
    </div>
  );
}
Example #8
0
export default function FilterBar({
  bordered,
  className,
  children,
  info,
  style,
  success
}) {
  const color = {
    default: Color.blue(),
    success: Color.green(),
    info: Color.lightBlue()
  }
  let colorKey = 'default'
  if (info) colorKey = 'info'
  if (success) colorKey = 'success'
  return (
    <div
      style={style}
      className={`${css`
        background: #fff;
        height: 6rem;
        margin-bottom: 1rem;
        ${
          bordered
            ? `
        border-top: 1px solid ${Color.borderGray()};
        border-left: 1px solid ${Color.borderGray()};
        border-right: 1px solid ${Color.borderGray()};
        border-radius: ${borderRadius};
        `
            : ''
        } display: flex;
        font-size: 1.7rem;
        width: 100%;
        align-items: center;
        justify-content: space-around;
        > nav {
          font-family: sans-serif;
          font-weight: bold;
          cursor: pointer;
          display: flex;
          align-items: center;
          justify-content: center;
          height: 100%;
          width: 100%;
          border-bottom: 1px solid ${Color.borderGray()};
          color: ${Color.menuGray()};
          > a {
            color: ${Color.menuGray()};
            text-decoration: none;
          }
          &.alert {
            color: ${Color.pink()}!important;
          }
        }
        > nav.active {
          border-bottom: 3px solid ${color[colorKey]};
          color: ${color[colorKey]};
          > a {
            color: ${color[colorKey]};
          }
          @media (max-width: ${mobileMaxWidth}) {
            border-bottom: 6px solid ${color[colorKey]};
          }
        }
        > nav.active.alert {
          border-bottom: 3px solid ${Color.pink()}!important;
        }
        > nav:first-child {
          ${
            bordered ? 'border-bottom-left-radius: 5px;' : ''
          } @media (max-width: ${mobileMaxWidth}) {
            border-bottom-left-radius: 0;
          }
        }
        > nav:last-child {
          @media (max-width: ${mobileMaxWidth}) {
            border-bottom-right-radius: 0;
          }
          ${bordered ? 'border-bottom-right-radius: 5px;' : ''};
        }
        > nav:hover {
          transition: border-bottom 0.5s;
          color: ${color[colorKey]};
          border-bottom: 3px solid ${color[colorKey]};
          &.alert {
            color: ${Color.pink()}!important;
            border-bottom: 3px solid ${Color.pink()}!important;
          }
          > a {
            color: ${color[colorKey]};
            transition: color 0.5s, font-weight 0.5s;
            font-weight: bold;
          }
        }
        @media (max-width: ${mobileMaxWidth}) {
          height: 7rem;
          border-radius: 0;
          border-left: none;
          border-right: none;
          border-top: none;
        }
      `} ${className}`}
    >
      {children}
    </div>
  )
}
Example #9
0
function ContentPanel({
  autoExpand,
  commentsLoadLimit,
  contentObj,
  contentObj: { contentId, feedId, newPost, type },
  dispatch,
  inputAtBottom,
  onAddTags,
  onAddTagToContents,
  onAttachStar,
  onByUserStatusChange,
  onCommentSubmit,
  onDeleteComment,
  onDeleteContent,
  onEditComment,
  onEditContent,
  onEditRewardComment,
  onInitContent,
  onLikeContent,
  onLoadMoreComments,
  onLoadMoreReplies,
  onLoadTags,
  onLoadRepliesOfReply,
  onReplySubmit,
  onSetDifficulty,
  onShowComments,
  onTargetCommentSubmit,
  style = {},
  userId
}) {
  const [urlMouseEntered, setUrlMouseEntered] = useState(false);
  const [profileMouseEntered, setProfileMouseEntered] = useState(false);
  const [videoShown, setVideoShown] = useState(false);
  const loading = useRef(false);
  const mounted = useRef(true);
  useEffect(() => {
    mounted.current = true;
    if (!contentObj.loaded && !loading.current) {
      onMount();
    }
    async function onMount() {
      if (!newPost && mounted.current) {
        loading.current = true;
        onInitContent({
          content: await loadContent({ contentId, type }),
          feedId
        });
        loading.current = false;
      }
    }
    return function cleanUp() {
      mounted.current = false;
    };
  }, [contentObj]);
  const isThreaded = !!contentObj.targetObj;
  return (
    <Context.Provider
      value={{
        commentsLoadLimit,
        onAddTags,
        onAddTagToContents,
        onAttachStar,
        onByUserStatusChange,
        onCommentSubmit,
        onDeleteComment,
        onDeleteContent,
        onEditComment,
        onEditContent,
        onEditRewardComment,
        onLikeContent,
        onLoadMoreComments,
        onLoadMoreReplies,
        onLoadTags,
        onLoadRepliesOfReply,
        onReplySubmit,
        onSetDifficulty,
        onShowComments,
        onTargetCommentSubmit
      }}
    >
      <ErrorBoundary>
        <div
          style={{
            height: !contentObj.loaded && '15rem',
            position: 'relative',
            ...style
          }}
        >
          <div
            style={{
              position: 'relative',
              zIndex: 3,
              boxShadow: isThreaded ? `0 4px 10px -3px ${Color.black(0.8)}` : ''
            }}
            className={container}
          >
            {!contentObj.loaded && <Loading />}
            {contentObj.loaded && (
              <>
                <Heading
                  contentObj={contentObj}
                  myId={userId}
                  action={
                    contentObj.commentId
                      ? contentObj.targetObj.comment.notFound
                        ? 'replied on'
                        : 'replied to'
                      : contentObj.rootType === 'subject'
                      ? 'responded to'
                      : contentObj.rootType === 'user'
                      ? 'left a message to'
                      : 'commented on'
                  }
                  onPlayVideoClick={() => setVideoShown(true)}
                  attachedVideoShown={videoShown}
                />
                <div className="body">
                  <Body
                    autoExpand={autoExpand}
                    contentObj={contentObj}
                    inputAtBottom={inputAtBottom}
                    attachedVideoShown={videoShown}
                    myId={userId}
                  />
                </div>
              </>
            )}
          </div>
          {contentObj.loaded && contentObj.targetObj?.comment && (
            <TargetContent
              style={{
                position: 'relative',
                zIndex: 2,
                boxShadow:
                  contentObj.targetObj?.subject ||
                  contentObj.rootType === 'video' ||
                  contentObj.rootType === 'url' ||
                  contentObj.rootType === 'user'
                    ? `0 4px 10px -3px ${Color.black(0.8)}`
                    : ''
              }}
              targetObj={contentObj.targetObj}
              rootObj={contentObj.rootObj}
              rootId={contentObj.rootId}
              rootType={contentObj.rootType}
              feedId={feedId}
            />
          )}
          {contentObj.loaded && contentObj.targetObj?.subject && (
            <div>
              <ContentListItem
                style={{
                  zIndex: 1,
                  position: 'relative',
                  boxShadow:
                    contentObj.rootType === 'video' ||
                    contentObj.rootType === 'url'
                      ? `0 4px 10px -3px ${Color.black(0.8)}`
                      : ''
                }}
                expandable
                onClick={() =>
                  window.open(`/subjects/${contentObj.targetObj.subject.id}`)
                }
                contentObj={contentObj.targetObj.subject}
              />
            </div>
          )}
          {type === 'comment' && contentObj.rootType === 'video' && (
            <ContentListItem
              style={{
                position: 'relative'
              }}
              expandable
              onClick={() => window.open(`/videos/${contentObj.rootObj.id}`)}
              contentObj={contentObj.rootObj}
            />
          )}
          {(type === 'comment' || type === 'subject') &&
            contentObj.rootType === 'url' && (
              <div
                onTouchStart={() => setUrlMouseEntered(true)}
                onMouseEnter={() => setUrlMouseEntered(true)}
                className={css`
                  padding: 1rem;
                  background: ${Color.whiteGray()};
                  margin-top: ${urlMouseEntered ? '-0.5rem' : '-2rem'};
                  border: 1px solid ${Color.borderGray()};
                  border-radius: ${borderRadius};
                  transition: margin-top 0.5s, background 0.5s;
                  &:hover {
                    background: #fff;
                  }
                  @media (max-width: ${mobileMaxWidth}) {
                    margin-top: -0.5rem;
                  }
                `}
              >
                <Embedly
                  small
                  title={cleanString(contentObj.rootObj.title)}
                  url={contentObj.rootObj.content}
                  id={contentObj.rootId}
                  thumbUrl={contentObj.rootObj.thumbUrl}
                  actualTitle={contentObj.rootObj.actualTitle}
                  actualDescription={contentObj.rootObj.actualDescription}
                  siteUrl={contentObj.rootObj.siteUrl}
                />
              </div>
            )}
          {type === 'comment' && contentObj.rootType === 'user' && (
            <div
              onTouchStart={() => setProfileMouseEntered(true)}
              onMouseEnter={() => setProfileMouseEntered(true)}
              className={css`
                cursor: pointer;
                background: ${Color.whiteGray()};
                margin-top: ${profileMouseEntered ? '-0.5rem' : '-2rem'};
                border: 1px solid ${Color.borderGray()};
                border-radius: ${borderRadius};
                transition: margin-top 0.5s, background 0.5s;
                padding-bottom: 1rem;
                &:hover {
                  background: #fff;
                }
                @media (max-width: ${mobileMaxWidth}) {
                  border-left: 0;
                  border-right: 0;
                  margin-top: -0.5rem;
                }
              `}
              onClick={() =>
                window.open(`/users/${contentObj.rootObj.username}`)
              }
            >
              <Profile profile={contentObj.rootObj} />
            </div>
          )}
        </div>
      </ErrorBoundary>
    </Context.Provider>
  );
}
Example #10
0
export const container = ({ username, usernameHovered }) => css`
  display: flex;
  border: none;
  flex-direction: column;
  width: 100%;
  z-index: 400;
  a {
    text-decoration: none;
  }
  .heading {
    padding: 1rem;
    border: 1px solid ${Color.borderGray()};
    border-bottom: none;
    border-radius: ${borderRadius};
    border-bottom-left-radius: 0;
    border-bottom-right-radius: 0;
    display: flex;
    background: #fff;
    width: 100%;
    align-items: center;
    justify-content: flex-start;
    .names {
      width: CALC(100% - 8rem);
      text-align: center;
      overflow: hidden;
      text-overflow: ellipsis;
      a {
        color: ${Color.darkerGray()};
        font-weight: bold;
        font-size: 2.2rem;
      }
      span {
        color: ${Color.darkerGray()};
        font-size: 1.2rem;
      }
    }
    &:hover {
      transition: background 0.5s;
      background: ${Color.highlightGray()};
    }
  }
  .widget__profile-pic {
    width: 8rem;
    height: 8rem;
  }
  .details {
    font-size: 1.3rem;
    border: 1px solid ${Color.borderGray()};
    border-bottom-left-radius: ${borderRadius};
    border-bottom-right-radius: ${borderRadius};
    background: #fff;
    padding: 1rem;
    .login-message {
      font-size: 2rem;
      color: ${Color.darkerGray()};
      font-weight: bold;
    }
  }
  @media (max-width: ${mobileMaxWidth}) {
    border-radius: 0;
    .heading {
      border: 0;
      border-radius: 0;
      justify-content: center;
      .names {
        text-align: center;
        a {
          font-size: 4rem;
        }
        span {
          font-size: 1.5rem;
        }
        width: 50%;
      }
    }
    .details {
      border: 0;
      border-top: 1px solid ${Color.borderGray()};
      border-bottom: 1px solid ${Color.borderGray()};
      border-radius: 0;
      text-align: center;
      font-size: 3rem;
      .login-message {
        font-size: 3rem;
      }
      button {
        font-size: 3rem;
      }
    }
  }
`;
Example #11
0
function SectionPanel({
  button,
  canEdit,
  children,
  customColorTheme,
  emptyMessage,
  innerRef,
  inverted,
  isEmpty,
  isSearching,
  loaded,
  loadMore,
  loadMoreButtonShown,
  onEditTitle,
  onSearch,
  placeholder = 'Enter Title',
  profileTheme,
  searchPlaceholder,
  searchQuery = '',
  style = {},
  title
}) {
  const [loading, setLoading] = useState(false);
  const [onEdit, setOnEdit] = useState(false);
  const [editedTitle, setEditedTitle] = useState(title);
  const themeColor = customColorTheme || profileTheme || 'logoBlue';
  const TitleInputRef = useRef(null);
  useOutsideClick(TitleInputRef, () => {
    setOnEdit(false);
    setEditedTitle(title);
  });

  return (
    <div
      className={css`
        border: 1px solid ${Color.borderGray()};
        width: 100%;
        background: #fff;
        border-radius: ${borderRadius};
        margin-bottom: 1rem;
        > header {
          display: grid;
          width: 100%;
          grid-template-areas: 'title search buttons';
          grid-template-columns: auto ${onSearch ? '40%' : 'auto'} auto;
          background: #fff;
          color: ${profileThemes[themeColor].color};
          border-top-left-radius: ${borderRadius};
          border-top-right-radius: ${borderRadius};
          padding: 1rem;
          padding-top: ${inverted ? '1.7rem' : '1rem'};
          font-weight: bold;
          font-size: 2.5rem;
          align-items: center;
        }
        > main {
          position: relative;
          display: flex;
          flex-direction: column;
          padding: 1rem;
          width: 100%;
          justify-content: center;
          min-height: 15rem;
        }
        @media (max-width: ${mobileMaxWidth}) {
          border-radius: 0;
          border: 0;
          > header {
            border-radius: 0;
          }
        }
      `}
    >
      <header ref={innerRef}>
        <div
          style={{
            gridArea: 'title',
            marginRight: '1rem',
            display: 'flex'
          }}
        >
          <div
            style={{
              display: 'flex',
              flexDirection: 'column',
              width: '100%'
            }}
          >
            {onEdit ? (
              <Input
                inputRef={TitleInputRef}
                maxLength={100}
                placeholder={placeholder}
                autoFocus
                onChange={text => setEditedTitle(addEmoji(text))}
                onKeyPress={event => {
                  if (!stringIsEmpty(editedTitle) && event.key === 'Enter') {
                    onChangeTitle(editedTitle);
                  }
                }}
                value={editedTitle}
              />
            ) : (
              <div style={{ lineHeight: '3rem' }}>{title}</div>
            )}
            {canEdit && onEditTitle && !onEdit ? (
              <div
                className={css`
                  &:hover {
                    text-decoration: underline;
                  }
                `}
                style={{
                  color: Color.gray(),
                  fontWeight: 'normal',
                  marginTop: '0.5rem',
                  fontSize: '1.5rem',
                  cursor: 'pointer',
                  display: 'flex',
                  lineHeight: '1.7rem',
                  alignItems: 'flex-end'
                }}
                onClick={() => {
                  setOnEdit(true);
                  setEditedTitle(title);
                }}
              >
                <span>
                  <Icon icon="pencil-alt" />
                  &nbsp;&nbsp;Edit
                </span>
              </div>
            ) : (
              ''
            )}
          </div>
        </div>
        {onSearch && (
          <SearchInput
            addonColor={themeColor}
            borderColor={themeColor}
            style={{
              color: '#fff',
              gridArea: 'search',
              width: '100%',
              justifySelf: 'center',
              zIndex: 0
            }}
            onChange={search}
            placeholder={searchPlaceholder}
            value={searchQuery}
          />
        )}
        <div style={{ gridArea: 'buttons', justifySelf: 'end' }}>{button}</div>
      </header>
      <main style={style}>
        {loaded ? (
          <Body
            content={children}
            emptyMessage={emptyMessage}
            loadMoreButtonShown={loadMoreButtonShown}
            isEmpty={isEmpty}
            isSearching={isSearching}
            searchQuery={searchQuery}
            statusMsgStyle={css`
              font-size: 3rem;
              font-weight: bold;
              top: 0;
              bottom: 0;
              left: 0;
              right: 0;
              position: absolute;
              display: flex;
              justify-content: center;
              align-items: center;
              color: ${Color.darkerGray()};
            `}
          />
        ) : (
          <Loading />
        )}
        {loadMoreButtonShown && (
          <div style={{ display: 'flex', justifyContent: 'center' }}>
            <Button
              transparent
              disabled={loading}
              onClick={onLoadMore}
              style={{ fontSize: '2rem' }}
            >
              Load More
            </Button>
          </div>
        )}
      </main>
    </div>
  );

  async function onChangeTitle(title) {
    await onEditTitle(title);
    setOnEdit(false);
  }

  function search(text) {
    onSearch(text);
  }

  async function onLoadMore() {
    if (!loading) {
      setLoading(true);
      await loadMore();
      setLoading(false);
    }
  }
}
Example #12
0
function ContentListItem({
  onClick = () => {},
  contentObj,
  contentObj: { type },
  expandable,
  profileTheme,
  selectable,
  selected,
  style
}) {
  const themeColor = profileTheme || 'logoBlue';
  const [mouseEntered, setMouseEntered] = useState(false);
  return (
    <div
      onTouchStart={() => setMouseEntered(true)}
      onMouseEnter={() => setMouseEntered(true)}
      onClick={onClick}
      style={{
        cursor: 'pointer',
        borderRadius,
        boxShadow: selected ? `0 0 5px ${Color[themeColor](0.8)}` : null,
        border: selected ? `0.5rem solid ${Color[themeColor](0.8)}` : null,
        ...style
      }}
      className={css`
        border: 1px solid ${Color.borderGray()};
        background: ${expandable ? Color.whiteGray() : '#fff'};
        .label {
          color: ${expandable ? Color.darkerGray() : Color.darkGray()};
          transition: color 1s;
        }
        margin-top: ${expandable ? (mouseEntered ? '-0.5rem' : '-2rem') : ''};
        transition: background 0.5s, border 0.5s, margin-top 0.5s;
        &:hover {
          border-color: ${Color.darkerBorderGray()};
          .label {
            color: ${expandable ? Color.darkGray() : Color.darkerGray()};
          }
          background: ${expandable ? '#fff' : Color.highlightGray()};
        }
        @media (max-width: ${mobileMaxWidth}) {
          margin-top: -0.5rem;
        }
      `}
    >
      <Link
        style={{ textDecoration: 'none' }}
        to={
          expandable || selectable
            ? ''
            : `/${type === 'url' ? 'link' : type}s/${contentObj.id}`
        }
      >
        <div style={{ padding: '1rem' }}>
          <div
            style={{
              display: 'flex',
              width: '100%',
              fontSize: '1.3rem',
              minHeight: type === 'subject' ? '10rem' : ''
            }}
          >
            {type === 'video' && (
              <div
                style={{
                  display: 'flex',
                  alignItems: 'center',
                  width: '25%'
                }}
              >
                <VideoThumbImage
                  difficulty={contentObj.difficulty}
                  videoId={contentObj.id}
                  src={`https://img.youtube.com/vi/${
                    contentObj.content
                  }/mqdefault.jpg`}
                />
              </div>
            )}
            <div
              style={{
                width: type !== 'subject' && type !== 'url' ? '75%' : '100%',
                padding: '1rem 0',
                ...(type === 'url' ? { paddingTop: '0.5rem' } : {})
              }}
            >
              {type === 'video' && (
                <>
                  <div style={{ marginLeft: '1rem' }}>
                    <p
                      style={{
                        fontSize: '2rem',
                        fontWeight: 'bold',
                        lineHeight: 1.5
                      }}
                      className="label"
                    >
                      {cleanString(contentObj.title)}
                    </p>
                    <p style={{ color: Color.gray() }}>
                      Uploaded by {contentObj.uploader.username}
                    </p>
                  </div>
                  <div
                    style={{
                      marginTop: '1rem',
                      marginLeft: '1rem',
                      color: Color.darkerGray()
                    }}
                  >
                    <LongText
                      className={css`
                        p {
                          text-overflow: ellipsis;
                          overflow: hidden;
                        }
                      `}
                      cleanString
                      noExpand
                      maxLines={4}
                    >
                      {contentObj.description}
                    </LongText>
                  </div>
                </>
              )}
              {type === 'subject' && (
                <div
                  style={{
                    display: 'flex'
                  }}
                >
                  <div className="label">
                    <LongText
                      noExpand
                      cleanString
                      maxLines={4}
                      style={{
                        fontWeight: 'bold',
                        fontSize: '2.5rem'
                      }}
                    >
                      {contentObj.title}
                    </LongText>
                    <p style={{ color: Color.gray() }}>
                      Posted by {contentObj.uploader.username}
                    </p>
                    {contentObj.description && (
                      <div
                        style={{
                          marginTop: '1rem',
                          width: '100%',
                          textAlign: 'left',
                          color: Color.darkerGray()
                        }}
                      >
                        <LongText noExpand cleanString maxLines={4}>
                          {contentObj.description}
                        </LongText>
                      </div>
                    )}
                  </div>
                </div>
              )}
              {type === 'url' && (
                <div>
                  <span
                    style={{
                      fontWeight: 'bold',
                      fontSize: '2rem'
                    }}
                    className="label"
                  >
                    {cleanString(contentObj.title)}
                  </span>
                  <Embedly
                    small
                    noLink
                    style={{ marginTop: '0.5rem' }}
                    title={cleanString(contentObj.title)}
                    url={contentObj.content}
                    {...contentObj}
                  />
                </div>
              )}
            </div>
            {type === 'subject' && contentObj.rootObj?.id && (
              <div
                style={{ display: 'flex', alignItems: 'center', width: '25%' }}
              >
                {contentObj.rootObj?.type === 'video' && (
                  <VideoThumbImage
                    difficulty={contentObj.rootObj.difficulty}
                    videoId={contentObj.rootObj.id}
                    src={`https://img.youtube.com/vi/${
                      contentObj.rootObj.content
                    }/mqdefault.jpg`}
                  />
                )}
                {contentObj.rootObj?.type === 'url' && (
                  <Embedly
                    imageOnly
                    noLink
                    title={cleanString(contentObj.rootObj.title)}
                    url={contentObj.rootObj?.content}
                    {...contentObj.rootObj}
                  />
                )}
              </div>
            )}
          </div>
        </div>
        {!!contentObj.difficulty && type === 'subject' && (
          <div
            style={{
              marginLeft: '-1px',
              marginRight: '-1px',
              paddingBottom: !!contentObj.difficulty && '1rem'
            }}
          >
            <DifficultyBar
              style={{ fontSize: '1.3rem' }}
              difficulty={contentObj.difficulty}
            />
          </div>
        )}
      </Link>
    </div>
  );
}
Example #13
0
export default function MyRank({ myId, rank, twinkleXP }) {
  const rankedColor =
    rank === 1
      ? Color.gold()
      : rank === 2
      ? '#fff'
      : rank === 3
      ? Color.bronze()
      : undefined;
  return (
    <div
      style={{
        marginTop: '1rem',
        marginBottom: myId ? '1rem' : 0,
        background: myId
          ? rank > 0
            ? rank < 4
              ? Color.black()
              : '#fff'
            : '#fff'
          : null
      }}
      className={css`
        width: 100%;
        margin-bottom: 0px;
        text-align: center;
        padding: 1rem;
        border: ${rank > 0 && rank < 4
          ? ''
          : `1px solid ${Color.borderGray()}`};
        border-radius: ${borderRadius};
        p {
          font-weight: bold;
        }
        a {
          font-size: 1.5rem;
          font-weight: bold;
        }
      `}
    >
      {
        <p>
          <span
            style={{
              color: rankedColor || Color.logoGreen(),
              fontSize: '3rem'
            }}
          >
            {twinkleXP ? addCommasToNumber(twinkleXP) : 0}
          </span>{' '}
          <span
            style={{
              color: rankedColor || Color.gold(),
              fontSize: '3rem'
            }}
          >
            XP
          </span>
          &nbsp;&nbsp;
          <span
            style={{
              color:
                rankedColor ||
                (rank > 0 && rank <= 10 ? Color.pink() : Color.darkGray()),
              fontSize: '2rem'
            }}
          >
            {rank ? `Rank #${rank}` : 'Unranked'}
          </span>
        </p>
      }
    </div>
  );
}
Example #14
0
export default function RankBar({ className, profile, style }) {
  const rankColor =
    profile.rank === 1
      ? Color.gold()
      : profile.rank === 2
      ? '#fff'
      : profile.rank === 3
      ? Color.bronze()
      : undefined;

  return (
    <div
      style={style}
      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 ${Color.borderGray()};` : ''}
          background: ${profile.rank < 4 ? Color.black() : '#fff'};
          @media (max-width: ${mobileMaxWidth}) {
            margin-left: 0;
            margin-right: 0;
            border-radius: 0;
            border-left: none;
            border-right: none;
          }
        `} ${className}`}
    >
      <span>
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoBlue() : Color.darkGray())
          }}
        >
          Rank
        </span>{' '}
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoBlue() : Color.darkGray())
          }}
        >
          #{profile.rank}
        </span>{' '}
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoBlue() : Color.darkGray())
          }}
        >
          with
        </span>
      </span>{' '}
      <span>
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.logoGreen() : Color.darkGray())
          }}
        >
          {addCommasToNumber(profile.twinkleXP)}
        </span>{' '}
        <span
          style={{
            color:
              rankColor ||
              (profile.rank <= 10 ? Color.gold() : Color.darkGray())
          }}
        >
          XP
        </span>
        {!!profile.xpThisMonth && (
          <span
            style={{
              fontSize: '1.7rem',
              color:
                rankColor ||
                (profile.xpThisMonth >= 1000 ? Color.pink() : Color.darkGray())
            }}
          >
            {' '}
            (↑
            {addCommasToNumber(profile.xpThisMonth)} this month)
          </span>
        )}
      </span>
    </div>
  );
}