This commit is contained in:
tonyrewin 2022-11-24 11:07:56 +03:00
commit 1054dee23d
30 changed files with 437 additions and 373 deletions

View File

@ -0,0 +1,25 @@
const { chromium } = require('playwright')
const checkUrl = async (page, targetUrl, pageName) => {
const response = await page.goto(targetUrl)
if (response.status() > 399) {
throw new Error(`Failed with response code ${response.status()}`)
}
await page.screenshot({ path: `${pageName}.jpg` })
}
async function run() {
const browser = await chromium.launch()
const page = await browser.newPage()
const targetUrl = process.env.ENVIRONMENT_URL || 'https://testing.discours.io'
await checkUrl(page, targetUrl, 'main')
await checkUrl(page, `${targetUrl}/authors`, 'authors')
await checkUrl(page, `${targetUrl}/topics`, 'topics')
await page.close()
await browser.close()
}
await run()

View File

@ -26,6 +26,7 @@
"server": "node server/server.mjs", "server": "node server/server.mjs",
"start": "astro dev", "start": "astro dev",
"start:local": "cross-env PUBLIC_API_URL=http://127.0.0.1:8080 astro dev", "start:local": "cross-env PUBLIC_API_URL=http://127.0.0.1:8080 astro dev",
"start:staging": "cross-env PUBLIC_API_URL=https://testapi.discours.io astro dev",
"typecheck": "astro check && tsc --noEmit", "typecheck": "astro check && tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch", "typecheck:watch": "tsc --noEmit --watch",
"vercel-build": "astro build" "vercel-build": "astro build"

View File

