Merge main
|
@ -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",
|
||||
"*.{scss,css}": "stylelint",
|
||||
"*.{ts,tsx,js}": "eslint --fix"
|
||||
"*.{ts,tsx,js,mjs}": "eslint --fix"
|
||||
}
|
||||
|
|
|
@ -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')
|
||||
}
|
||||
}
|
|
@ -1,13 +1,13 @@
|
|||
import { renderPage } from 'vite-plugin-ssr/server'
|
||||
import { renderPage } from 'vike/server'
|
||||
|
||||
export default async function handler(req, res) {
|
||||
const { url, cookies } = req
|
||||
|
||||
const pageContext = await renderPage({ urlOriginal: url, cookies })
|
||||
|
||||
const { httpResponse, errorWhileRendering } = pageContext
|
||||
const { httpResponse, errorWhileRendering, is404 } = pageContext
|
||||
|
||||
if (errorWhileRendering) {
|
||||
if (errorWhileRendering && !is404) {
|
||||
console.error(errorWhileRendering)
|
||||
res.statusCode = 500
|
||||
res.end()
|
||||
|
|
3477
package-lock.json
generated
58
package.json
|
@ -3,6 +3,7 @@
|
|||
"version": "0.8.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"check": "npm run lint && npm run typecheck",
|
||||
|
@ -35,16 +36,15 @@
|
|||
"idb": "7.1.1",
|
||||
"intl-messageformat": "10.5.3",
|
||||
"just-throttle": "4.2.0",
|
||||
"mailgun.js": "8.2.1",
|
||||
"node-fetch": "3.3.1"
|
||||
"mailgun.js": "8.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "7.21.8",
|
||||
"@graphql-codegen/cli": "3.2.2",
|
||||
"@graphql-codegen/typescript": "3.0.4",
|
||||
"@graphql-codegen/typescript-operations": "3.0.4",
|
||||
"@graphql-codegen/typescript-urql": "3.7.3",
|
||||
"@graphql-codegen/urql-introspection": "2.2.1",
|
||||
"@graphql-codegen/cli": "5.0.0",
|
||||
"@graphql-codegen/typescript": "4.0.1",
|
||||
"@graphql-codegen/typescript-operations": "4.0.1",
|
||||
"@graphql-codegen/typescript-urql": "4.0.0",
|
||||
"@graphql-codegen/urql-introspection": "3.0.0",
|
||||
"@graphql-tools/url-loader": "7.17.18",
|
||||
"@graphql-typed-document-node/core": "3.2.0",
|
||||
"@hocuspocus/provider": "2.0.6",
|
||||
|
@ -89,27 +89,27 @@
|
|||
"@tiptap/extension-text": "2.0.3",
|
||||
"@tiptap/extension-underline": "2.0.3",
|
||||
"@tiptap/extension-youtube": "2.0.3",
|
||||
"@types/js-cookie": "3.0.4",
|
||||
"@types/node": "20.1.1",
|
||||
"@typescript-eslint/eslint-plugin": "6.7.3",
|
||||
"@typescript-eslint/parser": "6.7.3",
|
||||
"@types/js-cookie": "3.0.5",
|
||||
"@types/node": "20.8.10",
|
||||
"@typescript-eslint/eslint-plugin": "6.9.1",
|
||||
"@typescript-eslint/parser": "6.9.1",
|
||||
"@urql/core": "3.2.2",
|
||||
"@urql/devtools": "2.0.3",
|
||||
"babel-preset-solid": "1.7.4",
|
||||
"babel-preset-solid": "1.8.4",
|
||||
"bootstrap": "5.3.2",
|
||||
"clsx": "2.0.0",
|
||||
"cross-env": "7.0.3",
|
||||
"debounce": "1.2.1",
|
||||
"eslint": "8.50.0",
|
||||
"eslint": "8.53.0",
|
||||
"eslint-config-stylelint": "20.0.0",
|
||||
"eslint-import-resolver-typescript": "3.6.1",
|
||||
"eslint-plugin-import": "2.28.1",
|
||||
"eslint-plugin-jest": "27.4.0",
|
||||
"eslint-plugin-jsx-a11y": "6.7.1",
|
||||
"eslint-plugin-import": "2.29.0",
|
||||
"eslint-plugin-jest": "27.6.0",
|
||||
"eslint-plugin-jsx-a11y": "6.8.0",
|
||||
"eslint-plugin-promise": "6.1.1",
|
||||
"eslint-plugin-solid": "0.13.0",
|
||||
"eslint-plugin-sonarjs": "0.21.0",
|
||||
"eslint-plugin-unicorn": "48.0.1",
|
||||
"eslint-plugin-sonarjs": "0.23.0",
|
||||
"eslint-plugin-unicorn": "49.0.0",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"graphql": "16.6.0",
|
||||
"graphql-tag": "2.12.6",
|
||||
|
@ -120,7 +120,7 @@
|
|||
"javascript-time-ago": "2.5.9",
|
||||
"jest": "29.7.0",
|
||||
"js-cookie": "3.0.5",
|
||||
"lint-staged": "14.0.1",
|
||||
"lint-staged": "15.0.2",
|
||||
"loglevel": "1.8.1",
|
||||
"loglevel-plugin-prefix": "0.8.4",
|
||||
"markdown-it": "13.0.1",
|
||||
|
@ -130,30 +130,30 @@
|
|||
"markdown-it-replace-link": "1.2.0",
|
||||
"nanostores": "0.7.4",
|
||||
"prettier": "3.0.3",
|
||||
"prettier-eslint": "15.0.1",
|
||||
"prettier-eslint": "16.1.2",
|
||||
"prosemirror-history": "1.3.0",
|
||||
"prosemirror-trailing-node": "2.0.3",
|
||||
"prosemirror-view": "1.30.2",
|
||||
"rollup": "3.21.6",
|
||||
"sass": "1.68.0",
|
||||
"solid-js": "1.7.5",
|
||||
"sass": "1.69.5",
|
||||
"solid-js": "1.8.5",
|
||||
"solid-popper": "0.3.0",
|
||||
"solid-tiptap": "0.6.0",
|
||||
"solid-transition-group": "0.2.2",
|
||||
"solid-transition-group": "0.2.3",
|
||||
"sort-package-json": "2.6.0",
|
||||
"stylelint": "15.10.3",
|
||||
"stylelint-config-standard-scss": "11.0.0",
|
||||
"stylelint": "15.11.0",
|
||||
"stylelint-config-standard-scss": "11.1.0",
|
||||
"stylelint-order": "6.0.3",
|
||||
"stylelint-scss": "5.2.1",
|
||||
"stylelint-scss": "5.3.0",
|
||||
"swiper": "9.4.1",
|
||||
"typescript": "5.2.2",
|
||||
"typograf": "7.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-sass-dts": "1.3.11",
|
||||
"vite-plugin-solid": "2.7.0",
|
||||
"vite-plugin-ssr": "0.4.123",
|
||||
"vite-plugin-solid": "2.7.2",
|
||||
"y-prosemirror": "1.2.1",
|
||||
"yjs": "13.6.0"
|
||||
},
|
||||
|
|
4
public/icons/check-success.svg
Normal 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 |
3
public/icons/social-facebook.svg
Normal 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 |
5
public/icons/social-instagram.svg
Normal 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 |
7
public/icons/social-linkedin.svg
Normal 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 |
5
public/icons/social-telegram.svg
Normal 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 |
4
public/icons/social-twitter.svg
Normal 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 |
3
public/icons/social-vk.svg
Normal 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 |
8
public/icons/user-image-black.svg
Normal 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 |
8
public/icons/user-image-gray.svg
Normal 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 |
|
@ -1,5 +1,4 @@
|
|||
{
|
||||
"...subscribing": "...subscribing",
|
||||
"About": "About",
|
||||
"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",
|
||||
|
@ -90,16 +89,19 @@
|
|||
"Create gallery": "Create gallery",
|
||||
"Create post": "Create post",
|
||||
"Create video": "Create video",
|
||||
"Culture": "Culture",
|
||||
"Date of Birth": "Date of Birth",
|
||||
"Decline": "Decline",
|
||||
"Delete": "Delete",
|
||||
"Delete cover": "Delete cover",
|
||||
"Delete userpic": "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": "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.<br/><em>We are convinced that one voice is good, but many is better. We create the most amazing stories together</em>",
|
||||
"Discours is created with our common effort": "Discours exists because of our common effort",
|
||||
"Discussing": "Discussing",
|
||||
"Discussion rules": "Discussion rules",
|
||||
"Discussion rules in social networks": "Discussion rules",
|
||||
"Discussions": "Discussions",
|
||||
"Dogma": "Dogma",
|
||||
"Draft successfully deleted": "Draft successfully deleted",
|
||||
|
@ -120,6 +122,7 @@
|
|||
"Enter your new password": "Enter your new password",
|
||||
"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.",
|
||||
"Experience": "Experience",
|
||||
"FAQ": "Tips and suggestions",
|
||||
"Favorite": "Favorites",
|
||||
"Favorite topics": "Favorite topics",
|
||||
|
@ -148,6 +151,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": "Here you can upload your photo",
|
||||
"Hide table of contents": "Hide table of contents",
|
||||
"Highlight": "Highlight",
|
||||
"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",
|
||||
"Insert footnote": "Insert footnote",
|
||||
"Insert video link": "Insert video link",
|
||||
"Interview": "Interview",
|
||||
"Introduce": "Introduction",
|
||||
"Invalid email": "Check if your email is correct",
|
||||
"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!",
|
||||
"Just start typing...": "Just start typing...",
|
||||
"Knowledge base": "Knowledge base",
|
||||
"Language": "Language",
|
||||
"Last rev.": "Посл. изм.",
|
||||
"Let's log in": "Let's log in",
|
||||
"Link copied": "Link copied",
|
||||
|
@ -195,6 +201,7 @@
|
|||
"Manifest": "Manifest",
|
||||
"Manifesto": "Manifesto",
|
||||
"Many files, choose only one": "Many files, choose only one",
|
||||
"Mark as read": "Mark as read",
|
||||
"Material card": "Material card",
|
||||
"Message": "Message",
|
||||
"More": "More",
|
||||
|
@ -227,6 +234,7 @@
|
|||
"Our regular contributor": "Our regular contributor",
|
||||
"Paragraphs": "Абзацев",
|
||||
"Participating": "Participating",
|
||||
"Participation": "Participation",
|
||||
"Partners": "Partners",
|
||||
"Password": "Password",
|
||||
"Password again": "Password again",
|
||||
|
@ -245,6 +253,8 @@
|
|||
"Please enter password": "Please enter a password",
|
||||
"Please enter password again": "Please enter password again",
|
||||
"Please, confirm email": "Please confirm email",
|
||||
"Podcasts": "Podcasts",
|
||||
"Poetry": "Poetry",
|
||||
"Popular": "Popular",
|
||||
"Popular authors": "Popular authors",
|
||||
"Principles": "Community principles",
|
||||
|
@ -264,6 +274,7 @@
|
|||
"Remove link": "Remove link",
|
||||
"Reply": "Reply",
|
||||
"Report": "Complain",
|
||||
"Reports": "Reports",
|
||||
"Required": "Required",
|
||||
"Resend code": "Send confirmation",
|
||||
"Restore password": "Restore password",
|
||||
|
@ -287,31 +298,36 @@
|
|||
"Show table of contents": "Show table of contents",
|
||||
"Slug": "Slug",
|
||||
"Social networks": "Social networks",
|
||||
"Society": "Society",
|
||||
"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",
|
||||
"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": "Special projects",
|
||||
"Specify the source and the name of the author": "Specify the source and the name of the author",
|
||||
"Start conversation": "Start a conversation",
|
||||
"Subsccriptions": "Subscriptions",
|
||||
"Subscribe": "Subscribe",
|
||||
"Subscribe to the best publications newsletter": "Subscribe to the best publications newsletter",
|
||||
"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 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",
|
||||
"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",
|
||||
"Substrate": "Substrate",
|
||||
"Success": "Success",
|
||||
"Successfully authorized": "Authorization successful",
|
||||
"Suggest an idea": "Suggest an idea",
|
||||
"Support the project": "Support the project",
|
||||
"Support us": "Help the magazine",
|
||||
"Terms of use": "Site rules",
|
||||
"Text checking": "Text checking",
|
||||
"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 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",
|
||||
|
@ -339,6 +355,7 @@
|
|||
"Unnamed draft": "Unnamed draft",
|
||||
"Upload": "Upload",
|
||||
"Upload error": "Upload error",
|
||||
"Upload userpic": "Upload userpic",
|
||||
"Upload video": "Upload video",
|
||||
"Uploading image": "Uploading image",
|
||||
"Username": "Username",
|
||||
|
@ -375,7 +392,6 @@
|
|||
"You've reached a non-existed page": "You've reached a non-existed page",
|
||||
"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",
|
||||
"accomplices": "accomplices",
|
||||
"actions": "actions",
|
||||
"add link": "add link",
|
||||
"all topics": "all topics",
|
||||
|
@ -399,7 +415,6 @@
|
|||
"feed": "feed",
|
||||
"follower": "follower",
|
||||
"followersWithCount": "{count} {count, plural, one {follower} other {followers}}",
|
||||
"general feed": "general tape",
|
||||
"header 1": "header 1",
|
||||
"header 2": "header 2",
|
||||
"header 3": "header 3",
|
||||
|
@ -426,6 +441,7 @@
|
|||
"subscriber": "subscriber",
|
||||
"subscriber_rp": "subscriber",
|
||||
"subscribers": "subscribers",
|
||||
"subscribing...": "subscribing...",
|
||||
"subscription": "subscription",
|
||||
"subscription_rp": "subscription",
|
||||
"subscriptions": "subscriptions",
|
||||
|
@ -435,5 +451,6 @@
|
|||
"user already exist": "user already exists",
|
||||
"video": "video",
|
||||
"view": "view",
|
||||
"viewsWithCount": "{count} {count, plural, one {view} other {views}}",
|
||||
"yesterday": "yesterday"
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
{
|
||||
"...subscribing": "...подписываем",
|
||||
"A short introduction to keep the reader interested": "Добавьте вступление, чтобы заинтересовать читателя",
|
||||
"About": "О себе",
|
||||
"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 link or click plus to embed media": "Добавьте ссылку или нажмите плюс для вставки медиа",
|
||||
"Add an embed widget": "Добавить embed-виджет",
|
||||
|
@ -94,16 +92,19 @@
|
|||
"Create gallery": "Создать галерею",
|
||||
"Create post": "Создать публикацию",
|
||||
"Create video": "Создать видео",
|
||||
"Culture": "Культура",
|
||||
"Date of Birth": "Дата рождения",
|
||||
"Decline": "Отмена",
|
||||
"Delete": "Удалить",
|
||||
"Delete cover": "Удалить обложку",
|
||||
"Delete userpic": "Удалить аватар",
|
||||
"Description": "Описание",
|
||||
"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": "Дискурс — это интеллектуальная среда, веб-пространство и инструменты, которые позволяют авторам сотрудничать с читателями и объединяться для совместного создания публикаций и медиапроектов.<br/>Мы убеждены, один голос хорошо, а много — лучше. Самые потрясающиe истории мы создаём вместе.",
|
||||
"Discours is created with our common effort": "Дискурс существует благодаря нашему общему вкладу",
|
||||
"Discussing": "Обсуждаемое",
|
||||
"Discussion rules": "Правила сообществ самиздата в соцсетях",
|
||||
"Discussion rules": "Правила дискуссий",
|
||||
"Discussion rules in social networks": "Правила сообществ самиздата в соцсетях",
|
||||
"Discussions": "Дискуссии",
|
||||
"Dogma": "Догма",
|
||||
"Draft successfully deleted": "Черновик успешно удален",
|
||||
|
@ -125,6 +126,7 @@
|
|||
"Enter your new password": "Введите новый пароль",
|
||||
"Error": "Ошибка",
|
||||
"Everything is ok, please give us your email address": "Ничего страшного, просто укажите свою почту, чтобы получить ссылку для сброса пароля.",
|
||||
"Experience": "Личный опыт",
|
||||
"FAQ": "Советы и предложения",
|
||||
"Favorite": "Избранное",
|
||||
"Favorite topics": "Избранные темы",
|
||||
|
@ -156,6 +158,7 @@
|
|||
"Help to edit": "Помочь редактировать",
|
||||
"Here you can customize your profile the way you want.": "Здесь можно настроить свой профиль так, как вы хотите.",
|
||||
"Here you can manage all your Discourse subscriptions": "Здесь можно управлять всеми своими подписками на Дискурсе",
|
||||
"Here you can upload your photo": "Здесь вы можете загрузить свою фотографию",
|
||||
"Hide table of contents": "Скрыть главление",
|
||||
"Highlight": "Подсветка",
|
||||
"Hooray! Welcome!": "Ура! Добро пожаловать!",
|
||||
|
@ -176,6 +179,7 @@
|
|||
"Independant magazine with an open horizontal cooperation about culture, science and society": "Независимый журнал с открытой горизонтальной редакцией о культуре, науке и обществе",
|
||||
"Insert footnote": "Вставить сноску",
|
||||
"Insert video link": "Вставить ссылку на видео",
|
||||
"Interview": "Интервью",
|
||||
"Introduce": "Представление",
|
||||
"Invalid email": "Проверьте правильность ввода почты",
|
||||
"Invalid image URL": "Некорректная ссылка на изображение",
|
||||
|
@ -192,6 +196,7 @@
|
|||
"Just start typing...": "Просто начните печатать...",
|
||||
"Karma": "Карма",
|
||||
"Knowledge base": "База знаний",
|
||||
"Language": "Язык",
|
||||
"Last rev.": "Посл. изм.",
|
||||
"Let's log in": "Давайте авторизуемся",
|
||||
"Link copied": "Ссылка скопирована",
|
||||
|
@ -205,6 +210,7 @@
|
|||
"Manifest": "Манифест",
|
||||
"Manifesto": "Манифест",
|
||||
"Many files, choose only one": "Много файлов, выберете один",
|
||||
"Mark as read": "Отметить прочитанным",
|
||||
"Material card": "Карточка материала",
|
||||
"Message": "Написать",
|
||||
"More": "Ещё",
|
||||
|
@ -238,6 +244,7 @@
|
|||
"Our regular contributor": "Наш постоянный автор",
|
||||
"Paragraphs": "Абзацев",
|
||||
"Participating": "Участвовать",
|
||||
"Participation": "Соучастие",
|
||||
"Partners": "Партнёры",
|
||||
"Password": "Пароль",
|
||||
"Password again": "Пароль ещё раз",
|
||||
|
@ -256,6 +263,8 @@
|
|||
"Please enter password": "Пожалуйста, введите пароль",
|
||||
"Please enter password again": "Пожалуйста, введите пароль ещё рез",
|
||||
"Please, confirm email": "Пожалуйста, подтвердите электронную почту",
|
||||
"Podcasts": "Подкасты",
|
||||
"Poetry": "Поэзия",
|
||||
"Popular": "Популярное",
|
||||
"Popular authors": "Популярные авторы",
|
||||
"Preview": "Предпросмотр",
|
||||
|
@ -280,6 +289,7 @@
|
|||
"Remove link": "Убрать ссылку",
|
||||
"Reply": "Ответить",
|
||||
"Report": "Пожаловаться",
|
||||
"Reports": "Репортажи",
|
||||
"Required": "Поле обязательно для заполнения",
|
||||
"Resend code": "Выслать подтверждение",
|
||||
"Restore password": "Восстановить пароль",
|
||||
|
@ -305,17 +315,20 @@
|
|||
"Show table of contents": "Показать главление",
|
||||
"Slug": "Постоянная ссылка",
|
||||
"Social networks": "Социальные сети",
|
||||
"Society": "Общество",
|
||||
"Something went wrong, check email and password": "Что-то пошло не так. Проверьте адрес электронной почты и пароль",
|
||||
"Something went wrong, please try again": "Что-то пошло не так, попробуйте еще раз",
|
||||
"Song lyrics": "Текст песни...",
|
||||
"Song title": "Название песни",
|
||||
"Sorry, this address is already taken, please choose another one.": "Увы, этот адрес уже занят, выберите другой",
|
||||
"Special Projects": "Спецпроекты",
|
||||
"Special projects": "Спецпроекты",
|
||||
"Specify the source and the name of the author": "Укажите источник и имя автора",
|
||||
"Start conversation": "Начать беседу",
|
||||
"Subheader": "Подзаголовок",
|
||||
"Subscribe": "Подписаться",
|
||||
"Subscribe to comments": "Подписаться на комментарии",
|
||||
"Subscribe to the best publications newsletter": "Подпишитесь на рассылку лучших публикаций",
|
||||
"Subscribe us": "Подпишитесь на нас",
|
||||
"Subscribe what you like to tune your personal feed": "Подпишитесь на интересующие вас темы, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
||||
"Subscribe who you like to tune your personal feed": "Подпишитесь на интересующих вас авторов, чтобы настроить вашу персональную ленту и моментально узнавать о новых публикациях и обсуждениях",
|
||||
|
@ -327,10 +340,12 @@
|
|||
"Success": "Успешно",
|
||||
"Successfully authorized": "Авторизация успешна",
|
||||
"Suggest an idea": "Предложить идею",
|
||||
"Support the project": "Поддержать проект",
|
||||
"Support us": "Помочь журналу",
|
||||
"Terms of use": "Правила сайта",
|
||||
"Text checking": "Проверка текста",
|
||||
"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 publishing settings. Are you sure you want to leave the page without saving?": "В настройках публикации есть несохраненные изменения. Уверены, что хотите покинуть страницу без сохранения?",
|
||||
"This comment has not yet been rated": "Этот комментарий еще пока никто не оценил",
|
||||
|
@ -358,6 +373,7 @@
|
|||
"Unnamed draft": "Черновик без названия",
|
||||
"Upload": "Загрузить",
|
||||
"Upload error": "Ошибка загрузки",
|
||||
"Upload userpic": "Загрузить аватар",
|
||||
"Upload video": "Загрузить видео",
|
||||
"Uploading image": "Загружаем изображение",
|
||||
"Username": "Имя пользователя",
|
||||
|
@ -422,7 +438,6 @@
|
|||
"feed": "лента",
|
||||
"follower": "подписчик",
|
||||
"followersWithCount": "{count} {count, plural, one {подписчик} few {подписчика} other {подписчиков}}",
|
||||
"general feed": "Общая лента",
|
||||
"header 1": "заголовок 1",
|
||||
"header 2": "заголовок 2",
|
||||
"header 3": "заголовок 3",
|
||||
|
@ -453,11 +468,13 @@
|
|||
"subscriber": "подписчик",
|
||||
"subscriber_rp": "подписчика",
|
||||
"subscribers": "подписчиков",
|
||||
"subscribing...": "Подписка...",
|
||||
"terms of use": "правилами пользования сайтом",
|
||||
"today": "сегодня",
|
||||
"topics": "темы",
|
||||
"user already exist": "пользователь уже существует",
|
||||
"video": "видео",
|
||||
"view": "просмотр",
|
||||
"viewsWithCount": "{count} {count, plural, one {просмотр} few {просмотрa} other {просмотров}}",
|
||||
"yesterday": "вчера"
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
h1 {
|
||||
@include font-size(4rem);
|
||||
|
||||
line-height: 1.1;
|
||||
margin-top: 0.5em;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@include font-size(4rem);
|
||||
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
|
@ -34,10 +36,12 @@ img {
|
|||
img {
|
||||
display: block;
|
||||
margin-bottom: 0.5em;
|
||||
cursor: zoom-in;
|
||||
}
|
||||
|
||||
blockquote,
|
||||
blockquote[data-type='punchline'] {
|
||||
clear: both;
|
||||
font-size: 2.6rem;
|
||||
font-weight: bold;
|
||||
line-height: 1.4;
|
||||
|
@ -61,6 +65,7 @@ img {
|
|||
ta-quotation {
|
||||
border: solid #000;
|
||||
border-width: 0 0 0 2px;
|
||||
clear: both;
|
||||
display: block;
|
||||
font-weight: 500;
|
||||
line-height: 1.6;
|
||||
|
@ -71,6 +76,10 @@ img {
|
|||
&[data-float='right'] {
|
||||
@include font-size(2.2rem);
|
||||
line-height: 1.4;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
clear: none;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
@ -92,9 +101,11 @@ img {
|
|||
ta-sub,
|
||||
ta-selection-frame,
|
||||
ta-border-sub {
|
||||
background: #f1f2f3;
|
||||
display: block;
|
||||
@include font-size(1.4rem);
|
||||
|
||||
background: #f1f2f3;
|
||||
clear: both;
|
||||
display: block;
|
||||
margin: 3.2rem 0;
|
||||
padding: 3.2rem;
|
||||
|
||||
|
@ -173,9 +184,10 @@ img {
|
|||
|
||||
:global(.img-align-left) {
|
||||
float: left;
|
||||
margin: 1em 8.3333% 1.5em 0;
|
||||
margin: 0 8.3333% 1.5em 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
:global(.width-30) {
|
||||
width: 30%;
|
||||
}
|
||||
|
@ -183,6 +195,7 @@ img {
|
|||
:global(.width-50) {
|
||||
width: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.img-align-left.width-50) {
|
||||
@include media-breakpoint-up(xl) {
|
||||
|
@ -191,13 +204,15 @@ img {
|
|||
}
|
||||
|
||||
:global(.img-align-right) {
|
||||
@include media-breakpoint-up(sm) {
|
||||
float: right;
|
||||
margin: 1em 0 1.5em 8.3333%;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.img-align-right.width-50) {
|
||||
@include media-breakpoint-up(xl) {
|
||||
margin-right: -16.6666%;
|
||||
margin-right: -8.3333%;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -240,7 +255,6 @@ img {
|
|||
.shoutAuthorsList {
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
margin: 2em 0;
|
||||
padding-bottom: 2em;
|
||||
|
||||
h4 {
|
||||
color: #696969;
|
||||
|
@ -296,22 +310,24 @@ img {
|
|||
display: flex;
|
||||
justify-content: flex-start;
|
||||
padding: 3rem 0 0;
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.shoutStatsItem {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
align-items: center;
|
||||
font-weight: 500;
|
||||
display: flex;
|
||||
margin: 0 6% 1em 0;
|
||||
margin: 0 2rem 1em 0;
|
||||
vertical-align: baseline;
|
||||
cursor: pointer;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
@include media-breakpoint-up(xl) {
|
||||
margin-right: 3.2rem;
|
||||
}
|
||||
|
||||
|
@ -355,6 +371,14 @@ img {
|
|||
}
|
||||
}
|
||||
|
||||
.shoutStatsItemBookmarks {
|
||||
margin-left: auto;
|
||||
|
||||
@include media-breakpoint-up(lg) {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.shoutStatsItemInner {
|
||||
cursor: pointer;
|
||||
|
||||
|
@ -378,31 +402,71 @@ img {
|
|||
|
||||
.shoutStatsItemAdditionalData {
|
||||
color: rgb(0 0 0 / 40%);
|
||||
cursor: default;
|
||||
font-weight: normal;
|
||||
justify-self: flex-end;
|
||||
margin-right: 0;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
cursor: default;
|
||||
|
||||
.icon {
|
||||
opacity: 0.4;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@include media-breakpoint-down(lg) {
|
||||
flex: 1 100%;
|
||||
order: 9;
|
||||
|
||||
.shoutStatsItemAdditionalDataItem {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.shoutStatsItemViews {
|
||||
color: rgb(0 0 0 / 0.4);
|
||||
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 {
|
||||
font-weight: normal;
|
||||
display: inline-block;
|
||||
margin-left: 2rem;
|
||||
//margin-left: 2rem;
|
||||
margin-right: 0;
|
||||
margin-bottom: 0;
|
||||
cursor: default;
|
||||
|
@ -416,6 +480,7 @@ img {
|
|||
|
||||
.topicsList {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
border-bottom: 1px solid #e8e8e8;
|
||||
letter-spacing: 0.08em;
|
||||
margin-top: 1.6rem;
|
||||
|
@ -455,12 +520,15 @@ img {
|
|||
}
|
||||
|
||||
.commentsHeaderWrapper {
|
||||
@include media-breakpoint-up(sm) {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
}
|
||||
|
||||
.commentsHeader {
|
||||
@include font-size(2.4rem);
|
||||
|
||||
margin-bottom: 1em;
|
||||
|
||||
.newReactions {
|
||||
|
@ -494,6 +562,7 @@ img {
|
|||
|
||||
button {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
border-radius: 0.8rem;
|
||||
margin-right: 1.2rem;
|
||||
padding: 0.9rem 1.2rem;
|
||||
|
@ -575,13 +644,14 @@ a[data-toggle='tooltip'] {
|
|||
width: 0;
|
||||
height: 0;
|
||||
border-style: solid;
|
||||
border-width: 4px 4px 0 4px;
|
||||
border-width: 4px 4px 0;
|
||||
border-color: var(--black-500) transparent transparent transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.lead {
|
||||
@include font-size(1.8rem);
|
||||
|
||||
font-weight: 600;
|
||||
|
||||
b,
|
||||
|
@ -589,3 +659,19 @@ a[data-toggle='tooltip'] {
|
|||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
.articlePopupOpener {
|
||||
.iconHover {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.iconHover {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AudioHeader.module.scss'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { createSignal, Show } from 'solid-js'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { Topic } from '../../../graphql/types.gen'
|
||||
import { CardTopic } from '../../Feed/CardTopic'
|
||||
import { Image } from '../../_shared/Image'
|
||||
|
||||
type Props = {
|
||||
title: string
|
||||
|
@ -19,7 +19,7 @@ export const AudioHeader = (props: Props) => {
|
|||
return (
|
||||
<div class={clsx(styles.AudioHeader, { [styles.expandedImage]: expandedImage() })}>
|
||||
<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}>
|
||||
<button type="button" class={styles.expand} onClick={() => setExpandedImage(!expandedImage())}>
|
||||
<Icon name="expand-circle" />
|
||||
|
|
|
@ -51,7 +51,6 @@
|
|||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: #141414;
|
||||
|
@ -108,7 +107,7 @@
|
|||
position: relative;
|
||||
width: 100%;
|
||||
cursor: pointer;
|
||||
border-bottom: 2px solid #cccccc;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
.progressFilled {
|
||||
|
@ -126,7 +125,6 @@
|
|||
position: absolute;
|
||||
bottom: -10px;
|
||||
right: -8px;
|
||||
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
|
@ -140,7 +138,6 @@
|
|||
padding-top: 14px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
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');
|
||||
|
||||
.volume {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
height: 19px;
|
||||
float: left;
|
||||
outline: none;
|
||||
|
@ -182,7 +179,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
@each $vendor in $vendors-thumb {
|
||||
&#{$vendor} {
|
||||
position: relative;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
box-sizing: content-box;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
|
@ -190,7 +187,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
border: 4px solid var(--default-color);
|
||||
background-color: var(--background-color);
|
||||
cursor: pointer;
|
||||
margin: -7px 0 0 0;
|
||||
margin: -7px 0 0;
|
||||
}
|
||||
&:active#{$vendor} {
|
||||
transform: scale(1.2);
|
||||
|
@ -201,6 +198,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
&::-moz-range-progress {
|
||||
background-color: var(--background-color);
|
||||
}
|
||||
|
||||
&::-moz-focus-outer {
|
||||
border: 0;
|
||||
}
|
||||
|
@ -209,7 +207,6 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
.playlist {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
list-style-type: none;
|
||||
margin: 32px 0 16px;
|
||||
padding: 0;
|
||||
|
@ -222,7 +219,6 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
.playlistItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
min-height: 56px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
@ -319,6 +315,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
&:not([disabled]):hover {
|
||||
border-color: var(--background-color-invert);
|
||||
background: var(--background-color-invert);
|
||||
|
||||
img {
|
||||
filter: var(--icon-filter-hover);
|
||||
}
|
||||
|
@ -334,7 +331,7 @@ $vendors-thumb: ('::-webkit-slider-thumb', '::-moz-moz-range-thumb', '::-ms-thum
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 8px 0 24px 0;
|
||||
padding: 8px 0 24px;
|
||||
|
||||
.description,
|
||||
.lyrics {
|
||||
|
|
|
@ -3,7 +3,6 @@ import { PlayerHeader } from './PlayerHeader'
|
|||
import { PlayerPlaylist } from './PlayerPlaylist'
|
||||
import styles from './AudioPlayer.module.scss'
|
||||
import { MediaItem } from '../../../pages/types'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
|
||||
type Props = {
|
||||
media: MediaItem[]
|
||||
|
@ -145,8 +144,7 @@ export const AudioPlayer = (props: Props) => {
|
|||
<audio
|
||||
ref={(el) => (audioRef.current = el)}
|
||||
onTimeUpdate={handleAudioTimeUpdate}
|
||||
// TEMP SOLUTION for http/https
|
||||
src={currentTack().url.startsWith('https') ? currentTack().url : imageProxy(currentTack().url)}
|
||||
src={currentTack().url}
|
||||
onCanPlay={() => {
|
||||
// start to play the next track on src change
|
||||
if (isPlaying()) {
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
.comment {
|
||||
margin: 0 0 0.5em;
|
||||
padding: 1rem;
|
||||
padding: 0 1rem;
|
||||
transition: background-color 0.3s;
|
||||
position: relative;
|
||||
list-style: none;
|
||||
background: rgb(0 0 0 / 0.1);
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
padding-right: 0;
|
||||
}
|
||||
|
||||
&.isNew {
|
||||
border-radius: 6px;
|
||||
background: rgb(38 56 217 / 5%);
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin-right: -1.2rem;
|
||||
}
|
||||
|
||||
.comment {
|
||||
margin-right: -1rem;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: '';
|
||||
left: 0;
|
||||
left: -14px;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
|
@ -26,9 +29,9 @@
|
|||
border-bottom: 2px solid #ccc;
|
||||
border-left: 2px solid #ccc;
|
||||
border-radius: 0 0 0 1.2rem;
|
||||
top: -1rem;
|
||||
height: 2.4rem;
|
||||
width: 1.2rem;
|
||||
top: -24px;
|
||||
height: 50px;
|
||||
width: 12px;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
@ -57,24 +60,29 @@
|
|||
align-items: center;
|
||||
margin-bottom: 1.4rem;
|
||||
}
|
||||
|
||||
.commentControl:not(.commentControlReply) {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.commentContent {
|
||||
padding: 0 1rem 1rem 0;
|
||||
|
||||
&:hover {
|
||||
.commentControlReply,
|
||||
.commentControlShare,
|
||||
.commentControlDelete,
|
||||
.commentControlEdit,
|
||||
.commentControlComplain {
|
||||
.commentControlComplain,
|
||||
.commentControl {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.commentControls {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
margin-bottom: 0.5em;
|
||||
p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.commentControlReply,
|
||||
|
@ -104,7 +112,7 @@
|
|||
|
||||
.commentControl {
|
||||
border: none;
|
||||
color: #696969;
|
||||
color: var(--secondary-color);
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
line-height: 1.2;
|
||||
|
@ -117,8 +125,8 @@
|
|||
vertical-align: top;
|
||||
|
||||
&:hover {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
background: var(--background-color-invert);
|
||||
color: var(--default-color-invert);
|
||||
|
||||
.icon {
|
||||
filter: invert(1);
|
||||
|
@ -173,9 +181,10 @@
|
|||
}
|
||||
|
||||
.articleAuthor {
|
||||
color: #2638d9;
|
||||
font-size: 12px;
|
||||
margin-right: 12px;
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: var(--blue-500);
|
||||
margin: 0.3rem 1rem 0;
|
||||
}
|
||||
|
||||
.articleLink {
|
||||
|
@ -203,6 +212,10 @@
|
|||
margin-right: 1em;
|
||||
vertical-align: middle;
|
||||
width: 1em;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
margin-left: 1em;
|
||||
}
|
||||
}
|
||||
|
||||
.commentDates {
|
|
@ -2,26 +2,26 @@ import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js'
|
|||
import { clsx } from 'clsx'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
|
||||
import MD from './MD'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { Userpic } from '../Author/Userpic'
|
||||
import { CommentRatingControl } from './CommentRatingControl'
|
||||
import { CommentDate } from './CommentDate'
|
||||
import { ShowIfAuthenticated } from '../_shared/ShowIfAuthenticated'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import MD from '../MD'
|
||||
import { Userpic } from '../../Author/Userpic'
|
||||
import { CommentRatingControl } from '../CommentRatingControl'
|
||||
import { CommentDate } from '../CommentDate'
|
||||
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
import { useSession } from '../../context/session'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { useReactions } from '../../context/reactions'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { useConfirm } from '../../context/confirm'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { useReactions } from '../../../context/reactions'
|
||||
import { useSnackbar } from '../../../context/snackbar'
|
||||
import { useConfirm } from '../../../context/confirm'
|
||||
|
||||
import { Author, Reaction, ReactionKind } from '../../graphql/types.gen'
|
||||
import { router } from '../../stores/router'
|
||||
import { Author, Reaction, ReactionKind } from '../../../graphql/types.gen'
|
||||
import { router } from '../../../stores/router'
|
||||
|
||||
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 = {
|
||||
comment: Reaction
|
||||
|
@ -135,7 +135,6 @@ export const Comment = (props: Props) => {
|
|||
<Userpic
|
||||
name={comment().createdBy.name}
|
||||
userpic={comment().createdBy.userpic}
|
||||
isBig={false}
|
||||
class={clsx({
|
||||
[styles.compactUserpic]: props.compact
|
||||
})}
|
||||
|
@ -148,13 +147,7 @@ export const Comment = (props: Props) => {
|
|||
>
|
||||
<div class={styles.commentDetails}>
|
||||
<div class={styles.commentAuthor}>
|
||||
<AuthorCard
|
||||
author={comment()?.createdBy as Author}
|
||||
hideDescription={true}
|
||||
hideFollow={true}
|
||||
isComments={true}
|
||||
hasLink={true}
|
||||
/>
|
||||
<AuthorLink author={comment()?.createdBy as Author} />
|
||||
</div>
|
||||
|
||||
<Show when={props.isArticleAuthor}>
|
||||
|
@ -173,9 +166,7 @@ export const Comment = (props: Props) => {
|
|||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<CommentDate comment={comment()} isShort={true} />
|
||||
|
||||
<CommentDate showOnHover={true} comment={comment()} isShort={true} />
|
||||
<CommentRatingControl comment={comment()} />
|
||||
</div>
|
||||
</Show>
|
||||
|
@ -190,6 +181,7 @@ export const Comment = (props: Props) => {
|
|||
placeholder={t('Write a comment...')}
|
||||
onSubmit={(value) => handleUpdate(value)}
|
||||
submitByCtrlEnter={true}
|
||||
onCancel={() => setEditMode(false)}
|
||||
setClear={clearEditor()}
|
||||
/>
|
||||
</Suspense>
|
||||
|
@ -197,7 +189,7 @@ export const Comment = (props: Props) => {
|
|||
</div>
|
||||
|
||||
<Show when={!props.compact}>
|
||||
<div class={styles.commentControls}>
|
||||
<div>
|
||||
<ShowIfAuthenticated>
|
||||
<button
|
||||
disabled={loading()}
|
1
src/components/Article/Comment/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { Comment } from './Comment'
|
|
@ -1,18 +1,21 @@
|
|||
.commentDates {
|
||||
color: #9fa1a7;
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: var(--secondary-color);
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-wrap: wrap;
|
||||
@include font-size(1.2rem);
|
||||
font-size: 1.2rem;
|
||||
justify-content: flex-start;
|
||||
margin: 0 1em 0 0;
|
||||
margin: 0 1rem;
|
||||
height: 1.6rem;
|
||||
|
||||
.date {
|
||||
font-weight: 500;
|
||||
margin-right: 1rem;
|
||||
position: relative;
|
||||
|
||||
.icon {
|
||||
line-height: 1;
|
||||
|
@ -23,6 +26,26 @@
|
|||
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 {
|
|
@ -1,7 +1,7 @@
|
|||
import { Show } from 'solid-js'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import type { Reaction } from '../../graphql/types.gen'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import type { Reaction } from '../../../graphql/types.gen'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './CommentDate.module.scss'
|
||||
|
||||
|
@ -9,6 +9,7 @@ type Props = {
|
|||
comment: Reaction
|
||||
isShort?: boolean
|
||||
isLastInRow?: boolean
|
||||
showOnHover?: boolean
|
||||
}
|
||||
|
||||
export const CommentDate = (props: Props) => {
|
||||
|
@ -23,12 +24,19 @@ export const CommentDate = (props: Props) => {
|
|||
}
|
||||
|
||||
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>
|
||||
<Show when={props.comment.updatedAt}>
|
||||
<time class={styles.date}>
|
||||
<Icon name="edit" class={styles.icon} />
|
||||
<span class={styles.text}>
|
||||
{t('Edited')} {formattedDate(props.comment.updatedAt)}
|
||||
</span>
|
||||
</time>
|
||||
</Show>
|
||||
</div>
|
1
src/components/Article/CommentDate/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { CommentDate } from './CommentDate'
|
|
@ -10,11 +10,9 @@ import { useReactions } from '../../context/reactions'
|
|||
import { MediaItem } from '../../pages/types'
|
||||
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { TableOfContents } from '../TableOfContents'
|
||||
import { AudioPlayer } from './AudioPlayer'
|
||||
import { SharePopup } from './SharePopup'
|
||||
import { getShareUrl, SharePopup } from './SharePopup'
|
||||
import { ShoutRatingControl } from './ShoutRatingControl'
|
||||
import { CommentsTree } from './CommentsTree'
|
||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||
|
@ -22,10 +20,14 @@ import { AudioHeader } from './AudioHeader'
|
|||
import { Popover } from '../_shared/Popover'
|
||||
import { VideoPlayer } from '../_shared/VideoPlayer'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { SolidSwiper } from '../_shared/SolidSwiper'
|
||||
import { ImageSwiper } from '../_shared/SolidSwiper'
|
||||
import styles from './Article.module.scss'
|
||||
import { CardTopic } from '../Feed/CardTopic'
|
||||
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 = {
|
||||
article: Shout
|
||||
|
@ -48,6 +50,8 @@ const scrollTo = (el: HTMLElement) => {
|
|||
}
|
||||
|
||||
export const FullArticle = (props: Props) => {
|
||||
const [selectedImage, setSelectedImage] = createSignal('')
|
||||
|
||||
const { t, formatDate } = useLocalize()
|
||||
const {
|
||||
user,
|
||||
|
@ -168,7 +172,7 @@ export const FullArticle = (props: Props) => {
|
|||
document.body.appendChild(tooltip)
|
||||
|
||||
if (element.hasAttribute('href')) {
|
||||
element.setAttribute('href', 'javascript: void(0);')
|
||||
element.setAttribute('href', 'javascript: void(0)')
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<Title>{props.article.title}</Title>
|
||||
|
@ -266,7 +284,9 @@ export const FullArticle = (props: Props) => {
|
|||
>
|
||||
<div
|
||||
class={styles.shoutCover}
|
||||
style={{ 'background-image': `url('${imageProxy(props.article.cover)}')` }}
|
||||
style={{
|
||||
'background-image': `url('${getImageUrl(props.article.cover, { width: 1600 })}')`
|
||||
}}
|
||||
/>
|
||||
</Show>
|
||||
</div>
|
||||
|
@ -308,7 +328,7 @@ export const FullArticle = (props: Props) => {
|
|||
</Show>
|
||||
|
||||
<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()} />}>
|
||||
<MD body={body()} />
|
||||
</Show>
|
||||
|
@ -329,7 +349,7 @@ export const FullArticle = (props: Props) => {
|
|||
<div class="wide-container">
|
||||
<div class="row">
|
||||
<div class="col-md-20 offset-md-2">
|
||||
<SolidSwiper images={media()} />
|
||||
<ImageSwiper images={media()} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -346,22 +366,46 @@ export const FullArticle = (props: Props) => {
|
|||
|
||||
<Popover content={t('Comment')}>
|
||||
{(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-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>
|
||||
)}
|
||||
</Popover>
|
||||
|
||||
<Show when={props.article.stat?.viewed}>
|
||||
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemViews)}>
|
||||
<Icon name="eye" class={styles.icon} />
|
||||
<Icon name="eye" class={clsx(styles.icon, styles.iconHover)} />
|
||||
{props.article.stat?.viewed}
|
||||
{t('viewsWithCount', { count: props.article.stat?.viewed })}
|
||||
</div>
|
||||
</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')}>
|
||||
{(triggerRef: (el) => void) => (
|
||||
<div class={styles.shoutStatsItem} ref={triggerRef}>
|
||||
|
@ -380,16 +424,7 @@ export const FullArticle = (props: Props) => {
|
|||
</div>
|
||||
)}
|
||||
</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()}>
|
||||
<Popover content={t('Edit')}>
|
||||
{(triggerRef: (el) => void) => (
|
||||
|
@ -405,20 +440,33 @@ export const FullArticle = (props: Props) => {
|
|||
)}
|
||||
</Popover>
|
||||
</Show>
|
||||
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalData)}>
|
||||
<div class={clsx(styles.shoutStatsItem, styles.shoutStatsItemAdditionalDataItem)}>
|
||||
{formattedDate()}
|
||||
|
||||
<FeedArticlePopup
|
||||
isOwner={canEdit()}
|
||||
containerCssClass={clsx(stylesHeader.control, styles.articlePopupOpener)}
|
||||
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>
|
||||
<div class={styles.help}>
|
||||
|
||||
<Show when={isAuthenticated() && !canEdit()}>
|
||||
<div class={styles.help}>
|
||||
<button class="button">{t('Cooperate')}</button>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={canEdit()}>
|
||||
<div class={styles.help}>
|
||||
<button class="button button--light">{t('Invite to collab')}</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.article.topics.length}>
|
||||
<div class={styles.topicsList}>
|
||||
|
@ -437,9 +485,9 @@ export const FullArticle = (props: Props) => {
|
|||
<h4>{t('Authors')}</h4>
|
||||
</Show>
|
||||
<For each={props.article.authors}>
|
||||
{(a) => (
|
||||
{(author) => (
|
||||
<div class="col-xl-12">
|
||||
<AuthorCard author={a} hasLink={true} liteButtons={true} />
|
||||
<AuthorBadge iconButtons={true} showMessageButton={true} author={author} />
|
||||
</div>
|
||||
)}
|
||||
</For>
|
||||
|
@ -456,6 +504,9 @@ export const FullArticle = (props: Props) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={selectedImage()}>
|
||||
<Lightbox image={selectedImage()} onClose={handleLightboxClose} />
|
||||
</Show>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
51
src/components/Author/AhtorLink/AhtorLink.module.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
21
src/components/Author/AhtorLink/AuthorLink.tsx
Normal 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>
|
||||
)
|
||||
}
|
1
src/components/Author/AhtorLink/index.ts
Normal file
|
@ -0,0 +1 @@
|
|||
export { AuthorLink } from './AuthorLink'
|
|
@ -1,26 +1,44 @@
|
|||
.AuthorBadge {
|
||||
align-items: flex-start;
|
||||
display: flex;
|
||||
flex-flow: row nowrap;
|
||||
margin-bottom: 2rem;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
margin-bottom: 3rem;
|
||||
|
||||
&.nameOnly {
|
||||
align-items: center;
|
||||
|
||||
.info {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
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 {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
border: none;
|
||||
display: flex;
|
||||
flex: 0 calc(100% - 5.2rem);
|
||||
flex-direction: column;
|
||||
line-height: 1.3;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
flex: 1 100%;
|
||||
|
@ -33,6 +51,11 @@
|
|||
.name {
|
||||
color: var(--default-color);
|
||||
font-weight: 500;
|
||||
|
||||
& span:hover {
|
||||
color: var(--default-color-invert);
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
|
||||
.bio {
|
||||
|
@ -42,7 +65,14 @@
|
|||
|
||||
.actions {
|
||||
flex: 0 20%;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
margin-left: 5.2rem;
|
||||
gap: 1rem;
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin-left: 2rem;
|
||||
|
@ -56,9 +86,33 @@
|
|||
}
|
||||
}
|
||||
|
||||
.subscribeButton {
|
||||
.actionButton {
|
||||
border-radius: 0.8rem !important;
|
||||
margin-right: 0 !important;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './AuthorBadge.module.scss'
|
||||
import stylesButton from '../../_shared/Button/Button.module.scss'
|
||||
import { Userpic } from '../Userpic'
|
||||
import { Author, FollowingEntity } from '../../../graphql/types.gen'
|
||||
import { createMemo, createSignal, Match, Show, Switch } from 'solid-js'
|
||||
|
@ -8,22 +9,29 @@ import { Button } from '../../_shared/Button'
|
|||
import { useSession } from '../../../context/session'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { CheckButton } from '../../_shared/CheckButton'
|
||||
import { openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
|
||||
type Props = {
|
||||
author: Author
|
||||
minimizeSubscribeButton?: boolean
|
||||
showMessageButton?: boolean
|
||||
iconButtons?: boolean
|
||||
nameOnly?: boolean
|
||||
}
|
||||
export const AuthorBadge = (props: Props) => {
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
const {
|
||||
session,
|
||||
actions: { loadSession, requireAuthentication }
|
||||
subscriptions,
|
||||
actions: { loadSubscriptions, requireAuthentication }
|
||||
} = useSession()
|
||||
|
||||
const { changeSearchParam } = useRouter()
|
||||
const { t, formatDate } = useLocalize()
|
||||
const subscribed = createMemo<boolean>(() => {
|
||||
return session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||
})
|
||||
const subscribed = createMemo(() =>
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug)
|
||||
)
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
setIsSubscribing(true)
|
||||
|
@ -32,7 +40,7 @@ export const AuthorBadge = (props: Props) => {
|
|||
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
||||
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
||||
|
||||
await loadSession()
|
||||
await loadSubscriptions()
|
||||
setIsSubscribing(false)
|
||||
}
|
||||
const handleSubscribe = (really: boolean) => {
|
||||
|
@ -41,17 +49,49 @@ export const AuthorBadge = (props: Props) => {
|
|||
}, '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 (
|
||||
<div class={clsx(styles.AuthorBadge)}>
|
||||
<>
|
||||
<span class={styles.actionButtonLabel}>{t('Following')}</span>
|
||||
<span class={styles.actionButtonLabelHovered}>{t('Unfollow')}</span>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.AuthorBadge, { [styles.nameOnly]: props.nameOnly })}>
|
||||
<div class={styles.basicInfo}>
|
||||
<Userpic
|
||||
hasLink={true}
|
||||
isMedium={true}
|
||||
size={'M'}
|
||||
name={props.author.name}
|
||||
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>
|
||||
<div class={styles.name}>
|
||||
<span>{props.author.name}</span>
|
||||
</div>
|
||||
<Show when={!props.nameOnly}>
|
||||
<Switch
|
||||
fallback={
|
||||
<div class={styles.bio}>
|
||||
|
@ -68,8 +108,10 @@ export const AuthorBadge = (props: Props) => {
|
|||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</Show>
|
||||
</a>
|
||||
<Show when={props.author.slug !== session()?.user.slug}>
|
||||
</div>
|
||||
<Show when={props.author.slug !== session()?.user.slug && !props.nameOnly}>
|
||||
<div class={styles.actions}>
|
||||
<Show
|
||||
when={!props.minimizeSubscribeButton}
|
||||
|
@ -85,23 +127,40 @@ export const AuthorBadge = (props: Props) => {
|
|||
when={subscribed()}
|
||||
fallback={
|
||||
<Button
|
||||
variant="primary"
|
||||
size="S"
|
||||
value={isSubscribing() ? t('...subscribing') : t('Subscribe')}
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="M"
|
||||
value={subscribeValue()}
|
||||
onClick={() => handleSubscribe(true)}
|
||||
class={styles.subscribeButton}
|
||||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.iconed]: props.iconButtons,
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
})}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="S"
|
||||
value={t('Following')}
|
||||
variant={props.iconButtons ? 'secondary' : 'bordered'}
|
||||
size="M"
|
||||
value={unsubscribeValue()}
|
||||
onClick={() => handleSubscribe(false)}
|
||||
class={styles.subscribeButton}
|
||||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.iconed]: props.iconButtons,
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
})}
|
||||
/>
|
||||
</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>
|
||||
</Show>
|
||||
</div>
|
||||
|
|
|
@ -8,10 +8,50 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
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 & {
|
||||
align-items: flex-start;
|
||||
margin-bottom: 2rem;
|
||||
|
@ -36,10 +76,16 @@
|
|||
@include media-breakpoint-down(lg) {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.buttonWriteMessage {
|
||||
border-radius: 0.8rem;
|
||||
padding-bottom: 0.6rem;
|
||||
padding-top: 0.6rem;
|
||||
}
|
||||
}
|
||||
|
||||
.authorDetails {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
align-items: center;
|
||||
|
@ -57,10 +103,6 @@
|
|||
flex-wrap: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&.authorDetailsShrinked {
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.authorDetailsWrapper {
|
||||
|
@ -84,29 +126,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.authorNameContainer {
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.authorName {
|
||||
border: none !important;
|
||||
font-size: 1.6rem;
|
||||
font-weight: 500;
|
||||
margin-bottom: 0.8rem;
|
||||
@include font-size(4rem);
|
||||
|
||||
.listWrapper & {
|
||||
display: block;
|
||||
|
||||
&:before {
|
||||
content: '';
|
||||
height: 100%;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
z-index: 2;
|
||||
}
|
||||
}
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.authorAbout {
|
||||
|
@ -117,42 +140,6 @@
|
|||
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 {
|
||||
display: none;
|
||||
}
|
||||
|
@ -195,9 +182,10 @@
|
|||
}
|
||||
|
||||
.authorSubscribeSocialLabel {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
color: #000;
|
||||
display: block;
|
||||
@include font-size(1.6rem);
|
||||
left: 100%;
|
||||
padding-left: 0.4rem;
|
||||
position: absolute;
|
||||
|
@ -228,7 +216,8 @@
|
|||
}
|
||||
}
|
||||
|
||||
&[href*='telegram.com/'] {
|
||||
&[href*='telegram.com/'],
|
||||
&[href*='t.me/'] {
|
||||
&::before {
|
||||
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 {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -648,33 +435,35 @@
|
|||
vertical-align: top;
|
||||
border-bottom: unset !important;
|
||||
|
||||
&:last-child {
|
||||
margin-right: 0;
|
||||
.subscribersItem {
|
||||
position: relative;
|
||||
|
||||
&:nth-child(1) {
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.userpic {
|
||||
background: var(--background-color);
|
||||
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) {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
> * {
|
||||
line-height: 1.8rem;
|
||||
min-width: auto;
|
||||
&:not(:last-child) {
|
||||
margin-right: -4px;
|
||||
box-shadow: 0 0 0 1px var(--background-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.subscribersCounter {
|
||||
.subscribersCounter {
|
||||
font-weight: 500;
|
||||
margin-left: -0.6rem;
|
||||
margin-left: 1rem;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: none !important;
|
||||
|
||||
.subscribersCounter {
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.listWrapper {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { Author } from '../../../graphql/types.gen'
|
||||
import { Userpic } from '../Userpic'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { createEffect, createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import { translit } from '../../../utils/ru2en'
|
||||
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 { openPage, redirectPage } from '@nanostores/router'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
import { SubscriptionFilter } from '../../../pages/types'
|
||||
import { isAuthor } from '../../../utils/isAuthor'
|
||||
|
@ -20,48 +18,30 @@ import { TopicBadge } from '../../Topic/TopicBadge'
|
|||
import { Button } from '../../_shared/Button'
|
||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||
import styles from './AuthorCard.module.scss'
|
||||
import stylesButton from '../../_shared/Button/Button.module.scss'
|
||||
|
||||
type Props = {
|
||||
caption?: string
|
||||
hideWriteButton?: boolean
|
||||
hideDescription?: boolean
|
||||
hideFollow?: boolean
|
||||
hasLink?: boolean
|
||||
subscribed?: boolean
|
||||
author: Author
|
||||
isAuthorPage?: boolean
|
||||
noSocialButtons?: boolean
|
||||
isAuthorsList?: boolean
|
||||
truncateBio?: boolean
|
||||
liteButtons?: boolean
|
||||
isTextButton?: boolean
|
||||
isComments?: boolean
|
||||
isFeedMode?: boolean
|
||||
isNowrap?: boolean
|
||||
class?: string
|
||||
followers?: Author[]
|
||||
following?: Array<Author | Topic>
|
||||
showPublicationsCounter?: boolean
|
||||
hideBio?: boolean
|
||||
isCurrentUser?: boolean
|
||||
}
|
||||
|
||||
export const AuthorCard = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
const {
|
||||
session,
|
||||
subscriptions,
|
||||
isSessionLoaded,
|
||||
actions: { loadSession, requireAuthentication }
|
||||
actions: { loadSubscriptions, requireAuthentication }
|
||||
} = useSession()
|
||||
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
const [following, setFollowing] = createSignal<Array<Author | Topic>>(props.following)
|
||||
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
|
||||
const [userpicUrl, setUserpicUrl] = createSignal<string>()
|
||||
|
||||
const subscribed = createMemo<boolean>(() => {
|
||||
return session()?.news?.authors?.some((u) => u === props.author.slug) || false
|
||||
})
|
||||
const subscribed = createMemo<boolean>(() =>
|
||||
subscriptions().authors.some((author) => author.slug === props.author.slug)
|
||||
)
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
setIsSubscribing(true)
|
||||
|
@ -70,11 +50,11 @@ export const AuthorCard = (props: Props) => {
|
|||
? follow({ what: FollowingEntity.Author, slug: props.author.slug })
|
||||
: unfollow({ what: FollowingEntity.Author, slug: props.author.slug }))
|
||||
|
||||
await loadSession()
|
||||
await loadSubscriptions()
|
||||
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(() => {
|
||||
if (lang() !== 'ru') {
|
||||
|
@ -101,7 +81,7 @@ export const AuthorCard = (props: Props) => {
|
|||
|
||||
const handleSubscribe = () => {
|
||||
requireAuthentication(() => {
|
||||
subscribe(true)
|
||||
subscribe(!subscribed())
|
||||
}, 'subscribe')
|
||||
}
|
||||
|
||||
|
@ -117,89 +97,36 @@ export const AuthorCard = (props: Props) => {
|
|||
}
|
||||
})
|
||||
|
||||
if (props.isAuthorPage && props.author.userpic?.includes('assets.discours.io')) {
|
||||
setUserpicUrl(props.author.userpic.replace('100x', '500x500'))
|
||||
}
|
||||
const followButtonText = () => {
|
||||
if (isSubscribing()) {
|
||||
return t('subscribing...')
|
||||
} else if (subscribed()) {
|
||||
return (
|
||||
<div
|
||||
class={clsx(styles.author, props.class)}
|
||||
classList={{
|
||||
['row']: props.isAuthorPage,
|
||||
[styles.authorPage]: props.isAuthorPage,
|
||||
[styles.authorComments]: props.isComments,
|
||||
[styles.authorsListItem]: props.isAuthorsList,
|
||||
[styles.feedMode]: props.isFeedMode,
|
||||
[styles.nowrapView]: props.isNowrap
|
||||
}}
|
||||
>
|
||||
<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}
|
||||
/>
|
||||
<>
|
||||
<span class={stylesButton.buttonSubscribeLabel}>{t('Following')}</span>
|
||||
<span class={stylesButton.buttonSubscribeLabelHovered}>{t('Unfollow')}</span>
|
||||
</>
|
||||
)
|
||||
} else {
|
||||
return t('Follow')
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
return (
|
||||
<div class={clsx(styles.author, 'row')}>
|
||||
<div class="col-md-5">
|
||||
<Userpic
|
||||
size={'XL'}
|
||||
name={props.author.name}
|
||||
userpic={userpicUrl()}
|
||||
hasLink={props.hasLink}
|
||||
isBig={props.isAuthorPage}
|
||||
isAuthorsList={props.isAuthorsList}
|
||||
isFeedMode={props.isFeedMode}
|
||||
userpic={props.author.userpic}
|
||||
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={clsx('col-md-15 col-xl-13', styles.authorDetails)}>
|
||||
<div class={styles.authorDetailsWrapper}>
|
||||
<div class={styles.authorNameContainer}>
|
||||
<ConditionalWrapper
|
||||
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>
|
||||
|
||||
<div class={styles.authorName}>{name()}</div>
|
||||
<div class={styles.authorAbout} innerHTML={props.author.bio} />
|
||||
<Show
|
||||
when={
|
||||
(props.followers && props.followers.length > 0) ||
|
||||
|
@ -210,10 +137,17 @@ export const AuthorCard = (props: Props) => {
|
|||
<Show when={props.followers && props.followers.length > 0}>
|
||||
<a href="?modal=followers" class={styles.subscribers}>
|
||||
<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>
|
||||
<div class={styles.subscribersCounter}>
|
||||
{t('SubscriberWithCount', { count: props.followers.length })}
|
||||
{t('SubscriberWithCount', { count: props.followers.length ?? 0 })}
|
||||
</div>
|
||||
</a>
|
||||
</Show>
|
||||
|
@ -223,9 +157,23 @@ export const AuthorCard = (props: Props) => {
|
|||
<For each={props.following.slice(0, 3)}>
|
||||
{(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) {
|
||||
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
|
||||
}}
|
||||
|
@ -258,102 +206,35 @@ export const AuthorCard = (props: Props) => {
|
|||
</For>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={canFollow()}>
|
||||
<div class={styles.authorSubscribe}>
|
||||
|
||||
<Show
|
||||
when={subscribed()}
|
||||
when={isProfileOwner()}
|
||||
fallback={
|
||||
<button
|
||||
<div class={styles.authorActions}>
|
||||
<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>
|
||||
value={followButtonText()}
|
||||
isSubscribeButton={true}
|
||||
class={clsx({
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
})}
|
||||
/>
|
||||
<Button
|
||||
variant={'secondary'}
|
||||
value={t('Message')}
|
||||
onClick={initChat}
|
||||
class={styles.buttonWriteMessage}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<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}>
|
||||
<button
|
||||
class={styles.button}
|
||||
classList={{
|
||||
'button--light': !props.isAuthorsList,
|
||||
'button--subscribe-topic': props.isAuthorsList,
|
||||
[styles.buttonWrite]: props.liteButtons && props.isAuthorsList
|
||||
}}
|
||||
onClick={initChat}
|
||||
>
|
||||
<Show when={!props.isTextButton && !props.isAuthorPage}>
|
||||
<Icon name="comment" class={styles.icon} />
|
||||
</Show>
|
||||
<Show when={!props.liteButtons || props.isTextButton}>{t('Message')}</Show>
|
||||
</button>
|
||||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={props.isCurrentUser}>
|
||||
<div class={styles.authorSubscribe}>
|
||||
<div class={styles.authorActions}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => redirectPage(router, 'profileSettings')}
|
||||
value={t('Edit profile')}
|
||||
class={styles.button}
|
||||
/>
|
||||
|
||||
<SharePopup
|
||||
containerCssClass={styles.shareControl}
|
||||
title={props.author.name}
|
||||
description={props.author.bio}
|
||||
imageUrl={props.author.userpic}
|
||||
|
|
|
@ -1,14 +1,9 @@
|
|||
.Userpic {
|
||||
align-items: baseline;
|
||||
background: #f7f7f8;
|
||||
background: #f7f7f7;
|
||||
border-radius: 100%;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin-right: 1.2rem;
|
||||
min-width: 32px;
|
||||
max-width: 32px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
align-items: center;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
|
||||
|
@ -20,24 +15,17 @@
|
|||
}
|
||||
|
||||
.letters {
|
||||
background-color: white;
|
||||
border-radius: 50%;
|
||||
border: 1.5px solid black;
|
||||
color: #000;
|
||||
font-size: small;
|
||||
text-align: center;
|
||||
text-transform: uppercase;
|
||||
line-height: 32px;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-width: 32px;
|
||||
}
|
||||
|
||||
.anonymous {
|
||||
height: 17px !important;
|
||||
object-fit: contain;
|
||||
width: 20px !important;
|
||||
margin: auto;
|
||||
width: 100%;
|
||||
border-radius: 100%;
|
||||
padding-top: 2px; // line-height hack
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
color: var(--default-color);
|
||||
text-transform: uppercase;
|
||||
background: var(--background-color);
|
||||
box-shadow: 0 0 0 1px var(--background-color-invert) inset;
|
||||
}
|
||||
|
||||
a:link,
|
||||
|
@ -55,18 +43,49 @@
|
|||
}
|
||||
}
|
||||
|
||||
&.medium {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
min-width: 40px;
|
||||
max-width: 40px;
|
||||
&.XS {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
min-width: 20px;
|
||||
overflow: hidden;
|
||||
|
||||
.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;
|
||||
margin: 0 auto 1rem;
|
||||
max-width: 168px;
|
||||
|
@ -101,12 +120,3 @@
|
|||
line-height: 6.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
.feedMode {
|
||||
.letters {
|
||||
font-size: 0.8rem;
|
||||
line-height: 14px;
|
||||
min-width: 16px;
|
||||
max-width: 16px;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import { Show } from 'solid-js'
|
||||
import { createMemo, Show } from 'solid-js'
|
||||
import styles from './Userpic.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { Image } from '../../_shared/Image'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
|
@ -12,11 +12,8 @@ type Props = {
|
|||
slug?: string
|
||||
onClick?: () => void
|
||||
loading?: boolean
|
||||
isBig?: boolean
|
||||
isMedium?: boolean
|
||||
hasLink?: boolean
|
||||
isAuthorsList?: boolean
|
||||
isFeedMode?: boolean
|
||||
size?: 'XS' | 'S' | 'M' | 'L' | 'XL' // 20 | 28 | 32 | 40 | 168
|
||||
}
|
||||
|
||||
export const Userpic = (props: Props) => {
|
||||
|
@ -26,13 +23,29 @@ export const Userpic = (props: Props) => {
|
|||
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 (
|
||||
<div
|
||||
class={clsx(styles.Userpic, props.class, {
|
||||
[styles.big]: props.isBig,
|
||||
[styles.medium]: props.isMedium,
|
||||
[styles.authorsList]: props.isAuthorsList,
|
||||
[styles.feedMode]: props.isFeedMode,
|
||||
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
|
||||
['cursorPointer']: props.onClick
|
||||
})}
|
||||
onClick={props.onClick}
|
||||
|
@ -42,18 +55,8 @@ export const Userpic = (props: Props) => {
|
|||
condition={props.hasLink}
|
||||
wrapper={(children) => <a href={`/author/${props.slug}`}>{children}</a>}
|
||||
>
|
||||
<Show
|
||||
when={!props.userpic}
|
||||
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 keyed={true} when={props.userpic} fallback={<div class={styles.letters}>{letters()}</div>}>
|
||||
<Image src={props.userpic} width={avatarSize()} height={avatarSize()} alt={props.name} />
|
||||
</Show>
|
||||
</ConditionalWrapper>
|
||||
</Show>
|
||||
|
|
|
@ -22,7 +22,9 @@
|
|||
|
||||
a {
|
||||
color: rgb(255 255 255 / 64%);
|
||||
transition: color 0.3s, background-color 0.3s;
|
||||
transition:
|
||||
color 0.3s,
|
||||
background-color 0.3s;
|
||||
|
||||
&:hover {
|
||||
background: #fff;
|
||||
|
@ -66,6 +68,7 @@
|
|||
}
|
||||
|
||||
.footerCopyrightSocial {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
.icon {
|
||||
|
@ -94,6 +97,15 @@
|
|||
margin-left: 0.3em;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
a:link {
|
||||
border: none;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
img {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.socialItemvk {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { createMemo, For } from 'solid-js'
|
||||
import styles from './Footer.module.scss'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import Subscribe from './Subscribe'
|
||||
import { Subscribe } from '../_shared/Subscribe'
|
||||
|
||||
import { clsx } from 'clsx'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.aboutDiscours {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
background: #fef2f2;
|
||||
font-weight: 500;
|
||||
margin-bottom: 6.4rem;
|
||||
|
@ -8,6 +9,7 @@
|
|||
|
||||
h4 {
|
||||
@include font-size(4rem);
|
||||
|
||||
font-weight: bold;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 2rem;
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
.draggable {
|
||||
margin: 8px 0;
|
||||
padding: 8px 0;
|
||||
|
||||
&:hover {
|
||||
background: var(--placeholder-color-semi);
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
z-index: 2;
|
||||
font-weight: 500;
|
||||
transition: 0.6s ease-in-out;
|
||||
background: rgba(white, 0.3);
|
||||
background: rgb(255 255 255 / 30%);
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid var(--secondary-color);
|
||||
left: 100%;
|
||||
|
|
|
@ -44,9 +44,8 @@ import { EditorFloatingMenu } from './EditorFloatingMenu'
|
|||
import './Prosemirror.scss'
|
||||
import { Image } from '@tiptap/extension-image'
|
||||
import { Footnote } from './extensions/Footnote'
|
||||
import { handleFileUpload } from '../../utils/handleFileUpload'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { useSnackbar } from '../../context/snackbar'
|
||||
import { handleImageUpload } from '../../utils/handleImageUpload'
|
||||
|
||||
type Props = {
|
||||
shoutId: number
|
||||
|
@ -154,7 +153,7 @@ export const Editor = (props: Props) => {
|
|||
}
|
||||
|
||||
showSnackbar({ body: t('Uploading image') })
|
||||
const result = await handleFileUpload(uplFile)
|
||||
const result = await handleImageUpload(uplFile)
|
||||
|
||||
editor()
|
||||
.chain()
|
||||
|
@ -174,7 +173,7 @@ export const Editor = (props: Props) => {
|
|||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: imageProxy(result.url)
|
||||
src: result.url
|
||||
}
|
||||
}
|
||||
]
|
||||
|
|
|
@ -269,6 +269,7 @@ figure[data-type='capturedImage'] {
|
|||
flex-direction: column-reverse;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-type-no-unknown */
|
||||
footnote {
|
||||
display: inline-flex;
|
||||
position: relative;
|
||||
|
@ -276,7 +277,7 @@ footnote {
|
|||
width: 0.8rem;
|
||||
height: 1em;
|
||||
|
||||
&:before {
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 10px;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
.simplifiedEditorField {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
min-height: 100px;
|
||||
|
||||
.emptyNode:first-child::before {
|
||||
|
@ -92,7 +93,7 @@
|
|||
}
|
||||
|
||||
&.isFocused {
|
||||
//background: red;
|
||||
// background: red;
|
||||
.controls {
|
||||
opacity: 1;
|
||||
bottom: 0;
|
||||
|
@ -111,7 +112,7 @@
|
|||
|
||||
&.bordered {
|
||||
box-sizing: border-box;
|
||||
padding: 16px 12px 6px 12px;
|
||||
padding: 16px 12px 6px;
|
||||
border-radius: 2px;
|
||||
border: 2px solid var(--black-100);
|
||||
background: var(--white-500);
|
||||
|
|
|
@ -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 {
|
||||
createEditorTransaction,
|
||||
|
@ -21,7 +21,6 @@ import { Modal } from '../Nav/Modal'
|
|||
import { hideModal, showModal } from '../../stores/ui'
|
||||
import { Blockquote } from '@tiptap/extension-blockquote'
|
||||
import { UploadModalContent } from './UploadModalContent'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { clsx } from 'clsx'
|
||||
import styles from './SimplifiedEditor.module.scss'
|
||||
import { Placeholder } from '@tiptap/extension-placeholder'
|
||||
|
@ -54,6 +53,7 @@ type Props = {
|
|||
onlyBubbleControls?: boolean
|
||||
controlsAlwaysVisible?: boolean
|
||||
autoFocus?: boolean
|
||||
isCancelButtonVisible?: boolean
|
||||
}
|
||||
|
||||
export const MAX_DESCRIPTION_LIMIT = 400
|
||||
|
@ -62,6 +62,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
const { t } = useLocalize()
|
||||
const [counter, setCounter] = createSignal<number>()
|
||||
|
||||
const isCancelButtonVisible = createMemo(() => props.isCancelButtonVisible !== false)
|
||||
const wrapperEditorElRef: {
|
||||
current: HTMLElement
|
||||
} = {
|
||||
|
@ -174,7 +175,7 @@ const SimplifiedEditor = (props: Props) => {
|
|||
{
|
||||
type: 'image',
|
||||
attrs: {
|
||||
src: imageProxy(image.url)
|
||||
src: image.url
|
||||
}
|
||||
}
|
||||
]
|
||||
|
@ -328,7 +329,9 @@ const SimplifiedEditor = (props: Props) => {
|
|||
</div>
|
||||
<Show when={!props.onChange}>
|
||||
<div class={styles.buttons}>
|
||||
<Show when={isCancelButtonVisible()}>
|
||||
<Button value={t('Cancel')} variant="secondary" onClick={handleClear} />
|
||||
</Show>
|
||||
<Button
|
||||
value={props.submitButtonText ?? t('Send')}
|
||||
variant="primary"
|
||||
|
|
|
@ -6,11 +6,11 @@ import { createSignal, Show } from 'solid-js'
|
|||
import { InlineForm } from '../InlineForm'
|
||||
import { hideModal } from '../../../stores/ui'
|
||||
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload'
|
||||
import { handleFileUpload } from '../../../utils/handleFileUpload'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
import { verifyImg } from '../../../utils/verifyImg'
|
||||
import { UploadedFile } from '../../../pages/types'
|
||||
import { handleImageUpload } from '../../../utils/handleImageUpload'
|
||||
|
||||
type Props = {
|
||||
onClose: (image?: UploadedFile) => void
|
||||
|
@ -24,10 +24,10 @@ export const UploadModalContent = (props: Props) => {
|
|||
const [dragError, setDragError] = createSignal<string | undefined>()
|
||||
|
||||
const { selectFiles } = createFileUploader({ multiple: false, accept: 'image/*' })
|
||||
const runUpload = async (file) => {
|
||||
const runUpload = async (file: UploadFile) => {
|
||||
try {
|
||||
setIsUploading(true)
|
||||
const result = await handleFileUpload(file)
|
||||
const result = await handleImageUpload(file)
|
||||
props.onClose(result)
|
||||
setIsUploading(false)
|
||||
} catch (error) {
|
||||
|
@ -41,7 +41,7 @@ export const UploadModalContent = (props: Props) => {
|
|||
try {
|
||||
const data = await fetch(value)
|
||||
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 = {
|
||||
source: blob.toString(),
|
||||
name: file.name,
|
||||
|
@ -55,7 +55,7 @@ export const UploadModalContent = (props: Props) => {
|
|||
}
|
||||
|
||||
const handleUpload = async () => {
|
||||
await selectFiles(async ([uploadFile]) => {
|
||||
selectFiles(async ([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') {
|
||||
setDragActive(true)
|
||||
} 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 (
|
||||
<div class={styles.uploadModalContent}>
|
||||
<Show when={!isUploading()} fallback={<Loading />}>
|
||||
|
@ -113,7 +122,7 @@ export const UploadModalContent = (props: Props) => {
|
|||
hideModal()
|
||||
props.onClose()
|
||||
}}
|
||||
validate={async (value) => ((await verifyImg(value)) ? '' : t('Invalid image URL'))}
|
||||
validate={handleValidate}
|
||||
onSubmit={handleImageFormSubmit}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
@ -79,6 +79,7 @@ export const Footnote = Node.create({
|
|||
},
|
||||
deleteFootnote:
|
||||
() =>
|
||||
// eslint-disable-next-line unicorn/consistent-function-scoping
|
||||
({ tr, state }) => {
|
||||
const { selection } = state
|
||||
const { $from, $to } = selection
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
.shoutCardCover img {
|
||||
.shoutCardCover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
}
|
||||
|
@ -89,6 +89,7 @@
|
|||
}
|
||||
|
||||
.shoutCardCoverContainer {
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
|
@ -99,13 +100,13 @@
|
|||
overflow: hidden;
|
||||
padding-bottom: 56.2%;
|
||||
position: relative;
|
||||
transform-origin: 50% 50%;
|
||||
transition: transform 1s ease-in-out;
|
||||
|
||||
img {
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
position: absolute;
|
||||
transform-origin: 50% 50%;
|
||||
transition: transform 1s ease-in-out;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
|
@ -117,6 +118,7 @@
|
|||
|
||||
.shoutAuthor {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
font-weight: 500;
|
||||
margin-right: 1.6rem;
|
||||
|
||||
|
@ -133,9 +135,10 @@
|
|||
}
|
||||
|
||||
.shoutDate {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: #9fa1a7;
|
||||
font-weight: 500;
|
||||
@include font-size(1.2rem);
|
||||
}
|
||||
|
||||
.shoutDetails {
|
||||
|
@ -154,6 +157,7 @@
|
|||
|
||||
.shoutCardTitle {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
font-weight: 700;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 0.8rem;
|
||||
|
@ -174,6 +178,7 @@
|
|||
|
||||
.shoutCardTitlesContainerFeedMode & {
|
||||
@include font-size(3.2rem);
|
||||
|
||||
line-height: 1.1;
|
||||
}
|
||||
}
|
||||
|
@ -187,6 +192,7 @@
|
|||
|
||||
.shoutCardSubtitle {
|
||||
@include font-size(1.8rem);
|
||||
|
||||
color: #141414;
|
||||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
|
@ -398,7 +404,6 @@
|
|||
|
||||
.shoutCardWithCover {
|
||||
aspect-ratio: 16/9;
|
||||
//padding: 0 2.4rem;
|
||||
width: 100%;
|
||||
|
||||
@include media-breakpoint-down(xl) {
|
||||
|
@ -407,9 +412,21 @@
|
|||
padding-top: 30%;
|
||||
}
|
||||
|
||||
&.swiper-slide {
|
||||
.shoutCardContent {
|
||||
swiper-slide & {
|
||||
margin-bottom: 0;
|
||||
|
||||
@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%;
|
||||
}
|
||||
}
|
||||
|
@ -467,9 +484,11 @@
|
|||
&::after {
|
||||
background: rgb(0 0 0 / 60%);
|
||||
content: '';
|
||||
height: 100%;
|
||||
height: 102%;
|
||||
left: -1%;
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
top: -1%;
|
||||
width: 102%;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
@ -573,6 +592,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.shoutCardDetailsItemLabel {
|
||||
@include media-breakpoint-down(sm) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.shoutCardComments,
|
||||
.shoutCardDetailsViewed {
|
||||
align-items: center;
|
||||
|
@ -589,6 +614,7 @@
|
|||
|
||||
.shoutCardLinkContainer {
|
||||
background: var(--default-color);
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -705,6 +731,7 @@
|
|||
.shoutCardTitle,
|
||||
.shoutCardSubtitle {
|
||||
@include font-size(1.8rem);
|
||||
|
||||
display: inline;
|
||||
}
|
||||
|
|
@ -1,22 +1,22 @@
|
|||
import { createMemo, createSignal, For, Show } from 'solid-js'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import styles from './ArticleCard.module.scss'
|
||||
import type { Shout } from '../../../graphql/types.gen'
|
||||
import { capitalize } from '../../../utils/capitalize'
|
||||
import { Icon } from '../../_shared/Icon'
|
||||
import { clsx } from 'clsx'
|
||||
import { CardTopic } from './CardTopic'
|
||||
import { ShoutRatingControl } from '../Article/ShoutRatingControl'
|
||||
import { getShareUrl, SharePopup } from '../Article/SharePopup'
|
||||
import stylesHeader from '../Nav/Header/Header.module.scss'
|
||||
import { getDescription } from '../../utils/meta'
|
||||
import { FeedArticlePopup } from './FeedArticlePopup'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { CardTopic } from '../CardTopic'
|
||||
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
|
||||
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
import { FeedArticlePopup } from '../FeedArticlePopup'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { getPagePath, openPage } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../stores/router'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { useSession } from '../../context/session'
|
||||
import { capitalize } from '../../utils/capitalize'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { Popover } from '../../_shared/Popover'
|
||||
import { Image } from '../../_shared/Image'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { AuthorLink } from '../../Author/AhtorLink'
|
||||
import stylesHeader from '../../Nav/Header/Header.module.scss'
|
||||
import styles from './ArticleCard.module.scss'
|
||||
|
||||
interface ArticleCardProps {
|
||||
settings?: {
|
||||
|
@ -118,7 +118,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
<div class={styles.shoutCardCoverContainer}>
|
||||
<div class={styles.shoutCardCover}>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -158,7 +158,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
[styles.shoutCardTitlesContainerFeedMode]: props.settings?.isFeedMode
|
||||
})}
|
||||
>
|
||||
<a href={`/${props.article.slug || ''}`}>
|
||||
<a href={getPagePath(router, 'article', { slug: props.article.slug })}>
|
||||
<div class={styles.shoutCardTitle}>
|
||||
<span class={styles.shoutCardLinkWrapper}>
|
||||
<span class={styles.shoutCardLinkContainer}>{title}</span>
|
||||
|
@ -180,17 +180,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
<div class={styles.shoutAuthor}>
|
||||
<For each={props.article.authors}>
|
||||
{(author) => {
|
||||
return (
|
||||
<AuthorCard
|
||||
author={author}
|
||||
hideWriteButton={true}
|
||||
hideBio={true}
|
||||
hideFollow={true}
|
||||
truncateBio={true}
|
||||
isFeedMode={true}
|
||||
hasLink={!props.settings?.noAuthorLink}
|
||||
/>
|
||||
)
|
||||
return <AuthorLink size={'XS'} author={author} />
|
||||
}}
|
||||
</For>
|
||||
</div>
|
||||
|
@ -200,10 +190,10 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</Show>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={props.settings?.isFeedMode}>
|
||||
<Show when={props.article.description}>
|
||||
<section class={styles.shoutCardDescription} innerHTML={props.article.description} />
|
||||
</Show>
|
||||
<Show when={props.settings?.isFeedMode}>
|
||||
<Show when={!props.settings?.noimage && props.article.cover}>
|
||||
<div class={styles.shoutCardCoverContainer}>
|
||||
<Show
|
||||
|
@ -221,7 +211,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
</div>
|
||||
</Show>
|
||||
<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>
|
||||
</Show>
|
||||
|
@ -240,9 +230,16 @@ export const ArticleCard = (props: ArticleCardProps) => {
|
|||
name="comment-hover"
|
||||
class={clsx(styles.icon, styles.iconHover, styles.feedControlIcon)}
|
||||
/>
|
||||
<span class={styles.shoutCardLinkContainer}>
|
||||
{props.article.stat?.commented || t('Add comment')}
|
||||
<Show
|
||||
when={props.article.stat?.commented}
|
||||
fallback={
|
||||
<span class={clsx(styles.shoutCardLinkContainer, styles.shoutCardDetailsItemLabel)}>
|
||||
{t('Add comment')}
|
||||
</span>
|
||||
}
|
||||
>
|
||||
{props.article.stat?.commented}
|
||||
</Show>
|
||||
</a>
|
||||
</div>
|
||||
|
1
src/components/Feed/ArticleCard/index.tsx
Normal file
|
@ -0,0 +1 @@
|
|||
export { ArticleCard } from './ArticleCard'
|
|
@ -33,7 +33,7 @@ export const Beside = (props: Props) => {
|
|||
<Show when={!!props.values}>
|
||||
<div
|
||||
class={clsx(
|
||||
'col-md-8',
|
||||
'col-lg-8',
|
||||
styles[
|
||||
`besideRatingColumn${props.wrapper.charAt(0).toUpperCase() + props.wrapper.slice(1)}`
|
||||
]
|
||||
|
@ -95,7 +95,7 @@ export const Beside = (props: Props) => {
|
|||
</ul>
|
||||
</div>
|
||||
</Show>
|
||||
<div class={clsx('col-md-16', styles.shoutCardContainer)}>
|
||||
<div class={clsx('col-lg-16', styles.shoutCardContainer)}>
|
||||
<ArticleCard
|
||||
article={props.beside}
|
||||
settings={{ isBigTitle: true, isBeside: true, nodate: props.nodate }}
|
||||
|
|
|
@ -3,8 +3,9 @@
|
|||
border: 1px solid rgb(0 0 0 / 15%);
|
||||
border-radius: 1.6rem;
|
||||
padding: 1.6rem !important;
|
||||
text-align: left;
|
||||
|
||||
@include media-breakpoint-between(sm, md) {
|
||||
@include media-breakpoint-down(md) {
|
||||
left: auto !important;
|
||||
right: 0;
|
||||
transform: none !important;
|
||||
|
@ -13,6 +14,8 @@
|
|||
button {
|
||||
font-size: inherit;
|
||||
font-weight: 500;
|
||||
text-align: left;
|
||||
white-space: nowrap;
|
||||
|
||||
&:hover {
|
||||
background: #000;
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
}
|
||||
|
||||
.sidebarItemName {
|
||||
align-items: baseline;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
position: relative;
|
||||
|
||||
|
@ -26,6 +26,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.userpic {
|
||||
margin-right: 1.2rem;
|
||||
}
|
||||
|
||||
.selected {
|
||||
font-weight: 700;
|
||||
}
|
||||
|
@ -121,6 +125,7 @@
|
|||
|
||||
h4 {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
font-weight: bold;
|
||||
color: #9fa1a7;
|
||||
cursor: pointer;
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
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 { useTopicsStore } from '../../../stores/zine/topics'
|
||||
import { useArticlesStore } from '../../../stores/zine/articles'
|
||||
import { useSeenStore } from '../../../stores/zine/seen'
|
||||
import { useSession } from '../../../context/session'
|
||||
|
@ -13,18 +10,12 @@ import { Userpic } from '../../Author/Userpic'
|
|||
import { getPagePath } from '@nanostores/router'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
|
||||
type FeedSidebarProps = {
|
||||
authors: Author[]
|
||||
}
|
||||
|
||||
export const Sidebar = (props: FeedSidebarProps) => {
|
||||
export const Sidebar = () => {
|
||||
const { t } = useLocalize()
|
||||
const { seen } = useSeenStore()
|
||||
const { session } = useSession()
|
||||
const { subscriptions } = useSession()
|
||||
const { page } = useRouter()
|
||||
const { authorEntities } = useAuthorsStore({ authors: props.authors })
|
||||
const { articlesByTopic } = useArticlesStore()
|
||||
const { topicEntities } = useTopicsStore()
|
||||
const [isSubscriptionsVisible, setSubscriptionsVisible] = createSignal(true)
|
||||
|
||||
const checkTopicIsSeen = (topicSlug: string) => {
|
||||
|
@ -47,7 +38,7 @@ export const Sidebar = (props: FeedSidebarProps) => {
|
|||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
<Icon name="feed-all" class={styles.icon} />
|
||||
{t('general feed')}
|
||||
{t('All')}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -73,7 +64,7 @@ export const Sidebar = (props: FeedSidebarProps) => {
|
|||
>
|
||||
<span class={styles.sidebarItemName}>
|
||||
<Icon name="feed-collaborate" class={styles.icon} />
|
||||
{t('Accomplices')}
|
||||
{t('Participation')}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -118,7 +109,7 @@ export const Sidebar = (props: FeedSidebarProps) => {
|
|||
</li>
|
||||
</ul>
|
||||
|
||||
<Show when={session()?.news?.authors || session()?.news?.topics}>
|
||||
<Show when={subscriptions().authors.length > 0 || subscriptions().topics.length > 0}>
|
||||
<h4
|
||||
classList={{ [styles.opened]: isSubscriptionsVisible() }}
|
||||
onClick={() => {
|
||||
|
@ -129,39 +120,31 @@ export const Sidebar = (props: FeedSidebarProps) => {
|
|||
</h4>
|
||||
|
||||
<ul class={clsx(styles.subscriptions, { [styles.hidden]: !isSubscriptionsVisible() })}>
|
||||
<For each={session()?.news?.authors}>
|
||||
{(authorSlug: string) => (
|
||||
<For each={subscriptions().authors}>
|
||||
{(author) => (
|
||||
<li>
|
||||
<a
|
||||
href={`/author/${authorSlug}`}
|
||||
classList={{ [styles.unread]: checkAuthorIsSeen(authorSlug) }}
|
||||
href={`/author/${author.slug}`}
|
||||
classList={{ [styles.unread]: checkAuthorIsSeen(author.slug) }}
|
||||
>
|
||||
<div class={styles.sidebarItemName}>
|
||||
<Show when={authorEntities()[authorSlug]}>
|
||||
<Userpic
|
||||
name={authorEntities()[authorSlug].name}
|
||||
userpic={authorEntities()[authorSlug].userpic}
|
||||
/>
|
||||
</Show>
|
||||
<Show when={!authorEntities()[authorSlug]}>
|
||||
<Icon name="hash" class={styles.icon} />
|
||||
</Show>
|
||||
{authorEntities()[authorSlug]?.name}
|
||||
<Userpic name={author.name} userpic={author.userpic} size="XS" class={styles.userpic} />
|
||||
{author.name}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
<For each={session()?.news?.topics}>
|
||||
{(topicSlug: string) => (
|
||||
<For each={subscriptions().topics}>
|
||||
{(topic) => (
|
||||
<li>
|
||||
<a
|
||||
href={`/topic/${topicSlug}`}
|
||||
classList={{ [styles.unread]: checkTopicIsSeen(topicSlug) }}
|
||||
href={`/topic/${topic.slug}`}
|
||||
classList={{ [styles.unread]: checkTopicIsSeen(topic.slug) }}
|
||||
>
|
||||
<div class={styles.sidebarItemName}>
|
||||
<Icon name="hash" class={styles.icon} />
|
||||
{topicEntities()[topicSlug]?.title ?? topicSlug}
|
||||
{topic.title}
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -2,7 +2,7 @@ import { Show, createMemo } from 'solid-js'
|
|||
import './DialogCard.module.scss'
|
||||
import styles from './DialogAvatar.module.scss'
|
||||
import { clsx } from 'clsx'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
|
||||
type Props = {
|
||||
name: string
|
||||
|
@ -47,7 +47,10 @@ const DialogAvatar = (props: Props) => {
|
|||
style={{ 'background-color': `${randomBg()}` }}
|
||||
>
|
||||
<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>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -5,6 +5,7 @@ import GroupDialogAvatar from './GroupDialogAvatar'
|
|||
import { clsx } from 'clsx'
|
||||
import styles from './DialogCard.module.scss'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
|
||||
type DialogProps = {
|
||||
online?: boolean
|
||||
|
@ -40,27 +41,41 @@ const DialogCard = (props: DialogProps) => {
|
|||
})}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Switch
|
||||
fallback={
|
||||
<Show
|
||||
when={props.isChatHeader}
|
||||
fallback={
|
||||
<div class={styles.avatar}>
|
||||
<Switch fallback={<DialogAvatar name={props.members[0].slug} url={props.members[0].userpic} />}>
|
||||
<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} />
|
||||
</div>
|
||||
</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
|
||||
<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 && !props.isChatHeader}>
|
||||
<Match when={props.message}>
|
||||
<div innerHTML={props.message} />
|
||||
</Match>
|
||||
<Match when={props.isChatHeader && companions().length > 1}>{names()}</Match>
|
||||
</Switch>
|
||||
</div>
|
||||
</div>
|
||||
<Show when={!props.isChatHeader}>
|
||||
<div class={styles.activity}>
|
||||
<Show when={props.lastUpdate}>
|
||||
<div class={styles.time}>{formatTime(new Date(props.lastUpdate * 1000))}</div>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.Search {
|
||||
flex: 1;
|
||||
|
||||
.field {
|
||||
position: relative;
|
||||
background: #fff;
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
position: relative;
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
min-height: 710px;
|
||||
min-height: 600px;
|
||||
}
|
||||
|
||||
input {
|
||||
|
@ -40,9 +40,9 @@
|
|||
.authImage {
|
||||
@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;
|
||||
color: #fff;
|
||||
color: var(--default-color-invert);
|
||||
display: flex;
|
||||
padding: 3em;
|
||||
position: relative;
|
||||
|
@ -69,7 +69,7 @@
|
|||
z-index: 1;
|
||||
|
||||
a {
|
||||
color: #fff;
|
||||
color: var(--default-color-invert);
|
||||
|
||||
&:hover {
|
||||
color: rgb(255 255 255 / 70%);
|
||||
|
@ -87,20 +87,22 @@
|
|||
|
||||
.disclaimer {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
color: #9fa1a7;
|
||||
margin-bottom: 0;
|
||||
|
||||
a {
|
||||
color: #fff !important;
|
||||
color: var(--default-color-invert) !important;
|
||||
|
||||
&:hover {
|
||||
color: rgb(255, 255, 255, 0.6) !important;
|
||||
color: rgb(255 255 255 / 60%) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.authActions {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
margin-top: 1.6rem;
|
||||
text-align: center;
|
||||
|
||||
|
@ -192,8 +194,8 @@
|
|||
border-color: #d00820;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
border-color: black;
|
||||
color: var(--default-color-invert);
|
||||
border-color: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,18 +2,22 @@
|
|||
position: relative;
|
||||
|
||||
.confirmModalTitle {
|
||||
@include font-size(2rem);
|
||||
@include font-size(3.2rem);
|
||||
|
||||
font-weight: 700;
|
||||
color: var(--default-color);
|
||||
text-align: center;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
margin: 0 10%;
|
||||
}
|
||||
}
|
||||
|
||||
.confirmModalActions {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 4rem;
|
||||
gap: 2rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.confirmAction {
|
||||
flex: 1;
|
||||
|
|
|
@ -76,6 +76,8 @@
|
|||
height: 20px;
|
||||
object-fit: contain;
|
||||
object-position: left;
|
||||
position: relative;
|
||||
top: 0.1rem;
|
||||
transition: height 0.2s;
|
||||
vertical-align: middle;
|
||||
width: 100px;
|
||||
|
@ -112,6 +114,7 @@
|
|||
|
||||
.mainNavigationWrapper {
|
||||
@include font-size(1.7rem);
|
||||
|
||||
position: relative;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
|
@ -125,10 +128,11 @@
|
|||
|
||||
.mainNavigation {
|
||||
font-size: 1.4rem !important;
|
||||
//margin: 0 0 0 -0.4rem !important;
|
||||
opacity: 1;
|
||||
transition: opacity 0.3s;
|
||||
|
||||
// margin: 0 0 0 -0.4rem !important;
|
||||
|
||||
@include media-breakpoint-down(lg) {
|
||||
background: var(--background-color);
|
||||
bottom: 0;
|
||||
|
@ -155,7 +159,7 @@
|
|||
display: block;
|
||||
font-size: 3.2rem !important;
|
||||
font-weight: bold;
|
||||
margin: 0 0 5rem;
|
||||
margin: 0 0 4rem;
|
||||
}
|
||||
|
||||
li {
|
||||
|
@ -192,14 +196,15 @@
|
|||
}
|
||||
|
||||
:global(.view-switcher) {
|
||||
margin-top: 0;
|
||||
margin: 0 -0.5rem;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
li {
|
||||
margin-bottom: 0 !important;
|
||||
|
||||
&:first-letter {
|
||||
&::first-letter {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
}
|
||||
|
@ -224,9 +229,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.mainNavigationSocial a {
|
||||
.mainNavigationSocial {
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
|
||||
.mainNavigation .mainNavigationMobile & {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
|
||||
a {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
&:hover {
|
||||
.icon {
|
||||
|
@ -236,48 +249,37 @@
|
|||
|
||||
.icon {
|
||||
height: 3.8rem;
|
||||
margin-right: 0.3em;
|
||||
width: 3.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.languageSelectorMobile {
|
||||
border: 2px solid #e8e8e8;
|
||||
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 {
|
||||
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 {
|
||||
background: var(--link-hover-background) !important;
|
||||
color: var(--link-hover-color) !important;
|
||||
|
@ -293,9 +295,10 @@
|
|||
.burgerContainer {
|
||||
box-sizing: content-box;
|
||||
display: inline-flex;
|
||||
//float: right;
|
||||
padding-left: 0;
|
||||
|
||||
// float: right;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
padding-left: divide($container-padding-x, 2);
|
||||
}
|
||||
|
@ -307,7 +310,7 @@
|
|||
|
||||
.burger {
|
||||
cursor: pointer;
|
||||
height: 1.8rem;
|
||||
height: 1.6rem;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
@ -355,13 +358,13 @@
|
|||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0.8rem;
|
||||
bottom: 0.7rem;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
|
||||
&::before {
|
||||
transform: rotate(45deg);
|
||||
top: 0.8rem;
|
||||
top: 0.7rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -383,6 +386,7 @@
|
|||
|
||||
.articleHeader {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
left: $container-padding-x;
|
||||
margin: 0.2em 0;
|
||||
overflow: hidden;
|
||||
|
@ -480,6 +484,28 @@
|
|||
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 {
|
||||
|
@ -549,7 +575,7 @@
|
|||
}
|
||||
|
||||
.userControlItemVerbose {
|
||||
margin-left: 1.2em !important;
|
||||
margin-left: 0.9em !important;
|
||||
|
||||
&:first-child {
|
||||
margin-left: 0 !important;
|
||||
|
@ -618,13 +644,14 @@
|
|||
}
|
||||
|
||||
.textLabel {
|
||||
background-color: var(--link-hover-background);
|
||||
color: var(--link-hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
a:hover {
|
||||
//background-color: var(--link-hover-background) !important;
|
||||
.textLabel {
|
||||
background-color: var(--link-hover-background);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,7 +12,7 @@ import { Icon } from '../../_shared/Icon'
|
|||
import type { Topic } from '../../../graphql/types.gen'
|
||||
|
||||
import { useModalStore } from '../../../stores/ui'
|
||||
import { router, useRouter } from '../../../stores/router'
|
||||
import { router, ROUTES, useRouter } from '../../../stores/router'
|
||||
|
||||
import { getDescription } from '../../../utils/meta'
|
||||
|
||||
|
@ -23,6 +23,7 @@ import styles from './Header.module.scss'
|
|||
import { apiClient } from '../../../utils/apiClient'
|
||||
import { RANDOM_TOPICS_COUNT } from '../../Views/Home'
|
||||
import { Link } from './Link'
|
||||
import { Subscribe } from '../../_shared/Subscribe'
|
||||
|
||||
type Props = {
|
||||
title?: string
|
||||
|
@ -37,11 +38,14 @@ type HeaderSearchParams = {
|
|||
source?: string
|
||||
}
|
||||
|
||||
const handleSwitchLanguage = (event) => {
|
||||
location.href = `${location.href}${location.href.includes('?') ? '&' : '?'}lng=${event.target.value}`
|
||||
}
|
||||
|
||||
export const Header = (props: Props) => {
|
||||
const { t, lang } = useLocalize()
|
||||
|
||||
const { modal } = useModalStore()
|
||||
|
||||
const { page } = useRouter()
|
||||
const {
|
||||
actions: { requireAuthentication }
|
||||
} = useSession()
|
||||
|
@ -58,7 +62,6 @@ export const Header = (props: Props) => {
|
|||
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
|
||||
const [isZineVisible, setIsZineVisible] = createSignal(false)
|
||||
const [isFeedVisible, setIsFeedVisible] = createSignal(false)
|
||||
|
||||
const toggleFixed = () => setFixed((oldFixed) => !oldFixed)
|
||||
|
||||
const tag = (topic: Topic) =>
|
||||
|
@ -147,6 +150,15 @@ export const Header = (props: Props) => {
|
|||
setRandomTopics(topics)
|
||||
})
|
||||
|
||||
const handleToggleMenuByLink = (event: MouseEvent, route: keyof typeof ROUTES) => {
|
||||
if (!fixed()) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
if (page().route === route) {
|
||||
toggleFixed()
|
||||
}
|
||||
}
|
||||
return (
|
||||
<header
|
||||
class={styles.mainHeader}
|
||||
|
@ -195,6 +207,7 @@ export const Header = (props: Props) => {
|
|||
routeName="home"
|
||||
active={isZineVisible()}
|
||||
body={t('journal')}
|
||||
onClick={(event) => handleToggleMenuByLink(event, 'home')}
|
||||
/>
|
||||
<Link
|
||||
onMouseOver={() => toggleSubnavigation(true, setIsFeedVisible)}
|
||||
|
@ -202,6 +215,7 @@ export const Header = (props: Props) => {
|
|||
routeName="feed"
|
||||
active={isFeedVisible()}
|
||||
body={t('feed')}
|
||||
onClick={(event) => handleToggleMenuByLink(event, 'feed')}
|
||||
/>
|
||||
<Link
|
||||
onMouseOver={() => toggleSubnavigation(true, setIsTopicsVisible)}
|
||||
|
@ -209,12 +223,14 @@ export const Header = (props: Props) => {
|
|||
routeName="topics"
|
||||
active={isTopicsVisible()}
|
||||
body={t('topics')}
|
||||
onClick={(event) => handleToggleMenuByLink(event, 'topics')}
|
||||
/>
|
||||
<Link
|
||||
onMouseOver={(event) => hideSubnavigation(event, 0)}
|
||||
onMouseOut={(event) => hideSubnavigation(event, 0)}
|
||||
routeName="authors"
|
||||
body={t('authors')}
|
||||
onClick={(event) => handleToggleMenuByLink(event, 'authors')}
|
||||
/>
|
||||
<Link
|
||||
onMouseOver={() => toggleSubnavigation(true, setIsKnowledgeBaseVisible)}
|
||||
|
@ -222,20 +238,21 @@ export const Header = (props: Props) => {
|
|||
routeName="guide"
|
||||
body={t('Knowledge base')}
|
||||
active={isKnowledgeBaseVisible()}
|
||||
onClick={(event) => handleToggleMenuByLink(event, 'guide')}
|
||||
/>
|
||||
</ul>
|
||||
|
||||
<div class={styles.mainNavigationMobile}>
|
||||
<h4>{t('Join the community')}</h4>
|
||||
<h4>{t('Participating')}</h4>
|
||||
<ul class="view-switcher">
|
||||
<li>
|
||||
<a href="/create">{t('Create post')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about/manifest#participation">{t('Support us')}</a>
|
||||
<a href="/connect">{t('Suggest an idea')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/about/help">{t('How to help')}</a>
|
||||
<a href="/about/help">{t('Support the project')}</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
|
@ -243,52 +260,60 @@ export const Header = (props: Props) => {
|
|||
<ul class="view-switcher">
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<a href="https://www.instagram.com/discoursio/">
|
||||
Instagram
|
||||
<Icon name="user-link-instagram" class={styles.icon} />
|
||||
Instagram
|
||||
</a>
|
||||
</li>
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<a href="https://facebook.com/discoursio">
|
||||
Facebook
|
||||
<Icon name="user-link-facebook" class={styles.icon} />
|
||||
Facebook
|
||||
</a>
|
||||
</li>
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<a href="https://twitter.com/discours_io">
|
||||
Twitter
|
||||
<Icon name="user-link-twitter" class={styles.icon} />
|
||||
Twitter
|
||||
</a>
|
||||
</li>
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<a href="https://t.me/discoursio">
|
||||
Telegram
|
||||
<Icon name="user-link-telegram" class={styles.icon} />
|
||||
Telegram
|
||||
</a>
|
||||
</li>
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<a href="https://dzen.ru/discoursio">
|
||||
Dzen
|
||||
<Icon name="user-link-dzen" class={styles.icon} />
|
||||
Dzen
|
||||
</a>
|
||||
</li>
|
||||
<li class={styles.mainNavigationSocial}>
|
||||
<a href="https://vk.com/discoursio">
|
||||
VK
|
||||
<Icon name="user-link-vk" class={styles.icon} />
|
||||
VK
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<h4>{t('Newsletter')}</h4>
|
||||
<form action="." class={styles.mobileSubscription}>
|
||||
<div class="pretty-form__item">
|
||||
<input type="email" placeholder="Ваш email" id="subscription-email" />
|
||||
<label for="subscription-email">{t('Your email')}</label>
|
||||
<button class={styles.mobileSubscriptionSubmit}>
|
||||
<Icon name="arrow-right" />
|
||||
</button>
|
||||
<Subscribe variant={'mobileSubscription'} />
|
||||
|
||||
<h4>{t('Language')}</h4>
|
||||
<select
|
||||
class={styles.languageSelectorMobile}
|
||||
onChange={handleSwitchLanguage}
|
||||
value={lang()}
|
||||
>
|
||||
<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>
|
||||
</form>
|
||||
|
||||
<p
|
||||
class={styles.mobileDescription}
|
||||
|
@ -298,7 +323,6 @@ export const Header = (props: Props) => {
|
|||
/>
|
||||
<div class={styles.mobileCopyright}>
|
||||
{t('Discours')} © 2015–{new Date().getFullYear()}{' '}
|
||||
<a href="/">{t('Terms of use')}</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -382,31 +406,31 @@ export const Header = (props: Props) => {
|
|||
<a href="/expo">{t('Art')}</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
<a href="/podcasts">Подкасты</a>
|
||||
<a href="/podcasts">{t('Podcasts')}</a>
|
||||
</li>
|
||||
<li class="item">
|
||||
<a href="">Спецпроекты</a>
|
||||
<a href="">{t('Special Projects')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/interview">#Интервью</a>
|
||||
<a href="/topic/interview">#{t('Interview')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/reportage">#Репортажи</a>
|
||||
<a href="/topic/reportage">#{t('Reports')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/empiric">#Личный опыт</a>
|
||||
<a href="/topic/empiric">#{t('Experience')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/society">#Общество</a>
|
||||
<a href="/topic/society">#{t('Society')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/culture">#Культура</a>
|
||||
<a href="/topic/culture">#{t('Culture')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/theory">#Теории</a>
|
||||
<a href="/topic/theory">#{t('Theory')}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="/topic/poetry">#Поэзия</a>
|
||||
<a href="/topic/poetry">#{t('Poetry')}</a>
|
||||
</li>
|
||||
<li class={styles.rightItem}>
|
||||
<a href="/topics">
|
||||
|
@ -455,7 +479,7 @@ export const Header = (props: Props) => {
|
|||
<a href={getPagePath(router, 'feed')}>
|
||||
<span class={styles.subnavigationItemName}>
|
||||
<Icon name="feed-all" class={styles.icon} />
|
||||
{t('general feed')}
|
||||
{t('All')}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
@ -472,7 +496,7 @@ export const Header = (props: Props) => {
|
|||
<a href={getPagePath(router, 'feedCollaborations')}>
|
||||
<span class={styles.subnavigationItemName}>
|
||||
<Icon name="feed-collaborate" class={styles.icon} />
|
||||
{t('Accomplices')}
|
||||
{t('Participation')}
|
||||
</span>
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
@ -10,13 +10,17 @@ type Props = {
|
|||
routeName?: keyof typeof ROUTES
|
||||
body: string
|
||||
active?: boolean
|
||||
onClick?: (event: MouseEvent) => void
|
||||
}
|
||||
|
||||
export const Link = (props: Props) => {
|
||||
const { page } = useRouter()
|
||||
const isSelected = page().route === props.routeName
|
||||
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
|
||||
condition={!isSelected && Boolean(props.routeName)}
|
||||
wrapper={(children) => <a href={getPagePath(router, props.routeName)}>{children}</a>}
|
||||
|
|
|
@ -86,8 +86,9 @@ export const HeaderAuth = (props: Props) => {
|
|||
fallback={
|
||||
<Button
|
||||
value={<span class={styles.textLabel}>{buttonProps.value}</span>}
|
||||
variant={'outline'}
|
||||
variant={'light'}
|
||||
onClick={buttonProps.action}
|
||||
class={styles.editorControl}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
@ -95,9 +96,10 @@ export const HeaderAuth = (props: Props) => {
|
|||
{(ref) => (
|
||||
<Button
|
||||
ref={ref}
|
||||
variant={'outline'}
|
||||
variant={'light'}
|
||||
onClick={buttonProps.action}
|
||||
value={<Icon name={buttonProps.icon} class={styles.icon} />}
|
||||
class={styles.editorControl}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
|
@ -120,12 +122,14 @@ export const HeaderAuth = (props: Props) => {
|
|||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={!isSaveButtonVisible()}>
|
||||
<div class={styles.userControlItem}>
|
||||
<a href="#">
|
||||
<Icon name="search" class={styles.icon} />
|
||||
<Icon name="search" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
|
||||
<Show when={isNotificationsVisible()}>
|
||||
<div class={styles.userControlItem} onClick={handleBellIconClick}>
|
||||
|
@ -140,6 +144,15 @@ export const HeaderAuth = (props: Props) => {
|
|||
</div>
|
||||
</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()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemVerbose)}>
|
||||
{renderIconedButton({
|
||||
|
@ -163,8 +176,9 @@ export const HeaderAuth = (props: Props) => {
|
|||
<Button
|
||||
ref={ref}
|
||||
value={<Icon name="burger" />}
|
||||
variant={'outline'}
|
||||
variant={'light'}
|
||||
onClick={handleBurgerButtonClick}
|
||||
class={styles.settingsControl}
|
||||
/>
|
||||
)}
|
||||
</Popover>
|
||||
|
@ -182,19 +196,17 @@ export const HeaderAuth = (props: Props) => {
|
|||
</div>
|
||||
}
|
||||
>
|
||||
<Show when={!isSaveButtonVisible()}>
|
||||
<div class={clsx(styles.userControlItem, styles.userControlItemInbox)}>
|
||||
<a href="/inbox">
|
||||
{/*FIXME: replace with route*/}
|
||||
<div classList={{ entered: page().path === '/inbox' }}>
|
||||
<Icon name="inbox-white" counter={session()?.news?.unread || 0} class={styles.icon} />
|
||||
<Icon
|
||||
name="inbox-white-hover"
|
||||
counter={session()?.news?.unread || 0}
|
||||
class={clsx(styles.icon, styles.iconHover)}
|
||||
/>
|
||||
<Icon name="inbox-white" class={styles.icon} />
|
||||
<Icon name="inbox-white-hover" class={clsx(styles.icon, styles.iconHover)} />
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</Show>
|
||||
<ProfilePopup
|
||||
onVisibilityChange={(isVisible) => {
|
||||
props.setIsProfilePopupVisible(isVisible)
|
||||
|
@ -205,6 +217,7 @@ export const HeaderAuth = (props: Props) => {
|
|||
<button class={styles.button}>
|
||||
<div classList={{ entered: page().path === `/${session().user?.slug}` }}>
|
||||
<Userpic
|
||||
size={'M'}
|
||||
name={session().user.name}
|
||||
userpic={session().user.userpic}
|
||||
class={styles.userpic}
|
||||
|
|
|
@ -63,17 +63,21 @@
|
|||
width: 100%;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
max-width: 460px;
|
||||
max-width: 600px;
|
||||
}
|
||||
|
||||
@include media-breakpoint-up(md) {
|
||||
width: 50%;
|
||||
width: 65%;
|
||||
}
|
||||
|
||||
.close {
|
||||
right: 1.6rem;
|
||||
top: 1.6rem;
|
||||
}
|
||||
|
||||
.modalInner {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
)
|
||||
}
|
|
@ -8,12 +8,25 @@
|
|||
.navigation {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
@include media-breakpoint-down(md) {
|
||||
display: flex;
|
||||
|
||||
li {
|
||||
margin-right: 2.4rem;
|
||||
}
|
||||
}
|
||||
|
||||
a {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.active {
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: var(--default-color-invert);
|
||||
background: var(--background-color-invert);
|
||||
border-bottom: 2px solid;
|
||||
cursor: inherit;
|
||||
font-weight: bold;
|
||||
pointer-events: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,15 +2,28 @@
|
|||
min-height: 2px;
|
||||
background-color: var(--default-color);
|
||||
color: #fff;
|
||||
font-size: 2rem;
|
||||
font-weight: 500;
|
||||
transition: background-color 0.3s;
|
||||
|
||||
&.error {
|
||||
background-color: #d00820;
|
||||
}
|
||||
|
||||
&.success {
|
||||
.icon {
|
||||
height: 1.8em;
|
||||
margin-right: 0.5em;
|
||||
margin-top: 0.1em;
|
||||
width: 1.8em;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
transition: height 0.3s, color 0.3s;
|
||||
transition:
|
||||
height 0.3s,
|
||||
color 0.3s;
|
||||
height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -3,6 +3,7 @@ import { useSnackbar } from '../../context/snackbar'
|
|||
import styles from './Snackbar.module.scss'
|
||||
import { Transition } from 'solid-transition-group'
|
||||
import { clsx } from 'clsx'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
|
||||
export const Snackbar = () => {
|
||||
const { snackbarMessage } = useSnackbar()
|
||||
|
@ -10,7 +11,8 @@ export const Snackbar = () => {
|
|||
return (
|
||||
<div
|
||||
class={clsx(styles.snackbar, {
|
||||
[styles.error]: snackbarMessage()?.type === 'error'
|
||||
[styles.error]: snackbarMessage()?.type === 'error',
|
||||
[styles.success]: snackbarMessage()?.type === 'success'
|
||||
})}
|
||||
>
|
||||
<Transition
|
||||
|
@ -19,7 +21,12 @@ export const Snackbar = () => {
|
|||
onExit={(el, done) => setTimeout(() => done(), 300)}
|
||||
>
|
||||
<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>
|
||||
</Transition>
|
||||
</div>
|
||||
|
|
|
@ -17,31 +17,31 @@ export const Topics = () => {
|
|||
</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/podcasts">Подкасты</a>
|
||||
<a href="/podcasts">{t('Podcasts')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="">Спецпроекты</a>
|
||||
<a href="">{t('Special Projects')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/interview">#Интервью</a>
|
||||
<a href="/topic/interview">#{t('Interview')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/reportage">#Репортажи</a>
|
||||
<a href="/topic/reportage">#{t('Reports')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/empiric">#Личный опыт</a>
|
||||
<a href="/topic/empiric">#{t('Experience')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/society">#Общество</a>
|
||||
<a href="/topic/society">#{t('Society')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/culture">#Культура</a>
|
||||
<a href="/topic/culture">#{t('Culture')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/theory">#Теории</a>
|
||||
<a href="/topic/theory">#{t('Theory')}</a>
|
||||
</li>
|
||||
<li class={styles.item}>
|
||||
<a href="/topic/poetry">#Поэзия</a>
|
||||
<a href="/topic/poetry">#{t('Poetry')}</a>
|
||||
</li>
|
||||
<li class={clsx(styles.item, styles.right)}>
|
||||
<a href={getPagePath(router, 'topics')}>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
font-size: 15px;
|
||||
line-height: 24px;
|
||||
white-space: pre-line;
|
||||
padding: 4rem 0;
|
||||
}
|
||||
|
||||
.title {
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
.NotificationView {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 72px;
|
||||
align-items: flex-start;
|
||||
min-height: 72px;
|
||||
margin-left: -16px;
|
||||
border-radius: 16px;
|
||||
padding: 16px;
|
||||
background-color: var(--yellow-50);
|
||||
// TODO: check markup
|
||||
font-size: 15px;
|
||||
// font-weight: 700;
|
||||
line-height: 20px;
|
||||
cursor: pointer;
|
||||
transition: background-color 100ms;
|
||||
max-width: 700px;
|
||||
|
||||
&.seen {
|
||||
background-color: transparent;
|
||||
|
@ -30,7 +30,8 @@
|
|||
}
|
||||
|
||||
.userpic {
|
||||
margin-right: 15px;
|
||||
min-width: 40px;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
.timeContainer {
|
||||
|
|
|
@ -18,7 +18,8 @@ type Props = {
|
|||
}
|
||||
|
||||
// NOTE: not a graphql generated type
|
||||
export enum NotificationType {
|
||||
|
||||
export enum NewNotificationType {
|
||||
NewComment = 'NEW_COMMENT',
|
||||
NewReply = 'NEW_REPLY',
|
||||
NewFollower = 'NEW_FOLLOWER',
|
||||
|
@ -27,6 +28,13 @@ export enum NotificationType {
|
|||
NewDislike = 'NEW_DISLIKE'
|
||||
}
|
||||
|
||||
export type NotificationUser = {
|
||||
id: number
|
||||
name: string
|
||||
slug: string
|
||||
userpic: string
|
||||
}
|
||||
|
||||
const TEMPLATES = {
|
||||
// FIXME: set proper templates
|
||||
'follower:join': 'new follower',
|
||||
|
@ -54,7 +62,7 @@ export const NotificationView = (props: Props) => {
|
|||
actions: { markNotificationAsRead }
|
||||
} = useNotifications()
|
||||
const [data, setData] = createSignal<SSEMessage>(null)
|
||||
const [kind, setKind] = createSignal<NotificationType>()
|
||||
const [kind, setKind] = createSignal<NewNotificationType>()
|
||||
const { changeSearchParam } = useRouter<ArticlePageSearchParams>()
|
||||
const { t, formatDate, formatTime } = useLocalize()
|
||||
|
||||
|
@ -68,7 +76,7 @@ export const NotificationView = (props: Props) => {
|
|||
if (!data()) {
|
||||
return null
|
||||
}
|
||||
let caption: string, author: Author, ntype: NotificationType
|
||||
let caption: string, author: Author, ntype: NewNotificationType
|
||||
|
||||
// TODO: count occurencies from in-browser notifications-db
|
||||
|
||||
|
@ -76,17 +84,19 @@ export const NotificationView = (props: Props) => {
|
|||
case 'follower': {
|
||||
caption = ''
|
||||
author = data().payload
|
||||
ntype = NotificationType.NewFollower
|
||||
ntype = NewNotificationType.NewFollower
|
||||
break
|
||||
}
|
||||
case 'shout': {
|
||||
case 'shout':
|
||||
{
|
||||
caption = data().payload.title
|
||||
author = data().payload.authors[-1]
|
||||
ntype = NotificationType.NewShout
|
||||
ntype = NewNotificationType.NewShout
|
||||
break
|
||||
}
|
||||
break
|
||||
case 'reaction': {
|
||||
ntype = data().payload.replyTo ? NotificationType.NewReply : NotificationType.NewComment
|
||||
ntype = data().payload.replyTo ? NewNotificationType.NewReply : NewNotificationType.NewComment
|
||||
console.log(data().payload.kind)
|
||||
// TODO: handle all needed reaction kinds
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ $transition-duration: 200ms;
|
|||
bottom: 0;
|
||||
width: 0;
|
||||
z-index: 10000;
|
||||
background-color: rgb(0 0 0 / 0%);
|
||||
overflow: hidden;
|
||||
transition:
|
||||
background-color $transition-duration,
|
||||
|
@ -18,12 +17,48 @@ $transition-duration: 200ms;
|
|||
|
||||
.panel {
|
||||
position: relative;
|
||||
background-color: #fff;
|
||||
width: 700px;
|
||||
padding: 48px 96px 96px 48px;
|
||||
background-color: var(--background-color);
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
transform: translateX(100%);
|
||||
transition: transform $transition-duration;
|
||||
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 {
|
||||
|
@ -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 {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 20px;
|
||||
top: 1.2rem;
|
||||
right: 1rem;
|
||||
padding: 1rem;
|
||||
cursor: pointer;
|
||||
z-index: 1;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--background-color-invert);
|
||||
|
||||
.closeIcon {
|
||||
filter: invert(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notificationView + .notificationView {
|
||||
|
@ -66,8 +100,7 @@ $transition-duration: 200ms;
|
|||
}
|
||||
|
||||
.periodTitle {
|
||||
// TODO: check markup
|
||||
margin: 32px 0 16px 0;
|
||||
margin: 32px 0 16px;
|
||||
color: var(--black-400);
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
|
|
|
@ -4,10 +4,13 @@ import { useEscKeyDownHandler } from '../../utils/useEscKeyDownHandler'
|
|||
import { useOutsideClickHandler } from '../../utils/useOutsideClickHandler'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { createEffect, createMemo, For, Show } from 'solid-js'
|
||||
import { useNotifications } from '../../context/notifications'
|
||||
import { createEffect, createMemo, createSignal, For, on, onCleanup, onMount, Show } from 'solid-js'
|
||||
import { PAGE_SIZE, useNotifications } from '../../context/notifications'
|
||||
import { NotificationView } from './NotificationView'
|
||||
import { EmptyMessage } from './EmptyMessage'
|
||||
import { Button } from '../_shared/Button'
|
||||
import throttle from 'just-throttle'
|
||||
import { useSession } from '../../context/session'
|
||||
|
||||
type Props = {
|
||||
isOpen: boolean
|
||||
|
@ -39,8 +42,17 @@ const isEarlier = (date: Date) => {
|
|||
}
|
||||
|
||||
export const NotificationsPanel = (props: Props) => {
|
||||
const [isLoading, setIsLoading] = createSignal(false)
|
||||
|
||||
const { isAuthenticated } = useSession()
|
||||
const { t } = useLocalize()
|
||||
const { sortedNotifications } = useNotifications()
|
||||
const {
|
||||
sortedNotifications,
|
||||
unreadNotificationsCount
|
||||
// loadedNotificationsCount,
|
||||
// totalNotificationsCount,
|
||||
// actions: { loadNotifications, markAllNotificationsAsRead }
|
||||
} = useNotifications()
|
||||
const handleHide = () => {
|
||||
props.onClose()
|
||||
}
|
||||
|
@ -79,17 +91,68 @@ export const NotificationsPanel = (props: Props) => {
|
|||
handleHide()
|
||||
}
|
||||
|
||||
const todayNotifications = createMemo(() => {
|
||||
return sortedNotifications().filter((notification) => isToday(new Date(notification.createdAt)))
|
||||
// const todayNotifications = createMemo(() => {
|
||||
// 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(() => {
|
||||
return sortedNotifications().filter((notification) => isYesterday(new Date(notification.createdAt)))
|
||||
})
|
||||
|
||||
const earlierNotifications = createMemo(() => {
|
||||
return sortedNotifications().filter((notification) => isEarlier(new Date(notification.createdAt)))
|
||||
})
|
||||
createEffect(
|
||||
on(
|
||||
() => isAuthenticated(),
|
||||
async () => {
|
||||
if (isAuthenticated()) {
|
||||
setIsLoading(true)
|
||||
await loadNextPage()
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -99,50 +162,75 @@ export const NotificationsPanel = (props: Props) => {
|
|||
>
|
||||
<div ref={(el) => (panelRef.current = el)} class={styles.panel}>
|
||||
<div class={styles.closeButton} onClick={handleHide}>
|
||||
{/*TODO: check markup (hover)*/}
|
||||
<Icon name="close" />
|
||||
<Icon class={styles.closeIcon} name="close" />
|
||||
</div>
|
||||
<div class={styles.title}>{t('Notifications')}</div>
|
||||
<Show when={sortedNotifications().length > 0} fallback={<EmptyMessage />}>
|
||||
<Show when={todayNotifications().length > 0}>
|
||||
<div class={styles.periodTitle}>{t('today')}</div>
|
||||
<For each={todayNotifications()}>
|
||||
{(notification) => (
|
||||
<NotificationView
|
||||
notification={notification}
|
||||
class={styles.notificationView}
|
||||
onClick={handleNotificationViewClick}
|
||||
dateTimeFormat={'ago'}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
<div class={clsx('wide-container', styles.content)} ref={(el) => (scrollContainerRef.current = el)}>
|
||||
<Show
|
||||
when={sortedNotifications().length > 0}
|
||||
fallback={
|
||||
<Show when={!isLoading()}>
|
||||
<EmptyMessage />
|
||||
</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>
|
||||
}
|
||||
>
|
||||
<div class="row position-relative">
|
||||
<div class="col-xs-24">
|
||||
{/*<Show when={todayNotifications().length > 0}>*/}
|
||||
{/* <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 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 when={isLoading()}>
|
||||
<div class={styles.loading}>{t('Loading')}</div>
|
||||
</Show>
|
||||
</div>
|
||||
|
||||
<Show when={unreadNotificationsCount() > 0}>
|
||||
<div class={styles.actions}>
|
||||
{/*<Button*/}
|
||||
{/* onClick={() => markAllNotificationsAsRead()}*/}
|
||||
{/* variant="secondary"*/}
|
||||
{/* value={t('Mark as read')}*/}
|
||||
{/*/>*/}
|
||||
</div>
|
||||
</Show>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -45,7 +45,6 @@
|
|||
.TableOfContentsFixedWrapperLefted {
|
||||
margin-top: -2em;
|
||||
right: auto;
|
||||
left: 70px;
|
||||
|
||||
.TableOfContentsPrimaryButton {
|
||||
left: auto;
|
||||
|
|
|
@ -56,6 +56,7 @@
|
|||
|
||||
.topicTitle {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
font-weight: bold;
|
||||
margin-bottom: 1.2rem;
|
||||
margin-top: 0.5rem !important;
|
||||
|
@ -84,6 +85,7 @@
|
|||
|
||||
.topicDescription {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
font-weight: 500;
|
||||
color: #696969;
|
||||
line-height: 1.3;
|
||||
|
@ -115,10 +117,17 @@
|
|||
}
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
border-radius: 0.8rem !important;
|
||||
margin-right: 0 !important;
|
||||
width: 9em;
|
||||
}
|
||||
|
||||
.isSubscribing {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
/*
|
||||
.isSubscribed {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
|
@ -145,6 +154,7 @@
|
|||
display: none;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
.cardMode {
|
||||
margin-bottom: 0;
|
||||
|
|
|
@ -13,6 +13,8 @@ import { CheckButton } from '../_shared/CheckButton'
|
|||
import { capitalize } from '../../utils/capitalize'
|
||||
|
||||
import styles from './Card.module.scss'
|
||||
import { Button } from '../_shared/Button'
|
||||
import stylesButton from '../_shared/Button/Button.module.scss'
|
||||
|
||||
interface TopicProps {
|
||||
topic: Topic
|
||||
|
@ -34,19 +36,15 @@ interface TopicProps {
|
|||
export const TopicCard = (props: TopicProps) => {
|
||||
const { t } = useLocalize()
|
||||
const {
|
||||
session,
|
||||
subscriptions,
|
||||
isSessionLoaded,
|
||||
actions: { loadSession, requireAuthentication }
|
||||
actions: { loadSubscriptions, requireAuthentication }
|
||||
} = useSession()
|
||||
|
||||
const [isSubscribing, setIsSubscribing] = createSignal(false)
|
||||
|
||||
const subscribed = createMemo(() => {
|
||||
if (!session()?.user?.slug || !session()?.news?.topics) {
|
||||
return false
|
||||
}
|
||||
|
||||
return session()?.news.topics.includes(props.topic.slug)
|
||||
return subscriptions().topics.some((topic) => topic.slug === props.topic.slug)
|
||||
})
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
|
@ -56,7 +54,7 @@ export const TopicCard = (props: TopicProps) => {
|
|||
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
|
||||
|
||||
await loadSession()
|
||||
await loadSubscriptions()
|
||||
setIsSubscribing(false)
|
||||
}
|
||||
|
||||
|
@ -66,6 +64,24 @@ export const TopicCard = (props: TopicProps) => {
|
|||
}, '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 (
|
||||
<div class={styles.topicContainer}>
|
||||
<div
|
||||
|
@ -127,27 +143,18 @@ export const TopicCard = (props: TopicProps) => {
|
|||
<CheckButton text={t('Follow')} checked={subscribed()} onClick={handleSubscribe} />
|
||||
}
|
||||
>
|
||||
<button
|
||||
<Button
|
||||
variant="bordered"
|
||||
size="M"
|
||||
value={subscribeValue()}
|
||||
onClick={handleSubscribe}
|
||||
class="button--light button--subscribe-topic"
|
||||
classList={{
|
||||
isSubscribeButton={true}
|
||||
class={clsx(styles.actionButton, {
|
||||
[styles.isSubscribing]: isSubscribing(),
|
||||
[styles.isSubscribed]: subscribed()
|
||||
}}
|
||||
[stylesButton.subscribed]: subscribed()
|
||||
})}
|
||||
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>
|
||||
</ShowOnlyOnClient>
|
||||
|
|
|
@ -15,16 +15,21 @@
|
|||
.topicActions {
|
||||
margin-top: 2.8rem;
|
||||
|
||||
button,
|
||||
a {
|
||||
background: #000;
|
||||
.write {
|
||||
display: inline-flex;
|
||||
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;
|
||||
font-weight: 500;
|
||||
border-radius: 2px;
|
||||
color: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 100%;
|
||||
margin: 0 1.2rem 1em;
|
||||
padding: 0.8rem 1.6rem;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { follow, unfollow } from '../../stores/zine/common'
|
|||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { Button } from '../_shared/Button'
|
||||
|
||||
type Props = {
|
||||
topic: Topic
|
||||
|
@ -14,19 +15,22 @@ type Props = {
|
|||
|
||||
export const FullTopic = (props: Props) => {
|
||||
const {
|
||||
session,
|
||||
actions: { requireAuthentication }
|
||||
subscriptions,
|
||||
actions: { requireAuthentication, loadSubscriptions }
|
||||
} = useSession()
|
||||
const { t } = useLocalize()
|
||||
const subscribed = createMemo(() => session()?.news?.topics?.includes(props.topic?.slug))
|
||||
|
||||
const handleSubscribe = (isFollowed: boolean) => {
|
||||
requireAuthentication(() => {
|
||||
if (isFollowed) {
|
||||
unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||
} else {
|
||||
follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||
}
|
||||
const { t } = useLocalize()
|
||||
|
||||
const subscribed = createMemo(() =>
|
||||
subscriptions().topics.some((topic) => 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')
|
||||
}
|
||||
|
||||
|
@ -36,16 +40,18 @@ export const FullTopic = (props: Props) => {
|
|||
<p>{props.topic.body}</p>
|
||||
<div class={clsx(styles.topicActions)}>
|
||||
<Show when={!subscribed()}>
|
||||
<button onClick={() => handleSubscribe(false)} class="button">
|
||||
{t('Follow the topic')}
|
||||
</button>
|
||||
<Button variant="primary" onClick={() => handleSubscribe(true)} value={t('Follow the topic')} />
|
||||
</Show>
|
||||
<Show when={subscribed()}>
|
||||
<button onClick={() => handleSubscribe(true)} class="button">
|
||||
{t('Unfollow the topic')}
|
||||
</button>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={() => handleSubscribe(false)}
|
||||
value={t('Unfollow the topic')}
|
||||
/>
|
||||
</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>
|
||||
<Show when={props.topic.pic}>
|
||||
<img src={props.topic.pic} alt={props.topic.title} />
|
||||
|
|
|
@ -36,11 +36,12 @@
|
|||
}
|
||||
|
||||
.info {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
border: none;
|
||||
display: flex;
|
||||
flex: 0 calc(100% - 5.2rem);
|
||||
flex-direction: column;
|
||||
@include font-size(1.4rem);
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@include media-breakpoint-up(sm) {
|
||||
|
|
|
@ -2,12 +2,12 @@ import { clsx } from 'clsx'
|
|||
import styles from './TopicBadge.module.scss'
|
||||
import { FollowingEntity, Topic } from '../../../graphql/types.gen'
|
||||
import { createMemo, createSignal, Show } from 'solid-js'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { follow, unfollow } from '../../../stores/zine/common'
|
||||
import { CheckButton } from '../../_shared/CheckButton'
|
||||
import { getImageUrl } from '../../../utils/getImageUrl'
|
||||
|
||||
type Props = {
|
||||
topic: Topic
|
||||
|
@ -19,17 +19,13 @@ export const TopicBadge = (props: Props) => {
|
|||
const { t } = useLocalize()
|
||||
const {
|
||||
isAuthenticated,
|
||||
session,
|
||||
actions: { loadSession }
|
||||
subscriptions,
|
||||
actions: { loadSubscriptions }
|
||||
} = useSession()
|
||||
|
||||
const subscribed = createMemo(() => {
|
||||
if (!session()?.user?.slug || !session()?.news?.topics) {
|
||||
return false
|
||||
}
|
||||
|
||||
return session()?.news.topics.includes(props.topic.slug)
|
||||
})
|
||||
const subscribed = createMemo(() =>
|
||||
subscriptions().topics.some((topic) => topic.slug === props.topic.slug)
|
||||
)
|
||||
|
||||
const subscribe = async (really = true) => {
|
||||
setIsSubscribing(true)
|
||||
|
@ -38,7 +34,7 @@ export const TopicBadge = (props: Props) => {
|
|||
? follow({ what: FollowingEntity.Topic, slug: props.topic.slug })
|
||||
: unfollow({ what: FollowingEntity.Topic, slug: props.topic.slug }))
|
||||
|
||||
await loadSession()
|
||||
await loadSubscriptions()
|
||||
setIsSubscribing(false)
|
||||
}
|
||||
|
||||
|
@ -47,7 +43,11 @@ export const TopicBadge = (props: Props) => {
|
|||
<a
|
||||
href={`/topic/${props.topic.slug}`}
|
||||
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}>
|
||||
<span class={styles.title}>{props.topic.title}</span>
|
||||
|
@ -80,7 +80,7 @@ export const TopicBadge = (props: Props) => {
|
|||
<Button
|
||||
variant="primary"
|
||||
size="S"
|
||||
value={isSubscribing() ? t('...subscribing') : t('Subscribe')}
|
||||
value={isSubscribing() ? t('subscribing...') : t('Subscribe')}
|
||||
onClick={() => subscribe(true)}
|
||||
class={styles.subscribeButton}
|
||||
/>
|
||||
|
|
|
@ -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 { setAuthorsSort, useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { useRouter } from '../../stores/router'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { clsx } from 'clsx'
|
||||
import { useSession } from '../../context/session'
|
||||
import { SearchField } from '../_shared/SearchField'
|
||||
import { scrollHandler } from '../../utils/scroll'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { dummyFilter } from '../../utils/dummyFilter'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
|
||||
import styles from './AllAuthors.module.scss'
|
||||
|
||||
|
@ -35,9 +33,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
|
||||
const [searchQuery, setSearchQuery] = createSignal('')
|
||||
|
||||
const { session } = useSession()
|
||||
|
||||
onMount(() => {
|
||||
createEffect(() => {
|
||||
if (!searchParams().by) {
|
||||
changeSearchParam({
|
||||
by: 'shouts'
|
||||
|
@ -52,7 +48,14 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
const byLetter = createMemo<{ [letter: string]: Author[] }>(() => {
|
||||
return sortedAuthors().reduce(
|
||||
(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 = '@'
|
||||
|
||||
|
@ -72,8 +75,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
return keys
|
||||
})
|
||||
|
||||
const subscribed = (s) => Boolean(session()?.news?.authors && session()?.news?.authors?.includes(s || ''))
|
||||
|
||||
const filteredAuthors = createMemo(() => {
|
||||
return dummyFilter(sortedAuthors(), searchQuery(), lang())
|
||||
})
|
||||
|
@ -84,7 +85,6 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
<div class="col-lg-20 col-xl-18">
|
||||
<h1>{t('Authors')}</h1>
|
||||
<p>{t('Subscribe who you like to tune your personal feed')}</p>
|
||||
|
||||
<ul class={clsx(styles.viewSwitcher, 'view-switcher')}>
|
||||
<li
|
||||
classList={{
|
||||
|
@ -172,15 +172,7 @@ export const AllAuthorsView = (props: AllAuthorsViewProps) => {
|
|||
{(author) => (
|
||||
<div class="row">
|
||||
<div class="col-lg-20 col-xl-18">
|
||||
<AuthorCard
|
||||
author={author as Author}
|
||||
hasLink={true}
|
||||
subscribed={subscribed(author.slug)}
|
||||
noSocialButtons={true}
|
||||
isAuthorsList={true}
|
||||
truncateBio={true}
|
||||
isTextButton={true}
|
||||
/>
|
||||
<AuthorBadge author={author as Author} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
|
|
@ -87,6 +87,7 @@
|
|||
|
||||
.alphabet {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
color: rgba(0 0 0 / 20%);
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
@ -106,6 +107,7 @@
|
|||
|
||||
.articlesCounter {
|
||||
@include font-size(1.2rem);
|
||||
|
||||
margin-left: 0.5em;
|
||||
vertical-align: super;
|
||||
}
|
||||
|
|
|
@ -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 { setTopicsSort, useTopicsStore } from '../../stores/zine/topics'
|
||||
|
@ -34,9 +34,9 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
|||
sortBy: searchParams().by || 'shouts'
|
||||
})
|
||||
|
||||
const { session } = useSession()
|
||||
const { subscriptions } = useSession()
|
||||
|
||||
onMount(() => {
|
||||
createEffect(() => {
|
||||
if (!searchParams().by) {
|
||||
changeSearchParam({
|
||||
by: 'shouts'
|
||||
|
@ -68,7 +68,7 @@ export const AllTopicsView = (props: AllTopicsViewProps) => {
|
|||
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 [searchQuery, setSearchQuery] = createSignal('')
|
||||
|
|
|
@ -30,12 +30,14 @@
|
|||
|
||||
.ratingContainer {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
}
|
||||
|
||||
.ratingControl {
|
||||
@include font-size(1.5rem);
|
||||
|
||||
display: inline-flex;
|
||||
margin-left: 1em;
|
||||
vertical-align: middle;
|
||||
|
@ -81,8 +83,8 @@
|
|||
max-height 0.5s,
|
||||
margin-bottom 0s 0.3s;
|
||||
|
||||
&:after {
|
||||
background-image: linear-gradient(to top, #fff, rgb(255 255 255 / 0.8), rgb(255 255 255 / 0));
|
||||
&::after {
|
||||
background-image: linear-gradient(to top, #fff, rgb(255 255 255 / 80%), rgb(255 255 255 / 0%));
|
||||
bottom: 0;
|
||||
content: '';
|
||||
display: block;
|
||||
|
@ -97,16 +99,17 @@
|
|||
max-height: 200em;
|
||||
margin-bottom: -2em;
|
||||
|
||||
&:after {
|
||||
&::after {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.longBioExpandedControl {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
border-radius: 1.2rem;
|
||||
display: block;
|
||||
height: auto;
|
||||
@include font-size(1.6rem);
|
||||
padding-bottom: 1.2rem;
|
||||
padding-top: 1.2rem;
|
||||
position: relative;
|
||||
|
|
|
@ -17,7 +17,6 @@ import { Comment } from '../../Article/Comment'
|
|||
import { useLocalize } from '../../../context/localize'
|
||||
import { AuthorRatingControl } from '../../Author/AuthorRatingControl'
|
||||
import { getPagePath } from '@nanostores/router'
|
||||
import { useSession } from '../../../context/session'
|
||||
import { Loading } from '../../_shared/Loading'
|
||||
|
||||
type Props = {
|
||||
|
@ -35,7 +34,6 @@ export const AuthorView = (props: Props) => {
|
|||
const { authorEntities } = useAuthorsStore({ authors: [props.author] })
|
||||
|
||||
const { page: getPage } = useRouter()
|
||||
const { user } = useSession()
|
||||
const author = createMemo(() => authorEntities()[props.authorSlug])
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
const [isBioExpanded, setIsBioExpanded] = createSignal(false)
|
||||
|
@ -128,13 +126,7 @@ export const AuthorView = (props: Props) => {
|
|||
<Show when={author()} fallback={<Loading />}>
|
||||
<>
|
||||
<div class={styles.authorHeader}>
|
||||
<AuthorCard
|
||||
author={author()}
|
||||
isAuthorPage={true}
|
||||
followers={followers()}
|
||||
following={following()}
|
||||
isCurrentUser={author().slug === user()?.slug}
|
||||
/>
|
||||
<AuthorCard author={author()} followers={followers()} following={following()} />
|
||||
</div>
|
||||
<div class={clsx(styles.groupControls, 'row')}>
|
||||
<div class="col-md-16">
|
||||
|
|
|
@ -50,10 +50,11 @@
|
|||
.additionalInput {
|
||||
@include font-size(1.4rem);
|
||||
|
||||
-moz-appearance: textfield;
|
||||
appearance: textfield;
|
||||
|
||||
&::-webkit-outer-spin-button,
|
||||
&::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
|
@ -213,12 +214,29 @@
|
|||
background-position: center;
|
||||
background-size: cover;
|
||||
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 {
|
||||
position: fixed;
|
||||
left: 40px;
|
||||
top: 106px;
|
||||
width: 240px;
|
||||
padding-top: 100px;
|
||||
|
|
|
@ -8,12 +8,11 @@ import { ShoutForm, useEditorContext } from '../../context/editor'
|
|||
import { Editor, Panel } from '../Editor'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import styles from './Edit.module.scss'
|
||||
import { imageProxy } from '../../utils/imageProxy'
|
||||
import { GrowingTextarea } from '../_shared/GrowingTextarea'
|
||||
import { VideoUploader } from '../Editor/VideoUploader'
|
||||
import { AudioUploader } from '../Editor/AudioUploader'
|
||||
import { slugify } from '../../utils/slugify'
|
||||
import { SolidSwiper } from '../_shared/SolidSwiper'
|
||||
import { ImageSwiper } from '../_shared/SolidSwiper'
|
||||
import { DropArea } from '../_shared/DropArea'
|
||||
import { LayoutType, MediaItem } from '../../pages/types'
|
||||
import { clone } from '../../utils/clone'
|
||||
|
@ -24,6 +23,8 @@ import { createStore } from 'solid-js/store'
|
|||
import SimplifiedEditor from '../Editor/SimplifiedEditor'
|
||||
import { isDesktop } from '../../utils/media-query'
|
||||
import { TableOfContents } from '../TableOfContents'
|
||||
import { getImageUrl } from '../../utils/getImageUrl'
|
||||
import { Popover } from '../_shared/Popover'
|
||||
|
||||
type Props = {
|
||||
shout: Shout
|
||||
|
@ -362,14 +363,28 @@ export const EditView = (props: Props) => {
|
|||
>
|
||||
<div
|
||||
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>
|
||||
</div>
|
||||
|
||||
<Show when={props.shout.layout === 'image'}>
|
||||
<SolidSwiper
|
||||
<ImageSwiper
|
||||
editorMode={true}
|
||||
images={mediaItems()}
|
||||
onImageChange={handleMediaChange}
|
||||
|
|
|
@ -1,12 +1,13 @@
|
|||
.Expo {
|
||||
display: block;
|
||||
background: #fef2f2;
|
||||
padding: 0 0 4rem 0;
|
||||
padding: 0 0 4rem;
|
||||
min-height: 100vh;
|
||||
|
||||
.navigation {
|
||||
padding: 0 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.showMore {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
|
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
.feedNavigation {
|
||||
@include font-size(1.6rem);
|
||||
|
||||
font-weight: 500;
|
||||
|
||||
h4 {
|
||||
|
@ -51,6 +52,7 @@
|
|||
|
||||
h4 {
|
||||
@include font-size(2.2rem);
|
||||
|
||||
font-weight: bold;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
@ -131,7 +133,7 @@
|
|||
}
|
||||
|
||||
&:hover {
|
||||
&:before {
|
||||
&::before {
|
||||
background-image: url(/icons/knowledge-base-bullet-hover.svg);
|
||||
}
|
||||
}
|
||||
|
@ -156,6 +158,7 @@
|
|||
.commentDetails {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
|
@ -169,6 +172,10 @@
|
|||
a {
|
||||
border: none;
|
||||
padding-bottom: 0.2em;
|
||||
|
||||
&:hover * {
|
||||
background: var(--background-color-invert);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
import { createEffect, createSignal, For, on, onMount, Show } from 'solid-js'
|
||||
import { Icon } from '../_shared/Icon'
|
||||
import { ArticleCard } from '../Feed/ArticleCard'
|
||||
import { AuthorCard } from '../Author/AuthorCard'
|
||||
import { Sidebar } from '../Feed/Sidebar'
|
||||
import { useArticlesStore, resetSortedArticles } from '../../stores/zine/articles'
|
||||
import { useAuthorsStore } from '../../stores/zine/authors'
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||
import { clsx } from 'clsx'
|
||||
|
@ -18,6 +16,8 @@ import stylesTopic from '../Feed/CardTopic.module.scss'
|
|||
import stylesBeside from '../../components/Feed/Beside.module.scss'
|
||||
import { CommentDate } from '../Article/CommentDate'
|
||||
import { Loading } from '../_shared/Loading'
|
||||
import { AuthorBadge } from '../Author/AuthorBadge'
|
||||
import { AuthorLink } from '../Author/AhtorLink'
|
||||
|
||||
export const FEED_PAGE_SIZE = 20
|
||||
|
||||
|
@ -48,8 +48,6 @@ export const FeedView = (props: Props) => {
|
|||
|
||||
// state
|
||||
const { sortedArticles } = useArticlesStore()
|
||||
|
||||
const { sortedAuthors } = useAuthorsStore()
|
||||
const { topTopics } = useTopicsStore()
|
||||
const { topAuthors } = useTopAuthorsStore()
|
||||
const [isLoadMoreButtonVisible, setIsLoadMoreButtonVisible] = createSignal(false)
|
||||
|
@ -113,7 +111,7 @@ export const FeedView = (props: Props) => {
|
|||
<div class="wide-container feed">
|
||||
<div class="row">
|
||||
<div class={clsx('col-md-5 col-xl-4', styles.feedNavigation)}>
|
||||
<Sidebar authors={sortedAuthors()} />
|
||||
<Sidebar />
|
||||
</div>
|
||||
|
||||
<div class="col-md-12 offset-xl-1">
|
||||
|
@ -163,13 +161,7 @@ export const FeedView = (props: Props) => {
|
|||
<For each={topAuthors().slice(0, 5)}>
|
||||
{(author) => (
|
||||
<li>
|
||||
<AuthorCard
|
||||
author={author}
|
||||
hideWriteButton={true}
|
||||
hasLink={true}
|
||||
truncateBio={true}
|
||||
isTextButton={true}
|
||||
/>
|
||||
<AuthorBadge author={author} />
|
||||
</li>
|
||||
)}
|
||||
</For>
|
||||
|
@ -207,13 +199,7 @@ export const FeedView = (props: Props) => {
|
|||
/>
|
||||
</div>
|
||||
<div class={styles.commentDetails}>
|
||||
<AuthorCard
|
||||
author={comment.createdBy as Author}
|
||||
isFeedMode={true}
|
||||
hideWriteButton={true}
|
||||
hideFollow={true}
|
||||
hasLink={true}
|
||||
/>
|
||||
<AuthorLink author={comment.createdBy as Author} size={'XS'} />
|
||||
<CommentDate comment={comment} isShort={true} isLastInRow={true} />
|
||||
</div>
|
||||
<div class={clsx('text-truncate', styles.commentArticleTitle)}>
|
||||
|
|
|
@ -8,10 +8,8 @@ import { Row1 } from '../Feed/Row1'
|
|||
import Hero from '../Discours/Hero'
|
||||
import { Beside } from '../Feed/Beside'
|
||||
import RowShort from '../Feed/RowShort'
|
||||
import { Slider } from '../_shared/Slider'
|
||||
import Group from '../Feed/Group'
|
||||
import type { Shout } from '../../graphql/types.gen'
|
||||
|
||||
import { useTopicsStore } from '../../stores/zine/topics'
|
||||
import {
|
||||
loadShouts,
|
||||
|
@ -22,8 +20,8 @@ import {
|
|||
import { useTopAuthorsStore } from '../../stores/zine/topAuthors'
|
||||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||
import { splitToPages } from '../../utils/splitToPages'
|
||||
import { ArticleCard } from '../Feed/ArticleCard'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
||||
|
||||
type Props = {
|
||||
shouts: Shout[]
|
||||
|
@ -129,21 +127,9 @@ export const HomeView = (props: Props) => {
|
|||
nodate={true}
|
||||
/>
|
||||
|
||||
<Slider title={t('Top month articles')}>
|
||||
<For each={topMonthArticles()}>
|
||||
{(a: Shout) => (
|
||||
<ArticleCard
|
||||
article={a}
|
||||
settings={{
|
||||
additionalClass: 'swiper-slide',
|
||||
isFloorImportant: true,
|
||||
isWithCover: true,
|
||||
nodate: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Slider>
|
||||
<Show when={topMonthArticles()}>
|
||||
<ArticleCardSwiper title={t('Top month articles')} slides={topMonthArticles()} />
|
||||
</Show>
|
||||
|
||||
<Row2 articles={sortedArticles().slice(10, 12)} nodate={true} />
|
||||
|
||||
|
@ -159,21 +145,9 @@ export const HomeView = (props: Props) => {
|
|||
|
||||
{randomLayout()}
|
||||
|
||||
<Slider title={t('Favorite')}>
|
||||
<For each={topArticles()}>
|
||||
{(a: Shout) => (
|
||||
<ArticleCard
|
||||
article={a}
|
||||
settings={{
|
||||
additionalClass: 'swiper-slide',
|
||||
isFloorImportant: true,
|
||||
isWithCover: true,
|
||||
nodate: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Slider>
|
||||
<Show when={topArticles()}>
|
||||
<ArticleCardSwiper title={t('Favorite')} slides={topArticles()} />
|
||||
</Show>
|
||||
|
||||
<Beside
|
||||
beside={sortedArticles()[20]}
|
||||
|
|
|
@ -252,20 +252,22 @@ export const InboxView = () => {
|
|||
|
||||
<div class={styles.messageForm}>
|
||||
<Show when={messageToReply()}>
|
||||
<QuotedMessage
|
||||
variant="reply"
|
||||
author={
|
||||
currentDialog().members.find((member) => member.id === Number(messageToReply().author))
|
||||
.name
|
||||
}
|
||||
body={messageToReply().body}
|
||||
cancel={() => setMessageToReply(null)}
|
||||
/>
|
||||
<p>FIXME: messageToReply</p>
|
||||
{/*<QuotedMessage*/}
|
||||
{/* variant="reply"*/}
|
||||
{/* author={*/}
|
||||
{/* currentDialog().members.find((member) => member.id === Number(messageToReply().author))*/}
|
||||
{/* .name*/}
|
||||
{/* }*/}
|
||||
{/* body={messageToReply().body}*/}
|
||||
{/* cancel={() => setMessageToReply(null)}*/}
|
||||
{/*/>*/}
|
||||
</Show>
|
||||
<div class={styles.wrapper}>
|
||||
<SimplifiedEditor
|
||||
smallHeight={true}
|
||||
imageEnabled={true}
|
||||
isCancelButtonVisible={false}
|
||||
placeholder={t('Write message')}
|
||||
setClear={isClear()}
|
||||
onSubmit={(message) => handleSubmit(message)}
|
||||
|
|
|
@ -119,7 +119,10 @@
|
|||
font-weight: 400;
|
||||
line-height: 1.3;
|
||||
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 {
|
||||
|
@ -154,6 +157,7 @@
|
|||
padding: 1rem 0;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.cancel {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
|
|
@ -4,7 +4,6 @@ import { createSignal, onMount, Show } from 'solid-js'
|
|||
import { TopicSelect, UploadModalContent } from '../../Editor'
|
||||
import { Button } from '../../_shared/Button'
|
||||
import { hideModal, showModal } from '../../../stores/ui'
|
||||
import { imageProxy } from '../../../utils/imageProxy'
|
||||
import { ShoutForm, useEditorContext } from '../../../context/editor'
|
||||
import { useLocalize } from '../../../context/localize'
|
||||
import { Modal } from '../../Nav/Modal'
|
||||
|
@ -20,6 +19,7 @@ import { GrowingTextarea } from '../../_shared/GrowingTextarea'
|
|||
import { createStore } from 'solid-js/store'
|
||||
import { UploadedFile } from '../../../pages/types'
|
||||
import SimplifiedEditor, { MAX_DESCRIPTION_LIMIT } from '../../Editor/SimplifiedEditor'
|
||||
import { Image } from '../../_shared/Image'
|
||||
|
||||
type Props = {
|
||||
shoutId: number
|
||||
|
@ -141,11 +141,7 @@ export const PublishSettings = (props: Props) => {
|
|||
>
|
||||
<Show when={settingsForm.coverImageUrl ?? initialData.coverImageUrl}>
|
||||
<div class={styles.shoutCardCover}>
|
||||
<img
|
||||
src={imageProxy(settingsForm.coverImageUrl)}
|
||||
alt={initialData.title}
|
||||
loading="lazy"
|
||||
/>
|
||||
<Image src={settingsForm.coverImageUrl} alt={initialData.title} width={1600} />
|
||||
</div>
|
||||
</Show>
|
||||
<div class={styles.text}>
|
||||
|
|
|
@ -13,10 +13,9 @@ import { useAuthorsStore } from '../../stores/zine/authors'
|
|||
import { restoreScrollPosition, saveScrollPosition } from '../../utils/scroll'
|
||||
import { splitToPages } from '../../utils/splitToPages'
|
||||
import { clsx } from 'clsx'
|
||||
import { Slider } from '../_shared/Slider'
|
||||
import { Row1 } from '../Feed/Row1'
|
||||
import { ArticleCard } from '../Feed/ArticleCard'
|
||||
import { useLocalize } from '../../context/localize'
|
||||
import { ArticleCardSwiper } from '../_shared/SolidSwiper/ArticleCardSwiper'
|
||||
|
||||
type TopicsPageSearchParams = {
|
||||
by: 'comments' | '' | 'recent' | 'viewed' | 'rating' | 'commented'
|
||||
|
@ -136,21 +135,7 @@ export const TopicView = (props: TopicProps) => {
|
|||
wrapper={'author'}
|
||||
/>
|
||||
|
||||
<Slider title={title()}>
|
||||
<For each={sortedArticles().slice(5, 11)}>
|
||||
{(a: Shout) => (
|
||||
<ArticleCard
|
||||
article={a}
|
||||
settings={{
|
||||
additionalClass: 'swiper-slide',
|
||||
isFloorImportant: true,
|
||||
isWithCover: true,
|
||||
nodate: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Slider>
|
||||
<ArticleCardSwiper title={title()} slides={sortedArticles().slice(5, 11)} />
|
||||
|
||||
<Beside
|
||||
beside={sortedArticles()[12]}
|
||||
|
@ -163,22 +148,7 @@ export const TopicView = (props: TopicProps) => {
|
|||
<Row1 article={sortedArticles()[15]} />
|
||||
|
||||
<Show when={sortedArticles().length > 15}>
|
||||
<Slider slidesPerView={3}>
|
||||
<For each={sortedArticles().slice(16, 22)}>
|
||||
{(a: Shout) => (
|
||||
<ArticleCard
|
||||
article={a}
|
||||
settings={{
|
||||
additionalClass: 'swiper-slide',
|
||||
isFloorImportant: true,
|
||||
isWithCover: false,
|
||||
nodate: true
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</For>
|
||||
</Slider>
|
||||
|
||||
<ArticleCardSwiper slides={sortedArticles().slice(16, 22)} />
|
||||
<Row3 articles={sortedArticles().slice(23, 26)} />
|
||||
<Row2 articles={sortedArticles().slice(26, 28)} />
|
||||
</Show>
|
||||
|
|
|
@ -8,8 +8,8 @@
|
|||
white-space: nowrap;
|
||||
|
||||
&.primary {
|
||||
background: #000;
|
||||
color: #fff;
|
||||
background: var(--background-color-invert);
|
||||
color: var(--default-color-invert);
|
||||
|
||||
&:hover {
|
||||
color: #ccc;
|
||||
|
@ -131,4 +131,65 @@
|
|||
font-size: 15px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ type Props = {
|
|||
onClick?: (event?: MouseEvent) => void
|
||||
class?: string
|
||||
ref?: HTMLButtonElement | ((el: HTMLButtonElement) => void)
|
||||
isSubscribeButton?: boolean
|
||||
}
|
||||
|
||||
export const Button = (props: Props) => {
|
||||
|
@ -33,7 +34,8 @@ export const Button = (props: Props) => {
|
|||
styles[props.size ?? 'M'],
|
||||
styles[props.variant ?? 'primary'],
|
||||
{
|
||||
[styles.loading]: props.loading
|
||||
[styles.loading]: props.loading,
|
||||
[styles.subscribeButton]: props.isSubscribeButton
|
||||
},
|
||||
props.class
|
||||
)}
|
||||
|
|
|
@ -21,9 +21,11 @@
|
|||
&:hover {
|
||||
background: var(--background-color-invert);
|
||||
color: var(--default-color-invert);
|
||||
|
||||
.check {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.close {
|
||||
display: block;
|
||||
}
|
||||
|
|