add useconfirm for req actions & fixes for toc (#155)

* add useconfirm for req actions & fixes for toc

* add interval for editor

* revert editor interval

* refactor by review comments

* add sticky pos for table of contents

* refactor toc for editor

* add debounce

* refactor by review comments

* Merge remote-tracking branch 'origin/main' into fix/useconfirm_n_tableofcont

# Conflicts:
#	package.json
#	src/components/Article/Comment.tsx
#	src/pages/profile/profileSettings.page.tsx

---------

Co-authored-by: bniwredyc <bniwredyc@gmail.com>
This commit is contained in:
Arkadzi Rakouski 2023-08-13 18:51:02 +03:00 committed by GitHub
parent 4fea17a2ce
commit b5708d26cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 144 additions and 89 deletions

View File

@ -30,7 +30,6 @@
}, },
"dependencies": { "dependencies": {
"@hocuspocus/provider": "2.0.6", "@hocuspocus/provider": "2.0.6",
"fast-deep-equal": "3.1.3",
"form-data": "4.0.0", "form-data": "4.0.0",
"i18next": "22.4.15", "i18next": "22.4.15",
"mailgun.js": "8.2.1", "mailgun.js": "8.2.1",
@ -101,6 +100,7 @@
"cookie-signature": "1.2.1", "cookie-signature": "1.2.1",
"cosmiconfig-toml-loader": "1.0.0", "cosmiconfig-toml-loader": "1.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"debounce": "1.2.1",
"eslint": "8.40.0", "eslint": "8.40.0",
"eslint-config-stylelint": "18.0.0", "eslint-config-stylelint": "18.0.0",
"eslint-import-resolver-typescript": "3.5.5", "eslint-import-resolver-typescript": "3.5.5",
@ -111,6 +111,7 @@
"eslint-plugin-solid": "0.12.1", "eslint-plugin-solid": "0.12.1",
"eslint-plugin-sonarjs": "0.19.0", "eslint-plugin-sonarjs": "0.19.0",
"eslint-plugin-unicorn": "47.0.0", "eslint-plugin-unicorn": "47.0.0",
"fast-deep-equal": "3.1.3",
"graphql": "16.6.0", "graphql": "16.6.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
"graphql-ws": "5.12.1", "graphql-ws": "5.12.1",

View File

@ -302,6 +302,8 @@
"Topic is supported by": "Topic is supported by", "Topic is supported by": "Topic is supported by",
"Topics": "Topics", "Topics": "Topics",
"Topics which supported by author": "Topics which supported by author", "Topics which supported by author": "Topics which supported by author",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?",
"Try to find another way": "Try to find another way", "Try to find another way": "Try to find another way",
"Unfollow": "Unfollow", "Unfollow": "Unfollow",
"Unfollow the topic": "Unfollow the topic", "Unfollow the topic": "Unfollow the topic",

View File

@ -319,6 +319,8 @@
"Topic is supported by": "Тему поддерживают", "Topic is supported by": "Тему поддерживают",
"Topics": "Темы", "Topics": "Темы",
"Topics which supported by author": "Автор поддерживает темы", "Topics which supported by author": "Автор поддерживает темы",
"There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"Try to find another way": "Попробуйте найти по-другому", "Try to find another way": "Попробуйте найти по-другому",
"Unfollow": "Отписаться", "Unfollow": "Отписаться",
"Unfollow the topic": "Отписаться от темы", "Unfollow the topic": "Отписаться от темы",

View File

@ -1,11 +1,13 @@
h1 { h1 {
@include font-size(4rem); @include font-size(4rem);
line-height: 1.1; line-height: 1.1;
margin-top: 0.5em; margin-top: 0.5em;
} }
h2 { h2 {
@include font-size(4rem); @include font-size(4rem);
line-height: 1.1; line-height: 1.1;
} }
@ -44,7 +46,7 @@ img {
margin: 3.2rem 0; margin: 3.2rem 0;
position: relative; position: relative;
&:before { &::before {
background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNiAyMSIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wIDEwLjUwMDFWMjFIMTAuNzA2MVYxMC41MDAxSDQuNTg4MzNDNC41ODgzMyA3LjE4NjU4IDcuMzI3NTggNC41MDAwOCAxMC43MDYxIDQuNTAwMDhWMEM0Ljc5Mjk3IDAgMCA0LjcwMDczIDAgMTAuNDk5OVYxMC41MDAxWk0yNiA0LjUwMDA4VjBDMjAuMDg2OSAwIDE1LjI5MzkgNC43MDA3MyAxNS4yOTM5IDEwLjQ5OTlWMjAuOTk5OUgyNlYxMC41MDA1SDE5Ljg4MjJDMTkuODgyNCA3LjE4NyAyMi42MjE3IDQuNTAwNSAyNiA0LjUwMDVWNC41MDAwOFoiIGZpbGw9ImJsYWNrIi8+PC9zdmc+') background: url('data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNiAyMSIgZmlsbD0ibm9uZSI+PHBhdGggZmlsbC1ydWxlPSJldmVub2RkIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0wIDEwLjUwMDFWMjFIMTAuNzA2MVYxMC41MDAxSDQuNTg4MzNDNC41ODgzMyA3LjE4NjU4IDcuMzI3NTggNC41MDAwOCAxMC43MDYxIDQuNTAwMDhWMEM0Ljc5Mjk3IDAgMCA0LjcwMDczIDAgMTAuNDk5OVYxMC41MDAxWk0yNiA0LjUwMDA4VjBDMjAuMDg2OSAwIDE1LjI5MzkgNC43MDA3MyAxNS4yOTM5IDEwLjQ5OTlWMjAuOTk5OUgyNlYxMC41MDA1SDE5Ljg4MjJDMTkuODgyNCA3LjE4NyAyMi42MjE3IDQuNTAwNSAyNiA0LjUwMDVWNC41MDAwOFoiIGZpbGw9ImJsYWNrIi8+PC9zdmc+')
no-repeat; no-repeat;
content: ''; content: '';
@ -60,17 +62,19 @@ img {
blockquote[data-type='quote'], blockquote[data-type='quote'],
ta-quotation { ta-quotation {
@include font-size(1.4rem); @include font-size(1.4rem);
border: solid #000; border: solid #000;
border-width: 0 0 0 2px; border-width: 0 0 0 2px;
display: block; display: block;
font-weight: 500; font-weight: 500;
line-height: 1.6; line-height: 1.6;
margin: 1.6rem 0 0 calc(-8.33333% - 2px); margin: 1.6rem 0 0 calc(-8.3333% - 2px);
padding: 0 0 0 8.33333%; padding: 0 0 0 8.3333%;
&[data-float='left'], &[data-float='left'],
&[data-float='right'] { &[data-float='right'] {
@include font-size(2.2rem); @include font-size(2.2rem);
line-height: 1.4; line-height: 1.4;
} }
@ -84,7 +88,7 @@ img {
} }
} }
&:before { &::before {
display: none; display: none;
} }
} }
@ -95,13 +99,15 @@ img {
ta-border-sub { ta-border-sub {
background: #f1f2f3; background: #f1f2f3;
display: block; display: block;
@include font-size(1.4rem); @include font-size(1.4rem);
margin: 3.2rem 0; margin: 3.2rem 0;
padding: 3.2rem; padding: 3.2rem;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
margin: 3.2rem -8.33333%; margin: 3.2rem -8.3333%;
padding: 3.2rem 8.33333%; padding: 3.2rem 8.3333%;
} }
p:last-child { p:last-child {
@ -144,7 +150,7 @@ img {
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
margin: 0 8.33333% 3.2rem -16.66666%; margin: 0 8.3333% 3.2rem -16.6666%;
} }
} }
@ -154,7 +160,7 @@ img {
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
margin: 0 -16.66666% 3.2rem 8.33333%; margin: 0 -16.6666% 3.2rem 8.3333%;
} }
} }
@ -168,13 +174,13 @@ img {
h2 { h2 {
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
margin-left: -16.6666666666%; margin-left: -16.6666%;
} }
} }
:global(.img-align-left) { :global(.img-align-left) {
float: left; float: left;
margin: 1em 8.333333333% 0.5em 0; margin: 1em 8.3333% 0.5em 0;
} }
:global(.width-30) { :global(.width-30) {
@ -187,18 +193,18 @@ img {
:global(.img-align-left.width-50) { :global(.img-align-left.width-50) {
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
margin-left: -16.6666666666%; margin-left: -16.6666%;
} }
} }
:global(.img-align-right) { :global(.img-align-right) {
float: right; float: right;
margin: 1em 0 0.5em 8.333333333%; margin: 1em 0 0.5em 8.3333%;
} }
:global(.img-align-right.width-50) { :global(.img-align-right.width-50) {
@include media-breakpoint-up(xl) { @include media-breakpoint-up(xl) {
margin-right: -16.6666666666%; margin-right: -16.6666%;
} }
} }
@ -498,6 +504,7 @@ img {
button { button {
@include font-size(1.5rem); @include font-size(1.5rem);
border-radius: 0.8rem; border-radius: 0.8rem;
margin-right: 1.2rem; margin-right: 1.2rem;
padding: 0.9rem 1.2rem; padding: 0.9rem 1.2rem;

View File

@ -17,7 +17,6 @@ import { useSnackbar } from '../../context/snackbar'
import { useConfirm } from '../../context/confirm' 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 styles from './Comment.module.scss' import styles from './Comment.module.scss'
@ -48,6 +47,7 @@ export const Comment = (props: Props) => {
const { const {
actions: { showConfirm } actions: { showConfirm }
} = useConfirm() } = useConfirm()
const { const {
actions: { showSnackbar } actions: { showSnackbar }
} = useSnackbar() } = useSnackbar()

View File

@ -132,7 +132,7 @@ export const FullArticle = (props: Props) => {
<> <>
<Title>{props.article.title}</Title> <Title>{props.article.title}</Title>
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row position-relative">
<article class="col-md-16 col-lg-14 col-xl-12 offset-md-5"> <article class="col-md-16 col-lg-14 col-xl-12 offset-md-5">
{/*TODO: Check styles.shoutTopic*/} {/*TODO: Check styles.shoutTopic*/}
<Show when={props.article.layout !== 'audio'}> <Show when={props.article.layout !== 'audio'}>
@ -212,7 +212,7 @@ export const FullArticle = (props: Props) => {
</Show> </Show>
</article> </article>
<Show when={isDesktop() && body()}> <Show when={isDesktop() && body()}>
<TableOfContents variant="article" parentSelector="#shoutBody" /> <TableOfContents variant="article" parentSelector="#shoutBody" body={body()} />
</Show> </Show>
</div> </div>
</div> </div>

View File

@ -1,5 +1,4 @@
import { Show } from 'solid-js' import { Show } from 'solid-js'
import type { Author, User } from '../../../graphql/types.gen'
import styles from './Userpic.module.scss' import styles from './Userpic.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { imageProxy } from '../../../utils/imageProxy' import { imageProxy } from '../../../utils/imageProxy'

View File

@ -62,6 +62,7 @@ const providers: Record<string, HocuspocusProvider> = {}
export const Editor = (props: Props) => { export const Editor = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { user } = useSession() const { user } = useSession()
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false) const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const docName = `shout-${props.shoutId}` const docName = `shout-${props.shoutId}`
@ -247,7 +248,7 @@ export const Editor = (props: Props) => {
<> <>
<div ref={(el) => (editorElRef.current = el)} id="editorBody" /> <div ref={(el) => (editorElRef.current = el)} id="editorBody" />
<Show when={isDesktop() && html()}> <Show when={isDesktop() && html()}>
<TableOfContents variant="editor" parentSelector="#editorBody" /> <TableOfContents variant="editor" parentSelector="#editorBody" body={html()} />
</Show> </Show>
<TextBubbleMenu <TextBubbleMenu
isCommonMarkup={isCommonMarkup()} isCommonMarkup={isCommonMarkup()}

View File

@ -24,11 +24,12 @@
box-sizing: content-box; box-sizing: content-box;
flex: 0 0 auto; flex: 0 0 auto;
@media (min-width: 768px) { @media (width >= 768px) {
padding-left: calc(21.9% + 3px); padding-left: calc(21.9% + 3px);
max-width: 72.7%; max-width: 72.7%;
} }
@media (min-width: 1200px) {
@media (width >= 1200px) {
padding-left: calc(21.5% + 3px); padding-left: calc(21.5% + 3px);
max-width: 64.9%; max-width: 64.9%;
} }
@ -38,32 +39,35 @@
.articleEditor figure, .articleEditor figure,
.articleEditor .uploadedImage, .articleEditor .uploadedImage,
.articleEditor article[data-type='incut'] { .articleEditor article[data-type='incut'] {
@media (min-width: 768px) { @media (width >= 768px) {
margin-left: calc(21.9% + 3px) !important; margin-left: calc(21.9% + 3px) !important;
max-width: 73.6%; max-width: 73.6%;
} }
@media (min-width: 1200px) {
@media (width >= 1200px) {
margin-left: calc(21.4% + 3px) !important; margin-left: calc(21.4% + 3px) !important;
max-width: 65.3%; max-width: 65.3%;
} }
} }
.articleEditor h2 { .articleEditor h2 {
@media (min-width: 768px) { @media (width >= 768px) {
padding-left: calc(21.9% + 2px); padding-left: calc(21.9% + 2px);
max-width: 72.7%; max-width: 72.7%;
} }
@media (min-width: 1200px) {
@media (width >= 1200px) {
padding-left: 21.5%; padding-left: 21.5%;
max-width: 87.1%; max-width: 87.1%;
} }
} }
.articleEditor h3 { .articleEditor h3 {
@media (min-width: 768px) { @media (width >= 768px) {
padding-left: calc(21.9% + 2px); padding-left: calc(21.9% + 2px);
} }
@media (min-width: 1200px) {
@media (width >= 1200px) {
padding-left: 21.5%; padding-left: 21.5%;
max-width: 87.1%; max-width: 87.1%;
} }
@ -73,7 +77,7 @@
.articleEditor * h2, .articleEditor * h2,
.articleEditor * h3, .articleEditor * h3,
.articleEditor * h4 { .articleEditor * h4 {
@media (min-width: 768px) { @media (width >= 768px) {
padding-left: unset; padding-left: unset;
max-width: unset; max-width: unset;
} }
@ -183,6 +187,7 @@ mark.highlight {
&[data-type='quote'] { &[data-type='quote'] {
@include font-size(1.4rem); @include font-size(1.4rem);
border: solid #000; border: solid #000;
border-width: 0 0 0 2px; border-width: 0 0 0 2px;
margin: 1.6rem 0; margin: 1.6rem 0;
@ -204,7 +209,9 @@ mark.highlight {
&[data-type='punchline'] { &[data-type='punchline'] {
border: solid #000; border: solid #000;
border-width: 2px 0; border-width: 2px 0;
@include font-size(3.2rem); @include font-size(3.2rem);
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
margin: 1em 0; margin: 1em 0;
@ -213,6 +220,7 @@ mark.highlight {
&[data-float='left'], &[data-float='left'],
&[data-float='right'] { &[data-float='right'] {
@include font-size(2.2rem); @include font-size(2.2rem);
line-height: 1.4; line-height: 1.4;
} }
@ -230,7 +238,9 @@ mark.highlight {
.ProseMirror article[data-type='incut'] { .ProseMirror article[data-type='incut'] {
background: #f1f2f3; background: #f1f2f3;
@include font-size(1.4rem); @include font-size(1.4rem);
margin: 1em -1rem; margin: 1em -1rem;
padding: 2em 2rem; padding: 2em 2rem;
transition: background 0.3s ease-in-out; transition: background 0.3s ease-in-out;

View File

@ -19,7 +19,6 @@
.confirmModalActions { .confirmModalActions {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 16px; margin-top: 16px;
} }
@ -27,26 +26,23 @@
display: block; display: block;
width: 100%; width: 100%;
margin-right: 12px; margin-right: 12px;
font-weight: 700; font-weight: 700;
margin-top: 32px; margin-top: 32px;
padding: 1.6rem !important; padding: 1.6rem !important;
border: 1px solid black; border: 1px solid black;
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.08); background-color: rgb(0 0 0 / 8%);
} }
} }
.confirmModalButtonPrimary { .confirmModalButtonPrimary {
margin-right: 0; margin-right: 0;
background-color: black; background-color: black;
color: white; color: white;
border: none; border: none;
&:hover { &:hover {
background-color: rgba(0, 0, 0, 0.6); background-color: rgb(0 0 0 / 60%);
} }
} }

View File

@ -1,7 +1,6 @@
import { Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js' import { Show, createSignal, createEffect, onMount, onCleanup } from 'solid-js'
import { getPagePath } from '@nanostores/router' import { getPagePath, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { redirectPage } from '@nanostores/router'
import { Modal } from './Modal' import { Modal } from './Modal'
import { AuthModal } from './AuthModal' import { AuthModal } from './AuthModal'

View File

@ -1,28 +1,26 @@
.TableOfContentsFixedWrapper { .TableOfContentsFixedWrapper {
position: fixed; position: absolute;
top: 150px; top: 0;
right: 20px; right: 0;
width: 281px; width: 281px;
min-height: 100%;
} }
.TableOfContentsFixedWrapperLefted { .TableOfContentsFixedWrapperLefted {
right: auto; right: auto;
left: 20px; left: 70px;
} }
.TableOfContentsContainer { .TableOfContentsContainer {
position: absolute; position: sticky;
right: 0; top: 150px;
top: 0; right: 20px;
display: flex; display: flex;
width: 100%; width: 100%;
height: auto; height: auto;
padding: 20px; padding: 20px;
flex-direction: column; flex-direction: column;
align-items: flex-start; align-items: flex-start;
background-color: transparent; background-color: transparent;
} }
@ -34,7 +32,6 @@
.TableOfContentsHeading { .TableOfContentsHeading {
margin: 0; margin: 0;
color: #000; color: #000;
font-size: 22px; font-size: 22px;
font-style: normal; font-style: normal;
@ -46,20 +43,17 @@
position: absolute; position: absolute;
right: 20px; right: 20px;
top: 10px; top: 10px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
width: 40px; width: 40px;
height: 40px; height: 40px;
background: transparent; background: transparent;
border: none; border: none;
cursor: pointer; cursor: pointer;
&:hover { &:hover {
box-shadow: 0px 0px 1px 1px rgba(0, 0, 0, 0.3); box-shadow: 0 0 1px 1px rgb(0 0 0 / 30%);
} }
} }
@ -70,18 +64,16 @@
.TableOfContentsHeadingsList { .TableOfContentsHeadingsList {
position: relative; position: relative;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
list-style-type: none; list-style-type: none;
margin: 0; margin: 0;
padding: 0 38px 0 0; padding: 0 38px 0 0;
width: 100%;
} }
.TableOfContentsHeadingsItem { .TableOfContentsHeadingsItem {
margin-top: 20px; margin-top: 20px;
color: #000; color: #000;
font-size: 14px; font-size: 14px;
font-style: normal; font-style: normal;
@ -91,7 +83,7 @@
letter-spacing: -0.14px; letter-spacing: -0.14px;
&:hover { &:hover {
transform: scale(1.05); color: rgb(0 0 0 / 50%);
} }
} }

View File

@ -1,10 +1,12 @@
import { onMount, For, Show, createSignal } from 'solid-js' import { For, Show, createSignal, createEffect, on } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { DEFAULT_HEADER_OFFSET } from '../../stores/router' import { DEFAULT_HEADER_OFFSET } from '../../stores/router'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { debounce } from 'debounce'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './TableOfContents.module.scss' import styles from './TableOfContents.module.scss'
@ -12,6 +14,7 @@ import styles from './TableOfContents.module.scss'
interface Props { interface Props {
variant: 'article' | 'editor' variant: 'article' | 'editor'
parentSelector: string parentSelector: string
body: string
} }
const scrollToHeader = (element) => { const scrollToHeader = (element) => {
@ -30,21 +33,33 @@ export const TableOfContents = (props: Props) => {
const [headings, setHeadings] = createSignal<Element[]>([]) const [headings, setHeadings] = createSignal<Element[]>([])
const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false) const [areHeadingsLoaded, setAreHeadingsLoaded] = createSignal<boolean>(false)
const [isVisible, setIsVisible] = createSignal<boolean>(true) const [isVisible, setIsVisible] = createSignal<boolean>(props.variant === 'article')
const toggleIsVisible = () => { const toggleIsVisible = () => {
setIsVisible((visible) => !visible) setIsVisible((visible) => !visible)
} }
onMount(() => { const updateHeadings = () => {
const { parentSelector } = props const { parentSelector } = props
// eslint-disable-next-line unicorn/prefer-spread // eslint-disable-next-line unicorn/prefer-spread
setHeadings(Array.from(document.querySelector(parentSelector).querySelectorAll('h2, h3, h4'))) setHeadings(Array.from(document.querySelector(parentSelector).querySelectorAll('h2, h3, h4')))
setAreHeadingsLoaded(true) setAreHeadingsLoaded(true)
}) }
const debouncedUpdateHeadings = debounce(updateHeadings, 500)
createEffect(
on(
() => props.body,
() => debouncedUpdateHeadings()
)
)
return ( return (
<Show when={areHeadingsLoaded()}> <Show
when={
areHeadingsLoaded() && (props.variant === 'article' ? headings().length > 2 : headings().length > 1)
}
>
<div <div
class={clsx(styles.TableOfContentsFixedWrapper, { class={clsx(styles.TableOfContentsFixedWrapper, {
[styles.TableOfContentsFixedWrapperLefted]: props.variant === 'editor' [styles.TableOfContentsFixedWrapperLefted]: props.variant === 'editor'

View File

@ -3,16 +3,11 @@ import { useLocalize } from '../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Title } from '@solidjs/meta' import { Title } from '@solidjs/meta'
import type { Shout, Topic } from '../../graphql/types.gen' import type { Shout, Topic } from '../../graphql/types.gen'
import { apiClient } from '../../utils/apiClient'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { ShoutForm, useEditorContext } from '../../context/editor' import { ShoutForm, useEditorContext } from '../../context/editor'
import { Editor, Panel, TopicSelect, UploadModalContent } from '../Editor' import { Editor, Panel } from '../Editor'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Button } from '../_shared/Button'
import styles from './Edit.module.scss' import styles from './Edit.module.scss'
import { useSession } from '../../context/session'
import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui'
import { imageProxy } from '../../utils/imageProxy' import { imageProxy } from '../../utils/imageProxy'
import { GrowingTextarea } from '../_shared/GrowingTextarea' import { GrowingTextarea } from '../_shared/GrowingTextarea'
import { VideoUploader } from '../Editor/VideoUploader' import { VideoUploader } from '../Editor/VideoUploader'
@ -25,6 +20,7 @@ import { clone } from '../../utils/clone'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { AutoSaveNotice } from '../Editor/AutoSaveNotice' import { AutoSaveNotice } from '../Editor/AutoSaveNotice'
import { PublishSettings } from './PublishSettings' import { PublishSettings } from './PublishSettings'
import { createStore } from 'solid-js/store'
type Props = { type Props = {
shout: Shout shout: Shout
@ -76,7 +72,7 @@ export const EditView = (props: Props) => {
}) })
} }
const [prevForm, setPrevForm] = createSignal<ShoutForm>(clone(form)) const [prevForm, setPrevForm] = createStore<ShoutForm>(clone(form))
const [saving, setSaving] = createSignal(false) const [saving, setSaving] = createSignal(false)
const mediaItems: Accessor<MediaItem[]> = createMemo(() => { const mediaItems: Accessor<MediaItem[]> = createMemo(() => {
@ -94,6 +90,20 @@ export const EditView = (props: Props) => {
}) })
}) })
onMount(() => {
// eslint-disable-next-line unicorn/consistent-function-scoping
const handleBeforeUnload = (event) => {
if (!deepEqual(prevForm, form)) {
event.returnValue = t(
`There are unsaved changes in your publishing settings. Are you sure you want to leave the page without saving?`
)
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
})
const handleTitleInputChange = (value) => { const handleTitleInputChange = (value) => {
setForm('title', value) setForm('title', value)
setForm('slug', slugify(value)) setForm('slug', slugify(value))
@ -174,7 +184,7 @@ export const EditView = (props: Props) => {
const autoSaveRecursive = () => { const autoSaveRecursive = () => {
autoSaveTimeOutId = setTimeout(async () => { autoSaveTimeOutId = setTimeout(async () => {
const hasChanges = !deepEqual(form, prevForm()) const hasChanges = !deepEqual(form, prevForm)
if (hasChanges) { if (hasChanges) {
setSaving(true) setSaving(true)
if (props.shout.visibility === 'owner') { if (props.shout.visibility === 'owner') {

View File

@ -1,6 +1,6 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './PublishSettings.module.scss' import styles from './PublishSettings.module.scss'
import { createEffect, createSignal, onMount, Show } from 'solid-js' import { createSignal, onMount, Show } from 'solid-js'
import { TopicSelect, UploadModalContent } from '../../Editor' import { TopicSelect, UploadModalContent } from '../../Editor'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { hideModal, showModal } from '../../../stores/ui' import { hideModal, showModal } from '../../../stores/ui'

View File

@ -1,7 +1,6 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './GrowingTextarea.module.scss' import styles from './GrowingTextarea.module.scss'
import { createSignal, Show, Switch } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { style } from 'solid-js/web'
type Props = { type Props = {
class?: string class?: string

View File

@ -65,6 +65,17 @@ const topic2topicInput = (topic: Topic): TopicInput => {
} }
} }
const saveDraftToLocalStorage = (formToSave: ShoutForm) => {
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
}
const getDraftFromLocalStorage = (shoutId: number) => {
return JSON.parse(localStorage.getItem(`shout-${shoutId}`))
}
const removeDraftFromLocalStorage = (shoutId: number) => {
localStorage.removeItem(`shout-${shoutId}`)
}
export const EditorProvider = (props: { children: JSX.Element }) => { export const EditorProvider = (props: { children: JSX.Element }) => {
const { t } = useLocalize() const { t } = useLocalize()
@ -164,17 +175,6 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
await updateShout(draftForm, { publish: false }) await updateShout(draftForm, { publish: false })
} }
const saveDraftToLocalStorage = (formToSave: ShoutForm) => {
localStorage.setItem(`shout-${formToSave.shoutId}`, JSON.stringify(formToSave))
}
const getDraftFromLocalStorage = (shoutId: number) => {
return JSON.parse(localStorage.getItem(`shout-${shoutId}`))
}
const removeDraftFromLocalStorage = (shoutId: number) => {
localStorage.removeItem(`shout-${shoutId}`)
}
const publishShout = async (formToPublish: ShoutForm) => { const publishShout = async (formToPublish: ShoutForm) => {
if (isEditorPanelVisible()) { if (isEditorPanelVisible()) {
toggleEditorPanel() toggleEditorPanel()

View File

@ -34,14 +34,16 @@ const useProfileForm = () => {
if (!currentSlug()) return if (!currentSlug()) return
try { try {
await loadAuthor({ slug: currentSlug() }) await loadAuthor({ slug: currentSlug() })
setForm({ const updatedFormValues = {
name: currentAuthor()?.name, name: currentAuthor()?.name,
slug: currentAuthor()?.slug, slug: currentAuthor()?.slug,
bio: currentAuthor()?.bio, bio: currentAuthor()?.bio,
about: currentAuthor()?.about, about: currentAuthor()?.about,
userpic: currentAuthor()?.userpic, userpic: currentAuthor()?.userpic,
links: currentAuthor()?.links links: currentAuthor()?.links
}) }
setForm(updatedFormValues)
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }

View File

@ -1,8 +1,10 @@
import { PageLayout } from '../../components/_shared/PageLayout' import { PageLayout } from '../../components/_shared/PageLayout'
import { Icon } from '../../components/_shared/Icon' import { Icon } from '../../components/_shared/Icon'
import ProfileSettingsNavigation from '../../components/Discours/ProfileSettingsNavigation' import ProfileSettingsNavigation from '../../components/Discours/ProfileSettingsNavigation'
import { For, createSignal, Show, onMount } from 'solid-js' import { For, createSignal, Show, onMount, onCleanup } from 'solid-js'
import deepEqual from 'fast-deep-equal'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './Settings.module.scss' import styles from './Settings.module.scss'
import { useProfileForm } from '../../context/profile' import { useProfileForm } from '../../context/profile'
import { validateUrl } from '../../utils/validateUrl' import { validateUrl } from '../../utils/validateUrl'
@ -13,6 +15,8 @@ import { useSnackbar } from '../../context/snackbar'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { handleFileUpload } from '../../utils/handleFileUpload' import { handleFileUpload } from '../../utils/handleFileUpload'
import { Userpic } from '../../components/Author/Userpic' import { Userpic } from '../../components/Author/Userpic'
import { createStore } from 'solid-js/store'
import { clone } from '../../utils/clone'
export const ProfileSettingsPage = () => { export const ProfileSettingsPage = () => {
const { t } = useLocalize() const { t } = useLocalize()
@ -24,11 +28,12 @@ export const ProfileSettingsPage = () => {
const { const {
actions: { showSnackbar } actions: { showSnackbar }
} = useSnackbar() } = useSnackbar()
const { const {
actions: { loadSession } actions: { loadSession }
} = useSession() } = useSession()
const { form, updateFormField, submit, slugError } = useProfileForm() const { form, updateFormField, submit, slugError } = useProfileForm()
const [prevForm, setPrevForm] = createStore(clone(form))
const handleChangeSocial = (value: string) => { const handleChangeSocial = (value: string) => {
if (validateUrl(value)) { if (validateUrl(value)) {
@ -45,6 +50,7 @@ export const ProfileSettingsPage = () => {
try { try {
await submit(form) await submit(form)
setPrevForm(clone(form))
showSnackbar({ body: t('Profile successfully saved') }) showSnackbar({ body: t('Profile successfully saved') })
} catch { } catch {
showSnackbar({ type: 'error', body: t('Error') }) showSnackbar({ type: 'error', body: t('Error') })
@ -70,9 +76,23 @@ export const ProfileSettingsPage = () => {
} }
const [hostname, setHostname] = createSignal<string | null>(null) const [hostname, setHostname] = createSignal<string | null>(null)
onMount(() => setHostname(window?.location.host))
console.log('!!! form:', form) onMount(() => {
setHostname(window?.location.host)
// eslint-disable-next-line unicorn/consistent-function-scoping
const handleBeforeUnload = (event) => {
if (!deepEqual(form, prevForm)) {
event.returnValue = t(
'There are unsaved changes in your profile settings. Are you sure you want to leave the page without saving?'
)
}
}
window.addEventListener('beforeunload', handleBeforeUnload)
onCleanup(() => window.removeEventListener('beforeunload', handleBeforeUnload))
})
return ( return (
<PageLayout> <PageLayout>
<Show when={form}> <Show when={form}>