WIP Comments

This commit is contained in:
kvakazyambra 2022-11-27 00:27:54 +03:00
parent f6ec3558d6
commit 57f1d026da
11 changed files with 410 additions and 169 deletions

View File

@ -1,3 +1,3 @@
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M1 1L6 6M6 6L1 11M6 6L11 1M6 6L11 11" stroke="#696969" stroke-width="2"/>
<path d="M1 1L6 6M6 6L1 11M6 6L11 1M6 6L11 11" stroke="#000" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 186 B

After

Width:  |  Height:  |  Size: 183 B

View File

@ -1,3 +1,4 @@
<svg width="12" height="14" viewBox="0 0 12 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6 0C2.69432 0 0 2.6826 0 5.97389C0 9.26518 2.69432 11.9478 6 11.9478V14L7.02604 13.3453C8.5166 12.3934 11.2347 10.3384 11.8659 7.22363C11.9523 6.82188 12 6.40385 12 5.97389C12 2.6826 9.30568 0 6 0Z" fill="#696969"/>
<path
d="M6 0C2.69432 0 0 2.6826 0 5.97389C0 9.26518 2.69432 11.9478 6 11.9478V14L7.02604 13.3453C8.5166 12.3934 11.2347 10.3384 11.8659 7.22363C11.9523 6.82188 12 6.40385 12 5.97389C12 2.6826 9.30568 0 6 0Z" fill="#000"/>
</svg>

Before

Width:  |  Height:  |  Size: 329 B

After

Width:  |  Height:  |  Size: 328 B

View File

@ -0,0 +1,204 @@
.comment {
background-color: #fff;
margin: 0 -2.4rem 1.5em;
padding: 0.8rem 2.4rem;
transition: background-color 0.3s;
&:hover {
background-color: #f6f6f6;
.commentControlShare,
.commentControlDelete,
.commentControlEdit,
.commentControlComplain {
opacity: 1;
}
}
.shout-body {
@include font-size(1.5rem);
margin-bottom: 1em;
*:last-child {
margin-bottom: 0;
}
}
.author {
align-items: center;
margin-bottom: 1.4rem;
}
}
.commentLevel1 {
margin-left: 3.2rem;
}
.commentLevel2 {
margin-left: 6.4rem;
}
.commentLevel3 {
margin-left: 9.6rem;
}
.commentLevel4 {
margin-left: 12.8rem;
}
.commentLevel5 {
margin-left: 16rem;
}
.commentControls {
@include font-size(1.2rem);
margin-bottom: 0.5em;
}
.commentControlShare,
.commentControlDelete,
.commentControlEdit,
.commentControlComplain {
@include media-breakpoint-up(md) {
opacity: 0;
transition: opacity 0.3s;
}
}
.commentControlShare,
.commentControlDelete,
.commentControlEdit {
.icon {
line-height: 1.2;
}
}
.commentControlShare {
.icon {
height: 1.2rem;
width: 1.2rem;
}
}
.commentControl {
border: none;
color: #696969;
cursor: pointer;
display: inline-flex;
line-height: 1.2;
margin-right: 0.8rem;
padding: 0.2em 0.3em;
transition: opacity 0.2s, color 0.3s, background-color 0.3s;
vertical-align: top;
&:hover {
background: #000;
color: #fff;
.icon {
filter: invert(1);
opacity: 1;
}
}
.icon {
filter: invert(0);
margin-right: 0.3em;
opacity: 0.6;
transition: filter 0.3s, opacity 0.2s;
img {
margin-bottom: -0.1em;
}
}
}
.commentControlReply {
.icon {
height: 1.2em;
width: 1.2em;
}
}
.commentBody {
@include font-size(1.5rem);
line-height: 1.47;
}
.commentAuthor,
.commentDate,
.commentRating {
@include font-size(1.2rem);
}
.commentDate {
color: rgb(0 0 0 / 30%);
flex: 1;
@include media-breakpoint-down(md) {
margin-left: 1rem;
}
}
.commentDetails {
display: flex;
margin-bottom: 1.2rem;
}
.commentRating {
align-items: center;
display: flex;
font-weight: bold;
}
.commentRatingValue {
padding: 0 0.3em;
}
.commentRatingPositive {
color: #2bb452;
}
.commentRatingNegative {
color: #d00820;
}
.commentRatingControl {
border-left: 6px solid transparent;
border-right: 6px solid transparent;
height: 0;
width: 0;
}
.commentRatingControlUp {
border-bottom: 8px solid rgb(0 0 0 / 40%);
}
.commentRatingControlDown {
border-top: 8px solid rgb(0 0 0 / 40%);
}
.replyForm {
background: #fff;
border: 2px solid rgb(38 56 217 / 50%);
border-radius: 0.8rem;
margin-left: 2.4rem;
position: relative;
textarea {
border: none;
border-radius: 0.8rem;
padding-top: 1.2rem;
}
}
.replyFormControls {
padding: 0.5rem 1.6rem 1.6rem;
text-align: right;
button {
@include font-size(1.6rem);
margin-left: 1.2rem;
}
}

