Merge pull request #396 from Discours/feature/biome

Feature/biome
This commit is contained in:
Tony 2024-02-05 19:47:58 +03:00 committed by GitHub
commit 7c2f8370b5
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
204 changed files with 1509 additions and 5343 deletions

View File

@ -1,7 +0,0 @@
node_modules
public
*.cjs
dist/
.vercel/
src/graphql/client/*
src/graphql/schema/*

View File

@ -1,109 +0,0 @@
module.exports = {
plugins: ['@typescript-eslint', 'import', 'sonarjs', 'unicorn', 'promise', 'solid', 'jest'],
extends: [
'eslint:recommended',
'plugin:import/recommended',
'plugin:import/typescript',
'prettier',
'plugin:sonarjs/recommended',
'plugin:unicorn/recommended',
'plugin:promise/recommended',
'plugin:solid/recommended',
'plugin:jest/recommended',
],
overrides: [
{
files: ['**/*.ts', '**/*.tsx'],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 2021,
ecmaFeatures: { jsx: true },
sourceType: 'module',
project: './tsconfig.json',
},
extends: [
'plugin:@typescript-eslint/recommended',
// 'plugin:@typescript-eslint/recommended-requiring-type-checking', // 30-01-2024 699 problems
],
rules: {
'@typescript-eslint/no-unused-vars': [
'warn',
{
argsIgnorePattern: '^_',
},
],
'@typescript-eslint/no-non-null-assertion': 'error',
'@typescript-eslint/no-explicit-any': 'warn',
},
},
],
env: {
browser: true,
node: true,
// mocha: true,
},
globals: {},
rules: {
// Solid
'solid/reactivity': 'off',
'solid/no-innerhtml': 'off',
/** Unicorn **/
'unicorn/no-null': 'off',
'unicorn/filename-case': 'off',
'unicorn/no-array-for-each': 'off',
'unicorn/no-array-reduce': 'off',
'unicorn/prefer-string-replace-all': 'warn',
'unicorn/prevent-abbreviations': 'off',
'unicorn/prefer-module': 'off',
'unicorn/import-style': 'off',
'unicorn/numeric-separators-style': 'off',
'unicorn/prefer-node-protocol': 'off',
'unicorn/prefer-dom-node-append': 'warn',
'unicorn/prefer-top-level-await': 'warn',
'unicorn/consistent-function-scoping': 'warn',
'unicorn/no-array-callback-reference': 'warn',
'unicorn/no-array-method-this-argument': 'warn',
'unicorn/no-for-loop': 'off',
'unicorn/prefer-switch': 'warn',
'sonarjs/no-duplicate-string': ['warn', { threshold: 5 }],
'sonarjs/prefer-immediate-return': 'warn',
// Promise
'promise/catch-or-return': 'off',
'promise/always-return': 'off',
eqeqeq: 'error',
'no-param-reassign': 'error',
'no-nested-ternary': 'error',
'no-shadow': 'error',
'import/order': [
'warn',
{
groups: ['type', 'builtin', 'external', 'internal', 'parent', 'sibling', 'index'],
distinctGroup: false,
pathGroups: [
{
pattern: '*.scss',
patternOptions: { matchBase: true },
group: 'index',
position: 'after',
},
],
'newlines-between': 'always',
alphabetize: {
order: 'asc',
caseInsensitive: true,
},
},
],
},
settings: {
'import/resolver': {
typescript: true,
node: true,
},
},
}

View File

@ -1,31 +1,35 @@
name: 'deploy' name: "deploy"
on: on: [push]
push:
branches:
- main
- dev
- feature/email-templates
jobs: jobs:
test: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout code - uses: actions/checkout@v3
uses: actions/checkout@v2 - uses: actions/setup-node@v3
- name: Set up Node.js
uses: actions/setup-node@v3
with: with:
node-version: '18' node-version: '18'
- name: Install dependencies - name: Install dependencies
run: npm install run: npm ci
- name: Run tests - name: Install Biome
run: npm run check run: npm install --global --save-exact @biomejs/biome
update_mailgun_template: - name: Lint with Biome
run: npx biome ci .
- name: Lint styles
run: npm run lint:styles
- name: Check types
run: npm run typecheck
- name: Test production build
run: npm run build
email-templates:
runs-on: ubuntu-latest runs-on: ubuntu-latest
name: Update templates on Mailgun name: Update templates on Mailgun
if: github.event_name == 'push' && github.ref == 'refs/heads/feature/email-templates' if: github.event_name == 'push' && github.ref == 'refs/heads/feature/email-templates'

View File

@ -1,18 +1,42 @@
name: CI name: "deploy"
on: [push] on: [push]
jobs: jobs:
build: test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: actions/setup-node@v2 - uses: actions/setup-node@v4
with:
node-version: '18'
- name: Install deps - name: Install dependencies
run: npm ci run: npm ci
- name: Check - name: Check types
run: npm run check run: npm run typecheck
- name: Lint with Biome
run: npx biome ci .
- name: Lint styles
run: npm run lint:styles
- name: Test production build
run: npm run build
e2e:
timeout-minutes: 60
runs-on: ubuntu-latest
if: github.event.deployment_status.state == 'success'
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- name: Install dependencies
run: npm ci
- name: Install Playwright
run: npx playwright install --with-deps
- name: Run Playwright tests
run: npx playwright test
env:
BASE_URL: ${{ github.event.deployment_status.target_url }}

5
.gitignore vendored
View File

@ -17,3 +17,8 @@ stats.html
pnpm-lock.yaml pnpm-lock.yaml
bun.lockb bun.lockb
.jj .jj
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/plawright-report/

View File

@ -1,21 +0,0 @@
---
stages:
- deploy
deploy:
image:
name: alpine/git
entrypoint: [""]
stage: deploy
environment:
name: production
url: https://new.discours.io
only:
- main
script:
- mkdir ~/.ssh
- echo "${HOST_KEY}" > ~/.ssh/known_hosts
- echo "${SSH_PRIVATE_KEY}" > ~/.ssh/id_rsa
- chmod 0400 ~/.ssh/id_rsa
- git remote add github git@github.com:Discours/discoursio-webapp.git
- git push github HEAD:main

View File

@ -1,4 +0,0 @@
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npm run pre-push

View File

@ -1,5 +1,5 @@
{ {
"*.{js,ts,tsx,json,scss,css,html}": "prettier --write", "*.{js,ts,cjs,mjs,d.mts,jsx,tsx,json,jsonc,scss,css}": [
"package.json": "sort-package-json", "npm run check"
"public/locales/**/*.json": "sort-json" ]
} }

View File

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

View File

@ -1,16 +0,0 @@
{
"htmlWhitespaceSensitivity": "ignore",
"semi": false,
"singleQuote": true,
"proseWrap": "always",
"printWidth": 108,
"plugins": [],
"overrides": [
{
"files": "*.ts",
"options": {
"parser": "typescript"
}
}
]
}

View File

@ -1,31 +0,0 @@
{
"extends": [
"stylelint-config-standard-scss"
],
"plugins": [
"stylelint-order",
"stylelint-scss"
],
"rules": {
"selector-class-pattern": null,
"no-descending-specificity": null,
"scss/function-no-unknown": null,
"scss/no-global-function-names": null,
"function-url-quotes": null,
"font-family-no-missing-generic-family-keyword": null,
"order/order": [
"custom-properties",
"declarations"
],
"scss/dollar-variable-pattern": ["^[a-z][a-zA-Z]+$", {
"ignore": "global"
}],
"selector-pseudo-class-no-unknown": [
true,
{
"ignorePseudoClasses": ["global", "export"]
}
]
},
"defaultSeverity": "warning"
}

View File

