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