1
0
Fork 0
mirror of https://github.com/muerwre/vault-frontend.git synced 2025-04-25 04:46:40 +07:00

Compare commits

..

30 commits

Author SHA1 Message Date
dependabot[bot]
e8ed0c0466
Merge 88c414f45d into 2a0adb26e0 2025-04-15 05:37:11 +00:00
Fedor Katurov
2a0adb26e0 add sansevieria theme
All checks were successful
Build & Publish / Build & Publish (push) Successful in 2m7s
2025-04-05 19:57:42 +07:00
Fedor Katurov
29c8bcd145 fix images in comment atachments was having wrong height
All checks were successful
Build & Publish / Build & Publish (push) Successful in 2m7s
2025-04-04 22:48:46 +07:00
Fedor Katurov
4d55906ae8 fix trailing slashes
All checks were successful
Build & Publish / Build & Publish (push) Successful in 3m7s
2025-04-02 20:42:40 +07:00
Fedor Katurov
a676e98174 add standalone build
All checks were successful
Build & Publish / Build & Publish (push) Successful in 3m35s
2025-03-24 18:22:25 +07:00
Fedor Katurov
f083b488ba add forgejo workflow
Some checks failed
Build & Publish / Build & Publish (push) Has been cancelled
2025-03-24 15:57:40 +07:00
Fedor Katurov
1281a3c595 fix images pattern 2025-03-24 15:42:43 +07:00
Fedor Katurov
06cf7050a9 play video in embed instead of new window on desktops 2025-02-26 16:48:54 +07:00
Fedor Katurov
5056047546 fix header apperance for guests 2025-02-17 11:55:15 +07:00
Fedor Katurov
521f5ce436 fix search results 2025-02-17 11:53:06 +07:00
Fedor Katurov
606700f5d2 fix photoswipe 2025-02-13 15:37:29 +07:00
Fedor Katurov
5e71294e71 change photoswipe icons 2025-02-12 19:59:06 +07:00
Fedor Katurov
bf1382af0b bump photoswipe 2025-02-12 18:43:27 +07:00
Fedor Katurov
4eb605a398 bump swiperjs, fix types 2025-02-12 18:01:21 +07:00
Fedor Katurov
16689ae3a6 fix input radius 2025-02-10 15:00:09 +07:00
Fedor Katurov
6f2715a9ae put Vault last on page title 2025-02-07 07:25:59 +07:00
Fedor Katurov
9e79cba7bf fix search appearance 2025-02-07 06:07:37 +07:00
Fedor Katurov
24c66ccfdb improve cell text appearance 2025-02-07 02:42:45 +07:00
Fedor Katurov
b257e9b5d9 scroll to cell on flow view change 2025-02-07 01:39:58 +07:00
Fedor Katurov
69c61acc41 make better flow cell text 2025-02-07 01:11:12 +07:00
Fedor Katurov
7924c2bdd9 fix flow hero height 2025-02-06 22:53:11 +07:00
Fedor Katurov
f0606a894a fix header stickyness 2025-01-27 15:34:58 +07:00
Fedor Katurov
1d0ecc54a9 made first good profile layout 2025-01-27 15:05:56 +07:00
Fedor Katurov
42f8f96e34 only use new-comment tag if there's really new comment 2025-01-26 19:01:42 +07:00
Fedor Katurov
fd8907dd3a use container queries in flow 2025-01-26 18:58:49 +07:00
Fedor Katurov
71306d4c14 only add new-comment hash to nodes with comments 2025-01-24 18:22:13 +07:00
Fedor Katurov
032a246963 fix typecheck error 2025-01-24 17:59:05 +07:00
Fedor Katurov
ba0604ab9d add eslint-plugin-prettier 2025-01-24 17:51:59 +07:00
Fedor Katurov
0e4d2bf44d scroll to new comments from recent and notifications 2025-01-24 17:46:24 +07:00
Fedor Katurov
5ef19f49c5 change user profile page layout 2025-01-22 14:36:26 +07:00
133 changed files with 2232 additions and 845 deletions

View file

@ -2,6 +2,8 @@
node_modules node_modules
out out
dist dist
.husky
.next
.idea .idea
.history .history
.vscode .vscode

View file

@ -11,10 +11,10 @@ steps:
image: plugins/docker image: plugins/docker
when: when:
branch: branch:
- master - never
environment: environment:
NEXT_PUBLIC_API_HOST: https://pig.vault48.org/ NEXT_PUBLIC_API_HOST: https://vault48.org/api/
NEXT_PUBLIC_REMOTE_CURRENT: https://pig.vault48.org/static/ NEXT_PUBLIC_REMOTE_CURRENT: https://vault48.org/static/
NEXT_PUBLIC_PUBLIC_HOST: https://vault48.org/ NEXT_PUBLIC_PUBLIC_HOST: https://vault48.org/
NEXT_PUBLIC_BOT_USERNAME: vault48bot NEXT_PUBLIC_BOT_USERNAME: vault48bot
settings: settings:

View file

@ -2,6 +2,6 @@
# NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/ # NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
# NEXT_PUBLIC_API_HOST=http://localhost:7777/ # NEXT_PUBLIC_API_HOST=http://localhost:7777/
# NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:7777/static/ # NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:7777/static/
NEXT_PUBLIC_API_HOST=https://pig.vault48.org/ NEXT_PUBLIC_API_HOST=https://vault48.org/api/
NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/ NEXT_PUBLIC_REMOTE_CURRENT=https://vault48.org/static/
NEXT_PUBLIC_BOT_USERNAME=vault48testbot NEXT_PUBLIC_BOT_USERNAME=vault48testbot

View file