View File

@ -1,119 +0,0 @@
.comment {
background-color: #fff;
margin: 0 -2.4rem 1.5em;
padding: 0.8rem 2.4rem;
transition: background-color 0.3s;
&:hover {
background-color: #f6f6f6;
.comment-control--share,
.comment-control--delete,
.comment-control--edit,
.comment-control--complain {
opacity: 1;
}
}
.shout-body {
@include font-size(1.5rem);
margin-bottom: 1em;
*:last-child {
margin-bottom: 0;
}
}
.circlewrap {
position: absolute;
}
.author {
align-items: center;
margin-bottom: 1.4rem;
}
.author__name {
font-weight: bold;
@include font-size(1.2rem);
margin-bottom: 0;
}
.author__details {
margin-left: 4rem;
}
.shout-date {
@include font-size(1.2rem);
flex: 1;
color: rgb(0 0 0 / 30%);
}
}
.comment--level-1 {
margin-left: 2.4rem;
}
.comment--level-2 {
margin-left: 4.8rem;
}
.comment--level-3 {
margin-left: 7.2rem;
}
.comment--level-4 {
margin-left: 9.6rem;
}
.comment--level-5 {
margin-left: 12rem;
}
.shout-controls {
align-items: baseline;
display: flex;
justify-content: space-between;
padding-top: 0.8rem;
}
.comment-controls {
margin-bottom: 0.5em;
}
.comment-control--share,
.comment-control--delete,
.comment-control--edit,
.comment-control--complain {
opacity: 0;
transition: opacity 0.3s;
}
.comment-control {
background: rgb(0 0 0 / 5%);
border: none;
cursor: pointer;
display: inline-flex;
line-height: 1.2;
margin-right: 0.8rem;
padding: 0.2em 0.3em;
vertical-align: top;
.icon {
margin-right: 0.3em;
img {
margin-bottom: -0.1em;
}
}
}
.comment-control--reply {
.icon {
height: 1.2em;
width: 1.2em;
}
}

View File

