mirror of
https://github.com/LemmyNet/lemmy-ui.git
synced 2024-12-01 16:51:13 +00:00
Merge branch 'main' into breakout-role-utils
This commit is contained in:
commit
02ffa85b58
35 changed files with 2252 additions and 2104 deletions
|
@ -3,11 +3,12 @@
|
||||||
"env": {
|
"env": {
|
||||||
"browser": true
|
"browser": true
|
||||||
},
|
},
|
||||||
"plugins": ["@typescript-eslint"],
|
"plugins": ["@typescript-eslint", "jsx-a11y"],
|
||||||
"extends": [
|
"extends": [
|
||||||
"eslint:recommended",
|
"eslint:recommended",
|
||||||
"plugin:@typescript-eslint/recommended",
|
"plugin:@typescript-eslint/recommended",
|
||||||
"plugin:inferno/recommended"
|
"plugin:inferno/recommended",
|
||||||
|
"plugin:jsx-a11y/recommended"
|
||||||
],
|
],
|
||||||
"parser": "@typescript-eslint/parser",
|
"parser": "@typescript-eslint/parser",
|
||||||
"parserOptions": {
|
"parserOptions": {
|
||||||
|
@ -20,6 +21,16 @@
|
||||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||||
"@typescript-eslint/no-empty-function": 0,
|
"@typescript-eslint/no-empty-function": 0,
|
||||||
"arrow-body-style": 0,
|
"arrow-body-style": 0,
|
||||||
|
"jsx-a11y/alt-text": 1,
|
||||||
|
"jsx-a11y/anchor-is-valid": 1,
|
||||||
|
"jsx-a11y/aria-activedescendant-has-tabindex": 1,
|
||||||
|
"jsx-a11y/aria-role": 1,
|
||||||
|
"jsx-a11y/click-events-have-key-events": 1,
|
||||||
|
"jsx-a11y/iframe-has-title": 1,
|
||||||
|
"jsx-a11y/interactive-supports-focus": 1,
|
||||||
|
"jsx-a11y/no-redundant-roles": 1,
|
||||||
|
"jsx-a11y/no-static-element-interactions": 1,
|
||||||
|
"jsx-a11y/role-has-required-aria-props": 1,
|
||||||
"curly": 0,
|
"curly": 0,
|
||||||
"eol-last": 0,
|
"eol-last": 0,
|
||||||
"eqeqeq": 0,
|
"eqeqeq": 0,
|
||||||
|
|
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
2
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
|
@ -8,7 +8,7 @@ body:
|
||||||
value: |
|
value: |
|
||||||
Found a bug? Please fill out the sections below. 👍
|
Found a bug? Please fill out the sections below. 👍
|
||||||
Thanks for taking the time to fill out this bug report!
|
Thanks for taking the time to fill out this bug report!
|
||||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy/issues/new/choose)
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Requirements
|
label: Requirements
|
||||||
|
|
6
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
6
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
|
@ -1,16 +1,16 @@
|
||||||
name: "\U0001F680 Feature request"
|
name: "\U0001F680 Feature request"
|
||||||
description: Suggest an idea for improving Lemmy
|
description: Suggest an idea for improving Lemmy's UI
|
||||||
labels: ["enhancement"]
|
labels: ["enhancement"]
|
||||||
body:
|
body:
|
||||||
- type: markdown
|
- type: markdown
|
||||||
attributes:
|
attributes:
|
||||||
value: |
|
value: |
|
||||||
Have a suggestion about Lemmy's UI?
|
Have a suggestion about Lemmy's UI?
|
||||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy/issues/new/choose)
|
||||||
- type: checkboxes
|
- type: checkboxes
|
||||||
attributes:
|
attributes:
|
||||||
label: Requirements
|
label: Requirements
|
||||||
description: Before you create a bug report please do the following.
|
description: Before you create a feature request please do the following.
|
||||||
options:
|
options:
|
||||||
- label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
- label: Is this a feature request? For questions or discussions use https://lemmy.ml/c/lemmy_support
|
||||||
required: true
|
required: true
|
||||||
|
|
|
@ -1,2 +1,3 @@
|
||||||
src/shared/translations
|
src/shared/translations
|
||||||
lemmy-translations
|
lemmy-translations
|
||||||
|
src/assets/css/themes/*.css
|
||||||
|
|
|
@ -106,6 +106,7 @@
|
||||||
"bootstrap-v4": "npm:bootstrap@^4.6.2",
|
"bootstrap-v4": "npm:bootstrap@^4.6.2",
|
||||||
"eslint": "^8.40.0",
|
"eslint": "^8.40.0",
|
||||||
"eslint-plugin-inferno": "^7.32.2",
|
"eslint-plugin-inferno": "^7.32.2",
|
||||||
|
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||||
"eslint-plugin-prettier": "^4.2.1",
|
"eslint-plugin-prettier": "^4.2.1",
|
||||||
"husky": "^8.0.3",
|
"husky": "^8.0.3",
|
||||||
"import-sort-style-module": "^6.0.0",
|
"import-sort-style-module": "^6.0.0",
|
||||||
|
|
|
@ -1,108 +1,13 @@
|
||||||
$white: #fff;
|
@import "variables.darkly";
|
||||||
$gray-100: #f8f9fa;
|
|
||||||
$gray-200: #ebebeb;
|
|
||||||
$gray-300: #dee2e6;
|
|
||||||
$gray-400: #ced4da;
|
|
||||||
$gray-500: #adb5bd;
|
|
||||||
$gray-600: #888;
|
|
||||||
$gray-700: #444;
|
|
||||||
$gray-800: #303030;
|
|
||||||
$gray-900: #222;
|
|
||||||
$black: #000;
|
|
||||||
$blue: #375a7f;
|
|
||||||
$indigo: #6610f2;
|
|
||||||
$purple: #6f42c1;
|
|
||||||
$pink: #e83e8c;
|
|
||||||
$red: #e74c3c;
|
|
||||||
$orange: #fd7e14;
|
|
||||||
$yellow: #f39c12;
|
|
||||||
$green: #00bc8c;
|
|
||||||
$teal: #20c997;
|
|
||||||
$cyan: #3498db;
|
|
||||||
$primary: $blue;
|
$primary: $blue;
|
||||||
$secondary: #444;
|
$secondary: #444;
|
||||||
$success: $green;
|
|
||||||
$info: $cyan;
|
|
||||||
$warning: $yellow;
|
|
||||||
$danger: $red;
|
|
||||||
$light: $gray-800;
|
$light: $gray-800;
|
||||||
$dark: $gray-300;
|
|
||||||
$yiq-contrasted-threshold: 175;
|
$theme-colors: (
|
||||||
$body-bg: $gray-900;
|
"primary": $primary,
|
||||||
$body-color: $gray-300;
|
"secondary": $secondary,
|
||||||
|
"light": $light,
|
||||||
|
);
|
||||||
|
|
||||||
$link-color: $red;
|
$link-color: $red;
|
||||||
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
|
||||||
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
|
||||||
$font-size-base: 0.9375rem;
|
|
||||||
$h1-font-size: 3rem;
|
|
||||||
$h2-font-size: 2.5rem;
|
|
||||||
$h3-font-size: 2rem;
|
|
||||||
$text-muted: $gray-600;
|
|
||||||
$table-accent-bg: $gray-800;
|
|
||||||
$table-border-color: $gray-700;
|
|
||||||
$input-border-color: $body-bg;
|
|
||||||
$input-group-addon-color: $gray-500;
|
|
||||||
$input-group-addon-bg: $gray-700;
|
|
||||||
$custom-file-color: $gray-500;
|
|
||||||
$custom-file-border-color: $body-bg;
|
|
||||||
$dropdown-bg: $gray-900;
|
|
||||||
$dropdown-border-color: $gray-700;
|
|
||||||
$dropdown-divider-bg: $gray-700;
|
|
||||||
$dropdown-link-color: $white;
|
|
||||||
$dropdown-link-hover-color: $white;
|
|
||||||
$dropdown-link-hover-bg: $primary;
|
|
||||||
$nav-link-padding-x: 2rem;
|
|
||||||
$nav-link-disabled-color: $gray-500;
|
|
||||||
$nav-tabs-border-color: $gray-700;
|
|
||||||
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
|
|
||||||
transparent;
|
|
||||||
$nav-tabs-link-active-color: $white;
|
|
||||||
$nav-tabs-link-active-border-color: $nav-tabs-border-color
|
|
||||||
$nav-tabs-border-color transparent;
|
|
||||||
$navbar-padding-y: 1rem;
|
|
||||||
$navbar-dark-color: rgba($white, 0.6);
|
|
||||||
$navbar-dark-hover-color: $white;
|
|
||||||
$navbar-light-color: rgba($white, 0.6);
|
|
||||||
$navbar-light-hover-color: $white;
|
|
||||||
$navbar-light-active-color: $white;
|
|
||||||
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
|
|
||||||
$pagination-color: $white;
|
|
||||||
$pagination-bg: $success;
|
|
||||||
$pagination-border-width: 0;
|
|
||||||
$pagination-border-color: transparent;
|
|
||||||
$pagination-hover-color: $white;
|
|
||||||
$pagination-hover-bg: lighten($success, 10%);
|
|
||||||
$pagination-hover-border-color: transparent;
|
|
||||||
$pagination-active-bg: $pagination-hover-bg;
|
|
||||||
$pagination-active-border-color: transparent;
|
|
||||||
$pagination-disabled-color: $white;
|
|
||||||
$pagination-disabled-bg: darken($success, 15%);
|
|
||||||
$pagination-disabled-border-color: transparent;
|
|
||||||
$jumbotron-bg: $gray-800;
|
|
||||||
$card-cap-bg: $gray-700;
|
|
||||||
$card-bg: $gray-800;
|
|
||||||
$popover-bg: $gray-800;
|
|
||||||
$popover-header-bg: $gray-700;
|
|
||||||
$toast-background-color: $gray-700;
|
|
||||||
$toast-header-background-color: $gray-800;
|
|
||||||
$modal-content-bg: $gray-800;
|
|
||||||
$modal-content-border-color: $gray-700;
|
|
||||||
$modal-header-border-color: $gray-700;
|
|
||||||
$progress-bg: $gray-700;
|
|
||||||
$list-group-bg: $gray-800;
|
|
||||||
$list-group-border-color: $gray-700;
|
|
||||||
$list-group-hover-bg: $gray-700;
|
|
||||||
$breadcrumb-bg: $gray-700;
|
|
||||||
$close-color: $white;
|
|
||||||
$close-text-shadow: none;
|
|
||||||
$pre-color: inherit;
|
|
||||||
$mark-bg: #333;
|
|
||||||
$custom-select-bg: $secondary;
|
|
||||||
$custom-select-color: $white;
|
|
||||||
$input-bg: $secondary;
|
|
||||||
$input-color: $white;
|
|
||||||
$input-disabled-bg: darken($secondary, 10%);
|
|
||||||
$light: $gray-800;
|
|
||||||
$navbar-light-brand-color: $white;
|
|
||||||
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
|
||||||
|
|
|
@ -1,35 +1,59 @@
|
||||||
|
// Colors
|
||||||
$white: #fff;
|
$white: #fff;
|
||||||
$gray-100: #f8f9fa;
|
|
||||||
$gray-200: #ebebeb;
|
$gray-200: #ebebeb;
|
||||||
$gray-300: #dee2e6;
|
$gray-300: #dee2e6;
|
||||||
$gray-400: #ced4da;
|
|
||||||
$gray-500: #adb5bd;
|
$gray-500: #adb5bd;
|
||||||
$gray-600: #888;
|
$gray-600: #888;
|
||||||
$gray-700: #444;
|
$gray-700: #444;
|
||||||
$gray-800: #303030;
|
$gray-800: #303030;
|
||||||
$gray-900: #222;
|
$gray-900: #222;
|
||||||
$black: #000;
|
|
||||||
|
// Writing these maps is necessary for Bootstrap theming:
|
||||||
|
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||||
|
$grays: (
|
||||||
|
"gray-200": $gray-200,
|
||||||
|
"gray-600": $gray-600,
|
||||||
|
"gray-700": $gray-700,
|
||||||
|
"gray-800": $gray-800,
|
||||||
|
"gray-900": $gray-900,
|
||||||
|
);
|
||||||
|
|
||||||
$blue: #375a7f;
|
$blue: #375a7f;
|
||||||
$indigo: #6610f2;
|
|
||||||
$purple: #6f42c1;
|
|
||||||
$pink: #e83e8c;
|
|
||||||
$red: #e74c3c;
|
$red: #e74c3c;
|
||||||
$orange: #fd7e14;
|
|
||||||
$yellow: #f39c12;
|
$yellow: #f39c12;
|
||||||
$green: #00bc8c;
|
$green: #00bc8c;
|
||||||
$teal: #20c997;
|
|
||||||
$cyan: #3498db;
|
$cyan: #3498db;
|
||||||
$primary: $blue;
|
|
||||||
|
// Writing these maps is necessary for Bootstrap theming:
|
||||||
|
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||||
|
$colors: (
|
||||||
|
"blue": $blue,
|
||||||
|
"red": $red,
|
||||||
|
"yellow": $yellow,
|
||||||
|
"green": $green,
|
||||||
|
"cyan": $cyan,
|
||||||
|
);
|
||||||
|
|
||||||
|
$primary: $green;
|
||||||
$secondary: $gray-700;
|
$secondary: $gray-700;
|
||||||
$success: $green;
|
$success: $green;
|
||||||
$info: $cyan;
|
|
||||||
$warning: $yellow;
|
|
||||||
$danger: $red;
|
|
||||||
$dark: $gray-300;
|
$dark: $gray-300;
|
||||||
$yiq-contrasted-threshold: 175;
|
|
||||||
$body-bg: $gray-900;
|
// Writing these maps is necessary for Bootstrap theming:
|
||||||
|
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||||
|
$theme-colors: (
|
||||||
|
"primary": $primary,
|
||||||
|
"secondary": $secondary,
|
||||||
|
"dark": $dark,
|
||||||
|
);
|
||||||
|
|
||||||
$body-color: $gray-300;
|
$body-color: $gray-300;
|
||||||
|
$body-bg: $gray-900;
|
||||||
$link-color: $success;
|
$link-color: $success;
|
||||||
|
$mark-bg: #333;
|
||||||
|
$text-muted: $gray-600;
|
||||||
|
$yiq-contrasted-threshold: 175;
|
||||||
|
|
||||||
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI",
|
||||||
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji",
|
||||||
"Segoe UI Emoji", "Segoe UI Symbol";
|
"Segoe UI Emoji", "Segoe UI Symbol";
|
||||||
|
@ -37,28 +61,10 @@ $font-size-base: 0.9375rem;
|
||||||
$h1-font-size: 3rem;
|
$h1-font-size: 3rem;
|
||||||
$h2-font-size: 2.5rem;
|
$h2-font-size: 2.5rem;
|
||||||
$h3-font-size: 2rem;
|
$h3-font-size: 2rem;
|
||||||
$text-muted: $gray-600;
|
|
||||||
$table-accent-bg: $gray-800;
|
$card-cap-bg: $gray-700;
|
||||||
$table-border-color: $gray-700;
|
$card-bg: $gray-800;
|
||||||
$input-border-color: $body-bg;
|
|
||||||
$input-group-addon-color: $gray-500;
|
|
||||||
$input-group-addon-bg: $gray-700;
|
|
||||||
$custom-file-color: $gray-500;
|
|
||||||
$custom-file-border-color: $body-bg;
|
|
||||||
$dropdown-bg: $gray-900;
|
|
||||||
$dropdown-border-color: $gray-700;
|
|
||||||
$dropdown-divider-bg: $gray-700;
|
|
||||||
$dropdown-link-color: $white;
|
|
||||||
$dropdown-link-hover-color: $white;
|
|
||||||
$dropdown-link-hover-bg: $primary;
|
|
||||||
$nav-link-padding-x: 2rem;
|
|
||||||
$nav-link-disabled-color: $gray-500;
|
|
||||||
$nav-tabs-border-color: $gray-700;
|
|
||||||
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
|
|
||||||
transparent;
|
|
||||||
$nav-tabs-link-active-color: $white;
|
|
||||||
$nav-tabs-link-active-border-color: $nav-tabs-border-color
|
|
||||||
$nav-tabs-border-color transparent;
|
|
||||||
$navbar-padding-y: 1rem;
|
$navbar-padding-y: 1rem;
|
||||||
$navbar-dark-color: rgba($white, 0.6);
|
$navbar-dark-color: rgba($white, 0.6);
|
||||||
$navbar-dark-hover-color: $white;
|
$navbar-dark-hover-color: $white;
|
||||||
|
@ -66,6 +72,41 @@ $navbar-light-color: rgba($white, 0.6);
|
||||||
$navbar-light-hover-color: $white;
|
$navbar-light-hover-color: $white;
|
||||||
$navbar-light-active-color: $white;
|
$navbar-light-active-color: $white;
|
||||||
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
|
$navbar-light-toggler-border-color: rgba($gray-900, 0.1);
|
||||||
|
$navbar-light-brand-color: $white;
|
||||||
|
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
||||||
|
|
||||||
|
$nav-link-padding-x: 2rem;
|
||||||
|
$nav-link-disabled-color: $gray-500;
|
||||||
|
|
||||||
|
$nav-tabs-border-color: $gray-700;
|
||||||
|
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color
|
||||||
|
transparent;
|
||||||
|
$nav-tabs-link-active-color: $white;
|
||||||
|
$nav-tabs-link-active-border-color: $nav-tabs-border-color
|
||||||
|
$nav-tabs-border-color transparent;
|
||||||
|
|
||||||
|
$input-bg: $gray-700;
|
||||||
|
$input-color: $white;
|
||||||
|
$input-disabled-bg: darken($gray-700, 10%);
|
||||||
|
$input-border-color: $body-bg;
|
||||||
|
$input-group-addon-color: $gray-500;
|
||||||
|
$input-group-addon-bg: $gray-700;
|
||||||
|
|
||||||
|
$hr-border-color: rgba($body-color, 0.25);
|
||||||
|
|
||||||
|
$table-accent-bg: $gray-800;
|
||||||
|
$table-border-color: $gray-700;
|
||||||
|
|
||||||
|
$custom-file-color: $gray-500;
|
||||||
|
$custom-file-border-color: $body-bg;
|
||||||
|
|
||||||
|
$dropdown-bg: $gray-900;
|
||||||
|
$dropdown-border-color: $gray-700;
|
||||||
|
$dropdown-divider-bg: $gray-700;
|
||||||
|
$dropdown-link-color: $white;
|
||||||
|
$dropdown-link-hover-color: $white;
|
||||||
|
$dropdown-link-hover-bg: $primary;
|
||||||
|
|
||||||
$pagination-color: $white;
|
$pagination-color: $white;
|
||||||
$pagination-bg: $success;
|
$pagination-bg: $success;
|
||||||
$pagination-border-width: 0;
|
$pagination-border-width: 0;
|
||||||
|
@ -78,9 +119,8 @@ $pagination-active-border-color: transparent;
|
||||||
$pagination-disabled-color: $white;
|
$pagination-disabled-color: $white;
|
||||||
$pagination-disabled-bg: darken($success, 15%);
|
$pagination-disabled-bg: darken($success, 15%);
|
||||||
$pagination-disabled-border-color: transparent;
|
$pagination-disabled-border-color: transparent;
|
||||||
|
|
||||||
$jumbotron-bg: $gray-800;
|
$jumbotron-bg: $gray-800;
|
||||||
$card-cap-bg: $gray-700;
|
|
||||||
$card-bg: $gray-800;
|
|
||||||
$popover-bg: $gray-800;
|
$popover-bg: $gray-800;
|
||||||
$popover-header-bg: $gray-700;
|
$popover-header-bg: $gray-700;
|
||||||
$toast-background-color: $gray-700;
|
$toast-background-color: $gray-700;
|
||||||
|
@ -96,12 +136,6 @@ $breadcrumb-bg: $gray-700;
|
||||||
$close-color: $white;
|
$close-color: $white;
|
||||||
$close-text-shadow: none;
|
$close-text-shadow: none;
|
||||||
$pre-color: inherit;
|
$pre-color: inherit;
|
||||||
$mark-bg: #333;
|
$custom-select-bg: $gray-700;
|
||||||
$custom-select-bg: $secondary;
|
|
||||||
$custom-select-color: $white;
|
$custom-select-color: $white;
|
||||||
$input-bg: $secondary;
|
|
||||||
$input-color: $white;
|
|
||||||
$input-disabled-bg: darken($secondary, 10%);
|
|
||||||
$light: $gray-800;
|
$light: $gray-800;
|
||||||
$navbar-light-brand-color: $white;
|
|
||||||
$navbar-light-brand-hover-color: $navbar-light-brand-color;
|
|
||||||
|
|
|
@ -1,47 +1,9 @@
|
||||||
$white: #fff;
|
@import "variables.darkly";
|
||||||
$gray-100: #f8f9fa;
|
|
||||||
$gray-200: #e9ecef;
|
|
||||||
$gray-300: #dee2e6;
|
|
||||||
$gray-400: #ced4da;
|
|
||||||
$gray-500: #adb5bd;
|
|
||||||
$gray-600: #6c757d;
|
|
||||||
$gray-700: #495057;
|
|
||||||
$gray-800: #343a40;
|
|
||||||
$gray-900: #212529;
|
|
||||||
$black: #000;
|
|
||||||
$blue: #007bff;
|
|
||||||
$indigo: #6610f2;
|
|
||||||
$white: #ffffff;
|
|
||||||
$orange: #f1641e;
|
|
||||||
$cyan: #02bdc2;
|
|
||||||
$green: #00c853;
|
|
||||||
$primary: #f1641e;
|
|
||||||
$secondary: #c80000;
|
$secondary: #c80000;
|
||||||
$info: $blue;
|
|
||||||
$body-color: $gray-700;
|
|
||||||
$link-color: $primary;
|
|
||||||
$red: #d8486a;
|
|
||||||
$border-radius: 0.5rem;
|
|
||||||
$border-radius-lg: 0.5rem;
|
|
||||||
$border-radius-sm: 1rem;
|
|
||||||
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
|
|
||||||
"Segoe UI", "Helvetica", Arial, sans-serif;
|
|
||||||
$headings-color: $gray-700;
|
|
||||||
$input-btn-focus-color: rgba($primary, 0.75);
|
|
||||||
$form-feedback-valid-color: $info;
|
|
||||||
$navbar-light-color: $gray-600;
|
|
||||||
$black: #222222;
|
|
||||||
$navbar-dark-toggler-border-color: rgba($black, 0.1);
|
|
||||||
$navbar-light-active-color: $gray-900;
|
|
||||||
$card-color: $gray-700;
|
|
||||||
$card-cap-color: $gray-700;
|
|
||||||
$info: $blue;
|
|
||||||
$body-bg: #fff;
|
|
||||||
$success: $indigo;
|
|
||||||
$danger: darken($primary, 24%);
|
$danger: darken($primary, 24%);
|
||||||
$navbar-light-hover-color: $gray-900;
|
|
||||||
$card-bg: $gray-100;
|
$theme-colors: (
|
||||||
$border-color: $gray-700;
|
"secondary": $secondary,
|
||||||
$mark-bg: rgb(255, 252, 239);
|
"danger": $danger,
|
||||||
$font-weight-bold: 600;
|
);
|
||||||
$rounded-pill: 0.25rem;
|
|
||||||
|
|
|
@ -1,47 +1,80 @@
|
||||||
$white: #fff;
|
// Colors
|
||||||
$gray-100: #f8f9fa;
|
$gray-100: #f8f9fa;
|
||||||
$gray-200: #e9ecef;
|
$gray-200: #e9ecef;
|
||||||
$gray-300: #dee2e6;
|
|
||||||
$gray-400: #ced4da;
|
|
||||||
$gray-500: #adb5bd;
|
|
||||||
$gray-600: #6c757d;
|
$gray-600: #6c757d;
|
||||||
$gray-700: #495057;
|
$gray-700: #495057;
|
||||||
$gray-800: #343a40;
|
$gray-800: #343a40;
|
||||||
$gray-900: #212529;
|
$gray-900: #212529;
|
||||||
$black: #000;
|
$black: #222;
|
||||||
|
|
||||||
|
// Writing these maps is necessary for Bootstrap theming:
|
||||||
|
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||||
|
$grays: (
|
||||||
|
"gray-200": $gray-200,
|
||||||
|
"gray-600": $gray-600,
|
||||||
|
"gray-700": $gray-700,
|
||||||
|
"gray-800": $gray-800,
|
||||||
|
"gray-900": $gray-900,
|
||||||
|
);
|
||||||
|
|
||||||
$blue: #007bff;
|
$blue: #007bff;
|
||||||
$indigo: #6610f2;
|
$indigo: #6610f2;
|
||||||
$white: #ffffff;
|
$red: #d8486a;
|
||||||
$orange: #f1641e;
|
$orange: #f1641e;
|
||||||
$cyan: #02bdc2;
|
|
||||||
$green: #00c853;
|
$green: #00c853;
|
||||||
|
$cyan: #02bdc2;
|
||||||
|
|
||||||
|
// Writing these maps is necessary for Bootstrap theming:
|
||||||
|
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||||
|
$colors: (
|
||||||
|
"red": $red,
|
||||||
|
"orange": $orange,
|
||||||
|
"cyan": $cyan,
|
||||||
|
"green": $green,
|
||||||
|
);
|
||||||
|
|
||||||
$primary: $orange;
|
$primary: $orange;
|
||||||
$secondary: $green;
|
$secondary: $green;
|
||||||
$info: $cyan;
|
$success: $indigo;
|
||||||
|
$info: $blue;
|
||||||
|
$danger: darken($primary, 25%);
|
||||||
|
|
||||||
|
// Writing these maps is necessary for Bootstrap theming:
|
||||||
|
// https://getbootstrap.com/docs/4.6/getting-started/introduction/
|
||||||
|
$theme-colors: (
|
||||||
|
"primary": $primary,
|
||||||
|
"secondary": $secondary,
|
||||||
|
"success": $success,
|
||||||
|
"info": $info,
|
||||||
|
"danger": $danger,
|
||||||
|
);
|
||||||
|
|
||||||
$body-color: $gray-700;
|
$body-color: $gray-700;
|
||||||
|
$body-bg: #fff;
|
||||||
$link-color: $primary;
|
$link-color: $primary;
|
||||||
$red: #d8486a;
|
$border-color: $gray-700;
|
||||||
|
$mark-bg: rgb(255, 252, 239);
|
||||||
|
$headings-color: $gray-700;
|
||||||
|
|
||||||
|
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
|
||||||
|
"Segoe UI", "Helvetica", Arial, sans-serif;
|
||||||
|
$font-weight-bold: 600;
|
||||||
|
|
||||||
|
$card-color: $gray-700;
|
||||||
|
$card-cap-color: $gray-700;
|
||||||
|
$card-bg: $gray-100;
|
||||||
|
|
||||||
|
$navbar-dark-toggler-border-color: rgba($black, 0.1);
|
||||||
|
$navbar-light-color: $gray-600;
|
||||||
|
$navbar-light-hover-color: $gray-900;
|
||||||
|
$navbar-light-active-color: $gray-900;
|
||||||
|
|
||||||
|
$form-feedback-valid-color: $info;
|
||||||
|
$input-btn-focus-color: rgba($primary, 0.75);
|
||||||
|
|
||||||
$border-radius: 0.5rem;
|
$border-radius: 0.5rem;
|
||||||
$border-radius-lg: 0.5rem;
|
$border-radius-lg: 0.5rem;
|
||||||
$border-radius-sm: 1rem;
|
$border-radius-sm: 1rem;
|
||||||
$font-family-sans-serif: -apple-system, BlinkMacSystemFont, "Droid Sans",
|
|
||||||
"Segoe UI", "Helvetica", Arial, sans-serif;
|
|
||||||
$headings-color: $gray-700;
|
|
||||||
$input-btn-focus-color: rgba($primary, 0.75);
|
|
||||||
$form-feedback-valid-color: $info;
|
|
||||||
$navbar-light-color: $gray-600;
|
|
||||||
$black: #222222;
|
|
||||||
$navbar-dark-toggler-border-color: rgba($black, 0.1);
|
|
||||||
$navbar-light-active-color: $gray-900;
|
|
||||||
$card-color: $gray-700;
|
|
||||||
$card-cap-color: $gray-700;
|
|
||||||
$info: $blue;
|
|
||||||
$body-bg: #fff;
|
|
||||||
$success: $indigo;
|
|
||||||
$danger: darken($primary, 25%);
|
|
||||||
$navbar-light-hover-color: $gray-900;
|
|
||||||
$card-bg: $gray-100;
|
|
||||||
$border-color: $gray-700;
|
|
||||||
$mark-bg: rgb(255, 252, 239);
|
|
||||||
$font-weight-bold: 600;
|
|
||||||
$rounded-pill: 0.25rem;
|
$rounded-pill: 0.25rem;
|
||||||
|
|
||||||
|
$hr-border-color: rgba($body-color, 0.25);
|
||||||
|
|
|
@ -450,7 +450,7 @@ hr {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
border-top: 1px solid rgba(222, 226, 230, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
small,
|
small,
|
||||||
|
@ -3245,7 +3245,7 @@ input[type="button"].btn-block {
|
||||||
.dropdown-item:focus {
|
.dropdown-item:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.dropdown-item.active,
|
.dropdown-item.active,
|
||||||
.dropdown-item:active {
|
.dropdown-item:active {
|
||||||
|
|
|
@ -19,14 +19,13 @@
|
||||||
--white: #fff;
|
--white: #fff;
|
||||||
--gray: #888;
|
--gray: #888;
|
||||||
--gray-dark: #303030;
|
--gray-dark: #303030;
|
||||||
--primary: #375a7f;
|
--primary: #00bc8c;
|
||||||
--secondary: #444;
|
--secondary: #444;
|
||||||
--success: #00bc8c;
|
--success: #00bc8c;
|
||||||
--info: #3498db;
|
--info: #3498db;
|
||||||
--warning: #f39c12;
|
--warning: #f39c12;
|
||||||
--danger: #e74c3c;
|
--danger: #e74c3c;
|
||||||
--light: #303030;
|
--light: #303030;
|
||||||
--medium-light: var(--secondary);
|
|
||||||
--dark: #dee2e6;
|
--dark: #dee2e6;
|
||||||
--breakpoint-xs: 0;
|
--breakpoint-xs: 0;
|
||||||
--breakpoint-sm: 576px;
|
--breakpoint-sm: 576px;
|
||||||
|
@ -451,7 +450,7 @@ hr {
|
||||||
margin-top: 1rem;
|
margin-top: 1rem;
|
||||||
margin-bottom: 1rem;
|
margin-bottom: 1rem;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.1);
|
border-top: 1px solid rgba(222, 226, 230, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
small,
|
small,
|
||||||
|
@ -1625,21 +1624,21 @@ pre code {
|
||||||
.table-primary,
|
.table-primary,
|
||||||
.table-primary > th,
|
.table-primary > th,
|
||||||
.table-primary > td {
|
.table-primary > td {
|
||||||
background-color: #c7d1db;
|
background-color: #b8ecdf;
|
||||||
}
|
}
|
||||||
.table-primary th,
|
.table-primary th,
|
||||||
.table-primary td,
|
.table-primary td,
|
||||||
.table-primary thead th,
|
.table-primary thead th,
|
||||||
.table-primary tbody + tbody {
|
.table-primary tbody + tbody {
|
||||||
border-color: #97a9bc;
|
border-color: #7adcc3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-hover .table-primary:hover {
|
.table-hover .table-primary:hover {
|
||||||
background-color: #b7c4d1;
|
background-color: #a4e7d6;
|
||||||
}
|
}
|
||||||
.table-hover .table-primary:hover > td,
|
.table-hover .table-primary:hover > td,
|
||||||
.table-hover .table-primary:hover > th {
|
.table-hover .table-primary:hover > th {
|
||||||
background-color: #b7c4d1;
|
background-color: #a4e7d6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-secondary,
|
.table-secondary,
|
||||||
|
@ -1908,9 +1907,9 @@ pre code {
|
||||||
.form-control:focus {
|
.form-control:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #444;
|
background-color: #444;
|
||||||
border-color: #739ac2;
|
border-color: #3dffcd;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.form-control::placeholder {
|
.form-control::placeholder {
|
||||||
color: #888;
|
color: #888;
|
||||||
|
@ -2408,7 +2407,7 @@ textarea.form-control.is-invalid {
|
||||||
.btn:focus,
|
.btn:focus,
|
||||||
.btn.focus {
|
.btn.focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.btn.disabled,
|
.btn.disabled,
|
||||||
.btn:disabled {
|
.btn:disabled {
|
||||||
|
@ -2424,38 +2423,38 @@ fieldset:disabled a.btn {
|
||||||
|
|
||||||
.btn-primary {
|
.btn-primary {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.btn-primary:hover {
|
.btn-primary:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #2b4764;
|
background-color: #009670;
|
||||||
border-color: #28415b;
|
border-color: #008966;
|
||||||
}
|
}
|
||||||
.btn-primary:focus,
|
.btn-primary:focus,
|
||||||
.btn-primary.focus {
|
.btn-primary.focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #2b4764;
|
background-color: #009670;
|
||||||
border-color: #28415b;
|
border-color: #008966;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(85, 115, 146, 0.5);
|
box-shadow: 0 0 0 0.2rem rgba(38, 198, 157, 0.5);
|
||||||
}
|
}
|
||||||
.btn-primary.disabled,
|
.btn-primary.disabled,
|
||||||
.btn-primary:disabled {
|
.btn-primary:disabled {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.btn-primary:not(:disabled):not(.disabled):active,
|
.btn-primary:not(:disabled):not(.disabled):active,
|
||||||
.btn-primary:not(:disabled):not(.disabled).active,
|
.btn-primary:not(:disabled):not(.disabled).active,
|
||||||
.show > .btn-primary.dropdown-toggle {
|
.show > .btn-primary.dropdown-toggle {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #28415b;
|
background-color: #008966;
|
||||||
border-color: #243a53;
|
border-color: #007c5d;
|
||||||
}
|
}
|
||||||
.btn-primary:not(:disabled):not(.disabled):active:focus,
|
.btn-primary:not(:disabled):not(.disabled):active:focus,
|
||||||
.btn-primary:not(:disabled):not(.disabled).active:focus,
|
.btn-primary:not(:disabled):not(.disabled).active:focus,
|
||||||
.show > .btn-primary.dropdown-toggle:focus {
|
.show > .btn-primary.dropdown-toggle:focus {
|
||||||
box-shadow: 0 0 0 0.2rem rgba(85, 115, 146, 0.5);
|
box-shadow: 0 0 0 0.2rem rgba(38, 198, 157, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-secondary {
|
.btn-secondary {
|
||||||
|
@ -2711,34 +2710,34 @@ fieldset:disabled a.btn {
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-primary {
|
.btn-outline-primary {
|
||||||
color: #375a7f;
|
color: #00bc8c;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.btn-outline-primary:hover {
|
.btn-outline-primary:hover {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.btn-outline-primary:focus,
|
.btn-outline-primary:focus,
|
||||||
.btn-outline-primary.focus {
|
.btn-outline-primary.focus {
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.5);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
.btn-outline-primary.disabled,
|
.btn-outline-primary.disabled,
|
||||||
.btn-outline-primary:disabled {
|
.btn-outline-primary:disabled {
|
||||||
color: #375a7f;
|
color: #00bc8c;
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
}
|
}
|
||||||
.btn-outline-primary:not(:disabled):not(.disabled):active,
|
.btn-outline-primary:not(:disabled):not(.disabled):active,
|
||||||
.btn-outline-primary:not(:disabled):not(.disabled).active,
|
.btn-outline-primary:not(:disabled):not(.disabled).active,
|
||||||
.show > .btn-outline-primary.dropdown-toggle {
|
.show > .btn-outline-primary.dropdown-toggle {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.btn-outline-primary:not(:disabled):not(.disabled):active:focus,
|
.btn-outline-primary:not(:disabled):not(.disabled):active:focus,
|
||||||
.btn-outline-primary:not(:disabled):not(.disabled).active:focus,
|
.btn-outline-primary:not(:disabled):not(.disabled).active:focus,
|
||||||
.show > .btn-outline-primary.dropdown-toggle:focus {
|
.show > .btn-outline-primary.dropdown-toggle:focus {
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.5);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-outline-secondary {
|
.btn-outline-secondary {
|
||||||
|
@ -3246,13 +3245,13 @@ input[type="button"].btn-block {
|
||||||
.dropdown-item:focus {
|
.dropdown-item:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.dropdown-item.active,
|
.dropdown-item.active,
|
||||||
.dropdown-item:active {
|
.dropdown-item:active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.dropdown-item.disabled,
|
.dropdown-item.disabled,
|
||||||
.dropdown-item:disabled {
|
.dropdown-item:disabled {
|
||||||
|
@ -3617,19 +3616,19 @@ input[type="button"].btn-block {
|
||||||
}
|
}
|
||||||
.custom-control-input:checked ~ .custom-control-label::before {
|
.custom-control-input:checked ~ .custom-control-label::before {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.custom-control-input:focus ~ .custom-control-label::before {
|
.custom-control-input:focus ~ .custom-control-label::before {
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
|
.custom-control-input:focus:not(:checked) ~ .custom-control-label::before {
|
||||||
border-color: #739ac2;
|
border-color: #3dffcd;
|
||||||
}
|
}
|
||||||
.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
|
.custom-control-input:not(:disabled):active ~ .custom-control-label::before {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #97b3d2;
|
background-color: #70ffda;
|
||||||
border-color: #97b3d2;
|
border-color: #70ffda;
|
||||||
}
|
}
|
||||||
.custom-control-input[disabled] ~ .custom-control-label,
|
.custom-control-input[disabled] ~ .custom-control-label,
|
||||||
.custom-control-input:disabled ~ .custom-control-label {
|
.custom-control-input:disabled ~ .custom-control-label {
|
||||||
|
@ -3677,8 +3676,8 @@ input[type="button"].btn-block {
|
||||||
.custom-checkbox
|
.custom-checkbox
|
||||||
.custom-control-input:indeterminate
|
.custom-control-input:indeterminate
|
||||||
~ .custom-control-label::before {
|
~ .custom-control-label::before {
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.custom-checkbox
|
.custom-checkbox
|
||||||
.custom-control-input:indeterminate
|
.custom-control-input:indeterminate
|
||||||
|
@ -3688,12 +3687,12 @@ input[type="button"].btn-block {
|
||||||
.custom-checkbox
|
.custom-checkbox
|
||||||
.custom-control-input:disabled:checked
|
.custom-control-input:disabled:checked
|
||||||
~ .custom-control-label::before {
|
~ .custom-control-label::before {
|
||||||
background-color: rgba(55, 90, 127, 0.5);
|
background-color: rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
.custom-checkbox
|
.custom-checkbox
|
||||||
.custom-control-input:disabled:indeterminate
|
.custom-control-input:disabled:indeterminate
|
||||||
~ .custom-control-label::before {
|
~ .custom-control-label::before {
|
||||||
background-color: rgba(55, 90, 127, 0.5);
|
background-color: rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-radio .custom-control-label::before {
|
.custom-radio .custom-control-label::before {
|
||||||
|
@ -3705,7 +3704,7 @@ input[type="button"].btn-block {
|
||||||
.custom-radio
|
.custom-radio
|
||||||
.custom-control-input:disabled:checked
|
.custom-control-input:disabled:checked
|
||||||
~ .custom-control-label::before {
|
~ .custom-control-label::before {
|
||||||
background-color: rgba(55, 90, 127, 0.5);
|
background-color: rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-switch {
|
.custom-switch {
|
||||||
|
@ -3739,7 +3738,7 @@ input[type="button"].btn-block {
|
||||||
.custom-switch
|
.custom-switch
|
||||||
.custom-control-input:disabled:checked
|
.custom-control-input:disabled:checked
|
||||||
~ .custom-control-label::before {
|
~ .custom-control-label::before {
|
||||||
background-color: rgba(55, 90, 127, 0.5);
|
background-color: rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.custom-select {
|
.custom-select {
|
||||||
|
@ -3760,9 +3759,9 @@ input[type="button"].btn-block {
|
||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
.custom-select:focus {
|
.custom-select:focus {
|
||||||
border-color: #739ac2;
|
border-color: #3dffcd;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.custom-select:focus::-ms-value {
|
.custom-select:focus::-ms-value {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
|
@ -3820,8 +3819,8 @@ input[type="button"].btn-block {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
.custom-file-input:focus ~ .custom-file-label {
|
.custom-file-input:focus ~ .custom-file-label {
|
||||||
border-color: #739ac2;
|
border-color: #3dffcd;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.custom-file-input[disabled] ~ .custom-file-label,
|
.custom-file-input[disabled] ~ .custom-file-label,
|
||||||
.custom-file-input:disabled ~ .custom-file-label {
|
.custom-file-input:disabled ~ .custom-file-label {
|
||||||
|
@ -3878,13 +3877,13 @@ input[type="button"].btn-block {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
.custom-range:focus::-webkit-slider-thumb {
|
.custom-range:focus::-webkit-slider-thumb {
|
||||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.custom-range:focus::-moz-range-thumb {
|
.custom-range:focus::-moz-range-thumb {
|
||||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.custom-range:focus::-ms-thumb {
|
.custom-range:focus::-ms-thumb {
|
||||||
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 1px #222, 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
.custom-range::-moz-focus-outer {
|
.custom-range::-moz-focus-outer {
|
||||||
border: 0;
|
border: 0;
|
||||||
|
@ -3893,7 +3892,7 @@ input[type="button"].btn-block {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
margin-top: -0.25rem;
|
margin-top: -0.25rem;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||||
|
@ -3906,7 +3905,7 @@ input[type="button"].btn-block {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-range::-webkit-slider-thumb:active {
|
.custom-range::-webkit-slider-thumb:active {
|
||||||
background-color: #97b3d2;
|
background-color: #70ffda;
|
||||||
}
|
}
|
||||||
.custom-range::-webkit-slider-runnable-track {
|
.custom-range::-webkit-slider-runnable-track {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -3920,7 +3919,7 @@ input[type="button"].btn-block {
|
||||||
.custom-range::-moz-range-thumb {
|
.custom-range::-moz-range-thumb {
|
||||||
width: 1rem;
|
width: 1rem;
|
||||||
height: 1rem;
|
height: 1rem;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||||
|
@ -3933,7 +3932,7 @@ input[type="button"].btn-block {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-range::-moz-range-thumb:active {
|
.custom-range::-moz-range-thumb:active {
|
||||||
background-color: #97b3d2;
|
background-color: #70ffda;
|
||||||
}
|
}
|
||||||
.custom-range::-moz-range-track {
|
.custom-range::-moz-range-track {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -3950,7 +3949,7 @@ input[type="button"].btn-block {
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
margin-right: 0.2rem;
|
margin-right: 0.2rem;
|
||||||
margin-left: 0.2rem;
|
margin-left: 0.2rem;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border: 0;
|
border: 0;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
transition: background-color 0.15s ease-in-out, border-color 0.15s ease-in-out,
|
||||||
|
@ -3963,7 +3962,7 @@ input[type="button"].btn-block {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.custom-range::-ms-thumb:active {
|
.custom-range::-ms-thumb:active {
|
||||||
background-color: #97b3d2;
|
background-color: #70ffda;
|
||||||
}
|
}
|
||||||
.custom-range::-ms-track {
|
.custom-range::-ms-track {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
@ -4075,7 +4074,7 @@ input[type="button"].btn-block {
|
||||||
.nav-pills .nav-link.active,
|
.nav-pills .nav-link.active,
|
||||||
.nav-pills .show > .nav-link {
|
.nav-pills .show > .nav-link {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
|
|
||||||
.nav-fill > .nav-link,
|
.nav-fill > .nav-link,
|
||||||
|
@ -4762,7 +4761,7 @@ input[type="button"].btn-block {
|
||||||
.page-link:focus {
|
.page-link:focus {
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.25);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-item:first-child .page-link {
|
.page-item:first-child .page-link {
|
||||||
|
@ -4856,17 +4855,17 @@ a.badge:focus {
|
||||||
|
|
||||||
.badge-primary {
|
.badge-primary {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
}
|
}
|
||||||
a.badge-primary:hover,
|
a.badge-primary:hover,
|
||||||
a.badge-primary:focus {
|
a.badge-primary:focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #28415b;
|
background-color: #008966;
|
||||||
}
|
}
|
||||||
a.badge-primary:focus,
|
a.badge-primary:focus,
|
||||||
a.badge-primary.focus {
|
a.badge-primary.focus {
|
||||||
outline: 0;
|
outline: 0;
|
||||||
box-shadow: 0 0 0 0.2rem rgba(55, 90, 127, 0.5);
|
box-shadow: 0 0 0 0.2rem rgba(0, 188, 140, 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
.badge-secondary {
|
.badge-secondary {
|
||||||
|
@ -5021,15 +5020,15 @@ a.badge-dark.focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-primary {
|
.alert-primary {
|
||||||
color: #1d2f42;
|
color: #006249;
|
||||||
background-color: #d7dee5;
|
background-color: #ccf2e8;
|
||||||
border-color: #c7d1db;
|
border-color: #b8ecdf;
|
||||||
}
|
}
|
||||||
.alert-primary hr {
|
.alert-primary hr {
|
||||||
border-top-color: #b7c4d1;
|
border-top-color: #a4e7d6;
|
||||||
}
|
}
|
||||||
.alert-primary .alert-link {
|
.alert-primary .alert-link {
|
||||||
color: #0d161f;
|
color: #002f23;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert-secondary {
|
.alert-secondary {
|
||||||
|
@ -5142,7 +5141,7 @@ a.badge-dark.focus {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
transition: width 0.6s ease;
|
transition: width 0.6s ease;
|
||||||
}
|
}
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
@ -5232,8 +5231,8 @@ a.badge-dark.focus {
|
||||||
.list-group-item.active {
|
.list-group-item.active {
|
||||||
z-index: 2;
|
z-index: 2;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #375a7f;
|
background-color: #00bc8c;
|
||||||
border-color: #375a7f;
|
border-color: #00bc8c;
|
||||||
}
|
}
|
||||||
.list-group-item + .list-group-item {
|
.list-group-item + .list-group-item {
|
||||||
border-top-width: 0;
|
border-top-width: 0;
|
||||||
|
@ -5373,18 +5372,18 @@ a.badge-dark.focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item-primary {
|
.list-group-item-primary {
|
||||||
color: #1d2f42;
|
color: #006249;
|
||||||
background-color: #c7d1db;
|
background-color: #b8ecdf;
|
||||||
}
|
}
|
||||||
.list-group-item-primary.list-group-item-action:hover,
|
.list-group-item-primary.list-group-item-action:hover,
|
||||||
.list-group-item-primary.list-group-item-action:focus {
|
.list-group-item-primary.list-group-item-action:focus {
|
||||||
color: #1d2f42;
|
color: #006249;
|
||||||
background-color: #b7c4d1;
|
background-color: #a4e7d6;
|
||||||
}
|
}
|
||||||
.list-group-item-primary.list-group-item-action.active {
|
.list-group-item-primary.list-group-item-action.active {
|
||||||
color: #fff;
|
color: #fff;
|
||||||
background-color: #1d2f42;
|
background-color: #006249;
|
||||||
border-color: #1d2f42;
|
border-color: #006249;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-group-item-secondary {
|
.list-group-item-secondary {
|
||||||
|
@ -6290,14 +6289,14 @@ a.close.disabled {
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-primary {
|
.bg-primary {
|
||||||
background-color: #375a7f !important;
|
background-color: #00bc8c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.bg-primary:hover,
|
a.bg-primary:hover,
|
||||||
a.bg-primary:focus,
|
a.bg-primary:focus,
|
||||||
button.bg-primary:hover,
|
button.bg-primary:hover,
|
||||||
button.bg-primary:focus {
|
button.bg-primary:focus {
|
||||||
background-color: #28415b !important;
|
background-color: #008966 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.bg-secondary {
|
.bg-secondary {
|
||||||
|
@ -6426,7 +6425,7 @@ button.bg-dark:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-primary {
|
.border-primary {
|
||||||
border-color: #375a7f !important;
|
border-color: #00bc8c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.border-secondary {
|
.border-secondary {
|
||||||
|
@ -9447,12 +9446,12 @@ button.bg-dark:focus {
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-primary {
|
.text-primary {
|
||||||
color: #375a7f !important;
|
color: #00bc8c !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.text-primary:hover,
|
a.text-primary:hover,
|
||||||
a.text-primary:focus {
|
a.text-primary:focus {
|
||||||
color: #20344a !important;
|
color: #007053 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-secondary {
|
.text-secondary {
|
||||||
|
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
116
src/server/handlers/catch-all-handler.tsx
Normal file
116
src/server/handlers/catch-all-handler.tsx
Normal file
|
@ -0,0 +1,116 @@
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { StaticRouter, matchPath } from "inferno-router";
|
||||||
|
import { renderToString } from "inferno-server";
|
||||||
|
import IsomorphicCookie from "isomorphic-cookie";
|
||||||
|
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||||
|
import { App } from "../../shared/components/app/app";
|
||||||
|
import { getHttpBaseInternal } from "../../shared/env";
|
||||||
|
import {
|
||||||
|
InitialFetchRequest,
|
||||||
|
IsoDataOptionalSite,
|
||||||
|
RouteData,
|
||||||
|
} from "../../shared/interfaces";
|
||||||
|
import { routes } from "../../shared/routes";
|
||||||
|
import {
|
||||||
|
FailedRequestState,
|
||||||
|
wrapClient,
|
||||||
|
} from "../../shared/services/HttpService";
|
||||||
|
import { ErrorPageData, initializeSite, isAuthPath } from "../../shared/utils";
|
||||||
|
import { createSsrHtml } from "../utils/create-ssr-html";
|
||||||
|
import { getErrorPageData } from "../utils/get-error-page-data";
|
||||||
|
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
|
||||||
|
|
||||||
|
export default async (req: Request, res: Response) => {
|
||||||
|
try {
|
||||||
|
const activeRoute = routes.find(route => matchPath(req.path, route));
|
||||||
|
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
||||||
|
|
||||||
|
const getSiteForm: GetSite = { auth };
|
||||||
|
|
||||||
|
const headers = setForwardedHeaders(req.headers);
|
||||||
|
const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
|
||||||
|
|
||||||
|
const { path, url, query } = req;
|
||||||
|
|
||||||
|
// Get site data first
|
||||||
|
// This bypasses errors, so that the client can hit the error on its own,
|
||||||
|
// in order to remove the jwt on the browser. Necessary for wrong jwts
|
||||||
|
let site: GetSiteResponse | undefined = undefined;
|
||||||
|
let routeData: RouteData = {};
|
||||||
|
let errorPageData: ErrorPageData | undefined = undefined;
|
||||||
|
let try_site = await client.getSite(getSiteForm);
|
||||||
|
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
||||||
|
console.error(
|
||||||
|
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
||||||
|
);
|
||||||
|
getSiteForm.auth = undefined;
|
||||||
|
auth = undefined;
|
||||||
|
try_site = await client.getSite(getSiteForm);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!auth && isAuthPath(path)) {
|
||||||
|
return res.redirect("/login");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (try_site.state === "success") {
|
||||||
|
site = try_site.data;
|
||||||
|
initializeSite(site);
|
||||||
|
|
||||||
|
if (path !== "/setup" && !site.site_view.local_site.site_setup) {
|
||||||
|
return res.redirect("/setup");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (site && activeRoute?.fetchInitialData) {
|
||||||
|
const initialFetchReq: InitialFetchRequest = {
|
||||||
|
client,
|
||||||
|
auth,
|
||||||
|
path,
|
||||||
|
query,
|
||||||
|
site,
|
||||||
|
};
|
||||||
|
|
||||||
|
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
||||||
|
}
|
||||||
|
} else if (try_site.state === "failed") {
|
||||||
|
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
||||||
|
}
|
||||||
|
|
||||||
|
const error = Object.values(routeData).find(
|
||||||
|
res => res.state === "failed"
|
||||||
|
) as FailedRequestState | undefined;
|
||||||
|
|
||||||
|
// Redirect to the 404 if there's an API error
|
||||||
|
if (error) {
|
||||||
|
console.error(error.msg);
|
||||||
|
if (error.msg === "instance_is_private") {
|
||||||
|
return res.redirect(`/signup`);
|
||||||
|
} else {
|
||||||
|
errorPageData = getErrorPageData(new Error(error.msg), site);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const isoData: IsoDataOptionalSite = {
|
||||||
|
path,
|
||||||
|
site_res: site,
|
||||||
|
routeData,
|
||||||
|
errorPageData,
|
||||||
|
};
|
||||||
|
|
||||||
|
const wrapper = (
|
||||||
|
<StaticRouter location={url} context={isoData}>
|
||||||
|
<App />
|
||||||
|
</StaticRouter>
|
||||||
|
);
|
||||||
|
|
||||||
|
const root = renderToString(wrapper);
|
||||||
|
|
||||||
|
res.send(await createSsrHtml(root, isoData));
|
||||||
|
} catch (err) {
|
||||||
|
// If an error is caught here, the error page couldn't even be rendered
|
||||||
|
console.error(err);
|
||||||
|
res.statusCode = 500;
|
||||||
|
return res.send(
|
||||||
|
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
18
src/server/handlers/robots-handler.ts
Normal file
18
src/server/handlers/robots-handler.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import type { Response } from "express";
|
||||||
|
|
||||||
|
export default async ({ res }: { res: Response }) => {
|
||||||
|
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||||
|
|
||||||
|
res.send(`User-Agent: *
|
||||||
|
Disallow: /login
|
||||||
|
Disallow: /settings
|
||||||
|
Disallow: /create_community
|
||||||
|
Disallow: /create_post
|
||||||
|
Disallow: /create_private_message
|
||||||
|
Disallow: /inbox
|
||||||
|
Disallow: /setup
|
||||||
|
Disallow: /admin
|
||||||
|
Disallow: /password_change
|
||||||
|
Disallow: /search/
|
||||||
|
`);
|
||||||
|
};
|
14
src/server/handlers/service-worker-handler.ts
Normal file
14
src/server/handlers/service-worker-handler.ts
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import type { Response } from "express";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
export default async ({ res }: { res: Response }) => {
|
||||||
|
res
|
||||||
|
.setHeader("Content-Type", "application/javascript")
|
||||||
|
.sendFile(
|
||||||
|
path.resolve(
|
||||||
|
`./dist/service-worker${
|
||||||
|
process.env.NODE_ENV === "development" ? "-development" : ""
|
||||||
|
}.js`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
};
|
32
src/server/handlers/theme-handler.ts
Normal file
32
src/server/handlers/theme-handler.ts
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
const extraThemesFolder =
|
||||||
|
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
||||||
|
|
||||||
|
export default async (req: Request, res: Response) => {
|
||||||
|
res.contentType("text/css");
|
||||||
|
|
||||||
|
const theme = req.params.name;
|
||||||
|
|
||||||
|
if (!theme.endsWith(".css")) {
|
||||||
|
res.statusCode = 400;
|
||||||
|
res.send("Theme must be a css file");
|
||||||
|
}
|
||||||
|
|
||||||
|
const customTheme = path.resolve(`./${extraThemesFolder}/${theme}`);
|
||||||
|
|
||||||
|
if (existsSync(customTheme)) {
|
||||||
|
res.sendFile(customTheme);
|
||||||
|
} else {
|
||||||
|
const internalTheme = path.resolve(`./dist/assets/css/themes/${theme}`);
|
||||||
|
|
||||||
|
// If the theme doesn't exist, just send litely
|
||||||
|
if (existsSync(internalTheme)) {
|
||||||
|
res.sendFile(internalTheme);
|
||||||
|
} else {
|
||||||
|
res.sendFile(path.resolve("./dist/assets/css/themes/litely.css"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
6
src/server/handlers/themes-list-handler.ts
Normal file
6
src/server/handlers/themes-list-handler.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
import type { Response } from "express";
|
||||||
|
import { buildThemeList } from "../utils/build-themes-list";
|
||||||
|
|
||||||
|
export default async ({ res }: { res: Response }) => {
|
||||||
|
res.type("json").send(JSON.stringify(await buildThemeList()));
|
||||||
|
};
|
|
@ -1,463 +1,38 @@
|
||||||
import express from "express";
|
import express from "express";
|
||||||
import { existsSync } from "fs";
|
|
||||||
import { readdir, readFile } from "fs/promises";
|
|
||||||
import { IncomingHttpHeaders } from "http";
|
|
||||||
import { Helmet } from "inferno-helmet";
|
|
||||||
import { matchPath, StaticRouter } from "inferno-router";
|
|
||||||
import { renderToString } from "inferno-server";
|
|
||||||
import IsomorphicCookie from "isomorphic-cookie";
|
|
||||||
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import process from "process";
|
import process from "process";
|
||||||
import serialize from "serialize-javascript";
|
import CatchAllHandler from "./handlers/catch-all-handler";
|
||||||
import sharp from "sharp";
|
import RobotsHandler from "./handlers/robots-handler";
|
||||||
import { App } from "../shared/components/app/app";
|
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
||||||
import { getHttpBaseExternal, getHttpBaseInternal } from "../shared/env";
|
import ThemeHandler from "./handlers/theme-handler";
|
||||||
import {
|
import ThemesListHandler from "./handlers/themes-list-handler";
|
||||||
ILemmyConfig,
|
import setDefaultCsp from "./middleware/set-default-csp";
|
||||||
InitialFetchRequest,
|
|
||||||
IsoDataOptionalSite,
|
|
||||||
RouteData,
|
|
||||||
} from "../shared/interfaces";
|
|
||||||
import { routes } from "../shared/routes";
|
|
||||||
import { FailedRequestState, wrapClient } from "../shared/services/HttpService";
|
|
||||||
import {
|
|
||||||
ErrorPageData,
|
|
||||||
favIconPngUrl,
|
|
||||||
favIconUrl,
|
|
||||||
initializeSite,
|
|
||||||
isAuthPath,
|
|
||||||
} from "../shared/utils";
|
|
||||||
|
|
||||||
const server = express();
|
const server = express();
|
||||||
|
|
||||||
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||||
? process.env["LEMMY_UI_HOST"].split(":")
|
? process.env["LEMMY_UI_HOST"].split(":")
|
||||||
: ["0.0.0.0", "1234"];
|
: ["0.0.0.0", "1234"];
|
||||||
const extraThemesFolder =
|
|
||||||
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
|
||||||
|
|
||||||
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
|
||||||
server.use(function (_req, res, next) {
|
|
||||||
res.setHeader(
|
|
||||||
"Content-Security-Policy",
|
|
||||||
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
|
|
||||||
);
|
|
||||||
next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
|
||||||
|
|
||||||
server.use(express.json());
|
server.use(express.json());
|
||||||
server.use(express.urlencoded({ extended: false }));
|
server.use(express.urlencoded({ extended: false }));
|
||||||
server.use("/static", express.static(path.resolve("./dist")));
|
server.use("/static", express.static(path.resolve("./dist")));
|
||||||
|
|
||||||
const robotstxt = `User-Agent: *
|
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||||
Disallow: /login
|
server.use(setDefaultCsp);
|
||||||
Disallow: /settings
|
|
||||||
Disallow: /create_community
|
|
||||||
Disallow: /create_post
|
|
||||||
Disallow: /create_private_message
|
|
||||||
Disallow: /inbox
|
|
||||||
Disallow: /setup
|
|
||||||
Disallow: /admin
|
|
||||||
Disallow: /password_change
|
|
||||||
Disallow: /search/
|
|
||||||
`;
|
|
||||||
|
|
||||||
server.get("/service-worker.js", async (_req, res) => {
|
|
||||||
res.setHeader("Content-Type", "application/javascript");
|
|
||||||
res.sendFile(
|
|
||||||
path.resolve(
|
|
||||||
`./dist/service-worker${
|
|
||||||
process.env.NODE_ENV === "development" ? "-development" : ""
|
|
||||||
}.js`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.get("/robots.txt", async (_req, res) => {
|
|
||||||
res.setHeader("content-type", "text/plain; charset=utf-8");
|
|
||||||
res.send(robotstxt);
|
|
||||||
});
|
|
||||||
|
|
||||||
server.get("/css/themes/:name", async (req, res) => {
|
|
||||||
res.contentType("text/css");
|
|
||||||
const theme = req.params.name;
|
|
||||||
if (!theme.endsWith(".css")) {
|
|
||||||
res.statusCode = 400;
|
|
||||||
res.send("Theme must be a css file");
|
|
||||||
}
|
|
||||||
|
|
||||||
const customTheme = path.resolve(`./${extraThemesFolder}/${theme}`);
|
|
||||||
if (existsSync(customTheme)) {
|
|
||||||
res.sendFile(customTheme);
|
|
||||||
} else {
|
|
||||||
const internalTheme = path.resolve(`./dist/assets/css/themes/${theme}`);
|
|
||||||
|
|
||||||
// If the theme doesn't exist, just send litely
|
|
||||||
if (existsSync(internalTheme)) {
|
|
||||||
res.sendFile(internalTheme);
|
|
||||||
} else {
|
|
||||||
res.sendFile(path.resolve("./dist/assets/css/themes/litely.css"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
async function buildThemeList(): Promise<string[]> {
|
|
||||||
const themes = ["darkly", "darkly-red", "litely", "litely-red"];
|
|
||||||
if (existsSync(extraThemesFolder)) {
|
|
||||||
const dirThemes = await readdir(extraThemesFolder);
|
|
||||||
const cssThemes = dirThemes
|
|
||||||
.filter(d => d.endsWith(".css"))
|
|
||||||
.map(d => d.replace(".css", ""));
|
|
||||||
themes.push(...cssThemes);
|
|
||||||
}
|
|
||||||
return themes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
server.get("/css/themelist", async (_req, res) => {
|
server.get("/robots.txt", RobotsHandler);
|
||||||
res.type("json");
|
server.get("/service-worker.js", ServiceWorkerHandler);
|
||||||
res.send(JSON.stringify(await buildThemeList()));
|
server.get("/css/themes/:name", ThemeHandler);
|
||||||
});
|
server.get("/css/themelist", ThemesListHandler);
|
||||||
|
server.get("/*", CatchAllHandler);
|
||||||
// server.use(cookieParser());
|
|
||||||
server.get("/*", async (req, res) => {
|
|
||||||
try {
|
|
||||||
const activeRoute = routes.find(route => matchPath(req.path, route));
|
|
||||||
let auth: string | undefined = IsomorphicCookie.load("jwt", req);
|
|
||||||
|
|
||||||
const getSiteForm: GetSite = { auth };
|
|
||||||
|
|
||||||
const headers = setForwardedHeaders(req.headers);
|
|
||||||
const client = wrapClient(new LemmyHttp(getHttpBaseInternal(), headers));
|
|
||||||
|
|
||||||
const { path, url, query } = req;
|
|
||||||
|
|
||||||
// Get site data first
|
|
||||||
// This bypasses errors, so that the client can hit the error on its own,
|
|
||||||
// in order to remove the jwt on the browser. Necessary for wrong jwts
|
|
||||||
let site: GetSiteResponse | undefined = undefined;
|
|
||||||
let routeData: RouteData = {};
|
|
||||||
let errorPageData: ErrorPageData | undefined = undefined;
|
|
||||||
let try_site = await client.getSite(getSiteForm);
|
|
||||||
if (try_site.state === "failed" && try_site.msg == "not_logged_in") {
|
|
||||||
console.error(
|
|
||||||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
|
||||||
);
|
|
||||||
getSiteForm.auth = undefined;
|
|
||||||
auth = undefined;
|
|
||||||
try_site = await client.getSite(getSiteForm);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!auth && isAuthPath(path)) {
|
|
||||||
return res.redirect("/login");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (try_site.state === "success") {
|
|
||||||
site = try_site.data;
|
|
||||||
initializeSite(site);
|
|
||||||
|
|
||||||
if (path !== "/setup" && !site.site_view.local_site.site_setup) {
|
|
||||||
return res.redirect("/setup");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (site && activeRoute?.fetchInitialData) {
|
|
||||||
const initialFetchReq: InitialFetchRequest = {
|
|
||||||
client,
|
|
||||||
auth,
|
|
||||||
path,
|
|
||||||
query,
|
|
||||||
site,
|
|
||||||
};
|
|
||||||
|
|
||||||
routeData = await activeRoute.fetchInitialData(initialFetchReq);
|
|
||||||
}
|
|
||||||
} else if (try_site.state === "failed") {
|
|
||||||
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
|
||||||
}
|
|
||||||
|
|
||||||
const error = Object.values(routeData).find(
|
|
||||||
res => res.state === "failed"
|
|
||||||
) as FailedRequestState | undefined;
|
|
||||||
|
|
||||||
// Redirect to the 404 if there's an API error
|
|
||||||
if (error) {
|
|
||||||
console.error(error.msg);
|
|
||||||
if (error.msg === "instance_is_private") {
|
|
||||||
return res.redirect(`/signup`);
|
|
||||||
} else {
|
|
||||||
errorPageData = getErrorPageData(new Error(error.msg), site);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const isoData: IsoDataOptionalSite = {
|
|
||||||
path,
|
|
||||||
site_res: site,
|
|
||||||
routeData,
|
|
||||||
errorPageData,
|
|
||||||
};
|
|
||||||
|
|
||||||
const wrapper = (
|
|
||||||
<StaticRouter location={url} context={isoData}>
|
|
||||||
<App />
|
|
||||||
</StaticRouter>
|
|
||||||
);
|
|
||||||
|
|
||||||
const root = renderToString(wrapper);
|
|
||||||
|
|
||||||
res.send(await createSsrHtml(root, isoData));
|
|
||||||
} catch (err) {
|
|
||||||
// If an error is caught here, the error page couldn't even be rendered
|
|
||||||
console.error(err);
|
|
||||||
res.statusCode = 500;
|
|
||||||
return res.send(
|
|
||||||
process.env.NODE_ENV === "development" ? err.message : "Server error"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
server.listen(Number(port), hostname, () => {
|
server.listen(Number(port), hostname, () => {
|
||||||
console.log(`http://${hostname}:${port}`);
|
console.log(`http://${hostname}:${port}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
|
||||||
[key: string]: string;
|
|
||||||
} {
|
|
||||||
const out: { [key: string]: string } = {};
|
|
||||||
if (headers.host) {
|
|
||||||
out.host = headers.host;
|
|
||||||
}
|
|
||||||
const realIp = headers["x-real-ip"];
|
|
||||||
if (realIp) {
|
|
||||||
out["x-real-ip"] = realIp as string;
|
|
||||||
}
|
|
||||||
const forwardedFor = headers["x-forwarded-for"];
|
|
||||||
if (forwardedFor) {
|
|
||||||
out["x-forwarded-for"] = forwardedFor as string;
|
|
||||||
}
|
|
||||||
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|
||||||
process.on("SIGINT", () => {
|
process.on("SIGINT", () => {
|
||||||
console.info("Interrupted");
|
console.info("Interrupted");
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
const iconSizes = [72, 96, 144, 192, 512];
|
|
||||||
const defaultLogoPathDirectory = path.join(
|
|
||||||
process.cwd(),
|
|
||||||
"dist",
|
|
||||||
"assets",
|
|
||||||
"icons"
|
|
||||||
);
|
|
||||||
|
|
||||||
export async function generateManifestBase64({
|
|
||||||
my_user,
|
|
||||||
site_view: {
|
|
||||||
site,
|
|
||||||
local_site: { community_creation_admin_only },
|
|
||||||
},
|
|
||||||
}: GetSiteResponse) {
|
|
||||||
const url = getHttpBaseExternal();
|
|
||||||
|
|
||||||
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
|
||||||
|
|
||||||
const manifest = {
|
|
||||||
name: site.name,
|
|
||||||
description: site.description ?? "A link aggregator for the fediverse",
|
|
||||||
start_url: url,
|
|
||||||
scope: url,
|
|
||||||
display: "standalone",
|
|
||||||
id: "/",
|
|
||||||
background_color: "#222222",
|
|
||||||
theme_color: "#222222",
|
|
||||||
icons: await Promise.all(
|
|
||||||
iconSizes.map(async size => {
|
|
||||||
let src = await readFile(
|
|
||||||
path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`)
|
|
||||||
).then(buf => buf.toString("base64"));
|
|
||||||
|
|
||||||
if (icon) {
|
|
||||||
src = await sharp(icon)
|
|
||||||
.resize(size, size)
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
.then(buf => buf.toString("base64"));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
sizes: `${size}x${size}`,
|
|
||||||
type: "image/png",
|
|
||||||
src: `data:image/png;base64,${src}`,
|
|
||||||
purpose: "any maskable",
|
|
||||||
};
|
|
||||||
})
|
|
||||||
),
|
|
||||||
shortcuts: [
|
|
||||||
{
|
|
||||||
name: "Search",
|
|
||||||
short_name: "Search",
|
|
||||||
description: "Perform a search.",
|
|
||||||
url: "/search",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "Communities",
|
|
||||||
url: "/communities",
|
|
||||||
short_name: "Communities",
|
|
||||||
description: "Browse communities",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
.concat(
|
|
||||||
my_user
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: "Create Post",
|
|
||||||
url: "/create_post",
|
|
||||||
short_name: "Create Post",
|
|
||||||
description: "Create a post.",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
)
|
|
||||||
.concat(
|
|
||||||
my_user?.local_user_view.person.admin || !community_creation_admin_only
|
|
||||||
? [
|
|
||||||
{
|
|
||||||
name: "Create Community",
|
|
||||||
url: "/create_community",
|
|
||||||
short_name: "Create Community",
|
|
||||||
description: "Create a community",
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: []
|
|
||||||
),
|
|
||||||
related_applications: [
|
|
||||||
{
|
|
||||||
platform: "f-droid",
|
|
||||||
url: "https://f-droid.org/packages/com.jerboa/",
|
|
||||||
id: "com.jerboa",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
return Buffer.from(JSON.stringify(manifest)).toString("base64");
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchIconPng(iconUrl: string) {
|
|
||||||
return await fetch(iconUrl)
|
|
||||||
.then(res => res.blob())
|
|
||||||
.then(blob => blob.arrayBuffer());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getErrorPageData(error: Error, site?: GetSiteResponse) {
|
|
||||||
const errorPageData: ErrorPageData = {};
|
|
||||||
|
|
||||||
if (site) {
|
|
||||||
errorPageData.error = error.message;
|
|
||||||
}
|
|
||||||
|
|
||||||
const adminMatrixIds = site?.admins
|
|
||||||
.map(({ person: { matrix_user_id } }) => matrix_user_id)
|
|
||||||
.filter(id => id) as string[] | undefined;
|
|
||||||
if (adminMatrixIds && adminMatrixIds.length > 0) {
|
|
||||||
errorPageData.adminMatrixIds = adminMatrixIds;
|
|
||||||
}
|
|
||||||
|
|
||||||
return errorPageData;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createSsrHtml(root: string, isoData: IsoDataOptionalSite) {
|
|
||||||
const site = isoData.site_res;
|
|
||||||
const appleTouchIcon = site?.site_view.site.icon
|
|
||||||
? `data:image/png;base64,${sharp(
|
|
||||||
await fetchIconPng(site.site_view.site.icon)
|
|
||||||
)
|
|
||||||
.resize(180, 180)
|
|
||||||
.extend({
|
|
||||||
bottom: 20,
|
|
||||||
top: 20,
|
|
||||||
left: 20,
|
|
||||||
right: 20,
|
|
||||||
background: "#222222",
|
|
||||||
})
|
|
||||||
.png()
|
|
||||||
.toBuffer()
|
|
||||||
.then(buf => buf.toString("base64"))}`
|
|
||||||
: favIconPngUrl;
|
|
||||||
|
|
||||||
const erudaStr =
|
|
||||||
process.env["LEMMY_UI_DEBUG"] === "true"
|
|
||||||
? renderToString(
|
|
||||||
<>
|
|
||||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
|
||||||
<script>eruda.init();</script>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const helmet = Helmet.renderStatic();
|
|
||||||
|
|
||||||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
|
|
||||||
|
|
||||||
return `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html ${helmet.htmlAttributes.toString()}>
|
|
||||||
<head>
|
|
||||||
<script>window.isoData = ${serialize(isoData)}</script>
|
|
||||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
|
||||||
|
|
||||||
<!-- A remote debugging utility for mobile -->
|
|
||||||
${erudaStr}
|
|
||||||
|
|
||||||
<!-- Custom injected script -->
|
|
||||||
${customHtmlHeader}
|
|
||||||
|
|
||||||
${helmet.title.toString()}
|
|
||||||
${helmet.meta.toString()}
|
|
||||||
|
|
||||||
<!-- Required meta tags -->
|
|
||||||
<meta name="Description" content="Lemmy">
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
|
|
||||||
<link
|
|
||||||
id="favicon"
|
|
||||||
rel="shortcut icon"
|
|
||||||
type="image/x-icon"
|
|
||||||
href=${site?.site_view.site.icon ?? favIconUrl}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Web app manifest -->
|
|
||||||
${
|
|
||||||
site &&
|
|
||||||
`<link
|
|
||||||
rel="manifest"
|
|
||||||
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
|
||||||
site
|
|
||||||
)}`}
|
|
||||||
/>`
|
|
||||||
}
|
|
||||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
|
||||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
|
||||||
|
|
||||||
<!-- Styles -->
|
|
||||||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
|
||||||
|
|
||||||
<!-- Current theme and more -->
|
|
||||||
${helmet.link.toString()}
|
|
||||||
|
|
||||||
</head>
|
|
||||||
|
|
||||||
<body ${helmet.bodyAttributes.toString()}>
|
|
||||||
<noscript>
|
|
||||||
<div class="alert alert-danger rounded-0" role="alert">
|
|
||||||
<b>Javascript is disabled. Actions will not work.</b>
|
|
||||||
</div>
|
|
||||||
</noscript>
|
|
||||||
|
|
||||||
<div id='root'>${root}</div>
|
|
||||||
<script defer src='/static/js/client.js'></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
10
src/server/middleware/set-default-csp.ts
Normal file
10
src/server/middleware/set-default-csp.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import type { NextFunction, Response } from "express";
|
||||||
|
|
||||||
|
export default function ({ res, next }: { res: Response; next: NextFunction }) {
|
||||||
|
res.setHeader(
|
||||||
|
"Content-Security-Policy",
|
||||||
|
`default-src 'self'; manifest-src *; connect-src *; img-src * data:; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; form-action 'self'; base-uri 'self'; frame-src *; media-src *`
|
||||||
|
);
|
||||||
|
|
||||||
|
next();
|
||||||
|
}
|
18
src/server/utils/build-themes-list.ts
Normal file
18
src/server/utils/build-themes-list.ts
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { existsSync } from "fs";
|
||||||
|
import { readdir } from "fs/promises";
|
||||||
|
|
||||||
|
const extraThemesFolder =
|
||||||
|
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
||||||
|
|
||||||
|
const themes = ["darkly", "darkly-red", "litely", "litely-red"];
|
||||||
|
|
||||||
|
export async function buildThemeList(): Promise<string[]> {
|
||||||
|
if (existsSync(extraThemesFolder)) {
|
||||||
|
const dirThemes = await readdir(extraThemesFolder);
|
||||||
|
const cssThemes = dirThemes
|
||||||
|
.filter(d => d.endsWith(".css"))
|
||||||
|
.map(d => d.replace(".css", ""));
|
||||||
|
themes.push(...cssThemes);
|
||||||
|
}
|
||||||
|
return themes;
|
||||||
|
}
|
109
src/server/utils/create-ssr-html.tsx
Normal file
109
src/server/utils/create-ssr-html.tsx
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
import { Helmet } from "inferno-helmet";
|
||||||
|
import { renderToString } from "inferno-server";
|
||||||
|
import serialize from "serialize-javascript";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
||||||
|
import { favIconPngUrl, favIconUrl } from "../../shared/utils";
|
||||||
|
import { fetchIconPng } from "./fetch-icon-png";
|
||||||
|
import { generateManifestBase64 } from "./generate-manifest-base64";
|
||||||
|
|
||||||
|
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
||||||
|
|
||||||
|
export async function createSsrHtml(
|
||||||
|
root: string,
|
||||||
|
isoData: IsoDataOptionalSite
|
||||||
|
) {
|
||||||
|
const site = isoData.site_res;
|
||||||
|
|
||||||
|
const appleTouchIcon = site?.site_view.site.icon
|
||||||
|
? `data:image/png;base64,${sharp(
|
||||||
|
await fetchIconPng(site.site_view.site.icon)
|
||||||
|
)
|
||||||
|
.resize(180, 180)
|
||||||
|
.extend({
|
||||||
|
bottom: 20,
|
||||||
|
top: 20,
|
||||||
|
left: 20,
|
||||||
|
right: 20,
|
||||||
|
background: "#222222",
|
||||||
|
})
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
.then(buf => buf.toString("base64"))}`
|
||||||
|
: favIconPngUrl;
|
||||||
|
|
||||||
|
const erudaStr =
|
||||||
|
process.env["LEMMY_UI_DEBUG"] === "true"
|
||||||
|
? renderToString(
|
||||||
|
<>
|
||||||
|
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||||
|
<script>eruda.init();</script>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
: "";
|
||||||
|
|
||||||
|
const helmet = Helmet.renderStatic();
|
||||||
|
|
||||||
|
const config: ILemmyConfig = { wsHost: process.env.LEMMY_UI_LEMMY_WS_HOST };
|
||||||
|
|
||||||
|
return `
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html ${helmet.htmlAttributes.toString()}>
|
||||||
|
<head>
|
||||||
|
<script>window.isoData = ${serialize(isoData)}</script>
|
||||||
|
<script>window.lemmyConfig = ${serialize(config)}</script>
|
||||||
|
|
||||||
|
<!-- A remote debugging utility for mobile -->
|
||||||
|
${erudaStr}
|
||||||
|
|
||||||
|
<!-- Custom injected script -->
|
||||||
|
${customHtmlHeader}
|
||||||
|
|
||||||
|
${helmet.title.toString()}
|
||||||
|
${helmet.meta.toString()}
|
||||||
|
|
||||||
|
<!-- Required meta tags -->
|
||||||
|
<meta name="Description" content="Lemmy">
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no, user-scalable=no">
|
||||||
|
<link
|
||||||
|
id="favicon"
|
||||||
|
rel="shortcut icon"
|
||||||
|
type="image/x-icon"
|
||||||
|
href=${site?.site_view.site.icon ?? favIconUrl}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Web app manifest -->
|
||||||
|
${
|
||||||
|
site &&
|
||||||
|
`<link
|
||||||
|
rel="manifest"
|
||||||
|
href=${`data:application/manifest+json;base64,${await generateManifestBase64(
|
||||||
|
site
|
||||||
|
)}`}
|
||||||
|
/>`
|
||||||
|
}
|
||||||
|
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||||
|
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||||
|
|
||||||
|
<!-- Styles -->
|
||||||
|
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
||||||
|
|
||||||
|
<!-- Current theme and more -->
|
||||||
|
${helmet.link.toString()}
|
||||||
|
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body ${helmet.bodyAttributes.toString()}>
|
||||||
|
<noscript>
|
||||||
|
<div class="alert alert-danger rounded-0" role="alert">
|
||||||
|
<b>Javascript is disabled. Actions will not work.</b>
|
||||||
|
</div>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<div id='root'>${root}</div>
|
||||||
|
<script defer src='/static/js/client.js'></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
}
|
5
src/server/utils/fetch-icon-png.ts
Normal file
5
src/server/utils/fetch-icon-png.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export async function fetchIconPng(iconUrl: string) {
|
||||||
|
return await fetch(iconUrl)
|
||||||
|
.then(res => res.blob())
|
||||||
|
.then(blob => blob.arrayBuffer());
|
||||||
|
}
|
107
src/server/utils/generate-manifest-base64.ts
Normal file
107
src/server/utils/generate-manifest-base64.ts
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
import { readFile } from "fs/promises";
|
||||||
|
import { GetSiteResponse } from "lemmy-js-client";
|
||||||
|
import path from "path";
|
||||||
|
import sharp from "sharp";
|
||||||
|
import { getHttpBaseExternal } from "../../shared/env";
|
||||||
|
import { fetchIconPng } from "./fetch-icon-png";
|
||||||
|
|
||||||
|
const iconSizes = [72, 96, 144, 192, 512];
|
||||||
|
|
||||||
|
const defaultLogoPathDirectory = path.join(
|
||||||
|
process.cwd(),
|
||||||
|
"dist",
|
||||||
|
"assets",
|
||||||
|
"icons"
|
||||||
|
);
|
||||||
|
|
||||||
|
export async function generateManifestBase64({
|
||||||
|
my_user,
|
||||||
|
site_view: {
|
||||||
|
site,
|
||||||
|
local_site: { community_creation_admin_only },
|
||||||
|
},
|
||||||
|
}: GetSiteResponse) {
|
||||||
|
const url = getHttpBaseExternal();
|
||||||
|
|
||||||
|
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
||||||
|
|
||||||
|
const manifest = {
|
||||||
|
name: site.name,
|
||||||
|
description: site.description ?? "A link aggregator for the fediverse",
|
||||||
|
start_url: url,
|
||||||
|
scope: url,
|
||||||
|
display: "standalone",
|
||||||
|
id: "/",
|
||||||
|
background_color: "#222222",
|
||||||
|
theme_color: "#222222",
|
||||||
|
icons: await Promise.all(
|
||||||
|
iconSizes.map(async size => {
|
||||||
|
let src = await readFile(
|
||||||
|
path.join(defaultLogoPathDirectory, `icon-${size}x${size}.png`)
|
||||||
|
).then(buf => buf.toString("base64"));
|
||||||
|
|
||||||
|
if (icon) {
|
||||||
|
src = await sharp(icon)
|
||||||
|
.resize(size, size)
|
||||||
|
.png()
|
||||||
|
.toBuffer()
|
||||||
|
.then(buf => buf.toString("base64"));
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
sizes: `${size}x${size}`,
|
||||||
|
type: "image/png",
|
||||||
|
src: `data:image/png;base64,${src}`,
|
||||||
|
purpose: "any maskable",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
),
|
||||||
|
shortcuts: [
|
||||||
|
{
|
||||||
|
name: "Search",
|
||||||
|
short_name: "Search",
|
||||||
|
description: "Perform a search.",
|
||||||
|
url: "/search",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Communities",
|
||||||
|
url: "/communities",
|
||||||
|
short_name: "Communities",
|
||||||
|
description: "Browse communities",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
.concat(
|
||||||
|
my_user
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: "Create Post",
|
||||||
|
url: "/create_post",
|
||||||
|
short_name: "Create Post",
|
||||||
|
description: "Create a post.",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
)
|
||||||
|
.concat(
|
||||||
|
my_user?.local_user_view.person.admin || !community_creation_admin_only
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
name: "Create Community",
|
||||||
|
url: "/create_community",
|
||||||
|
short_name: "Create Community",
|
||||||
|
description: "Create a community",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []
|
||||||
|
),
|
||||||
|
related_applications: [
|
||||||
|
{
|
||||||
|
platform: "f-droid",
|
||||||
|
url: "https://f-droid.org/packages/com.jerboa/",
|
||||||
|
id: "com.jerboa",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
return Buffer.from(JSON.stringify(manifest)).toString("base64");
|
||||||
|
}
|
20
src/server/utils/get-error-page-data.ts
Normal file
20
src/server/utils/get-error-page-data.ts
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
import { GetSiteResponse } from "lemmy-js-client";
|
||||||
|
import { ErrorPageData } from "../../shared/utils";
|
||||||
|
|
||||||
|
export function getErrorPageData(error: Error, site?: GetSiteResponse) {
|
||||||
|
const errorPageData: ErrorPageData = {};
|
||||||
|
|
||||||
|
if (site) {
|
||||||
|
errorPageData.error = error.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
const adminMatrixIds = site?.admins
|
||||||
|
.map(({ person: { matrix_user_id } }) => matrix_user_id)
|
||||||
|
.filter(id => id) as string[] | undefined;
|
||||||
|
|
||||||
|
if (adminMatrixIds && adminMatrixIds.length > 0) {
|
||||||
|
errorPageData.adminMatrixIds = adminMatrixIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
return errorPageData;
|
||||||
|
}
|
25
src/server/utils/set-forwarded-headers.ts
Normal file
25
src/server/utils/set-forwarded-headers.ts
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import { IncomingHttpHeaders } from "http";
|
||||||
|
|
||||||
|
export function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
||||||
|
[key: string]: string;
|
||||||
|
} {
|
||||||
|
const out: { [key: string]: string } = {};
|
||||||
|
|
||||||
|
if (headers.host) {
|
||||||
|
out.host = headers.host;
|
||||||
|
}
|
||||||
|
|
||||||
|
const realIp = headers["x-real-ip"];
|
||||||
|
|
||||||
|
if (realIp) {
|
||||||
|
out["x-real-ip"] = realIp as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const forwardedFor = headers["x-forwarded-for"];
|
||||||
|
|
||||||
|
if (forwardedFor) {
|
||||||
|
out["x-forwarded-for"] = forwardedFor as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
return out;
|
||||||
|
}
|
|
@ -15,17 +15,19 @@ export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
|
||||||
const banner = this.props.banner;
|
const banner = this.props.banner;
|
||||||
const icon = this.props.icon;
|
const icon = this.props.icon;
|
||||||
return (
|
return (
|
||||||
<div className="position-relative mb-2">
|
(banner || icon) && (
|
||||||
{banner && <PictrsImage src={banner} banner alt="" />}
|
<div className="position-relative mb-2">
|
||||||
{icon && (
|
{banner && <PictrsImage src={banner} banner alt="" />}
|
||||||
<PictrsImage
|
{icon && (
|
||||||
src={icon}
|
<PictrsImage
|
||||||
iconOverlay
|
src={icon}
|
||||||
pushup={!!this.props.banner}
|
iconOverlay
|
||||||
alt=""
|
pushup={!!this.props.banner}
|
||||||
/>
|
alt=""
|
||||||
)}
|
/>
|
||||||
</div>
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -443,16 +443,17 @@ export class Home extends Component<any, HomeState> {
|
||||||
admins={admins}
|
admins={admins}
|
||||||
counts={counts}
|
counts={counts}
|
||||||
showLocal={showLocal(this.isoData)}
|
showLocal={showLocal(this.isoData)}
|
||||||
|
isMobile={true}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{showTrendingMobile && (
|
{showTrendingMobile && (
|
||||||
<div className="col-12 card border-secondary mb-3">
|
<div className="card border-secondary mb-3">
|
||||||
<div className="card-body">{this.trendingCommunities(true)}</div>
|
{this.trendingCommunities()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{showSubscribedMobile && (
|
{showSubscribedMobile && (
|
||||||
<div className="col-12 card border-secondary mb-3">
|
<div className="card border-secondary mb-3">
|
||||||
<div className="card-body">{this.subscribedCommunities}</div>
|
{this.subscribedCommunities(true)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -471,19 +472,7 @@ export class Home extends Component<any, HomeState> {
|
||||||
return (
|
return (
|
||||||
<div id="sidebarContainer">
|
<div id="sidebarContainer">
|
||||||
<section id="sidebarMain" className="card border-secondary mb-3">
|
<section id="sidebarMain" className="card border-secondary mb-3">
|
||||||
<div className="card-body">
|
{this.trendingCommunities()}
|
||||||
{this.trendingCommunities()}
|
|
||||||
{canCreateCommunity(this.state.siteRes) && (
|
|
||||||
<LinkButton
|
|
||||||
path="/create_community"
|
|
||||||
translationKey="create_a_community"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<LinkButton
|
|
||||||
path="/communities"
|
|
||||||
translationKey="explore_communities"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</section>
|
</section>
|
||||||
<SiteSidebar
|
<SiteSidebar
|
||||||
site={site}
|
site={site}
|
||||||
|
@ -492,18 +481,20 @@ export class Home extends Component<any, HomeState> {
|
||||||
showLocal={showLocal(this.isoData)}
|
showLocal={showLocal(this.isoData)}
|
||||||
/>
|
/>
|
||||||
{this.hasFollows && (
|
{this.hasFollows && (
|
||||||
<section
|
<div className="accordion">
|
||||||
id="sidebarSubscribed"
|
<section
|
||||||
className="card border-secondary mb-3"
|
id="sidebarSubscribed"
|
||||||
>
|
className="card border-secondary mb-3"
|
||||||
<div className="card-body">{this.subscribedCommunities}</div>
|
>
|
||||||
</section>
|
{this.subscribedCommunities(false)}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
trendingCommunities(isMobile = false) {
|
trendingCommunities() {
|
||||||
switch (this.state.trendingCommunitiesRes?.state) {
|
switch (this.state.trendingCommunitiesRes?.state) {
|
||||||
case "loading":
|
case "loading":
|
||||||
return (
|
return (
|
||||||
|
@ -514,68 +505,103 @@ export class Home extends Component<any, HomeState> {
|
||||||
case "success": {
|
case "success": {
|
||||||
const trending = this.state.trendingCommunitiesRes.data.communities;
|
const trending = this.state.trendingCommunitiesRes.data.communities;
|
||||||
return (
|
return (
|
||||||
<div className={!isMobile ? "mb-2" : ""}>
|
<>
|
||||||
<h5>
|
<header className="card-header d-flex align-items-center">
|
||||||
<T i18nKey="trending_communities">
|
<h5 className="mb-0">
|
||||||
#
|
<T i18nKey="trending_communities">
|
||||||
<Link className="text-body" to="/communities">
|
|
||||||
#
|
#
|
||||||
</Link>
|
<Link className="text-body" to="/communities">
|
||||||
</T>
|
#
|
||||||
</h5>
|
</Link>
|
||||||
<ul className="list-inline mb-0">
|
</T>
|
||||||
{trending.map(cv => (
|
</h5>
|
||||||
<li
|
</header>
|
||||||
key={cv.community.id}
|
<div className="card-body">
|
||||||
className="list-inline-item d-inline-block"
|
{trending.length > 0 && (
|
||||||
>
|
<ul className="list-inline">
|
||||||
<CommunityLink community={cv.community} />
|
{trending.map(cv => (
|
||||||
</li>
|
<li key={cv.community.id} className="list-inline-item">
|
||||||
))}
|
<CommunityLink community={cv.community} />
|
||||||
</ul>
|
</li>
|
||||||
</div>
|
))}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{canCreateCommunity(this.state.siteRes) && (
|
||||||
|
<LinkButton
|
||||||
|
path="/create_community"
|
||||||
|
translationKey="create_a_community"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<LinkButton
|
||||||
|
path="/communities"
|
||||||
|
translationKey="explore_communities"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
get subscribedCommunities() {
|
subscribedCommunities(isMobile = false) {
|
||||||
const { subscribedCollapsed } = this.state;
|
const { subscribedCollapsed } = this.state;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<h5>
|
<header
|
||||||
<T class="d-inline" i18nKey="subscribed_to_communities">
|
className="card-header d-flex align-items-center"
|
||||||
#
|
id="sidebarSubscribedHeader"
|
||||||
<Link className="text-body" to="/communities">
|
>
|
||||||
|
<h5 className="mb-0 d-inline">
|
||||||
|
<T class="d-inline" i18nKey="subscribed_to_communities">
|
||||||
#
|
#
|
||||||
</Link>
|
<Link className="text-body" to="/communities">
|
||||||
</T>
|
#
|
||||||
<button
|
</Link>
|
||||||
className="btn btn-sm text-muted"
|
</T>
|
||||||
onClick={linkEvent(this, this.handleCollapseSubscribe)}
|
</h5>
|
||||||
aria-label={i18n.t("collapse")}
|
{!isMobile && (
|
||||||
data-tippy-content={i18n.t("collapse")}
|
<button
|
||||||
>
|
type="button"
|
||||||
<Icon
|
className="btn btn-sm text-muted"
|
||||||
icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
|
onClick={linkEvent(this, this.handleCollapseSubscribe)}
|
||||||
classes="icon-inline"
|
aria-label={
|
||||||
/>
|
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||||
</button>
|
}
|
||||||
</h5>
|
data-tippy-content={
|
||||||
{!subscribedCollapsed && (
|
subscribedCollapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||||
<ul className="list-inline mb-0">
|
}
|
||||||
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
data-bs-toggle="collapse"
|
||||||
<li
|
data-bs-target="#sidebarSubscribedBody"
|
||||||
key={cfv.community.id}
|
aria-expanded="true"
|
||||||
className="list-inline-item d-inline-block"
|
aria-controls="sidebarSubscribedBody"
|
||||||
>
|
>
|
||||||
<CommunityLink community={cfv.community} />
|
<Icon
|
||||||
</li>
|
icon={`${subscribedCollapsed ? "plus" : "minus"}-square`}
|
||||||
))}
|
classes="icon-inline"
|
||||||
</ul>
|
/>
|
||||||
)}
|
</button>
|
||||||
</div>
|
)}
|
||||||
|
</header>
|
||||||
|
<div
|
||||||
|
id="sidebarSubscribedBody"
|
||||||
|
className="collapse show"
|
||||||
|
aria-labelledby="sidebarSubscribedHeader"
|
||||||
|
>
|
||||||
|
<div className="card-body">
|
||||||
|
<ul className="list-inline mb-0">
|
||||||
|
{UserService.Instance.myUserInfo?.follows.map(cfv => (
|
||||||
|
<li
|
||||||
|
key={cfv.community.id}
|
||||||
|
className="list-inline-item d-inline-block"
|
||||||
|
>
|
||||||
|
<CommunityLink community={cfv.community} />
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -298,27 +298,22 @@ export class Signup extends Component<any, State> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{this.renderCaptcha()}
|
{this.renderCaptcha()}
|
||||||
{siteView.local_site.enable_nsfw && (
|
<div className="form-group row">
|
||||||
<div className="form-group row">
|
<div className="col-sm-10">
|
||||||
<div className="col-sm-10">
|
<div className="form-check">
|
||||||
<div className="form-check">
|
<input
|
||||||
<input
|
className="form-check-input"
|
||||||
className="form-check-input"
|
id="register-show-nsfw"
|
||||||
id="register-show-nsfw"
|
type="checkbox"
|
||||||
type="checkbox"
|
checked={this.state.form.show_nsfw}
|
||||||
checked={this.state.form.show_nsfw}
|
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
|
||||||
onChange={linkEvent(this, this.handleRegisterShowNsfwChange)}
|
/>
|
||||||
/>
|
<label className="form-check-label" htmlFor="register-show-nsfw">
|
||||||
<label
|
{i18n.t("show_nsfw")}
|
||||||
className="form-check-label"
|
</label>
|
||||||
htmlFor="register-show-nsfw"
|
|
||||||
>
|
|
||||||
{i18n.t("show_nsfw")}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
<input
|
<input
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
autoComplete="false"
|
autoComplete="false"
|
||||||
|
|
|
@ -12,6 +12,7 @@ interface SiteSidebarProps {
|
||||||
showLocal: boolean;
|
showLocal: boolean;
|
||||||
counts?: SiteAggregates;
|
counts?: SiteAggregates;
|
||||||
admins?: PersonView[];
|
admins?: PersonView[];
|
||||||
|
isMobile?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SiteSidebarState {
|
interface SiteSidebarState {
|
||||||
|
@ -29,39 +30,58 @@ export class SiteSidebar extends Component<SiteSidebarProps, SiteSidebarState> {
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<section id="sidebarInfo" className="card border-secondary mb-3">
|
<div className="accordion">
|
||||||
<div className="card-body">
|
<section id="sidebarInfo" className="card border-secondary mb-3">
|
||||||
<div>
|
<header
|
||||||
<div className="mb-2">{this.siteName()}</div>
|
className="card-header d-flex align-items-center"
|
||||||
|
id="sidebarInfoHeader"
|
||||||
|
>
|
||||||
|
{this.siteName()}
|
||||||
{!this.state.collapsed && (
|
{!this.state.collapsed && (
|
||||||
<>
|
<BannerIconHeader banner={this.props.site.banner} />
|
||||||
<BannerIconHeader banner={this.props.site.banner} />
|
|
||||||
{this.siteInfo()}
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div
|
||||||
|
id="sidebarInfoBody"
|
||||||
|
className="collapse show"
|
||||||
|
aria-labelledby="sidebarInfoHeader"
|
||||||
|
>
|
||||||
|
<div className="card-body">{this.siteInfo()}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</section>
|
||||||
</section>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
siteName() {
|
siteName() {
|
||||||
return (
|
return (
|
||||||
<h5 className="mb-0 d-inline">
|
<>
|
||||||
{this.props.site.name}
|
<h5 className="mb-0 d-inline">{this.props.site.name}</h5>
|
||||||
<button
|
{!this.props.isMobile && (
|
||||||
className="btn btn-sm text-muted"
|
<button
|
||||||
onClick={linkEvent(this, this.handleCollapseSidebar)}
|
type="button"
|
||||||
aria-label={i18n.t("collapse")}
|
className="btn btn-sm"
|
||||||
data-tippy-content={i18n.t("collapse")}
|
onClick={linkEvent(this, this.handleCollapseSidebar)}
|
||||||
>
|
aria-label={
|
||||||
{this.state.collapsed ? (
|
this.state.collapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||||
<Icon icon="plus-square" classes="icon-inline" />
|
}
|
||||||
) : (
|
data-tippy-content={
|
||||||
<Icon icon="minus-square" classes="icon-inline" />
|
this.state.collapsed ? i18n.t("expand") : i18n.t("collapse")
|
||||||
)}
|
}
|
||||||
</button>
|
data-bs-toggle="collapse"
|
||||||
</h5>
|
data-bs-target="#sidebarInfoBody"
|
||||||
|
aria-expanded="true"
|
||||||
|
aria-controls="sidebarInfoBody"
|
||||||
|
>
|
||||||
|
{this.state.collapsed ? (
|
||||||
|
<Icon icon="plus-square" classes="icon-inline" />
|
||||||
|
) : (
|
||||||
|
<Icon icon="minus-square" classes="icon-inline" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
import classNames from "classnames";
|
||||||
import { Component } from "inferno";
|
import { Component } from "inferno";
|
||||||
import { Link } from "inferno-router";
|
import { Link } from "inferno-router";
|
||||||
import { Person } from "lemmy-js-client";
|
import { Person } from "lemmy-js-client";
|
||||||
|
@ -48,7 +49,10 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
||||||
{!this.props.realLink ? (
|
{!this.props.realLink ? (
|
||||||
<Link
|
<Link
|
||||||
title={apubName}
|
title={apubName}
|
||||||
className={this.props.muted ? "text-muted" : "text-info"}
|
className={classNames("d-inline-flex align-items-baseline", {
|
||||||
|
"text-muted": this.props.muted,
|
||||||
|
"text-info": !this.props.muted,
|
||||||
|
})}
|
||||||
to={link}
|
to={link}
|
||||||
>
|
>
|
||||||
{this.avatarAndName(displayName)}
|
{this.avatarAndName(displayName)}
|
||||||
|
@ -56,7 +60,9 @@ export class PersonListing extends Component<PersonListingProps, any> {
|
||||||
) : (
|
) : (
|
||||||
<a
|
<a
|
||||||
title={apubName}
|
title={apubName}
|
||||||
className={this.props.muted ? "text-muted" : "text-info"}
|
className={`d-inline-flex align-items-baseline ${
|
||||||
|
this.props.muted ? "text-muted" : "text-info"
|
||||||
|
}`}
|
||||||
href={link}
|
href={link}
|
||||||
rel={relTags}
|
rel={relTags}
|
||||||
>
|
>
|
||||||
|
|
|
@ -20,7 +20,6 @@ import {
|
||||||
communityToChoice,
|
communityToChoice,
|
||||||
elementUrl,
|
elementUrl,
|
||||||
emDash,
|
emDash,
|
||||||
enableNsfw,
|
|
||||||
fetchCommunities,
|
fetchCommunities,
|
||||||
fetchThemeList,
|
fetchThemeList,
|
||||||
fetchUsers,
|
fetchUsers,
|
||||||
|
@ -642,22 +641,20 @@ export class Settings extends Component<any, SettingsState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
{enableNsfw(this.state.siteRes) && (
|
<div className="form-group">
|
||||||
<div className="form-group">
|
<div className="form-check">
|
||||||
<div className="form-check">
|
<input
|
||||||
<input
|
className="form-check-input"
|
||||||
className="form-check-input"
|
id="user-show-nsfw"
|
||||||
id="user-show-nsfw"
|
type="checkbox"
|
||||||
type="checkbox"
|
checked={this.state.saveUserSettingsForm.show_nsfw}
|
||||||
checked={this.state.saveUserSettingsForm.show_nsfw}
|
onChange={linkEvent(this, this.handleShowNsfwChange)}
|
||||||
onChange={linkEvent(this, this.handleShowNsfwChange)}
|
/>
|
||||||
/>
|
<label className="form-check-label" htmlFor="user-show-nsfw">
|
||||||
<label className="form-check-label" htmlFor="user-show-nsfw">
|
{i18n.t("show_nsfw")}
|
||||||
{i18n.t("show_nsfw")}
|
</label>
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
</div>
|
||||||
<div className="form-group">
|
<div className="form-group">
|
||||||
<div className="form-check">
|
<div className="form-check">
|
||||||
<input
|
<input
|
||||||
|
|
|
@ -201,7 +201,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="post-listing">
|
<div className="post-listing mt-2">
|
||||||
{!this.state.showEdit ? (
|
{!this.state.showEdit ? (
|
||||||
<>
|
<>
|
||||||
{this.listing()}
|
{this.listing()}
|
||||||
|
@ -386,10 +386,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{this.props.showCommunity && (
|
{this.props.showCommunity && (
|
||||||
<span>
|
<>
|
||||||
<span className="mx-1"> {i18n.t("to")} </span>
|
{" "}
|
||||||
<CommunityLink community={post_view.community} />
|
{i18n.t("to")} <CommunityLink community={post_view.community} />
|
||||||
</span>
|
</>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
{post_view.post.language_id !== 0 && (
|
{post_view.post.language_id !== 0 && (
|
||||||
|
@ -497,7 +497,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className={`d-inline-block ${
|
className={`d-inline ${
|
||||||
!post.featured_community && !post.featured_local
|
!post.featured_community && !post.featured_local
|
||||||
? "text-body"
|
? "text-body"
|
||||||
: "text-primary"
|
: "text-primary"
|
||||||
|
@ -505,8 +505,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
to={`/post/${post.id}`}
|
to={`/post/${post.id}`}
|
||||||
title={i18n.t("comments")}
|
title={i18n.t("comments")}
|
||||||
>
|
>
|
||||||
<div
|
<span
|
||||||
className="d-inline-block"
|
className="d-inline"
|
||||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||||
/>
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
|
@ -519,88 +519,78 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="post-title overflow-hidden">
|
<div className="post-title overflow-hidden">
|
||||||
<h5>
|
<h5 className="d-inline">
|
||||||
{url ? (
|
{url && this.props.showBody ? (
|
||||||
this.props.showBody ? (
|
<a
|
||||||
<a
|
className={
|
||||||
className={`d-inline-block ${
|
!post.featured_community && !post.featured_local
|
||||||
!post.featured_community && !post.featured_local
|
? "text-body"
|
||||||
? "text-body"
|
: "text-primary"
|
||||||
: "text-primary"
|
}
|
||||||
}`}
|
href={url}
|
||||||
href={url}
|
title={url}
|
||||||
title={url}
|
rel={relTags}
|
||||||
rel={relTags}
|
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
||||||
>
|
></a>
|
||||||
<div
|
|
||||||
className="d-inline-block"
|
|
||||||
dangerouslySetInnerHTML={mdToHtmlInline(post.name)}
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
this.postLink
|
|
||||||
)
|
|
||||||
) : (
|
) : (
|
||||||
this.postLink
|
this.postLink
|
||||||
)}
|
)}
|
||||||
{(url && isImage(url)) ||
|
|
||||||
(post.thumbnail_url && (
|
|
||||||
<button
|
|
||||||
className="btn btn-link text-monospace text-muted small d-inline-block"
|
|
||||||
data-tippy-content={i18n.t("expand_here")}
|
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
|
||||||
>
|
|
||||||
<Icon
|
|
||||||
icon={
|
|
||||||
!this.state.imageExpanded ? "plus-square" : "minus-square"
|
|
||||||
}
|
|
||||||
classes="icon-inline"
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
{post.removed && (
|
|
||||||
<small className="ml-2 text-muted font-italic">
|
|
||||||
{i18n.t("removed")}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{post.deleted && (
|
|
||||||
<small
|
|
||||||
className="unselectable pointer ml-2 text-muted font-italic"
|
|
||||||
data-tippy-content={i18n.t("deleted")}
|
|
||||||
>
|
|
||||||
<Icon icon="trash" classes="icon-inline text-danger" />
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{post.locked && (
|
|
||||||
<small
|
|
||||||
className="unselectable pointer ml-2 text-muted font-italic"
|
|
||||||
data-tippy-content={i18n.t("locked")}
|
|
||||||
>
|
|
||||||
<Icon icon="lock" classes="icon-inline text-danger" />
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{post.featured_community && (
|
|
||||||
<small
|
|
||||||
className="unselectable pointer ml-2 text-muted font-italic"
|
|
||||||
data-tippy-content={i18n.t("featured")}
|
|
||||||
>
|
|
||||||
<Icon icon="pin" classes="icon-inline text-primary" />
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{post.featured_local && (
|
|
||||||
<small
|
|
||||||
className="unselectable pointer ml-2 text-muted font-italic"
|
|
||||||
data-tippy-content={i18n.t("featured")}
|
|
||||||
>
|
|
||||||
<Icon icon="pin" classes="icon-inline text-secondary" />
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{post.nsfw && (
|
|
||||||
<small className="ml-2 text-muted font-italic">
|
|
||||||
{i18n.t("nsfw")}
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
</h5>
|
</h5>
|
||||||
|
{(url && isImage(url)) ||
|
||||||
|
(post.thumbnail_url && (
|
||||||
|
<button
|
||||||
|
className="btn btn-link text-monospace text-muted small d-inline-block"
|
||||||
|
data-tippy-content={i18n.t("expand_here")}
|
||||||
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon={
|
||||||
|
!this.state.imageExpanded ? "plus-square" : "minus-square"
|
||||||
|
}
|
||||||
|
classes="icon-inline"
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
{post.removed && (
|
||||||
|
<small className="ml-2 badge text-bg-secondary">
|
||||||
|
{i18n.t("removed")}
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{post.deleted && (
|
||||||
|
<small
|
||||||
|
className="unselectable pointer ml-2 text-muted font-italic"
|
||||||
|
data-tippy-content={i18n.t("deleted")}
|
||||||
|
>
|
||||||
|
<Icon icon="trash" classes="icon-inline text-danger" />
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{post.locked && (
|
||||||
|
<small
|
||||||
|
className="unselectable pointer ml-2 text-muted font-italic"
|
||||||
|
data-tippy-content={i18n.t("locked")}
|
||||||
|
>
|
||||||
|
<Icon icon="lock" classes="icon-inline text-danger" />
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{post.featured_community && (
|
||||||
|
<small
|
||||||
|
className="unselectable pointer ml-2 text-muted font-italic"
|
||||||
|
data-tippy-content={i18n.t("featured")}
|
||||||
|
>
|
||||||
|
<Icon icon="pin" classes="icon-inline text-primary" />
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{post.featured_local && (
|
||||||
|
<small
|
||||||
|
className="unselectable pointer ml-2 text-muted font-italic"
|
||||||
|
data-tippy-content={i18n.t("featured")}
|
||||||
|
>
|
||||||
|
<Icon icon="pin" classes="icon-inline text-secondary" />
|
||||||
|
</small>
|
||||||
|
)}
|
||||||
|
{post.nsfw && (
|
||||||
|
<small className="ml-2 badge text-bg-danger">{i18n.t("nsfw")}</small>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -631,11 +621,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const post = this.postView.post;
|
const post = this.postView.post;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold mb-1">
|
<div className="d-flex align-items-center justify-content-start flex-wrap text-muted font-weight-bold">
|
||||||
{this.commentsButton}
|
{this.commentsButton}
|
||||||
{canShare() && (
|
{canShare() && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link"
|
className="btn btn-sm btn-link"
|
||||||
onClick={linkEvent(this, this.handleShare)}
|
onClick={linkEvent(this, this.handleShare)}
|
||||||
type="button"
|
type="button"
|
||||||
>
|
>
|
||||||
|
@ -654,12 +644,23 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{mobile && !this.props.viewOnly && this.mobileVotes}
|
{mobile && !this.props.viewOnly && this.mobileVotes}
|
||||||
{UserService.Instance.myUserInfo &&
|
{UserService.Instance.myUserInfo &&
|
||||||
!this.props.viewOnly &&
|
!this.props.viewOnly &&
|
||||||
this.postActions(mobile)}
|
this.postActions()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
postActions(mobile = false) {
|
get hasAdvancedButtons() {
|
||||||
|
return (
|
||||||
|
this.myPost ||
|
||||||
|
(this.showBody && this.postView.post.body) ||
|
||||||
|
amMod(this.props.moderators) ||
|
||||||
|
amAdmin() ||
|
||||||
|
this.canMod_ ||
|
||||||
|
this.canAdmin_
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postActions() {
|
||||||
// Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
|
// Possible enhancement: Priority+ pattern instead of just hard coding which get hidden behind the show more button.
|
||||||
// Possible enhancement: Make each button a component.
|
// Possible enhancement: Make each button a component.
|
||||||
const post_view = this.postView;
|
const post_view = this.postView;
|
||||||
|
@ -667,37 +668,53 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<>
|
<>
|
||||||
{this.saveButton}
|
{this.saveButton}
|
||||||
{this.crossPostButton}
|
{this.crossPostButton}
|
||||||
{mobile && this.showMoreButton}
|
|
||||||
{(!mobile || this.state.showAdvanced) && (
|
{this.showBody && post_view.post.body && this.viewSourceButton}
|
||||||
<>
|
|
||||||
{!this.myPost && (
|
{this.hasAdvancedButtons && (
|
||||||
<>
|
<div className="dropdown">
|
||||||
{this.reportButton}
|
<button
|
||||||
{this.blockButton}
|
className="btn btn-link btn-animate text-muted py-0 dropdown-toggle"
|
||||||
</>
|
onClick={linkEvent(this, this.handleShowAdvanced)}
|
||||||
)}
|
data-tippy-content={i18n.t("more")}
|
||||||
{this.myPost && (this.showBody || this.state.showAdvanced) && (
|
data-bs-toggle="dropdown"
|
||||||
<>
|
aria-expanded="false"
|
||||||
{this.editButton}
|
aria-controls="advancedButtonsDropdown"
|
||||||
{this.deleteButton}
|
aria-label={i18n.t("more")}
|
||||||
</>
|
>
|
||||||
)}
|
<Icon icon="more-vertical" inline />
|
||||||
</>
|
</button>
|
||||||
|
|
||||||
|
<ul className="dropdown-menu" id="advancedButtonsDropdown">
|
||||||
|
{!this.myPost ? (
|
||||||
|
<>
|
||||||
|
<li>{this.reportButton}</li>
|
||||||
|
<li>{this.blockButton}</li>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<li>{this.editButton}</li>
|
||||||
|
<li>{this.deleteButton}</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Any mod can do these, not limited to hierarchy*/}
|
||||||
|
{(amMod(this.props.moderators) || amAdmin()) && (
|
||||||
|
<>
|
||||||
|
<li>
|
||||||
|
<hr className="dropdown-divider" />
|
||||||
|
</li>
|
||||||
|
<li>{this.lockButton}</li>
|
||||||
|
{this.featureButtons}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{(this.canMod_ || this.canAdmin_) && (
|
||||||
|
<li>{this.modRemoveButton}</li>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
{this.state.showAdvanced && (
|
|
||||||
<>
|
|
||||||
{this.showBody && post_view.post.body && this.viewSourceButton}
|
|
||||||
{/* Any mod can do these, not limited to hierarchy*/}
|
|
||||||
{(amMod(this.props.moderators) || amAdmin()) && (
|
|
||||||
<>
|
|
||||||
{this.lockButton}
|
|
||||||
{this.featureButton}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{(this.canMod_ || this.canAdmin_) && <>{this.modRemoveButton}</>}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!mobile && this.showMoreButton}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -706,7 +723,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const post_view = this.postView;
|
const post_view = this.postView;
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
className="btn btn-link text-muted py-0 pl-0 text-muted"
|
className="btn btn-link text-muted pl-0 text-muted"
|
||||||
title={i18n.t("number_of_comments", {
|
title={i18n.t("number_of_comments", {
|
||||||
count: Number(post_view.counts.comments),
|
count: Number(post_view.counts.comments),
|
||||||
formattedCount: Number(post_view.counts.comments),
|
formattedCount: Number(post_view.counts.comments),
|
||||||
|
@ -846,12 +863,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get reportButton() {
|
get reportButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleShowReportDialog)}
|
onClick={linkEvent(this, this.handleShowReportDialog)}
|
||||||
data-tippy-content={i18n.t("show_report_dialog")}
|
|
||||||
aria-label={i18n.t("show_report_dialog")}
|
aria-label={i18n.t("show_report_dialog")}
|
||||||
>
|
>
|
||||||
<Icon icon="flag" inline />
|
<Icon classes="mr-1" icon="flag" inline />
|
||||||
|
{i18n.t("create_report")}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -859,12 +876,16 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get blockButton() {
|
get blockButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleBlockPersonClick)}
|
onClick={linkEvent(this, this.handleBlockPersonClick)}
|
||||||
data-tippy-content={i18n.t("block_user")}
|
|
||||||
aria-label={i18n.t("block_user")}
|
aria-label={i18n.t("block_user")}
|
||||||
>
|
>
|
||||||
{this.state.blockLoading ? <Spinner /> : <Icon icon="slash" inline />}
|
{this.state.blockLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<Icon classes="mr-1" icon="slash" inline />
|
||||||
|
)}
|
||||||
|
{i18n.t("block_user")}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -872,12 +893,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
get editButton() {
|
get editButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleEditClick)}
|
onClick={linkEvent(this, this.handleEditClick)}
|
||||||
data-tippy-content={i18n.t("edit")}
|
|
||||||
aria-label={i18n.t("edit")}
|
aria-label={i18n.t("edit")}
|
||||||
>
|
>
|
||||||
<Icon icon="edit" inline />
|
<Icon classes="mr-1" icon="edit" inline />
|
||||||
|
{i18n.t("edit")}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -887,37 +908,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const label = !deleted ? i18n.t("delete") : i18n.t("restore");
|
const label = !deleted ? i18n.t("delete") : i18n.t("restore");
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
data-tippy-content={label}
|
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
{this.state.deleteLoading ? (
|
{this.state.deleteLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<>
|
||||||
icon="trash"
|
<Icon
|
||||||
classes={classNames({ "text-danger": deleted })}
|
icon="trash"
|
||||||
inline
|
classes={classNames("mr-1", { "text-danger": deleted })}
|
||||||
/>
|
inline
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get showMoreButton() {
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
|
||||||
onClick={linkEvent(this, this.handleShowAdvanced)}
|
|
||||||
data-tippy-content={i18n.t("more")}
|
|
||||||
aria-label={i18n.t("more")}
|
|
||||||
>
|
|
||||||
<Icon icon="more-vertical" inline />
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
get viewSourceButton() {
|
get viewSourceButton() {
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
@ -940,25 +950,27 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const label = locked ? i18n.t("unlock") : i18n.t("lock");
|
const label = locked ? i18n.t("unlock") : i18n.t("lock");
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleModLock)}
|
onClick={linkEvent(this, this.handleModLock)}
|
||||||
data-tippy-content={label}
|
|
||||||
aria-label={label}
|
aria-label={label}
|
||||||
>
|
>
|
||||||
{this.state.lockLoading ? (
|
{this.state.lockLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<Icon
|
<>
|
||||||
icon="lock"
|
<Icon
|
||||||
classes={classNames({ "text-danger": locked })}
|
icon="lock"
|
||||||
inline
|
classes={classNames("mr-1", { "text-danger": locked })}
|
||||||
/>
|
inline
|
||||||
|
/>
|
||||||
|
{label}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
get featureButton() {
|
get featureButtons() {
|
||||||
const featuredCommunity = this.postView.post.featured_community;
|
const featuredCommunity = this.postView.post.featured_community;
|
||||||
const labelCommunity = featuredCommunity
|
const labelCommunity = featuredCommunity
|
||||||
? i18n.t("unfeature_from_community")
|
? i18n.t("unfeature_from_community")
|
||||||
|
@ -969,48 +981,56 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
? i18n.t("unfeature_from_local")
|
? i18n.t("unfeature_from_local")
|
||||||
: i18n.t("feature_in_local");
|
: i18n.t("feature_in_local");
|
||||||
return (
|
return (
|
||||||
<span>
|
<>
|
||||||
<button
|
<li>
|
||||||
className="btn btn-link btn-animate text-muted py-0 pl-0"
|
|
||||||
onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
|
|
||||||
data-tippy-content={labelCommunity}
|
|
||||||
aria-label={labelCommunity}
|
|
||||||
>
|
|
||||||
{this.state.featureCommunityLoading ? (
|
|
||||||
<Spinner />
|
|
||||||
) : (
|
|
||||||
<span>
|
|
||||||
<Icon
|
|
||||||
icon="pin"
|
|
||||||
classes={classNames({ "text-success": featuredCommunity })}
|
|
||||||
inline
|
|
||||||
/>
|
|
||||||
{i18n.t("community")}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
{amAdmin() && (
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(this, this.handleModFeaturePostLocal)}
|
onClick={linkEvent(this, this.handleModFeaturePostCommunity)}
|
||||||
data-tippy-content={labelLocal}
|
data-tippy-content={labelCommunity}
|
||||||
aria-label={labelLocal}
|
aria-label={labelCommunity}
|
||||||
>
|
>
|
||||||
{this.state.featureLocalLoading ? (
|
{this.state.featureCommunityLoading ? (
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<span>
|
<>
|
||||||
<Icon
|
<Icon
|
||||||
icon="pin"
|
icon="pin"
|
||||||
classes={classNames({ "text-success": featuredLocal })}
|
classes={classNames("mr-1", {
|
||||||
|
"text-success": featuredCommunity,
|
||||||
|
})}
|
||||||
inline
|
inline
|
||||||
/>
|
/>
|
||||||
{i18n.t("local")}
|
{i18n.t("community")}
|
||||||
</span>
|
</>
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
)}
|
</li>
|
||||||
</span>
|
<li>
|
||||||
|
{amAdmin() && (
|
||||||
|
<button
|
||||||
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
|
onClick={linkEvent(this, this.handleModFeaturePostLocal)}
|
||||||
|
data-tippy-content={labelLocal}
|
||||||
|
aria-label={labelLocal}
|
||||||
|
>
|
||||||
|
{this.state.featureLocalLoading ? (
|
||||||
|
<Spinner />
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Icon
|
||||||
|
icon="pin"
|
||||||
|
classes={classNames("mr-1", {
|
||||||
|
"text-success": featuredLocal,
|
||||||
|
})}
|
||||||
|
inline
|
||||||
|
/>
|
||||||
|
{i18n.t("local")}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1018,7 +1038,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
const removed = this.postView.post.removed;
|
const removed = this.postView.post.removed;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className="btn btn-link btn-animate text-muted py-0"
|
className="btn btn-link btn-sm d-flex align-items-center rounded-0 dropdown-item"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
this,
|
this,
|
||||||
!removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
|
!removed ? this.handleModRemoveShow : this.handleModRemoveSubmit
|
||||||
|
|
|
@ -96,9 +96,7 @@ export class PostListings extends Component<PostListingsProps, any> {
|
||||||
onAddAdmin={this.props.onAddAdmin}
|
onAddAdmin={this.props.onAddAdmin}
|
||||||
onTransferCommunity={this.props.onTransferCommunity}
|
onTransferCommunity={this.props.onTransferCommunity}
|
||||||
/>
|
/>
|
||||||
{idx + 1 !== this.posts.length && (
|
{idx + 1 !== this.posts.length && <hr className="my-3" />}
|
||||||
<hr className="my-3 border border-primary" />
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
|
|
97
yarn.lock
97
yarn.lock
|
@ -1202,6 +1202,13 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.11"
|
regenerator-runtime "^0.13.11"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.20.7":
|
||||||
|
version "7.22.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.5.tgz#8564dd588182ce0047d55d7a75e93921107b57ec"
|
||||||
|
integrity sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==
|
||||||
|
dependencies:
|
||||||
|
regenerator-runtime "^0.13.11"
|
||||||
|
|
||||||
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.21.9":
|
"@babel/template@^7.18.10", "@babel/template@^7.20.7", "@babel/template@^7.21.9":
|
||||||
version "7.21.9"
|
version "7.21.9"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb"
|
resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.21.9.tgz#bf8dad2859130ae46088a99c1f265394877446fb"
|
||||||
|
@ -2159,6 +2166,13 @@ argparse@^2.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
|
||||||
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
|
||||||
|
|
||||||
|
aria-query@^5.1.3:
|
||||||
|
version "5.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.2.1.tgz#bc285d9d654d1df121bcd0c134880d415ca67c15"
|
||||||
|
integrity sha512-7uFg4b+lETFgdaJyETnILsXgnnzVnkHcgRbwbPwevm5x/LmUlt3MjczMRe1zg824iBgXZNRPTBftNYyRSKLp2g==
|
||||||
|
dependencies:
|
||||||
|
dequal "^2.0.3"
|
||||||
|
|
||||||
array-buffer-byte-length@^1.0.0:
|
array-buffer-byte-length@^1.0.0:
|
||||||
version "1.0.0"
|
version "1.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead"
|
resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.0.tgz#fabe8bc193fea865f317fe7807085ee0dee5aead"
|
||||||
|
@ -2177,7 +2191,7 @@ array-flatten@^2.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
|
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-2.1.2.tgz#24ef80a28c1a893617e2149b0c6d0d788293b099"
|
||||||
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
|
integrity sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==
|
||||||
|
|
||||||
array-includes@^3.1.5:
|
array-includes@^3.1.5, array-includes@^3.1.6:
|
||||||
version "3.1.6"
|
version "3.1.6"
|
||||||
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
|
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
|
||||||
integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
|
integrity sha512-sgTbLvL6cNnw24FnbaDyjmvddQ2ML8arZsgaJhoABMoplz/4QRhtrYS+alr1BUM1Bwp6dhx8vVCBSLG+StwOFw==
|
||||||
|
@ -2205,6 +2219,16 @@ array-uniq@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
resolved "https://registry.yarnpkg.com/array-uniq/-/array-uniq-1.0.3.tgz#af6ac877a25cc7f74e058894753858dfdb24fdb6"
|
||||||
integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==
|
integrity sha512-MNha4BWQ6JbwhFhj03YK552f7cb3AzoE8SzeljgChvL1dl3IcvggXVz1DilzySZkCja+CXuZbdW7yATchWn8/Q==
|
||||||
|
|
||||||
|
array.prototype.flatmap@^1.3.1:
|
||||||
|
version "1.3.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/array.prototype.flatmap/-/array.prototype.flatmap-1.3.1.tgz#1aae7903c2100433cb8261cd4ed310aab5c4a183"
|
||||||
|
integrity sha512-8UGn9O1FDVvMNB0UlLv4voxRMze7+FpHyF5mSMRjWHUMlpoDViniy05870VlxhfgTnLbpuwTzvD76MTtWxB/mQ==
|
||||||
|
dependencies:
|
||||||
|
call-bind "^1.0.2"
|
||||||
|
define-properties "^1.1.4"
|
||||||
|
es-abstract "^1.20.4"
|
||||||
|
es-shim-unscopables "^1.0.0"
|
||||||
|
|
||||||
array.prototype.reduce@^1.0.5:
|
array.prototype.reduce@^1.0.5:
|
||||||
version "1.0.5"
|
version "1.0.5"
|
||||||
resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac"
|
resolved "https://registry.yarnpkg.com/array.prototype.reduce/-/array.prototype.reduce-1.0.5.tgz#6b20b0daa9d9734dd6bc7ea66b5bbce395471eac"
|
||||||
|
@ -2244,6 +2268,11 @@ assert-plus@1.0.0, assert-plus@^1.0.0:
|
||||||
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
resolved "https://registry.yarnpkg.com/assert-plus/-/assert-plus-1.0.0.tgz#f12e0f3c5d77b0b1cdd9146942e4e96c1e4dd525"
|
||||||
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
|
integrity sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==
|
||||||
|
|
||||||
|
ast-types-flow@^0.0.7:
|
||||||
|
version "0.0.7"
|
||||||
|
resolved "https://registry.yarnpkg.com/ast-types-flow/-/ast-types-flow-0.0.7.tgz#f70b735c6bca1a5c9c22d982c3e39e7feba3bdad"
|
||||||
|
integrity sha512-eBvWn1lvIApYMhzQMsu9ciLfkBY499mFZlNqG+/9WR7PVlroQw0vG30cOQQbaKz3sCEc44TAOu2ykzqXSNnwag==
|
||||||
|
|
||||||
astral-regex@^2.0.0:
|
astral-regex@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
resolved "https://registry.yarnpkg.com/astral-regex/-/astral-regex-2.0.0.tgz#483143c567aeed4785759c0865786dc77d7d2e31"
|
||||||
|
@ -2284,6 +2313,18 @@ aws4@^1.8.0:
|
||||||
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
|
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.12.0.tgz#ce1c9d143389679e253b314241ea9aa5cec980d3"
|
||||||
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
|
integrity sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==
|
||||||
|
|
||||||
|
axe-core@^4.6.2:
|
||||||
|
version "4.7.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.7.2.tgz#040a7342b20765cb18bb50b628394c21bccc17a0"
|
||||||
|
integrity sha512-zIURGIS1E1Q4pcrMjp+nnEh+16G56eG/MUllJH8yEvw7asDo7Ac9uhC9KIH5jzpITueEZolfYglnCGIuSBz39g==
|
||||||
|
|
||||||
|
axobject-query@^3.1.1:
|
||||||
|
version "3.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/axobject-query/-/axobject-query-3.2.1.tgz#39c378a6e3b06ca679f29138151e45b2b32da62a"
|
||||||
|
integrity sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==
|
||||||
|
dependencies:
|
||||||
|
dequal "^2.0.3"
|
||||||
|
|
||||||
babel-loader@^9.1.2:
|
babel-loader@^9.1.2:
|
||||||
version "9.1.2"
|
version "9.1.2"
|
||||||
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c"
|
resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-9.1.2.tgz#a16a080de52d08854ee14570469905a5fc00d39c"
|
||||||
|
@ -2446,16 +2487,16 @@ bonjour-service@^1.0.11:
|
||||||
fast-deep-equal "^3.1.3"
|
fast-deep-equal "^3.1.3"
|
||||||
multicast-dns "^7.2.5"
|
multicast-dns "^7.2.5"
|
||||||
|
|
||||||
|
"bootstrap-v4@npm:bootstrap@^4.6.2":
|
||||||
|
version "4.6.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-4.6.2.tgz#8e0cd61611728a5bf65a3a2b8d6ff6c77d5d7479"
|
||||||
|
integrity sha512-51Bbp/Uxr9aTuy6ca/8FbFloBUJZLHwnhTcnjIeRn2suQWsWzcuJhGjKDB5eppVte/8oCdOL3VuwxvZDUggwGQ==
|
||||||
|
|
||||||
bootstrap@^5.2.3:
|
bootstrap@^5.2.3:
|
||||||
version "5.3.0"
|
version "5.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.0.tgz#0718a7cc29040ee8dbf1bd652b896f3436a87c29"
|
resolved "https://registry.yarnpkg.com/bootstrap/-/bootstrap-5.3.0.tgz#0718a7cc29040ee8dbf1bd652b896f3436a87c29"
|
||||||
integrity sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==
|
integrity sha512-UnBV3E3v4STVNQdms6jSGO2CvOkjUMdDAVR2V5N4uCMdaIkaQjbcEAMqRimDHIs4uqBYzDAKCQwCB+97tJgHQw==
|
||||||
|
|
||||||
bootswatch@^5.2.3:
|
|
||||||
version "5.3.0"
|
|
||||||
resolved "https://registry.yarnpkg.com/bootswatch/-/bootswatch-5.3.0.tgz#7c7dd50bbe8519b0c6dbe01f4f9c3100b60228bd"
|
|
||||||
integrity sha512-ga2hHognDrh5h3+CaBBug6ktx3MTlnDzH57s+Mvjt9ZcNxqwpK+m3sE3YIUSr8zf2iG05elOb1mnqqcdbce2ow==
|
|
||||||
|
|
||||||
boxen@^1.2.1:
|
boxen@^1.2.1:
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
resolved "https://registry.yarnpkg.com/boxen/-/boxen-1.3.0.tgz#55c6c39a8ba58d9c61ad22cd877532deb665a20b"
|
||||||
|
@ -3152,6 +3193,11 @@ cyclist@^1.0.1:
|
||||||
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.2.tgz#673b5f233bf34d8e602b949429f8171d9121bea3"
|
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.2.tgz#673b5f233bf34d8e602b949429f8171d9121bea3"
|
||||||
integrity sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==
|
integrity sha512-0sVXIohTfLqVIW3kb/0n6IiWF3Ifj5nm2XaSrLq2DI6fKIGa2fYAZdk917rUneaeLVpYfFcyXE2ft0fe3remsA==
|
||||||
|
|
||||||
|
damerau-levenshtein@^1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz#b43d286ccbd36bc5b2f7ed41caf2d0aba1f8a6e7"
|
||||||
|
integrity sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==
|
||||||
|
|
||||||
dashdash@^1.12.0:
|
dashdash@^1.12.0:
|
||||||
version "1.14.1"
|
version "1.14.1"
|
||||||
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
resolved "https://registry.yarnpkg.com/dashdash/-/dashdash-1.14.1.tgz#853cfa0f7cbe2fed5de20326b8dd581035f6e2f0"
|
||||||
|
@ -3319,6 +3365,11 @@ depd@~1.1.2:
|
||||||
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9"
|
||||||
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
|
integrity sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==
|
||||||
|
|
||||||
|
dequal@^2.0.3:
|
||||||
|
version "2.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
|
||||||
|
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
|
||||||
|
|
||||||
destroy@1.2.0:
|
destroy@1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||||
|
@ -3696,6 +3747,28 @@ eslint-plugin-inferno@^7.32.2:
|
||||||
resolve "^2.0.0-next.4"
|
resolve "^2.0.0-next.4"
|
||||||
semver "^7.3.8"
|
semver "^7.3.8"
|
||||||
|
|
||||||
|
eslint-plugin-jsx-a11y@^6.7.1:
|
||||||
|
version "6.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz#fca5e02d115f48c9a597a6894d5bcec2f7a76976"
|
||||||
|
integrity sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==
|
||||||
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.20.7"
|
||||||
|
aria-query "^5.1.3"
|
||||||
|
array-includes "^3.1.6"
|
||||||
|
array.prototype.flatmap "^1.3.1"
|
||||||
|
ast-types-flow "^0.0.7"
|
||||||
|
axe-core "^4.6.2"
|
||||||
|
axobject-query "^3.1.1"
|
||||||
|
damerau-levenshtein "^1.0.8"
|
||||||
|
emoji-regex "^9.2.2"
|
||||||
|
has "^1.0.3"
|
||||||
|
jsx-ast-utils "^3.3.3"
|
||||||
|
language-tags "=1.0.5"
|
||||||
|
minimatch "^3.1.2"
|
||||||
|
object.entries "^1.1.6"
|
||||||
|
object.fromentries "^2.0.6"
|
||||||
|
semver "^6.3.0"
|
||||||
|
|
||||||
eslint-plugin-prettier@^4.2.1:
|
eslint-plugin-prettier@^4.2.1:
|
||||||
version "4.2.1"
|
version "4.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
|
resolved "https://registry.yarnpkg.com/eslint-plugin-prettier/-/eslint-plugin-prettier-4.2.1.tgz#651cbb88b1dab98bfd42f017a12fa6b2d993f94b"
|
||||||
|
@ -5590,6 +5663,18 @@ klona@^2.0.6:
|
||||||
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
|
resolved "https://registry.yarnpkg.com/klona/-/klona-2.0.6.tgz#85bffbf819c03b2f53270412420a4555ef882e22"
|
||||||
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
|
integrity sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==
|
||||||
|
|
||||||
|
language-subtag-registry@~0.3.2:
|
||||||
|
version "0.3.22"
|
||||||
|
resolved "https://registry.yarnpkg.com/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz#2e1500861b2e457eba7e7ae86877cbd08fa1fd1d"
|
||||||
|
integrity sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==
|
||||||
|
|
||||||
|
language-tags@=1.0.5:
|
||||||
|
version "1.0.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/language-tags/-/language-tags-1.0.5.tgz#d321dbc4da30ba8bf3024e040fa5c14661f9193a"
|
||||||
|
integrity sha512-qJhlO9cGXi6hBGKoxEG/sKZDAHD5Hnu9Hs4WbOY3pCWXDhw0N8x1NenNzm2EnNLkLkk7J2SdxAkDSbb6ftT+UQ==
|
||||||
|
dependencies:
|
||||||
|
language-subtag-registry "~0.3.2"
|
||||||
|
|
||||||
latest-version@^3.0.0:
|
latest-version@^3.0.0:
|
||||||
version "3.1.0"
|
version "3.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
resolved "https://registry.yarnpkg.com/latest-version/-/latest-version-3.1.0.tgz#a205383fea322b33b5ae3b18abee0dc2f356ee15"
|
||||||
|
|
Loading…
Reference in a new issue