Merge main

This commit is contained in:
ilya-bkv 2023-11-13 17:43:08 +03:00
parent d65d9c4188
commit e4f545b935
166 changed files with 5879 additions and 3200 deletions

View File

@ -1,6 +1,6 @@
{ {
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write", "*.{js,mjs,ts,tsx,json,scss,css,html}": "prettier --write",
"package.json": "sort-package-json", "package.json": "sort-package-json",
"*.{scss,css}": "stylelint", "*.{scss,css}": "stylelint",
"*.{ts,tsx,js}": "eslint --fix" "*.{ts,tsx,js,mjs}": "eslint --fix"
} }

View File

@ -1,24 +0,0 @@
import fetch from 'node-fetch'
export default async function handler(req, res) {
const imageUrl = req.query.url
if (!imageUrl) {
return res.status(400).send('Missing URL parameter')
}
try {
const imageRes = await fetch(imageUrl)
if (!imageRes.ok) {
return res.status(404).send('Image not found')
}
res.setHeader('Content-Type', imageRes.headers.get('content-type'))
imageRes.body.pipe(res)
} catch (err) {
console.error(err)
return res.status(404).send('Error')
}
}

View File

@ -1,13 +1,13 @@
import { renderPage } from 'vite-plugin-ssr/server' import { renderPage } from 'vike/server'
export default async function handler(req, res) { export default async function handler(req, res) {
const { url, cookies } = req const { url, cookies } = req
const pageContext = await renderPage({ urlOriginal: url, cookies }) const pageContext = await renderPage({ urlOriginal: url, cookies })
const { httpResponse, errorWhileRendering } = pageContext const { httpResponse, errorWhileRendering, is404 } = pageContext
if (errorWhileRendering) { if (errorWhileRendering && !is404) {
console.error(errorWhileRendering) console.error(errorWhileRendering)
res.statusCode = 500 res.statusCode = 500
res.end() res.end()

3477
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
"version": "0.8.0", "version": "0.8.0",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module",
"scripts": { "scripts": {
"build": "vite build", "build": "vite build",
"check": "npm run lint && npm run typecheck", "check": "npm run lint && npm run typecheck",
@ -35,16 +36,15 @@
"idb": "7.1.1", "idb": "7.1.1",
"intl-messageformat": "10.5.3", "intl-messageformat": "10.5.3",
"just-throttle": "4.2.0", "just-throttle": "4.2.0",
"mailgun.js": "8.2.1", "mailgun.js": "8.2.1"
"node-fetch": "3.3.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.21.8", "@babel/core": "7.21.8",
"@graphql-codegen/cli": "3.2.2", "@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/typescript": "3.0.4", "@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "3.0.4", "@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql": "3.7.3", "@graphql-codegen/typescript-urql": "4.0.0",
"@graphql-codegen/urql-introspection": "2.2.1", "@graphql-codegen/urql-introspection": "3.0.0",
"@graphql-tools/url-loader": "7.17.18", "@graphql-tools/url-loader": "7.17.18",
"@graphql-typed-document-node/core": "3.2.0", "@graphql-typed-document-node/core": "3.2.0",
"@hocuspocus/provider": "2.0.6", "@hocuspocus/provider": "2.0.6",
@ -89,27 +89,27 @@
"@tiptap/extension-text": "2.0.3", "@tiptap/extension-text": "2.0.3",
"@tiptap/extension-underline": "2.0.3", "@tiptap/extension-underline": "2.0.3",
"@tiptap/extension-youtube": "2.0.3", "@tiptap/extension-youtube": "2.0.3",
"@types/js-cookie": "3.0.4", "@types/js-cookie": "3.0.5",
"@types/node": "20.1.1", "@types/node": "20.8.10",
"@typescript-eslint/eslint-plugin": "6.7.3", "@typescript-eslint/eslint-plugin": "6.9.1",
"@typescript-eslint/parser": "6.7.3", "@typescript-eslint/parser": "6.9.1",
"@urql/core": "3.2.2", "@urql/core": "3.2.2",
"@urql/devtools": "2.0.3", "@urql/devtools": "2.0.3",
"babel-preset-solid": "1.7.4", "babel-preset-solid": "1.8.4",
"bootstrap": "5.3.2", "bootstrap": "5.3.2",
"clsx": "2.0.0", "clsx": "2.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"debounce": "1.2.1", "debounce": "1.2.1",
"eslint": "8.50.0", "eslint": "8.53.0",
"eslint-config-stylelint": "20.0.0", "eslint-config-stylelint": "20.0.0",
"eslint-import-resolver-typescript": "3.6.1", "eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.28.1", "eslint-plugin-import": "2.29.0",
"eslint-plugin-jest": "27.4.0", "eslint-plugin-jest": "27.6.0",
"eslint-plugin-jsx-a11y": "6.7.1", "eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-promise": "6.1.1", "eslint-plugin-promise": "6.1.1",
"eslint-plugin-solid": "0.13.0", "eslint-plugin-solid": "0.13.0",
"eslint-plugin-sonarjs": "0.21.0", "eslint-plugin-sonarjs": "0.23.0",
"eslint-plugin-unicorn": "48.0.1", "eslint-plugin-unicorn": "49.0.0",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"graphql": "16.6.0", "graphql": "16.6.0",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
@ -120,7 +120,7 @@
"javascript-time-ago": "2.5.9", "javascript-time-ago": "2.5.9",
"jest": "29.7.0", "jest": "29.7.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"lint-staged": "14.0.1", "lint-staged": "15.0.2",
"loglevel": "1.8.1", "loglevel": "1.8.1",
"loglevel-plugin-prefix": "0.8.4", "loglevel-plugin-prefix": "0.8.4",
"markdown-it": "13.0.1", "markdown-it": "13.0.1",
@ -130,30 +130,30 @@
"markdown-it-replace-link": "1.2.0", "markdown-it-replace-link": "1.2.0",
"nanostores": "0.7.4", "nanostores": "0.7.4",
"prettier": "3.0.3", "prettier": "3.0.3",
"prettier-eslint": "15.0.1", "prettier-eslint": "16.1.2",
"prosemirror-history": "1.3.0", "prosemirror-history": "1.3.0",
"prosemirror-trailing-node": "2.0.3", "prosemirror-trailing-node": "2.0.3",
"prosemirror-view": "1.30.2", "prosemirror-view": "1.30.2",
"rollup": "3.21.6", "rollup": "3.21.6",
"sass": "1.68.0", "sass": "1.69.5",
"solid-js": "1.7.5", "solid-js": "1.8.5",
"solid-popper": "0.3.0", "solid-popper": "0.3.0",
"solid-tiptap": "0.6.0", "solid-tiptap": "0.6.0",
"solid-transition-group": "0.2.2", "solid-transition-group": "0.2.3",
"sort-package-json": "2.6.0", "sort-package-json": "2.6.0",
"stylelint": "15.10.3", "stylelint": "15.11.0",
"stylelint-config-standard-scss": "11.0.0", "stylelint-config-standard-scss": "11.1.0",
"stylelint-order": "6.0.3", "stylelint-order": "6.0.3",
"stylelint-scss": "5.2.1", "stylelint-scss": "5.3.0",
"swiper": "9.4.1", "swiper": "9.4.1",
"typescript": "5.2.2", "typescript": "5.2.2",
"typograf": "7.1.0", "typograf": "7.1.0",
"uniqolor": "1.1.0", "uniqolor": "1.1.0",
"vite": "4.3.9", "vike": "0.4.144",
"vite": "4.5.0",
"vite-plugin-mkcert": "1.16.0", "vite-plugin-mkcert": "1.16.0",
"vite-plugin-sass-dts": "1.3.11", "vite-plugin-sass-dts": "1.3.11",
"vite-plugin-solid": "2.7.0", "vite-plugin-solid": "2.7.2",
"vite-plugin-ssr": "0.4.123",
"y-prosemirror": "1.2.1", "y-prosemirror": "1.2.1",
"yjs": "13.6.0" "yjs": "13.6.0"
}, },

View File

@ -0,0 +1,4 @@
<svg width="25" height="28" viewBox="0 0 25 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M19.5 8.01738L17.4826 6L10.5029 12.9797L7.51738 9.99421L5.5 12.0116L10.5029 17.0145L19.5 8.01738Z" fill="#2BB452"/>
</svg>

After

Width:  |  Height:  |  Size: 271 B

View File

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M6.3459 13.1364H8.69673V22.1618C8.69673 22.34 8.85155 22.4844 9.04263 22.4844H13.0285C13.2196 22.4844 13.3744 22.34 13.3744 22.1618V13.1789H16.0769C16.2526 13.1789 16.4005 13.0559 16.4205 12.8931L16.831 9.57044C16.8423 9.47902 16.8112 9.38747 16.7456 9.31889C16.68 9.25025 16.586 9.21096 16.4874 9.21096H13.3746V7.12812C13.3746 6.50025 13.7371 6.18186 14.4521 6.18186C14.554 6.18186 16.4874 6.18186 16.4874 6.18186C16.6785 6.18186 16.8333 6.03741 16.8333 5.85928V2.80934C16.8333 2.63115 16.6785 2.48676 16.4874 2.48676H13.6825C13.6627 2.48586 13.6188 2.48438 13.554 2.48438C13.0673 2.48438 11.3757 2.57347 10.0394 3.71992C8.55878 4.99038 8.76459 6.51154 8.81378 6.77528V9.21089H6.3459C6.15483 9.21089 6 9.35528 6 9.53347V12.8137C6 12.9919 6.15483 13.1364 6.3459 13.1364Z" fill="#141414"/>
</svg>

After

Width:  |  Height:  |  Size: 901 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="vk logo">
<path id="Vector" d="M14.88 12.4844C14.88 13.054 14.7111 13.6108 14.3946 14.0844C14.0782 14.558 13.6284 14.9272 13.1021 15.1451C12.5759 15.3631 11.9968 15.4202 11.4381 15.309C10.8795 15.1979 10.3663 14.9236 9.96353 14.5208C9.56076 14.1181 9.28646 13.6049 9.17534 13.0462C9.06421 12.4876 9.12125 11.9085 9.33923 11.3822C9.55721 10.856 9.92634 10.4062 10.4 10.0897C10.8736 9.77328 11.4304 9.60438 12 9.60438C12.7636 9.60525 13.4956 9.90896 14.0355 10.4489C14.5754 10.9888 14.8791 11.7208 14.88 12.4844ZM21 8.52437V16.4444C20.9985 17.7806 20.467 19.0617 19.5221 20.0065C18.5773 20.9514 17.2962 21.4829 15.96 21.4844H8.04C6.70377 21.4829 5.42271 20.9514 4.47785 20.0065C3.533 19.0617 3.00151 17.7806 3 16.4444V8.52437C3.00151 7.18815 3.533 5.90708 4.47785 4.96223C5.42271 4.01737 6.70377 3.48589 8.04 3.48438H15.96C17.2962 3.48589 18.5773 4.01737 19.5221 4.96223C20.467 5.90708 20.9985 7.18815 21 8.52437ZM16.32 12.4844C16.32 11.63 16.0666 10.7947 15.5919 10.0843C15.1173 9.37389 14.4426 8.82019 13.6532 8.49322C12.8638 8.16625 11.9952 8.0807 11.1572 8.24738C10.3192 8.41407 9.54946 8.82551 8.9453 9.42967C8.34114 10.0338 7.9297 10.8036 7.76301 11.6416C7.59632 12.4796 7.68187 13.3482 8.00884 14.1376C8.33581 14.9269 8.88952 15.6016 9.59994 16.0763C10.3104 16.551 11.1456 16.8044 12 16.8044C13.1453 16.8031 14.2434 16.3475 15.0533 15.5376C15.8631 14.7278 16.3187 13.6297 16.32 12.4844ZM17.76 7.80438C17.76 7.59077 17.6967 7.38196 17.578 7.20436C17.4593 7.02675 17.2906 6.88833 17.0933 6.80659C16.896 6.72484 16.6788 6.70345 16.4693 6.74513C16.2598 6.7868 16.0674 6.88966 15.9163 7.0407C15.7653 7.19174 15.6624 7.38418 15.6208 7.59368C15.5791 7.80318 15.6005 8.02033 15.6822 8.21767C15.764 8.41502 15.9024 8.58369 16.08 8.70236C16.2576 8.82103 16.4664 8.88438 16.68 8.88438C16.9664 8.88438 17.2411 8.77059 17.4437 8.56805C17.6462 8.36551 17.76 8.09081 17.76 7.80438Z" fill="black"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,7 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="linkedin">
<g id="Group">
<path id="Vector" d="M6.93855 21.4004V9.51472H2.99144V21.4004H6.93896H6.93855ZM4.96582 7.89221C6.34197 7.89221 7.19872 6.97953 7.19872 5.83894C7.17296 4.67236 6.34197 3.78516 4.99199 3.78516C3.64109 3.78516 2.75879 4.67236 2.75879 5.83884C2.75879 6.97943 3.61522 7.89211 4.93996 7.89211H4.96551L4.96582 7.89221ZM9.12333 21.4004H13.0701V14.7636C13.0701 14.4089 13.0959 14.0532 13.2002 13.7998C13.4854 13.0897 14.1348 12.3548 15.2254 12.3548C16.6533 12.3548 17.2248 13.4446 17.2248 15.0426V21.4004H21.1715V14.5855C21.1715 10.9349 19.2246 9.23607 16.6278 9.23607C14.4987 9.23607 13.5637 10.4271 13.0442 11.2383H13.0704V9.51513H9.12353C9.17505 10.6302 9.12322 21.4008 9.12322 21.4008L9.12333 21.4004Z" fill="black"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 859 B

View File

@ -0,0 +1,5 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="telegram logo">
<path id="Icon" d="M2.35326 12.1563L6.96167 13.7367L8.7454 19.0076C8.85953 19.3452 9.3088 19.4699 9.607 19.2459L12.1758 17.3218C12.4451 17.1202 12.8286 17.1101 13.11 17.2978L17.7432 20.3886C18.0622 20.6016 18.5141 20.441 18.5941 20.0869L21.9882 5.08587C22.0756 4.69898 21.6618 4.37623 21.2609 4.51871L2.34786 11.2226C1.88113 11.388 1.88519 11.9952 2.35326 12.1563ZM8.45793 12.8954L17.4645 7.79852C17.6263 7.70719 17.7929 7.90829 17.6539 8.02676L10.2209 14.3753C9.9596 14.5988 9.79107 14.8978 9.74334 15.2224L9.49014 16.9465C9.4566 17.1767 9.10467 17.1996 9.03553 16.9768L8.06173 13.8328C7.9502 13.4742 8.11273 13.0912 8.45793 12.8954Z" fill="#141414"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 784 B

View File

@ -0,0 +1,4 @@
<svg width="20" height="16" viewBox="0 0 20 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M19.5467 1.84651C19.2325 1.98268 18.9095 2.0976 18.5794 2.1909C18.9702 1.75908 19.2681 1.25097 19.45 0.694956C19.4908 0.570325 19.4485 0.433906 19.3437 0.352395C19.239 0.270821 19.0935 0.26105 18.9782 0.327809C18.2772 0.734041 17.521 1.02598 16.728 1.19669C15.9293 0.434032 14.8444 0 13.7228 0C11.3554 0 9.42934 1.88194 9.42934 4.19514C9.42934 4.37733 9.44114 4.55851 9.4645 4.73716C6.52677 4.48513 3.79561 3.07422 1.92014 0.826269C1.8533 0.746145 1.75033 0.702962 1.64491 0.71122C1.53943 0.71929 1.44465 0.777413 1.39136 0.866741C1.01098 1.50452 0.809882 2.23396 0.809882 2.97613C0.809882 3.98698 1.17924 4.94608 1.83169 5.6955C1.6333 5.62836 1.44078 5.54446 1.25704 5.44479C1.1584 5.39114 1.03801 5.39196 0.940011 5.44687C0.841946 5.50178 0.780463 5.60277 0.777882 5.71315C0.77743 5.73175 0.777431 5.75035 0.777431 5.76919C0.777431 7.27806 1.60852 8.63652 2.87917 9.37693C2.77 9.36627 2.66091 9.35083 2.55252 9.33059C2.44078 9.30972 2.32588 9.34799 2.25052 9.43127C2.17504 9.51448 2.15007 9.63047 2.18485 9.73638C2.65517 11.1712 3.86607 12.2265 5.32993 12.5483C4.11581 13.2913 2.72736 13.6806 1.26982 13.6806C0.965688 13.6806 0.659818 13.6631 0.360464 13.6285C0.211755 13.6112 0.0695618 13.697 0.0189168 13.8352C-0.0317281 13.9734 0.0219491 14.1276 0.148465 14.2068C2.02091 15.3799 4.186 16 6.40954 16C10.7808 16 13.5153 13.9859 15.0394 12.2962C16.9401 10.1893 18.0301 7.40061 18.0301 4.64519C18.0301 4.53007 18.0283 4.41383 18.0247 4.29796C18.7746 3.74592 19.4202 3.07782 19.9456 2.30992C20.0254 2.1933 20.0167 2.03916 19.9243 1.93181C19.832 1.82439 19.6781 1.78965 19.5467 1.84651Z" fill="currentColor"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,3 @@
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M19.2129 13.7512C19.8763 14.4052 20.5765 15.0206 21.1715 15.7406C21.4344 16.0605 21.6832 16.3907 21.8736 16.762C22.1433 17.2898 21.899 17.8707 21.4303 17.9022L18.5166 17.9009C17.7652 17.9638 17.1657 17.6584 16.6616 17.1395C16.2582 16.7246 15.8846 16.2831 15.4967 15.8542C15.3377 15.6789 15.1713 15.5139 14.9724 15.3835C14.5747 15.1228 14.2294 15.2026 14.0021 15.6215C13.7706 16.0476 13.7181 16.5193 13.6954 16.9942C13.6642 17.687 13.4568 17.8691 12.7676 17.9008C11.2947 17.9709 9.89691 17.7459 8.59838 16.9957C7.45355 16.3343 6.56579 15.4006 5.79308 14.3435C4.28861 12.2852 3.13649 10.0234 2.10101 7.6983C1.86793 7.17444 2.03838 6.89324 2.6108 6.88329C3.56132 6.86464 4.5117 6.86597 5.46334 6.88196C5.84966 6.88767 6.10541 7.11141 6.25458 7.47993C6.76884 8.75675 7.39809 9.97154 8.18794 11.0975C8.39829 11.3973 8.61277 11.6971 8.9182 11.9081C9.25609 12.1417 9.51335 12.0643 9.6723 11.6842C9.77317 11.4432 9.81733 11.1837 9.84007 10.9256C9.91537 10.0376 9.92529 9.15109 9.79321 8.26621C9.71213 7.71396 9.40407 7.35645 8.85833 7.25194C8.57985 7.19866 8.62131 7.09402 8.75615 6.93352C8.99035 6.65669 9.21061 6.48438 9.6497 6.48438H12.9426C13.461 6.58769 13.5761 6.82284 13.6471 7.34955L13.6499 11.0429C13.6442 11.2468 13.7508 11.8519 14.1145 11.9869C14.4056 12.0829 14.5975 11.8478 14.7721 11.6614C15.5605 10.8165 16.1232 9.81793 16.6259 8.78396C16.849 8.32931 17.0408 7.85714 17.2267 7.38538C17.3644 7.0353 17.5806 6.86305 17.9711 6.87068L21.1403 6.87353C21.2343 6.87353 21.3294 6.87493 21.4204 6.89065C21.9544 6.98255 22.1007 7.21452 21.9358 7.74109C21.6759 8.56725 21.1703 9.25572 20.6759 9.94738C20.1473 10.6858 19.5821 11.399 19.058 12.1418C18.5764 12.8201 18.6147 13.162 19.2129 13.7512Z" fill="#141414"/>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path d="M2.70423 20.2826H18.9296C19.6763 20.2826 20.2817 19.6773 20.2817 18.9305V15.9658C20.2817 15.6139 20.1445 15.2759 19.8992 15.0235L16.5187 11.5449C15.9878 10.9986 15.1106 10.9985 14.5796 11.5447L13.0829 13.084C12.5107 13.6726 11.5502 13.6196 11.0462 12.9716L8.31344 9.45821C7.77885 8.77092 6.74349 8.76071 6.19546 9.43733L1.65353 15.0449C1.45852 15.2857 1.35211 15.5861 1.35211 15.896V18.9305C1.35211 19.6773 1.95747 20.2826 2.70423 20.2826Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5775 2.70423H2.70423V17.5775H17.5775V2.70423ZM0 0V20.2817H20.2817V0H0Z" fill="currentColor"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 5.40845H21.9718V21.9718H5.40845V24H24V5.40845Z" fill="currentColor"/>
<path d="M14.8732 6.08451C14.8732 7.20463 13.9652 8.11268 12.8451 8.11268C11.7249 8.11268 10.8169 7.20463 10.8169 6.08451C10.8169 4.96438 11.7249 4.05634 12.8451 4.05634C13.9652 4.05634 14.8732 4.96438 14.8732 6.08451Z" fill="currentColor"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,8 @@
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Icon">
<path d="M2.70423 20.2826H18.9296C19.6763 20.2826 20.2817 19.6773 20.2817 18.9305V15.9658C20.2817 15.6139 20.1445 15.2759 19.8992 15.0235L16.5187 11.5449C15.9878 10.9986 15.1106 10.9985 14.5796 11.5447L13.0829 13.084C12.5107 13.6726 11.5502 13.6196 11.0462 12.9716L8.31344 9.45821C7.77885 8.77092 6.74349 8.76071 6.19546 9.43733L1.65353 15.0449C1.45852 15.2857 1.35211 15.5861 1.35211 15.896V18.9305C1.35211 19.6773 1.95747 20.2826 2.70423 20.2826Z" fill="#9FA1A7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5775 2.70423H2.70423V17.5775H17.5775V2.70423ZM0 0V20.2817H20.2817V0H0Z" fill="#9FA1A7"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M24 5.40845H21.9718V21.9718H5.40845V24H24V5.40845Z" fill="#9FA1A7"/>
<path d="M14.8732 6.08451C14.8732 7.20463 13.9652 8.11268 12.8451 8.11268C11.7249 8.11268 10.8169 7.20463 10.8169 6.08451C10.8169 4.96438 11.7249 4.05634 12.8451 4.05634C13.9652 4.05634 14.8732 4.96438 14.8732 6.08451Z" fill="#9FA1A7"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -1,5 +1,4 @@
{ {
"...subscribing": "...subscribing",
"About": "About", "About": "About",
"About the project": "About the project", "About the project": "About the project",
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title", "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title",
@ -90,16 +89,19 @@
"Create gallery": "Create gallery", "Create gallery": "Create gallery",
"Create post": "Create post", "Create post": "Create post",
"Create video": "Create video", "Create video": "Create video",
"Culture": "Culture",
"Date of Birth": "Date of Birth", "Date of Birth": "Date of Birth",
"Decline": "Decline", "Decline": "Decline",
"Delete": "Delete", "Delete": "Delete",
"Delete cover": "Delete cover", "Delete cover": "Delete cover",
"Delete userpic": "Delete userpic",
"Description": "Description", "Description": "Description",
"Discours": "Discours", "Discours": "Discours",
"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&#160;an&#160;intellectual environment, a&#160;web space and tools that allows authors to&#160;collaborate with readers and come together to&#160;co-create publications and media projects.<br/><em>We&#160;are convinced that one voice is&#160;good, but many is&#160;better. We&#160;create the most amazing stories together</em>", "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&#160;an&#160;intellectual environment, a&#160;web space and tools that allows authors to&#160;collaborate with readers and come together to&#160;co-create publications and media projects.<br/><em>We&#160;are convinced that one voice is&#160;good, but many is&#160;better. We&#160;create the most amazing stories together</em>",
"Discours is created with our common effort": "Discours exists because of our common effort", "Discours is created with our common effort": "Discours exists because of our common effort",
"Discussing": "Discussing", "Discussing": "Discussing",
"Discussion rules": "Discussion rules", "Discussion rules": "Discussion rules",
"Discussion rules in social networks": "Discussion rules",
"Discussions": "Discussions", "Discussions": "Discussions",
"Dogma": "Dogma", "Dogma": "Dogma",
"Draft successfully deleted": "Draft successfully deleted", "Draft successfully deleted": "Draft successfully deleted",
@ -120,6 +122,7 @@
"Enter your new password": "Enter your new password", "Enter your new password": "Enter your new password",
"Error": "Error", "Error": "Error",
"Everything is ok, please give us your email address": "It's okay, just enter your email address to receive a password reset link.", "Everything is ok, please give us your email address": "It's okay, just enter your email address to receive a password reset link.",
"Experience": "Experience",
"FAQ": "Tips and suggestions", "FAQ": "Tips and suggestions",
"Favorite": "Favorites", "Favorite": "Favorites",
"Favorite topics": "Favorite topics", "Favorite topics": "Favorite topics",
@ -148,6 +151,7 @@
"Help to edit": "Help to edit", "Help to edit": "Help to edit",
"Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.", "Here you can customize your profile the way you want.": "Here you can customize your profile the way you want.",
"Here you can manage all your Discourse subscriptions": "Here you can manage all your Discourse subscriptions", "Here you can manage all your Discourse subscriptions": "Here you can manage all your Discourse subscriptions",
"Here you can upload your photo": "Here you can upload your photo",
"Hide table of contents": "Hide table of contents", "Hide table of contents": "Hide table of contents",
"Highlight": "Highlight", "Highlight": "Highlight",
"Hooray! Welcome!": "Hooray! Welcome!", "Hooray! Welcome!": "Hooray! Welcome!",
@ -168,6 +172,7 @@
"Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society", "Independant magazine with an open horizontal cooperation about culture, science and society": "Independant magazine with an open horizontal cooperation about culture, science and society",
"Insert footnote": "Insert footnote", "Insert footnote": "Insert footnote",
"Insert video link": "Insert video link", "Insert video link": "Insert video link",
"Interview": "Interview",
"Introduce": "Introduction", "Introduce": "Introduction",
"Invalid email": "Check if your email is correct", "Invalid email": "Check if your email is correct",
"Invalid image URL": "Invalid image URL", "Invalid image URL": "Invalid image URL",
@ -182,6 +187,7 @@
"Join the global community of authors!": "Join the global community of authors from all over the world!", "Join the global community of authors!": "Join the global community of authors from all over the world!",
"Just start typing...": "Just start typing...", "Just start typing...": "Just start typing...",
"Knowledge base": "Knowledge base", "Knowledge base": "Knowledge base",
"Language": "Language",
"Last rev.": "Посл. изм.", "Last rev.": "Посл. изм.",
"Let's log in": "Let's log in", "Let's log in": "Let's log in",
"Link copied": "Link copied", "Link copied": "Link copied",
@ -195,6 +201,7 @@
"Manifest": "Manifest", "Manifest": "Manifest",
"Manifesto": "Manifesto", "Manifesto": "Manifesto",
"Many files, choose only one": "Many files, choose only one", "Many files, choose only one": "Many files, choose only one",
"Mark as read": "Mark as read",
"Material card": "Material card", "Material card": "Material card",
"Message": "Message", "Message": "Message",
"More": "More", "More": "More",
@ -227,6 +234,7 @@
"Our regular contributor": "Our regular contributor", "Our regular contributor": "Our regular contributor",
"Paragraphs": "Абзацев", "Paragraphs": "Абзацев",
"Participating": "Participating", "Participating": "Participating",
"Participation": "Participation",
"Partners": "Partners", "Partners": "Partners",
"Password": "Password", "Password": "Password",
"Password again": "Password again", "Password again": "Password again",
@ -245,6 +253,8 @@
"Please enter password": "Please enter a password", "Please enter password": "Please enter a password",
"Please enter password again": "Please enter password again", "Please enter password again": "Please enter password again",
"Please, confirm email": "Please confirm email", "Please, confirm email": "Please confirm email",
"Podcasts": "Podcasts",
"Poetry": "Poetry",
"Popular": "Popular", "Popular": "Popular",
"Popular authors": "Popular authors", "Popular authors": "Popular authors",
"Principles": "Community principles", "Principles": "Community principles",
@ -264,6 +274,7 @@
"Remove link": "Remove link", "Remove link": "Remove link",
"Reply": "Reply", "Reply": "Reply",
"Report": "Complain", "Report": "Complain",
"Reports": "Reports",
"Required": "Required", "Required": "Required",
"Resend code": "Send confirmation", "Resend code": "Send confirmation",
"Restore password": "Restore password", "Restore password": "Restore password",
@ -287,31 +298,36 @@
"Show table of contents": "Show table of contents", "Show table of contents": "Show table of contents",
"Slug": "Slug", "Slug": "Slug",
"Social networks": "Social networks", "Social networks": "Social networks",
"Society": "Society",
"Something went wrong, check email and password": "Something went wrong. Check your email and password", "Something went wrong, check email and password": "Something went wrong. Check your email and password",
"Something went wrong, please try again": "Something went wrong, please try again", "Something went wrong, please try again": "Something went wrong, please try again",
"Song lyrics": "Song lyrics...", "Song lyrics": "Song lyrics...",
"Song title": "Song title", "Song title": "Song title",
"Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one", "Sorry, this address is already taken, please choose another one.": "Sorry, this address is already taken, please choose another one",
"Special Projects": "Special Projects",
"Special projects": "Special projects", "Special projects": "Special projects",
"Specify the source and the name of the author": "Specify the source and the name of the author", "Specify the source and the name of the author": "Specify the source and the name of the author",
"Start conversation": "Start a conversation", "Start conversation": "Start a conversation",
"Subsccriptions": "Subscriptions", "Subsccriptions": "Subscriptions",
"Subscribe": "Subscribe", "Subscribe": "Subscribe",
"Subscribe to the best publications newsletter": "Subscribe to the best publications newsletter",
"Subscribe us": "Subscribe us", "Subscribe us": "Subscribe us",
"Subscribe what you like to tune your personal feed": "Subscribe to topics that interest you to customize your personal feed and get instant updates on new posts and discussions", "Subscribe what you like to tune your personal feed": "Subscribe to topics that interest you to customize your personal feed and get instant updates on new posts and discussions",
"Subscribe who you like to tune your personal feed": "Subscribe to authors you're interested in to customize your personal feed and get instant updates on new posts and discussions", "Subscribe who you like to tune your personal feed": "Subscribe to authors you're interested in to customize your personal feed and get instant updates on new posts and discussions",
"SubscriberWithCount": "{count, plural, =0 {no followers} one {{count} follower} other {{count} followers}", "SubscriberWithCount": "{count, plural, =0 {no followers} one {{count} follower} other {{count} followers}}",
"Subscription": "Subscription", "Subscription": "Subscription",
"SubscriptionWithCount": "{count, plural, =0 {no subscriptions} one {{count} subscription} other {{count} subscriptions}", "SubscriptionWithCount": "{count, plural, =0 {no subscriptions} one {{count} subscription} other {{count} subscriptions}}",
"Subscriptions": "Subscriptions", "Subscriptions": "Subscriptions",
"Substrate": "Substrate", "Substrate": "Substrate",
"Success": "Success", "Success": "Success",
"Successfully authorized": "Authorization successful", "Successfully authorized": "Authorization successful",
"Suggest an idea": "Suggest an idea", "Suggest an idea": "Suggest an idea",
"Support the project": "Support the project",
"Support us": "Help the magazine", "Support us": "Help the magazine",
"Terms of use": "Site rules", "Terms of use": "Site rules",
"Text checking": "Text checking", "Text checking": "Text checking",
"Thank you": "Thank you", "Thank you": "Thank you",
"Theory": "Theory",
"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?", "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?",
"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 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?",
"This comment has not yet been rated": "This comment has not yet been rated", "This comment has not yet been rated": "This comment has not yet been rated",
@ -339,6 +355,7 @@
"Unnamed draft": "Unnamed draft", "Unnamed draft": "Unnamed draft",
"Upload": "Upload", "Upload": "Upload",
"Upload error": "Upload error", "Upload error": "Upload error",
"Upload userpic": "Upload userpic",
"Upload video": "Upload video", "Upload video": "Upload video",
"Uploading image": "Uploading image", "Uploading image": "Uploading image",
"Username": "Username", "Username": "Username",
@ -375,7 +392,6 @@
"You've reached a non-existed page": "You've reached a non-existed page", "You've reached a non-existed page": "You've reached a non-existed page",
"Your email": "Your email", "Your email": "Your email",
"Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses", "Your name will appear on your profile page and as your signature in publications, comments and responses.": "Your name will appear on your profile page and as your signature in publications, comments and responses",
"accomplices": "accomplices",
"actions": "actions", "actions": "actions",
"add link": "add link", "add link": "add link",
"all topics": "all topics", "all topics": "all topics",
@ -399,7 +415,6 @@
"feed": "feed", "feed": "feed",
"follower": "follower", "follower": "follower",
"followersWithCount": "{count} {count, plural, one {follower} other {followers}}", "followersWithCount": "{count} {count, plural, one {follower} other {followers}}",
"general feed": "general tape",
"header 1": "header 1", "header 1": "header 1",
"header 2": "header 2", "header 2": "header 2",
"header 3": "header 3", "header 3": "header 3",
@ -426,6 +441,7 @@
"subscriber": "subscriber", "subscriber": "subscriber",
"subscriber_rp": "subscriber", "subscriber_rp": "subscriber",
"subscribers": "subscribers", "subscribers": "subscribers",
"subscribing...": "subscribing...",
"subscription": "subscription", "subscription": "subscription",
"subscription_rp": "subscription", "subscription_rp": "subscription",
"subscriptions": "subscriptions", "subscriptions": "subscriptions",
@ -435,5 +451,6 @@
"user already exist": "user already exists", "user already exist": "user already exists",
"video": "video", "video": "video",
"view": "view", "view": "view",
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
"yesterday": "yesterday" "yesterday": "yesterday"
} }

View File

@ -1,9 +1,7 @@
{ {
"...subscribing": "...подписываем",
"A short introduction to keep the reader interested": "Добавьте вступление, чтобы заинтересовать читателя", "A short introduction to keep the reader interested": "Добавьте вступление, чтобы заинтересовать читателя",
"About": "О себе", "About": "О себе",
"About the project": "О проекте", "About the project": "О проекте",
"Accomplices": "Соучастники",
"Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Добавьте несколько тем, чтобы читатель знал, о чем ваш материал, и мог найти его на страницах интересных ему тем. Темы можно менять местами, первая тема становится заглавной", "Add a few topics so that the reader knows what your content is about and can find it on pages of topics that interest them. Topics can be swapped, the first topic becomes the title": "Добавьте несколько тем, чтобы читатель знал, о чем ваш материал, и мог найти его на страницах интересных ему тем. Темы можно менять местами, первая тема становится заглавной",
"Add a link or click plus to embed media": "Добавьте ссылку или нажмите плюс для вставки медиа", "Add a link or click plus to embed media": "Добавьте ссылку или нажмите плюс для вставки медиа",
"Add an embed widget": "Добавить embed-виджет", "Add an embed widget": "Добавить embed-виджет",
@ -94,16 +92,19 @@
"Create gallery": "Создать галерею", "Create gallery": "Создать галерею",
"Create post": "Создать публикацию", "Create post": "Создать публикацию",
"Create video": "Создать видео", "Create video": "Создать видео",
"Culture": "Культура",
"Date of Birth": "Дата рождения", "Date of Birth": "Дата рождения",
"Decline": "Отмена", "Decline": "Отмена",
"Delete": "Удалить", "Delete": "Удалить",
"Delete cover": "Удалить обложку", "Delete cover": "Удалить обложку",
"Delete userpic": "Удалить аватар",
"Description": "Описание", "Description": "Описание",
"Discours": "Дискурс", "Discours": "Дискурс",
"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": "Дискурс&#160;&#8212; это интеллектуальная среда, веб-пространство и&#160;инструменты, которые позволяют авторам сотрудничать&#160;с&#160;читателями и&#160;объединяться для совместного создания публикаций и&#160;медиапроектов.<br/>Мы&#160;убеждены, один голос хорошо, а&#160;много&#160;&#8212; лучше. Самые потрясающиe истории мы создаём вместе.", "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": "Дискурс&#160;&#8212; это интеллектуальная среда, веб-пространство и&#160;инструменты, которые позволяют авторам сотрудничать&#160;с&#160;читателями и&#160;объединяться для совместного создания публикаций и&#160;медиапроектов.<br/>Мы&#160;убеждены, один голос хорошо, а&#160;много&#160;&#8212; лучше. Самые потрясающиe истории мы создаём вместе.",
"Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу", "Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу",
"Discussing": "Обсуждаемое", "Discussing": "Обсуждаемое",
"Discussion rules": "Правила сообществ самиздата в&nbsp;соцсетях", "Discussion rules": "Правила дискуссий",
"Discussion rules in social networks": "Правила сообществ самиздата в&nbsp;соцсетях",
"Discussions": "Дискуссии", "Discussions": "Дискуссии",
"Dogma": "Догма", "Dogma": "Догма",
"Draft successfully deleted": "Черновик успешно удален", "Draft successfully deleted": "Черновик успешно удален",
@ -125,6 +126,7 @@
"Enter your new password": "Введите новый пароль", "Enter your new password": "Введите новый пароль",
"Error": "Ошибка", "Error": "Ошибка",
"Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.", "Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.",
"Experience": "Личный опыт",
"FAQ": "Советы и предложения", "FAQ": "Советы и предложения",
"Favorite": "Избранное", "Favorite": "Избранное",
"Favorite topics": "Избранные темы", "Favorite topics": "Избранные темы",
@ -156,6 +158,7 @@
"Help to edit": "Помочь редактировать", "Help to edit": "Помочь редактировать",
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.", "Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
"Here you can manage all your Discourse subscriptions": "Здесь можно управлять всеми своими подписками на Дискурсе", "Here you can manage all your Discourse subscriptions": "Здесь можно управлять всеми своими подписками на Дискурсе",
"Here you can upload your photo": "Здесь вы можете загрузить свою фотографию",
"Hide table of contents": "Скрыть главление", "Hide table of contents": "Скрыть главление",
"Highlight": "Подсветка", "Highlight": "Подсветка",
"Hooray! Welcome!": "Ура! Добро пожаловать!", "Hooray! Welcome!": "Ура! Добро пожаловать!",
@ -176,6 +179,7 @@
"Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе", "Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе",
"Insert footnote": "Вставить сноску", "Insert footnote": "Вставить сноску",
"Insert video link": "Вставить ссылку на видео", "Insert video link": "Вставить ссылку на видео",
"Interview": "Интервью",
"Introduce": "Представление", "Introduce": "Представление",
"Invalid email": "Проверьте правильность ввода почты", "Invalid email": "Проверьте правильность ввода почты",
"Invalid image URL": "Некорректная ссылка на изображение", "Invalid image URL": "Некорректная ссылка на изображение",
@ -192,6 +196,7 @@
"Just start typing...": "Просто начните печатать...", "Just start typing...": "Просто начните печатать...",
"Karma": "Карма", "Karma": "Карма",
"Knowledge base": "База знаний", "Knowledge base": "База знаний",
"Language": "Язык",
"Last rev.": "Посл. изм.", "Last rev.": "Посл. изм.",
"Let's log in": "Давайте авторизуемся", "Let's log in": "Давайте авторизуемся",
"Link copied": "Ссылка скопирована", "Link copied": "Ссылка скопирована",
@ -205,6 +210,7 @@
"Manifest": "Манифест", "Manifest": "Манифест",
"Manifesto": "Манифест", "Manifesto": "Манифест",
"Many files, choose only one": "Много файлов, выберете один", "Many files, choose only one": "Много файлов, выберете один",
"Mark as read": "Отметить прочитанным",
"Material card": "Карточка материала", "Material card": "Карточка материала",
"Message": "Написать", "Message": "Написать",
"More": "Ещё", "More": "Ещё",
@ -238,6 +244,7 @@
"Our regular contributor": "Наш постоянный автор", "Our regular contributor": "Наш постоянный автор",
"Paragraphs": "Абзацев", "Paragraphs": "Абзацев",
"Participating": "Участвовать", "Participating": "Участвовать",
"Participation": "Соучастие",
"Partners": "Партнёры", "Partners": "Партнёры",
"Password": "Пароль", "Password": "Пароль",
"Password again": "Пароль ещё раз", "Password again": "Пароль ещё раз",
@ -256,6 +263,8 @@
"Please enter password": "Пожалуйста, введите пароль", "Please enter password": "Пожалуйста, введите пароль",
"Please enter password again": "Пожалуйста, введите пароль ещё рез", "Please enter password again": "Пожалуйста, введите пароль ещё рез",
"Please, confirm email": "Пожалуйста, подтвердите электронную почту", "Please, confirm email": "Пожалуйста, подтвердите электронную почту",
"Podcasts": "Подкасты",
"Poetry": "Поэзия",
"Popular": "Популярное", "Popular": "Популярное",
"Popular authors": "Популярные авторы", "Popular authors": "Популярные авторы",
"Preview": "Предпросмотр", "Preview": "Предпросмотр",
@ -280,6 +289,7 @@
"Remove link": "Убрать ссылку", "Remove link": "Убрать ссылку",
"Reply": "Ответить", "Reply": "Ответить",
"Report": "Пожаловаться", "Report": "Пожаловаться",
"Reports": "Репортажи",
"Required": "Поле обязательно для заполнения", "Required": "Поле обязательно для заполнения",
"Resend code": "Выслать подтверждение", "Resend code": "Выслать подтверждение",
"Restore password": "Восстановить пароль", "Restore password": "Восстановить пароль",
@ -305,17 +315,20 @@
"Show table of contents": "Показать главление", "Show table of contents": "Показать главление",
"Slug": "Постоянная ссылка", "Slug": "Постоянная ссылка",
"Social networks": "Социальные сети", "Social networks": "Социальные сети",
"Society": "Общество",
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль", "Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз", "Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
"Song lyrics": "Текст песни...", "Song lyrics": "Текст песни...",
"Song title": "Название песни", "Song title": "Название песни",
"Sorry, this address is already taken, please choose another one.": "Увы, этот адрес уже занят, выберите другой", "Sorry, this address is already taken, please choose another one.": "Увы, этот адрес уже занят, выберите другой",
"Special Projects": "Спецпроекты",
"Special projects": "Спецпроекты", "Special projects": "Спецпроекты",
"Specify the source and the name of the author": "Укажите источник и имя автора", "Specify the source and the name of the author": "Укажите источник и имя автора",
"Start conversation": "Начать беседу", "Start conversation": "Начать беседу",
"Subheader": "Подзаголовок", "Subheader": "Подзаголовок",
"Subscribe": "Подписаться", "Subscribe": "Подписаться",
"Subscribe to comments": "Подписаться на комментарии", "Subscribe to comments": "Подписаться на комментарии",
"Subscribe to the best publications newsletter": "Подпишитесь на рассылку лучших публикаций",
"Subscribe us": "Подпишитесь на&nbsp;нас", "Subscribe us": "Подпишитесь на&nbsp;нас",
"Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях", "Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
"Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях", "Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
@ -327,10 +340,12 @@
"Success": "Успешно", "Success": "Успешно",
"Successfully authorized": "Авторизация успешна", "Successfully authorized": "Авторизация успешна",
"Suggest an idea": "Предложить идею", "Suggest an idea": "Предложить идею",
"Support the project": "Поддержать проект",
"Support us": "Помочь журналу", "Support us": "Помочь журналу",
"Terms of use": "Правила сайта", "Terms of use": "Правила сайта",
"Text checking": "Проверка текста", "Text checking": "Проверка текста",
"Thank you": "Благодарности", "Thank you": "Благодарности",
"Theory": "Теории",
"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?": "В настройках вашего профиля есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"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?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил", "This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
@ -358,6 +373,7 @@
"Unnamed draft": "Черновик без названия", "Unnamed draft": "Черновик без названия",
"Upload": "Загрузить", "Upload": "Загрузить",
"Upload error": "Ошибка загрузки", "Upload error": "Ошибка загрузки",
"Upload userpic": "Загрузить аватар",
"Upload video": "Загрузить видео", "Upload video": "Загрузить видео",
"Uploading image": "Загружаем изображение", "Uploading image": "Загружаем изображение",
"Username": "Имя пользователя", "Username": "Имя пользователя",
@ -422,7 +438,6 @@
"feed": "лента", "feed": "лента",
"follower": "подписчик", "follower": "подписчик",
"followersWithCount": "{count} {count, plural, one {подписчик} few {подписчика} other {подписчиков}}", "followersWithCount": "{count} {count, plural, one {подписчик} few {подписчика} other {подписчиков}}",
"general feed": "Общая лента",
"header 1": "заголовок 1", "header 1": "заголовок 1",
"header 2": "заголовок 2", "header 2": "заголовок 2",
"header 3": "заголовок 3", "header 3": "заголовок 3",
@ -453,11 +468,13 @@
"subscriber": "подписчик", "subscriber": "подписчик",
"subscriber_rp": "подписчика", "subscriber_rp": "подписчика",
"subscribers": "подписчиков", "subscribers": "подписчиков",
"subscribing...": "Подписка...",
"terms of use": "правилами пользования сайтом", "terms of use": "правилами пользования сайтом",
"today": "сегодня", "today": "сегодня",
"topics": "темы", "topics": "темы",
"user already exist": "пользователь уже существует", "user already exist": "пользователь уже существует",
"video": "видео", "video": "видео",
"view": "просмотр", "view": "просмотр",
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
"yesterday": "вчера" "yesterday": "вчера"
} }

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;
} }
@ -34,10 +36,12 @@ img {
img { img {
display: block; display: block;
margin-bottom: 0.5em; margin-bottom: 0.5em;
cursor: zoom-in;
} }
blockquote, blockquote,
blockquote[data-type='punchline'] { blockquote[data-type='punchline'] {
clear: both;
font-size: 2.6rem; font-size: 2.6rem;
font-weight: bold; font-weight: bold;
line-height: 1.4; line-height: 1.4;
@ -61,6 +65,7 @@ img {
ta-quotation { ta-quotation {
border: solid #000; border: solid #000;
border-width: 0 0 0 2px; border-width: 0 0 0 2px;
clear: both;
display: block; display: block;
font-weight: 500; font-weight: 500;
line-height: 1.6; line-height: 1.6;
@ -71,6 +76,10 @@ img {
&[data-float='right'] { &[data-float='right'] {
@include font-size(2.2rem); @include font-size(2.2rem);
line-height: 1.4; line-height: 1.4;
@include media-breakpoint-up(sm) {
clear: none;
}
} }
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
@ -92,9 +101,11 @@ img {
ta-sub, ta-sub,
ta-selection-frame, ta-selection-frame,
ta-border-sub { ta-border-sub {
background: #f1f2f3;
display: block;
@include font-size(1.4rem); @include font-size(1.4rem);
background: #f1f2f3;
clear: both;
display: block;
margin: 3.2rem 0; margin: 3.2rem 0;
padding: 3.2rem; padding: 3.2rem;
@ -173,15 +184,17 @@ img {
:global(.img-align-left) { :global(.img-align-left) {
float: left; float: left;
margin: 1em 8.3333% 1.5em 0; margin: 0 8.3333% 1.5em 0;
} }
:global(.width-30) { @include media-breakpoint-up(sm) {
width: 30%; :global(.width-30) {
} width: 30%;
}
:global(.width-50) { :global(.width-50) {
width: 50%; width: 50%;
}
} }
:global(.img-align-left.width-50) { :global(.img-align-left.width-50) {
@ -191,13 +204,15 @@ img {
} }
:global(.img-align-right) { :global(.img-align-right) {
float: right; @include media-breakpoint-up(sm) {
margin: 1em 0 1.5em 8.3333%; float: right;
margin: 1em 0 1.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.6666%; margin-right: -8.3333%;
} }
} }
@ -240,7 +255,6 @@ img {
.shoutAuthorsList { .shoutAuthorsList {
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8;
margin: 2em 0; margin: 2em 0;
padding-bottom: 2em;
h4 { h4 {
color: #696969; color: #696969;
@ -296,22 +310,24 @@ img {
display: flex; display: flex;
justify-content: flex-start; justify-content: flex-start;
padding: 3rem 0 0; padding: 3rem 0 0;
position: relative;
@include media-breakpoint-down(sm) { @include media-breakpoint-down(lg) {
flex-wrap: wrap; flex-wrap: wrap;
} }
} }
.shoutStatsItem { .shoutStatsItem {
@include font-size(1.5rem); @include font-size(1.5rem);
align-items: center; align-items: center;
font-weight: 500; font-weight: 500;
display: flex; display: flex;
margin: 0 6% 1em 0; margin: 0 2rem 1em 0;
vertical-align: baseline; vertical-align: baseline;
cursor: pointer; cursor: pointer;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(xl) {
margin-right: 3.2rem; margin-right: 3.2rem;
} }
@ -355,6 +371,14 @@ img {
} }
} }
.shoutStatsItemBookmarks {
margin-left: auto;
@include media-breakpoint-up(lg) {
margin-left: 0;
}
}
.shoutStatsItemInner { .shoutStatsItemInner {
cursor: pointer; cursor: pointer;
@ -378,31 +402,71 @@ img {
.shoutStatsItemAdditionalData { .shoutStatsItemAdditionalData {
color: rgb(0 0 0 / 40%); color: rgb(0 0 0 / 40%);
cursor: default;
font-weight: normal; font-weight: normal;
justify-self: flex-end; justify-self: flex-end;
margin-right: 0;
margin-left: auto;
white-space: nowrap; white-space: nowrap;
cursor: default;
.icon { .icon {
opacity: 0.4; opacity: 0.4;
height: 2rem; height: 2rem;
} }
@include media-breakpoint-down(sm) { @include media-breakpoint-down(lg) {
flex: 1 100%; flex: 1 100%;
order: 9;
.shoutStatsItemAdditionalDataItem {
margin-left: 0;
}
} }
} }
.shoutStatsItemViews { .shoutStatsItemViews {
color: rgb(0 0 0 / 0.4);
cursor: default; cursor: default;
font-weight: normal;
margin-left: auto;
white-space: nowrap;
@include media-breakpoint-down(lg) {
bottom: 0;
flex: 1 40%;
justify-content: end;
margin-right: 0;
order: 10;
position: absolute;
right: 0;
.icon {
display: none !important;
}
}
}
.shoutStatsItemLabel {
font-weight: normal;
margin-left: 0.3em;
}
.commentsTextLabel {
display: none;
@include media-breakpoint-up(sm) {
display: block;
}
}
.shoutStatsItemCount {
@include media-breakpoint-down(lg) {
display: none;
}
} }
.shoutStatsItemAdditionalDataItem { .shoutStatsItemAdditionalDataItem {
font-weight: normal; font-weight: normal;
display: inline-block; display: inline-block;
margin-left: 2rem; //margin-left: 2rem;
margin-right: 0; margin-right: 0;
margin-bottom: 0; margin-bottom: 0;
cursor: default; cursor: default;
@ -416,6 +480,7 @@ img {
.topicsList { .topicsList {
@include font-size(1.2rem); @include font-size(1.2rem);
border-bottom: 1px solid #e8e8e8; border-bottom: 1px solid #e8e8e8;
letter-spacing: 0.08em; letter-spacing: 0.08em;
margin-top: 1.6rem; margin-top: 1.6rem;
@ -455,12 +520,15 @@ img {
} }
.commentsHeaderWrapper { .commentsHeaderWrapper {
display: flex; @include media-breakpoint-up(sm) {
justify-content: space-between; display: flex;
justify-content: space-between;
}
} }
.commentsHeader { .commentsHeader {
@include font-size(2.4rem); @include font-size(2.4rem);
margin-bottom: 1em; margin-bottom: 1em;
.newReactions { .newReactions {
@ -494,6 +562,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;
@ -575,13 +644,14 @@ a[data-toggle='tooltip'] {
width: 0; width: 0;
height: 0; height: 0;
border-style: solid; border-style: solid;
border-width: 4px 4px 0 4px; border-width: 4px 4px 0;
border-color: var(--black-500) transparent transparent transparent; border-color: var(--black-500) transparent transparent transparent;
} }
} }
.lead { .lead {
@include font-size(1.8rem); @include font-size(1.8rem);
font-weight: 600; font-weight: 600;
b, b,
@ -589,3 +659,19 @@ a[data-toggle='tooltip'] {
font-weight: 700; font-weight: 700;
} }
} }
.articlePopupOpener {
.iconHover {
display: none;
}
&:hover {
.icon {
display: none;
}
.iconHover {
display: inline-block;
}
}
}

View File

@ -1,11 +1,11 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AudioHeader.module.scss' import styles from './AudioHeader.module.scss'
import { imageProxy } from '../../../utils/imageProxy'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { createSignal, Show } from 'solid-js' import { createSignal, Show } from 'solid-js'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Topic } from '../../../graphql/types.gen' import { Topic } from '../../../graphql/types.gen'
import { CardTopic } from '../../Feed/CardTopic' import { CardTopic } from '../../Feed/CardTopic'
import { Image } from '../../_shared/Image'
type Props = { type Props = {
title: string title: string
@ -19,7 +19,7 @@ export const AudioHeader = (props: Props) => {
return ( return (
<div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}> <div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}>
<div class={styles.cover}> <div class={styles.cover}>
<img class={styles.image} src={imageProxy(props.cover)} alt={props.title} /> <Image class={styles.image} src={props.cover} alt={props.title} width={200} />
<Show when={props.cover}> <Show when={props.cover}>
<button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}> <button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}>
<Icon name="expand-circle" /> <Icon name="expand-circle" />

View File

@ -51,7 +51,6 @@
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: #141414; background: #141414;
@ -108,7 +107,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
cursor: pointer; cursor: pointer;
border-bottom: 2px solid #cccccc; border-bottom: 2px solid #ccc;
} }
.progressFilled { .progressFilled {
@ -126,7 +125,6 @@
position: absolute; position: absolute;
bottom: -10px; bottom: -10px;
right: -8px; right: -8px;
width: 8px; width: 8px;
height: 8px; height: 8px;
border-radius: 50%; border-radius: 50%;
@ -140,7 +138,6 @@
padding-top: 14px; padding-top: 14px;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
font-weight: 500; font-weight: 500;
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
@ -157,7 +154,7 @@ $vendors-track: ('::-webkit-slider-runnable-track', '::-moz-range-track', '::-ms
$vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thumb'); $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thumb');
.volume { .volume {
-webkit-appearance: none; appearance: none;
height: 19px; height: 19px;
float: left; float: left;
outline: none; outline: none;
@ -182,7 +179,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
@each $vendor in $vendors-thumb { @each $vendor in $vendors-thumb {
&#{$vendor} { &#{$vendor} {
position: relative; position: relative;
-webkit-appearance: none; appearance: none;
box-sizing: content-box; box-sizing: content-box;
width: 8px; width: 8px;
height: 8px; height: 8px;
@ -190,7 +187,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
border: 4px solid var(--default-color); border: 4px solid var(--default-color);
background-color: var(--background-color); background-color: var(--background-color);
cursor: pointer; cursor: pointer;
margin: -7px 0 0 0; margin: -7px 0 0;
} }
&:active#{$vendor} { &:active#{$vendor} {
transform: scale(1.2); transform: scale(1.2);
@ -201,6 +198,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
&::-moz-range-progress { &::-moz-range-progress {
background-color: var(--background-color); background-color: var(--background-color);
} }
&::-moz-focus-outer { &::-moz-focus-outer {
border: 0; border: 0;
} }
@ -209,7 +207,6 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
.playlist { .playlist {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
list-style-type: none; list-style-type: none;
margin: 32px 0 16px; margin: 32px 0 16px;
padding: 0; padding: 0;
@ -222,7 +219,6 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
.playlistItem { .playlistItem {
display: flex; display: flex;
align-items: center; align-items: center;
min-height: 56px; min-height: 56px;
padding: 16px 0; padding: 16px 0;
} }
@ -319,6 +315,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
&:not([disabled]):hover { &:not([disabled]):hover {
border-color: var(--background-color-invert); border-color: var(--background-color-invert);
background: var(--background-color-invert); background: var(--background-color-invert);
img { img {
filter: var(--icon-filter-hover); filter: var(--icon-filter-hover);
} }
@ -334,7 +331,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 16px; gap: 16px;
padding: 8px 0 24px 0; padding: 8px 0 24px;
.description, .description,
.lyrics { .lyrics {

View File

@ -3,7 +3,6 @@ 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' import { MediaItem } from '../../../pages/types'
import { imageProxy } from '../../../utils/imageProxy'
type Props = { type Props = {
media: MediaItem[] media: MediaItem[]
@ -145,8 +144,7 @@ export const AudioPlayer = (props: Props) => {
<audio <audio
ref={(el) => (audioRef.current = el)} ref={(el) => (audioRef.current = el)}
onTimeUpdate={handleAudioTimeUpdate} onTimeUpdate={handleAudioTimeUpdate}
// TEMP SOLUTION for http/https src={currentTack().url}
src={currentTack().url.startsWith('https') ? currentTack().url : imageProxy(currentTack().url)}
onCanPlay={() => { onCanPlay={() => {
// start to play the next track on src change // start to play the next track on src change
if (isPlaying()) { if (isPlaying()) {

View File

@ -1,24 +1,27 @@
.comment { .comment {
margin: 0 0 0.5em; margin: 0 0 0.5em;
padding: 1rem; padding: 0 1rem;
transition: background-color 0.3s; transition: background-color 0.3s;
position: relative; position: relative;
list-style: none; list-style: none;
background: rgb(0 0 0 / 0.1);
@include media-breakpoint-down(sm) {
padding-right: 0;
}
&.isNew { &.isNew {
border-radius: 6px; border-radius: 6px;
background: rgb(38 56 217 / 5%); background: rgb(38 56 217 / 5%);
} }
@include media-breakpoint-down(sm) {
margin-right: -1.2rem;
}
.comment { .comment {
margin-right: -1rem;
&::before, &::before,
&::after { &::after {
content: ''; content: '';
left: 0; left: -14px;
position: absolute; position: absolute;
} }
@ -26,9 +29,9 @@
border-bottom: 2px solid #ccc; border-bottom: 2px solid #ccc;
border-left: 2px solid #ccc; border-left: 2px solid #ccc;
border-radius: 0 0 0 1.2rem; border-radius: 0 0 0 1.2rem;
top: -1rem; top: -24px;
height: 2.4rem; height: 50px;
width: 1.2rem; width: 12px;
} }
&::after { &::after {
@ -57,24 +60,29 @@
align-items: center; align-items: center;
margin-bottom: 1.4rem; margin-bottom: 1.4rem;
} }
.commentControl:not(.commentControlReply) {
opacity: 0;
}
} }
.commentContent { .commentContent {
padding: 0 1rem 1rem 0;
&:hover { &:hover {
.commentControlReply, .commentControlReply,
.commentControlShare, .commentControlShare,
.commentControlDelete, .commentControlDelete,
.commentControlEdit, .commentControlEdit,
.commentControlComplain { .commentControlComplain,
.commentControl {
opacity: 1; opacity: 1;
} }
} }
}
.commentControls { p:last-child {
@include font-size(1.2rem); margin-bottom: 0;
}
margin-bottom: 0.5em;
} }
.commentControlReply, .commentControlReply,
@ -104,7 +112,7 @@
.commentControl { .commentControl {
border: none; border: none;
color: #696969; color: var(--secondary-color);
cursor: pointer; cursor: pointer;
display: inline-flex; display: inline-flex;
line-height: 1.2; line-height: 1.2;
@ -117,8 +125,8 @@
vertical-align: top; vertical-align: top;
&:hover { &:hover {
background: #000; background: var(--background-color-invert);
color: #fff; color: var(--default-color-invert);
.icon { .icon {
filter: invert(1); filter: invert(1);
@ -173,9 +181,10 @@
} }
.articleAuthor { .articleAuthor {
color: #2638d9; @include font-size(1.2rem);
font-size: 12px;
margin-right: 12px; color: var(--blue-500);
margin: 0.3rem 1rem 0;
} }
.articleLink { .articleLink {
@ -203,6 +212,10 @@
margin-right: 1em; margin-right: 1em;
vertical-align: middle; vertical-align: middle;
width: 1em; width: 1em;
@include media-breakpoint-up(md) {
margin-left: 1em;
}
} }
.commentDates { .commentDates {

View File

@ -2,26 +2,26 @@ import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import MD from './MD' import MD from '../MD'
import { AuthorCard } from '../Author/AuthorCard' import { Userpic } from '../../Author/Userpic'
import { Userpic } from '../Author/Userpic' import { CommentRatingControl } from '../CommentRatingControl'
import { CommentRatingControl } from './CommentRatingControl' import { CommentDate } from '../CommentDate'
import { CommentDate } from './CommentDate' import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated' import { Icon } from '../../_shared/Icon'
import { Icon } from '../_shared/Icon'
import { useSession } from '../../context/session' 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 { useSnackbar } from '../../context/snackbar' 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'
import { AuthorLink } from '../../Author/AhtorLink'
const SimplifiedEditor = lazy(() => import('../Editor/SimplifiedEditor')) const SimplifiedEditor = lazy(() => import('../../Editor/SimplifiedEditor'))
type Props = { type Props = {
comment: Reaction comment: Reaction
@ -135,7 +135,6 @@ export const Comment = (props: Props) => {
<Userpic <Userpic
name={comment().createdBy.name} name={comment().createdBy.name}
userpic={comment().createdBy.userpic} userpic={comment().createdBy.userpic}
isBig={false}
class={clsx({ class={clsx({
[styles.compactUserpic]: props.compact [styles.compactUserpic]: props.compact
})} })}
@ -148,13 +147,7 @@ export const Comment = (props: Props) => {
> >
<div class={styles.commentDetails}> <div class={styles.commentDetails}>
<div class={styles.commentAuthor}> <div class={styles.commentAuthor}>
<AuthorCard <AuthorLink author={comment()?.createdBy as Author} />
author={comment()?.createdBy as Author}
hideDescription={true}
hideFollow={true}
isComments={true}
hasLink={true}
/>
</div> </div>
<Show when={props.isArticleAuthor}> <Show when={props.isArticleAuthor}>
@ -173,9 +166,7 @@ export const Comment = (props: Props) => {
</a> </a>
</div> </div>
</Show> </Show>
<CommentDate showOnHover={true} comment={comment()} isShort={true} />
<CommentDate comment={comment()} isShort={true} />
<CommentRatingControl comment={comment()} /> <CommentRatingControl comment={comment()} />
</div> </div>
</Show> </Show>
@ -190,6 +181,7 @@ export const Comment = (props: Props) => {
placeholder={t('Write a comment...')} placeholder={t('Write a comment...')}
onSubmit={(value) => handleUpdate(value)} onSubmit={(value) => handleUpdate(value)}
submitByCtrlEnter={true} submitByCtrlEnter={true}
onCancel={() => setEditMode(false)}
setClear={clearEditor()} setClear={clearEditor()}
/> />
</Suspense> </Suspense>
@ -197,7 +189,7 @@ export const Comment = (props: Props) => {
</div> </div>
<Show when={!props.compact}> <Show when={!props.compact}>
<div class={styles.commentControls}> <div>
<ShowIfAuthenticated> <ShowIfAuthenticated>
<button <button
disabled={loading()} disabled={loading()}

View File

@ -0,0 +1 @@
export { Comment } from './Comment'

View File

@ -1,18 +1,21 @@
.commentDates { .commentDates {
color: #9fa1a7; @include font-size(1.2rem);
color: var(--secondary-color);
align-items: center; align-items: center;
align-self: center; align-self: center;
display: flex; display: flex;
flex: 1; flex: 1;
flex-wrap: wrap; flex-wrap: wrap;
@include font-size(1.2rem);
font-size: 1.2rem; font-size: 1.2rem;
justify-content: flex-start; justify-content: flex-start;
margin: 0 1em 0 0; margin: 0 1rem;
height: 1.6rem;
.date { .date {
font-weight: 500; font-weight: 500;
margin-right: 1rem; margin-right: 1rem;
position: relative;
.icon { .icon {
line-height: 1; line-height: 1;
@ -23,6 +26,26 @@
vertical-align: middle; vertical-align: middle;
} }
} }
&.showOnHover {
.text {
position: absolute;
left: 1.5rem;
top: 0.2rem;
opacity: 0;
white-space: nowrap;
transition: opacity 0.3s ease-in-out;
}
.icon {
cursor: pointer;
&:hover + .text {
opacity: 1;
left: 1.5rem;
}
}
}
} }
.commentDatesLastInRow { .commentDatesLastInRow {

View File

@ -1,7 +1,7 @@
import { Show } from 'solid-js' import { Show } from 'solid-js'
import { Icon } from '../_shared/Icon' 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 { useLocalize } from '../../../context/localize'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './CommentDate.module.scss' import styles from './CommentDate.module.scss'
@ -9,6 +9,7 @@ type Props = {
comment: Reaction comment: Reaction
isShort?: boolean isShort?: boolean
isLastInRow?: boolean isLastInRow?: boolean
showOnHover?: boolean
} }
export const CommentDate = (props: Props) => { export const CommentDate = (props: Props) => {
@ -23,12 +24,19 @@ export const CommentDate = (props: Props) => {
} }
return ( return (
<div class={clsx(styles.commentDates, { [styles.commentDatesLastInRow]: props.isLastInRow })}> <div
class={clsx(styles.commentDates, {
[styles.commentDatesLastInRow]: props.isLastInRow,
[styles.showOnHover]: props.showOnHover
})}
>
<time class={styles.date}>{formattedDate(props.comment.createdAt)}</time> <time class={styles.date}>{formattedDate(props.comment.createdAt)}</time>
<Show when={props.comment.updatedAt}> <Show when={props.comment.updatedAt}>
<time class={styles.date}> <time class={styles.date}>
<Icon name="edit" class={styles.icon} /> <Icon name="edit" class={styles.icon} />
{t('Edited')} {formattedDate(props.comment.updatedAt)} <span class={styles.text}>
{t('Edited')} {formattedDate(props.comment.updatedAt)}
</span>
</time> </time>
</Show> </Show>
</div> </div>

View File

@ -0,0 +1 @@
export { CommentDate } from './CommentDate'

View File

@ -10,11 +10,9 @@ import { useReactions } from '../../context/reactions'
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 { getDescription } from '../../utils/meta' import { getDescription } from '../../utils/meta'
import { imageProxy } from '../../utils/imageProxy'
import { AuthorCard } from '../Author/AuthorCard'
import { TableOfContents } from '../TableOfContents' import { TableOfContents } from '../TableOfContents'
import { AudioPlayer } from './AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import { SharePopup } from './SharePopup' import { getShareUrl, SharePopup } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl' import { ShoutRatingControl } from './ShoutRatingControl'
import { CommentsTree } from './CommentsTree' import { CommentsTree } from './CommentsTree'
import stylesHeader from '../Nav/Header/Header.module.scss' import stylesHeader from '../Nav/Header/Header.module.scss'
@ -22,10 +20,14 @@ import { AudioHeader } from './AudioHeader'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { VideoPlayer } from '../_shared/VideoPlayer' import { VideoPlayer } from '../_shared/VideoPlayer'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { SolidSwiper } from '../_shared/SolidSwiper' import { ImageSwiper } from '../_shared/SolidSwiper'
import styles from './Article.module.scss' import styles from './Article.module.scss'
import { CardTopic } from '../Feed/CardTopic' import { CardTopic } from '../Feed/CardTopic'
import { createPopper } from '@popperjs/core' 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'
type Props = { type Props = {
article: Shout article: Shout
@ -48,6 +50,8 @@ const scrollTo = (el: HTMLElement) => {
} }
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const [selectedImage, setSelectedImage] = createSignal('')
const { t, formatDate } = useLocalize() const { t, formatDate } = useLocalize()
const { const {
user, user,
@ -168,7 +172,7 @@ export const FullArticle = (props: Props) => {
document.body.appendChild(tooltip) document.body.appendChild(tooltip)
if (element.hasAttribute('href')) { if (element.hasAttribute('href')) {
element.setAttribute('href', 'javascript: void(0);') element.setAttribute('href', 'javascript: void(0)')
} }
const popperInstance = createPopper(element, tooltip, { const popperInstance = createPopper(element, tooltip, {
@ -229,6 +233,20 @@ export const FullArticle = (props: Props) => {
}) })
}) })
const openLightbox = (image) => {
setSelectedImage(image)
}
const handleLightboxClose = () => {
setSelectedImage()
}
const handleArticleBodyClick = (event) => {
if (event.target.tagName === 'IMG') {
const src = event.target.src
openLightbox(getImageUrl(src))
}
}
return ( return (
<> <>
<Title>{props.article.title}</Title> <Title>{props.article.title}</Title>
@ -266,7 +284,9 @@ export const FullArticle = (props: Props) => {
> >
<div <div
class={styles.shoutCover} class={styles.shoutCover}
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }} style={{
'background-image': `url('${getImageUrl(props.article.cover, { width: 1600 })}')`
}}
/> />
</Show> </Show>
</div> </div>
@ -308,7 +328,7 @@ export const FullArticle = (props: Props) => {
</Show> </Show>
<Show when={body()}> <Show when={body()}>
<div id="shoutBody" class={styles.shoutBody}> <div id="shoutBody" class={styles.shoutBody} onClick={handleArticleBodyClick}>
<Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}> <Show when={!body().startsWith('<')} fallback={<div innerHTML={body()} />}>
<MD body={body()} /> <MD body={body()} />
</Show> </Show>
@ -329,7 +349,7 @@ export const FullArticle = (props: Props) => {
<div class="wide-container"> <div class="wide-container">
<div class="row"> <div class="row">
<div class="col-md-20 offset-md-2"> <div class="col-md-20 offset-md-2">
<SolidSwiper images={media()} /> <ImageSwiper images={media()} />
</div> </div>
</div> </div>
</div> </div>
@ -346,22 +366,46 @@ export const FullArticle = (props: Props) => {
<Popover content={t('Comment')}> <Popover content={t('Comment')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef} onClick={scrollToComments}> <div class={clsx(styles.shoutStatsItem)} ref={triggerRef} onClick={scrollToComments}>
<Icon name="comment" class={styles.icon} /> <Icon name="comment" class={styles.icon} />
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
{props.article.stat?.commented ?? ''} <Show
when={props.article.stat?.commented}
fallback={<span class={styles.commentsTextLabel}>{t('Add comment')}</span>}
>
{props.article.stat?.commented}
</Show>
</div> </div>
)} )}
</Popover> </Popover>
<Show when={props.article.stat?.viewed}> <Show when={props.article.stat?.viewed}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}> <div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
<Icon name="eye" class={styles.icon} /> {t('viewsWithCount', { count: props.article.stat?.viewed })}
<Icon name="eye" class={clsx(styles.icon, styles.iconHover)} />
{props.article.stat?.viewed}
</div> </div>
</Show> </Show>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
{formattedDate()}
</div>
</div>
<Popover content={t('Add to bookmarks')}>
{(triggerRef: (el) => void) => (
<div
class={clsx(styles.shoutStatsItem, styles.shoutStatsItemBookmarks)}
ref={triggerRef}
onClick={handleBookmarkButtonClick}
>
<div class={styles.shoutStatsItemInner}>
<Icon name="bookmark" class={styles.icon} />
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
</div>
</div>
)}
</Popover>
<Popover content={t('Share')}> <Popover content={t('Share')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef}> <div class={styles.shoutStatsItem} ref={triggerRef}>
@ -380,16 +424,7 @@ export const FullArticle = (props: Props) => {
</div> </div>
)} )}
</Popover> </Popover>
<Popover content={t('Add to bookmarks')}>
{(triggerRef: (el) => void) => (
<div class={styles.shoutStatsItem} ref={triggerRef} onClick={handleBookmarkButtonClick}>
<div class={styles.shoutStatsItemInner}>
<Icon name="bookmark" class={styles.icon} />
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
</div>
</div>
)}
</Popover>
<Show when={canEdit()}> <Show when={canEdit()}>
<Popover content={t('Edit')}> <Popover content={t('Edit')}>
{(triggerRef: (el) => void) => ( {(triggerRef: (el) => void) => (
@ -405,20 +440,33 @@ export const FullArticle = (props: Props) => {
)} )}
</Popover> </Popover>
</Show> </Show>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}> <FeedArticlePopup
{formattedDate()} isOwner={canEdit()}
</div> containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
</div> title={props.article.title}
description={getDescription(props.article.body)}
imageUrl={props.article.cover}
shareUrl={getShareUrl({ pathname: `/${props.article.slug}` })}
trigger={
<button>
<Icon name="ellipsis" class={clsx(styles.icon)} />
<Icon name="ellipsis" class={clsx(styles.icon, styles.iconHover)} />
</button>
}
/>
</div> </div>
<div class={styles.help}>
<Show when={isAuthenticated() && !canEdit()}> <Show when={isAuthenticated() && !canEdit()}>
<div class={styles.help}>
<button class="button">{t('Cooperate')}</button> <button class="button">{t('Cooperate')}</button>
</Show> </div>
<Show when={canEdit()}> </Show>
<Show when={canEdit()}>
<div class={styles.help}>
<button class="button button--light">{t('Invite to collab')}</button> <button class="button button--light">{t('Invite to collab')}</button>
</Show> </div>
</div> </Show>
<Show when={props.article.topics.length}> <Show when={props.article.topics.length}>
<div class={styles.topicsList}> <div class={styles.topicsList}>
@ -437,9 +485,9 @@ export const FullArticle = (props: Props) => {
<h4>{t('Authors')}</h4> <h4>{t('Authors')}</h4>
</Show> </Show>
<For each={props.article.authors}> <For each={props.article.authors}>
{(a) => ( {(author) => (
<div class="col-xl-12"> <div class="col-xl-12">
<AuthorCard author={a} hasLink={true} liteButtons={true} /> <AuthorBadge iconButtons={true} showMessageButton={true} author={author} />
</div> </div>
)} )}
</For> </For>
@ -456,6 +504,9 @@ export const FullArticle = (props: Props) => {
</div> </div>
</div> </div>
</div> </div>
<Show when={selectedImage()}>
<Lightbox image={selectedImage()} onClose={handleLightboxClose} />
</Show>
</> </>
) )
} }

View File

@ -0,0 +1,51 @@
.AuthorLink {
.link {
display: inline-flex;
flex-flow: row nowrap;
align-items: center;
gap: 1rem;
justify-content: center;
padding: 0;
border-bottom: none;
vertical-align: text-bottom;
&:hover {
background: unset !important;
border-bottom: none;
color: var(--default-color) !important;
}
.name {
font-weight: 500;
line-height: 1;
margin-bottom: -2px;
&:hover {
color: var(--default-color-invert);
background: var(--background-color-invert);
}
}
}
// adjust size
&.XS {
.link {
gap: 0.5rem;
}
.name {
font-size: 1.2rem;
margin: 0;
}
}
&.M {
.link {
gap: 1rem;
}
.name {
font-size: 1.4rem;
}
}
}

View File

@ -0,0 +1,21 @@
import { clsx } from 'clsx'
import styles from './AhtorLink.module.scss'
import { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic'
type Props = {
author: Author
size?: 'XS' | 'M' | 'L'
class?: string
}
export const AuthorLink = (props: Props) => {
return (
<div class={clsx(styles.AuthorLink, props.class, styles[props.size ?? 'M'])}>
<a class={styles.link} href={`/author/${props.author.slug}`}>
<Userpic size={props.size ?? 'M'} name={props.author.name} userpic={props.author.userpic} />
<div class={styles.name}>{props.author.name}</div>
</a>
</div>
)
}

View File

@ -0,0 +1 @@
export { AuthorLink } from './AuthorLink'

View File

@ -1,26 +1,44 @@
.AuthorBadge { .AuthorBadge {
align-items: flex-start; align-items: flex-start;
display: flex; display: flex;
flex-flow: row nowrap; gap: 1rem;
margin-bottom: 2rem; margin-bottom: 3rem;
@include media-breakpoint-down(sm) { &.nameOnly {
flex-wrap: wrap; align-items: center;
margin-bottom: 3rem;
.info {
margin-bottom: 0;
}
}
@include media-breakpoint-up(sm) {
margin-bottom: 2rem;
} }
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
text-align: left; text-align: left;
} }
.basicInfo {
display: flex;
flex-direction: row;
align-items: center;
flex: 0 calc(100% - 5.2rem);
gap: 1rem;
@include media-breakpoint-down(sm) {
flex: 0 100%;
}
}
.info { .info {
@include font-size(1.4rem); @include font-size(1.4rem);
border: none; border: none;
display: flex; display: flex;
flex: 0 calc(100% - 5.2rem);
flex-direction: column; flex-direction: column;
line-height: 1.3; line-height: 1.3;
margin-bottom: 1rem;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
flex: 1 100%; flex: 1 100%;
@ -33,6 +51,11 @@
.name { .name {
color: var(--default-color); color: var(--default-color);
font-weight: 500; font-weight: 500;
& span:hover {
color: var(--default-color-invert);
background: var(--background-color-invert);
}
} }
.bio { .bio {
@ -42,7 +65,14 @@
.actions { .actions {
flex: 0 20%; flex: 0 20%;
display: flex;
flex-direction: row;
margin-left: 5.2rem; margin-left: 5.2rem;
gap: 1rem;
@include media-breakpoint-down(sm) {
margin-left: 0;
}
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
margin-left: 2rem; margin-left: 2rem;
@ -56,9 +86,33 @@
} }
} }
.subscribeButton { .actionButton {
border-radius: 0.8rem !important; border-radius: 0.8rem !important;
margin-right: 0 !important; margin-right: 0 !important;
width: 9em; width: 9em;
&.iconed {
padding: 6px !important;
min-width: 4rem;
width: unset;
&:hover img {
filter: invert(1);
}
}
&:hover {
.actionButtonLabel {
display: none;
}
.actionButtonLabelHovered {
display: block;
}
}
}
.actionButtonLabelHovered {
display: none;
} }
} }

View File

@ -1,5 +1,6 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './AuthorBadge.module.scss' import styles from './AuthorBadge.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { Author, FollowingEntity } from '../../../graphql/types.gen' import { Author, FollowingEntity } from '../../../graphql/types.gen'
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js' import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
@ -8,22 +9,29 @@ import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
import { CheckButton } from '../../_shared/CheckButton' 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
minimizeSubscribeButton?: boolean minimizeSubscribeButton?: boolean
showMessageButton?: boolean
iconButtons?: boolean
nameOnly?: boolean
} }
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
const { const {
session, session,
actions: { loadSession, requireAuthentication } subscriptions,
actions: { loadSubscriptions, requireAuthentication }
} = useSession() } = useSession()
const { changeSearchParam } = useRouter()
const { t, formatDate } = useLocalize() const { t, formatDate } = useLocalize()
const subscribed = createMemo<boolean>(() => { const subscribed = createMemo(() =>
return session()?.news?.authors?.some((u) => u === props.author.slug) || false subscriptions().authors.some((author) => author.slug === props.author.slug)
}) )
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
setIsSubscribing(true) setIsSubscribing(true)
@ -32,7 +40,7 @@ export const AuthorBadge = (props: Props) => {
? follow({ what: FollowingEntity.Author, slug: props.author.slug }) ? follow({ what: FollowingEntity.Author, slug: props.author.slug })
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) : unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
await loadSession() await loadSubscriptions()
setIsSubscribing(false) setIsSubscribing(false)
} }
const handleSubscribe = (really: boolean) => { const handleSubscribe = (really: boolean) => {
@ -41,35 +49,69 @@ export const AuthorBadge = (props: Props) => {
}, 'subscribe') }, 'subscribe')
} }
const initChat = () => {
requireAuthentication(() => {
openPage(router, `inbox`)
changeSearchParam({
initChat: props.author.id.toString()
})
}, 'discussions')
}
const subscribeValue = createMemo(() => {
if (props.iconButtons) {
return <Icon name="author-subscribe" class={stylesButton.icon} />
}
return isSubscribing() ? t('subscribing...') : t('Subscribe')
})
const unsubscribeValue = () => {
if (props.iconButtons) {
return <Icon name="author-unsubscribe" class={stylesButton.icon} />
}
return (
<>
<span class={styles.actionButtonLabel}>{t('Following')}</span>
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
</>
)
}
return ( return (
<div class={clsx(styles.AuthorBadge)}> <div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
<Userpic <div class={styles.basicInfo}>
hasLink={true} <Userpic
isMedium={true} hasLink={true}
name={props.author.name} size={'M'}
userpic={props.author.userpic} name={props.author.name}
slug={props.author.slug} userpic={props.author.userpic}
/> slug={props.author.slug}
<a href={`/author/${props.author.slug}`} class={styles.info}> />
<div class={styles.name}>{props.author.name}</div> <a href={`/author/${props.author.slug}`} class={styles.info}>
<Switch <div class={styles.name}>
fallback={ <span>{props.author.name}</span>
<div class={styles.bio}> </div>
{t('Registered since {date}', { date: formatDate(new Date(props.author.createdAt)) })} <Show when={!props.nameOnly}>
</div> <Switch
} fallback={
> <div class={styles.bio}>
<Match when={props.author.bio}> {t('Registered since {date}', { date: formatDate(new Date(props.author.createdAt)) })}
<div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} /> </div>
</Match> }
<Match when={props.author?.stat && props.author?.stat.shouts > 0}> >
<div class={styles.bio}> <Match when={props.author.bio}>
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })} <div class={clsx('text-truncate', styles.bio)} innerHTML={props.author.bio} />
</div> </Match>
</Match> <Match when={props.author?.stat && props.author?.stat.shouts > 0}>
</Switch> <div class={styles.bio}>
</a> {t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
<Show when={props.author.slug !== session()?.user.slug}> </div>
</Match>
</Switch>
</Show>
</a>
</div>
<Show when={props.author.slug !== session()?.user.slug && !props.nameOnly}>
<div class={styles.actions}> <div class={styles.actions}>
<Show <Show
when={!props.minimizeSubscribeButton} when={!props.minimizeSubscribeButton}
@ -85,23 +127,40 @@ export const AuthorBadge = (props: Props) => {
when={subscribed()} when={subscribed()}
fallback={ fallback={
<Button <Button
variant="primary" variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S" size="M"
value={isSubscribing() ? t('...subscribing') : t('Subscribe')} value={subscribeValue()}
onClick={() => handleSubscribe(true)} onClick={() => handleSubscribe(true)}
class={styles.subscribeButton} isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed()
})}
/> />
} }
> >
<Button <Button
variant="bordered" variant={props.iconButtons ? 'secondary' : 'bordered'}
size="S" size="M"
value={t('Following')} value={unsubscribeValue()}
onClick={() => handleSubscribe(false)} onClick={() => handleSubscribe(false)}
class={styles.subscribeButton} isSubscribeButton={true}
class={clsx(styles.actionButton, {
[styles.iconed]: props.iconButtons,
[stylesButton.subscribed]: subscribed()
})}
/> />
</Show> </Show>
</Show> </Show>
<Show when={props.showMessageButton}>
<Button
variant={props.iconButtons ? 'secondary' : 'bordered'}
size="M"
value={props.iconButtons ? <Icon name="inbox-white" /> : t('Message')}
onClick={initChat}
class={clsx(styles.actionButton, { [styles.iconed]: props.iconButtons })}
/>
</Show>
</div> </div>
</Show> </Show>
</div> </div>

View File

@ -8,10 +8,50 @@
margin-bottom: 0; margin-bottom: 0;
} }
@include media-breakpoint-down(md) {
justify-content: center;
}
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
margin-bottom: 2.4rem; margin-bottom: 2.4rem;
} }
.authorName {
@include font-size(4rem);
font-weight: 700;
margin-bottom: 0.2em;
}
.authorAbout {
@include font-size(2rem);
color: #696969;
font-weight: 500;
margin-top: 1.5rem;
}
.authorActions {
margin: 2rem -0.8rem 0 0;
padding-left: 0;
display: flex;
flex-direction: row;
gap: 1rem;
@include media-breakpoint-down(md) {
justify-content: center;
}
}
.authorDetails {
display: block;
@include media-breakpoint-down(md) {
flex: 1 100%;
text-align: center;
}
}
.listWrapper & { .listWrapper & {
align-items: flex-start; align-items: flex-start;
margin-bottom: 2rem; margin-bottom: 2rem;
@ -36,10 +76,16 @@
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
flex-wrap: wrap; flex-wrap: wrap;
} }
.buttonWriteMessage {
border-radius: 0.8rem;
padding-bottom: 0.6rem;
padding-top: 0.6rem;
}
} }
.authorDetails { .authorDetails {
flex: 1; flex: 0 0 auto;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
align-items: center; align-items: center;
@ -57,10 +103,6 @@
flex-wrap: nowrap; flex-wrap: nowrap;
} }
} }
&.authorDetailsShrinked {
flex: 0 0 auto;
}
} }
.authorDetailsWrapper { .authorDetailsWrapper {
@ -84,29 +126,10 @@
} }
} }
.authorNameContainer {
line-height: 1.1;
}
.authorName { .authorName {
border: none !important; @include font-size(4rem);
font-size: 1.6rem;
font-weight: 500;
margin-bottom: 0.8rem;
.listWrapper & { line-height: 1.1;
display: block;
&:before {
content: '';
height: 100%;
left: 0;
position: absolute;
top: 0;
width: 100%;
z-index: 2;
}
}
} }
.authorAbout { .authorAbout {
@ -117,42 +140,6 @@
word-break: break-word; word-break: break-word;
} }
.authorSubscribe {
align-items: center;
@include media-breakpoint-down(md) {
flex-wrap: wrap;
}
.button {
padding-left: 2rem;
padding-right: 2rem;
margin-right: 0.5em;
&:first-of-type {
margin-left: 0;
}
&:last-child {
margin-right: 0;
}
&:hover {
.buttonUnfollowLabel {
display: block;
}
.buttonSubscribedLabel {
display: none;
}
}
.buttonUnfollowLabel {
display: none;
}
}
}
.authorSubscribeSocialLabel { .authorSubscribeSocialLabel {
display: none; display: none;
} }
@ -195,9 +182,10 @@
} }
.authorSubscribeSocialLabel { .authorSubscribeSocialLabel {
@include font-size(1.6rem);
color: #000; color: #000;
display: block; display: block;
@include font-size(1.6rem);
left: 100%; left: 100%;
padding-left: 0.4rem; padding-left: 0.4rem;
position: absolute; position: absolute;
@ -228,7 +216,8 @@
} }
} }
&[href*='telegram.com/'] { &[href*='telegram.com/'],
&[href*='t.me/'] {
&::before { &::before {
background-image: url(/icons/user-link-telegram.svg); background-image: url(/icons/user-link-telegram.svg);
} }
@ -426,208 +415,6 @@
} }
} }
.shareControl {
display: inline-block;
}
.buttonSubscribe {
align-items: center;
aspect-ratio: 1/1;
border-radius: 100%;
display: inline-flex;
float: right;
img {
display: block;
}
}
.buttonLabel {
display: none;
}
.buttonLabelVisible {
display: block;
}
.buttonWrite {
background: #ccc;
color: #000;
display: inline-flex;
font-weight: 500;
transition:
background-color 0.3s,
color 0.3s;
&:hover {
background: #000;
color: #fff;
img {
filter: invert(1);
}
}
.icon {
display: inline-block;
margin-right: 0.5em;
}
img {
height: 15px;
transition: filter 0.3s;
}
}
.authorPage {
align-items: center;
@include media-breakpoint-down(md) {
justify-content: center;
}
.authorName {
@include font-size(4rem);
font-weight: 700;
margin-bottom: 0.2em;
}
.authorAbout {
color: #696969;
@include font-size(2rem);
font-weight: 500;
margin-top: 1.5rem;
}
.authorSubscribe {
margin: 2rem -0.8rem 0 0;
padding-left: 0;
@include media-breakpoint-down(md) {
justify-content: center;
}
}
.authorDetails {
display: block;
@include media-breakpoint-down(md) {
flex: 1 100%;
text-align: center;
}
}
.buttonLabel {
display: block;
}
.buttonSubscribe {
aspect-ratio: auto;
background-color: #000;
border-color: #000;
border-radius: 0.8rem;
color: #fff;
float: none;
padding-bottom: 0.6rem;
padding-top: 0.6rem;
width: 10em;
.icon {
margin-right: 0.5em;
img {
filter: invert(1);
}
}
&:hover {
background: #fff;
color: #000;
.icon img {
filter: invert(0);
}
}
}
.buttonSubscribe img {
vertical-align: text-top;
}
.button {
min-height: 4rem;
margin: 0 0.8rem 0 0;
vertical-align: middle;
@include media-breakpoint-down(sm) {
margin-bottom: 0.5em;
}
}
}
.authorsListItem {
margin-bottom: 1em !important;
.authorName {
@include font-size(2.2rem);
font-weight: bold;
}
.authorSubscribe {
align-items: baseline;
@include media-breakpoint-down(sm) {
padding: 1rem 0 0;
}
}
.buttonLabel {
display: block;
}
}
.nowrapView {
flex-wrap: nowrap;
align-items: center;
margin: 0;
}
.authorComments {
.authorName {
@include font-size(1.2rem);
line-height: 1.2;
margin-bottom: 0;
}
.circlewrap {
margin-top: -0.4em;
}
}
.isSubscribing {
opacity: 0.5;
}
.feedMode {
align-items: center;
margin-bottom: 0.4rem;
.authorName,
.authorAbout {
@include font-size(1.2rem);
margin-bottom: 0;
}
.circlewrap {
height: 1.6rem;
margin-right: 0.4rem;
min-width: 1.6rem;
width: 1.6rem;
}
}
.subscribersContainer { .subscribersContainer {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -648,33 +435,35 @@
vertical-align: top; vertical-align: top;
border-bottom: unset !important; border-bottom: unset !important;
&:last-child { .subscribersItem {
margin-right: 0; position: relative;
}
.userpic { &:nth-child(1) {
background: var(--background-color); z-index: 2;
box-shadow: 0 0 0 2px var(--background-color);
height: 1.8rem;
min-width: 1.8rem;
max-width: 1.8rem;
vertical-align: top;
width: 1.8rem;
&:not(:first-child) {
margin-left: -1.8rem;
} }
> * { &:nth-child(2) {
line-height: 1.8rem; z-index: 1;
min-width: auto; }
&:not(:last-child) {
margin-right: -4px;
box-shadow: 0 0 0 1px var(--background-color);
} }
} }
}
.subscribersCounter { .subscribersCounter {
font-weight: 500; font-weight: 500;
margin-left: -0.6rem; margin-left: 1rem;
}
&:hover {
background: none !important;
.subscribersCounter {
background: var(--background-color-invert);
}
}
} }
.listWrapper { .listWrapper {

View File

@ -1,6 +1,5 @@
import type { Author } from '../../../graphql/types.gen' import type { Author } from '../../../graphql/types.gen'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import { Icon } from '../../_shared/Icon'
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
@ -11,7 +10,6 @@ import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { openPage, redirectPage } from '@nanostores/router' import { openPage, redirectPage } from '@nanostores/router'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
@ -20,48 +18,30 @@ import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { getShareUrl, SharePopup } from '../../Article/SharePopup' import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import styles from './AuthorCard.module.scss' import styles from './AuthorCard.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss'
type Props = { type Props = {
caption?: string
hideWriteButton?: boolean
hideDescription?: boolean
hideFollow?: boolean
hasLink?: boolean
subscribed?: boolean
author: Author author: Author
isAuthorPage?: boolean
noSocialButtons?: boolean
isAuthorsList?: boolean
truncateBio?: boolean
liteButtons?: boolean
isTextButton?: boolean
isComments?: boolean
isFeedMode?: boolean
isNowrap?: boolean
class?: string
followers?: Author[] followers?: Author[]
following?: Array<Author | Topic> following?: Array<Author | Topic>
showPublicationsCounter?: boolean
hideBio?: boolean
isCurrentUser?: boolean
} }
export const AuthorCard = (props: Props) => { export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { const {
session, session,
subscriptions,
isSessionLoaded, isSessionLoaded,
actions: { loadSession, requireAuthentication } actions: { loadSubscriptions, requireAuthentication }
} = useSession() } = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
const [following, setFollowing] = createSignal<Array<Author | Topic>>(props.following) const [following, setFollowing] = createSignal<Array<Author | Topic>>(props.following)
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const [userpicUrl, setUserpicUrl] = createSignal<string>()
const subscribed = createMemo<boolean>(() => { const subscribed = createMemo<boolean>(() =>
return session()?.news?.authors?.some((u) => u === props.author.slug) || false subscriptions().authors.some((author) => author.slug === props.author.slug)
}) )
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
setIsSubscribing(true) setIsSubscribing(true)
@ -70,11 +50,11 @@ export const AuthorCard = (props: Props) => {
? follow({ what: FollowingEntity.Author, slug: props.author.slug }) ? follow({ what: FollowingEntity.Author, slug: props.author.slug })
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug })) : unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
await loadSession() await loadSubscriptions()
setIsSubscribing(false) setIsSubscribing(false)
} }
const canFollow = createMemo(() => !props.hideFollow && session()?.user?.slug !== props.author.slug) const isProfileOwner = createMemo(() => session()?.user?.slug === props.author.slug)
const name = createMemo(() => { const name = createMemo(() => {
if (lang() !== 'ru') { if (lang() !== 'ru') {
@ -101,7 +81,7 @@ export const AuthorCard = (props: Props) => {
const handleSubscribe = () => { const handleSubscribe = () => {
requireAuthentication(() => { requireAuthentication(() => {
subscribe(true) subscribe(!subscribed())
}, 'subscribe') }, 'subscribe')
} }
@ -117,89 +97,36 @@ export const AuthorCard = (props: Props) => {
} }
}) })
if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) { const followButtonText = () => {
setUserpicUrl(props.author.userpic.replace('100x', '500x500')) if (isSubscribing()) {
return t('subscribing...')
} else if (subscribed()) {
return (
<>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
</>
)
} else {
return t('Follow')
}
} }
return ( return (
<div <div class={clsx(styles.author, 'row')}>
class={clsx(styles.author, props.class)} <div class="col-md-5">
classList={{ <Userpic
['row']: props.isAuthorPage, size={'XL'}
[styles.authorPage]: props.isAuthorPage, name={props.author.name}
[styles.authorComments]: props.isComments, userpic={props.author.userpic}
[styles.authorsListItem]: props.isAuthorsList, slug={props.author.slug}
[styles.feedMode]: props.isFeedMode, class={styles.circlewrap}
[styles.nowrapView]: props.isNowrap />
}} </div>
> <div class={clsx('col-md-15 col-xl-13', styles.authorDetails)}>
<Show
when={props.isAuthorPage}
fallback={
<Userpic
name={props.author.name}
userpic={props.author.userpic}
hasLink={props.hasLink}
isBig={props.isAuthorPage}
isAuthorsList={props.isAuthorsList}
isFeedMode={props.isFeedMode}
slug={props.author.slug}
class={styles.circlewrap}
/>
}
>
<div class="col-md-5">
<Userpic
name={props.author.name}
userpic={userpicUrl()}
hasLink={props.hasLink}
isBig={props.isAuthorPage}
isAuthorsList={props.isAuthorsList}
isFeedMode={props.isFeedMode}
slug={props.author.slug}
class={styles.circlewrap}
/>
</div>
</Show>
<div
class={styles.authorDetails}
classList={{
'col-md-15 col-xl-13': props.isAuthorPage,
[styles.authorDetailsShrinked]: props.isAuthorPage
}}
>
<div class={styles.authorDetailsWrapper}> <div class={styles.authorDetailsWrapper}>
<div class={styles.authorNameContainer}> <div class={styles.authorName}>{name()}</div>
<ConditionalWrapper <div class={styles.authorAbout} innerHTML={props.author.bio} />
condition={props.hasLink}
wrapper={(children) => (
<a class={styles.authorName} href={`/author/${props.author.slug}`}>
{children}
</a>
)}
>
<span class={clsx({ [styles.authorName]: !props.hasLink })}>{name()}</span>
</ConditionalWrapper>
</div>
<Show
when={props.author.bio && !props.hideBio}
fallback={
props.showPublicationsCounter ? (
<div class={styles.authorAbout}>
{t('PublicationsWithCount', { count: props.author.stat?.shouts ?? 0 })}
</div>
) : (
''
)
}
>
<div
class={styles.authorAbout}
classList={{ 'text-truncate': props.truncateBio }}
innerHTML={props.author.bio}
/>
</Show>
<Show <Show
when={ when={
(props.followers && props.followers.length > 0) || (props.followers && props.followers.length > 0) ||
@ -210,10 +137,17 @@ export const AuthorCard = (props: Props) => {
<Show when={props.followers && props.followers.length > 0}> <Show when={props.followers && props.followers.length > 0}>
<a href="?modal=followers" class={styles.subscribers}> <a href="?modal=followers" class={styles.subscribers}>
<For each={props.followers.slice(0, 3)}> <For each={props.followers.slice(0, 3)}>
{(f) => <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} />} {(f) => (
<Userpic
size={'XS'}
name={f.name}
userpic={f.userpic}
class={styles.subscribersItem}
/>
)}
</For> </For>
<div class={styles.subscribersCounter}> <div class={styles.subscribersCounter}>
{t('SubscriberWithCount', { count: props.followers.length })} {t('SubscriberWithCount', { count: props.followers.length ?? 0 })}
</div> </div>
</a> </a>
</Show> </Show>
@ -223,9 +157,23 @@ export const AuthorCard = (props: Props) => {
<For each={props.following.slice(0, 3)}> <For each={props.following.slice(0, 3)}>
{(f) => { {(f) => {
if ('name' in f) { if ('name' in f) {
return <Userpic name={f.name} userpic={f.userpic} class={styles.userpic} /> return (
<Userpic
size={'XS'}
name={f.name}
userpic={f.userpic}
class={styles.subscribersItem}
/>
)
} else if ('title' in f) { } else if ('title' in f) {
return <Userpic name={f.title} userpic={f.pic} class={styles.userpic} /> return (
<Userpic
size={'XS'}
name={f.title}
userpic={f.pic}
class={styles.subscribersItem}
/>
)
} }
return null return null
}} }}
@ -258,102 +206,35 @@ export const AuthorCard = (props: Props) => {
</For> </For>
</div> </div>
</Show> </Show>
<Show when={canFollow()}>
<div class={styles.authorSubscribe}>
<Show
when={subscribed()}
fallback={
<button
onClick={handleSubscribe}
class={clsx('button', styles.button)}
classList={{
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
'button--subscribe': !props.isAuthorsList,
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
[styles.isSubscribing]: isSubscribing()
}}
disabled={isSubscribing()}
>
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
<Icon name="author-subscribe" class={styles.icon} />
</Show>
<Show when={props.isTextButton || props.isAuthorPage}>
<span class={clsx(styles.buttonLabel, styles.buttonLabelVisible)}>
{t('Follow')}
</span>
</Show>
</button>
}
>
<button
onClick={() => subscribe(false)}
class={clsx('button', styles.button)}
classList={{
[styles.buttonSubscribe]: !props.isAuthorsList && !props.isTextButton,
'button--subscribe': !props.isAuthorsList,
'button--subscribe-topic': props.isAuthorsList || props.isTextButton,
[styles.buttonWrite]: props.isAuthorsList || props.isTextButton,
[styles.isSubscribing]: isSubscribing()
}}
disabled={isSubscribing()}
>
<Show when={!props.isAuthorsList && !props.isTextButton && !props.isAuthorPage}>
<Icon name="author-unsubscribe" class={styles.icon} />
</Show>
<Show when={props.isTextButton || props.isAuthorPage}>
<span
class={clsx(
styles.buttonLabel,
styles.buttonLabelVisible,
styles.buttonUnfollowLabel
)}
>
{t('Unfollow')}
</span>
<span
class={clsx(
styles.buttonLabel,
styles.buttonLabelVisible,
styles.buttonSubscribedLabel
)}
>
{t('Following')}
</span>
</Show>
</button>
</Show>
<Show when={!props.hideWriteButton}> <Show
<button when={isProfileOwner()}
class={styles.button} fallback={
classList={{ <div class={styles.authorActions}>
'button--light': !props.isAuthorsList, <Button
'button--subscribe-topic': props.isAuthorsList, onClick={handleSubscribe}
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList value={followButtonText()}
}} isSubscribeButton={true}
class={clsx({
[stylesButton.subscribed]: subscribed()
})}
/>
<Button
variant={'secondary'}
value={t('Message')}
onClick={initChat} onClick={initChat}
> class={styles.buttonWriteMessage}
<Show when={!props.isTextButton && !props.isAuthorPage}> />
<Icon name="comment" class={styles.icon} /> </div>
</Show> }
<Show when={!props.liteButtons || props.isTextButton}>{t('Message')}</Show> >
</button> <div class={styles.authorActions}>
</Show>
</div>
</Show>
<Show when={props.isCurrentUser}>
<div class={styles.authorSubscribe}>
<Button <Button
variant="secondary" variant="secondary"
onClick={() => redirectPage(router, 'profileSettings')} onClick={() => redirectPage(router, 'profileSettings')}
value={t('Edit profile')} value={t('Edit profile')}
class={styles.button}
/> />
<SharePopup <SharePopup
containerCssClass={styles.shareControl}
title={props.author.name} title={props.author.name}
description={props.author.bio} description={props.author.bio}
imageUrl={props.author.userpic} imageUrl={props.author.userpic}

View File

@ -1,14 +1,9 @@
.Userpic { .Userpic {
align-items: baseline; background: #f7f7f7;
background: #f7f7f8;
border-radius: 100%; border-radius: 100%;
display: flex; display: flex;
justify-content: center; justify-content: center;
margin-right: 1.2rem; align-items: center;
min-width: 32px;
max-width: 32px;
height: 32px;
width: 32px;
overflow: hidden; overflow: hidden;
position: relative; position: relative;
@ -20,24 +15,17 @@
} }
.letters { .letters {
background-color: white; display: flex;
border-radius: 50%;
border: 1.5px solid black;
color: #000;
font-size: small;
text-align: center;
text-transform: uppercase;
line-height: 32px;
width: 100%;
height: 100%; height: 100%;
min-width: 32px; width: 100%;
} border-radius: 100%;
padding-top: 2px; // line-height hack
.anonymous { justify-content: center;
height: 17px !important; align-items: center;
object-fit: contain; color: var(--default-color);
width: 20px !important; text-transform: uppercase;
margin: auto; background: var(--background-color);
box-shadow: 0 0 0 1px var(--background-color-invert) inset;
} }
a:link, a:link,
@ -55,18 +43,49 @@
} }
} }
&.medium { &.XS {
width: 40px; width: 20px;
height: 40px; height: 20px;
min-width: 40px; min-width: 20px;
max-width: 40px; overflow: hidden;
.letters { .letters {
line-height: 40px; font-size: 0.8rem;
} }
} }
&.big { &.S {
width: 28px;
height: 28px;
min-width: 28px;
overflow: hidden;
.letters {
font-size: 1rem;
}
}
&.M {
height: 32px;
width: 32px;
min-width: 32px;
.letters {
font-size: 1.2rem;
}
}
&.L {
height: 40px;
width: 40px;
min-width: 40px;
.letters {
font-size: 1.2rem;
}
}
&.XL {
aspect-ratio: 1/1; aspect-ratio: 1/1;
margin: 0 auto 1rem; margin: 0 auto 1rem;
max-width: 168px; max-width: 168px;
@ -101,12 +120,3 @@
line-height: 6.4rem; line-height: 6.4rem;
} }
} }
.feedMode {
.letters {
font-size: 0.8rem;
line-height: 14px;
min-width: 16px;
max-width: 16px;
}
}

View File

@ -1,9 +1,9 @@
import { Show } from 'solid-js' import { createMemo, Show } from 'solid-js'
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 { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Loading } from '../../_shared/Loading' import { Loading } from '../../_shared/Loading'
import { Image } from '../../_shared/Image'
type Props = { type Props = {
name: string name: string
@ -12,11 +12,8 @@ type Props = {
slug?: string slug?: string
onClick?: () => void onClick?: () => void
loading?: boolean loading?: boolean
isBig?: boolean
isMedium?: boolean
hasLink?: boolean hasLink?: boolean
isAuthorsList?: boolean size?: 'XS' | 'S' | 'M' | 'L' | 'XL' // 20 | 28 | 32 | 40 | 168
isFeedMode?: boolean
} }
export const Userpic = (props: Props) => { export const Userpic = (props: Props) => {
@ -26,13 +23,29 @@ export const Userpic = (props: Props) => {
return names[0][0] + (names.length > 1 ? names[1][0] : '') return names[0][0] + (names.length > 1 ? names[1][0] : '')
} }
const avatarSize = createMemo(() => {
switch (props.size) {
case 'XS': {
return 40
}
case 'S': {
return 56
}
case 'L': {
return 80
}
case 'XL': {
return 336
}
default: {
return 64
}
}
})
return ( return (
<div <div
class={clsx(styles.Userpic, props.class, { class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
[styles.big]: props.isBig,
[styles.medium]: props.isMedium,
[styles.authorsList]: props.isAuthorsList,
[styles.feedMode]: props.isFeedMode,
['cursorPointer']: props.onClick ['cursorPointer']: props.onClick
})} })}
onClick={props.onClick} onClick={props.onClick}
@ -42,18 +55,8 @@ export const Userpic = (props: Props) => {
condition={props.hasLink} condition={props.hasLink}
wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>} wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>}
> >
<Show <Show keyed={true} when={props.userpic} fallback={<div class={styles.letters}>{letters()}</div>}>
when={!props.userpic} <Image src={props.userpic} width={avatarSize()} height={avatarSize()} alt={props.name} />
fallback={
<img
class={clsx({ [styles.anonymous]: !props.userpic })}
src={imageProxy(props.userpic) || '/icons/user-default.svg'}
alt={props.name || ''}
loading="lazy"
/>
}
>
<div class={styles.letters}>{letters()}</div>
</Show> </Show>
</ConditionalWrapper> </ConditionalWrapper>
</Show> </Show>

View File

@ -22,7 +22,9 @@
a { a {
color: rgb(255 255 255 / 64%); color: rgb(255 255 255 / 64%);
transition: color 0.3s, background-color 0.3s; transition:
color 0.3s,
background-color 0.3s;
&:hover { &:hover {
background: #fff; background: #fff;
@ -66,6 +68,7 @@
} }
.footerCopyrightSocial { .footerCopyrightSocial {
align-items: center;
display: flex; display: flex;
.icon { .icon {
@ -94,6 +97,15 @@
margin-left: 0.3em; margin-left: 0.3em;
text-align: right; text-align: right;
} }
a:link {
border: none;
padding-bottom: 0;
}
img {
margin: 0 auto;
}
} }
.socialItemvk { .socialItemvk {

View File

@ -1,7 +1,7 @@
import { createMemo, For } from 'solid-js' import { createMemo, For } from 'solid-js'
import styles from './Footer.module.scss' import styles from './Footer.module.scss'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import Subscribe from './Subscribe' import { Subscribe } from '../_shared/Subscribe'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'

View File

@ -1,5 +1,6 @@
.aboutDiscours { .aboutDiscours {
@include font-size(1.6rem); @include font-size(1.6rem);
background: #fef2f2; background: #fef2f2;
font-weight: 500; font-weight: 500;
margin-bottom: 6.4rem; margin-bottom: 6.4rem;
@ -8,6 +9,7 @@
h4 { h4 {
@include font-size(4rem); @include font-size(4rem);
font-weight: bold; font-weight: bold;
line-height: 1.1; line-height: 1.1;
margin-bottom: 2rem; margin-bottom: 2rem;

View File

@ -5,6 +5,7 @@
.draggable { .draggable {
margin: 8px 0; margin: 8px 0;
padding: 8px 0; padding: 8px 0;
&:hover { &:hover {
background: var(--placeholder-color-semi); background: var(--placeholder-color-semi);
} }

View File

@ -13,7 +13,7 @@
z-index: 2; z-index: 2;
font-weight: 500; font-weight: 500;
transition: 0.6s ease-in-out; transition: 0.6s ease-in-out;
background: rgba(white, 0.3); background: rgb(255 255 255 / 30%);
backdrop-filter: blur(4px); backdrop-filter: blur(4px);
border: 1px solid var(--secondary-color); border: 1px solid var(--secondary-color);
left: 100%; left: 100%;

View File

@ -44,9 +44,8 @@ import { EditorFloatingMenu } from './EditorFloatingMenu'
import './Prosemirror.scss' import './Prosemirror.scss'
import { Image } from '@tiptap/extension-image' import { Image } from '@tiptap/extension-image'
import { Footnote } from './extensions/Footnote' import { Footnote } from './extensions/Footnote'
import { handleFileUpload } from '../../utils/handleFileUpload'
import { imageProxy } from '../../utils/imageProxy'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { handleImageUpload } from '../../utils/handleImageUpload'
type Props = { type Props = {
shoutId: number shoutId: number
@ -154,7 +153,7 @@ export const Editor = (props: Props) => {
} }
showSnackbar({ body: t('Uploading image') }) showSnackbar({ body: t('Uploading image') })
const result = await handleFileUpload(uplFile) const result = await handleImageUpload(uplFile)
editor() editor()
.chain() .chain()
@ -174,7 +173,7 @@ export const Editor = (props: Props) => {
{ {
type: 'image', type: 'image',
attrs: { attrs: {
src: imageProxy(result.url) src: result.url
} }
} }
] ]

View File

@ -269,6 +269,7 @@ figure[data-type='capturedImage'] {
flex-direction: column-reverse; flex-direction: column-reverse;
} }
/* stylelint-disable-next-line selector-type-no-unknown */
footnote { footnote {
display: inline-flex; display: inline-flex;
position: relative; position: relative;
@ -276,7 +277,7 @@ footnote {
width: 0.8rem; width: 0.8rem;
height: 1em; height: 1em;
&:before { &::before {
content: ''; content: '';
position: absolute; position: absolute;
width: 10px; width: 10px;

View File

@ -9,6 +9,7 @@
.simplifiedEditorField { .simplifiedEditorField {
@include font-size(1.4rem); @include font-size(1.4rem);
min-height: 100px; min-height: 100px;
.emptyNode:first-child::before { .emptyNode:first-child::before {
@ -92,7 +93,7 @@
} }
&.isFocused { &.isFocused {
//background: red; // background: red;
.controls { .controls {
opacity: 1; opacity: 1;
bottom: 0; bottom: 0;
@ -111,7 +112,7 @@
&.bordered { &.bordered {
box-sizing: border-box; box-sizing: border-box;
padding: 16px 12px 6px 12px; padding: 16px 12px 6px;
border-radius: 2px; border-radius: 2px;
border: 2px solid var(--black-100); border: 2px solid var(--black-100);
background: var(--white-500); background: var(--white-500);

View File

@ -1,4 +1,4 @@
import { createEffect, 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 {
createEditorTransaction, createEditorTransaction,
@ -21,7 +21,6 @@ import { Modal } from '../Nav/Modal'
import { hideModal, showModal } from '../../stores/ui' import { hideModal, showModal } from '../../stores/ui'
import { Blockquote } from '@tiptap/extension-blockquote' import { Blockquote } from '@tiptap/extension-blockquote'
import { UploadModalContent } from './UploadModalContent' import { UploadModalContent } from './UploadModalContent'
import { imageProxy } from '../../utils/imageProxy'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './SimplifiedEditor.module.scss' import styles from './SimplifiedEditor.module.scss'
import { Placeholder } from '@tiptap/extension-placeholder' import { Placeholder } from '@tiptap/extension-placeholder'
@ -54,6 +53,7 @@ type Props = {
onlyBubbleControls?: boolean onlyBubbleControls?: boolean
controlsAlwaysVisible?: boolean controlsAlwaysVisible?: boolean
autoFocus?: boolean autoFocus?: boolean
isCancelButtonVisible?: boolean
} }
export const MAX_DESCRIPTION_LIMIT = 400 export const MAX_DESCRIPTION_LIMIT = 400
@ -62,6 +62,7 @@ const SimplifiedEditor = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const [counter, setCounter] = createSignal<number>() const [counter, setCounter] = createSignal<number>()
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
const wrapperEditorElRef: { const wrapperEditorElRef: {
current: HTMLElement current: HTMLElement
} = { } = {
@ -174,7 +175,7 @@ const SimplifiedEditor = (props: Props) => {
{ {
type: 'image', type: 'image',
attrs: { attrs: {
src: imageProxy(image.url) src: image.url
} }
} }
] ]
@ -328,7 +329,9 @@ const SimplifiedEditor = (props: Props) => {
</div> </div>
<Show when={!props.onChange}> <Show when={!props.onChange}>
<div class={styles.buttons}> <div class={styles.buttons}>
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} /> <Show when={isCancelButtonVisible()}>
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
</Show>
<Button <Button
value={props.submitButtonText ?? t('Send')} value={props.submitButtonText ?? t('Send')}
variant="primary" variant="primary"

View File

@ -6,11 +6,11 @@ import { createSignal, Show } from 'solid-js'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload' import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
import { handleFileUpload } from '../../../utils/handleFileUpload'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Loading } from '../../_shared/Loading' import { Loading } from '../../_shared/Loading'
import { verifyImg } from '../../../utils/verifyImg' import { verifyImg } from '../../../utils/verifyImg'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { handleImageUpload } from '../../../utils/handleImageUpload'
type Props = { type Props = {
onClose: (image?: UploadedFile) => void onClose: (image?: UploadedFile) => void
@ -24,10 +24,10 @@ export const UploadModalContent = (props: Props) => {
const [dragError, setDragError] = createSignal<string | undefined>() const [dragError, setDragError] = createSignal<string | undefined>()
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' }) const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
const runUpload = async (file) => { const runUpload = async (file: UploadFile) => {
try { try {
setIsUploading(true) setIsUploading(true)
const result = await handleFileUpload(file) const result = await handleImageUpload(file)
props.onClose(result) props.onClose(result)
setIsUploading(false) setIsUploading(false)
} catch (error) { } catch (error) {
@ -41,7 +41,7 @@ export const UploadModalContent = (props: Props) => {
try { try {
const data = await fetch(value) const data = await fetch(value)
const blob = await data.blob() const blob = await data.blob()
const file = await new File([blob], 'convertedFromUrl', { type: data.headers.get('Content-Type') }) const file = new File([blob], 'convertedFromUrl', { type: data.headers.get('Content-Type') })
const fileToUpload: UploadFile = { const fileToUpload: UploadFile = {
source: blob.toString(), source: blob.toString(),
name: file.name, name: file.name,
@ -55,7 +55,7 @@ export const UploadModalContent = (props: Props) => {
} }
const handleUpload = async () => { const handleUpload = async () => {
await selectFiles(async ([uploadFile]) => { selectFiles(async ([uploadFile]) => {
await runUpload(uploadFile) await runUpload(uploadFile)
}) })
} }
@ -72,7 +72,7 @@ export const UploadModalContent = (props: Props) => {
} }
} }
}) })
const handleDrag = (event) => { const handleDrag = (event: MouseEvent) => {
if (event.type === 'dragenter' || event.type === 'dragover') { if (event.type === 'dragenter' || event.type === 'dragover') {
setDragActive(true) setDragActive(true)
} else if (event.type === 'dragleave') { } else if (event.type === 'dragleave') {
@ -80,6 +80,15 @@ export const UploadModalContent = (props: Props) => {
} }
} }
const handleValidate = async (value: string) => {
const validationResult = await verifyImg(value)
if (!validationResult) {
return t('Invalid image URL')
}
return ''
}
return ( return (
<div class={styles.uploadModalContent}> <div class={styles.uploadModalContent}>
<Show when={!isUploading()} fallback={<Loading />}> <Show when={!isUploading()} fallback={<Loading />}>
@ -113,7 +122,7 @@ export const UploadModalContent = (props: Props) => {
hideModal() hideModal()
props.onClose() props.onClose()
}} }}
validate={async (value) => ((await verifyImg(value)) ? '' : t('Invalid image URL'))} validate={handleValidate}
onSubmit={handleImageFormSubmit} onSubmit={handleImageFormSubmit}
/> />
</div> </div>

View File

@ -79,6 +79,7 @@ export const Footnote = Node.create({
}, },
deleteFootnote: deleteFootnote:
() => () =>
// eslint-disable-next-line unicorn/consistent-function-scoping
({ tr, state }) => { ({ tr, state }) => {
const { selection } = state const { selection } = state
const { $from, $to } = selection const { $from, $to } = selection

View File

@ -12,7 +12,7 @@
} }
&:hover { &:hover {
.shoutCardCover img { .shoutCardCover {
transform: scale(1.05); transform: scale(1.05);
} }
} }
@ -89,6 +89,7 @@
} }
.shoutCardCoverContainer { .shoutCardCoverContainer {
overflow: hidden;
position: relative; position: relative;
} }
@ -99,13 +100,13 @@
overflow: hidden; overflow: hidden;
padding-bottom: 56.2%; padding-bottom: 56.2%;
position: relative; position: relative;
transform-origin: 50% 50%;
transition: transform 1s ease-in-out;
img { img {
height: 100%; height: 100%;
object-fit: cover; object-fit: cover;
position: absolute; position: absolute;
transform-origin: 50% 50%;
transition: transform 1s ease-in-out;
width: 100%; width: 100%;
} }
@ -117,6 +118,7 @@
.shoutAuthor { .shoutAuthor {
@include font-size(1.4rem); @include font-size(1.4rem);
font-weight: 500; font-weight: 500;
margin-right: 1.6rem; margin-right: 1.6rem;
@ -133,9 +135,10 @@
} }
.shoutDate { .shoutDate {
@include font-size(1.2rem);
color: #9fa1a7; color: #9fa1a7;
font-weight: 500; font-weight: 500;
@include font-size(1.2rem);
} }
.shoutDetails { .shoutDetails {
@ -154,6 +157,7 @@
.shoutCardTitle { .shoutCardTitle {
@include font-size(2.2rem); @include font-size(2.2rem);
font-weight: 700; font-weight: 700;
line-height: 1.2; line-height: 1.2;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
@ -174,6 +178,7 @@
.shoutCardTitlesContainerFeedMode & { .shoutCardTitlesContainerFeedMode & {
@include font-size(3.2rem); @include font-size(3.2rem);
line-height: 1.1; line-height: 1.1;
} }
} }
@ -187,6 +192,7 @@
.shoutCardSubtitle { .shoutCardSubtitle {
@include font-size(1.8rem); @include font-size(1.8rem);
color: #141414; color: #141414;
font-weight: 400; font-weight: 400;
line-height: 1.3; line-height: 1.3;
@ -398,7 +404,6 @@
.shoutCardWithCover { .shoutCardWithCover {
aspect-ratio: 16/9; aspect-ratio: 16/9;
//padding: 0 2.4rem;
width: 100%; width: 100%;
@include media-breakpoint-down(xl) { @include media-breakpoint-down(xl) {
@ -407,9 +412,21 @@
padding-top: 30%; padding-top: 30%;
} }
&.swiper-slide { swiper-slide & {
.shoutCardContent { margin-bottom: 0;
@include media-breakpoint-down(md) {
@include media-breakpoint-down(lg) {
aspect-ratio: 1/1;
}
@include media-breakpoint-down(md) {
aspect-ratio: 1/1.5;
}
@include media-breakpoint-down(sm) {
aspect-ratio: 1/1;
.shoutCardContent {
padding-left: 10%; padding-left: 10%;
} }
} }
@ -467,9 +484,11 @@
&::after { &::after {
background: rgb(0 0 0 / 60%); background: rgb(0 0 0 / 60%);
content: ''; content: '';
height: 100%; height: 102%;
left: -1%;
position: absolute; position: absolute;
width: 100%; top: -1%;
width: 102%;
z-index: 1; z-index: 1;
} }
} }
@ -573,6 +592,12 @@
} }
} }
.shoutCardDetailsItemLabel {
@include media-breakpoint-down(sm) {
display: none;
}
}
.shoutCardComments, .shoutCardComments,
.shoutCardDetailsViewed { .shoutCardDetailsViewed {
align-items: center; align-items: center;
@ -589,6 +614,7 @@
.shoutCardLinkContainer { .shoutCardLinkContainer {
background: var(--default-color); background: var(--default-color);
color: var(--link-hover-color);
} }
} }
} }
@ -705,6 +731,7 @@
.shoutCardTitle, .shoutCardTitle,
.shoutCardSubtitle { .shoutCardSubtitle {
@include font-size(1.8rem); @include font-size(1.8rem);
display: inline; display: inline;
} }

View File

@ -1,22 +1,22 @@
import { createMemo, createSignal, For, Show } from 'solid-js' import { createMemo, createSignal, For, Show } from 'solid-js'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../../graphql/types.gen'
import { Icon } from '../_shared/Icon' import { capitalize } from '../../../utils/capitalize'
import styles from './ArticleCard.module.scss' import { Icon } from '../../_shared/Icon'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { CardTopic } from './CardTopic' import { CardTopic } from '../CardTopic'
import { ShoutRatingControl } from '../Article/ShoutRatingControl' import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { getShareUrl, SharePopup } from '../Article/SharePopup' import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import stylesHeader from '../Nav/Header/Header.module.scss' import { getDescription } from '../../../utils/meta'
import { getDescription } from '../../utils/meta' import { FeedArticlePopup } from '../FeedArticlePopup'
import { FeedArticlePopup } from './FeedArticlePopup' import { useLocalize } from '../../../context/localize'
import { useLocalize } from '../../context/localize'
import { getPagePath, openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../../stores/router'
import { imageProxy } from '../../utils/imageProxy' import { Popover } from '../../_shared/Popover'
import { Popover } from '../_shared/Popover' import { Image } from '../../_shared/Image'
import { AuthorCard } from '../Author/AuthorCard' import { useSession } from '../../../context/session'
import { useSession } from '../../context/session' import { AuthorLink } from '../../Author/AhtorLink'
import { capitalize } from '../../utils/capitalize' import stylesHeader from '../../Nav/Header/Header.module.scss'
import styles from './ArticleCard.module.scss'
interface ArticleCardProps { interface ArticleCardProps {
settings?: { settings?: {
@ -118,7 +118,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutCardCoverContainer}> <div class={styles.shoutCardCoverContainer}>
<div class={styles.shoutCardCover}> <div class={styles.shoutCardCover}>
<Show when={props.article.cover}> <Show when={props.article.cover}>
<img src={imageProxy(props.article.cover)} alt={title || ''} loading="lazy" /> <Image src={props.article.cover} alt={title} width={1200} />
</Show> </Show>
</div> </div>
</div> </div>
@ -158,7 +158,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode [styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
})} })}
> >
<a href={`/${props.article.slug || ''}`}> <a href={getPagePath(router, 'article', { slug: props.article.slug })}>
<div class={styles.shoutCardTitle}> <div class={styles.shoutCardTitle}>
<span class={styles.shoutCardLinkWrapper}> <span class={styles.shoutCardLinkWrapper}>
<span class={styles.shoutCardLinkContainer}>{title}</span> <span class={styles.shoutCardLinkContainer}>{title}</span>
@ -180,17 +180,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
<div class={styles.shoutAuthor}> <div class={styles.shoutAuthor}>
<For each={props.article.authors}> <For each={props.article.authors}>
{(author) => { {(author) => {
return ( return <AuthorLink size={'XS'} author={author} />
<AuthorCard
author={author}
hideWriteButton={true}
hideBio={true}
hideFollow={true}
truncateBio={true}
isFeedMode={true}
hasLink={!props.settings?.noAuthorLink}
/>
)
}} }}
</For> </For>
</div> </div>
@ -200,10 +190,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
</Show> </Show>
</div> </div>
</Show> </Show>
<Show when={props.article.description}>
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
</Show>
<Show when={props.settings?.isFeedMode}> <Show when={props.settings?.isFeedMode}>
<Show when={props.article.description}>
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
</Show>
<Show when={!props.settings?.noimage && props.article.cover}> <Show when={!props.settings?.noimage && props.article.cover}>
<div class={styles.shoutCardCoverContainer}> <div class={styles.shoutCardCoverContainer}>
<Show <Show
@ -221,7 +211,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</div> </div>
</Show> </Show>
<div class={styles.shoutCardCover}> <div class={styles.shoutCardCover}>
<img src={imageProxy(props.article.cover)} alt={title || ''} loading="lazy" /> <Image src={props.article.cover} alt={title} width={1200} loading="lazy" />
</div> </div>
</div> </div>
</Show> </Show>
@ -240,9 +230,16 @@ export const ArticleCard = (props: ArticleCardProps) => {
name="comment-hover" name="comment-hover"
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)} class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
/> />
<span class={styles.shoutCardLinkContainer}> <Show
{props.article.stat?.commented || t('Add comment')} when={props.article.stat?.commented}
</span> fallback={
<span class={clsx(styles.shoutCardLinkContainer, styles.shoutCardDetailsItemLabel)}>
{t('Add comment')}
</span>
}
>
{props.article.stat?.commented}
</Show>
</a> </a>
</div> </div>

View File

@ -0,0 +1 @@
export { ArticleCard } from './ArticleCard'

View File

@ -33,7 +33,7 @@ export const Beside = (props: Props) => {
<Show when={!!props.values}> <Show when={!!props.values}>
<div <div
class={clsx( class={clsx(
'col-md-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)}`
] ]
@ -95,7 +95,7 @@ export const Beside = (props: Props) => {
</ul> </ul>
</div> </div>
</Show> </Show>
<div class={clsx('col-md-16', styles.shoutCardContainer)}> <div class={clsx('col-lg-16', styles.shoutCardContainer)}>
<ArticleCard <ArticleCard
article={props.beside} article={props.beside}
settings={{ isBigTitle: true, isBeside: true, nodate: props.nodate }} settings={{ isBigTitle: true, isBeside: true, nodate: props.nodate }}

View File

@ -3,8 +3,9 @@
border: 1px solid rgb(0 0 0 / 15%); border: 1px solid rgb(0 0 0 / 15%);
border-radius: 1.6rem; border-radius: 1.6rem;
padding: 1.6rem !important; padding: 1.6rem !important;
text-align: left;
@include media-breakpoint-between(sm, md) { @include media-breakpoint-down(md) {
left: auto !important; left: auto !important;
right: 0; right: 0;
transform: none !important; transform: none !important;
@ -13,6 +14,8 @@
button { button {
font-size: inherit; font-size: inherit;
font-weight: 500; font-weight: 500;
text-align: left;
white-space: nowrap;
&:hover { &:hover {
background: #000; background: #000;

View File

@ -16,7 +16,7 @@
} }
.sidebarItemName { .sidebarItemName {
align-items: baseline; align-items: center;
display: flex; display: flex;
position: relative; position: relative;
@ -26,6 +26,10 @@
} }
} }
.userpic {
margin-right: 1.2rem;
}
.selected { .selected {
font-weight: 700; font-weight: 700;
} }
@ -121,6 +125,7 @@
h4 { h4 {
@include font-size(1.2rem); @include font-size(1.2rem);
font-weight: bold; font-weight: bold;
color: #9fa1a7; color: #9fa1a7;
cursor: pointer; cursor: pointer;

View File

@ -1,8 +1,5 @@
import { createSignal, For, Show } from 'solid-js' import { createSignal, For, Show } from 'solid-js'
import type { Author } from '../../../graphql/types.gen'
import { useAuthorsStore } from '../../../stores/zine/authors'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { useTopicsStore } from '../../../stores/zine/topics'
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 { useSession } from '../../../context/session'
@ -13,18 +10,12 @@ import { Userpic } from '../../Author/Userpic'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
type FeedSidebarProps = { export const Sidebar = () => {
authors: Author[]
}
export const Sidebar = (props: FeedSidebarProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { seen } = useSeenStore() const { seen } = useSeenStore()
const { session } = useSession() const { subscriptions } = useSession()
const { page } = useRouter() const { page } = useRouter()
const { authorEntities } = useAuthorsStore({ authors: props.authors })
const { articlesByTopic } = useArticlesStore() const { articlesByTopic } = useArticlesStore()
const { topicEntities } = useTopicsStore()
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true) const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
const checkTopicIsSeen = (topicSlug: string) => { const checkTopicIsSeen = (topicSlug: string) => {
@ -47,7 +38,7 @@ export const Sidebar = (props: FeedSidebarProps) => {
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
<Icon name="feed-all" class={styles.icon} /> <Icon name="feed-all" class={styles.icon} />
{t('general feed')} {t('All')}
</span> </span>
</a> </a>
</li> </li>
@ -73,7 +64,7 @@ export const Sidebar = (props: FeedSidebarProps) => {
> >
<span class={styles.sidebarItemName}> <span class={styles.sidebarItemName}>
<Icon name="feed-collaborate" class={styles.icon} /> <Icon name="feed-collaborate" class={styles.icon} />
{t('Accomplices')} {t('Participation')}
</span> </span>
</a> </a>
</li> </li>
@ -118,7 +109,7 @@ export const Sidebar = (props: FeedSidebarProps) => {
</li> </li>
</ul> </ul>
<Show when={session()?.news?.authors || session()?.news?.topics}> <Show when={subscriptions().authors.length > 0 || subscriptions().topics.length > 0}>
<h4 <h4
classList={{ [styles.opened]: isSubscriptionsVisible() }} classList={{ [styles.opened]: isSubscriptionsVisible() }}
onClick={() => { onClick={() => {
@ -129,39 +120,31 @@ export const Sidebar = (props: FeedSidebarProps) => {
</h4> </h4>
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}> <ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
<For each={session()?.news?.authors}> <For each={subscriptions().authors}>
{(authorSlug: string) => ( {(author) => (
<li> <li>
<a <a
href={`/author/${authorSlug}`} href={`/author/${author.slug}`}
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }} classList={{ [styles.unread]: checkAuthorIsSeen(author.slug) }}
> >
<div class={styles.sidebarItemName}> <div class={styles.sidebarItemName}>
<Show when={authorEntities()[authorSlug]}> <Userpic name={author.name} userpic={author.userpic} size="XS" class={styles.userpic} />
<Userpic {author.name}
name={authorEntities()[authorSlug].name}
userpic={authorEntities()[authorSlug].userpic}
/>
</Show>
<Show when={!authorEntities()[authorSlug]}>
<Icon name="hash" class={styles.icon} />
</Show>
{authorEntities()[authorSlug]?.name}
</div> </div>
</a> </a>
</li> </li>
)} )}
</For> </For>
<For each={session()?.news?.topics}> <For each={subscriptions().topics}>
{(topicSlug: string) => ( {(topic) => (
<li> <li>
<a <a
href={`/topic/${topicSlug}`} href={`/topic/${topic.slug}`}
classList={{ [styles.unread]: checkTopicIsSeen(topicSlug) }} classList={{ [styles.unread]: checkTopicIsSeen(topic.slug) }}
> >
<div class={styles.sidebarItemName}> <div class={styles.sidebarItemName}>
<Icon name="hash" class={styles.icon} /> <Icon name="hash" class={styles.icon} />
{topicEntities()[topicSlug]?.title ?? topicSlug} {topic.title}
</div> </div>
</a> </a>
</li> </li>

View File

@ -2,7 +2,7 @@ import { Show, createMemo } from 'solid-js'
import './DialogCard.module.scss' import './DialogCard.module.scss'
import styles from './DialogAvatar.module.scss' import styles from './DialogAvatar.module.scss'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { imageProxy } from '../../utils/imageProxy' import { getImageUrl } from '../../utils/getImageUrl'
type Props = { type Props = {
name: string name: string
@ -47,7 +47,10 @@ const DialogAvatar = (props: Props) => {
style={{ 'background-color': `${randomBg()}` }} style={{ 'background-color': `${randomBg()}` }}
> >
<Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}> <Show when={Boolean(props.url)} fallback={<div class={styles.letter}>{nameFirstLetter()}</div>}>
<div class={styles.imageHolder} style={{ 'background-image': `url(${imageProxy(props.url)})` }} /> <div
class={styles.imageHolder}
style={{ 'background-image': `url(${getImageUrl(props.url, { width: 40, height: 40 })})` }}
/>
</Show> </Show>
</div> </div>
) )

View File

@ -5,6 +5,7 @@ import GroupDialogAvatar from './GroupDialogAvatar'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import styles from './DialogCard.module.scss' import styles from './DialogCard.module.scss'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { AuthorBadge } from '../Author/AuthorBadge'
type DialogProps = { type DialogProps = {
online?: boolean online?: boolean
@ -40,27 +41,41 @@ const DialogCard = (props: DialogProps) => {
})} })}
onClick={props.onClick} onClick={props.onClick}
> >
<div class={styles.avatar}> <Switch
<Switch fallback={<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />}> fallback={
<Match when={props.members.length >= 3}> <Show
when={props.isChatHeader}
fallback={
<div class={styles.avatar}>
<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />
</div>
}
>
<AuthorBadge nameOnly={true} author={props.members[0]} />
</Show>
}
>
<Match when={props.members.length >= 3}>
<div class={styles.avatar}>
<GroupDialogAvatar users={props.members} /> <GroupDialogAvatar users={props.members} />
</Match> </div>
</Switch> </Match>
</div> </Switch>
<div class={styles.row}>
<div class={styles.name}>
{companions()?.length > 1 ? t('Group Chat') : companions()[0]?.name}
</div>
<div class={styles.message}>
<Switch>
<Match when={props.message && !props.isChatHeader}>
<div innerHTML={props.message} />
</Match>
<Match when={props.isChatHeader && companions().length > 1}>{names()}</Match>
</Switch>
</div>
</div>
<Show when={!props.isChatHeader}> <Show when={!props.isChatHeader}>
<div class={styles.row}>
<div class={styles.name}>
{companions()?.length > 1 ? t('Group Chat') : companions()[0]?.name}
</div>
<div class={styles.message}>
<Switch>
<Match when={props.message}>
<div innerHTML={props.message} />
</Match>
<Match when={props.isChatHeader && companions().length > 1}>{names()}</Match>
</Switch>
</div>
</div>
<div class={styles.activity}> <div class={styles.activity}>
<Show when={props.lastUpdate}> <Show when={props.lastUpdate}>
<div class={styles.time}>{formatTime(new Date(props.lastUpdate * 1000))}</div> <div class={styles.time}>{formatTime(new Date(props.lastUpdate * 1000))}</div>

View File

@ -1,5 +1,6 @@
.Search { .Search {
flex: 1; flex: 1;
.field { .field {
position: relative; position: relative;
background: #fff; background: #fff;

View File

@ -4,7 +4,7 @@
position: relative; position: relative;
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
min-height: 710px; min-height: 600px;
} }
input { input {
@ -40,9 +40,9 @@
.authImage { .authImage {
@include font-size(1.5rem); @include font-size(1.5rem);
background: #141414 url('/auth-page.jpg') center no-repeat; background: var(--background-color-invert) url('/auth-page.jpg') center no-repeat;
background-size: cover; background-size: cover;
color: #fff; color: var(--default-color-invert);
display: flex; display: flex;
padding: 3em; padding: 3em;
position: relative; position: relative;
@ -69,7 +69,7 @@
z-index: 1; z-index: 1;
a { a {
color: #fff; color: var(--default-color-invert);
&:hover { &:hover {
color: rgb(255 255 255 / 70%); color: rgb(255 255 255 / 70%);
@ -87,20 +87,22 @@
.disclaimer { .disclaimer {
@include font-size(1.2rem); @include font-size(1.2rem);
color: #9fa1a7; color: #9fa1a7;
margin-bottom: 0; margin-bottom: 0;
a { a {
color: #fff !important; color: var(--default-color-invert) !important;
&:hover { &:hover {
color: rgb(255, 255, 255, 0.6) !important; color: rgb(255 255 255 / 60%) !important;
} }
} }
} }
.authActions { .authActions {
@include font-size(1.5rem); @include font-size(1.5rem);
margin-top: 1.6rem; margin-top: 1.6rem;
text-align: center; text-align: center;
@ -192,8 +194,8 @@
border-color: #d00820; border-color: #d00820;
&:hover { &:hover {
color: white; color: var(--default-color-invert);
border-color: black; border-color: var(--background-color-invert);
} }
} }
} }

View File

@ -2,18 +2,22 @@
position: relative; position: relative;
.confirmModalTitle { .confirmModalTitle {
@include font-size(2rem); @include font-size(3.2rem);
font-weight: 700; font-weight: 700;
color: var(--default-color); color: var(--default-color);
text-align: center; text-align: center;
@include media-breakpoint-up(sm) {
margin: 0 10%;
}
} }
.confirmModalActions { .confirmModalActions {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
margin-top: 4rem; margin-top: 4rem;
gap: 2rem; gap: 0.5rem;
.confirmAction { .confirmAction {
flex: 1; flex: 1;

View File

@ -76,6 +76,8 @@
height: 20px; height: 20px;
object-fit: contain; object-fit: contain;
object-position: left; object-position: left;
position: relative;
top: 0.1rem;
transition: height 0.2s; transition: height 0.2s;
vertical-align: middle; vertical-align: middle;
width: 100px; width: 100px;
@ -112,6 +114,7 @@
.mainNavigationWrapper { .mainNavigationWrapper {
@include font-size(1.7rem); @include font-size(1.7rem);
position: relative; position: relative;
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
@ -125,10 +128,11 @@
.mainNavigation { .mainNavigation {
font-size: 1.4rem !important; font-size: 1.4rem !important;
//margin: 0 0 0 -0.4rem !important;
opacity: 1; opacity: 1;
transition: opacity 0.3s; transition: opacity 0.3s;
// margin: 0 0 0 -0.4rem !important;
@include media-breakpoint-down(lg) { @include media-breakpoint-down(lg) {
background: var(--background-color); background: var(--background-color);
bottom: 0; bottom: 0;
@ -155,7 +159,7 @@
display: block; display: block;
font-size: 3.2rem !important; font-size: 3.2rem !important;
font-weight: bold; font-weight: bold;
margin: 0 0 5rem; margin: 0 0 4rem;
} }
li { li {
@ -192,14 +196,15 @@
} }
:global(.view-switcher) { :global(.view-switcher) {
margin-top: 0; margin: 0 -0.5rem;
overflow: hidden; overflow: hidden;
padding: 0;
} }
li { li {
margin-bottom: 0 !important; margin-bottom: 0 !important;
&:first-letter { &::first-letter {
text-transform: capitalize; text-transform: capitalize;
} }
} }
@ -224,60 +229,57 @@
} }
} }
.mainNavigationSocial a { .mainNavigationSocial {
display: flex; font-size: 2rem;
justify-content: space-between; font-weight: 500;
.mainNavigation .mainNavigationMobile & {
margin-bottom: 0 !important;
}
a {
align-items: center;
display: flex;
&:hover {
.icon {
filter: invert(1);
}
}
&:hover {
.icon { .icon {
filter: invert(1); height: 3.8rem;
margin-right: 0.3em;
width: 3.8rem;
} }
} }
}
.icon { .languageSelectorMobile {
height: 3.8rem; border: 2px solid #e8e8e8;
width: 3.8rem; border-radius: 1.6rem;
} display: block;
font-family: inherit;
font-size: 1.7rem;
height: 5.6rem;
margin-bottom: 5rem;
padding: 0 1.2rem;
width: 100%;
}
.mainNavigationAdditionalLinks {
border-top: 1px solid #ccc;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 1rem;
padding: 1.6rem 0 2rem;
} }
.mobileDescription { .mobileDescription {
color: #696969; color: #696969;
} }
.mobileSubscription {
margin-bottom: 5rem;
input[type='email'] {
background: #f7f7f8;
border: none;
border-radius: 1.6rem;
padding-right: 5.6rem;
&:not(:placeholder-shown) {
& ~ .mobileSubscriptionSubmit {
display: block;
}
}
}
}
.mobileSubscriptionSubmit {
aspect-ratio: 1/1;
display: none;
height: 100%;
position: absolute;
right: 0;
top: 0;
img {
aspect-ratio: 1/1;
left: 50%;
position: relative;
transform: translateX(-50%);
width: 16px !important;
}
}
.mainNavigationItemActive { .mainNavigationItemActive {
background: var(--link-hover-background) !important; background: var(--link-hover-background) !important;
color: var(--link-hover-color) !important; color: var(--link-hover-color) !important;
@ -293,9 +295,10 @@
.burgerContainer { .burgerContainer {
box-sizing: content-box; box-sizing: content-box;
display: inline-flex; display: inline-flex;
//float: right;
padding-left: 0; padding-left: 0;
// float: right;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
padding-left: divide($container-padding-x, 2); padding-left: divide($container-padding-x, 2);
} }
@ -307,7 +310,7 @@
.burger { .burger {
cursor: pointer; cursor: pointer;
height: 1.8rem; height: 1.6rem;
display: inline-block; display: inline-block;
position: relative; position: relative;
vertical-align: middle; vertical-align: middle;
@ -355,13 +358,13 @@
} }
&::after { &::after {
bottom: 0.8rem; bottom: 0.7rem;
transform: rotate(-45deg); transform: rotate(-45deg);
} }
&::before { &::before {
transform: rotate(45deg); transform: rotate(45deg);
top: 0.8rem; top: 0.7rem;
} }
} }
} }
@ -383,6 +386,7 @@
.articleHeader { .articleHeader {
@include font-size(1.4rem); @include font-size(1.4rem);
left: $container-padding-x; left: $container-padding-x;
margin: 0.2em 0; margin: 0.2em 0;
overflow: hidden; overflow: hidden;
@ -480,6 +484,28 @@
width: 100%; width: 100%;
} }
} }
.editorControl {
border-radius: 1.2em;
&:hover {
background: var(--background-color-invert);
}
}
.settingsControl {
border-radius: 100%;
padding: 0.8rem !important;
min-width: 4rem !important;
&:hover {
background: var(--background-color-invert);
img {
filter: invert(1);
}
}
}
} }
.userControlItem { .userControlItem {
@ -549,7 +575,7 @@
} }
.userControlItemVerbose { .userControlItemVerbose {
margin-left: 1.2em !important; margin-left: 0.9em !important;
&:first-child { &:first-child {
margin-left: 0 !important; margin-left: 0 !important;
@ -618,13 +644,14 @@
} }
.textLabel { .textLabel {
background-color: var(--link-hover-background);
color: var(--link-hover-color); color: var(--link-hover-color);
} }
} }
a:hover { a:hover {
//background-color: var(--link-hover-background) !important; .textLabel {
background-color: var(--link-hover-background);
}
} }
} }

View File

@ -12,7 +12,7 @@ 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 { useModalStore } from '../../../stores/ui'
import { router, useRouter } from '../../../stores/router' import { router, ROUTES, useRouter } from '../../../stores/router'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
@ -23,6 +23,7 @@ import styles from './Header.module.scss'
import { apiClient } from '../../../utils/apiClient' import { apiClient } from '../../../utils/apiClient'
import { RANDOM_TOPICS_COUNT } from '../../Views/Home' import { RANDOM_TOPICS_COUNT } from '../../Views/Home'
import { Link } from './Link' import { Link } from './Link'
import { Subscribe } from '../../_shared/Subscribe'
type Props = { type Props = {
title?: string title?: string
@ -37,11 +38,14 @@ type HeaderSearchParams = {
source?: string source?: string
} }
const handleSwitchLanguage = (event) => {
location.href = `${location.href}${location.href.includes('?') ? '&' : '?'}lng=${event.target.value}`
}
export const Header = (props: Props) => { export const Header = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { modal } = useModalStore() const { modal } = useModalStore()
const { page } = useRouter()
const { const {
actions: { requireAuthentication } actions: { requireAuthentication }
} = useSession() } = useSession()
@ -58,7 +62,6 @@ export const Header = (props: Props) => {
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
const [isZineVisible, setIsZineVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false)
const [isFeedVisible, setIsFeedVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false)
const toggleFixed = () => setFixed((oldFixed) => !oldFixed) const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
const tag = (topic: Topic) => const tag = (topic: Topic) =>
@ -147,6 +150,15 @@ export const Header = (props: Props) => {
setRandomTopics(topics) setRandomTopics(topics)
}) })
const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => {
if (!fixed()) {
return
}
event.preventDefault()
if (page().route === route) {
toggleFixed()
}
}
return ( return (
<header <header
class={styles.mainHeader} class={styles.mainHeader}
@ -195,6 +207,7 @@ export const Header = (props: Props) => {
routeName="home" routeName="home"
active={isZineVisible()} active={isZineVisible()}
body={t('journal')} body={t('journal')}
onClick={(event) => handleToggleMenuByLink(event, 'home')}
/> />
<Link <Link
onMouseOver={() => toggleSubnavigation(true, setIsFeedVisible)} onMouseOver={() => toggleSubnavigation(true, setIsFeedVisible)}
@ -202,6 +215,7 @@ export const Header = (props: Props) => {
routeName="feed" routeName="feed"
active={isFeedVisible()} active={isFeedVisible()}
body={t('feed')} body={t('feed')}
onClick={(event) => handleToggleMenuByLink(event, 'feed')}
/> />
<Link <Link
onMouseOver={() => toggleSubnavigation(true, setIsTopicsVisible)} onMouseOver={() => toggleSubnavigation(true, setIsTopicsVisible)}
@ -209,12 +223,14 @@ export const Header = (props: Props) => {
routeName="topics" routeName="topics"
active={isTopicsVisible()} active={isTopicsVisible()}
body={t('topics')} body={t('topics')}
onClick={(event) => handleToggleMenuByLink(event, 'topics')}
/> />
<Link <Link
onMouseOver={(event) => hideSubnavigation(event, 0)} onMouseOver={(event) => hideSubnavigation(event, 0)}
onMouseOut={(event) => hideSubnavigation(event, 0)} onMouseOut={(event) => hideSubnavigation(event, 0)}
routeName="authors" routeName="authors"
body={t('authors')} body={t('authors')}
onClick={(event) => handleToggleMenuByLink(event, 'authors')}
/> />
<Link <Link
onMouseOver={() => toggleSubnavigation(true, setIsKnowledgeBaseVisible)} onMouseOver={() => toggleSubnavigation(true, setIsKnowledgeBaseVisible)}
@ -222,20 +238,21 @@ export const Header = (props: Props) => {
routeName="guide" routeName="guide"
body={t('Knowledge base')} body={t('Knowledge base')}
active={isKnowledgeBaseVisible()} active={isKnowledgeBaseVisible()}
onClick={(event) => handleToggleMenuByLink(event, 'guide')}
/> />
</ul> </ul>
<div class={styles.mainNavigationMobile}> <div class={styles.mainNavigationMobile}>
<h4>{t('Join the community')}</h4> <h4>{t('Participating')}</h4>
<ul class="view-switcher"> <ul class="view-switcher">
<li> <li>
<a href="/create">{t('Create post')}</a> <a href="/create">{t('Create post')}</a>
</li> </li>
<li> <li>
<a href="/about/manifest#participation">{t('Support us')}</a> <a href="/connect">{t('Suggest an idea')}</a>
</li> </li>
<li> <li>
<a href="/about/help">{t('How to help')}</a> <a href="/about/help">{t('Support the project')}</a>
</li> </li>
</ul> </ul>
@ -243,52 +260,60 @@ export const Header = (props: Props) => {
<ul class="view-switcher"> <ul class="view-switcher">
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://www.instagram.com/discoursio/"> <a href="https://www.instagram.com/discoursio/">
Instagram
<Icon name="user-link-instagram" class={styles.icon} /> <Icon name="user-link-instagram" class={styles.icon} />
Instagram
</a> </a>
</li> </li>
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://facebook.com/discoursio"> <a href="https://facebook.com/discoursio">
Facebook
<Icon name="user-link-facebook" class={styles.icon} /> <Icon name="user-link-facebook" class={styles.icon} />
Facebook
</a> </a>
</li> </li>
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://twitter.com/discours_io"> <a href="https://twitter.com/discours_io">
Twitter
<Icon name="user-link-twitter" class={styles.icon} /> <Icon name="user-link-twitter" class={styles.icon} />
Twitter
</a> </a>
</li> </li>
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://t.me/discoursio"> <a href="https://t.me/discoursio">
Telegram
<Icon name="user-link-telegram" class={styles.icon} /> <Icon name="user-link-telegram" class={styles.icon} />
Telegram
</a> </a>
</li> </li>
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://dzen.ru/discoursio"> <a href="https://dzen.ru/discoursio">
Dzen
<Icon name="user-link-dzen" class={styles.icon} /> <Icon name="user-link-dzen" class={styles.icon} />
Dzen
</a> </a>
</li> </li>
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://vk.com/discoursio"> <a href="https://vk.com/discoursio">
VK
<Icon name="user-link-vk" class={styles.icon} /> <Icon name="user-link-vk" class={styles.icon} />
VK
</a> </a>
</li> </li>
</ul> </ul>
<h4>{t('Newsletter')}</h4> <h4>{t('Newsletter')}</h4>
<form action="." class={styles.mobileSubscription}> <Subscribe variant={'mobileSubscription'} />
<div class="pretty-form__item">
<input type="email" placeholder="Ваш email" id="subscription-email" /> <h4>{t('Language')}</h4>
<label for="subscription-email">{t('Your email')}</label> <select
<button class={styles.mobileSubscriptionSubmit}> class={styles.languageSelectorMobile}
<Icon name="arrow-right" /> onChange={handleSwitchLanguage}
</button> value={lang()}
</div> >
</form> <option value="ru">🇷🇺 Русский</option>
<option value="en">🇬🇧 English</option>
</select>
<div class={styles.mainNavigationAdditionalLinks}>
<a href="/about/dogma">{t('Dogma')}</a>
<a href="/about/discussion-rules" innerHTML={t('Discussion rules')} />
<a href="/about/principles">{t('Principles')}</a>
</div>
<p <p
class={styles.mobileDescription} class={styles.mobileDescription}
@ -298,7 +323,6 @@ export const Header = (props: Props) => {
/> />
<div class={styles.mobileCopyright}> <div class={styles.mobileCopyright}>
{t('Discours')} &copy; 2015&ndash;{new Date().getFullYear()}{' '} {t('Discours')} &copy; 2015&ndash;{new Date().getFullYear()}{' '}
<a href="/">{t('Terms of use')}</a>
</div> </div>
</div> </div>
</div> </div>
@ -382,31 +406,31 @@ export const Header = (props: Props) => {
<a href="/expo">{t('Art')}</a> <a href="/expo">{t('Art')}</a>
</li> </li>
<li class="item"> <li class="item">
<a href="/podcasts">Подкасты</a> <a href="/podcasts">{t('Podcasts')}</a>
</li> </li>
<li class="item"> <li class="item">
<a href="">Спецпроекты</a> <a href="">{t('Special Projects')}</a>
</li> </li>
<li> <li>
<a href="/topic/interview">#Интервью</a> <a href="/topic/interview">#{t('Interview')}</a>
</li> </li>
<li> <li>
<a href="/topic/reportage">#Репортажи</a> <a href="/topic/reportage">#{t('Reports')}</a>
</li> </li>
<li> <li>
<a href="/topic/empiric">#Личный опыт</a> <a href="/topic/empiric">#{t('Experience')}</a>
</li> </li>
<li> <li>
<a href="/topic/society">#Общество</a> <a href="/topic/society">#{t('Society')}</a>
</li> </li>
<li> <li>
<a href="/topic/culture">#Культура</a> <a href="/topic/culture">#{t('Culture')}</a>
</li> </li>
<li> <li>
<a href="/topic/theory">#Теории</a> <a href="/topic/theory">#{t('Theory')}</a>
</li> </li>
<li> <li>
<a href="/topic/poetry">#Поэзия</a> <a href="/topic/poetry">#{t('Poetry')}</a>
</li> </li>
<li class={styles.rightItem}> <li class={styles.rightItem}>
<a href="/topics"> <a href="/topics">
@ -455,7 +479,7 @@ export const Header = (props: Props) => {
<a href={getPagePath(router, 'feed')}> <a href={getPagePath(router, 'feed')}>
<span class={styles.subnavigationItemName}> <span class={styles.subnavigationItemName}>
<Icon name="feed-all" class={styles.icon} /> <Icon name="feed-all" class={styles.icon} />
{t('general feed')} {t('All')}
</span> </span>
</a> </a>
</li> </li>
@ -472,7 +496,7 @@ export const Header = (props: Props) => {
<a href={getPagePath(router, 'feedCollaborations')}> <a href={getPagePath(router, 'feedCollaborations')}>
<span class={styles.subnavigationItemName}> <span class={styles.subnavigationItemName}>
<Icon name="feed-collaborate" class={styles.icon} /> <Icon name="feed-collaborate" class={styles.icon} />
{t('Accomplices')} {t('Participation')}
</span> </span>
</a> </a>
</li> </li>

View File

@ -10,13 +10,17 @@ type Props = {
routeName?: keyof typeof ROUTES routeName?: keyof typeof ROUTES
body: string body: string
active?: boolean active?: boolean
onClick?: (event: MouseEvent) => void
} }
export const Link = (props: Props) => { export const Link = (props: Props) => {
const { page } = useRouter() const { page } = useRouter()
const isSelected = page().route === props.routeName const isSelected = page().route === props.routeName
return ( return (
<li classList={{ 'view-switcher__item--selected': page().route === props.routeName }}> <li
onClick={props.onClick}
classList={{ 'view-switcher__item--selected': page().route === props.routeName }}
>
<ConditionalWrapper <ConditionalWrapper
condition={!isSelected && Boolean(props.routeName)} condition={!isSelected && Boolean(props.routeName)}
wrapper={(children) => <a href={getPagePath(router, props.routeName)}>{children}</a>} wrapper={(children) => <a href={getPagePath(router, props.routeName)}>{children}</a>}

View File

@ -86,8 +86,9 @@ export const HeaderAuth = (props: Props) => {
fallback={ fallback={
<Button <Button
value={<span class={styles.textLabel}>{buttonProps.value}</span>} value={<span class={styles.textLabel}>{buttonProps.value}</span>}
variant={'outline'} variant={'light'}
onClick={buttonProps.action} onClick={buttonProps.action}
class={styles.editorControl}
/> />
} }
> >
@ -95,9 +96,10 @@ export const HeaderAuth = (props: Props) => {
{(ref) => ( {(ref) => (
<Button <Button
ref={ref} ref={ref}
variant={'outline'} variant={'light'}
onClick={buttonProps.action} onClick={buttonProps.action}
value={<Icon name={buttonProps.icon} class={styles.icon} />} value={<Icon name={buttonProps.icon} class={styles.icon} />}
class={styles.editorControl}
/> />
)} )}
</Popover> </Popover>
@ -120,12 +122,14 @@ export const HeaderAuth = (props: Props) => {
</div> </div>
</Show> </Show>
<div class={styles.userControlItem}> <Show when={!isSaveButtonVisible()}>
<a href="#"> <div class={styles.userControlItem}>
<Icon name="search" class={styles.icon} /> <a href="#">
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="search" class={styles.icon} />
</a> <Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
</div> </a>
</div>
</Show>
<Show when={isNotificationsVisible()}> <Show when={isNotificationsVisible()}>
<div class={styles.userControlItem} onClick={handleBellIconClick}> <div class={styles.userControlItem} onClick={handleBellIconClick}>
@ -140,6 +144,15 @@ export const HeaderAuth = (props: Props) => {
</div> </div>
</Show> </Show>
<Show when={!session()}>
<div class={styles.userControlItem} onClick={handleBellIconClick}>
<div class={styles.button}>
<Icon name="bell-white" counter={1} class={styles.icon} />
<Icon name="bell-white-hover" counter={1} class={clsx(styles.icon, styles.iconHover)} />
</div>
</div>
</Show>
<Show when={isSaveButtonVisible()}> <Show when={isSaveButtonVisible()}>
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}> <div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
{renderIconedButton({ {renderIconedButton({
@ -163,8 +176,9 @@ export const HeaderAuth = (props: Props) => {
<Button <Button
ref={ref} ref={ref}
value={<Icon name="burger" />} value={<Icon name="burger" />}
variant={'outline'} variant={'light'}
onClick={handleBurgerButtonClick} onClick={handleBurgerButtonClick}
class={styles.settingsControl}
/> />
)} )}
</Popover> </Popover>
@ -182,19 +196,17 @@ export const HeaderAuth = (props: Props) => {
</div> </div>
} }
> >
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}> <Show when={!isSaveButtonVisible()}>
<a href="/inbox"> <div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
{/*FIXME: replace with route*/} <a href="/inbox">
<div classList={{ entered: page().path === '/inbox' }}> {/*FIXME: replace with route*/}
<Icon name="inbox-white" counter={session()?.news?.unread || 0} class={styles.icon} /> <div classList={{ entered: page().path === '/inbox' }}>
<Icon <Icon name="inbox-white" class={styles.icon} />
name="inbox-white-hover" <Icon name="inbox-white-hover" class={clsx(styles.icon, styles.iconHover)} />
counter={session()?.news?.unread || 0} </div>
class={clsx(styles.icon, styles.iconHover)} </a>
/> </div>
</div> </Show>
</a>
</div>
<ProfilePopup <ProfilePopup
onVisibilityChange={(isVisible) => { onVisibilityChange={(isVisible) => {
props.setIsProfilePopupVisible(isVisible) props.setIsProfilePopupVisible(isVisible)
@ -205,6 +217,7 @@ export const HeaderAuth = (props: Props) => {
<button class={styles.button}> <button class={styles.button}>
<div classList={{ entered: page().path === `/${session().user?.slug}` }}> <div classList={{ entered: page().path === `/${session().user?.slug}` }}>
<Userpic <Userpic
size={'M'}
name={session().user.name} name={session().user.name}
userpic={session().user.userpic} userpic={session().user.userpic}
class={styles.userpic} class={styles.userpic}

View File

@ -63,17 +63,21 @@
width: 100%; width: 100%;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {
max-width: 460px; max-width: 600px;
} }
@include media-breakpoint-up(md) { @include media-breakpoint-up(md) {
width: 50%; width: 65%;
} }
.close { .close {
right: 1.6rem; right: 1.6rem;
top: 1.6rem; top: 1.6rem;
} }
.modalInner {
padding: 0;
}
} }
} }

View File

@ -1,50 +0,0 @@
import { AuthorCard } from '../Author/AuthorCard'
import type { Author } from '../../graphql/types.gen'
import { translit } from '../../utils/ru2en'
import { hideModal } from '../../stores/ui'
import { createMemo, For } from 'solid-js'
import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize'
export const ProfileModal = () => {
const {
session,
actions: { signOut }
} = useSession()
const quit = () => {
signOut()
hideModal()
}
const { t, lang } = useLocalize()
const author = createMemo<Author>(() => {
const a: Author = {
id: null,
name: 'anonymous',
userpic: '',
slug: ''
}
if (session()?.user?.slug) {
const u = session().user
a.name = lang() === 'ru' ? u.name : translit(u.name)
a.slug = u.slug
a.userpic = u.userpic
}
return a
})
// TODO: ProfileModal markup and styles
return (
<div class="row view profile">
<h1>{session()?.user?.username}</h1>
<AuthorCard author={author()} />
<div class="profile-bio">{session()?.user?.bio || ''}</div>
<For each={session()?.user?.links || []}>{(l: string) => <a href={l}>{l}</a>}</For>
<span onClick={quit}>{t('Quit')}</span>
</div>
)
}

View File

@ -8,12 +8,25 @@
.navigation { .navigation {
@include font-size(1.4rem); @include font-size(1.4rem);
@include media-breakpoint-down(md) {
display: flex;
li {
margin-right: 2.4rem;
}
}
a {
border: none;
}
.active { .active {
a { a {
text-decoration: none; border-bottom: 2px solid;
color: var(--default-color-invert);
background: var(--background-color-invert);
cursor: inherit; cursor: inherit;
font-weight: bold;
pointer-events: none;
text-decoration: none;
} }
} }
} }

View File

@ -2,15 +2,28 @@
min-height: 2px; min-height: 2px;
background-color: var(--default-color); background-color: var(--default-color);
color: #fff; color: #fff;
font-size: 2rem;
font-weight: 500;
transition: background-color 0.3s; transition: background-color 0.3s;
&.error { &.error {
background-color: #d00820; background-color: #d00820;
} }
&.success {
.icon {
height: 1.8em;
margin-right: 0.5em;
margin-top: 0.1em;
width: 1.8em;
}
}
} }
.content { .content {
transition: height 0.3s, color 0.3s; transition:
height 0.3s,
color 0.3s;
height: 60px; height: 60px;
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -3,6 +3,7 @@ import { useSnackbar } from '../../context/snackbar'
import styles from './Snackbar.module.scss' import styles from './Snackbar.module.scss'
import { Transition } from 'solid-transition-group' import { Transition } from 'solid-transition-group'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Icon } from '../_shared/Icon'
export const Snackbar = () => { export const Snackbar = () => {
const { snackbarMessage } = useSnackbar() const { snackbarMessage } = useSnackbar()
@ -10,7 +11,8 @@ export const Snackbar = () => {
return ( return (
<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'
})} })}
> >
<Transition <Transition
@ -19,7 +21,12 @@ export const Snackbar = () => {
onExit={(el, done) => setTimeout(() => done(), 300)} onExit={(el, done) => setTimeout(() => done(), 300)}
> >
<Show when={snackbarMessage()}> <Show when={snackbarMessage()}>
<div class={styles.content}>{snackbarMessage().body}</div> <div class={styles.content}>
<Show when={snackbarMessage()?.type === 'success'}>
<Icon name="check-success" class={styles.icon} />
</Show>
{snackbarMessage().body}
</div>
</Show> </Show>
</Transition> </Transition>
</div> </div>

View File

@ -17,31 +17,31 @@ export const Topics = () => {
</a> </a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/podcasts">Подкасты</a> <a href="/podcasts">{t('Podcasts')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="">Спецпроекты</a> <a href="">{t('Special Projects')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/interview">#Интервью</a> <a href="/topic/interview">#{t('Interview')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/reportage">#Репортажи</a> <a href="/topic/reportage">#{t('Reports')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/empiric">#Личный опыт</a> <a href="/topic/empiric">#{t('Experience')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/society">#Общество</a> <a href="/topic/society">#{t('Society')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/culture">#Культура</a> <a href="/topic/culture">#{t('Culture')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/theory">#Теории</a> <a href="/topic/theory">#{t('Theory')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/poetry">#Поэзия</a> <a href="/topic/poetry">#{t('Poetry')}</a>
</li> </li>
<li class={clsx(styles.item, styles.right)}> <li class={clsx(styles.item, styles.right)}>
<a href={getPagePath(router, 'topics')}> <a href={getPagePath(router, 'topics')}>

View File

@ -5,6 +5,7 @@
font-size: 15px; font-size: 15px;
line-height: 24px; line-height: 24px;
white-space: pre-line; white-space: pre-line;
padding: 4rem 0;
} }
.title { .title {

View File

@ -1,17 +1,17 @@
.NotificationView { .NotificationView {
@include font-size(1.5rem);
display: flex; display: flex;
align-items: center; align-items: flex-start;
height: 72px; min-height: 72px;
margin-left: -16px; margin-left: -16px;
border-radius: 16px; border-radius: 16px;
padding: 16px; padding: 16px;
background-color: var(--yellow-50); background-color: var(--yellow-50);
// TODO: check markup
font-size: 15px;
// font-weight: 700;
line-height: 20px; line-height: 20px;
cursor: pointer; cursor: pointer;
transition: background-color 100ms; transition: background-color 100ms;
max-width: 700px;
&.seen { &.seen {
background-color: transparent; background-color: transparent;
@ -30,7 +30,8 @@
} }
.userpic { .userpic {
margin-right: 15px; min-width: 40px;
margin-right: 1rem;
} }
.timeContainer { .timeContainer {

View File

@ -18,7 +18,8 @@ type Props = {
} }
// NOTE: not a graphql generated type // NOTE: not a graphql generated type
export enum NotificationType {
export enum NewNotificationType {
NewComment = 'NEW_COMMENT', NewComment = 'NEW_COMMENT',
NewReply = 'NEW_REPLY', NewReply = 'NEW_REPLY',
NewFollower = 'NEW_FOLLOWER', NewFollower = 'NEW_FOLLOWER',
@ -27,6 +28,13 @@ export enum NotificationType {
NewDislike = 'NEW_DISLIKE' NewDislike = 'NEW_DISLIKE'
} }
export type NotificationUser = {
id: number
name: string
slug: string
userpic: string
}
const TEMPLATES = { const TEMPLATES = {
// FIXME: set proper templates // FIXME: set proper templates
'follower:join': 'new follower', 'follower:join': 'new follower',
@ -54,7 +62,7 @@ export const NotificationView = (props: Props) => {
actions: { markNotificationAsRead } actions: { markNotificationAsRead }
} = useNotifications() } = useNotifications()
const [data, setData] = createSignal<SSEMessage>(null) const [data, setData] = createSignal<SSEMessage>(null)
const [kind, setKind] = createSignal<NotificationType>() const [kind, setKind] = createSignal<NewNotificationType>()
const { changeSearchParam } = useRouter<ArticlePageSearchParams>() const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
const { t, formatDate, formatTime } = useLocalize() const { t, formatDate, formatTime } = useLocalize()
@ -68,7 +76,7 @@ export const NotificationView = (props: Props) => {
if (!data()) { if (!data()) {
return null return null
} }
let caption: string, author: Author, ntype: NotificationType let caption: string, author: Author, ntype: NewNotificationType
// TODO: count occurencies from in-browser notifications-db // TODO: count occurencies from in-browser notifications-db
@ -76,17 +84,19 @@ export const NotificationView = (props: Props) => {
case 'follower': { case 'follower': {
caption = '' caption = ''
author = data().payload author = data().payload
ntype = NotificationType.NewFollower ntype = NewNotificationType.NewFollower
break break
} }
case 'shout': { case 'shout':
caption = data().payload.title {
author = data().payload.authors[-1] caption = data().payload.title
ntype = NotificationType.NewShout author = data().payload.authors[-1]
ntype = NewNotificationType.NewShout
break
}
break break
}
case 'reaction': { case 'reaction': {
ntype = data().payload.replyTo ? NotificationType.NewReply : NotificationType.NewComment ntype = data().payload.replyTo ? NewNotificationType.NewReply : NewNotificationType.NewComment
console.log(data().payload.kind) console.log(data().payload.kind)
// TODO: handle all needed reaction kinds // TODO: handle all needed reaction kinds
} }

View File

@ -10,7 +10,6 @@ $transition-duration: 200ms;
bottom: 0; bottom: 0;
width: 0; width: 0;
z-index: 10000; z-index: 10000;
background-color: rgb(0 0 0 / 0%);
overflow: hidden; overflow: hidden;
transition: transition:
background-color $transition-duration, background-color $transition-duration,
@ -18,12 +17,48 @@ $transition-duration: 200ms;
.panel { .panel {
position: relative; position: relative;
background-color: #fff; background-color: var(--background-color);
width: 700px; width: 50%;
padding: 48px 96px 96px 48px; height: 100%;
transform: translateX(100%); transform: translateX(100%);
transition: transform $transition-duration; transition: transform $transition-duration;
overflow-y: auto; display: flex;
flex-direction: column;
.title {
@include font-size(2rem);
color: var(--black-500);
font-style: normal;
font-weight: 700;
position: sticky;
top: 0;
padding: 1.23rem 38px;
border-bottom: 2px solid var(--black-100);
}
.content {
overflow-y: auto;
flex: 1;
padding: 0 38px 1rem;
.loading {
@include font-size(1.2rem);
text-align: center;
padding: 1rem;
color: var(--black-300);
}
}
.actions {
padding: 24px 38px;
width: 100%;
bottom: 0;
left: 0;
background: var(--background-color);
border-top: 1px solid var(--black-100);
}
} }
&.isOpened { &.isOpened {
@ -39,22 +74,21 @@ $transition-duration: 200ms;
} }
} }
.title {
// TODO: check markup
color: var(--black-500, #141414);
font-size: 32px;
font-style: normal;
font-weight: 700;
line-height: 36px;
margin-bottom: 32px;
}
.closeButton { .closeButton {
position: absolute; position: absolute;
top: 0; top: 1.2rem;
right: 0; right: 1rem;
padding: 20px; padding: 1rem;
cursor: pointer; cursor: pointer;
z-index: 1;
&:hover {
background-color: var(--background-color-invert);
.closeIcon {
filter: invert(1);
}
}
} }
.notificationView + .notificationView { .notificationView + .notificationView {
@ -66,8 +100,7 @@ $transition-duration: 200ms;
} }
.periodTitle { .periodTitle {
// TODO: check markup margin: 32px 0 16px;
margin: 32px 0 16px 0;
color: var(--black-400); color: var(--black-400);
font-size: 12px; font-size: 12px;
font-weight: 500; font-weight: 500;

View File

@ -4,10 +4,13 @@ import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler' import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { createEffect, createMemo, For, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
import { useNotifications } from '../../context/notifications' import { PAGE_SIZE, useNotifications } from '../../context/notifications'
import { NotificationView } from './NotificationView' import { NotificationView } from './NotificationView'
import { EmptyMessage } from './EmptyMessage' import { EmptyMessage } from './EmptyMessage'
import { Button } from '../_shared/Button'
import throttle from 'just-throttle'
import { useSession } from '../../context/session'
type Props = { type Props = {
isOpen: boolean isOpen: boolean
@ -39,8 +42,17 @@ const isEarlier = (date: Date) => {
} }
export const NotificationsPanel = (props: Props) => { export const NotificationsPanel = (props: Props) => {
const [isLoading, setIsLoading] = createSignal(false)
const { isAuthenticated } = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const { sortedNotifications } = useNotifications() const {
sortedNotifications,
unreadNotificationsCount
// loadedNotificationsCount,
// totalNotificationsCount,
// actions: { loadNotifications, markAllNotificationsAsRead }
} = useNotifications()
const handleHide = () => { const handleHide = () => {
props.onClose() props.onClose()
} }
@ -79,17 +91,68 @@ export const NotificationsPanel = (props: Props) => {
handleHide() handleHide()
} }
const todayNotifications = createMemo(() => { // const todayNotifications = createMemo(() => {
return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt))) // return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt)))
// })
//
// const yesterdayNotifications = createMemo(() => {
// return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt)))
// })
//
// const earlierNotifications = createMemo(() => {
// return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt)))
// })
const scrollContainerRef: { current: HTMLDivElement } = { current: null }
const loadNextPage = async () => {
// await loadNotifications({ limit: PAGE_SIZE, offset: loadedNotificationsCount() })
// if (loadedNotificationsCount() < totalNotificationsCount()) {
// const hasMore = scrollContainerRef.current.scrollHeight <= scrollContainerRef.current.offsetHeight
//
// if (hasMore) {
// await loadNextPage()
// }
// }
}
const handleScroll = async () => {
if (!scrollContainerRef.current || isLoading()) {
return
}
// if (totalNotificationsCount() === loadedNotificationsCount()) {
// return
// }
const isNearBottom =
scrollContainerRef.current.scrollHeight - scrollContainerRef.current.scrollTop <=
scrollContainerRef.current.clientHeight * 1.5
if (isNearBottom) {
setIsLoading(true)
await loadNextPage()
setIsLoading(false)
}
}
const handleScrollThrottled = throttle(handleScroll, 50)
onMount(() => {
scrollContainerRef.current.addEventListener('scroll', handleScrollThrottled)
onCleanup(() => {
scrollContainerRef.current.removeEventListener('scroll', handleScrollThrottled)
})
}) })
const yesterdayNotifications = createMemo(() => { createEffect(
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt))) on(
}) () => isAuthenticated(),
async () => {
const earlierNotifications = createMemo(() => { if (isAuthenticated()) {
return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt))) setIsLoading(true)
}) await loadNextPage()
setIsLoading(false)
}
}
)
)
return ( return (
<div <div
@ -99,50 +162,75 @@ export const NotificationsPanel = (props: Props) => {
> >
<div ref={(el) => (panelRef.current = el)} class={styles.panel}> <div ref={(el) => (panelRef.current = el)} class={styles.panel}>
<div class={styles.closeButton} onClick={handleHide}> <div class={styles.closeButton} onClick={handleHide}>
{/*TODO: check markup (hover)*/} <Icon class={styles.closeIcon} name="close" />
<Icon name="close" />
</div> </div>
<div class={styles.title}>{t('Notifications')}</div> <div class={styles.title}>{t('Notifications')}</div>
<Show when={sortedNotifications().length > 0} fallback={<EmptyMessage />}> <div class={clsx('wide-container', styles.content)} ref={(el) => (scrollContainerRef.current = el)}>
<Show when={todayNotifications().length > 0}> <Show
<div class={styles.periodTitle}>{t('today')}</div> when={sortedNotifications().length > 0}
<For each={todayNotifications()}> fallback={
{(notification) => ( <Show when={!isLoading()}>
<NotificationView <EmptyMessage />
notification={notification} </Show>
class={styles.notificationView} }
onClick={handleNotificationViewClick} >
dateTimeFormat={'ago'} <div class="row position-relative">
/> <div class="col-xs-24">
)} {/*<Show when={todayNotifications().length > 0}>*/}
</For> {/* <div class={styles.periodTitle}>{t('today')}</div>*/}
{/* <For each={todayNotifications()}>*/}
{/* {(notification) => (*/}
{/* <NotificationView*/}
{/* notification={notification}*/}
{/* class={styles.notificationView}*/}
{/* onClick={handleNotificationViewClick}*/}
{/* dateTimeFormat={'ago'}*/}
{/* />*/}
{/* )}*/}
{/* </For>*/}
{/*</Show>*/}
{/*<Show when={yesterdayNotifications().length > 0}>*/}
{/* <div class={styles.periodTitle}>{t('yesterday')}</div>*/}
{/* <For each={yesterdayNotifications()}>*/}
{/* {(notification) => (*/}
{/* <NotificationView*/}
{/* notification={notification}*/}
{/* class={styles.notificationView}*/}
{/* onClick={handleNotificationViewClick}*/}
{/* dateTimeFormat={'time'}*/}
{/* />*/}
{/* )}*/}
{/* </For>*/}
{/*</Show>*/}
{/*<Show when={earlierNotifications().length > 0}>*/}
{/* <div class={styles.periodTitle}>{t('earlier')}</div>*/}
{/* <For each={earlierNotifications()}>*/}
{/* {(notification) => (*/}
{/* <NotificationView*/}
{/* notification={notification}*/}
{/* class={styles.notificationView}*/}
{/* onClick={handleNotificationViewClick}*/}
{/* dateTimeFormat={'date'}*/}
{/* />*/}
{/* )}*/}
{/* </For>*/}
{/*</Show>*/}
</div>
</div>
</Show> </Show>
<Show when={yesterdayNotifications().length > 0}> <Show when={isLoading()}>
<div class={styles.periodTitle}>{t('yesterday')}</div> <div class={styles.loading}>{t('Loading')}</div>
<For each={yesterdayNotifications()}>
{(notification) => (
<NotificationView
notification={notification}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'time'}
/>
)}
</For>
</Show>
<Show when={earlierNotifications().length > 0}>
<div class={styles.periodTitle}>{t('earlier')}</div>
<For each={earlierNotifications()}>
{(notification) => (
<NotificationView
notification={notification}
class={styles.notificationView}
onClick={handleNotificationViewClick}
dateTimeFormat={'date'}
/>
)}
</For>
</Show> </Show>
</div>
<Show when={unreadNotificationsCount() > 0}>
<div class={styles.actions}>
{/*<Button*/}
{/* onClick={() => markAllNotificationsAsRead()}*/}
{/* variant="secondary"*/}
{/* value={t('Mark as read')}*/}
{/*/>*/}
</div>
</Show> </Show>
</div> </div>
</div> </div>

View File

@ -45,7 +45,6 @@
.TableOfContentsFixedWrapperLefted { .TableOfContentsFixedWrapperLefted {
margin-top: -2em; margin-top: -2em;
right: auto; right: auto;
left: 70px;
.TableOfContentsPrimaryButton { .TableOfContentsPrimaryButton {
left: auto; left: auto;

View File

@ -56,6 +56,7 @@
.topicTitle { .topicTitle {
@include font-size(2.2rem); @include font-size(2.2rem);
font-weight: bold; font-weight: bold;
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
margin-top: 0.5rem !important; margin-top: 0.5rem !important;
@ -84,6 +85,7 @@
.topicDescription { .topicDescription {
@include font-size(1.4rem); @include font-size(1.4rem);
font-weight: 500; font-weight: 500;
color: #696969; color: #696969;
line-height: 1.3; line-height: 1.3;
@ -115,10 +117,17 @@
} }
} }
.actionButton {
border-radius: 0.8rem !important;
margin-right: 0 !important;
width: 9em;
}
.isSubscribing { .isSubscribing {
opacity: 0.5; opacity: 0.5;
} }
/*
.isSubscribed { .isSubscribed {
background: #000; background: #000;
color: #fff; color: #fff;
@ -145,6 +154,7 @@
display: none; display: none;
} }
} }
*/
.cardMode { .cardMode {
margin-bottom: 0; margin-bottom: 0;

View File

@ -13,6 +13,8 @@ import { CheckButton } from '../_shared/CheckButton'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
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'
interface TopicProps { interface TopicProps {
topic: Topic topic: Topic
@ -34,19 +36,15 @@ interface TopicProps {
export const TopicCard = (props: TopicProps) => { export const TopicCard = (props: TopicProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
session, subscriptions,
isSessionLoaded, isSessionLoaded,
actions: { loadSession, requireAuthentication } actions: { loadSubscriptions, requireAuthentication }
} = useSession() } = useSession()
const [isSubscribing, setIsSubscribing] = createSignal(false) const [isSubscribing, setIsSubscribing] = createSignal(false)
const subscribed = createMemo(() => { const subscribed = createMemo(() => {
if (!session()?.user?.slug || !session()?.news?.topics) { return subscriptions().topics.some((topic) => topic.slug === props.topic.slug)
return false
}
return session()?.news.topics.includes(props.topic.slug)
}) })
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
@ -56,7 +54,7 @@ export const TopicCard = (props: TopicProps) => {
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) ? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })) : unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
await loadSession() await loadSubscriptions()
setIsSubscribing(false) setIsSubscribing(false)
} }
@ -66,6 +64,24 @@ export const TopicCard = (props: TopicProps) => {
}, 'subscribe') }, 'subscribe')
} }
const subscribeValue = () => {
return (
<>
<Show when={props.iconButton}>
<Show when={subscribed()} fallback="+">
<Icon name="check-subscribed" />
</Show>
</Show>
<Show when={!props.iconButton}>
<Show when={subscribed()} fallback={t('Follow')}>
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
</Show>
</Show>
</>
)
}
return ( return (
<div class={styles.topicContainer}> <div class={styles.topicContainer}>
<div <div
@ -127,27 +143,18 @@ export const TopicCard = (props: TopicProps) => {
<CheckButton text={t('Follow')} checked={subscribed()} onClick={handleSubscribe} /> <CheckButton text={t('Follow')} checked={subscribed()} onClick={handleSubscribe} />
} }
> >
<button <Button
variant="bordered"
size="M"
value={subscribeValue()}
onClick={handleSubscribe} onClick={handleSubscribe}
class="button--light button--subscribe-topic" isSubscribeButton={true}
classList={{ class={clsx(styles.actionButton, {
[styles.isSubscribing]: isSubscribing(), [styles.isSubscribing]: isSubscribing(),
[styles.isSubscribed]: subscribed() [stylesButton.subscribed]: subscribed()
}} })}
disabled={isSubscribing()} disabled={isSubscribing()}
> />
<Show when={props.iconButton}>
<Show when={subscribed()} fallback="+">
<Icon name="check-subscribed" />
</Show>
</Show>
<Show when={!props.iconButton}>
<Show when={subscribed()} fallback={t('Follow')}>
<span class={styles.buttonUnfollowLabel}>{t('Unfollow')}</span>
<span class={styles.buttonSubscribedLabel}>{t('Following')}</span>
</Show>
</Show>
</button>
</Show> </Show>
</Show> </Show>
</ShowOnlyOnClient> </ShowOnlyOnClient>

View File

@ -15,16 +15,21 @@
.topicActions { .topicActions {
margin-top: 2.8rem; margin-top: 2.8rem;
button, .write {
a { display: inline-flex;
background: #000; align-items: center;
justify-content: center;
height: 40px;
min-width: 64px;
font-size: 17px;
padding: 8px 16px;
background: var(--background-color-invert);
color: var(--default-color-invert);
border: none; border: none;
font-weight: 500;
border-radius: 2px; border-radius: 2px;
color: #fff;
cursor: pointer; cursor: pointer;
font-size: 100%;
margin: 0 1.2rem 1em; margin: 0 1.2rem 1em;
padding: 0.8rem 1.6rem;
white-space: nowrap; white-space: nowrap;
} }
} }

View File

@ -7,6 +7,7 @@ import { follow, unfollow } from '../../stores/zine/common'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Button } from '../_shared/Button'
type Props = { type Props = {
topic: Topic topic: Topic
@ -14,19 +15,22 @@ type Props = {
export const FullTopic = (props: Props) => { export const FullTopic = (props: Props) => {
const { const {
session, subscriptions,
actions: { requireAuthentication } actions: { requireAuthentication, loadSubscriptions }
} = useSession() } = useSession()
const { t } = useLocalize()
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug))
const handleSubscribe = (isFollowed: boolean) => { const { t } = useLocalize()
requireAuthentication(() => {
if (isFollowed) { const subscribed = createMemo(() =>
unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }) subscriptions().topics.some((topic) => topic.slug === props.topic?.slug)
} else { )
follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
} const handleSubscribe = (really: boolean) => {
requireAuthentication(async () => {
await (really
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
loadSubscriptions()
}, 'follow') }, 'follow')
} }
@ -36,16 +40,18 @@ export const FullTopic = (props: Props) => {
<p>{props.topic.body}</p> <p>{props.topic.body}</p>
<div class={clsx(styles.topicActions)}> <div class={clsx(styles.topicActions)}>
<Show when={!subscribed()}> <Show when={!subscribed()}>
<button onClick={() => handleSubscribe(false)} class="button"> <Button variant="primary" onClick={() => handleSubscribe(true)} value={t('Follow the topic')} />
{t('Follow the topic')}
</button>
</Show> </Show>
<Show when={subscribed()}> <Show when={subscribed()}>
<button onClick={() => handleSubscribe(true)} class="button"> <Button
{t('Unfollow the topic')} variant="primary"
</button> onClick={() => handleSubscribe(false)}
value={t('Unfollow the topic')}
/>
</Show> </Show>
<a href={`/create/?topicId=${props.topic.id}`}>{t('Write about the topic')}</a> <a class={styles.write} href={`/create/?topicId=${props.topic.id}`}>
{t('Write about the topic')}
</a>
</div> </div>
<Show when={props.topic.pic}> <Show when={props.topic.pic}>
<img src={props.topic.pic} alt={props.topic.title} /> <img src={props.topic.pic} alt={props.topic.title} />

View File

@ -36,11 +36,12 @@
} }
.info { .info {
@include font-size(1.4rem);
border: none; border: none;
display: flex; display: flex;
flex: 0 calc(100% - 5.2rem); flex: 0 calc(100% - 5.2rem);
flex-direction: column; flex-direction: column;
@include font-size(1.4rem);
margin-bottom: 1rem; margin-bottom: 1rem;
@include media-breakpoint-up(sm) { @include media-breakpoint-up(sm) {

View File

@ -2,12 +2,12 @@ import { clsx } from 'clsx'
import styles from './TopicBadge.module.scss' import styles from './TopicBadge.module.scss'
import { FollowingEntity, Topic } from '../../../graphql/types.gen' import { FollowingEntity, Topic } from '../../../graphql/types.gen'
import { createMemo, createSignal, Show } from 'solid-js' import { createMemo, createSignal, Show } from 'solid-js'
import { imageProxy } from '../../../utils/imageProxy'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { follow, unfollow } from '../../../stores/zine/common' import { follow, unfollow } from '../../../stores/zine/common'
import { CheckButton } from '../../_shared/CheckButton' import { CheckButton } from '../../_shared/CheckButton'
import { getImageUrl } from '../../../utils/getImageUrl'
type Props = { type Props = {
topic: Topic topic: Topic
@ -19,17 +19,13 @@ export const TopicBadge = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const {
isAuthenticated, isAuthenticated,
session, subscriptions,
actions: { loadSession } actions: { loadSubscriptions }
} = useSession() } = useSession()
const subscribed = createMemo(() => { const subscribed = createMemo(() =>
if (!session()?.user?.slug || !session()?.news?.topics) { subscriptions().topics.some((topic) => topic.slug === props.topic.slug)
return false )
}
return session()?.news.topics.includes(props.topic.slug)
})
const subscribe = async (really = true) => { const subscribe = async (really = true) => {
setIsSubscribing(true) setIsSubscribing(true)
@ -38,7 +34,7 @@ export const TopicBadge = (props: Props) => {
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug }) ? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })) : unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
await loadSession() await loadSubscriptions()
setIsSubscribing(false) setIsSubscribing(false)
} }
@ -47,7 +43,11 @@ export const TopicBadge = (props: Props) => {
<a <a
href={`/topic/${props.topic.slug}`} href={`/topic/${props.topic.slug}`}
class={clsx(styles.picture, { [styles.withImage]: props.topic.pic })} class={clsx(styles.picture, { [styles.withImage]: props.topic.pic })}
style={props.topic.pic && { 'background-image': `url('${imageProxy(props.topic.pic)}')` }} style={
props.topic.pic && {
'background-image': `url('${getImageUrl(props.topic.pic, { width: 40, height: 40 })}')`
}
}
/> />
<a href={`/topic/${props.topic.slug}`} class={styles.info}> <a href={`/topic/${props.topic.slug}`} class={styles.info}>
<span class={styles.title}>{props.topic.title}</span> <span class={styles.title}>{props.topic.title}</span>
@ -80,7 +80,7 @@ export const TopicBadge = (props: Props) => {
<Button <Button
variant="primary" variant="primary"
size="S" size="S"
value={isSubscribing() ? t('...subscribing') : t('Subscribe')} value={isSubscribing() ? t('subscribing...') : t('Subscribe')}
onClick={() => subscribe(true)} onClick={() => subscribe(true)}
class={styles.subscribeButton} class={styles.subscribeButton}
/> />

View File

@ -1,15 +1,13 @@
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import type { Author } from '../../graphql/types.gen' import type { Author } from '../../graphql/types.gen'
import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors' import { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
import { useRouter } from '../../stores/router' import { useRouter } from '../../stores/router'
import { AuthorCard } from '../Author/AuthorCard'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { useSession } from '../../context/session'
import { SearchField } from '../_shared/SearchField' import { SearchField } from '../_shared/SearchField'
import { scrollHandler } from '../../utils/scroll' import { scrollHandler } from '../../utils/scroll'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { dummyFilter } from '../../utils/dummyFilter' import { dummyFilter } from '../../utils/dummyFilter'
import { AuthorBadge } from '../Author/AuthorBadge'
import styles from './AllAuthors.module.scss' import styles from './AllAuthors.module.scss'
@ -35,9 +33,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')
const { session } = useSession() createEffect(() => {
onMount(() => {
if (!searchParams().by) { if (!searchParams().by) {
changeSearchParam({ changeSearchParam({
by: 'shouts' by: 'shouts'
@ -52,7 +48,14 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => { const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
return sortedAuthors().reduce( return sortedAuthors().reduce(
(acc, author) => { (acc, author) => {
let letter = author.name.trim().split(' ').pop().at(0).toUpperCase() let letter = ''
if (author && author.name) {
const nameParts = author.name.trim().split(' ')
const lastName = nameParts.pop()
if (lastName && lastName.length > 0) {
letter = lastName[0].toUpperCase()
}
}
if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@' if (/[^ËА-яё]/.test(letter) && lang() === 'ru') letter = '@'
@ -72,8 +75,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
return keys return keys
}) })
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
const filteredAuthors = createMemo(() => { const filteredAuthors = createMemo(() => {
return dummyFilter(sortedAuthors(), searchQuery(), lang()) return dummyFilter(sortedAuthors(), searchQuery(), lang())
}) })
@ -84,7 +85,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
<div class="col-lg-20 col-xl-18"> <div class="col-lg-20 col-xl-18">
<h1>{t('Authors')}</h1> <h1>{t('Authors')}</h1>
<p>{t('Subscribe who you like to tune your personal feed')}</p> <p>{t('Subscribe who you like to tune your personal feed')}</p>
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}> <ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
<li <li
classList={{ classList={{
@ -172,15 +172,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
{(author) => ( {(author) => (
<div class="row"> <div class="row">
<div class="col-lg-20 col-xl-18"> <div class="col-lg-20 col-xl-18">
<AuthorCard <AuthorBadge author={author as Author} />
author={author as Author}
hasLink={true}
subscribed={subscribed(author.slug)}
noSocialButtons={true}
isAuthorsList={true}
truncateBio={true}
isTextButton={true}
/>
</div> </div>
</div> </div>
)} )}

View File

@ -87,6 +87,7 @@
.alphabet { .alphabet {
@include font-size(1.5rem); @include font-size(1.5rem);
color: rgba(0 0 0 / 20%); color: rgba(0 0 0 / 20%);
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
@ -106,6 +107,7 @@
.articlesCounter { .articlesCounter {
@include font-size(1.2rem); @include font-size(1.2rem);
margin-left: 0.5em; margin-left: 0.5em;
vertical-align: super; vertical-align: super;
} }

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js' import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
import type { Topic } from '../../graphql/types.gen' import type { Topic } from '../../graphql/types.gen'
import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics' import { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
@ -34,9 +34,9 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
sortBy: searchParams().by || 'shouts' sortBy: searchParams().by || 'shouts'
}) })
const { session } = useSession() const { subscriptions } = useSession()
onMount(() => { createEffect(() => {
if (!searchParams().by) { if (!searchParams().by) {
changeSearchParam({ changeSearchParam({
by: 'shouts' by: 'shouts'
@ -68,7 +68,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
return keys return keys
}) })
const subscribed = (s) => Boolean(session()?.news?.topics && session()?.news?.topics?.includes(s || '')) const subscribed = (topicSlug: string) => subscriptions().topics.some((topic) => topic.slug === topicSlug)
const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE) const showMore = () => setLimit((oldLimit) => oldLimit + PAGE_SIZE)
const [searchQuery, setSearchQuery] = createSignal('') const [searchQuery, setSearchQuery] = createSignal('')

View File

@ -30,12 +30,14 @@
.ratingContainer { .ratingContainer {
@include font-size(1.5rem); @include font-size(1.5rem);
display: inline-flex; display: inline-flex;
vertical-align: top; vertical-align: top;
} }
.ratingControl { .ratingControl {
@include font-size(1.5rem); @include font-size(1.5rem);
display: inline-flex; display: inline-flex;
margin-left: 1em; margin-left: 1em;
vertical-align: middle; vertical-align: middle;
@ -81,8 +83,8 @@
max-height 0.5s, max-height 0.5s,
margin-bottom 0s 0.3s; margin-bottom 0s 0.3s;
&:after { &::after {
background-image: linear-gradient(to top, #fff, rgb(255 255 255 / 0.8), rgb(255 255 255 / 0)); background-image: linear-gradient(to top, #fff, rgb(255 255 255 / 80%), rgb(255 255 255 / 0%));
bottom: 0; bottom: 0;
content: ''; content: '';
display: block; display: block;
@ -97,16 +99,17 @@
max-height: 200em; max-height: 200em;
margin-bottom: -2em; margin-bottom: -2em;
&:after { &::after {
display: none; display: none;
} }
} }
.longBioExpandedControl { .longBioExpandedControl {
@include font-size(1.6rem);
border-radius: 1.2rem; border-radius: 1.2rem;
display: block; display: block;
height: auto; height: auto;
@include font-size(1.6rem);
padding-bottom: 1.2rem; padding-bottom: 1.2rem;
padding-top: 1.2rem; padding-top: 1.2rem;
position: relative; position: relative;

View File

@ -17,7 +17,6 @@ import { Comment } from '../../Article/Comment'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { AuthorRatingControl } from '../../Author/AuthorRatingControl' import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { useSession } from '../../../context/session'
import { Loading } from '../../_shared/Loading' import { Loading } from '../../_shared/Loading'
type Props = { type Props = {
@ -35,7 +34,6 @@ export const AuthorView = (props: Props) => {
const { authorEntities } = useAuthorsStore({ authors: [props.author] }) const { authorEntities } = useAuthorsStore({ authors: [props.author] })
const { page: getPage } = useRouter() const { page: getPage } = useRouter()
const { user } = useSession()
const author = createMemo(() => authorEntities()[props.authorSlug]) const author = createMemo(() => authorEntities()[props.authorSlug])
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
const [isBioExpanded, setIsBioExpanded] = createSignal(false) const [isBioExpanded, setIsBioExpanded] = createSignal(false)
@ -128,13 +126,7 @@ export const AuthorView = (props: Props) => {
<Show when={author()} fallback={<Loading />}> <Show when={author()} fallback={<Loading />}>
<> <>
<div class={styles.authorHeader}> <div class={styles.authorHeader}>
<AuthorCard <AuthorCard author={author()} followers={followers()} following={following()} />
author={author()}
isAuthorPage={true}
followers={followers()}
following={following()}
isCurrentUser={author().slug === user()?.slug}
/>
</div> </div>
<div class={clsx(styles.groupControls, 'row')}> <div class={clsx(styles.groupControls, 'row')}>
<div class="col-md-16"> <div class="col-md-16">

View File

@ -50,10 +50,11 @@
.additionalInput { .additionalInput {
@include font-size(1.4rem); @include font-size(1.4rem);
-moz-appearance: textfield; appearance: textfield;
&::-webkit-outer-spin-button, &::-webkit-outer-spin-button,
&::-webkit-inner-spin-button { &::-webkit-inner-spin-button {
-webkit-appearance: none; appearance: none;
margin: 0; margin: 0;
} }
@ -213,12 +214,29 @@
background-position: center; background-position: center;
background-size: cover; background-size: cover;
background-repeat: no-repeat; background-repeat: no-repeat;
position: relative;
.delete {
position: absolute;
width: 32px;
height: 32px;
padding: 10px;
border-radius: 50%;
top: 4px;
right: 4px;
background-color: rgb(0 0 0 / 50%);
cursor: pointer;
display: none;
}
&:hover .delete {
display: block;
}
} }
} }
.wrapperTableOfContents { .wrapperTableOfContents {
position: fixed; position: fixed;
left: 40px;
top: 106px; top: 106px;
width: 240px; width: 240px;
padding-top: 100px; padding-top: 100px;

View File

@ -8,12 +8,11 @@ import { ShoutForm, useEditorContext } from '../../context/editor'
import { Editor, Panel } from '../Editor' import { Editor, Panel } from '../Editor'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import styles from './Edit.module.scss' import styles from './Edit.module.scss'
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'
import { AudioUploader } from '../Editor/AudioUploader' import { AudioUploader } from '../Editor/AudioUploader'
import { slugify } from '../../utils/slugify' import { slugify } from '../../utils/slugify'
import { SolidSwiper } from '../_shared/SolidSwiper' import { ImageSwiper } from '../_shared/SolidSwiper'
import { DropArea } from '../_shared/DropArea' import { DropArea } from '../_shared/DropArea'
import { LayoutType, MediaItem } from '../../pages/types' import { LayoutType, MediaItem } from '../../pages/types'
import { clone } from '../../utils/clone' import { clone } from '../../utils/clone'
@ -24,6 +23,8 @@ import { createStore } from 'solid-js/store'
import SimplifiedEditor from '../Editor/SimplifiedEditor' import SimplifiedEditor from '../Editor/SimplifiedEditor'
import { isDesktop } from '../../utils/media-query' import { isDesktop } from '../../utils/media-query'
import { TableOfContents } from '../TableOfContents' import { TableOfContents } from '../TableOfContents'
import { getImageUrl } from '../../utils/getImageUrl'
import { Popover } from '../_shared/Popover'
type Props = { type Props = {
shout: Shout shout: Shout
@ -362,14 +363,28 @@ export const EditView = (props: Props) => {
> >
<div <div
class={styles.cover} class={styles.cover}
style={{ 'background-image': `url(${imageProxy(form.coverImageUrl)})` }} style={{
/> 'background-image': `url(${getImageUrl(form.coverImageUrl, { width: 1600 })})`
}}
>
<Popover content={t('Delete cover')}>
{(triggerRef: (el) => void) => (
<div
ref={triggerRef}
class={styles.delete}
onClick={() => setForm('coverImageUrl', null)}
>
<Icon name="close-white" />
</div>
)}
</Popover>
</div>
</Show> </Show>
</Show> </Show>
</div> </div>
<Show when={props.shout.layout === 'image'}> <Show when={props.shout.layout === 'image'}>
<SolidSwiper <ImageSwiper
editorMode={true} editorMode={true}
images={mediaItems()} images={mediaItems()}
onImageChange={handleMediaChange} onImageChange={handleMediaChange}

View File

@ -1,12 +1,13 @@
.Expo { .Expo {
display: block; display: block;
background: #fef2f2; background: #fef2f2;
padding: 0 0 4rem 0; padding: 0 0 4rem;
min-height: 100vh; min-height: 100vh;
.navigation { .navigation {
padding: 0 0; padding: 0;
} }
.showMore { .showMore {
display: flex; display: flex;
width: 100%; width: 100%;

View File

@ -9,6 +9,7 @@
.feedNavigation { .feedNavigation {
@include font-size(1.6rem); @include font-size(1.6rem);
font-weight: 500; font-weight: 500;
h4 { h4 {
@ -51,6 +52,7 @@
h4 { h4 {
@include font-size(2.2rem); @include font-size(2.2rem);
font-weight: bold; font-weight: bold;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@ -131,7 +133,7 @@
} }
&:hover { &:hover {
&:before { &::before {
background-image: url(/icons/knowledge-base-bullet-hover.svg); background-image: url(/icons/knowledge-base-bullet-hover.svg);
} }
} }
@ -156,6 +158,7 @@
.commentDetails { .commentDetails {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 0.5rem;
justify-content: space-between; justify-content: space-between;
} }
@ -169,6 +172,10 @@
a { a {
border: none; border: none;
padding-bottom: 0.2em; padding-bottom: 0.2em;
&:hover * {
background: var(--background-color-invert);
}
} }
} }

View File

@ -1,10 +1,8 @@
import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js' import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { ArticleCard } from '../Feed/ArticleCard' import { ArticleCard } from '../Feed/ArticleCard'
import { AuthorCard } from '../Author/AuthorCard'
import { Sidebar } from '../Feed/Sidebar' import { Sidebar } from '../Feed/Sidebar'
import { useArticlesStore, resetSortedArticles } from '../../stores/zine/articles' import { useArticlesStore, resetSortedArticles } from '../../stores/zine/articles'
import { useAuthorsStore } from '../../stores/zine/authors'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { clsx } from 'clsx' import { clsx } from 'clsx'
@ -18,6 +16,8 @@ import stylesTopic from '../Feed/CardTopic.module.scss'
import stylesBeside from '../../components/Feed/Beside.module.scss' import stylesBeside from '../../components/Feed/Beside.module.scss'
import { CommentDate } from '../Article/CommentDate' import { CommentDate } from '../Article/CommentDate'
import { Loading } from '../_shared/Loading' import { Loading } from '../_shared/Loading'
import { AuthorBadge } from '../Author/AuthorBadge'
import { AuthorLink } from '../Author/AhtorLink'
export const FEED_PAGE_SIZE = 20 export const FEED_PAGE_SIZE = 20
@ -48,8 +48,6 @@ export const FeedView = (props: Props) => {
// state // state
const { sortedArticles } = useArticlesStore() const { sortedArticles } = useArticlesStore()
const { sortedAuthors } = useAuthorsStore()
const { topTopics } = useTopicsStore() const { topTopics } = useTopicsStore()
const { topAuthors } = useTopAuthorsStore() const { topAuthors } = useTopAuthorsStore()
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false) const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
@ -113,7 +111,7 @@ export const FeedView = (props: Props) => {
<div class="wide-container feed"> <div class="wide-container feed">
<div class="row"> <div class="row">
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}> <div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
<Sidebar authors={sortedAuthors()} /> <Sidebar />
</div> </div>
<div class="col-md-12 offset-xl-1"> <div class="col-md-12 offset-xl-1">
@ -163,13 +161,7 @@ export const FeedView = (props: Props) => {
<For each={topAuthors().slice(0, 5)}> <For each={topAuthors().slice(0, 5)}>
{(author) => ( {(author) => (
<li> <li>
<AuthorCard <AuthorBadge author={author} />
author={author}
hideWriteButton={true}
hasLink={true}
truncateBio={true}
isTextButton={true}
/>
</li> </li>
)} )}
</For> </For>
@ -207,13 +199,7 @@ export const FeedView = (props: Props) => {
/> />
</div> </div>
<div class={styles.commentDetails}> <div class={styles.commentDetails}>
<AuthorCard <AuthorLink author={comment.createdBy as Author} size={'XS'} />
author={comment.createdBy as Author}
isFeedMode={true}
hideWriteButton={true}
hideFollow={true}
hasLink={true}
/>
<CommentDate comment={comment} isShort={true} isLastInRow={true} /> <CommentDate comment={comment} isShort={true} isLastInRow={true} />
</div> </div>
<div class={clsx('text-truncate', styles.commentArticleTitle)}> <div class={clsx('text-truncate', styles.commentArticleTitle)}>

View File

@ -8,10 +8,8 @@ import { Row1 } from '../Feed/Row1'
import Hero from '../Discours/Hero' import Hero from '../Discours/Hero'
import { Beside } from '../Feed/Beside' import { Beside } from '../Feed/Beside'
import RowShort from '../Feed/RowShort' import RowShort from '../Feed/RowShort'
import { Slider } from '../_shared/Slider'
import Group from '../Feed/Group' import Group from '../Feed/Group'
import type { Shout } from '../../graphql/types.gen' import type { Shout } from '../../graphql/types.gen'
import { useTopicsStore } from '../../stores/zine/topics' import { useTopicsStore } from '../../stores/zine/topics'
import { import {
loadShouts, loadShouts,
@ -22,8 +20,8 @@ import {
import { useTopAuthorsStore } from '../../stores/zine/topAuthors' import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages' import { splitToPages } from '../../utils/splitToPages'
import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
type Props = { type Props = {
shouts: Shout[] shouts: Shout[]
@ -129,21 +127,9 @@ export const HomeView = (props: Props) => {
nodate={true} nodate={true}
/> />
<Slider title={t('Top month articles')}> <Show when={topMonthArticles()}>
<For each={topMonthArticles()}> <ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} />
{(a: Shout) => ( </Show>
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} /> <Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
@ -159,21 +145,9 @@ export const HomeView = (props: Props) => {
{randomLayout()} {randomLayout()}
<Slider title={t('Favorite')}> <Show when={topArticles()}>
<For each={topArticles()}> <ArticleCardSwiper title={t('Favorite')} slides={topArticles()} />
{(a: Shout) => ( </Show>
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Beside <Beside
beside={sortedArticles()[20]} beside={sortedArticles()[20]}

View File

@ -252,20 +252,22 @@ export const InboxView = () => {
<div class={styles.messageForm}> <div class={styles.messageForm}>
<Show when={messageToReply()}> <Show when={messageToReply()}>
<QuotedMessage <p>FIXME: messageToReply</p>
variant="reply" {/*<QuotedMessage*/}
author={ {/* variant="reply"*/}
currentDialog().members.find((member) => member.id === Number(messageToReply().author)) {/* author={*/}
.name {/* currentDialog().members.find((member) => member.id === Number(messageToReply().author))*/}
} {/* .name*/}
body={messageToReply().body} {/* }*/}
cancel={() => setMessageToReply(null)} {/* body={messageToReply().body}*/}
/> {/* cancel={() => setMessageToReply(null)}*/}
{/*/>*/}
</Show> </Show>
<div class={styles.wrapper}> <div class={styles.wrapper}>
<SimplifiedEditor <SimplifiedEditor
smallHeight={true} smallHeight={true}
imageEnabled={true} imageEnabled={true}
isCancelButtonVisible={false}
placeholder={t('Write message')} placeholder={t('Write message')}
setClear={isClear()} setClear={isClear()}
onSubmit={(message) => handleSubmit(message)} onSubmit={(message) => handleSubmit(message)}

View File

@ -119,7 +119,10 @@
font-weight: 400; font-weight: 400;
line-height: 1.3; line-height: 1.3;
margin-bottom: 0.8rem; margin-bottom: 0.8rem;
transition: color 0.2s, background-color 0.2s, box-shadow 0.2s; transition:
color 0.2s,
background-color 0.2s,
box-shadow 0.2s;
} }
.shoutAuthor { .shoutAuthor {
@ -154,6 +157,7 @@
padding: 1rem 0; padding: 1rem 0;
gap: 1rem; gap: 1rem;
} }
.cancel { .cancel {
margin-right: auto; margin-right: auto;
} }

View File

@ -4,7 +4,6 @@ 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'
import { imageProxy } from '../../../utils/imageProxy'
import { ShoutForm, useEditorContext } from '../../../context/editor' import { ShoutForm, useEditorContext } from '../../../context/editor'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
@ -20,6 +19,7 @@ import { GrowingTextarea } from '../../_shared/GrowingTextarea'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor' import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor'
import { Image } from '../../_shared/Image'
type Props = { type Props = {
shoutId: number shoutId: number
@ -141,11 +141,7 @@ export const PublishSettings = (props: Props) => {
> >
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}> <Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}>
<div class={styles.shoutCardCover}> <div class={styles.shoutCardCover}>
<img <Image src={settingsForm.coverImageUrl} alt={initialData.title} width={1600} />
src={imageProxy(settingsForm.coverImageUrl)}
alt={initialData.title}
loading="lazy"
/>
</div> </div>
</Show> </Show>
<div class={styles.text}> <div class={styles.text}>

View File

@ -13,10 +13,9 @@ import { useAuthorsStore } from '../../stores/zine/authors'
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
import { splitToPages } from '../../utils/splitToPages' import { splitToPages } from '../../utils/splitToPages'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Slider } from '../_shared/Slider'
import { Row1 } from '../Feed/Row1' import { Row1 } from '../Feed/Row1'
import { ArticleCard } from '../Feed/ArticleCard'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
type TopicsPageSearchParams = { type TopicsPageSearchParams = {
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented' by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
@ -136,21 +135,7 @@ export const TopicView = (props: TopicProps) => {
wrapper={'author'} wrapper={'author'}
/> />
<Slider title={title()}> <ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} />
<For each={sortedArticles().slice(5, 11)}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: true,
nodate: true
}}
/>
)}
</For>
</Slider>
<Beside <Beside
beside={sortedArticles()[12]} beside={sortedArticles()[12]}
@ -163,22 +148,7 @@ export const TopicView = (props: TopicProps) => {
<Row1 article={sortedArticles()[15]} /> <Row1 article={sortedArticles()[15]} />
<Show when={sortedArticles().length > 15}> <Show when={sortedArticles().length > 15}>
<Slider slidesPerView={3}> <ArticleCardSwiper slides={sortedArticles().slice(16, 22)} />
<For each={sortedArticles().slice(16, 22)}>
{(a: Shout) => (
<ArticleCard
article={a}
settings={{
additionalClass: 'swiper-slide',
isFloorImportant: true,
isWithCover: false,
nodate: true
}}
/>
)}
</For>
</Slider>
<Row3 articles={sortedArticles().slice(23, 26)} /> <Row3 articles={sortedArticles().slice(23, 26)} />
<Row2 articles={sortedArticles().slice(26, 28)} /> <Row2 articles={sortedArticles().slice(26, 28)} />
</Show> </Show>

View File

@ -8,8 +8,8 @@
white-space: nowrap; white-space: nowrap;
&.primary { &.primary {
background: #000; background: var(--background-color-invert);
color: #fff; color: var(--default-color-invert);
&:hover { &:hover {
color: #ccc; color: #ccc;
@ -131,4 +131,65 @@
font-size: 15px; font-size: 15px;
padding: 1rem 1.2rem; padding: 1rem 1.2rem;
} }
&.subscribeButton {
aspect-ratio: auto;
background-color: #000;
border: 2px solid #000;
border-radius: 0.8rem;
color: #fff;
float: none;
padding-bottom: 0.6rem;
padding-top: 0.6rem;
width: 10em;
.icon {
img {
filter: invert(1);
}
}
&:hover {
background: #fff;
color: #000;
.icon img {
filter: invert(0) !important;
}
.buttonSubscribeLabel {
display: none;
}
.buttonSubscribeLabelHovered {
display: block;
}
}
.buttonSubscribeLabelHovered {
display: none;
}
img {
vertical-align: text-top;
}
}
&.subscribed {
background: #fff;
color: #000;
.icon img {
filter: invert(0);
}
&:hover {
background: #000;
color: #fff;
.icon img {
filter: invert(1) !important;
}
}
}
} }

View File

@ -13,6 +13,7 @@ type Props = {
onClick?: (event?: MouseEvent) => void onClick?: (event?: MouseEvent) => void
class?: string class?: string
ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void) ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void)
isSubscribeButton?: boolean
} }
export const Button = (props: Props) => { export const Button = (props: Props) => {
@ -33,7 +34,8 @@ export const Button = (props: Props) => {
styles[props.size ?? 'M'], styles[props.size ?? 'M'],
styles[props.variant ?? 'primary'], styles[props.variant ?? 'primary'],
{ {
[styles.loading]: props.loading [styles.loading]: props.loading,
[styles.subscribeButton]: props.isSubscribeButton
}, },
props.class props.class
)} )}

View File

@ -21,9 +21,11 @@
&:hover { &:hover {
background: var(--background-color-invert); background: var(--background-color-invert);
color: var(--default-color-invert); color: var(--default-color-invert);
.check { .check {
display: none; display: none;
} }
.close { .close {
display: block; display: block;
} }

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