editor+expo-fixes
All checks were successful
deploy / testbuild (push) Successful in 2m15s
deploy / Update templates on Mailgun (push) Has been skipped

This commit is contained in:
Untone 2024-10-12 03:01:14 +03:00
parent 73f78823e0
commit 30395dcfe6
7 changed files with 426 additions and 186 deletions

View File

@ -140,7 +140,8 @@ export const EditorComponent = (props: Props) => {
}), }),
FloatingMenu.configure({ FloatingMenu.configure({
tippyOptions: { tippyOptions: {
placement: 'left' placement: 'left',
appendTo: document.body
}, },
element: floatingMenuRef()! element: floatingMenuRef()!
}), }),
@ -151,8 +152,8 @@ export const EditorComponent = (props: Props) => {
content: props.initialContent || null, content: props.initialContent || null,
onTransaction: ({ editor: e, transaction }) => { onTransaction: ({ editor: e, transaction }) => {
if (transaction.docChanged) { if (transaction.docChanged) {
const html = e.getHTML() //const html = e.getHTML()
html && props.onChange(html) //html && props.onChange(html)
const wordCount: number = e.storage.characterCount.words() const wordCount: number = e.storage.characterCount.words()
const charsCount: number = e.storage.characterCount.characters() const charsCount: number = e.storage.characterCount.characters()
charsCount && countWords({ words: wordCount, characters: charsCount }) charsCount && countWords({ words: wordCount, characters: charsCount })

View File

@ -1,7 +1,6 @@
import { A } from '@solidjs/router' import { A } from '@solidjs/router'
import clsx from 'clsx' import clsx from 'clsx'
import { For, Show, createEffect, createSignal, on } from 'solid-js' import { For, Show, createEffect, createSignal, on } from 'solid-js'
import { ConditionalWrapper } from '~/components/_shared/ConditionalWrapper'
import { Loading } from '~/components/_shared/Loading' import { Loading } from '~/components/_shared/Loading'
import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper' import { ArticleCardSwiper } from '~/components/_shared/SolidSwiper/ArticleCardSwiper'
import { EXPO_LAYOUTS, SHOUTS_PER_PAGE } from '~/context/feed' import { EXPO_LAYOUTS, SHOUTS_PER_PAGE } from '~/context/feed'
@ -18,20 +17,24 @@ import styles from '~/styles/views/Expo.module.scss'
export const ExpoNav = (props: { layout: ExpoLayoutType | '' }) => { export const ExpoNav = (props: { layout: ExpoLayoutType | '' }) => {
const { t } = useLocalize() const { t } = useLocalize()
return ( return (
<div class="wide-container"> <div class="wide-container">
<ul class={clsx('view-switcher')}> <ul class={clsx('view-switcher')}>
<For each={[...EXPO_LAYOUTS, '']}> <For each={[...EXPO_LAYOUTS, '']}>
{(layoutKey) => ( {(layoutKey) => (
<li class={clsx({ 'view-switcher__item--selected': props.layout === layoutKey })}> <li class={clsx({ 'view-switcher__item--selected': props.layout === layoutKey })}>
<ConditionalWrapper {props.layout !== layoutKey ? (
condition={props.layout !== layoutKey} <A href={`/expo/${layoutKey}`}>
wrapper={(children) => <A href={`/expo/${layoutKey}`}>{children}</A>}
>
<span class="linkReplacement"> <span class="linkReplacement">
{layoutKey in EXPO_TITLES ? t(EXPO_TITLES[layoutKey as ExpoLayoutType]) : t('All')} {layoutKey in EXPO_TITLES ? t(EXPO_TITLES[layoutKey as ExpoLayoutType]) : t('All')}
</span> </span>
</ConditionalWrapper> </A>
) : (
<span class="linkReplacement">
{layoutKey in EXPO_TITLES ? t(EXPO_TITLES[layoutKey as ExpoLayoutType]) : t('All')}
</span>
)}
</li> </li>
)} )}
</For> </For>
@ -88,13 +91,14 @@ export const Expo = (props: Props) => {
) )
) )
try {
return ( return (
<div class={styles.Expo}> <div class={styles.Expo}>
<Show when={props.shouts} fallback={<Loading />} keyed> <Show when={props.shouts} fallback={<Loading />} keyed>
{(feed: Shout[]) => ( {(feed) => (
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<For each={feed.slice(0, SHOUTS_PER_PAGE) || []}> <For each={Array.from(feed || []).slice(0, SHOUTS_PER_PAGE)}>
{(shout) => ( {(shout) => (
<div id={`shout-${shout.id}`} class="col-md-6 mt-md-5 col-sm-8 mt-sm-3"> <div id={`shout-${shout.id}`} class="col-md-6 mt-md-5 col-sm-8 mt-sm-3">
<ArticleCard <ArticleCard
@ -120,4 +124,8 @@ export const Expo = (props: Props) => {
</Show> </Show>
</div> </div>
) )
} catch (error) {
console.error('Error in Expo component:', error)
return <div>An error occurred. Please try again later.</div>
}
} }

View File

@ -17,7 +17,7 @@ type Props = {
textAreaRef?: (el: HTMLTextAreaElement) => void textAreaRef?: (el: HTMLTextAreaElement) => void
} }
const GrowingTextarea = (props: Props) => { export const GrowingTextarea = (props: Props) => {
const [value, setValue] = createSignal<string>('') const [value, setValue] = createSignal<string>('')
const [isFocused, setIsFocused] = createSignal(false) const [isFocused, setIsFocused] = createSignal(false)

View File

@ -32,6 +32,7 @@ export const ArticleCardSwiper = (props: Props) => {
}) })
return ( return (
<Show when={props.slides && props.slides.length > 0}>
<ShowOnlyOnClient> <ShowOnlyOnClient>
<div <div
class={clsx({ class={clsx({
@ -110,5 +111,6 @@ export const ArticleCardSwiper = (props: Props) => {
</div> </div>
</div> </div>
</ShowOnlyOnClient> </ShowOnlyOnClient>
</Show>
) )
} }

View File

@ -1,17 +1,28 @@
import { HocuspocusProvider } from '@hocuspocus/provider'
import { useMatch, useNavigate } from '@solidjs/router' import { useMatch, useNavigate } from '@solidjs/router'
import { Editor } from '@tiptap/core' import { Editor } from '@tiptap/core'
import Collaboration from '@tiptap/extension-collaboration'
import CollaborationCursor from '@tiptap/extension-collaboration-cursor'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import { Accessor, createContext, createSignal, useContext } from 'solid-js' import { Accessor, createContext, createEffect, createSignal, on, onCleanup, useContext } from 'solid-js'
import { SetStoreFunction, createStore } from 'solid-js/store' import { SetStoreFunction, createStore } from 'solid-js/store'
import { debounce } from 'throttle-debounce'
import uniqolor from 'uniqolor'
import { Doc } from 'yjs'
import { useSnackbar } from '~/context/ui' import { useSnackbar } from '~/context/ui'
import deleteShoutQuery from '~/graphql/mutation/core/article-delete' import createShoutMutation from '~/graphql/mutation/core/article-create'
import updateShoutQuery from '~/graphql/mutation/core/article-update' import deleteShoutMutation from '~/graphql/mutation/core/article-delete'
import updateShoutMutation from '~/graphql/mutation/core/article-update'
import { Topic, TopicInput } from '~/graphql/schema/core.gen' import { Topic, TopicInput } from '~/graphql/schema/core.gen'
import { slugify } from '~/intl/translit' import { slugify } from '~/intl/translit'
import { useFeed } from '../context/feed' import { useFeed } from '../context/feed'
import { useLocalize } from './localize' import { useLocalize } from './localize'
import { useSession } from './session' import { useSession } from './session'
export const AUTO_SAVE_DELAY = 3000
const yDocs: Record<string, Doc> = {}
const providers: Record<string, HocuspocusProvider> = {}
export type WordCounter = { export type WordCounter = {
characters: number characters: number
words: number words: number
@ -52,6 +63,9 @@ export type EditorContextType = {
setEditing: SetStoreFunction<Editor | undefined> setEditing: SetStoreFunction<Editor | undefined>
isCollabMode: Accessor<boolean> isCollabMode: Accessor<boolean>
setIsCollabMode: SetStoreFunction<boolean> setIsCollabMode: SetStoreFunction<boolean>
handleInputChange: (key: keyof ShoutForm, value: string) => void
saving: Accessor<boolean>
hasChanges: Accessor<boolean>
} }
export const EditorContext = createContext<EditorContextType>({} as EditorContextType) export const EditorContext = createContext<EditorContextType>({} as EditorContextType)
@ -79,29 +93,34 @@ const removeDraftFromLocalStorage = (shoutId: number) => {
localStorage?.removeItem(`shout-${shoutId}`) localStorage?.removeItem(`shout-${shoutId}`)
} }
export const EditorProvider = (props: { children: JSX.Element }) => { const defaultForm: ShoutForm = {
const localize = useLocalize()
const navigate = useNavigate()
const matchEdit = useMatch(() => '/edit')
const matchEditSettings = useMatch(() => '/editSettings')
const { client } = useSession()
const { addFeed } = useFeed()
const snackbar = useSnackbar()
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
const [form, setForm] = createStore<ShoutForm>({
body: '', body: '',
slug: '', slug: '',
shoutId: 0, shoutId: 0,
title: '', title: '',
selectedTopics: [] selectedTopics: []
}) }
export const EditorProvider = (props: { children: JSX.Element }) => {
const localize = useLocalize()
const navigate = useNavigate()
const matchEdit = useMatch(() => '/edit')
const matchEditSettings = useMatch(() => '/editSettings')
const { client, session } = useSession()
const { addFeed } = useFeed()
const snackbar = useSnackbar()
const [isEditorPanelVisible, setIsEditorPanelVisible] = createSignal<boolean>(false)
const [form, setForm] = createStore<ShoutForm>(defaultForm)
const [formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>) const [formErrors, setFormErrors] = createStore({} as Record<keyof ShoutForm, string>)
const [wordCounter, setWordCounter] = createSignal<WordCounter>({ const [wordCounter, setWordCounter] = createSignal<WordCounter>({ characters: 0, words: 0 })
characters: 0,
words: 0
})
const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value) const toggleEditorPanel = () => setIsEditorPanelVisible((value) => !value)
const [isCollabMode, setIsCollabMode] = createSignal<boolean>(false) const [isCollabMode, setIsCollabMode] = createSignal<boolean>(false)
// current publishing editor instance to connect settings, panel and editor
const [editing, setEditing] = createSignal<Editor | undefined>(undefined)
const [saving, setSaving] = createSignal(false)
const [hasChanges, setHasChanges] = createSignal(false)
const countWords = (value: WordCounter) => setWordCounter(value) const countWords = (value: WordCounter) => setWordCounter(value)
const validate = () => { const validate = () => {
if (!form.title) { if (!form.title) {
@ -131,11 +150,14 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
} }
const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => { const updateShout = async (formToUpdate: ShoutForm, { publish }: { publish: boolean }) => {
if (!formToUpdate.shoutId) { if (!formToUpdate.shoutId && formToUpdate.body) {
console.error(formToUpdate) console.debug('[updateShout] no shoutId, but body:', formToUpdate)
return { error: 'not enought data' } const resp = await client()
?.mutation(createShoutMutation, { shout: { layout: formToUpdate.layout, body: formToUpdate.body } })
.toPromise()
return resp?.data?.create_shout
} }
const resp = await client()?.mutation(updateShoutQuery, { const resp = await client()?.mutation(updateShoutMutation, {
shout_id: formToUpdate.shoutId, shout_id: formToUpdate.shoutId,
shout_input: { shout_input: {
body: formToUpdate.body, body: formToUpdate.body,
@ -157,15 +179,9 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
} }
const saveShout = async (formToSave: ShoutForm) => { const saveShout = async (formToSave: ShoutForm) => {
if (isEditorPanelVisible()) { isEditorPanelVisible() && toggleEditorPanel()
toggleEditorPanel()
}
if (matchEdit() && !validate()) { if ((matchEdit() && !validate()) || (matchEditSettings() && !validateSettings())) {
return
}
if (matchEditSettings() && !validateSettings()) {
return return
} }
@ -176,12 +192,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
return return
} }
removeDraftFromLocalStorage(formToSave.shoutId) removeDraftFromLocalStorage(formToSave.shoutId)
navigate(shout?.published_at ? `/article/${shout.slug}` : '/edit')
if (shout?.published_at) {
navigate(`/article/${shout.slug}`)
} else {
navigate('/edit')
}
} catch (error) { } catch (error) {
console.error('[saveShout]', error) console.error('[saveShout]', error)
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
@ -197,25 +208,21 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
} }
const publishShout = async (formToPublish: ShoutForm) => { const publishShout = async (formToPublish: ShoutForm) => {
if (isEditorPanelVisible()) { isEditorPanelVisible() && toggleEditorPanel()
toggleEditorPanel()
if ((matchEdit() && !validate()) || (matchEditSettings() && !validateSettings())) {
return
} }
if (matchEdit()) { if (matchEdit()) {
if (!validate()) return
const slug = slugify(form.title) const slug = slugify(form.title)
setForm('slug', slug) setForm('slug', slug)
navigate(`/edit/${form.shoutId}/settings`) navigate(`/edit/${form.shoutId}/settings`)
const { error } = await updateShout(formToPublish, { publish: false }) const { error } = await updateShout(formToPublish, { publish: false })
if (error) { if (error) {
snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' }) snackbar?.showSnackbar({ type: 'error', body: localize?.t(error) || '' })
}
return return
} }
if (!validateSettings()) {
return
} }
try { try {
@ -237,7 +244,7 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
return return
} }
try { try {
const resp = await client()?.mutation(updateShoutQuery, { shout_id, publish: true }).toPromise() const resp = await client()?.mutation(deleteShoutMutation, { shout_id, publish: true }).toPromise()
const result = resp?.data?.update_shout const result = resp?.data?.update_shout
if (result) { if (result) {
const { shout: newShout, error } = result const { shout: newShout, error } = result
@ -255,13 +262,13 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
} }
} catch (error) { } catch (error) {
console.error('[publishShoutById]', error) console.error('[publishShoutById]', error)
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') }) snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
} }
} }
const deleteShout = async (shout_id: number) => { const deleteShout = async (shout_id: number) => {
try { try {
const resp = await client()?.mutation(deleteShoutQuery, { shout_id }).toPromise() const resp = await client()?.mutation(deleteShoutMutation, { shout_id }).toPromise()
return resp?.data?.delete_shout return resp?.data?.delete_shout
} catch { } catch {
snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' }) snackbar?.showSnackbar({ type: 'error', body: localize?.t('Error') || '' })
@ -269,8 +276,82 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
} }
} }
// current publishing editor instance to connect settings, panel and editor const debouncedAutoSave = debounce(AUTO_SAVE_DELAY, async () => {
const [editing, setEditing] = createSignal<Editor | undefined>(undefined) console.log('autoSave called')
if (hasChanges()) {
console.debug('saving draft', form)
setSaving(true)
saveDraftToLocalStorage(form)
await saveDraft(form)
setSaving(false)
setHasChanges(false)
}
})
onCleanup(debouncedAutoSave.cancel)
createEffect(
on(
isCollabMode,
(x?: boolean) => () => {
const editorInstance = editing()
if (!editorInstance) return
try {
const docName = `shout-${form.shoutId}`
const token = session()?.access_token || ''
const profile = session()?.user?.app_data?.profile
if (!(token && profile)) {
throw new Error('Missing authentication data')
}
if (!yDocs[docName]) {
yDocs[docName] = new Doc()
}
if (!providers[docName]) {
providers[docName] = new HocuspocusProvider({
url: 'wss://hocuspocus.discours.io',
name: docName,
document: yDocs[docName],
token
})
console.log(`[collab mode] HocuspocusProvider connected for ${docName}`)
}
if (x) {
const newExtensions = [
Collaboration.configure({ document: yDocs[docName] }),
CollaborationCursor.configure({
provider: providers[docName],
user: { name: profile.name, color: uniqolor(profile.slug).color }
})
]
const extensions = editing()?.options.extensions.concat(newExtensions)
editorInstance.setOptions({ ...editorInstance.options, extensions })
providers[docName].connect()
} else if (editorInstance) {
providers[docName].disconnect()
const updatedExtensions = editorInstance.options.extensions.filter(
(ext) => ext.name !== 'collaboration' && ext.name !== 'collaborationCursor'
)
editorInstance.setOptions({
...editorInstance.options,
extensions: updatedExtensions
})
}
} catch (error) {
console.error('[collab mode] error', error)
}
},
{ defer: true }
)
)
const handleInputChange = (key: keyof ShoutForm, value: string) => {
console.log(`[handleInputChange] ${key}: ${value}`)
setForm(key, value)
setHasChanges(true)
debouncedAutoSave()
}
const actions = { const actions = {
saveShout, saveShout,
@ -286,7 +367,10 @@ export const EditorProvider = (props: { children: JSX.Element }) => {
setFormErrors, setFormErrors,
setEditing, setEditing,
isCollabMode, isCollabMode,
setIsCollabMode setIsCollabMode,
handleInputChange,
saving,
hasChanges
} }
const value: EditorContextType = { const value: EditorContextType = {

View File

@ -1,8 +1,10 @@
import { Params, RouteSectionProps, createAsync } from '@solidjs/router' import { Params, RouteSectionProps, createAsync } from '@solidjs/router'
import { Show, createEffect, createSignal, on } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on } from 'solid-js'
import { isServer } from 'solid-js/web'
import { TopicsNav } from '~/components/HeaderNav/TopicsNav' import { TopicsNav } from '~/components/HeaderNav/TopicsNav'
import { Expo, ExpoNav } from '~/components/Views/ExpoView' import { Expo, ExpoNav } from '~/components/Views/ExpoView'
import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper' import { LoadMoreItems, LoadMoreWrapper } from '~/components/_shared/LoadMoreWrapper'
import { Loading } from '~/components/_shared/Loading'
import { PageLayout } from '~/components/_shared/PageLayout' import { PageLayout } from '~/components/_shared/PageLayout'
import { EXPO_LAYOUTS, EXPO_TITLES, SHOUTS_PER_PAGE, useFeed } from '~/context/feed' import { EXPO_LAYOUTS, EXPO_TITLES, SHOUTS_PER_PAGE, useFeed } from '~/context/feed'
import { useLocalize } from '~/context/localize' import { useLocalize } from '~/context/localize'
@ -18,7 +20,7 @@ const fetchExpoShouts = async (layouts: string[]) => {
limit: SHOUTS_PER_PAGE, limit: SHOUTS_PER_PAGE,
offset: 0 offset: 0
} as LoadShoutsOptions) } as LoadShoutsOptions)
return result || [] return result
} }
export const route = { export const route = {
@ -33,14 +35,14 @@ export default (props: RouteSectionProps<Shout[]>) => {
const { t } = useLocalize() const { t } = useLocalize()
const { expoFeed, setExpoFeed, feedByLayout } = useFeed() const { expoFeed, setExpoFeed, feedByLayout } = useFeed()
const [loadMoreVisible, setLoadMoreVisible] = createSignal(false) const [loadMoreVisible, setLoadMoreVisible] = createSignal(false)
const getTitle = (l?: string) => EXPO_TITLES[(l as ExpoLayoutType) || ''] const getTitle = createMemo(() => (l?: string) => EXPO_TITLES[(l as ExpoLayoutType) || ''])
const shouts = createAsync( const shouts = createAsync(async () =>
async () => isServer
props.data || (await fetchExpoShouts(props.params.layout ? [props.params.layout] : EXPO_LAYOUTS)) ? props.data
: await fetchExpoShouts(props.params.layout ? [props.params.layout] : EXPO_LAYOUTS)
) )
// Функция для загрузки дополнительных шотов
const loadMore = async () => { const loadMore = async () => {
saveScrollPosition() saveScrollPosition()
const limit = SHOUTS_PER_PAGE const limit = SHOUTS_PER_PAGE
@ -48,46 +50,52 @@ export default (props: RouteSectionProps<Shout[]>) => {
const offset = expoFeed()?.length || 0 const offset = expoFeed()?.length || 0
const filters: LoadShoutsFilters = { layouts, featured: true } const filters: LoadShoutsFilters = { layouts, featured: true }
const options: LoadShoutsOptions = { filters, limit, offset } const options: LoadShoutsOptions = { filters, limit, offset }
const shoutsFetcher = loadShouts(options) const fetcher = await loadShouts(options)
const result = await shoutsFetcher() const result = (await fetcher()) || []
setLoadMoreVisible(Boolean(result?.length)) setLoadMoreVisible(Boolean(result?.length))
if (result) { if (result && Array.isArray(result)) {
setExpoFeed((prev) => Array.from(new Set([...(prev || []), ...result])).sort(byCreated)) setExpoFeed((prev) => Array.from(new Set([...(prev || []), ...result])).sort(byCreated))
} }
restoreScrollPosition() restoreScrollPosition()
return result as LoadMoreItems return result as LoadMoreItems
} }
// Эффект для загрузки данных при изменении layout
createEffect( createEffect(
on( on(
() => props.params.layout as ExpoLayoutType, () => props.params.layout,
async (layout?: ExpoLayoutType) => { async (currentLayout) => {
const layouts = layout ? [layout] : EXPO_LAYOUTS const layouts = currentLayout ? [currentLayout] : EXPO_LAYOUTS
const offset = (layout ? feedByLayout()[layout]?.length : expoFeed()?.length) || 0 const offset = (currentLayout ? feedByLayout()[currentLayout]?.length : expoFeed()?.length) || 0
const options: LoadShoutsOptions = { const options: LoadShoutsOptions = {
filters: { layouts, featured: true }, filters: { layouts, featured: true },
limit: SHOUTS_PER_PAGE, limit: SHOUTS_PER_PAGE,
offset offset
} }
const shoutsFetcher = loadShouts(options) const result = await loadShouts(options)
const result = await shoutsFetcher() if (result && Array.isArray(result)) {
setExpoFeed(result || []) setExpoFeed(result)
} else {
setExpoFeed([])
}
} }
) )
) )
return ( return (
<PageLayout <PageLayout
withPadding={true} withPadding={true}
zeroBottomPadding={true} zeroBottomPadding={true}
title={`${t('Discours')} :: ${getTitle(props.params.layout || '')}`} title={`${t('Discours')} :: ${getTitle()((props.params.layout as ExpoLayoutType) || '')}`}
> >
<TopicsNav /> <TopicsNav />
<ExpoNav layout={(props.params.layout || '') as ExpoLayoutType | ''} /> <ExpoNav layout={(props.params.layout as ExpoLayoutType) || ''} />
<Show when={shouts()} fallback={<Loading />} keyed>
{(sss) => (
<LoadMoreWrapper loadFunction={loadMore} pageSize={SHOUTS_PER_PAGE} hidden={!loadMoreVisible()}> <LoadMoreWrapper loadFunction={loadMore} pageSize={SHOUTS_PER_PAGE} hidden={!loadMoreVisible()}>
<Show when={shouts()} keyed> <Expo shouts={sss as Shout[]} layout={(props.params.layout as ExpoLayoutType) || ''} />
{(sss: Shout[]) => <Expo shouts={sss} layout={props.params.layout as ExpoLayoutType} />}
</Show>
</LoadMoreWrapper> </LoadMoreWrapper>
)}
</Show>
</PageLayout> </PageLayout>
) )
} }

View File

@ -198,6 +198,143 @@ button {
} }
} }
.button--subscribe {
background: var(--background-color);
color: var(--default-color);
border: 2px solid var(--black-100);
font-size: 1.5rem;
justify-content: center;
padding: 0.6rem 1.2rem;
transition: background-color 0.2s;
img {
height: auto;
transition: filter 0.2s;
}
&:hover {
background: var(--background-color-invert);
color: var(--default-color-invert);
img {
filter: invert(1);
}
}
}
.button--light {
font-size:1.5rem;
background-color: var(--black-100);
border-radius: 0.8rem;
color: var(--default-color);
font-weight: 500;
height: auto;
padding: 0.6rem 1.2rem 0.6rem 1rem;
&:hover {
background: var(--black-300);
}
}
.button--subscribe-topic {
background: var(--background-color);
color: var(--default-color);
border: 2px solid var(--default-color);
border-radius: 0.8rem;
font-size: 1.4rem;
line-height: 2.8rem;
height: 3.2rem;
padding: 0 1rem;
&:hover {
background: var(--background-color-invert);
color: var(--default-color-invert);
opacity: 1;
.icon {
filter: invert(1);
}
}
&[disabled]:hover {
background: var(--background-color);
color: var(--default-color);
}
.icon {
display: inline-block;
margin-right: 0.3em;
vertical-align: text-bottom;
width: 1.4em;
}
}
.button--content-index {
@include media-breakpoint-up(md) {
margin-top: -0.5rem;
position: sticky;
top: 135px;
}
@include media-breakpoint-up(sm) {
right: $container-padding-x;
}
background: none;
border: 2px solid var(--white-500);
height: 3.2rem;
float: right;
padding: 0;
position: absolute;
right: $container-padding-x * 0.5;
top: -0.5rem;
width: 3.2rem;
z-index: 1;
.icon {
background: #fff;
transition: filter 0.3s;
}
.icon,
img {
height: 100%;
vertical-align: middle;
width: auto;
}
&:hover {
.icon {
filter: invert(1);
}
}
.expanded {
border-radius: 100%;
overflow: hidden;
img {
height: auto;
margin-top: 0.8rem;
}
}
}
.button--submit,
.button--outline {
font-size:2rem;
padding: 1.6rem 2rem;
}
.button--outline {
background: none;
box-shadow: inset 0 0 0 2px #000;
color: #000;
&:hover {
box-shadow: inset 0 0 0 2px var(--black-300);
}
}
form { form {
input[type='text'], input[type='text'],