Feature/lint (#317)

* prettier

---------

Co-authored-by: Igor Lobanov <igor.lobanov@onetwotrip.com>
This commit is contained in:
Ilya Y 2023-11-14 18:10:00 +03:00 committed by GitHub
parent 4162d4318c
commit 784bb435c3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
216 changed files with 2019 additions and 1626 deletions

View File

@ -1,41 +1,41 @@
module.exports = { module.exports = {
plugins: ['@typescript-eslint', 'import', 'sonarjs', 'unicorn', 'promise', 'solid', 'jest'], plugins: ["@typescript-eslint", "import", "sonarjs", "unicorn", "promise", "solid", "jest"],
extends: [ extends: [
'eslint:recommended', "eslint:recommended",
'plugin:import/recommended', "plugin:import/recommended",
'plugin:import/typescript', "plugin:import/typescript",
'prettier', "prettier",
'plugin:sonarjs/recommended', "plugin:sonarjs/recommended",
'plugin:unicorn/recommended', "plugin:unicorn/recommended",
'plugin:promise/recommended', "plugin:promise/recommended",
'plugin:solid/recommended', "plugin:solid/recommended",
'plugin:jest/recommended' "plugin:jest/recommended"
], ],
overrides: [ overrides: [
{ {
files: ['**/*.ts', '**/*.tsx'], files: ["**/*.ts", "**/*.tsx"],
parser: '@typescript-eslint/parser', parser: "@typescript-eslint/parser",
parserOptions: { parserOptions: {
ecmaVersion: 2021, ecmaVersion: 2021,
ecmaFeatures: { jsx: true }, ecmaFeatures: { jsx: true },
sourceType: 'module', sourceType: "module",
project: './tsconfig.json' project: "./tsconfig.json"
}, },
extends: [ extends: [
'plugin:@typescript-eslint/recommended' "plugin:@typescript-eslint/recommended"
// Maybe one day... // Maybe one day...
// 'plugin:@typescript-eslint/recommended-requiring-type-checking' // 'plugin:@typescript-eslint/recommended-requiring-type-checking'
], ],
rules: { rules: {
'@typescript-eslint/no-unused-vars': [ "@typescript-eslint/no-unused-vars": [
'warn', "warn",
{ {
argsIgnorePattern: '^_' argsIgnorePattern: "^_"
} }
], ],
'@typescript-eslint/no-non-null-assertion': 'error', "@typescript-eslint/no-non-null-assertion": "error",
// TODO: Remove any usage and enable // TODO: Remove any usage and enable
'@typescript-eslint/no-explicit-any': 'off' "@typescript-eslint/no-explicit-any": "off"
} }
} }
], ],
@ -47,42 +47,60 @@ module.exports = {
globals: {}, globals: {},
rules: { rules: {
// Solid // Solid
'solid/reactivity': 'off', // FIXME "solid/reactivity": "off", // FIXME
'solid/no-innerhtml': 'off', "solid/no-innerhtml": "off",
/** Unicorn **/ /** Unicorn **/
'unicorn/no-null': 'off', "unicorn/no-null": "off",
'unicorn/filename-case': 'off', "unicorn/filename-case": "off",
'unicorn/no-array-for-each': 'off', "unicorn/no-array-for-each": "off",
'unicorn/no-array-reduce': 'off', "unicorn/no-array-reduce": "off",
'unicorn/prefer-string-replace-all': 'warn', "unicorn/prefer-string-replace-all": "warn",
'unicorn/prevent-abbreviations': 'off', "unicorn/prevent-abbreviations": "off",
'unicorn/prefer-module': 'off', "unicorn/prefer-module": "off",
'unicorn/import-style': 'off', "unicorn/import-style": "off",
'unicorn/numeric-separators-style': 'off', "unicorn/numeric-separators-style": "off",
'unicorn/prefer-node-protocol': 'off', "unicorn/prefer-node-protocol": "off",
'unicorn/prefer-dom-node-append': 'off', // FIXME "unicorn/prefer-dom-node-append": "off", // FIXME
'unicorn/prefer-top-level-await': 'warn', "unicorn/prefer-top-level-await": "warn",
'unicorn/consistent-function-scoping': 'warn', "unicorn/consistent-function-scoping": "warn",
'unicorn/no-array-callback-reference': 'warn', "unicorn/no-array-callback-reference": "warn",
'unicorn/no-array-method-this-argument': 'warn', "unicorn/no-array-method-this-argument": "warn",
'unicorn/no-for-loop': 'off', "unicorn/no-for-loop": "off",
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }], "sonarjs/no-duplicate-string": ["warn", { threshold: 5 }],
// Promise // Promise
// 'promise/catch-or-return': 'off', // Should be enabled // 'promise/catch-or-return': 'off', // Should be enabled
'promise/always-return': 'off', "promise/always-return": "off",
eqeqeq: 'error', eqeqeq: "error",
'no-param-reassign': 'error', "no-param-reassign": "error",
'no-nested-ternary': 'error', "no-nested-ternary": "error",
'no-shadow': 'error' "no-shadow": "error",
"import/order": ["warn", {
groups: ["type", "builtin", "external", "internal", "parent", "sibling", "index"],
distinctGroup: false,
pathGroups: [
{
pattern: "*.scss",
patternOptions: { matchBase: true },
group: "index",
position: "after"
}
],
"newlines-between": "always",
alphabetize: {
order: "asc",
caseInsensitive: true
}
}]
}, },
settings: { settings: {
'import/resolver': { "import/resolver": {
typescript: true, typescript: true,
node: true node: true
} }
} }
} };

View File

@ -4,7 +4,6 @@
"singleQuote": true, "singleQuote": true,
"proseWrap": "always", "proseWrap": "always",
"printWidth": 108, "printWidth": 108,
"trailingComma": "none",
"plugins": [], "plugins": [],
"overrides": [ "overrides": [
{ {

View File

@ -1,7 +1,7 @@
import { renderPage } from 'vike/server' import { renderPage } from 'vike/server'
export const config = { export const config = {
runtime: 'edge' runtime: 'edge',
} }
export default async function handler(request) { export default async function handler(request) {
const { url, cookies } = request const { url, cookies } = request

View File

@ -15,7 +15,7 @@ export default async function handler(req, res) {
from: 'Discours Feedback Robot <robot@discours.io>', from: 'Discours Feedback Robot <robot@discours.io>',
to: 'welcome@discours.io', to: 'welcome@discours.io',
subject, subject,
text text,
} }
try { try {

View File

@ -13,18 +13,18 @@ export default async (req, res) => {
const response = await mg.lists.members.createMember('newsletter@discours.io', { const response = await mg.lists.members.createMember('newsletter@discours.io', {
address: email, address: email,
subscribed: true, subscribed: true,
upsert: 'yes' upsert: 'yes',
}) })
return res.status(200).json({ return res.status(200).json({
success: true, success: true,
message: 'Email was added to newsletter list', message: 'Email was added to newsletter list',
response: JSON.stringify(response) response: JSON.stringify(response),
}) })
} catch (error) { } catch (error) {
return res.status(400).json({ return res.status(400).json({
success: false, success: false,
message: error.message message: error.message,
}) })
} }
} }

View File

@ -1,18 +1,15 @@
import { hideModal, MODALS, showModal } from '../stores/ui' import type { PageProps, RootSearchParams } from '../pages/types'
import { Meta, MetaProvider } from '@solidjs/meta'
import { Component, createEffect, createMemo } from 'solid-js' import { Component, createEffect, createMemo } from 'solid-js'
import { ROUTES, useRouter } from '../stores/router'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
import type { PageProps, RootSearchParams } from '../pages/types' import { ConfirmProvider } from '../context/confirm'
import { HomePage } from '../pages/index.page' import { EditorProvider } from '../context/editor'
import { AllTopicsPage } from '../pages/allTopics.page' import { LocalizeProvider } from '../context/localize'
import { TopicPage } from '../pages/topic.page' import { NotificationsProvider } from '../context/notifications'
import { AllAuthorsPage } from '../pages/allAuthors.page' import { SessionProvider } from '../context/session'
import { AuthorPage } from '../pages/author.page' import { SnackbarProvider } from '../context/snackbar'
import { FeedPage } from '../pages/feed.page'
import { ArticlePage } from '../pages/article.page'
import { SearchPage } from '../pages/search.page'
import { FourOuFourPage } from '../pages/fourOuFour.page'
import { DiscussionRulesPage } from '../pages/about/discussionRules.page' import { DiscussionRulesPage } from '../pages/about/discussionRules.page'
import { DogmaPage } from '../pages/about/dogma.page' import { DogmaPage } from '../pages/about/dogma.page'
import { GuidePage } from '../pages/about/guide.page' import { GuidePage } from '../pages/about/guide.page'
@ -23,22 +20,26 @@ import { PrinciplesPage } from '../pages/about/principles.page'
import { ProjectsPage } from '../pages/about/projects.page' import { ProjectsPage } from '../pages/about/projects.page'
import { TermsOfUsePage } from '../pages/about/termsOfUse.page' import { TermsOfUsePage } from '../pages/about/termsOfUse.page'
import { ThanksPage } from '../pages/about/thanks.page' import { ThanksPage } from '../pages/about/thanks.page'
import { CreatePage } from '../pages/create.page' import { AllAuthorsPage } from '../pages/allAuthors.page'
import { EditPage } from '../pages/edit.page' import { AllTopicsPage } from '../pages/allTopics.page'
import { ArticlePage } from '../pages/article.page'
import { AuthorPage } from '../pages/author.page'
import { ConnectPage } from '../pages/connect.page' import { ConnectPage } from '../pages/connect.page'
import { InboxPage } from '../pages/inbox.page' import { CreatePage } from '../pages/create.page'
import { ExpoPage } from '../pages/expo/expo.page'
import { SessionProvider } from '../context/session'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { DraftsPage } from '../pages/drafts.page' import { DraftsPage } from '../pages/drafts.page'
import { SnackbarProvider } from '../context/snackbar' import { EditPage } from '../pages/edit.page'
import { LocalizeProvider } from '../context/localize' import { ExpoPage } from '../pages/expo/expo.page'
import { ConfirmProvider } from '../context/confirm' import { FeedPage } from '../pages/feed.page'
import { EditorProvider } from '../context/editor' import { FourOuFourPage } from '../pages/fourOuFour.page'
import { NotificationsProvider } from '../context/notifications' import { InboxPage } from '../pages/inbox.page'
import { Meta, MetaProvider } from '@solidjs/meta' import { HomePage } from '../pages/index.page'
import { ProfileSecurityPage } from '../pages/profile/profileSecurity.page'
import { ProfileSettingsPage } from '../pages/profile/profileSettings.page'
import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.page'
import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router'
import { hideModal, MODALS, showModal } from '../stores/ui'
// TODO: lazy load // TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage')) // const SomePage = lazy(() => import('./Pages/SomePage'))
@ -80,7 +81,7 @@ const pagesMap: Record<keyof typeof ROUTES, Component<PageProps>> = {
profileSettings: ProfileSettingsPage, profileSettings: ProfileSettingsPage,
profileSecurity: ProfileSecurityPage, profileSecurity: ProfileSecurityPage,
profileSubscriptions: ProfileSubscriptionsPage, profileSubscriptions: ProfileSubscriptionsPage,
fourOuFour: FourOuFourPage fourOuFour: FourOuFourPage,
} }
export const App = (props: PageProps) => { export const App = (props: PageProps) => {

View File

@ -1,11 +1,13 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AudioHeader.module.scss'
import { MediaItem } from '../../../pages/types'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import { Topic } from '../../../graphql/types.gen' import { Topic } from '../../../graphql/types.gen'
import { CardTopic } from '../../Feed/CardTopic' import { MediaItem } from '../../../pages/types'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
import { CardTopic } from '../../Feed/CardTopic'
import styles from './AudioHeader.module.scss'
type Props = { type Props = {
title: string title: string

View File

@ -1,8 +1,11 @@
import { createEffect, createMemo, createSignal, on, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, on, onMount, Show } from 'solid-js'
import { MediaItem } from '../../../pages/types'
import { PlayerHeader } from './PlayerHeader' import { PlayerHeader } from './PlayerHeader'
import { PlayerPlaylist } from './PlayerPlaylist' import { PlayerPlaylist } from './PlayerPlaylist'
import styles from './AudioPlayer.module.scss' import styles from './AudioPlayer.module.scss'
import { MediaItem } from '../../../pages/types'
type Props = { type Props = {
media: MediaItem[] media: MediaItem[]
@ -35,8 +38,8 @@ export const AudioPlayer = (props: Props) => {
() => { () => {
setCurrentTrackDuration(0) setCurrentTrackDuration(0)
}, },
{ defer: true } { defer: true },
) ),
) )
const handlePlayMedia = async (trackIndex: number) => { const handlePlayMedia = async (trackIndex: number) => {
@ -131,7 +134,7 @@ export const AudioPlayer = (props: Props) => {
<div <div
class={styles.progressFilled} class={styles.progressFilled}
style={{ style={{
width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%` width: `${(currentTime() / currentTrackDuration()) * 100 || 0}%`,
}} }}
/> />
</div> </div>

View File

@ -1,11 +1,11 @@
import { createSignal, Show } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss'
type Props = { type Props = {
onPlayMedia: () => void onPlayMedia: () => void
@ -18,7 +18,7 @@ type Props = {
export const PlayerHeader = (props: Props) => { export const PlayerHeader = (props: Props) => {
const volumeContainerRef: { current: HTMLDivElement } = { const volumeContainerRef: { current: HTMLDivElement } = {
current: null current: null,
} }
const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false) const [isVolumeBarOpened, setIsVolumeBarOpened] = createSignal(false)
@ -30,7 +30,7 @@ export const PlayerHeader = (props: Props) => {
useOutsideClickHandler({ useOutsideClickHandler({
containerRef: volumeContainerRef, containerRef: volumeContainerRef,
predicate: () => isVolumeBarOpened(), predicate: () => isVolumeBarOpened(),
handler: () => toggleVolumeBar() handler: () => toggleVolumeBar(),
}) })
return ( return (
@ -42,7 +42,7 @@ export const PlayerHeader = (props: Props) => {
onClick={props.onPlayMedia} onClick={props.onPlayMedia}
class={clsx( class={clsx(
styles.playButton, styles.playButton,
props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay props.isPlaying ? styles.playButtonInvertPause : styles.playButtonInvertPlay,
)} )}
aria-label="Play" aria-label="Play"
data-playing="false" data-playing="false"

View File

@ -1,13 +1,15 @@
import { createSignal, For, Show } from 'solid-js' import { createSignal, For, Show } from 'solid-js'
import { SharePopup, getShareUrl } from '../SharePopup'
import { getDescription } from '../../../utils/meta'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Popover } from '../../_shared/Popover'
import { Icon } from '../../_shared/Icon'
import styles from './AudioPlayer.module.scss'
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { getDescription } from '../../../utils/meta'
import { GrowingTextarea } from '../../_shared/GrowingTextarea'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import SimplifiedEditor from '../../Editor/SimplifiedEditor' import SimplifiedEditor from '../../Editor/SimplifiedEditor'
import { SharePopup, getShareUrl } from '../SharePopup'
import styles from './AudioPlayer.module.scss'
type Props = { type Props = {
media: MediaItem[] media: MediaItem[]

View File

@ -1,24 +1,22 @@
import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
import { Userpic } from '../../Author/Userpic' import { useConfirm } from '../../../context/confirm'
import { CommentRatingControl } from '../CommentRatingControl'
import { CommentDate } from '../CommentDate'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { Icon } from '../../_shared/Icon'
import { useSession } from '../../../context/session'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useReactions } from '../../../context/reactions' import { useReactions } from '../../../context/reactions'
import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
import { useConfirm } from '../../../context/confirm'
import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen' import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { AuthorLink } from '../../Author/AhtorLink'
import { Userpic } from '../../Author/Userpic'
import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl'
import styles from './Comment.module.scss' import styles from './Comment.module.scss'
import { AuthorLink } from '../../Author/AhtorLink'
const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor')) const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
@ -43,15 +41,15 @@ export const Comment = (props: Props) => {
const { session } = useSession() const { session } = useSession()
const { const {
actions: { createReaction, deleteReaction, updateReaction } actions: { createReaction, deleteReaction, updateReaction },
} = useReactions() } = useReactions()
const { const {
actions: { showConfirm } actions: { showConfirm },
} = useConfirm() } = useConfirm()
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const isCommentAuthor = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug) const isCommentAuthor = createMemo(() => props.comment.createdBy?.slug === session()?.user?.slug)
@ -65,7 +63,7 @@ export const Comment = (props: Props) => {
confirmBody: t('Are you sure you want to delete this comment?'), confirmBody: t('Are you sure you want to delete this comment?'),
confirmButtonLabel: t('Delete'), confirmButtonLabel: t('Delete'),
confirmButtonVariant: 'danger', confirmButtonVariant: 'danger',
declineButtonVariant: 'primary' declineButtonVariant: 'primary',
}) })
if (isConfirmed) { if (isConfirmed) {
@ -86,7 +84,7 @@ export const Comment = (props: Props) => {
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
replyTo: props.comment.id, replyTo: props.comment.id,
body: value, body: value,
shout: props.comment.shout.id shout: props.comment.shout.id,
}) })
setClearEditor(true) setClearEditor(true)
setIsReplyVisible(false) setIsReplyVisible(false)
@ -107,7 +105,7 @@ export const Comment = (props: Props) => {
await updateReaction(props.comment.id, { await updateReaction(props.comment.id, {
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
body: value, body: value,
shout: props.comment.shout.id shout: props.comment.shout.id,
}) })
setEditMode(false) setEditMode(false)
setLoading(false) setLoading(false)
@ -122,7 +120,7 @@ export const Comment = (props: Props) => {
<li <li
id={`comment_${comment().id}`} id={`comment_${comment().id}`}
class={clsx(styles.comment, props.class, { class={clsx(styles.comment, props.class, {
[styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen [styles.isNew]: !isCommentAuthor() && createdAt > props.lastSeen,
})} })}
> >
<Show when={!!body()}> <Show when={!!body()}>
@ -135,7 +133,7 @@ export const Comment = (props: Props) => {
name={comment().createdBy.name} name={comment().createdBy.name}
userpic={comment().createdBy.userpic} userpic={comment().createdBy.userpic}
class={clsx({ class={clsx({
[styles.compactUserpic]: props.compact [styles.compactUserpic]: props.compact,
})} })}
/> />
<small> <small>

View File

@ -1,8 +1,11 @@
import { Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import type { Reaction } from '../../../graphql/types.gen' import type { Reaction } from '../../../graphql/types.gen'
import { useLocalize } from '../../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import styles from './CommentDate.module.scss' import styles from './CommentDate.module.scss'
type Props = { type Props = {
@ -27,7 +30,7 @@ export const CommentDate = (props: Props) => {
<div <div
class={clsx(styles.commentDates, { class={clsx(styles.commentDates, {
[styles.commentDatesLastInRow]: props.isLastInRow, [styles.commentDatesLastInRow]: props.isLastInRow,
[styles.showOnHover]: props.showOnHover [styles.showOnHover]: props.showOnHover,
})} })}
> >
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time> <time class={styles.date}>{formattedDate(props.comment.createdAt)}</time>

View File

@ -1,16 +1,19 @@
import { clsx } from 'clsx'
import styles from './CommentRatingControl.module.scss'
import type { Reaction } from '../../graphql/types.gen' import type { Reaction } from '../../graphql/types.gen'
import { ReactionKind } from '../../graphql/types.gen'
import { useSession } from '../../context/session' import { clsx } from 'clsx'
import { useReactions } from '../../context/reactions'
import { createMemo } from 'solid-js' import { createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar'
import { ReactionKind } from '../../graphql/types.gen'
import { loadShout } from '../../stores/zine/articles' import { loadShout } from '../../stores/zine/articles'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { VotersList } from '../_shared/VotersList' import { VotersList } from '../_shared/VotersList'
import styles from './CommentRatingControl.module.scss'
type Props = { type Props = {
comment: Reaction comment: Reaction
} }
@ -19,11 +22,11 @@ export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user } = useSession() const { user } = useSession()
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const { const {
reactionEntities, reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy } actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions() } = useReactions()
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
@ -32,7 +35,7 @@ export const CommentRatingControl = (props: Props) => {
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.createdBy.slug === user()?.slug &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id r.replyTo === props.comment.id,
) )
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like)) const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike)) const isDownvoted = createMemo(() => checkReaction(ReactionKind.Dislike))
@ -43,8 +46,8 @@ export const CommentRatingControl = (props: Props) => {
(r) => (r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id r.replyTo === props.comment.id,
) ),
) )
const deleteCommentReaction = async (reactionKind: ReactionKind) => { const deleteCommentReaction = async (reactionKind: ReactionKind) => {
@ -53,7 +56,7 @@ export const CommentRatingControl = (props: Props) => {
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.createdBy.slug === user()?.slug &&
r.shout.id === props.comment.shout.id && r.shout.id === props.comment.shout.id &&
r.replyTo === props.comment.id r.replyTo === props.comment.id,
) )
return deleteReaction(reactionToDelete.id) return deleteReaction(reactionToDelete.id)
} }
@ -68,7 +71,7 @@ export const CommentRatingControl = (props: Props) => {
await createReaction({ await createReaction({
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.comment.shout.id, shout: props.comment.shout.id,
replyTo: props.comment.id replyTo: props.comment.id,
}) })
} }
} catch { } catch {
@ -77,7 +80,7 @@ export const CommentRatingControl = (props: Props) => {
await loadShout(props.comment.shout.slug) await loadShout(props.comment.shout.slug)
await loadReactionsBy({ await loadReactionsBy({
by: { shout: props.comment.shout.slug } by: { shout: props.comment.shout.slug },
}) })
} }
@ -88,7 +91,7 @@ export const CommentRatingControl = (props: Props) => {
disabled={!canVote() || !user()} disabled={!canVote() || !user()}
onClick={() => handleRatingChange(true)} onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, { class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
[styles.voted]: isUpvoted() [styles.voted]: isUpvoted(),
})} })}
/> />
<Popup <Popup
@ -96,7 +99,7 @@ export const CommentRatingControl = (props: Props) => {
<div <div
class={clsx(styles.commentRatingValue, { class={clsx(styles.commentRatingValue, {
[styles.commentRatingPositive]: props.comment.stat.rating > 0, [styles.commentRatingPositive]: props.comment.stat.rating > 0,
[styles.commentRatingNegative]: props.comment.stat.rating < 0 [styles.commentRatingNegative]: props.comment.stat.rating < 0,
})} })}
> >
{props.comment.stat.rating || 0} {props.comment.stat.rating || 0}
@ -114,7 +117,7 @@ export const CommentRatingControl = (props: Props) => {
disabled={!canVote() || !user()} disabled={!canVote() || !user()}
onClick={() => handleRatingChange(false)} onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, { class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted() [styles.voted]: isDownvoted(),
})} })}
/> />
</div> </div>

View File

@ -1,16 +1,19 @@
import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
import { Comment } from './Comment'
import styles from './Article.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen' import { Show, createMemo, createSignal, onMount, For } from 'solid-js'
import { useSession } from '../../context/session'
import { Button } from '../_shared/Button'
import { useReactions } from '../../context/reactions'
import { byCreated } from '../../utils/sortby'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
import { byCreated } from '../../utils/sortby'
import { Button } from '../_shared/Button'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
import SimplifiedEditor from '../Editor/SimplifiedEditor' import SimplifiedEditor from '../Editor/SimplifiedEditor'
import { Comment } from './Comment'
import styles from './Article.module.scss'
type CommentsOrder = 'createdAt' | 'rating' | 'newOnly' type CommentsOrder = 'createdAt' | 'rating' | 'newOnly'
const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => { const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
@ -48,11 +51,11 @@ export const CommentsTree = (props: Props) => {
const { const {
reactionEntities, reactionEntities,
actions: { createReaction } actions: { createReaction },
} = useReactions() } = useReactions()
const comments = createMemo(() => const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT') Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),
) )
const sortedComments = createMemo(() => { const sortedComments = createMemo(() => {
@ -96,7 +99,7 @@ export const CommentsTree = (props: Props) => {
await createReaction({ await createReaction({
kind: ReactionKind.Comment, kind: ReactionKind.Comment,
body: value, body: value,
shout: props.shoutId shout: props.shoutId,
}) })
setClearEditor(true) setClearEditor(true)
} catch (error) { } catch (error) {
@ -154,7 +157,7 @@ export const CommentsTree = (props: Props) => {
<Comment <Comment
sortedComments={sortedComments()} sortedComments={sortedComments()}
isArticleAuthor={Boolean( isArticleAuthor={Boolean(
props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug) props.articleAuthors.some((a) => a.slug === reaction.createdBy.slug),
)} )}
comment={reaction} comment={reaction}
clickedReply={(id) => setClickedReplyId(id)} clickedReply={(id) => setClickedReplyId(id)}

View File

@ -1,34 +1,36 @@
import type { Author, Shout } from '../../graphql/types.gen'
import { getPagePath } from '@nanostores/router'
import { createPopper } from '@popperjs/core'
import { clsx } from 'clsx'
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js' import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup } from 'solid-js'
import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router'
import type { Author, Shout } from '../../graphql/types.gen'
import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { MediaItem } from '../../pages/types' import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { getImageUrl } from '../../utils/getImageUrl'
import { getDescription } from '../../utils/meta' import { getDescription } from '../../utils/meta'
import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image'
import { Lightbox } from '../_shared/Lightbox'
import { Popover } from '../_shared/Popover'
import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { TableOfContents } from '../TableOfContents' import { TableOfContents } from '../TableOfContents'
import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree'
import { getShareUrl, SharePopup } from './SharePopup' import { getShareUrl, SharePopup } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl' import { ShoutRatingControl } from './ShoutRatingControl'
import { CommentsTree } from './CommentsTree'
import stylesHeader from '../Nav/Header/Header.module.scss'
import { AudioHeader } from './AudioHeader'
import { Popover } from '../_shared/Popover'
import { VideoPlayer } from '../_shared/VideoPlayer'
import { Icon } from '../_shared/Icon'
import { ImageSwiper } from '../_shared/SolidSwiper'
import styles from './Article.module.scss' import styles from './Article.module.scss'
import { CardTopic } from '../Feed/CardTopic' import stylesHeader from '../Nav/Header/Header.module.scss'
import { createPopper } from '@popperjs/core'
import { AuthorBadge } from '../Author/AuthorBadge'
import { getImageUrl } from '../../utils/getImageUrl'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { Lightbox } from '../_shared/Lightbox'
import { Image } from '../_shared/Image'
import article from '../Editor/extensions/Article'
type Props = { type Props = {
article: Shout article: Shout
@ -46,7 +48,7 @@ const scrollTo = (el: HTMLElement) => {
window.scrollTo({ window.scrollTo({
top: top + window.scrollY - DEFAULT_HEADER_OFFSET, top: top + window.scrollY - DEFAULT_HEADER_OFFSET,
left: 0, left: 0,
behavior: 'smooth' behavior: 'smooth',
}) })
} }
@ -57,7 +59,7 @@ export const FullArticle = (props: Props) => {
const { const {
user, user,
isAuthenticated, isAuthenticated,
actions: { requireAuthentication } actions: { requireAuthentication },
} = useSession() } = useSession()
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
@ -67,7 +69,7 @@ export const FullArticle = (props: Props) => {
const mainTopic = createMemo( const mainTopic = createMemo(
() => () =>
props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) || props.article.topics?.find((topic) => topic?.slug === props.article.mainTopic) ||
props.article.topics[0] props.article.topics[0],
) )
const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug) const canEdit = () => props.article.authors?.some((a) => a.slug === user()?.slug)
@ -121,7 +123,7 @@ export const FullArticle = (props: Props) => {
if (searchParams()?.scrollTo === 'comments' && commentsRef.current) { if (searchParams()?.scrollTo === 'comments' && commentsRef.current) {
scrollToComments() scrollToComments()
changeSearchParam({ changeSearchParam({
scrollTo: null scrollTo: null,
}) })
} }
}) })
@ -129,7 +131,7 @@ export const FullArticle = (props: Props) => {
createEffect(() => { createEffect(() => {
if (searchParams().commentId && isReactionsLoaded()) { if (searchParams().commentId && isReactionsLoaded()) {
const commentElement = document.querySelector<HTMLElement>( const commentElement = document.querySelector<HTMLElement>(
`[id='comment_${searchParams().commentId}']` `[id='comment_${searchParams().commentId}']`,
) )
changeSearchParam({ commentId: null }) changeSearchParam({ commentId: null })
@ -141,12 +143,12 @@ export const FullArticle = (props: Props) => {
}) })
const { const {
actions: { loadReactionsBy } actions: { loadReactionsBy },
} = useReactions() } = useReactions()
onMount(async () => { onMount(async () => {
await loadReactionsBy({ await loadReactionsBy({
by: { shout: props.article.slug } by: { shout: props.article.slug },
}) })
setIsReactionsLoaded(true) setIsReactionsLoaded(true)
@ -165,7 +167,7 @@ export const FullArticle = (props: Props) => {
} }
const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll( const tooltipElements: NodeListOf<HTMLElement> = document.querySelectorAll(
'[data-toggle="tooltip"], footnote' '[data-toggle="tooltip"], footnote',
) )
if (!tooltipElements) { if (!tooltipElements) {
return return
@ -190,19 +192,19 @@ export const FullArticle = (props: Props) => {
modifiers: [ modifiers: [
{ {
name: 'eventListeners', name: 'eventListeners',
options: { scroll: false } options: { scroll: false },
}, },
{ {
name: 'offset', name: 'offset',
options: { options: {
offset: [0, 8] offset: [0, 8],
} },
}, },
{ {
name: 'flip', name: 'flip',
options: { fallbackPlacements: ['top'] } options: { fallbackPlacements: ['top'] },
} },
] ],
}) })
tooltip.style.visibility = 'hidden' tooltip.style.visibility = 'hidden'

View File

@ -1,12 +1,14 @@
import { Icon } from '../_shared/Icon' import type { PopupProps } from '../_shared/Popup'
import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share' import { createSocialShare, TWITTER, VK, FACEBOOK, TELEGRAM } from '@solid-primitives/share'
import styles from '../_shared/Popup/Popup.module.scss'
import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import { useLocalize } from '../../context/localize'
import { createEffect, createSignal } from 'solid-js' import { createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { Icon } from '../_shared/Icon'
import { Popup } from '../_shared/Popup'
import styles from '../_shared/Popup/Popup.module.scss'
type SharePopupProps = { type SharePopupProps = {
title: string title: string
@ -26,7 +28,7 @@ export const SharePopup = (props: SharePopupProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const [isVisible, setIsVisible] = createSignal(false) const [isVisible, setIsVisible] = createSignal(false)
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
createEffect(() => { createEffect(() => {
@ -38,7 +40,7 @@ export const SharePopup = (props: SharePopupProps) => {
const [share] = createSocialShare(() => ({ const [share] = createSocialShare(() => ({
title: props.title, title: props.title,
url: props.shareUrl, url: props.shareUrl,
description: props.description description: props.description,
})) }))
const copyLink = async () => { const copyLink = async () => {

View File

@ -1,13 +1,15 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js' import { createMemo, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions'
import { useSession } from '../../context/session'
import { ReactionKind, Shout } from '../../graphql/types.gen' import { ReactionKind, Shout } from '../../graphql/types.gen'
import { loadShout } from '../../stores/zine/articles' import { loadShout } from '../../stores/zine/articles'
import { useSession } from '../../context/session' import { Icon } from '../_shared/Icon'
import { useReactions } from '../../context/reactions'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'
import { VotersList } from '../_shared/VotersList' import { VotersList } from '../_shared/VotersList'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import styles from './ShoutRatingControl.module.scss' import styles from './ShoutRatingControl.module.scss'
interface ShoutRatingControlProps { interface ShoutRatingControlProps {
@ -19,12 +21,12 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
user, user,
actions: { requireAuthentication } actions: { requireAuthentication },
} = useSession() } = useSession()
const { const {
reactionEntities, reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy } actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions() } = useReactions()
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
@ -33,7 +35,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.createdBy.slug === user()?.slug &&
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.replyTo !r.replyTo,
) )
const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like)) const isUpvoted = createMemo(() => checkReaction(ReactionKind.Like))
@ -45,8 +47,8 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
(r) => (r) =>
[ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) && [ReactionKind.Like, ReactionKind.Dislike].includes(r.kind) &&
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.replyTo !r.replyTo,
) ),
) )
const deleteShoutReaction = async (reactionKind: ReactionKind) => { const deleteShoutReaction = async (reactionKind: ReactionKind) => {
@ -55,7 +57,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
r.kind === reactionKind && r.kind === reactionKind &&
r.createdBy.slug === user()?.slug && r.createdBy.slug === user()?.slug &&
r.shout.id === props.shout.id && r.shout.id === props.shout.id &&
!r.replyTo !r.replyTo,
) )
return deleteReaction(reactionToDelete.id) return deleteReaction(reactionToDelete.id)
} }
@ -69,13 +71,13 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
} else { } else {
await createReaction({ await createReaction({
kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike, kind: isUpvote ? ReactionKind.Like : ReactionKind.Dislike,
shout: props.shout.id shout: props.shout.id,
}) })
} }
loadShout(props.shout.slug) loadShout(props.shout.slug)
loadReactionsBy({ loadReactionsBy({
by: { shout: props.shout.slug } by: { shout: props.shout.slug },
}) })
}, 'vote') }, 'vote')
} }

View File

@ -1,8 +1,9 @@
import { createEffect, JSX, Show } from 'solid-js' import { createEffect, JSX, Show } from 'solid-js'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { hideModal } from '../../stores/ui'
import { useRouter } from '../../stores/router'
import { RootSearchParams } from '../../pages/types' import { RootSearchParams } from '../../pages/types'
import { useRouter } from '../../stores/router'
import { hideModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types' import { AuthModalSearchParams } from '../Nav/AuthModal/types'
type Props = { type Props = {
@ -25,9 +26,9 @@ export const AuthGuard = (props: Props) => {
changeSearchParam( changeSearchParam(
{ {
source: 'authguard', source: 'authguard',
modal: 'auth' modal: 'auth',
}, },
true true,
) )
} }
} }

View File

@ -1,8 +1,10 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AhtorLink.module.scss'
import { Author } from '../../../graphql/types.gen' import { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AhtorLink.module.scss'
type Props = { type Props = {
author: Author author: Author
size?: 'XS' | 'M' | 'L' size?: 'XS' | 'M' | 'L'

View File

@ -1,17 +1,19 @@
import { openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router'
import { follow, unfollow } from '../../../stores/zine/common'
import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton'
import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss' import stylesButton from '../../_shared/Button/Button.module.scss'
import { Userpic } from '../Userpic'
import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session'
import { follow, unfollow } from '../../../stores/zine/common'
import { CheckButton } from '../../_shared/CheckButton'
import { openPage } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
type Props = { type Props = {
author: Author author: Author
@ -25,12 +27,12 @@ export const AuthorBadge = (props: Props) => {
const { const {
session, session,
subscriptions, subscriptions,
actions: { loadSubscriptions, requireAuthentication } actions: { loadSubscriptions, requireAuthentication },
} = useSession() } = useSession()
const { changeSearchParam } = useRouter() const { changeSearchParam } = useRouter()
const { t, formatDate } = useLocalize() const { t, formatDate } = useLocalize()
const subscribed = createMemo(() => const subscribed = createMemo(() =>
subscriptions().authors.some((author) => author.slug === props.author.slug) subscriptions().authors.some((author) => author.slug === props.author.slug),
) )
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
@ -53,7 +55,7 @@ export const AuthorBadge = (props: Props) => {
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, `inbox`)
changeSearchParam({ changeSearchParam({
initChat: props.author.id.toString() initChat: props.author.id.toString(),
}) })
}, 'discussions') }, 'discussions')
} }
@ -126,7 +128,7 @@ export const AuthorBadge = (props: Props) => {
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons, [styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed() [stylesButton.subscribed]: subscribed(),
})} })}
/> />
} }
@ -151,7 +153,7 @@ export const AuthorBadge = (props: Props) => {
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons, [styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed() [stylesButton.subscribed]: subscribed(),
})} })}
/> />
</Show> </Show>

View File

@ -1,22 +1,25 @@
import type { Author } from '../../../graphql/types.gen' import type { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { translit } from '../../../utils/ru2en'
import { follow, unfollow } from '../../../stores/zine/common'
import { clsx } from 'clsx'
import { useSession } from '../../../context/session'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router'
import { openPage, redirectPage } from '@nanostores/router' import { openPage, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal' import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router'
import { follow, unfollow } from '../../../stores/zine/common'
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
import { AuthorBadge } from '../AuthorBadge' import { translit } from '../../../utils/ru2en'
import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { getShareUrl, SharePopup } from '../../Article/SharePopup' import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { Modal } from '../../Nav/Modal'
import { TopicBadge } from '../../Topic/TopicBadge'
import { AuthorBadge } from '../AuthorBadge'
import { Userpic } from '../Userpic'
import styles from './AuthorCard.module.scss' import styles from './AuthorCard.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss' import stylesButton from '../../_shared/Button/Button.module.scss'
@ -32,7 +35,7 @@ export const AuthorCard = (props: Props) => {
session, session,
subscriptions, subscriptions,
isSessionLoaded, isSessionLoaded,
actions: { loadSubscriptions, requireAuthentication } actions: { loadSubscriptions, requireAuthentication },
} = useSession() } = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
@ -40,7 +43,7 @@ export const AuthorCard = (props: Props) => {
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const subscribed = createMemo<boolean>(() => const subscribed = createMemo<boolean>(() =>
subscriptions().authors.some((author) => author.slug === props.author.slug) subscriptions().authors.some((author) => author.slug === props.author.slug),
) )
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
@ -74,7 +77,7 @@ export const AuthorCard = (props: Props) => {
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, `inbox`)
changeSearchParam({ changeSearchParam({
initChat: props.author.id.toString() initChat: props.author.id.toString(),
}) })
}, 'discussions') }, 'discussions')
} }
@ -218,7 +221,7 @@ export const AuthorCard = (props: Props) => {
value={followButtonText()} value={followButtonText()}
isSubscribeButton={true} isSubscribeButton={true}
class={clsx({ class={clsx({
[stylesButton.subscribed]: subscribed() [stylesButton.subscribed]: subscribed(),
})} })}
/> />
<Button <Button

View File

@ -1,7 +1,9 @@
import styles from './AuthorRatingControl.module.scss'
import { clsx } from 'clsx'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { clsx } from 'clsx'
import styles from './AuthorRatingControl.module.scss'
interface AuthorRatingControlProps { interface AuthorRatingControlProps {
author: Author author: Author
class?: string class?: string
@ -20,7 +22,7 @@ export const AuthorRatingControl = (props: AuthorRatingControlProps) => {
<div <div
class={clsx(styles.rating, props.class, { class={clsx(styles.rating, props.class, {
[styles.isUpvoted]: isUpvoted, [styles.isUpvoted]: isUpvoted,
[styles.isDownvoted]: isDownvoted [styles.isDownvoted]: isDownvoted,
})} })}
> >
<button <button

View File

@ -1,9 +1,11 @@
import { createMemo, Show } from 'solid-js'
import styles from './Userpic.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Loading } from '../../_shared/Loading'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
import { Loading } from '../../_shared/Loading'
import styles from './Userpic.module.scss'
type Props = { type Props = {
name: string name: string
@ -46,7 +48,7 @@ export const Userpic = (props: Props) => {
return ( return (
<div <div
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], { class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
['cursorPointer']: props.onClick ['cursorPointer']: props.onClick,
})} })}
onClick={props.onClick} onClick={props.onClick}
> >

View File

@ -1,8 +1,9 @@
import styles from './Banner.module.scss'
import { showModal } from '../../stores/ui'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { showModal } from '../../stores/ui'
import styles from './Banner.module.scss'
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()

View File

@ -1,8 +1,9 @@
import '../../styles/help.scss' import '../../styles/help.scss'
import { createSignal, onMount } from 'solid-js' import { createSignal, onMount } from 'solid-js'
import { showModal } from '../../stores/ui'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { showModal } from '../../stores/ui'
export const Donate = () => { export const Donate = () => {
const { t } = useLocalize() const { t } = useLocalize()
@ -11,7 +12,7 @@ export const Donate = () => {
const cpOptions = { const cpOptions = {
publicId: 'pk_0a37bab30ffc6b77b2f93d65f2aed', publicId: 'pk_0a37bab30ffc6b77b2f93d65f2aed',
description: t('Help discours to grow'), description: t('Help discours to grow'),
currency: 'RUB' currency: 'RUB',
} }
let amountSwitchElement: HTMLDivElement | undefined let amountSwitchElement: HTMLDivElement | undefined
@ -22,13 +23,13 @@ export const Donate = () => {
const [period, setPeriod] = createSignal(monthly) const [period, setPeriod] = createSignal(monthly)
const [amount, setAmount] = createSignal(0) const [amount, setAmount] = createSignal(0)
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const initiated = () => { const initiated = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const { const {
cp: { CloudPayments } cp: { CloudPayments },
} = window as any // Checkout(cpOptions) } = window as any // Checkout(cpOptions)
setWidget(new CloudPayments()) setWidget(new CloudPayments())
console.log('[donate] payments initiated') console.log('[donate] payments initiated')
@ -42,8 +43,8 @@ export const Donate = () => {
amount: amount() || 0, //сумма amount: amount() || 0, //сумма
vat: 20, //ставка НДС vat: 20, //ставка НДС
method: 0, // тег-1214 признак способа расчета - признак способа расчета method: 0, // тег-1214 признак способа расчета - признак способа расчета
object: 0 // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета object: 0, // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
} },
], ],
// taxationSystem: 0, //система налогообложения; необязательный, если у вас одна система налогообложения // taxationSystem: 0, //система налогообложения; необязательный, если у вас одна система налогообложения
// email: 'user@example.com', //e-mail покупателя, если нужно отправить письмо с чеком // email: 'user@example.com', //e-mail покупателя, если нужно отправить письмо с чеком
@ -53,8 +54,8 @@ export const Donate = () => {
electronic: amount(), // Сумма оплаты электронными деньгами electronic: amount(), // Сумма оплаты электронными деньгами
advancePayment: 0, // Сумма из предоплаты (зачетом аванса) (2 знака после запятой) advancePayment: 0, // Сумма из предоплаты (зачетом аванса) (2 знака после запятой)
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой) credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
provision: 0 // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой) provision: 0, // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
} },
}) })
} }
@ -93,10 +94,10 @@ export const Donate = () => {
recurrent: { recurrent: {
interval: period(), // local solid's signal interval: period(), // local solid's signal
period: 1, // internal widget's period: 1, // internal widget's
CustomerReciept: customerReciept() // чек для регулярных платежей CustomerReciept: customerReciept(), // чек для регулярных платежей
} },
} },
} },
}, },
(opts) => { (opts) => {
// success // success
@ -111,9 +112,9 @@ export const Donate = () => {
showSnackbar({ showSnackbar({
type: 'error', type: 'error',
body: reason body: reason,
}) })
} },
) )
} }

View File

@ -1,5 +1,5 @@
import { hideModal } from '../../stores/ui'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { hideModal } from '../../stores/ui'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
export const Feedback = () => { export const Feedback = () => {
@ -14,9 +14,9 @@ export const Feedback = () => {
method, method,
headers: { headers: {
accept: 'application/json', accept: 'application/json',
'content-type': 'application/json; charset=utf-8' 'content-type': 'application/json; charset=utf-8',
}, },
body: JSON.stringify({ contact: contactElement?.value, message: msgElement?.textContent }) body: JSON.stringify({ contact: contactElement?.value, message: msgElement?.textContent }),
}) })
hideModal() hideModal()
} }

View File

@ -1,10 +1,11 @@
import { clsx } from 'clsx'
import { createMemo, For } from 'solid-js' import { createMemo, For } from 'solid-js'
import styles from './Footer.module.scss'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Subscribe } from '../_shared/Subscribe' import { Subscribe } from '../_shared/Subscribe'
import { clsx } from 'clsx' import styles from './Footer.module.scss'
import { useLocalize } from '../../context/localize'
export const Footer = () => { export const Footer = () => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
@ -17,25 +18,25 @@ export const Footer = () => {
items: [ items: [
{ {
title: 'Manifest', title: 'Manifest',
slug: '/about/manifest' slug: '/about/manifest',
}, },
{ {
title: 'How it works', title: 'How it works',
slug: '/about/guide' slug: '/about/guide',
}, },
{ {
title: 'Dogma', title: 'Dogma',
slug: '/about/dogma' slug: '/about/dogma',
}, },
{ {
title: 'Principles', title: 'Principles',
slug: '/about/principles' slug: '/about/principles',
}, },
{ {
title: 'How to write an article', title: 'How to write an article',
slug: '/how-to-write-a-good-article' slug: '/how-to-write-a-good-article',
} },
] ],
}, },
{ {
@ -43,25 +44,25 @@ export const Footer = () => {
items: [ items: [
{ {
title: 'Suggest an idea', title: 'Suggest an idea',
slug: '/connect' slug: '/connect',
}, },
{ {
title: 'Become an author', title: 'Become an author',
slug: '/create' slug: '/create',
}, },
{ {
title: 'Support us', title: 'Support us',
slug: '/about/help' slug: '/about/help',
}, },
{ {
title: 'Feedback', title: 'Feedback',
slug: '/#feedback' slug: '/#feedback',
}, },
{ {
title: 'Work with us', title: 'Work with us',
slug: 'https://docs.google.com/forms/d/e/1FAIpQLSeNNvIzKlXElJtkPkYiXl-jQjlvsL9u4-kpnoRjz1O8Wo40xQ/viewform' slug: 'https://docs.google.com/forms/d/e/1FAIpQLSeNNvIzKlXElJtkPkYiXl-jQjlvsL9u4-kpnoRjz1O8Wo40xQ/viewform',
} },
] ],
}, },
{ {
@ -69,46 +70,46 @@ export const Footer = () => {
items: [ items: [
{ {
title: 'Authors', title: 'Authors',
slug: '/authors' slug: '/authors',
}, },
{ {
title: 'Communities', title: 'Communities',
slug: '/community' slug: '/community',
}, },
{ {
title: 'Partners', title: 'Partners',
slug: '/about/partners' slug: '/about/partners',
}, },
{ {
title: 'Special projects', title: 'Special projects',
slug: '/about/projects' slug: '/about/projects',
}, },
{ {
title: changeLangTitle(), title: changeLangTitle(),
slug: changeLangLink(), slug: changeLangLink(),
rel: 'external' rel: 'external',
} },
] ],
} },
]) ])
const SOCIAL = [ const SOCIAL = [
{ {
name: 'facebook', name: 'facebook',
href: 'https://facebook.com/discoursio' href: 'https://facebook.com/discoursio',
}, },
{ {
name: 'vk', name: 'vk',
href: 'https://vk.com/discoursio' href: 'https://vk.com/discoursio',
}, },
{ {
name: 'twitter', name: 'twitter',
href: 'https://twitter.com/discours_io' href: 'https://twitter.com/discours_io',
}, },
{ {
name: 'telegram', name: 'telegram',
href: 'https://t.me/discoursio' href: 'https://t.me/discoursio',
} },
] ]
return ( return (
<footer class={styles.discoursFooter}> <footer class={styles.discoursFooter}>
@ -143,7 +144,7 @@ export const Footer = () => {
<div class={clsx(styles.footerCopyright, 'row')}> <div class={clsx(styles.footerCopyright, 'row')}>
<div class="col-md-18 col-lg-20"> <div class="col-md-18 col-lg-20">
{t( {t(
'Independant magazine with an open horizontal cooperation about culture, science and society' 'Independant magazine with an open horizontal cooperation about culture, science and society',
)} )}
. {t('Discours')} &copy; 2015&ndash;{new Date().getFullYear()}{' '} . {t('Discours')} &copy; 2015&ndash;{new Date().getFullYear()}{' '}
<a href="/about/terms-of-use">{t('Terms of use')}</a> <a href="/about/terms-of-use">{t('Terms of use')}</a>

View File

@ -1,10 +1,10 @@
import styles from './Hero.module.scss'
import { showModal } from '../../stores/ui'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui'
import { AuthModalSearchParams } from '../Nav/AuthModal/types' import { AuthModalSearchParams } from '../Nav/AuthModal/types'
import styles from './Hero.module.scss'
export default () => { export default () => {
const { t } = useLocalize() const { t } = useLocalize()
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParam } = useRouter<AuthModalSearchParams>()
@ -17,7 +17,7 @@ export default () => {
<h4 innerHTML={t('Horizontal collaborative journalistic platform')} /> <h4 innerHTML={t('Horizontal collaborative journalistic platform')} />
<p <p
innerHTML={t( innerHTML={t(
'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects' 'Discours is an intellectual environment, a web space and tools that allows authors to collaborate with readers and come together to co-create publications and media projects',
)} )}
/> />
<div class={styles.aboutDiscoursActions}> <div class={styles.aboutDiscoursActions}>
@ -29,7 +29,7 @@ export default () => {
onClick={() => { onClick={() => {
showModal('auth') showModal('auth')
changeSearchParam({ changeSearchParam({
mode: 'register' mode: 'register',
}) })
}} }}
> >

View File

@ -1,12 +1,15 @@
import { clsx } from 'clsx'
import styles from './Draft.module.scss'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize'
import { useConfirm } from '../../context/confirm'
import { useSnackbar } from '../../context/snackbar'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { useConfirm } from '../../context/confirm'
import { useLocalize } from '../../context/localize'
import { useSnackbar } from '../../context/snackbar'
import { router } from '../../stores/router' import { router } from '../../stores/router'
import { Icon } from '../_shared/Icon'
import styles from './Draft.module.scss'
type Props = { type Props = {
class?: string class?: string
@ -18,11 +21,11 @@ type Props = {
export const Draft = (props: Props) => { export const Draft = (props: Props) => {
const { t, formatDate } = useLocalize() const { t, formatDate } = useLocalize()
const { const {
actions: { showConfirm } actions: { showConfirm },
} = useConfirm() } = useConfirm()
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const handlePublishLinkClick = (e) => { const handlePublishLinkClick = (e) => {
@ -37,7 +40,7 @@ export const Draft = (props: Props) => {
confirmBody: t('Are you sure you want to delete this draft?'), confirmBody: t('Are you sure you want to delete this draft?'),
confirmButtonLabel: t('Delete'), confirmButtonLabel: t('Delete'),
confirmButtonVariant: 'danger', confirmButtonVariant: 'danger',
declineButtonVariant: 'primary' declineButtonVariant: 'primary',
}) })
if (isConfirmed) { if (isConfirmed) {
props.onDelete(props.shout) props.onDelete(props.shout)

View File

@ -1,12 +1,15 @@
import { Buffer } from 'buffer'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AudioUploader.module.scss'
import { DropArea } from '../../_shared/DropArea'
import { useLocalize } from '../../../context/localize'
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { composeMediaItems } from '../../../utils/composeMediaItems' import { composeMediaItems } from '../../../utils/composeMediaItems'
import { DropArea } from '../../_shared/DropArea'
import { AudioPlayer } from '../../Article/AudioPlayer' import { AudioPlayer } from '../../Article/AudioPlayer'
import { Buffer } from 'buffer'
import styles from './AudioUploader.module.scss'
window.Buffer = Buffer window.Buffer = Buffer

View File

@ -1,7 +1,9 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AutoSaveNotice.module.scss'
import { Loading } from '../../_shared/Loading'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Loading } from '../../_shared/Loading'
import styles from './AutoSaveNotice.module.scss'
type Props = { type Props = {
active: boolean active: boolean

View File

@ -1,9 +1,11 @@
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import styles from './BubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover' import { Popover } from '../../_shared/Popover'
import styles from './BubbleMenu.module.scss'
type Props = { type Props = {
editor: Editor editor: Editor
ref: (el: HTMLElement) => void ref: (el: HTMLElement) => void

View File

@ -1,12 +1,14 @@
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import styles from './BubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Popover } from '../../_shared/Popover'
import { UploadModalContent } from '../UploadModalContent'
import { Modal } from '../../Nav/Modal'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { renderUploadedImage } from '../../../utils/renderUploadedImage' import { renderUploadedImage } from '../../../utils/renderUploadedImage'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import { Modal } from '../../Nav/Modal'
import { UploadModalContent } from '../UploadModalContent'
import styles from './BubbleMenu.module.scss'
type Props = { type Props = {
editor: Editor editor: Editor

View File

@ -1,9 +1,12 @@
import { createSignal, Show, For } from 'solid-js'
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import styles from './BubbleMenu.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Icon } from '../../_shared/Icon' import { createSignal, Show, For } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import styles from './BubbleMenu.module.scss'
type Props = { type Props = {
editor: Editor editor: Editor

View File

@ -1,52 +1,56 @@
import { createEffect, createSignal, onCleanup } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import uniqolor from 'uniqolor'
import * as Y from 'yjs'
import type { Doc } from 'yjs/dist/src/utils/Doc' import type { Doc } from 'yjs/dist/src/utils/Doc'
import { HocuspocusProvider } from '@hocuspocus/provider'
import { isTextSelection } from '@tiptap/core'
import { Bold } from '@tiptap/extension-bold' import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { Italic } from '@tiptap/extension-italic'
import { Strike } from '@tiptap/extension-strike'
import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Underline } from '@tiptap/extension-underline'
import { FloatingMenu } from '@tiptap/extension-floating-menu'
import { BulletList } from '@tiptap/extension-bullet-list' import { BulletList } from '@tiptap/extension-bullet-list'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { ListItem } from '@tiptap/extension-list-item'
import { CharacterCount } from '@tiptap/extension-character-count' import { CharacterCount } from '@tiptap/extension-character-count'
import { Placeholder } from '@tiptap/extension-placeholder' import { Collaboration } from '@tiptap/extension-collaboration'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { Document } from '@tiptap/extension-document'
import { Dropcursor } from '@tiptap/extension-dropcursor'
import { FloatingMenu } from '@tiptap/extension-floating-menu'
import Focus from '@tiptap/extension-focus'
import { Gapcursor } from '@tiptap/extension-gapcursor' import { Gapcursor } from '@tiptap/extension-gapcursor'
import { HardBreak } from '@tiptap/extension-hard-break' import { HardBreak } from '@tiptap/extension-hard-break'
import { Heading } from '@tiptap/extension-heading' import { Heading } from '@tiptap/extension-heading'
import { Highlight } from '@tiptap/extension-highlight' import { Highlight } from '@tiptap/extension-highlight'
import { Link } from '@tiptap/extension-link' import { HorizontalRule } from '@tiptap/extension-horizontal-rule'
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor'
import { isTextSelection } from '@tiptap/core'
import { Paragraph } from '@tiptap/extension-paragraph'
import Focus from '@tiptap/extension-focus'
import { Collaboration } from '@tiptap/extension-collaboration'
import { HocuspocusProvider } from '@hocuspocus/provider'
import { CustomBlockquote } from './extensions/CustomBlockquote'
import { Figure } from './extensions/Figure'
import { Figcaption } from './extensions/Figcaption'
import { Embed } from './extensions/Embed'
import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize'
import { useEditorContext } from '../../context/editor'
import { TrailingNode } from './extensions/TrailingNode'
import Article from './extensions/Article'
import { TextBubbleMenu } from './TextBubbleMenu'
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import './Prosemirror.scss'
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { Footnote } from './extensions/Footnote' import { Italic } from '@tiptap/extension-italic'
import { Link } from '@tiptap/extension-link'
import { ListItem } from '@tiptap/extension-list-item'
import { OrderedList } from '@tiptap/extension-ordered-list'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Strike } from '@tiptap/extension-strike'
import { Text } from '@tiptap/extension-text'
import { Underline } from '@tiptap/extension-underline'
import { createEffect, createSignal, onCleanup } from 'solid-js'
import { createTiptapEditor, useEditorHTML } from 'solid-tiptap'
import uniqolor from 'uniqolor'
import * as Y from 'yjs'
import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { handleImageUpload } from '../../utils/handleImageUpload' import { handleImageUpload } from '../../utils/handleImageUpload'
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu'
import Article from './extensions/Article'
import { CustomBlockquote } from './extensions/CustomBlockquote'
import { Embed } from './extensions/Embed'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { Footnote } from './extensions/Footnote'
import { TrailingNode } from './extensions/TrailingNode'
import { TextBubbleMenu } from './TextBubbleMenu'
import './Prosemirror.scss'
type Props = { type Props = {
shoutId: number shoutId: number
initialContent?: string initialContent?: string
@ -61,7 +65,7 @@ const allowedImageTypes = new Set([
'image/png', 'image/png',
'image/tiff', 'image/tiff',
'image/webp', 'image/webp',
'image/x-icon' 'image/x-icon',
]) ])
const yDocs: Record<string, Doc> = {} const yDocs: Record<string, Doc> = {}
@ -75,7 +79,7 @@ export const Editor = (props: Props) => {
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const docName = `shout-${props.shoutId}` const docName = `shout-${props.shoutId}`
@ -88,47 +92,47 @@ export const Editor = (props: Props) => {
providers[docName] = new HocuspocusProvider({ providers[docName] = new HocuspocusProvider({
url: 'wss://hocuspocus.discours.io', url: 'wss://hocuspocus.discours.io',
name: docName, name: docName,
document: yDocs[docName] document: yDocs[docName],
}) })
} }
const editorElRef: { const editorElRef: {
current: HTMLDivElement current: HTMLDivElement
} = { } = {
current: null current: null,
} }
const textBubbleMenuRef: { const textBubbleMenuRef: {
current: HTMLDivElement current: HTMLDivElement
} = { } = {
current: null current: null,
} }
const incutBubbleMenuRef: { const incutBubbleMenuRef: {
current: HTMLElement current: HTMLElement
} = { } = {
current: null current: null,
} }
const figureBubbleMenuRef: { const figureBubbleMenuRef: {
current: HTMLElement current: HTMLElement
} = { } = {
current: null current: null,
} }
const blockquoteBubbleMenuRef: { const blockquoteBubbleMenuRef: {
current: HTMLElement current: HTMLElement
} = { } = {
current: null current: null,
} }
const floatingMenuRef: { const floatingMenuRef: {
current: HTMLDivElement current: HTMLDivElement
} = { } = {
current: null current: null,
} }
const ImageFigure = Figure.extend({ const ImageFigure = Figure.extend({
name: 'capturedImage', name: 'capturedImage',
content: 'figcaption image' content: 'figcaption image',
}) })
const handleClipboardPaste = async () => { const handleClipboardPaste = async () => {
@ -149,7 +153,7 @@ export const Editor = (props: Props) => {
source: blob.toString(), source: blob.toString(),
name: file.name, name: file.name,
size: file.size, size: file.size,
file file,
} }
showSnackbar({ body: t('Uploading image') }) showSnackbar({ body: t('Uploading image') })
@ -166,17 +170,17 @@ export const Editor = (props: Props) => {
content: [ content: [
{ {
type: 'text', type: 'text',
text: result.originalFilename text: result.originalFilename,
} },
] ],
}, },
{ {
type: 'image', type: 'image',
attrs: { attrs: {
src: result.url src: result.url,
} },
} },
] ],
}) })
.run() .run()
} catch (error) { } catch (error) {
@ -190,7 +194,7 @@ export const Editor = (props: Props) => {
element: editorElRef.current, element: editorElRef.current,
editorProps: { editorProps: {
attributes: { attributes: {
class: 'articleEditor' class: 'articleEditor',
}, },
transformPastedHTML(html) { transformPastedHTML(html) {
return html.replaceAll(/<img.*?>/g, '') return html.replaceAll(/<img.*?>/g, '')
@ -198,7 +202,7 @@ export const Editor = (props: Props) => {
handlePaste: () => { handlePaste: () => {
handleClipboardPaste() handleClipboardPaste()
return false return false
} },
}, },
extensions: [ extensions: [
Document, Document,
@ -211,31 +215,31 @@ export const Editor = (props: Props) => {
Strike, Strike,
HorizontalRule.configure({ HorizontalRule.configure({
HTMLAttributes: { HTMLAttributes: {
class: 'horizontalRule' class: 'horizontalRule',
} },
}), }),
Underline, Underline,
Link.configure({ Link.configure({
openOnClick: false openOnClick: false,
}), }),
Heading.configure({ Heading.configure({
levels: [2, 3, 4] levels: [2, 3, 4],
}), }),
BulletList, BulletList,
OrderedList, OrderedList,
ListItem, ListItem,
Collaboration.configure({ Collaboration.configure({
document: yDocs[docName] document: yDocs[docName],
}), }),
CollaborationCursor.configure({ CollaborationCursor.configure({
provider: providers[docName], provider: providers[docName],
user: { user: {
name: user().name, name: user().name,
color: uniqolor(user().slug).color color: uniqolor(user().slug).color,
} },
}), }),
Placeholder.configure({ Placeholder.configure({
placeholder: t('Add a link or click plus to embed media') placeholder: t('Add a link or click plus to embed media'),
}), }),
Focus, Focus,
Gapcursor, Gapcursor,
@ -243,8 +247,8 @@ export const Editor = (props: Props) => {
Highlight.configure({ Highlight.configure({
multicolor: true, multicolor: true,
HTMLAttributes: { HTMLAttributes: {
class: 'highlight' class: 'highlight',
} },
}), }),
ImageFigure, ImageFigure,
Image, Image,
@ -267,8 +271,8 @@ export const Editor = (props: Props) => {
return result return result
}, },
tippyOptions: { tippyOptions: {
sticky: true sticky: true,
} },
}), }),
BubbleMenu.configure({ BubbleMenu.configure({
pluginKey: 'blockquoteBubbleMenu', pluginKey: 'blockquoteBubbleMenu',
@ -286,8 +290,8 @@ export const Editor = (props: Props) => {
if (selectedElement) { if (selectedElement) {
return selectedElement.getBoundingClientRect() return selectedElement.getBoundingClientRect()
} }
} },
} },
}), }),
BubbleMenu.configure({ BubbleMenu.configure({
pluginKey: 'incutBubbleMenu', pluginKey: 'incutBubbleMenu',
@ -305,31 +309,31 @@ export const Editor = (props: Props) => {
if (selectedElement) { if (selectedElement) {
return selectedElement.getBoundingClientRect() return selectedElement.getBoundingClientRect()
} }
} },
} },
}), }),
BubbleMenu.configure({ BubbleMenu.configure({
pluginKey: 'imageBubbleMenu', pluginKey: 'imageBubbleMenu',
element: figureBubbleMenuRef.current, element: figureBubbleMenuRef.current,
shouldShow: ({ editor: e, view }) => { shouldShow: ({ editor: e, view }) => {
return view.hasFocus() && e.isActive('image') return view.hasFocus() && e.isActive('image')
} },
}), }),
FloatingMenu.configure({ FloatingMenu.configure({
tippyOptions: { tippyOptions: {
placement: 'left' placement: 'left',
}, },
element: floatingMenuRef.current element: floatingMenuRef.current,
}), }),
TrailingNode, TrailingNode,
Article Article,
], ],
enablePasteRules: [Link], enablePasteRules: [Link],
content: initialContent ?? null content: initialContent ?? null,
})) }))
const { const {
actions: { countWords, setEditor } actions: { countWords, setEditor },
} = useEditorContext() } = useEditorContext()
setEditor(editor) setEditor(editor)
@ -341,7 +345,7 @@ export const Editor = (props: Props) => {
if (html()) { if (html()) {
countWords({ countWords({
characters: editor().storage.characterCount.characters(), characters: editor().storage.characterCount.characters(),
words: editor().storage.characterCount.words() words: editor().storage.characterCount.words(),
}) })
} }
}) })

View File

@ -1,17 +1,21 @@
import { createEffect, createSignal, Show } from 'solid-js'
import type { Editor } from '@tiptap/core'
import { Icon } from '../../_shared/Icon'
import { InlineForm } from '../InlineForm'
import styles from './EditorFloatingMenu.module.scss'
import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal'
import { Menu } from './Menu'
import type { MenuItem } from './Menu/Menu' import type { MenuItem } from './Menu/Menu'
import { showModal } from '../../../stores/ui' import type { Editor } from '@tiptap/core'
import { UploadModalContent } from '../UploadModalContent'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler' import { createEffect, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { showModal } from '../../../stores/ui'
import { renderUploadedImage } from '../../../utils/renderUploadedImage' import { renderUploadedImage } from '../../../utils/renderUploadedImage'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import { Modal } from '../../Nav/Modal'
import { InlineForm } from '../InlineForm'
import { UploadModalContent } from '../UploadModalContent'
import { Menu } from './Menu'
import styles from './EditorFloatingMenu.module.scss'
type FloatingMenuProps = { type FloatingMenuProps = {
editor: Editor editor: Editor
@ -80,7 +84,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
if (menuOpen()) { if (menuOpen()) {
setMenuOpen(false) setMenuOpen(false)
} }
} },
}) })
const handleUpload = (image: UploadedFile) => { const handleUpload = (image: UploadedFile) => {

View File

@ -1,7 +1,8 @@
import styles from './Menu.module.scss' import { useLocalize } from '../../../../context/localize'
import { Icon } from '../../../_shared/Icon' import { Icon } from '../../../_shared/Icon'
import { Popover } from '../../../_shared/Popover' import { Popover } from '../../../_shared/Popover'
import { useLocalize } from '../../../../context/localize'
import styles from './Menu.module.scss'
export type MenuItem = 'image' | 'embed' | 'horizontal-rule' export type MenuItem = 'image' | 'embed' | 'horizontal-rule'

View File

@ -39,7 +39,9 @@
border: 1px solid #e9e9ee; border: 1px solid #e9e9ee;
border-radius: 2px; border-radius: 2px;
opacity: 0; opacity: 0;
transition: height 0.3s ease-in-out, opacity 0.3s ease-in-out; transition:
height 0.3s ease-in-out,
opacity 0.3s ease-in-out;
&.visible { &.visible {
height: 32px; height: 32px;

View File

@ -1,9 +1,11 @@
import styles from './InlineForm.module.scss'
import { Icon } from '../../_shared/Icon'
import { createSignal, onMount } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Popover } from '../../_shared/Popover' import { createSignal, onMount } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover'
import styles from './InlineForm.module.scss'
type Props = { type Props = {
onClose: () => void onClose: () => void

View File

@ -1,8 +1,9 @@
import { Editor } from '@tiptap/core' import { Editor } from '@tiptap/core'
import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize'
import { validateUrl } from '../../../utils/validateUrl' import { validateUrl } from '../../../utils/validateUrl'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
import { useLocalize } from '../../../context/localize'
import { createEditorTransaction } from 'solid-tiptap'
type Props = { type Props = {
editor: Editor editor: Editor
@ -24,7 +25,7 @@ export const InsertLinkForm = (props: Props) => {
() => props.editor, () => props.editor,
(ed) => { (ed) => {
return (ed && ed.getAttributes('link').href) || '' return (ed && ed.getAttributes('link').href) || ''
} },
) )
const handleClearLinkForm = () => { const handleClearLinkForm = () => {
if (currentUrl()) { if (currentUrl()) {

View File

@ -1,17 +1,19 @@
import { clsx } from 'clsx'
import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import styles from './Panel.module.scss'
import { useEditorContext } from '../../../context/editor'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { router } from '../../../stores/router' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useEditorHTML } from 'solid-tiptap' import { useEditorHTML } from 'solid-tiptap'
import Typograf from 'typograf' import Typograf from 'typograf'
import { createSignal, Show } from 'solid-js'
import { useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize'
import { router } from '../../../stores/router'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Button } from '../../_shared/Button'
import { DarkModeToggle } from '../../_shared/DarkModeToggle' import { DarkModeToggle } from '../../_shared/DarkModeToggle'
import { Icon } from '../../_shared/Icon'
import styles from './Panel.module.scss'
const typograf = new Typograf({ locale: ['ru', 'en-US'] }) const typograf = new Typograf({ locale: ['ru', 'en-US'] })
@ -26,11 +28,11 @@ export const Panel = (props: Props) => {
wordCounter, wordCounter,
editorRef, editorRef,
form, form,
actions: { toggleEditorPanel, saveShout, publishShout } actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext() } = useEditorContext()
const containerRef: { current: HTMLElement } = { const containerRef: { current: HTMLElement } = {
current: null current: null,
} }
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false) const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
@ -39,7 +41,7 @@ export const Panel = (props: Props) => {
useOutsideClickHandler({ useOutsideClickHandler({
containerRef, containerRef,
predicate: () => isEditorPanelVisible(), predicate: () => isEditorPanelVisible(),
handler: () => toggleEditorPanel() handler: () => toggleEditorPanel(),
}) })
useEscKeyDownHandler(() => { useEscKeyDownHandler(() => {

View File

@ -1,3 +1,15 @@
import { Blockquote } from '@tiptap/extension-blockquote'
import { Bold } from '@tiptap/extension-bold'
import { BubbleMenu } from '@tiptap/extension-bubble-menu'
import { CharacterCount } from '@tiptap/extension-character-count'
import { Document } from '@tiptap/extension-document'
import { Image } from '@tiptap/extension-image'
import { Italic } from '@tiptap/extension-italic'
import { Link } from '@tiptap/extension-link'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder'
import { Text } from '@tiptap/extension-text'
import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { Portal } from 'solid-js/web' import { Portal } from 'solid-js/web'
import { import {
@ -5,34 +17,25 @@ import {
createTiptapEditor, createTiptapEditor,
useEditorHTML, useEditorHTML,
useEditorIsEmpty, useEditorIsEmpty,
useEditorIsFocused useEditorIsFocused,
} from 'solid-tiptap' } from 'solid-tiptap'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
import { Document } from '@tiptap/extension-document'
import { Text } from '@tiptap/extension-text'
import { Paragraph } from '@tiptap/extension-paragraph'
import { Bold } from '@tiptap/extension-bold'
import { Button } from '../_shared/Button'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { UploadedFile } from '../../pages/types'
import { hideModal, showModal } from '../../stores/ui'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { Italic } from '@tiptap/extension-italic'
import { Modal } from '../Nav/Modal' import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui'
import { Blockquote } from '@tiptap/extension-blockquote'
import { UploadModalContent } from './UploadModalContent'
import { clsx } from 'clsx'
import styles from './SimplifiedEditor.module.scss'
import { Placeholder } from '@tiptap/extension-placeholder'
import { InsertLinkForm } from './InsertLinkForm'
import { Link } from '@tiptap/extension-link'
import { UploadedFile } from '../../pages/types'
import { Figure } from './extensions/Figure'
import { Image } from '@tiptap/extension-image'
import { Figcaption } from './extensions/Figcaption' import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { InsertLinkForm } from './InsertLinkForm'
import { TextBubbleMenu } from './TextBubbleMenu' import { TextBubbleMenu } from './TextBubbleMenu'
import { BubbleMenu } from '@tiptap/extension-bubble-menu' import { UploadModalContent } from './UploadModalContent'
import { CharacterCount } from '@tiptap/extension-character-count'
import styles from './SimplifiedEditor.module.scss'
type Props = { type Props = {
placeholder: string placeholder: string
@ -66,28 +69,28 @@ const SimplifiedEditor = (props: Props) => {
const wrapperEditorElRef: { const wrapperEditorElRef: {
current: HTMLElement current: HTMLElement
} = { } = {
current: null current: null,
} }
const editorElRef: { const editorElRef: {
current: HTMLElement current: HTMLElement
} = { } = {
current: null current: null,
} }
const textBubbleMenuRef: { const textBubbleMenuRef: {
current: HTMLDivElement current: HTMLDivElement
} = { } = {
current: null current: null,
} }
const { const {
actions: { setEditor } actions: { setEditor },
} = useEditorContext() } = useEditorContext()
const ImageFigure = Figure.extend({ const ImageFigure = Figure.extend({
name: 'capturedImage', name: 'capturedImage',
content: 'figcaption image' content: 'figcaption image',
}) })
const content = props.initialContent const content = props.initialContent
@ -95,8 +98,8 @@ const SimplifiedEditor = (props: Props) => {
element: editorElRef.current, element: editorElRef.current,
editorProps: { editorProps: {
attributes: { attributes: {
class: styles.simplifiedEditorField class: styles.simplifiedEditorField,
} },
}, },
extensions: [ extensions: [
Document, Document,
@ -105,16 +108,16 @@ const SimplifiedEditor = (props: Props) => {
Bold, Bold,
Italic, Italic,
Link.configure({ Link.configure({
openOnClick: false openOnClick: false,
}), }),
CharacterCount.configure({ CharacterCount.configure({
limit: MAX_DESCRIPTION_LIMIT limit: MAX_DESCRIPTION_LIMIT,
}), }),
Blockquote.configure({ Blockquote.configure({
HTMLAttributes: { HTMLAttributes: {
class: styles.blockQuote class: styles.blockQuote,
} },
}), }),
BubbleMenu.configure({ BubbleMenu.configure({
pluginKey: 'textBubbleMenu', pluginKey: 'textBubbleMenu',
@ -124,18 +127,18 @@ const SimplifiedEditor = (props: Props) => {
const { selection } = state const { selection } = state
const { empty } = selection const { empty } = selection
return view.hasFocus() && !empty return view.hasFocus() && !empty
} },
}), }),
ImageFigure, ImageFigure,
Image, Image,
Figcaption, Figcaption,
Placeholder.configure({ Placeholder.configure({
emptyNodeClass: styles.emptyNode, emptyNodeClass: styles.emptyNode,
placeholder: props.placeholder placeholder: props.placeholder,
}) }),
], ],
autofocus: props.autoFocus, autofocus: props.autoFocus,
content: content ?? null content: content ?? null,
})) }))
setEditor(editor) setEditor(editor)
@ -147,7 +150,7 @@ const SimplifiedEditor = (props: Props) => {
() => editor(), () => editor(),
(ed) => { (ed) => {
return ed && ed.isActive(name) return ed && ed.isActive(name)
} },
) )
const html = useEditorHTML(() => editor()) const html = useEditorHTML(() => editor())
@ -168,17 +171,17 @@ const SimplifiedEditor = (props: Props) => {
content: [ content: [
{ {
type: 'text', type: 'text',
text: image.originalFilename text: image.originalFilename,
} },
] ],
}, },
{ {
type: 'image', type: 'image',
attrs: { attrs: {
src: image.url src: image.url,
} },
} },
] ],
}) })
.run() .run()
hideModal() hideModal()
@ -238,7 +241,7 @@ const SimplifiedEditor = (props: Props) => {
const maxHeightStyle = { const maxHeightStyle = {
overflow: 'auto', overflow: 'auto',
'max-height': `${props.maxHeight}px` 'max-height': `${props.maxHeight}px`,
} }
return ( return (
@ -249,7 +252,7 @@ const SimplifiedEditor = (props: Props) => {
[styles.minimal]: props.variant === 'minimal', [styles.minimal]: props.variant === 'minimal',
[styles.bordered]: props.variant === 'bordered', [styles.bordered]: props.variant === 'bordered',
[styles.isFocused]: isFocused() || !isEmpty(), [styles.isFocused]: isFocused() || !isEmpty(),
[styles.labelVisible]: props.label && counter() > 0 [styles.labelVisible]: props.label && counter() > 0,
})} })}
> >
<Show when={props.maxLength && editor()}> <Show when={props.maxLength && editor()}>

View File

@ -1,14 +1,17 @@
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import styles from './TextBubbleMenu.module.scss'
import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect } from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover' import { Popover } from '../../_shared/Popover'
import { InsertLinkForm } from '../InsertLinkForm' import { InsertLinkForm } from '../InsertLinkForm'
import SimplifiedEditor from '../SimplifiedEditor' import SimplifiedEditor from '../SimplifiedEditor'
import styles from './TextBubbleMenu.module.scss'
type BubbleMenuProps = { type BubbleMenuProps = {
editor: Editor editor: Editor
isCommonMarkup: boolean isCommonMarkup: boolean
@ -22,7 +25,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
const isActive = (name: string, attributes?: unknown) => const isActive = (name: string, attributes?: unknown) =>
createEditorTransaction( createEditorTransaction(
() => props.editor, () => props.editor,
(editor) => editor && editor.isActive(name, attributes) (editor) => editor && editor.isActive(name, attributes),
) )
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
@ -79,7 +82,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
} }
const value = ed.getAttributes('footnote').value const value = ed.getAttributes('footnote').value
setFootNote(value) setFootNote(value)
} },
) )
const handleAddFootnote = (footnote) => { const handleAddFootnote = (footnote) => {
@ -148,7 +151,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
<button <button
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: textSizeBubbleOpen() [styles.bubbleMenuButtonActive]: textSizeBubbleOpen(),
})} })}
onClick={toggleTextSizePopup} onClick={toggleTextSizePopup}
> >
@ -165,7 +168,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH1() [styles.bubbleMenuButtonActive]: isH1(),
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 2 }).run() props.editor.chain().focus().toggleHeading({ level: 2 }).run()
@ -182,7 +185,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH2() [styles.bubbleMenuButtonActive]: isH2(),
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 3 }).run() props.editor.chain().focus().toggleHeading({ level: 3 }).run()
@ -199,7 +202,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isH3() [styles.bubbleMenuButtonActive]: isH3(),
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleHeading({ level: 4 }).run() props.editor.chain().focus().toggleHeading({ level: 4 }).run()
@ -219,7 +222,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isQuote() [styles.bubbleMenuButtonActive]: isQuote(),
})} })}
onClick={handleSetPunchline} onClick={handleSetPunchline}
> >
@ -233,7 +236,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isPunchLine() [styles.bubbleMenuButtonActive]: isPunchLine(),
})} })}
onClick={handleSetQuote} onClick={handleSetQuote}
> >
@ -250,7 +253,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isIncut() [styles.bubbleMenuButtonActive]: isIncut(),
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleArticle().run() props.editor.chain().focus().toggleArticle().run()
@ -274,7 +277,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBold() [styles.bubbleMenuButtonActive]: isBold(),
})} })}
onClick={() => props.editor.chain().focus().toggleBold().run()} onClick={() => props.editor.chain().focus().toggleBold().run()}
> >
@ -288,7 +291,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isItalic() [styles.bubbleMenuButtonActive]: isItalic(),
})} })}
onClick={() => props.editor.chain().focus().toggleItalic().run()} onClick={() => props.editor.chain().focus().toggleItalic().run()}
> >
@ -304,7 +307,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isHighlight() [styles.bubbleMenuButtonActive]: isHighlight(),
})} })}
onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()} onClick={() => props.editor.chain().focus().toggleHighlight({ color: '#f6e3a1' }).run()}
> >
@ -321,7 +324,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
type="button" type="button"
onClick={() => setLinkEditorOpen(true)} onClick={() => setLinkEditorOpen(true)}
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isLink() [styles.bubbleMenuButtonActive]: isLink(),
})} })}
> >
<Icon name="editor-link" /> <Icon name="editor-link" />
@ -336,7 +339,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isFootnote() [styles.bubbleMenuButtonActive]: isFootnote(),
})} })}
onClick={handleOpenFootnoteEditor} onClick={handleOpenFootnoteEditor}
> >
@ -349,7 +352,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
<button <button
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: listBubbleOpen() [styles.bubbleMenuButtonActive]: listBubbleOpen(),
})} })}
onClick={toggleListPopup} onClick={toggleListPopup}
> >
@ -366,7 +369,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isBulletList() [styles.bubbleMenuButtonActive]: isBulletList(),
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleBulletList().run() props.editor.chain().focus().toggleBulletList().run()
@ -383,7 +386,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
ref={triggerRef} ref={triggerRef}
type="button" type="button"
class={clsx(styles.bubbleMenuButton, { class={clsx(styles.bubbleMenuButton, {
[styles.bubbleMenuButtonActive]: isOrderedList() [styles.bubbleMenuButtonActive]: isOrderedList(),
})} })}
onClick={() => { onClick={() => {
props.editor.chain().focus().toggleOrderedList().run() props.editor.chain().focus().toggleOrderedList().run()

View File

@ -1,13 +1,17 @@
import type { Topic } from '../../../graphql/types.gen' import type { Topic } from '../../../graphql/types.gen'
import { createOptions, Select } from '@thisbeyond/solid-select' import { createOptions, Select } from '@thisbeyond/solid-select'
import { useLocalize } from '../../../context/localize'
import '@thisbeyond/solid-select/style.css'
import './TopicSelect.scss'
import styles from './TopicSelect.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { slugify } from '../../../utils/slugify'
import { useLocalize } from '../../../context/localize'
import { clone } from '../../../utils/clone' import { clone } from '../../../utils/clone'
import { slugify } from '../../../utils/slugify'
import '@thisbeyond/solid-select/style.css'
import './TopicSelect.scss'
import styles from './TopicSelect.module.scss'
type TopicSelectProps = { type TopicSelectProps = {
topics: Topic[] topics: Topic[]
@ -33,7 +37,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
disable: (topic) => { disable: (topic) => {
return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug) return props.selectedTopics.some((selectedTopic) => selectedTopic.slug === topic.slug)
}, },
createable: createValue createable: createValue,
}) })
const handleChange = (selectedTopics: Topic[]) => { const handleChange = (selectedTopics: Topic[]) => {
@ -57,7 +61,7 @@ export const TopicSelect = (props: TopicSelectProps) => {
return ( return (
<div <div
class={clsx(styles.selectedItem, { class={clsx(styles.selectedItem, {
[styles.mainTopic]: isMainTopic [styles.mainTopic]: isMainTopic,
})} })}
onClick={() => handleSelectedItemClick(item)} onClick={() => handleSelectedItemClick(item)}
> >

View File

@ -1,16 +1,18 @@
import styles from './UploadModalContent.module.scss'
import { clsx } from 'clsx'
import { Icon } from '../../_shared/Icon'
import { Button } from '../../_shared/Button'
import { createSignal, Show } from 'solid-js'
import { InlineForm } from '../InlineForm'
import { hideModal } from '../../../stores/ui'
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload' import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Loading } from '../../_shared/Loading'
import { verifyImg } from '../../../utils/verifyImg'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { hideModal } from '../../../stores/ui'
import { handleImageUpload } from '../../../utils/handleImageUpload' import { handleImageUpload } from '../../../utils/handleImageUpload'
import { verifyImg } from '../../../utils/verifyImg'
import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon'
import { Loading } from '../../_shared/Loading'
import { InlineForm } from '../InlineForm'
import styles from './UploadModalContent.module.scss'
type Props = { type Props = {
onClose: (image?: UploadedFile) => void onClose: (image?: UploadedFile) => void
@ -46,7 +48,7 @@ export const UploadModalContent = (props: Props) => {
source: blob.toString(), source: blob.toString(),
name: file.name, name: file.name,
size: file.size, size: file.size,
file: file file: file,
} }
await runUpload(fileToUpload) await runUpload(fileToUpload)
} catch (error) { } catch (error) {
@ -70,7 +72,7 @@ export const UploadModalContent = (props: Props) => {
} else { } else {
setDragError(t('Image format not supported')) setDragError(t('Image format not supported'))
} }
} },
}) })
const handleDrag = (event: MouseEvent) => { const handleDrag = (event: MouseEvent) => {
if (event.type === 'dragenter' || event.type === 'dragover') { if (event.type === 'dragenter' || event.type === 'dragover') {

View File

@ -1,14 +1,17 @@
import { clsx } from 'clsx'
import styles from './VideoUploader.module.scss'
import { useLocalize } from '../../../context/localize'
import { createDropzone } from '@solid-primitives/upload'
import { createSignal, For, Show } from 'solid-js'
import { useSnackbar } from '../../../context/snackbar'
import { validateUrl } from '../../../utils/validateUrl'
import type { MediaItem } from '../../../pages/types' import type { MediaItem } from '../../../pages/types'
import { createDropzone } from '@solid-primitives/upload'
import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useSnackbar } from '../../../context/snackbar'
import { composeMediaItems } from '../../../utils/composeMediaItems' import { composeMediaItems } from '../../../utils/composeMediaItems'
import { validateUrl } from '../../../utils/validateUrl'
import { VideoPlayer } from '../../_shared/VideoPlayer' import { VideoPlayer } from '../../_shared/VideoPlayer'
import styles from './VideoUploader.module.scss'
type Props = { type Props = {
video: MediaItem[] video: MediaItem[]
onVideoAdd: (value: MediaItem[]) => void onVideoAdd: (value: MediaItem[]) => void
@ -22,13 +25,13 @@ export const VideoUploader = (props: Props) => {
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false) const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const urlInput: { const urlInput: {
current: HTMLInputElement current: HTMLInputElement
} = { } = {
current: null current: null,
} }
const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({ const { setRef: dropzoneRef, files: droppedFiles } = createDropzone({
@ -39,13 +42,13 @@ export const VideoUploader = (props: Props) => {
} else if (droppedFiles()[0].file.type.startsWith('video/')) { } else if (droppedFiles()[0].file.type.startsWith('video/')) {
await showSnackbar({ await showSnackbar({
body: t( body: t(
'This functionality is currently not available, we would like to work on this issue. Use the download link.' 'This functionality is currently not available, we would like to work on this issue. Use the download link.',
) ),
}) })
} else { } else {
setError(t('Video format not supported')) setError(t('Video format not supported'))
} }
} },
}) })
const handleDrag = (event) => { const handleDrag = (event) => {
if (event.type === 'dragenter' || event.type === 'dragover') { if (event.type === 'dragenter' || event.type === 'dragover') {
@ -84,8 +87,8 @@ export const VideoUploader = (props: Props) => {
onClick={() => onClick={() =>
showSnackbar({ showSnackbar({
body: t( body: t(
'This functionality is currently not available, we would like to work on this issue. Use the download link.' 'This functionality is currently not available, we would like to work on this issue. Use the download link.',
) ),
}) })
} }
ref={dropzoneRef} ref={dropzoneRef}

View File

@ -14,8 +14,8 @@ export default Node.create({
name: 'article', name: 'article',
defaultOptions: { defaultOptions: {
HTMLAttributes: { HTMLAttributes: {
'data-type': 'incut' 'data-type': 'incut',
} },
}, },
group: 'block', group: 'block',
content: 'block+', content: 'block+',
@ -23,8 +23,8 @@ export default Node.create({
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'article' tag: 'article',
} },
] ]
}, },
@ -35,11 +35,11 @@ export default Node.create({
addAttributes() { addAttributes() {
return { return {
'data-float': { 'data-float': {
default: null default: null,
}, },
'data-bg': { 'data-bg': {
default: null default: null,
} },
} }
}, },
@ -60,7 +60,7 @@ export default Node.create({
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.updateAttributes(this.name, { 'data-bg': value }) return commands.updateAttributes(this.name, { 'data-bg': value })
},
} }
} },
}
}) })

View File

@ -14,18 +14,18 @@ declare module '@tiptap/core' {
export const CustomBlockquote = Blockquote.extend({ export const CustomBlockquote = Blockquote.extend({
name: 'blockquote', name: 'blockquote',
defaultOptions: { defaultOptions: {
HTMLAttributes: {} HTMLAttributes: {},
}, },
group: 'block', group: 'block',
content: 'block+', content: 'block+',
addAttributes() { addAttributes() {
return { return {
'data-float': { 'data-float': {
default: null default: null,
}, },
'data-type': { 'data-type': {
default: null default: null,
} },
} }
}, },
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
@ -41,7 +41,7 @@ export const CustomBlockquote = Blockquote.extend({
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value }) return commands.updateAttributes(this.name, { 'data-float': value })
},
} }
} },
}
}) })

View File

@ -16,20 +16,20 @@ export const CustomImage = Image.extend({
addAttributes() { addAttributes() {
return { return {
src: { src: {
default: null default: null,
}, },
alt: { alt: {
default: null default: null,
}, },
width: { width: {
default: null default: null,
}, },
height: { height: {
default: null default: null,
}, },
'data-float': { 'data-float': {
default: null default: null,
} },
} }
}, },
addCommands() { addCommands() {
@ -39,14 +39,14 @@ export const CustomImage = Image.extend({
({ commands }) => { ({ commands }) => {
return commands.insertContent({ return commands.insertContent({
type: this.name, type: this.name,
attrs: options attrs: options,
}) })
}, },
setImageFloat: setImageFloat:
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value }) return commands.updateAttributes(this.name, { 'data-float': value })
},
} }
} },
}
}) })

View File

@ -25,14 +25,14 @@ export const Embed = Node.create<IframeOptions>({
return { return {
src: { default: null }, src: { default: null },
width: { default: null }, width: { default: null },
height: { default: null } height: { default: null },
} }
}, },
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'iframe' tag: 'iframe',
} },
] ]
}, },
renderHTML({ HTMLAttributes }) { renderHTML({ HTMLAttributes }) {
@ -49,7 +49,7 @@ export const Embed = Node.create<IframeOptions>({
iframe.src = node.attrs.src iframe.src = node.attrs.src
div.append(iframe) div.append(iframe)
return { return {
dom: div dom: div,
} }
} }
}, },
@ -64,7 +64,7 @@ export const Embed = Node.create<IframeOptions>({
tr.replaceRangeWith(selection.from, selection.to, node) tr.replaceRangeWith(selection.from, selection.to, node)
} }
return true return true
},
} }
} },
}
}) })

View File

@ -12,7 +12,7 @@ export const Figcaption = Node.create({
addOptions() { addOptions() {
return { return {
HTMLAttributes: {} HTMLAttributes: {},
} }
}, },
@ -25,8 +25,8 @@ export const Figcaption = Node.create({
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'figcaption' tag: 'figcaption',
} },
] ]
}, },
@ -39,7 +39,7 @@ export const Figcaption = Node.create({
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.focus(value) return commands.focus(value)
},
} }
} },
}
}) })

View File

@ -12,7 +12,7 @@ export const Figure = Node.create({
name: 'figure', name: 'figure',
addOptions() { addOptions() {
return { return {
HTMLAttributes: {} HTMLAttributes: {},
} }
}, },
group: 'block', group: 'block',
@ -22,15 +22,15 @@ export const Figure = Node.create({
addAttributes() { addAttributes() {
return { return {
'data-float': null 'data-float': null,
} }
}, },
parseHTML() { parseHTML() {
return [ return [
{ {
tag: `figure[data-type="${this.name}"]` tag: `figure[data-type="${this.name}"]`,
} },
] ]
}, },
@ -54,10 +54,10 @@ export const Figure = Node.create({
event.preventDefault() event.preventDefault()
} }
return false return false
} },
} },
} },
}) }),
] ]
}, },
@ -67,7 +67,7 @@ export const Figure = Node.create({
(value) => (value) =>
({ commands }) => { ({ commands }) => {
return commands.updateAttributes(this.name, { 'data-float': value }) return commands.updateAttributes(this.name, { 'data-float': value })
},
} }
} },
}
}) })

View File

@ -14,7 +14,7 @@ export const Footnote = Node.create({
name: 'footnote', name: 'footnote',
addOptions() { addOptions() {
return { return {
HTMLAttributes: {} HTMLAttributes: {},
} }
}, },
group: 'inline', group: 'inline',
@ -29,18 +29,18 @@ export const Footnote = Node.create({
parseHTML: (element) => element.dataset.value || null, parseHTML: (element) => element.dataset.value || null,
renderHTML: (attributes) => { renderHTML: (attributes) => {
return { return {
'data-value': attributes.value 'data-value': attributes.value,
}
}
} }
},
},
} }
}, },
parseHTML() { parseHTML() {
return [ return [
{ {
tag: 'footnote' tag: 'footnote',
} },
] ]
}, },
@ -92,7 +92,7 @@ export const Footnote = Node.create({
} }
return false return false
},
} }
} },
}
}) })

View File

@ -22,7 +22,7 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
addOptions() { addOptions() {
return { return {
node: 'paragraph', node: 'paragraph',
notAfter: ['paragraph'] notAfter: ['paragraph'],
} }
}, },
@ -61,9 +61,9 @@ export const TrailingNode = Extension.create<TrailingNodeOptions>({
const lastNode = tr.doc.lastChild const lastNode = tr.doc.lastChild
return !nodeEqualsType({ node: lastNode, types: disabledNodes }) return !nodeEqualsType({ node: lastNode, types: disabledNodes })
} },
} },
}) }),
] ]
} },
}) })

View File

@ -1,22 +1,25 @@
import { createMemo, createSignal, For, Show } from 'solid-js'
import type { Shout } from '../../../graphql/types.gen' import type { Shout } from '../../../graphql/types.gen'
import { capitalize } from '../../../utils/capitalize'
import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx'
import { CardTopic } from '../CardTopic'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { getDescription } from '../../../utils/meta'
import { FeedArticlePopup } from '../FeedArticlePopup'
import { useLocalize } from '../../../context/localize'
import { getPagePath, openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router' import { clsx } from 'clsx'
import { Popover } from '../../_shared/Popover' import { createMemo, createSignal, For, Show } from 'solid-js'
import { Image } from '../../_shared/Image'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router'
import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image'
import { Popover } from '../../_shared/Popover'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AhtorLink' import { AuthorLink } from '../../Author/AhtorLink'
import stylesHeader from '../../Nav/Header/Header.module.scss' import { CardTopic } from '../CardTopic'
import { FeedArticlePopup } from '../FeedArticlePopup'
import styles from './ArticleCard.module.scss' import styles from './ArticleCard.module.scss'
import stylesHeader from '../../Nav/Header/Header.module.scss'
interface ArticleCardProps { interface ArticleCardProps {
settings?: { settings?: {
@ -45,7 +48,7 @@ interface ArticleCardProps {
} }
const getTitleAndSubtitle = ( const getTitleAndSubtitle = (
article: Shout article: Shout,
): { ): {
title: string title: string
subtitle: string subtitle: string
@ -90,7 +93,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
event.preventDefault() event.preventDefault()
openPage(router, 'article', { slug: props.article.slug }) openPage(router, 'article', { slug: props.article.slug })
changeSearchParam({ changeSearchParam({
scrollTo: 'comments' scrollTo: 'comments',
}) })
} }
@ -111,7 +114,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[styles.shoutCardCompact]: props.settings?.isCompact, [styles.shoutCardCompact]: props.settings?.isCompact,
[styles.shoutCardSingle]: props.settings?.isSingle, [styles.shoutCardSingle]: props.settings?.isSingle,
[styles.shoutCardBeside]: props.settings?.isBeside, [styles.shoutCardBeside]: props.settings?.isBeside,
[styles.shoutCardNoImage]: !props.article.cover [styles.shoutCardNoImage]: !props.article.cover,
}} }}
> >
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}> <Show when={!props.settings?.noimage && !props.settings?.isFeedMode}>
@ -155,7 +158,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div <div
class={clsx(styles.shoutCardTitlesContainer, { class={clsx(styles.shoutCardTitlesContainer, {
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode [styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode,
})} })}
> >
<a href={getPagePath(router, 'article', { slug: props.article.slug })}> <a href={getPagePath(router, 'article', { slug: props.article.slug })}>

View File

@ -1,14 +1,18 @@
// TODO: additional entities list column + article // TODO: additional entities list column + article
import { For, Show } from 'solid-js'
import { ArticleCard } from './ArticleCard'
import { TopicCard } from '../Topic/Card'
import styles from './Beside.module.scss'
import type { Author, Shout, Topic, User } from '../../graphql/types.gen' import type { Author, Shout, Topic, User } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { For, Show } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { TopicCard } from '../Topic/Card'
import { ArticleCard } from './ArticleCard'
import styles from './Beside.module.scss'
type Props = { type Props = {
title?: string title?: string
@ -36,7 +40,7 @@ export const Beside = (props: Props) => {
'col-lg-8', 'col-lg-8',
styles[ styles[
`besideRatingColumn${props.wrapper.charAt(0).toUpperCase() + props.wrapper.slice(1)}` `besideRatingColumn${props.wrapper.charAt(0).toUpperCase() + props.wrapper.slice(1)}`
] ],
)} )}
> >
<Show when={!!props.title}> <Show when={!!props.title}>

View File

@ -1,6 +1,8 @@
import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { router } from '../../stores/router' import { router } from '../../stores/router'
import styles from './CardTopic.module.scss' import styles from './CardTopic.module.scss'
type CardTopicProps = { type CardTopicProps = {
@ -16,7 +18,7 @@ export const CardTopic = (props: CardTopicProps) => {
<div <div
class={clsx(styles.shoutTopic, props.class, { class={clsx(styles.shoutTopic, props.class, {
[styles.shoutTopicFloorImportant]: props.isFloorImportant, [styles.shoutTopicFloorImportant]: props.isFloorImportant,
[styles.shoutTopicFeedMode]: props.isFeedMode [styles.shoutTopicFeedMode]: props.isFeedMode,
})} })}
> >
<a href={getPagePath(router, 'topic', { slug: props.slug })}>{props.title}</a> <a href={getPagePath(router, 'topic', { slug: props.slug })}>{props.title}</a>

View File

@ -1,9 +1,12 @@
import styles from './FeedArticlePopup.module.scss'
import type { PopupProps } from '../_shared/Popup' import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import { useLocalize } from '../../context/localize'
import { createEffect, createSignal, Show } from 'solid-js' import { createEffect, createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Popup } from '../_shared/Popup'
import styles from './FeedArticlePopup.module.scss'
type FeedArticlePopupProps = { type FeedArticlePopupProps = {
title: string title: string
shareUrl?: string shareUrl?: string

View File

@ -1,6 +1,8 @@
import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
import './Group.scss' import './Group.scss'
@ -26,7 +28,7 @@ export default (props: GroupProps) => {
noicon: true, noicon: true,
isFloorImportant: true, isFloorImportant: true,
isBigTitle: true, isBigTitle: true,
nodate: true nodate: true,
}} }}
/> />
</div> </div>
@ -59,7 +61,7 @@ export default (props: GroupProps) => {
isBigTitle: true, isBigTitle: true,
isCompact: true, isCompact: true,
isFloorImportant: true, isFloorImportant: true,
nodate: true nodate: true,
}} }}
/> />
)} )}
@ -76,7 +78,7 @@ export default (props: GroupProps) => {
isBigTitle: true, isBigTitle: true,
isCompact: true, isCompact: true,
isFloorImportant: true, isFloorImportant: true,
nodate: true nodate: true,
}} }}
/> />
)} )}

View File

@ -1,5 +1,7 @@
import { Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { Show } from 'solid-js'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
export const Row1 = (props: { export const Row1 = (props: {
@ -19,7 +21,7 @@ export const Row1 = (props: {
isSingle: true, isSingle: true,
nodate: props.nodate, nodate: props.nodate,
noAuthorLink: props.noAuthorLink, noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor noauthor: props.noauthor,
}} }}
/> />
</div> </div>

View File

@ -1,11 +1,13 @@
import { createComputed, createSignal, Show, For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { createComputed, createSignal, Show, For } from 'solid-js'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
const x = [ const x = [
['12', '12'], ['12', '12'],
['8', '16'], ['8', '16'],
['16', '8'] ['16', '8'],
] ]
export const Row2 = (props: { export const Row2 = (props: {
@ -35,7 +37,7 @@ export const Row2 = (props: {
isWithCover: props.isEqual || x[y()][i()] === '16', isWithCover: props.isEqual || x[y()][i()] === '16',
nodate: props.isEqual || props.nodate, nodate: props.isEqual || props.nodate,
noAuthorLink: props.noAuthorLink, noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor noauthor: props.noauthor,
}} }}
/> />
</div> </div>

View File

@ -1,6 +1,8 @@
import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import type { JSX } from 'solid-js/jsx-runtime'
import { For, Show } from 'solid-js'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
export const Row3 = (props: { export const Row3 = (props: {
@ -24,7 +26,7 @@ export const Row3 = (props: {
settings={{ settings={{
nodate: props.nodate, nodate: props.nodate,
noAuthorLink: props.noAuthorLink, noAuthorLink: props.noAuthorLink,
noauthor: props.noauthor noauthor: props.noauthor,
}} }}
/> />
</div> </div>

View File

@ -1,4 +1,5 @@
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
export const Row5 = (props: { articles: Shout[]; nodate?: boolean }) => { export const Row5 = (props: { articles: Shout[]; nodate?: boolean }) => {

View File

@ -1,5 +1,7 @@
import { For } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { For } from 'solid-js'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'
export default (props: { articles: Shout[] }) => ( export default (props: { articles: Shout[] }) => (
@ -16,7 +18,7 @@ export default (props: { articles: Shout[] }) => (
isWithCover: true, isWithCover: true,
isBigTitle: true, isBigTitle: true,
isVertical: true, isVertical: true,
nodate: true nodate: true,
}} }}
/> />
</div> </div>

View File

@ -1,14 +1,16 @@
import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js' import { createSignal, For, Show } from 'solid-js'
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router'
import { useArticlesStore } from '../../../stores/zine/articles' import { useArticlesStore } from '../../../stores/zine/articles'
import { useSeenStore } from '../../../stores/zine/seen' import { useSeenStore } from '../../../stores/zine/seen'
import { useSession } from '../../../context/session' import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import styles from './Sidebar.module.scss'
import { clsx } from 'clsx'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import { getPagePath } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router' import styles from './Sidebar.module.scss'
export const Sidebar = () => { export const Sidebar = () => {
const { t } = useLocalize() const { t } = useLocalize()
@ -33,7 +35,7 @@ export const Sidebar = () => {
<a <a
href={getPagePath(router, 'feed')} href={getPagePath(router, 'feed')}
class={clsx({ class={clsx({
[styles.selected]: page().route === 'feed' [styles.selected]: page().route === 'feed',
})} })}
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
@ -46,7 +48,7 @@ export const Sidebar = () => {
<a <a
href={getPagePath(router, 'feedMy')} href={getPagePath(router, 'feedMy')}
class={clsx({ class={clsx({
[styles.selected]: page().route === 'feedMy' [styles.selected]: page().route === 'feedMy',
})} })}
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
@ -59,7 +61,7 @@ export const Sidebar = () => {
<a <a
href={getPagePath(router, 'feedCollaborations')} href={getPagePath(router, 'feedCollaborations')}
class={clsx({ class={clsx({
[styles.selected]: page().route === 'feedCollaborations' [styles.selected]: page().route === 'feedCollaborations',
})} })}
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
@ -72,7 +74,7 @@ export const Sidebar = () => {
<a <a
href={getPagePath(router, 'feedDiscussions')} href={getPagePath(router, 'feedDiscussions')}
class={clsx({ class={clsx({
[styles.selected]: page().route === 'feedDiscussions' [styles.selected]: page().route === 'feedDiscussions',
})} })}
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
@ -85,7 +87,7 @@ export const Sidebar = () => {
<a <a
href={getPagePath(router, 'feedBookmarks')} href={getPagePath(router, 'feedBookmarks')}
class={clsx({ class={clsx({
[styles.selected]: page().route === 'feedBookmarks' [styles.selected]: page().route === 'feedBookmarks',
})} })}
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
@ -98,7 +100,7 @@ export const Sidebar = () => {
<a <a
href={getPagePath(router, 'feedNotifications')} href={getPagePath(router, 'feedNotifications')}
class={clsx({ class={clsx({
[styles.selected]: page().route === 'feedNotifications' [styles.selected]: page().route === 'feedNotifications',
})} })}
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>

View File

@ -1,11 +1,14 @@
import { createSignal, For, createEffect } from 'solid-js'
import styles from './CreateModalContent.module.scss'
import InviteUser from './InviteUser'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { hideModal } from '../../stores/ui'
import { createSignal, For, createEffect } from 'solid-js'
import { useInbox } from '../../context/inbox' import { useInbox } from '../../context/inbox'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { hideModal } from '../../stores/ui'
import InviteUser from './InviteUser'
import styles from './CreateModalContent.module.scss'
type inviteUser = Author & { selected: boolean } type inviteUser = Author & { selected: boolean }
type Props = { type Props = {
@ -46,7 +49,7 @@ const CreateModalContent = (props: Props) => {
const handleClick = (user) => { const handleClick = (user) => {
setCollectionToInvite((userCollection) => { setCollectionToInvite((userCollection) => {
return userCollection.map((clickedUser) => return userCollection.map((clickedUser) =>
user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser user.id === clickedUser.id ? { ...clickedUser, selected: !clickedUser.selected } : clickedUser,
) )
}) })
} }

View File

@ -1,8 +1,10 @@
import { Show, createMemo } from 'solid-js'
import './DialogCard.module.scss'
import styles from './DialogAvatar.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo } from 'solid-js'
import { getImageUrl } from '../../utils/getImageUrl' import { getImageUrl } from '../../utils/getImageUrl'
import './DialogCard.module.scss'
import styles from './DialogAvatar.module.scss'
type Props = { type Props = {
name: string name: string
@ -25,7 +27,7 @@ const colors = [
'#668cff', '#668cff',
'#c34cfe', '#c34cfe',
'#e699ff', '#e699ff',
'#6633ff' '#6633ff',
] ]
const getById = (letter: string) => const getById = (letter: string) =>
@ -42,7 +44,7 @@ const DialogAvatar = (props: Props) => {
class={clsx(styles.DialogAvatar, props.class, { class={clsx(styles.DialogAvatar, props.class, {
[styles.online]: props.online, [styles.online]: props.online,
[styles.bordered]: props.bordered, [styles.bordered]: props.bordered,
[styles.small]: props.size === 'small' [styles.small]: props.size === 'small',
})} })}
style={{ 'background-color': `${randomBg()}` }} style={{ 'background-color': `${randomBg()}` }}
> >

View File

@ -1,12 +1,16 @@
import { Show, Switch, Match, createMemo } from 'solid-js'
import DialogAvatar from './DialogAvatar'
import type { ChatMember } from '../../graphql/types.gen' import type { ChatMember } from '../../graphql/types.gen'
import GroupDialogAvatar from './GroupDialogAvatar'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './DialogCard.module.scss' import { Show, Switch, Match, createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import DialogAvatar from './DialogAvatar'
import GroupDialogAvatar from './GroupDialogAvatar'
import styles from './DialogCard.module.scss'
type DialogProps = { type DialogProps = {
online?: boolean online?: boolean
message?: string message?: string
@ -22,14 +26,14 @@ type DialogProps = {
const DialogCard = (props: DialogProps) => { const DialogCard = (props: DialogProps) => {
const { t, formatTime } = useLocalize() const { t, formatTime } = useLocalize()
const companions = createMemo( const companions = createMemo(
() => props.members && props.members.filter((member) => member.id !== props.ownId) () => props.members && props.members.filter((member) => member.id !== props.ownId),
) )
const names = createMemo( const names = createMemo(
() => () =>
companions() companions()
?.map((companion) => companion.name) ?.map((companion) => companion.name)
.join(', ') .join(', '),
) )
return ( return (
@ -37,7 +41,7 @@ const DialogCard = (props: DialogProps) => {
<div <div
class={clsx(styles.DialogCard, { class={clsx(styles.DialogCard, {
[styles.opened]: props.isOpened, [styles.opened]: props.isOpened,
[styles.hovered]: !props.isChatHeader [styles.hovered]: !props.isChatHeader,
})} })}
onClick={props.onClick} onClick={props.onClick}
> >

View File

@ -1,7 +1,9 @@
import type { Chat } from '../../graphql/types.gen' import type { Chat } from '../../graphql/types.gen'
import styles from './DialogHeader.module.scss'
import DialogCard from './DialogCard' import DialogCard from './DialogCard'
import styles from './DialogHeader.module.scss'
type DialogHeader = { type DialogHeader = {
chat: Chat chat: Chat
ownId: number ownId: number

View File

@ -1,10 +1,14 @@
import { For } from 'solid-js'
import './DialogCard.module.scss'
import styles from './GroupDialogAvatar.module.scss'
import { clsx } from 'clsx'
import type { ChatMember } from '../../graphql/types.gen' import type { ChatMember } from '../../graphql/types.gen'
import { clsx } from 'clsx'
import { For } from 'solid-js'
import DialogAvatar from './DialogAvatar' import DialogAvatar from './DialogAvatar'
import './DialogCard.module.scss'
import styles from './GroupDialogAvatar.module.scss'
type Props = { type Props = {
users: ChatMember[] users: ChatMember[]
} }

View File

@ -1,8 +1,11 @@
import styles from './InviteUser.module.scss'
import DialogAvatar from './DialogAvatar'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import DialogAvatar from './DialogAvatar'
import styles from './InviteUser.module.scss'
type DialogProps = { type DialogProps = {
author: Author author: Author
selected: boolean selected: boolean

View File

@ -1,12 +1,16 @@
import { createSignal, Show } from 'solid-js'
import { clsx } from 'clsx'
import styles from './Message.module.scss'
import DialogAvatar from './DialogAvatar'
import type { Message as MessageType, ChatMember } from '../../graphql/types.gen' import type { Message as MessageType, ChatMember } from '../../graphql/types.gen'
import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import DialogAvatar from './DialogAvatar'
import { MessageActionsPopup } from './MessageActionsPopup' import { MessageActionsPopup } from './MessageActionsPopup'
import QuotedMessage from './QuotedMessage' import QuotedMessage from './QuotedMessage'
import { useLocalize } from '../../context/localize'
import styles from './Message.module.scss'
type Props = { type Props = {
content: MessageType content: MessageType

View File

@ -1,7 +1,9 @@
import { createEffect, createSignal, For } from 'solid-js'
import type { PopupProps } from '../_shared/Popup' import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import { createEffect, createSignal, For } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Popup } from '../_shared/Popup'
export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete' export type MessageActionType = 'reply' | 'copy' | 'pin' | 'forward' | 'select' | 'delete'
@ -18,7 +20,7 @@ export const MessageActionsPopup = (props: MessageActionsPopupProps) => {
{ name: t('Pin'), action: 'pin' }, { name: t('Pin'), action: 'pin' },
{ name: t('Forward'), action: 'forward' }, { name: t('Forward'), action: 'forward' },
{ name: t('Select'), action: 'select' }, { name: t('Select'), action: 'select' },
{ name: t('Delete'), action: 'delete' } { name: t('Delete'), action: 'delete' },
] ]
createEffect(() => { createEffect(() => {
if (props.actionSelect) props.actionSelect(selectedAction()) if (props.actionSelect) props.actionSelect(selectedAction())

View File

@ -1,4 +1,5 @@
import { Show } from 'solid-js' import { Show } from 'solid-js'
import styles from './MessagesFallback.module.scss' import styles from './MessagesFallback.module.scss'
type MessagesFallback = { type MessagesFallback = {

View File

@ -1,7 +1,9 @@
import { Show } from 'solid-js'
import styles from './QuotedMessage.module.scss'
import { Icon } from '../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show } from 'solid-js'
import { Icon } from '../_shared/Icon'
import styles from './QuotedMessage.module.scss'
type QuotedMessage = { type QuotedMessage = {
body: string body: string
@ -17,7 +19,7 @@ const QuotedMessage = (props: QuotedMessage) => {
class={clsx(styles.QuotedMessage, { class={clsx(styles.QuotedMessage, {
[styles.reply]: props.variant === 'reply', [styles.reply]: props.variant === 'reply',
[styles.inline]: props.variant === 'inline', [styles.inline]: props.variant === 'inline',
[styles.own]: props.isOwn [styles.own]: props.isOwn,
})} })}
> >
<Show when={props.variant === 'reply'}> <Show when={props.variant === 'reply'}>

View File

@ -1,7 +1,9 @@
import styles from './Search.module.scss'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './Search.module.scss'
type Props = { type Props = {
placeholder: string placeholder: string
onChange: (value: () => string) => void onChange: (value: () => string) => void

View File

@ -1,9 +1,11 @@
import styles from './AuthModalHeader.module.scss'
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { useLocalize } from '../../../../context/localize' import { useLocalize } from '../../../../context/localize'
import { useRouter } from '../../../../stores/router' import { useRouter } from '../../../../stores/router'
import { AuthModalSearchParams } from '../types' import { AuthModalSearchParams } from '../types'
import styles from './AuthModalHeader.module.scss'
type Props = { type Props = {
modalType: 'login' | 'register' modalType: 'login' | 'register'
} }
@ -14,7 +16,7 @@ export const AuthModalHeader = (props: Props) => {
const { source } = searchParams() const { source } = searchParams()
const generateModalTextsFromSource = ( const generateModalTextsFromSource = (
modalType: 'login' | 'register' modalType: 'login' | 'register',
): { title: string; description: string } => { ): { title: string; description: string } => {
const title = modalType === 'login' ? 'Welcome to Discours' : 'Create account' const title = modalType === 'login' ? 'Welcome to Discours' : 'Create account'
@ -22,53 +24,53 @@ export const AuthModalHeader = (props: Props) => {
case 'create': { case 'create': {
return { return {
title: t(`${title} to publish articles`), title: t(`${title} to publish articles`),
description: '' description: '',
} }
} }
case 'bookmark': { case 'bookmark': {
return { return {
title: t(`${title} to add to your bookmarks`), title: t(`${title} to add to your bookmarks`),
description: t( description: t(
'In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to' 'In&nbsp;bookmarks, you can save favorite discussions and&nbsp;materials that you want to return to',
) ),
} }
} }
case 'discussions': { case 'discussions': {
return { return {
title: t(`${title} to participate in discussions`), title: t(`${title} to participate in discussions`),
description: t( description: t(
"You&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;new responses" "You&nbsp;ll be able to participate in&nbsp;discussions, rate others' comments and&nbsp;learn about&nbsp;new responses",
) ),
} }
} }
case 'follow': { case 'follow': {
return { return {
title: t(`${title} to subscribe`), title: t(`${title} to subscribe`),
description: t( description: t(
'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed' 'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed',
) ),
} }
} }
case 'subscribe': { case 'subscribe': {
return { return {
title: t(`${title} to subscribe to new publications`), title: t(`${title} to subscribe to new publications`),
description: t( description: t(
'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed' 'This way you&nbsp;ll be able to subscribe to&nbsp;authors, interesting topics and&nbsp;customize your feed',
) ),
} }
} }
case 'vote': { case 'vote': {
return { return {
title: t(`${title} to vote`), title: t(`${title} to vote`),
description: t( description: t(
'This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted' 'This way we&nbsp;ll realize that you&nbsp;re a real person and&nbsp;ll take your vote into account. And&nbsp;you&nbsp;ll see how others voted',
) ),
} }
} }
default: { default: {
return { return {
title: t(title), title: t(title),
description: '' description: '',
} }
} }
} }

View File

@ -1,18 +1,21 @@
import styles from './AuthModal.module.scss'
import { clsx } from 'clsx'
import { hideModal } from '../../../stores/ui'
import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useRouter } from '../../../stores/router'
import type { ConfirmEmailSearchParams } from './types' import type { ConfirmEmailSearchParams } from './types'
import { ApiError } from '../../../utils/apiClient'
import { useSession } from '../../../context/session' import { clsx } from 'clsx'
import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import styles from './AuthModal.module.scss'
export const EmailConfirm = () => { export const EmailConfirm = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
session, session,
actions: { confirmEmail } actions: { confirmEmail },
} = useSession() } = useSession()
const [isTokenExpired, setIsTokenExpired] = createSignal(false) const [isTokenExpired, setIsTokenExpired] = createSignal(false)

View File

@ -1,14 +1,18 @@
import styles from './AuthModal.module.scss' import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, JSX, Show } from 'solid-js' import { createSignal, JSX, Show } from 'solid-js'
import { useRouter } from '../../../stores/router'
import { email, setEmail } from './sharedLogic'
import type { AuthModalSearchParams } from './types'
import { ApiError } from '../../../utils/apiClient'
import { signSendLink } from '../../../stores/auth'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { signSendLink } from '../../../stores/auth'
import { useRouter } from '../../../stores/router'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail' import { validateEmail } from '../../../utils/validateEmail'
import { email, setEmail } from './sharedLogic'
import styles from './AuthModal.module.scss'
type FormFields = { type FormFields = {
email: string email: string
} }
@ -83,7 +87,7 @@ export const ForgotPasswordForm = () => {
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email 'pretty-form__item--error': validationErrors().email,
})} })}
> >
<input <input
@ -116,7 +120,7 @@ export const ForgotPasswordForm = () => {
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
changeSearchParam({ changeSearchParam({
mode: 'register' mode: 'register',
}) })
}} }}
> >
@ -138,7 +142,7 @@ export const ForgotPasswordForm = () => {
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParam({
mode: 'login' mode: 'login',
}) })
} }
> >

View File

@ -1,19 +1,23 @@
import styles from './AuthModal.module.scss'
import { clsx } from 'clsx'
import { SocialProviders } from './SocialProviders'
import { ApiError } from '../../../utils/apiClient'
import { createSignal, Show } from 'solid-js'
import { email, setEmail } from './sharedLogic'
import { useRouter } from '../../../stores/router'
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { hideModal } from '../../../stores/ui'
import { useSession } from '../../../context/session' import { clsx } from 'clsx'
import { signSendLink } from '../../../stores/auth' import { createSignal, Show } from 'solid-js'
import { validateEmail } from '../../../utils/validateEmail'
import { useSnackbar } from '../../../context/snackbar'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar'
import { signSendLink } from '../../../stores/auth'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders'
import styles from './AuthModal.module.scss'
type FormFields = { type FormFields = {
email: string email: string
@ -36,11 +40,11 @@ export const LoginForm = () => {
const authFormRef: { current: HTMLFormElement } = { current: null } const authFormRef: { current: HTMLFormElement } = { current: null }
const { const {
actions: { showSnackbar } actions: { showSnackbar },
} = useSnackbar() } = useSnackbar()
const { const {
actions: { signIn } actions: { signIn },
} = useSession() } = useSession()
const { changeSearchParam } = useRouter<AuthModalSearchParams>() const { changeSearchParam } = useRouter<AuthModalSearchParams>()
@ -144,7 +148,7 @@ export const LoginForm = () => {
</Show> </Show>
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email 'pretty-form__item--error': validationErrors().email,
})} })}
> >
<input <input
@ -164,7 +168,7 @@ export const LoginForm = () => {
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().password 'pretty-form__item--error': validationErrors().password,
})} })}
> >
<input <input
@ -198,7 +202,7 @@ export const LoginForm = () => {
class="link" class="link"
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParam({
mode: 'forgot-password' mode: 'forgot-password',
}) })
} }
> >
@ -215,7 +219,7 @@ export const LoginForm = () => {
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParam({
mode: 'register' mode: 'register',
}) })
} }
> >

View File

@ -1,20 +1,24 @@
import { Show, createSignal } from 'solid-js'
import type { JSX } from 'solid-js'
import styles from './AuthModal.module.scss'
import { clsx } from 'clsx'
import { SocialProviders } from './SocialProviders'
import { ApiError } from '../../../utils/apiClient'
import { email, setEmail } from './sharedLogic'
import { useRouter } from '../../../stores/router'
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { hideModal } from '../../../stores/ui' import type { JSX } from 'solid-js'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { register } from '../../../stores/auth' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { register } from '../../../stores/auth'
import { checkEmail, useEmailChecks } from '../../../stores/emailChecks'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { ApiError } from '../../../utils/apiClient'
import { validateEmail } from '../../../utils/validateEmail' import { validateEmail } from '../../../utils/validateEmail'
import { AuthModalHeader } from './AuthModalHeader'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { AuthModalHeader } from './AuthModalHeader'
import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders'
import styles from './AuthModal.module.scss'
type FormFields = { type FormFields = {
fullName: string fullName: string
email: string email: string
@ -126,7 +130,7 @@ export const RegisterForm = () => {
await register({ await register({
name: cleanName, name: cleanName,
email: cleanEmail, email: cleanEmail,
password: password() password: password(),
}) })
setIsSuccess(true) setIsSuccess(true)
@ -156,7 +160,7 @@ export const RegisterForm = () => {
</Show> </Show>
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().fullName 'pretty-form__item--error': validationErrors().fullName,
})} })}
> >
<input <input
@ -174,7 +178,7 @@ export const RegisterForm = () => {
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().email 'pretty-form__item--error': validationErrors().email,
})} })}
> >
<input <input
@ -199,7 +203,7 @@ export const RegisterForm = () => {
onClick={(event) => { onClick={(event) => {
event.preventDefault() event.preventDefault()
changeSearchParam({ changeSearchParam({
mode: 'login' mode: 'login',
}) })
}} }}
> >
@ -211,7 +215,7 @@ export const RegisterForm = () => {
<div <div
class={clsx('pretty-form__item', { class={clsx('pretty-form__item', {
'pretty-form__item--error': validationErrors().password 'pretty-form__item--error': validationErrors().password,
})} })}
> >
<input <input
@ -252,7 +256,7 @@ export const RegisterForm = () => {
class={styles.authLink} class={styles.authLink}
onClick={() => onClick={() =>
changeSearchParam({ changeSearchParam({
mode: 'login' mode: 'login',
}) })
} }
> >

View File

@ -1,9 +1,9 @@
import { Icon } from '../../_shared/Icon' import { useLocalize } from '../../../context/localize'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { apiBaseUrl } from '../../../utils/config'
import { Icon } from '../../_shared/Icon'
import styles from './SocialProviders.module.scss' import styles from './SocialProviders.module.scss'
import { apiBaseUrl } from '../../../utils/config'
import { useLocalize } from '../../../context/localize'
type Provider = 'facebook' | 'google' | 'vk' | 'github' type Provider = 'facebook' | 'google' | 'vk' | 'github'

View File

@ -1,22 +1,26 @@
import { Dynamic } from 'solid-js/web'
import { Show, Component, createEffect, createMemo } from 'solid-js'
import { hideModal } from '../../../stores/ui'
import { useRouter } from '../../../stores/router'
import { clsx } from 'clsx'
import styles from './AuthModal.module.scss'
import { LoginForm } from './LoginForm'
import { isMobile } from '../../../utils/media-query'
import { RegisterForm } from './RegisterForm'
import { ForgotPasswordForm } from './ForgotPasswordForm'
import { EmailConfirm } from './EmailConfirm'
import type { AuthModalMode, AuthModalSearchParams } from './types' import type { AuthModalMode, AuthModalSearchParams } from './types'
import { clsx } from 'clsx'
import { Show, Component, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui'
import { isMobile } from '../../../utils/media-query'
import { EmailConfirm } from './EmailConfirm'
import { ForgotPasswordForm } from './ForgotPasswordForm'
import { LoginForm } from './LoginForm'
import { RegisterForm } from './RegisterForm'
import styles from './AuthModal.module.scss'
const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = { const AUTH_MODAL_MODES: Record<AuthModalMode, Component> = {
login: LoginForm, login: LoginForm,
register: RegisterForm, register: RegisterForm,
'forgot-password': ForgotPasswordForm, 'forgot-password': ForgotPasswordForm,
'confirm-email': EmailConfirm 'confirm-email': EmailConfirm,
} }
export const AuthModal = () => { export const AuthModal = () => {
@ -40,7 +44,7 @@ export const AuthModal = () => {
<div <div
ref={rootRef} ref={rootRef}
class={clsx(styles.view, { class={clsx(styles.view, {
row: !source row: !source,
})} })}
classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }} classList={{ [styles.signUp]: mode() === 'register' || mode() === 'confirm-email' }}
> >
@ -54,7 +58,7 @@ export const AuthModal = () => {
<h4>{t(`Join the global community of authors!`)}</h4> <h4>{t(`Join the global community of authors!`)}</h4>
<p class={styles.authBenefits}> <p class={styles.authBenefits}>
{t( {t(
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine' 'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine',
)} )}
.&nbsp; .&nbsp;
{t('New stories every day and even more!')} {t('New stories every day and even more!')}
@ -77,7 +81,7 @@ export const AuthModal = () => {
</Show> </Show>
<div <div
class={clsx(styles.auth, { class={clsx(styles.auth, {
'col-md-12': !source 'col-md-12': !source,
})} })}
> >
<Dynamic component={AUTH_MODAL_MODES[mode()]} /> <Dynamic component={AUTH_MODAL_MODES[mode()]} />

View File

@ -1,6 +1,7 @@
import { useConfirm } from '../../../context/confirm' import { useConfirm } from '../../../context/confirm'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import styles from './ConfirmModal.module.scss' import styles from './ConfirmModal.module.scss'
export const ConfirmModal = () => { export const ConfirmModal = () => {
@ -8,7 +9,7 @@ export const ConfirmModal = () => {
const { const {
confirmMessage, confirmMessage,
actions: { resolveConfirm } actions: { resolveConfirm },
} = useConfirm() } = useConfirm()
return ( return (

View File

@ -1,5 +1,6 @@
import './Confirmed.scss' import './Confirmed.scss'
import { onMount } from 'solid-js' import { onMount } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
export const Confirmed = (props: { token?: string }) => { export const Confirmed = (props: { token?: string }) => {

View File

@ -1,29 +1,28 @@
import { Show, createSignal, createEffect, onMount, onCleanup, For } from 'solid-js'
import { getPagePath, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { Modal } from '../Modal'
import { AuthModal } from '../AuthModal'
import { HeaderAuth } from '../HeaderAuth'
import { ConfirmModal } from '../ConfirmModal'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { Snackbar } from '../Snackbar'
import { Icon } from '../../_shared/Icon'
import type { Topic } from '../../../graphql/types.gen' import type { Topic } from '../../../graphql/types.gen'
import { useModalStore } from '../../../stores/ui' import { getPagePath, redirectPage } from '@nanostores/router'
import { router, ROUTES, useRouter } from '../../../stores/router' import { clsx } from 'clsx'
import { Show, createSignal, createEffect, onMount, onCleanup, For } from 'solid-js'
import { getDescription } from '../../../utils/meta'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { router, ROUTES, useRouter } from '../../../stores/router'
import { useModalStore } from '../../../stores/ui'
import { apiClient } from '../../../utils/apiClient'
import { getDescription } from '../../../utils/meta'
import { Icon } from '../../_shared/Icon'
import { Subscribe } from '../../_shared/Subscribe'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { RANDOM_TOPICS_COUNT } from '../../Views/Home'
import { AuthModal } from '../AuthModal'
import { ConfirmModal } from '../ConfirmModal'
import { HeaderAuth } from '../HeaderAuth'
import { Modal } from '../Modal'
import { Snackbar } from '../Snackbar'
import { Link } from './Link'
import styles from './Header.module.scss' import styles from './Header.module.scss'
import { apiClient } from '../../../utils/apiClient'
import { RANDOM_TOPICS_COUNT } from '../../Views/Home'
import { Link } from './Link'
import { Subscribe } from '../../_shared/Subscribe'
type Props = { type Props = {
title?: string title?: string
@ -47,7 +46,7 @@ export const Header = (props: Props) => {
const { modal } = useModalStore() const { modal } = useModalStore()
const { page } = useRouter() const { page } = useRouter()
const { const {
actions: { requireAuthentication } actions: { requireAuthentication },
} = useSession() } = useSession()
const { searchParams } = useRouter<HeaderSearchParams>() const { searchParams } = useRouter<HeaderSearchParams>()
@ -167,7 +166,7 @@ export const Header = (props: Props) => {
[styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(), [styles.headerScrolledTop]: !getIsScrollingBottom() && getIsScrolled(),
[styles.headerScrolledBottom]: [styles.headerScrolledBottom]:
(getIsScrollingBottom() && getIsScrolled() && !isProfilePopupVisible()) || isSharePopupVisible(), (getIsScrollingBottom() && getIsScrolled() && !isProfilePopupVisible()) || isSharePopupVisible(),
[styles.headerWithTitle]: Boolean(props.title) [styles.headerWithTitle]: Boolean(props.title),
}} }}
> >
<Modal <Modal
@ -318,7 +317,7 @@ export const Header = (props: Props) => {
<p <p
class={styles.mobileDescription} class={styles.mobileDescription}
innerHTML={t( innerHTML={t(
'Independant magazine with an open horizontal cooperation about culture, science and society' 'Independant magazine with an open horizontal cooperation about culture, science and society',
)} )}
/> />
<div class={styles.mobileCopyright}> <div class={styles.mobileCopyright}>

View File

@ -1,8 +1,10 @@
import styles from './Header.module.scss'
import { router, ROUTES, useRouter } from '../../../stores/router'
import { clsx } from 'clsx'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { router, ROUTES, useRouter } from '../../../stores/router'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import styles from './Header.module.scss'
type Props = { type Props = {
onMouseOver: (event?: MouseEvent, time?: number) => void onMouseOver: (event?: MouseEvent, time?: number) => void

View File

@ -1,19 +1,22 @@
import styles from './Header/Header.module.scss'
import { clsx } from 'clsx'
import { router, useRouter } from '../../stores/router'
import { Icon } from '../_shared/Icon'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { ProfilePopup } from './ProfilePopup'
import { Userpic } from '../Author/Userpic'
import { showModal } from '../../stores/ui'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { Button } from '../_shared/Button' import { clsx } from 'clsx'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
import { Popover } from '../_shared/Popover' import { useLocalize } from '../../context/localize'
import { useNotifications } from '../../context/notifications' import { useNotifications } from '../../context/notifications'
import { useSession } from '../../context/session'
import { router, useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui'
import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon'
import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Userpic } from '../Author/Userpic'
import { ProfilePopup } from './ProfilePopup'
import styles from './Header/Header.module.scss'
type Props = { type Props = {
setIsProfilePopupVisible: (value: boolean) => void setIsProfilePopupVisible: (value: boolean) => void
@ -32,12 +35,12 @@ export const HeaderAuth = (props: Props) => {
const { session, isSessionLoaded, isAuthenticated } = useSession() const { session, isSessionLoaded, isAuthenticated } = useSession()
const { const {
unreadNotificationsCount, unreadNotificationsCount,
actions: { showNotificationsPanel } actions: { showNotificationsPanel },
} = useNotifications() } = useNotifications()
const { const {
form, form,
actions: { toggleEditorPanel, saveShout, publishShout } actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext() } = useEditorContext()
const handleBellIconClick = (event: Event) => { const handleBellIconClick = (event: Event) => {
@ -158,7 +161,7 @@ export const HeaderAuth = (props: Props) => {
{renderIconedButton({ {renderIconedButton({
value: t('Save'), value: t('Save'),
icon: 'save', icon: 'save',
action: handleSaveButtonClick action: handleSaveButtonClick,
})} })}
</div> </div>
@ -166,7 +169,7 @@ export const HeaderAuth = (props: Props) => {
{renderIconedButton({ {renderIconedButton({
value: t('Publish'), value: t('Publish'),
icon: 'publish', icon: 'publish',
action: handlePublishButtonClick action: handlePublishButtonClick,
})} })}
</div> </div>

View File

@ -1,13 +1,15 @@
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Show } from 'solid-js'
import { router } from '../../../stores/router'
import { hideModal, useModalStore } from '../../../stores/ui' import { hideModal, useModalStore } from '../../../stores/ui'
import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '../../../utils/useEscKeyDownHandler'
import { Icon } from '../../_shared/Icon'
import styles from './Modal.module.scss' import styles from './Modal.module.scss'
import { redirectPage } from '@nanostores/router'
import { router } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
interface Props { interface Props {
name: string name: string
@ -50,7 +52,7 @@ export const Modal = (props: Props) => {
[styles.narrow]: props.variant === 'narrow', [styles.narrow]: props.variant === 'narrow',
['col-auto col-md-20 offset-md-2 col-lg-14 offset-lg-5']: props.variant === 'medium', ['col-auto col-md-20 offset-md-2 col-lg-14 offset-lg-5']: props.variant === 'medium',
[styles.noPadding]: props.noPadding, [styles.noPadding]: props.noPadding,
[styles.maxHeight]: props.maxHeight [styles.maxHeight]: props.maxHeight,
})} })}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >

View File

@ -1,5 +1,6 @@
import type { JSX } from 'solid-js/jsx-runtime'
import type { ModalType } from '../../../stores/ui' import type { ModalType } from '../../../stores/ui'
import type { JSX } from 'solid-js/jsx-runtime'
import { showModal } from '../../../stores/ui' import { showModal } from '../../../stores/ui'
export default (props: { name: ModalType; children: JSX.Element }) => { export default (props: { name: ModalType; children: JSX.Element }) => {

View File

@ -1,17 +1,20 @@
import { useSession } from '../../context/session'
import type { PopupProps } from '../_shared/Popup' import type { PopupProps } from '../_shared/Popup'
import { Popup } from '../_shared/Popup'
import styles from '../_shared/Popup/Popup.module.scss'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { router } from '../../stores/router'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useSession } from '../../context/session'
import { router } from '../../stores/router'
import { Popup } from '../_shared/Popup'
import styles from '../_shared/Popup/Popup.module.scss'
type ProfilePopupProps = Omit<PopupProps, 'children'> type ProfilePopupProps = Omit<PopupProps, 'children'>
export const ProfilePopup = (props: ProfilePopupProps) => { export const ProfilePopup = (props: ProfilePopupProps) => {
const { const {
user, user,
actions: { signOut } actions: { signOut },
} = useSession() } = useSession()
const { t } = useLocalize() const { t } = useLocalize()

View File

@ -1,8 +1,10 @@
import styles from './ProfileSettingsNavigation.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import styles from './ProfileSettingsNavigation.module.scss'
export const ProfileSettingsNavigation = () => { export const ProfileSettingsNavigation = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { page } = useRouter() const { page } = useRouter()

View File

@ -1,11 +1,13 @@
import { Show } from 'solid-js'
import { useSnackbar } from '../../context/snackbar'
import styles from './Snackbar.module.scss'
import { Transition } from 'solid-transition-group'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show } from 'solid-js'
import { Transition } from 'solid-transition-group'
import { useSnackbar } from '../../context/snackbar'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import styles from './Snackbar.module.scss'
export const Snackbar = () => { export const Snackbar = () => {
const { snackbarMessage } = useSnackbar() const { snackbarMessage } = useSnackbar()
@ -13,7 +15,7 @@ export const Snackbar = () => {
<div <div
class={clsx(styles.snackbar, { class={clsx(styles.snackbar, {
[styles.error]: snackbarMessage()?.type === 'error', [styles.error]: snackbarMessage()?.type === 'error',
[styles.success]: snackbarMessage()?.type === 'success' [styles.success]: snackbarMessage()?.type === 'success',
})} })}
> >
<ShowOnlyOnClient> <ShowOnlyOnClient>

View File

@ -1,9 +1,11 @@
import { Icon } from '../../_shared/Icon'
import { useLocalize } from '../../../context/localize'
import styles from './Topics.module.scss'
import { clsx } from 'clsx'
import { router, useRouter } from '../../../stores/router'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx'
import { useLocalize } from '../../../context/localize'
import { router, useRouter } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
import styles from './Topics.module.scss'
export const Topics = () => { export const Topics = () => {
const { t } = useLocalize() const { t } = useLocalize()

View File

@ -1,7 +1,9 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './EmptyMessage.module.scss'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import styles from './EmptyMessage.module.scss'
export const EmptyMessage = () => { export const EmptyMessage = () => {
const { t } = useLocalize() const { t } = useLocalize()

View File

@ -1,15 +1,18 @@
import { clsx } from 'clsx'
import type { Notification } from '../../../graphql/types.gen' import type { Notification } from '../../../graphql/types.gen'
import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { NotificationType } from '../../../graphql/types.gen'
import { getPagePath, openPage } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router'
import { useNotifications } from '../../../context/notifications'
import { useLocalize } from '../../../context/localize'
import type { ArticlePageSearchParams } from '../../Article/FullArticle' import type { ArticlePageSearchParams } from '../../Article/FullArticle'
import { TimeAgo } from '../../_shared/TimeAgo'
import styles from './NotificationView.module.scss' import { getPagePath, openPage } from '@nanostores/router'
import { clsx } from 'clsx'
import { createMemo, createSignal, onMount, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize'
import { useNotifications } from '../../../context/notifications'
import { NotificationType } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router'
import { GroupAvatar } from '../../_shared/GroupAvatar' import { GroupAvatar } from '../../_shared/GroupAvatar'
import { TimeAgo } from '../../_shared/TimeAgo'
import styles from './NotificationView.module.scss'
type Props = { type Props = {
notification: Notification notification: Notification
@ -36,7 +39,7 @@ type NotificationData = {
export const NotificationView = (props: Props) => { export const NotificationView = (props: Props) => {
const { const {
actions: { markNotificationAsRead, hideNotificationsPanel } actions: { markNotificationAsRead, hideNotificationsPanel },
} = useNotifications() } = useNotifications()
const { changeSearchParam } = useRouter<ArticlePageSearchParams>() const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
@ -89,7 +92,7 @@ export const NotificationView = (props: Props) => {
return ( return (
<> <>
{t('NotificationNewCommentText1', { {t('NotificationNewCommentText1', {
commentsCount: props.notification.occurrences commentsCount: props.notification.occurrences,
})}{' '} })}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}> <a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
{shoutTitle} {shoutTitle}
@ -99,7 +102,7 @@ export const NotificationView = (props: Props) => {
{lastUser().name} {lastUser().name}
</a>{' '} </a>{' '}
{t('NotificationNewCommentText3', { {t('NotificationNewCommentText3', {
restUsersCount: data().users.length - 1 restUsersCount: data().users.length - 1,
})} })}
</> </>
) )
@ -108,7 +111,7 @@ export const NotificationView = (props: Props) => {
return ( return (
<> <>
{t('NotificationNewReplyText1', { {t('NotificationNewReplyText1', {
commentsCount: props.notification.occurrences commentsCount: props.notification.occurrences,
})}{' '} })}{' '}
<a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}> <a href={getPagePath(router, 'article', { slug: data().shout.slug })} onClick={handleLinkClick}>
{shoutTitle} {shoutTitle}
@ -118,7 +121,7 @@ export const NotificationView = (props: Props) => {
{lastUser().name} {lastUser().name}
</a>{' '} </a>{' '}
{t('NotificationNewReplyText3', { {t('NotificationNewReplyText3', {
restUsersCount: data().users.length - 1 restUsersCount: data().users.length - 1,
})} })}
</> </>
) )
@ -158,7 +161,7 @@ export const NotificationView = (props: Props) => {
<Show when={data()}> <Show when={data()}>
<div <div
class={clsx(styles.NotificationView, props.class, { class={clsx(styles.NotificationView, props.class, {
[styles.seen]: props.notification.seen [styles.seen]: props.notification.seen,
})} })}
onClick={handleClick} onClick={handleClick}
> >

View File

@ -1,16 +1,19 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './NotificationsPanel.module.scss' import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
import { throttle } from 'throttle-debounce'
import { useLocalize } from '../../context/localize'
import { PAGE_SIZE, useNotifications } from '../../context/notifications'
import { useSession } from '../../context/session'
import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler' import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
import { PAGE_SIZE, useNotifications } from '../../context/notifications'
import { NotificationView } from './NotificationView'
import { EmptyMessage } from './EmptyMessage'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { throttle } from 'throttle-debounce' import { Icon } from '../_shared/Icon'
import { useSession } from '../../context/session'
import { EmptyMessage } from './EmptyMessage'
import { NotificationView } from './NotificationView'
import styles from './NotificationsPanel.module.scss'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
@ -51,20 +54,20 @@ export const NotificationsPanel = (props: Props) => {
unreadNotificationsCount, unreadNotificationsCount,
loadedNotificationsCount, loadedNotificationsCount,
totalNotificationsCount, totalNotificationsCount,
actions: { loadNotifications, markAllNotificationsAsRead } actions: { loadNotifications, markAllNotificationsAsRead },
} = useNotifications() } = useNotifications()
const handleHide = () => { const handleHide = () => {
props.onClose() props.onClose()
} }
const panelRef: { current: HTMLDivElement } = { const panelRef: { current: HTMLDivElement } = {
current: null current: null,
} }
useOutsideClickHandler({ useOutsideClickHandler({
containerRef: panelRef, containerRef: panelRef,
predicate: () => props.isOpen, predicate: () => props.isOpen,
handler: () => handleHide() handler: () => handleHide(),
}) })
let windowScrollTop = 0 let windowScrollTop = 0
@ -150,14 +153,14 @@ export const NotificationsPanel = (props: Props) => {
await loadNextPage() await loadNextPage()
setIsLoading(false) setIsLoading(false)
} }
} },
) ),
) )
return ( return (
<div <div
class={clsx(styles.container, { class={clsx(styles.container, {
[styles.isOpened]: props.isOpen [styles.isOpened]: props.isOpen,
})} })}
> >
<div ref={(el) => (panelRef.current = el)} class={styles.panel}> <div ref={(el) => (panelRef.current = el)} class={styles.panel}>

View File

@ -1,12 +1,14 @@
import { For, Show, createSignal, createEffect, on, onMount, onCleanup } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { DEFAULT_HEADER_OFFSET } from '../../stores/router' import { For, Show, createSignal, createEffect, on, onMount, onCleanup } from 'solid-js'
import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import styles from './TableOfContents.module.scss'
import { isDesktop } from '../../utils/media-query'
import { throttle, debounce } from 'throttle-debounce' import { throttle, debounce } from 'throttle-debounce'
import { useLocalize } from '../../context/localize'
import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
import { isDesktop } from '../../utils/media-query'
import { Icon } from '../_shared/Icon'
import styles from './TableOfContents.module.scss'
interface Props { interface Props {
variant: 'article' | 'editor' variant: 'article' | 'editor'
parentSelector: string parentSelector: string
@ -23,7 +25,7 @@ const scrollToHeader = (element) => {
top: top:
element.getBoundingClientRect().top - element.getBoundingClientRect().top -
document.body.getBoundingClientRect().top - document.body.getBoundingClientRect().top -
DEFAULT_HEADER_OFFSET DEFAULT_HEADER_OFFSET,
}) })
} }
@ -43,7 +45,7 @@ export const TableOfContents = (props: Props) => {
const updateHeadings = () => { const updateHeadings = () => {
setHeadings( setHeadings(
// eslint-disable-next-line unicorn/prefer-spread // eslint-disable-next-line unicorn/prefer-spread
Array.from(document.querySelector(props.parentSelector).querySelectorAll<HTMLElement>('h2, h3, h4')) Array.from(document.querySelector(props.parentSelector).querySelectorAll<HTMLElement>('h2, h3, h4')),
) )
setAreHeadingsLoaded(true) setAreHeadingsLoaded(true)
} }
@ -58,8 +60,8 @@ export const TableOfContents = (props: Props) => {
createEffect( createEffect(
on( on(
() => props.body, () => props.body,
() => debouncedUpdateHeadings() () => debouncedUpdateHeadings(),
) ),
) )
onMount(() => { onMount(() => {
@ -75,7 +77,7 @@ export const TableOfContents = (props: Props) => {
> >
<div <div
class={clsx(styles.TableOfContentsFixedWrapper, { class={clsx(styles.TableOfContentsFixedWrapper, {
[styles.TableOfContentsFixedWrapperLefted]: props.variant === 'editor' [styles.TableOfContentsFixedWrapperLefted]: props.variant === 'editor',
})} })}
> >
<div class={styles.TableOfContentsContainer}> <div class={styles.TableOfContentsContainer}>
@ -92,7 +94,7 @@ export const TableOfContents = (props: Props) => {
class={clsx(styles.TableOfContentsHeadingsItem, { class={clsx(styles.TableOfContentsHeadingsItem, {
[styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3', [styles.TableOfContentsHeadingsItemH3]: h.nodeName === 'H3',
[styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4', [styles.TableOfContentsHeadingsItemH4]: h.nodeName === 'H4',
[styles.active]: index() === activeHeaderIndex() [styles.active]: index() === activeHeaderIndex(),
})} })}
innerHTML={h.textContent} innerHTML={h.textContent}
onClick={(e) => { onClick={(e) => {
@ -111,9 +113,9 @@ export const TableOfContents = (props: Props) => {
class={clsx( class={clsx(
styles.TableOfContentsPrimaryButton, styles.TableOfContentsPrimaryButton,
{ {
[styles.TableOfContentsPrimaryButtonLefted]: props.variant === 'editor' && !isVisible() [styles.TableOfContentsPrimaryButtonLefted]: props.variant === 'editor' && !isVisible(),
}, },
'd-none d-xl-block' 'd-none d-xl-block',
)} )}
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()
@ -149,9 +151,9 @@ export const TableOfContents = (props: Props) => {
class={clsx( class={clsx(
styles.TableOfContentsPrimaryButton, styles.TableOfContentsPrimaryButton,
{ {
[styles.TableOfContentsPrimaryButtonLefted]: props.variant === 'editor' && !isVisible() [styles.TableOfContentsPrimaryButtonLefted]: props.variant === 'editor' && !isVisible(),
}, },
'd-xl-none' 'd-xl-none',
)} )}
onClick={(e) => { onClick={(e) => {
e.preventDefault() e.preventDefault()

View File

@ -1,19 +1,20 @@
import { createMemo, createSignal, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { FollowingEntity } from '../../graphql/types.gen'
import { follow, unfollow } from '../../stores/zine/common'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { createMemo, createSignal, Show } from 'solid-js'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Icon } from '../_shared/Icon'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { CardTopic } from '../Feed/CardTopic' import { useSession } from '../../context/session'
import { CheckButton } from '../_shared/CheckButton' import { FollowingEntity } from '../../graphql/types.gen'
import { follow, unfollow } from '../../stores/zine/common'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
import { Button } from '../_shared/Button'
import { CheckButton } from '../_shared/CheckButton'
import { Icon } from '../_shared/Icon'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { CardTopic } from '../Feed/CardTopic'
import styles from './Card.module.scss' import styles from './Card.module.scss'
import { Button } from '../_shared/Button'
import stylesButton from '../_shared/Button/Button.module.scss' import stylesButton from '../_shared/Button/Button.module.scss'
interface TopicProps { interface TopicProps {
@ -38,7 +39,7 @@ export const TopicCard = (props: TopicProps) => {
const { const {
subscriptions, subscriptions,
isSessionLoaded, isSessionLoaded,
actions: { loadSubscriptions, requireAuthentication } actions: { loadSubscriptions, requireAuthentication },
} = useSession() } = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
@ -89,7 +90,7 @@ export const TopicCard = (props: TopicProps) => {
classList={{ classList={{
row: !props.subscribeButtonBottom, row: !props.subscribeButtonBottom,
[styles.topicCompact]: props.compact, [styles.topicCompact]: props.compact,
[styles.topicInRow]: props.isTopicInRow [styles.topicInRow]: props.isTopicInRow,
}} }}
> >
<div <div
@ -97,7 +98,7 @@ export const TopicCard = (props: TopicProps) => {
[clsx('col-sm-18 col-md-24 col-lg-14 col-xl-15', styles.topicDetails)]: props.isNarrow, [clsx('col-sm-18 col-md-24 col-lg-14 col-xl-15', styles.topicDetails)]: props.isNarrow,
[clsx('col-24 col-sm-17 col-md-18', styles.topicDetails)]: props.compact, [clsx('col-24 col-sm-17 col-md-18', styles.topicDetails)]: props.compact,
[clsx('col-sm-17 col-md-18', styles.topicDetails)]: [clsx('col-sm-17 col-md-18', styles.topicDetails)]:
!props.subscribeButtonBottom && !props.isNarrow && !props.compact !props.subscribeButtonBottom && !props.isNarrow && !props.compact,
}} }}
> >
<Show when={props.topic.title && !props.isCardMode}> <Show when={props.topic.title && !props.isCardMode}>
@ -132,7 +133,7 @@ export const TopicCard = (props: TopicProps) => {
classList={{ classList={{
'col-sm-6 col-md-24 col-lg-10 col-xl-9': props.isNarrow, 'col-sm-6 col-md-24 col-lg-10 col-xl-9': props.isNarrow,
'col-24 col-sm-7 col-md-6': props.compact, 'col-24 col-sm-7 col-md-6': props.compact,
'col-sm-7 col-md-6': !props.subscribeButtonBottom && !props.isNarrow && !props.compact 'col-sm-7 col-md-6': !props.subscribeButtonBottom && !props.isNarrow && !props.compact,
}} }}
> >
<ShowOnlyOnClient> <ShowOnlyOnClient>
@ -151,7 +152,7 @@ export const TopicCard = (props: TopicProps) => {
isSubscribeButton={true} isSubscribeButton={true}
class={clsx(styles.actionButton, { class={clsx(styles.actionButton, {
[styles.isSubscribing]: isSubscribing(), [styles.isSubscribing]: isSubscribing(),
[stylesButton.subscribed]: subscribed() [stylesButton.subscribed]: subscribed(),
})} })}
disabled={isSubscribing()} disabled={isSubscribing()}
/> />

View File

@ -1,7 +1,9 @@
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { Icon } from '../_shared/Icon'
import './FloorHeader.scss'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import './FloorHeader.scss'
export default (props: { topic: Topic; color: string }) => { export default (props: { topic: Topic; color: string }) => {
const { t } = useLocalize() const { t } = useLocalize()

Some files were not shown because too many files have changed in this diff Show More