@ -3,13 +3,17 @@ import './Full.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import ArticleComment from './Comment' import ArticleComment from './Comment'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { createMemo, For, onMount, Show } from 'solid-js' import { createMemo, createSignal, For, onMount, Show } from 'solid-js'
import type { Author, Reaction, Shout } from '../../graphql/types.gen' import type { Author, Reaction, Shout } from '../../graphql/types.gen'
import { t } from '../../utils/intl' import { t } from '../../utils/intl'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import MD from './MD' import MD from './MD'
import { SharePopup } from './SharePopup' import { SharePopup } from './SharePopup'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import stylesHeader from '../Nav/Header.module.scss'
import styles from '../../styles/Article.module.scss'
import RatingControl from './RatingControl'
import { clsx } from 'clsx'
const MAX_COMMENT_LEVEL = 6 const MAX_COMMENT_LEVEL = 6
@ -39,6 +43,7 @@ const formatDate = (date: Date) => {
export const FullArticle = (props: ArticleProps) => { export const FullArticle = (props: ArticleProps) => {
const { session } = useSession() const { session } = useSession()
const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt))) const formattedDate = createMemo(() => formatDate(new Date(props.article.createdAt)))
const [isSharePopupVisible, setIsSharePopupVisible] = createSignal(false)
const mainTopic = () => const mainTopic = () =>
(props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic)?.title || '').replace( (props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic)?.title || '').replace(
@ -64,8 +69,8 @@ export const FullArticle = (props: ArticleProps) => {
return ( return (
<div class="shout wide-container"> <div class="shout wide-container">
<article class="col-md-6 shift-content"> <article class="col-md-6 shift-content">
<div class="shout__header"> <div class={styles.shoutHeader}>
<div class="shout__topic"> <div class={styles.shoutTopic}>
<a href={`/topic/${props.article.mainTopic}`} innerHTML={mainTopic() || ''} /> <a href={`/topic/${props.article.mainTopic}`} innerHTML={mainTopic() || ''} />
</div> </div>
@ -74,7 +79,7 @@ export const FullArticle = (props: ArticleProps) => {
<h4>{capitalize(props.article.subtitle, false)}</h4> <h4>{capitalize(props.article.subtitle, false)}</h4>
</Show> </Show>
<div class="shout__author"> <div class={styles.shoutAuthor}>
<For each={props.article.authors}> <For each={props.article.authors}>
{(a: Author, index) => ( {(a: Author, index) => (
<> <>
@ -84,11 +89,11 @@ export const FullArticle = (props: ArticleProps) => {
)} )}
</For> </For>
</div> </div>
<div class="shout__cover" style={{ 'background-image': `url('${props.article.cover}')` }} /> <div class={styles.shoutCover} style={{ 'background-image': `url('${props.article.cover}')` }} />
</div> </div>
<Show when={Boolean(props.article.body)}> <Show when={Boolean(props.article.body)}>
<div class="shout__body"> <div class={styles.shoutBody}>
<Show <Show
when={!props.article.body.startsWith('<')} when={!props.article.body.startsWith('<')}
fallback={<div innerHTML={props.article.body} />} fallback={<div innerHTML={props.article.body} />}
@ -100,63 +105,82 @@ export const FullArticle = (props: ArticleProps) => {
</article> </article>
<div class="col-md-8 shift-content"> <div class="col-md-8 shift-content">
<div class="shout-stats"> <div class={styles.shoutStats}>
<div class="shout-stats__item shout-stats__item--likes"> <div class={styles.shoutStatsItem}>
<Icon name="like" /> <RatingControl rating={props.article.stat?.rating} />
</div>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemLikes)}>
<Icon name="like" class={styles.icon} />
{props.article.stat?.rating || ''} {props.article.stat?.rating || ''}
</div> </div>
<div class="shout-stats__item"> <div class={styles.shoutStatsItem}>
<Icon name="comment" /> <Icon name="comment" class={styles.icon} />
{props.article.stat?.commented || ''} {props.article.stat?.commented || ''}
</div> </div>
<div class="shout-stats__item">
<Icon name="view" />
{props.article.stat?.viewed}
</div>
{/*FIXME*/} {/*FIXME*/}
{/*<div class="shout-stats__item">*/} {/*<div class={styles.shoutStatsItem}>*/}
{/* <a href="#bookmark" onClick={() => console.log(props.article.slug, 'articles')}>*/} {/* <a href="#bookmark" onClick={() => console.log(props.article.slug, 'articles')}>*/}
{/* <Icon name={'bookmark' + (bookmarked() ? '' : '-x')} />*/} {/* <Icon name={'bookmark' + (bookmarked() ? '' : '-x')} />*/}
{/* </a>*/} {/* </a>*/}
{/*</div>*/} {/*</div>*/}
<div class="shout-stats__item"> <div class={styles.shoutStatsItem}>
<SharePopup <SharePopup
trigger={ onVisibilityChange={(isVisible) => {
<a href="#" onClick={(event) => event.preventDefault()}> setIsSharePopupVisible(isVisible)
<Icon name="share" /> }}
</a> containerCssClass={stylesHeader.control}
} trigger={<Icon name="share" class={styles.icon} />}
/> />
</div> </div>
<div class={styles.shoutStatsItem}>
<Icon name="bookmark" class={styles.icon} />
</div>
{/*FIXME*/} {/*FIXME*/}
{/*<Show when={canEdit()}>*/} {/*<Show when={canEdit()}>*/}
{/* <div class="shout-stats__item">*/} {/* <div class={styles.shoutStatsItem}>*/}
{/* <a href="/edit">*/} {/* <a href="/edit">*/}
{/* <Icon name="edit" />*/} {/* <Icon name="edit" />*/}
{/* {t('Edit')}*/} {/* {t('Edit')}*/}
{/* </a>*/} {/* </a>*/}
{/* </div>*/} {/* </div>*/}
{/*</Show>*/} {/*</Show>*/}
<div class="shout-stats__item shout-stats__item--date">{formattedDate}</div> <div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
{formattedDate}
</div>
<Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
<Icon name="view" class={styles.icon} />
{props.article.stat?.viewed}
</div>
</Show>
</div>
</div> </div>
<div class="topics-list"> <div class={styles.topicsList}>
<For each={props.article.topics}> <For each={props.article.topics}>
{(topic) => ( {(topic) => (
<div class="shout__topic"> <div class={styles.shoutTopic}>
<a href={`/topic/${topic.slug}`}>{topic.title}</a> <a href={`/topic/${topic.slug}`}>{topic.title}</a>
</div> </div>
)} )}
</For> </For>
</div> </div>
<div class="shout__authors-list"> <div class={styles.shoutAuthorsList}>
<Show when={props.article?.authors?.length > 1}> <Show when={props.article?.authors?.length > 1}>
<h4>{t('Authors')}</h4> <h4>{t('Authors')}</h4>
</Show> </Show>
<For each={props.article?.authors}> <For each={props.article?.authors}>
{(a: Author) => <AuthorCard author={a} compact={false} hasLink={true} />} {(a: Author) => (
<div class="col-md-6">
<AuthorCard author={a} compact={false} hasLink={true} liteButtons={true} />
</div>
)}
</For> </For>
</div> </div>
@ -176,7 +200,7 @@ export const FullArticle = (props: ArticleProps) => {
</For> </For>
</Show> </Show>
<Show when={!session()?.user?.slug}> <Show when={!session()?.user?.slug}>
<div class="comment-warning" id="comments"> <div class={styles.commentWarning} id="comments">
{t('To leave a comment you please')} {t('To leave a comment you please')}
<a <a
href={''} href={''}
@ -190,7 +214,7 @@ export const FullArticle = (props: ArticleProps) => {
</div> </div>
</Show> </Show>
<Show when={session()?.user?.slug}> <Show when={session()?.user?.slug}>
<textarea class="write-comment" rows="1" placeholder={t('Write comment')} /> <textarea class={styles.writeComment} rows="1" placeholder={t('Write comment')} />
</Show> </Show>
</div> </div>
</div> </div>

View File

@ -0,0 +1,29 @@
.rating {
align-items: center;
display: flex;
}
.ratingValue {
font-weight: bold;
margin: 0 0.5em;
}
.ratingControl {
align-items: center;
border: 2px solid;
border-radius: 100%;
display: flex;
justify-content: center;
height: 0.9em;
line-height: 0;
@include font-size(3.6rem);
padding: 0;
width: 0.9em;
&:hover {
background: #000;
border-color: #000;
color: #fff;
}
}

View File

@ -0,0 +1,19 @@
import styles from './RatingControl.module.scss'
import { clsx } from 'clsx'
interface RatingControlProps {
rating?: number
class?: string
}
export const RatingControl = (props: RatingControlProps) => {
return (
<div class={clsx(props.class, styles.rating)}>
<button class={styles.ratingControl}>&minus;</button>
<span class={styles.ratingValue}>{props?.rating || ''}</span>
<button class={styles.ratingControl}>+</button>
</div>
)
}
export default RatingControl

View File

@ -166,6 +166,7 @@
} }
.icon { .icon {
display: inline-block;
margin-right: 0.5em; margin-right: 0.5em;
} }
@ -206,6 +207,7 @@
background-color: #000; background-color: #000;
border-color: #000; border-color: #000;
border-radius: 0.8rem; border-radius: 0.8rem;
color: #fff;
float: none; float: none;
padding-bottom: 0.6rem; padding-bottom: 0.6rem;
padding-top: 0.6rem; padding-top: 0.6rem;

View File

@ -9,7 +9,6 @@ import { locale } from '../../stores/ui'
import { follow, unfollow } from '../../stores/zine/common' import { follow, unfollow } from '../../stores/zine/common'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { StatMetrics } from '../_shared/StatMetrics'
interface AuthorCardProps { interface AuthorCardProps {
caption?: string caption?: string
@ -23,6 +22,7 @@ interface AuthorCardProps {
noSocialButtons?: boolean noSocialButtons?: boolean
isAuthorsList?: boolean isAuthorsList?: boolean
truncateBio?: boolean truncateBio?: boolean
liteButtons?: boolean
} }
export const AuthorCard = (props: AuthorCardProps) => { export const AuthorCard = (props: AuthorCardProps) => {
@ -117,9 +117,17 @@ export const AuthorCard = (props: AuthorCardProps) => {
</Show> </Show>
<Show when={!props.compact && !props.isAuthorsList}> <Show when={!props.compact && !props.isAuthorsList}>
<button class={clsx(styles.buttonWrite, styles.button, 'button button--subscribe-topic')}> <button
<Icon name="edit" class={styles.icon} /> class={styles.button}
{t('Write')} classList={{
[styles.buttonSubscribe]: !props.isAuthorsList,
'button--subscribe': !props.isAuthorsList,
'button--subscribe-topic': props.isAuthorsList,
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
}}
>
<Icon name="comment" class={styles.icon} />
<Show when={!props.liteButtons}>{t('Write')}</Show>
</button> </button>
<Show when={!props.noSocialButtons}> <Show when={!props.noSocialButtons}>

View File

@ -1,11 +1,13 @@
.user-details { .user-details {
margin-bottom: 4.4rem; margin-bottom: 5.4rem;
} }
.author-page { .author-page {
.view-switcher { .view-switcher {
@include font-size(1.5rem); @include font-size(1.5rem);
margin-top: 0;
button { button {
font-size: 100%; font-size: 100%;
} }

View File

@ -460,36 +460,6 @@
} }
} }
.rating {
align-items: center;
display: flex;
}
.ratingValue {
font-weight: bold;
margin: 0 0.5em;
}
.ratingControl {
align-items: center;
border: 2px solid;
border-radius: 100%;
display: flex;
justify-content: center;
height: 0.9em;
line-height: 0;
@include font-size(3.6rem);
padding: 0;
width: 0.9em;
&:hover {
background: #000;
border-color: #000;
color: #fff;
}
}
.shoutCardVertical { .shoutCardVertical {
aspect-ratio: auto; aspect-ratio: auto;
height: 100%; height: 100%;

View File

@ -8,6 +8,7 @@ import styles from './Card.module.scss'
import { locale } from '../../stores/ui' import { locale } from '../../stores/ui'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import CardTopic from './CardTopic' import CardTopic from './CardTopic'
import RatingControl from '../Article/RatingControl'
interface ArticleCardProps { interface ArticleCardProps {
settings?: { settings?: {
@ -158,11 +159,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<Show when={props.settings?.isFeedMode}> <Show when={props.settings?.isFeedMode}>
<section class={styles.shoutCardDetails}> <section class={styles.shoutCardDetails}>
<div class={styles.shoutCardDetailsContent}> <div class={styles.shoutCardDetailsContent}>
<div class={clsx(styles.shoutCardDetailsItem, styles.rating)}> <RatingControl rating={stat?.rating} class={styles.shoutCardDetailsItem} />
<button class={styles.ratingControl}>&minus;</button>
<span class={styles.ratingValue}>{stat?.rating || ''}</span>
<button class={styles.ratingControl}>+</button>
</div>
<div <div
class={clsx( class={clsx(
styles.shoutCardDetailsItem, styles.shoutCardDetailsItem,

View File

@ -18,7 +18,7 @@
border-radius: 50%; border-radius: 50%;
border: 3px solid #fff; border: 3px solid #fff;
} }
> .imageHolder { .imageHolder {
background-size: cover; background-size: cover;
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -26,12 +26,12 @@
border-radius: 100%; border-radius: 100%;
} }
> .letter { .letter {
display: block; display: block;
border-radius: 100%; border-radius: 100%;
} }
> .letter { .letter {
margin-bottom: -2px; margin-bottom: -2px;
font-weight: 500; font-weight: 500;
font-size: 18px; font-size: 18px;
@ -43,7 +43,7 @@
width: 24px; width: 24px;
height: 24px; height: 24px;
> .letter { .letter {
font-size: 14px; font-size: 14px;
} }
} }

View File

@ -36,7 +36,10 @@ const DialogAvatar = (props: Props) => {
return ( return (
<div <div
class={clsx(styles.DialogAvatar, props.online && styles.online, `${styles[props.size]}`)} class={clsx(styles.DialogAvatar, {
[styles.online]: props.online,
[styles.small]: props.size === 'small'
})}
style={{ 'background-color': `${randomBg()}` }} style={{ 'background-color': `${randomBg()}` }}
> >
<Show when={props.url} fallback={() => <div class={styles.letter}>{nameFirstLetter}</div>}> <Show when={props.url} fallback={() => <div class={styles.letter}>{nameFirstLetter}</div>}>

View File

@ -62,7 +62,7 @@
height: 22px; height: 22px;
line-height: 6px; line-height: 6px;
> span { span {
margin-bottom: -2px; margin-bottom: -2px;
} }
} }

View File

@ -1,8 +1,6 @@
import styles from './DialogCard.module.scss' import styles from './DialogCard.module.scss'
import DialogAvatar from './DialogAvatar' import DialogAvatar from './DialogAvatar'
import type { Author, AuthResult } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
import { createEffect, createMemo, createSignal } from 'solid-js'
import { apiClient } from '../../utils/apiClient' import { apiClient } from '../../utils/apiClient'
type DialogProps = { type DialogProps = {

View File

@ -129,7 +129,7 @@ export const Header = (props: Props) => {
containerCssClass={styles.control} containerCssClass={styles.control}
trigger={<Icon name="share-outline" class={styles.icon} />} trigger={<Icon name="share-outline" class={styles.icon} />}
/> />
<a href={'/inbox'} class={styles.control}> <a href={getPagePath(router, 'inbox')} class={styles.control}>
<Icon name="comments-outline" class={styles.icon} /> <Icon name="comments-outline" class={styles.icon} />
</a> </a>
<a href="#" class={styles.control} onClick={(event) => event.preventDefault()}> <a href="#" class={styles.control} onClick={(event) => event.preventDefault()}>

View File

@ -1,8 +1,7 @@
import { PageWrap } from '../_shared/PageWrap' import { PageWrap } from '../_shared/PageWrap'
import { InboxView } from '../Views/Inbox' import { InboxView } from '../Views/Inbox'
import type { PageProps } from '../types'
export const InboxPage = (props: PageProps) => { export const InboxPage = () => {
return ( return (
<PageWrap hideFooter={true}> <PageWrap hideFooter={true}>
<InboxView /> <InboxView />

View File

@ -70,7 +70,7 @@ export const LayoutShoutsPage = (props: PageProps) => {
onCleanup(() => resetSortedArticles()) onCleanup(() => resetSortedArticles())
const ModeSwitcher = () => ( const ModeSwitcher = () => (
<div class="container"> <div class="wide-container">
<div class={clsx(styles.groupControls, 'row group__controls')}> <div class={clsx(styles.groupControls, 'row group__controls')}>
<div class="col-md-8"> <div class="col-md-8">
<ul class="view-switcher"> <ul class="view-switcher">

View File

@ -4,8 +4,6 @@ import { t } from '../../utils/intl'
import type { Shout, Reaction } from '../../graphql/types.gen' import type { Shout, Reaction } from '../../graphql/types.gen'
import { useReactionsStore } from '../../stores/zine/reactions' import { useReactionsStore } from '../../stores/zine/reactions'
import '../../styles/Article.scss'
interface ArticlePageProps { interface ArticlePageProps {
article: Shout article: Shout
reactions?: Reaction[] reactions?: Reaction[]
@ -32,16 +30,14 @@ export const ArticleView = (props: ArticlePageProps) => {
}) })
return ( return (
<div class="article-page"> <Show fallback={<div class="center">{t('Loading')}</div>} when={props.article}>
<Show fallback={<div class="center">{t('Loading')}</div>} when={props.article}> <Suspense>
<Suspense> <FullArticle
<FullArticle article={props.article}
article={props.article} reactions={reactionsByShout()[props.article.slug]}
reactions={reactionsByShout()[props.article.slug]} isCommentsLoading={getIsCommentsLoading()}
isCommentsLoading={getIsCommentsLoading()} />
/> </Suspense>
</Suspense> </Show>
</Show>
</div>
) )
} }

View File

@ -100,8 +100,6 @@ export const AuthorView = (props: AuthorProps) => {
<span class="mode-switcher__control">{t('All posts')}</span> <span class="mode-switcher__control">{t('All posts')}</span>
</div> </div>
</div> </div>
<h3 class="col-12">{title()}</h3>
</div> </div>
</div> </div>

View File

@ -1,24 +1,19 @@
import { For, createSignal, Show, onMount, createEffect } from 'solid-js' import { For, createSignal, Show, onMount, createEffect, createMemo } from 'solid-js'
import { PageWrap } from '../_shared/PageWrap' import type { Author } from '../../graphql/types.gen'
import type { Author, Chat } from '../../graphql/types.gen'
import { AuthorCard } from '../Author/Card' import { AuthorCard } from '../Author/Card'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Loading } from '../Loading' import { Loading } from '../Loading'
import DialogCard from '../Inbox/DialogCard' import DialogCard from '../Inbox/DialogCard'
import Search from '../Inbox/Search' import Search from '../Inbox/Search'
import { loadAllAuthors, useAuthorsStore } from '../../stores/zine/authors'
import MarkdownIt from 'markdown-it'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import '../../styles/Inbox.scss' import '../../styles/Inbox.scss'
// Для моков // Для моков
import { createClient } from '@urql/core' import { createClient } from '@urql/core'
import Message from '../Inbox/Message' import Message from '../Inbox/Message'
import { loadAuthorsBy, loadChats, chats, setChats } from '../../stores/inbox' import { loadAuthorsBy, loadChats } from '../../stores/inbox'
import { t } from '../../utils/intl'
const md = new MarkdownIt({
linkify: true
})
const OWNER_ID = '501' const OWNER_ID = '501'
const client = createClient({ const client = createClient({
url: 'https://graphqlzero.almansi.me/api' url: 'https://graphqlzero.almansi.me/api'
@ -63,7 +58,7 @@ const postMessage = async (msg: string) => {
const handleGetChats = async () => { const handleGetChats = async () => {
try { try {
const response = await loadChats() const response = await loadChats()
setChats(response as unknown as Chat[]) console.log('!!! response:', response)
} catch (error) { } catch (error) {
console.log(error) console.log(error)
} }
@ -75,14 +70,10 @@ export const InboxView = () => {
const [cashedAuthors, setCashedAuthors] = createSignal<Author[]>([]) const [cashedAuthors, setCashedAuthors] = createSignal<Author[]>([])
const [postMessageText, setPostMessageText] = createSignal('') const [postMessageText, setPostMessageText] = createSignal('')
const [loading, setLoading] = createSignal<boolean>(false) const [loading, setLoading] = createSignal<boolean>(false)
const [currentSlug, setCurrentSlug] = createSignal<Author['slug'] | null>() // const [currentSlug, setCurrentSlug] = createSignal<Author['slug'] | null>()
const { session } = useSession() const { session } = useSession()
createEffect(() => { const currentSlug = createMemo(() => session()?.user?.slug)
console.log('!!! session():', session())
setCurrentSlug(session()?.user?.slug)
console.log('!!! chats:', chats())
})
// Поиск по диалогам // Поиск по диалогам
const getQuery = (query) => { const getQuery = (query) => {
@ -90,7 +81,7 @@ export const InboxView = () => {
const match = userSearch(authors(), query()) const match = userSearch(authors(), query())
setAuthors(match) setAuthors(match)
} else { } else {
setAuthors(cashedAuthors) setAuthors(cashedAuthors())
} }
} }
@ -153,10 +144,10 @@ export const InboxView = () => {
<div class="chat-list__types"> <div class="chat-list__types">
<ul> <ul>
<li> <li>
<strong>Все</strong> <strong>{t('All')}</strong>
</li> </li>
<li onClick={handleGetChats}>Переписки</li> <li onClick={handleGetChats}>{t('Conversations')}</li>
<li>Группы</li> <li>{t('Groups')}</li>
</ul> </ul>
</div> </div>
<div class="holder"> <div class="holder">

View File

@ -76,7 +76,7 @@ export const TopicView = (props: TopicProps) => {
<div class={styles.topicPage}> <div class={styles.topicPage}>
<Show when={topic()}> <Show when={topic()}>
<FullTopic topic={topic()} /> <FullTopic topic={topic()} />
<div class="container"> <div class="wide-container">
<div class={clsx(styles.groupControls, 'row group__controls')}> <div class={clsx(styles.groupControls, 'row group__controls')}>
<div class="col-md-8"> <div class="col-md-8">
<ul class="view-switcher"> <ul class="view-switcher">

View File

@ -23,6 +23,8 @@ const pseudonames = {
authors: 'authors' authors: 'authors'
} }
const nos = (s) => s.slice(-1)
export const StatMetrics = (props: StatMetricsProps) => { export const StatMetrics = (props: StatMetricsProps) => {
return ( return (
<div class={styles.statMetrics}> <div class={styles.statMetrics}>

View File

@ -5,21 +5,23 @@ export default gql`
loadChats(limit: $limit, offset: $offset) { loadChats(limit: $limit, offset: $offset) {
error error
chats { chats {
title id
description
updatedAt
messages { messages {
id id
author
body body
replyTo author
createdAt }
admins {
slug
name
} }
users { users {
slug slug
name name
userpic
} }
unread
description
updatedAt
} }
} }
} }

View File

@ -180,5 +180,8 @@
"view": "просмотр", "view": "просмотр",
"zine": "журнал", "zine": "журнал",
"shout": "пост", "shout": "пост",
"discussion": "дискурс" "discussion": "дискурс",
"Conversations": "Переписки",
"Groups": "Группы",
"All": "Все"
} }

View File

@ -1,9 +1,5 @@
import { createSignal } from 'solid-js'
import type { Chat } from '../graphql/types.gen'
import { apiClient } from '../utils/apiClient' import { apiClient } from '../utils/apiClient'
export const [chats, setChats] = createSignal<Chat[]>([])
export const loadAuthorsBy = async (by?: any): Promise<void> => { export const loadAuthorsBy = async (by?: any): Promise<void> => {
return await apiClient.getAuthorsBy({ by }) return await apiClient.getAuthorsBy({ by })
} }

View File

@ -0,0 +1,212 @@
h1 {
@include font-size(4rem);
line-height: 1.1;
margin-top: 0.5em;
}
h2 {
line-height: 1.1;
}
img {
max-width: 100%;
}
.shoutHeader {
margin-bottom: 2em;
@include media-breakpoint-up(md) {
margin: 0 0 2em;
}
}
.shoutCover {
background-size: cover;
height: 0;
padding-bottom: 56.2%;
}
.shoutBody {
font-size: 1.7rem;
line-height: 1.6;
img {
display: block;
margin-bottom: 0.5em;
}
blockquote {
border-left: 4px solid;
font-size: 2rem;
font-weight: 500;
font-style: italic;
line-height: 1.4;
margin: 1.5em 0;
padding: 0 0 0 1em;
@include media-breakpoint-up(md) {
margin-left: -16.6666%;
}
}
mark {
background: none;
font-size: 2rem;
font-weight: bold;
line-height: 1.4;
}
}
.shoutAuthor,
.shoutDate {
@include font-size(1.5rem);
}
.shoutAuthor {
margin-bottom: 1.5em;
a {
border: none;
color: rgb(0 0 0 / 60%);
&:hover {
color: #fff;
}
}
}
.shoutAuthorsList {
margin-top: 2em;
h4 {
color: #696969;
font-size: 1.5rem;
font-weight: normal;
}
}
.writeComment {
border: 2px solid #f6f6f6;
@include font-size(1.7rem);
outline: none;
padding: 0.2em 0.4em;
width: 100%;
&::placeholder {
color: #858585;
}
}
.commentWarning {
background: #f6f6f6;
@include font-size(2.2rem);
margin-bottom: 1em;
padding: 2.4rem 1.8rem;
}
.topic a {
/* white-space: nowrap; */
color: black;
padding: 0.3vh;
&:hover {
font-weight: 500;
}
}
.shoutStats {
border-bottom: 1px solid #e8e8e8;
border-top: 4px solid #000;
display: flex;
justify-content: flex-start;
padding: 3.2rem 0;
}
.shoutStatsItem {
@include font-size(1.7rem);
font-weight: 500;
display: inline-block;
margin: 0 3.2rem 1em 0;
vertical-align: baseline;
.icon {
display: inline-block;
margin-right: 0.2em;
transition: filter 0.2s;
vertical-align: middle;
}
img {
display: block;
}
a {
border: none;
&:hover {
.icon {
filter: invert(1);
}
}
}
}
.shoutStatsItemLikes {
.icon {
vertical-align: baseline;
}
.icon:last-of-type {
// transform: rotate(180deg);
transform-origin: center;
margin-left: 0.3em;
vertical-align: middle;
}
}
.shoutStatsItemAdditionalData {
color: rgb(0 0 0 / 40%);
font-weight: normal;
justify-self: flex-end;
margin-right: 0;
margin-left: auto;
white-space: nowrap;
.icon {
opacity: 0.4;
}
}
.shoutStatsItemAdditionalDataItem {
display: inline-block;
margin-left: 2rem;
}
.topicsList {
@include font-size(1.2rem);
letter-spacing: 0.08em;
margin: 1.6rem 0;
.shoutTopic {
display: inline-block;
margin: 0.8rem 0.8rem 0.8rem 0;
a {
background: #f6f6f6;
color: #000;
border: none;
padding: 0.4rem 0.8rem;
transition: background-color 0.2s;
text-transform: uppercase;
&:hover {
background-color: rgb(0 0 0 / 20%);
}
}
}
}

View File

@ -1,204 +0,0 @@
.article-page {
h1 {
@include font-size(4rem);
line-height: 1.1;
margin-top: 0.5em;
}
h2 {
line-height: 1.1;
}
img {
max-width: 100%;
}
.shout__header {
margin-bottom: 2em;
@include media-breakpoint-up(md) {
margin: 0 0 2em;
}
}
.shout__cover {
background-size: cover;
height: 0;
padding-bottom: 56.2%;
}
.shout__body {
font-size: 1.7rem;
line-height: 1.6;
img {
display: block;
margin-bottom: 0.5em;
}
blockquote {
border-left: 4px solid;
font-size: 2rem;
font-weight: 500;
font-style: italic;
line-height: 1.4;
margin: 1.5em 0;
padding: 0 0 0 1em;
@include media-breakpoint-up(md) {
margin-left: -16.6666%;
}
}
mark {
background: none;
font-size: 2rem;
font-weight: bold;
line-height: 1.4;
}
}
.shout__author,
.shout__date {
@include font-size(1.5rem);
}
.shout__author {
margin-bottom: 1.5em;
a {
border: none;
color: rgb(0 0 0 / 60%);
&:hover {
color: #fff;
}
}
}
.shout__authors-list {
margin-top: 2em;
h4 {
color: #696969;
font-size: 1.5rem;
font-weight: normal;
}
}
.write-comment {
border: 2px solid #f6f6f6;
@include font-size(1.7rem);
outline: none;
padding: 0.2em 0.4em;
width: 100%;
&::placeholder {
color: #858585;
}
}
.comment-warning {
background: #f6f6f6;
@include font-size(2.2rem);
margin-bottom: 1em;
padding: 2.4rem 1.8rem;
}
.topic a {
/* white-space: nowrap; */
color: black;
padding: 0.3vh;
&:hover {
font-weight: 500;
}
}
.shout-stats {
border-bottom: 1px solid #e8e8e8;
border-top: 4px solid #000;
display: flex;
justify-content: flex-start;
padding: 3.2rem 0;
}
.shout-stats__item {
@include font-size(1.7rem);
font-weight: 500;
display: inline-block;
margin: 0 3.2rem 1em 0;
vertical-align: baseline;
.icon {
display: inline-block;
margin-right: 0.2em;
transition: filter 0.2s;
vertical-align: middle;
}
img {
display: block;
}
a {
border: none;
&:hover {
.icon {
filter: invert(1);
}
}
}
}
.shout-stats__item--likes {
.icon {
vertical-align: baseline;
}
.icon:last-of-type {
// transform: rotate(180deg);
transform-origin: center;
margin-left: 0.3em;
vertical-align: middle;
}
}
.shout-stats__item--date {
color: rgb(0 0 0 / 40%);
font-weight: normal;
justify-self: flex-end;
margin-right: 0;
margin-left: auto;
}
.topics-list {
@include font-size(1.2rem);
letter-spacing: 0.08em;
margin: 1.6rem 0;
.shout__topic {
display: inline-block;
margin: 0.8rem 0.8rem 0.8rem 0;
a {
background: #f6f6f6;
color: #000;
border: none;
padding: 0.4rem 0.8rem;
transition: background-color 0.2s;
text-transform: uppercase;
&:hover {
background-color: rgb(0 0 0 / 20%);
}
}
}
}
}

View File

@ -5,11 +5,6 @@ main {
position: relative; position: relative;
} }
// TODO: добавлять когда открыт чат
body {
//overflow: hidden;
}
.messages { .messages {
top: 74px; top: 74px;
height: calc(100% - 74px); height: calc(100% - 74px);
@ -24,7 +19,7 @@ body {
position: fixed; position: fixed;
z-index: 9; z-index: 9;
> .row { .row {
flex: 1; flex: 1;
} }
@ -171,7 +166,7 @@ body {
background: #fff; background: #fff;
padding: 2px 0 12px 0; padding: 2px 0 12px 0;
> .wrapper { .wrapper {
border: 2px solid #cccccc; border: 2px solid #cccccc;
border-radius: 16px; border-radius: 16px;
padding: 4px; padding: 4px;
@ -179,7 +174,7 @@ body {
flex-direction: row; flex-direction: row;
align-items: center; align-items: center;
> .grow-wrap { .grow-wrap {
display: grid; display: grid;
width: 100%; width: 100%;
@ -190,7 +185,7 @@ body {
transition: height 1.3s ease-in-out; transition: height 1.3s ease-in-out;
} }
& > textarea { & textarea {
margin-bottom: 0; margin-bottom: 0;
font-family: inherit; font-family: inherit;
border: none; border: none;
@ -207,7 +202,7 @@ body {
} }
&::after, &::after,
& > textarea { & textarea {
/* Identical styling required!! */ /* Identical styling required!! */
font-weight: 400; font-weight: 400;
font-size: 14px; font-size: 14px;
@ -218,7 +213,7 @@ body {
} }
} }
> button { button {
border: none; border: none;
cursor: pointer; cursor: pointer;
text-align: center; text-align: center;
@ -232,7 +227,7 @@ body {
} }
} }
> .icon { .icon {
width: 100%; width: 100%;
height: 100%; height: 100%;
opacity: 0.2; opacity: 0.2;

View File

@ -4,7 +4,12 @@ import type {
ShoutInput, ShoutInput,
Topic, Topic,
Author, Author,
LoadShoutsOptions LoadShoutsOptions,
QueryLoadChatsArgs,
QueryLoadAuthorsByArgs,
QueryLoadMessagesByArgs,
MutationCreateChatArgs,
MutationCreateMessageArgs
} from '../graphql/types.gen' } from '../graphql/types.gen'
import { publicGraphQLClient } from '../graphql/publicGraphQLClient' import { publicGraphQLClient } from '../graphql/publicGraphQLClient'
import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient' import { getToken, privateGraphQLClient } from '../graphql/privateGraphQLClient'
@ -29,7 +34,6 @@ import chatMessagesLoadBy from '../graphql/query/chat-messages-load-by'
import authorBySlug from '../graphql/query/author-by-slug' import authorBySlug from '../graphql/query/author-by-slug'
import topicBySlug from '../graphql/query/topic-by-slug' import topicBySlug from '../graphql/query/topic-by-slug'
import createChat from '../graphql/mutation/create-chat' import createChat from '../graphql/mutation/create-chat'
import createMessage from '../graphql/mutation/create-chat-message'
import reactionsLoadBy from '../graphql/query/reactions-load-by' import reactionsLoadBy from '../graphql/query/reactions-load-by'
import { REACTIONS_AMOUNT_PER_PAGE } from '../stores/zine/reactions' import { REACTIONS_AMOUNT_PER_PAGE } from '../stores/zine/reactions'
import authorsLoadBy from '../graphql/query/authors-load-by' import authorsLoadBy from '../graphql/query/authors-load-by'
@ -221,12 +225,12 @@ export const apiClient = {
// CUDL // CUDL
createChat: async ({ title, members }) => { createChat: async (options: MutationCreateChatArgs) => {
return await privateGraphQLClient.mutation(createChat, { title: title, members: members }).toPromise() return await privateGraphQLClient.mutation(createChat, options).toPromise()
}, },
createMessage: async ({ chat, body }) => { createMessage: async (options: MutationCreateMessageArgs) => {
return await privateGraphQLClient.mutation(createChat, { chat: chat, body: body }).toPromise() return await privateGraphQLClient.mutation(createChat, options).toPromise()
}, },
updateReaction: async ({ reaction }) => { updateReaction: async ({ reaction }) => {
@ -239,8 +243,8 @@ export const apiClient = {
return response.data.deleteReaction return response.data.deleteReaction
}, },
getAuthorsBy: async ({ by, limit = 50, offset = 0 }) => { getAuthorsBy: async (options: QueryLoadAuthorsByArgs) => {
const resp = await publicGraphQLClient.query(authorsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient.query(authorsLoadBy, options).toPromise()
return resp.data.loadAuthorsBy return resp.data.loadAuthorsBy
}, },
getShout: async (slug: string) => { getShout: async (slug: string) => {
@ -263,30 +267,22 @@ export const apiClient = {
}, },
getReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => { getReactionsBy: async ({ by, limit = REACTIONS_AMOUNT_PER_PAGE, offset = 0 }) => {
const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise() const resp = await publicGraphQLClient.query(reactionsLoadBy, { by, limit, offset }).toPromise()
console.error(resp) if (resp.error) {
console.error(resp.error)
return
}
return resp.data.loadReactionsBy return resp.data.loadReactionsBy
}, },
// inbox // inbox
getChats: async ({ limit, offset }) => { getChats: async (options: QueryLoadChatsArgs) => {
const resp = await privateGraphQLClient.query(myChats, { limit, offset }).toPromise() const resp = await privateGraphQLClient.query(myChats, options).toPromise()
console.log('!!! resp.data.myChats:', resp) console.debug('[getChats]', resp)
return resp.data.myChats return resp.data.myChats
}, },
getChatMessages: async ({ getChatMessages: async (options: QueryLoadMessagesByArgs) => {
chat, const resp = await privateGraphQLClient.query(chatMessagesLoadBy, options).toPromise()
limit = 50,
offset = 0
}: {
chat: string
limit?: number
offset?: number
}) => {
const by = {
chat
}
const resp = await privateGraphQLClient.query(chatMessagesLoadBy, { by, offset, limit }).toPromise()
return resp.data.loadChat return resp.data.loadChat
} }
} }

View File

@ -1,4 +1,4 @@
export const isDev = import.meta.env.MODE === 'development' export const isDev = import.meta.env.MODE === 'development'
const defaultApiUrl = 'https://v2.discours.io' const defaultApiUrl = 'https://testapi.discours.io'
export const apiBaseUrl = import.meta.env.PUBLIC_API_URL || defaultApiUrl export const apiBaseUrl = import.meta.env.PUBLIC_API_URL || defaultApiUrl