@ -1,13 +1,16 @@
import './Comment.scss'
import styles from './Comment.module.scss'
import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card'
import { Show, createMemo } from 'solid-js'
import { Show, createMemo, createSignal } from 'solid-js'
import { clsx } from 'clsx'
import type { Author, Reaction as Point } from '../../graphql/types.gen'
import { t } from '../../utils/intl'
// import { createReaction, updateReaction, deleteReaction } from '../../stores/zine/reactions'
import MD from './MD'
import { deleteReaction } from '../../stores/zine/reactions'
import { formatDate } from '../../utils'
import { SharePopup } from './SharePopup'
import stylesHeader from '../Nav/Header.module.scss'
export default (props: {
level?: number
@ -15,6 +18,9 @@ export default (props: {
canEdit?: boolean
compact?: boolean
}) => {
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
const [isReplyVisible, setIsReplyVisible] = createSignal(false)
const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim())
const remove = () => {
@ -23,19 +29,22 @@ export default (props: {
deleteReaction(comment().id)
}
}
const formattedDate = createMemo(() =>
formatDate(new Date(comment()?.createdAt), { hour: 'numeric', minute: 'numeric' })
)
return (
<div class={clsx('comment', { [`comment--level-${props.level}`]: Boolean(props.level) })}>
<div class={clsx(styles.comment, { [styles[`commentLevel${props.level}`]]: Boolean(props.level) })}>
<Show when={!!body()}>
<div class="comment__content">
<div class={styles.commentContent}>
<Show
when={!props.compact}
fallback={
<div class="comment__details">
<div class={styles.commentDetails}>
<a href={`/author/${comment()?.createdBy?.slug}`}>
@{(comment()?.createdBy || { name: 'anonymous' }).name}
</a>
<div class="comment__article">
<div class={styles.commentArticle}>
<Icon name="reply-arrow" />
<a href={`#comment-${comment()?.id}`}>
#{(comment()?.shout || { title: 'Lorem ipsum titled' }).title}
@ -44,60 +53,100 @@ export default (props: {
</div>
}
>
<div class="comment__details">
<div class="comment-author">
<div class={styles.commentDetails}>
<div class={styles.commentAuthor}>
<AuthorCard
author={comment()?.createdBy as Author}
hideDescription={true}
hideFollow={true}
isComments={true}
hasLink={true}
/>
</div>
<div class="comment-date">{comment()?.createdAt}</div>
<div class="comment-rating">{comment().stat?.rating || 0}</div>
<div class={styles.commentDate}>{formattedDate()}</div>
<div
class={styles.commentRating}
classList={{
[styles.commentRatingPositive]: comment().stat?.rating > 0,
[styles.commentRatingNegative]: comment().stat?.rating < 0
}}
>
<button class={clsx(styles.commentRatingControl, styles.commentRatingControlUp)}></button>
<div class={styles.commentRatingValue}>{comment().stat?.rating || 0}</div>
<button class={clsx(styles.commentRatingControl, styles.commentRatingControlDown)}></button>
</div>
</div>
</Show>
<div class="comment-body" contenteditable={props.canEdit} id={'comment-' + (comment().id || '')}>
<div
class={styles.commentBody}
contenteditable={props.canEdit}
id={'comment-' + (comment().id || '')}
>
<MD body={body()} />
</div>
<Show when={!props.compact}>
<div class="comment-controls">
<button class="comment-control comment-control--reply">
<Icon name="reply" />
<div class={styles.commentControls}>
<button
class={clsx(styles.commentControl, styles.commentControlReply)}
onClick={() => setIsReplyVisible(!isReplyVisible())}
>
<Icon name="reply" class={styles.icon} />
{t('Reply')}
</button>
<Show when={props.canEdit}>
{/*FIXME implement edit comment modal*/}
{/*<button*/}
{/* class="comment-control comment-control--edit"*/}
{/* class={clsx(styles.commentControl, styles.commentControlEdit)}*/}
{/* onClick={() => showModal('editComment')}*/}
{/*>*/}
{/* <Icon name="edit" />*/}
{/* <Icon name="edit" class={styles.icon} />*/}
{/* {t('Edit')}*/}
{/*</button>*/}
<button class="comment-control comment-control--delete" onClick={() => remove()}>
<Icon name="delete" />
<button
class={clsx(styles.commentControl, styles.commentControlDelete)}
onClick={() => remove()}
>
<Icon name="delete" class={styles.icon} />
{t('Delete')}
</button>
</Show>
{/*FIXME implement modals */}
<SharePopup
onVisibilityChange={(isVisible) => {
setIsSharePopupVisible(isVisible)
}}
containerCssClass={stylesHeader.control}
trigger={
<button class={clsx(styles.commentControl, styles.commentControlShare)}>
<Icon name="share" class={styles.icon} />
{t('Share')}
</button>
}
/>
{/*<button*/}
{/* class="comment-control comment-control--share"*/}
{/* onClick={() => showModal('shareComment')}*/}
{/*>*/}
{/* {t('Share')}*/}
{/*</button>*/}
{/*<button*/}
{/* class="comment-control comment-control--complain"*/}
{/* class={clsx(styles.commentControl, styles.commentControlComplain)}*/}
{/* onClick={() => showModal('reportComment')}*/}
{/*>*/}
{/* {t('Report')}*/}
{/*</button>*/}
</div>
<Show when={isReplyVisible()}>
<form class={styles.replyForm}>
<textarea name="reply" id="reply" rows="5"></textarea>
<div class={styles.replyFormControls}>
<button class="button button--light" onClick={() => setIsReplyVisible(false)}>
Отмена
</button>
<button class="button">Отправить</button>
</div>
</form>
</Show>
</Show>
</div>
</Show>

View File

@ -7,6 +7,7 @@ import styles from '../../styles/Article.module.scss'
import { useReactionsStore } from '../../stores/zine/reactions'
import { createEffect, createMemo, createSignal, onMount, Suspense } from 'solid-js'
import type { Reaction } from '../../graphql/types.gen'
import { clsx } from 'clsx'
const ARTICLE_COMMENTS_PAGE_SIZE = 50
const MAX_COMMENT_LEVEL = 6
@ -44,9 +45,27 @@ export const CommentsTree = (props: { shout: string; reactions?: Reaction[] }) =
return (
<>
<Show when={reactions()}>
<h2 id="comments">
{t('Comments')} {reactions().length.toString() || ''}
</h2>
<div class={styles.commentsHeaderWrapper}>
<h2 id="comments" class={styles.commentsHeader}>
{t('Comments')} {reactions().length.toString() || ''}
</h2>
<ul class={clsx(styles.commentsViewSwitcher, 'view-switcher')}>
<li class="selected">
<a href="#">По порядку</a>
</li>
<li>
<a href="#">По рейтингу</a>
</li>
</ul>
</div>
<form class={styles.commentForm}>
<div class="pretty-form__item">
<input type="text" id="new-comment" placeholder="Коментарий" />
<label for="new-comment">Коментарий</label>
</div>
</form>
<For each={reactions()}>
{(reaction: Reaction) => (

View File

@ -1,4 +1,4 @@
import { capitalize } from '../../utils'
import { capitalize, formatDate } from '../../utils'
import './Full.scss'
import { Icon } from '../_shared/Icon'
import { AuthorCard } from '../Author/Card'
@ -16,16 +16,6 @@ interface ArticleProps {
article: Shout
}
const formatDate = (date: Date) => {
return date
.toLocaleDateString('ru', {
month: 'long',
day: 'numeric',
year: 'numeric'
})
.replace(' г.', '')
}
export const FullArticle = (props: ArticleProps) => {
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
@ -90,7 +80,7 @@ export const FullArticle = (props: ArticleProps) => {
<div class="col-md-8 shift-content">
<div class={styles.shoutStats}>
<div class={styles.shoutStatsItem}>
<RatingControl rating={props.article.stat?.rating} />
<RatingControl rating={props.article.stat?.rating} class={styles.ratingControl} />
</div>
<div class={styles.shoutStatsItem}>
@ -139,6 +129,11 @@ export const FullArticle = (props: ArticleProps) => {
</div>
</div>
<div class={styles.help}>
<button class="button">Соучаствовать</button>
<button class="button button--light">Пригласить к участию</button>
</div>
<div class={styles.topicsList}>
<For each={props.article.topics}>
{(topic) => (
@ -155,7 +150,7 @@ export const FullArticle = (props: ArticleProps) => {
</Show>
<For each={props.article?.authors}>
{(a: Author) => (
<div class="col-md-6">
<div class="col-xl-6">
<AuthorCard author={a} compact={false} hasLink={true} liteButtons={true} />
</div>
)}

View File

@ -49,7 +49,10 @@
.authorSubscribe {
align-items: center;
display: flex;
padding: 0 0 0 42px;
@include media-breakpoint-up(sm) {
padding: 0 0 0 42px;
}
a {
background: #f7f7f7;
@ -256,3 +259,14 @@
display: block;
}
}
.authorComments {
.authorName {
@include font-size(1.2rem);
margin-bottom: 0;
}
.circlewrap {
margin-top: -0.6em;
}
}

View File

@ -23,6 +23,7 @@ interface AuthorCardProps {
isAuthorsList?: boolean
truncateBio?: boolean
liteButtons?: boolean
isComments?: boolean
}
export const AuthorCard = (props: AuthorCardProps) => {
@ -47,6 +48,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
class={clsx(styles.author)}
classList={{
[styles.authorPage]: props.isAuthorPage,
[styles.authorComments]: props.isComments,
[styles.authorsListItem]: props.isAuthorsList
}}
>
@ -55,6 +57,7 @@ export const AuthorCard = (props: AuthorCardProps) => {
hasLink={props.hasLink}
isBig={props.isAuthorPage}
isAuthorsList={props.isAuthorsList}
class={styles.circlewrap}
/>
<div class={styles.authorDetails}>

View File

@ -77,7 +77,9 @@ img {
}
.shoutAuthorsList {
margin-top: 2em;
border-bottom: 1px solid #e8e8e8;
margin: 2em 0;
padding-bottom: 2em;
h4 {
color: #696969;
@ -118,15 +120,18 @@ img {
}
.shoutStats {
border-bottom: 1px solid #e8e8e8;
border-top: 4px solid #000;
display: flex;
justify-content: flex-start;
padding: 3.2rem 0;
padding: 3rem 0 0;
@include media-breakpoint-down(sm) {
flex-wrap: wrap;
}
}
.shoutStatsItem {
@include font-size(1.7rem);
@include font-size(1.5rem);
font-weight: 500;
display: inline-block;
@ -179,18 +184,32 @@ img {
.icon {
opacity: 0.4;
}
@include media-breakpoint-down(sm) {
flex: 1 100%;
}
}
.shoutStatsItemAdditionalDataItem {
font-weight: normal;
display: inline-block;
margin-left: 2rem;
margin-right: 0;
@include media-breakpoint-down(sm) {
&:first-child {
margin-left: 0;
}
}
}
.topicsList {
@include font-size(1.2rem);
border-bottom: 1px solid #e8e8e8;
letter-spacing: 0.08em;
margin: 1.6rem 0;
margin-top: 1.6rem;
padding-bottom: 1.6rem;
.shoutTopic {
display: inline-block;
@ -210,3 +229,45 @@ img {
}
}
}
.commentsHeaderWrapper {
display: flex;
justify-content: space-between;
}
.commentsHeader {
@include font-size(2.4rem);
margin-bottom: 1em;
}
.ratingControl {
button {
font-size: 2.225rem;
}
}
.commentForm {
margin-bottom: 2.4rem;
input,
textarea {
border-radius: 0.8rem !important;
}
}
.commentsViewSwitcher {
margin-top: 0;
}
.help {
border-bottom: 1px solid #e8e8e8;
margin-bottom: 1.6rem;
padding-bottom: 3.2rem;
button {
@include font-size(1.5rem);
border-radius: 0.8rem;
margin-right: 1.2rem;
padding: 1.1rem 1.2rem 0.9rem;
}
}

View File

@ -67,3 +67,17 @@ export const snake2camel = (s: string) =>
.split(/(?=[A-Z])/)
.join('-')
.toLowerCase()
export const formatDate = (date: Date, options: Intl.DateTimeFormatOptions = {}) => {
const opts = Object.assign(
{},
{
month: 'long',
day: 'numeric',
year: 'numeric'
},
options
)
return date.toLocaleDateString('ru', opts).replace(' г.', '')
}