@ -1,6 +1,7 @@
module.exports = { module.exports = {
extends: ['plugin:react/recommended', 'plugin:@next/next/recommended'], extends: ['plugin:react/recommended', 'plugin:@next/next/recommended'],
rules: { rules: {
'prettier/prettier': 'error',
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks 'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies 'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
'react/prop-types': 0, 'react/prop-types': 0,
@ -9,13 +10,21 @@ module.exports = {
'@next/next/no-img-element': 0, '@next/next/no-img-element': 0,
'unused-imports/no-unused-imports': 'warn', 'unused-imports/no-unused-imports': 'warn',
// 'no-unused-vars': 'warn', // 'no-unused-vars': 'warn',
'quotes': [2, 'single', { 'avoidEscape': true }], quotes: [2, 'single', { avoidEscape: true }],
'import/order': [ 'import/order': [
'error', 'error',
{ {
alphabetize: { order: 'asc' }, alphabetize: { order: 'asc' },
'newlines-between': 'always', 'newlines-between': 'always',
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'], groups: [
'builtin',
'external',
'internal',
'parent',
'sibling',
'index',
'unknown',
],
pathGroups: [ pathGroups: [
{ {
pattern: 'react', pattern: 'react',
@ -34,18 +43,17 @@ module.exports = {
paths: [ paths: [
{ {
name: 'ramda', name: 'ramda',
message: message: "import from '~/utils/ramda' instead",
'import from \'~/utils/ramda\' instead',
}, },
], ],
}, },
] ],
}, },
parserOptions: { parserOptions: {
ecmaVersion: 7, ecmaVersion: 7,
sourceType: 'module', sourceType: 'module',
}, },
plugins: ['import', 'react-hooks', 'unused-imports'], plugins: ['import', 'react-hooks', 'unused-imports', 'prettier'],
parser: '@typescript-eslint/parser', parser: '@typescript-eslint/parser',
settings: { settings: {
react: { react: {

View file

@ -0,0 +1,46 @@
name: Build & Publish
on:
push:
branches: [master]
jobs:
push_to_registry:
name: Build & Publish
runs-on: ubuntu-22.04
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Registry Login
uses: docker/login-action@v3
with:
registry: git.vault48.org
username: ${{ secrets.username }}
password: ${{ secrets.password }}
- name: Extract docker metadata
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: git.vault48.org/${{ env.GITHUB_REPOSITORY }}
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/nextjs-standalone/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
NEXT_PUBLIC_API_HOST=https://vault48.org/api/
NEXT_PUBLIC_REMOTE_CURRENT=https://vault48.org/static/
NEXT_PUBLIC_PUBLIC_HOST=https://vault48.org/
NEXT_PUBLIC_BOT_USERNAME=vault48bot

View file

@ -0,0 +1,51 @@
# As written here:
# https://dev.to/leduc1901/reduce-docker-image-size-for-your-nextjs-app-5911
# Base ───────────────────────────────────────────────────────────────────────
FROM node:14-alpine as base
WORKDIR /opt/app
ENV PATH /opt/app/node_modules/.bin:$PATH
# Build ──────────────────────────────────────────────────────────────────────
FROM base as builder
ARG NEXT_PUBLIC_API_HOST
ARG NEXT_PUBLIC_REMOTE_CURRENT
ARG NEXT_PUBLIC_PUBLIC_HOST
ARG NEXT_PUBLIC_BOT_USERNAME
ENV NEXT_PUBLIC_API_HOST $NEXT_PUBLIC_API_HOST
ENV NEXT_PUBLIC_REMOTE_CURRENT $NEXT_PUBLIC_REMOTE_CURRENT
ENV NEXT_PUBLIC_PUBLIC_HOST $NEXT_PUBLIC_PUBLIC_HOST
ENV NEXT_PUBLIC_BOT_USERNAME $NEXT_PUBLIC_BOT_USERNAME
# ENV NEXT_PUBLIC_API_HOST https://vault48.org/api/
# ENV NEXT_PUBLIC_REMOTE_CURRENT https://vault48.org/static/
# ENV NEXT_PUBLIC_PUBLIC_HOST https://vault48.org/
# ENV NEXT_PUBLIC_BOT_USERNAME vault48bot
COPY package.json .
COPY yarn.lock .
RUN true \
&& yarn install --frozen-lockfile\
&& true
COPY . /opt/app
# pkg packs nodejs with given script, so we don't need it in next section
RUN yarn next build
FROM node:14-alpine as runner
WORKDIR /opt/app
COPY --from=builder /opt/app/public ./public
COPY --from=builder /opt/app/.next/standalone .
COPY --from=builder /opt/app/.next/static ./.next/static
EXPOSE 3000
ENTRYPOINT ["node", "server.js"]

View file

@ -2,21 +2,26 @@
const withBundleAnalyzer = require('@next/bundle-analyzer')({ const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true', enabled: process.env.ANALYZE === 'true',
}); });
const withTM = require('next-transpile-modules')(['ramda', '@v9v/ts-react-telegram-login']); const withTM = require('next-transpile-modules')([
'ramda',
'@v9v/ts-react-telegram-login',
]);
module.exports = withBundleAnalyzer( module.exports = withBundleAnalyzer(
withTM({ withTM({
output: 'standalone',
/** rewrite old-style node paths */ /** rewrite old-style node paths */
async rewrites() { async rewrites() {
return [ return [
{ {
source: '/post:id', // everything except 'post' is for backwards compatibility here
source: '/(post|photo|blog|song|video|cell):id',
destination: '/node/:id', destination: '/node/:id',
}, },
{ {
source: '/~:username', source: '/~:username',
destination: '/profile/:username', destination: '/profile/:username',
} },
]; ];
}, },
@ -26,8 +31,8 @@ module.exports = withBundleAnalyzer(
remotePatterns: [ remotePatterns: [
{ {
protocol: 'https', protocol: 'https',
hostname: '*.vault48.org', hostname: 'vault48.org',
pathname: '/**', pathname: '/static/**',
}, },
{ {
protocol: 'https', protocol: 'https',
@ -41,5 +46,5 @@ module.exports = withBundleAnalyzer(
}, },
], ],
}, },
}) }),
); );

View file

@ -26,7 +26,7 @@
"mobx-persist-store": "^1.0.4", "mobx-persist-store": "^1.0.4",
"mobx-react-lite": "^3.2.3", "mobx-react-lite": "^3.2.3",
"next": "^12.3.0", "next": "^12.3.0",
"photoswipe": "^4.1.3", "photoswipe": "^5.4.4",
"raleway-cyrillic": "^4.0.2", "raleway-cyrillic": "^4.0.2",
"ramda": "^0.26.1", "ramda": "^0.26.1",
"react": "^17.0.2", "react": "^17.0.2",
@ -36,12 +36,13 @@
"react-lazyload": "^3.2.0", "react-lazyload": "^3.2.0",
"react-masonry-css": "^1.0.16", "react-masonry-css": "^1.0.16",
"react-popper": "^2.2.3", "react-popper": "^2.2.3",
"react-resize-detector": "^12.0.2",
"react-router": "^5.1.2", "react-router": "^5.1.2",
"react-router-dom": "^5.1.2", "react-router-dom": "^5.1.2",
"react-sticky-box": "^1.0.2", "react-sticky-box": "^1.0.2",
"sass": "^1.49.0", "sass": "^1.49.0",
"sharp": "^0.32.6", "sharp": "^0.32.6",
"swiper": "^11.0.3", "swiper": "^11.2.2",
"swr": "^1.0.1", "swr": "^1.0.1",
"throttle-debounce": "^2.1.0", "throttle-debounce": "^2.1.0",
"typescript": "^4.0.5", "typescript": "^4.0.5",
@ -92,13 +93,14 @@
"@typescript-eslint/parser": "^5.10.1", "@typescript-eslint/parser": "^5.10.1",
"eslint": "^7.32.0", "eslint": "^7.32.0",
"eslint-plugin-import": "^2.25.4", "eslint-plugin-import": "^2.25.4",
"eslint-plugin-prettier": "^5.2.3",
"eslint-plugin-react": "^7.28.0", "eslint-plugin-react": "^7.28.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-unused-imports": "^3.0.0", "eslint-plugin-unused-imports": "^3.0.0",
"husky": "^7.0.4", "husky": "^7.0.4",
"lint-staged": "^12.1.6", "lint-staged": "^12.1.6",
"next-transpile-modules": "^9.0.0", "next-transpile-modules": "^9.0.0",
"prettier": "^2.7.1" "prettier": "^3.0.0"
}, },
"lint-staged": { "lint-staged": {
"./**/*.{js,jsx,ts,tsx}": [ "./**/*.{js,jsx,ts,tsx}": [

View file

@ -0,0 +1,752 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="1920"
height="1080"
viewBox="0 0 508 285.75"
version="1.1"
id="svg1"
inkscape:version="1.4 (e7c3feb100, 2024-10-09)"
sodipodi:docname="sansivieria.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview1"
pagecolor="#101315"
bordercolor="#2a2a2a"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#101315"
inkscape:document-units="mm"
inkscape:zoom="0.5"
inkscape:cx="977"
inkscape:cy="444"
inkscape:window-width="1920"
inkscape:window-height="1011"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs1">
<linearGradient
id="linearGradient16"
inkscape:collect="always">
<stop
style="stop-color:#222d2f;stop-opacity:1;"
offset="0"
id="stop17" />
<stop
style="stop-color:#222d2f;stop-opacity:0;"
offset="1"
id="stop18" />
</linearGradient>
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient10"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571"
gradientTransform="matrix(16.722314,0.28277544,-0.23964041,14.171465,-209.55779,-1089.6093)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient12"
cx="125.84482"
cy="74.220642"
fx="125.84482"
fy="74.220642"
r="37.123039"
gradientTransform="matrix(1.4233343,-0.04031753,0.06568704,2.3189569,-58.149762,-94.310274)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient14"
cx="49.86562"
cy="41.432327"
fx="49.86562"
fy="41.432327"
r="11.167304"
gradientTransform="matrix(3.311949,0.13402602,-0.12963621,3.2034712,-109.91564,-96.433414)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient16"
cx="106.77225"
cy="129.32372"
fx="106.77225"
fy="129.32372"
r="14.686029"
gradientTransform="matrix(1,0,0,2.4664414,0,-190.75799)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient18"
cx="29.229187"
cy="220.45612"
fx="29.229187"
fy="220.45612"
r="17.13831"
gradientTransform="matrix(3.4889397,0.0218328,-0.03388246,5.4145064,-60.182826,-1005.8674)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient19"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(16.535647,2.5076138,-2.1250983,14.013272,-43.219263,-1048.7787)"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient21"
cx="67.744797"
cy="80.206696"
fx="67.744797"
fy="80.206696"
r="10.474073"
gradientTransform="matrix(1,0,0,6.1194546,0,-409.67547)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient22"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(16.647228,-1.6079609,1.36268,14.107833,15.35168,-1041.1789)"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient23"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(29.974473,5.5523576,-6.7662765,17.665097,379.82488,-1380.4222)"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient24"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,6.1194546,394.75833,-326.5963)"
cx="67.744797"
cy="80.206696"
fx="67.744797"
fy="80.206696"
r="10.474073" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient25"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,2.4664414,252.4125,-155.83299)"
cx="106.77225"
cy="129.32372"
fx="106.77225"
fy="129.32372"
r="14.686029" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient26"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(14.749655,7.8843782,-6.6816823,12.499719,804.1388,-1075.9347)"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient27"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(3.311949,0.13402602,-0.12963621,3.2034712,320.29686,104.12075)"
cx="49.86562"
cy="41.432327"
fx="49.86562"
fy="41.432327"
r="11.167304" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient28"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.4233343,-0.04031753,0.06568704,2.3189569,206.43357,95.660559)"
cx="125.84482"
cy="74.220642"
fx="125.84482"
fy="74.220642"
r="37.123039" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient29"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,2.4664414,335.49167,-275.95382)"
cx="106.77225"
cy="129.32372"
fx="106.77225"
fy="129.32372"
r="14.686029" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient30"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.98984555,0.14214704,-0.86986237,6.0573149,403.60754,-406.5492)"
cx="67.744797"
cy="80.206696"
fx="67.744797"
fy="80.206696"
r="10.474073" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient31"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.3259837,-0.51891557,0.84543937,2.1603491,-23.826148,-41.823374)"
cx="125.84482"
cy="74.220642"
fx="125.84482"
fy="74.220642"
r="37.123039" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient32"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(0.85314014,0.52168181,-1.2866976,2.1042201,266.4049,-230.63907)"
cx="106.77225"
cy="129.32372"
fx="106.77225"
fy="129.32372"
r="14.686029" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient33"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,6.1194546,201.6125,-463.65047)"
cx="67.744797"
cy="80.206696"
fx="67.744797"
fy="80.206696"
r="10.474073" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient34"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,6.1194546,363.5375,-277.3838)"
cx="67.744797"
cy="80.206696"
fx="67.744797"
fy="80.206696"
r="10.474073" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient35"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(3.311949,0.13402602,-0.12963621,3.2034712,67.88436,-113.89592)"
cx="49.86562"
cy="41.432327"
fx="49.86562"
fy="41.432327"
r="11.167304" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient36"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1,0,0,6.1194546,93.6625,-251.9838)"
cx="67.744797"
cy="80.206696"
fx="67.744797"
fy="80.206696"
r="10.474073" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient37"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(14.527707,8.2862231,-7.0222289,12.311626,456.5098,-892.3642)"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient38"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(16.679888,1.223556,-1.0369127,14.135511,161.35064,-1106.3396)"
cx="14.584812"
cy="82.411865"
fx="14.584812"
fy="82.411865"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient39"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.8382036,0.44885537,-1.9792444,2.5359589,232.64281,-265.6639)"
cx="106.77225"
cy="129.32372"
fx="106.77225"
fy="129.32372"
r="14.686029" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient40"
cx="72.206665"
cy="181.65135"
fx="72.206665"
fy="181.65135"
r="35.266216"
gradientTransform="matrix(1.4063187,-1.2240507,0.2942198,0.33803077,-84.911033,213.79688)"
gradientUnits="userSpaceOnUse" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient41"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(2.3563545,2.5741948,-0.48133443,0.72808163,56.169463,-297.28826)"
cx="72.206665"
cy="181.65135"
fx="72.206665"
fy="181.65135"
r="35.266216" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient42"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(40.365604,0.28277544,-0.5784624,14.171465,-345.36595,-960.51812)"
cx="14.630581"
cy="83.018234"
fx="14.630581"
fy="83.018234"
r="6.6161571" />
<radialGradient
inkscape:collect="always"
xlink:href="#linearGradient16"
id="radialGradient43"
gradientUnits="userSpaceOnUse"
gradientTransform="matrix(1.1265465,-3.3029953,0.86977819,0.07260568,39.478992,494.56215)"
cx="72.206665"
cy="181.65135"
fx="72.206665"
fy="181.65135"
r="35.266216" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
style="opacity:0.189">
<path
style="opacity:1;fill:url(#radialGradient10);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 7.9686552,-11.362181 C 24.186436,4.9096141 20.925377,139.36464 10.35345,176.18591 30.87243,106.69149 15.979892,-6.6575459 15.979892,-6.6575459 Z"
id="path1"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient12);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 77.496457,-10.771584 C 98.675378,-4.1423852 147.30523,120.84028 151.74254,161.47094 143.99181,89.426312 86.700266,-9.505318 86.700266,-9.505318 Z"
id="path3"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient21);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke"
d="M 50.909713,144.11888 C 75.767669,75.5024 71.594125,15.927649 71.594125,15.927649 72.487753,19.967458 71.20699,94.882544 50.909713,144.11888 Z"
id="path4"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient16);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke"
d="M 112.47803,166.30448 C 109.99767,110.56088 83.105972,93.860658 83.105972,93.860658 84.614446,93.705287 111.63798,121.99159 112.47803,166.30448 Z"
id="path5"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient14);fill-opacity:1;stroke-width:0.236032;paint-order:markers fill stroke"
d="M 33.085655,76.535494 C 62.698491,36.591224 54.328663,4.9286614 54.328663,4.9286614 55.446727,5.6159474 57.511667,44.610258 33.085655,76.535494 Z"
id="path6"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient18);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 20.245001,320.5176 C 6.7844252,301.90038 31.12444,169.62661 47.34881,134.92299 16.168746,200.33176 13.07219,314.613 13.07219,314.613 Z"
id="path8"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient19);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 28.749908,48.834683 C 42.655832,67.121646 21.514857,199.94426 6.1326556,235.02929 35.725236,168.88716 36.06312,54.564469 36.06312,54.564469 Z"
id="path18"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient22);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 353.28303,5.5957663 C 371.23504,19.931483 383.18269,153.89432 376.83769,191.67411 389.37536,120.30668 361.77441,9.3653543 361.77441,9.3653543 Z"
id="path21"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient23);fill-opacity:1;stroke-width:0.409226;paint-order:markers fill stroke"
d="M 289.38487,36.930288 C 311.30374,62.340577 245.32976,229.63528 209.84002,272.44105 277.84039,191.82778 301.70235,45.309878 301.70235,45.309878 Z"
id="path22"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient24);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke"
d="M 445.66805,227.19805 C 470.526,158.58157 466.35246,99.006816 466.35246,99.006816 c 0.89363,4.039804 -0.38714,78.954894 -20.68441,128.191234 z"
id="path23"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient25);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke"
d="m 364.89053,201.22948 c -2.48036,-55.7436 -29.37206,-72.44382 -29.37206,-72.44382 1.50848,-0.15537 28.53201,28.13093 29.37206,72.44382 z"
id="path24"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient26);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 505.52554,-17.272921 C 512.52821,4.6074413 448.25566,122.75069 422.04247,150.68704 472.01951,98.219928 510.50617,-9.4302867 510.50617,-9.4302867 Z"
id="path25"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient27);fill-opacity:1;stroke-width:0.236032;paint-order:markers fill stroke"
d="m 463.29815,277.08966 c 29.61284,-39.94427 21.24301,-71.60683 21.24301,-71.60683 1.11807,0.68728 3.18301,39.68159 -21.24301,71.60683 z"
id="path26"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient28);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="m 342.07979,179.19925 c 21.17892,6.6292 69.80877,131.61186 74.24608,172.24252 C 408.57514,279.39715 351.2836,180.46552 351.2836,180.46552 Z"
id="path27"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient29);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke"
d="M 447.9697,81.108647 C 445.48934,25.365047 418.59764,8.6648247 418.59764,8.6648247 420.10611,8.5094537 447.12965,36.795757 447.9697,81.108647 Z"
id="path28"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient30);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke"
d="M 375.28008,148.85834 C 409.63924,84.472103 413.97645,24.909041 413.97645,24.909041 414.28676,29.03485 402.37004,103.00716 375.28008,148.85834 Z"
id="path29"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient31);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="M 132.06997,-9.0360324 C 154.24318,-9.9535204 242.24625,91.244241 260.15235,127.98539 228.51241,62.797848 141.16027,-10.954379 141.16027,-10.954379 Z"
id="path30"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient32);fill-opacity:1;stroke-width:0.252673;paint-order:markers fill stroke"
d="m 176.09143,132.66299 c 26.96433,-48.851061 12.73415,-77.127601 12.73415,-77.127601 1.36799,0.654388 9.6664,38.884257 -12.73415,77.127601 z"
id="path31"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient33);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke"
d="m 252.52221,90.14388 c 24.85796,-68.61648 20.68441,-128.191231 20.68441,-128.191231 0.89363,4.039809 -0.38713,78.954895 -20.68441,128.191231 z"
id="path32"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient34);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke"
d="m 414.44721,276.41055 c 24.85796,-68.61648 20.68441,-128.19123 20.68441,-128.19123 0.89363,4.0398 -0.38713,78.95489 -20.68441,128.19123 z"
id="path33"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient35);fill-opacity:1;stroke-width:0.236032;paint-order:markers fill stroke"
d="m 210.88565,59.072993 c 29.61284,-39.94427 21.24301,-71.60683 21.24301,-71.60683 1.11807,0.68728 3.18301,39.68159 -21.24301,71.60683 z"
id="path34"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient36);fill-opacity:1;stroke-width:0.278221;paint-order:markers fill stroke"
d="m 144.57221,301.81055 c 24.85796,-68.61648 20.68441,-128.19123 20.68441,-128.19123 0.89363,4.0398 -0.38713,78.95489 -20.68441,128.19123 z"
id="path35"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient37);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="m 128.95328,157.70314 c 6.39951,22.06432 -61.091348,138.39905 -88.06141,165.60544 51.39821,-51.07569 92.82491,-157.62907 92.82491,-157.62907 z"
id="path36"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient38);fill-opacity:1;stroke-width:0.264583;paint-order:markers fill stroke"
d="m 317.84224,-17.558062 c 15.27619,17.15883035 4.4524,151.217152 -8.17529,187.384992 24.398,-68.22932 15.90901,-182.236897 15.90901,-182.236897 z"
id="path37"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient39);fill-opacity:1;stroke-width:0.379027;paint-order:markers fill stroke"
d="m 152.86855,151.94887 c 40.17315,-58.428075 4.14217,-87.669483 4.14217,-87.669483 2.89756,0.517335 29.87342,41.730563 -4.14217,87.669483 z"
id="path38"
sodipodi:nodetypes="ccc" />
<path
style="opacity:1;fill:url(#radialGradient40);stroke-width:0.264583;paint-order:markers fill stroke"
d="m 49.382576,217.43743 c 0,0 -6.653671,-14.88405 8.304578,-22.20819 14.958243,-7.32414 13.273203,-8.13708 15.367897,-13.85674 2.094691,-5.71967 -1.198008,-13.70608 9.063795,-15.85949 10.261807,-2.15341 18.283724,-2.9363 18.283724,-2.9363 0,0 -19.814459,1.20074 -21.594628,5.49345 -1.780168,4.29271 -3.367799,16.28098 -5.50391,19.07253 -2.136113,2.79156 -16.533591,8.35055 -19.322419,12.32281 -2.788824,3.97228 -5.515291,6.43747 -4.599037,17.97193 z"
id="path39" />
<path
style="opacity:1;fill:url(#radialGradient41);stroke-width:0.497551;paint-order:markers fill stroke"
d="m 83.525533,-13.021216 c 0,0 22.757677,-18.774764 38.480827,10.4224151 15.72313,29.1971799 16.63815,25.3856049 26.53783,27.9768449 9.8997,2.591216 22.17978,-6.897867 28.25778,14.048196 6.07803,20.946062 9.35264,37.607416 9.35264,37.607416 0,0 -6.88631,-41.388032 -14.36784,-43.771411 -7.4815,-2.383401 -27.53547,-1.905957 -32.64353,-5.518604 -5.10808,-3.612626 -17.79732,-32.1907473 -25.00364,-36.8027354 -7.20635,-4.6119666 -11.92571,-9.5732386 -30.614067,-3.9621216 z"
id="path40" />
<path
style="opacity:1;fill:url(#radialGradient42);fill-opacity:1;stroke-width:0.411073;paint-order:markers fill stroke"
d="m 179.71605,117.729 c 39.14772,16.27179 31.27592,150.72682 5.75661,187.54809 49.53029,-69.49442 13.58154,-182.84346 13.58154,-182.84346 z"
id="path41"
sodipodi:nodetypes="cccc" />
<path
style="opacity:1;fill:url(#radialGradient43);stroke-width:0.497551;paint-order:markers fill stroke"
d="m 275.69438,334.06791 c 0,0 -27.39639,-10.94721 -9.33533,-38.7589 18.06108,-27.81169 14.2783,-26.78409 11.79767,-36.71206 -2.48065,-9.92799 -16.70081,-16.1433 -1.24602,-31.53255 15.45478,-15.38928 28.49726,-26.26206 28.49726,-26.26206 0,0 -32.99779,25.91404 -31.49597,33.62106 1.50178,7.70702 11.55003,25.06854 10.83388,31.28389 -0.71612,6.21537 -19.69098,31.06855 -20.27617,39.60429 -0.58517,8.53577 -2.67094,15.05774 11.22468,28.75633 z"
id="path42" />
<g
id="g45"
style="fill:#222d2f;fill-opacity:1"
transform="matrix(0.46951769,0,0,0.46951769,41.781874,49.804731)">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="path43"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse44"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse45"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g48"
transform="matrix(0.0163187,0.47012984,-0.47012984,0.0163187,270.01266,112.55728)"
style="fill:#222d2f;fill-opacity:1">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse46"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse47"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse48"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g51"
transform="matrix(0.023208,0.66860544,-0.66860544,0.023208,180.70099,196.14232)"
style="fill:#222d2f;fill-opacity:1">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse49"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse50"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse51"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g54"
transform="matrix(0.01520429,0.43802433,-0.43802433,0.01520429,471.34681,27.597705)"
style="fill:#222d2f;fill-opacity:1">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse52"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse53"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse54"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g57"
transform="matrix(0.59664612,0.3078445,-0.3078445,0.59664612,475.59896,71.35259)"
style="fill:#222d2f;fill-opacity:1">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse55"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse56"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse57"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g60"
style="fill:#222d2f;fill-opacity:1"
transform="matrix(0.46951769,0,0,0.46951769,207.54243,5.6518051)">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse58"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse59"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse60"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g63"
style="fill:#222d2f;fill-opacity:1"
transform="matrix(0.46951769,0,0,0.46951769,128.96519,22.489785)">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse61"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse62"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse63"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g66"
style="fill:#222d2f;fill-opacity:1"
transform="matrix(0.46951769,0,0,0.46951769,282.75208,196.48225)">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse64"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse65"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse66"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
<g
id="g69"
style="fill:#222d2f;fill-opacity:1"
transform="matrix(0.46951769,0,0,0.46951769,359.08426,164.67718)">
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse67"
cx="80.380508"
cy="86.709709"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse68"
cx="85.1054"
cy="87.707977"
rx="1.6184589"
ry="1.640871" />
<ellipse
style="opacity:1;fill:#222d2f;fill-opacity:1;stroke-width:0.0631466;paint-order:markers fill stroke"
id="ellipse69"
cx="80.849136"
cy="92.244881"
rx="1.6184589"
ry="1.640871" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 30 KiB

View file

@ -14,7 +14,7 @@ interface Props extends DivProps {
username?: string; username?: string;
size?: number; size?: number;
hasUpdates?: boolean; hasUpdates?: boolean;
preset?: typeof imagePresets[keyof typeof imagePresets]; preset?: (typeof imagePresets)[keyof typeof imagePresets];
} }
const Avatar = forwardRef<HTMLDivElement, Props>( const Avatar = forwardRef<HTMLDivElement, Props>(

View file

@ -31,7 +31,7 @@ const Columns: FC<ColumnsProps> = ({
if (!childs) return; if (!childs) return;
const timeout = setTimeout(() => setColumns([...childs]), 150); const timeout = setTimeout(() => setColumns([...childs.values()]), 150);
return () => clearTimeout(timeout); return () => clearTimeout(timeout);
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps

View file

@ -8,6 +8,8 @@ import { URLS } from '~/constants/urls';
import { INode } from '~/types'; import { INode } from '~/types';
import { getPrettyDate } from '~/utils/dom'; import { getPrettyDate } from '~/utils/dom';
import { getNewCommentAnchor } from '../../../constants/dom/links';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface Props { interface Props {
@ -16,12 +18,15 @@ interface Props {
onClick?: MouseEventHandler; onClick?: MouseEventHandler;
} }
const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => { const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => (
return (
<Anchor <Anchor
key={node.id} key={node.id}
className={styles.item} className={styles.item}
href={URLS.NODE_URL(node.id)} href={
hasNew
? getNewCommentAnchor(URLS.NODE_URL(node.id))
: URLS.NODE_URL(node.id)
}
onClick={onClick} onClick={onClick}
> >
<div <div
@ -41,7 +46,6 @@ const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => {
</div> </div>
</div> </div>
</Anchor> </Anchor>
); );
};
export { NodeHorizontalCard }; export { NodeHorizontalCard };

View file

@ -27,8 +27,8 @@
&.new { &.new {
&::after { &::after {
content: ' '; content: ' ';
width: 12px; width: 8px;
height: 12px; height: 8px;
border-radius: 100%; border-radius: 100%;
background: $color_danger; background: $color_danger;
box-shadow: $content_bg 0 0 0 5px; box-shadow: $content_bg 0 0 0 5px;

View file

@ -13,9 +13,11 @@ interface Props extends DivProps {
const SubTitle: FC<Props> = ({ isLoading, children, ...rest }) => ( const SubTitle: FC<Props> = ({ isLoading, children, ...rest }) => (
<div {...rest} className={classNames(styles.title, rest.className)}> <div {...rest} className={classNames(styles.title, rest.className)}>
<span className={styles.name}>
<Placeholder active={isLoading} loading> <Placeholder active={isLoading} loading>
{children} {children}
</Placeholder> </Placeholder>
</span>
</div> </div>
); );

View file

@ -1,7 +1,25 @@
@import "src/styles/variables.scss"; @import 'src/styles/variables.scss';
.title { .title {
font: $font_12_semibold; font: $font_12_semibold;
text-transform: uppercase; text-transform: uppercase;
opacity: 0.3; display: flex;
flex-direction: row;
align-items: center;
gap: $gap / 2;
color: var(--gray_75);
a {
text-decoration: none;
color: inherit;
}
&::after {
content: ' ';
display: flex;
height: 2px;
background-color: var(--gray_90);
flex: 1;
border-radius: 2px;
}
} }

View file

@ -25,7 +25,7 @@
background: none; background: none;
padding: 0 $gap 0 $gap; padding: 0 $gap 0 $gap;
font: $font_14_semibold; font: $font_14_semibold;
border-radius: $radius; border-radius: $input_radius;
} }
} }

View file

@ -5,7 +5,7 @@
background: $input_bg_color; background: $input_bg_color;
min-height: $input_height; min-height: $input_height;
border-radius: $radius; border-radius: $input_radius;
position: relative; position: relative;
color: $input_text_color; color: $input_text_color;
font: $input_font; font: $input_font;

View file

@ -57,7 +57,9 @@ const NodeImageSwiperBlock: FC<Props> = observer(({ node }) => {
useEffect(() => { useEffect(() => {
controlledSwiper?.slideTo(0, 0); controlledSwiper?.slideTo(0, 0);
return () => controlledSwiper?.slideTo(0, 0); return () => {
controlledSwiper?.slideTo(0, 0);
};
}, [controlledSwiper, images, node.id]); }, [controlledSwiper, images, node.id]);
useEffect(() => { useEffect(() => {

View file

@ -29,11 +29,6 @@
.title { .title {
padding-left: 5px; padding-left: 5px;
a {
text-decoration: none;
color: inherit;
}
} }
.text { .text {

View file

@ -9,6 +9,8 @@ import { Square } from '~/components/common/Square';
import { NotificationItem } from '~/types/notifications'; import { NotificationItem } from '~/types/notifications';
import { formatText, getURLFromString } from '~/utils/dom'; import { formatText, getURLFromString } from '~/utils/dom';
import { getCommentAnchor } from '../../../constants/dom/links';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
interface NotificationCommentProps { interface NotificationCommentProps {
@ -17,7 +19,10 @@ interface NotificationCommentProps {
} }
const NotificationComment: FC<NotificationCommentProps> = ({ item, isNew }) => ( const NotificationComment: FC<NotificationCommentProps> = ({ item, isNew }) => (
<Anchor href={item.url} className={styles.link}> <Anchor
href={getCommentAnchor(item.url, item.itemId)}
className={styles.link}
>
<div className={classNames(styles.message, { [styles.new]: isNew })}> <div className={classNames(styles.message, { [styles.new]: isNew })}>
<div className={styles.icon}> <div className={styles.icon}>
<Avatar <Avatar

View file

@ -7,11 +7,11 @@ export const API = {
USER: { USER: {
LOGIN: '/auth', LOGIN: '/auth',
OAUTH_WINDOW: (provider: OAuthProvider) => OAUTH_WINDOW: (provider: OAuthProvider) =>
`${CONFIG.apiHost}oauth/${provider}/redirect`, `${CONFIG.apiHost}oauth/${provider}/redirect/`,
ME: '/auth', ME: '/auth',
UPDATE_PHOTO: '/auth/photo', UPDATE_PHOTO: '/auth/photo',
UPDATE_COVER: '/auth/photo', UPDATE_COVER: '/auth/photo',
PROFILE: (username: string) => `/users/${username}/profile`, PROFILE: (username: string) => `/users/${username}`,
MESSAGES: (username: string) => `/users/${username}/messages`, MESSAGES: (username: string) => `/users/${username}/messages`,
MESSAGE_SEND: (username: string) => `/users/${username}/messages`, MESSAGE_SEND: (username: string) => `/users/${username}/messages`,
MESSAGE_DELETE: (username: string, id: number) => MESSAGE_DELETE: (username: string, id: number) =>

View file

@ -20,7 +20,7 @@ export const COMMENT_BLOCK_DETECTORS = [
]; ];
export type ICommentBlock = { export type ICommentBlock = {
type: typeof COMMENT_BLOCK_TYPES[keyof typeof COMMENT_BLOCK_TYPES]; type: (typeof COMMENT_BLOCK_TYPES)[keyof typeof COMMENT_BLOCK_TYPES];
content: string; content: string;
}; };

View file

@ -5,3 +5,5 @@ export const isTablet = () => {
return window.innerWidth < 599; return window.innerWidth < 599;
}; };
export const headerHeight = 64; // px

View file

@ -0,0 +1,15 @@
export const NEW_COMMENT_ANCHOR_NAME = 'new-comment';
export const COMMENT_ANCHOR_PREFIX = 'comment';
export const getCommentId = (id: number) =>
[COMMENT_ANCHOR_PREFIX, id].join('-');
export const getNewCommentAnchor = (url: string) =>
[url, NEW_COMMENT_ANCHOR_NAME].join('#');
export const getCommentAnchor = (url: string, commentId: number) =>
[url, getCommentId(commentId)].join('#');
export const isCommentAnchor = (hash: string | undefined) =>
hash?.startsWith(COMMENT_ANCHOR_PREFIX) ||
hash?.startsWith(NEW_COMMENT_ANCHOR_NAME);

View file

@ -1,3 +1,5 @@
import { lazy } from 'react';
import { LoginDialog } from '~/containers/auth/LoginDialog'; import { LoginDialog } from '~/containers/auth/LoginDialog';
import { LoginSocialRegisterDialog } from '~/containers/auth/LoginSocialRegisterDialog'; import { LoginSocialRegisterDialog } from '~/containers/auth/LoginSocialRegisterDialog';
import { RestorePasswordDialog } from '~/containers/auth/RestorePasswordDialog'; import { RestorePasswordDialog } from '~/containers/auth/RestorePasswordDialog';
@ -6,9 +8,14 @@ import { TelegramAttachDialog } from '~/containers/auth/TelegramAttachDialog';
import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog'; import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog';
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog'; import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
import { LoadingDialog } from '~/containers/dialogs/LoadingDialog'; import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe';
import { TestDialog } from '~/containers/dialogs/TestDialog'; import { TestDialog } from '~/containers/dialogs/TestDialog';
const PhotoSwipe = lazy(() =>
import('~/containers/dialogs/PhotoSwipe').then((it) => ({
default: it.PhotoSwipe,
})),
);
export enum Dialog { export enum Dialog {
Login = 'Login', Login = 'Login',
Register = 'Register', Register = 'Register',

View file

@ -1,4 +1,3 @@
export enum SidebarName { export enum SidebarName {
Settings = 'settings', Settings = 'settings',
Tag = 'tag', Tag = 'tag',

View file

@ -1,6 +1,7 @@
export enum Theme { export enum Theme {
Default = 'Default', Default = 'Default',
Horizon = 'Horizon', Horizon = 'Horizon',
Sansevieria = 'Sansevieria',
} }
interface ThemeColors { interface ThemeColors {
@ -17,7 +18,7 @@ export const themeColors: Record<Theme, ThemeColors> = {
'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)', 'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)',
'linear-gradient(170deg, #582cd0, #592071)', 'linear-gradient(170deg, #582cd0, #592071)',
], ],
background: 'url(\'/images/noise_top.png\') 0% 0% #23201f', background: "url('/images/noise_top.png') 0% 0% #23201f",
}, },
[Theme.Horizon]: { [Theme.Horizon]: {
name: 'Веспера', name: 'Веспера',
@ -28,4 +29,13 @@ export const themeColors: Record<Theme, ThemeColors> = {
], ],
background: 'url("/images/horizon_bg.svg") 50% 50% / cover rgb(28, 30, 38)', background: 'url("/images/horizon_bg.svg") 50% 50% / cover rgb(28, 30, 38)',
}, },
[Theme.Sansevieria]: {
name: 'Сансевирия',
colors: [
'linear-gradient(165deg, #f4e7aa -50%, #a23500 150%)',
'linear-gradient(165deg, #ff7e56 -50%, #280003 150%)',
'linear-gradient(170deg, #476695, #22252d)',
],
background: '#1f2625',
},
}; };

View file

@ -37,7 +37,7 @@ export const imagePresets = {
flow_horizontal: 'flow_horizontal', flow_horizontal: 'flow_horizontal',
} as const; } as const;
export type ImagePreset = typeof imagePresets[keyof typeof imagePresets]; export type ImagePreset = (typeof imagePresets)[keyof typeof imagePresets];
export const imageSrcSets: Partial<Record<ImagePreset, number>> = { export const imageSrcSets: Partial<Record<ImagePreset, number>> = {
[imagePresets[1600]]: 1600, [imagePresets[1600]]: 1600,
@ -49,7 +49,7 @@ export const imageSrcSets: Partial<Record<ImagePreset, number>> = {
export const flowDisplayToPreset: Record< export const flowDisplayToPreset: Record<
FlowDisplayVariant, FlowDisplayVariant,
typeof imagePresets[keyof typeof imagePresets] (typeof imagePresets)[keyof typeof imagePresets]
> = { > = {
single: 'flow_square', single: 'flow_square',
quadro: 'flow_square', quadro: 'flow_square',

View file

@ -3,10 +3,12 @@ import dynamic from 'next/dynamic';
import type { BorisSuperpowersProps } from './index'; import type { BorisSuperpowersProps } from './index';
export const BorisSuperPowersSSR = dynamic<BorisSuperpowersProps>( export const BorisSuperPowersSSR = dynamic<BorisSuperpowersProps>(
() => import('~/containers/boris/BorisSuperpowers/index') () =>
.then(it => it.BorisSuperpowers), import('~/containers/boris/BorisSuperpowers/index').then(
(it) => it.BorisSuperpowers,
),
{ {
ssr: false, ssr: false,
loading: () => <div />, loading: () => <div />,
} },
); );

View file

@ -8,7 +8,7 @@ import { DialogComponentProps } from '~/types/modal';
import { values } from '~/utils/ramda'; import { values } from '~/utils/ramda';
export interface EditorCreateDialogProps extends DialogComponentProps { export interface EditorCreateDialogProps extends DialogComponentProps {
type: typeof NODE_TYPES[keyof typeof NODE_TYPES]; type: (typeof NODE_TYPES)[keyof typeof NODE_TYPES];
isInLab: boolean; isInLab: boolean;
} }

View file

@ -11,7 +11,7 @@ import { TextEditor } from '../components/TextEditor';
import { VideoEditor } from '../components/VideoEditor'; import { VideoEditor } from '../components/VideoEditor';
export const NODE_EDITORS: Record< export const NODE_EDITORS: Record<
typeof NODE_TYPES[keyof typeof NODE_TYPES], (typeof NODE_TYPES)[keyof typeof NODE_TYPES],
FC<NodeEditorProps> FC<NodeEditorProps>
> = { > = {
[NODE_TYPES.IMAGE]: ImageEditor, [NODE_TYPES.IMAGE]: ImageEditor,
@ -22,7 +22,7 @@ export const NODE_EDITORS: Record<
}; };
export const NODE_EDITOR_DATA: Record< export const NODE_EDITOR_DATA: Record<
typeof NODE_TYPES[keyof typeof NODE_TYPES], (typeof NODE_TYPES)[keyof typeof NODE_TYPES],
Partial<INode> Partial<INode>
> = { > = {
[NODE_TYPES.TEXT]: { [NODE_TYPES.TEXT]: {

View file

@ -1,7 +1,8 @@
import { FC, createElement } from 'react'; import { FC, createElement, Suspense } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { LoaderCircle } from '~/components/common/LoaderCircle';
import { ModalWrapper } from '~/components/common/ModalWrapper'; import { ModalWrapper } from '~/components/common/ModalWrapper';
import { DIALOG_CONTENT } from '~/constants/modal'; import { DIALOG_CONTENT } from '~/constants/modal';
import { useModalStore } from '~/store/modal/useModalStore'; import { useModalStore } from '~/store/modal/useModalStore';
@ -18,10 +19,12 @@ const Modal: FC<Props> = observer(() => {
return ( return (
<ModalWrapper onOverlayClick={hide}> <ModalWrapper onOverlayClick={hide}>
<Suspense fallback={<LoaderCircle />}>
{createElement(DIALOG_CONTENT[current!]! as any, { {createElement(DIALOG_CONTENT[current!]! as any, {
onRequestClose: hide, onRequestClose: hide,
...props, ...props,
})} })}
</Suspense>
</ModalWrapper> </ModalWrapper>
); );
}); });

View file

@ -1,10 +1,12 @@
import { useEffect, useRef, VFC } from 'react'; import { useEffect, useRef } from 'react';
import 'photoswipe/style.css';
import classNames from 'classnames';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js'; import PSWP from 'photoswipe';
import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js'; import { renderToStaticMarkup } from 'react-dom/server';
import { Icon } from '~/components/common/Icon';
import { imagePresets } from '~/constants/urls'; import { imagePresets } from '~/constants/urls';
import { useWindowSize } from '~/hooks/dom/useWindowSize'; import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useModal } from '~/hooks/modal/useModal'; import { useModal } from '~/hooks/modal/useModal';
@ -13,125 +15,55 @@ import { DialogComponentProps } from '~/types/modal';
import { getURL } from '~/utils/dom'; import { getURL } from '~/utils/dom';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
export interface Props extends DialogComponentProps {
export interface PhotoSwipeProps extends DialogComponentProps {
items: IFile[]; items: IFile[];
index: number; index: number;
} }
const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => { const arrowNextSVG = renderToStaticMarkup(<Icon icon="right" size={40} />);
let ref = useRef<HTMLDivElement>(null); const arrowPrevSVG = renderToStaticMarkup(<Icon icon="left" size={40} />);
const closeSVG = renderToStaticMarkup(<Icon icon="close" size={32} />);
const padding = { top: 10, left: 10, right: 10, bottom: 10 } as const;
const PhotoSwipe = observer(({ index, items }: Props) => {
const { hideModal } = useModal(); const { hideModal } = useModal();
const { isTablet } = useWindowSize(); const { isTablet } = useWindowSize();
const pswp = useRef(new PSWP());
useEffect(() => { useEffect(() => {
new Promise(async (resolve) => { const dataSource = items.map((file) => ({
const images = await Promise.all( src: getURL(file, imagePresets[1600]),
items.map( width: file.metadata?.width,
(file) => height: file.metadata?.height,
new Promise((resolve) => { }));
const src = getURL(
file,
isTablet ? imagePresets[900] : imagePresets[1600],
);
if (file.metadata?.width && file.metadata.height) { pswp.current.options = {
resolve({ ...pswp.current.options,
src, dataSource,
w: file.metadata.width,
h: file.metadata.height,
});
return;
}
const img = new Image();
img.onload = () => {
resolve({
src,
h: img.naturalHeight,
w: img.naturalWidth,
});
};
img.onerror = () => {
resolve({});
};
img.src = getURL(file, imagePresets[1600]);
}),
),
);
resolve(images);
}).then((images) => {
const ps = new PhotoSwipeJs(ref.current, PhotoSwipeUI_Default, images, {
index: index || 0, index: index || 0,
closeOnScroll: false, closeOnVerticalDrag: true,
history: false, padding,
}); mainClass: styles.wrap,
zoom: false,
counter: false,
bgOpacity: 0.1,
arrowNextSVG,
arrowPrevSVG,
closeSVG,
};
ps.init(); pswp.current.on('closingAnimationEnd', hideModal);
ps.listen('destroy', hideModal); pswp.current.init();
ps.listen('close', hideModal);
}); return () => {
pswp.current?.off('close', hideModal);
// eslint-disable-next-line react-hooks/exhaustive-deps
pswp.current?.destroy();
};
}, [hideModal, items, index, isTablet]); }, [hideModal, items, index, isTablet]);
return ( return null;
<div
className="pswp"
tabIndex={-1}
role="dialog"
aria-hidden="true"
ref={ref}
>
<div className={classNames('pswp__bg', styles.bg)} />
<div className={classNames('pswp__scroll-wrap', styles.wrap)}>
<div className="pswp__container">
<div className="pswp__item" />
<div className="pswp__item" />
<div className="pswp__item" />
</div>
<div className="pswp__ui pswp__ui--hidden">
<div className={classNames('pswp__top-bar', styles.bar)}>
<div className="pswp__counter" />
<button
className="pswp__button pswp__button--close"
title="Close (Esc)"
/>
<div className="pswp__preloader">
<div className="pswp__preloader__icn">
<div className="pswp__preloader__cut">
<div className="pswp__preloader__donut" />
</div>
</div>
</div>
</div>
<div className="pswp__share-modal pswp__share-modal--hidden pswp__single-tap">
<div className="pswp__share-tooltip" />
</div>
<button
className="pswp__button pswp__button--arrow--left"
title="Previous (arrow left)"
/>
<button
className="pswp__button pswp__button--arrow--right"
title="Next (arrow right)"
/>
<div className="pswp__caption">
<div className="pswp__caption__center" />
</div>
</div>
</div>
</div>
);
}); });
export { PhotoSwipe }; export { PhotoSwipe };

View file

@ -1,4 +1,4 @@
@import "src/styles/variables"; @import 'src/styles/variables';
.wrap { .wrap {
:global(.pswp__img) { :global(.pswp__img) {

View file

@ -23,6 +23,7 @@ interface Props {
const FlowCellMenu: FC<Props> = ({ const FlowCellMenu: FC<Props> = ({
onClose, onClose,
currentView,
hasDescription, hasDescription,
toggleViewDescription, toggleViewDescription,
descriptionEnabled, descriptionEnabled,
@ -59,7 +60,7 @@ const FlowCellMenu: FC<Props> = ({
/> />
</div> </div>
{hasDescription && ( {hasDescription && currentView !== 'single' && (
<Group <Group
className={styles.description} className={styles.description}
horizontal horizontal

View file

@ -1,6 +1,7 @@
import { FC, ReactElement } from 'react'; import { FC, ReactElement } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { transparentize, darken, desaturate, getLuminance } from 'color2k';
import { Markdown } from '~/components/common/Markdown'; import { Markdown } from '~/components/common/Markdown';
import { formatText } from '~/utils/dom'; import { formatText } from '~/utils/dom';
@ -11,13 +12,29 @@ import styles from './styles.module.scss';
interface Props extends DivProps { interface Props extends DivProps {
children: string; children: string;
heading: string | ReactElement; heading: string | ReactElement;
color?: string;
} }
const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => ( const FlowCellText: FC<Props> = ({ children, heading, color, ...rest }) => {
<div {...rest} className={classNames(styles.text, rest.className)}> const colorIsBright = !!color && getLuminance(color) > 0.4;
const textColor = colorIsBright
? desaturate(darken(color, 0.5), 0.1)
: undefined;
return (
<div
{...rest}
className={classNames(styles.text, rest.className)}
style={{
backgroundColor: color && transparentize(color, 0.5),
color: textColor,
}}
>
{heading && <div className={styles.heading}>{heading}</div>} {heading && <div className={styles.heading}>{heading}</div>}
<Markdown className={styles.description}>{formatText(children)}</Markdown> <Markdown className={styles.description}>{formatText(children)}</Markdown>
</div> </div>
); );
};
export { FlowCellText }; export { FlowCellText };

View file

@ -1,10 +1,24 @@
@import "src/styles/variables"; @import 'src/styles/variables';
.text { .text {
padding: $gap; @include blur;
line-height: 1.3em; line-height: 1.3em;
display: flex;
flex-direction: column;
min-height: 0;
}
.description {
mask-image: linear-gradient(
to bottom,
rgba(255, 255, 255, 1) 50%,
rgba(0, 0, 0, 0) 95%
);
flex: 1;
overflow: hidden;
} }
.heading { .heading {
margin-bottom: 0.4em; margin-bottom: 0.25em;
} }

View file

@ -1,4 +1,4 @@
import { FC, useMemo } from 'react'; import { FC, useCallback, useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
@ -9,6 +9,8 @@ import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls'; import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls';
import { FlowDisplay, INode } from '~/types'; import { FlowDisplay, INode } from '~/types';
import { isFullyVisible } from '../../../../../utils/dom';
import { CellShade } from './components/CellShade'; import { CellShade } from './components/CellShade';
import { FlowCellImage } from './components/FlowCellImage'; import { FlowCellImage } from './components/FlowCellImage';
import { FlowCellMenu } from './components/FlowCellMenu'; import { FlowCellMenu } from './components/FlowCellMenu';
@ -25,7 +27,7 @@ interface Props {
text?: string; text?: string;
flow: FlowDisplay; flow: FlowDisplay;
canEdit?: boolean; canEdit?: boolean;
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void; onChange: (id: INode['id'], flow: FlowDisplay) => void;
} }
const FlowCell: FC<Props> = ({ const FlowCell: FC<Props> = ({
@ -37,7 +39,7 @@ const FlowCell: FC<Props> = ({
text, text,
title, title,
canEdit = false, canEdit = false,
onChangeCellView, onChange,
}) => { }) => {
const { isTablet } = useWindowSize(); const { isTablet } = useWindowSize();
@ -45,6 +47,30 @@ const FlowCell: FC<Props> = ({
((!!flow.display && flow.display !== 'single') || !image) && ((!!flow.display && flow.display !== 'single') || !image) &&
flow.show_description && flow.show_description &&
!!text; !!text;
const {
isActive: isMenuActive,
activate,
ref,
deactivate,
} = useClickOutsideFocus();
const onChangeWithScroll = useCallback<typeof onChange>(
(...args) => {
onChange(...args);
setTimeout(() => {
if (!isFullyVisible(ref.current)) {
ref.current?.scrollIntoView({
behavior: 'auto',
block: 'center',
});
}
}, 0);
},
[onChange, ref],
);
const { const {
hasDescription, hasDescription,
setViewHorizontal, setViewHorizontal,
@ -52,13 +78,7 @@ const FlowCell: FC<Props> = ({
setViewQuadro, setViewQuadro,
setViewSingle, setViewSingle,
toggleViewDescription, toggleViewDescription,
} = useFlowCellControls(id, text, flow, onChangeCellView); } = useFlowCellControls(id, text, flow, onChangeWithScroll);
const {
isActive: isMenuActive,
activate,
ref,
deactivate,
} = useClickOutsideFocus();
const shadeSize = useMemo(() => { const shadeSize = useMemo(() => {
const min = isTablet ? 10 : 15; const min = isTablet ? 10 : 15;
@ -111,8 +131,9 @@ const FlowCell: FC<Props> = ({
<FlowCellText <FlowCellText
className={styles.text} className={styles.text}
heading={<h4 className={styles.title}>{title}</h4>} heading={<h4 className={styles.title}>{title}</h4>}
color={color}
> >
{text!} {text}
</FlowCellText> </FlowCellText>
)} )}
@ -124,7 +145,7 @@ const FlowCell: FC<Props> = ({
/> />
)} )}
{!!title && ( {!!title && !withText && (
<CellShade <CellShade
color={color} color={color}
className={styles.shade} className={styles.shade}

View file

@ -1,5 +1,7 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
$compact_size: 200px;
.cell { .cell {
@include inner_shadow; @include inner_shadow;
@ -9,6 +11,7 @@
width: 100%; width: 100%;
height: 100%; height: 100%;
background: $content_bg; background: $content_bg;
container: cell / inline-size;
} }
.thumb { .thumb {
@ -33,20 +36,17 @@
.text { .text {
position: absolute; position: absolute;
bottom: 5px;
left: 5px;
z-index: 1; z-index: 1;
overflow: hidden; overflow: hidden;
border-radius: $radius; border-radius: $radius;
max-height: calc(100% - 10px);
max-width: calc(100% - 10px);
box-sizing: border-box; box-sizing: border-box;
font: $font_16_regular; inset: 50% 0 0 0;
padding: $gap $gap * 1.5 0 $gap * 1.5;
font: $font_14_medium;
line-height: 1.25em;
@include tablet { @container (max-width: $compact_size) {
font: $font_14_regular; padding: $gap / 2 $gap 0 $gap;
left: 5px;
bottom: 5px;
} }
& :global(.grey) { & :global(.grey) {
@ -54,14 +54,34 @@
opacity: 0.5; opacity: 0.5;
} }
.quadro &, @container (max-width: #{$compact_size}) {
.horizontal & { padding: $gap / 2 $gap 0 $gap;
max-width: calc(50% - 15px); }
.horizontal &,
.quadro & {
@container (max-width: #{$compact_size * 2}) {
padding: $gap / 2 $gap 0 $gap;
}
}
.horizontal & {
inset: 0 calc(50% + $gap / 2) 0 0;
border-radius: $radius 0 0 $radius;
} }
.quadro &,
.vertical & { .vertical & {
max-height: calc(50% - 15px); inset: calc(50% + $gap / 2) 0 0 0;
border-radius: 0 0 $radius $radius;
}
.quadro & {
inset: calc(50% + $gap / 2) calc(50% + $gap / 2) 0 0;
border-radius: 0 $radius 0 $radius;
}
.title {
margin-bottom: 0.1em;
} }
} }
@ -76,11 +96,21 @@
.title { .title {
font: $font_cell_title; font: $font_cell_title;
line-height: 1.2em;
text-transform: uppercase; text-transform: uppercase;
word-break: break-word; word-break: break-word;
color: inherit;
margin-bottom: -0.125em;
@include tablet { @container (max-width: #{$compact_size}) {
font: $font_18_semibold; font: $font_cell_title_compact;
}
.horizontal &,
.quadro & {
@container (max-width: #{$compact_size * 2}) {
font: $font_cell_title_compact;
}
} }
} }
@ -107,12 +137,7 @@
} }
.display_modal { .display_modal {
@include appear;
position: absolute; position: absolute;
top: 0; inset: 0;
left: 0;
right: 0;
bottom: 0;
z-index: 11; z-index: 11;
} }

View file

@ -46,7 +46,7 @@ export const FlowGrid: FC<Props> = observer(
text={node.description} text={node.description}
title={node.title} title={node.title}
canEdit={fetched && isUser && canEditNode(node, user)} canEdit={fetched && isUser && canEditNode(node, user)}
onChangeCellView={onChangeCellView} onChange={onChangeCellView}
/> />
</div> </div>
))} ))}

View file

@ -1,11 +1,5 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
@mixin mobile {
@media (max-width: $cell * 2) {
@content;
}
}
.cell { .cell {
&.horizontal, &.horizontal,
&.quadro { &.quadro {

View file

@ -1,33 +0,0 @@
import { FC } from 'react';
import { NodeHorizontalCard } from '~/components/common/NodeHorizontalCard';
import { IFlowNode } from '~/types';
import styles from './styles.module.scss';
interface Props {
recent: IFlowNode[];
updated: IFlowNode[];
}
const FlowRecent: FC<Props> = ({ recent, updated }) => {
return (
<>
<div className={styles.updates}>
{updated &&
updated.map((node) => (
<NodeHorizontalCard node={node} key={node.id} hasNew />
))}
</div>
<div className={styles.recent}>
{recent &&
recent.map((node) => (
<NodeHorizontalCard node={node} key={node.id} />
))}
</div>
</>
);
};
export { FlowRecent };

View file

@ -1,11 +0,0 @@
@import "src/styles/variables";
.recent {
@media (max-width: $flow_hide_recents) {
display: none;
}
}
.updates {
}

View file

@ -2,17 +2,15 @@ import { FC, FormEvent, useCallback, useMemo } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Group } from '~/components/common/Group'; import { Card } from '~/components/common/Card';
import { Icon } from '~/components/common/Icon'; import { Icon } from '~/components/common/Icon';
import { Superpower } from '~/components/common/Superpower'; import { NodeHorizontalCard } from '~/components/common/NodeHorizontalCard';
import { SubTitle } from '~/components/common/SubTitle';
import { InputText } from '~/components/input/InputText'; import { InputText } from '~/components/input/InputText';
import { Toggle } from '~/components/input/Toggle';
import { experimentalFeatures } from '~/constants/features';
import styles from '~/containers/flow/FlowStamp/styles.module.scss'; import styles from '~/containers/flow/FlowStamp/styles.module.scss';
import { useFlowContext } from '~/utils/providers/FlowProvider'; import { useFlowContext } from '~/utils/providers/FlowProvider';
import { useSearchContext } from '~/utils/providers/SearchProvider'; import { useSearchContext } from '~/utils/providers/SearchProvider';
import { FlowRecent } from './components/FlowRecent';
import { FlowSearchResults } from './components/FlowSearchResults'; import { FlowSearchResults } from './components/FlowSearchResults';
interface Props { interface Props {
@ -64,7 +62,8 @@ const FlowStamp: FC<Props> = ({ isFluid, onToggleLayout }) => {
return ( return (
<div className={styles.wrap}> <div className={styles.wrap}>
<form className={styles.search} onSubmit={onSearchSubmit}> <Card className={styles.search}>
<form onSubmit={onSearchSubmit}>
<InputText <InputText
title="Поиск" title="Поиск"
value={searchText} value={searchText}
@ -73,16 +72,13 @@ const FlowStamp: FC<Props> = ({ isFluid, onToggleLayout }) => {
onKeyUp={onKeyUp} onKeyUp={onKeyUp}
/> />
</form> </form>
</Card>
{searchText ? ( {searchText ? (
<div className={styles.search_results}> <Card className={styles.grid}>
<div className={styles.grid}> <SubTitle>Результаты поиска</SubTitle>
<div className={styles.label}>
<span className={styles.label_text}>Результаты поиска</span>
<span className="line" />
</div>
<div className={styles.items}> <div className={classNames(styles.items, styles.scrollable)}>
<FlowSearchResults <FlowSearchResults
hasMore={searchHasMore} hasMore={searchHasMore}
isLoading={searchIsLoading} isLoading={searchIsLoading}
@ -90,34 +86,31 @@ const FlowStamp: FC<Props> = ({ isFluid, onToggleLayout }) => {
onLoadMore={onSearchLoadMore} onLoadMore={onSearchLoadMore}
/> />
</div> </div>
</div> </Card>
</div>
) : ( ) : (
<div className={styles.grid}> <Card
<div className={classNames(styles.label, styles.whatsnew)}> className={classNames(styles.grid, {
<span className={styles.label_text}>Что нового?</span> [styles.noUpdates]: !updates.length,
<span className="line" /> })}
</div> >
<SubTitle>Что нового?</SubTitle>
<div className={styles.items}> {updates.length > 0 && (
<FlowRecent updated={updates} recent={recent} /> <div className={classNames(styles.items, styles.updates)}>
</div> {updates.map((node) => (
<NodeHorizontalCard node={node} key={node.id} hasNew />
))}
</div> </div>
)} )}
{experimentalFeatures.liquidFlow && ( {recent.length > 0 && (
<Superpower> <div className={classNames(styles.items, styles.recent)}>
<div className={styles.toggles}> {recent.map((node) => (
<Group <NodeHorizontalCard node={node} key={node.id} />
horizontal ))}
onClick={onToggleLayout}
className={styles.fluid_toggle}
>
<Toggle value={isFluid} />
<div className={styles.toggles__label}>Жидкое течение</div>
</Group>
</div> </div>
</Superpower> )}
</Card>
)} )}
</div> </div>
); );

View file

@ -1,22 +1,24 @@
@import '../../../styles/variables'; @import '~/styles/variables';
.wrap { .wrap {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
border-radius: $radius; gap: $gap;
}
.search {
background-color: var(--content_bg_lighter);
} }
.grid { .grid {
@include outer_shadow();
display: flex; display: flex;
justify-content: stretch; justify-content: stretch;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
border-radius: $radius;
position: relative;
background: $content_bg;
overflow: hidden; overflow: hidden;
gap: $gap;
padding: $gap;
&::after { &::after {
content: ''; content: '';
@ -33,51 +35,33 @@
display: none; display: none;
} }
} }
&.noUpdates {
@container sizer (width < #{$flow_hide_recents}) {
display: none;
}
}
}
.items.recent {
@container sizer (width < #{$flow_hide_recents}) {
display: none;
background-color: red;
}
} }
.items { .items {
padding: 0 $gap 0 $gap;
flex: 1; flex: 1;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
overflow: hidden;
}
.label { @container sizer (width >= #{$flow_hide_recents}) {
display: flex; &.scrollable {
flex-direction: row; overflow: auto;
min-width: 0;
padding: $gap;
border-radius: $radius;
@include title_with_line();
color: transparentize(white, $amount: 0.8);
&_search {
color: white;
padding-left: $gap * 1.2;
} }
& > :global(.line) {
margin-right: $gap;
} }
} }
.label_text {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.search {
@include outer_shadow();
background: $content_bg_lighter;
padding: $gap;
border-radius: $radius;
}
.search_icon { .search_icon {
stroke-width: 0.5; stroke-width: 0.5;
pointer-events: none; pointer-events: none;
@ -89,34 +73,3 @@
stroke-width: 0.5; stroke-width: 0.5;
transition: opacity 0.25s; transition: opacity 0.25s;
} }
.toggles {
& > div {
padding: $gap;
font: $font_14_semibold;
}
&__label {
cursor: pointer;
}
}
.fluid_toggle {
@include desktop {
display: none;
}
}
.whatsnew {
@media (max-width: $flow_hide_recents) {
display: none;
}
}
.search_results {
overflow: auto;
@include tablet {
margin-top: $gap;
}
}

View file

@ -59,7 +59,11 @@ const Header: FC<HeaderProps> = observer(() => {
className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })} className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })}
> >
<div className={styles.container}> <div className={styles.container}>
<div className={styles.logo_wrapper}> <div
className={classNames(styles.logo_wrapper, {
[styles.guest]: !isUser,
})}
>
<Logo /> <Logo />
</div> </div>

View file

@ -4,9 +4,12 @@ import type { HeaderProps } from '~/containers/main/Header/index';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
export const HeaderSSR = dynamic<HeaderProps>(() => import('./index').then(it => it.Header), { export const HeaderSSR = dynamic<HeaderProps>(
() => import('./index').then((it) => it.Header),
{
ssr: false, ssr: false,
loading: () => <div className={styles.wrap} />, loading: () => <div className={styles.wrap} />,
}); },
);
export const HeaderSSRPlaceholder = () => <div className={styles.wrap} />; export const HeaderSSRPlaceholder = () => <div className={styles.wrap} />;

View file

@ -106,7 +106,9 @@
transform: translate(50%, 0) scaleX(0); transform: translate(50%, 0) scaleX(0);
opacity: 0; opacity: 0;
border-radius: 3px; border-radius: 3px;
transition: transform 0.5s, opacity 0.25s; transition:
transform 0.5s,
opacity 0.25s;
} }
&::after { &::after {
@ -159,7 +161,7 @@
} }
} }
.logo_wrapper { .logo_wrapper:not(.guest) {
@include tablet { @include tablet {
display: none; display: none;
} }

View file

@ -3,5 +3,6 @@ import dynamic from 'next/dynamic';
import type { SubmitBarProps } from './index'; import type { SubmitBarProps } from './index';
export const SubmitBarSSR = dynamic<SubmitBarProps>( export const SubmitBarSSR = dynamic<SubmitBarProps>(
() => import('./index').then(it => it.SubmitBar), () => import('./index').then((it) => it.SubmitBar),
{ ssr: false }); { ssr: false },
);

View file

@ -0,0 +1,28 @@
import classNames from 'classnames';
import { useResizeDetector } from 'react-resize-detector';
import styles from './styles.module.scss';
interface Props {
id: string;
title: string;
className?: string;
}
export const CommentVideoFrame = ({ id, title, className }: Props) => {
const { ref, width = 0, height = 0 } = useResizeDetector();
return (
<div className={classNames(styles.wrap, className)} ref={ref}>
<iframe
width={width}
height={height}
src={`https://www.youtube.com/embed/${id}?autoplay=1`}
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture"
frameBorder="0"
allowFullScreen
title={title}
/>
</div>
);
};

View file

@ -0,0 +1,8 @@
@import '~/styles/variables';
.wrap {
width: 100%;
aspect-ratio: calc(16 / 9);
overflow: hidden;
border-radius: $radius;
}

View file

@ -1,15 +1,21 @@
import { FC, memo, useMemo } from 'react'; import { FC, memo, useCallback, useMemo } from 'react';
import { Icon } from '~/components/common/Icon'; import { Icon } from '~/components/common/Icon';
import { ICommentBlockProps } from '~/constants/comment'; import { ICommentBlockProps } from '~/constants/comment';
import { useWindowSize } from '~/hooks/dom/useWindowSize';
import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata'; import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata';
import { getYoutubeThumb } from '~/utils/dom'; import { getYoutubeThumb } from '~/utils/dom';
import { useVideoPlayer } from '~/utils/providers/VideoPlayerProvider';
import { CommentVideoFrame } from './components/CommentVideoFrame';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
type Props = ICommentBlockProps & {}; type Props = ICommentBlockProps & {};
const CommentEmbedBlock: FC<Props> = memo(({ block }) => { const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
const { isTablet } = useWindowSize();
const { url, setUrl } = useVideoPlayer();
const id = useMemo(() => { const id = useMemo(() => {
const match = block.content.match( const match = block.content.match(
/https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/, /https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/,
@ -18,7 +24,7 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
return (match && match[1]) || ''; return (match && match[1]) || '';
}, [block.content]); }, [block.content]);
const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]); const address = `https://youtube.com/watch?v=${id}`;
const preview = useMemo( const preview = useMemo(
() => getYoutubeThumb(block.content), () => getYoutubeThumb(block.content),
@ -28,11 +34,35 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
const metadata = useYoutubeMetadata(id); const metadata = useYoutubeMetadata(id);
const title = metadata?.metadata?.title || ''; const title = metadata?.metadata?.title || '';
const onClick = useCallback(() => {
if (isTablet) {
window.open(address, '_blank');
return;
}
setUrl(address);
}, [isTablet, setUrl, address]);
const closeVideo = useCallback(() => setUrl(''), [setUrl]);
return ( return (
<div className={styles.embed}> <div className={styles.embed}>
<a href={url} target="_blank" rel="noreferrer" /> {url === address ? (
<div className={styles.video}>
<div className={styles.preview}> <div className={styles.close} onClick={closeVideo}>
<Icon icon="close" />
</div>
<div className={styles.animation}>
<CommentVideoFrame id={id} title={title} />
</div>
</div>
) : (
<div
className={styles.preview}
role="button"
onClick={onClick}
tabIndex={-1}
>
<div style={{ backgroundImage: `url("${preview}")` }}> <div style={{ backgroundImage: `url("${preview}")` }}>
<div className={styles.backdrop}> <div className={styles.backdrop}>
<div className={styles.play}> <div className={styles.play}>
@ -43,6 +73,7 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
</div> </div>
</div> </div>
</div> </div>
)}
</div> </div>
); );
}); });

View file

@ -1,8 +1,8 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
.embed { .embed {
padding: 0 $gap; padding: 0 0;
height: $comment_height; min-height: $comment_height;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
background: 50% 50% no-repeat; background: 50% 50% no-repeat;
@ -43,7 +43,7 @@
left: 0; left: 0;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: $content_bg_backdrop 50% 50%; background: $content_bg_backdrop;
background-size: cover; background-size: cover;
z-index: 15; z-index: 15;
border-radius: $radius; border-radius: $radius;
@ -69,6 +69,7 @@
justify-content: stretch; justify-content: stretch;
box-sizing: border-box; box-sizing: border-box;
z-index: 2; z-index: 2;
cursor: pointer;
& > div { & > div {
width: 100%; width: 100%;
@ -98,3 +99,47 @@
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
} }
@keyframes appear {
0% {
grid-template-columns: 0fr;
opacity: 0;
}
50% {
grid-template-columns: 1fr;
}
100% {
opacity: 1;
}
}
.video {
width: 100%;
position: relative;
padding: $gap / 2;
}
.close {
display: flex;
align-items: center;
justify-content: center;
background: var(--color_danger);
width: 64px;
height: 24px;
position: absolute;
bottom: calc(100% - #{$gap / 2});
right: 24px;
border-radius: $radius $radius 0 0;
z-index: 10;
cursor: pointer;
}
.animation {
background-color: var(--content_bg_darker);
display: grid;
animation: appear 0.5s forwards;
width: 100%;
border-radius: $radius;
}

View file

@ -7,7 +7,7 @@
&.multiple { &.multiple {
// Desktop devices // Desktop devices
@include flexbin(25vh, $flexbin-space); @include flexbin(300px, $flexbin-space);
// Tablet devices // Tablet devices
@media (max-width: $flexbin-tablet-max) { @media (max-width: $flexbin-tablet-max) {
@ -22,13 +22,16 @@
} }
.image { .image {
max-height: 500px; max-height: 300px;
border-radius: $radius; border-radius: $radius;
max-width: 100%; max-width: 100%;
.multiple & { .multiple & {
max-height: 250px; // both of that were 250px,
max-inline-size: 250px; // if you know why it should be like this, tell me
// it messes up with the flexbin above
max-height: 300px;
max-inline-size: 300px;
} }
} }

View file

@ -8,6 +8,7 @@ import { CommentWrapper } from '~/containers/comments/CommentWrapper';
import { IComment, ICommentGroup, IFile } from '~/types'; import { IComment, ICommentGroup, IFile } from '~/types';
import { CommendDeleted } from '../../../../../components/node/CommendDeleted'; import { CommendDeleted } from '../../../../../components/node/CommendDeleted';
import { getCommentId } from '../../../../../constants/dom/links';
import { CommentContent } from './components/CommentContent'; import { CommentContent } from './components/CommentContent';
import { CommentDistance } from './components/CommentDistance'; import { CommentDistance } from './components/CommentDistance';
@ -83,6 +84,9 @@ const Comment: FC<Props> = memo(
); );
return ( return (
<>
<a id={getCommentId(comment.id)} className={styles.anchor} />
<CommentContent <CommentContent
prefix={prefix} prefix={prefix}
saveComment={saveComment} saveComment={saveComment}
@ -95,6 +99,7 @@ const Comment: FC<Props> = memo(
onShowImageModal={onShowImageModal} onShowImageModal={onShowImageModal}
key={comment.id} key={comment.id}
/> />
</>
); );
})} })}
</div> </div>

View file

@ -15,3 +15,9 @@
.highlighted { .highlighted {
box-shadow: $color_primary 0 0 0px 2px; box-shadow: $color_primary 0 0 0px 2px;
} }
.anchor {
display: block;
position: relative;
top: -($header_height * 2);
}

View file

@ -1,9 +1,13 @@
import { FC, useMemo } from 'react'; import { FC, useEffect, useMemo } from 'react';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { LoadMoreButton } from '~/components/input/LoadMoreButton'; import { LoadMoreButton } from '~/components/input/LoadMoreButton';
import { ANNOUNCE_USER_ID, BORIS_NODE_ID } from '~/constants/boris/constants'; import { ANNOUNCE_USER_ID, BORIS_NODE_ID } from '~/constants/boris/constants';
import {
isCommentAnchor,
NEW_COMMENT_ANCHOR_NAME,
} from '~/constants/dom/links';
import { Comment } from '~/containers/node/NodeComments/components/Comment'; import { Comment } from '~/containers/node/NodeComments/components/Comment';
import { useGrouppedComments } from '~/hooks/node/useGrouppedComments'; import { useGrouppedComments } from '~/hooks/node/useGrouppedComments';
import { ICommentGroup } from '~/types'; import { ICommentGroup } from '~/types';
@ -11,6 +15,7 @@ import { useCommentContext } from '~/utils/context/CommentContextProvider';
import { useNodeContext } from '~/utils/context/NodeContextProvider'; import { useNodeContext } from '~/utils/context/NodeContextProvider';
import { useUserContext } from '~/utils/context/UserContextProvider'; import { useUserContext } from '~/utils/context/UserContextProvider';
import { canEditComment, canLikeComment } from '~/utils/node'; import { canEditComment, canLikeComment } from '~/utils/node';
import { VideoPlayerProvider } from '~/utils/providers/VideoPlayerProvider';
import styles from './styles.module.scss'; import styles from './styles.module.scss';
@ -18,6 +23,11 @@ interface Props {
order: 'ASC' | 'DESC'; order: 'ASC' | 'DESC';
} }
const isFirstGroupWithNewComment = (
group: ICommentGroup,
prevGroup: ICommentGroup | undefined,
) => group.hasNew && (!prevGroup || !prevGroup.hasNew);
const NodeComments: FC<Props> = observer(({ order }) => { const NodeComments: FC<Props> = observer(({ order }) => {
const user = useUserContext(); const user = useUserContext();
const { node } = useNodeContext(); const { node } = useNodeContext();
@ -35,7 +45,7 @@ const NodeComments: FC<Props> = observer(({ order }) => {
onSaveComment, onSaveComment,
} = useCommentContext(); } = useCommentContext();
const groupped: ICommentGroup[] = useGrouppedComments( const groupped = useGrouppedComments(
comments, comments,
order, order,
lastSeenCurrent ?? undefined, lastSeenCurrent ?? undefined,
@ -59,11 +69,35 @@ const NodeComments: FC<Props> = observer(({ order }) => {
return null; return null;
} }
/** Scrolls down to new comments or specific one from anchor */
useEffect(() => {
const anchor = location.hash?.replace('#', '');
if (!isLoading && isCommentAnchor(anchor)) {
setTimeout(
() =>
document
.getElementById(anchor)
?.scrollIntoView({ behavior: 'smooth' }),
300,
);
}
}, [isLoading]);
return ( return (
<VideoPlayerProvider>
<div className={styles.wrap}> <div className={styles.wrap}>
{order === 'DESC' && more} {order === 'DESC' && more}
{groupped.map((group) => ( {groupped.map((group, index) => (
<>
{isFirstGroupWithNewComment(group, groupped[index - 1]) && (
<a
id={NEW_COMMENT_ANCHOR_NAME}
className={styles.newCommentAnchor}
/>
)}
<Comment <Comment
nodeId={node.id!} nodeId={node.id!}
key={group.ids.join()} key={group.ids.join()}
@ -79,10 +113,12 @@ const NodeComments: FC<Props> = observer(({ order }) => {
isSame={group.user.id === user.id} isSame={group.user.id === user.id}
saveComment={onSaveComment} saveComment={onSaveComment}
/> />
</>
))} ))}
{order === 'ASC' && more} {order === 'ASC' && more}
</div> </div>
</VideoPlayerProvider>
); );
}); });

View file

@ -13,3 +13,9 @@
.more { .more {
margin-bottom: $gap; margin-bottom: $gap;
} }
.newCommentAnchor {
position: relative;
top: -($header_height * 2);
display: block;
}

View file

@ -1,6 +1,7 @@
import { FC } from 'react'; import { FC } from 'react';
import { Avatar } from '~/components/common/Avatar'; import { Avatar } from '~/components/common/Avatar';
import { Card } from '~/components/common/Card';
import { Placeholder } from '~/components/placeholders/Placeholder'; import { Placeholder } from '~/components/placeholders/Placeholder';
import { imagePresets } from '~/constants/urls'; import { imagePresets } from '~/constants/urls';
import { IUser } from '~/types/auth'; import { IUser } from '~/types/auth';
@ -11,11 +12,18 @@ interface Props {
profile: IUser; profile: IUser;
isLoading: boolean; isLoading: boolean;
username: string; username: string;
description: string;
} }
const ProfilePageLeft: FC<Props> = ({ username, profile, isLoading }) => { const ProfilePageLeft: FC<Props> = ({
username,
profile,
description,
isLoading,
}) => {
return ( return (
<div className={styles.wrap}> <Card className={styles.wrap} elevation={0} seamless>
<Card seamless>
<Avatar <Avatar
username={username} username={username}
url={profile?.photo?.url} url={profile?.photo?.url}
@ -32,7 +40,11 @@ const ProfilePageLeft: FC<Props> = ({ username, profile, isLoading }) => {
{isLoading ? <Placeholder /> : `~${profile?.username}`} {isLoading ? <Placeholder /> : `~${profile?.username}`}
</div> </div>
</div> </div>
</Card>
<div className={styles.region}>
<div className={styles.description}>{description}</div>
</div> </div>
</Card>
); );
}; };

View file

@ -1,29 +1,26 @@
@import 'src/styles/variables'; @import 'src/styles/variables';
.wrap { .wrap {
@include outer_shadow;
@include blur;
padding: $gap $gap $gap * 2;
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: stretch; align-items: stretch;
justify-content: stretch; justify-content: stretch;
flex-direction: column; flex-direction: column;
height: 100%;
border-radius: $radius; border-radius: $radius;
} }
.top {
padding: 0;
}
.avatar { .avatar {
width: 100%; width: 100%;
height: 0; height: 0;
padding-bottom: 100%; padding-bottom: 100%;
margin-bottom: $gap * 2;
} }
.region { .region {
width: 100%; padding: $gap;
text-align: center;
} }
.name { .name {
@ -44,8 +41,7 @@
.description { .description {
@include clamp(3, 21px * 3); @include clamp(3, 21px * 3);
line-height: 21px;
font: $font_14_regular; font: $font_14_regular;
margin-top: $gap * 3; line-height: 1.25em;
display: none; opacity: 0.5;
} }

View file

@ -28,7 +28,7 @@ const ThemeSwitcher: FC<ThemeSwitcherProps> = () => {
> >
<Group> <Group>
<div className={styles.palette}> <div className={styles.palette}>
{item.colors.map((color) => ( {[...item.colors].reverse().map((color) => (
<div <div
key={color} key={color}
className={styles.sample} className={styles.sample}

View file

@ -14,7 +14,7 @@ import type { SidebarComponentProps } from '~/types/sidebar';
import { isNil } from '~/utils/ramda'; import { isNil } from '~/utils/ramda';
const tabs = ['profile', 'notifications', 'bookmarks'] as const; const tabs = ['profile', 'notifications', 'bookmarks'] as const;
type TabName = typeof tabs[number]; type TabName = (typeof tabs)[number];
interface SettingsSidebarProps interface SettingsSidebarProps
extends SidebarComponentProps<SidebarName.Settings> { extends SidebarComponentProps<SidebarName.Settings> {

View file

@ -10,7 +10,7 @@ export const useLastSeenBoris = () => {
async (date: string) => { async (date: string) => {
await update({ last_seen_boris: date }, false); await update({ last_seen_boris: date }, false);
}, },
[update] [update],
); );
return { setLastSeen, lastSeen }; return { setLastSeen, lastSeen };

View file

@ -20,7 +20,7 @@ export const useLoginLogoutRestore = () => {
showToastInfo(getRandomPhrase('WELCOME')); showToastInfo(getRandomPhrase('WELCOME'));
return result.user; return result.user;
}, },
[auth] [auth],
); );
return { logout, login }; return { logout, login };

View file

@ -5,8 +5,9 @@ import { API } from '~/constants/api';
import { getErrorMessage } from '~/utils/errors/getErrorMessage'; import { getErrorMessage } from '~/utils/errors/getErrorMessage';
export const useRestoreCode = (code: string) => { export const useRestoreCode = (code: string) => {
const { data, isValidating, error } = useSWR(API.USER.REQUEST_CODE(code), () => const { data, isValidating, error } = useSWR(
apiCheckRestoreCode({ code }) API.USER.REQUEST_CODE(code),
() => apiCheckRestoreCode({ code }),
); );
const codeUser = data?.user; const codeUser = data?.user;

View file

@ -18,7 +18,7 @@ const validationSchema = object({
.test( .test(
'sameAsPassword', 'sameAsPassword',
'Должен совпадать с паролем', 'Должен совпадать с паролем',
(val, ctx) => val === ctx.parent.newPassword (val, ctx) => val === ctx.parent.newPassword,
), ),
}); });
@ -26,15 +26,21 @@ export type RestorePasswordData = Asserts<typeof validationSchema>;
export const useRestorePasswordForm = ( export const useRestorePasswordForm = (
code: string, code: string,
fetcher: (props: { code: string; password: string }) => Promise<{ token: string; user: IUser }>, fetcher: (props: {
onSuccess: () => void code: string;
password: string;
}) => Promise<{ token: string; user: IUser }>,
onSuccess: () => void,
) => { ) => {
const auth = useAuthStore(); const auth = useAuthStore();
const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>( const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>(
async (values, { setErrors }) => { async (values, { setErrors }) => {
try { try {
const { token, user } = await fetcher({ password: values.newPassword, code }); const { token, user } = await fetcher({
password: values.newPassword,
code,
});
auth.setUser(user); auth.setUser(user);
auth.setToken(token); auth.setToken(token);
onSuccess(); onSuccess();
@ -47,7 +53,7 @@ export const useRestorePasswordForm = (
} }
} }
}, },
[onSuccess, fetcher, code, auth] [onSuccess, fetcher, code, auth],
); );
return useFormik<RestorePasswordData>({ return useFormik<RestorePasswordData>({

View file

@ -15,7 +15,7 @@ type RestoreRequestData = Asserts<typeof validationSchema>;
export const useRestoreRequestForm = ( export const useRestoreRequestForm = (
fetcher: (field: string) => Promise<unknown>, fetcher: (field: string) => Promise<unknown>,
onSuccess: () => void onSuccess: () => void,
) => { ) => {
const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>( const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>(
async (values, { setErrors }) => { async (values, { setErrors }) => {
@ -31,7 +31,7 @@ export const useRestoreRequestForm = (
} }
} }
}, },
[fetcher, onSuccess] [fetcher, onSuccess],
); );
return useFormik({ return useFormik({

View file

@ -13,6 +13,6 @@ export const useSessionCookie = () => {
autorun(() => { autorun(() => {
setCookie('session', auth.token, 30); setCookie('session', auth.token, 30);
}), }),
[auth] [auth],
); );
}; };

View file

@ -9,9 +9,7 @@ import { showErrorToast } from '~/utils/errors/showToast';
const validationSchema = object({ const validationSchema = object({
username: string().required(ERRORS.REQUIRED), username: string().required(ERRORS.REQUIRED),
password: string() password: string().required(ERRORS.REQUIRED).min(6, ERRORS.PASSWORD_IS_SHORT),
.required(ERRORS.REQUIRED)
.min(6, ERRORS.PASSWORD_IS_SHORT),
}); });
type SocialRegisterData = Asserts<typeof validationSchema>; type SocialRegisterData = Asserts<typeof validationSchema>;
@ -23,7 +21,7 @@ export const useSocialRegisterForm = (
username: string; username: string;
password: string; password: string;
}) => Promise<{ token: string }>, }) => Promise<{ token: string }>,
onSuccess: (token: string) => void onSuccess: (token: string) => void,
) => { ) => {
const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>( const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>(
async (values, { setErrors }) => { async (values, { setErrors }) => {
@ -43,7 +41,7 @@ export const useSocialRegisterForm = (
} }
} }
}, },
[token, onSuccess, fetcher] [token, onSuccess, fetcher],
); );
return useFormik<SocialRegisterData>({ return useFormik<SocialRegisterData>({

View file

@ -7,7 +7,10 @@ const today = new Date();
export const useUserActiveStatus = (lastSeen?: string) => { export const useUserActiveStatus = (lastSeen?: string) => {
try { try {
const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined; const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined;
return lastSeenDate && differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS; return (
lastSeenDate &&
differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS
);
} catch (e) { } catch (e) {
return false; return false;
} }

View file

@ -6,12 +6,14 @@ import { initialBackendStats } from '~/constants/boris/constants';
import { BorisUsageStats } from '~/types/boris'; import { BorisUsageStats } from '~/types/boris';
export const useBorisStats = () => { export const useBorisStats = () => {
const { data: backend = initialBackendStats, isValidating: isValidatingBackend } = useSWR( const {
API.BORIS.GET_BACKEND_STATS, data: backend = initialBackendStats,
() => getBorisBackendStats() isValidating: isValidatingBackend,
); } = useSWR(API.BORIS.GET_BACKEND_STATS, () => getBorisBackendStats());
const { data: issues = [] } = useSWR(API.BORIS.GITHUB_ISSUES, () => getGithubIssues()); const { data: issues = [] } = useSWR(API.BORIS.GITHUB_ISSUES, () =>
getGithubIssues(),
);
const stats: BorisUsageStats = { const stats: BorisUsageStats = {
backend, backend,

View file

@ -3,9 +3,16 @@ import { useMemo } from 'react';
import { normalizeBrightColor } from '~/utils/color'; import { normalizeBrightColor } from '~/utils/color';
import { stringToColour } from '~/utils/dom'; import { stringToColour } from '~/utils/dom';
export const useColorFromString = (val?: string, saturation = 3, lightness = 3) => { export const useColorFromString = (
val?: string,
saturation = 3,
lightness = 3,
) => {
return useMemo( return useMemo(
() => (val && normalizeBrightColor(stringToColour(val), saturation, lightness)) || '', () =>
[lightness, saturation, val] (val &&
normalizeBrightColor(stringToColour(val), saturation, lightness)) ||
'',
[lightness, saturation, val],
); );
}; };

View file

@ -6,7 +6,7 @@ export const useColorGradientFromString = (
val?: string, val?: string,
saturation = 3, saturation = 3,
lightness = 3, lightness = 3,
angle = 155 angle = 155,
) => ) =>
useMemo(() => { useMemo(() => {
if (!val) { if (!val) {

View file

@ -1,6 +1,9 @@
import { useEffect, useMemo, useState } from 'react'; import { useEffect, useMemo, useState } from 'react';
export const usePersistedState = (key: string, initial: string): [string, (val: string) => any] => { export const usePersistedState = (
key: string,
initial: string,
): [string, (val: string) => any] => {
const stored = useMemo(() => { const stored = useMemo(() => {
try { try {
return localStorage.getItem(`vault_${key}`) || initial; return localStorage.getItem(`vault_${key}`) || initial;

View file

@ -4,15 +4,18 @@ export const useFocusEvent = (initialState = false, delay = 0) => {
const [focused, setFocused] = useState(initialState); const [focused, setFocused] = useState(initialState);
const onFocus = useCallback( const onFocus = useCallback(
event => { (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
setFocused(true); setFocused(true);
}, },
[setFocused] [setFocused],
);
const onBlur = useCallback(
() => setTimeout(() => setFocused(false), delay),
[delay],
); );
const onBlur = useCallback(() => setTimeout(() => setFocused(false), delay), [delay]);
return { focused, onBlur, onFocus }; return { focused, onBlur, onFocus };
}; };

View file

@ -7,12 +7,13 @@ export const useFormatWrapper = (onChange: (val: string) => void) => {
target: HTMLTextAreaElement, target: HTMLTextAreaElement,
prefix = '', prefix = '',
suffix = '' suffix = '',
) => event => { ) =>
(event) => {
event.preventDefault(); event.preventDefault();
wrapTextInsideInput(target, prefix, suffix, onChange); wrapTextInsideInput(target, prefix, suffix, onChange);
}, },
[onChange] [onChange],
); );
}; };
@ -21,7 +22,7 @@ export const wrapTextInsideInput = (
target: HTMLTextAreaElement, target: HTMLTextAreaElement,
prefix: string, prefix: string,
suffix: string, suffix: string,
onChange: (val: string) => void onChange: (val: string) => void,
) => { ) => {
if (!target) return; if (!target) return;
@ -34,7 +35,7 @@ export const wrapTextInsideInput = (
onChange( onChange(
target.value.substring(0, start) + target.value.substring(0, start) +
replacement + replacement +
target.value.substring(end, target.value.length) target.value.substring(end, target.value.length),
); );
target.focus(); target.focus();

View file

@ -2,7 +2,8 @@ import { useCallback, useEffect } from 'react';
export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => { export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => {
const onLoadMore = useCallback(() => { const onLoadMore = useCallback(() => {
const pos = window.scrollY + window.innerHeight - document.body.scrollHeight; const pos =
window.scrollY + window.innerHeight - document.body.scrollHeight;
if (isLoading || pos < -600) return; if (isLoading || pos < -600) return;

View file

@ -5,13 +5,13 @@ import { getImageFromPaste } from '~/utils/uploader';
// useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image // useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image
export const useInputPasteUpload = (onUpload: (files: File[]) => void) => { export const useInputPasteUpload = (onUpload: (files: File[]) => void) => {
return useCallback( return useCallback(
async event => { async (event) => {
const image = await getImageFromPaste(event); const image = await getImageFromPaste(event);
if (!image) return; if (!image) return;
onUpload([image]); onUpload([image]);
}, },
[onUpload] [onUpload],
); );
}; };

View file

@ -17,7 +17,11 @@ const sameWidth = {
}, },
}; };
export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean): Modifier<any>[] => export const usePopperModifiers = (
offsetX = 0,
offsetY = 10,
justify?: boolean,
): Modifier<any>[] =>
useMemo( useMemo(
() => () =>
[ [
@ -35,5 +39,5 @@ export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean)
}, },
...(justify ? [sameWidth] : []), ...(justify ? [sameWidth] : []),
] as Modifier<any>[], ] as Modifier<any>[],
[offsetX, offsetY, justify] [offsetX, offsetY, justify],
); );

View file

@ -11,7 +11,7 @@ const getHeight = () => {
body.offsetHeight, body.offsetHeight,
html.clientHeight, html.clientHeight,
html.scrollHeight, html.scrollHeight,
html.offsetHeight html.offsetHeight,
); );
}; };
export const useScrollHeight = () => getHeight(); export const useScrollHeight = () => getHeight();

View file

@ -18,6 +18,6 @@ export const useScrollToTop = (deps?: any[]) => {
}); });
}, },
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
deps && Array.isArray(deps) ? deps : [] deps && Array.isArray(deps) ? deps : [],
); );
}; };

View file

@ -1,7 +1,9 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
export const useScrollTop = () => { export const useScrollTop = () => {
const [top, setTop] = useState(typeof window !== 'undefined' ? window.scrollY : 0); const [top, setTop] = useState(
typeof window !== 'undefined' ? window.scrollY : 0,
);
useEffect(() => { useEffect(() => {
setTop(window.scrollY); setTop(window.scrollY);

View file

@ -6,11 +6,12 @@ export const useFlowCellControls = (
id: INode['id'], id: INode['id'],
description: string | undefined, description: string | undefined,
flow: FlowDisplay, flow: FlowDisplay,
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void,
) => { ) => {
const onChange = useCallback( const onChange = useCallback(
(value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }), (value: Partial<FlowDisplay>) =>
[flow, id, onChangeCellView] onChangeCellView(id, { ...flow, ...value }),
[flow, id, onChangeCellView],
); );
const hasDescription = !!description && description.length > 32; const hasDescription = !!description && description.length > 32;

View file

@ -17,6 +17,6 @@ export const useFlowSetCellView = () => {
showErrorToast(error); showErrorToast(error);
} }
}, },
[updateNode] [updateNode],
); );
}; };

View file

@ -21,15 +21,19 @@ export const useGetLabStats = () => {
heroes: lab.heroes, heroes: lab.heroes,
tags: lab.tags, tags: lab.tags,
}, },
onSuccess: data => { onSuccess: (data) => {
lab.setHeroes(data.heroes); lab.setHeroes(data.heroes);
lab.setTags(data.tags); lab.setTags(data.tags);
}, },
refreshInterval, refreshInterval,
} },
); );
const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR( const {
data: updatesData,
isValidating: isValidatingUpdates,
mutate: mutateUpdates,
} = useSWR(
isUser ? API.LAB.UPDATES : null, isUser ? API.LAB.UPDATES : null,
async () => { async () => {
const result = await getLabUpdates(); const result = await getLabUpdates();
@ -37,26 +41,27 @@ export const useGetLabStats = () => {
}, },
{ {
fallbackData: lab.updates, fallbackData: lab.updates,
onSuccess: data => { onSuccess: (data) => {
lab.setUpdates(data); lab.setUpdates(data);
}, },
refreshInterval, refreshInterval,
} },
); );
const heroes = useMemo(() => stats?.heroes || [], [stats]); const heroes = useMemo(() => stats?.heroes || [], [stats]);
const tags = useMemo(() => stats?.tags || [], [stats]); const tags = useMemo(() => stats?.tags || [], [stats]);
const updates = useMemo(() => updatesData || [], [updatesData]); const updates = useMemo(() => updatesData || [], [updatesData]);
const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates); const isLoading =
(!stats || !updates) && (isValidatingStats || isValidatingUpdates);
const seenNode = useCallback( const seenNode = useCallback(
async (nodeId: number) => { async (nodeId: number) => {
await mutateUpdates( await mutateUpdates(
updates.filter(it => it.id !== nodeId), updates.filter((it) => it.id !== nodeId),
false false,
); );
}, },
[mutateUpdates, updates] [mutateUpdates, updates],
); );
return { heroes, tags, updates, isLoading, seenNode }; return { heroes, tags, updates, isLoading, seenNode };

View file

@ -11,7 +11,7 @@ const getKey = (username: string): string | null => {
}; };
export const useMessages = (username: string) => { export const useMessages = (username: string) => {
const { data, isValidating } = useSWR(getKey(username), async () => const { data, isValidating } = useSWR(getKey(username), async () =>
apiGetUserMessages({ username }) apiGetUserMessages({ username }),
); );
const messages: IMessage[] = useMemo(() => data?.messages || [], [data]); const messages: IMessage[] = useMemo(() => data?.messages || [], [data]);

View file

@ -5,7 +5,9 @@ import { useModalStore } from '~/store/modal/useModalStore';
import { DialogComponentProps } from '~/types/modal'; import { DialogComponentProps } from '~/types/modal';
export type DialogContentProps = { export type DialogContentProps = {
[K in keyof typeof DIALOG_CONTENT]: typeof DIALOG_CONTENT[K] extends (props: infer U) => any [K in keyof typeof DIALOG_CONTENT]: (typeof DIALOG_CONTENT)[K] extends (
props: infer U,
) => any
? U extends DialogComponentProps ? U extends DialogComponentProps
? keyof Omit<U, 'onRequestClose' | 'children'> extends never ? keyof Omit<U, 'onRequestClose' | 'children'> extends never
? {} ? {}
@ -21,7 +23,7 @@ export const useModal = () => {
<T extends Dialog>(dialog: T, props: DialogContentProps[T]) => { <T extends Dialog>(dialog: T, props: DialogContentProps[T]) => {
setCurrent(dialog, props); setCurrent(dialog, props);
}, },
[setCurrent] [setCurrent],
); );
return { showModal, hideModal: hide, current, isOpened: !!current }; return { showModal, hideModal: hide, current, isOpened: !!current };

View file

@ -10,6 +10,6 @@ export const useShowModal = <T extends Dialog>(dialog: T) => {
(props: DialogContentProps[T]) => { (props: DialogContentProps[T]) => {
modal.showModal(dialog, props); modal.showModal(dialog, props);
}, },
[dialog, modal] [dialog, modal],
); );
}; };

View file

@ -11,6 +11,6 @@ export const useImageModal = () => {
(images: IFile[], index: number) => { (images: IFile[], index: number) => {
showModal({ items: images, index }); showModal({ items: images, index });
}, },
[showModal] [showModal],
); );
}; };

View file

@ -17,7 +17,7 @@ export const useNavigation = () => {
craHistory.push(url); craHistory.push(url);
} }
}, },
[craHistory, nextRouter] [craHistory, nextRouter],
); );
return { push }; return { push };

View file

@ -16,9 +16,13 @@ export const useCreateNode = () => {
if (node.is_promoted) { if (node.is_promoted) {
flow.setNodes([result.node, ...flow.nodes]); flow.setNodes([result.node, ...flow.nodes]);
} else { } else {
await lab.unshift({ node: result.node, comment_count: 0, last_seen: node.created_at }); await lab.unshift({
node: result.node,
comment_count: 0,
last_seen: node.created_at,
});
} }
}, },
[flow, lab] [flow, lab],
); );
}; };

View file

@ -6,13 +6,13 @@ import { groupCommentsByUser } from '~/utils/fn';
export const useGrouppedComments = ( export const useGrouppedComments = (
comments: IComment[], comments: IComment[],
order: 'ASC' | 'DESC', order: 'ASC' | 'DESC',
lastSeen?: string lastSeen?: string,
) => ) =>
useMemo( useMemo(
() => () =>
(order === 'DESC' ? [...comments].reverse() : comments).reduce( (order === 'DESC' ? [...comments].reverse() : comments).reduce(
groupCommentsByUser(lastSeen), groupCommentsByUser(lastSeen),
[] [],
), ),
[comments, lastSeen, order] [comments, lastSeen, order],
); );

View file

@ -6,7 +6,10 @@ import { useModal } from '~/hooks/modal/useModal';
import { INode } from '~/types'; import { INode } from '~/types';
import { showErrorToast } from '~/utils/errors/showToast'; import { showErrorToast } from '~/utils/errors/showToast';
export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Promise<unknown>) => { export const useNodeActions = (
node: INode,
update: (node: Partial<INode>) => Promise<unknown>,
) => {
const { showModal } = useModal(); const { showModal } = useModal();
const onLike = useCallback(async () => { const onLike = useCallback(async () => {
@ -35,17 +38,20 @@ export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Pr
const onLock = useCallback(async () => { const onLock = useCallback(async () => {
try { try {
const result = await apiLockNode({ id: node.id, is_locked: !node.deleted_at }); const result = await apiLockNode({
id: node.id,
is_locked: !node.deleted_at,
});
await update({ deleted_at: result.deleted_at }); await update({ deleted_at: result.deleted_at });
} catch (error) { } catch (error) {
showErrorToast(error); showErrorToast(error);
} }
}, [node.deleted_at, node.id, update]); }, [node.deleted_at, node.id, update]);
const onEdit = useCallback(() => showModal(Dialog.EditNode, { nodeId: node.id! }), [ const onEdit = useCallback(
node, () => showModal(Dialog.EditNode, { nodeId: node.id! }),
showModal, [node, showModal],
]); );
return { onLike, onStar, onLock, onEdit }; return { onLike, onStar, onLock, onEdit };
}; };

View file

@ -4,7 +4,8 @@ import { UploadType } from '~/constants/uploads';
import { INode } from '~/types'; import { INode } from '~/types';
export const useNodeAudios = (node: INode) => { export const useNodeAudios = (node: INode) => {
return useMemo(() => node.files.filter(file => file && file.type === UploadType.Audio), [ return useMemo(
node.files, () => node.files.filter((file) => file && file.type === UploadType.Audio),
]); [node.files],
);
}; };

View file

@ -1,6 +1,11 @@
import { useCallback, useRef } from 'react'; import { useCallback, useRef } from 'react';
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik'; import {
FormikConfig,
FormikHelpers,
useFormik,
useFormikContext,
} from 'formik';
import { object } from 'yup'; import { object } from 'yup';
import { INode } from '~/types'; import { INode } from '~/types';
@ -10,9 +15,9 @@ import { showErrorToast } from '~/utils/errors/showToast';
const validationSchema = object().shape({}); const validationSchema = object().shape({});
const afterSubmit = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) => ( const afterSubmit =
error?: unknown ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) =>
) => { (error?: unknown) => {
setSubmitting(false); setSubmitting(false);
if (error) { if (error) {
@ -28,13 +33,13 @@ const afterSubmit = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INod
if (resetForm) { if (resetForm) {
resetForm(); resetForm();
} }
}; };
export const useNodeFormFormik = ( export const useNodeFormFormik = (
values: INode, values: INode,
uploader: Uploader, uploader: Uploader,
stopEditing: () => void, stopEditing: () => void,
sendSaveRequest: (node: INode) => Promise<unknown> sendSaveRequest: (node: INode) => Promise<unknown>,
) => { ) => {
const { current: initialValues } = useRef(values); const { current: initialValues } = useRef(values);
@ -53,7 +58,7 @@ export const useNodeFormFormik = (
afterSubmit(helpers)(error); afterSubmit(helpers)(error);
} }
}, },
[sendSaveRequest, uploader.files] [sendSaveRequest, uploader.files],
); );
return useFormik<INode>({ return useFormik<INode>({

View file

@ -4,7 +4,8 @@ import { UploadType } from '~/constants/uploads';
import { INode } from '~/types'; import { INode } from '~/types';
export const useNodeImages = (node: INode) => { export const useNodeImages = (node: INode) => {
return useMemo(() => node.files.filter(file => file && file.type === UploadType.Image), [ return useMemo(
node.files, () => node.files.filter((file) => file && file.type === UploadType.Image),
]); [node.files],
);
}; };

View file

@ -27,6 +27,6 @@ export const useUpdateNode = (id: number) => {
await lab.updateNode(result.node.id!, result.node); await lab.updateNode(result.node.id!, result.node);
} }
}, },
[update, flow, lab] [update, flow, lab],
); );
}; };

View file

@ -20,7 +20,7 @@ export const useGetProfile = (username?: string) => {
}, },
{ {
refreshInterval: 60000, refreshInterval: 60000,
} },
); );
const profile = data || EMPTY_USER; const profile = data || EMPTY_USER;
@ -29,7 +29,7 @@ export const useGetProfile = (username?: string) => {
async (user: Partial<IUser>) => { async (user: Partial<IUser>) => {
await mutate({ ...profile, ...user }); await mutate({ ...profile, ...user });
}, },
[mutate, profile] [mutate, profile],
); );
return { profile, isLoading: !data && isValidating, update }; return { profile, isLoading: !data && isValidating, update };

View file

@ -9,10 +9,8 @@ import { flatten } from '~/utils/ramda';
const RESULTS_COUNT = 20; const RESULTS_COUNT = 20;
const getKey: (text: string) => SWRInfiniteKeyLoader = text => ( const getKey: (text: string) => SWRInfiniteKeyLoader =
pageIndex, (text) => (pageIndex, previousPageData: INode[]) => {
previousPageData: INode[]
) => {
if ((pageIndex > 0 && !previousPageData?.length) || !text) return null; if ((pageIndex > 0 && !previousPageData?.length) || !text) return null;
const props: GetSearchResultsRequest = { const props: GetSearchResultsRequest = {
@ -22,7 +20,7 @@ const getKey: (text: string) => SWRInfiniteKeyLoader = text => (
}; };
return JSON.stringify(props); return JSON.stringify(props);
}; };
export const useSearch = () => { export const useSearch = () => {
const [searchText, setSearchText] = useState(''); const [searchText, setSearchText] = useState('');
@ -40,7 +38,7 @@ export const useSearch = () => {
const result = await getSearchResults(props); const result = await getSearchResults(props);
return result.nodes; return result.nodes;
} },
); );
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]); const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);

View file

@ -26,5 +26,5 @@ export const useTagAutocomplete = (
}, },
); );
return useMemo(() => (search ? data ?? [] : []), [data, search]); return useMemo(() => (search ? (data ?? []) : []), [data, search]);
}; };

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