@ -1,11 +1,6 @@
{ {
"extends": [ "extends": ["stylelint-config-standard-scss"],
"stylelint-config-standard-scss" "plugins": ["stylelint-order", "stylelint-scss"],
],
"plugins": [
"stylelint-order",
"stylelint-scss"
],
"rules": { "rules": {
"keyframes-name-pattern": null, "keyframes-name-pattern": null,
"declaration-block-no-redundant-longhand-properties": null, "declaration-block-no-redundant-longhand-properties": null,
@ -15,13 +10,13 @@
"scss/no-global-function-names": null, "scss/no-global-function-names": null,
"function-url-quotes": null, "function-url-quotes": null,
"font-family-no-missing-generic-family-keyword": null, "font-family-no-missing-generic-family-keyword": null,
"order/order": [ "order/order": ["custom-properties", "declarations"],
"custom-properties", "scss/dollar-variable-pattern": [
"declarations" "^[a-z][a-zA-Z]+$",
{
"ignore": "global"
}
], ],
"scss/dollar-variable-pattern": ["^[a-z][a-zA-Z]+$", {
"ignore": "global"
}],
"selector-pseudo-class-no-unknown": [ "selector-pseudo-class-no-unknown": [
true, true,
{ {

View File

@ -1,6 +1,6 @@
MIT License MIT License
Copyright (c) 2021-2023 Discours Copyright (c) 2021-2024 Discours
Permission is hereby granted, free of charge, to any person obtaining a copy Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@ -3,12 +3,6 @@
npm install npm install
npm start npm start
``` ```
with different backends
```
npm run start:local
npm run start:production
npm run start:staging
```
## Useful commands ## Useful commands
run checks run checks
@ -19,14 +13,18 @@ type checking with watch
``` ```
npm run typecheck:watch npm run typecheck:watch
``` ```
fix styles, imports, formatting and autofixable linting errors:
```
npm run fix
```
## Code generation ## Code generation
generate new SolidJS component: generate new SolidJS component:
``` ```
npx hygen component new NewComponentName npm run hygen component new NewComponentName
``` ```
generate new SolidJS context: generate new SolidJS context:
``` ```
npx hygen context new NewContextName npm run hygen context new NewContextName
``` ```

View File

@ -1,21 +0,0 @@
const { chromium } = require('playwright')
const checkUrl = async (page, targetUrl, pageName) => {
const response = await page.goto(targetUrl)
if (response.status() > 399) {
throw new Error(`Failed with response code ${response.status()}`)
}
await page.screenshot({ path: `${pageName}.jpg` })
}
const browser = await chromium.launch()
const page = await browser.newPage()
const targetUrl = process.env.ENVIRONMENT_URL || 'https://testing.discours.io'
await checkUrl(page, targetUrl, 'main')
await checkUrl(page, `${targetUrl}/authors`, 'authors')
await checkUrl(page, `${targetUrl}/topics`, 'topics')
await page.close()
await browser.close()

View File

@ -1 +0,0 @@

73
biome.json Normal file
View File

@ -0,0 +1,73 @@
{
"$schema": "https://biomejs.dev/schemas/1.5.3/schema.json",
"files": {
"include": ["*.tsx", "*.ts", "*.js", "*.json"],
"ignore": ["./dist", "./node_modules", ".husky", "docs", "gen"]
},
"vcs": {
"defaultBranch": "dev",
"useIgnoreFile": true
},
"organizeImports": {
"enabled": true,
"ignore": ["./api", "./gen"]
},
"formatter": {
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 108,
"ignore": ["./src/graphql/schema", "./gen"]
},
"javascript": {
"formatter": {
"semicolons": "asNeeded",
"quoteStyle": "single",
"trailingComma": "all",
"enabled": true,
"jsxQuoteStyle": "double",
"arrowParentheses": "always"
}
},
"linter": {
"ignore": ["*.scss", "*.md", ".DS_Store", "*.svg"],
"enabled": true,
"rules": {
"all": true,
"complexity": {
"noForEach": "off",
"useOptionalChain": "warn",
"useLiteralKeys": "off"
},
"correctness": {
"useHookAtTopLevel": "off"
},
"a11y": {
"useHeadingContent": "off",
"useKeyWithClickEvents": "off",
"useKeyWithMouseEvents": "off",
"useAnchorContent": "off",
"useValidAnchor": "off",
"useMediaCaption": "off",
"useAltText": "off",
"useButtonType": "off",
"noRedundantAlt": "off",
"noSvgWithoutTitle": "off"
},
"nursery": {
"useImportRestrictions": "off",
"useImportType": "off",
"useFilenamingConvention": "off"
},
"style": {
"useBlockStatements": "off",
"noImplicitBoolean": "off",
"useNamingConvention": "off",
"noDefaultExport": "off"
},
"suspicious": {
"noConsoleLog": "off",
"noAssignInExpressions": "off"
}
}
}
}

View File

@ -5,8 +5,7 @@ import type { Accessor, JSX } from 'solid-js'
import { createContext, createSignal, useContext } from 'solid-js' import { createContext, createSignal, useContext } from 'solid-js'
type <%= h.changeCase.pascal(name) %>ContextType = { type <%= h.changeCase.pascal(name) %>ContextType = {
actions: {
}
} }
const <%= h.changeCase.pascal(name) %>Context = createContext<<%= h.changeCase.pascal(name) %>ContextType>() const <%= h.changeCase.pascal(name) %>Context = createContext<<%= h.changeCase.pascal(name) %>ContextType>()
@ -19,9 +18,7 @@ export const <%= h.changeCase.pascal(name) %>Provider = (props: { children: JSX.
const actions = { const actions = {
} }
const value: <%= h.changeCase.pascal(name) %>ContextType = { actions } const value: <%= h.changeCase.pascal(name) %>ContextType = { ...actions }
return <<%= h.changeCase.pascal(name) %>Context.Provider value={value}>{props.children}</<%= h.changeCase.pascal(name) %>Context.Provider> return <<%= h.changeCase.pascal(name) %>Context.Provider value={value}>{props.children}</<%= h.changeCase.pascal(name) %>Context.Provider>
} }

View File

@ -1,5 +1,5 @@
--- ---
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
--- ---
--- ---
to: app/hello.js to: app/hello.js
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
``` ```
console.log(hello) console.log(hello)

View File

@ -1,5 +1,5 @@
--- ---
to: _templates/<%= name %>/<%= action || 'new' %>/hello.ejs.t to: gen/<%= name %>/<%= action || 'new' %>/hello.ejs.t
--- ---
--- ---
to: app/hello.js to: app/hello.js
@ -14,5 +14,3 @@ https://github.com/jondot/hygen
``` ```
console.log(hello) console.log(hello)

View File

@ -1,5 +1,5 @@
--- ---
to: _templates/<%= name %>/<%= action || 'new' %>/prompt.js to: gen/<%= name %>/<%= action || 'new' %>/prompt.js
--- ---
// see types of prompts: // see types of prompts:

4539
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,6 +1,6 @@
{ {
"name": "discoursio-webapp", "name": "discoursio-webapp",
"version": "0.9.1", "version": "0.9.2",
"private": true, "private": true,
"license": "MIT", "license": "MIT",
"type": "module", "type": "module",
@ -10,22 +10,22 @@
"codegen": "graphql-codegen", "codegen": "graphql-codegen",
"deploy": "graphql-codegen && npm run typecheck && vite build && vercel", "deploy": "graphql-codegen && npm run typecheck && vite build && vercel",
"dev": "vite", "dev": "vite",
"fix": "npm run lint:code:fix && npm run lint:styles:fix", "e2e": "npx playwright test --project=chromium",
"format": "npx prettier \"{,!(node_modules)/**/}*.{js,ts,tsx,json,scss,css}\" --write --ignore-path .gitignore", "fix": "npm run check:code:fix && stylelint **/*.{scss,css} --fix",
"format": "npx @biomejs/biome format src/. --write",
"hygen": "HYGEN_TMPLS=gen hygen",
"postinstall": "npm run codegen", "postinstall": "npm run codegen",
"lint": "npm run lint:code && npm run lint:styles", "check:code": "npx @biomejs/biome check src --log-kind=compact --verbose",
"lint:code": "eslint .", "check:code:fix": "npx @biomejs/biome check src --log-kind=compact --verbose --apply-unsafe",
"lint:code:fix": "eslint . --fix", "lint": "npm run lint:code && stylelint **/*.{scss,css}",
"lint:code": "npx @biomejs/biome lint src --log-kind=compact --verbose",
"lint:code:fix": "npx @biomejs/biome lint src --apply-unsafe --log-kind=compact --verbose",
"lint:styles": "stylelint **/*.{scss,css}", "lint:styles": "stylelint **/*.{scss,css}",
"lint:styles:fix": "stylelint **/*.{scss,css} --fix", "lint:styles:fix": "stylelint **/*.{scss,css} --fix",
"pre-commit": "lint-staged", "pre-commit": "lint-staged",
"pre-push": "npm run typecheck",
"prepare": "husky install", "prepare": "husky install",
"preview": "vite preview", "preview": "vite preview",
"start": "vite", "start": "vite",
"start:local": "cross-env PUBLIC_API_URL=http://127.0.0.1:8080 vite",
"start:production": "cross-env PUBLIC_API_URL=https://v2.discours.io vite",
"start:staging": "cross-env PUBLIC_API_URL=https://testapi.discours.io vite",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
"typecheck:watch": "tsc --noEmit --watch" "typecheck:watch": "tsc --noEmit --watch"
}, },
@ -44,19 +44,21 @@
}, },
"devDependencies": { "devDependencies": {
"@babel/core": "7.23.3", "@babel/core": "7.23.3",
"@biomejs/biome": "^1.5.3",
"@graphql-codegen/cli": "5.0.0", "@graphql-codegen/cli": "5.0.0",
"@graphql-codegen/typescript": "4.0.1", "@graphql-codegen/typescript": "4.0.1",
"@graphql-codegen/typescript-operations": "4.0.1", "@graphql-codegen/typescript-operations": "4.0.1",
"@graphql-codegen/typescript-urql": "4.0.0", "@graphql-codegen/typescript-urql": "4.0.0",
"@graphql-codegen/urql-introspection": "3.0.0", "@graphql-codegen/urql-introspection": "3.0.0",
"@graphql-tools/url-loader": "7.17.18", "@graphql-tools/url-loader": "8.0.1",
"@graphql-typed-document-node/core": "3.2.0", "@graphql-typed-document-node/core": "3.2.0",
"@hocuspocus/provider": "2.0.6", "@hocuspocus/provider": "2.0.6",
"@microsoft/fetch-event-source": "^2.0.1", "@microsoft/fetch-event-source": "^2.0.1",
"@nanostores/router": "0.11.0", "@nanostores/router": "0.13.0",
"@nanostores/solid": "0.4.2", "@nanostores/solid": "0.4.2",
"@playwright/test": "1.41.2",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@sentry/browser": "5.30.0", "@sentry/browser": "7.99.0",
"@solid-primitives/media": "2.2.3", "@solid-primitives/media": "2.2.3",
"@solid-primitives/memo": "1.2.4", "@solid-primitives/memo": "1.2.4",
"@solid-primitives/share": "2.0.4", "@solid-primitives/share": "2.0.4",
@ -95,25 +97,13 @@
"@tiptap/extension-youtube": "2.0.3", "@tiptap/extension-youtube": "2.0.3",
"@types/js-cookie": "3.0.6", "@types/js-cookie": "3.0.6",
"@types/node": "20.9.0", "@types/node": "20.9.0",
"@typescript-eslint/eslint-plugin": "6.10.0", "@urql/core": "4.2.3",
"@typescript-eslint/parser": "6.10.0",
"@urql/core": "3.2.2",
"@urql/devtools": "2.0.3", "@urql/devtools": "2.0.3",
"babel-preset-solid": "1.8.4", "babel-preset-solid": "1.8.4",
"bootstrap": "5.3.2", "bootstrap": "5.3.2",
"change-case": "5.2.0", "change-case": "5.2.0",
"clsx": "2.0.0", "clsx": "2.0.0",
"cross-env": "7.0.3", "cross-env": "7.0.3",
"eslint": "8.53.0",
"eslint-config-stylelint": "20.0.0",
"eslint-import-resolver-typescript": "3.6.1",
"eslint-plugin-import": "2.29.0",
"eslint-plugin-jest": "27.6.0",
"eslint-plugin-jsx-a11y": "6.8.0",
"eslint-plugin-promise": "6.1.1",
"eslint-plugin-solid": "0.13.0",
"eslint-plugin-sonarjs": "0.23.0",
"eslint-plugin-unicorn": "49.0.0",
"fast-deep-equal": "3.1.3", "fast-deep-equal": "3.1.3",
"graphql": "16.8.1", "graphql": "16.8.1",
"graphql-tag": "2.12.6", "graphql-tag": "2.12.6",
@ -127,8 +117,6 @@
"loglevel": "1.8.1", "loglevel": "1.8.1",
"loglevel-plugin-prefix": "0.8.4", "loglevel-plugin-prefix": "0.8.4",
"nanostores": "0.9.5", "nanostores": "0.9.5",
"prettier": "3.1.0",
"prettier-eslint": "16.1.2",
"prosemirror-history": "1.3.0", "prosemirror-history": "1.3.0",
"prosemirror-trailing-node": "2.0.3", "prosemirror-trailing-node": "2.0.3",
"prosemirror-view": "1.30.2", "prosemirror-view": "1.30.2",

77
playwright.config.ts Normal file
View File

@ -0,0 +1,77 @@
import { defineConfig, devices } from '@playwright/test'
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// require('dotenv').config();
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './tests',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('/')`. */
// baseURL: 'http://127.0.0.1:3000',
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
// {
// name: 'Mobile Chrome',
// use: { ...devices['Pixel 5'] },
// },
// {
// name: 'Mobile Safari',
// use: { ...devices['iPhone 12'] },
// },
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'npm run start',
// url: 'http://127.0.0.1:3000',
// reuseExistingServer: !process.env.CI,
// },
})

View File

@ -43,7 +43,7 @@ import { ProfileSubscriptionsPage } from '../pages/profile/profileSubscriptions.
import { SearchPage } from '../pages/search.page' import { SearchPage } from '../pages/search.page'
import { TopicPage } from '../pages/topic.page' import { TopicPage } from '../pages/topic.page'
import { ROUTES, useRouter } from '../stores/router' import { ROUTES, useRouter } from '../stores/router'
import { hideModal, MODALS, showModal } from '../stores/ui' import { MODALS, hideModal, showModal } from '../stores/ui'
// TODO: lazy load // TODO: lazy load
// const SomePage = lazy(() => import('./Pages/SomePage')) // const SomePage = lazy(() => import('./Pages/SomePage'))

View File

@ -1,11 +1,11 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { Topic } from '../../../graphql/schema/core.gen' import { Topic } from '../../../graphql/schema/core.gen'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { CardTopic } from '../../Feed/CardTopic'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
import { CardTopic } from '../../Feed/CardTopic'
import styles from './AudioHeader.module.scss' import styles from './AudioHeader.module.scss'

View File

@ -1,4 +1,4 @@
import { createEffect, createMemo, createSignal, on, onMount, Show } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on, onMount } from 'solid-js'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler' import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'

View File

@ -1,5 +1,5 @@
import { gtag } from 'ga-gtag' import { gtag } from 'ga-gtag'
import { createSignal, For, lazy, Show } from 'solid-js' import { For, Show, createSignal, lazy } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'

View File

@ -1,6 +1,6 @@
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal, For, lazy, Suspense } from 'solid-js' import { For, Show, Suspense, createMemo, createSignal, lazy } from 'solid-js'
import { useConfirm } from '../../../context/confirm' import { useConfirm } from '../../../context/confirm'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -9,10 +9,10 @@ import { useSession } from '../../../context/session'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
import { Author, Reaction, ReactionKind } from '../../../graphql/schema/core.gen' import { Author, Reaction, ReactionKind } from '../../../graphql/schema/core.gen'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { AuthorLink } from '../../Author/AuthorLink' import { AuthorLink } from '../../Author/AuthorLink'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import { Icon } from '../../_shared/Icon'
import { ShowIfAuthenticated } from '../../_shared/ShowIfAuthenticated'
import { CommentDate } from '../CommentDate' import { CommentDate } from '../CommentDate'
import { CommentRatingControl } from '../CommentRatingControl' import { CommentRatingControl } from '../CommentRatingControl'
@ -39,23 +39,14 @@ export const Comment = (props: Props) => {
const [editMode, setEditMode] = createSignal(false) const [editMode, setEditMode] = createSignal(false)
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const { author } = useSession() const { author } = useSession()
const { createReaction, deleteReaction, updateReaction } = useReactions()
const { const { showConfirm } = useConfirm()
actions: { createReaction, deleteReaction, updateReaction }, const { showSnackbar } = useSnackbar()
} = useReactions()
const {
actions: { showConfirm },
} = useConfirm()
const {
actions: { showSnackbar },
} = useSnackbar()
const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === author()?.slug) const isCommentAuthor = createMemo(() => props.comment.created_by?.slug === author()?.slug)
const comment = createMemo(() => props.comment) const comment = createMemo(() => props.comment)
const body = createMemo(() => (comment().body || '').trim()) const body = createMemo(() => (comment().body || '').trim())
const remove = async () => { const remove = async () => {
if (comment()?.id) { if (comment()?.id) {
try { try {

View File

@ -19,13 +19,8 @@ type Props = {
export const CommentRatingControl = (props: Props) => { export const CommentRatingControl = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { author } = useSession() const { author } = useSession()
const { const { showSnackbar } = useSnackbar()
actions: { showSnackbar }, const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
} = useSnackbar()
const {
reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions()
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
Object.values(reactionEntities).some( Object.values(reactionEntities).some(
@ -86,7 +81,7 @@ export const CommentRatingControl = (props: Props) => {
<div class={styles.commentRating}> <div class={styles.commentRating}>
<button <button
role="button" role="button"
disabled={!canVote() || !author()} disabled={!(canVote() && author())}
onClick={() => handleRatingChange(true)} onClick={() => handleRatingChange(true)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, { class={clsx(styles.commentRatingControl, styles.commentRatingControlUp, {
[styles.voted]: isUpvoted(), [styles.voted]: isUpvoted(),
@ -112,7 +107,7 @@ export const CommentRatingControl = (props: Props) => {
</Popup> </Popup>
<button <button
role="button" role="button"
disabled={!canVote() || !author()} disabled={!(canVote() && author())}
onClick={() => handleRatingChange(false)} onClick={() => handleRatingChange(false)}
class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, { class={clsx(styles.commentRatingControl, styles.commentRatingControlDown, {
[styles.voted]: isDownvoted(), [styles.voted]: isDownvoted(),

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createMemo, createSignal, onMount, For, lazy } from 'solid-js' import { For, Show, createMemo, createSignal, lazy, onMount } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
@ -22,8 +22,8 @@ const sortCommentsByRating = (a: Reaction, b: Reaction): -1 | 0 | 1 => {
return 0 return 0
} }
const x = (a?.stat && a.stat.rating) || 0 const x = a.stat?.rating || 0
const y = (b?.stat && b.stat.rating) || 0 const y = b.stat?.rating || 0
if (x > y) { if (x > y) {
return 1 return 1
@ -49,11 +49,7 @@ export const CommentsTree = (props: Props) => {
const [newReactions, setNewReactions] = createSignal<Reaction[]>([]) const [newReactions, setNewReactions] = createSignal<Reaction[]>([])
const [clearEditor, setClearEditor] = createSignal(false) const [clearEditor, setClearEditor] = createSignal(false)
const [clickedReplyId, setClickedReplyId] = createSignal<number>() const [clickedReplyId, setClickedReplyId] = createSignal<number>()
const { reactionEntities, createReaction } = useReactions()
const {
reactionEntities,
actions: { createReaction },
} = useReactions()
const comments = createMemo(() => const comments = createMemo(() =>
Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'), Object.values(reactionEntities).filter((reaction) => reaction.kind === 'COMMENT'),

View File

@ -1,9 +1,6 @@
import type { CoverImageProps } from './types' import type { CoverImageProps } from './types'
import { CoverImage1 } from './images/CoverImage1' import { CoverImage1 } from './images/CoverImage1'
import { CoverImage10 } from './images/CoverImage10'
import { CoverImage11 } from './images/CoverImage11'
import { CoverImage12 } from './images/CoverImage12'
import { CoverImage2 } from './images/CoverImage2' import { CoverImage2 } from './images/CoverImage2'
import { CoverImage3 } from './images/CoverImage3' import { CoverImage3 } from './images/CoverImage3'
import { CoverImage4 } from './images/CoverImage4' import { CoverImage4 } from './images/CoverImage4'
@ -12,6 +9,9 @@ import { CoverImage6 } from './images/CoverImage6'
import { CoverImage7 } from './images/CoverImage7' import { CoverImage7 } from './images/CoverImage7'
import { CoverImage8 } from './images/CoverImage8' import { CoverImage8 } from './images/CoverImage8'
import { CoverImage9 } from './images/CoverImage9' import { CoverImage9 } from './images/CoverImage9'
import { CoverImage10 } from './images/CoverImage10'
import { CoverImage11 } from './images/CoverImage11'
import { CoverImage12 } from './images/CoverImage12'
// not pretty, but I don't want to use dynamic imports // not pretty, but I don't want to use dynamic imports
const coverImages = [ const coverImages = [

View File

@ -5,7 +5,7 @@ import { createPopper } from '@popperjs/core'
import { Link, Meta } from '@solidjs/meta' import { Link, Meta } from '@solidjs/meta'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { install } from 'ga-gtag' import { install } from 'ga-gtag'
import { createEffect, For, createMemo, onMount, Show, createSignal, onCleanup, on } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { isServer } from 'solid-js/web' import { isServer } from 'solid-js/web'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
@ -15,9 +15,14 @@ import { MediaItem } from '../../pages/types'
import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router' import { DEFAULT_HEADER_OFFSET, router, useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import { capitalize } from '../../utils/capitalize' import { capitalize } from '../../utils/capitalize'
import { isCyrillic } from '../../utils/cyrillic'
import { getImageUrl, getOpenGraphImageUrl } from '../../utils/getImageUrl' import { getImageUrl, getOpenGraphImageUrl } from '../../utils/getImageUrl'
import { getDescription, getKeywords } from '../../utils/meta' import { getDescription, getKeywords } from '../../utils/meta'
import { isCyrillic } from '../../utils/translate'
import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { Modal } from '../Nav/Modal'
import { TableOfContents } from '../TableOfContents'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Image } from '../_shared/Image' import { Image } from '../_shared/Image'
import { InviteMembers } from '../_shared/InviteMembers' import { InviteMembers } from '../_shared/InviteMembers'
@ -26,20 +31,15 @@ import { Popover } from '../_shared/Popover'
import { ShareModal } from '../_shared/ShareModal' import { ShareModal } from '../_shared/ShareModal'
import { ImageSwiper } from '../_shared/SolidSwiper' import { ImageSwiper } from '../_shared/SolidSwiper'
import { VideoPlayer } from '../_shared/VideoPlayer' import { VideoPlayer } from '../_shared/VideoPlayer'
import { AuthorBadge } from '../Author/AuthorBadge'
import { CardTopic } from '../Feed/CardTopic'
import { FeedArticlePopup } from '../Feed/FeedArticlePopup'
import { Modal } from '../Nav/Modal'
import { TableOfContents } from '../TableOfContents'
import { AudioHeader } from './AudioHeader' import { AudioHeader } from './AudioHeader'
import { AudioPlayer } from './AudioPlayer' import { AudioPlayer } from './AudioPlayer'
import { CommentsTree } from './CommentsTree' import { CommentsTree } from './CommentsTree'
import { getShareUrl, SharePopup } from './SharePopup' import { SharePopup, getShareUrl } from './SharePopup'
import { ShoutRatingControl } from './ShoutRatingControl' import { ShoutRatingControl } from './ShoutRatingControl'
import styles from './Article.module.scss'
import stylesHeader from '../Nav/Header/Header.module.scss' import stylesHeader from '../Nav/Header/Header.module.scss'
import styles from './Article.module.scss'
type Props = { type Props = {
article: Shout article: Shout
@ -69,32 +69,27 @@ const scrollTo = (el: HTMLElement) => {
const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi const imgSrcRegExp = /<img[^>]+src\s*=\s*["']([^"']+)["']/gi
export const FullArticle = (props: Props) => { export const FullArticle = (props: Props) => {
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
const { loadReactionsBy } = useReactions()
const [selectedImage, setSelectedImage] = createSignal('') const [selectedImage, setSelectedImage] = createSignal('')
const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false) const [isReactionsLoaded, setIsReactionsLoaded] = createSignal(false)
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const { t, formatDate, lang } = useLocalize() const { t, formatDate, lang } = useLocalize()
const { const { author, isAuthenticated, requireAuthentication } = useSession()
author,
isAuthenticated,
actions: { requireAuthentication },
} = useSession()
const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000))) const formattedDate = createMemo(() => formatDate(new Date(props.article.published_at * 1000)))
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
const mainTopic = createMemo(() => { const mainTopic = createMemo(() => {
const main_topic_slug = props.article.topics.length > 0 ? props.article.main_topic : null const mainTopicSlug = props.article.topics.length > 0 ? props.article.main_topic : null
const mt = props.article.topics.find((tpc: Topic) => tpc.slug === main_topic_slug) const mt = props.article.topics.find((tpc: Topic) => tpc.slug === mainTopicSlug)
if (mt) { if (mt) {
mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title mt.title = lang() === 'en' ? capitalize(mt.slug.replace(/-/, ' ')) : mt.title
return mt return mt
} else {
return props.article.topics[0]
} }
return props.article.topics[0]
}) })
const canEdit = () => props.article.authors?.some((a) => Boolean(a) && a?.slug === author()?.slug)
const handleBookmarkButtonClick = (ev) => { const handleBookmarkButtonClick = (ev) => {
requireAuthentication(() => { requireAuthentication(() => {
// TODO: implement bookmark clicked // TODO: implement bookmark clicked
@ -154,8 +149,6 @@ export const FullArticle = (props: Props) => {
scrollTo(commentsRef.current) scrollTo(commentsRef.current)
} }
const { searchParams, changeSearchParams } = useRouter<ArticlePageSearchParams>()
createEffect(() => { createEffect(() => {
if (props.scrollToComments) { if (props.scrollToComments) {
scrollToComments() scrollToComments()
@ -185,10 +178,6 @@ export const FullArticle = (props: Props) => {
} }
}) })
const {
actions: { loadReactionsBy },
} = useReactions()
const clickHandlers = [] const clickHandlers = []
const documentClickHandlers = [] const documentClickHandlers = []
@ -284,7 +273,7 @@ export const FullArticle = (props: Props) => {
} }
const handleArticleBodyClick = (event) => { const handleArticleBodyClick = (event) => {
if (event.target.tagName === 'IMG' && !event.target.dataset['disableLightbox']) { if (event.target.tagName === 'IMG' && !event.target.dataset.disableLightbox) {
const src = event.target.src const src = event.target.src
openLightbox(getImageUrl(src)) openLightbox(getImageUrl(src))
} }
@ -293,7 +282,7 @@ export const FullArticle = (props: Props) => {
// Check iframes size // Check iframes size
const articleContainer: { current: HTMLElement } = { current: null } const articleContainer: { current: HTMLElement } = { current: null }
const updateIframeSizes = () => { const updateIframeSizes = () => {
if (!articleContainer?.current || !props.article.body) return if (!(articleContainer?.current && props.article.body)) return
const iframes = articleContainer?.current?.querySelectorAll('iframe') const iframes = articleContainer?.current?.querySelectorAll('iframe')
if (!iframes) return if (!iframes) return
const containerWidth = articleContainer.current?.offsetWidth const containerWidth = articleContainer.current?.offsetWidth
@ -337,8 +326,8 @@ export const FullArticle = (props: Props) => {
const cover = props.article.cover ?? 'production/image/logo_image.png' const cover = props.article.cover ?? 'production/image/logo_image.png'
const ogImage = getOpenGraphImageUrl(cover, { const ogImage = getOpenGraphImageUrl(cover, {
title: props.article.title, title: props.article.title,
topic: mainTopic().title, topic: mainTopic()?.title || '',
author: props.article.authors[0].name, author: props.article?.authors[0]?.name || '',
width: 1200, width: 1200,
}) })

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, Show } from 'solid-js' import { Show, createMemo, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { useReactions } from '../../context/reactions' import { useReactions } from '../../context/reactions'
@ -19,16 +19,8 @@ interface ShoutRatingControlProps {
export const ShoutRatingControl = (props: ShoutRatingControlProps) => { export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const { author, requireAuthentication } = useSession()
author, const { reactionEntities, createReaction, deleteReaction, loadReactionsBy } = useReactions()
actions: { requireAuthentication },
} = useSession()
const {
reactionEntities,
actions: { createReaction, deleteReaction, loadReactionsBy },
} = useReactions()
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
const checkReaction = (reactionKind: ReactionKind) => const checkReaction = (reactionKind: ReactionKind) =>
@ -60,7 +52,7 @@ export const ShoutRatingControl = (props: ShoutRatingControlProps) => {
return deleteReaction(reactionToDelete.id) return deleteReaction(reactionToDelete.id)
} }
const handleRatingChange = async (isUpvote: boolean) => { const handleRatingChange = (isUpvote: boolean) => {
requireAuthentication(async () => { requireAuthentication(async () => {
setIsLoading(true) setIsLoading(true)
if (isUpvoted()) { if (isUpvoted()) {

View File

@ -1,5 +1,5 @@
import { JSX, createSignal } from 'solid-js'
import './Tooltip.scss' import './Tooltip.scss'
import { createSignal, JSX } from 'solid-js'
interface TooltipProps { interface TooltipProps {
children?: JSX.Element children?: JSX.Element

View File

@ -1,4 +1,4 @@
import { createEffect, JSX, Show } from 'solid-js' import { JSX, Show, createEffect } from 'solid-js'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { RootSearchParams } from '../../pages/types' import { RootSearchParams } from '../../pages/types'
@ -15,7 +15,7 @@ export const AuthGuard = (props: Props) => {
const { isAuthenticated, isSessionLoaded } = useSession() const { isAuthenticated, isSessionLoaded } = useSession()
const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>() const { changeSearchParams } = useRouter<RootSearchParams & AuthModalSearchParams>()
createEffect(async () => { createEffect(() => {
if (props.disabled) { if (props.disabled) {
return return
} }

View File

@ -1,6 +1,6 @@
import { openPage } from '@nanostores/router' import { openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Match, on, Show, Switch } from 'solid-js' import { Match, Show, Switch, createEffect, createMemo, createSignal, on } from 'solid-js'
import { useFollowing } from '../../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -8,16 +8,16 @@ import { useMediaQuery } from '../../../context/mediaQuery'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { Author, FollowingEntity } from '../../../graphql/schema/core.gen' import { Author, FollowingEntity } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { isCyrillic } from '../../../utils/cyrillic'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { CheckButton } from '../../_shared/CheckButton' import { CheckButton } from '../../_shared/CheckButton'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AuthorBadge.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss' import stylesButton from '../../_shared/Button/Button.module.scss'
import styles from './AuthorBadge.module.scss'
type FollowedInfo = { type FollowedInfo = {
value?: boolean value?: boolean
@ -36,11 +36,7 @@ type Props = {
} }
export const AuthorBadge = (props: Props) => { export const AuthorBadge = (props: Props) => {
const { mediaMatches } = useMediaQuery() const { mediaMatches } = useMediaQuery()
const { const { author, requireAuthentication } = useSession()
author,
actions: { requireAuthentication },
} = useSession()
const [isMobileView, setIsMobileView] = createSignal(false) const [isMobileView, setIsMobileView] = createSignal(false)
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
@ -55,7 +51,7 @@ export const AuthorBadge = (props: Props) => {
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity // eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, 'inbox')
changeSearchParams({ changeSearchParams({
initChat: props.author.id.toString(), initChat: props.author.id.toString(),
}) })

View File

@ -2,7 +2,7 @@ import type { Author, Community } from '../../../graphql/schema/core.gen'
import { openPage, redirectPage } from '@nanostores/router' import { openPage, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, For, onMount, Show } from 'solid-js' import { For, Show, createEffect, createMemo, createSignal, onMount } from 'solid-js'
import { useFollowing } from '../../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -10,19 +10,19 @@ import { useSession } from '../../../context/session'
import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen' import { FollowingEntity, Topic } from '../../../graphql/schema/core.gen'
import { SubscriptionFilter } from '../../../pages/types' import { SubscriptionFilter } from '../../../pages/types'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { isCyrillic } from '../../../utils/cyrillic'
import { isAuthor } from '../../../utils/isAuthor' import { isAuthor } from '../../../utils/isAuthor'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { Button } from '../../_shared/Button' import { isCyrillic } from '../../../utils/translate'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient' import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { TopicBadge } from '../../Topic/TopicBadge' import { TopicBadge } from '../../Topic/TopicBadge'
import { Button } from '../../_shared/Button'
import { ShowOnlyOnClient } from '../../_shared/ShowOnlyOnClient'
import { AuthorBadge } from '../AuthorBadge' import { AuthorBadge } from '../AuthorBadge'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AuthorCard.module.scss'
import stylesButton from '../../_shared/Button/Button.module.scss' import stylesButton from '../../_shared/Button/Button.module.scss'
import styles from './AuthorCard.module.scss'
type Props = { type Props = {
author: Author author: Author
@ -31,12 +31,7 @@ type Props = {
} }
export const AuthorCard = (props: Props) => { export const AuthorCard = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { const { author, isSessionLoaded, requireAuthentication } = useSession()
author,
isSessionLoaded,
actions: { requireAuthentication },
} = useSession()
const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([]) const [authorSubs, setAuthorSubs] = createSignal<Array<Author | Topic | Community>>([])
const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all') const [subscriptionFilter, setSubscriptionFilter] = createSignal<SubscriptionFilter>('all')
const [isFollowed, setIsFollowed] = createSignal<boolean>() const [isFollowed, setIsFollowed] = createSignal<boolean>()
@ -66,7 +61,7 @@ export const AuthorCard = (props: Props) => {
const initChat = () => { const initChat = () => {
// eslint-disable-next-line solid/reactivity // eslint-disable-next-line solid/reactivity
requireAuthentication(() => { requireAuthentication(() => {
openPage(router, `inbox`) openPage(router, 'inbox')
changeSearchParams({ changeSearchParams({
initChat: props.author.id.toString(), initChat: props.author.id.toString(),
}) })
@ -157,7 +152,9 @@ export const AuthorCard = (props: Props) => {
class={styles.subscribersItem} class={styles.subscribersItem}
/> />
) )
} else if ('title' in f) { }
if ('title' in f) {
return ( return (
<Userpic <Userpic
size={'XS'} size={'XS'}
@ -167,6 +164,7 @@ export const AuthorCard = (props: Props) => {
/> />
) )
} }
return null return null
}} }}
</For> </For>
@ -188,7 +186,7 @@ export const AuthorCard = (props: Props) => {
class={styles.socialLink} class={styles.socialLink}
href={link.startsWith('http') ? link : `https://${link}`} href={link.startsWith('http') ? link : `https://${link}`}
target="_blank" target="_blank"
rel="nofollow noopener" rel="nofollow noopener noreferrer"
> >
<span class={styles.authorSubscribeSocialLabel}> <span class={styles.authorSubscribeSocialLabel}>
{link.startsWith('http') ? link : `https://${link}`} {link.startsWith('http') ? link : `https://${link}`}

View File

@ -4,8 +4,8 @@ import { createMemo } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Author } from '../../../graphql/schema/core.gen' import { Author } from '../../../graphql/schema/core.gen'
import { capitalize } from '../../../utils/capitalize' import { capitalize } from '../../../utils/capitalize'
import { isCyrillic } from '../../../utils/cyrillic'
import { translit } from '../../../utils/ru2en' import { translit } from '../../../utils/ru2en'
import { isCyrillic } from '../../../utils/translate'
import { Userpic } from '../Userpic' import { Userpic } from '../Userpic'
import styles from './AhtorLink.module.scss' import styles from './AhtorLink.module.scss'

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, Show } from 'solid-js' import { Show, createMemo } from 'solid-js'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
@ -22,7 +22,7 @@ export const Userpic = (props: Props) => {
const letters = () => { const letters = () => {
if (!props.name) return if (!props.name) return
const names = props.name ? props.name.split(' ') : [] const names = props.name ? props.name.split(' ') : []
return names[0][0 ?? names[0][0]] + '.' + (names.length > 1 ? names[1][0] + '.' : '') return `${names[0][0 ?? names[0][0]]}.${names.length > 1 ? `${names[1][0]}.` : ''}`
} }
const avatarSize = createMemo(() => { const avatarSize = createMemo(() => {
@ -48,7 +48,7 @@ export const Userpic = (props: Props) => {
return ( return (
<div <div
class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], { class={clsx(styles.Userpic, props.class, styles[props.size ?? 'M'], {
['cursorPointer']: props.onClick, cursorPointer: props.onClick,
})} })}
onClick={props.onClick} onClick={props.onClick}
> >

View File

@ -7,6 +7,9 @@ import { showModal } from '../../stores/ui'
import styles from './Donate.module.scss' import styles from './Donate.module.scss'
// biome-ignore lint/suspicious/noExplicitAny: <explanation>
type DWindow = Window & { cp: any }
export const Donate = () => { export const Donate = () => {
const { t } = useLocalize() const { t } = useLocalize()
const once = '' const once = ''
@ -24,43 +27,44 @@ export const Donate = () => {
const [showingPayment, setShowingPayment] = createSignal<boolean>() const [showingPayment, setShowingPayment] = createSignal<boolean>()
const [period, setPeriod] = createSignal(monthly) const [period, setPeriod] = createSignal(monthly)
const [amount, setAmount] = createSignal(0) const [amount, setAmount] = createSignal(0)
const { const { showSnackbar } = useSnackbar()
actions: { showSnackbar },
} = useSnackbar()
const initiated = () => { const initiated = () => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any try {
const CloudPayments = window['cp'] // Checkout(cpOptions) const { cp: CloudPayments } = window as unknown as DWindow
setWidget(new CloudPayments())
console.log('[donate] payments initiated') setWidget(new CloudPayments())
setCustomerReciept({ console.log('[donate] payments initiated')
Items: [ setCustomerReciept({
//товарные позиции Items: [
{ //товарные позиции
label: cpOptions.description, //наименование товара {
price: amount() || 0, //цена label: cpOptions.description, //наименование товара
quantity: 1, //количество price: amount() || 0, //цена
amount: amount() || 0, //сумма quantity: 1, //количество
vat: 20, //ставка НДС amount: amount() || 0, //сумма
method: 0, // тег-1214 признак способа расчета - признак способа расчета vat: 20, //ставка НДС
object: 0, // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета method: 0, // тег-1214 признак способа расчета - признак способа расчета
object: 0, // тег-1212 признак предмета расчета - признак предмета товара, работы, услуги, платежа, выплаты, иного предмета расчета
},
],
// taxationSystem: 0, //система налогообложения; необязательный, если у вас одна система налогообложения
// email: 'user@example.com', //e-mail покупателя, если нужно отправить письмо с чеком
// phone: '', //телефон покупателя в любом формате, если нужно отправить сообщение со ссылкой на чек
isBso: false, //чек является бланком строгой отчетности
amounts: {
electronic: amount(), // Сумма оплаты электронными деньгами
advancePayment: 0, // Сумма из предоплаты (зачетом аванса) (2 знака после запятой)
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
provision: 0, // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
}, },
], })
// taxationSystem: 0, //система налогообложения; необязательный, если у вас одна система налогообложения } catch (error) {
// email: 'user@example.com', //e-mail покупателя, если нужно отправить письмо с чеком console.error(error)
// phone: '', //телефон покупателя в любом формате, если нужно отправить сообщение со ссылкой на чек }
isBso: false, //чек является бланком строгой отчетности
amounts: {
electronic: amount(), // Сумма оплаты электронными деньгами
advancePayment: 0, // Сумма из предоплаты (зачетом аванса) (2 знака после запятой)
credit: 0, // Сумма постоплатой(в кредит) (2 знака после запятой)
provision: 0, // Сумма оплаты встречным предоставлением (сертификаты, др. мат.ценности) (2 знака после запятой)
},
})
} }
onMount(() => { onMount(() => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const script = document.createElement('script') const script = document.createElement('script')
script.type = 'text/javascript' script.type = 'text/javascript'
script.src = 'https://widget.cloudpayments.ru/bundles/cloudpayments.js' script.src = 'https://widget.cloudpayments.ru/bundles/cloudpayments.js'
@ -76,8 +80,8 @@ export const Donate = () => {
const choice: HTMLInputElement | undefined | null = const choice: HTMLInputElement | undefined | null =
amountSwitchElement?.querySelector('input[type=radio]:checked') amountSwitchElement?.querySelector('input[type=radio]:checked')
setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0')) setAmount(Number.parseInt(customAmountElement?.value || choice?.value || '0'))
console.log('[donate] input amount ' + amount) console.log(`[donate] input amount ${amount}`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any // biome-ignore lint/suspicious/noExplicitAny: it's a widget!
;(widget() as any).charge( ;(widget() as any).charge(
{ {
// options // options
@ -105,7 +109,7 @@ export const Donate = () => {
console.debug('[donate] options', opts) console.debug('[donate] options', opts)
showModal('thank') showModal('thank')
}, },
function (reason: string, options) { (reason: string, options) => {
// fail // fail
// действие при неуспешной оплате // действие при неуспешной оплате
console.debug('[donate] options', options) console.debug('[donate] options', options)

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, For } from 'solid-js' import { For, createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
@ -11,7 +11,7 @@ export const Footer = () => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const changeLangTitle = createMemo(() => (lang() === 'ru' ? 'English' : 'Русский')) const changeLangTitle = createMemo(() => (lang() === 'ru' ? 'English' : 'Русский'))
const changeLangLink = createMemo(() => '?lng=' + (lang() === 'ru' ? 'en' : 'ru')) const changeLangLink = createMemo(() => `?lng=${lang() === 'ru' ? 'en' : 'ru'}`)
const links = createMemo(() => [ const links = createMemo(() => [
{ {
header: 'About the project', header: 'About the project',
@ -89,7 +89,7 @@ export const Footer = () => {
}, },
]) ])
const SOCIAL = [ const social = [
{ {
name: 'facebook', name: 'facebook',
href: 'https://facebook.com/discoursio', href: 'https://facebook.com/discoursio',
@ -146,7 +146,7 @@ export const Footer = () => {
<a href="/about/terms-of-use">{t('Terms of use')}</a> <a href="/about/terms-of-use">{t('Terms of use')}</a>
</div> </div>
<div class={clsx(styles.footerCopyrightSocial, 'col-md-6 col-lg-4')}> <div class={clsx(styles.footerCopyrightSocial, 'col-md-6 col-lg-4')}>
<For each={SOCIAL}> <For each={social}>
{(social) => ( {(social) => (
<div class={clsx(styles.socialItem, styles[`socialItem${social.name}`])}> <div class={clsx(styles.socialItem, styles[`socialItem${social.name}`])}>
<a href={social.href}> <a href={social.href}>

View File

@ -20,13 +20,8 @@ type Props = {
export const Draft = (props: Props) => { export const Draft = (props: Props) => {
const { t, formatDate } = useLocalize() const { t, formatDate } = useLocalize()
const { const { showConfirm } = useConfirm()
actions: { showConfirm }, const { showSnackbar } = useSnackbar()
} = useConfirm()
const {
actions: { showSnackbar },
} = useSnackbar()
const handlePublishLinkClick = (e) => { const handlePublishLinkClick = (e) => {
e.preventDefault() e.preventDefault()

View File

@ -6,8 +6,8 @@ import { Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { MediaItem } from '../../../pages/types' import { MediaItem } from '../../../pages/types'
import { composeMediaItems } from '../../../utils/composeMediaItems' import { composeMediaItems } from '../../../utils/composeMediaItems'
import { DropArea } from '../../_shared/DropArea'
import { AudioPlayer } from '../../Article/AudioPlayer' import { AudioPlayer } from '../../Article/AudioPlayer'
import { DropArea } from '../../_shared/DropArea'
import styles from './AudioUploader.module.scss' import styles from './AudioUploader.module.scss'

View File

@ -3,9 +3,9 @@ import type { Editor } from '@tiptap/core'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { renderUploadedImage } from '../../../utils/renderUploadedImage' import { renderUploadedImage } from '../../../utils/renderUploadedImage'
import { Modal } from '../../Nav/Modal'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Popover } from '../../_shared/Popover' import { Popover } from '../../_shared/Popover'
import { Modal } from '../../Nav/Modal'
import { UploadModalContent } from '../UploadModalContent' import { UploadModalContent } from '../UploadModalContent'
import styles from './BubbleMenu.module.scss' import styles from './BubbleMenu.module.scss'

View File

@ -1,7 +1,7 @@
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show, For } from 'solid-js' import { For, Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'

View File

@ -38,8 +38,9 @@ import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { handleImageUpload } from '../../utils/handleImageUpload' import { handleImageUpload } from '../../utils/handleImageUpload'
import { FigureBubbleMenu, BlockquoteBubbleMenu, IncutBubbleMenu } from './BubbleMenu' import { BlockquoteBubbleMenu, FigureBubbleMenu, IncutBubbleMenu } from './BubbleMenu'
import { EditorFloatingMenu } from './EditorFloatingMenu' import { EditorFloatingMenu } from './EditorFloatingMenu'
import { TextBubbleMenu } from './TextBubbleMenu'
import Article from './extensions/Article' import Article from './extensions/Article'
import { CustomBlockquote } from './extensions/CustomBlockquote' import { CustomBlockquote } from './extensions/CustomBlockquote'
import { Figcaption } from './extensions/Figcaption' import { Figcaption } from './extensions/Figcaption'
@ -49,7 +50,6 @@ import { Iframe } from './extensions/Iframe'
import { Span } from './extensions/Span' import { Span } from './extensions/Span'
import { ToggleTextWrap } from './extensions/ToggleTextWrap' import { ToggleTextWrap } from './extensions/ToggleTextWrap'
import { TrailingNode } from './extensions/TrailingNode' import { TrailingNode } from './extensions/TrailingNode'
import { TextBubbleMenu } from './TextBubbleMenu'
import './Prosemirror.scss' import './Prosemirror.scss'
@ -80,9 +80,7 @@ export const Editor = (props: Props) => {
const [isCommonMarkup, setIsCommonMarkup] = createSignal(false) const [isCommonMarkup, setIsCommonMarkup] = createSignal(false)
const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false) const [shouldShowTextBubbleMenu, setShouldShowTextBubbleMenu] = createSignal(false)
const { const { showSnackbar } = useSnackbar()
actions: { showSnackbar },
} = useSnackbar()
const docName = `shout-${props.shoutId}` const docName = `shout-${props.shoutId}`
@ -337,10 +335,7 @@ export const Editor = (props: Props) => {
content: initialContent ?? null, content: initialContent ?? null,
})) }))
const { const { countWords, setEditor } = useEditorContext()
actions: { countWords, setEditor },
} = useEditorContext()
setEditor(editor) setEditor(editor)
const html = useEditorHTML(() => editor()) const html = useEditorHTML(() => editor())

View File

@ -1,15 +1,15 @@
import type { MenuItem } from './Menu/Menu'
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import type { MenuItem } from './Menu/Menu'
import { createEffect, createSignal, Show } from 'solid-js' import { Show, createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
import { showModal } from '../../../stores/ui' import { showModal } from '../../../stores/ui'
import { renderUploadedImage } from '../../../utils/renderUploadedImage' import { renderUploadedImage } from '../../../utils/renderUploadedImage'
import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler' import { useOutsideClickHandler } from '../../../utils/useOutsideClickHandler'
import { Icon } from '../../_shared/Icon'
import { Modal } from '../../Nav/Modal' import { Modal } from '../../Nav/Modal'
import { Icon } from '../../_shared/Icon'
import { InlineForm } from '../InlineForm' import { InlineForm } from '../InlineForm'
import { UploadModalContent } from '../UploadModalContent' import { UploadModalContent } from '../UploadModalContent'
@ -22,7 +22,7 @@ type FloatingMenuProps = {
ref: (el: HTMLDivElement) => void ref: (el: HTMLDivElement) => void
} }
const embedData = async (data) => { const embedData = (data) => {
const element = document.createRange().createContextualFragment(data) const element = document.createRange().createContextualFragment(data)
const { attributes } = element.firstChild as HTMLIFrameElement const { attributes } = element.firstChild as HTMLIFrameElement
@ -69,7 +69,7 @@ export const EditorFloatingMenu = (props: FloatingMenuProps) => {
.run() .run()
} }
const validateEmbed = async (value) => { const validateEmbed = (value) => {
const element = document.createRange().createContextualFragment(value) const element = document.createRange().createContextualFragment(value)
if (element.firstChild?.nodeName !== 'IFRAME') { if (element.firstChild?.nodeName !== 'IFRAME') {
return t('Error') return t('Error')

View File

@ -24,7 +24,7 @@ export const InsertLinkForm = (props: Props) => {
const currentUrl = createEditorTransaction( const currentUrl = createEditorTransaction(
() => props.editor, () => props.editor,
(ed) => { (ed) => {
return (ed && ed.getAttributes('link').href) || '' return ed?.getAttributes('link').href || ''
}, },
) )
const handleClearLinkForm = () => { const handleClearLinkForm = () => {

View File

@ -1,6 +1,6 @@
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useEditorHTML } from 'solid-tiptap' import { useEditorHTML } from 'solid-tiptap'
import Typograf from 'typograf' import Typograf from 'typograf'
@ -24,18 +24,10 @@ type Props = {
export const Panel = (props: Props) => { export const Panel = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { const { isEditorPanelVisible, wordCounter, editorRef, form, toggleEditorPanel, saveShout, publishShout } =
isEditorPanelVisible, useEditorContext()
wordCounter,
editorRef,
form,
actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext()
const containerRef: { current: HTMLElement } = {
current: null,
}
const containerRef: { current: HTMLElement } = { current: null }
const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false) const [isShortcutsVisible, setIsShortcutsVisible] = createSignal(false)
const [isTypographyFixed, setIsTypographyFixed] = createSignal(false) const [isTypographyFixed, setIsTypographyFixed] = createSignal(false)
@ -59,8 +51,9 @@ export const Panel = (props: Props) => {
publishShout(form) publishShout(form)
} }
const html = useEditorHTML(() => editorRef.current())
const handleFixTypographyClick = () => { const handleFixTypographyClick = () => {
const html = useEditorHTML(() => editorRef.current())
editorRef.current().commands.setContent(typograf.execute(html())) editorRef.current().commands.setContent(typograf.execute(html()))
setIsTypographyFixed(true) setIsTypographyFixed(true)
} }

View File

@ -10,7 +10,7 @@ import { Paragraph } from '@tiptap/extension-paragraph'
import { Placeholder } from '@tiptap/extension-placeholder' import { Placeholder } from '@tiptap/extension-placeholder'
import { Text } from '@tiptap/extension-text' import { Text } from '@tiptap/extension-text'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { Show, createEffect, createMemo, createSignal, onCleanup, onMount } from 'solid-js'
import { Portal } from 'solid-js/web' import { Portal } from 'solid-js/web'
import { import {
createEditorTransaction, createEditorTransaction,
@ -24,17 +24,17 @@ import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { UploadedFile } from '../../pages/types' import { UploadedFile } from '../../pages/types'
import { hideModal, showModal } from '../../stores/ui' import { hideModal, showModal } from '../../stores/ui'
import { Modal } from '../Nav/Modal'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Modal } from '../Nav/Modal'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import { LinkBubbleMenuModule } from './LinkBubbleMenu' import { LinkBubbleMenuModule } from './LinkBubbleMenu'
import { TextBubbleMenu } from './TextBubbleMenu' import { TextBubbleMenu } from './TextBubbleMenu'
import { UploadModalContent } from './UploadModalContent' import { UploadModalContent } from './UploadModalContent'
import { Figcaption } from './extensions/Figcaption'
import { Figure } from './extensions/Figure'
import styles from './SimplifiedEditor.module.scss' import styles from './SimplifiedEditor.module.scss'
@ -94,9 +94,7 @@ const SimplifiedEditor = (props: Props) => {
current: null, current: null,
} }
const { const { setEditor } = useEditorContext()
actions: { setEditor },
} = useEditorContext()
const ImageFigure = Figure.extend({ const ImageFigure = Figure.extend({
name: 'capturedImage', name: 'capturedImage',
@ -173,7 +171,7 @@ const SimplifiedEditor = (props: Props) => {
createEditorTransaction( createEditorTransaction(
() => editor(), () => editor(),
(ed) => { (ed) => {
return ed && ed.isActive(name) return ed?.isActive(name)
}, },
) )
@ -218,7 +216,7 @@ const SimplifiedEditor = (props: Props) => {
} }
}) })
const handleKeyDown = async (event) => { const handleKeyDown = (event) => {
if (isEmpty() || !isFocused()) { if (isEmpty() || !isFocused()) {
return return
} }

View File

@ -1,7 +1,7 @@
import type { Editor } from '@tiptap/core' import type { Editor } from '@tiptap/core'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Switch, Match, createSignal, Show, onMount, onCleanup, createEffect, lazy } from 'solid-js' import { Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
import { createEditorTransaction } from 'solid-tiptap' import { createEditorTransaction } from 'solid-tiptap'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -26,7 +26,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
const isActive = (name: string, attributes?: unknown) => const isActive = (name: string, attributes?: unknown) =>
createEditorTransaction( createEditorTransaction(
() => props.editor, () => props.editor,
(editor) => editor && editor.isActive(name, attributes), (editor) => editor?.isActive(name, attributes),
) )
const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false) const [textSizeBubbleOpen, setTextSizeBubbleOpen] = createSignal(false)
@ -71,7 +71,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
} }
setListBubbleOpen((prev) => !prev) setListBubbleOpen((prev) => !prev)
} }
const handleKeyDown = async (event) => { const handleKeyDown = (event) => {
if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) { if (event.code === 'KeyK' && (event.metaKey || event.ctrlKey) && !props.editor.state.selection.empty) {
event.preventDefault() event.preventDefault()
setLinkEditorOpen(true) setLinkEditorOpen(true)
@ -160,7 +160,7 @@ export const TextBubbleMenu = (props: BubbleMenuProps) => {
submitButtonText={t('Send')} submitButtonText={t('Send')}
/> />
</Match> </Match>
<Match when={!linkEditorOpen() || !footnoteEditorOpen()}> <Match when={!(linkEditorOpen() && footnoteEditorOpen())}>
<> <>
<Show when={!props.isCommonMarkup}> <Show when={!props.isCommonMarkup}>
<> <>

View File

@ -1,6 +1,6 @@
import type { Topic } from '../../../graphql/schema/core.gen' import type { Topic } from '../../../graphql/schema/core.gen'
import { createOptions, Select } from '@thisbeyond/solid-select' import { Select, createOptions } from '@thisbeyond/solid-select'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal } from 'solid-js' import { createSignal } from 'solid-js'

View File

@ -1,6 +1,6 @@
import { createDropzone, createFileUploader, UploadFile } from '@solid-primitives/upload' import { UploadFile, createDropzone, createFileUploader } from '@solid-primitives/upload'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { UploadedFile } from '../../../pages/types' import { UploadedFile } from '../../../pages/types'
@ -56,7 +56,7 @@ export const UploadModalContent = (props: Props) => {
} }
} }
const handleUpload = async () => { const handleUpload = () => {
selectFiles(async ([uploadFile]) => { selectFiles(async ([uploadFile]) => {
await runUpload(uploadFile) await runUpload(uploadFile)
}) })

View File

@ -2,7 +2,7 @@ import type { MediaItem } from '../../../pages/types'
import { createDropzone } from '@solid-primitives/upload' import { createDropzone } from '@solid-primitives/upload'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js' import { For, Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSnackbar } from '../../../context/snackbar' import { useSnackbar } from '../../../context/snackbar'
@ -24,9 +24,7 @@ export const VideoUploader = (props: Props) => {
const [error, setError] = createSignal<string>() const [error, setError] = createSignal<string>()
const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false) const [incorrectUrl, setIncorrectUrl] = createSignal<boolean>(false)
const { const { showSnackbar } = useSnackbar()
actions: { showSnackbar },
} = useSnackbar()
const urlInput: { const urlInput: {
current: HTMLInputElement current: HTMLInputElement
@ -59,7 +57,7 @@ export const VideoUploader = (props: Props) => {
} }
} }
const handleUrlInput = async (value: string) => { const handleUrlInput = (value: string) => {
setError() setError()
if (validateUrl(value)) { if (validateUrl(value)) {
props.onVideoAdd(composeMediaItems([{ url: value }])) props.onVideoAdd(composeMediaItems([{ url: value }]))

View File

@ -1,4 +1,4 @@
import { mergeAttributes, Node } from '@tiptap/core' import { Node, mergeAttributes } from '@tiptap/core'
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {

View File

@ -1,4 +1,4 @@
import { mergeAttributes, Node } from '@tiptap/core' import { Node, mergeAttributes } from '@tiptap/core'
import { Plugin } from '@tiptap/pm/state' import { Plugin } from '@tiptap/pm/state'
declare module '@tiptap/core' { declare module '@tiptap/core' {

View File

@ -1,4 +1,4 @@
import { mergeAttributes, Node } from '@tiptap/core' import { Node, mergeAttributes } from '@tiptap/core'
declare module '@tiptap/core' { declare module '@tiptap/core' {
interface Commands<ReturnType> { interface Commands<ReturnType> {

View File

@ -41,9 +41,8 @@ export const ToggleTextWrap = Extension.create({
if (changesApplied) { if (changesApplied) {
dispatch(tr) dispatch(tr)
return true return true
} else {
return false
} }
return false
}, },
} }
}, },

View File

@ -2,25 +2,25 @@ import type { Author, Shout, Topic } from '../../../graphql/schema/core.gen'
import { getPagePath, openPage } from '@nanostores/router' import { getPagePath, openPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, For, Show } from 'solid-js' import { For, Show, createMemo, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { capitalize } from '../../../utils/capitalize' import { capitalize } from '../../../utils/capitalize'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { CoverImage } from '../../Article/CoverImage'
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AuthorLink'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Image } from '../../_shared/Image' import { Image } from '../../_shared/Image'
import { Popover } from '../../_shared/Popover' import { Popover } from '../../_shared/Popover'
import { CoverImage } from '../../Article/CoverImage'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { ShoutRatingControl } from '../../Article/ShoutRatingControl'
import { AuthorLink } from '../../Author/AuthorLink'
import { CardTopic } from '../CardTopic' import { CardTopic } from '../CardTopic'
import { FeedArticlePopup } from '../FeedArticlePopup' import { FeedArticlePopup } from '../FeedArticlePopup'
import styles from './ArticleCard.module.scss'
import stylesHeader from '../../Nav/Header/Header.module.scss' import stylesHeader from '../../Nav/Header/Header.module.scss'
import styles from './ArticleCard.module.scss'
export type ArticleCardProps = { export type ArticleCardProps = {
// TODO: refactor this, please // TODO: refactor this, please
@ -83,28 +83,47 @@ const getTitleAndSubtitle = (
} }
} }
// TODO: simple fast auto translated title/substitle
return { title, subtitle } return { title, subtitle }
} }
const getMainTopicTitle = (article: Shout, lng: string) => {
const mainTopicSlug = article.main_topic || ''
const mainTopic = article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug)
const mainTopicTitle =
mainTopicSlug && lng === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || ''
return [mainTopicTitle, mainTopicSlug]
}
const LAYOUT_ASPECT = {
music: styles.aspectRatio1x1,
literature: styles.aspectRatio16x9,
video: styles.aspectRatio16x9,
image: styles.aspectRatio4x3,
}
export const ArticleCard = (props: ArticleCardProps) => { export const ArticleCard = (props: ArticleCardProps) => {
const { t, lang, formatDate } = useLocalize() const { t, lang, formatDate } = useLocalize()
const { author } = useSession() const { author } = useSession()
const mainTopicSlug = props.article.main_topic || '' const { changeSearchParams } = useRouter()
const mainTopic = props.article.topics?.find((tpc: Topic) => tpc.slug === mainTopicSlug) const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const mainTopicTitle = const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
mainTopicSlug && lang() === 'en' ? mainTopicSlug.replace(/-/, ' ') : mainTopic?.title || '' const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = getDescription(props.article.body)
const aspectRatio = () => LAYOUT_ASPECT[props.article.layout]
const [mainTopicTitle, mainTopicSlug] = getMainTopicTitle(props.article, lang())
const { title, subtitle } = getTitleAndSubtitle(props.article)
const formattedDate = createMemo<string>(() => const formattedDate = createMemo<string>(() =>
props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '', props.article.published_at ? formatDate(new Date(props.article.published_at * 1000)) : '',
) )
const { title, subtitle } = getTitleAndSubtitle(props.article)
const canEdit = () => const canEdit = () =>
props.article.authors?.some((a) => a && a?.slug === author()?.slug) || props.article.authors?.some((a) => a && a?.slug === author()?.slug) ||
props.article.created_by?.id === author()?.id props.article.created_by?.id === author()?.id
const { changeSearchParams } = useRouter()
const scrollToComments = (event) => { const scrollToComments = (event) => {
event.preventDefault() event.preventDefault()
openPage(router, 'article', { slug: props.article.slug }) openPage(router, 'article', { slug: props.article.slug })
@ -112,28 +131,6 @@ export const ArticleCard = (props: ArticleCardProps) => {
scrollTo: 'comments', scrollTo: 'comments',
}) })
} }
const [isActionPopupActive, setIsActionPopupActive] = createSignal(false)
const [isCoverImageLoadError, setIsCoverImageLoadError] = createSignal(false)
const [isCoverImageLoading, setIsCoverImageLoading] = createSignal(true)
const description = getDescription(props.article.body)
const aspectRatio = () => {
switch (props.article.layout) {
case 'music': {
return styles.aspectRatio1x1
}
case 'image': {
return styles.aspectRatio4x3
}
case 'video':
case 'literature': {
return styles.aspectRatio16x9
}
}
}
return ( return (
<section <section
class={clsx(styles.shoutCard, props.settings?.additionalClass, { class={clsx(styles.shoutCard, props.settings?.additionalClass, {
@ -152,7 +149,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
[aspectRatio()]: props.withAspectRatio, [aspectRatio()]: props.withAspectRatio,
})} })}
> >
<Show when={!props.settings?.noimage && !props.settings?.isFeedMode}> <Show when={!(props.settings?.noimage || props.settings?.isFeedMode)}>
<div class={styles.shoutCardCoverContainer}> <div class={styles.shoutCardCoverContainer}>
<div <div
class={clsx(styles.shoutCardCover, { class={clsx(styles.shoutCardCover, {
@ -223,7 +220,7 @@ export const ArticleCard = (props: ArticleCardProps) => {
</Show> </Show>
</a> </a>
</div> </div>
<Show when={!props.settings?.noauthor || !props.settings?.nodate}> <Show when={!(props.settings?.noauthor && props.settings?.nodate)}>
<div <div
class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })} class={clsx(styles.shoutDetails, { [styles.shoutDetailsFeedMode]: props.settings?.isFeedMode })}
> >

View File

@ -7,9 +7,9 @@ import { For, Show } from 'solid-js'
import { useFollowing } from '../../context/following' import { useFollowing } from '../../context/following'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon'
import { AuthorBadge } from '../Author/AuthorBadge' import { AuthorBadge } from '../Author/AuthorBadge'
import { TopicCard } from '../Topic/Card' import { TopicCard } from '../Topic/Card'
import { Icon } from '../_shared/Icon'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'

View File

@ -1,7 +1,7 @@
import type { PopupProps } from '../../_shared/Popup' import type { PopupProps } from '../../_shared/Popup'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { Popup } from '../../_shared/Popup' import { Popup } from '../../_shared/Popup'

View File

@ -1,5 +1,5 @@
import type { Shout } from '../../graphql/schema/core.gen'
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import type { Shout } from '../../graphql/schema/core.gen'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'

View File

@ -1,6 +1,6 @@
import type { Shout } from '../../graphql/schema/core.gen' import type { Shout } from '../../graphql/schema/core.gen'
import { createSignal, createEffect, For, Show } from 'solid-js' import { For, Show, createEffect, createSignal } from 'solid-js'
import { ArticleCard } from './ArticleCard' import { ArticleCard } from './ArticleCard'

View File

@ -1,5 +1,5 @@
import type { Shout } from '../../graphql/schema/core.gen'
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import type { Shout } from '../../graphql/schema/core.gen'
import { For, Show } from 'solid-js' import { For, Show } from 'solid-js'

View File

@ -1,6 +1,6 @@
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, For, Show } from 'solid-js' import { For, Show, createSignal } from 'solid-js'
import { useFollowing } from '../../../context/following' import { useFollowing } from '../../../context/following'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -8,8 +8,8 @@ import { Author } from '../../../graphql/schema/core.gen'
import { router, useRouter } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { useArticlesStore } from '../../../stores/zine/articles' import { useArticlesStore } from '../../../stores/zine/articles'
import { useSeenStore } from '../../../stores/zine/seen' import { useSeenStore } from '../../../stores/zine/seen'
import { Icon } from '../../_shared/Icon'
import { Userpic } from '../../Author/Userpic' import { Userpic } from '../../Author/Userpic'
import { Icon } from '../../_shared/Icon'
import styles from './Sidebar.module.scss' import styles from './Sidebar.module.scss'

View File

@ -1,6 +1,6 @@
import type { Author } from '../../graphql/schema/core.gen' import type { Author } from '../../graphql/schema/core.gen'
import { createSignal, For, createEffect } from 'solid-js' import { For, createEffect, createSignal } from 'solid-js'
import { useInbox } from '../../context/inbox' import { useInbox } from '../../context/inbox'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
@ -21,6 +21,8 @@ const CreateModalContent = (props: Props) => {
const [chatTitle, setChatTitle] = createSignal<string>('') const [chatTitle, setChatTitle] = createSignal<string>('')
const [usersId, setUsersId] = createSignal<number[]>([]) const [usersId, setUsersId] = createSignal<number[]>([])
const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers) const [collectionToInvite, setCollectionToInvite] = createSignal<inviteUser[]>(inviteUsers)
const { createChat, loadChats } = useInbox()
let textInput: HTMLInputElement let textInput: HTMLInputElement
const reset = () => { const reset = () => {
@ -36,7 +38,7 @@ const CreateModalContent = (props: Props) => {
return user.selected === true return user.selected === true
}) })
.map((user) => { .map((user) => {
return user['id'] return user.id
}) })
return [...s] return [...s]
}) })
@ -54,14 +56,12 @@ const CreateModalContent = (props: Props) => {
}) })
} }
const { actions } = useInbox()
const handleCreate = async () => { const handleCreate = async () => {
try { try {
const initChat = await actions.createChat(usersId(), chatTitle()) const initChat = await createChat(usersId(), chatTitle())
console.debug('[components.Inbox] create chat result:', initChat) console.debug('[components.Inbox] create chat result:', initChat)
hideModal() hideModal()
await actions.loadChats() await loadChats()
} catch (error) { } catch (error) {
console.error(error) console.error(error)
} }

View File

@ -1,7 +1,7 @@
import type { ChatMember } from '../../graphql/schema/chat.gen' import type { ChatMember } from '../../graphql/schema/chat.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, Switch, Match, createMemo } from 'solid-js' import { Match, Show, Switch, createMemo } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Author } from '../../graphql/schema/core.gen' import { Author } from '../../graphql/schema/core.gen'
@ -26,8 +26,8 @@ type DialogProps = {
const DialogCard = (props: DialogProps) => { const DialogCard = (props: DialogProps) => {
const { t, formatTime } = useLocalize() const { t, formatTime } = useLocalize()
const companions = createMemo( const companions = createMemo(() =>
() => props.members && props.members.filter((member: ChatMember) => member.id !== props.ownId), props.members?.filter((member: ChatMember) => member.id !== props.ownId),
) )
const names = createMemo<string>(() => (companions() || []).map((companion) => companion.name).join(', ')) const names = createMemo<string>(() => (companions() || []).map((companion) => companion.name).join(', '))

View File

@ -1,7 +1,7 @@
import type { Message as MessageType, ChatMember } from '../../graphql/schema/chat.gen' import type { ChatMember, Message as MessageType } from '../../graphql/schema/chat.gen'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'

View File

@ -1,6 +1,6 @@
import type { PopupProps } from '../_shared/Popup' import type { PopupProps } from '../_shared/Popup'
import { createEffect, createSignal, For } from 'solid-js' import { For, createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
import { Popup } from '../_shared/Popup' import { Popup } from '../_shared/Popup'

View File

@ -187,7 +187,6 @@
font-size: 12px; font-size: 12px;
line-height: 16px; line-height: 16px;
margin-top: 0.3em; margin-top: 0.3em;
color: var(--danger-color); color: var(--danger-color);
a { a {

View File

@ -1,7 +1,7 @@
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, JSX, Show } from 'solid-js' import { JSX, Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -21,9 +21,7 @@ type ValidationErrors = Partial<Record<keyof FormFields, string | JSX.Element>>
export const ChangePasswordForm = () => { export const ChangePasswordForm = () => {
const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>() const { searchParams, changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const { const { changePassword } = useSession()
actions: { changePassword },
} = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [newPassword, setNewPassword] = createSignal<string>() const [newPassword, setNewPassword] = createSignal<string>()
@ -68,7 +66,7 @@ export const ChangePasswordForm = () => {
)} )}
</div> </div>
<Show when={validationErrors()}> <Show when={validationErrors()}>
<div>{validationErrors()['password']}</div> <div>{validationErrors().password}</div>
</Show> </Show>
<PasswordField <PasswordField
errorMessage={(err) => setPasswordError(err)} errorMessage={(err) => setPasswordError(err)}

View File

@ -1,12 +1,12 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createSignal, Show } from 'solid-js' import { Show, createEffect, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { useRouter } from '../../../stores/router' import { useRouter } from '../../../stores/router'
import { hideModal } from '../../../stores/ui' import { hideModal } from '../../../stores/ui'
import { setEmail, email } from './sharedLogic' import { email, setEmail } from './sharedLogic'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'

View File

@ -1,7 +1,7 @@
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, JSX, Show } from 'solid-js' import { JSX, Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -25,9 +25,7 @@ export const ForgotPasswordForm = () => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
setEmail(newEmail.toLowerCase()) setEmail(newEmail.toLowerCase())
} }
const { const { forgotPassword } = useSession()
actions: { forgotPassword },
} = useSession()
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
const [isUserNotFound, setIsUserNotFound] = createSignal(false) const [isUserNotFound, setIsUserNotFound] = createSignal(false)
@ -63,7 +61,7 @@ export const ForgotPasswordForm = () => {
redirect_uri: window.location.origin, redirect_uri: window.location.origin,
}) })
console.debug('[ForgotPasswordForm] authorizer response:', data) console.debug('[ForgotPasswordForm] authorizer response:', data)
if (errors && errors.some((error) => error.message.includes('bad user credentials'))) { if (errors?.some((error) => error.message.includes('bad user credentials'))) {
setIsUserNotFound(true) setIsUserNotFound(true)
} }
if (data.message) setMessage(data.message) if (data.message) setMessage(data.message)

View File

@ -1,7 +1,7 @@
import type { AuthModalSearchParams } from './types' import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createSignal, Show } from 'solid-js' import { Show, createSignal } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
@ -12,8 +12,8 @@ import { validateEmail } from '../../../utils/validateEmail'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import { email, setEmail } from './sharedLogic'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
@ -25,28 +25,18 @@ type FormFields = {
type ValidationErrors = Partial<Record<keyof FormFields, string>> type ValidationErrors = Partial<Record<keyof FormFields, string>>
export const LoginForm = () => { export const LoginForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal('')
const [isSubmitting, setIsSubmitting] = createSignal(false) const [isSubmitting, setIsSubmitting] = createSignal(false)
const [password, setPassword] = createSignal('')
const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({}) const [validationErrors, setValidationErrors] = createSignal<ValidationErrors>({})
// TODO: better solution for interactive error messages // TODO: better solution for interactive error messages
const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false) const [isEmailNotConfirmed, setIsEmailNotConfirmed] = createSignal(false)
const [isLinkSent, setIsLinkSent] = createSignal(false) const [isLinkSent, setIsLinkSent] = createSignal(false)
const authFormRef: { current: HTMLFormElement } = { current: null } const authFormRef: { current: HTMLFormElement } = { current: null }
const { showSnackbar } = useSnackbar()
const { const { signIn } = useSession()
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { signIn },
} = useSession()
const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const [password, setPassword] = createSignal('')
const handleEmailInput = (newEmail: string) => { const handleEmailInput = (newEmail: string) => {
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
@ -58,7 +48,7 @@ export const LoginForm = () => {
setPassword(newPassword) setPassword(newPassword)
} }
const handleSendLinkAgainClick = async (event: Event) => { const handleSendLinkAgainClick = (event: Event) => {
event.preventDefault() event.preventDefault()
setIsLinkSent(true) setIsLinkSent(true)
@ -67,7 +57,7 @@ export const LoginForm = () => {
changeSearchParams({ mode: 'forgot-password' }) changeSearchParams({ mode: 'forgot-password' })
// NOTE: temporary solution, needs logic in authorizer // NOTE: temporary solution, needs logic in authorizer
/* FIXME: /* FIXME:
const { actions: { authorizer } } = useSession() const { authorizer } = useSession()
const result = await authorizer().verifyEmail({ token }) const result = await authorizer().verifyEmail({ token })
if (!result) setSubmitError('cant sign send link') if (!result) setSubmitError('cant sign send link')
*/ */
@ -82,15 +72,15 @@ export const LoginForm = () => {
const newValidationErrors: ValidationErrors = {} const newValidationErrors: ValidationErrors = {}
if (!email()) { const validateAndSetError = (field, message) => {
newValidationErrors.email = t('Please enter email') if (!field()) {
} else if (!validateEmail(email())) { newValidationErrors[field.name] = t(message)
newValidationErrors.email = t('Invalid email') }
} }
if (!password()) { validateAndSetError(email, 'Please enter email')
newValidationErrors.password = t('Please enter password') validateAndSetError(() => validateEmail(email()), 'Invalid email')
} validateAndSetError(password, 'Please enter password')
if (Object.keys(newValidationErrors).length > 0) { if (Object.keys(newValidationErrors).length > 0) {
setValidationErrors(newValidationErrors) setValidationErrors(newValidationErrors)

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createSignal, on, Show } from 'solid-js' import { Show, createEffect, createSignal, on } from 'solid-js'
import { useLocalize } from '../../../../context/localize' import { useLocalize } from '../../../../context/localize'
import { Icon } from '../../../_shared/Icon' import { Icon } from '../../../_shared/Icon'
@ -50,7 +50,7 @@ export const PasswordField = (props: Props) => {
on( on(
() => error(), () => error(),
() => { () => {
props.errorMessage && props.errorMessage(error()) props.errorMessage?.(error())
}, },
{ defer: true }, { defer: true },
), ),

View File

@ -1,5 +1,5 @@
import type { AuthModalSearchParams } from './types'
import type { JSX } from 'solid-js' import type { JSX } from 'solid-js'
import type { AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal } from 'solid-js' import { Show, createSignal } from 'solid-js'
@ -13,8 +13,8 @@ import { validateEmail } from '../../../utils/validateEmail'
import { AuthModalHeader } from './AuthModalHeader' import { AuthModalHeader } from './AuthModalHeader'
import { PasswordField } from './PasswordField' import { PasswordField } from './PasswordField'
import { email, setEmail } from './sharedLogic'
import { SocialProviders } from './SocialProviders' import { SocialProviders } from './SocialProviders'
import { email, setEmail } from './sharedLogic'
import styles from './AuthModal.module.scss' import styles from './AuthModal.module.scss'
@ -34,9 +34,7 @@ export const RegisterForm = () => {
const { changeSearchParams } = useRouter<AuthModalSearchParams>() const { changeSearchParams } = useRouter<AuthModalSearchParams>()
const { t } = useLocalize() const { t } = useLocalize()
const { emailChecks } = useEmailChecks() const { emailChecks } = useEmailChecks()
const { const { signUp } = useSession()
actions: { signUp },
} = useSession()
const [submitError, setSubmitError] = createSignal('') const [submitError, setSubmitError] = createSignal('')
const [fullName, setFullName] = createSignal('') const [fullName, setFullName] = createSignal('')
const [password, setPassword] = createSignal('') const [password, setPassword] = createSignal('')
@ -67,11 +65,9 @@ export const RegisterForm = () => {
} }
setValidationErrors(({ email: _notNeeded, ...rest }) => rest) setValidationErrors(({ email: _notNeeded, ...rest }) => rest)
setValidationErrors(({ fullName: _notNeeded, ...rest }) => rest) setValidationErrors(({ fullName: _notNeeded, ...rest }) => rest)
setSubmitError('') setSubmitError('')
const newValidationErrors: ValidationErrors = {} const newValidationErrors: ValidationErrors = {}
const cleanName = fullName().trim() const cleanName = fullName().trim()
const cleanEmail = email().trim() const cleanEmail = email().trim()
@ -90,9 +86,7 @@ export const RegisterForm = () => {
} }
setValidationErrors(newValidationErrors) setValidationErrors(newValidationErrors)
const emailCheckResult = await checkEmail(cleanEmail) const emailCheckResult = await checkEmail(cleanEmail)
const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult const isValid = Object.keys(newValidationErrors).length === 0 && !emailCheckResult
if (!isValid) { if (!isValid) {
@ -113,7 +107,7 @@ export const RegisterForm = () => {
redirect_uri: window.location.origin, redirect_uri: window.location.origin,
} }
const { errors } = await signUp(opts) const { errors } = await signUp(opts)
if (errors && errors.some((error) => error.message.includes('has already signed up'))) { if (errors?.some((error) => error.message.includes('has already signed up'))) {
setValidationErrors((prev) => ({ setValidationErrors((prev) => ({
...prev, ...prev,
email: ( email: (

View File

@ -35,8 +35,10 @@
width: auto; width: auto;
} }
a { a, button {
border: none !important; border: none !important;
outline: none;
box-shadow: none;
} }
.facebook, .facebook,

View File

@ -10,9 +10,7 @@ export const PROVIDERS = ['facebook', 'google', 'github'] // 'vk' | 'telegram'
export const SocialProviders = () => { export const SocialProviders = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { const { oauth } = useSession()
actions: { oauth },
} = useSession()
return ( return (
<div class={styles.container}> <div class={styles.container}>
@ -20,9 +18,9 @@ export const SocialProviders = () => {
<div class={styles.social}> <div class={styles.social}>
<For each={PROVIDERS}> <For each={PROVIDERS}>
{(provider) => ( {(provider) => (
<a href="#" class={styles[provider]} onClick={(_e) => oauth(provider)}> <button class={styles[provider]} onClick={(_e) => oauth(provider)}>
<Icon name={provider} /> <Icon name={provider} />
</a> </button>
)} )}
</For> </For>
</div> </div>

View File

@ -1,7 +1,7 @@
import type { AuthModalMode, AuthModalSearchParams } from './types' import type { AuthModalMode, AuthModalSearchParams } from './types'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, Component, createEffect, createMemo } from 'solid-js' import { Component, Show, createEffect, createMemo } from 'solid-js'
import { Dynamic } from 'solid-js/web' import { Dynamic } from 'solid-js/web'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
@ -29,7 +29,6 @@ export const AuthModal = () => {
const rootRef: { current: HTMLDivElement } = { current: null } const rootRef: { current: HTMLDivElement } = { current: null }
const { t } = useLocalize() const { t } = useLocalize()
const { searchParams } = useRouter<AuthModalSearchParams>() const { searchParams } = useRouter<AuthModalSearchParams>()
const { source } = searchParams() const { source } = searchParams()
const mode = createMemo<AuthModalMode>(() => { const mode = createMemo<AuthModalMode>(() => {
@ -57,7 +56,7 @@ export const AuthModal = () => {
classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }} classList={{ [styles.hidden]: mode() !== 'register' && mode() !== 'confirm-email' }}
> >
<div> <div>
<h4>{t(`Join the global community of authors!`)}</h4> <h4>{t('Join the global community of authors!')}</h4>
<p class={styles.authBenefits}> <p class={styles.authBenefits}>
{t( {t(
'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine', 'Get to know the most intelligent people of our time, edit and discuss the articles, share your expertise, rate and decide what to publish in the magazine',

View File

@ -6,11 +6,7 @@ import styles from './ConfirmModal.module.scss'
export const ConfirmModal = () => { export const ConfirmModal = () => {
const { t } = useLocalize() const { t } = useLocalize()
const { confirmMessage, resolveConfirm } = useConfirm()
const {
confirmMessage,
actions: { resolveConfirm },
} = useConfirm()
return ( return (
<div class={styles.confirmModal}> <div class={styles.confirmModal}>

View File

@ -2,18 +2,18 @@ import type { Topic } from '../../../graphql/schema/core.gen'
import { getPagePath, redirectPage } from '@nanostores/router' import { getPagePath, redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { Show, createSignal, createEffect, onMount, onCleanup, For } from 'solid-js' import { For, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useSession } from '../../../context/session' import { useSession } from '../../../context/session'
import { apiClient } from '../../../graphql/client/core' import { apiClient } from '../../../graphql/client/core'
import { router, ROUTES, useRouter } from '../../../stores/router' import { ROUTES, router, useRouter } from '../../../stores/router'
import { useModalStore } from '../../../stores/ui' import { useModalStore } from '../../../stores/ui'
import { getDescription } from '../../../utils/meta' import { getDescription } from '../../../utils/meta'
import { SharePopup, getShareUrl } from '../../Article/SharePopup'
import { RANDOM_TOPICS_COUNT } from '../../Views/Home'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { Subscribe } from '../../_shared/Subscribe' import { Subscribe } from '../../_shared/Subscribe'
import { getShareUrl, SharePopup } from '../../Article/SharePopup'
import { RANDOM_TOPICS_COUNT } from '../../Views/Home'
import { AuthModal } from '../AuthModal' import { AuthModal } from '../AuthModal'
import { ConfirmModal } from '../ConfirmModal' import { ConfirmModal } from '../ConfirmModal'
import { HeaderAuth } from '../HeaderAuth' import { HeaderAuth } from '../HeaderAuth'
@ -46,12 +46,8 @@ export const Header = (props: Props) => {
const { t, lang } = useLocalize() const { t, lang } = useLocalize()
const { modal } = useModalStore() const { modal } = useModalStore()
const { page } = useRouter() const { page } = useRouter()
const { const { requireAuthentication } = useSession()
actions: { requireAuthentication },
} = useSession()
const { searchParams } = useRouter<HeaderSearchParams>() const { searchParams } = useRouter<HeaderSearchParams>()
const [randomTopics, setRandomTopics] = createSignal([]) const [randomTopics, setRandomTopics] = createSignal([])
const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false) const [getIsScrollingBottom, setIsScrollingBottom] = createSignal(false)
const [getIsScrolled, setIsScrolled] = createSignal(false) const [getIsScrolled, setIsScrolled] = createSignal(false)
@ -62,9 +58,7 @@ export const Header = (props: Props) => {
const [isTopicsVisible, setIsTopicsVisible] = createSignal(false) const [isTopicsVisible, setIsTopicsVisible] = createSignal(false)
const [isZineVisible, setIsZineVisible] = createSignal(false) const [isZineVisible, setIsZineVisible] = createSignal(false)
const [isFeedVisible, setIsFeedVisible] = createSignal(false) const [isFeedVisible, setIsFeedVisible] = createSignal(false)
const toggleFixed = () => { const toggleFixed = () => setFixed(!fixed())
setFixed(!fixed())
}
const tag = (topic: Topic) => const tag = (topic: Topic) =>
/[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title /[ЁА-яё]/.test(topic.title || '') && lang() !== 'ru' ? topic.slug : topic.title
@ -82,7 +76,7 @@ export const Header = (props: Props) => {
document.body.classList.toggle('fixed', fixed() || modal() !== null) document.body.classList.toggle('fixed', fixed() || modal() !== null)
document.body.classList.toggle(styles.fixed, fixed() && !modal()) document.body.classList.toggle(styles.fixed, fixed() && !modal())
if (!fixed() && !modal()) { if (!(fixed() || modal())) {
mainContent.style.marginTop = '' mainContent.style.marginTop = ''
window.scrollTo(0, windowScrollTop) window.scrollTo(0, windowScrollTop)
} }
@ -135,13 +129,13 @@ export const Header = (props: Props) => {
} }
} }
let timer let timer: string | number | NodeJS.Timeout
const clearTimer = () => { const clearTimer = () => {
clearTimeout(timer) clearTimeout(timer)
} }
const hideSubnavigation = (event, time = 500) => { const hideSubnavigation = (_event, time = 500) => {
timer = setTimeout(() => { timer = setTimeout(() => {
toggleSubnavigation(false) toggleSubnavigation(false)
}, time) }, time)
@ -264,7 +258,7 @@ export const Header = (props: Props) => {
</li> </li>
</ul> </ul>
<h4 innerHTML={t('Subscribe us')} /> <h4>{t('Subscribe us')}</h4>
<ul class="view-switcher"> <ul class="view-switcher">
<li class={styles.mainNavigationSocial}> <li class={styles.mainNavigationSocial}>
<a href="https://www.instagram.com/discoursio/"> <a href="https://www.instagram.com/discoursio/">
@ -358,14 +352,14 @@ export const Header = (props: Props) => {
<Icon name="comment" class={styles.icon} /> <Icon name="comment" class={styles.icon} />
<Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="comment-hover" class={clsx(styles.icon, styles.iconHover)} />
</div> </div>
<a href="#" class={styles.control} onClick={handleCreateButtonClick}> <button class={styles.control} onClick={handleCreateButtonClick}>
<Icon name="pencil-outline" class={styles.icon} /> <Icon name="pencil-outline" class={styles.icon} />
<Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="pencil-outline-hover" class={clsx(styles.icon, styles.iconHover)} />
</a> </button>
<a href="#" class={styles.control} onClick={handleBookmarkButtonClick}> <button class={styles.control} onClick={handleBookmarkButtonClick}>
<Icon name="bookmark" class={styles.icon} /> <Icon name="bookmark" class={styles.icon} />
<Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} /> <Icon name="bookmark-hover" class={clsx(styles.icon, styles.iconHover)} />
</a> </button>
</div> </div>
</Show> </Show>
@ -417,7 +411,7 @@ export const Header = (props: Props) => {
<a href="/podcasts">{t('Podcasts')}</a> <a href="/podcasts">{t('Podcasts')}</a>
</li> </li>
<li class="item"> <li class="item">
<a href="">{t('Special Projects')}</a> <a href="/about/projects">{t('Special Projects')}</a>
</li> </li>
<li> <li>
<a href="/topic/interview">#{t('Interview')}</a> <a href="/topic/interview">#{t('Interview')}</a>

View File

@ -1,7 +1,7 @@
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { router, ROUTES, useRouter } from '../../../stores/router' import { ROUTES, router, useRouter } from '../../../stores/router'
import { ConditionalWrapper } from '../../_shared/ConditionalWrapper' import { ConditionalWrapper } from '../../_shared/ConditionalWrapper'
import styles from './Header.module.scss' import styles from './Header.module.scss'

View File

@ -1,6 +1,6 @@
import { getPagePath } from '@nanostores/router' import { getPagePath } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createMemo, createSignal, onCleanup, onMount, Show } from 'solid-js' import { Show, createMemo, createSignal, onCleanup, onMount } from 'solid-js'
import { useEditorContext } from '../../context/editor' import { useEditorContext } from '../../context/editor'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
@ -8,11 +8,11 @@ import { useNotifications } from '../../context/notifications'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { router, useRouter } from '../../stores/router' import { router, useRouter } from '../../stores/router'
import { showModal } from '../../stores/ui' import { showModal } from '../../stores/ui'
import { Userpic } from '../Author/Userpic'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient' import { ShowOnlyOnClient } from '../_shared/ShowOnlyOnClient'
import { Userpic } from '../Author/Userpic'
import { ProfilePopup } from './ProfilePopup' import { ProfilePopup } from './ProfilePopup'
@ -33,15 +33,8 @@ export const HeaderAuth = (props: Props) => {
const { t } = useLocalize() const { t } = useLocalize()
const { page } = useRouter() const { page } = useRouter()
const { session, author, isAuthenticated, isSessionLoaded } = useSession() const { session, author, isAuthenticated, isSessionLoaded } = useSession()
const { const { unreadNotificationsCount, showNotificationsPanel } = useNotifications()
unreadNotificationsCount, const { form, toggleEditorPanel, saveShout, publishShout } = useEditorContext()
actions: { showNotificationsPanel },
} = useNotifications()
const {
form,
actions: { toggleEditorPanel, saveShout, publishShout },
} = useEditorContext()
const handleBellIconClick = (event: Event) => { const handleBellIconClick = (event: Event) => {
event.preventDefault() event.preventDefault()

View File

@ -2,7 +2,7 @@ import type { JSX } from 'solid-js'
import { redirectPage } from '@nanostores/router' import { redirectPage } from '@nanostores/router'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, Show } from 'solid-js' import { Show, createEffect, createMemo, createSignal } from 'solid-js'
import { useMediaQuery } from '../../../context/mediaQuery' import { useMediaQuery } from '../../../context/mediaQuery'
import { router } from '../../../stores/router' import { router } from '../../../stores/router'
@ -32,7 +32,7 @@ export const Modal = (props: Props) => {
const handleHide = () => { const handleHide = () => {
if (modal()) { if (modal()) {
if (allowClose()) { if (allowClose()) {
props.onClose && props.onClose() props.onClose?.()
} else { } else {
redirectPage(router, 'home') redirectPage(router, 'home')
} }
@ -64,7 +64,7 @@ export const Modal = (props: Props) => {
<div <div
class={clsx(styles.modal, { class={clsx(styles.modal, {
[styles.narrow]: props.variant === 'narrow', [styles.narrow]: props.variant === 'narrow',
['col-auto col-md-20 offset-md-2 col-lg-14 offset-lg-5']: props.variant === 'medium', 'col-auto col-md-20 offset-md-2 col-lg-14 offset-lg-5': props.variant === 'medium',
[styles.noPadding]: props.noPadding, [styles.noPadding]: props.noPadding,
[styles.maxHeight]: props.maxHeight, [styles.maxHeight]: props.maxHeight,
})} })}

View File

@ -1,5 +1,5 @@
import type { ModalType } from '../../../stores/ui'
import type { JSX } from 'solid-js/jsx-runtime' import type { JSX } from 'solid-js/jsx-runtime'
import type { ModalType } from '../../../stores/ui'
import { showModal } from '../../../stores/ui' import { showModal } from '../../../stores/ui'

View File

@ -12,11 +12,7 @@ import styles from '../_shared/Popup/Popup.module.scss'
type ProfilePopupProps = Omit<PopupProps, 'children'> type ProfilePopupProps = Omit<PopupProps, 'children'>
export const ProfilePopup = (props: ProfilePopupProps) => { export const ProfilePopup = (props: ProfilePopupProps) => {
const { const { author, signOut } = useSession()
author,
actions: { signOut },
} = useSession()
const { t } = useLocalize() const { t } = useLocalize()
return ( return (

View File

@ -1,11 +1,9 @@
@mixin searchFilterControl { @mixin search-filter-control {
@include font-size(1.4rem); @include font-size(1.4rem);
height: 4rem; height: 4rem;
padding: 0 2rem; padding: 0 2rem;
background: rgb(64 64 64 / 50%);
background: rgb(64 64 64 / 0.5);
border-radius: 10rem; border-radius: 10rem;
color: #fff; color: #fff;
font-weight: 500; font-weight: 500;
@ -16,7 +14,7 @@
} }
&:active { &:active {
color: rgb(255 255 255 / 0.4); color: rgb(255 255 255 / 40%);
} }
} }
@ -28,9 +26,7 @@
@include font-size(4.8rem); @include font-size(4.8rem);
width: 100%; width: 100%;
padding: 0 0 0.5rem; padding: 0 0 0.5rem;
background: none; background: none;
border: none; border: none;
border-bottom: 2px solid #fff; border-bottom: 2px solid #fff;
@ -39,14 +35,13 @@
outline: none; outline: none;
&::placeholder { &::placeholder {
color: rgb(255 255 255 / 0.32); color: rgb(255 255 255 / 32%);
} }
&:not(:placeholder-shown) + .searchButton img { &:not(:placeholder-shown) + .searchButton img {
filter: invert(1); filter: invert(1);
} }
&::-moz-selection,
&::selection { &::selection {
color: #2638d9; color: #2638d9;
} }
@ -56,7 +51,6 @@
position: absolute; position: absolute;
right: 0; right: 0;
top: 2rem; top: 2rem;
width: 3.2rem; width: 3.2rem;
height: 3.2rem; height: 3.2rem;
@ -67,9 +61,10 @@
.searchDescription { .searchDescription {
margin-bottom: 44px; margin-bottom: 44px;
@include font-size(1.6rem); @include font-size(1.6rem);
color: rgb(255 255 255 / 0.64); color: rgb(255 255 255 / 64%);
} }
.topicsList { .topicsList {
@ -77,12 +72,11 @@
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
gap: 1rem; gap: 1rem;
margin-top: 9.6rem !important; margin-top: 9.6rem !important;
} }
.topTopic { .topTopic {
@include searchFilterControl; @include search-filter-control;
} }
.filterSwitcher { .filterSwitcher {
@ -108,22 +102,19 @@
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
gap: 1rem; gap: 1rem;
margin: 6.4rem 0; margin: 6.4rem 0;
} }
.filterResultsControl { .filterResultsControl {
@include searchFilterControl; @include search-filter-control;
} }
.searchLoader { .searchLoader {
width: 28px; width: 28px;
height: 28px; height: 28px;
border: 5px solid #fff; border: 5px solid #fff;
border-bottom-color: transparent; border-bottom-color: transparent;
border-radius: 50%; border-radius: 50%;
animation: rotation 1s linear infinite; animation: rotation 1s linear infinite;
} }

View File

@ -1,15 +1,15 @@
import type { Shout } from '../../../graphql/schema/core.gen' import type { Shout } from '../../../graphql/schema/core.gen'
import { createResource, createSignal, For, onCleanup, Show } from 'solid-js' import { For, Show, createResource, createSignal, onCleanup } from 'solid-js'
import { debounce } from 'throttle-debounce' import { debounce } from 'throttle-debounce'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { loadShoutsSearch } from '../../../stores/zine/articles' import { loadShoutsSearch } from '../../../stores/zine/articles'
import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll' import { restoreScrollPosition, saveScrollPosition } from '../../../utils/scroll'
import { byScore } from '../../../utils/sortby' import { byScore } from '../../../utils/sortby'
import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed'
import { Button } from '../../_shared/Button' import { Button } from '../../_shared/Button'
import { Icon } from '../../_shared/Icon' import { Icon } from '../../_shared/Icon'
import { FEED_PAGE_SIZE } from '../../Views/Feed/Feed'
import { SearchResultItem } from './SearchResultItem' import { SearchResultItem } from './SearchResultItem'

View File

@ -22,7 +22,7 @@ export const Snackbar = () => {
<Transition <Transition
enterClass={styles.enter} enterClass={styles.enter}
exitToClass={styles.exitTo} exitToClass={styles.exitTo}
onExit={(el, done) => setTimeout(() => done(), 300)} onExit={(_el, done) => setTimeout(() => done(), 300)}
> >
<Show when={snackbarMessage()}> <Show when={snackbarMessage()}>
<div class={styles.content}> <div class={styles.content}>

View File

@ -22,7 +22,7 @@ export const Topics = () => {
<a href="/podcasts">{t('Podcasts')}</a> <a href="/podcasts">{t('Podcasts')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="">{t('Special Projects')}</a> <a href="/about/projects">{t('Special Projects')}</a>
</li> </li>
<li class={styles.item}> <li class={styles.item}>
<a href="/topic/interview">#{t('Interview')}</a> <a href="/topic/interview">#{t('Interview')}</a>

View File

@ -5,10 +5,10 @@ import { For, Show } from 'solid-js'
import { useLocalize } from '../../../context/localize' import { useLocalize } from '../../../context/localize'
import { useNotifications } from '../../../context/notifications' import { useNotifications } from '../../../context/notifications'
import { NotificationGroup as Group } from '../../../graphql/schema/notifier.gen' import { NotificationGroup as Group } from '../../../graphql/schema/notifier.gen'
import { useRouter, router } from '../../../stores/router' import { router, useRouter } from '../../../stores/router'
import { ArticlePageSearchParams } from '../../Article/FullArticle'
import { GroupAvatar } from '../../_shared/GroupAvatar' import { GroupAvatar } from '../../_shared/GroupAvatar'
import { TimeAgo } from '../../_shared/TimeAgo' import { TimeAgo } from '../../_shared/TimeAgo'
import { ArticlePageSearchParams } from '../../Article/FullArticle'
import styles from './NotificationView.module.scss' import styles from './NotificationView.module.scss'
@ -25,7 +25,7 @@ const getTitle = (title: string) => {
const shoutTitleWords = title.split(' ') const shoutTitleWords = title.split(' ')
while (shoutTitle.length <= 30 && i < shoutTitleWords.length) { while (shoutTitle.length <= 30 && i < shoutTitleWords.length) {
shoutTitle += shoutTitleWords[i] + ' ' shoutTitle += `${shoutTitleWords[i]} `
i++ i++
} }
@ -45,9 +45,7 @@ const reactionsCaption = (threadId: string) =>
export const NotificationGroup = (props: NotificationGroupProps) => { export const NotificationGroup = (props: NotificationGroupProps) => {
const { t, formatTime, formatDate } = useLocalize() const { t, formatTime, formatDate } = useLocalize()
const { changeSearchParams } = useRouter<ArticlePageSearchParams>() const { changeSearchParams } = useRouter<ArticlePageSearchParams>()
const { const { hideNotificationsPanel, markSeenThread } = useNotifications()
actions: { hideNotificationsPanel, markSeenThread },
} = useNotifications()
const handleClick = (threadId: string) => { const handleClick = (threadId: string) => {
props.onClick() props.onClick()

View File

@ -1,5 +1,5 @@
import { clsx } from 'clsx' import { clsx } from 'clsx'
import { createEffect, createMemo, createSignal, on, onCleanup, onMount, Show } from 'solid-js' import { Show, createEffect, createMemo, createSignal, on, onCleanup, onMount } from 'solid-js'
import { throttle } from 'throttle-debounce' import { throttle } from 'throttle-debounce'
import { useLocalize } from '../../context/localize' import { useLocalize } from '../../context/localize'
@ -46,7 +46,6 @@ const isEarlier = (date: Date) => {
export const NotificationsPanel = (props: Props) => { export const NotificationsPanel = (props: Props) => {
const [isLoading, setIsLoading] = createSignal(false) const [isLoading, setIsLoading] = createSignal(false)
const { isAuthenticated } = useSession() const { isAuthenticated } = useSession()
const { t } = useLocalize() const { t } = useLocalize()
const { const {
@ -55,7 +54,8 @@ export const NotificationsPanel = (props: Props) => {
unreadNotificationsCount, unreadNotificationsCount,
loadedNotificationsCount, loadedNotificationsCount,
totalNotificationsCount, totalNotificationsCount,
actions: { loadNotificationsGrouped, markSeenAll }, loadNotificationsGrouped,
markSeenAll,
} = useNotifications() } = useNotifications()
const handleHide = () => { const handleHide = () => {
props.onClose() props.onClose()

View File

@ -1,7 +1,7 @@
import { createFileUploader } from '@solid-primitives/upload' import { createFileUploader } from '@solid-primitives/upload'
import { clsx } from 'clsx' import { clsx } from 'clsx'
import deepEqual from 'fast-deep-equal' import deepEqual from 'fast-deep-equal'
import { createEffect, createSignal, For, lazy, Match, onCleanup, onMount, Show, Switch } from 'solid-js' import { For, Match, Show, Switch, createEffect, createSignal, lazy, onCleanup, onMount } from 'solid-js'
import { createStore } from 'solid-js/store' import { createStore } from 'solid-js/store'
import { useConfirm } from '../../context/confirm' import { useConfirm } from '../../context/confirm'
@ -9,20 +9,20 @@ import { useLocalize } from '../../context/localize'
import { useProfileForm } from '../../context/profile' import { useProfileForm } from '../../context/profile'
import { useSession } from '../../context/session' import { useSession } from '../../context/session'
import { useSnackbar } from '../../context/snackbar' import { useSnackbar } from '../../context/snackbar'
import { showModal, hideModal } from '../../stores/ui' import { hideModal, showModal } from '../../stores/ui'
import { clone } from '../../utils/clone' import { clone } from '../../utils/clone'
import { getImageUrl } from '../../utils/getImageUrl' import { getImageUrl } from '../../utils/getImageUrl'
import { handleImageUpload } from '../../utils/handleImageUpload' import { handleImageUpload } from '../../utils/handleImageUpload'
import { profileSocialLinks } from '../../utils/profileSocialLinks' import { profileSocialLinks } from '../../utils/profileSocialLinks'
import { validateUrl } from '../../utils/validateUrl' import { validateUrl } from '../../utils/validateUrl'
import { Modal } from '../Nav/Modal'
import { ProfileSettingsNavigation } from '../Nav/ProfileSettingsNavigation'
import { Button } from '../_shared/Button' import { Button } from '../_shared/Button'
import { Icon } from '../_shared/Icon' import { Icon } from '../_shared/Icon'
import { ImageCropper } from '../_shared/ImageCropper' import { ImageCropper } from '../_shared/ImageCropper'
import { Loading } from '../_shared/Loading' import { Loading } from '../_shared/Loading'
import { Popover } from '../_shared/Popover' import { Popover } from '../_shared/Popover'
import { SocialNetworkInput } from '../_shared/SocialNetworkInput' import { SocialNetworkInput } from '../_shared/SocialNetworkInput'
import { Modal } from '../Nav/Modal'
import { ProfileSettingsNavigation } from '../Nav/ProfileSettingsNavigation'
import styles from '../../pages/profile/Settings.module.scss' import styles from '../../pages/profile/Settings.module.scss'
@ -31,7 +31,6 @@ const GrowingTextarea = lazy(() => import('../../components/_shared/GrowingTexta
export const ProfileSettings = () => { export const ProfileSettings = () => {
const { t } = useLocalize() const { t } = useLocalize()
const [prevForm, setPrevForm] = createStore({}) const [prevForm, setPrevForm] = createStore({})
const [isFormInitialized, setIsFormInitialized] = createSignal(false) const [isFormInitialized, setIsFormInitialized] = createSignal(false)
const [social, setSocial] = createSignal([]) const [social, setSocial] = createSignal([])
@ -44,21 +43,10 @@ export const ProfileSettings = () => {
const [hostname, setHostname] = createSignal<string | null>(null) const [hostname, setHostname] = createSignal<string | null>(null)
const [slugError, setSlugError] = createSignal<string>() const [slugError, setSlugError] = createSignal<string>()
const [nameError, setNameError] = createSignal<string>() const [nameError, setNameError] = createSignal<string>()
const { form, submit, updateFormField, setForm } = useProfileForm()
const { const { showSnackbar } = useSnackbar()
form, const { loadAuthor } = useSession()
actions: { submit, updateFormField, setForm }, const { showConfirm } = useConfirm()
} = useProfileForm()
const {
actions: { showSnackbar },
} = useSnackbar()
const {
actions: { loadAuthor },
} = useSession()
const {
actions: { showConfirm },
} = useConfirm()
createEffect(() => { createEffect(() => {
if (Object.keys(form).length > 0 && !isFormInitialized()) { if (Object.keys(form).length > 0 && !isFormInitialized()) {

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