Compare commits
1 commit
main
...
fix_banner
Author | SHA1 | Date | |
---|---|---|---|
|
8ba574ce28 |
317 changed files with 19650 additions and 123078 deletions
6
.babelrc
6
.babelrc
|
@ -10,11 +10,11 @@
|
|||
}
|
||||
}
|
||||
],
|
||||
["@babel/typescript", { "isTSX": true, "allExtensions": true }]
|
||||
["@babel/typescript", {"isTSX": true, "allExtensions": true}]
|
||||
],
|
||||
"plugins": [
|
||||
"@babel/plugin-transform-runtime",
|
||||
["babel-plugin-inferno", { "imports": true }],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }]
|
||||
["babel-plugin-inferno", { "imports": true }],
|
||||
["@babel/plugin-proposal-class-properties", { "loose": true }],
|
||||
]
|
||||
}
|
||||
|
|
152
.drone.yml
Normal file
152
.drone.yml
Normal file
|
@ -0,0 +1,152 @@
|
|||
---
|
||||
kind: pipeline
|
||||
name: amd64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: amd64
|
||||
|
||||
steps:
|
||||
|
||||
- name: fetch git submodules
|
||||
image: node:14-alpine
|
||||
commands:
|
||||
- apk add git
|
||||
- git submodule init
|
||||
- git submodule update --recursive --remote
|
||||
- git fetch --tags
|
||||
|
||||
- name: yarn
|
||||
image: node:14-alpine
|
||||
commands:
|
||||
- yarn
|
||||
|
||||
- name: yarn lint
|
||||
image: node:14-alpine
|
||||
commands:
|
||||
- yarn lint
|
||||
|
||||
- name: yarn build:dev
|
||||
image: node:14-alpine
|
||||
commands:
|
||||
- yarn build:dev
|
||||
|
||||
- name: publish release docker image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
dockerfile: Dockerfile
|
||||
repo: dessalines/lemmy-ui
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-amd64
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
ref:
|
||||
- refs/tags/*
|
||||
|
||||
- name: publish release docker manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
target: "dessalines/lemmy-ui:${DRONE_TAG}"
|
||||
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
ignore_missing: true
|
||||
when:
|
||||
ref:
|
||||
- refs/tags/*
|
||||
|
||||
- name: publish latest release docker manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
target: "dessalines/lemmy-ui:latest"
|
||||
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
ignore_missing: true
|
||||
when:
|
||||
ref:
|
||||
- refs/tags/*
|
||||
|
||||
---
|
||||
kind: pipeline
|
||||
name: arm64
|
||||
|
||||
platform:
|
||||
os: linux
|
||||
arch: arm64
|
||||
|
||||
steps:
|
||||
|
||||
- name: fetch git submodules
|
||||
image: node:14-alpine
|
||||
commands:
|
||||
- apk add git
|
||||
- git submodule init
|
||||
- git submodule update --recursive --remote
|
||||
- git fetch --tags
|
||||
when:
|
||||
ref:
|
||||
- refs/heads/main
|
||||
- refs/tags/*
|
||||
|
||||
- name: publish release docker image
|
||||
image: plugins/docker
|
||||
settings:
|
||||
dockerfile: Dockerfile
|
||||
repo: dessalines/lemmy-ui
|
||||
auto_tag: true
|
||||
auto_tag_suffix: linux-arm64
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
when:
|
||||
ref:
|
||||
- refs/tags/*
|
||||
|
||||
- name: publish release docker manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
target: "dessalines/lemmy-ui:${DRONE_TAG}"
|
||||
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
ignore_missing: true
|
||||
when:
|
||||
ref:
|
||||
- refs/tags/*
|
||||
|
||||
- name: publish latest release docker manifest
|
||||
image: plugins/manifest
|
||||
settings:
|
||||
username:
|
||||
from_secret: docker_username
|
||||
password:
|
||||
from_secret: docker_password
|
||||
target: "dessalines/lemmy-ui:latest"
|
||||
template: "dessalines/lemmy-ui:${DRONE_TAG}-OS-ARCH"
|
||||
platforms:
|
||||
- linux/amd64
|
||||
- linux/arm64
|
||||
ignore_missing: true
|
||||
when:
|
||||
ref:
|
||||
- refs/tags/*
|
|
@ -1,7 +1,3 @@
|
|||
generate_translations.js
|
||||
webpack.config.js
|
||||
src/api_tests
|
||||
**/*.png
|
||||
**/*.css
|
||||
**/*.scss
|
||||
**/*.svg
|
||||
|
|
|
@ -3,12 +3,12 @@
|
|||
"env": {
|
||||
"browser": true
|
||||
},
|
||||
"plugins": ["@typescript-eslint", "jsx-a11y", "prettier"],
|
||||
"plugins": [
|
||||
"@typescript-eslint"
|
||||
],
|
||||
"extends": [
|
||||
"eslint:recommended",
|
||||
"plugin:@typescript-eslint/recommended",
|
||||
"plugin:inferno/recommended",
|
||||
"plugin:jsx-a11y/recommended"
|
||||
"plugin:@typescript-eslint/recommended"
|
||||
],
|
||||
"parser": "@typescript-eslint/parser",
|
||||
"parserOptions": {
|
||||
|
@ -19,7 +19,6 @@
|
|||
"@typescript-eslint/ban-ts-comment": 0,
|
||||
"@typescript-eslint/no-explicit-any": 0,
|
||||
"@typescript-eslint/explicit-module-boundary-types": 0,
|
||||
"@typescript-eslint/no-empty-function": 0,
|
||||
"arrow-body-style": 0,
|
||||
"curly": 0,
|
||||
"eol-last": 0,
|
||||
|
@ -39,9 +38,8 @@
|
|||
"no-useless-constructor": 0,
|
||||
"no-useless-escape": 0,
|
||||
"no-var": 0,
|
||||
"prefer-const": 1,
|
||||
"prefer-const": 0,
|
||||
"prefer-rest-params": 0,
|
||||
"prettier/prettier": "error",
|
||||
"quote-props": 0,
|
||||
"unicorn/filename-case": 0
|
||||
}
|
||||
|
|
1
.github/CODEOWNERS
vendored
1
.github/CODEOWNERS
vendored
|
@ -1 +0,0 @@
|
|||
* @dessalines @SleeplessOne1917 @alectrocute @jsit
|
29
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
Normal file
29
.github/ISSUE_TEMPLATE/BUG_REPORT.md
vendored
Normal file
|
@ -0,0 +1,29 @@
|
|||
---
|
||||
name: "\U0001F41E Bug Report"
|
||||
about: Create a report to help us improve Lemmy
|
||||
title: ''
|
||||
labels: bug
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
Found a bug? Please fill out the sections below. 👍
|
||||
|
||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
||||
|
||||
### Issue Summary
|
||||
|
||||
A summary of the bug.
|
||||
|
||||
|
||||
### Steps to Reproduce
|
||||
|
||||
1. (for example) I clicked login, and an endless spinner show up.
|
||||
2. I tried to install lemmy via this guide, and I'm getting this error.
|
||||
3. ...
|
||||
|
||||
### Technical details
|
||||
|
||||
* Please post your log: `sudo docker-compose logs > lemmy_log.out`.
|
||||
* What OS are you trying to install lemmy on?
|
||||
* Any browser console errors?
|
65
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
65
.github/ISSUE_TEMPLATE/BUG_REPORT.yml
vendored
|
@ -1,65 +0,0 @@
|
|||
name: "\U0001F41E Bug report"
|
||||
description: Report a bug to help us improve Lemmy-UI.
|
||||
labels: ["bug", "triage"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to help improve Lemmy-UI by reporting a bug!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Requirements
|
||||
description: Before you create a bug report, please carefully check the following –
|
||||
options:
|
||||
- label: This is a bug report, and if not, please post to https://lemmy.ml/c/lemmy_support instead.
|
||||
required: true
|
||||
- label: Please [check](https://github.com/LemmyNet/lemmy-ui/issues) to see if this issue already exists.
|
||||
required: true
|
||||
- label: It's a single bug. Do not report multiple bugs in one issue.
|
||||
required: true
|
||||
- label: It's a frontend issue, not a backend issue; Otherwise please create an issue on the [backend repo](https://github.com/LemmyNet/lemmy) instead.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: summary
|
||||
attributes:
|
||||
label: Summary
|
||||
description: Explain the bug and upload images, screenshots or videos if possible.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: |
|
||||
In a numbered list, walk us through the steps needed to reproduce the bug.
|
||||
The better your description is _(go 'here', click 'there'...)_, the quicker we can fix it.
|
||||
value: |
|
||||
1.
|
||||
2.
|
||||
3.
|
||||
4.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: technical
|
||||
attributes:
|
||||
label: Technical Details
|
||||
description: |
|
||||
Describe your environment (OS, browser, model of smartphone, etc)
|
||||
If relevant, also share any console errors and/or screenshots here.
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: lemmy-ui-version
|
||||
attributes:
|
||||
label: Lemmy Instance Version
|
||||
description: What's the version of the Lemmy instance where the bug can be reproduced?
|
||||
placeholder: ex. 0.18-rc.6
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: lemmy-instance
|
||||
attributes:
|
||||
label: Lemmy Instance URL
|
||||
description: What's the URL of the Lemmy instance where the bug can be reproduced?
|
||||
placeholder: https://lemmy.ml
|
44
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
44
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.md
vendored
Normal file
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
name: "\U0001F680 Feature request"
|
||||
about: Suggest an idea for improving Lemmy
|
||||
title: ''
|
||||
labels: enhancement
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
For backend issues, use [lemmy](https://github.com/LemmyNet/lemmy)
|
||||
|
||||
### Is your proposal related to a problem?
|
||||
|
||||
<!--
|
||||
Provide a clear and concise description of what the problem is.
|
||||
For example, "I'm always frustrated when..."
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
||||
|
||||
### Describe the solution you'd like
|
||||
|
||||
<!--
|
||||
Provide a clear and concise description of what you want to happen.
|
||||
-->
|
||||
|
||||
(Describe your proposed solution here.)
|
||||
|
||||
### Describe alternatives you've considered
|
||||
|
||||
<!--
|
||||
Let us know about other solutions you've tried or researched.
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
||||
|
||||
### Additional context
|
||||
|
||||
<!--
|
||||
Is there anything else you can add about the proposal?
|
||||
You might want to link to related issues here, if you haven't already.
|
||||
-->
|
||||
|
||||
(Write your answer here.)
|
27
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
27
.github/ISSUE_TEMPLATE/FEATURE_REQUEST.yml
vendored
|
@ -1,27 +0,0 @@
|
|||
name: "\U0001F680 Feature request"
|
||||
description: Suggest an idea for Lemmy-UI.
|
||||
labels: ["enhancement"]
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: |
|
||||
Thanks for taking the time to help improve Lemmy-UI by suggesting a feature!
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Requirements
|
||||
description: Before you create a feature request, please carefully check the following –
|
||||
options:
|
||||
- label: This is a feature request and not a bug report. Otherwise, please create a new [bug report](https://github.com/LemmyNet/lemmy-ui/issues/new?assignees=&labels=bug%2Ctriage&projects=&template=BUG_REPORT.yml) instead.
|
||||
required: true
|
||||
- label: Please [check](https://github.com/LemmyNet/lemmy-ui/issues) to see if this request (or a similar one) already exists.
|
||||
required: true
|
||||
- label: It's a single feature. Please don't request multiple features in one issue.
|
||||
required: true
|
||||
- type: textarea
|
||||
id: solution
|
||||
attributes:
|
||||
label: Describe the feature you'd like
|
||||
description: |
|
||||
Provide a clear and concise description of the feature. Explain why it's needed.
|
||||
validations:
|
||||
required: true
|
10
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/QUESTION.md
vendored
Normal file
|
@ -0,0 +1,10 @@
|
|||
---
|
||||
name: "? Question"
|
||||
about: General questions about Lemmy
|
||||
title: ''
|
||||
labels: question
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
What's the question you have about lemmy?
|
8
.github/ISSUE_TEMPLATE/config.yml
vendored
8
.github/ISSUE_TEMPLATE/config.yml
vendored
|
@ -1,8 +0,0 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Question
|
||||
url: https://lemmy.ml/c/lemmy_support
|
||||
about: Please ask and answer general questions here.
|
||||
- name: Technical Discussion
|
||||
url: https://github.com/LemmyNet/lemmy-ui/discussions
|
||||
about: Please discuss technical topics with other contributors here.
|
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
|
@ -1,12 +0,0 @@
|
|||
## Description
|
||||
|
||||
<!-- Please describe exactly what this PR changes, including URLs and issue
|
||||
numbers. If it fixes an issue, add "Fixes #XXXX" -->
|
||||
|
||||
## Screenshots
|
||||
|
||||
<!-- Please include before and after screenshots if applicable -->
|
||||
|
||||
### Before
|
||||
|
||||
### After
|
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -27,5 +27,3 @@ package-lock.json
|
|||
|
||||
src/shared/translations
|
||||
|
||||
stats.json
|
||||
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
src/shared/translations
|
||||
lemmy-translations
|
||||
src/assets/css/themes/*.css
|
||||
stats.json
|
||||
dist
|
4
.prettierrc.js
Normal file
4
.prettierrc.js
Normal file
|
@ -0,0 +1,4 @@
|
|||
module.exports = Object.assign(require("eslint-plugin-prettier"), {
|
||||
arrowParens: "avoid",
|
||||
semi: true,
|
||||
});
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"arrowParens": "avoid",
|
||||
"semi": true
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
pipeline:
|
||||
fetch_git_submodules:
|
||||
image: node:alpine
|
||||
commands:
|
||||
- apk add git
|
||||
- git submodule init
|
||||
- git submodule update --recursive --remote
|
||||
# - git fetch --tags
|
||||
|
||||
yarn:
|
||||
image: node:alpine
|
||||
commands:
|
||||
- yarn
|
||||
|
||||
yarn_lint:
|
||||
image: node:alpine
|
||||
commands:
|
||||
- yarn lint
|
||||
|
||||
yarn_build_dev:
|
||||
image: node:alpine
|
||||
commands:
|
||||
- yarn build:dev
|
||||
|
||||
publish_release_docker:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
secrets: [docker_username, docker_password]
|
||||
settings:
|
||||
repo: dessalines/lemmy-ui
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64
|
||||
auto_tag: true
|
||||
when:
|
||||
event: tag
|
||||
|
||||
nightly_build:
|
||||
image: woodpeckerci/plugin-docker-buildx
|
||||
secrets: [docker_username, docker_password]
|
||||
settings:
|
||||
repo: dessalines/lemmy-ui
|
||||
dockerfile: Dockerfile
|
||||
platforms: linux/amd64
|
||||
tag: dev
|
||||
when:
|
||||
event: cron
|
|
@ -1,3 +1,3 @@
|
|||
# Contributing
|
||||
|
||||
See [here](https://join-lemmy.org/docs/contributors/01-overview.html) for contributing Instructions.
|
||||
See [here](https://join-lemmy.org/docs/en/contributing/contributing.html) for contributing Instructions.
|
||||
|
|
24
Dockerfile
24
Dockerfile
|
@ -1,16 +1,11 @@
|
|||
FROM node:20.2-alpine as builder
|
||||
RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache
|
||||
RUN curl -sf https://gobinaries.com/tj/node-prune | sh
|
||||
FROM node:alpine as builder
|
||||
RUN apk update && apk add yarn python3 build-base gcc wget git --no-cache
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
ENV npm_config_target_arch=x64
|
||||
ENV npm_config_target_platform=linux
|
||||
ENV npm_config_target_libc=musl
|
||||
|
||||
# Cache deps
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --production --prefer-offline --pure-lockfile
|
||||
RUN yarn install --pure-lockfile
|
||||
|
||||
# Build
|
||||
COPY generate_translations.js \
|
||||
|
@ -26,17 +21,8 @@ COPY .git .git
|
|||
# Set UI version
|
||||
RUN echo "export const VERSION = '$(git describe --tag)';" > "src/shared/version.ts"
|
||||
|
||||
RUN yarn --production --prefer-offline
|
||||
RUN NODE_OPTIONS="--max-old-space-size=8192" yarn build:prod
|
||||
|
||||
# Prune the image
|
||||
RUN node-prune /usr/src/app/node_modules
|
||||
|
||||
RUN rm -rf ./node_modules/import-sort-parser-typescript
|
||||
RUN rm -rf ./node_modules/typescript
|
||||
RUN rm -rf ./node_modules/npm
|
||||
|
||||
RUN du -sh ./node_modules/* | sort -nr | grep '\dM.*'
|
||||
RUN yarn
|
||||
RUN yarn build:prod
|
||||
|
||||
FROM node:alpine as runner
|
||||
COPY --from=builder /usr/src/app/dist /app/dist
|
||||
|
|
25
README.md
25
README.md
|
@ -1,20 +1,5 @@
|
|||
# Lemmy-UI
|
||||
|
||||
The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno.
|
||||
|
||||
Based off of MrFoxPro's [inferno-isomorphic-template](https://github.com/MrFoxPro/inferno-isomorphic-template).
|
||||
|
||||
## Configuration
|
||||
|
||||
The following environment variables can be used to configure lemmy-ui:
|
||||
|
||||
| `ENV_VAR` | type | default | description |
|
||||
| ------------------------------ | -------- | ---------------- | ----------------------------------------------------------------------------------- |
|
||||
| `LEMMY_UI_HOST` | `string` | `0.0.0.0:1234` | The IP / port that the lemmy-ui isomorphic node server is hosted at. |
|
||||
| `LEMMY_UI_LEMMY_INTERNAL_HOST` | `string` | `0.0.0.0:8536` | The internal IP / port that lemmy is hosted at. Often `lemmy:8536` if using docker. |
|
||||
| `LEMMY_UI_LEMMY_EXTERNAL_HOST` | `string` | `0.0.0.0:8536` | The external IP / port that lemmy is hosted at. Often `DOMAIN.TLD`. |
|
||||
| `LEMMY_UI_HTTPS` | `bool` | `false` | Whether to use https. |
|
||||
| `LEMMY_UI_EXTRA_THEMES_FOLDER` | `string` | `./extra_themes` | A location for additional lemmy css themes. |
|
||||
| `LEMMY_UI_DEBUG` | `bool` | `false` | Loads the [Eruda](https://github.com/liriliri/eruda) debugging utility. |
|
||||
| `LEMMY_UI_DISABLE_CSP` | `bool` | `false` | Disables CSP security headers |
|
||||
| `LEMMY_UI_CUSTOM_HTML_HEADER` | `string` | | Injects a custom script into `<head>`. |
|
||||
# lemmy-ui
|
||||
|
||||
The official web app for [Lemmy](https://github.com/LemmyNet/lemmy), written in inferno.
|
||||
|
||||
Based off of MrFoxPro's [inferno-isomorphic-template](https://github.com/MrFoxPro/inferno-isomorphic-template).
|
||||
|
|
|
@ -4,8 +4,7 @@ set -e
|
|||
new_tag="$1"
|
||||
|
||||
# Old deploy
|
||||
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64 --push
|
||||
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag --platform=linux/amd64
|
||||
# sudo docker build . --tag dessalines/lemmy-ui:$new_tag
|
||||
# sudo docker push dessalines/lemmy-ui:$new_tag
|
||||
|
||||
# Upgrade version
|
||||
|
|
|
@ -1,37 +0,0 @@
|
|||
FROM node:20.2-alpine as builder
|
||||
RUN apk update && apk add curl yarn python3 build-base gcc wget git --no-cache
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
ENV npm_config_target_arch=x64
|
||||
ENV npm_config_target_platform=linux
|
||||
ENV npm_config_target_libc=musl
|
||||
|
||||
# Cache deps
|
||||
COPY package.json yarn.lock ./
|
||||
RUN yarn --prefer-offline --pure-lockfile
|
||||
|
||||
# Build
|
||||
COPY generate_translations.js \
|
||||
tsconfig.json \
|
||||
webpack.config.js \
|
||||
.babelrc \
|
||||
./
|
||||
|
||||
COPY lemmy-translations lemmy-translations
|
||||
COPY src src
|
||||
COPY .git .git
|
||||
|
||||
# Set UI version
|
||||
RUN echo "export const VERSION = 'dev';" > "src/shared/version.ts"
|
||||
|
||||
RUN yarn --prefer-offline
|
||||
RUN yarn build:dev
|
||||
|
||||
FROM node:alpine as runner
|
||||
COPY --from=builder /usr/src/app/dist /app/dist
|
||||
COPY --from=builder /usr/src/app/node_modules /app/node_modules
|
||||
|
||||
EXPOSE 1234
|
||||
WORKDIR /app
|
||||
CMD node dist/js/server.js
|
|
@ -30,70 +30,30 @@ fs.readdir(translationDir, (_err, files) => {
|
|||
const baseLanguage = "en";
|
||||
|
||||
fs.readFile(`${translationDir}${baseLanguage}.json`, "utf8", (_, fileStr) => {
|
||||
const noOptionKeys = [];
|
||||
const optionKeys = [];
|
||||
const optionRegex = /\{\{(.+?)\}\}/g;
|
||||
const optionMap = new Map();
|
||||
|
||||
for (const [key, val] of Object.entries(JSON.parse(fileStr))) {
|
||||
const options = [];
|
||||
for (
|
||||
let match = optionRegex.exec(val);
|
||||
match;
|
||||
match = optionRegex.exec(val)
|
||||
) {
|
||||
options.push(match[1]);
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
optionMap.set(key, options);
|
||||
optionKeys.push(key);
|
||||
} else {
|
||||
noOptionKeys.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
const indent = " ";
|
||||
const keys = Object.keys(JSON.parse(fileStr));
|
||||
|
||||
const data = `import { i18n } from "i18next";
|
||||
|
||||
declare module "i18next" {
|
||||
export type NoOptionI18nKeys =
|
||||
${noOptionKeys.map(key => `${indent}| "${key}"`).join("\n")};
|
||||
|
||||
export type OptionI18nKeys =
|
||||
${optionKeys.map(key => `${indent}| "${key}"`).join("\n")};
|
||||
|
||||
export type I18nKeys = NoOptionI18nKeys | OptionI18nKeys;
|
||||
|
||||
export type TTypedOptions<TKey extends OptionI18nKeys> =${Array.from(
|
||||
optionMap.entries()
|
||||
).reduce(
|
||||
(acc, [key, options]) =>
|
||||
`${acc} TKey extends \"${key}\" ? ${
|
||||
options.reduce((acc, cur) => acc + `${cur}: string | number; `, "{ ") +
|
||||
"}"
|
||||
} :\n${indent}`,
|
||||
""
|
||||
)} (Record<string, unknown> | string);
|
||||
|
||||
export type I18nKeys =
|
||||
${keys.map(key => ` | "${key}"`).join("\n")};
|
||||
|
||||
export interface TFunctionTyped {
|
||||
// Translation requires options
|
||||
<
|
||||
TKey extends OptionI18nKeys | OptionI18nKeys[],
|
||||
TResult extends TFunctionResult = string,
|
||||
TInterpolationMap extends TTypedOptions<TKey> = StringMap
|
||||
> (
|
||||
key: TKey,
|
||||
options: TOptions<TInterpolationMap> | string
|
||||
): TResult;
|
||||
|
||||
// Translation does not require options
|
||||
// basic usage
|
||||
<
|
||||
TResult extends TFunctionResult = string,
|
||||
TInterpolationMap extends Record<string, unknown> = StringMap
|
||||
> (
|
||||
key: NoOptionI18nKeys | NoOptionI18nKeys[],
|
||||
>(
|
||||
key: I18nKeys | I18nKeys[],
|
||||
options?: TOptions<TInterpolationMap> | string
|
||||
): TResult;
|
||||
// overloaded usage
|
||||
<
|
||||
TResult extends TFunctionResult = string,
|
||||
TInterpolationMap extends Record<string, unknown> = StringMap
|
||||
>(
|
||||
key: I18nKeys | I18nKeys[],
|
||||
defaultValue?: string,
|
||||
options?: TOptions<TInterpolationMap> | string
|
||||
): TResult;
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit a241fe1255a6363c7ae1ec5a09520c066745e6ce
|
||||
Subproject commit 0412b6b349e5e8d6ac3ed88801187833e95c72c9
|
211
package.json
211
package.json
|
@ -1,140 +1,111 @@
|
|||
{
|
||||
"name": "lemmy-ui",
|
||||
"version": "0.18.1-rc.9",
|
||||
"description": "An isomorphic UI for lemmy",
|
||||
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
||||
"license": "AGPL-3.0",
|
||||
"version": "0.14.5",
|
||||
"author": "Dessalines <tyhou13@gmx.com>",
|
||||
"license": "AGPL-3.0",
|
||||
"scripts": {
|
||||
"analyze": "webpack --mode=none",
|
||||
"prebuild:dev": "yarn clean && node generate_translations.js",
|
||||
"build:dev": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=development",
|
||||
"prebuild:prod": "yarn clean && node generate_translations.js",
|
||||
"build:prod": "webpack --env COMMIT_HASH=$(git rev-parse --short HEAD) --mode=production",
|
||||
"build:dev": "webpack --mode=development",
|
||||
"build:prod": "webpack --mode=production",
|
||||
"clean": "yarn run rimraf dist",
|
||||
"dev": "yarn build:dev --watch",
|
||||
"lint": "yarn translations:generate && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx \"src/**\" && prettier --check \"src/**/*.{ts,tsx,js,css,scss}\"",
|
||||
"dev": "yarn start",
|
||||
"lint": "node generate_translations.js && tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
|
||||
"prebuild:dev": "yarn clean && node generate_translations.js",
|
||||
"prebuild:prod": "yarn clean && node generate_translations.js",
|
||||
"prepare": "husky install",
|
||||
"themes:build": "sass src/assets/css/themes/:src/assets/css/themes",
|
||||
"themes:watch": "sass --watch src/assets/css/themes/:src/assets/css/themes",
|
||||
"translations:generate": "node generate_translations.js",
|
||||
"translations:init": "git submodule init && yarn translations:update",
|
||||
"translations:update": "git submodule update --remote --recursive"
|
||||
"start": "yarn build:dev --watch"
|
||||
},
|
||||
"repository": "https://github.com/LemmyNet/lemmy-ui",
|
||||
"dependencies": {
|
||||
"@typescript-eslint/parser": "^5.6.0",
|
||||
"autosize": "^5.0.1",
|
||||
"check-password-strength": "^2.0.3",
|
||||
"choices.js": "^9.0.1",
|
||||
"emoji-short-name": "^1.0.0",
|
||||
"express": "~4.17.1",
|
||||
"i18next": "^21.5.4",
|
||||
"inferno": "^7.4.11",
|
||||
"inferno-create-element": "^7.4.11",
|
||||
"inferno-helmet": "^5.2.1",
|
||||
"inferno-hydrate": "^7.4.11",
|
||||
"inferno-i18next-dess": "^0.0.1",
|
||||
"inferno-router": "^7.4.11",
|
||||
"inferno-server": "^7.4.11",
|
||||
"isomorphic-cookie": "^1.2.4",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"markdown-it": "^12.1.0",
|
||||
"markdown-it-container": "^3.0.0",
|
||||
"markdown-it-html5-embed": "^1.0.0",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"moment": "^2.29.1",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"rxjs": "^7.4.0",
|
||||
"serialize-javascript": "^6.0.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"toastify-js": "^1.11.2",
|
||||
"tributejs": "^5.1.3",
|
||||
"websocket-ts": "^1.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.16.0",
|
||||
"@babel/plugin-transform-runtime": "^7.16.4",
|
||||
"@babel/plugin-transform-typescript": "^7.16.1",
|
||||
"@babel/preset-env": "7.16.4",
|
||||
"@babel/preset-typescript": "^7.16.0",
|
||||
"@babel/runtime": "^7.16.3",
|
||||
"@types/autosize": "^4.0.0",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^16.11.11",
|
||||
"@types/node-fetch": "^2.5.11",
|
||||
"@types/serialize-javascript": "^5.0.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.6.0",
|
||||
"babel-loader": "^8.2.3",
|
||||
"babel-plugin-inferno": "^6.3.0",
|
||||
"bootstrap": "^5.1.3",
|
||||
"bootswatch": "^5.1.3",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"copy-webpack-plugin": "^10.0.0",
|
||||
"css-loader": "^6.5.1",
|
||||
"eslint": "^8.4.0",
|
||||
"eslint-plugin-prettier": "^4.0.0",
|
||||
"husky": "^7.0.4",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"iso-639-1": "^2.1.10",
|
||||
"lemmy-js-client": "0.14.0-rc.1",
|
||||
"lint-staged": "^12.1.2",
|
||||
"mini-css-extract-plugin": "^2.4.5",
|
||||
"node-fetch": "^2.6.1",
|
||||
"node-sass": "^7.0.0",
|
||||
"prettier": "^2.5.1",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"prettier-plugin-organize-imports": "^2.3.4",
|
||||
"prettier-plugin-packagejson": "^2.2.15",
|
||||
"rimraf": "^3.0.2",
|
||||
"run-node-webpack-plugin": "^1.3.0",
|
||||
"sass-loader": "^12.3.0",
|
||||
"sortpack": "^2.2.0",
|
||||
"style-loader": "^3.3.1",
|
||||
"terser": "^5.10.0",
|
||||
"typescript": "^4.5.2",
|
||||
"webpack": "5.65.0",
|
||||
"webpack-cli": "^4.9.1",
|
||||
"webpack-dev-server": "4.6.0",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"lint-staged": {
|
||||
"*.{ts,tsx,js}": [
|
||||
"prettier --write",
|
||||
"eslint --fix"
|
||||
],
|
||||
"*.{css, scss}": [
|
||||
"prettier --write"
|
||||
],
|
||||
"package.json": [
|
||||
"sortpack"
|
||||
]
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-proposal-decorators": "^7.21.0",
|
||||
"@babel/plugin-transform-runtime": "^7.21.4",
|
||||
"@babel/plugin-transform-typescript": "^7.21.3",
|
||||
"@babel/preset-env": "7.21.5",
|
||||
"@babel/preset-typescript": "^7.21.5",
|
||||
"@babel/runtime": "^7.21.5",
|
||||
"@emoji-mart/data": "^1.1.0",
|
||||
"autosize": "^6.0.1",
|
||||
"babel-loader": "^9.1.2",
|
||||
"babel-plugin-inferno": "^6.6.0",
|
||||
"bootstrap": "^5.2.3",
|
||||
"check-password-strength": "^2.0.7",
|
||||
"classnames": "^2.3.1",
|
||||
"clean-webpack-plugin": "^4.0.0",
|
||||
"cookie": "^0.5.0",
|
||||
"copy-webpack-plugin": "^11.0.0",
|
||||
"cross-fetch": "^3.1.5",
|
||||
"css-loader": "^6.7.3",
|
||||
"date-fns": "^2.30.0",
|
||||
"emoji-mart": "^5.4.0",
|
||||
"emoji-short-name": "^2.0.0",
|
||||
"express": "~4.18.2",
|
||||
"history": "^5.3.0",
|
||||
"html-to-text": "^9.0.5",
|
||||
"i18next": "^22.4.15",
|
||||
"inferno": "^8.1.1",
|
||||
"inferno-create-element": "^8.1.1",
|
||||
"inferno-helmet": "^5.2.1",
|
||||
"inferno-hydrate": "^8.1.1",
|
||||
"inferno-i18next-dess": "0.0.2",
|
||||
"inferno-router": "^8.1.1",
|
||||
"inferno-server": "^8.1.1",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"lemmy-js-client": "0.18.0-rc.2",
|
||||
"lodash.isequal": "^4.5.0",
|
||||
"lodash.merge": "^4.6.2",
|
||||
"markdown-it": "^13.0.1",
|
||||
"markdown-it-container": "^3.0.0",
|
||||
"markdown-it-emoji": "^2.0.2",
|
||||
"markdown-it-footnote": "^3.0.3",
|
||||
"markdown-it-html5-embed": "^1.0.0",
|
||||
"markdown-it-sub": "^1.0.0",
|
||||
"markdown-it-sup": "^1.0.0",
|
||||
"mini-css-extract-plugin": "^2.7.5",
|
||||
"register-service-worker": "^1.7.2",
|
||||
"run-node-webpack-plugin": "^1.3.0",
|
||||
"sanitize-html": "^2.10.0",
|
||||
"sass": "^1.62.1",
|
||||
"sass-loader": "^13.2.2",
|
||||
"serialize-javascript": "^6.0.1",
|
||||
"service-worker-webpack": "^1.0.0",
|
||||
"sharp": "^0.32.1",
|
||||
"tippy.js": "^6.3.7",
|
||||
"toastify-js": "^1.12.0",
|
||||
"tributejs": "^5.1.3",
|
||||
"webpack": "5.82.1",
|
||||
"webpack-cli": "^5.1.1",
|
||||
"webpack-node-externals": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.21.8",
|
||||
"@types/autosize": "^4.0.0",
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/cookie": "^0.5.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/html-to-text": "^9.0.0",
|
||||
"@types/lodash.isequal": "^4.5.6",
|
||||
"@types/markdown-it": "^12.2.3",
|
||||
"@types/markdown-it-container": "^2.0.5",
|
||||
"@types/node": "^20.1.2",
|
||||
"@types/path-browserify": "^1.0.0",
|
||||
"@types/sanitize-html": "^2.9.0",
|
||||
"@types/serialize-javascript": "^5.0.1",
|
||||
"@types/toastify-js": "^1.11.1",
|
||||
"@typescript-eslint/eslint-plugin": "^5.59.5",
|
||||
"@typescript-eslint/parser": "^5.59.5",
|
||||
"eslint": "^8.40.0",
|
||||
"eslint-plugin-inferno": "^7.32.2",
|
||||
"eslint-plugin-jsx-a11y": "^6.7.1",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"husky": "^8.0.3",
|
||||
"import-sort-style-module": "^6.0.0",
|
||||
"lint-staged": "^13.2.2",
|
||||
"prettier": "^2.8.8",
|
||||
"prettier-plugin-import-sort": "^0.0.7",
|
||||
"prettier-plugin-organize-imports": "^3.2.2",
|
||||
"prettier-plugin-packagejson": "^2.4.3",
|
||||
"rimraf": "^5.0.0",
|
||||
"sortpack": "^2.3.4",
|
||||
"style-loader": "^3.3.2",
|
||||
"terser": "^5.17.3",
|
||||
"typescript": "^5.0.4",
|
||||
"typescript-language-server": "^3.3.2",
|
||||
"webpack-bundle-analyzer": "^4.9.0",
|
||||
"webpack-dev-server": "4.15.0"
|
||||
},
|
||||
"packageManager": "yarn@1.22.19",
|
||||
"engines": {
|
||||
"node": ">=8.9.0"
|
||||
},
|
||||
"engineStrict": true,
|
||||
"importSort": {
|
||||
".js, .jsx, .ts, .tsx": {
|
||||
"style": "module",
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
.navbar-toggler {
|
||||
border: 0px;
|
||||
}
|
||||
|
||||
.navbar-expand-lg .navbar-nav .nav-link {
|
||||
padding-right: 0.75rem !important;
|
||||
padding-left: 0.75rem !important;
|
||||
|
@ -21,7 +25,7 @@
|
|||
}
|
||||
|
||||
.upvote:hover {
|
||||
color: var(--bs-info);
|
||||
color: var(--info);
|
||||
}
|
||||
|
||||
.upvote {
|
||||
|
@ -29,14 +33,14 @@
|
|||
}
|
||||
|
||||
.downvote:hover {
|
||||
color: var(--bs-danger);
|
||||
color: var(--danger);
|
||||
}
|
||||
|
||||
.downvote {
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
.form-select {
|
||||
.custom-select {
|
||||
-moz-appearance: none;
|
||||
}
|
||||
|
||||
|
@ -46,7 +50,7 @@
|
|||
}
|
||||
|
||||
.md-div p:last-child {
|
||||
margin-bottom: 0;
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
.md-div img {
|
||||
|
@ -58,39 +62,56 @@
|
|||
.md-div h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.md-div h2 {
|
||||
font-size: 1.8rem;
|
||||
}
|
||||
|
||||
.md-div h3 {
|
||||
font-size: 1.6rem;
|
||||
}
|
||||
|
||||
.md-div h4 {
|
||||
font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.md-div h5 {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
.md-div pre {
|
||||
white-space: pre;
|
||||
overflow-x: auto;
|
||||
.md-div table {
|
||||
border-collapse: collapse;
|
||||
width: 100%;
|
||||
margin-bottom: 1rem;
|
||||
border: 1px solid var(--dark);
|
||||
}
|
||||
|
||||
.md-div table th,
|
||||
.md-div table td {
|
||||
padding: 0.3rem;
|
||||
vertical-align: top;
|
||||
border-top: 1px solid var(--dark);
|
||||
border: 1px solid var(--dark);
|
||||
}
|
||||
|
||||
.md-div table thead th {
|
||||
vertical-align: bottom;
|
||||
border-bottom: 2px solid var(--dark);
|
||||
}
|
||||
|
||||
.md-div table tbody + tbody {
|
||||
border-top: 2px solid var(--dark);
|
||||
}
|
||||
|
||||
.vote-bar {
|
||||
min-width: 5ch;
|
||||
margin-top: -6.5px;
|
||||
}
|
||||
|
||||
.post-title a:visited:not(:hover) {
|
||||
color: var(--bs-gray) !important;
|
||||
.post-title {
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.post-title a:visited {
|
||||
color: var(--gray) !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
display: inline-grid;
|
||||
display: inline-flex;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
|
@ -107,55 +128,11 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.icon-emoji {
|
||||
width: 4em;
|
||||
height: auto;
|
||||
max-height: inherit;
|
||||
}
|
||||
|
||||
.icon-emoji-admin {
|
||||
max-width: 24px;
|
||||
max-height: 24px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.icon-inline {
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.emoji-picker-container {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
z-index: 1000;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 992px) {
|
||||
.emoji-picker-container {
|
||||
width: 100vw;
|
||||
transform: translateX(0%);
|
||||
position: fixed;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.emoji-picker-container > section {
|
||||
width: 100% !important;
|
||||
}
|
||||
}
|
||||
|
||||
.click-away-container {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999;
|
||||
}
|
||||
|
||||
.spinner-large {
|
||||
display: grid;
|
||||
display: block;
|
||||
margin: 1em auto;
|
||||
width: 2em;
|
||||
|
@ -170,14 +147,21 @@
|
|||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(359deg);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
position: absolute;
|
||||
background-color: var(--light);
|
||||
min-width: 160px;
|
||||
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
|
||||
z-index: 2000;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 2px solid var(--bs-secondary);
|
||||
border-left: 2px solid var(--secondary);
|
||||
margin: 0.5em 5px;
|
||||
padding: 0.1em 5px;
|
||||
}
|
||||
|
@ -192,16 +176,11 @@ blockquote {
|
|||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.comments {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.thumbnail {
|
||||
object-fit: cover;
|
||||
aspect-ratio: 1/1;
|
||||
width: 5rem;
|
||||
height: 5rem;
|
||||
min-height: 60px;
|
||||
max-height: 80px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.thumbnail svg {
|
||||
|
@ -216,7 +195,7 @@ blockquote {
|
|||
}
|
||||
|
||||
hr {
|
||||
border-top: 1px solid var(--bs-light);
|
||||
border-top: 1px solid var(--light);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
|
@ -228,10 +207,6 @@ hr {
|
|||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.text-xs-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.overflow-wrap-anywhere {
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
@ -264,16 +239,11 @@ hr {
|
|||
-ms-transform: scale(1.2);
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: Fix this in markup rather than this overly specific selector:
|
||||
* https://getbootstrap.com/docs/5.3/components/buttons/#block-buttons
|
||||
*/
|
||||
.btn.d-block + .btn.d-block {
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
|
||||
.mini-overlay {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 2px;
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
background: rgba(0, 0, 0, 0.4);
|
||||
|
@ -333,6 +303,16 @@ pre {
|
|||
transition: width 0.2s ease-out 0s !important;
|
||||
}
|
||||
|
||||
.show-input {
|
||||
width: 13em !important;
|
||||
}
|
||||
.hide-input {
|
||||
background: transparent !important;
|
||||
background-color: transparent !important;
|
||||
width: 0px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
br.big {
|
||||
display: block;
|
||||
content: "";
|
||||
|
@ -346,12 +326,10 @@ br.big {
|
|||
}
|
||||
|
||||
.avatar-overlay {
|
||||
width: 20vw;
|
||||
height: 20vw;
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
max-width: 120px;
|
||||
max-height: 120px;
|
||||
min-width: 80px;
|
||||
min-height: 80px;
|
||||
}
|
||||
|
||||
.avatar-pushup {
|
||||
|
@ -359,9 +337,8 @@ br.big {
|
|||
}
|
||||
|
||||
.img-icon {
|
||||
width: calc(var(--bs-body-line-height) * 1em);
|
||||
height: calc(var(--bs-body-line-height) * 1em);
|
||||
border-radius: 0.25em;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.tribute-container ul {
|
||||
|
@ -369,65 +346,25 @@ br.big {
|
|||
margin-top: 2px;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
background: var(--bs-light);
|
||||
background: var(--light);
|
||||
}
|
||||
|
||||
.tribute-container li {
|
||||
padding: 5px;
|
||||
padding: 5px 5px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tribute-container li.highlight {
|
||||
background: var(--bs-primary);
|
||||
background: var(--primary);
|
||||
}
|
||||
|
||||
.tribute-container li span {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.tribute-container li.no-match {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.tribute-container .menu-highlighted {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.honeypot {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.slight-radius {
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.modlog-choices-font-size {
|
||||
font-size: 0.9375rem !important;
|
||||
}
|
||||
|
||||
.preview-lines {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
}
|
||||
|
||||
#emoji-picker {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.skip-link {
|
||||
top: -40px;
|
||||
transition: top 0.3s ease;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.skip-link {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
.skip-link:focus {
|
||||
top: 0;
|
||||
display:none !important;
|
||||
}
|
||||
|
|
865
src/assets/css/themes/_variables.bootstra_386-tmp.scss
Normal file
865
src/assets/css/themes/_variables.bootstra_386-tmp.scss
Normal file
|
@ -0,0 +1,865 @@
|
|||
//
|
||||
// Variables
|
||||
// --------------------------------------------------
|
||||
|
||||
//== Colors
|
||||
//
|
||||
//## Gray and brand colors for use across Bootstrap.
|
||||
|
||||
//// colors from bs-2
|
||||
// Grays
|
||||
// -------------------------
|
||||
$black: #000;
|
||||
$grayDark: #555;
|
||||
$gray: #bbb;
|
||||
$grayLight: #bbb;
|
||||
$white: #fff;
|
||||
|
||||
// Accent colors
|
||||
// -------------------------
|
||||
$blue: #5555ff;
|
||||
$cyan: #55ffff;
|
||||
$cyanDark: #00aaaa;
|
||||
$blueDark: #000084;
|
||||
$green: #55ff55;
|
||||
$greenDark: #00aa00;
|
||||
$magenta: #ff55ff;
|
||||
$magentaDark: #aa00aa;
|
||||
$red: #ff5555;
|
||||
$redDark: #aa0000;
|
||||
$yellow: #fefe54;
|
||||
$brown: #aa5500;
|
||||
$orange: #a85400;
|
||||
$pink: #fe54fe;
|
||||
$purple: #fe5454;
|
||||
|
||||
// end colors
|
||||
|
||||
$gray-base: $gray;
|
||||
$gray-darker: $grayDark;
|
||||
$gray-dark: $grayDark;
|
||||
$gray-light: $grayLight;
|
||||
$gray-lighter: $grayLight;
|
||||
|
||||
$brand-primary: $gray;
|
||||
$brand-primary-bg: $cyanDark;
|
||||
$brand-success: $greenDark;
|
||||
$brand-info: $brown;
|
||||
$brand-warning: $magentaDark;
|
||||
$brand-danger: $redDark;
|
||||
|
||||
//== Scaffolding
|
||||
//
|
||||
//## Settings for some of the most global styles.
|
||||
|
||||
//** Background color for `<body>`.
|
||||
$body-bg: $blueDark;
|
||||
//** Global text color on `<body>`.
|
||||
$text-color: $gray-light;
|
||||
|
||||
//** Global textual link color.
|
||||
$link-color: $brand-primary;
|
||||
//** Link hover color set via `darken()` function.
|
||||
$link-hover-color: $white;
|
||||
//** Link hover decoration.
|
||||
$link-hover-decoration: none;
|
||||
|
||||
//== Typography
|
||||
//
|
||||
//## Font, line-height, and color for body text, headings, and more.
|
||||
|
||||
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
$font-family-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
//** Default monospace fonts for `<code>`, `<kbd>`, and `<pre>`.
|
||||
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
$font-family-base: $font-family-sans-serif;
|
||||
|
||||
$baseWidth: 10px;
|
||||
$font-size-base: 18px;
|
||||
$font-size-large: $font-size-base;
|
||||
$font-size-small: $font-size-base;
|
||||
|
||||
$font-size-h1: $font-size-base;
|
||||
$font-size-h2: $font-size-base;
|
||||
$font-size-h3: $font-size-base;
|
||||
$font-size-h4: $font-size-base;
|
||||
$font-size-h5: $font-size-base;
|
||||
$font-size-h6: $font-size-base;
|
||||
|
||||
//** Unit-less `line-height` for use in components like buttons.
|
||||
$baseLineHeight: 19px;
|
||||
$line-height-base: $baseLineHeight;
|
||||
//** Computed "line-height" (`font-size` * `line-height`) for use with `margin`, `padding`, etc.
|
||||
$line-height-computed: $line-height-base;
|
||||
|
||||
//** By default, this inherits from the `<body>`.
|
||||
$headings-font-family: inherit;
|
||||
$headings-font-weight: normal;
|
||||
$headings-line-height: $line-height-base;
|
||||
$headings-color: inherit;
|
||||
|
||||
$space: $baseWidth;
|
||||
$halfbaseLineHeight: ($baseLineHeight / 2);
|
||||
$borderWidth: 2px;
|
||||
$baseLineWidth: ($baseLineHeight / 2);
|
||||
$halfSpace: ($baseWidth / 2);
|
||||
$lhsNB: ($baseWidth / 2 + 1);
|
||||
$rhsNB: ($baseWidth / 2 - 1);
|
||||
$lhs: ($lhsNB - ($borderWidth));
|
||||
$rhs: ($rhsNB - ($borderWidth / 2));
|
||||
$tsNB: ($baseLineHeight / 2);
|
||||
$bsNB: $tsNB;
|
||||
$ts: ($tsNB - ($borderWidth / 2));
|
||||
$bs: $ts;
|
||||
$tsMargin: 3px;
|
||||
|
||||
//== Iconography
|
||||
//
|
||||
//## Specify custom location and filename of the included Glyphicons icon font. Useful for those including Bootstrap via Bower.
|
||||
|
||||
//** Load fonts from this directory.
|
||||
$icon-font-path: "../fonts/";
|
||||
//** File name for all font files.
|
||||
$icon-font-name: "glyphicons-halflings-regular";
|
||||
//** Element ID within SVG icon file.
|
||||
$icon-font-svg-id: "glyphicons_halflingsregular";
|
||||
|
||||
//== Components
|
||||
//
|
||||
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
|
||||
|
||||
$padding-base-vertical: 0px;
|
||||
$padding-base-horizontal: 0px;
|
||||
|
||||
$padding-large-vertical: 0px;
|
||||
$padding-large-horizontal: $halfSpace;
|
||||
|
||||
$padding-small-vertical: 0px;
|
||||
$padding-small-horizontal: 0px;
|
||||
|
||||
$padding-xs-vertical: 0px;
|
||||
$padding-xs-horizontal: 0px;
|
||||
|
||||
$line-height-large: $baseLineHeight;
|
||||
$line-height-small: $baseLineHeight;
|
||||
|
||||
$border-radius-base: 0;
|
||||
$border-radius-large: 0;
|
||||
$border-radius-small: 0;
|
||||
|
||||
//** Global color for active items (e.g., navs or dropdowns).
|
||||
$component-active-color: $white;
|
||||
//** Global background color for active items (e.g., navs or dropdowns).
|
||||
$component-active-bg: $black;
|
||||
|
||||
//** Width of the `border` for generating carets that indicator dropdowns.
|
||||
$caret-width-base: 4px;
|
||||
//** Carets increase slightly in size for larger components.
|
||||
$caret-width-large: 5px;
|
||||
|
||||
//== Tables
|
||||
//
|
||||
//## Customizes the `.table` component with basic values, each used across all table variations.
|
||||
|
||||
//** Padding for `<th>`s and `<td>`s.
|
||||
$table-cell-padding: $ts $rhs $bs $lhs;
|
||||
//** Padding for cells in `.table-condensed`.
|
||||
$table-condensed-cell-padding: $ts $rhs $bs $lhs;
|
||||
|
||||
//** Default background color used for all tables.
|
||||
$table-bg: transparent;
|
||||
//** Background color used for `.table-striped`.
|
||||
$table-bg-accent: $black;
|
||||
//** Background color used for `.table-hover`.
|
||||
$table-bg-hover: #f5f5f5;
|
||||
$table-bg-active: $table-bg-hover;
|
||||
|
||||
//** Border color for table and cell borders.
|
||||
$table-border-color: $gray;
|
||||
|
||||
//== Buttons
|
||||
//
|
||||
//## For each of Bootstrap's buttons, define text, background and border color.
|
||||
|
||||
$btn-font-weight: normal;
|
||||
|
||||
$btn-default-color: $black;
|
||||
$btn-default-bg: $grayLight;
|
||||
$btn-default-border: $grayLight;
|
||||
|
||||
$btn-primary-color: $black;
|
||||
$btn-primary-bg: $cyanDark;
|
||||
$btn-primary-border: $grayLight;
|
||||
|
||||
$btn-success-color: #fff;
|
||||
$btn-success-bg: $brand-success;
|
||||
$btn-success-border: $btn-success-bg;
|
||||
|
||||
$btn-info-color: #fff;
|
||||
$btn-info-bg: $brand-info;
|
||||
$btn-info-border: $btn-info-bg;
|
||||
|
||||
$btn-warning-color: #fff;
|
||||
$btn-warning-bg: $brand-warning;
|
||||
$btn-warning-border: $btn-warning-bg;
|
||||
|
||||
$btn-danger-color: #fff;
|
||||
$btn-danger-bg: $brand-danger;
|
||||
$btn-danger-border: $btn-danger-bg;
|
||||
|
||||
$btn-link-disabled-color: $gray-light;
|
||||
|
||||
//== Forms
|
||||
//
|
||||
//##
|
||||
|
||||
//** `<input>` background color
|
||||
$input-bg: $cyanDark;
|
||||
//** `<input disabled>` background color
|
||||
$input-bg-disabled: $gray-lighter;
|
||||
|
||||
//** Text color for `<input>`s
|
||||
$input-color: $white;
|
||||
//** `<input>` border color
|
||||
$input-border: #ccc;
|
||||
|
||||
// TODO: Rename `$input-border-radius` to `$input-border-radius-base` in v4
|
||||
//** Default `.form-control` border radius
|
||||
// This has no effect on `<select>`s in some browsers, due to the limited stylability of `<select>`s in CSS.
|
||||
$input-border-radius: $border-radius-base;
|
||||
//** Large `.form-control` border radius
|
||||
$input-border-radius-large: $border-radius-large;
|
||||
//** Small `.form-control` border radius
|
||||
$input-border-radius-small: $border-radius-small;
|
||||
|
||||
//** Border color for inputs on focus
|
||||
$input-border-focus: $black;
|
||||
|
||||
//** Placeholder text color
|
||||
$input-color-placeholder: $black;
|
||||
|
||||
//** Default `.form-control` height
|
||||
$input-height-base: $line-height-computed;
|
||||
//** Large `.form-control` height
|
||||
$input-height-large: $input-height-base;
|
||||
//** Small `.form-control` height
|
||||
$input-height-small: $input-height-base;
|
||||
|
||||
$legend-color: $gray-dark;
|
||||
$legend-border-color: #e5e5e5;
|
||||
|
||||
//** Background color for textual input addons
|
||||
$input-group-addon-bg: $gray-lighter;
|
||||
//** Border color for textual input addons
|
||||
$input-group-addon-border-color: $input-border;
|
||||
|
||||
//** Disabled cursor for form controls and buttons.
|
||||
$cursor-disabled: not-allowed;
|
||||
|
||||
//== Dropdowns
|
||||
//
|
||||
//## Dropdown menu container and contents.
|
||||
|
||||
//** Background for the dropdown menu.
|
||||
$dropdown-bg: $gray;
|
||||
//** Dropdown menu `border-color`.
|
||||
$dropdown-border: rgb(0, 0, 0);
|
||||
//** Dropdown menu `border-color` **for IE8**.
|
||||
$dropdown-fallback-border: #ccc;
|
||||
//** Divider color for between dropdown items.
|
||||
$dropdown-divider-bg: $black;
|
||||
|
||||
//** Dropdown link text color.
|
||||
$dropdown-link-color: $black;
|
||||
//** Hover color for dropdown links.
|
||||
$dropdown-link-hover-color: $gray;
|
||||
//** Hover background for dropdown links.
|
||||
$dropdown-link-hover-bg: $black;
|
||||
|
||||
//** Active dropdown menu item text color.
|
||||
$dropdown-link-active-color: $component-active-color;
|
||||
//** Active dropdown menu item background color.
|
||||
$dropdown-link-active-bg: $component-active-bg;
|
||||
|
||||
//** Disabled dropdown menu item background color.
|
||||
$dropdown-link-disabled-color: $gray-light;
|
||||
|
||||
//** Text color for headers within dropdown menus.
|
||||
$dropdown-header-color: $black;
|
||||
|
||||
//** Deprecated `$dropdown-caret-color` as of v3.1.0
|
||||
$dropdown-caret-color: #000;
|
||||
|
||||
//-- Z-index master list
|
||||
//
|
||||
// Warning: Avoid customizing these values. They're used for a bird's eye view
|
||||
// of components dependent on the z-axis and are designed to all work together.
|
||||
//
|
||||
// Note: These variables are not generated into the Customizer.
|
||||
|
||||
$zindex-navbar: 1000;
|
||||
$zindex-dropdown: 1000;
|
||||
$zindex-popover: 1060;
|
||||
$zindex-tooltip: 1070;
|
||||
$zindex-navbar-fixed: 1030;
|
||||
$zindex-modal: 1040;
|
||||
|
||||
//== Media queries breakpoints
|
||||
//
|
||||
//## Define the breakpoints at which your layout will change, adapting to different screen sizes.
|
||||
|
||||
// Extra small screen / phone
|
||||
//** Deprecated `$screen-xs` as of v3.0.1
|
||||
$screen-xs: 480px;
|
||||
//** Deprecated `$screen-xs-min` as of v3.2.0
|
||||
$screen-xs-min: $screen-xs;
|
||||
//** Deprecated `$screen-phone` as of v3.0.1
|
||||
$screen-phone: $screen-xs-min;
|
||||
|
||||
// Small screen / tablet
|
||||
//** Deprecated `$screen-sm` as of v3.0.1
|
||||
$screen-sm: 768px;
|
||||
$screen-sm-min: $screen-sm;
|
||||
//** Deprecated `$screen-tablet` as of v3.0.1
|
||||
$screen-tablet: $screen-sm-min;
|
||||
|
||||
// Medium screen / desktop
|
||||
//** Deprecated `$screen-md` as of v3.0.1
|
||||
$screen-md: 992px;
|
||||
$screen-md-min: $screen-md;
|
||||
//** Deprecated `$screen-desktop` as of v3.0.1
|
||||
$screen-desktop: $screen-md-min;
|
||||
|
||||
// Large screen / wide desktop
|
||||
//** Deprecated `$screen-lg` as of v3.0.1
|
||||
$screen-lg: 1200px;
|
||||
$screen-lg-min: $screen-lg;
|
||||
//** Deprecated `$screen-lg-desktop` as of v3.0.1
|
||||
$screen-lg-desktop: $screen-lg-min;
|
||||
|
||||
// So media queries don't overlap when required, provide a maximum
|
||||
$screen-xs-max: ($screen-sm-min - 1);
|
||||
$screen-sm-max: ($screen-md-min - 1);
|
||||
$screen-md-max: ($screen-lg-min - 1);
|
||||
|
||||
//== Grid system
|
||||
//
|
||||
//## Define your custom responsive grid.
|
||||
|
||||
//** Number of columns in the grid.
|
||||
$grid-columns: 12;
|
||||
//** Padding between columns. Gets divided in half for the left and right.
|
||||
$grid-gutter-width: ($baseWidth * 2);
|
||||
// Navbar collapse
|
||||
//** Point at which the navbar becomes uncollapsed.
|
||||
$grid-float-breakpoint: $screen-sm-min;
|
||||
//** Point at which the navbar begins collapsing.
|
||||
$grid-float-breakpoint-max: ($grid-float-breakpoint);
|
||||
|
||||
//== Container sizes
|
||||
//
|
||||
//## Define the maximum width of `.container` for different screen sizes.
|
||||
|
||||
// Small screen / tablet
|
||||
$container-tablet: (720px + $grid-gutter-width);
|
||||
//** For `$screen-sm-min` and up.
|
||||
$container-sm: $container-tablet;
|
||||
|
||||
// Medium screen / desktop
|
||||
$container-desktop: (940px + $grid-gutter-width);
|
||||
//** For `$screen-md-min` and up.
|
||||
$container-md: $container-desktop;
|
||||
|
||||
// Large screen / wide desktop
|
||||
$container-large-desktop: (1140px + $grid-gutter-width);
|
||||
//** For `$screen-lg-min` and up.
|
||||
$container-lg: $container-large-desktop;
|
||||
|
||||
//== Navbar
|
||||
//
|
||||
//##
|
||||
|
||||
// Basics of a navbar
|
||||
$navbar-height: 0px;
|
||||
$navbar-margin-bottom: $line-height-computed;
|
||||
$navbar-border-radius: $border-radius-base;
|
||||
$navbar-padding-horizontal: ($baseWidth * 2);
|
||||
$navbar-padding-vertical: 0;
|
||||
$navbar-collapse-max-height: 340px;
|
||||
|
||||
$navbar-default-color: $black;
|
||||
$navbar-default-bg: $grayLight;
|
||||
$navbar-default-border: $navbar-default-bg;
|
||||
|
||||
// Navbar links
|
||||
$navbar-default-link-color: $black;
|
||||
$navbar-default-link-hover-color: $white;
|
||||
$navbar-default-link-hover-bg: $black;
|
||||
$navbar-default-link-active-color: $white;
|
||||
$navbar-default-link-active-bg: $black;
|
||||
$navbar-default-link-disabled-color: $gray;
|
||||
$navbar-default-link-disabled-bg: transparent;
|
||||
|
||||
// Navbar brand label
|
||||
$navbar-default-brand-color: $navbar-default-link-color;
|
||||
$navbar-default-brand-hover-color: $navbar-default-brand-color;
|
||||
$navbar-default-brand-hover-bg: transparent;
|
||||
|
||||
// Navbar toggle
|
||||
$navbar-default-toggle-hover-bg: #ddd;
|
||||
$navbar-default-toggle-icon-bar-bg: #888;
|
||||
$navbar-default-toggle-border-color: #ddd;
|
||||
|
||||
// Inverted navbar
|
||||
// Reset inverted navbar basics
|
||||
$navbar-inverse-color: $gray;
|
||||
$navbar-inverse-bg: $black;
|
||||
$navbar-inverse-border: $navbar-inverse-bg;
|
||||
|
||||
// Inverted navbar links
|
||||
$navbar-inverse-link-color: $gray-light;
|
||||
$navbar-inverse-link-hover-color: $black;
|
||||
$navbar-inverse-link-hover-bg: $grayLight;
|
||||
$navbar-inverse-link-active-color: $white;
|
||||
$navbar-inverse-link-active-bg: $grayDark;
|
||||
$navbar-inverse-link-disabled-color: $gray;
|
||||
$navbar-inverse-link-disabled-bg: transparent;
|
||||
|
||||
// Inverted navbar brand label
|
||||
$navbar-inverse-brand-color: $navbar-inverse-link-color;
|
||||
$navbar-inverse-brand-hover-color: #fff;
|
||||
$navbar-inverse-brand-hover-bg: transparent;
|
||||
|
||||
// Inverted navbar toggle
|
||||
$navbar-inverse-toggle-hover-bg: $grayLight;
|
||||
$navbar-inverse-toggle-icon-bar-bg: #fff;
|
||||
$navbar-inverse-toggle-border-color: #333;
|
||||
|
||||
//== Navs
|
||||
//
|
||||
//##
|
||||
|
||||
//=== Shared nav styles
|
||||
$nav-link-padding: 0 $baseWidth;
|
||||
$nav-link-hover-bg: $gray-lighter;
|
||||
|
||||
$nav-disabled-link-color: $gray-light;
|
||||
$nav-disabled-link-hover-color: $gray-light;
|
||||
|
||||
//== Tabs
|
||||
$nav-tabs-border-color: #ddd;
|
||||
|
||||
$nav-tabs-link-hover-border-color: $gray-lighter;
|
||||
|
||||
$nav-tabs-active-link-hover-bg: $black;
|
||||
$nav-tabs-active-link-hover-color: $white;
|
||||
|
||||
$nav-tabs-justified-active-link-border-color: $body-bg;
|
||||
|
||||
//== Pills
|
||||
$nav-pills-border-radius: $border-radius-base;
|
||||
$nav-pills-active-link-hover-bg: $component-active-bg;
|
||||
$nav-pills-active-link-hover-color: $component-active-color;
|
||||
|
||||
//== Pagination
|
||||
//
|
||||
//##
|
||||
|
||||
$pagination-color: $black;
|
||||
$pagination-bg: $gray;
|
||||
$pagination-border: #ddd;
|
||||
|
||||
$pagination-hover-color: $link-hover-color;
|
||||
$pagination-hover-bg: $gray-lighter;
|
||||
$pagination-hover-border: #ddd;
|
||||
|
||||
$pagination-active-color: #fff;
|
||||
$pagination-active-bg: $brand-primary;
|
||||
$pagination-active-border: $brand-primary;
|
||||
|
||||
$pagination-disabled-color: $gray-light;
|
||||
$pagination-disabled-bg: #fff;
|
||||
$pagination-disabled-border: #ddd;
|
||||
|
||||
//== Pager
|
||||
//
|
||||
//##
|
||||
|
||||
$pager-bg: $pagination-bg;
|
||||
$pager-border: $pagination-border;
|
||||
$pager-border-radius: 0;
|
||||
|
||||
$pager-hover-bg: $pagination-hover-bg;
|
||||
|
||||
$pager-active-bg: $pagination-active-bg;
|
||||
$pager-active-color: $pagination-active-color;
|
||||
|
||||
$pager-disabled-color: $pagination-disabled-color;
|
||||
|
||||
//== Jumbotron
|
||||
//
|
||||
//##
|
||||
|
||||
$jumbotron-padding: ($ts) ($rhs + $baseWidth) ($bs) ($lhs + $baseWidth);
|
||||
$jumbotron-color: $white;
|
||||
$jumbotron-bg: transparent;
|
||||
$jumbotron-heading-color: inherit;
|
||||
$jumbotron-font-size: $font-size-base;
|
||||
|
||||
//== Form states and alerts
|
||||
//
|
||||
//## Define colors for form feedback states and, by default, alerts.
|
||||
|
||||
$state-success-text: $green;
|
||||
$state-success-bg: $greenDark;
|
||||
$state-success-border: $state-success-bg;
|
||||
|
||||
$state-info-text: $yellow;
|
||||
$state-info-bg: $brown;
|
||||
$state-info-border: $state-info-bg;
|
||||
|
||||
$state-warning-text: $magenta;
|
||||
$state-warning-bg: $magentaDark;
|
||||
$state-warning-border: $state-warning-bg;
|
||||
|
||||
$state-danger-text: $red;
|
||||
$state-danger-bg: $black;
|
||||
$state-danger-border: $state-danger-bg;
|
||||
|
||||
//== Tooltips
|
||||
//
|
||||
//##
|
||||
|
||||
//** Tooltip max width
|
||||
$tooltip-max-width: ($baseWidth * 25);
|
||||
//** Tooltip text color
|
||||
$tooltip-color: $white;
|
||||
//** Tooltip background color
|
||||
$tooltip-bg: $grayDark;
|
||||
$tooltip-opacity: 1;
|
||||
|
||||
//** Tooltip arrow width
|
||||
$tooltip-arrow-width: 0px;
|
||||
//** Tooltip arrow color
|
||||
$tooltip-arrow-color: $tooltip-bg;
|
||||
|
||||
//== Popovers
|
||||
//
|
||||
//##
|
||||
|
||||
//** Popover body background color
|
||||
$popover-bg: $gray;
|
||||
//** Popover maximum width
|
||||
$popover-max-width: ($baseWidth * 20);
|
||||
//** Popover border color
|
||||
$popover-border-color: rgb(0, 0, 0);
|
||||
//** Popover fallback border color
|
||||
$popover-fallback-border-color: #ccc;
|
||||
|
||||
//** Popover title background color
|
||||
$popover-title-bg: $greenDark;
|
||||
|
||||
//** Popover arrow width
|
||||
$popover-arrow-width: 10px;
|
||||
//** Popover arrow color
|
||||
$popover-arrow-color: $popover-bg;
|
||||
|
||||
//** Popover outer arrow width
|
||||
$popover-arrow-outer-width: ($popover-arrow-width + 1);
|
||||
//** Popover outer arrow color
|
||||
$popover-arrow-outer-color: $popover-border-color;
|
||||
//** Popover outer arrow fallback color
|
||||
$popover-arrow-outer-fallback-color: $popover-fallback-border-color;
|
||||
|
||||
//== Labels
|
||||
//
|
||||
//##
|
||||
|
||||
//** Default label background color
|
||||
$label-default-bg: $gray-light;
|
||||
//** Primary label background color
|
||||
$label-primary-bg: $brand-primary-bg;
|
||||
//** Success label background color
|
||||
$label-success-bg: $brand-success;
|
||||
//** Info label background color
|
||||
$label-info-bg: $brand-info;
|
||||
//** Warning label background color
|
||||
$label-warning-bg: $brand-warning;
|
||||
//** Danger label background color
|
||||
$label-danger-bg: $brand-danger;
|
||||
|
||||
//** Default label text color
|
||||
$label-color: #fff;
|
||||
//** Default text color of a linked label
|
||||
$label-link-hover-color: #fff;
|
||||
|
||||
//== Modals
|
||||
//
|
||||
//##
|
||||
|
||||
//** Padding applied to the modal body
|
||||
$modal-inner-padding: 0 $baseWidth;
|
||||
|
||||
//** Padding applied to the modal title
|
||||
$modal-title-padding: 0 $baseWidth;
|
||||
//** Modal title line-height
|
||||
$modal-title-line-height: $line-height-base;
|
||||
|
||||
//** Background color of modal content area
|
||||
$modal-content-bg: $gray;
|
||||
//** Modal content border color
|
||||
$modal-content-border-color: rgb(0, 0, 0);
|
||||
//** Modal content border color **for IE8**
|
||||
$modal-content-fallback-border-color: #999;
|
||||
|
||||
//** Modal backdrop background color
|
||||
$modal-backdrop-bg: #000;
|
||||
//** Modal backdrop opacity
|
||||
// $modal-backdrop-opacity: @include 5;
|
||||
//** Modal header border color
|
||||
$modal-header-border-color: #e5e5e5;
|
||||
//** Modal footer border color
|
||||
$modal-footer-border-color: $modal-header-border-color;
|
||||
|
||||
$modal-lg: 900px;
|
||||
$modal-md: 600px;
|
||||
$modal-sm: 300px;
|
||||
|
||||
//== Alerts
|
||||
//
|
||||
//## Define alert colors, border radius, and padding.
|
||||
|
||||
$alert-padding: $line-height-base ($baseWidth * 2);
|
||||
$alert-border-radius: $border-radius-base;
|
||||
$alert-link-font-weight: normal;
|
||||
|
||||
$alert-success-bg: $state-success-bg;
|
||||
$alert-success-text: $state-success-text;
|
||||
$alert-success-border: $state-success-border;
|
||||
|
||||
$alert-info-bg: $state-info-bg;
|
||||
$alert-info-text: $state-info-text;
|
||||
$alert-info-border: $state-info-border;
|
||||
|
||||
$alert-warning-bg: $state-warning-bg;
|
||||
$alert-warning-text: $state-warning-text;
|
||||
$alert-warning-border: $state-warning-border;
|
||||
|
||||
$alert-danger-bg: $state-danger-bg;
|
||||
$alert-danger-text: $state-danger-text;
|
||||
$alert-danger-border: $state-danger-border;
|
||||
|
||||
//== Progress bars
|
||||
//
|
||||
//##
|
||||
|
||||
//** Background color of the whole progress component
|
||||
$progress-bg: $black;
|
||||
//** Progress bar text color
|
||||
$progress-bar-color: $black;
|
||||
//** Variable for setting rounded corners on progress bar.
|
||||
$progress-border-radius: $border-radius-base;
|
||||
|
||||
//** Default progress bar color
|
||||
$progress-bar-bg: $brand-primary;
|
||||
//** Success progress bar color
|
||||
$progress-bar-success-bg: $brand-success;
|
||||
//** Warning progress bar color
|
||||
$progress-bar-warning-bg: $brand-warning;
|
||||
//** Danger progress bar color
|
||||
$progress-bar-danger-bg: $brand-danger;
|
||||
//** Info progress bar color
|
||||
$progress-bar-info-bg: $brand-info;
|
||||
|
||||
//== List group
|
||||
//
|
||||
//##
|
||||
|
||||
//** Background color on `.list-group-item`
|
||||
$list-group-bg: $gray;
|
||||
//** `.list-group-item` border color
|
||||
$list-group-border: #ddd;
|
||||
//** List group border radius
|
||||
$list-group-border-radius: $border-radius-base;
|
||||
|
||||
//** Background color of single list items on hover
|
||||
$list-group-hover-bg: $black;
|
||||
//** Text color of active list items
|
||||
$list-group-active-color: $component-active-color;
|
||||
//** Background color of active list items
|
||||
$list-group-active-bg: $component-active-bg;
|
||||
//** Border color of active list elements
|
||||
$list-group-active-border: $list-group-active-bg;
|
||||
//** Text color for content within active list items
|
||||
$list-group-active-text-color: $component-active-color;
|
||||
|
||||
//** Text color of disabled list items
|
||||
$list-group-disabled-color: $gray-dark;
|
||||
//** Background color of disabled list items
|
||||
$list-group-disabled-bg: $gray-lighter;
|
||||
//** Text color for content within disabled list items
|
||||
$list-group-disabled-text-color: $list-group-disabled-color;
|
||||
|
||||
$list-group-link-color: $black;
|
||||
$list-group-link-hover-color: $list-group-link-color;
|
||||
$list-group-link-heading-color: #333;
|
||||
|
||||
//== Panels
|
||||
//
|
||||
//##
|
||||
|
||||
$panel-bg: $gray;
|
||||
$panel-body-padding: 0 $rhsNB 0 $lhsNB;
|
||||
$panel-heading-padding: 0 $rhsNB 0 $lhsNB;
|
||||
$panel-footer-padding: $panel-heading-padding;
|
||||
$panel-border-radius: $border-radius-base;
|
||||
|
||||
//** Border color for elements within panels
|
||||
$panel-inner-border: #ddd;
|
||||
$panel-footer-bg: #f5f5f5;
|
||||
|
||||
$panel-default-text: $white;
|
||||
$panel-default-border: #ddd;
|
||||
$panel-default-heading-bg: $grayDark;
|
||||
|
||||
$panel-primary-text: $white;
|
||||
$panel-primary-border: $brand-primary;
|
||||
$panel-primary-heading-bg: $cyanDark;
|
||||
|
||||
$panel-success-text: $state-success-text;
|
||||
$panel-success-border: $state-success-border;
|
||||
$panel-success-heading-bg: $state-success-bg;
|
||||
|
||||
$panel-info-text: $state-info-text;
|
||||
$panel-info-border: $state-info-border;
|
||||
$panel-info-heading-bg: $state-info-bg;
|
||||
|
||||
$panel-warning-text: $state-warning-text;
|
||||
$panel-warning-border: $state-warning-border;
|
||||
$panel-warning-heading-bg: $state-warning-bg;
|
||||
|
||||
$panel-danger-text: $state-danger-text;
|
||||
$panel-danger-border: $state-danger-border;
|
||||
$panel-danger-heading-bg: $state-danger-bg;
|
||||
|
||||
//== Thumbnails
|
||||
//
|
||||
//##
|
||||
|
||||
//** Padding around the thumbnail image
|
||||
$thumbnail-padding: 4px;
|
||||
//** Thumbnail background color
|
||||
$thumbnail-bg: $body-bg;
|
||||
//** Thumbnail border color
|
||||
$thumbnail-border: #ddd;
|
||||
//** Thumbnail border radius
|
||||
$thumbnail-border-radius: $border-radius-base;
|
||||
|
||||
//** Custom text color for thumbnail captions
|
||||
$thumbnail-caption-color: $text-color;
|
||||
//** Padding around the thumbnail caption
|
||||
$thumbnail-caption-padding: 9px;
|
||||
|
||||
//== Wells
|
||||
//
|
||||
//##
|
||||
|
||||
$well-bg: $greenDark;
|
||||
$well-border: $well-bg;
|
||||
|
||||
//== Badges
|
||||
//
|
||||
//##
|
||||
|
||||
$badge-color: $black;
|
||||
//** Linked badge text color on hover
|
||||
$badge-link-hover-color: #fff;
|
||||
$badge-bg: $gray-light;
|
||||
|
||||
//** Badge text color in active nav link
|
||||
$badge-active-color: $link-color;
|
||||
//** Badge background color in active nav link
|
||||
$badge-active-bg: $black;
|
||||
|
||||
$badge-font-weight: normal;
|
||||
$badge-line-height: $line-height-base;
|
||||
$badge-border-radius: 0;
|
||||
|
||||
//== Breadcrumbs
|
||||
//
|
||||
//##
|
||||
|
||||
$breadcrumb-padding-vertical: 8px;
|
||||
$breadcrumb-padding-horizontal: 15px;
|
||||
//** Breadcrumb background color
|
||||
$breadcrumb-bg: #f5f5f5;
|
||||
//** Breadcrumb text color
|
||||
$breadcrumb-color: #ccc;
|
||||
//** Text color of current page in the breadcrumb
|
||||
$breadcrumb-active-color: $gray-light;
|
||||
//** Textual separator for between breadcrumb elements
|
||||
$breadcrumb-separator: "/";
|
||||
|
||||
//== Carousel
|
||||
//
|
||||
//##
|
||||
|
||||
$carousel-text-shadow: none;
|
||||
|
||||
$carousel-control-color: #fff;
|
||||
$carousel-control-width: 15%;
|
||||
$carousel-control-opacity: 1;
|
||||
$carousel-control-font-size: $font-size-base;
|
||||
|
||||
$carousel-indicator-active-bg: #fff;
|
||||
$carousel-indicator-border-color: #fff;
|
||||
|
||||
$carousel-caption-color: #fff;
|
||||
|
||||
//== Close
|
||||
//
|
||||
//##
|
||||
|
||||
$close-font-weight: normal;
|
||||
$close-color: #000;
|
||||
$close-text-shadow: none;
|
||||
|
||||
//== Code
|
||||
//
|
||||
//##
|
||||
|
||||
$code-color: #c7254e;
|
||||
$code-bg: #f9f2f4;
|
||||
|
||||
$kbd-color: #fff;
|
||||
$kbd-bg: #333;
|
||||
|
||||
$pre-bg: #f5f5f5;
|
||||
$pre-color: $gray-dark;
|
||||
$pre-border-color: #ccc;
|
||||
$pre-scrollable-max-height: 340px;
|
||||
|
||||
//== Type
|
||||
//
|
||||
//##
|
||||
|
||||
//** Horizontal offset for forms and lists.
|
||||
$component-offset-horizontal: 180px;
|
||||
//** Text muted color
|
||||
$text-muted: $gray-dark;
|
||||
//** Abbreviations and acronyms border color
|
||||
$abbr-border-color: $gray-light;
|
||||
//** Headings small color
|
||||
$headings-small-color: $gray-light;
|
||||
//** Blockquote small color
|
||||
$blockquote-small-color: $gray-light;
|
||||
//** Blockquote font size
|
||||
$blockquote-font-size: $font-size-base;
|
||||
//** Blockquote border color
|
||||
$blockquote-border-color: $gray-lighter;
|
||||
//** Page header border color
|
||||
$page-header-border-color: $gray-lighter;
|
||||
//** Width of horizontal description list titles
|
||||
$dl-horizontal-offset: $component-offset-horizontal;
|
||||
//** Horizontal line color.
|
||||
$hr-border: $black;
|
|
@ -1 +0,0 @@
|
|||
@import "variables.darkly";
|
|
@ -1,117 +0,0 @@
|
|||
@import "./variables";
|
||||
|
||||
// Colors
|
||||
$white: #f3f3f3;
|
||||
$gray-200: #ebebeb;
|
||||
$gray-300: #dee2e6;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-600: #666;
|
||||
$gray-700: #333;
|
||||
$gray-800: #202020;
|
||||
$gray-900: #111;
|
||||
$black: #000;
|
||||
|
||||
$blue: #375a7f;
|
||||
$red: #e74c3c;
|
||||
$yellow: #f39c12;
|
||||
$green: #00bc8c;
|
||||
$cyan: #3498db;
|
||||
|
||||
$primary: $green;
|
||||
$secondary: $gray-700;
|
||||
$success: $green;
|
||||
$dark: $gray-300;
|
||||
|
||||
$body-color: $gray-200;
|
||||
$body-bg: $black;
|
||||
$link-color: $success;
|
||||
$border-color: rgba($body-color, 0.25);
|
||||
$mark-bg: $gray-900;
|
||||
$text-muted: $gray-600;
|
||||
$yiq-contrasted-threshold: 175;
|
||||
|
||||
$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;
|
||||
|
||||
$card-cap-bg: $gray-900;
|
||||
$card-bg: $gray-900;
|
||||
$card-color: $gray-300;
|
||||
|
||||
$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);
|
||||
$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-900;
|
||||
$input-color: $white;
|
||||
$input-disabled-bg: darken($gray-900, 20%);
|
||||
$input-border-color: $gray-800;
|
||||
$input-group-addon-color: $gray-800;
|
||||
$input-group-addon-bg: $gray-800;
|
||||
|
||||
$hr-border-color: rgba($body-color, 0.25);
|
||||
|
||||
$table-border-color: $gray-700;
|
||||
|
||||
$custom-file-color: $gray-500;
|
||||
$custom-file-border-color: $body-bg;
|
||||
|
||||
$dropdown-bg: $gray-900;
|
||||
$dropdown-border-color: $gray-800;
|
||||
$dropdown-divider-bg: $gray-700;
|
||||
$dropdown-link-color: $white;
|
||||
$dropdown-link-hover-color: $white;
|
||||
$dropdown-link-hover-bg: $primary;
|
||||
|
||||
$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-900;
|
||||
$popover-bg: $gray-900;
|
||||
$popover-header-bg: $gray-900;
|
||||
$toast-background-color: $gray-800;
|
||||
$toast-header-background-color: $gray-900;
|
||||
$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;
|
||||
$custom-select-bg: $gray-700;
|
||||
$custom-select-color: $white;
|
||||
$light: $gray-900;
|
|
@ -1,7 +0,0 @@
|
|||
@import "variables.darkly";
|
||||
|
||||
$primary: $blue;
|
||||
$secondary: #444;
|
||||
$light: $gray-800;
|
||||
|
||||
$link-color: $red;
|
|
@ -1,44 +1,60 @@
|
|||
@import "./variables";
|
||||
|
||||
// Colors
|
||||
$white: #fff;
|
||||
$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: $green;
|
||||
$primary: $blue;
|
||||
$secondary: $gray-700;
|
||||
$success: $green;
|
||||
$info: $cyan;
|
||||
$warning: $yellow;
|
||||
$danger: $red;
|
||||
$dark: $gray-300;
|
||||
|
||||
$body-color: $gray-300;
|
||||
$body-bg: $gray-900;
|
||||
$link-color: $success;
|
||||
$border-color: rgba($body-color, 0.25);
|
||||
$mark-bg: #333;
|
||||
$text-muted: $gray-600;
|
||||
$yiq-contrasted-threshold: 175;
|
||||
|
||||
$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";
|
||||
$body-bg: $gray-900;
|
||||
$body-color: $gray-300;
|
||||
$link-color: $success;
|
||||
$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;
|
||||
|
||||
$card-cap-bg: $gray-700;
|
||||
$card-bg: $gray-800;
|
||||
|
||||
$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;
|
||||
|
@ -46,40 +62,6 @@ $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);
|
||||
$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-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-bg: $success;
|
||||
$pagination-border-width: 0;
|
||||
|
@ -92,8 +74,9 @@ $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;
|
||||
|
@ -109,6 +92,12 @@ $breadcrumb-bg: $gray-700;
|
|||
$close-color: $white;
|
||||
$close-text-shadow: none;
|
||||
$pre-color: inherit;
|
||||
$custom-select-bg: $gray-700;
|
||||
$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: $navbar-dark-active-color;
|
||||
$navbar-light-brand-hover-color: $navbar-dark-active-color;
|
||||
|
|
|
@ -1,14 +1,3 @@
|
|||
@import "./variables";
|
||||
|
||||
// Colors
|
||||
$white: #fff;
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-200: #ebebeb;
|
||||
$gray-300: #bbb;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-800: #303030;
|
||||
$gray-900: #222;
|
||||
|
||||
$blue: #5555ff;
|
||||
$cyan: #55ffff;
|
||||
$green: #55ff55;
|
||||
|
@ -18,41 +7,33 @@ $yellow: #fefe54;
|
|||
$orange: #a85400;
|
||||
$pink: #fe54fe;
|
||||
$purple: #fe5454;
|
||||
|
||||
$primary: #fefe54;
|
||||
$secondary: $gray-900;
|
||||
$body-bg: #000084;
|
||||
$gray-300: #bbb;
|
||||
$body-color: $gray-300;
|
||||
$link-hover-color: $white;
|
||||
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
$navbar-dark-color: $gray-300;
|
||||
$navbar-light-brand-color: $gray-300;
|
||||
$success: #00aa00;
|
||||
$danger: #aa0000;
|
||||
$info: #00aaaa;
|
||||
$warning: #aa00aa;
|
||||
$light: $gray-800;
|
||||
$dark: black;
|
||||
|
||||
$body-bg: #000084;
|
||||
$body-color: $gray-300;
|
||||
|
||||
$link-hover-color: $white;
|
||||
|
||||
$font-family-sans-serif: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
$font-family-monospace: DOS, Monaco, Menlo, Consolas, "Courier New", monospace;
|
||||
|
||||
$navbar-dark-color: $gray-300;
|
||||
$navbar-light-brand-color: $gray-300;
|
||||
$navbar-dark-active-color: $gray-100;
|
||||
$enable-rounded: false;
|
||||
$input-color: $white;
|
||||
$input-bg: rgb(102, 102, 102);
|
||||
$input-disabled-bg: $gray-800;
|
||||
$nav-tabs-link-active-color: $gray-100;
|
||||
$navbar-dark-hover-color: rgba($gray-300, 0.75);
|
||||
$light: $gray-800;
|
||||
$navbar-light-disabled-color: $gray-800;
|
||||
$navbar-light-active-color: $gray-100;
|
||||
$navbar-light-hover-color: $gray-200;
|
||||
$navbar-light-color: $gray-300;
|
||||
|
||||
$enable-rounded: false;
|
||||
|
||||
$input-color: $white;
|
||||
$input-bg: rgb(102, 102, 102);
|
||||
$input-placeholder-color: $gray-500;
|
||||
$input-disabled-bg: $gray-800;
|
||||
|
||||
$card-bg: $gray-800;
|
||||
$card-border-color: $white;
|
||||
$input-placeholder-color: $gray-500;
|
||||
$mark-bg: #463b00;
|
||||
$secondary: $gray-900;
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
@import "variables.litely";
|
|
@ -1,4 +0,0 @@
|
|||
@import "variables.litely";
|
||||
|
||||
$secondary: #c80000;
|
||||
$danger: darken($primary, 24%);
|
|
@ -1,52 +1,32 @@
|
|||
@import "./variables";
|
||||
|
||||
// Colors
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-600: #6c757d;
|
||||
$gray-700: #495057;
|
||||
$gray-800: #343a40;
|
||||
$gray-900: #212529;
|
||||
$black: #222;
|
||||
|
||||
$blue: #007bff;
|
||||
$indigo: #6610f2;
|
||||
$red: #d8486a;
|
||||
$white: #ffffff;
|
||||
$orange: #f1641e;
|
||||
$green: #00a846;
|
||||
$cyan: #02bdc2;
|
||||
|
||||
$primary: $orange;
|
||||
$green: #00c853;
|
||||
$secondary: $green;
|
||||
$success: $indigo;
|
||||
$info: $blue;
|
||||
$danger: darken($primary, 25%);
|
||||
|
||||
$body-color: $gray-700;
|
||||
$body-bg: #fff;
|
||||
$link-color: $primary;
|
||||
$border-color: rgba($body-color, 0.25);
|
||||
$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);
|
||||
|
||||
$link-color: theme-color("primary");
|
||||
$primary: $orange;
|
||||
$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($component-active-bg, 0.75);
|
||||
$form-feedback-valid-color: theme-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;
|
||||
|
||||
$hr-border-color: rgba($body-color, 0.25);
|
||||
|
|
|
@ -1,3 +0,0 @@
|
|||
$link-decoration: none;
|
||||
$min-contrast-ratio: 3;
|
||||
$font-size-base: 0.875rem;
|
37
src/assets/css/themes/_variables.vaporwave-dark.scss
Normal file
37
src/assets/css/themes/_variables.vaporwave-dark.scss
Normal file
|
@ -0,0 +1,37 @@
|
|||
$blue: #01cdfe;
|
||||
$indigo: #b967ff;
|
||||
$purple: #b967ff;
|
||||
$pink: rgb(255, 64, 186);
|
||||
$red: rgb(255, 95, 110);
|
||||
$orange: rgb(255, 167, 93);
|
||||
$yellow: #fffb96;
|
||||
$green: #05ffa1;
|
||||
$teal: #01cdfe;
|
||||
$cyan: #01cdfe;
|
||||
$enable-shadows: true;
|
||||
$enable-gradients: true;
|
||||
$enable-responsive-font-sizes: true;
|
||||
$body-bg: $gray-900;
|
||||
$body-color: $gray-200;
|
||||
$border-radius: 1rem;
|
||||
$border-radius-lg: 1rem;
|
||||
$font-family-monospace: Arial, "Noto Sans", sans-serif;
|
||||
$yiq-text-light: $gray-300;
|
||||
$secondary: $blue;
|
||||
$text-muted: $gray-500;
|
||||
$primary: $pink;
|
||||
$navbar-light-hover-color: rgba($primary, 0.7);
|
||||
$light: darken($gray-100, 1.5);
|
||||
$font-family-sans-serif: "Lucida Console", Monaco, monospace;
|
||||
$card-bg: $body-bg;
|
||||
$navbar-dark-color: rgba($body-bg, 0.5);
|
||||
$navbar-light-active-color: rgba($gray-200, 0.9);
|
||||
$navbar-light-disabled-color: rgba($gray-200, 0.3);
|
||||
$navbar-light-color: rgba($white, 0.5);
|
||||
$input-bg: $gray-700;
|
||||
$input-color: $gray-200;
|
||||
$input-disabled-bg: $gray-800;
|
||||
$input-border-color: $gray-800;
|
||||
$mark-bg: $gray-600;
|
||||
$pre-color: $gray-200;
|
||||
mark-bg: $gray-600;
|
25
src/assets/css/themes/_variables.vaporwave-light.scss
Normal file
25
src/assets/css/themes/_variables.vaporwave-light.scss
Normal file
|
@ -0,0 +1,25 @@
|
|||
$blue: #01cdfe;
|
||||
$indigo: #b967ff;
|
||||
$purple: #b967ff;
|
||||
$pink: rgb(255, 64, 186);
|
||||
$red: rgb(255, 95, 110);
|
||||
$orange: rgb(255, 167, 93);
|
||||
$yellow: #fffb96;
|
||||
$green: #05ffa1;
|
||||
$teal: #01cdfe;
|
||||
$cyan: #01cdfe;
|
||||
$enable-shadows: true;
|
||||
$enable-gradients: true;
|
||||
$enable-responsive-font-sizes: true;
|
||||
$body-bg: $gray-100;
|
||||
$body-color: $gray-700;
|
||||
$border-radius: 1rem;
|
||||
$border-radius-lg: 1rem;
|
||||
$font-family-monospace: Arial, "Noto Sans", sans-serif;
|
||||
$yiq-text-light: $gray-300;
|
||||
$secondary: $blue;
|
||||
$text-muted: $gray-500;
|
||||
$primary: $pink;
|
||||
$navbar-light-hover-color: rgba($primary, 0.7);
|
||||
$light: darken($gray-100, 1.5);
|
||||
$font-family-sans-serif: "Lucida Console", Monaco, monospace;
|
83
src/assets/css/themes/cyborg.min.css
vendored
Normal file
83
src/assets/css/themes/cyborg.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -1,59 +0,0 @@
|
|||
@import "variables.darkly-compact";
|
||||
|
||||
/*
|
||||
GENERAL
|
||||
*/
|
||||
|
||||
// Desktop Breakpoint
|
||||
$container-max-widths: (
|
||||
lg: 1920px,
|
||||
);
|
||||
|
||||
// Reduce hr height
|
||||
hr.my-3 {
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/*
|
||||
POST-LISTING
|
||||
*/
|
||||
|
||||
.post-listing {
|
||||
line-height: 1;
|
||||
|
||||
.post-title h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post-title + p {
|
||||
padding-top: 0.125rem !important;
|
||||
padding-bottom: 0.125rem !important;
|
||||
}
|
||||
|
||||
.community-link {
|
||||
padding-left: 0.125rem;
|
||||
}
|
||||
|
||||
.person-listing {
|
||||
padding-right: 0.125rem;
|
||||
}
|
||||
|
||||
ul.list-inline {
|
||||
&.mt-2 {
|
||||
margin-top: 0.125rem !important;
|
||||
}
|
||||
&.mb-1 {
|
||||
margin-bottom: 0.125rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
--bs-btn-padding-y: 0;
|
||||
}
|
||||
.img-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
@import "variables.darkly-pureblack";
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
@import "variables.darkly-red";
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
File diff suppressed because it is too large
Load diff
1
src/assets/css/themes/darkly.min.css
vendored
Normal file
1
src/assets/css/themes/darkly.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
@import "variables.darkly";
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
File diff suppressed because it is too large
Load diff
1
src/assets/css/themes/i386.min.css
vendored
Normal file
1
src/assets/css/themes/i386.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,16 +0,0 @@
|
|||
@import "variables.i386";
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
||||
|
||||
.btn-outline-secondary {
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
.dropdown-item.active,
|
||||
.dropdown-item:hover,
|
||||
option:disabled {
|
||||
color: $secondary;
|
||||
}
|
||||
|
||||
.input-group-text {
|
||||
background: $gray-500;
|
||||
}
|
12
src/assets/css/themes/journal.min.css
vendored
Normal file
12
src/assets/css/themes/journal.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load diff
|
@ -1,59 +0,0 @@
|
|||
@import "variables.litely-compact";
|
||||
|
||||
/*
|
||||
GENERAL
|
||||
*/
|
||||
|
||||
// Desktop Breakpoint
|
||||
$container-max-widths: (
|
||||
lg: 1920px,
|
||||
);
|
||||
|
||||
// Reduce hr height
|
||||
hr.my-3 {
|
||||
margin-top: 0.5rem !important;
|
||||
margin-bottom: 0.5rem !important;
|
||||
}
|
||||
|
||||
/*
|
||||
POST-LISTING
|
||||
*/
|
||||
|
||||
.post-listing {
|
||||
line-height: 1;
|
||||
|
||||
.post-title h5 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.post-title + p {
|
||||
padding-top: 0.125rem !important;
|
||||
padding-bottom: 0.125rem !important;
|
||||
}
|
||||
|
||||
.community-link {
|
||||
padding-left: 0.125rem;
|
||||
}
|
||||
|
||||
.person-listing {
|
||||
padding-right: 0.125rem;
|
||||
}
|
||||
|
||||
ul.list-inline {
|
||||
&.mt-2 {
|
||||
margin-top: 0.125rem !important;
|
||||
}
|
||||
&.mb-1 {
|
||||
margin-bottom: 0.125rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
--bs-btn-padding-y: 0;
|
||||
}
|
||||
.img-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
File diff suppressed because it is too large
Load diff
|
@ -1,2 +0,0 @@
|
|||
@import "variables.litely-red";
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
File diff suppressed because it is too large
Load diff
1
src/assets/css/themes/litely.min.css
vendored
Normal file
1
src/assets/css/themes/litely.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
@ -1,2 +0,0 @@
|
|||
@import "variables.litely";
|
||||
@import "../../../../node_modules/bootstrap/scss/bootstrap";
|
12
src/assets/css/themes/litera.min.css
vendored
Normal file
12
src/assets/css/themes/litera.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
src/assets/css/themes/materia.min.css
vendored
Normal file
12
src/assets/css/themes/materia.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
src/assets/css/themes/minty.min.css
vendored
Normal file
12
src/assets/css/themes/minty.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
src/assets/css/themes/nord.min.css
vendored
Normal file
12
src/assets/css/themes/nord.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
src/assets/css/themes/sketchy.min.css
vendored
Normal file
12
src/assets/css/themes/sketchy.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
68
src/assets/css/themes/solar.min.css
vendored
Normal file
68
src/assets/css/themes/solar.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
12
src/assets/css/themes/united.min.css
vendored
Normal file
12
src/assets/css/themes/united.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/assets/css/themes/vaporwave-dark.min.css
vendored
Normal file
1
src/assets/css/themes/vaporwave-dark.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
src/assets/css/themes/vaporwave.min.css
vendored
Normal file
1
src/assets/css/themes/vaporwave.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
49
src/assets/manifest.webmanifest
Normal file
49
src/assets/manifest.webmanifest
Normal file
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"name": "Lemmy",
|
||||
"description": "A link aggregator for the fediverse",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#222222",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/static/assets/icons/icon-72x72.png",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-96x96.png",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-128x128.png",
|
||||
"sizes": "128x128",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-144x144.png",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-152x152.png",
|
||||
"sizes": "152x152",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-384x384.png",
|
||||
"sizes": "384x384",
|
||||
"type": "image/png"
|
||||
},
|
||||
{
|
||||
"src": "/static/assets/icons/icon-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png"
|
||||
}
|
||||
]
|
||||
}
|
File diff suppressed because one or more lines are too long
Before Width: | Height: | Size: 53 KiB |
|
@ -1,28 +1,15 @@
|
|||
import { initializeSite, setupDateFns } from "@utils/app";
|
||||
import { hydrate } from "inferno-hydrate";
|
||||
import { Router } from "inferno-router";
|
||||
import { BrowserRouter } from "inferno-router";
|
||||
import { App } from "../shared/components/app/app";
|
||||
import { HistoryService } from "../shared/services";
|
||||
import { initializeSite } from "../shared/utils";
|
||||
|
||||
import "bootstrap/js/dist/collapse";
|
||||
import "bootstrap/js/dist/dropdown";
|
||||
const site = window.isoData.site_res;
|
||||
initializeSite(site);
|
||||
|
||||
async function startClient() {
|
||||
initializeSite(window.isoData.site_res);
|
||||
const wrapper = (
|
||||
<BrowserRouter>
|
||||
<App siteRes={window.isoData.site_res} />
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
await setupDateFns();
|
||||
|
||||
const wrapper = (
|
||||
<Router history={HistoryService.history}>
|
||||
<App />
|
||||
</Router>
|
||||
);
|
||||
|
||||
const root = document.getElementById("root");
|
||||
|
||||
if (root) {
|
||||
hydrate(wrapper, root);
|
||||
}
|
||||
}
|
||||
|
||||
startClient();
|
||||
hydrate(wrapper, document.getElementById("root"));
|
||||
|
|
|
@ -1,133 +0,0 @@
|
|||
import { initializeSite, isAuthPath } from "@utils/app";
|
||||
import { getHttpBaseInternal } from "@utils/env";
|
||||
import { ErrorPageData } from "@utils/types";
|
||||
import * as cookie from "cookie";
|
||||
import fetch from "cross-fetch";
|
||||
import type { Request, Response } from "express";
|
||||
import { StaticRouter, matchPath } from "inferno-router";
|
||||
import { renderToString } from "inferno-server";
|
||||
import { GetSite, GetSiteResponse, LemmyHttp } from "lemmy-js-client";
|
||||
import { App } from "../../shared/components/app/app";
|
||||
import {
|
||||
InitialFetchRequest,
|
||||
IsoDataOptionalSite,
|
||||
RouteData,
|
||||
} from "../../shared/interfaces";
|
||||
import { routes } from "../../shared/routes";
|
||||
import {
|
||||
FailedRequestState,
|
||||
wrapClient,
|
||||
} from "../../shared/services/HttpService";
|
||||
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 = req.headers.cookie
|
||||
? cookie.parse(req.headers.cookie).jwt
|
||||
: undefined;
|
||||
|
||||
const getSiteForm: GetSite = { auth };
|
||||
|
||||
const headers = setForwardedHeaders(req.headers);
|
||||
|
||||
const client = wrapClient(
|
||||
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, 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);
|
||||
}
|
||||
|
||||
if (!activeRoute) {
|
||||
res.status(404);
|
||||
}
|
||||
} else if (try_site.state === "failed") {
|
||||
res.status(500);
|
||||
errorPageData = getErrorPageData(new Error(try_site.msg), site);
|
||||
}
|
||||
|
||||
const error = Object.values(routeData).find(
|
||||
res => res.state === "failed" && res.msg !== "couldnt_find_object" // TODO: find a better way of handling errors
|
||||
) 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 {
|
||||
res.status(500);
|
||||
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"
|
||||
);
|
||||
}
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
import { getHttpBaseExternal, getHttpBaseInternal } from "@utils/env";
|
||||
import fetch from "cross-fetch";
|
||||
import type { Request, Response } from "express";
|
||||
import { LemmyHttp } from "lemmy-js-client";
|
||||
import { wrapClient } from "../../shared/services/HttpService";
|
||||
import generateManifestJson from "../utils/generate-manifest-json";
|
||||
import { setForwardedHeaders } from "../utils/set-forwarded-headers";
|
||||
|
||||
let manifest: Awaited<ReturnType<typeof generateManifestJson>> | undefined =
|
||||
undefined;
|
||||
|
||||
export default async (req: Request, res: Response) => {
|
||||
if (!manifest || manifest.start_url !== getHttpBaseExternal()) {
|
||||
const headers = setForwardedHeaders(req.headers);
|
||||
const client = wrapClient(
|
||||
new LemmyHttp(getHttpBaseInternal(), { fetchFunction: fetch, headers })
|
||||
);
|
||||
const site = await client.getSite({});
|
||||
|
||||
if (site.state === "success") {
|
||||
manifest = await generateManifestJson(site.data);
|
||||
} else {
|
||||
res.sendStatus(500);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader("content-type", "application/manifest+json");
|
||||
|
||||
res.send(manifest);
|
||||
};
|
|
@ -1,19 +0,0 @@
|
|||
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: /login_reset
|
||||
Disallow: /settings
|
||||
Disallow: /create_community
|
||||
Disallow: /create_post
|
||||
Disallow: /create_private_message
|
||||
Disallow: /inbox
|
||||
Disallow: /setup
|
||||
Disallow: /admin
|
||||
Disallow: /password_change
|
||||
Disallow: /search/
|
||||
`);
|
||||
};
|
|
@ -1,17 +0,0 @@
|
|||
import type { Response } from "express";
|
||||
|
||||
export default async ({ res }: { res: Response }) => {
|
||||
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||
|
||||
res.send(
|
||||
`Contact: mailto:security@lemmy.ml
|
||||
Contact: mailto:admin@` +
|
||||
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
|
||||
`
|
||||
Contact: mailto:security@` +
|
||||
process.env.LEMMY_UI_LEMMY_EXTERNAL_HOST +
|
||||
`
|
||||
Expires: 2024-01-01T04:59:00.000Z
|
||||
`
|
||||
);
|
||||
};
|
|
@ -1,14 +0,0 @@
|
|||
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`
|
||||
)
|
||||
);
|
||||
};
|
|
@ -1,31 +0,0 @@
|
|||
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")) {
|
||||
return res.status(400).send("Theme must be a css file");
|
||||
}
|
||||
|
||||
const customTheme = path.resolve(extraThemesFolder, theme);
|
||||
|
||||
if (existsSync(customTheme)) {
|
||||
return 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)) {
|
||||
return res.sendFile(internalTheme);
|
||||
} else {
|
||||
return res.sendFile(path.resolve("./dist/assets/css/themes/litely.css"));
|
||||
}
|
||||
}
|
||||
};
|
|
@ -1,6 +0,0 @@
|
|||
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,51 +1,205 @@
|
|||
import { setupDateFns } from "@utils/app";
|
||||
import { getStaticDir } from "@utils/env";
|
||||
import express from "express";
|
||||
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 process from "process";
|
||||
import CatchAllHandler from "./handlers/catch-all-handler";
|
||||
import ManifestHandler from "./handlers/manifest-handler";
|
||||
import RobotsHandler from "./handlers/robots-handler";
|
||||
import SecurityHandler from "./handlers/security-handler";
|
||||
import ServiceWorkerHandler from "./handlers/service-worker-handler";
|
||||
import ThemeHandler from "./handlers/theme-handler";
|
||||
import ThemesListHandler from "./handlers/themes-list-handler";
|
||||
import { setCacheControl, setDefaultCsp } from "./middleware";
|
||||
import serialize from "serialize-javascript";
|
||||
import { App } from "../shared/components/app/app";
|
||||
import { SYMBOLS } from "../shared/components/common/symbols";
|
||||
import { httpBaseInternal } from "../shared/env";
|
||||
import {
|
||||
ILemmyConfig,
|
||||
InitialFetchRequest,
|
||||
IsoData,
|
||||
} from "../shared/interfaces";
|
||||
import { routes } from "../shared/routes";
|
||||
import { initializeSite, setOptionalAuth } from "../shared/utils";
|
||||
|
||||
const server = express();
|
||||
|
||||
const [hostname, port] = process.env["LEMMY_UI_HOST"]
|
||||
? process.env["LEMMY_UI_HOST"].split(":")
|
||||
: ["0.0.0.0", "1234"];
|
||||
|
||||
server.use(express.json());
|
||||
server.use(express.urlencoded({ extended: false }));
|
||||
server.use(
|
||||
getStaticDir(),
|
||||
express.static(path.resolve("./dist"), {
|
||||
maxAge: 24 * 60 * 60 * 1000, // 1 day
|
||||
immutable: true,
|
||||
})
|
||||
);
|
||||
server.use(setCacheControl);
|
||||
server.use("/static", express.static(path.resolve("./dist")));
|
||||
|
||||
if (!process.env["LEMMY_UI_DISABLE_CSP"] && !process.env["LEMMY_UI_DEBUG"]) {
|
||||
server.use(setDefaultCsp);
|
||||
}
|
||||
const robotstxt = `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/
|
||||
`;
|
||||
|
||||
server.get("/.well-known/security.txt", SecurityHandler);
|
||||
server.get("/robots.txt", RobotsHandler);
|
||||
server.get("/service-worker.js", ServiceWorkerHandler);
|
||||
server.get("/manifest.webmanifest", ManifestHandler);
|
||||
server.get("/css/themes/:name", ThemeHandler);
|
||||
server.get("/css/themelist", ThemesListHandler);
|
||||
server.get("/*", CatchAllHandler);
|
||||
server.get("/robots.txt", async (_req, res) => {
|
||||
res.setHeader("content-type", "text/plain; charset=utf-8");
|
||||
res.send(robotstxt);
|
||||
});
|
||||
|
||||
// server.use(cookieParser());
|
||||
server.get("/*", async (req, res) => {
|
||||
try {
|
||||
const activeRoute = routes.find(route => matchPath(req.path, route)) || {};
|
||||
const context = {} as any;
|
||||
let auth: string = IsomorphicCookie.load("jwt", req);
|
||||
|
||||
let getSiteForm: GetSite = {};
|
||||
setOptionalAuth(getSiteForm, auth);
|
||||
|
||||
let promises: Promise<any>[] = [];
|
||||
|
||||
let headers = setForwardedHeaders(req.headers);
|
||||
|
||||
let initialFetchReq: InitialFetchRequest = {
|
||||
client: new LemmyHttp(httpBaseInternal, headers),
|
||||
auth,
|
||||
path: req.path,
|
||||
};
|
||||
|
||||
// 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 try_site: any = await initialFetchReq.client.getSite(getSiteForm);
|
||||
if (try_site.error == "not_logged_in") {
|
||||
console.error(
|
||||
"Incorrect JWT token, skipping auth so frontend can remove jwt cookie"
|
||||
);
|
||||
delete getSiteForm.auth;
|
||||
delete initialFetchReq.auth;
|
||||
try_site = await initialFetchReq.client.getSite(getSiteForm);
|
||||
}
|
||||
let site: GetSiteResponse = try_site;
|
||||
initializeSite(site);
|
||||
|
||||
if (activeRoute.fetchInitialData) {
|
||||
promises.push(...activeRoute.fetchInitialData(initialFetchReq));
|
||||
}
|
||||
|
||||
let routeData = await Promise.all(promises);
|
||||
|
||||
// Redirect to the 404 if there's an API error
|
||||
if (routeData[0] && routeData[0].error) {
|
||||
let errCode = routeData[0].error;
|
||||
console.error(errCode);
|
||||
return res.redirect(`/404?err=${errCode}`);
|
||||
}
|
||||
|
||||
let isoData: IsoData = {
|
||||
path: req.path,
|
||||
site_res: site,
|
||||
routeData,
|
||||
};
|
||||
|
||||
const wrapper = (
|
||||
<StaticRouter location={req.url} context={isoData}>
|
||||
<App siteRes={isoData.site_res} />
|
||||
</StaticRouter>
|
||||
);
|
||||
if (context.url) {
|
||||
return res.redirect(context.url);
|
||||
}
|
||||
|
||||
const cspHtml = (
|
||||
<meta
|
||||
http-equiv="Content-Security-Policy"
|
||||
content="default-src data: 'self'; connect-src * ws: wss:; frame-src *; img-src * data:; script-src 'self'; style-src 'self' 'unsafe-inline'; manifest-src 'self'"
|
||||
/>
|
||||
);
|
||||
|
||||
const root = renderToString(wrapper);
|
||||
const symbols = renderToString(SYMBOLS);
|
||||
const cspStr = process.env.LEMMY_EXTERNAL_HOST
|
||||
? renderToString(cspHtml)
|
||||
: "";
|
||||
const helmet = Helmet.renderStatic();
|
||||
|
||||
const config: ILemmyConfig = { wsHost: process.env.LEMMY_WS_HOST };
|
||||
|
||||
res.send(`
|
||||
<!DOCTYPE html>
|
||||
<html ${helmet.htmlAttributes.toString()} lang="en">
|
||||
<head>
|
||||
<script>window.isoData = ${serialize(isoData)}</script>
|
||||
<script>window.lemmyConfig = ${serialize(config)}</script>
|
||||
|
||||
<!-- A remote debugging utility for mobile
|
||||
<script src="//cdn.jsdelivr.net/npm/eruda"></script>
|
||||
<script>eruda.init();</script>
|
||||
-->
|
||||
|
||||
${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">
|
||||
|
||||
<!-- Content Security Policy -->
|
||||
${cspStr}
|
||||
|
||||
<!-- Web app manifest -->
|
||||
<link rel="manifest" href="/static/assets/manifest.webmanifest">
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="/static/styles/styles.css" />
|
||||
|
||||
<!-- Current theme and more -->
|
||||
${helmet.link.toString()}
|
||||
|
||||
<!-- Icons -->
|
||||
${symbols}
|
||||
|
||||
</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>
|
||||
`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return res.redirect(`/404?err=${err}`);
|
||||
}
|
||||
});
|
||||
|
||||
server.listen(Number(port), hostname, () => {
|
||||
setupDateFns();
|
||||
console.log(`http://${hostname}:${port}`);
|
||||
});
|
||||
|
||||
function setForwardedHeaders(headers: IncomingHttpHeaders): {
|
||||
[key: string]: string;
|
||||
} {
|
||||
let out = {
|
||||
host: headers.host,
|
||||
};
|
||||
if (headers["x-real-ip"]) {
|
||||
out["x-real-ip"] = headers["x-real-ip"];
|
||||
}
|
||||
if (headers["x-forwarded-for"]) {
|
||||
out["x-forwarded-for"] = headers["x-forwarded-for"];
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
process.on("SIGINT", () => {
|
||||
console.info("Interrupted");
|
||||
process.exit(0);
|
||||
|
|
|
@ -1,53 +0,0 @@
|
|||
import type { NextFunction, Request, Response } from "express";
|
||||
import { hasJwtCookie } from "./utils/has-jwt-cookie";
|
||||
|
||||
export function setDefaultCsp({
|
||||
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 * data:`
|
||||
);
|
||||
|
||||
next();
|
||||
}
|
||||
|
||||
// Set cache-control headers. If user is logged in, set `private` to prevent storing data in
|
||||
// shared caches (eg nginx) and leaking of private data. If user is not logged in, allow caching
|
||||
// all responses for 5 seconds to reduce load on backend and database. The specific cache
|
||||
// interval is rather arbitrary and could be set higher (less server load) or lower (fresher data).
|
||||
//
|
||||
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
|
||||
export function setCacheControl(
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction
|
||||
) {
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
return next();
|
||||
}
|
||||
|
||||
let caching: string;
|
||||
|
||||
if (
|
||||
req.path.match(/\.(js|css|txt|manifest\.webmanifest)\/?$/) ||
|
||||
req.path.includes("/css/themelist")
|
||||
) {
|
||||
// Static content gets cached publicly for a day
|
||||
caching = "public, max-age=86400";
|
||||
} else {
|
||||
if (hasJwtCookie(req)) {
|
||||
caching = "private";
|
||||
} else {
|
||||
caching = "public, max-age=5";
|
||||
}
|
||||
}
|
||||
|
||||
res.setHeader("Cache-Control", caching);
|
||||
|
||||
next();
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
import { existsSync } from "fs";
|
||||
import { readdir } from "fs/promises";
|
||||
|
||||
const extraThemesFolder =
|
||||
process.env["LEMMY_UI_EXTRA_THEMES_FOLDER"] || "./extra_themes";
|
||||
|
||||
const themes: ReadonlyArray<string> = [
|
||||
"darkly",
|
||||
"darkly-red",
|
||||
"darkly-compact",
|
||||
"darkly-pureblack",
|
||||
"litely",
|
||||
"litely-red",
|
||||
"litely-compact",
|
||||
"i386",
|
||||
];
|
||||
|
||||
export async function buildThemeList(): Promise<ReadonlyArray<string>> {
|
||||
if (existsSync(extraThemesFolder)) {
|
||||
const dirThemes = await readdir(extraThemesFolder);
|
||||
const cssThemes = dirThemes
|
||||
.filter(d => d.endsWith(".css"))
|
||||
.map(d => d.replace(".css", ""));
|
||||
return themes.concat(cssThemes);
|
||||
}
|
||||
return themes;
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
import { getStaticDir } from "@utils/env";
|
||||
import { Helmet } from "inferno-helmet";
|
||||
import { renderToString } from "inferno-server";
|
||||
import serialize from "serialize-javascript";
|
||||
import sharp from "sharp";
|
||||
import { favIconPngUrl, favIconUrl } from "../../shared/config";
|
||||
import { ILemmyConfig, IsoDataOptionalSite } from "../../shared/interfaces";
|
||||
import { buildThemeList } from "./build-themes-list";
|
||||
import { fetchIconPng } from "./fetch-icon-png";
|
||||
|
||||
const customHtmlHeader = process.env["LEMMY_UI_CUSTOM_HTML_HEADER"] || "";
|
||||
|
||||
let appleTouchIcon: string | undefined = undefined;
|
||||
|
||||
export async function createSsrHtml(
|
||||
root: string,
|
||||
isoData: IsoDataOptionalSite
|
||||
) {
|
||||
const site = isoData.site_res;
|
||||
|
||||
const fallbackTheme = `<link rel="stylesheet" type="text/css" href="/css/themes/${
|
||||
(await buildThemeList())[0]
|
||||
}.css" />`;
|
||||
|
||||
if (!appleTouchIcon) {
|
||||
appleTouchIcon = site?.site_view.site.icon
|
||||
? `data:image/png;base64,${await 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">
|
||||
<link
|
||||
id="favicon"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
href=${site?.site_view.site.icon ?? favIconUrl}
|
||||
/>
|
||||
|
||||
<!-- Web app manifest -->
|
||||
<link rel="manifest" href="/manifest.webmanifest" />
|
||||
<link rel="apple-touch-icon" href=${appleTouchIcon} />
|
||||
<link rel="apple-touch-startup-image" href=${appleTouchIcon} />
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" type="text/css" href="${getStaticDir()}/styles/styles.css" />
|
||||
|
||||
<!-- Current theme and more -->
|
||||
${helmet.link.toString() || fallbackTheme}
|
||||
|
||||
</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='${getStaticDir()}/js/client.js'></script>
|
||||
</body>
|
||||
</html>
|
||||
`;
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
import fetch from "cross-fetch";
|
||||
|
||||
export async function fetchIconPng(iconUrl: string) {
|
||||
return await fetch(iconUrl)
|
||||
.then(res => res.blob())
|
||||
.then(blob => blob.arrayBuffer());
|
||||
}
|
|
@ -1,98 +0,0 @@
|
|||
import { getHttpBaseExternal } from "@utils/env";
|
||||
import { readFile } from "fs/promises";
|
||||
import { GetSiteResponse } from "lemmy-js-client";
|
||||
import path from "path";
|
||||
import sharp from "sharp";
|
||||
import { fetchIconPng } from "./fetch-icon-png";
|
||||
|
||||
const iconSizes = [72, 96, 128, 144, 152, 192, 384, 512];
|
||||
|
||||
const defaultLogoPathDirectory = path.join(
|
||||
process.cwd(),
|
||||
"dist",
|
||||
"assets",
|
||||
"icons"
|
||||
);
|
||||
|
||||
export default async function ({
|
||||
my_user,
|
||||
site_view: {
|
||||
site,
|
||||
local_site: { community_creation_admin_only },
|
||||
},
|
||||
}: GetSiteResponse) {
|
||||
const url = getHttpBaseExternal();
|
||||
|
||||
const icon = site.icon ? await fetchIconPng(site.icon) : null;
|
||||
|
||||
return {
|
||||
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",
|
||||
},
|
||||
{
|
||||
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",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
import { ErrorPageData } from "@utils/types";
|
||||
import { GetSiteResponse } from "lemmy-js-client";
|
||||
|
||||
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;
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
import * as cookie from "cookie";
|
||||
import type { Request } from "express";
|
||||
|
||||
export function hasJwtCookie(req: Request): boolean {
|
||||
return Boolean(cookie.parse(req.headers.cookie ?? "").jwt?.length);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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;
|
||||
}
|
28
src/service-worker.ts
Normal file
28
src/service-worker.ts
Normal file
|
@ -0,0 +1,28 @@
|
|||
import { register } from "register-service-worker";
|
||||
|
||||
register("/service-worker.js", {
|
||||
registrationOptions: { scope: "./" },
|
||||
ready() {
|
||||
console.log("Service worker is active.");
|
||||
},
|
||||
registered() {
|
||||
console.log("Service worker has been registered.");
|
||||
},
|
||||
cached() {
|
||||
console.log("Content has been cached for offline use.");
|
||||
},
|
||||
updatefound() {
|
||||
console.log("New content is downloading.");
|
||||
},
|
||||
updated() {
|
||||
console.log("New content is available; please refresh.");
|
||||
},
|
||||
offline() {
|
||||
console.log(
|
||||
"No internet connection found. App is running in offline mode."
|
||||
);
|
||||
},
|
||||
error(error) {
|
||||
console.error("Error during service worker registration:", error);
|
||||
},
|
||||
});
|
|
@ -1,96 +1,65 @@
|
|||
import { isAuthPath, setIsoData } from "@utils/app";
|
||||
import { dataBsTheme } from "@utils/browser";
|
||||
import { Component, RefObject, createRef, linkEvent } from "inferno";
|
||||
import { Component } from "inferno";
|
||||
import { Helmet } from "inferno-helmet";
|
||||
import { Provider } from "inferno-i18next-dess";
|
||||
import { Route, Switch } from "inferno-router";
|
||||
import { IsoDataOptionalSite } from "../../interfaces";
|
||||
import { GetSiteResponse } from "lemmy-js-client";
|
||||
import { i18n } from "../../i18next";
|
||||
import { routes } from "../../routes";
|
||||
import { FirstLoadService, I18NextService, UserService } from "../../services";
|
||||
import AuthGuard from "../common/auth-guard";
|
||||
import ErrorGuard from "../common/error-guard";
|
||||
import { ErrorPage } from "./error-page";
|
||||
import { favIconPngUrl, favIconUrl } from "../../utils";
|
||||
import { Footer } from "./footer";
|
||||
import { Navbar } from "./navbar";
|
||||
import { NoMatch } from "./no-match";
|
||||
import "./styles.scss";
|
||||
import { Theme } from "./theme";
|
||||
|
||||
export class App extends Component<any, any> {
|
||||
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
||||
private readonly mainContentRef: RefObject<HTMLElement>;
|
||||
export interface AppProps {
|
||||
siteRes: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class App extends Component<AppProps, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.mainContentRef = createRef();
|
||||
}
|
||||
|
||||
handleJumpToContent(event) {
|
||||
event.preventDefault();
|
||||
this.mainContentRef.current?.focus();
|
||||
}
|
||||
|
||||
user = UserService.Instance.myUserInfo;
|
||||
|
||||
componentDidMount() {
|
||||
this.setState({ bsTheme: dataBsTheme(this.user) });
|
||||
}
|
||||
|
||||
render() {
|
||||
const siteRes = this.isoData.site_res;
|
||||
const siteView = siteRes?.site_view;
|
||||
|
||||
let siteRes = this.props.siteRes;
|
||||
return (
|
||||
<>
|
||||
<Provider i18next={I18NextService.i18n}>
|
||||
<div
|
||||
id="app"
|
||||
className="lemmy-site"
|
||||
data-bs-theme={this.state?.bsTheme}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className="btn skip-link bg-light position-absolute start-0 z-3"
|
||||
onClick={linkEvent(this, this.handleJumpToContent)}
|
||||
>
|
||||
{I18NextService.i18n.t("jump_to_content", "Jump to content")}
|
||||
</button>
|
||||
{siteView && (
|
||||
<Theme defaultTheme={siteView.local_site.default_theme} />
|
||||
)}
|
||||
<Navbar siteRes={siteRes} />
|
||||
<div className="mt-4 p-0 fl-1">
|
||||
<Provider i18next={i18n}>
|
||||
<div>
|
||||
<Theme myUserInfo={siteRes.my_user} />
|
||||
{siteRes &&
|
||||
siteRes.site_view &&
|
||||
this.props.siteRes.site_view.site.icon && (
|
||||
<Helmet>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
href={this.props.siteRes.site_view.site.icon || favIconUrl}
|
||||
/>
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
href={
|
||||
this.props.siteRes.site_view.site.icon || favIconPngUrl
|
||||
}
|
||||
/>
|
||||
</Helmet>
|
||||
)}
|
||||
<Navbar site_res={this.props.siteRes} />
|
||||
<div class="mt-4 p-0 fl-1">
|
||||
<Switch>
|
||||
{routes.map(
|
||||
({ path, component: RouteComponent, fetchInitialData }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
exact
|
||||
component={routeProps => {
|
||||
if (!fetchInitialData) {
|
||||
FirstLoadService.falsify();
|
||||
}
|
||||
|
||||
return (
|
||||
<ErrorGuard>
|
||||
<div tabIndex={-1}>
|
||||
{RouteComponent &&
|
||||
(isAuthPath(path ?? "") ? (
|
||||
<AuthGuard>
|
||||
<RouteComponent {...routeProps} />
|
||||
</AuthGuard>
|
||||
) : (
|
||||
<RouteComponent {...routeProps} />
|
||||
))}
|
||||
</div>
|
||||
</ErrorGuard>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<Route component={ErrorPage} />
|
||||
{routes.map(({ path, exact, component: C, ...rest }) => (
|
||||
<Route
|
||||
key={path}
|
||||
path={path}
|
||||
exact={exact}
|
||||
render={props => <C {...props} {...rest} />}
|
||||
/>
|
||||
))}
|
||||
<Route render={props => <NoMatch {...props} />} />
|
||||
</Switch>
|
||||
</div>
|
||||
<Footer site={siteRes} />
|
||||
<Footer site={this.props.siteRes} />
|
||||
</div>
|
||||
</Provider>
|
||||
</>
|
||||
|
|
|
@ -1,70 +0,0 @@
|
|||
import { setIsoData } from "@utils/app";
|
||||
import { removeAuthParam } from "@utils/helpers";
|
||||
import { Component } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import { Link } from "inferno-router";
|
||||
import { IsoDataOptionalSite } from "../../interfaces";
|
||||
import { I18NextService } from "../../services";
|
||||
|
||||
export class ErrorPage extends Component<any, any> {
|
||||
private isoData: IsoDataOptionalSite = setIsoData(this.context);
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { errorPageData } = this.isoData;
|
||||
|
||||
return (
|
||||
<div className="error-page container-lg text-center">
|
||||
<h1>
|
||||
{errorPageData
|
||||
? I18NextService.i18n.t("error_page_title")
|
||||
: I18NextService.i18n.t("not_found_page_title")}
|
||||
</h1>
|
||||
{errorPageData ? (
|
||||
<T i18nKey="error_page_paragraph" className="p-4" parent="p">
|
||||
#<a href="https://lemmy.ml/c/lemmy_support">#</a>#
|
||||
<a href="https://matrix.to/#/#lemmy-space:matrix.org">#</a>#
|
||||
</T>
|
||||
) : (
|
||||
<p>{I18NextService.i18n.t("not_found_page_message")}</p>
|
||||
)}
|
||||
{!errorPageData && (
|
||||
<Link to="/" replace>
|
||||
{I18NextService.i18n.t("not_found_return_home_button")}
|
||||
</Link>
|
||||
)}
|
||||
{errorPageData?.adminMatrixIds &&
|
||||
errorPageData.adminMatrixIds.length > 0 && (
|
||||
<>
|
||||
<div>
|
||||
{I18NextService.i18n.t("error_page_admin_matrix", {
|
||||
instance:
|
||||
this.isoData.site_res?.site_view.site.name ??
|
||||
"this instance",
|
||||
})}
|
||||
</div>
|
||||
<ul className="mx-auto mt-2" style={{ width: "fit-content" }}>
|
||||
{errorPageData.adminMatrixIds.map(matrixId => (
|
||||
<li key={matrixId} className="text-info">
|
||||
{matrixId}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</>
|
||||
)}
|
||||
{errorPageData?.error && (
|
||||
<T
|
||||
i18nKey="error_code_message"
|
||||
parent="p"
|
||||
interpolation={{ error: removeAuthParam(errorPageData.error) }}
|
||||
>
|
||||
#<strong className="text-danger">#</strong>#
|
||||
</T>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
import { Component } from "inferno";
|
||||
import { NavLink } from "inferno-router";
|
||||
import { GetSiteResponse } from "lemmy-js-client";
|
||||
import { docsUrl, joinLemmyUrl, repoUrl } from "../../config";
|
||||
import { I18NextService } from "../../services";
|
||||
import { i18n } from "../../i18next";
|
||||
import { docsUrl, joinLemmyUrl, repoUrl } from "../../utils";
|
||||
import { VERSION } from "../../version";
|
||||
|
||||
interface FooterProps {
|
||||
site?: GetSiteResponse;
|
||||
site: GetSiteResponse;
|
||||
}
|
||||
|
||||
export class Footer extends Component<FooterProps, any> {
|
||||
|
@ -16,54 +16,47 @@ export class Footer extends Component<FooterProps, any> {
|
|||
|
||||
render() {
|
||||
return (
|
||||
<footer className="app-footer container-lg navbar navbar-expand-md navbar-light navbar-bg p-3">
|
||||
<nav class="container navbar navbar-expand-md navbar-light navbar-bg p-3">
|
||||
<div className="navbar-collapse">
|
||||
<ul className="navbar-nav ms-auto">
|
||||
{this.props.site?.version !== VERSION && (
|
||||
<li className="nav-item">
|
||||
<span className="nav-link">UI: {VERSION}</span>
|
||||
<ul class="navbar-nav ml-auto">
|
||||
{this.props.site.version !== VERSION && (
|
||||
<li class="nav-item">
|
||||
<span class="nav-link">UI: {VERSION}</span>
|
||||
</li>
|
||||
)}
|
||||
<li className="nav-item">
|
||||
<span className="nav-link">BE: {this.props.site?.version}</span>
|
||||
<li class="nav-item">
|
||||
<span class="nav-link">BE: {this.props.site.version}</span>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<NavLink className="nav-link" to="/modlog">
|
||||
{I18NextService.i18n.t("modlog")}
|
||||
{i18n.t("modlog")}
|
||||
</NavLink>
|
||||
</li>
|
||||
{this.props.site?.site_view.local_site.legal_information && (
|
||||
<li className="nav-item">
|
||||
<NavLink className="nav-link" to="/legal">
|
||||
{I18NextService.i18n.t("legal_information")}
|
||||
</NavLink>
|
||||
</li>
|
||||
)}
|
||||
{this.props.site?.site_view.local_site.federation_enabled && (
|
||||
<li className="nav-item">
|
||||
{this.props.site.federated_instances && (
|
||||
<li class="nav-item">
|
||||
<NavLink className="nav-link" to="/instances">
|
||||
{I18NextService.i18n.t("instances")}
|
||||
{i18n.t("instances")}
|
||||
</NavLink>
|
||||
</li>
|
||||
)}
|
||||
<li className="nav-item">
|
||||
<li class="nav-item">
|
||||
<a className="nav-link" href={docsUrl}>
|
||||
{I18NextService.i18n.t("docs")}
|
||||
{i18n.t("docs")}
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<li class="nav-item">
|
||||
<a className="nav-link" href={repoUrl}>
|
||||
{I18NextService.i18n.t("code")}
|
||||
{i18n.t("code")}
|
||||
</a>
|
||||
</li>
|
||||
<li className="nav-item">
|
||||
<li class="nav-item">
|
||||
<a className="nav-link" href={joinLemmyUrl}>
|
||||
{I18NextService.i18n.t("join_lemmy")}
|
||||
{i18n.t("join_lemmy")}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</footer>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
26
src/shared/components/app/no-match.tsx
Normal file
26
src/shared/components/app/no-match.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import { I18nKeys } from "i18next";
|
||||
import { Component } from "inferno";
|
||||
import { i18n } from "../../i18next";
|
||||
|
||||
export class NoMatch extends Component<any, any> {
|
||||
private errCode = new URLSearchParams(this.props.location.search).get(
|
||||
"err"
|
||||
) as I18nKeys;
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<h1>404</h1>
|
||||
{this.errCode && (
|
||||
<h3>
|
||||
{i18n.t("code")}: {i18n.t(this.errCode)}
|
||||
</h3>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// Custom css
|
||||
@import "../../../../node_modules/tributejs/dist/tribute.css";
|
||||
@import "../../../../node_modules/toastify-js/src/toastify.css";
|
||||
@import "../../../../node_modules/choices.js/src/styles/choices.scss";
|
||||
@import "../../../../node_modules/tippy.js/dist/tippy.css";
|
||||
@import "../../../../node_modules/bootstrap/dist/css/bootstrap-utilities.min.css";
|
||||
@import "../../../assets/css/main.css";
|
||||
|
|
|
@ -1,55 +1,43 @@
|
|||
import { Component } from "inferno";
|
||||
import { Helmet } from "inferno-helmet";
|
||||
import { UserService } from "../../services";
|
||||
import { MyUserInfo } from "lemmy-js-client";
|
||||
|
||||
interface Props {
|
||||
defaultTheme: string;
|
||||
myUserInfo: MyUserInfo | undefined;
|
||||
}
|
||||
|
||||
export class Theme extends Component<Props> {
|
||||
render() {
|
||||
const user = UserService.Instance.myUserInfo;
|
||||
const hasTheme = user?.local_user_view.local_user.theme !== "browser";
|
||||
let user = this.props.myUserInfo;
|
||||
let hasTheme = user && user.local_user_view.local_user.theme !== "browser";
|
||||
|
||||
if (user && hasTheme) {
|
||||
return (
|
||||
<Helmet>
|
||||
return (
|
||||
<Helmet>
|
||||
{hasTheme ? (
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href={`/css/themes/${user.local_user_view.local_user.theme}.css`}
|
||||
href={`/static/assets/css/themes/${user.local_user_view.local_user.theme}.min.css`}
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
} else if (this.props.defaultTheme != "browser") {
|
||||
return (
|
||||
<Helmet>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href={`/css/themes/${this.props.defaultTheme}.css`}
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<Helmet>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/css/themes/litely.css"
|
||||
id="default-light"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/css/themes/darkly.css"
|
||||
id="default-dark"
|
||||
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
|
||||
/>
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
) : (
|
||||
[
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/static/assets/css/themes/litely.min.css"
|
||||
id="default-light"
|
||||
media="(prefers-color-scheme: light)"
|
||||
/>,
|
||||
<link
|
||||
rel="stylesheet"
|
||||
type="text/css"
|
||||
href="/static/assets/css/themes/darkly.min.css"
|
||||
id="default-dark"
|
||||
media="(prefers-color-scheme: no-preference), (prefers-color-scheme: dark)"
|
||||
/>,
|
||||
]
|
||||
)}
|
||||
</Helmet>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,69 +1,92 @@
|
|||
import { myAuthRequired } from "@utils/app";
|
||||
import { capitalizeFirstLetter } from "@utils/helpers";
|
||||
import { Component } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import { Link } from "inferno-router";
|
||||
import { CreateComment, EditComment, Language } from "lemmy-js-client";
|
||||
import { CommentNodeI } from "../../interfaces";
|
||||
import { I18NextService, UserService } from "../../services";
|
||||
import {
|
||||
CommentResponse,
|
||||
CreateComment,
|
||||
EditComment,
|
||||
UserOperation,
|
||||
} from "lemmy-js-client";
|
||||
import { Subscription } from "rxjs";
|
||||
import { i18n } from "../../i18next";
|
||||
import { CommentNode as CommentNodeI } from "../../interfaces";
|
||||
import { UserService, WebSocketService } from "../../services";
|
||||
import {
|
||||
authField,
|
||||
capitalizeFirstLetter,
|
||||
wsClient,
|
||||
wsJsonToRes,
|
||||
wsSubscribe,
|
||||
wsUserOp,
|
||||
} from "../../utils";
|
||||
import { Icon } from "../common/icon";
|
||||
import { MarkdownTextArea } from "../common/markdown-textarea";
|
||||
|
||||
interface CommentFormProps {
|
||||
/**
|
||||
* Can either be the parent, or the editable comment. The right side is a postId.
|
||||
*/
|
||||
node: CommentNodeI | number;
|
||||
finished?: boolean;
|
||||
postId?: number;
|
||||
node?: CommentNodeI; // Can either be the parent, or the editable comment
|
||||
edit?: boolean;
|
||||
disabled?: boolean;
|
||||
focus?: boolean;
|
||||
onReplyCancel?(): void;
|
||||
allLanguages: Language[];
|
||||
siteLanguages: number[];
|
||||
containerClass?: string;
|
||||
onUpsertComment(form: EditComment | CreateComment): void;
|
||||
onReplyCancel?(): any;
|
||||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, any> {
|
||||
interface CommentFormState {
|
||||
buttonTitle: string;
|
||||
finished: boolean;
|
||||
formId: string;
|
||||
}
|
||||
|
||||
export class CommentForm extends Component<CommentFormProps, CommentFormState> {
|
||||
private subscription: Subscription;
|
||||
private emptyState: CommentFormState = {
|
||||
buttonTitle: !this.props.node
|
||||
? capitalizeFirstLetter(i18n.t("post"))
|
||||
: this.props.edit
|
||||
? capitalizeFirstLetter(i18n.t("save"))
|
||||
: capitalizeFirstLetter(i18n.t("reply")),
|
||||
finished: false,
|
||||
formId: "empty_form",
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
|
||||
this.handleCommentSubmit = this.handleCommentSubmit.bind(this);
|
||||
this.handleReplyCancel = this.handleReplyCancel.bind(this);
|
||||
|
||||
this.state = this.emptyState;
|
||||
|
||||
this.parseMessage = this.parseMessage.bind(this);
|
||||
this.subscription = wsSubscribe(this.parseMessage);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.subscription.unsubscribe();
|
||||
}
|
||||
|
||||
render() {
|
||||
const initialContent =
|
||||
typeof this.props.node !== "number"
|
||||
? this.props.edit
|
||||
? this.props.node.comment_view.comment.content
|
||||
: undefined
|
||||
: undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={["comment-form", "mb-3", this.props.containerClass].join(
|
||||
" "
|
||||
)}
|
||||
>
|
||||
<div class="mb-3">
|
||||
{UserService.Instance.myUserInfo ? (
|
||||
<MarkdownTextArea
|
||||
initialContent={initialContent}
|
||||
showLanguage
|
||||
buttonTitle={this.buttonTitle}
|
||||
finished={this.props.finished}
|
||||
replyType={typeof this.props.node !== "number"}
|
||||
initialContent={
|
||||
this.props.edit
|
||||
? this.props.node.comment_view.comment.content
|
||||
: null
|
||||
}
|
||||
buttonTitle={this.state.buttonTitle}
|
||||
finished={this.state.finished}
|
||||
replyType={!!this.props.node}
|
||||
focus={this.props.focus}
|
||||
disabled={this.props.disabled}
|
||||
onSubmit={this.handleCommentSubmit}
|
||||
onReplyCancel={this.props.onReplyCancel}
|
||||
placeholder={I18NextService.i18n.t("comment_here") ?? undefined}
|
||||
allLanguages={this.props.allLanguages}
|
||||
siteLanguages={this.props.siteLanguages}
|
||||
onReplyCancel={this.handleReplyCancel}
|
||||
placeholder={i18n.t("comment_here")}
|
||||
/>
|
||||
) : (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<Icon icon="alert-triangle" classes="icon-inline me-2" />
|
||||
<div class="alert alert-light" role="alert">
|
||||
<Icon icon="alert-triangle" classes="icon-inline mr-2" />
|
||||
<T i18nKey="must_login" class="d-inline">
|
||||
#
|
||||
<Link className="alert-link" to="/login">
|
||||
|
@ -76,46 +99,56 @@ export class CommentForm extends Component<CommentFormProps, any> {
|
|||
);
|
||||
}
|
||||
|
||||
get buttonTitle(): string {
|
||||
return typeof this.props.node === "number"
|
||||
? capitalizeFirstLetter(I18NextService.i18n.t("post"))
|
||||
: this.props.edit
|
||||
? capitalizeFirstLetter(I18NextService.i18n.t("save"))
|
||||
: capitalizeFirstLetter(I18NextService.i18n.t("reply"));
|
||||
handleCommentSubmit(msg: { val: string; formId: string }) {
|
||||
let content = msg.val;
|
||||
this.state.formId = msg.formId;
|
||||
|
||||
let node = this.props.node;
|
||||
|
||||
if (this.props.edit) {
|
||||
let form: EditComment = {
|
||||
content,
|
||||
form_id: this.state.formId,
|
||||
comment_id: node.comment_view.comment.id,
|
||||
auth: authField(),
|
||||
};
|
||||
WebSocketService.Instance.send(wsClient.editComment(form));
|
||||
} else {
|
||||
let form: CreateComment = {
|
||||
content,
|
||||
form_id: this.state.formId,
|
||||
post_id: node ? node.comment_view.post.id : this.props.postId,
|
||||
parent_id: node ? node.comment_view.comment.id : null,
|
||||
auth: authField(),
|
||||
};
|
||||
WebSocketService.Instance.send(wsClient.createComment(form));
|
||||
}
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleCommentSubmit(content: string, form_id: string, language_id?: number) {
|
||||
const { node, onUpsertComment, edit } = this.props;
|
||||
if (typeof node === "number") {
|
||||
const post_id = node;
|
||||
onUpsertComment({
|
||||
content,
|
||||
post_id,
|
||||
language_id,
|
||||
form_id,
|
||||
auth: myAuthRequired(),
|
||||
});
|
||||
} else {
|
||||
if (edit) {
|
||||
const comment_id = node.comment_view.comment.id;
|
||||
onUpsertComment({
|
||||
content,
|
||||
comment_id,
|
||||
form_id,
|
||||
language_id,
|
||||
auth: myAuthRequired(),
|
||||
});
|
||||
} else {
|
||||
const post_id = node.comment_view.post.id;
|
||||
const parent_id = node.comment_view.comment.id;
|
||||
this.props.onUpsertComment({
|
||||
content,
|
||||
parent_id,
|
||||
post_id,
|
||||
form_id,
|
||||
language_id,
|
||||
auth: myAuthRequired(),
|
||||
});
|
||||
handleReplyCancel() {
|
||||
this.props.onReplyCancel();
|
||||
}
|
||||
|
||||
parseMessage(msg: any) {
|
||||
let op = wsUserOp(msg);
|
||||
console.log(msg);
|
||||
|
||||
// Only do the showing and hiding if logged in
|
||||
if (UserService.Instance.myUserInfo) {
|
||||
if (
|
||||
op == UserOperation.CreateComment ||
|
||||
op == UserOperation.EditComment
|
||||
) {
|
||||
let data = wsJsonToRes<CommentResponse>(msg).data;
|
||||
|
||||
// This only finishes this form, if the randomly generated form_id matches the one received
|
||||
if (this.state.formId == data.form_id) {
|
||||
this.setState({ finished: true });
|
||||
|
||||
// Necessary because it broke tribute for some reason
|
||||
this.setState({ finished: false });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,39 +1,13 @@
|
|||
import { colorList } from "@utils/app";
|
||||
import classNames from "classnames";
|
||||
import { Component } from "inferno";
|
||||
import {
|
||||
AddAdmin,
|
||||
AddModToCommunity,
|
||||
BanFromCommunity,
|
||||
BanPerson,
|
||||
BlockPerson,
|
||||
CommentId,
|
||||
CommunityModeratorView,
|
||||
CreateComment,
|
||||
CreateCommentLike,
|
||||
CreateCommentReport,
|
||||
DeleteComment,
|
||||
DistinguishComment,
|
||||
EditComment,
|
||||
GetComments,
|
||||
Language,
|
||||
MarkCommentReplyAsRead,
|
||||
MarkPersonMentionAsRead,
|
||||
PersonView,
|
||||
PurgeComment,
|
||||
PurgePerson,
|
||||
RemoveComment,
|
||||
SaveComment,
|
||||
TransferCommunity,
|
||||
} from "lemmy-js-client";
|
||||
import { CommentNodeI, CommentViewType } from "../../interfaces";
|
||||
import { CommunityModeratorView, PersonViewSafe } from "lemmy-js-client";
|
||||
import { CommentNode as CommentNodeI } from "../../interfaces";
|
||||
import { CommentNode } from "./comment-node";
|
||||
|
||||
interface CommentNodesProps {
|
||||
nodes: CommentNodeI[];
|
||||
moderators?: CommunityModeratorView[];
|
||||
admins?: PersonView[];
|
||||
maxCommentsShown?: number;
|
||||
admins?: PersonViewSafe[];
|
||||
postCreatorId?: number;
|
||||
noBorder?: boolean;
|
||||
noIndent?: boolean;
|
||||
viewOnly?: boolean;
|
||||
|
@ -41,102 +15,40 @@ interface CommentNodesProps {
|
|||
markable?: boolean;
|
||||
showContext?: boolean;
|
||||
showCommunity?: boolean;
|
||||
enableDownvotes?: boolean;
|
||||
viewType: CommentViewType;
|
||||
allLanguages: Language[];
|
||||
siteLanguages: number[];
|
||||
hideImages?: boolean;
|
||||
isChild?: boolean;
|
||||
depth?: number;
|
||||
finished: Map<CommentId, boolean | undefined>;
|
||||
onSaveComment(form: SaveComment): void;
|
||||
onCommentReplyRead(form: MarkCommentReplyAsRead): void;
|
||||
onPersonMentionRead(form: MarkPersonMentionAsRead): void;
|
||||
onCreateComment(form: EditComment | CreateComment): void;
|
||||
onEditComment(form: EditComment | CreateComment): void;
|
||||
onCommentVote(form: CreateCommentLike): void;
|
||||
onBlockPerson(form: BlockPerson): void;
|
||||
onDeleteComment(form: DeleteComment): void;
|
||||
onRemoveComment(form: RemoveComment): void;
|
||||
onDistinguishComment(form: DistinguishComment): void;
|
||||
onAddModToCommunity(form: AddModToCommunity): void;
|
||||
onAddAdmin(form: AddAdmin): void;
|
||||
onBanPersonFromCommunity(form: BanFromCommunity): void;
|
||||
onBanPerson(form: BanPerson): void;
|
||||
onTransferCommunity(form: TransferCommunity): void;
|
||||
onFetchChildren?(form: GetComments): void;
|
||||
onCommentReport(form: CreateCommentReport): void;
|
||||
onPurgePerson(form: PurgePerson): void;
|
||||
onPurgeComment(form: PurgeComment): void;
|
||||
maxCommentsShown?: number;
|
||||
enableDownvotes: boolean;
|
||||
}
|
||||
|
||||
export class CommentNodes extends Component<CommentNodesProps, any> {
|
||||
constructor(props: CommentNodesProps, context: any) {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
const maxComments = this.props.maxCommentsShown ?? this.props.nodes.length;
|
||||
|
||||
const borderColor = this.props.depth
|
||||
? colorList[(this.props.depth - 1) % colorList.length]
|
||||
: colorList[0];
|
||||
let maxComments = this.props.maxCommentsShown
|
||||
? this.props.maxCommentsShown
|
||||
: this.props.nodes.length;
|
||||
|
||||
return (
|
||||
this.props.nodes.length > 0 && (
|
||||
<ul
|
||||
className={classNames("comments", {
|
||||
"ms-1": !!this.props.isChild,
|
||||
"border-top border-light": !this.props.noBorder,
|
||||
})}
|
||||
style={
|
||||
this.props.isChild
|
||||
? `border-left: 2px solid ${borderColor} !important;`
|
||||
: undefined
|
||||
}
|
||||
>
|
||||
{this.props.nodes.slice(0, maxComments).map(node => (
|
||||
<CommentNode
|
||||
key={node.comment_view.comment.id}
|
||||
node={node}
|
||||
noBorder={this.props.noBorder}
|
||||
noIndent={this.props.noIndent}
|
||||
viewOnly={this.props.viewOnly}
|
||||
locked={this.props.locked}
|
||||
moderators={this.props.moderators}
|
||||
admins={this.props.admins}
|
||||
markable={this.props.markable}
|
||||
showContext={this.props.showContext}
|
||||
showCommunity={this.props.showCommunity}
|
||||
enableDownvotes={this.props.enableDownvotes}
|
||||
viewType={this.props.viewType}
|
||||
allLanguages={this.props.allLanguages}
|
||||
siteLanguages={this.props.siteLanguages}
|
||||
hideImages={this.props.hideImages}
|
||||
onCommentReplyRead={this.props.onCommentReplyRead}
|
||||
onPersonMentionRead={this.props.onPersonMentionRead}
|
||||
finished={this.props.finished}
|
||||
onCreateComment={this.props.onCreateComment}
|
||||
onEditComment={this.props.onEditComment}
|
||||
onCommentVote={this.props.onCommentVote}
|
||||
onBlockPerson={this.props.onBlockPerson}
|
||||
onSaveComment={this.props.onSaveComment}
|
||||
onDeleteComment={this.props.onDeleteComment}
|
||||
onRemoveComment={this.props.onRemoveComment}
|
||||
onDistinguishComment={this.props.onDistinguishComment}
|
||||
onAddModToCommunity={this.props.onAddModToCommunity}
|
||||
onAddAdmin={this.props.onAddAdmin}
|
||||
onBanPersonFromCommunity={this.props.onBanPersonFromCommunity}
|
||||
onBanPerson={this.props.onBanPerson}
|
||||
onTransferCommunity={this.props.onTransferCommunity}
|
||||
onFetchChildren={this.props.onFetchChildren}
|
||||
onCommentReport={this.props.onCommentReport}
|
||||
onPurgePerson={this.props.onPurgePerson}
|
||||
onPurgeComment={this.props.onPurgeComment}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
)
|
||||
<div className="comments">
|
||||
{this.props.nodes.slice(0, maxComments).map(node => (
|
||||
<CommentNode
|
||||
key={node.comment_view.comment.id}
|
||||
node={node}
|
||||
noBorder={this.props.noBorder}
|
||||
noIndent={this.props.noIndent}
|
||||
viewOnly={this.props.viewOnly}
|
||||
locked={this.props.locked}
|
||||
moderators={this.props.moderators}
|
||||
admins={this.props.admins}
|
||||
postCreatorId={this.props.postCreatorId}
|
||||
markable={this.props.markable}
|
||||
showContext={this.props.showContext}
|
||||
showCommunity={this.props.showCommunity}
|
||||
enableDownvotes={this.props.enableDownvotes}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,155 +0,0 @@
|
|||
import { myAuthRequired } from "@utils/app";
|
||||
import { Component, InfernoNode, linkEvent } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import {
|
||||
CommentReportView,
|
||||
CommentView,
|
||||
ResolveCommentReport,
|
||||
} from "lemmy-js-client";
|
||||
import { CommentNodeI, CommentViewType } from "../../interfaces";
|
||||
import { I18NextService } from "../../services";
|
||||
import { Icon, Spinner } from "../common/icon";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
import { CommentNode } from "./comment-node";
|
||||
|
||||
interface CommentReportProps {
|
||||
report: CommentReportView;
|
||||
onResolveReport(form: ResolveCommentReport): void;
|
||||
}
|
||||
|
||||
interface CommentReportState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class CommentReport extends Component<
|
||||
CommentReportProps,
|
||||
CommentReportState
|
||||
> {
|
||||
state: CommentReportState = {
|
||||
loading: false,
|
||||
};
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(
|
||||
nextProps: Readonly<{ children?: InfernoNode } & CommentReportProps>
|
||||
): void {
|
||||
if (this.props != nextProps) {
|
||||
this.setState({ loading: false });
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const r = this.props.report;
|
||||
const comment = r.comment;
|
||||
const tippyContent = I18NextService.i18n.t(
|
||||
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
|
||||
);
|
||||
|
||||
// Set the original post data ( a troll could change it )
|
||||
comment.content = r.comment_report.original_comment_text;
|
||||
|
||||
const comment_view: CommentView = {
|
||||
comment,
|
||||
creator: r.comment_creator,
|
||||
post: r.post,
|
||||
community: r.community,
|
||||
creator_banned_from_community: r.creator_banned_from_community,
|
||||
counts: r.counts,
|
||||
subscribed: "NotSubscribed",
|
||||
saved: false,
|
||||
creator_blocked: false,
|
||||
my_vote: r.my_vote,
|
||||
};
|
||||
|
||||
const node: CommentNodeI = {
|
||||
comment_view,
|
||||
children: [],
|
||||
depth: 0,
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="comment-report">
|
||||
<CommentNode
|
||||
node={node}
|
||||
viewType={CommentViewType.Flat}
|
||||
enableDownvotes={true}
|
||||
viewOnly={true}
|
||||
showCommunity={true}
|
||||
allLanguages={[]}
|
||||
siteLanguages={[]}
|
||||
hideImages
|
||||
// All of these are unused, since its viewonly
|
||||
finished={new Map()}
|
||||
onSaveComment={() => {}}
|
||||
onBlockPerson={() => {}}
|
||||
onDeleteComment={() => {}}
|
||||
onRemoveComment={() => {}}
|
||||
onCommentVote={() => {}}
|
||||
onCommentReport={() => {}}
|
||||
onDistinguishComment={() => {}}
|
||||
onAddModToCommunity={() => {}}
|
||||
onAddAdmin={() => {}}
|
||||
onTransferCommunity={() => {}}
|
||||
onPurgeComment={() => {}}
|
||||
onPurgePerson={() => {}}
|
||||
onCommentReplyRead={() => {}}
|
||||
onPersonMentionRead={() => {}}
|
||||
onBanPersonFromCommunity={() => {}}
|
||||
onBanPerson={() => {}}
|
||||
onCreateComment={() => Promise.resolve({ state: "empty" })}
|
||||
onEditComment={() => Promise.resolve({ state: "empty" })}
|
||||
/>
|
||||
<div>
|
||||
{I18NextService.i18n.t("reporter")}:{" "}
|
||||
<PersonListing person={r.creator} />
|
||||
</div>
|
||||
<div>
|
||||
{I18NextService.i18n.t("reason")}: {r.comment_report.reason}
|
||||
</div>
|
||||
{r.resolver && (
|
||||
<div>
|
||||
{r.comment_report.resolved ? (
|
||||
<T i18nKey="resolved_by">
|
||||
#
|
||||
<PersonListing person={r.resolver} />
|
||||
</T>
|
||||
) : (
|
||||
<T i18nKey="unresolved_by">
|
||||
#
|
||||
<PersonListing person={r.resolver} />
|
||||
</T>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
onClick={linkEvent(this, this.handleResolveReport)}
|
||||
data-tippy-content={tippyContent}
|
||||
aria-label={tippyContent}
|
||||
>
|
||||
{this.state.loading ? (
|
||||
<Spinner />
|
||||
) : (
|
||||
<Icon
|
||||
icon="check"
|
||||
classes={`icon-inline ${
|
||||
r.comment_report.resolved ? "text-success" : "text-danger"
|
||||
}`}
|
||||
/>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleResolveReport(i: CommentReport) {
|
||||
i.setState({ loading: true });
|
||||
i.props.onResolveReport({
|
||||
report_id: i.props.report.comment_report.id,
|
||||
resolved: !i.props.report.comment_report.resolved,
|
||||
auth: myAuthRequired(),
|
||||
});
|
||||
}
|
||||
}
|
107
src/shared/components/comment/comment_report.tsx
Normal file
107
src/shared/components/comment/comment_report.tsx
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { Component, linkEvent } from "inferno";
|
||||
import { T } from "inferno-i18next-dess";
|
||||
import {
|
||||
CommentReportView,
|
||||
CommentView,
|
||||
ResolveCommentReport,
|
||||
} from "lemmy-js-client";
|
||||
import { i18n } from "../../i18next";
|
||||
import { CommentNode as CommentNodeI } from "../../interfaces";
|
||||
import { WebSocketService } from "../../services";
|
||||
import { authField, wsClient } from "../../utils";
|
||||
import { Icon } from "../common/icon";
|
||||
import { PersonListing } from "../person/person-listing";
|
||||
import { CommentNode } from "./comment-node";
|
||||
|
||||
interface CommentReportProps {
|
||||
report: CommentReportView;
|
||||
}
|
||||
|
||||
export class CommentReport extends Component<CommentReportProps, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
let r = this.props.report;
|
||||
let comment = r.comment;
|
||||
|
||||
// Set the original post data ( a troll could change it )
|
||||
comment.content = r.comment_report.original_comment_text;
|
||||
|
||||
let comment_view: CommentView = {
|
||||
comment,
|
||||
creator: r.comment_creator,
|
||||
post: r.post,
|
||||
community: r.community,
|
||||
creator_banned_from_community: r.creator_banned_from_community,
|
||||
counts: r.counts,
|
||||
subscribed: false,
|
||||
saved: false,
|
||||
creator_blocked: false,
|
||||
my_vote: r.my_vote,
|
||||
};
|
||||
|
||||
let node: CommentNodeI = {
|
||||
comment_view,
|
||||
};
|
||||
|
||||
return (
|
||||
<div>
|
||||
<CommentNode
|
||||
node={node}
|
||||
moderators={[]}
|
||||
admins={[]}
|
||||
enableDownvotes={true}
|
||||
/>
|
||||
<div>
|
||||
{i18n.t("reporter")}: <PersonListing person={r.creator} />
|
||||
</div>
|
||||
<div>
|
||||
{i18n.t("reason")}: {r.comment_report.reason}
|
||||
</div>
|
||||
{r.resolver && (
|
||||
<div>
|
||||
{r.comment_report.resolved ? (
|
||||
<T i18nKey="resolved_by">
|
||||
#
|
||||
<PersonListing person={r.resolver} />
|
||||
</T>
|
||||
) : (
|
||||
<T i18nKey="unresolved_by">
|
||||
#
|
||||
<PersonListing person={r.resolver} />
|
||||
</T>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-link btn-animate text-muted py-0"
|
||||
onClick={linkEvent(this, this.handleResolveReport)}
|
||||
data-tippy-content={
|
||||
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
|
||||
}
|
||||
aria-label={
|
||||
r.comment_report.resolved ? "unresolve_report" : "resolve_report"
|
||||
}
|
||||
>
|
||||
<Icon
|
||||
icon="check"
|
||||
classes={`icon-inline ${
|
||||
r.comment_report.resolved ? "text-success" : "text-danger"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
handleResolveReport(i: CommentReport) {
|
||||
let form: ResolveCommentReport = {
|
||||
report_id: i.props.report.comment_report.id,
|
||||
resolved: !i.props.report.comment_report.resolved,
|
||||
auth: authField(),
|
||||
};
|
||||
WebSocketService.Instance.send(wsClient.resolveCommentReport(form));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue