mirror of
https://github.com/muerwre/vault-frontend.git
synced 2025-04-24 20:36:40 +07:00
Compare commits
30 commits
55d17f944c
...
e8ed0c0466
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e8ed0c0466 | ||
![]() |
2a0adb26e0 | ||
![]() |
29c8bcd145 | ||
![]() |
4d55906ae8 | ||
![]() |
a676e98174 | ||
![]() |
f083b488ba | ||
![]() |
1281a3c595 | ||
![]() |
06cf7050a9 | ||
![]() |
5056047546 | ||
![]() |
521f5ce436 | ||
![]() |
606700f5d2 | ||
![]() |
5e71294e71 | ||
![]() |
bf1382af0b | ||
![]() |
4eb605a398 | ||
![]() |
16689ae3a6 | ||
![]() |
6f2715a9ae | ||
![]() |
9e79cba7bf | ||
![]() |
24c66ccfdb | ||
![]() |
b257e9b5d9 | ||
![]() |
69c61acc41 | ||
![]() |
7924c2bdd9 | ||
![]() |
f0606a894a | ||
![]() |
1d0ecc54a9 | ||
![]() |
42f8f96e34 | ||
![]() |
fd8907dd3a | ||
![]() |
71306d4c14 | ||
![]() |
032a246963 | ||
![]() |
ba0604ab9d | ||
![]() |
0e4d2bf44d | ||
![]() |
5ef19f49c5 |
133 changed files with 2232 additions and 845 deletions
|
@ -2,6 +2,8 @@
|
|||
node_modules
|
||||
out
|
||||
dist
|
||||
.husky
|
||||
.next
|
||||
.idea
|
||||
.history
|
||||
.vscode
|
||||
|
|
|
@ -11,10 +11,10 @@ steps:
|
|||
image: plugins/docker
|
||||
when:
|
||||
branch:
|
||||
- master
|
||||
- never
|
||||
environment:
|
||||
NEXT_PUBLIC_API_HOST: https://pig.vault48.org/
|
||||
NEXT_PUBLIC_REMOTE_CURRENT: https://pig.vault48.org/static/
|
||||
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
|
||||
settings:
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
# NEXT_PUBLIC_REMOTE_CURRENT=https://pig.staging.vault48.org/static/
|
||||
# NEXT_PUBLIC_API_HOST=http://localhost:7777/
|
||||
# NEXT_PUBLIC_REMOTE_CURRENT=http://localhost:7777/static/
|
||||
NEXT_PUBLIC_API_HOST=https://pig.vault48.org/
|
||||
NEXT_PUBLIC_REMOTE_CURRENT=https://pig.vault48.org/static/
|
||||
NEXT_PUBLIC_API_HOST=https://vault48.org/api/
|
||||
NEXT_PUBLIC_REMOTE_CURRENT=https://vault48.org/static/
|
||||
NEXT_PUBLIC_BOT_USERNAME=vault48testbot
|
20
.eslintrc.js
20
.eslintrc.js
|
@ -1,6 +1,7 @@
|
|||
module.exports = {
|
||||
extends: ['plugin:react/recommended', 'plugin:@next/next/recommended'],
|
||||
rules: {
|
||||
'prettier/prettier': 'error',
|
||||
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
|
||||
'react-hooks/exhaustive-deps': 'warn', // Checks effect dependencies
|
||||
'react/prop-types': 0,
|
||||
|
@ -9,13 +10,21 @@ module.exports = {
|
|||
'@next/next/no-img-element': 0,
|
||||
'unused-imports/no-unused-imports': 'warn',
|
||||
// 'no-unused-vars': 'warn',
|
||||
'quotes': [2, 'single', { 'avoidEscape': true }],
|
||||
quotes: [2, 'single', { avoidEscape: true }],
|
||||
'import/order': [
|
||||
'error',
|
||||
{
|
||||
alphabetize: { order: 'asc' },
|
||||
'newlines-between': 'always',
|
||||
groups: ['builtin', 'external', 'internal', 'parent', 'sibling', 'index', 'unknown'],
|
||||
groups: [
|
||||
'builtin',
|
||||
'external',
|
||||
'internal',
|
||||
'parent',
|
||||
'sibling',
|
||||
'index',
|
||||
'unknown',
|
||||
],
|
||||
pathGroups: [
|
||||
{
|
||||
pattern: 'react',
|
||||
|
@ -34,18 +43,17 @@ module.exports = {
|
|||
paths: [
|
||||
{
|
||||
name: 'ramda',
|
||||
message:
|
||||
'import from \'~/utils/ramda\' instead',
|
||||
message: "import from '~/utils/ramda' instead",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
],
|
||||
},
|
||||
parserOptions: {
|
||||
ecmaVersion: 7,
|
||||
sourceType: 'module',
|
||||
},
|
||||
plugins: ['import', 'react-hooks', 'unused-imports'],
|
||||
plugins: ['import', 'react-hooks', 'unused-imports', 'prettier'],
|
||||
parser: '@typescript-eslint/parser',
|
||||
settings: {
|
||||
react: {
|
||||
|
|
46
.forgejo/workflows/build.yml
Normal file
46
.forgejo/workflows/build.yml
Normal 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
|
51
docker/nextjs-standalone/Dockerfile
Normal file
51
docker/nextjs-standalone/Dockerfile
Normal 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"]
|
|
@ -2,44 +2,49 @@
|
|||
const withBundleAnalyzer = require('@next/bundle-analyzer')({
|
||||
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(
|
||||
withTM({
|
||||
output: 'standalone',
|
||||
/** rewrite old-style node paths */
|
||||
async rewrites() {
|
||||
return [
|
||||
{
|
||||
source: '/post:id',
|
||||
// everything except 'post' is for backwards compatibility here
|
||||
source: '/(post|photo|blog|song|video|cell):id',
|
||||
destination: '/node/:id',
|
||||
},
|
||||
{
|
||||
source: '/~:username',
|
||||
destination: '/profile/:username',
|
||||
}
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
/** don't try to optimize fonts */
|
||||
optimizeFonts: false,
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.vault48.org',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.ytimg.com',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: 'vault48.org',
|
||||
pathname: '/static/**',
|
||||
},
|
||||
{
|
||||
protocol: 'https',
|
||||
hostname: '*.ytimg.com',
|
||||
pathname: '/**',
|
||||
},
|
||||
{
|
||||
protocol: 'http',
|
||||
hostname: 'localhost',
|
||||
pathname: '/**',
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
"mobx-persist-store": "^1.0.4",
|
||||
"mobx-react-lite": "^3.2.3",
|
||||
"next": "^12.3.0",
|
||||
"photoswipe": "^4.1.3",
|
||||
"photoswipe": "^5.4.4",
|
||||
"raleway-cyrillic": "^4.0.2",
|
||||
"ramda": "^0.26.1",
|
||||
"react": "^17.0.2",
|
||||
|
@ -36,12 +36,13 @@
|
|||
"react-lazyload": "^3.2.0",
|
||||
"react-masonry-css": "^1.0.16",
|
||||
"react-popper": "^2.2.3",
|
||||
"react-resize-detector": "^12.0.2",
|
||||
"react-router": "^5.1.2",
|
||||
"react-router-dom": "^5.1.2",
|
||||
"react-sticky-box": "^1.0.2",
|
||||
"sass": "^1.49.0",
|
||||
"sharp": "^0.32.6",
|
||||
"swiper": "^11.0.3",
|
||||
"swiper": "^11.2.2",
|
||||
"swr": "^1.0.1",
|
||||
"throttle-debounce": "^2.1.0",
|
||||
"typescript": "^4.0.5",
|
||||
|
@ -92,13 +93,14 @@
|
|||
"@typescript-eslint/parser": "^5.10.1",
|
||||
"eslint": "^7.32.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-prettier": "^5.2.3",
|
||||
"eslint-plugin-react": "^7.28.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-unused-imports": "^3.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"lint-staged": "^12.1.6",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"prettier": "^2.7.1"
|
||||
"prettier": "^3.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"./**/*.{js,jsx,ts,tsx}": [
|
||||
|
|
752
public/images/sansivieria.svg
Normal file
752
public/images/sansivieria.svg
Normal 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 |
|
@ -14,7 +14,7 @@ interface Props extends DivProps {
|
|||
username?: string;
|
||||
size?: number;
|
||||
hasUpdates?: boolean;
|
||||
preset?: typeof imagePresets[keyof typeof imagePresets];
|
||||
preset?: (typeof imagePresets)[keyof typeof imagePresets];
|
||||
}
|
||||
|
||||
const Avatar = forwardRef<HTMLDivElement, Props>(
|
||||
|
|
|
@ -31,7 +31,7 @@ const Columns: FC<ColumnsProps> = ({
|
|||
|
||||
if (!childs) return;
|
||||
|
||||
const timeout = setTimeout(() => setColumns([...childs]), 150);
|
||||
const timeout = setTimeout(() => setColumns([...childs.values()]), 150);
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
|
|
|
@ -8,6 +8,8 @@ import { URLS } from '~/constants/urls';
|
|||
import { INode } from '~/types';
|
||||
import { getPrettyDate } from '~/utils/dom';
|
||||
|
||||
import { getNewCommentAnchor } from '../../../constants/dom/links';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface Props {
|
||||
|
@ -16,32 +18,34 @@ interface Props {
|
|||
onClick?: MouseEventHandler;
|
||||
}
|
||||
|
||||
const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => {
|
||||
return (
|
||||
<Anchor
|
||||
key={node.id}
|
||||
className={styles.item}
|
||||
href={URLS.NODE_URL(node.id)}
|
||||
onClick={onClick}
|
||||
const NodeHorizontalCard: FC<Props> = ({ node, hasNew, onClick }) => (
|
||||
<Anchor
|
||||
key={node.id}
|
||||
className={styles.item}
|
||||
href={
|
||||
hasNew
|
||||
? getNewCommentAnchor(URLS.NODE_URL(node.id))
|
||||
: URLS.NODE_URL(node.id)
|
||||
}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.thumb, {
|
||||
[styles.new]: hasNew,
|
||||
[styles.lab]: !node.is_promoted,
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={classNames(styles.thumb, {
|
||||
[styles.new]: hasNew,
|
||||
[styles.lab]: !node.is_promoted,
|
||||
})}
|
||||
>
|
||||
<NodeThumbnail item={node} />
|
||||
</div>
|
||||
<NodeThumbnail item={node} />
|
||||
</div>
|
||||
|
||||
<div className={styles.info}>
|
||||
<div className={styles.title}>{node.title || '...'}</div>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.title}>{node.title || '...'}</div>
|
||||
|
||||
<div className={styles.comment}>
|
||||
<span>{getPrettyDate(node.created_at)}</span>
|
||||
</div>
|
||||
<div className={styles.comment}>
|
||||
<span>{getPrettyDate(node.created_at)}</span>
|
||||
</div>
|
||||
</Anchor>
|
||||
);
|
||||
};
|
||||
</div>
|
||||
</Anchor>
|
||||
);
|
||||
|
||||
export { NodeHorizontalCard };
|
||||
|
|
|
@ -27,8 +27,8 @@
|
|||
&.new {
|
||||
&::after {
|
||||
content: ' ';
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 100%;
|
||||
background: $color_danger;
|
||||
box-shadow: $content_bg 0 0 0 5px;
|
||||
|
|
|
@ -13,9 +13,11 @@ interface Props extends DivProps {
|
|||
|
||||
const SubTitle: FC<Props> = ({ isLoading, children, ...rest }) => (
|
||||
<div {...rest} className={classNames(styles.title, rest.className)}>
|
||||
<Placeholder active={isLoading} loading>
|
||||
{children}
|
||||
</Placeholder>
|
||||
<span className={styles.name}>
|
||||
<Placeholder active={isLoading} loading>
|
||||
{children}
|
||||
</Placeholder>
|
||||
</span>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
|
|
@ -1,7 +1,25 @@
|
|||
@import "src/styles/variables.scss";
|
||||
@import 'src/styles/variables.scss';
|
||||
|
||||
.title {
|
||||
font: $font_12_semibold;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
background: none;
|
||||
padding: 0 $gap 0 $gap;
|
||||
font: $font_14_semibold;
|
||||
border-radius: $radius;
|
||||
border-radius: $input_radius;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
|
||||
background: $input_bg_color;
|
||||
min-height: $input_height;
|
||||
border-radius: $radius;
|
||||
border-radius: $input_radius;
|
||||
position: relative;
|
||||
color: $input_text_color;
|
||||
font: $input_font;
|
||||
|
|
|
@ -57,7 +57,9 @@ const NodeImageSwiperBlock: FC<Props> = observer(({ node }) => {
|
|||
|
||||
useEffect(() => {
|
||||
controlledSwiper?.slideTo(0, 0);
|
||||
return () => controlledSwiper?.slideTo(0, 0);
|
||||
return () => {
|
||||
controlledSwiper?.slideTo(0, 0);
|
||||
};
|
||||
}, [controlledSwiper, images, node.id]);
|
||||
|
||||
useEffect(() => {
|
||||
|
|
|
@ -29,11 +29,6 @@
|
|||
|
||||
.title {
|
||||
padding-left: 5px;
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.text {
|
||||
|
|
|
@ -9,6 +9,8 @@ import { Square } from '~/components/common/Square';
|
|||
import { NotificationItem } from '~/types/notifications';
|
||||
import { formatText, getURLFromString } from '~/utils/dom';
|
||||
|
||||
import { getCommentAnchor } from '../../../constants/dom/links';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
interface NotificationCommentProps {
|
||||
|
@ -17,7 +19,10 @@ interface NotificationCommentProps {
|
|||
}
|
||||
|
||||
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={styles.icon}>
|
||||
<Avatar
|
||||
|
|
|
@ -7,11 +7,11 @@ export const API = {
|
|||
USER: {
|
||||
LOGIN: '/auth',
|
||||
OAUTH_WINDOW: (provider: OAuthProvider) =>
|
||||
`${CONFIG.apiHost}oauth/${provider}/redirect`,
|
||||
`${CONFIG.apiHost}oauth/${provider}/redirect/`,
|
||||
ME: '/auth',
|
||||
UPDATE_PHOTO: '/auth/photo',
|
||||
UPDATE_COVER: '/auth/photo',
|
||||
PROFILE: (username: string) => `/users/${username}/profile`,
|
||||
PROFILE: (username: string) => `/users/${username}`,
|
||||
MESSAGES: (username: string) => `/users/${username}/messages`,
|
||||
MESSAGE_SEND: (username: string) => `/users/${username}/messages`,
|
||||
MESSAGE_DELETE: (username: string, id: number) =>
|
||||
|
|
|
@ -20,7 +20,7 @@ export const COMMENT_BLOCK_DETECTORS = [
|
|||
];
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
|
|
|
@ -5,3 +5,5 @@ export const isTablet = () => {
|
|||
|
||||
return window.innerWidth < 599;
|
||||
};
|
||||
|
||||
export const headerHeight = 64; // px
|
||||
|
|
15
src/constants/dom/links.ts
Normal file
15
src/constants/dom/links.ts
Normal 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);
|
|
@ -1,3 +1,5 @@
|
|||
import { lazy } from 'react';
|
||||
|
||||
import { LoginDialog } from '~/containers/auth/LoginDialog';
|
||||
import { LoginSocialRegisterDialog } from '~/containers/auth/LoginSocialRegisterDialog';
|
||||
import { RestorePasswordDialog } from '~/containers/auth/RestorePasswordDialog';
|
||||
|
@ -6,9 +8,14 @@ import { TelegramAttachDialog } from '~/containers/auth/TelegramAttachDialog';
|
|||
import { EditorCreateDialog } from '~/containers/dialogs/EditorCreateDialog';
|
||||
import { EditorEditDialog } from '~/containers/dialogs/EditorEditDialog';
|
||||
import { LoadingDialog } from '~/containers/dialogs/LoadingDialog';
|
||||
import { PhotoSwipe } from '~/containers/dialogs/PhotoSwipe';
|
||||
import { TestDialog } from '~/containers/dialogs/TestDialog';
|
||||
|
||||
const PhotoSwipe = lazy(() =>
|
||||
import('~/containers/dialogs/PhotoSwipe').then((it) => ({
|
||||
default: it.PhotoSwipe,
|
||||
})),
|
||||
);
|
||||
|
||||
export enum Dialog {
|
||||
Login = 'Login',
|
||||
Register = 'Register',
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
|
||||
export enum SidebarName {
|
||||
Settings = 'settings',
|
||||
Tag = 'tag',
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
export enum Theme {
|
||||
Default = 'Default',
|
||||
Horizon = 'Horizon',
|
||||
Sansevieria = 'Sansevieria',
|
||||
}
|
||||
|
||||
interface ThemeColors {
|
||||
|
@ -17,7 +18,7 @@ export const themeColors: Record<Theme, ThemeColors> = {
|
|||
'linear-gradient(165deg, #ff7549 -50%, #ff3344 150%)',
|
||||
'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]: {
|
||||
name: 'Веспера',
|
||||
|
@ -28,4 +29,13 @@ export const themeColors: Record<Theme, ThemeColors> = {
|
|||
],
|
||||
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',
|
||||
},
|
||||
};
|
||||
|
|
|
@ -37,7 +37,7 @@ export const imagePresets = {
|
|||
flow_horizontal: 'flow_horizontal',
|
||||
} 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>> = {
|
||||
[imagePresets[1600]]: 1600,
|
||||
|
@ -49,7 +49,7 @@ export const imageSrcSets: Partial<Record<ImagePreset, number>> = {
|
|||
|
||||
export const flowDisplayToPreset: Record<
|
||||
FlowDisplayVariant,
|
||||
typeof imagePresets[keyof typeof imagePresets]
|
||||
(typeof imagePresets)[keyof typeof imagePresets]
|
||||
> = {
|
||||
single: 'flow_square',
|
||||
quadro: 'flow_square',
|
||||
|
|
|
@ -3,10 +3,12 @@ import dynamic from 'next/dynamic';
|
|||
import type { BorisSuperpowersProps } from './index';
|
||||
|
||||
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,
|
||||
loading: () => <div />,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
|
@ -8,7 +8,7 @@ import { DialogComponentProps } from '~/types/modal';
|
|||
import { values } from '~/utils/ramda';
|
||||
|
||||
export interface EditorCreateDialogProps extends DialogComponentProps {
|
||||
type: typeof NODE_TYPES[keyof typeof NODE_TYPES];
|
||||
type: (typeof NODE_TYPES)[keyof typeof NODE_TYPES];
|
||||
isInLab: boolean;
|
||||
}
|
||||
|
||||
|
|
|
@ -11,7 +11,7 @@ import { TextEditor } from '../components/TextEditor';
|
|||
import { VideoEditor } from '../components/VideoEditor';
|
||||
|
||||
export const NODE_EDITORS: Record<
|
||||
typeof NODE_TYPES[keyof typeof NODE_TYPES],
|
||||
(typeof NODE_TYPES)[keyof typeof NODE_TYPES],
|
||||
FC<NodeEditorProps>
|
||||
> = {
|
||||
[NODE_TYPES.IMAGE]: ImageEditor,
|
||||
|
@ -22,7 +22,7 @@ export const NODE_EDITORS: Record<
|
|||
};
|
||||
|
||||
export const NODE_EDITOR_DATA: Record<
|
||||
typeof NODE_TYPES[keyof typeof NODE_TYPES],
|
||||
(typeof NODE_TYPES)[keyof typeof NODE_TYPES],
|
||||
Partial<INode>
|
||||
> = {
|
||||
[NODE_TYPES.TEXT]: {
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
import { FC, createElement } from 'react';
|
||||
import { FC, createElement, Suspense } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { LoaderCircle } from '~/components/common/LoaderCircle';
|
||||
import { ModalWrapper } from '~/components/common/ModalWrapper';
|
||||
import { DIALOG_CONTENT } from '~/constants/modal';
|
||||
import { useModalStore } from '~/store/modal/useModalStore';
|
||||
|
@ -18,10 +19,12 @@ const Modal: FC<Props> = observer(() => {
|
|||
|
||||
return (
|
||||
<ModalWrapper onOverlayClick={hide}>
|
||||
{createElement(DIALOG_CONTENT[current!]! as any, {
|
||||
onRequestClose: hide,
|
||||
...props,
|
||||
})}
|
||||
<Suspense fallback={<LoaderCircle />}>
|
||||
{createElement(DIALOG_CONTENT[current!]! as any, {
|
||||
onRequestClose: hide,
|
||||
...props,
|
||||
})}
|
||||
</Suspense>
|
||||
</ModalWrapper>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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 PhotoSwipeUI_Default from 'photoswipe/dist/photoswipe-ui-default.js';
|
||||
import PhotoSwipeJs from 'photoswipe/dist/photoswipe.js';
|
||||
import PSWP from 'photoswipe';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
|
||||
import { Icon } from '~/components/common/Icon';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||
import { useModal } from '~/hooks/modal/useModal';
|
||||
|
@ -13,125 +15,55 @@ import { DialogComponentProps } from '~/types/modal';
|
|||
import { getURL } from '~/utils/dom';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export interface PhotoSwipeProps extends DialogComponentProps {
|
||||
export interface Props extends DialogComponentProps {
|
||||
items: IFile[];
|
||||
index: number;
|
||||
}
|
||||
|
||||
const PhotoSwipe: VFC<PhotoSwipeProps> = observer(({ index, items }) => {
|
||||
let ref = useRef<HTMLDivElement>(null);
|
||||
const arrowNextSVG = renderToStaticMarkup(<Icon icon="right" size={40} />);
|
||||
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 { isTablet } = useWindowSize();
|
||||
const pswp = useRef(new PSWP());
|
||||
|
||||
useEffect(() => {
|
||||
new Promise(async (resolve) => {
|
||||
const images = await Promise.all(
|
||||
items.map(
|
||||
(file) =>
|
||||
new Promise((resolve) => {
|
||||
const src = getURL(
|
||||
file,
|
||||
isTablet ? imagePresets[900] : imagePresets[1600],
|
||||
);
|
||||
const dataSource = items.map((file) => ({
|
||||
src: getURL(file, imagePresets[1600]),
|
||||
width: file.metadata?.width,
|
||||
height: file.metadata?.height,
|
||||
}));
|
||||
|
||||
if (file.metadata?.width && file.metadata.height) {
|
||||
resolve({
|
||||
src,
|
||||
w: file.metadata.width,
|
||||
h: file.metadata.height,
|
||||
});
|
||||
pswp.current.options = {
|
||||
...pswp.current.options,
|
||||
dataSource,
|
||||
index: index || 0,
|
||||
closeOnVerticalDrag: true,
|
||||
padding,
|
||||
mainClass: styles.wrap,
|
||||
zoom: false,
|
||||
counter: false,
|
||||
bgOpacity: 0.1,
|
||||
arrowNextSVG,
|
||||
arrowPrevSVG,
|
||||
closeSVG,
|
||||
};
|
||||
|
||||
return;
|
||||
}
|
||||
pswp.current.on('closingAnimationEnd', hideModal);
|
||||
pswp.current.init();
|
||||
|
||||
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,
|
||||
closeOnScroll: false,
|
||||
history: false,
|
||||
});
|
||||
|
||||
ps.init();
|
||||
ps.listen('destroy', hideModal);
|
||||
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]);
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
return null;
|
||||
});
|
||||
|
||||
export { PhotoSwipe };
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
@import "src/styles/variables";
|
||||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
:global(.pswp__img) {
|
||||
|
|
|
@ -23,6 +23,7 @@ interface Props {
|
|||
|
||||
const FlowCellMenu: FC<Props> = ({
|
||||
onClose,
|
||||
currentView,
|
||||
hasDescription,
|
||||
toggleViewDescription,
|
||||
descriptionEnabled,
|
||||
|
@ -59,7 +60,7 @@ const FlowCellMenu: FC<Props> = ({
|
|||
/>
|
||||
</div>
|
||||
|
||||
{hasDescription && (
|
||||
{hasDescription && currentView !== 'single' && (
|
||||
<Group
|
||||
className={styles.description}
|
||||
horizontal
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { FC, ReactElement } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
import { transparentize, darken, desaturate, getLuminance } from 'color2k';
|
||||
|
||||
import { Markdown } from '~/components/common/Markdown';
|
||||
import { formatText } from '~/utils/dom';
|
||||
|
@ -11,13 +12,29 @@ import styles from './styles.module.scss';
|
|||
interface Props extends DivProps {
|
||||
children: string;
|
||||
heading: string | ReactElement;
|
||||
color?: string;
|
||||
}
|
||||
|
||||
const FlowCellText: FC<Props> = ({ children, heading, ...rest }) => (
|
||||
<div {...rest} className={classNames(styles.text, rest.className)}>
|
||||
{heading && <div className={styles.heading}>{heading}</div>}
|
||||
<Markdown className={styles.description}>{formatText(children)}</Markdown>
|
||||
</div>
|
||||
);
|
||||
const FlowCellText: FC<Props> = ({ children, heading, color, ...rest }) => {
|
||||
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>}
|
||||
<Markdown className={styles.description}>{formatText(children)}</Markdown>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { FlowCellText };
|
||||
|
|
|
@ -1,10 +1,24 @@
|
|||
@import "src/styles/variables";
|
||||
@import 'src/styles/variables';
|
||||
|
||||
.text {
|
||||
padding: $gap;
|
||||
@include blur;
|
||||
|
||||
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 {
|
||||
margin-bottom: 0.4em;
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { FC, useMemo } from 'react';
|
||||
import { FC, useCallback, useMemo } from 'react';
|
||||
|
||||
import classNames from 'classnames';
|
||||
|
||||
|
@ -9,6 +9,8 @@ import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
|||
import { useFlowCellControls } from '~/hooks/flow/useFlowCellControls';
|
||||
import { FlowDisplay, INode } from '~/types';
|
||||
|
||||
import { isFullyVisible } from '../../../../../utils/dom';
|
||||
|
||||
import { CellShade } from './components/CellShade';
|
||||
import { FlowCellImage } from './components/FlowCellImage';
|
||||
import { FlowCellMenu } from './components/FlowCellMenu';
|
||||
|
@ -25,7 +27,7 @@ interface Props {
|
|||
text?: string;
|
||||
flow: FlowDisplay;
|
||||
canEdit?: boolean;
|
||||
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void;
|
||||
onChange: (id: INode['id'], flow: FlowDisplay) => void;
|
||||
}
|
||||
|
||||
const FlowCell: FC<Props> = ({
|
||||
|
@ -37,7 +39,7 @@ const FlowCell: FC<Props> = ({
|
|||
text,
|
||||
title,
|
||||
canEdit = false,
|
||||
onChangeCellView,
|
||||
onChange,
|
||||
}) => {
|
||||
const { isTablet } = useWindowSize();
|
||||
|
||||
|
@ -45,6 +47,30 @@ const FlowCell: FC<Props> = ({
|
|||
((!!flow.display && flow.display !== 'single') || !image) &&
|
||||
flow.show_description &&
|
||||
!!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 {
|
||||
hasDescription,
|
||||
setViewHorizontal,
|
||||
|
@ -52,13 +78,7 @@ const FlowCell: FC<Props> = ({
|
|||
setViewQuadro,
|
||||
setViewSingle,
|
||||
toggleViewDescription,
|
||||
} = useFlowCellControls(id, text, flow, onChangeCellView);
|
||||
const {
|
||||
isActive: isMenuActive,
|
||||
activate,
|
||||
ref,
|
||||
deactivate,
|
||||
} = useClickOutsideFocus();
|
||||
} = useFlowCellControls(id, text, flow, onChangeWithScroll);
|
||||
|
||||
const shadeSize = useMemo(() => {
|
||||
const min = isTablet ? 10 : 15;
|
||||
|
@ -111,8 +131,9 @@ const FlowCell: FC<Props> = ({
|
|||
<FlowCellText
|
||||
className={styles.text}
|
||||
heading={<h4 className={styles.title}>{title}</h4>}
|
||||
color={color}
|
||||
>
|
||||
{text!}
|
||||
{text}
|
||||
</FlowCellText>
|
||||
)}
|
||||
|
||||
|
@ -124,7 +145,7 @@ const FlowCell: FC<Props> = ({
|
|||
/>
|
||||
)}
|
||||
|
||||
{!!title && (
|
||||
{!!title && !withText && (
|
||||
<CellShade
|
||||
color={color}
|
||||
className={styles.shade}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
$compact_size: 200px;
|
||||
|
||||
.cell {
|
||||
@include inner_shadow;
|
||||
|
||||
|
@ -9,6 +11,7 @@
|
|||
width: 100%;
|
||||
height: 100%;
|
||||
background: $content_bg;
|
||||
container: cell / inline-size;
|
||||
}
|
||||
|
||||
.thumb {
|
||||
|
@ -33,20 +36,17 @@
|
|||
|
||||
.text {
|
||||
position: absolute;
|
||||
bottom: 5px;
|
||||
left: 5px;
|
||||
z-index: 1;
|
||||
overflow: hidden;
|
||||
border-radius: $radius;
|
||||
max-height: calc(100% - 10px);
|
||||
max-width: calc(100% - 10px);
|
||||
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 {
|
||||
font: $font_14_regular;
|
||||
left: 5px;
|
||||
bottom: 5px;
|
||||
@container (max-width: $compact_size) {
|
||||
padding: $gap / 2 $gap 0 $gap;
|
||||
}
|
||||
|
||||
& :global(.grey) {
|
||||
|
@ -54,14 +54,34 @@
|
|||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.quadro &,
|
||||
.horizontal & {
|
||||
max-width: calc(50% - 15px);
|
||||
@container (max-width: #{$compact_size}) {
|
||||
padding: $gap / 2 $gap 0 $gap;
|
||||
}
|
||||
|
||||
.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 & {
|
||||
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 {
|
||||
font: $font_cell_title;
|
||||
line-height: 1.2em;
|
||||
text-transform: uppercase;
|
||||
word-break: break-word;
|
||||
color: inherit;
|
||||
margin-bottom: -0.125em;
|
||||
|
||||
@include tablet {
|
||||
font: $font_18_semibold;
|
||||
@container (max-width: #{$compact_size}) {
|
||||
font: $font_cell_title_compact;
|
||||
}
|
||||
|
||||
.horizontal &,
|
||||
.quadro & {
|
||||
@container (max-width: #{$compact_size * 2}) {
|
||||
font: $font_cell_title_compact;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -107,12 +137,7 @@
|
|||
}
|
||||
|
||||
.display_modal {
|
||||
@include appear;
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
inset: 0;
|
||||
z-index: 11;
|
||||
}
|
||||
|
|
|
@ -46,7 +46,7 @@ export const FlowGrid: FC<Props> = observer(
|
|||
text={node.description}
|
||||
title={node.title}
|
||||
canEdit={fetched && isUser && canEditNode(node, user)}
|
||||
onChangeCellView={onChangeCellView}
|
||||
onChange={onChangeCellView}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
@mixin mobile {
|
||||
@media (max-width: $cell * 2) {
|
||||
@content;
|
||||
}
|
||||
}
|
||||
|
||||
.cell {
|
||||
&.horizontal,
|
||||
&.quadro {
|
||||
|
|
|
@ -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 };
|
|
@ -1,11 +0,0 @@
|
|||
@import "src/styles/variables";
|
||||
|
||||
.recent {
|
||||
@media (max-width: $flow_hide_recents) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.updates {
|
||||
|
||||
}
|
|
@ -2,17 +2,15 @@ import { FC, FormEvent, useCallback, useMemo } from 'react';
|
|||
|
||||
import classNames from 'classnames';
|
||||
|
||||
import { Group } from '~/components/common/Group';
|
||||
import { Card } from '~/components/common/Card';
|
||||
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 { Toggle } from '~/components/input/Toggle';
|
||||
import { experimentalFeatures } from '~/constants/features';
|
||||
import styles from '~/containers/flow/FlowStamp/styles.module.scss';
|
||||
import { useFlowContext } from '~/utils/providers/FlowProvider';
|
||||
import { useSearchContext } from '~/utils/providers/SearchProvider';
|
||||
|
||||
import { FlowRecent } from './components/FlowRecent';
|
||||
import { FlowSearchResults } from './components/FlowSearchResults';
|
||||
|
||||
interface Props {
|
||||
|
@ -64,60 +62,55 @@ const FlowStamp: FC<Props> = ({ isFluid, onToggleLayout }) => {
|
|||
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<form className={styles.search} onSubmit={onSearchSubmit}>
|
||||
<InputText
|
||||
title="Поиск"
|
||||
value={searchText}
|
||||
handler={setSearchText}
|
||||
suffix={after}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</form>
|
||||
<Card className={styles.search}>
|
||||
<form onSubmit={onSearchSubmit}>
|
||||
<InputText
|
||||
title="Поиск"
|
||||
value={searchText}
|
||||
handler={setSearchText}
|
||||
suffix={after}
|
||||
onKeyUp={onKeyUp}
|
||||
/>
|
||||
</form>
|
||||
</Card>
|
||||
|
||||
{searchText ? (
|
||||
<div className={styles.search_results}>
|
||||
<div className={styles.grid}>
|
||||
<div className={styles.label}>
|
||||
<span className={styles.label_text}>Результаты поиска</span>
|
||||
<span className="line" />
|
||||
</div>
|
||||
<Card className={styles.grid}>
|
||||
<SubTitle>Результаты поиска</SubTitle>
|
||||
|
||||
<div className={styles.items}>
|
||||
<FlowSearchResults
|
||||
hasMore={searchHasMore}
|
||||
isLoading={searchIsLoading}
|
||||
results={searchResults}
|
||||
onLoadMore={onSearchLoadMore}
|
||||
/>
|
||||
</div>
|
||||
<div className={classNames(styles.items, styles.scrollable)}>
|
||||
<FlowSearchResults
|
||||
hasMore={searchHasMore}
|
||||
isLoading={searchIsLoading}
|
||||
results={searchResults}
|
||||
onLoadMore={onSearchLoadMore}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<div className={styles.grid}>
|
||||
<div className={classNames(styles.label, styles.whatsnew)}>
|
||||
<span className={styles.label_text}>Что нового?</span>
|
||||
<span className="line" />
|
||||
</div>
|
||||
<Card
|
||||
className={classNames(styles.grid, {
|
||||
[styles.noUpdates]: !updates.length,
|
||||
})}
|
||||
>
|
||||
<SubTitle>Что нового?</SubTitle>
|
||||
|
||||
<div className={styles.items}>
|
||||
<FlowRecent updated={updates} recent={recent} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{updates.length > 0 && (
|
||||
<div className={classNames(styles.items, styles.updates)}>
|
||||
{updates.map((node) => (
|
||||
<NodeHorizontalCard node={node} key={node.id} hasNew />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{experimentalFeatures.liquidFlow && (
|
||||
<Superpower>
|
||||
<div className={styles.toggles}>
|
||||
<Group
|
||||
horizontal
|
||||
onClick={onToggleLayout}
|
||||
className={styles.fluid_toggle}
|
||||
>
|
||||
<Toggle value={isFluid} />
|
||||
<div className={styles.toggles__label}>Жидкое течение</div>
|
||||
</Group>
|
||||
</div>
|
||||
</Superpower>
|
||||
{recent.length > 0 && (
|
||||
<div className={classNames(styles.items, styles.recent)}>
|
||||
{recent.map((node) => (
|
||||
<NodeHorizontalCard node={node} key={node.id} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
@import '../../../styles/variables';
|
||||
@import '~/styles/variables';
|
||||
|
||||
.wrap {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
border-radius: $radius;
|
||||
gap: $gap;
|
||||
}
|
||||
|
||||
.search {
|
||||
background-color: var(--content_bg_lighter);
|
||||
}
|
||||
|
||||
.grid {
|
||||
@include outer_shadow();
|
||||
display: flex;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
border-radius: $radius;
|
||||
position: relative;
|
||||
background: $content_bg;
|
||||
overflow: hidden;
|
||||
gap: $gap;
|
||||
padding: $gap;
|
||||
|
||||
&::after {
|
||||
content: '';
|
||||
|
@ -33,49 +35,31 @@
|
|||
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 {
|
||||
padding: 0 $gap 0 $gap;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.label {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
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;
|
||||
@container sizer (width >= #{$flow_hide_recents}) {
|
||||
&.scrollable {
|
||||
overflow: auto;
|
||||
}
|
||||
}
|
||||
|
||||
& > :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 {
|
||||
|
@ -89,34 +73,3 @@
|
|||
stroke-width: 0.5;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -59,7 +59,11 @@ const Header: FC<HeaderProps> = observer(() => {
|
|||
className={classNames(styles.wrap, { [styles.is_scrolled]: isScrolled })}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.logo_wrapper}>
|
||||
<div
|
||||
className={classNames(styles.logo_wrapper, {
|
||||
[styles.guest]: !isUser,
|
||||
})}
|
||||
>
|
||||
<Logo />
|
||||
</div>
|
||||
|
||||
|
|
|
@ -4,9 +4,12 @@ import type { HeaderProps } from '~/containers/main/Header/index';
|
|||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
export const HeaderSSR = dynamic<HeaderProps>(() => import('./index').then(it => it.Header), {
|
||||
ssr: false,
|
||||
loading: () => <div className={styles.wrap} />,
|
||||
});
|
||||
export const HeaderSSR = dynamic<HeaderProps>(
|
||||
() => import('./index').then((it) => it.Header),
|
||||
{
|
||||
ssr: false,
|
||||
loading: () => <div className={styles.wrap} />,
|
||||
},
|
||||
);
|
||||
|
||||
export const HeaderSSRPlaceholder = () => <div className={styles.wrap} />;
|
||||
|
|
|
@ -106,7 +106,9 @@
|
|||
transform: translate(50%, 0) scaleX(0);
|
||||
opacity: 0;
|
||||
border-radius: 3px;
|
||||
transition: transform 0.5s, opacity 0.25s;
|
||||
transition:
|
||||
transform 0.5s,
|
||||
opacity 0.25s;
|
||||
}
|
||||
|
||||
&::after {
|
||||
|
@ -159,7 +161,7 @@
|
|||
}
|
||||
}
|
||||
|
||||
.logo_wrapper {
|
||||
.logo_wrapper:not(.guest) {
|
||||
@include tablet {
|
||||
display: none;
|
||||
}
|
||||
|
|
|
@ -3,5 +3,6 @@ import dynamic from 'next/dynamic';
|
|||
import type { SubmitBarProps } from './index';
|
||||
|
||||
export const SubmitBarSSR = dynamic<SubmitBarProps>(
|
||||
() => import('./index').then(it => it.SubmitBar),
|
||||
{ ssr: false });
|
||||
() => import('./index').then((it) => it.SubmitBar),
|
||||
{ ssr: false },
|
||||
);
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,8 @@
|
|||
@import '~/styles/variables';
|
||||
|
||||
.wrap {
|
||||
width: 100%;
|
||||
aspect-ratio: calc(16 / 9);
|
||||
overflow: hidden;
|
||||
border-radius: $radius;
|
||||
}
|
|
@ -1,15 +1,21 @@
|
|||
import { FC, memo, useMemo } from 'react';
|
||||
import { FC, memo, useCallback, useMemo } from 'react';
|
||||
|
||||
import { Icon } from '~/components/common/Icon';
|
||||
import { ICommentBlockProps } from '~/constants/comment';
|
||||
import { useWindowSize } from '~/hooks/dom/useWindowSize';
|
||||
import { useYoutubeMetadata } from '~/hooks/metadata/useYoutubeMetadata';
|
||||
import { getYoutubeThumb } from '~/utils/dom';
|
||||
import { useVideoPlayer } from '~/utils/providers/VideoPlayerProvider';
|
||||
|
||||
import { CommentVideoFrame } from './components/CommentVideoFrame';
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
type Props = ICommentBlockProps & {};
|
||||
|
||||
const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
|
||||
const { isTablet } = useWindowSize();
|
||||
const { url, setUrl } = useVideoPlayer();
|
||||
|
||||
const id = useMemo(() => {
|
||||
const match = block.content.match(
|
||||
/https?:\/\/(?:www\.)?(?:youtube\.com|youtu\.be)\/(?:watch)?(?:\?v=)?([\w\-=]+)/,
|
||||
|
@ -18,7 +24,7 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
|
|||
return (match && match[1]) || '';
|
||||
}, [block.content]);
|
||||
|
||||
const url = useMemo(() => `https://youtube.com/watch?v=${id}`, [id]);
|
||||
const address = `https://youtube.com/watch?v=${id}`;
|
||||
|
||||
const preview = useMemo(
|
||||
() => getYoutubeThumb(block.content),
|
||||
|
@ -28,21 +34,46 @@ const CommentEmbedBlock: FC<Props> = memo(({ block }) => {
|
|||
const metadata = useYoutubeMetadata(id);
|
||||
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 (
|
||||
<div className={styles.embed}>
|
||||
<a href={url} target="_blank" rel="noreferrer" />
|
||||
|
||||
<div className={styles.preview}>
|
||||
<div style={{ backgroundImage: `url("${preview}")` }}>
|
||||
<div className={styles.backdrop}>
|
||||
<div className={styles.play}>
|
||||
<Icon icon="play" size={32} />
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>{title}</div>
|
||||
{url === address ? (
|
||||
<div className={styles.video}>
|
||||
<div className={styles.close} onClick={closeVideo}>
|
||||
<Icon icon="close" />
|
||||
</div>
|
||||
<div className={styles.animation}>
|
||||
<CommentVideoFrame id={id} title={title} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className={styles.preview}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
tabIndex={-1}
|
||||
>
|
||||
<div style={{ backgroundImage: `url("${preview}")` }}>
|
||||
<div className={styles.backdrop}>
|
||||
<div className={styles.play}>
|
||||
<Icon icon="play" size={32} />
|
||||
</div>
|
||||
|
||||
<div className={styles.title}>{title}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.embed {
|
||||
padding: 0 $gap;
|
||||
height: $comment_height;
|
||||
padding: 0 0;
|
||||
min-height: $comment_height;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
background: 50% 50% no-repeat;
|
||||
|
@ -43,7 +43,7 @@
|
|||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: $content_bg_backdrop 50% 50%;
|
||||
background: $content_bg_backdrop;
|
||||
background-size: cover;
|
||||
z-index: 15;
|
||||
border-radius: $radius;
|
||||
|
@ -69,6 +69,7 @@
|
|||
justify-content: stretch;
|
||||
box-sizing: border-box;
|
||||
z-index: 2;
|
||||
cursor: pointer;
|
||||
|
||||
& > div {
|
||||
width: 100%;
|
||||
|
@ -98,3 +99,47 @@
|
|||
overflow: hidden;
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
&.multiple {
|
||||
// Desktop devices
|
||||
@include flexbin(25vh, $flexbin-space);
|
||||
@include flexbin(300px, $flexbin-space);
|
||||
|
||||
// Tablet devices
|
||||
@media (max-width: $flexbin-tablet-max) {
|
||||
|
@ -22,13 +22,16 @@
|
|||
}
|
||||
|
||||
.image {
|
||||
max-height: 500px;
|
||||
max-height: 300px;
|
||||
border-radius: $radius;
|
||||
max-width: 100%;
|
||||
|
||||
.multiple & {
|
||||
max-height: 250px;
|
||||
max-inline-size: 250px;
|
||||
// both of that were 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ import { CommentWrapper } from '~/containers/comments/CommentWrapper';
|
|||
import { IComment, ICommentGroup, IFile } from '~/types';
|
||||
|
||||
import { CommendDeleted } from '../../../../../components/node/CommendDeleted';
|
||||
import { getCommentId } from '../../../../../constants/dom/links';
|
||||
|
||||
import { CommentContent } from './components/CommentContent';
|
||||
import { CommentDistance } from './components/CommentDistance';
|
||||
|
@ -83,18 +84,22 @@ const Comment: FC<Props> = memo(
|
|||
);
|
||||
|
||||
return (
|
||||
<CommentContent
|
||||
prefix={prefix}
|
||||
saveComment={saveComment}
|
||||
nodeId={nodeId}
|
||||
comment={comment}
|
||||
canEdit={!!canEdit}
|
||||
canLike={!!canLike}
|
||||
onLike={() => onLike(comment.id, !comment.liked)}
|
||||
onDelete={(val: boolean) => onDelete(comment.id, val)}
|
||||
onShowImageModal={onShowImageModal}
|
||||
key={comment.id}
|
||||
/>
|
||||
<>
|
||||
<a id={getCommentId(comment.id)} className={styles.anchor} />
|
||||
|
||||
<CommentContent
|
||||
prefix={prefix}
|
||||
saveComment={saveComment}
|
||||
nodeId={nodeId}
|
||||
comment={comment}
|
||||
canEdit={!!canEdit}
|
||||
canLike={!!canLike}
|
||||
onLike={() => onLike(comment.id, !comment.liked)}
|
||||
onDelete={(val: boolean) => onDelete(comment.id, val)}
|
||||
onShowImageModal={onShowImageModal}
|
||||
key={comment.id}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
|
|
@ -15,3 +15,9 @@
|
|||
.highlighted {
|
||||
box-shadow: $color_primary 0 0 0px 2px;
|
||||
}
|
||||
|
||||
.anchor {
|
||||
display: block;
|
||||
position: relative;
|
||||
top: -($header_height * 2);
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { FC, useMemo } from 'react';
|
||||
import { FC, useEffect, useMemo } from 'react';
|
||||
|
||||
import { observer } from 'mobx-react-lite';
|
||||
|
||||
import { LoadMoreButton } from '~/components/input/LoadMoreButton';
|
||||
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 { useGrouppedComments } from '~/hooks/node/useGrouppedComments';
|
||||
import { ICommentGroup } from '~/types';
|
||||
|
@ -11,6 +15,7 @@ import { useCommentContext } from '~/utils/context/CommentContextProvider';
|
|||
import { useNodeContext } from '~/utils/context/NodeContextProvider';
|
||||
import { useUserContext } from '~/utils/context/UserContextProvider';
|
||||
import { canEditComment, canLikeComment } from '~/utils/node';
|
||||
import { VideoPlayerProvider } from '~/utils/providers/VideoPlayerProvider';
|
||||
|
||||
import styles from './styles.module.scss';
|
||||
|
||||
|
@ -18,6 +23,11 @@ interface Props {
|
|||
order: 'ASC' | 'DESC';
|
||||
}
|
||||
|
||||
const isFirstGroupWithNewComment = (
|
||||
group: ICommentGroup,
|
||||
prevGroup: ICommentGroup | undefined,
|
||||
) => group.hasNew && (!prevGroup || !prevGroup.hasNew);
|
||||
|
||||
const NodeComments: FC<Props> = observer(({ order }) => {
|
||||
const user = useUserContext();
|
||||
const { node } = useNodeContext();
|
||||
|
@ -35,7 +45,7 @@ const NodeComments: FC<Props> = observer(({ order }) => {
|
|||
onSaveComment,
|
||||
} = useCommentContext();
|
||||
|
||||
const groupped: ICommentGroup[] = useGrouppedComments(
|
||||
const groupped = useGrouppedComments(
|
||||
comments,
|
||||
order,
|
||||
lastSeenCurrent ?? undefined,
|
||||
|
@ -59,30 +69,56 @@ const NodeComments: FC<Props> = observer(({ order }) => {
|
|||
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 (
|
||||
<div className={styles.wrap}>
|
||||
{order === 'DESC' && more}
|
||||
<VideoPlayerProvider>
|
||||
<div className={styles.wrap}>
|
||||
{order === 'DESC' && more}
|
||||
|
||||
{groupped.map((group) => (
|
||||
<Comment
|
||||
nodeId={node.id!}
|
||||
key={group.ids.join()}
|
||||
group={group}
|
||||
highlighted={
|
||||
node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID
|
||||
}
|
||||
onLike={onLike}
|
||||
canLike={canLikeComment(group, user)}
|
||||
canEdit={canEditComment(group, user)}
|
||||
onDelete={onDeleteComment}
|
||||
onShowImageModal={onShowImageModal}
|
||||
isSame={group.user.id === user.id}
|
||||
saveComment={onSaveComment}
|
||||
/>
|
||||
))}
|
||||
{groupped.map((group, index) => (
|
||||
<>
|
||||
{isFirstGroupWithNewComment(group, groupped[index - 1]) && (
|
||||
<a
|
||||
id={NEW_COMMENT_ANCHOR_NAME}
|
||||
className={styles.newCommentAnchor}
|
||||
/>
|
||||
)}
|
||||
|
||||
{order === 'ASC' && more}
|
||||
</div>
|
||||
<Comment
|
||||
nodeId={node.id!}
|
||||
key={group.ids.join()}
|
||||
group={group}
|
||||
highlighted={
|
||||
node.id === BORIS_NODE_ID && group.user.id === ANNOUNCE_USER_ID
|
||||
}
|
||||
onLike={onLike}
|
||||
canLike={canLikeComment(group, user)}
|
||||
canEdit={canEditComment(group, user)}
|
||||
onDelete={onDeleteComment}
|
||||
onShowImageModal={onShowImageModal}
|
||||
isSame={group.user.id === user.id}
|
||||
saveComment={onSaveComment}
|
||||
/>
|
||||
</>
|
||||
))}
|
||||
|
||||
{order === 'ASC' && more}
|
||||
</div>
|
||||
</VideoPlayerProvider>
|
||||
);
|
||||
});
|
||||
|
||||
|
|
|
@ -13,3 +13,9 @@
|
|||
.more {
|
||||
margin-bottom: $gap;
|
||||
}
|
||||
|
||||
.newCommentAnchor {
|
||||
position: relative;
|
||||
top: -($header_height * 2);
|
||||
display: block;
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import { Avatar } from '~/components/common/Avatar';
|
||||
import { Card } from '~/components/common/Card';
|
||||
import { Placeholder } from '~/components/placeholders/Placeholder';
|
||||
import { imagePresets } from '~/constants/urls';
|
||||
import { IUser } from '~/types/auth';
|
||||
|
@ -11,28 +12,39 @@ interface Props {
|
|||
profile: IUser;
|
||||
isLoading: boolean;
|
||||
username: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ProfilePageLeft: FC<Props> = ({ username, profile, isLoading }) => {
|
||||
const ProfilePageLeft: FC<Props> = ({
|
||||
username,
|
||||
profile,
|
||||
description,
|
||||
isLoading,
|
||||
}) => {
|
||||
return (
|
||||
<div className={styles.wrap}>
|
||||
<Avatar
|
||||
username={username}
|
||||
url={profile?.photo?.url}
|
||||
className={styles.avatar}
|
||||
preset={imagePresets['600']}
|
||||
/>
|
||||
<Card className={styles.wrap} elevation={0} seamless>
|
||||
<Card seamless>
|
||||
<Avatar
|
||||
username={username}
|
||||
url={profile?.photo?.url}
|
||||
className={styles.avatar}
|
||||
preset={imagePresets['600']}
|
||||
/>
|
||||
|
||||
<div className={styles.region}>
|
||||
<div className={styles.name}>
|
||||
{isLoading ? <Placeholder /> : profile?.fullname}
|
||||
</div>
|
||||
|
||||
<div className={styles.username}>
|
||||
{isLoading ? <Placeholder /> : `~${profile?.username}`}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
<div className={styles.region}>
|
||||
<div className={styles.name}>
|
||||
{isLoading ? <Placeholder /> : profile?.fullname}
|
||||
</div>
|
||||
|
||||
<div className={styles.username}>
|
||||
{isLoading ? <Placeholder /> : `~${profile?.username}`}
|
||||
</div>
|
||||
<div className={styles.description}>{description}</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -1,29 +1,26 @@
|
|||
@import 'src/styles/variables';
|
||||
|
||||
.wrap {
|
||||
@include outer_shadow;
|
||||
@include blur;
|
||||
|
||||
padding: $gap $gap $gap * 2;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
justify-content: stretch;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
border-radius: $radius;
|
||||
}
|
||||
|
||||
.top {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
margin-bottom: $gap * 2;
|
||||
}
|
||||
|
||||
.region {
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
padding: $gap;
|
||||
}
|
||||
|
||||
.name {
|
||||
|
@ -44,8 +41,7 @@
|
|||
|
||||
.description {
|
||||
@include clamp(3, 21px * 3);
|
||||
line-height: 21px;
|
||||
font: $font_14_regular;
|
||||
margin-top: $gap * 3;
|
||||
display: none;
|
||||
line-height: 1.25em;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ const ThemeSwitcher: FC<ThemeSwitcherProps> = () => {
|
|||
>
|
||||
<Group>
|
||||
<div className={styles.palette}>
|
||||
{item.colors.map((color) => (
|
||||
{[...item.colors].reverse().map((color) => (
|
||||
<div
|
||||
key={color}
|
||||
className={styles.sample}
|
||||
|
|
|
@ -14,7 +14,7 @@ import type { SidebarComponentProps } from '~/types/sidebar';
|
|||
import { isNil } from '~/utils/ramda';
|
||||
|
||||
const tabs = ['profile', 'notifications', 'bookmarks'] as const;
|
||||
type TabName = typeof tabs[number];
|
||||
type TabName = (typeof tabs)[number];
|
||||
|
||||
interface SettingsSidebarProps
|
||||
extends SidebarComponentProps<SidebarName.Settings> {
|
||||
|
|
|
@ -10,7 +10,7 @@ export const useLastSeenBoris = () => {
|
|||
async (date: string) => {
|
||||
await update({ last_seen_boris: date }, false);
|
||||
},
|
||||
[update]
|
||||
[update],
|
||||
);
|
||||
|
||||
return { setLastSeen, lastSeen };
|
||||
|
|
|
@ -20,7 +20,7 @@ export const useLoginLogoutRestore = () => {
|
|||
showToastInfo(getRandomPhrase('WELCOME'));
|
||||
return result.user;
|
||||
},
|
||||
[auth]
|
||||
[auth],
|
||||
);
|
||||
|
||||
return { logout, login };
|
||||
|
|
|
@ -5,8 +5,9 @@ import { API } from '~/constants/api';
|
|||
import { getErrorMessage } from '~/utils/errors/getErrorMessage';
|
||||
|
||||
export const useRestoreCode = (code: string) => {
|
||||
const { data, isValidating, error } = useSWR(API.USER.REQUEST_CODE(code), () =>
|
||||
apiCheckRestoreCode({ code })
|
||||
const { data, isValidating, error } = useSWR(
|
||||
API.USER.REQUEST_CODE(code),
|
||||
() => apiCheckRestoreCode({ code }),
|
||||
);
|
||||
|
||||
const codeUser = data?.user;
|
||||
|
|
|
@ -18,7 +18,7 @@ const validationSchema = object({
|
|||
.test(
|
||||
'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 = (
|
||||
code: string,
|
||||
fetcher: (props: { code: string; password: string }) => Promise<{ token: string; user: IUser }>,
|
||||
onSuccess: () => void
|
||||
fetcher: (props: {
|
||||
code: string;
|
||||
password: string;
|
||||
}) => Promise<{ token: string; user: IUser }>,
|
||||
onSuccess: () => void,
|
||||
) => {
|
||||
const auth = useAuthStore();
|
||||
|
||||
const onSubmit = useCallback<FormikConfig<RestorePasswordData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
try {
|
||||
const { token, user } = await fetcher({ password: values.newPassword, code });
|
||||
const { token, user } = await fetcher({
|
||||
password: values.newPassword,
|
||||
code,
|
||||
});
|
||||
auth.setUser(user);
|
||||
auth.setToken(token);
|
||||
onSuccess();
|
||||
|
@ -47,7 +53,7 @@ export const useRestorePasswordForm = (
|
|||
}
|
||||
}
|
||||
},
|
||||
[onSuccess, fetcher, code, auth]
|
||||
[onSuccess, fetcher, code, auth],
|
||||
);
|
||||
|
||||
return useFormik<RestorePasswordData>({
|
||||
|
|
|
@ -15,7 +15,7 @@ type RestoreRequestData = Asserts<typeof validationSchema>;
|
|||
|
||||
export const useRestoreRequestForm = (
|
||||
fetcher: (field: string) => Promise<unknown>,
|
||||
onSuccess: () => void
|
||||
onSuccess: () => void,
|
||||
) => {
|
||||
const onSubmit = useCallback<FormikConfig<RestoreRequestData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
|
@ -31,7 +31,7 @@ export const useRestoreRequestForm = (
|
|||
}
|
||||
}
|
||||
},
|
||||
[fetcher, onSuccess]
|
||||
[fetcher, onSuccess],
|
||||
);
|
||||
|
||||
return useFormik({
|
||||
|
|
|
@ -13,6 +13,6 @@ export const useSessionCookie = () => {
|
|||
autorun(() => {
|
||||
setCookie('session', auth.token, 30);
|
||||
}),
|
||||
[auth]
|
||||
[auth],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -9,9 +9,7 @@ import { showErrorToast } from '~/utils/errors/showToast';
|
|||
|
||||
const validationSchema = object({
|
||||
username: string().required(ERRORS.REQUIRED),
|
||||
password: string()
|
||||
.required(ERRORS.REQUIRED)
|
||||
.min(6, ERRORS.PASSWORD_IS_SHORT),
|
||||
password: string().required(ERRORS.REQUIRED).min(6, ERRORS.PASSWORD_IS_SHORT),
|
||||
});
|
||||
|
||||
type SocialRegisterData = Asserts<typeof validationSchema>;
|
||||
|
@ -23,7 +21,7 @@ export const useSocialRegisterForm = (
|
|||
username: string;
|
||||
password: string;
|
||||
}) => Promise<{ token: string }>,
|
||||
onSuccess: (token: string) => void
|
||||
onSuccess: (token: string) => void,
|
||||
) => {
|
||||
const onSubmit = useCallback<FormikConfig<SocialRegisterData>['onSubmit']>(
|
||||
async (values, { setErrors }) => {
|
||||
|
@ -43,7 +41,7 @@ export const useSocialRegisterForm = (
|
|||
}
|
||||
}
|
||||
},
|
||||
[token, onSuccess, fetcher]
|
||||
[token, onSuccess, fetcher],
|
||||
);
|
||||
|
||||
return useFormik<SocialRegisterData>({
|
||||
|
|
|
@ -7,7 +7,10 @@ const today = new Date();
|
|||
export const useUserActiveStatus = (lastSeen?: string) => {
|
||||
try {
|
||||
const lastSeenDate = lastSeen ? parseISO(lastSeen) : undefined;
|
||||
return lastSeenDate && differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS;
|
||||
return (
|
||||
lastSeenDate &&
|
||||
differenceInDays(today, lastSeenDate) < INACTIVE_ACCOUNT_DAYS
|
||||
);
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -6,12 +6,14 @@ import { initialBackendStats } from '~/constants/boris/constants';
|
|||
import { BorisUsageStats } from '~/types/boris';
|
||||
|
||||
export const useBorisStats = () => {
|
||||
const { data: backend = initialBackendStats, isValidating: isValidatingBackend } = useSWR(
|
||||
API.BORIS.GET_BACKEND_STATS,
|
||||
() => getBorisBackendStats()
|
||||
);
|
||||
const {
|
||||
data: backend = initialBackendStats,
|
||||
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 = {
|
||||
backend,
|
||||
|
|
|
@ -3,9 +3,16 @@ import { useMemo } from 'react';
|
|||
import { normalizeBrightColor } from '~/utils/color';
|
||||
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(
|
||||
() => (val && normalizeBrightColor(stringToColour(val), saturation, lightness)) || '',
|
||||
[lightness, saturation, val]
|
||||
() =>
|
||||
(val &&
|
||||
normalizeBrightColor(stringToColour(val), saturation, lightness)) ||
|
||||
'',
|
||||
[lightness, saturation, val],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,7 +6,7 @@ export const useColorGradientFromString = (
|
|||
val?: string,
|
||||
saturation = 3,
|
||||
lightness = 3,
|
||||
angle = 155
|
||||
angle = 155,
|
||||
) =>
|
||||
useMemo(() => {
|
||||
if (!val) {
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
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(() => {
|
||||
try {
|
||||
return localStorage.getItem(`vault_${key}`) || initial;
|
||||
|
|
|
@ -4,15 +4,18 @@ export const useFocusEvent = (initialState = false, delay = 0) => {
|
|||
const [focused, setFocused] = useState(initialState);
|
||||
|
||||
const onFocus = useCallback(
|
||||
event => {
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
|
||||
setFocused(true);
|
||||
},
|
||||
[setFocused]
|
||||
[setFocused],
|
||||
);
|
||||
const onBlur = useCallback(
|
||||
() => setTimeout(() => setFocused(false), delay),
|
||||
[delay],
|
||||
);
|
||||
const onBlur = useCallback(() => setTimeout(() => setFocused(false), delay), [delay]);
|
||||
|
||||
return { focused, onBlur, onFocus };
|
||||
};
|
||||
|
|
|
@ -7,12 +7,13 @@ export const useFormatWrapper = (onChange: (val: string) => void) => {
|
|||
target: HTMLTextAreaElement,
|
||||
|
||||
prefix = '',
|
||||
suffix = ''
|
||||
) => event => {
|
||||
event.preventDefault();
|
||||
wrapTextInsideInput(target, prefix, suffix, onChange);
|
||||
},
|
||||
[onChange]
|
||||
suffix = '',
|
||||
) =>
|
||||
(event) => {
|
||||
event.preventDefault();
|
||||
wrapTextInsideInput(target, prefix, suffix, onChange);
|
||||
},
|
||||
[onChange],
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -21,7 +22,7 @@ export const wrapTextInsideInput = (
|
|||
target: HTMLTextAreaElement,
|
||||
prefix: string,
|
||||
suffix: string,
|
||||
onChange: (val: string) => void
|
||||
onChange: (val: string) => void,
|
||||
) => {
|
||||
if (!target) return;
|
||||
|
||||
|
@ -34,7 +35,7 @@ export const wrapTextInsideInput = (
|
|||
onChange(
|
||||
target.value.substring(0, start) +
|
||||
replacement +
|
||||
target.value.substring(end, target.value.length)
|
||||
target.value.substring(end, target.value.length),
|
||||
);
|
||||
|
||||
target.focus();
|
||||
|
|
|
@ -2,7 +2,8 @@ import { useCallback, useEffect } from 'react';
|
|||
|
||||
export const useInfiniteLoader = (loader: () => void, isLoading?: boolean) => {
|
||||
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;
|
||||
|
||||
|
|
|
@ -5,13 +5,13 @@ import { getImageFromPaste } from '~/utils/uploader';
|
|||
// useInputPasteUpload attaches event listener to input, that calls onUpload if user pasted any image
|
||||
export const useInputPasteUpload = (onUpload: (files: File[]) => void) => {
|
||||
return useCallback(
|
||||
async event => {
|
||||
async (event) => {
|
||||
const image = await getImageFromPaste(event);
|
||||
|
||||
if (!image) return;
|
||||
|
||||
onUpload([image]);
|
||||
},
|
||||
[onUpload]
|
||||
[onUpload],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -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(
|
||||
() =>
|
||||
[
|
||||
|
@ -35,5 +39,5 @@ export const usePopperModifiers = (offsetX = 0, offsetY = 10, justify?: boolean)
|
|||
},
|
||||
...(justify ? [sameWidth] : []),
|
||||
] as Modifier<any>[],
|
||||
[offsetX, offsetY, justify]
|
||||
[offsetX, offsetY, justify],
|
||||
);
|
||||
|
|
|
@ -11,7 +11,7 @@ const getHeight = () => {
|
|||
body.offsetHeight,
|
||||
html.clientHeight,
|
||||
html.scrollHeight,
|
||||
html.offsetHeight
|
||||
html.offsetHeight,
|
||||
);
|
||||
};
|
||||
export const useScrollHeight = () => getHeight();
|
||||
|
|
|
@ -18,6 +18,6 @@ export const useScrollToTop = (deps?: any[]) => {
|
|||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
deps && Array.isArray(deps) ? deps : []
|
||||
deps && Array.isArray(deps) ? deps : [],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import { useEffect, useState } from 'react';
|
||||
|
||||
export const useScrollTop = () => {
|
||||
const [top, setTop] = useState(typeof window !== 'undefined' ? window.scrollY : 0);
|
||||
const [top, setTop] = useState(
|
||||
typeof window !== 'undefined' ? window.scrollY : 0,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setTop(window.scrollY);
|
||||
|
|
|
@ -6,11 +6,12 @@ export const useFlowCellControls = (
|
|||
id: INode['id'],
|
||||
description: string | undefined,
|
||||
flow: FlowDisplay,
|
||||
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void
|
||||
onChangeCellView: (id: INode['id'], flow: FlowDisplay) => void,
|
||||
) => {
|
||||
const onChange = useCallback(
|
||||
(value: Partial<FlowDisplay>) => onChangeCellView(id, { ...flow, ...value }),
|
||||
[flow, id, onChangeCellView]
|
||||
(value: Partial<FlowDisplay>) =>
|
||||
onChangeCellView(id, { ...flow, ...value }),
|
||||
[flow, id, onChangeCellView],
|
||||
);
|
||||
|
||||
const hasDescription = !!description && description.length > 32;
|
||||
|
|
|
@ -17,6 +17,6 @@ export const useFlowSetCellView = () => {
|
|||
showErrorToast(error);
|
||||
}
|
||||
},
|
||||
[updateNode]
|
||||
[updateNode],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -21,15 +21,19 @@ export const useGetLabStats = () => {
|
|||
heroes: lab.heroes,
|
||||
tags: lab.tags,
|
||||
},
|
||||
onSuccess: data => {
|
||||
onSuccess: (data) => {
|
||||
lab.setHeroes(data.heroes);
|
||||
lab.setTags(data.tags);
|
||||
},
|
||||
refreshInterval,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const { data: updatesData, isValidating: isValidatingUpdates, mutate: mutateUpdates } = useSWR(
|
||||
const {
|
||||
data: updatesData,
|
||||
isValidating: isValidatingUpdates,
|
||||
mutate: mutateUpdates,
|
||||
} = useSWR(
|
||||
isUser ? API.LAB.UPDATES : null,
|
||||
async () => {
|
||||
const result = await getLabUpdates();
|
||||
|
@ -37,26 +41,27 @@ export const useGetLabStats = () => {
|
|||
},
|
||||
{
|
||||
fallbackData: lab.updates,
|
||||
onSuccess: data => {
|
||||
onSuccess: (data) => {
|
||||
lab.setUpdates(data);
|
||||
},
|
||||
refreshInterval,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const heroes = useMemo(() => stats?.heroes || [], [stats]);
|
||||
const tags = useMemo(() => stats?.tags || [], [stats]);
|
||||
const updates = useMemo(() => updatesData || [], [updatesData]);
|
||||
|
||||
const isLoading = (!stats || !updates) && (isValidatingStats || isValidatingUpdates);
|
||||
const isLoading =
|
||||
(!stats || !updates) && (isValidatingStats || isValidatingUpdates);
|
||||
const seenNode = useCallback(
|
||||
async (nodeId: number) => {
|
||||
await mutateUpdates(
|
||||
updates.filter(it => it.id !== nodeId),
|
||||
false
|
||||
updates.filter((it) => it.id !== nodeId),
|
||||
false,
|
||||
);
|
||||
},
|
||||
[mutateUpdates, updates]
|
||||
[mutateUpdates, updates],
|
||||
);
|
||||
|
||||
return { heroes, tags, updates, isLoading, seenNode };
|
||||
|
|
|
@ -11,7 +11,7 @@ const getKey = (username: string): string | null => {
|
|||
};
|
||||
export const useMessages = (username: string) => {
|
||||
const { data, isValidating } = useSWR(getKey(username), async () =>
|
||||
apiGetUserMessages({ username })
|
||||
apiGetUserMessages({ username }),
|
||||
);
|
||||
|
||||
const messages: IMessage[] = useMemo(() => data?.messages || [], [data]);
|
||||
|
|
|
@ -5,7 +5,9 @@ import { useModalStore } from '~/store/modal/useModalStore';
|
|||
import { DialogComponentProps } from '~/types/modal';
|
||||
|
||||
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
|
||||
? keyof Omit<U, 'onRequestClose' | 'children'> extends never
|
||||
? {}
|
||||
|
@ -21,7 +23,7 @@ export const useModal = () => {
|
|||
<T extends Dialog>(dialog: T, props: DialogContentProps[T]) => {
|
||||
setCurrent(dialog, props);
|
||||
},
|
||||
[setCurrent]
|
||||
[setCurrent],
|
||||
);
|
||||
|
||||
return { showModal, hideModal: hide, current, isOpened: !!current };
|
||||
|
|
|
@ -10,6 +10,6 @@ export const useShowModal = <T extends Dialog>(dialog: T) => {
|
|||
(props: DialogContentProps[T]) => {
|
||||
modal.showModal(dialog, props);
|
||||
},
|
||||
[dialog, modal]
|
||||
[dialog, modal],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -11,6 +11,6 @@ export const useImageModal = () => {
|
|||
(images: IFile[], index: number) => {
|
||||
showModal({ items: images, index });
|
||||
},
|
||||
[showModal]
|
||||
[showModal],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -17,7 +17,7 @@ export const useNavigation = () => {
|
|||
craHistory.push(url);
|
||||
}
|
||||
},
|
||||
[craHistory, nextRouter]
|
||||
[craHistory, nextRouter],
|
||||
);
|
||||
|
||||
return { push };
|
||||
|
|
|
@ -16,9 +16,13 @@ export const useCreateNode = () => {
|
|||
if (node.is_promoted) {
|
||||
flow.setNodes([result.node, ...flow.nodes]);
|
||||
} 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],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -6,13 +6,13 @@ import { groupCommentsByUser } from '~/utils/fn';
|
|||
export const useGrouppedComments = (
|
||||
comments: IComment[],
|
||||
order: 'ASC' | 'DESC',
|
||||
lastSeen?: string
|
||||
lastSeen?: string,
|
||||
) =>
|
||||
useMemo(
|
||||
() =>
|
||||
(order === 'DESC' ? [...comments].reverse() : comments).reduce(
|
||||
groupCommentsByUser(lastSeen),
|
||||
[]
|
||||
[],
|
||||
),
|
||||
[comments, lastSeen, order]
|
||||
[comments, lastSeen, order],
|
||||
);
|
||||
|
|
|
@ -6,7 +6,10 @@ import { useModal } from '~/hooks/modal/useModal';
|
|||
import { INode } from '~/types';
|
||||
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 onLike = useCallback(async () => {
|
||||
|
@ -35,17 +38,20 @@ export const useNodeActions = (node: INode, update: (node: Partial<INode>) => Pr
|
|||
|
||||
const onLock = useCallback(async () => {
|
||||
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 });
|
||||
} catch (error) {
|
||||
showErrorToast(error);
|
||||
}
|
||||
}, [node.deleted_at, node.id, update]);
|
||||
|
||||
const onEdit = useCallback(() => showModal(Dialog.EditNode, { nodeId: node.id! }), [
|
||||
node,
|
||||
showModal,
|
||||
]);
|
||||
const onEdit = useCallback(
|
||||
() => showModal(Dialog.EditNode, { nodeId: node.id! }),
|
||||
[node, showModal],
|
||||
);
|
||||
|
||||
return { onLike, onStar, onLock, onEdit };
|
||||
};
|
||||
|
|
|
@ -4,7 +4,8 @@ import { UploadType } from '~/constants/uploads';
|
|||
import { INode } from '~/types';
|
||||
|
||||
export const useNodeAudios = (node: INode) => {
|
||||
return useMemo(() => node.files.filter(file => file && file.type === UploadType.Audio), [
|
||||
node.files,
|
||||
]);
|
||||
return useMemo(
|
||||
() => node.files.filter((file) => file && file.type === UploadType.Audio),
|
||||
[node.files],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import { useCallback, useRef } from 'react';
|
||||
|
||||
import { FormikConfig, FormikHelpers, useFormik, useFormikContext } from 'formik';
|
||||
import {
|
||||
FormikConfig,
|
||||
FormikHelpers,
|
||||
useFormik,
|
||||
useFormikContext,
|
||||
} from 'formik';
|
||||
import { object } from 'yup';
|
||||
|
||||
import { INode } from '~/types';
|
||||
|
@ -10,31 +15,31 @@ import { showErrorToast } from '~/utils/errors/showToast';
|
|||
|
||||
const validationSchema = object().shape({});
|
||||
|
||||
const afterSubmit = ({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) => (
|
||||
error?: unknown
|
||||
) => {
|
||||
setSubmitting(false);
|
||||
const afterSubmit =
|
||||
({ resetForm, setSubmitting, setErrors }: FormikHelpers<INode>) =>
|
||||
(error?: unknown) => {
|
||||
setSubmitting(false);
|
||||
|
||||
if (error) {
|
||||
showErrorToast(error);
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
showErrorToast(error);
|
||||
return;
|
||||
}
|
||||
|
||||
if (getValidationErrors(error)) {
|
||||
setErrors(getValidationErrors(error)!);
|
||||
return;
|
||||
}
|
||||
if (getValidationErrors(error)) {
|
||||
setErrors(getValidationErrors(error)!);
|
||||
return;
|
||||
}
|
||||
|
||||
if (resetForm) {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
if (resetForm) {
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
export const useNodeFormFormik = (
|
||||
values: INode,
|
||||
uploader: Uploader,
|
||||
stopEditing: () => void,
|
||||
sendSaveRequest: (node: INode) => Promise<unknown>
|
||||
sendSaveRequest: (node: INode) => Promise<unknown>,
|
||||
) => {
|
||||
const { current: initialValues } = useRef(values);
|
||||
|
||||
|
@ -53,7 +58,7 @@ export const useNodeFormFormik = (
|
|||
afterSubmit(helpers)(error);
|
||||
}
|
||||
},
|
||||
[sendSaveRequest, uploader.files]
|
||||
[sendSaveRequest, uploader.files],
|
||||
);
|
||||
|
||||
return useFormik<INode>({
|
||||
|
|
|
@ -4,7 +4,8 @@ import { UploadType } from '~/constants/uploads';
|
|||
import { INode } from '~/types';
|
||||
|
||||
export const useNodeImages = (node: INode) => {
|
||||
return useMemo(() => node.files.filter(file => file && file.type === UploadType.Image), [
|
||||
node.files,
|
||||
]);
|
||||
return useMemo(
|
||||
() => node.files.filter((file) => file && file.type === UploadType.Image),
|
||||
[node.files],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -27,6 +27,6 @@ export const useUpdateNode = (id: number) => {
|
|||
await lab.updateNode(result.node.id!, result.node);
|
||||
}
|
||||
},
|
||||
[update, flow, lab]
|
||||
[update, flow, lab],
|
||||
);
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ export const useGetProfile = (username?: string) => {
|
|||
},
|
||||
{
|
||||
refreshInterval: 60000,
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const profile = data || EMPTY_USER;
|
||||
|
@ -29,7 +29,7 @@ export const useGetProfile = (username?: string) => {
|
|||
async (user: Partial<IUser>) => {
|
||||
await mutate({ ...profile, ...user });
|
||||
},
|
||||
[mutate, profile]
|
||||
[mutate, profile],
|
||||
);
|
||||
|
||||
return { profile, isLoading: !data && isValidating, update };
|
||||
|
|
|
@ -9,21 +9,19 @@ import { flatten } from '~/utils/ramda';
|
|||
|
||||
const RESULTS_COUNT = 20;
|
||||
|
||||
const getKey: (text: string) => SWRInfiniteKeyLoader = text => (
|
||||
pageIndex,
|
||||
previousPageData: INode[]
|
||||
) => {
|
||||
if ((pageIndex > 0 && !previousPageData?.length) || !text) return null;
|
||||
const getKey: (text: string) => SWRInfiniteKeyLoader =
|
||||
(text) => (pageIndex, previousPageData: INode[]) => {
|
||||
if ((pageIndex > 0 && !previousPageData?.length) || !text) return null;
|
||||
|
||||
const props: GetSearchResultsRequest = {
|
||||
text,
|
||||
skip: pageIndex * RESULTS_COUNT,
|
||||
take: RESULTS_COUNT,
|
||||
const props: GetSearchResultsRequest = {
|
||||
text,
|
||||
skip: pageIndex * RESULTS_COUNT,
|
||||
take: RESULTS_COUNT,
|
||||
};
|
||||
|
||||
return JSON.stringify(props);
|
||||
};
|
||||
|
||||
return JSON.stringify(props);
|
||||
};
|
||||
|
||||
export const useSearch = () => {
|
||||
const [searchText, setSearchText] = useState('');
|
||||
const [debouncedSearchText, setDebouncedSearchText] = useState('');
|
||||
|
@ -40,7 +38,7 @@ export const useSearch = () => {
|
|||
const result = await getSearchResults(props);
|
||||
|
||||
return result.nodes;
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const loadMore = useCallback(() => setSize(size + 1), [setSize, size]);
|
||||
|
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue