Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2bd06250fc |
@@ -10,7 +10,7 @@ orbs:
|
|||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:20.17.0
|
- image: cimg/node:20.8.0
|
||||||
- image: mongo:4.4
|
- image: mongo:4.4
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
@@ -27,7 +27,7 @@ jobs:
|
|||||||
# fallback to using the latest cache if no exact match is found
|
# fallback to using the latest cache if no exact match is found
|
||||||
- v1-dependencies-
|
- v1-dependencies-
|
||||||
|
|
||||||
- run: sudo npm install -g npm@10.8.2
|
- run: sudo npm install -g npm@10.2.0
|
||||||
- node/install-packages:
|
- node/install-packages:
|
||||||
app-dir: ~/homebrewery
|
app-dir: ~/homebrewery
|
||||||
cache-path: node_modules
|
cache-path: node_modules
|
||||||
@@ -45,7 +45,7 @@ jobs:
|
|||||||
|
|
||||||
test:
|
test:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/node:20.17.0
|
- image: cimg/node:20.8.0
|
||||||
|
|
||||||
working_directory: ~/homebrewery
|
working_directory: ~/homebrewery
|
||||||
parallelism: 1
|
parallelism: 1
|
||||||
@@ -67,18 +67,12 @@ jobs:
|
|||||||
- run:
|
- run:
|
||||||
name: Test - Definition Lists
|
name: Test - Definition Lists
|
||||||
command: npm run test:definition-lists
|
command: npm run test:definition-lists
|
||||||
- run:
|
|
||||||
name: Test - Hard Breaks
|
|
||||||
command: npm run test:hard-breaks
|
|
||||||
- run:
|
- run:
|
||||||
name: Test - Variables
|
name: Test - Variables
|
||||||
command: npm run test:variables
|
command: npm run test:variables
|
||||||
- run:
|
- run:
|
||||||
name: Test - Routes
|
name: Test - Routes
|
||||||
command: npm run test:route
|
command: npm run test:route
|
||||||
- run:
|
|
||||||
name: Test - HTML sanitization
|
|
||||||
command: npm run test:safehtml
|
|
||||||
- run:
|
- run:
|
||||||
name: Test - Coverage
|
name: Test - Coverage
|
||||||
command: npm run test:coverage
|
command: npm run test:coverage
|
||||||
|
|||||||
79
.eslintrc.js
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
module.exports = {
|
||||||
|
root : true,
|
||||||
|
parserOptions : {
|
||||||
|
ecmaVersion : 2021,
|
||||||
|
sourceType : 'module',
|
||||||
|
ecmaFeatures : {
|
||||||
|
jsx : true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
env : {
|
||||||
|
browser : true,
|
||||||
|
node : true
|
||||||
|
},
|
||||||
|
plugins : ['react', 'jest'],
|
||||||
|
rules : {
|
||||||
|
/** Errors **/
|
||||||
|
'camelcase' : ['error', { properties: 'never' }],
|
||||||
|
//'func-style' : ['error', 'expression', { allowArrowFunctions: true }],
|
||||||
|
'no-array-constructor' : 'error',
|
||||||
|
'no-iterator' : 'error',
|
||||||
|
'no-nested-ternary' : 'error',
|
||||||
|
'no-new-object' : 'error',
|
||||||
|
'no-proto' : 'error',
|
||||||
|
'react/jsx-no-bind' : ['error', { allowArrowFunctions: true }],
|
||||||
|
'react/jsx-uses-react' : 'error',
|
||||||
|
'react/prefer-es6-class' : ['error', 'never'],
|
||||||
|
'jest/valid-expect' : ['error', { maxArgs: 3 }],
|
||||||
|
|
||||||
|
/** Warnings **/
|
||||||
|
'max-lines' : ['warn', {
|
||||||
|
max : 200,
|
||||||
|
skipComments : true,
|
||||||
|
skipBlankLines : true,
|
||||||
|
}],
|
||||||
|
'max-depth' : ['warn', { max: 4 }],
|
||||||
|
'max-params' : ['warn', { max: 5 }],
|
||||||
|
'no-restricted-syntax' : ['warn', 'ClassDeclaration', 'SwitchStatement'],
|
||||||
|
'no-unused-vars' : ['warn', {
|
||||||
|
vars : 'all',
|
||||||
|
args : 'none',
|
||||||
|
varsIgnorePattern : 'config|_|cx|createClass'
|
||||||
|
}],
|
||||||
|
'react/jsx-uses-vars' : 'warn',
|
||||||
|
|
||||||
|
/** Fixable **/
|
||||||
|
'arrow-parens' : ['warn', 'always'],
|
||||||
|
'brace-style' : ['warn', '1tbs', { allowSingleLine: true }],
|
||||||
|
'jsx-quotes' : ['warn', 'prefer-single'],
|
||||||
|
'no-var' : 'warn',
|
||||||
|
'prefer-const' : 'warn',
|
||||||
|
'prefer-template' : 'warn',
|
||||||
|
'quotes' : ['warn', 'single', { 'allowTemplateLiterals': true }],
|
||||||
|
'semi' : ['warn', 'always'],
|
||||||
|
|
||||||
|
/** Whitespace **/
|
||||||
|
'array-bracket-spacing' : ['warn', 'never'],
|
||||||
|
'arrow-spacing' : ['warn', { before: false, after: false }],
|
||||||
|
'comma-spacing' : ['warn', { before: false, after: true }],
|
||||||
|
'indent' : ['warn', 'tab', { 'MemberExpression': 'off' }],
|
||||||
|
'keyword-spacing' : ['warn', {
|
||||||
|
before : true,
|
||||||
|
after : true,
|
||||||
|
overrides : {
|
||||||
|
if : { 'before': false, 'after': false }
|
||||||
|
}
|
||||||
|
}],
|
||||||
|
'key-spacing' : ['warn', {
|
||||||
|
multiLine : { beforeColon: true, afterColon: true, align: 'colon' },
|
||||||
|
singleLine : { beforeColon: false, afterColon: true }
|
||||||
|
}],
|
||||||
|
'linebreak-style' : 'off',
|
||||||
|
'no-trailing-spaces' : 'warn',
|
||||||
|
'no-whitespace-before-property' : 'warn',
|
||||||
|
'object-curly-spacing' : ['warn', 'always'],
|
||||||
|
'react/jsx-indent-props' : ['warn', 'tab'],
|
||||||
|
'space-in-parens' : ['warn', 'never'],
|
||||||
|
'template-curly-spacing' : ['warn', 'never'],
|
||||||
|
}
|
||||||
|
};
|
||||||
2
.gitattributes
vendored
@@ -1,3 +1 @@
|
|||||||
package-lock.json binary
|
package-lock.json binary
|
||||||
|
|
||||||
*.json text eol=lf
|
|
||||||
36
.github/pull_request_template.md
vendored
@@ -1,36 +0,0 @@
|
|||||||
<!--
|
|
||||||
Before submitting a Pull Request, please consider the following to speed up reviews:
|
|
||||||
- 👷♀️ Create small PRs. Large PRs can usually be broken down into incremental PRs.
|
|
||||||
- 🚩 Do you already have several open PRs? Consider finishing or asking for help with existing PRs first.
|
|
||||||
- 🔧 Does your PR reference a discussed and approved issue, especially for personal or edge-case requests?
|
|
||||||
- 💡 Is the solution agreed upon? Save rework time by discussing strategy before coding.
|
|
||||||
-->
|
|
||||||
|
|
||||||
## Description
|
|
||||||
|
|
||||||
|
|
||||||
## Related Issues or Discussions
|
|
||||||
|
|
||||||
- Closes #
|
|
||||||
|
|
||||||
## QA Instructions, Screenshots, Recordings
|
|
||||||
|
|
||||||
_Please replace this line with instructions on how to test or view your changes, as well as any before/after
|
|
||||||
images for UI changes._
|
|
||||||
|
|
||||||
### Reviewer Checklist
|
|
||||||
|
|
||||||
_Please replace the list below with specific features you want reviewers to look at._
|
|
||||||
|
|
||||||
*Reviewers, refer to this list when testing features, or suggest new items *
|
|
||||||
- [ ] Verify new features are functional
|
|
||||||
- [ ] Feature A does X
|
|
||||||
- [ ] Feature B does Y
|
|
||||||
- [ ] Verify old features have not broken
|
|
||||||
- [ ] Feature Z can still be used
|
|
||||||
- [ ] Test for edge cases / try to break things
|
|
||||||
- [ ] Feature A handles negative numbers
|
|
||||||
- [ ] Identify opportunities for simplification and refactoring
|
|
||||||
- [ ] Check for code legibility and appropriate comments
|
|
||||||
|
|
||||||
<details><summary>Copy this list</summary>
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
"stylelint-config-recess-order",
|
"stylelint-config-recess-order",
|
||||||
"stylelint-config-recommended"],
|
"stylelint-config-recommended"],
|
||||||
"plugins": [
|
"plugins": [
|
||||||
"@stylistic/stylelint-plugin",
|
"stylelint-stylistic",
|
||||||
"./stylelint_plugins/declaration-colon-align.js",
|
"./stylelint_plugins/declaration-colon-align.js",
|
||||||
"./stylelint_plugins/declaration-colon-min-space-before",
|
"./stylelint_plugins/declaration-colon-min-space-before",
|
||||||
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
"./stylelint_plugins/declaration-block-multi-line-min-declarations"
|
||||||
@@ -16,32 +16,32 @@
|
|||||||
"font-family-no-missing-generic-family-keyword" : null,
|
"font-family-no-missing-generic-family-keyword" : null,
|
||||||
"font-weight-notation" : "named-where-possible",
|
"font-weight-notation" : "named-where-possible",
|
||||||
"font-family-name-quotes" : "always-unless-keyword",
|
"font-family-name-quotes" : "always-unless-keyword",
|
||||||
"@stylistic/indentation" : "tab",
|
"stylistic/indentation" : "tab",
|
||||||
"no-duplicate-selectors" : true,
|
"no-duplicate-selectors" : true,
|
||||||
"@stylistic/color-hex-case" : "upper",
|
"stylistic/color-hex-case" : "upper",
|
||||||
"color-hex-length" : "long",
|
"color-hex-length" : "long",
|
||||||
"@stylistic/selector-combinator-space-after" : "always",
|
"stylistic/selector-combinator-space-after" : "always",
|
||||||
"@stylistic/selector-combinator-space-before" : "always",
|
"stylistic/selector-combinator-space-before" : "always",
|
||||||
"@stylistic/selector-attribute-operator-space-before" : "never",
|
"stylistic/selector-attribute-operator-space-before" : "never",
|
||||||
"@stylistic/selector-attribute-operator-space-after" : "never",
|
"stylistic/selector-attribute-operator-space-after" : "never",
|
||||||
"@stylistic/selector-attribute-brackets-space-inside" : "never",
|
"stylistic/selector-attribute-brackets-space-inside" : "never",
|
||||||
"selector-attribute-quotes" : "always",
|
"selector-attribute-quotes" : "always",
|
||||||
"selector-pseudo-element-colon-notation" : "double",
|
"selector-pseudo-element-colon-notation" : "double",
|
||||||
"@stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
"stylistic/selector-pseudo-class-parentheses-space-inside" : "never",
|
||||||
"@stylistic/block-opening-brace-space-before" : "always",
|
"stylistic/block-opening-brace-space-before" : "always",
|
||||||
"naturalcrit/declaration-colon-min-space-before" : 1,
|
"naturalcrit/declaration-colon-min-space-before" : 1,
|
||||||
"@stylistic/declaration-block-trailing-semicolon" : "always",
|
"stylistic/declaration-block-trailing-semicolon" : "always",
|
||||||
"@stylistic/declaration-colon-space-after" : "always",
|
"stylistic/declaration-colon-space-after" : "always",
|
||||||
"@stylistic/number-leading-zero" : "always",
|
"stylistic/number-leading-zero" : "always",
|
||||||
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
"function-url-quotes" : ["always", { "except": ["empty"] }],
|
||||||
"function-url-scheme-disallowed-list" : ["data","http"],
|
"function-url-scheme-disallowed-list" : ["data","http"],
|
||||||
"comment-whitespace-inside" : "always",
|
"comment-whitespace-inside" : "always",
|
||||||
"@stylistic/string-quotes" : "single",
|
"stylistic/string-quotes" : "single",
|
||||||
"@stylistic/media-feature-range-operator-space-before" : "always",
|
"stylistic/media-feature-range-operator-space-before" : "always",
|
||||||
"@stylistic/media-feature-range-operator-space-after" : "always",
|
"stylistic/media-feature-range-operator-space-after" : "always",
|
||||||
"@stylistic/media-feature-parentheses-space-inside" : "never",
|
"stylistic/media-feature-parentheses-space-inside" : "never",
|
||||||
"@stylistic/media-feature-colon-space-before" : "always",
|
"stylistic/media-feature-colon-space-before" : "always",
|
||||||
"@stylistic/media-feature-colon-space-after" : "always",
|
"stylistic/media-feature-colon-space-after" : "always",
|
||||||
"naturalcrit/declaration-colon-align" : true,
|
"naturalcrit/declaration-colon-align" : true,
|
||||||
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
"naturalcrit/declaration-block-multi-line-min-declarations": 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
FROM node:20-alpine
|
FROM node:18-alpine
|
||||||
RUN apk --no-cache add git
|
RUN apk --no-cache add git
|
||||||
|
|
||||||
ENV NODE_ENV=docker
|
ENV NODE_ENV=docker
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"presets": [
|
|
||||||
"@babel/preset-env",
|
|
||||||
"@babel/preset-react"
|
|
||||||
],
|
|
||||||
"plugins": [
|
|
||||||
"@babel/plugin-transform-runtime",
|
|
||||||
"babel-plugin-transform-import-meta"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
324
changelog.md
@@ -81,311 +81,9 @@ pre {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
## changelog
|
## changelog
|
||||||
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
For a full record of development, visit our [Github Page](https://github.com/naturalcrit/homebrewery).
|
||||||
|
|
||||||
### Wednesday 11/27/2024 - v3.16.1
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### 5e-Cleric
|
|
||||||
|
|
||||||
* [x] Allow linking to specific HTML IDs via `#ID` at the end of the URL, e.g.: `homebrewery.naturalcrit.com/share/share/a6RCXwaDS58i#p4` to link to Page 4 directly
|
|
||||||
|
|
||||||
Fixes issues [#2820](https://github.com/naturalcrit/homebrewery/issues/2820), [#3505](https://github.com/naturalcrit/homebrewery/issues/3505)
|
|
||||||
|
|
||||||
* [x] Fix generation of link to certain Google Drive brews
|
|
||||||
|
|
||||||
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
|
|
||||||
|
|
||||||
##### abquintic
|
|
||||||
|
|
||||||
* [x] Fix blank pages appearing when pasting text
|
|
||||||
|
|
||||||
Fixes issue [#3718](https://github.com/naturalcrit/homebrewery/issues/3718)
|
|
||||||
|
|
||||||
##### Gazook89
|
|
||||||
|
|
||||||
* [x] Add new brew viewing options to the view toolbar
|
|
||||||
- {{fac,single-spread}} {{openSans **SINGLE PAGE**}}
|
|
||||||
- {{fac,facing-spread}} {{openSans **TWO PAGE**}}
|
|
||||||
- {{fac,flow-spread}} {{openSans **GRID**}}
|
|
||||||
|
|
||||||
Fixes issue [#1379](https://github.com/naturalcrit/homebrewery/issues/1379)
|
|
||||||
|
|
||||||
* [x] Updates to tag input boxes
|
|
||||||
|
|
||||||
##### G-Ambatte
|
|
||||||
|
|
||||||
* [x] Admin tools to fix certain corrupted documents
|
|
||||||
|
|
||||||
Fixes issue [#3801](https://github.com/naturalcrit/homebrewery/issues/3801)
|
|
||||||
|
|
||||||
* [x] Fix print window being affected by document zoom
|
|
||||||
|
|
||||||
Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744)
|
|
||||||
|
|
||||||
|
|
||||||
##### calculuschild, 5e-Cleric, G-Ambatte, Gazook89, abquintic
|
|
||||||
|
|
||||||
* [x] Multiple code refactors, cleanups, and security fixes
|
|
||||||
}}
|
|
||||||
|
|
||||||
### Saturday 10/12/2024 - v3.16.0
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### 5e-Cleric
|
|
||||||
|
|
||||||
* [x] Added a new API endpoint `/metadata/:shareId` to fetch metadata about individual brews
|
|
||||||
|
|
||||||
Fixes issue [#2638](https://github.com/naturalcrit/homebrewery/issues/2638)
|
|
||||||
|
|
||||||
* [x] Added A3, A5, and Card page size snippets under {{openSans **:fas_paintbrush: STYLE TAB :fas_arrow_right: :fas_print: PRINT**}}
|
|
||||||
|
|
||||||
* [x] Adjust navbar styling for very long titles
|
|
||||||
|
|
||||||
Fixes issue [#2071](https://github.com/naturalcrit/homebrewery/issues/2071)
|
|
||||||
|
|
||||||
* [x] Added some sorting options to the {{openSans **VAULT** {{fas,fa-dungeon}}}} page
|
|
||||||
|
|
||||||
* [x] Fix `language` property not working in share page
|
|
||||||
|
|
||||||
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
|
|
||||||
|
|
||||||
##### abquintic
|
|
||||||
|
|
||||||
* [x] New {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_bookmark: PAGE NUMBER :fas_arrow_right:**}}
|
|
||||||
{{openSans **:fas_xmark: SKIP PAGE NUMBER**}} and {{openSans **:fas_arrow_rotate_left: RESTART PAGE NUMBER**}} snippets for more control over automatic page numbering.
|
|
||||||
|
|
||||||
Fixes issue [#513](https://github.com/naturalcrit/homebrewery/issues/513)
|
|
||||||
|
|
||||||
* [x] New Table of Contents control options via {{openSans **:fas_pencil: TEXT EDITOR :fas_arrow_right: :fas_book: TABLE OF CONTENTS**}} submenus. By default, H1-H3 is included in the ToC generation, but the new options allow marking `{{blocks}}` to include or exclude specific or ranges of contained headers. Also, a global option to increase the default range of H1-H3 to H1-H4/5/6. After applying these markers, you must regenerate the Table of Contents to see the changes.
|
|
||||||
|
|
||||||
* [x] Added a ":fas_lock: SYNC VIEWS" button onto the divider bar. When locked, scrolling on either panel will sync the other panel to the same page.
|
|
||||||
|
|
||||||
Fixes issue [#241](https://github.com/naturalcrit/homebrewery/issues/241)
|
|
||||||
|
|
||||||
##### Gazook89
|
|
||||||
|
|
||||||
* [x] Added a :fas_glasses: HIDE button to the page navigation bar
|
|
||||||
|
|
||||||
##### G-Ambatte
|
|
||||||
|
|
||||||
* [x] Automatic local backups of your files, in case of accidental data loss. Stores up to 5 snapshots of each brew edited in your browser, incrementing from a few minutes old to a maximum of several days. Restore a backup by clicking an entry in the new {{openSans **:fas_clock_rotate_left: HISTORY**}} button in the snippet bar.
|
|
||||||
|
|
||||||
Fixes issue [#3070](https://github.com/naturalcrit/homebrewery/issues/3070)
|
|
||||||
|
|
||||||
* [x] Fix issue with legacy brews breaking on Share page
|
|
||||||
|
|
||||||
Fixes issue [#3764](https://github.com/naturalcrit/homebrewery/issues/3764)
|
|
||||||
|
|
||||||
* [x] Fix print size when printing a zoomed document
|
|
||||||
|
|
||||||
Fixes issue [#3744](https://github.com/naturalcrit/homebrewery/issues/3744)
|
|
||||||
|
|
||||||
##### All
|
|
||||||
|
|
||||||
* [x] Background code cleanup, security fixes, dev tool improvements, dependency updates, prep for upcoming features, etc.
|
|
||||||
}}
|
|
||||||
|
|
||||||
### Wednesday 9/25/2024 - v3.15.1
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### calculuschild
|
|
||||||
|
|
||||||
* [x] Background fixes to handle Google Drive issues
|
|
||||||
|
|
||||||
* [x] Remove duplicate error logging
|
|
||||||
|
|
||||||
##### calculuschild, 5e-Cleric
|
|
||||||
|
|
||||||
* [x] Fix links in {{openSans **RECENT BREWS :fas_clock_rotate_left:**}} and user {{openSans **BREWS :fas_beer_mug_empty:**}} pointing to trashed Google Drive files after transferring from Google to Homebrewery storage
|
|
||||||
|
|
||||||
Fixes issue [#3776](https://github.com/naturalcrit/homebrewery/issues/3776)
|
|
||||||
}}
|
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Wednesday 9/04/2024 - v3.15.0
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### 5e-Cleric, abquintic, calculuschild, Gazook89, G-Ambatte, Ericsheid, Kaiburr
|
|
||||||
|
|
||||||
* [x] New {{openSans **VAULT** {{fas,fa-dungeon}}}} page 🎉🎉🎉
|
|
||||||
:
|
|
||||||
All **PUBLISHED** brews ({{openSans :fas_circle_info: **Properties**}} menu) will be searchable, by title or author, and filtered by renderer. More features and adjustments will be coming.
|
|
||||||
:
|
|
||||||
Note: If any of your own brews are not showing up in search (particularly if stored on Google Drive), please edit and re-save to ensure our database has the data needed from document to be searchable.
|
|
||||||
|
|
||||||
Fixes issue [#697](https://github.com/naturalcrit/homebrewery/issues/697)
|
|
||||||
|
|
||||||
##### Gazook89
|
|
||||||
|
|
||||||
* [x] Auto-focus on text editor when switching editor tabs
|
|
||||||
}}
|
|
||||||
|
|
||||||
### Wednesday 8/28/2024 - v3.14.3
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### calculuschild, G-Ambatte
|
|
||||||
|
|
||||||
* [x] New {{openSans **IMAGES → {{fac,image-wrap-left}} IMAGE WRAP LEFT/RIGHT**}} snippets
|
|
||||||
|
|
||||||
Fixes issue [#380](https://github.com/naturalcrit/homebrewery/issues/380)
|
|
||||||
|
|
||||||
* [x] Fix v3.14.2 bug with `꞉꞉꞉꞉` failing after tables
|
|
||||||
|
|
||||||
##### 5e-Cleric
|
|
||||||
|
|
||||||
* [x] Fix Account page crash when not logged in
|
|
||||||
|
|
||||||
Fixes issue [#3605](https://github.com/naturalcrit/homebrewery/issues/3605)
|
|
||||||
|
|
||||||
##### abquintic
|
|
||||||
|
|
||||||
* [x] Fix jump hotkeys conflicting with `CTRL + SHIFT`. Preview and Source movement shortcuts now use `CTRL + SHIFT + META + LEFT\RIGHTARROW`
|
|
||||||
|
|
||||||
##### G-Ambatte
|
|
||||||
|
|
||||||
* [x] Fix display issue with image wrap icons
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
|
||||||
### Tuesday 8/27/2024 - v3.14.2
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### calculuschild
|
|
||||||
|
|
||||||
* [x] Reroute invalid urls to homepage
|
|
||||||
|
|
||||||
Fixes issues [#3269](https://github.com/naturalcrit/homebrewery/issues/3629)
|
|
||||||
|
|
||||||
* [x] Background dependency updates
|
|
||||||
|
|
||||||
##### G-Ambatte
|
|
||||||
|
|
||||||
* [x] Add route to get brew styling via `/css/shareId`
|
|
||||||
|
|
||||||
Fixes issues [#1097](https://github.com/naturalcrit/homebrewery/issues/1097)
|
|
||||||
|
|
||||||
* [x] Fix `:emojis:` preventing code folding
|
|
||||||
|
|
||||||
Fixes issues [#3604](https://github.com/naturalcrit/homebrewery/issues/3604)
|
|
||||||
|
|
||||||
* [x] Fix mask image warping when rotated and stretched
|
|
||||||
|
|
||||||
Fixes issues [#3636](https://github.com/naturalcrit/homebrewery/issues/3636)
|
|
||||||
|
|
||||||
* [x] Fix Table of Contents uppercasing
|
|
||||||
|
|
||||||
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
|
|
||||||
|
|
||||||
##### abquintic
|
|
||||||
|
|
||||||
* [x] Create globally unique Header IDs across pages
|
|
||||||
|
|
||||||
Fixes issues [#1430](https://github.com/naturalcrit/homebrewery/issues/1430)
|
|
||||||
|
|
||||||
* [x] Fix colon `꞉꞉꞉꞉` being parsed in codeblocks
|
|
||||||
|
|
||||||
* [x] Prevent crashes when loading undefined renderer or theme bundle
|
|
||||||
|
|
||||||
* [x] Add Jump-To hotkeys
|
|
||||||
|
|
||||||
* Use `CTRL/META + SHIFT + LEFTARROW` to brewJump
|
|
||||||
* Use `CTRL/META + SHIFT + RIGHTARROW` to sourceJump
|
|
||||||
|
|
||||||
* [x] Prevent reload from clobbering modified fresh clones
|
|
||||||
|
|
||||||
##### 5e-Cleric, Gazook89
|
|
||||||
|
|
||||||
* [x] Viewer tools for zoom/page navigation
|
|
||||||
}}
|
|
||||||
|
|
||||||
### Tuesday 8/13/2024 - v3.14.1
|
|
||||||
|
|
||||||
{{taskList
|
|
||||||
##### abquintic
|
|
||||||
|
|
||||||
* [x] Allow Table of Contents to flow across columns
|
|
||||||
|
|
||||||
Fixes issues [#2563](https://github.com/naturalcrit/homebrewery/issues/2563)
|
|
||||||
|
|
||||||
* [x] Fix unusual margin spacing for adjacent `.descriptive` and `.wide` blocks
|
|
||||||
|
|
||||||
Fixes issues [#2688](https://github.com/naturalcrit/homebrewery/issues/2688)
|
|
||||||
|
|
||||||
* [x] Add code folding to :fas_paintbrush: {{openSans **STYLE**}} tab
|
|
||||||
|
|
||||||
##### G-Ambatte
|
|
||||||
|
|
||||||
* [x] Fix edge case where Table of Contents generator changed capitalization of headings
|
|
||||||
|
|
||||||
Fixes issues [#3572](https://github.com/naturalcrit/homebrewery/issues/3572)
|
|
||||||
|
|
||||||
* [x] Fix **Ink Friendly** snippet causing unselectable PDF text
|
|
||||||
|
|
||||||
Fixes issues [#3563](https://github.com/naturalcrit/homebrewery/issues/3563)
|
|
||||||
|
|
||||||
* [x] Prevent brews selecting themselves as a theme
|
|
||||||
|
|
||||||
Fixes issues [#3614](https://github.com/naturalcrit/homebrewery/issues/3614)
|
|
||||||
|
|
||||||
* [x] Fix info pages (`/faq`, `/migrate`, etc.) showing blank authorship info
|
|
||||||
|
|
||||||
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
|
||||||
|
|
||||||
* [x] Add `abs()`, `sign()` and `signed()` functions to variable syntax math handler
|
|
||||||
|
|
||||||
Fixes issues [#3537](https://github.com/naturalcrit/homebrewery/issues/3537)
|
|
||||||
|
|
||||||
* [x] Fix variable math handler not processing commas (i.e., in `$[max(varA,varB)]`
|
|
||||||
|
|
||||||
Fixes issues [#3613](https://github.com/naturalcrit/homebrewery/issues/3613)
|
|
||||||
|
|
||||||
* [x] Fix variable math handler scrambling variables with names that are subsets of other variables
|
|
||||||
|
|
||||||
Fixes issues [#3622](https://github.com/naturalcrit/homebrewery/issues/3622)
|
|
||||||
|
|
||||||
##### calculuschild
|
|
||||||
|
|
||||||
* [x] Fix `/migrate` page using an editor context instead of share context
|
|
||||||
|
|
||||||
##### 5e-Cleric
|
|
||||||
|
|
||||||
* [x] Fix Monster Stat Blocks losing color in Safari
|
|
||||||
}}
|
|
||||||
|
|
||||||
\page
|
|
||||||
|
|
||||||
### Monday 7/29/2024 - v3.14.0
|
|
||||||
{{taskList
|
|
||||||
|
|
||||||
##### abquintic, calculuschild
|
|
||||||
|
|
||||||
* [x] Alternative Brew Themes, including importing other brews as a base theme.
|
|
||||||
|
|
||||||
- In the :fas_circle_info: **Properties** menu, find the new {{openSans **THEME**}} dropdown. It lists Brew Themes, including a new **Blank** theme as a simpler basis for custom styling.
|
|
||||||
- Brews tagged with `meta:theme` will appear in the Brew Themes list. Selecting one loads its :fas_paintbrush: **Style** tab contents as the CSS basis for the current brew, allowing one brew to style multiple documents.
|
|
||||||
- Brews with `meta:theme` can also select their own Theme, i.e. layering Themes on top of each other.
|
|
||||||
- The next goal is to make **Published** Themes shareable between users.
|
|
||||||
|
|
||||||
|
|
||||||
Fixes issues [#1899](https://github.com/naturalcrit/homebrewery/issues/1899), [#3085](https://github.com/naturalcrit/homebrewery/issues/3085)
|
|
||||||
|
|
||||||
##### G-Ambatte
|
|
||||||
|
|
||||||
* [x] Fix Drop-cap font becoming corrupted when Bold
|
|
||||||
|
|
||||||
Fixes issues [#3551](https://github.com/naturalcrit/homebrewery/issues/3551)
|
|
||||||
|
|
||||||
* [x] Fixes to UI styling
|
|
||||||
|
|
||||||
Fixes issues [#3568](https://github.com/naturalcrit/homebrewery/issues/3568)
|
|
||||||
|
|
||||||
}}
|
|
||||||
|
|
||||||
|
|
||||||
### Saturday 6/7/2024 - v3.13.1
|
### Saturday 6/7/2024 - v3.13.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -433,6 +131,8 @@ Fixes issue [#3298](https://github.com/naturalcrit/homebrewery/issues/3298)
|
|||||||
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
Fixes issue [#3397](https://github.com/naturalcrit/homebrewery/issues/3397)
|
||||||
}}
|
}}
|
||||||
|
|
||||||
|
\column
|
||||||
|
|
||||||
### Monday 18/3/2024 - v3.12.0
|
### Monday 18/3/2024 - v3.12.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
@@ -724,7 +424,7 @@ Fixes issue [#2729](https://github.com/naturalcrit/homebrewery/issues/2729),
|
|||||||
### Thursday 17/08/2023 - v3.9.2
|
### Thursday 17/08/2023 - v3.9.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Fix links to certain old Google Drive files
|
* [x] Fix links to certain old Google Drive files
|
||||||
|
|
||||||
@@ -782,7 +482,7 @@ Fixes issue [#1924](https://github.com/naturalcrit/homebrewery/issues/1924)
|
|||||||
### Friday 02/06/2023 - v3.9.0
|
### Friday 02/06/2023 - v3.9.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
* [x] Fix some files not showing up on userpage when user has a large number of brews in Google Drive
|
||||||
|
|
||||||
@@ -879,7 +579,7 @@ Fixes issues [#2731](https://github.com/naturalcrit/homebrewery/issues/2731)
|
|||||||
### Monday 13/03/2023 - v3.7.2
|
### Monday 13/03/2023 - v3.7.2
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
* [x] Fix wide Monster Stat Blocks not spanning columns on Legacy
|
||||||
}}
|
}}
|
||||||
@@ -902,7 +602,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
|||||||
* [x] Updated the Google Drive icon
|
* [x] Updated the Google Drive icon
|
||||||
* [x] Backend fix to unit tests failing intermittently
|
* [x] Backend fix to unit tests failing intermittently
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Fix PDF pixelation on CoverPage text outlines
|
* [x] Fix PDF pixelation on CoverPage text outlines
|
||||||
}}
|
}}
|
||||||
@@ -914,7 +614,7 @@ Fixes issues [#1569](https://github.com/naturalcrit/homebrewery/issues/1569)
|
|||||||
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
|
**NOTE:** Some new snippets will now show a {{beta BETA}} tag. Feel free to use them, but be aware we may change how they work depending on your feedback.
|
||||||
}}
|
}}
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
|
* [x] New {{openSans **IMAGES → WATERCOLOR EDGE** {{fac,mask-edge}} }} and {{openSans **WATERCOLOR CORNER** {{fac,mask-corner}} }} snippets for V3, which adds a stylish watercolor texture to the edge of your images! (Thanks to /u/flamableconcrete on Reddit for providing these image masks!)
|
||||||
|
|
||||||
@@ -1058,7 +758,7 @@ Fixes issues [#1670](https://github.com/naturalcrit/homebrewery/issues/1670)
|
|||||||
### Thursday 28/10/2022 - v3.3.1
|
### Thursday 28/10/2022 - v3.3.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Fixes to several broken CSS styles from v3.3.0
|
* [x] Fixes to several broken CSS styles from v3.3.0
|
||||||
|
|
||||||
@@ -1073,7 +773,7 @@ Fixes issues [#2468](https://github.com/naturalcrit/homebrewery/issues/2468)
|
|||||||
### Friday 19/10/2022 - v3.3.0
|
### Friday 19/10/2022 - v3.3.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Fix for tables broken by Chrome v106
|
* [x] Fix for tables broken by Chrome v106
|
||||||
|
|
||||||
@@ -1156,7 +856,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
### Wednesday 31/08/2022 - v3.2.1
|
### Wednesday 31/08/2022 - v3.2.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] Reference Links should now work inside tables
|
* [x] Reference Links should now work inside tables
|
||||||
|
|
||||||
@@ -1182,7 +882,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
### Saturday 27/08/2022 - v3.2.0
|
### Saturday 27/08/2022 - v3.2.0
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild
|
##### Calculuschild
|
||||||
|
|
||||||
* [x] The V3 renderer is now the default for new brews.
|
* [x] The V3 renderer is now the default for new brews.
|
||||||
|
|
||||||
@@ -1209,7 +909,7 @@ Fixes issues [#2317](https://github.com/naturalcrit/homebrewery/issues/2317), [
|
|||||||
### Thursday 09/06/2022 - v3.1.1
|
### Thursday 09/06/2022 - v3.1.1
|
||||||
{{taskList
|
{{taskList
|
||||||
|
|
||||||
##### calculuschild:
|
##### Calculuschild:
|
||||||
|
|
||||||
* [x] Fixed class table decorations appearing on top of the table in PDF output.
|
* [x] Fixed class table decorations appearing on top of the table in PDF output.
|
||||||
|
|
||||||
|
|||||||
@@ -2,44 +2,35 @@ require('./admin.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
|
||||||
const BrewUtils = require('./brewUtils/brewUtils.jsx');
|
|
||||||
const NotificationUtils = require('./notificationUtils/notificationUtils.jsx');
|
|
||||||
|
|
||||||
const tabGroups = ['brew', 'notifications'];
|
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
||||||
|
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
||||||
|
const BrewCompress = require ('./brewCompress/brewCompress.jsx');
|
||||||
|
const Stats = require('./stats/stats.jsx');
|
||||||
|
|
||||||
const Admin = createClass({
|
const Admin = createClass({
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState : function(){
|
|
||||||
return ({
|
|
||||||
currentTab : 'brew'
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleClick : function(newTab){
|
|
||||||
if(this.state.currentTab === newTab) return;
|
|
||||||
this.setState({
|
|
||||||
currentTab : newTab
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='admin'>
|
return <div className='admin'>
|
||||||
|
|
||||||
<header>
|
<header>
|
||||||
<div className='container'>
|
<div className='container'>
|
||||||
<i className='fas fa-rocket' />
|
<i className='fas fa-rocket' />
|
||||||
homebrewery admin
|
homebrewery admin
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
<main className='container'>
|
<div className='container'>
|
||||||
<nav className='tabs'>
|
<Stats />
|
||||||
{tabGroups.map((tab, idx)=>{ return <button className={tab===this.state.currentTab ? 'active' : ''} key={idx} onClick={()=>{ return this.handleClick(tab); }}>{tab.toUpperCase()}</button>; })}
|
<hr />
|
||||||
</nav>
|
<BrewLookup />
|
||||||
{this.state.currentTab==='brew' && <BrewUtils />}
|
<hr />
|
||||||
{this.state.currentTab==='notifications' && <NotificationUtils />}
|
<BrewCleanup />
|
||||||
</main>
|
<hr />
|
||||||
|
<BrewCompress />
|
||||||
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -6,95 +6,39 @@
|
|||||||
|
|
||||||
@import 'font-awesome/css/font-awesome.css';
|
@import 'font-awesome/css/font-awesome.css';
|
||||||
|
|
||||||
html,body, #reactContainer, .naturalCrit { min-height : 100%; }
|
html,body, #reactContainer, .naturalCrit{
|
||||||
|
min-height : 100%;
|
||||||
|
}
|
||||||
|
|
||||||
@sidebarWidth : 250px;
|
@sidebarWidth : 250px;
|
||||||
|
|
||||||
body{
|
body{
|
||||||
height : 100%;
|
background-color : #eee;
|
||||||
padding : 0;
|
|
||||||
margin : 0;
|
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
color : #4b5055;
|
||||||
font-weight : 100;
|
font-weight : 100;
|
||||||
color : #4B5055;
|
|
||||||
background-color : #EEEEEE;
|
|
||||||
text-rendering : optimizeLegibility;
|
text-rendering : optimizeLegibility;
|
||||||
|
margin : 0;
|
||||||
|
padding : 0;
|
||||||
|
height : 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
:where(.admin) {
|
.admin{
|
||||||
|
|
||||||
header{
|
header{
|
||||||
padding : 20px 0px;
|
|
||||||
margin-bottom : 30px;
|
|
||||||
font-size : 2em;
|
|
||||||
color : white;
|
|
||||||
background-color : @red;
|
background-color : @red;
|
||||||
i { margin-right : 30px; }
|
font-size: 2em;
|
||||||
}
|
padding : 20px 0px;
|
||||||
|
|
||||||
hr { margin : 30px 0px; }
|
|
||||||
|
|
||||||
:where(.container) {
|
|
||||||
input {
|
|
||||||
height : 33px;
|
|
||||||
padding : 0px 10px;
|
|
||||||
margin-bottom : 20px;
|
|
||||||
font-family : monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
height : 37px;
|
|
||||||
vertical-align : middle;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl {
|
|
||||||
@maxItemWidth : 132px;
|
|
||||||
dt {
|
|
||||||
float : left;
|
|
||||||
width : @maxItemWidth;
|
|
||||||
clear : left;
|
|
||||||
text-align : right;
|
|
||||||
&::after { content : ' : '; }
|
|
||||||
}
|
|
||||||
dd {
|
|
||||||
height : 1em;
|
|
||||||
padding : 0 0 0.5em 0;
|
|
||||||
margin-left : @maxItemWidth + 6px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.tabs button {
|
|
||||||
margin-right : 3px;
|
|
||||||
margin-left : 3px;
|
|
||||||
color : black;
|
|
||||||
background-color : #EEEEEE;
|
|
||||||
border : 1px solid #444444;
|
|
||||||
border-radius : 5px;
|
|
||||||
&:hover {
|
|
||||||
color : #EEEEEE;
|
|
||||||
background-color : #444444;
|
|
||||||
}
|
|
||||||
&.active {
|
|
||||||
margin-right : 2px;
|
|
||||||
margin-left : 2px;
|
|
||||||
text-decoration : underline;
|
|
||||||
background-color : #CCCCCC;
|
|
||||||
border : 2px solid #444444;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.notificationUtils {
|
|
||||||
display : flex;
|
|
||||||
gap : 50px;
|
|
||||||
justify-content : space-between;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.error {
|
|
||||||
background: rgb(178, 54, 54);
|
|
||||||
color : white;
|
color : white;
|
||||||
font-weight: 900;
|
margin-bottom: 30px;
|
||||||
margin-block:10px;
|
i{
|
||||||
padding:10px;
|
margin-right: 30px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr{
|
||||||
|
margin : 30px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|||||||
10
client/admin/brewCleanup/brewCleanup.less
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.BrewCleanup{
|
||||||
|
.removeBox{
|
||||||
|
margin-top: 20px;
|
||||||
|
button{
|
||||||
|
background-color: @red;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
10
client/admin/brewCompress/brewCompress.less
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
.BrewCompress{
|
||||||
|
.removeBox{
|
||||||
|
margin-top: 20px;
|
||||||
|
button{
|
||||||
|
background-color: @red;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
require('./brewLookup.less');
|
require('./brewLookup.less');
|
||||||
|
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
@@ -17,40 +16,19 @@ const BrewLookup = createClass({
|
|||||||
query : '',
|
query : '',
|
||||||
foundBrew : null,
|
foundBrew : null,
|
||||||
searching : false,
|
searching : false,
|
||||||
error : null,
|
error : null
|
||||||
scriptCount : 0
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
handleChange(e){
|
handleChange(e){
|
||||||
this.setState({ query: e.target.value });
|
this.setState({ query: e.target.value });
|
||||||
},
|
},
|
||||||
lookup(){
|
lookup(){
|
||||||
this.setState({ searching: true, error: null, scriptCount: 0 });
|
this.setState({ searching: true, error: null });
|
||||||
|
|
||||||
request.get(`/admin/lookup/${this.state.query}`)
|
request.get(`/admin/lookup/${this.state.query}`)
|
||||||
.then((res)=>{
|
.then((res)=>this.setState({ foundBrew: res.body }))
|
||||||
const foundBrew = res.body;
|
|
||||||
const scriptCheck = foundBrew?.text.match(/(<\/?s)cript/g);
|
|
||||||
this.setState({
|
|
||||||
foundBrew : foundBrew,
|
|
||||||
scriptCount : scriptCheck?.length || 0,
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch((err)=>this.setState({ error: err }))
|
.catch((err)=>this.setState({ error: err }))
|
||||||
.finally(()=>{
|
.finally(()=>this.setState({ searching: false }));
|
||||||
this.setState({
|
|
||||||
searching : false
|
|
||||||
});
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
async cleanScript(){
|
|
||||||
if(!this.state.foundBrew?.shareId) return;
|
|
||||||
|
|
||||||
await request.put(`/admin/clean/script/${this.state.foundBrew.shareId}`)
|
|
||||||
.catch((err)=>{ this.setState({ error: err }); return; });
|
|
||||||
|
|
||||||
this.lookup();
|
|
||||||
},
|
},
|
||||||
|
|
||||||
renderFoundBrew(){
|
renderFoundBrew(){
|
||||||
@@ -69,23 +47,12 @@ const BrewLookup = createClass({
|
|||||||
<dt>Share Link</dt>
|
<dt>Share Link</dt>
|
||||||
<dd><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/share/{brew.shareId}</a></dd>
|
<dd><a href={`/share/${brew.shareId}`} target='_blank' rel='noopener noreferrer'>/share/{brew.shareId}</a></dd>
|
||||||
|
|
||||||
<dt>Created Time</dt>
|
|
||||||
<dd>{brew.createdAt ? Moment(brew.createdAt).toLocaleString() : 'No creation date'}</dd>
|
|
||||||
|
|
||||||
<dt>Last Updated</dt>
|
<dt>Last Updated</dt>
|
||||||
<dd>{Moment(brew.updatedAt).fromNow()}</dd>
|
<dd>{Moment(brew.updatedAt).fromNow()}</dd>
|
||||||
|
|
||||||
<dt>Num of Views</dt>
|
<dt>Num of Views</dt>
|
||||||
<dd>{brew.views}</dd>
|
<dd>{brew.views}</dd>
|
||||||
|
|
||||||
<dt>SCRIPT tags detected</dt>
|
|
||||||
<dd>{this.state.scriptCount}</dd>
|
|
||||||
</dl>
|
</dl>
|
||||||
{this.state.scriptCount > 0 &&
|
|
||||||
<div className='cleanButton'>
|
|
||||||
<button onClick={this.cleanScript}>CLEAN BREW</button>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
30
client/admin/brewLookup/brewLookup.less
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
|
||||||
|
.brewLookup{
|
||||||
|
input{
|
||||||
|
height : 33px;
|
||||||
|
margin-bottom : 20px;
|
||||||
|
padding : 0px 10px;
|
||||||
|
font-family : monospace;
|
||||||
|
}
|
||||||
|
button{
|
||||||
|
vertical-align : middle;
|
||||||
|
height : 37px;
|
||||||
|
}
|
||||||
|
dl{
|
||||||
|
@maxItemWidth : 132px;
|
||||||
|
dt{
|
||||||
|
float : left;
|
||||||
|
clear : left;
|
||||||
|
width : @maxItemWidth;
|
||||||
|
text-align : right;
|
||||||
|
&::after {
|
||||||
|
content: " : ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dd{
|
||||||
|
height : 1em;
|
||||||
|
margin-left : @maxItemWidth + 6px;
|
||||||
|
padding : 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.BrewCleanup {
|
|
||||||
.removeBox {
|
|
||||||
margin-top : 20px;
|
|
||||||
button {
|
|
||||||
margin-right : 10px;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
.BrewCompress {
|
|
||||||
.removeBox {
|
|
||||||
margin-top : 20px;
|
|
||||||
button {
|
|
||||||
margin-right : 10px;
|
|
||||||
background-color : @red;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
.brewLookup {
|
|
||||||
.cleanButton {
|
|
||||||
display : inline-block;
|
|
||||||
width : 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
const createClass = require('create-react-class');
|
|
||||||
|
|
||||||
|
|
||||||
const BrewCleanup = require('./brewCleanup/brewCleanup.jsx');
|
|
||||||
const BrewLookup = require('./brewLookup/brewLookup.jsx');
|
|
||||||
const BrewCompress = require ('./brewCompress/brewCompress.jsx');
|
|
||||||
const Stats = require('./stats/stats.jsx');
|
|
||||||
|
|
||||||
const BrewUtils = createClass({
|
|
||||||
render : function(){
|
|
||||||
return <>
|
|
||||||
<Stats />
|
|
||||||
<hr />
|
|
||||||
<BrewLookup />
|
|
||||||
<hr />
|
|
||||||
<BrewCleanup />
|
|
||||||
<hr />
|
|
||||||
<BrewCompress />
|
|
||||||
</>;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = BrewUtils;
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
.Stats {
|
|
||||||
position : relative;
|
|
||||||
|
|
||||||
.pending {
|
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
left : 0px;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
background-color : rgba(238,238,238, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
require('./notificationAdd.less');
|
|
||||||
const React = require('react');
|
|
||||||
const { useState, useRef } = require('react');
|
|
||||||
const request = require('superagent');
|
|
||||||
|
|
||||||
const NotificationAdd = ()=>{
|
|
||||||
const [notificationResult, setNotificationResult] = useState(null);
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const dismissKeyRef = useRef(null);
|
|
||||||
const titleRef = useRef(null);
|
|
||||||
const textRef = useRef(null);
|
|
||||||
const startAtRef = useRef(null);
|
|
||||||
const stopAtRef = useRef(null);
|
|
||||||
|
|
||||||
const saveNotification = async ()=>{
|
|
||||||
const dismissKey = dismissKeyRef.current.value;
|
|
||||||
const title = titleRef.current.value;
|
|
||||||
const text = textRef.current.value;
|
|
||||||
const startAt = new Date(startAtRef.current.value);
|
|
||||||
const stopAt = new Date(stopAtRef.current.value);
|
|
||||||
|
|
||||||
// Basic validation
|
|
||||||
if(!dismissKey || !title || !text || isNaN(startAt.getTime()) || isNaN(stopAt.getTime())) {
|
|
||||||
setError('All fields are required');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if(startAt >= stopAt) {
|
|
||||||
setError('End date must be after the start date!');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = {
|
|
||||||
dismissKey,
|
|
||||||
title,
|
|
||||||
text,
|
|
||||||
startAt : startAt?.toISOString() ?? '',
|
|
||||||
stopAt : stopAt?.toISOString() ?? '',
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
|
||||||
setSearching(true);
|
|
||||||
setError(null);
|
|
||||||
const response = await request.post('/admin/notification/add').send(data);
|
|
||||||
console.log(response.body);
|
|
||||||
|
|
||||||
// Reset form fields
|
|
||||||
dismissKeyRef.current.value = '';
|
|
||||||
titleRef.current.value = '';
|
|
||||||
textRef.current.value = '';
|
|
||||||
|
|
||||||
setNotificationResult('Notification successfully created.');
|
|
||||||
setSearching(false);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err.response.body.message);
|
|
||||||
setError(`Error saving notification: ${err.response.body.message}`);
|
|
||||||
setSearching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='notificationAdd'>
|
|
||||||
<h2>Add Notification</h2>
|
|
||||||
|
|
||||||
<label className='field'>
|
|
||||||
Dismiss Key:
|
|
||||||
<input className='fieldInput' type='text' ref={dismissKeyRef} required
|
|
||||||
placeholder='dismiss_notif_drive'
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className='field'>
|
|
||||||
Title:
|
|
||||||
<input className='fieldInput' type='text' ref={titleRef} required
|
|
||||||
placeholder='Stop using Google Drive as image host'
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className='field'>
|
|
||||||
Text:
|
|
||||||
<textarea className='fieldInput' type='text' ref={textRef} required
|
|
||||||
placeholder='Google Drive is not an image hosting site, you should not use it as such.'
|
|
||||||
>
|
|
||||||
</textarea>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className='field'>
|
|
||||||
Start Date:
|
|
||||||
<input type='date' className='fieldInput' ref={startAtRef} required/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label className='field'>
|
|
||||||
End Date:
|
|
||||||
<input type='date' className='fieldInput' ref={stopAtRef} required/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<div className='notificationResult'>{notificationResult}</div>
|
|
||||||
|
|
||||||
<button className='notificationSave' onClick={saveNotification} disabled={searching}>
|
|
||||||
<i className={`fas ${searching ? 'fa-spin fa-spinner' : 'fa-save'}`}/>
|
|
||||||
Save Notification
|
|
||||||
</button>
|
|
||||||
{error && <div className='error'>{error}</div>}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = NotificationAdd;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
.notificationAdd {
|
|
||||||
position : relative;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
width : 500px;
|
|
||||||
|
|
||||||
.field {
|
|
||||||
display : grid;
|
|
||||||
grid-template-columns : 120px 150px;
|
|
||||||
align-items : center;
|
|
||||||
justify-items : stretch;
|
|
||||||
width : 100%;
|
|
||||||
margin-bottom : 20px;
|
|
||||||
|
|
||||||
|
|
||||||
input {
|
|
||||||
height : 33px;
|
|
||||||
padding : 0px 10px;
|
|
||||||
margin-bottom : unset;
|
|
||||||
font-family : monospace;
|
|
||||||
}
|
|
||||||
|
|
||||||
textarea {
|
|
||||||
width : 50ch;
|
|
||||||
min-height : 7em;
|
|
||||||
max-height : 20em;
|
|
||||||
resize : vertical;
|
|
||||||
padding : 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width: 200px;
|
|
||||||
|
|
||||||
i { margin-right : 10px; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
require('./notificationLookup.less');
|
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
const { useState } = require('react');
|
|
||||||
const request = require('superagent');
|
|
||||||
const Moment = require('moment');
|
|
||||||
|
|
||||||
const NotificationDetail = ({ notification, onDelete })=>(
|
|
||||||
<>
|
|
||||||
<dl>
|
|
||||||
<dt>Key</dt>
|
|
||||||
<dd>{notification.dismissKey}</dd>
|
|
||||||
|
|
||||||
<dt>Title</dt>
|
|
||||||
<dd>{notification.title || 'No Title'}</dd>
|
|
||||||
|
|
||||||
<dt>Created</dt>
|
|
||||||
<dd>{Moment(notification.createdAt).format('LLLL')}</dd>
|
|
||||||
|
|
||||||
<dt>Start</dt>
|
|
||||||
<dd>{Moment(notification.startAt).format('LLLL') || 'No Start Time'}</dd>
|
|
||||||
|
|
||||||
<dt>Stop</dt>
|
|
||||||
<dd>{Moment(notification.stopAt).format('LLLL') || 'No End Time'}</dd>
|
|
||||||
|
|
||||||
<dt>Text</dt>
|
|
||||||
<dd>{notification.text || 'No Text'}</dd>
|
|
||||||
</dl>
|
|
||||||
<button onClick={()=>onDelete(notification.dismissKey)}>DELETE</button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const NotificationLookup = ()=>{
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
const [notifications, setNotifications] = useState([]);
|
|
||||||
|
|
||||||
const lookupAll = async ()=>{
|
|
||||||
setSearching(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await request.get('/admin/notification/all');
|
|
||||||
setNotifications(res.body || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
setError(`Error looking up notifications: ${err.response.body.message}`);
|
|
||||||
} finally {
|
|
||||||
setSearching(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const deleteNotification = async (dismissKey)=>{
|
|
||||||
if(!dismissKey) return;
|
|
||||||
|
|
||||||
const confirmed = window.confirm(
|
|
||||||
`Really delete notification ${dismissKey}?`
|
|
||||||
);
|
|
||||||
if(!confirmed) {
|
|
||||||
console.log('Delete notification cancelled');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log('Delete notification confirm');
|
|
||||||
try {
|
|
||||||
await request.delete(`/admin/notification/delete/${dismissKey}`);
|
|
||||||
lookupAll();
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
setError(`Error deleting notification: ${err.response.body.message}`);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNotificationsList = ()=>{
|
|
||||||
if(error)
|
|
||||||
return <div className='error'>{error}</div>;
|
|
||||||
|
|
||||||
if(notifications.length === 0)
|
|
||||||
return <div className='noNotification'>No notifications available.</div>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ul className='notificationList'>
|
|
||||||
{notifications.map((notification)=>(
|
|
||||||
<li key={notification.dismissKey} >
|
|
||||||
<details>
|
|
||||||
<summary>{notification.title || 'No Title'}</summary>
|
|
||||||
<NotificationDetail notification={notification} onDelete={deleteNotification} />
|
|
||||||
</details>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='notificationLookup'>
|
|
||||||
<h2>Check all Notifications</h2>
|
|
||||||
<button onClick={lookupAll}>
|
|
||||||
<i className={`fas ${searching ? 'fa-spin fa-spinner' : 'fa-search'}`} />
|
|
||||||
</button>
|
|
||||||
{renderNotificationsList()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = NotificationLookup;
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
|
|
||||||
.notificationLookup {
|
|
||||||
width : 450px;
|
|
||||||
height : fit-content;
|
|
||||||
|
|
||||||
.notificationList {
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
max-height : 500px;
|
|
||||||
margin-block : 20px;
|
|
||||||
overflow : auto;
|
|
||||||
border : 1px solid;
|
|
||||||
border-radius : 5px;
|
|
||||||
|
|
||||||
li {
|
|
||||||
padding : 10px;
|
|
||||||
background : #CCCCCC;
|
|
||||||
|
|
||||||
&:nth-child(even) { background : #DDDDDD; }
|
|
||||||
&:first-child {
|
|
||||||
border-top-left-radius : 5px;
|
|
||||||
border-top-right-radius : 5px;
|
|
||||||
}
|
|
||||||
&:last-child {
|
|
||||||
border-bottom-right-radius : 5px;
|
|
||||||
border-bottom-left-radius : 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
summary {
|
|
||||||
font-size : 20px;
|
|
||||||
font-weight : 900;
|
|
||||||
}
|
|
||||||
|
|
||||||
dl dt{
|
|
||||||
font-weight: 900;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.noNotification { margin-block : 20px; }
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
|
|
||||||
const NotificationLookup = require('./notificationLookup/notificationLookup.jsx');
|
|
||||||
const NotificationAdd = require('./notificationAdd/notificationAdd.jsx');
|
|
||||||
|
|
||||||
const NotificationUtils = ()=>{
|
|
||||||
return (
|
|
||||||
<section className='notificationUtils'>
|
|
||||||
<NotificationAdd />
|
|
||||||
<NotificationLookup />
|
|
||||||
</section>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = NotificationUtils;
|
|
||||||
28
client/admin/stats/stats.less
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
|
||||||
|
.Stats{
|
||||||
|
position : relative;
|
||||||
|
.pending{
|
||||||
|
position : absolute;
|
||||||
|
top : 0px;
|
||||||
|
left : 0px;
|
||||||
|
height : 100%;
|
||||||
|
width : 100%;
|
||||||
|
background-color : rgba(238,238,238, 0.5);
|
||||||
|
}
|
||||||
|
dl{
|
||||||
|
@maxItemWidth : 132px;
|
||||||
|
dt{
|
||||||
|
float : left;
|
||||||
|
clear : left;
|
||||||
|
width : @maxItemWidth;
|
||||||
|
text-align : right;
|
||||||
|
&::after {
|
||||||
|
content: " : ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dd{
|
||||||
|
margin : 0 0 0 @maxItemWidth + 10px;
|
||||||
|
padding : 0 0 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
import React, { useState, useRef, forwardRef, useEffect, cloneElement, Children } from 'react';
|
|
||||||
import './Anchored.less';
|
|
||||||
|
|
||||||
// Anchored is a wrapper component that must have as children an <AnchoredTrigger> and a <AnchoredBox> component.
|
|
||||||
// AnchoredTrigger must have a unique `id` prop, which is passed up to Anchored, saved in state on mount, and
|
|
||||||
// then passed down through props into AnchoredBox. The `id` is used for the CSS Anchor Positioning properties.
|
|
||||||
// **The Anchor Positioning API is not available in Firefox yet**
|
|
||||||
// So in Firefox the positioning isn't perfect but is likely sufficient, and FF team seems to be working on the API quickly.
|
|
||||||
|
|
||||||
|
|
||||||
const Anchored = ({ children })=>{
|
|
||||||
const [visible, setVisible] = useState(false);
|
|
||||||
const [anchorId, setAnchorId] = useState(null);
|
|
||||||
const boxRef = useRef(null);
|
|
||||||
const triggerRef = useRef(null);
|
|
||||||
|
|
||||||
// promote trigger id to Anchored id (to pass it back down to the box as "anchorId")
|
|
||||||
useEffect(()=>{
|
|
||||||
if(triggerRef.current){
|
|
||||||
setAnchorId(triggerRef.current.id);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// close box on outside click or Escape key
|
|
||||||
useEffect(()=>{
|
|
||||||
const handleClickOutside = (evt)=>{
|
|
||||||
if(
|
|
||||||
boxRef.current &&
|
|
||||||
!boxRef.current.contains(evt.target) &&
|
|
||||||
triggerRef.current &&
|
|
||||||
!triggerRef.current.contains(evt.target)
|
|
||||||
) {
|
|
||||||
setVisible(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleEscapeKey = (evt)=>{
|
|
||||||
if(evt.key === 'Escape') setVisible(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
window.addEventListener('click', handleClickOutside);
|
|
||||||
window.addEventListener('keydown', handleEscapeKey);
|
|
||||||
|
|
||||||
return ()=>{
|
|
||||||
window.removeEventListener('click', handleClickOutside);
|
|
||||||
window.removeEventListener('keydown', handleEscapeKey);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const toggleVisibility = ()=>setVisible((prev)=>!prev);
|
|
||||||
|
|
||||||
// Map children to inject necessary props
|
|
||||||
const mappedChildren = Children.map(children, (child)=>{
|
|
||||||
if(child.type === AnchoredTrigger) {
|
|
||||||
return cloneElement(child, { ref: triggerRef, toggleVisibility, visible });
|
|
||||||
}
|
|
||||||
if(child.type === AnchoredBox) {
|
|
||||||
return cloneElement(child, { ref: boxRef, visible, anchorId });
|
|
||||||
}
|
|
||||||
return child;
|
|
||||||
});
|
|
||||||
|
|
||||||
return <>{mappedChildren}</>;
|
|
||||||
};
|
|
||||||
|
|
||||||
// forward ref for AnchoredTrigger
|
|
||||||
const AnchoredTrigger = forwardRef(({ toggleVisibility, visible, children, className, ...props }, ref)=>(
|
|
||||||
<button
|
|
||||||
ref={ref}
|
|
||||||
className={`anchored-trigger${visible ? ' active' : ''} ${className}`}
|
|
||||||
onClick={toggleVisibility}
|
|
||||||
style={{ anchorName: `--${props.id}` }} // setting anchor properties here allows greater recyclability.
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</button>
|
|
||||||
));
|
|
||||||
|
|
||||||
// forward ref for AnchoredBox
|
|
||||||
const AnchoredBox = forwardRef(({ visible, children, className, anchorId, ...props }, ref)=>(
|
|
||||||
<div
|
|
||||||
ref={ref}
|
|
||||||
className={`anchored-box${visible ? ' active' : ''} ${className}`}
|
|
||||||
style={{ positionAnchor: `--${anchorId}` }} // setting anchor properties here allows greater recyclability.
|
|
||||||
{...props}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
));
|
|
||||||
|
|
||||||
export { Anchored, AnchoredTrigger, AnchoredBox };
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
|
|
||||||
|
|
||||||
.anchored-box {
|
|
||||||
position:absolute;
|
|
||||||
@supports (inset-block-start: anchor(bottom)){
|
|
||||||
inset-block-start: anchor(bottom);
|
|
||||||
}
|
|
||||||
justify-self: anchor-center;
|
|
||||||
visibility: hidden;
|
|
||||||
&.active {
|
|
||||||
visibility: visible;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,18 @@
|
|||||||
// Dialog box, for popups and modal blocking messages
|
// Dialog box, for popups and modal blocking messages
|
||||||
import React from 'react';
|
const React = require('react');
|
||||||
const { useRef, useEffect } = React;
|
const { useRef, useEffect } = React;
|
||||||
|
|
||||||
function Dialog({ dismisskeys = [], closeText = 'Close', blocking = false, ...rest }) {
|
function Dialog({ dismissKey, closeText = 'Close', blocking = false, ...rest }) {
|
||||||
const dialogRef = useRef(null);
|
const dialogRef = useRef(null);
|
||||||
|
|
||||||
useEffect(()=>{
|
useEffect(()=>{
|
||||||
if(dismisskeys.length !== 0) {
|
if(!dismissKey || !localStorage.getItem(dismissKey)) {
|
||||||
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
blocking ? dialogRef.current?.showModal() : dialogRef.current?.show();
|
||||||
}
|
}
|
||||||
}, [dialogRef.current, dismisskeys]);
|
}, []);
|
||||||
|
|
||||||
const dismiss = ()=>{
|
const dismiss = ()=>{
|
||||||
dismisskeys.forEach((key)=>{
|
dismissKey && localStorage.setItem(dismissKey, true);
|
||||||
if(key) {
|
|
||||||
localStorage.setItem(key, 'true');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dialogRef.current?.close();
|
dialogRef.current?.close();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,12 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./brewRenderer.less');
|
require('./brewRenderer.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState, useRef, useCallback, useMemo } = React;
|
const { useState, useRef, useEffect } = React;
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
const MarkdownLegacy = require('naturalcrit/markdownLegacy.js');
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
const ErrorBar = require('./errorBar/errorBar.jsx');
|
const ErrorBar = require('./errorBar/errorBar.jsx');
|
||||||
const ToolBar = require('./toolBar/toolBar.jsx');
|
|
||||||
|
|
||||||
//TODO: move to the brew renderer
|
//TODO: move to the brew renderer
|
||||||
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
const RenderWarnings = require('homebrewery/renderWarnings/renderWarnings.jsx');
|
||||||
@@ -16,7 +15,10 @@ const Frame = require('react-frame-component').default;
|
|||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../shared/helpers.js');
|
||||||
|
|
||||||
import { safeHTML } from './safeHTML.js';
|
const DOMPurify = require('dompurify');
|
||||||
|
const purifyConfig = { FORCE_BODY: true, SANITIZE_DOM: false };
|
||||||
|
|
||||||
|
const Themes = require('themes/themes.json');
|
||||||
|
|
||||||
const PAGE_HEIGHT = 1056;
|
const PAGE_HEIGHT = 1056;
|
||||||
|
|
||||||
@@ -28,7 +30,6 @@ const INITIAL_CONTENT = dedent`
|
|||||||
<base target=_blank>
|
<base target=_blank>
|
||||||
</head><body style='overflow: hidden'><div></div></body></html>`;
|
</head><body style='overflow: hidden'><div></div></body></html>`;
|
||||||
|
|
||||||
|
|
||||||
//v=====----------------------< Brew Page Component >---------------------=====v//
|
//v=====----------------------< Brew Page Component >---------------------=====v//
|
||||||
const BrewPage = (props)=>{
|
const BrewPage = (props)=>{
|
||||||
props = {
|
props = {
|
||||||
@@ -36,15 +37,15 @@ const BrewPage = (props)=>{
|
|||||||
index : 0,
|
index : 0,
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
const cleanText = safeHTML(props.contents);
|
const cleanText = props.contents; //DOMPurify.sanitize(props.contents, purifyConfig);
|
||||||
return <div className={props.className} id={`p${props.index + 1}`} style={props.style}>
|
return <div className={props.className} id={`p${props.index + 1}`} >
|
||||||
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
<div className='columnWrapper' dangerouslySetInnerHTML={{ __html: cleanText }} />
|
||||||
</div>;
|
</div>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
//v=====--------------------< Brew Renderer Component >-------------------=====v//
|
||||||
let renderedPages = [];
|
const renderedPages = [];
|
||||||
let rawPages = [];
|
let rawPages = [];
|
||||||
|
|
||||||
const BrewRenderer = (props)=>{
|
const BrewRenderer = (props)=>{
|
||||||
@@ -55,24 +56,15 @@ const BrewRenderer = (props)=>{
|
|||||||
theme : '5ePHB',
|
theme : '5ePHB',
|
||||||
lang : '',
|
lang : '',
|
||||||
errors : [],
|
errors : [],
|
||||||
currentEditorCursorPageNum : 1,
|
currentEditorPage : 0,
|
||||||
currentEditorViewPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
themeBundle : {},
|
|
||||||
onPageChange : ()=>{},
|
|
||||||
...props
|
...props
|
||||||
};
|
};
|
||||||
|
|
||||||
const [state, setState] = useState({
|
const [state, setState] = useState({
|
||||||
|
viewablePageNumber : 0,
|
||||||
|
height : PAGE_HEIGHT,
|
||||||
isMounted : false,
|
isMounted : false,
|
||||||
visibility : 'hidden'
|
visibility : 'hidden',
|
||||||
});
|
|
||||||
|
|
||||||
const [displayOptions, setDisplayOptions] = useState({
|
|
||||||
zoomLevel : 100,
|
|
||||||
spread : 'single',
|
|
||||||
startOnRight : true,
|
|
||||||
pageShadows : true
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mainRef = useRef(null);
|
const mainRef = useRef(null);
|
||||||
@@ -83,47 +75,49 @@ const BrewRenderer = (props)=>{
|
|||||||
rawPages = props.text.split(/^\\page$/gm);
|
rawPages = props.text.split(/^\\page$/gm);
|
||||||
}
|
}
|
||||||
|
|
||||||
const scrollToHash = (hash)=>{
|
useEffect(()=>{ // Unmounting steps
|
||||||
if(!hash) return;
|
return ()=>{window.removeEventListener('resize', updateSize);};
|
||||||
|
}, []);
|
||||||
|
|
||||||
const iframeDoc = document.getElementById('BrewRenderer').contentDocument;
|
const updateSize = ()=>{
|
||||||
let anchor = iframeDoc.querySelector(hash);
|
setState((prevState)=>({
|
||||||
|
...prevState,
|
||||||
if(anchor) {
|
height : mainRef.current.parentNode.clientHeight,
|
||||||
anchor.scrollIntoView({ behavior: 'smooth' });
|
}));
|
||||||
} else {
|
|
||||||
// Use MutationObserver to wait for the element if it's not immediately available
|
|
||||||
new MutationObserver((mutations, obs)=>{
|
|
||||||
anchor = iframeDoc.querySelector(hash);
|
|
||||||
if(anchor) {
|
|
||||||
anchor.scrollIntoView({ behavior: 'smooth' });
|
|
||||||
obs.disconnect();
|
|
||||||
}
|
|
||||||
}).observe(iframeDoc, { childList: true, subtree: true });
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateCurrentPage = useCallback(_.throttle((e)=>{
|
const handleScroll = (e)=>{
|
||||||
const { scrollTop, clientHeight, scrollHeight } = e.target;
|
const target = e.target;
|
||||||
const totalScrollableHeight = scrollHeight - clientHeight;
|
setState((prevState)=>({
|
||||||
const currentPageNumber = Math.max(Math.ceil((scrollTop / totalScrollableHeight) * rawPages.length), 1);
|
...prevState,
|
||||||
|
viewablePageNumber : Math.floor(target.scrollTop / target.scrollHeight * rawPages.length)
|
||||||
props.onPageChange(currentPageNumber);
|
}));
|
||||||
}, 200), []);
|
};
|
||||||
|
|
||||||
const isInView = (index)=>{
|
const isInView = (index)=>{
|
||||||
if(!state.isMounted)
|
if(!state.isMounted)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(index == props.currentEditorCursorPageNum - 1) //Already rendered before this step
|
if(index == props.currentEditorPage) //Already rendered before this step
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if(Math.abs(index - props.currentBrewRendererPageNum - 1) <= 3)
|
if(Math.abs(index - state.viewablePageNumber) <= 3)
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderPageInfo = ()=>{
|
||||||
|
return <div className='pageInfo' ref={mainRef}>
|
||||||
|
<div>
|
||||||
|
{props.renderer}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{state.viewablePageNumber + 1} / {rawPages.length}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
};
|
||||||
|
|
||||||
const renderDummyPage = (index)=>{
|
const renderDummyPage = (index)=>{
|
||||||
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
return <div className='phb page' id={`p${index + 1}`} key={index}>
|
||||||
<i className='fas fa-spinner fa-spin' />
|
<i className='fas fa-spinner fa-spin' />
|
||||||
@@ -131,9 +125,10 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const renderStyle = ()=>{
|
const renderStyle = ()=>{
|
||||||
const themeStyles = props.themeBundle?.joinedStyles ?? '<style>@import url("/themes/V3/Blank/style.css");</style>';
|
if(!props.style) return;
|
||||||
const cleanStyle = safeHTML(`${themeStyles} \n\n <style> ${props.style} </style>`);
|
const cleanStyle = props.style; //DOMPurify.sanitize(props.style, purifyConfig);
|
||||||
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: cleanStyle }} />;
|
//return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style>@layer styleTab {\n${sanitizeScriptTags(props.style)}\n} </style>` }} />;
|
||||||
|
return <div style={{ display: 'none' }} dangerouslySetInnerHTML={{ __html: `<style> ${cleanStyle} </style>` }} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderPage = (pageText, index)=>{
|
const renderPage = (pageText, index)=>{
|
||||||
@@ -143,13 +138,7 @@ const BrewRenderer = (props)=>{
|
|||||||
} else {
|
} else {
|
||||||
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
pageText += `\n\n \n\\column\n `; //Artificial column break at page end to emulate column-fill:auto (until `wide` is used, when column-fill:balance will reappear)
|
||||||
const html = Markdown.render(pageText, index);
|
const html = Markdown.render(pageText, index);
|
||||||
|
return <BrewPage className='page' index={index} key={index} contents={html} />;
|
||||||
const styles = {
|
|
||||||
...(!displayOptions.pageShadows ? { boxShadow: 'none' } : {})
|
|
||||||
// Add more conditions as needed
|
|
||||||
};
|
|
||||||
|
|
||||||
return <BrewPage className='page' index={index} key={index} contents={html} style={styles} />;
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,8 +150,7 @@ const BrewRenderer = (props)=>{
|
|||||||
renderedPages.length = 0;
|
renderedPages.length = 0;
|
||||||
|
|
||||||
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
// Render currently-edited page first so cross-page effects (variables, links) can propagate out first
|
||||||
if(rawPages.length > props.currentEditorCursorPageNum -1)
|
renderedPages[props.currentEditorPage] = renderPage(rawPages[props.currentEditorPage], props.currentEditorPage);
|
||||||
renderedPages[props.currentEditorCursorPageNum - 1] = renderPage(rawPages[props.currentEditorCursorPageNum - 1], props.currentEditorCursorPageNum - 1);
|
|
||||||
|
|
||||||
_.forEach(rawPages, (page, index)=>{
|
_.forEach(rawPages, (page, index)=>{
|
||||||
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
if((isInView(index) || !renderedPages[index]) && typeof window !== 'undefined'){
|
||||||
@@ -183,9 +171,9 @@ const BrewRenderer = (props)=>{
|
|||||||
};
|
};
|
||||||
|
|
||||||
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
const frameDidMount = ()=>{ //This triggers when iFrame finishes internal "componentDidMount"
|
||||||
scrollToHash(window.location.hash);
|
|
||||||
|
|
||||||
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
setTimeout(()=>{ //We still see a flicker where the style isn't applied yet, so wait 100ms before showing iFrame
|
||||||
|
updateSize();
|
||||||
|
window.addEventListener('resize', updateSize);
|
||||||
renderPages(); //Make sure page is renderable before showing
|
renderPages(); //Make sure page is renderable before showing
|
||||||
setState((prevState)=>({
|
setState((prevState)=>({
|
||||||
...prevState,
|
...prevState,
|
||||||
@@ -200,30 +188,15 @@ const BrewRenderer = (props)=>{
|
|||||||
document.dispatchEvent(new MouseEvent('click'));
|
document.dispatchEvent(new MouseEvent('click'));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDisplayOptionsChange = (newDisplayOptions)=>{
|
const rendererPath = props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||||
setDisplayOptions(newDisplayOptions);
|
const themePath = props.theme ?? '5ePHB';
|
||||||
};
|
const baseThemePath = Themes[rendererPath][themePath].baseTheme;
|
||||||
|
|
||||||
const pagesStyle = {
|
|
||||||
zoom : `${displayOptions.zoomLevel}%`,
|
|
||||||
columnGap : `${displayOptions.columnGap}px`,
|
|
||||||
rowGap : `${displayOptions.rowGap}px`
|
|
||||||
};
|
|
||||||
|
|
||||||
const styleObject = {};
|
|
||||||
|
|
||||||
if(global.config.deployment) {
|
|
||||||
styleObject.backgroundImage = `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='40px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${global.config.deployment}</text></svg>")`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderedStyle = useMemo(()=>renderStyle(), [props.style, props.themeBundle]);
|
|
||||||
renderedPages = useMemo(()=>renderPages(), [props.text]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/*render dummy page while iFrame is mounting.*/}
|
{/*render dummy page while iFrame is mounting.*/}
|
||||||
{!state.isMounted
|
{!state.isMounted
|
||||||
? <div className='brewRenderer' onScroll={updateCurrentPage}>
|
? <div className='brewRenderer' onScroll={handleScroll}>
|
||||||
<div className='pages'>
|
<div className='pages'>
|
||||||
{renderDummyPage(1)}
|
{renderDummyPage(1)}
|
||||||
</div>
|
</div>
|
||||||
@@ -231,38 +204,42 @@ const BrewRenderer = (props)=>{
|
|||||||
: null}
|
: null}
|
||||||
|
|
||||||
<ErrorBar errors={props.errors} />
|
<ErrorBar errors={props.errors} />
|
||||||
<div className='popups' ref={mainRef}>
|
<div className='popups'>
|
||||||
<RenderWarnings />
|
<RenderWarnings />
|
||||||
<NotificationPopup />
|
<NotificationPopup />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ToolBar displayOptions={displayOptions} currentPage={props.currentBrewRendererPageNum} totalPages={rawPages.length} onDisplayOptionsChange={handleDisplayOptionsChange} />
|
|
||||||
|
|
||||||
{/*render in iFrame so broken code doesn't crash the site.*/}
|
{/*render in iFrame so broken code doesn't crash the site.*/}
|
||||||
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
<Frame id='BrewRenderer' initialContent={INITIAL_CONTENT}
|
||||||
style={{ width: '100%', height: '100%', visibility: state.visibility }}
|
style={{ width: '100%', height: '100%', visibility: state.visibility }}
|
||||||
contentDidMount={frameDidMount}
|
contentDidMount={frameDidMount}
|
||||||
onClick={()=>{emitClick();}}
|
onClick={()=>{emitClick();}}
|
||||||
>
|
>
|
||||||
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
|
<div className={'brewRenderer'}
|
||||||
onScroll={updateCurrentPage}
|
onScroll={handleScroll}
|
||||||
onKeyDown={handleControlKeys}
|
onKeyDown={handleControlKeys}
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
style={ styleObject }>
|
style={{ height: state.height }}>
|
||||||
|
|
||||||
|
<link href={`/themes/${rendererPath}/Blank/style.css`} type='text/css' rel='stylesheet'/>
|
||||||
|
{baseThemePath &&
|
||||||
|
<link href={`/themes/${rendererPath}/${baseThemePath}/style.css`} type='text/css' rel='stylesheet'/>
|
||||||
|
}
|
||||||
|
<link href={`/themes/${rendererPath}/${themePath}/style.css`} type='text/css' rel='stylesheet'/>
|
||||||
|
|
||||||
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
{/* Apply CSS from Style tab and render pages from Markdown tab */}
|
||||||
{state.isMounted
|
{state.isMounted
|
||||||
&&
|
&&
|
||||||
<>
|
<>
|
||||||
{renderedStyle}
|
{renderStyle()}
|
||||||
<div lang={`${props.lang || 'en'}`} style={pagesStyle} className={
|
<div className='pages' lang={`${props.lang || 'en'}`}>
|
||||||
`pages ${displayOptions.startOnRight ? 'recto' : 'verso'} ${displayOptions.spread}` } >
|
{renderPages()}
|
||||||
{renderedPages}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
</Frame>
|
</Frame>
|
||||||
|
{renderPageInfo()}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,47 +1,10 @@
|
|||||||
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
@import (multiple, less) 'shared/naturalcrit/styles/reset.less';
|
||||||
|
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
overflow-y : scroll;
|
|
||||||
will-change : transform;
|
will-change : transform;
|
||||||
padding-top : 60px;
|
overflow-y : scroll;
|
||||||
height : 100vh;
|
|
||||||
&:has(.facing, .flow) {
|
|
||||||
padding : 60px 30px;
|
|
||||||
}
|
|
||||||
&.deployment {
|
|
||||||
background-color: darkred;
|
|
||||||
}
|
|
||||||
:where(.pages) {
|
:where(.pages) {
|
||||||
&.facing {
|
margin : 30px 0px;
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, auto);
|
|
||||||
grid-template-rows: repeat(3, auto);
|
|
||||||
gap: 10px 10px;
|
|
||||||
justify-content: center;
|
|
||||||
&.recto .page:first-child {
|
|
||||||
// sets first page on 'right' ('recto') of the preview, as if for a Cover page.
|
|
||||||
// todo: add a checkbox to toggle this setting
|
|
||||||
grid-column-start: 2;
|
|
||||||
}
|
|
||||||
& :where(.page) {
|
|
||||||
margin-left: unset !important;
|
|
||||||
margin-right: unset !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.flow {
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 10px;
|
|
||||||
justify-content: flex-start;
|
|
||||||
& :where(.page) {
|
|
||||||
flex: 0 0 auto;
|
|
||||||
margin-left: unset !important;
|
|
||||||
margin-right: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
& > :where(.page) {
|
& > :where(.page) {
|
||||||
width : 215.9mm;
|
width : 215.9mm;
|
||||||
height : 279.4mm;
|
height : 279.4mm;
|
||||||
@@ -50,36 +13,67 @@
|
|||||||
margin-left : auto;
|
margin-left : auto;
|
||||||
box-shadow : 1px 4px 14px #000000;
|
box-shadow : 1px 4px 14px #000000;
|
||||||
}
|
}
|
||||||
*[id] {
|
|
||||||
scroll-margin-top:100px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&::-webkit-scrollbar {
|
&::-webkit-scrollbar {
|
||||||
width: 20px;
|
width: 20px;
|
||||||
&:horizontal{
|
&:horizontal{
|
||||||
width : auto;
|
|
||||||
height: 20px;
|
height: 20px;
|
||||||
|
width:auto;
|
||||||
}
|
}
|
||||||
&-thumb {
|
&-thumb {
|
||||||
background : linear-gradient(90deg, #D3C1AF 15px, #00000000 15px);
|
background: linear-gradient(90deg, #d3c1af 15px, #00000000 15px);
|
||||||
&:horizontal { background : linear-gradient(0deg, #D3C1AF 15px, #00000000 15px); }
|
&:horizontal{
|
||||||
|
background: linear-gradient(0deg, #d3c1af 15px, #00000000 15px);
|
||||||
}
|
}
|
||||||
&-corner { visibility : hidden; }
|
}
|
||||||
|
&-corner {
|
||||||
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
.pane { position : relative; }
|
.pane { position : relative; }
|
||||||
|
.pageInfo {
|
||||||
|
position : absolute;
|
||||||
|
right : 17px;
|
||||||
|
bottom : 0;
|
||||||
|
z-index : 1000;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
|
div {
|
||||||
|
display : inline-block;
|
||||||
|
padding : 8px 10px;
|
||||||
|
&:not(:last-child) { border-right : 1px solid #666666; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.ppr_msg {
|
||||||
|
position : absolute;
|
||||||
|
bottom : 0;
|
||||||
|
left : 0px;
|
||||||
|
z-index : 1000;
|
||||||
|
padding : 8px 10px;
|
||||||
|
font-size : 10px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
background-color : #333333;
|
||||||
|
}
|
||||||
|
|
||||||
@media print {
|
@media print {
|
||||||
.toolBar { display : none; }
|
|
||||||
.brewRenderer {
|
.brewRenderer {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
padding-top : unset;
|
|
||||||
overflow-y: unset;
|
overflow-y: unset;
|
||||||
.pages {
|
.pages {
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
zoom: 100% !important;
|
&>.page {
|
||||||
& > .page { box-shadow : unset; }
|
box-shadow: unset;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,62 +1,43 @@
|
|||||||
require('./notificationPopup.less');
|
require('./notificationPopup.less');
|
||||||
import React, { useEffect, useState } from 'react';
|
const React = require('react');
|
||||||
import request from '../../utils/request-middleware.js';
|
const _ = require('lodash');
|
||||||
|
|
||||||
import Dialog from '../../../components/dialog.jsx';
|
import Dialog from '../../../components/dialog.jsx';
|
||||||
|
|
||||||
|
const DISMISS_KEY = 'dismiss_notification12-04-23';
|
||||||
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
const DISMISS_BUTTON = <i className='fas fa-times dismiss' />;
|
||||||
|
|
||||||
const NotificationPopup = ()=>{
|
const NotificationPopup = ()=>{
|
||||||
const [notifications, setNotifications] = useState([]);
|
return <Dialog className='notificationPopup' dismissKey={DISMISS_KEY} closeText={DISMISS_BUTTON} >
|
||||||
const [dissmissKeyList, setDismissKeyList] = useState([]);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
getNotifications();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const getNotifications = async ()=>{
|
|
||||||
setError(null);
|
|
||||||
try {
|
|
||||||
const res = await request.get('/admin/notification/all');
|
|
||||||
pickActiveNotifications(res.body || []);
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err);
|
|
||||||
setError(`Error looking up notifications: ${err?.response?.body?.message || err.message}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const pickActiveNotifications = (notifs)=>{
|
|
||||||
const now = new Date();
|
|
||||||
const filteredNotifications = notifs.filter((notification)=>{
|
|
||||||
const startDate = new Date(notification.startAt);
|
|
||||||
const stopDate = new Date(notification.stopAt);
|
|
||||||
const dismissed = localStorage.getItem(notification.dismissKey) ? true : false;
|
|
||||||
return now >= startDate && now <= stopDate && !dismissed;
|
|
||||||
});
|
|
||||||
setNotifications(filteredNotifications);
|
|
||||||
setDismissKeyList(filteredNotifications.map((notif)=>notif.dismissKey));
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNotificationsList = ()=>{
|
|
||||||
if(error) return <div className='error'>{error}</div>;
|
|
||||||
|
|
||||||
return notifications.map((notification)=>(
|
|
||||||
<li key={notification.dismissKey} >
|
|
||||||
<em>{notification.title}</em><br />
|
|
||||||
<p dangerouslySetInnerHTML={{ __html: notification.text }}></p>
|
|
||||||
</li>
|
|
||||||
));
|
|
||||||
};
|
|
||||||
|
|
||||||
return <Dialog className='notificationPopup' dismisskeys={dissmissKeyList} closeText={DISMISS_BUTTON} >
|
|
||||||
<div className='header'>
|
<div className='header'>
|
||||||
<i className='fas fa-info-circle info'></i>
|
<i className='fas fa-info-circle info'></i>
|
||||||
<h3>Notice</h3>
|
<h3>Notice</h3>
|
||||||
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
<small>This website is always improving and we are still adding new features and squashing bugs. Keep the following in mind:</small>
|
||||||
</div>
|
</div>
|
||||||
<ul>
|
<ul>
|
||||||
{renderNotificationsList()}
|
<li key='psa'>
|
||||||
|
<em>Don't store IMAGES in Google Drive</em><br />
|
||||||
|
Google Drive is not an image service, and will block images from being used
|
||||||
|
in brews if they get more views than expected. Google has confirmed they won't fix
|
||||||
|
this, so we recommend you look for another image hosting service such as imgur, ImgBB or Google Photos.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li key='googleDriveFolder'>
|
||||||
|
<em>Don't delete your Homebrewery folder on Google Drive!</em> <br />
|
||||||
|
We have had several reports of users losing their brews, not realizing
|
||||||
|
that they had deleted the files on their Google Drive. If you have a Homebrewery folder
|
||||||
|
on your Google Drive with *.txt files inside, <em>do not delete it</em>!
|
||||||
|
We cannot help you recover files that you have deleted from your own
|
||||||
|
Google Drive.
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li key='faq'>
|
||||||
|
<em>Protect your work! </em> <br />
|
||||||
|
If you opt not to use your Google Drive, keep in mind that we do not save a history of your projects. Please make frequent backups of your brews!
|
||||||
|
<a target='_blank' href='https://www.reddit.com/r/homebrewery/comments/adh6lh/faqs_psas_announcements/'>
|
||||||
|
See the FAQ
|
||||||
|
</a> to learn how to avoid losing your work!
|
||||||
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</Dialog>;
|
</Dialog>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
.popups {
|
.popups {
|
||||||
position : fixed;
|
position : fixed;
|
||||||
top : calc(@navbarHeight + @viewerToolsHeight);
|
top : @navbarHeight;
|
||||||
right : 24px;
|
right : 24px;
|
||||||
z-index : 10001;
|
z-index : 10001;
|
||||||
width : 450px;
|
width : 450px;
|
||||||
margin-top : 5px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notificationPopup {
|
.notificationPopup {
|
||||||
@@ -55,10 +54,7 @@
|
|||||||
margin-top : 1.4em;
|
margin-top : 1.4em;
|
||||||
font-size : 0.8em;
|
font-size : 0.8em;
|
||||||
line-height : 1.4em;
|
line-height : 1.4em;
|
||||||
em {
|
em { font-weight : 800; }
|
||||||
text-transform:capitalize;
|
|
||||||
font-weight : 800;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,46 +0,0 @@
|
|||||||
// Derived from the vue-html-secure package, customized for Homebrewery
|
|
||||||
|
|
||||||
let doc = null;
|
|
||||||
let div = null;
|
|
||||||
|
|
||||||
function safeHTML(htmlString) {
|
|
||||||
// If the Document interface doesn't exist, exit
|
|
||||||
if(typeof document == 'undefined') return null;
|
|
||||||
// If the test document and div don't exist, create them
|
|
||||||
if(!doc) doc = document.implementation.createHTMLDocument('');
|
|
||||||
if(!div) div = doc.createElement('div');
|
|
||||||
|
|
||||||
// Set the test div contents to the evaluation string
|
|
||||||
div.innerHTML = htmlString;
|
|
||||||
// Grab all nodes from the test div
|
|
||||||
const elements = div.querySelectorAll('*');
|
|
||||||
|
|
||||||
// Blacklisted tags
|
|
||||||
const blacklistTags = ['script', 'noscript', 'noembed'];
|
|
||||||
// Tests to remove attributes
|
|
||||||
const blacklistAttrs = [
|
|
||||||
(test)=>{return test.localName.indexOf('on') == 0;},
|
|
||||||
(test)=>{return test.localName.indexOf('type') == 0 && test.value.match(/submit/i);},
|
|
||||||
(test)=>{return test.value.replace(/[\u0000-\u0020\u00A0\u1680\u180E\u2000-\u2029\u205f\u3000]/g, '').toLowerCase().trim().indexOf('javascript:') == 0;}
|
|
||||||
];
|
|
||||||
|
|
||||||
|
|
||||||
elements.forEach((element)=>{
|
|
||||||
// Check each element for blacklisted type
|
|
||||||
if(blacklistTags.includes(element?.localName?.toLowerCase())) {
|
|
||||||
element.remove();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Check remaining elements for blacklisted attributes
|
|
||||||
for (const attribute of element.attributes){
|
|
||||||
if(blacklistAttrs.some((test)=>{return test(attribute);})) {
|
|
||||||
element.removeAttribute(attribute.localName);
|
|
||||||
break;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return div.innerHTML;
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports.safeHTML = safeHTML;
|
|
||||||
@@ -1,227 +0,0 @@
|
|||||||
require('./toolBar.less');
|
|
||||||
const React = require('react');
|
|
||||||
const { useState, useEffect } = React;
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anchored.jsx';
|
|
||||||
// import * as ZoomIcons from '../../../icons/icon-components/zoomIcons.jsx';
|
|
||||||
|
|
||||||
const MAX_ZOOM = 300;
|
|
||||||
const MIN_ZOOM = 10;
|
|
||||||
|
|
||||||
const ToolBar = ({ displayOptions, currentPage, totalPages, onDisplayOptionsChange })=>{
|
|
||||||
|
|
||||||
const [pageNum, setPageNum] = useState(currentPage);
|
|
||||||
const [toolsVisible, setToolsVisible] = useState(true);
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
setPageNum(currentPage);
|
|
||||||
}, [currentPage]);
|
|
||||||
|
|
||||||
const handleZoomButton = (zoom)=>{
|
|
||||||
handleOptionChange('zoomLevel', _.round(_.clamp(zoom, MIN_ZOOM, MAX_ZOOM)));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOptionChange = (optionKey, newValue)=>{
|
|
||||||
//setDisplayOptions(prevOptions => ({ ...prevOptions, [optionKey]: newValue }));
|
|
||||||
onDisplayOptionsChange({ ...displayOptions, [optionKey]: newValue });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handlePageInput = (pageInput)=>{
|
|
||||||
if(/[0-9]/.test(pageInput))
|
|
||||||
setPageNum(parseInt(pageInput)); // input type is 'text', so `page` comes in as a string, not number.
|
|
||||||
};
|
|
||||||
|
|
||||||
const scrollToPage = (pageNumber)=>{
|
|
||||||
pageNumber = _.clamp(pageNumber, 1, totalPages);
|
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
|
||||||
const brewRenderer = iframe?.contentWindow?.document.querySelector('.brewRenderer');
|
|
||||||
const page = brewRenderer?.querySelector(`#p${pageNumber}`);
|
|
||||||
page?.scrollIntoView({ block: 'start' });
|
|
||||||
setPageNum(pageNumber);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const calculateChange = (mode)=>{
|
|
||||||
const iframe = document.getElementById('BrewRenderer');
|
|
||||||
const iframeWidth = iframe.getBoundingClientRect().width;
|
|
||||||
const iframeHeight = iframe.getBoundingClientRect().height;
|
|
||||||
const pages = iframe.contentWindow.document.getElementsByClassName('page');
|
|
||||||
|
|
||||||
let desiredZoom = 0;
|
|
||||||
|
|
||||||
if(mode == 'fill'){
|
|
||||||
// find widest page, in case pages are different widths, so that the zoom is adapted to not cut the widest page off screen.
|
|
||||||
const widestPage = _.maxBy([...pages], 'offsetWidth').offsetWidth;
|
|
||||||
|
|
||||||
desiredZoom = (iframeWidth / widestPage) * 100;
|
|
||||||
|
|
||||||
} else if(mode == 'fit'){
|
|
||||||
// find the page with the largest single dim (height or width) so that zoom can be adapted to fit it.
|
|
||||||
const minDimRatio = [...pages].reduce((minRatio, page)=>Math.min(minRatio, iframeWidth / page.offsetWidth, iframeHeight / page.offsetHeight), Infinity);
|
|
||||||
|
|
||||||
desiredZoom = minDimRatio * 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
const margin = 5; // extra space so page isn't edge to edge (not truly "to fill")
|
|
||||||
|
|
||||||
const deltaZoom = (desiredZoom - displayOptions.zoomLevel) - margin;
|
|
||||||
return deltaZoom;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id='preview-toolbar' className={`toolBar ${toolsVisible ? 'visible' : 'hidden'}`} role='toolbar'>
|
|
||||||
<button className='toggleButton' title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{setToolsVisible(!toolsVisible);}}><i className='fas fa-glasses' /></button>
|
|
||||||
{/*v=====----------------------< Zoom Controls >---------------------=====v*/}
|
|
||||||
<div className='group' role='group' aria-label='Zoom' aria-hidden={!toolsVisible}>
|
|
||||||
<button
|
|
||||||
id='fill-width'
|
|
||||||
className='tool'
|
|
||||||
title='Set zoom to fill preview with one page'
|
|
||||||
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fill'))}
|
|
||||||
>
|
|
||||||
<i className='fac fit-width' />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id='zoom-to-fit'
|
|
||||||
className='tool'
|
|
||||||
title='Set zoom to fit entire page in preview'
|
|
||||||
onClick={()=>handleZoomButton(displayOptions.zoomLevel + calculateChange('fit'))}
|
|
||||||
>
|
|
||||||
<i className='fac zoom-to-fit' />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
id='zoom-out'
|
|
||||||
className='tool'
|
|
||||||
onClick={()=>handleZoomButton(displayOptions.zoomLevel - 20)}
|
|
||||||
disabled={displayOptions.zoomLevel <= MIN_ZOOM}
|
|
||||||
title='Zoom Out'
|
|
||||||
>
|
|
||||||
<i className='fas fa-magnifying-glass-minus' />
|
|
||||||
</button>
|
|
||||||
<input
|
|
||||||
id='zoom-slider'
|
|
||||||
className='range-input tool hover-tooltip'
|
|
||||||
type='range'
|
|
||||||
name='zoom'
|
|
||||||
title='Set Zoom'
|
|
||||||
list='zoomLevels'
|
|
||||||
min={MIN_ZOOM}
|
|
||||||
max={MAX_ZOOM}
|
|
||||||
step='1'
|
|
||||||
value={displayOptions.zoomLevel}
|
|
||||||
onChange={(e)=>handleZoomButton(parseInt(e.target.value))}
|
|
||||||
/>
|
|
||||||
<datalist id='zoomLevels'>
|
|
||||||
<option value='100' />
|
|
||||||
</datalist>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id='zoom-in'
|
|
||||||
className='tool'
|
|
||||||
onClick={()=>handleZoomButton(displayOptions.zoomLevel + 20)}
|
|
||||||
disabled={displayOptions.zoomLevel >= MAX_ZOOM}
|
|
||||||
title='Zoom In'
|
|
||||||
>
|
|
||||||
<i className='fas fa-magnifying-glass-plus' />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*v=====----------------------< Spread Controls >---------------------=====v*/}
|
|
||||||
<div className='group' role='group' aria-label='Spread' aria-hidden={!toolsVisible}>
|
|
||||||
<div className='radio-group' role='radiogroup'>
|
|
||||||
<button role='radio'
|
|
||||||
id='single-spread'
|
|
||||||
className='tool'
|
|
||||||
title='Single Page'
|
|
||||||
onClick={()=>{handleOptionChange('spread', 'active');}}
|
|
||||||
aria-checked={displayOptions.spread === 'single'}
|
|
||||||
><i className='fac single-spread' /></button>
|
|
||||||
<button role='radio'
|
|
||||||
id='facing-spread'
|
|
||||||
className='tool'
|
|
||||||
title='Facing Pages'
|
|
||||||
onClick={()=>{handleOptionChange('spread', 'facing');}}
|
|
||||||
aria-checked={displayOptions.spread === 'facing'}
|
|
||||||
><i className='fac facing-spread' /></button>
|
|
||||||
<button role='radio'
|
|
||||||
id='flow-spread'
|
|
||||||
className='tool'
|
|
||||||
title='Flow Pages'
|
|
||||||
onClick={()=>{handleOptionChange('spread', 'flow');}}
|
|
||||||
aria-checked={displayOptions.spread === 'flow'}
|
|
||||||
><i className='fac flow-spread' /></button>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
<Anchored>
|
|
||||||
<AnchoredTrigger id='spread-settings' className='tool' title='Spread options'><i className='fas fa-gear' /></AnchoredTrigger>
|
|
||||||
<AnchoredBox title='Options'>
|
|
||||||
<h1>Options</h1>
|
|
||||||
<label title='Modify the horizontal space between pages.'>
|
|
||||||
Column gap
|
|
||||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('columnGap', evt.target.value)} />
|
|
||||||
</label>
|
|
||||||
<label title='Modify the vertical space between rows of pages.'>
|
|
||||||
Row gap
|
|
||||||
<input type='range' min={0} max={200} defaultValue={10} className='range-input' onChange={(evt)=>handleOptionChange('rowGap', evt.target.value)} />
|
|
||||||
</label>
|
|
||||||
<label title='Start 1st page on the right side, such as if you have cover page.'>
|
|
||||||
Start on right
|
|
||||||
<input type='checkbox' checked={displayOptions.startOnRight} onChange={()=>{handleOptionChange('startOnRight', !displayOptions.startOnRight);}}
|
|
||||||
title={displayOptions.spread !== 'facing' ? 'Switch to Facing to enable toggle.' : null} />
|
|
||||||
</label>
|
|
||||||
<label title='Toggle the page shadow on every page.'>
|
|
||||||
Page shadows
|
|
||||||
<input type='checkbox' checked={displayOptions.pageShadows} onChange={()=>{handleOptionChange('pageShadows', !displayOptions.pageShadows);}} />
|
|
||||||
</label>
|
|
||||||
</AnchoredBox>
|
|
||||||
</Anchored>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/*v=====----------------------< Page Controls >---------------------=====v*/}
|
|
||||||
<div className='group' role='group' aria-label='Pages' aria-hidden={!toolsVisible}>
|
|
||||||
<button
|
|
||||||
id='previous-page'
|
|
||||||
className='previousPage tool'
|
|
||||||
type='button'
|
|
||||||
title='Previous Page(s)'
|
|
||||||
onClick={()=>scrollToPage(pageNum - 1)}
|
|
||||||
disabled={pageNum <= 1}
|
|
||||||
>
|
|
||||||
<i className='fas fa-arrow-left'></i>
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className='tool'>
|
|
||||||
<input
|
|
||||||
id='page-input'
|
|
||||||
className='text-input'
|
|
||||||
type='text'
|
|
||||||
name='page'
|
|
||||||
title='Current page(s) in view'
|
|
||||||
inputMode='numeric'
|
|
||||||
pattern='[0-9]'
|
|
||||||
value={pageNum}
|
|
||||||
onClick={(e)=>e.target.select()}
|
|
||||||
onChange={(e)=>handlePageInput(e.target.value)}
|
|
||||||
onBlur={()=>scrollToPage(pageNum)}
|
|
||||||
onKeyDown={(e)=>e.key == 'Enter' && scrollToPage(pageNum)}
|
|
||||||
/>
|
|
||||||
<span id='page-count' title='Total Page Count'>/ {totalPages}</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id='next-page'
|
|
||||||
className='tool'
|
|
||||||
type='button'
|
|
||||||
title='Next Page(s)'
|
|
||||||
onClick={()=>scrollToPage(pageNum + 1)}
|
|
||||||
disabled={pageNum >= totalPages}
|
|
||||||
>
|
|
||||||
<i className='fas fa-arrow-right'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = ToolBar;
|
|
||||||
@@ -1,187 +0,0 @@
|
|||||||
@import (less) './client/icons/customIcons.less';
|
|
||||||
|
|
||||||
.toolBar {
|
|
||||||
position : absolute;
|
|
||||||
z-index : 1;
|
|
||||||
box-sizing : border-box;
|
|
||||||
display : flex;
|
|
||||||
flex-wrap : wrap;
|
|
||||||
gap : 8px 30px;
|
|
||||||
align-items : center;
|
|
||||||
justify-content : center;
|
|
||||||
width : 100%;
|
|
||||||
height : auto;
|
|
||||||
padding : 2px 0;
|
|
||||||
font-family : 'Open Sans', sans-serif;
|
|
||||||
font-size : 13px;
|
|
||||||
color : #CCCCCC;
|
|
||||||
background-color : #555555;
|
|
||||||
& > *:not(.toggleButton) {
|
|
||||||
opacity : 1;
|
|
||||||
transition : all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.group {
|
|
||||||
box-sizing : border-box;
|
|
||||||
display : flex;
|
|
||||||
gap : 0 3px;
|
|
||||||
align-items : center;
|
|
||||||
justify-content : center;
|
|
||||||
height : 28px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.tool {
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.active, [aria-checked='true'] { background-color : #444444; }
|
|
||||||
|
|
||||||
.anchored-trigger {
|
|
||||||
&.active { background-color : #444444; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.anchored-box {
|
|
||||||
--box-color : #555555;
|
|
||||||
top : 30px;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
gap : 5px;
|
|
||||||
padding : 15px;
|
|
||||||
margin-top : 10px;
|
|
||||||
font-size : 0.8em;
|
|
||||||
color : #CCCCCC;
|
|
||||||
background-color : var(--box-color);
|
|
||||||
border-radius : 5px;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
padding-bottom : 0.3em;
|
|
||||||
margin-bottom : 0.5em;
|
|
||||||
border-bottom : 1px solid currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
padding-bottom : 0.3em;
|
|
||||||
margin : 1em 0 0.5em 0;
|
|
||||||
color : lightgray;
|
|
||||||
border-bottom : 1px solid currentColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
label {
|
|
||||||
display : flex;
|
|
||||||
gap : 6px;
|
|
||||||
align-items : center;
|
|
||||||
justify-content : space-between;
|
|
||||||
|
|
||||||
}
|
|
||||||
input {
|
|
||||||
height : unset;
|
|
||||||
&[type='range'] { padding : 0; }
|
|
||||||
}
|
|
||||||
&::before {
|
|
||||||
position : absolute;
|
|
||||||
top : -20px;
|
|
||||||
left : 50%;
|
|
||||||
width : 0px;
|
|
||||||
height : 0px;
|
|
||||||
pointer-events : none;
|
|
||||||
content : '';
|
|
||||||
border : 10px solid transparent;
|
|
||||||
border-bottom : 10px solid var(--box-color);
|
|
||||||
transform : translateX(-50%);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
.radio-group:has(button[role='radio']) {
|
|
||||||
display : flex;
|
|
||||||
height : 100%;
|
|
||||||
border : 1px solid #333333;
|
|
||||||
}
|
|
||||||
|
|
||||||
input {
|
|
||||||
position : relative;
|
|
||||||
height : 1.5em;
|
|
||||||
padding : 2px 5px;
|
|
||||||
font-family : 'Open Sans', sans-serif;
|
|
||||||
color : #000000;
|
|
||||||
background : #EEEEEE;
|
|
||||||
border : 1px solid gray;
|
|
||||||
&:focus { outline : 1px solid #D3D3D3; }
|
|
||||||
|
|
||||||
// `.range-input` if generic to all range inputs, or `#zoom-slider` if only for zoom slider
|
|
||||||
&.range-input {
|
|
||||||
padding : 2px 0;
|
|
||||||
color : #D3D3D3;
|
|
||||||
accent-color : #D3D3D3;
|
|
||||||
|
|
||||||
&::-webkit-slider-thumb, &::-moz-slider-thumb {
|
|
||||||
width : 5px;
|
|
||||||
height : 5px;
|
|
||||||
cursor : pointer;
|
|
||||||
outline : none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hover-tooltip[value]:hover::after {
|
|
||||||
position : absolute;
|
|
||||||
bottom : -30px;
|
|
||||||
left : 50%;
|
|
||||||
z-index : 1;
|
|
||||||
display : grid;
|
|
||||||
place-items : center;
|
|
||||||
width : 4ch;
|
|
||||||
height : 1.2lh;
|
|
||||||
pointer-events : none;
|
|
||||||
content : attr(value);
|
|
||||||
background-color : #555555;
|
|
||||||
border : 1px solid #A1A1A1;
|
|
||||||
transform : translate(-50%, 50%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// `.text-input` if generic to all range inputs, or `#page-input` if only for current page input
|
|
||||||
&#page-input {
|
|
||||||
width : 4ch;
|
|
||||||
margin-right : 1ch;
|
|
||||||
text-align : center;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
box-sizing : border-box;
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
justify-content : center;
|
|
||||||
width : auto;
|
|
||||||
min-width : 46px;
|
|
||||||
height : 100%;
|
|
||||||
&:hover { background-color : #444444; }
|
|
||||||
&:focus { border : 1px solid #D3D3D3;outline : none;}
|
|
||||||
&:disabled {
|
|
||||||
color : #777777;
|
|
||||||
background-color : unset !important;
|
|
||||||
}
|
|
||||||
i { font-size : 1.2em; }
|
|
||||||
}
|
|
||||||
|
|
||||||
&.hidden {
|
|
||||||
flex-wrap : nowrap;
|
|
||||||
width : 32px;
|
|
||||||
overflow : hidden;
|
|
||||||
background-color : unset;
|
|
||||||
opacity : 0.5;
|
|
||||||
transition : all 0.3s ease;
|
|
||||||
& > *:not(.toggleButton) {
|
|
||||||
opacity : 0;
|
|
||||||
transition : all 0.2s ease;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button.toggleButton {
|
|
||||||
position : absolute;
|
|
||||||
left : 0;
|
|
||||||
z-index : 5;
|
|
||||||
width : 32px;
|
|
||||||
min-width : unset;
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./editor.less');
|
require('./editor.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
const cx = require('classnames');
|
||||||
const dedent = require('dedent-tabs').default;
|
const dedent = require('dedent-tabs').default;
|
||||||
import Markdown from '../../../shared/naturalcrit/markdown.js';
|
const Markdown = require('../../../shared/naturalcrit/markdown.js');
|
||||||
|
|
||||||
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
|
||||||
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
const SnippetBar = require('./snippetbar/snippetbar.jsx');
|
||||||
@@ -21,7 +22,6 @@ const DEFAULT_STYLE_TEXT = dedent`
|
|||||||
color: black;
|
color: black;
|
||||||
}`;
|
}`;
|
||||||
|
|
||||||
let isJumping = false;
|
|
||||||
|
|
||||||
const Editor = createClass({
|
const Editor = createClass({
|
||||||
displayName : 'Editor',
|
displayName : 'Editor',
|
||||||
@@ -37,15 +37,8 @@ const Editor = createClass({
|
|||||||
onMetaChange : ()=>{},
|
onMetaChange : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{},
|
||||||
|
|
||||||
onCursorPageChange : ()=>{},
|
|
||||||
onViewPageChange : ()=>{},
|
|
||||||
|
|
||||||
editorTheme : 'default',
|
editorTheme : 'default',
|
||||||
renderer : 'legacy',
|
renderer : 'legacy'
|
||||||
|
|
||||||
currentEditorCursorPageNum : 1,
|
|
||||||
currentEditorViewPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getInitialState : function() {
|
getInitialState : function() {
|
||||||
@@ -63,15 +56,9 @@ const Editor = createClass({
|
|||||||
isMeta : function() {return this.state.view == 'meta';},
|
isMeta : function() {return this.state.view == 'meta';},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
|
|
||||||
this.updateEditorSize();
|
this.updateEditorSize();
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
window.addEventListener('resize', this.updateEditorSize);
|
window.addEventListener('resize', this.updateEditorSize);
|
||||||
document.getElementById('BrewRenderer').addEventListener('keydown', this.handleControlKeys);
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
|
||||||
|
|
||||||
this.codeEditor.current.codeMirror.on('cursorActivity', (cm)=>{this.updateCurrentCursorPage(cm.getCursor());});
|
|
||||||
this.codeEditor.current.codeMirror.on('scroll', _.throttle(()=>{this.updateCurrentViewPage(this.codeEditor.current.getTopVisibleLine());}, 200));
|
|
||||||
|
|
||||||
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
const editorTheme = window.localStorage.getItem(EDITOR_THEME_KEY);
|
||||||
if(editorTheme) {
|
if(editorTheme) {
|
||||||
@@ -86,35 +73,13 @@ const Editor = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
componentDidUpdate : function(prevProps, prevState, snapshot) {
|
||||||
|
|
||||||
this.highlightCustomMarkdown();
|
this.highlightCustomMarkdown();
|
||||||
if(prevProps.moveBrew !== this.props.moveBrew)
|
if(prevProps.moveBrew !== this.props.moveBrew) {
|
||||||
this.brewJump();
|
this.brewJump();
|
||||||
|
};
|
||||||
if(prevProps.moveSource !== this.props.moveSource)
|
if(prevProps.moveSource !== this.props.moveSource) {
|
||||||
this.sourceJump();
|
this.sourceJump();
|
||||||
|
};
|
||||||
if(this.props.liveScroll) {
|
|
||||||
if(prevProps.currentBrewRendererPageNum !== this.props.currentBrewRendererPageNum) {
|
|
||||||
this.sourceJump(this.props.currentBrewRendererPageNum, false);
|
|
||||||
} else if(prevProps.currentEditorViewPageNum !== this.props.currentEditorViewPageNum) {
|
|
||||||
this.brewJump(this.props.currentEditorViewPageNum, false);
|
|
||||||
} else if(prevProps.currentEditorCursorPageNum !== this.props.currentEditorCursorPageNum) {
|
|
||||||
this.brewJump(this.props.currentEditorCursorPageNum, false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
|
||||||
if(!(e.ctrlKey && e.metaKey && e.shiftKey)) return;
|
|
||||||
const LEFTARROW_KEY = 37;
|
|
||||||
const RIGHTARROW_KEY = 39;
|
|
||||||
if(e.keyCode == RIGHTARROW_KEY) this.brewJump();
|
|
||||||
if(e.keyCode == LEFTARROW_KEY) this.sourceJump();
|
|
||||||
if(e.keyCode == LEFTARROW_KEY || e.keyCode == RIGHTARROW_KEY) {
|
|
||||||
e.stopPropagation();
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
updateEditorSize : function() {
|
updateEditorSize : function() {
|
||||||
@@ -125,20 +90,6 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateCurrentCursorPage : function(cursor) {
|
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, cursor.line + 1);
|
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
|
||||||
this.props.onCursorPageChange(currentPage);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateCurrentViewPage : function(topScrollLine) {
|
|
||||||
const lines = this.props.brew.text.split('\n').slice(0, topScrollLine + 1);
|
|
||||||
const pageRegex = this.props.brew.renderer == 'V3' ? /^\\page$/ : /\\page/;
|
|
||||||
const currentPage = lines.reduce((count, line)=>count + (pageRegex.test(line) ? 1 : 0), 1);
|
|
||||||
this.props.onViewPageChange(currentPage);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleInject : function(injectText){
|
handleInject : function(injectText){
|
||||||
this.codeEditor.current?.injectText(injectText, false);
|
this.codeEditor.current?.injectText(injectText, false);
|
||||||
},
|
},
|
||||||
@@ -147,10 +98,19 @@ const Editor = createClass({
|
|||||||
this.props.setMoveArrows(newView === 'text');
|
this.props.setMoveArrows(newView === 'text');
|
||||||
this.setState({
|
this.setState({
|
||||||
view : newView
|
view : newView
|
||||||
}, ()=>{
|
}, this.updateEditorSize); //TODO: not sure if updateeditorsize needed
|
||||||
this.codeEditor.current?.codeMirror.focus();
|
},
|
||||||
this.updateEditorSize();
|
|
||||||
}); //TODO: not sure if updateeditorsize needed
|
getCurrentPage : function(){
|
||||||
|
const lines = this.props.brew.text.split('\n').slice(0, this.codeEditor.current.getCursorPosition().line + 1);
|
||||||
|
return _.reduce(lines, (r, line)=>{
|
||||||
|
if(
|
||||||
|
(this.props.renderer == 'legacy' && line.indexOf('\\page') !== -1)
|
||||||
|
||
|
||||||
|
(this.props.renderer == 'V3' && line.match(/^\\page$/))
|
||||||
|
) r++;
|
||||||
|
return r;
|
||||||
|
}, 1);
|
||||||
},
|
},
|
||||||
|
|
||||||
highlightCustomMarkdown : function(){
|
highlightCustomMarkdown : function(){
|
||||||
@@ -159,19 +119,8 @@ const Editor = createClass({
|
|||||||
const codeMirror = this.codeEditor.current.codeMirror;
|
const codeMirror = this.codeEditor.current.codeMirror;
|
||||||
|
|
||||||
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
codeMirror.operation(()=>{ // Batch CodeMirror styling
|
||||||
|
|
||||||
const foldLines = [];
|
|
||||||
|
|
||||||
//reset custom text styles
|
//reset custom text styles
|
||||||
const customHighlights = codeMirror.getAllMarks().filter((mark)=>{
|
const customHighlights = codeMirror.getAllMarks().filter((mark)=>!mark.__isFold); //Don't undo code folding
|
||||||
// Record details of folded sections
|
|
||||||
if(mark.__isFold) {
|
|
||||||
const fold = mark.find();
|
|
||||||
foldLines.push({ from: fold.from?.line, to: fold.to?.line });
|
|
||||||
}
|
|
||||||
return !mark.__isFold;
|
|
||||||
}); //Don't undo code folding
|
|
||||||
|
|
||||||
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
for (let i=customHighlights.length - 1;i>=0;i--) customHighlights[i].clear();
|
||||||
|
|
||||||
let editorPageCount = 2; // start page count from page 2
|
let editorPageCount = 2; // start page count from page 2
|
||||||
@@ -183,11 +132,6 @@ const Editor = createClass({
|
|||||||
codeMirror.removeLineClass(lineNumber, 'text');
|
codeMirror.removeLineClass(lineNumber, 'text');
|
||||||
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
codeMirror.removeLineClass(lineNumber, 'wrap', 'sourceMoveFlash');
|
||||||
|
|
||||||
// Don't process lines inside folded text
|
|
||||||
// If the current lineNumber is inside any folded marks, skip line styling
|
|
||||||
if(foldLines.some((fold)=>lineNumber >= fold.from && lineNumber <= fold.to))
|
|
||||||
return;
|
|
||||||
|
|
||||||
// Styling for \page breaks
|
// Styling for \page breaks
|
||||||
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
if((this.props.renderer == 'legacy' && line.includes('\\page')) ||
|
||||||
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
(this.props.renderer == 'V3' && line.match(/^\\page$/))) {
|
||||||
@@ -211,7 +155,7 @@ const Editor = createClass({
|
|||||||
|
|
||||||
// definition lists
|
// definition lists
|
||||||
if(line.includes('::')){
|
if(line.includes('::')){
|
||||||
if(/^:*$/.test(line) == true){ return; };
|
if(/^:*$/.test(line) == true){ return };
|
||||||
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
const regex = /^([^\n]*?:?\s?)(::[^\n]*)(?:\n|$)/ymd; // the `d` flag, for match indices, throws an ESLint error.
|
||||||
let match;
|
let match;
|
||||||
while ((match = regex.exec(line)) != null){
|
while ((match = regex.exec(line)) != null){
|
||||||
@@ -219,10 +163,10 @@ const Editor = createClass({
|
|||||||
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[1][0] }, { line: lineNumber, ch: match.indices[1][1] }, { className: 'dt-highlight' });
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
codeMirror.markText({ line: lineNumber, ch: match.indices[2][0] }, { line: lineNumber, ch: match.indices[2][1] }, { className: 'dd-highlight' });
|
||||||
const ddIndex = match.indices[2][0];
|
const ddIndex = match.indices[2][0];
|
||||||
const colons = /::/g;
|
let colons = /::/g;
|
||||||
const colonMatches = colons.exec(match[2]);
|
let colonMatches = colons.exec(match[2]);
|
||||||
if(colonMatches !== null){
|
if(colonMatches !== null){
|
||||||
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight' });
|
codeMirror.markText({ line: lineNumber, ch: colonMatches.index + ddIndex }, { line: lineNumber, ch: colonMatches.index + colonMatches[0].length + ddIndex }, { className: 'dl-colon-highlight'} )
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -236,7 +180,7 @@ const Editor = createClass({
|
|||||||
while (startIndex >= 0) {
|
while (startIndex >= 0) {
|
||||||
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
superRegex.lastIndex = subRegex.lastIndex = startIndex;
|
||||||
let isSuper = false;
|
let isSuper = false;
|
||||||
const match = subRegex.exec(line) || superRegex.exec(line);
|
let match = subRegex.exec(line) || superRegex.exec(line);
|
||||||
if (match) {
|
if (match) {
|
||||||
isSuper = !subRegex.lastIndex;
|
isSuper = !subRegex.lastIndex;
|
||||||
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
codeMirror.markText({ line: lineNumber, ch: match.index }, { line: lineNumber, ch: match.index + match[0].length }, { className: isSuper ? 'superscript' : 'subscript' });
|
||||||
@@ -287,20 +231,20 @@ const Editor = createClass({
|
|||||||
|
|
||||||
while (startIndex >= 0) {
|
while (startIndex >= 0) {
|
||||||
emojiRegex.lastIndex = startIndex;
|
emojiRegex.lastIndex = startIndex;
|
||||||
const match = emojiRegex.exec(line);
|
let match = emojiRegex.exec(line);
|
||||||
if (match) {
|
if (match) {
|
||||||
let tokens = Markdown.marked.lexer(match[0]);
|
let tokens = Markdown.marked.lexer(match[0]);
|
||||||
tokens = tokens[0].tokens.filter((t)=>t.type == 'emoji');
|
tokens = tokens[0].tokens.filter(t => t.type == 'emoji')
|
||||||
if (!tokens.length)
|
if (!tokens.length)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const startPos = { line: lineNumber, ch: match.index };
|
let startPos = { line: lineNumber, ch: match.index };
|
||||||
const endPos = { line: lineNumber, ch: match.index + match[0].length };
|
let endPos = { line: lineNumber, ch: match.index + match[0].length };
|
||||||
|
|
||||||
// Iterate over conflicting marks and clear them
|
// Iterate over conflicting marks and clear them
|
||||||
const marks = codeMirror.findMarks(startPos, endPos);
|
var marks = codeMirror.findMarks(startPos, endPos);
|
||||||
marks.forEach(function(marker) {
|
marks.forEach(function(marker) {
|
||||||
if(!marker.__isFold) marker.clear();
|
marker.clear();
|
||||||
});
|
});
|
||||||
codeMirror.markText(startPos, endPos, { className: 'emoji' });
|
codeMirror.markText(startPos, endPos, { className: 'emoji' });
|
||||||
}
|
}
|
||||||
@@ -313,72 +257,57 @@ const Editor = createClass({
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
brewJump : function(targetPage=this.props.currentEditorCursorPageNum, smooth=true){
|
brewJump : function(targetPage=this.getCurrentPage()){
|
||||||
if(!window || !this.isText() || isJumping)
|
if(!window) return;
|
||||||
return;
|
// console.log(`Scroll to: p${targetPage}`);
|
||||||
|
|
||||||
// Get current brewRenderer scroll position and calculate target position
|
|
||||||
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
const brewRenderer = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer')[0];
|
||||||
const currentPos = brewRenderer.scrollTop;
|
const currentPos = brewRenderer.scrollTop;
|
||||||
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
const targetPos = window.frames['BrewRenderer'].contentDocument.getElementById(`p${targetPage}`).getBoundingClientRect().top;
|
||||||
|
const interimPos = targetPos >= 0 ? -30 : 30;
|
||||||
|
|
||||||
const checkIfScrollComplete = ()=>{
|
|
||||||
let scrollingTimeout;
|
|
||||||
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
|
||||||
scrollingTimeout = setTimeout(()=>{
|
|
||||||
isJumping = false;
|
|
||||||
brewRenderer.removeEventListener('scroll', checkIfScrollComplete);
|
|
||||||
}, 150); // If 150 ms pass without a brewRenderer scroll event, assume scrolling is done
|
|
||||||
};
|
|
||||||
|
|
||||||
isJumping = true;
|
|
||||||
checkIfScrollComplete();
|
|
||||||
brewRenderer.addEventListener('scroll', checkIfScrollComplete);
|
|
||||||
|
|
||||||
if(smooth) {
|
|
||||||
const bouncePos = targetPos >= 0 ? -30 : 30; //Do a little bounce before scrolling
|
|
||||||
const bounceDelay = 100;
|
const bounceDelay = 100;
|
||||||
const scrollDelay = 500;
|
const scrollDelay = 500;
|
||||||
|
|
||||||
if(!this.throttleBrewMove) {
|
if(!this.throttleBrewMove) {
|
||||||
this.throttleBrewMove = _.throttle((currentPos, bouncePos, targetPos)=>{
|
this.throttleBrewMove = _.throttle((currentPos, interimPos, targetPos)=>{
|
||||||
brewRenderer.scrollTo({ top: currentPos + bouncePos, behavior: 'smooth' });
|
brewRenderer.scrollTo({ top: currentPos + interimPos, behavior: 'smooth' });
|
||||||
setTimeout(()=>{
|
setTimeout(()=>{
|
||||||
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'smooth', block: 'start' });
|
||||||
}, bounceDelay);
|
}, bounceDelay);
|
||||||
}, scrollDelay, { leading: true, trailing: false });
|
}, scrollDelay, { leading: true, trailing: false });
|
||||||
};
|
};
|
||||||
this.throttleBrewMove(currentPos, bouncePos, targetPos);
|
this.throttleBrewMove(currentPos, interimPos, targetPos);
|
||||||
} else {
|
|
||||||
brewRenderer.scrollTo({ top: currentPos + targetPos, behavior: 'instant', block: 'start' });
|
// const hashPage = (page != 1) ? `p${page}` : '';
|
||||||
}
|
// window.location.hash = hashPage;
|
||||||
},
|
},
|
||||||
|
|
||||||
sourceJump : function(targetPage=this.props.currentBrewRendererPageNum, smooth=true){
|
sourceJump : function(targetLine=null){
|
||||||
if(!this.isText() || isJumping)
|
if(this.isText()) {
|
||||||
return;
|
if(targetLine == null) {
|
||||||
|
targetLine = 0;
|
||||||
|
|
||||||
|
const pageCollection = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('page');
|
||||||
|
const brewRendererHeight = window.frames['BrewRenderer'].contentDocument.getElementsByClassName('brewRenderer').item(0).getBoundingClientRect().height;
|
||||||
|
|
||||||
|
let currentPage = 1;
|
||||||
|
for (const page of pageCollection) {
|
||||||
|
if(page.getBoundingClientRect().bottom > (brewRendererHeight / 2)) {
|
||||||
|
currentPage = parseInt(page.id.slice(1)) || 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
const textSplit = this.props.renderer == 'V3' ? /^\\page$/gm : /\\page/;
|
||||||
const textString = this.props.brew.text.split(textSplit).slice(0, targetPage-1).join(textSplit);
|
const textString = this.props.brew.text.split(textSplit).slice(0, currentPage-1).join(textSplit);
|
||||||
const targetLine = textString.match('\n') ? textString.split('\n').length - 1 : -1;
|
const textPosition = textString.length;
|
||||||
|
const lineCount = textString.match('\n') ? textString.slice(0, textPosition).split('\n').length : 0;
|
||||||
|
|
||||||
|
targetLine = lineCount - 1; //Scroll to `\page`, which is one line back.
|
||||||
|
|
||||||
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
let currentY = this.codeEditor.current.codeMirror.getScrollInfo().top;
|
||||||
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
let targetY = this.codeEditor.current.codeMirror.heightAtLine(targetLine, 'local', true);
|
||||||
|
|
||||||
const checkIfScrollComplete = ()=>{
|
|
||||||
let scrollingTimeout;
|
|
||||||
clearTimeout(scrollingTimeout); // Reset the timer every time a scroll event occurs
|
|
||||||
scrollingTimeout = setTimeout(()=>{
|
|
||||||
isJumping = false;
|
|
||||||
this.codeEditor.current.codeMirror.off('scroll', checkIfScrollComplete);
|
|
||||||
}, 150); // If 150 ms pass without a scroll event, assume scrolling is done
|
|
||||||
};
|
|
||||||
|
|
||||||
isJumping = true;
|
|
||||||
checkIfScrollComplete();
|
|
||||||
this.codeEditor.current.codeMirror.on('scroll', checkIfScrollComplete);
|
|
||||||
|
|
||||||
if(smooth) {
|
|
||||||
//Scroll 1/10 of the way every 10ms until 1px off.
|
//Scroll 1/10 of the way every 10ms until 1px off.
|
||||||
const incrementalScroll = setInterval(()=>{
|
const incrementalScroll = setInterval(()=>{
|
||||||
currentY += (targetY - currentY) / 10;
|
currentY += (targetY - currentY) / 10;
|
||||||
@@ -396,10 +325,7 @@ const Editor = createClass({
|
|||||||
clearInterval(incrementalScroll);
|
clearInterval(incrementalScroll);
|
||||||
}
|
}
|
||||||
}, 10);
|
}, 10);
|
||||||
} else {
|
}
|
||||||
this.codeEditor.current.codeMirror.scrollTo(null, targetY); // Scroll any remaining difference
|
|
||||||
this.codeEditor.current.setCursorPosition({ line: targetLine + 1, ch: 0 });
|
|
||||||
this.codeEditor.current.codeMirror.addLineClass(targetLine + 1, 'wrap', 'sourceMoveFlash');
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -455,8 +381,7 @@ const Editor = createClass({
|
|||||||
<MetadataEditor
|
<MetadataEditor
|
||||||
metadata={this.props.brew}
|
metadata={this.props.brew}
|
||||||
onChange={this.props.onMetaChange}
|
onChange={this.props.onMetaChange}
|
||||||
reportError={this.props.reportError}
|
reportError={this.props.reportError}/>
|
||||||
userThemes={this.props.userThemes}/>
|
|
||||||
</>;
|
</>;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -499,10 +424,7 @@ const Editor = createClass({
|
|||||||
historySize={this.historySize()}
|
historySize={this.historySize()}
|
||||||
currentEditorTheme={this.state.editorTheme}
|
currentEditorTheme={this.state.editorTheme}
|
||||||
updateEditorTheme={this.updateEditorTheme}
|
updateEditorTheme={this.updateEditorTheme}
|
||||||
snippetBundle={this.props.snippetBundle}
|
cursorPos={this.codeEditor.current?.getCursorPosition() || {}} />
|
||||||
cursorPos={this.codeEditor.current?.getCursorPosition() || {}}
|
|
||||||
updateBrew={this.props.updateBrew}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{this.renderEditor()}
|
{this.renderEditor()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
.editor {
|
.editor {
|
||||||
position : relative;
|
position : relative;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
container: editor / inline-size;
|
|
||||||
|
|
||||||
.codeEditor {
|
.codeEditor {
|
||||||
height : 100%;
|
height : 100%;
|
||||||
|
|||||||
@@ -3,11 +3,10 @@ require('./metadataEditor.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
import request from '../../utils/request-middleware.js';
|
const request = require('../../utils/request-middleware.js');
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Combobox = require('client/components/combobox.jsx');
|
const Combobox = require('client/components/combobox.jsx');
|
||||||
const TagInput = require('../tagInput/tagInput.jsx');
|
const StringArrayEditor = require('../stringArrayEditor/stringArrayEditor.jsx');
|
||||||
|
|
||||||
|
|
||||||
const Themes = require('themes/themes.json');
|
const Themes = require('themes/themes.json');
|
||||||
const validations = require('./validations.js');
|
const validations = require('./validations.js');
|
||||||
@@ -28,7 +27,6 @@ const MetadataEditor = createClass({
|
|||||||
return {
|
return {
|
||||||
metadata : {
|
metadata : {
|
||||||
editId : null,
|
editId : null,
|
||||||
shareId : null,
|
|
||||||
title : '',
|
title : '',
|
||||||
description : '',
|
description : '',
|
||||||
thumbnail : '',
|
thumbnail : '',
|
||||||
@@ -100,7 +98,7 @@ const MetadataEditor = createClass({
|
|||||||
if(renderer == 'legacy')
|
if(renderer == 'legacy')
|
||||||
this.props.metadata.theme = '5ePHB';
|
this.props.metadata.theme = '5ePHB';
|
||||||
}
|
}
|
||||||
this.props.onChange(this.props.metadata, 'renderer');
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
handlePublish : function(val){
|
handlePublish : function(val){
|
||||||
this.props.onChange({
|
this.props.onChange({
|
||||||
@@ -112,7 +110,7 @@ const MetadataEditor = createClass({
|
|||||||
handleTheme : function(theme){
|
handleTheme : function(theme){
|
||||||
this.props.metadata.renderer = theme.renderer;
|
this.props.metadata.renderer = theme.renderer;
|
||||||
this.props.metadata.theme = theme.path;
|
this.props.metadata.theme = theme.path;
|
||||||
this.props.onChange(this.props.metadata, 'theme');
|
this.props.onChange(this.props.metadata);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleLanguage : function(languageCode){
|
handleLanguage : function(languageCode){
|
||||||
@@ -193,42 +191,37 @@ const MetadataEditor = createClass({
|
|||||||
renderThemeDropdown : function(){
|
renderThemeDropdown : function(){
|
||||||
if(!global.enable_themes) return;
|
if(!global.enable_themes) return;
|
||||||
|
|
||||||
const mergedThemes = _.merge(Themes, this.props.userThemes);
|
|
||||||
|
|
||||||
const listThemes = (renderer)=>{
|
const listThemes = (renderer)=>{
|
||||||
return _.map(_.values(mergedThemes[renderer]), (theme)=>{
|
return _.map(_.values(Themes[renderer]), (theme)=>{
|
||||||
if(theme.path == this.props.metadata.shareId) return;
|
return <div className='item' key={''} onClick={()=>this.handleTheme(theme)} title={''}>
|
||||||
const preview = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`;
|
{`${theme.renderer} : ${theme.name}`}
|
||||||
const texture = theme.thumbnail || `/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`;
|
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownTexture.png`}/>
|
||||||
return <div className='item' key={`${renderer}_${theme.name}`} onClick={()=>this.handleTheme(theme)} title={''}>
|
|
||||||
{theme.author ?? renderer} : {theme.name}
|
|
||||||
<div className='texture-container'>
|
|
||||||
<img src={texture}/>
|
|
||||||
</div>
|
|
||||||
<div className='preview'>
|
<div className='preview'>
|
||||||
<h6>{theme.name} preview</h6>
|
<h6>{`${theme.name}`} preview</h6>
|
||||||
<img src={preview}/>
|
<img src={`/themes/${theme.renderer}/${theme.path}/dropdownPreview.png`}/>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const currentRenderer = this.props.metadata.renderer;
|
const currentTheme = Themes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme];
|
||||||
const currentTheme = mergedThemes[`${_.upperFirst(this.props.metadata.renderer)}`][this.props.metadata.theme]
|
|
||||||
?? { name: `!!! THEME MISSING !!! ID=${this.props.metadata.theme}` };
|
|
||||||
let dropdown;
|
let dropdown;
|
||||||
|
|
||||||
if(currentRenderer == 'legacy') {
|
if(this.props.metadata.renderer == 'legacy') {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='disabled value' trigger='disabled'>
|
<Nav.dropdown className='disabled value' trigger='disabled'>
|
||||||
<div> {`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i> </div>
|
<div>
|
||||||
|
{`Themes are not supported in the Legacy Renderer`} <i className='fas fa-caret-down'></i>
|
||||||
|
</div>
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
} else {
|
} else {
|
||||||
dropdown =
|
dropdown =
|
||||||
<Nav.dropdown className='value' trigger='click'>
|
<Nav.dropdown className='value' trigger='click'>
|
||||||
<div> {currentTheme.author ?? _.upperFirst(currentRenderer)} : {currentTheme.name} <i className='fas fa-caret-down'></i> </div>
|
<div>
|
||||||
|
{`${_.upperFirst(currentTheme.renderer)} : ${currentTheme.name}`} <i className='fas fa-caret-down'></i>
|
||||||
{listThemes(currentRenderer)}
|
</div>
|
||||||
|
{/*listThemes('Legacy')*/}
|
||||||
|
{listThemes('V3')}
|
||||||
</Nav.dropdown>;
|
</Nav.dropdown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,14 +297,17 @@ const MetadataEditor = createClass({
|
|||||||
onChange={(e)=>this.handleRenderer('V3', e)} />
|
onChange={(e)=>this.handleRenderer('V3', e)} />
|
||||||
V3
|
V3
|
||||||
</label>
|
</label>
|
||||||
<small><a href='/legacy' target='_blank' rel='noopener noreferrer'>Click here to see the demo page for the old Legacy renderer!</a></small>
|
|
||||||
|
<a href='/legacy' target='_blank' rel='noopener noreferrer'>
|
||||||
|
Click here to see the demo page for the old Legacy renderer!
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
return <div className='metadataEditor'>
|
return <div className='metadataEditor'>
|
||||||
<h1>Properties Editor</h1>
|
<h1 className='sectionHead'>Brew</h1>
|
||||||
|
|
||||||
<div className='field title'>
|
<div className='field title'>
|
||||||
<label>title</label>
|
<label>title</label>
|
||||||
@@ -341,11 +337,10 @@ const MetadataEditor = createClass({
|
|||||||
{this.renderThumbnail()}
|
{this.renderThumbnail()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TagInput label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
<StringArrayEditor label='tags' valuePatterns={[/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.\-]{0,40}$/]}
|
||||||
placeholder='add tag' unique={true}
|
placeholder='add tag' unique={true}
|
||||||
values={this.props.metadata.tags}
|
values={this.props.metadata.tags}
|
||||||
onChange={(e)=>this.handleFieldChange('tags', e)}
|
onChange={(e)=>this.handleFieldChange('tags', e)}/>
|
||||||
/>
|
|
||||||
|
|
||||||
<div className='field systems'>
|
<div className='field systems'>
|
||||||
<label>systems</label>
|
<label>systems</label>
|
||||||
@@ -360,25 +355,28 @@ const MetadataEditor = createClass({
|
|||||||
|
|
||||||
{this.renderRenderOptions()}
|
{this.renderRenderOptions()}
|
||||||
|
|
||||||
<h2>Authors</h2>
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Authors</h1>
|
||||||
|
|
||||||
{this.renderAuthors()}
|
{this.renderAuthors()}
|
||||||
|
|
||||||
<TagInput label='invited authors' valuePatterns={[/.+/]}
|
<StringArrayEditor label='invited authors' valuePatterns={[/.+/]}
|
||||||
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
|
||||||
placeholder='invite author' unique={true}
|
placeholder='invite author' unique={true}
|
||||||
values={this.props.metadata.invitedAuthors}
|
values={this.props.metadata.invitedAuthors}
|
||||||
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
notes={['Invited author usernames are case sensitive.', 'After adding an invited author, send them the edit link. There, they can choose to accept or decline the invitation.']}
|
||||||
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}
|
onChange={(e)=>this.handleFieldChange('invitedAuthors', e)}/>
|
||||||
/>
|
|
||||||
|
|
||||||
<h2>Privacy</h2>
|
<hr/>
|
||||||
|
|
||||||
|
<h1 className='sectionHead'>Privacy</h1>
|
||||||
|
|
||||||
<div className='field publish'>
|
<div className='field publish'>
|
||||||
<label>publish</label>
|
<label>publish</label>
|
||||||
<div className='value'>
|
<div className='value'>
|
||||||
{this.renderPublish()}
|
{this.renderPublish()}
|
||||||
<small>Published brews are searchable in <a href='/vault'>the Vault</a> and visible on your user page. Unpublished brews are not indexed in the Vault or visible on your user page, but can still be shared and indexed by search engines. You can unpublish a brew any time.</small>
|
<small>Published homebrews will be publicly viewable and searchable (eventually...)</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,265 +1,268 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
|
|
||||||
.metadataEditor{
|
.metadataEditor{
|
||||||
position : absolute;
|
position : absolute;
|
||||||
z-index : 5;
|
z-index : 5;
|
||||||
box-sizing : border-box;
|
box-sizing : border-box;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
|
||||||
padding : 25px;
|
padding : 25px;
|
||||||
|
background-color : #999;
|
||||||
|
height : calc(100vh - 54px); // 54px is the height of the navbar + snippet bar. probably a better way to dynamic get this.
|
||||||
overflow-y : auto;
|
overflow-y : auto;
|
||||||
background-color : #999999;
|
|
||||||
font-size : 13px;
|
|
||||||
|
|
||||||
h1 {
|
.sectionHead {
|
||||||
margin: 0 0 40px;
|
font-weight: 1000;
|
||||||
font-weight: bold;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin: 20px 0;
|
margin: 20px 0;
|
||||||
font-weight : bold;
|
|
||||||
border-bottom: 2px solid gray;
|
&:first-of-type {
|
||||||
color: #555;
|
margin-top: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
& > div { margin-bottom : 10px; }
|
& > div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
.field-group {
|
.field-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
width : 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.field-column {
|
.field-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex : 5 0 200px;
|
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
flex: 5 0 200px;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.field{
|
.field{
|
||||||
position : relative;
|
|
||||||
display : flex;
|
display : flex;
|
||||||
flex-wrap : wrap;
|
flex-wrap : wrap;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
min-width : 200px;
|
min-width : 200px;
|
||||||
|
position : relative;
|
||||||
&>label{
|
&>label{
|
||||||
width : 80px;
|
width : 80px;
|
||||||
|
font-size : 11px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : 1.8em;
|
line-height : 1.8em;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
font-size: .9em;
|
|
||||||
}
|
}
|
||||||
&>.value{
|
&>.value{
|
||||||
flex : 1 1 auto;
|
flex : 1 1 auto;
|
||||||
width : 50px;
|
width : 50px;
|
||||||
&:invalid { background : #FFB9B9; }
|
&:invalid {
|
||||||
small {
|
background : #ffb9b9;
|
||||||
display : block;
|
|
||||||
font-size : 0.9em;
|
|
||||||
font-style : italic;
|
|
||||||
line-height : 1.4em;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
input[type='text'], textarea {
|
input[type='text'], textarea {
|
||||||
border : 1px solid gray;
|
border : 1px solid gray;
|
||||||
&:focus { outline : 1px solid #444444; }
|
&:focus {
|
||||||
|
outline: 1px solid #444;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
&.thumbnail{
|
&.thumbnail{
|
||||||
height : 1.4em;
|
height : 1.4em;
|
||||||
label { line-height : 2.0em; }
|
label{
|
||||||
|
line-height: 2.0em;
|
||||||
|
}
|
||||||
.value{
|
.value{
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
button{
|
button{
|
||||||
.colorButton();
|
border: 1px solid #999;
|
||||||
padding : 0px 5px;
|
|
||||||
color: white;
|
color: white;
|
||||||
|
padding: 0px 5px;
|
||||||
background-color: black;
|
background-color: black;
|
||||||
border : 1px solid #999999;
|
&:hover{
|
||||||
&:hover { background-color : #777777; }
|
background-color: #777;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.description {
|
&.description {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
textarea.value {
|
textarea.value {
|
||||||
|
resize : none;
|
||||||
height : auto;
|
height : auto;
|
||||||
font-family : 'Open Sans', sans-serif;
|
font-family : 'Open Sans', sans-serif;
|
||||||
resize : none;
|
font-size : 0.8em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.language .language-dropdown {
|
&.language .language-dropdown {
|
||||||
z-index : 200;
|
|
||||||
max-width : 150px;
|
max-width : 150px;
|
||||||
|
z-index : 200;
|
||||||
|
}
|
||||||
|
small {
|
||||||
|
font-size : 0.6em;
|
||||||
|
font-style : italic;
|
||||||
|
line-height : 1.4em;
|
||||||
|
display : inline-block;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.thumbnail-preview {
|
.thumbnail-preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex : 1 1;
|
|
||||||
justify-self: center;
|
justify-self: center;
|
||||||
width: 80px;
|
width: 80px;
|
||||||
height: min-content;
|
height: min-content;
|
||||||
|
flex: 1 1;
|
||||||
max-height: 115px;
|
max-height: 115px;
|
||||||
aspect-ratio: 1 / 1;
|
aspect-ratio: 1 / 1;
|
||||||
object-fit: contain;
|
object-fit: contain;
|
||||||
background-color : #AAAAAA;
|
background-color: #AAA;
|
||||||
}
|
}
|
||||||
|
|
||||||
.systems.field .value{
|
.systems.field .value{
|
||||||
label{
|
label{
|
||||||
|
vertical-align : middle;
|
||||||
|
margin-right : 15px;
|
||||||
|
cursor : pointer;
|
||||||
|
font-size : 0.7em;
|
||||||
|
font-weight : 800;
|
||||||
|
user-select : none;
|
||||||
|
white-space : nowrap;
|
||||||
display : inline-flex;
|
display : inline-flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
margin-right : 15px;
|
}
|
||||||
font-size : 0.9em;
|
a {
|
||||||
|
font-size : 0.7em;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
white-space : nowrap;
|
display : inline-flex;
|
||||||
vertical-align : middle;
|
|
||||||
cursor : pointer;
|
|
||||||
user-select : none;
|
|
||||||
}
|
}
|
||||||
input{
|
input{
|
||||||
margin : 3px;
|
|
||||||
vertical-align : middle;
|
vertical-align : middle;
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
|
margin : 3px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.publish.field .value{
|
.publish.field .value{
|
||||||
position : relative;
|
position : relative;
|
||||||
margin-bottom: 15px;
|
margin-bottom: 15px;
|
||||||
button { width : 100%; }
|
button{
|
||||||
|
width:100%;
|
||||||
|
}
|
||||||
button.publish{
|
button.publish{
|
||||||
.colorButton(@blueLight);
|
.button(@blueLight);
|
||||||
}
|
}
|
||||||
button.unpublish{
|
button.unpublish{
|
||||||
.colorButton(@silver);
|
.button(@silver);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete.field .value{
|
.delete.field .value{
|
||||||
button{
|
button{
|
||||||
.colorButton(@red);
|
.button(@red);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.authors.field .value{
|
.authors.field .value{
|
||||||
|
font-size: 0.8em;
|
||||||
line-height : 1.5em;
|
line-height : 1.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.themes.field{
|
.themes.field{
|
||||||
|
font-size : 13.33px;
|
||||||
.navDropdownContainer {
|
.navDropdownContainer {
|
||||||
|
background-color : white;
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 100;
|
z-index : 100;
|
||||||
background-color : white;
|
|
||||||
&.disabled {
|
&.disabled {
|
||||||
font-style :italic;
|
font-style :italic;
|
||||||
color : dimgray;
|
font-style : italic;
|
||||||
background-color : darkgray;
|
background-color : darkgray;
|
||||||
|
color : dimgray;
|
||||||
}
|
}
|
||||||
&>div:first-child {
|
&>div:first-child {
|
||||||
padding : 3px 3px;
|
border : 2px solid rgb(118,118,118);
|
||||||
|
padding : 6px 3px;
|
||||||
background-color : inherit;
|
background-color : inherit;
|
||||||
border : 1px solid gray;
|
i {
|
||||||
i { float : right; }
|
float : right;
|
||||||
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color : white;
|
|
||||||
background-color : @blue;
|
background-color : @blue;
|
||||||
|
color : white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navDropdown .item > p {
|
|
||||||
width : 45%;
|
|
||||||
height : 1.1em;
|
|
||||||
overflow : hidden;
|
|
||||||
text-overflow : ellipsis;
|
|
||||||
white-space : nowrap;
|
|
||||||
}
|
|
||||||
.navDropdown {
|
.navDropdown {
|
||||||
|
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
||||||
position : absolute;
|
position : absolute;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
box-shadow : 0px 5px 10px rgba(0, 0, 0, 0.3);
|
|
||||||
.item {
|
.item {
|
||||||
position : relative;
|
|
||||||
padding : 3px 3px;
|
padding : 3px 3px;
|
||||||
|
border-top : 1px solid rgb(118, 118, 118);
|
||||||
|
position : relative;
|
||||||
overflow : visible;
|
overflow : visible;
|
||||||
background-color : white;
|
background-color : white;
|
||||||
border-top : 1px solid rgb(118, 118, 118);
|
|
||||||
.preview {
|
.preview {
|
||||||
|
display : flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background : #ccc;
|
||||||
|
border-radius : 5px;
|
||||||
|
box-shadow : 0 0 5px black;
|
||||||
|
width : 200px;
|
||||||
|
color :black;
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 0;
|
top : 0;
|
||||||
right : 0;
|
right : 0;
|
||||||
z-index : 1;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
width : 200px;
|
|
||||||
overflow : hidden;
|
|
||||||
color : black;
|
|
||||||
background : #CCCCCC;
|
|
||||||
border-radius : 5px;
|
|
||||||
box-shadow : 0 0 5px black;
|
|
||||||
opacity : 0;
|
opacity : 0;
|
||||||
transition : opacity 250ms ease;
|
transition : opacity 250ms ease;
|
||||||
|
z-index : 1;
|
||||||
|
overflow :hidden;
|
||||||
h6 {
|
h6 {
|
||||||
padding-block : 0.5em;
|
|
||||||
padding-inline : 1em;
|
|
||||||
font-weight : 900;
|
font-weight : 900;
|
||||||
|
padding-inline:1em;
|
||||||
|
padding-block :.5em;
|
||||||
border-bottom :2px solid hsl(0,0%,40%);
|
border-bottom :2px solid hsl(0,0%,40%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&:hover {
|
&:hover {
|
||||||
color : white;
|
|
||||||
background-color : @blue;
|
background-color : @blue;
|
||||||
|
color : white;
|
||||||
|
}
|
||||||
|
&:hover > .preview {
|
||||||
|
opacity: 1;
|
||||||
}
|
}
|
||||||
&:hover > .preview { opacity : 1; }
|
|
||||||
.texture-container {
|
|
||||||
position : absolute;
|
|
||||||
top : 0;
|
|
||||||
left : 0;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
min-height : 100%;
|
|
||||||
overflow : hidden;
|
|
||||||
>img {
|
>img {
|
||||||
position : absolute;
|
|
||||||
top : 0px;
|
|
||||||
right : 0;
|
|
||||||
width : 50%;
|
|
||||||
min-height : 100%;
|
|
||||||
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
|
||||||
mask-image : linear-gradient(90deg, transparent, black 20%);
|
mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
|
-webkit-mask-image : linear-gradient(90deg, transparent, black 20%);
|
||||||
|
position : absolute;
|
||||||
|
right : 0;
|
||||||
|
top : 0px;
|
||||||
|
width : 50%;
|
||||||
|
height : 100%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
.field .list {
|
.field .list {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex: 1 0;
|
flex: 1 0;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
|
|
||||||
> * { flex : 0 0 auto; }
|
> * {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
#groupedIcon {
|
#groupedIcon {
|
||||||
#backgroundColors;
|
#backgroundColors;
|
||||||
|
display: inline-block;
|
||||||
|
height: ~"calc(100% + 0.6em)";
|
||||||
position: relative;
|
position: relative;
|
||||||
top: -0.3em;
|
top: -0.3em;
|
||||||
right: -0.3em;
|
right: -0.3em;
|
||||||
display : inline-block;
|
|
||||||
min-width : 20px;
|
|
||||||
height : ~'calc(100% + 0.6em)';
|
|
||||||
color : white;
|
|
||||||
text-align : center;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
min-width: 20px;
|
||||||
|
text-align: center;
|
||||||
|
color: white;
|
||||||
|
|
||||||
i {
|
i {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -267,27 +270,37 @@
|
|||||||
transform: translateY(-50%);
|
transform: translateY(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(:last-child) { border-right : 1px solid black; }
|
&:not(:last-child) {
|
||||||
|
border-right: 1px solid black;
|
||||||
&:last-child { border-radius : 0 0.5em 0.5em 0; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.tag {
|
&:last-child {
|
||||||
padding : 0.3em;
|
border-radius: 0 0.5em 0.5em 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
background-color: #dddddd;
|
||||||
|
border-radius: .5em;
|
||||||
|
font-size: .9em;
|
||||||
margin: 2px;
|
margin: 2px;
|
||||||
font-size : 0.9em;
|
padding: .3em;
|
||||||
background-color : #DDDDDD;
|
|
||||||
border-radius : 0.5em;
|
|
||||||
|
|
||||||
.icon { #groupedIcon; }
|
.icon {
|
||||||
|
#groupedIcon
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.input-group {
|
||||||
height : ~'calc(.9em + 4px + .6em)';
|
height: ~"calc(.9em + 4px + .6em)";
|
||||||
|
|
||||||
input { border-radius : 0.5em 0 0 0.5em; }
|
input {
|
||||||
|
border-radius: .5em 0 0 .5em;
|
||||||
|
}
|
||||||
|
|
||||||
input:last-child { border-radius : 0.5em; }
|
input:last-child {
|
||||||
|
border-radius: .5em;
|
||||||
|
}
|
||||||
|
|
||||||
.value {
|
.value {
|
||||||
width: 7.5vw;
|
width: 7.5vw;
|
||||||
@@ -295,28 +308,19 @@
|
|||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input-group {
|
.invalid:focus {
|
||||||
height : ~'calc(.9em + 4px + .6em)';
|
background-color: pink;
|
||||||
|
|
||||||
input { border-radius : 0.5em 0 0 0.5em; }
|
|
||||||
|
|
||||||
input:last-child { border-radius : 0.5em; }
|
|
||||||
|
|
||||||
.value {
|
|
||||||
width : 7.5vw;
|
|
||||||
min-width : 75px;
|
|
||||||
height : 100%;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.invalid:focus { background-color : pink; }
|
|
||||||
|
|
||||||
.icon {
|
.icon {
|
||||||
#groupedIcon;
|
#groupedIcon;
|
||||||
top : -0.54em;
|
|
||||||
right : 1px;
|
|
||||||
height: 97%;
|
height: 97%;
|
||||||
|
font-size: .8em;
|
||||||
|
right: 1px;
|
||||||
|
top: -.54em;
|
||||||
|
|
||||||
i { font-size : 1.125em; }
|
i {
|
||||||
|
font-size: 1.125em;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 350, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 250, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
require('./snippetbar.less');
|
require('./snippetbar.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
|
|
||||||
import { loadHistory } from '../../utils/versionHistory.js';
|
|
||||||
|
|
||||||
//Import all themes
|
//Import all themes
|
||||||
|
|
||||||
|
const Themes = require('themes/themes.json');
|
||||||
|
|
||||||
const ThemeSnippets = {};
|
const ThemeSnippets = {};
|
||||||
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
ThemeSnippets['Legacy_5ePHB'] = require('themes/Legacy/5ePHB/snippets.js');
|
||||||
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
ThemeSnippets['V3_5ePHB'] = require('themes/V3/5ePHB/snippets.js');
|
||||||
@@ -39,9 +40,7 @@ const Snippetbar = createClass({
|
|||||||
foldCode : ()=>{},
|
foldCode : ()=>{},
|
||||||
unfoldCode : ()=>{},
|
unfoldCode : ()=>{},
|
||||||
updateEditorTheme : ()=>{},
|
updateEditorTheme : ()=>{},
|
||||||
cursorPos : {},
|
cursorPos : {}
|
||||||
snippetBundle : [],
|
|
||||||
updateBrew : ()=>{}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -49,70 +48,53 @@ const Snippetbar = createClass({
|
|||||||
return {
|
return {
|
||||||
renderer : this.props.renderer,
|
renderer : this.props.renderer,
|
||||||
themeSelector : false,
|
themeSelector : false,
|
||||||
snippets : [],
|
snippets : []
|
||||||
showHistory : false,
|
|
||||||
historyExists : false,
|
|
||||||
historyItems : []
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : async function(prevState) {
|
componentDidMount : async function() {
|
||||||
const snippets = this.compileSnippets();
|
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||||
|
const themePath = this.props.theme ?? '5ePHB';
|
||||||
|
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||||
|
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : snippets
|
snippets : snippets
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidUpdate : async function(prevProps, prevState) {
|
componentDidUpdate : async function(prevProps) {
|
||||||
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme || prevProps.snippetBundle != this.props.snippetBundle) {
|
if(prevProps.renderer != this.props.renderer || prevProps.theme != this.props.theme) {
|
||||||
|
const rendererPath = this.props.renderer == 'V3' ? 'V3' : 'Legacy';
|
||||||
|
const themePath = this.props.theme ?? '5ePHB';
|
||||||
|
let snippets = _.cloneDeep(ThemeSnippets[`${rendererPath}_${themePath}`]);
|
||||||
|
snippets = this.compileSnippets(rendererPath, themePath, snippets);
|
||||||
this.setState({
|
this.setState({
|
||||||
snippets : this.compileSnippets()
|
snippets : snippets
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update history list if it has changed
|
|
||||||
const checkHistoryItems = await loadHistory(this.props.brew);
|
|
||||||
|
|
||||||
// If all items have the noData property, there is no saved data
|
|
||||||
const checkHistoryExists = !checkHistoryItems.every((historyItem)=>{
|
|
||||||
return historyItem?.noData;
|
|
||||||
});
|
|
||||||
if(prevState.historyExists != checkHistoryExists){
|
|
||||||
this.setState({
|
|
||||||
historyExists : checkHistoryExists
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any history items have changed, update the list
|
|
||||||
if(checkHistoryExists && checkHistoryItems.some((historyItem, index)=>{
|
|
||||||
return index >= prevState.historyItems.length || !_.isEqual(historyItem, prevState.historyItems[index]);
|
|
||||||
})){
|
|
||||||
this.setState({
|
|
||||||
historyItems : checkHistoryItems
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
mergeCustomizer : function(oldValue, newValue, key) {
|
|
||||||
|
mergeCustomizer : function(valueA, valueB, key) {
|
||||||
if(key == 'snippets') {
|
if(key == 'snippets') {
|
||||||
const result = _.reverse(_.unionBy(_.reverse(newValue), _.reverse(oldValue), 'name')); // Join snippets together, with preference for the child theme over the parent theme
|
const result = _.reverse(_.unionBy(_.reverse(valueB), _.reverse(valueA), 'name')); // Join snippets together, with preference for the current theme over the base theme
|
||||||
return result.filter((snip)=>snip.gen || snip.subsnippets);
|
return _.filter(result, 'gen'); //Only keep snippets with a 'gen' property.
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
compileSnippets : function() {
|
compileSnippets : function(rendererPath, themePath, snippets) {
|
||||||
let compiledSnippets = [];
|
let compiledSnippets = snippets;
|
||||||
|
const baseSnippetsPath = Themes[rendererPath][themePath].baseSnippets;
|
||||||
|
|
||||||
let oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
const objB = _.keyBy(compiledSnippets, 'groupName');
|
||||||
|
|
||||||
for (let snippets of this.props.snippetBundle) {
|
if(baseSnippetsPath) {
|
||||||
if(typeof(snippets) == 'string') // load staticThemes as needed; they were sent as just a file name
|
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_${baseSnippetsPath}`]), 'groupName');
|
||||||
snippets = ThemeSnippets[snippets];
|
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||||
|
compiledSnippets = this.compileSnippets(rendererPath, baseSnippetsPath, _.cloneDeep(compiledSnippets));
|
||||||
const newSnippets = _.keyBy(_.cloneDeep(snippets), 'groupName');
|
} else {
|
||||||
compiledSnippets = _.values(_.mergeWith(oldSnippets, newSnippets, this.mergeCustomizer));
|
const objA = _.keyBy(_.cloneDeep(ThemeSnippets[`${rendererPath}_Blank`]), 'groupName');
|
||||||
|
compiledSnippets = _.values(_.mergeWith(objA, objB, this.mergeCustomizer));
|
||||||
oldSnippets = _.keyBy(compiledSnippets, 'groupName');
|
|
||||||
}
|
}
|
||||||
return compiledSnippets;
|
return compiledSnippets;
|
||||||
},
|
},
|
||||||
@@ -150,10 +132,8 @@ const Snippetbar = createClass({
|
|||||||
|
|
||||||
renderSnippetGroups : function(){
|
renderSnippetGroups : function(){
|
||||||
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
const snippets = this.state.snippets.filter((snippetGroup)=>snippetGroup.view === this.props.view);
|
||||||
if(snippets.length === 0) return null;
|
|
||||||
|
|
||||||
return <div className='snippets'>
|
return _.map(snippets, (snippetGroup)=>{
|
||||||
{_.map(snippets, (snippetGroup)=>{
|
|
||||||
return <SnippetGroup
|
return <SnippetGroup
|
||||||
brew={this.props.brew}
|
brew={this.props.brew}
|
||||||
groupName={snippetGroup.groupName}
|
groupName={snippetGroup.groupName}
|
||||||
@@ -163,68 +143,29 @@ const Snippetbar = createClass({
|
|||||||
onSnippetClick={this.handleSnippetClick}
|
onSnippetClick={this.handleSnippetClick}
|
||||||
cursorPos={this.props.cursorPos}
|
cursorPos={this.props.cursorPos}
|
||||||
/>;
|
/>;
|
||||||
})
|
|
||||||
}
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
replaceContent : function(item){
|
|
||||||
return this.props.updateBrew(item);
|
|
||||||
},
|
|
||||||
|
|
||||||
toggleHistoryMenu : function(){
|
|
||||||
this.setState({
|
|
||||||
showHistory : !this.state.showHistory
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHistoryItems : function() {
|
|
||||||
if(!this.state.historyExists) return;
|
|
||||||
|
|
||||||
return <div className='dropdown'>
|
|
||||||
{_.map(this.state.historyItems, (item, index)=>{
|
|
||||||
if(item.noData || !item.savedAt) return;
|
|
||||||
|
|
||||||
const saveTime = new Date(item.savedAt);
|
|
||||||
const diffMs = new Date() - saveTime;
|
|
||||||
const diffSecs = Math.floor(diffMs / 1000);
|
|
||||||
|
|
||||||
let diffString = `about ${diffSecs} seconds ago`;
|
|
||||||
|
|
||||||
if(diffSecs > 60) diffString = `about ${Math.floor(diffSecs / 60)} minutes ago`;
|
|
||||||
if(diffSecs > (60 * 60)) diffString = `about ${Math.floor(diffSecs / (60 * 60))} hours ago`;
|
|
||||||
if(diffSecs > (24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (24 * 60 * 60))} days ago`;
|
|
||||||
if(diffSecs > (7 * 24 * 60 * 60)) diffString = `about ${Math.floor(diffSecs / (7 * 24 * 60 * 60))} weeks ago`;
|
|
||||||
|
|
||||||
return <div className='snippet' key={index} onClick={()=>{this.replaceContent(item);}} >
|
|
||||||
<i className={`fas fa-${index+1}`} />
|
|
||||||
<span className='name' title={saveTime.toISOString()}>v{item.version} : {diffString}</span>
|
|
||||||
</div>;
|
|
||||||
})}
|
|
||||||
</div>;
|
|
||||||
},
|
|
||||||
|
|
||||||
renderEditorButtons : function(){
|
renderEditorButtons : function(){
|
||||||
if(!this.props.showEditButtons) return;
|
if(!this.props.showEditButtons) return;
|
||||||
|
|
||||||
const foldButtons = <>
|
let foldButtons;
|
||||||
<div className={`editorTool foldAll ${this.props.view !== 'meta' && this.props.foldCode ? 'active' : ''}`}
|
if(this.props.view == 'text'){
|
||||||
|
foldButtons =
|
||||||
|
<>
|
||||||
|
<div className={`editorTool foldAll ${this.props.foldCode ? 'active' : ''}`}
|
||||||
onClick={this.props.foldCode} >
|
onClick={this.props.foldCode} >
|
||||||
<i className='fas fa-compress-alt' />
|
<i className='fas fa-compress-alt' />
|
||||||
</div>
|
</div>
|
||||||
<div className={`editorTool unfoldAll ${this.props.view !== 'meta' && this.props.unfoldCode ? 'active' : ''}`}
|
<div className={`editorTool unfoldAll ${this.props.unfoldCode ? 'active' : ''}`}
|
||||||
onClick={this.props.unfoldCode} >
|
onClick={this.props.unfoldCode} >
|
||||||
<i className='fas fa-expand-alt' />
|
<i className='fas fa-expand-alt' />
|
||||||
</div>
|
</div>
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
return <div className='editors'>
|
return <div className='editors'>
|
||||||
<div className='historyTools'>
|
|
||||||
<div className={`editorTool snippetGroup history ${this.state.historyExists ? 'active' : ''}`}
|
|
||||||
onClick={this.toggleHistoryMenu} >
|
|
||||||
<i className='fas fa-clock-rotate-left' />
|
|
||||||
{ this.state.showHistory && this.renderHistoryItems() }
|
|
||||||
</div>
|
|
||||||
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
<div className={`editorTool undo ${this.props.historySize.undo ? 'active' : ''}`}
|
||||||
onClick={this.props.undo} >
|
onClick={this.props.undo} >
|
||||||
<i className='fas fa-undo' />
|
<i className='fas fa-undo' />
|
||||||
@@ -233,18 +174,15 @@ const Snippetbar = createClass({
|
|||||||
onClick={this.props.redo} >
|
onClick={this.props.redo} >
|
||||||
<i className='fas fa-redo' />
|
<i className='fas fa-redo' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div className='divider'></div>
|
||||||
<div className='codeTools'>
|
|
||||||
{foldButtons}
|
{foldButtons}
|
||||||
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
<div className={`editorTool editorTheme ${this.state.themeSelector ? 'active' : ''}`}
|
||||||
onClick={this.toggleThemeSelector} >
|
onClick={this.toggleThemeSelector} >
|
||||||
<i className='fas fa-palette' />
|
<i className='fas fa-palette' />
|
||||||
{this.state.themeSelector && this.renderThemeSelector()}
|
{this.state.themeSelector && this.renderThemeSelector()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
|
<div className='divider'></div>
|
||||||
<div className='tabs'>
|
|
||||||
<div className={cx('text', { selected: this.props.view === 'text' })}
|
<div className={cx('text', { selected: this.props.view === 'text' })}
|
||||||
onClick={()=>this.props.onViewChange('text')}>
|
onClick={()=>this.props.onViewChange('text')}>
|
||||||
<i className='fa fa-beer' />
|
<i className='fa fa-beer' />
|
||||||
@@ -257,8 +195,6 @@ const Snippetbar = createClass({
|
|||||||
onClick={()=>this.props.onViewChange('meta')}>
|
onClick={()=>this.props.onViewChange('meta')}>
|
||||||
<i className='fas fa-info-circle' />
|
<i className='fas fa-info-circle' />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>;
|
</div>;
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -296,9 +232,8 @@ const SnippetGroup = createClass({
|
|||||||
return _.map(snippets, (snippet)=>{
|
return _.map(snippets, (snippet)=>{
|
||||||
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
return <div className='snippet' key={snippet.name} onClick={(e)=>this.handleSnippetClick(e, snippet)}>
|
||||||
<i className={snippet.icon} />
|
<i className={snippet.icon} />
|
||||||
<span className={`name${snippet.disabled ? ' disabled' : ''}`} title={snippet.name}>{snippet.name}</span>
|
<span className='name'title={snippet.name}>{snippet.name}</span>
|
||||||
{snippet.experimental && <span className='beta'>beta</span>}
|
{snippet.experimental && <span className='beta'>beta</span>}
|
||||||
{snippet.disabled && <span className='beta' title='temporarily disabled due to large slowdown; under re-design'>disabled</span>}
|
|
||||||
{snippet.subsnippets && <>
|
{snippet.subsnippets && <>
|
||||||
<i className='fas fa-caret-right'></i>
|
<i className='fas fa-caret-right'></i>
|
||||||
<div className='dropdown side'>
|
<div className='dropdown side'>
|
||||||
|
|||||||
@@ -4,35 +4,18 @@
|
|||||||
.snippetBar {
|
.snippetBar {
|
||||||
@menuHeight : 25px;
|
@menuHeight : 25px;
|
||||||
position : relative;
|
position : relative;
|
||||||
display : flex;
|
height : @menuHeight;
|
||||||
flex-wrap : wrap-reverse;
|
|
||||||
justify-content : space-between;
|
|
||||||
height : auto;
|
|
||||||
color : black;
|
color : black;
|
||||||
background-color : #DDDDDD;
|
background-color : #DDDDDD;
|
||||||
|
|
||||||
.snippets {
|
|
||||||
display : flex;
|
|
||||||
justify-content : flex-start;
|
|
||||||
min-width : 327.58px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.editors {
|
.editors {
|
||||||
|
position : absolute;
|
||||||
|
top : 0px;
|
||||||
|
right : 0px;
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : flex-end;
|
justify-content : space-between;
|
||||||
min-width : 225px;
|
height : @menuHeight;
|
||||||
|
|
||||||
&:only-child { margin-left : auto; }
|
|
||||||
|
|
||||||
>div {
|
|
||||||
display : flex;
|
|
||||||
flex : 1;
|
|
||||||
justify-content : space-around;
|
|
||||||
|
|
||||||
&:first-child { border-left : none; }
|
|
||||||
|
|
||||||
& > div {
|
& > div {
|
||||||
position : relative;
|
|
||||||
width : @menuHeight;
|
width : @menuHeight;
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
@@ -63,26 +46,12 @@
|
|||||||
&.foldAll {
|
&.foldAll {
|
||||||
.tooltipLeft('Fold All');
|
.tooltipLeft('Fold All');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : grey;
|
color : inherit;
|
||||||
&.active { color : inherit; }
|
|
||||||
}
|
}
|
||||||
&.unfoldAll {
|
&.unfoldAll {
|
||||||
.tooltipLeft('Unfold All');
|
.tooltipLeft('Unfold All');
|
||||||
font-size : 0.75em;
|
font-size : 0.75em;
|
||||||
color : grey;
|
color : inherit;
|
||||||
&.active { color : inherit; }
|
|
||||||
}
|
|
||||||
&.history {
|
|
||||||
.tooltipLeft('History');
|
|
||||||
position : relative;
|
|
||||||
font-size : 0.75em;
|
|
||||||
color : grey;
|
|
||||||
border : none;
|
|
||||||
&.active { color : inherit; }
|
|
||||||
& > .dropdown {
|
|
||||||
right : -1px;
|
|
||||||
& > .snippet { padding-right : 10px; }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
&.editorTheme {
|
&.editorTheme {
|
||||||
.tooltipLeft('Editor Themes');
|
.tooltipLeft('Editor Themes');
|
||||||
@@ -112,7 +81,6 @@
|
|||||||
background-color : inherit;
|
background-color : inherit;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
.snippetBarButton {
|
.snippetBarButton {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
height : @menuHeight;
|
height : @menuHeight;
|
||||||
@@ -121,7 +89,6 @@
|
|||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
line-height : @menuHeight;
|
line-height : @menuHeight;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
text-wrap : nowrap;
|
|
||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
&:hover, &.selected { background-color : #999999; }
|
&:hover, &.selected { background-color : #999999; }
|
||||||
i {
|
i {
|
||||||
@@ -138,7 +105,7 @@
|
|||||||
.tooltipLeft('Edit Brew Properties');
|
.tooltipLeft('Edit Brew Properties');
|
||||||
}
|
}
|
||||||
.snippetGroup {
|
.snippetGroup {
|
||||||
|
border-right : 1px solid currentColor;
|
||||||
&:hover {
|
&:hover {
|
||||||
& > .dropdown { visibility : visible; }
|
& > .dropdown { visibility : visible; }
|
||||||
}
|
}
|
||||||
@@ -160,10 +127,10 @@
|
|||||||
cursor : pointer;
|
cursor : pointer;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
i {
|
i {
|
||||||
min-width : 25px;
|
|
||||||
height : 1.2em;
|
height : 1.2em;
|
||||||
margin-right : 8px;
|
margin-right : 8px;
|
||||||
font-size : 1.2em;
|
font-size : 1.2em;
|
||||||
|
min-width: 25px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
& ~ i {
|
& ~ i {
|
||||||
margin-right : 0;
|
margin-right : 0;
|
||||||
@@ -197,7 +164,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.name { margin-right : auto; }
|
.name { margin-right : auto; }
|
||||||
.disabled { text-decoration : line-through; }
|
|
||||||
.beta {
|
.beta {
|
||||||
align-self : center;
|
align-self : center;
|
||||||
padding : 4px 6px;
|
padding : 4px 6px;
|
||||||
@@ -224,18 +190,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@container editor (width < 553px) {
|
|
||||||
.snippetBar {
|
|
||||||
.editors {
|
|
||||||
flex : 1;
|
|
||||||
justify-content : space-between;
|
|
||||||
border-bottom : 1px solid;
|
|
||||||
}
|
|
||||||
.snippets {
|
|
||||||
flex : 1;
|
|
||||||
justify-content : space-evenly;
|
|
||||||
}
|
|
||||||
.editors > div.history > .dropdown { right : unset; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
149
client/homebrew/editor/stringArrayEditor/stringArrayEditor.jsx
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
|
|
||||||
|
const StringArrayEditor = createClass({
|
||||||
|
displayName : 'StringArrayEditor',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
label : '',
|
||||||
|
values : [],
|
||||||
|
valuePatterns : null,
|
||||||
|
validators : [],
|
||||||
|
placeholder : '',
|
||||||
|
notes : [],
|
||||||
|
unique : false,
|
||||||
|
cannotEdit : [],
|
||||||
|
onChange : ()=>{}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState : function() {
|
||||||
|
return {
|
||||||
|
valueContext : !!this.props.values ? this.props.values.map((value)=>({
|
||||||
|
value,
|
||||||
|
editing : false
|
||||||
|
})) : [],
|
||||||
|
temporaryValue : '',
|
||||||
|
updateValue : ''
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidUpdate : function(prevProps) {
|
||||||
|
if(!_.eq(this.props.values, prevProps.values)) {
|
||||||
|
this.setState({
|
||||||
|
valueContext : this.props.values ? this.props.values.map((newValue)=>({
|
||||||
|
value : newValue,
|
||||||
|
editing : this.state.valueContext.find(({ value })=>value === newValue)?.editing || false
|
||||||
|
})) : []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange : function(value) {
|
||||||
|
this.props.onChange({
|
||||||
|
target : {
|
||||||
|
value
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
addValue : function(value){
|
||||||
|
this.handleChange(_.uniq([...this.props.values, value]));
|
||||||
|
this.setState({
|
||||||
|
temporaryValue : ''
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
removeValue : function(index){
|
||||||
|
this.handleChange(this.props.values.filter((_, i)=>i !== index));
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValue : function(value, index){
|
||||||
|
const valueContext = this.state.valueContext;
|
||||||
|
valueContext[index].value = value;
|
||||||
|
valueContext[index].editing = false;
|
||||||
|
this.handleChange(valueContext.map((context)=>context.value));
|
||||||
|
this.setState({ valueContext, updateValue: '' });
|
||||||
|
},
|
||||||
|
|
||||||
|
editValue : function(index){
|
||||||
|
if(!!this.props.cannotEdit && this.props.cannotEdit.includes(this.props.values[index])) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const valueContext = this.state.valueContext.map((context, i)=>{
|
||||||
|
context.editing = index === i;
|
||||||
|
return context;
|
||||||
|
});
|
||||||
|
this.setState({ valueContext, updateValue: this.props.values[index] });
|
||||||
|
},
|
||||||
|
|
||||||
|
valueIsValid : function(value, index) {
|
||||||
|
const values = _.clone(this.props.values);
|
||||||
|
if(index !== undefined) {
|
||||||
|
values.splice(index, 1);
|
||||||
|
}
|
||||||
|
const matchesPatterns = !this.props.valuePatterns || this.props.valuePatterns.some((pattern)=>!!(value || '').match(pattern));
|
||||||
|
const uniqueIfSet = !this.props.unique || !values.includes(value);
|
||||||
|
const passesValidators = !this.props.validators || this.props.validators.every((validator)=>validator(value));
|
||||||
|
return matchesPatterns && uniqueIfSet && passesValidators;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleValueInputKeyDown : function(event, index) {
|
||||||
|
if(event.key === 'Enter') {
|
||||||
|
if(this.valueIsValid(event.target.value, index)) {
|
||||||
|
if(index !== undefined) {
|
||||||
|
this.updateValue(event.target.value, index);
|
||||||
|
} else {
|
||||||
|
this.addValue(event.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if(event.key === 'Escape') {
|
||||||
|
this.closeEditInput(index);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
closeEditInput : function(index) {
|
||||||
|
const valueContext = this.state.valueContext;
|
||||||
|
valueContext[index].editing = false;
|
||||||
|
this.setState({ valueContext, updateValue: '' });
|
||||||
|
},
|
||||||
|
|
||||||
|
render : function() {
|
||||||
|
const valueElements = Object.values(this.state.valueContext).map((context, i)=>context.editing
|
||||||
|
? <React.Fragment key={i}>
|
||||||
|
<div className='input-group'>
|
||||||
|
<input type='text' className={`value ${this.valueIsValid(this.state.updateValue, i) ? '' : 'invalid'}`} autoFocus placeholder={this.props.placeholder}
|
||||||
|
value={this.state.updateValue}
|
||||||
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e, i)}
|
||||||
|
onChange={(e)=>this.setState({ updateValue: e.target.value })}/>
|
||||||
|
{<div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.closeEditInput(i); }}><i className='fa fa-undo fa-fw'/></div>}
|
||||||
|
{this.valueIsValid(this.state.updateValue, i) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.updateValue(this.state.updateValue, i); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
|
</React.Fragment>
|
||||||
|
: <div className='badge' key={i} onClick={()=>this.editValue(i)}>{context.value}
|
||||||
|
{!!this.props.cannotEdit && this.props.cannotEdit.includes(context.value) ? null : <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.removeValue(i); }}><i className='fa fa-times fa-fw'/></div>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return <div className='field'>
|
||||||
|
<label>{this.props.label}</label>
|
||||||
|
<div style={{ flex: '1 0' }}>
|
||||||
|
<div className='list'>
|
||||||
|
{valueElements}
|
||||||
|
<div className='input-group'>
|
||||||
|
<input type='text' className={`value ${this.valueIsValid(this.state.temporaryValue) ? '' : 'invalid'}`} placeholder={this.props.placeholder}
|
||||||
|
value={this.state.temporaryValue}
|
||||||
|
onKeyDown={(e)=>this.handleValueInputKeyDown(e)}
|
||||||
|
onChange={(e)=>this.setState({ temporaryValue: e.target.value })}/>
|
||||||
|
{this.valueIsValid(this.state.temporaryValue) ? <div className='icon steel' onClick={(e)=>{ e.stopPropagation(); this.addValue(this.state.temporaryValue); }}><i className='fa fa-check fa-fw'/></div> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{this.props.notes ? this.props.notes.map((n, index)=><p key={index}><small>{n}</small></p>) : null}
|
||||||
|
</div>
|
||||||
|
</div>;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = StringArrayEditor;
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
require('./tagInput.less');
|
|
||||||
const React = require('react');
|
|
||||||
const { useState, useEffect } = React;
|
|
||||||
const _ = require('lodash');
|
|
||||||
|
|
||||||
const TagInput = ({ unique = true, values = [], ...props }) => {
|
|
||||||
const [tempInputText, setTempInputText] = useState('');
|
|
||||||
const [tagList, setTagList] = useState(values.map((value) => ({ value, editing: false })));
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
handleChange(tagList.map((context)=>context.value))
|
|
||||||
}, [tagList])
|
|
||||||
|
|
||||||
const handleChange = (value)=>{
|
|
||||||
props.onChange({
|
|
||||||
target : { value }
|
|
||||||
})
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleInputKeyDown = ({ evt, value, index, options = {} }) => {
|
|
||||||
if (_.includes(['Enter', ','], evt.key)) {
|
|
||||||
evt.preventDefault();
|
|
||||||
submitTag(evt.target.value, value, index);
|
|
||||||
if (options.clear) {
|
|
||||||
setTempInputText('');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitTag = (newValue, originalValue, index) => {
|
|
||||||
setTagList((prevContext) => {
|
|
||||||
// remove existing tag
|
|
||||||
if(newValue === null){
|
|
||||||
return [...prevContext].filter((context, i)=>i !== index);
|
|
||||||
}
|
|
||||||
// add new tag
|
|
||||||
if(originalValue === null){
|
|
||||||
return [...prevContext, { value: newValue, editing: false }]
|
|
||||||
}
|
|
||||||
// update existing tag
|
|
||||||
return prevContext.map((context, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
return { ...context, value: newValue, editing: false };
|
|
||||||
}
|
|
||||||
return context;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const editTag = (index) => {
|
|
||||||
setTagList((prevContext) => {
|
|
||||||
return prevContext.map((context, i) => {
|
|
||||||
if (i === index) {
|
|
||||||
return { ...context, editing: true };
|
|
||||||
}
|
|
||||||
return { ...context, editing: false };
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderReadTag = (context, index) => {
|
|
||||||
return (
|
|
||||||
<li key={index}
|
|
||||||
data-value={context.value}
|
|
||||||
className='tag'
|
|
||||||
onClick={() => editTag(index)}>
|
|
||||||
{context.value}
|
|
||||||
<button onClick={(evt)=>{evt.stopPropagation(); submitTag(null, context.value, index)}}><i className='fa fa-times fa-fw'/></button>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderWriteTag = (context, index) => {
|
|
||||||
return (
|
|
||||||
<input type='text'
|
|
||||||
key={index}
|
|
||||||
defaultValue={context.value}
|
|
||||||
onKeyDown={(evt) => handleInputKeyDown({evt, value: context.value, index: index})}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='field'>
|
|
||||||
<label>{props.label}</label>
|
|
||||||
<div className='value'>
|
|
||||||
<ul className='list'>
|
|
||||||
{tagList.map((context, index) => { return context.editing ? renderWriteTag(context, index) : renderReadTag(context, index); })}
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<input
|
|
||||||
type='text'
|
|
||||||
className='value'
|
|
||||||
placeholder={props.placeholder}
|
|
||||||
value={tempInputText}
|
|
||||||
onChange={(e) => setTempInputText(e.target.value)}
|
|
||||||
onKeyDown={(evt) => handleInputKeyDown({ evt, value: null, options: { clear: true } })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = TagInput;
|
|
||||||
@@ -10,7 +10,6 @@ const UserPage = require('./pages/userPage/userPage.jsx');
|
|||||||
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
const SharePage = require('./pages/sharePage/sharePage.jsx');
|
||||||
const NewPage = require('./pages/newPage/newPage.jsx');
|
const NewPage = require('./pages/newPage/newPage.jsx');
|
||||||
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
const ErrorPage = require('./pages/errorPage/errorPage.jsx');
|
||||||
const VaultPage = require('./pages/vaultPage/vaultPage.jsx');
|
|
||||||
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
const AccountPage = require('./pages/accountPage/accountPage.jsx');
|
||||||
|
|
||||||
const WithRoute = (props)=>{
|
const WithRoute = (props)=>{
|
||||||
@@ -67,15 +66,13 @@ const Homebrew = createClass({
|
|||||||
<Router location={this.props.url}>
|
<Router location={this.props.url}>
|
||||||
<div className='homebrew'>
|
<div className='homebrew'>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={this.props.brew} />} />
|
||||||
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} userThemes={this.props.userThemes}/>} />
|
<Route path='/new/:id' element={<WithRoute el={NewPage} brew={this.props.brew} />} />
|
||||||
<Route path='/new' element={<WithRoute el={NewPage} userThemes={this.props.userThemes}/> } />
|
<Route path='/new' element={<WithRoute el={NewPage}/>} />
|
||||||
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
<Route path='/user/:username' element={<WithRoute el={UserPage} brews={this.props.brews} />} />
|
||||||
<Route path='/vault' element={<WithRoute el={VaultPage}/>}/>
|
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/changelog' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} />} />
|
||||||
<Route path='/faq' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
|
||||||
<Route path='/migrate' element={<WithRoute el={SharePage} brew={this.props.brew} disableMeta={true} />} />
|
|
||||||
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
<Route path='/account' element={<WithRoute el={AccountPage} brew={this.props.brew} accountDetails={this.props.brew.accountDetails} />} />
|
||||||
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
<Route path='/legacy' element={<WithRoute el={HomePage} brew={this.props.brew} />} />
|
||||||
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
<Route path='/error' element={<WithRoute el={ErrorPage} brew={this.props.brew} />} />
|
||||||
|
|||||||
34
client/homebrew/navbar/editTitle.navitem.jsx
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const cx = require('classnames');
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
const MAX_TITLE_LENGTH = 50;
|
||||||
|
|
||||||
|
|
||||||
|
const EditTitle = createClass({
|
||||||
|
displayName : 'EditTitleNavItem',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
title : '',
|
||||||
|
onChange : function(){}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
handleChange : function(e){
|
||||||
|
if(e.target.value.length > MAX_TITLE_LENGTH) return;
|
||||||
|
this.props.onChange(e.target.value);
|
||||||
|
},
|
||||||
|
render : function(){
|
||||||
|
return <Nav.item className='editTitle'>
|
||||||
|
<input placeholder='Brew Title' type='text' value={this.props.title} onChange={this.handleChange} />
|
||||||
|
|
||||||
|
<div className={cx('charCount', { 'max': this.props.title.length >= MAX_TITLE_LENGTH })}>
|
||||||
|
{this.props.title.length}/{MAX_TITLE_LENGTH}
|
||||||
|
</div>
|
||||||
|
</Nav.item>;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = EditTitle;
|
||||||
@@ -104,18 +104,6 @@ const ErrorNavItem = createClass({
|
|||||||
</Nav.item>;
|
</Nav.item>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(HBErrorCode === '09') {
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
|
||||||
Oops!
|
|
||||||
<div className='errorContainer' onClick={clearError}>
|
|
||||||
Looks like there was a problem retreiving
|
|
||||||
the theme, or a theme that it inherits,
|
|
||||||
for this brew. Verify that brew <a className='lowercase' target='_blank' rel='noopener noreferrer' href={`/share/${response.body.brewId}`}>
|
|
||||||
{response.body.brewId}</a> still exists!
|
|
||||||
</div>
|
|
||||||
</Nav.item>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>
|
||||||
Oops!
|
Oops!
|
||||||
<div className='errorContainer'>
|
<div className='errorContainer'>
|
||||||
|
|||||||
@@ -21,9 +21,6 @@
|
|||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
text-transform : uppercase;
|
text-transform : uppercase;
|
||||||
.lowercase {
|
|
||||||
text-transform : none;
|
|
||||||
}
|
|
||||||
a{
|
a{
|
||||||
color : @teal;
|
color : @teal;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
@import 'naturalcrit/styles/colors.less';
|
@import 'naturalcrit/styles/colors.less';
|
||||||
|
|
||||||
@navbarHeight : 28px;
|
@navbarHeight : 28px;
|
||||||
@viewerToolsHeight : 32px;
|
|
||||||
|
|
||||||
@keyframes pinkColoring {
|
@keyframes pinkColoring {
|
||||||
0% { color : pink; }
|
0% { color : pink; }
|
||||||
@@ -25,20 +24,16 @@
|
|||||||
|
|
||||||
.homebrew nav {
|
.homebrew nav {
|
||||||
background-color : #333333;
|
background-color : #333333;
|
||||||
|
.navContent {
|
||||||
position : relative;
|
position : relative;
|
||||||
z-index : 2;
|
z-index : 2;
|
||||||
display : flex;
|
display : flex;
|
||||||
justify-content : space-between;
|
justify-content : space-between;
|
||||||
|
}
|
||||||
.navSection {
|
.navSection {
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
align-items : center;
|
||||||
&:last-child .navItem { border-left : 1px solid #666666; }
|
&:last-child .navItem { border-left : 1px solid #666666; }
|
||||||
|
|
||||||
&:has(.brewTitle) {
|
|
||||||
flex-grow : 1;
|
|
||||||
min-width : 300px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// "NaturalCrit" logo
|
// "NaturalCrit" logo
|
||||||
.navLogo {
|
.navLogo {
|
||||||
@@ -73,10 +68,6 @@
|
|||||||
.navItem {
|
.navItem {
|
||||||
#backgroundColorsHover;
|
#backgroundColorsHover;
|
||||||
.animate(background-color);
|
.animate(background-color);
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
justify-content : center;
|
|
||||||
height : 100%;
|
|
||||||
padding : 8px 12px;
|
padding : 8px 12px;
|
||||||
font-size : 10px;
|
font-size : 10px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
@@ -102,20 +93,39 @@
|
|||||||
animation-duration : 2s;
|
animation-duration : 2s;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
&.brewTitle {
|
&.editTitle { // this is not needed at all currently - you used to be able to edit the title via the navbar.
|
||||||
display : block;
|
padding : 2px 12px;
|
||||||
width : 100%;
|
input {
|
||||||
overflow : hidden;
|
width : 250px;
|
||||||
|
padding : 2px;
|
||||||
|
margin : 0;
|
||||||
|
font-family : 'Open Sans', sans-serif;
|
||||||
|
font-size : 12px;
|
||||||
|
font-weight : 800;
|
||||||
|
color : white;
|
||||||
|
text-align : center;
|
||||||
|
background-color : transparent;
|
||||||
|
border : 1px solid @blue;
|
||||||
|
outline : none;
|
||||||
|
}
|
||||||
|
.charCount {
|
||||||
|
display : inline-block;
|
||||||
|
margin-left : 8px;
|
||||||
|
color : #666666;
|
||||||
|
text-align : right;
|
||||||
|
vertical-align : bottom;
|
||||||
|
&.max { color : @red; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&.brewTitle {
|
||||||
|
flex-grow : 1;
|
||||||
font-size : 12px;
|
font-size : 12px;
|
||||||
font-weight : 800;
|
font-weight : 800;
|
||||||
color : white;
|
color : white;
|
||||||
text-align : center;
|
text-align : center;
|
||||||
text-overflow : ellipsis;
|
|
||||||
text-transform : initial;
|
text-transform : initial;
|
||||||
white-space : nowrap;
|
|
||||||
background-color : transparent;
|
background-color : transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
// "The Homebrewery" logo
|
// "The Homebrewery" logo
|
||||||
&.homebrewLogo {
|
&.homebrewLogo {
|
||||||
.animate(color);
|
.animate(color);
|
||||||
@@ -229,25 +239,23 @@
|
|||||||
}
|
}
|
||||||
.navDropdownContainer {
|
.navDropdownContainer {
|
||||||
position : relative;
|
position : relative;
|
||||||
height : 100%;
|
|
||||||
|
|
||||||
.navDropdown {
|
.navDropdown {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
//top: 28px;
|
top: 28px;
|
||||||
right: 0px;
|
right: 0px;
|
||||||
z-index: 10000;
|
z-index: 10000;
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
align-items : flex-end;
|
|
||||||
width: max-content;
|
width: max-content;
|
||||||
min-width:100%;
|
min-width:100%;
|
||||||
max-height: calc(100vh - 28px);
|
max-height: calc(100vh - 28px);
|
||||||
overflow: hidden auto;
|
overflow: hidden auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-end;
|
||||||
.navItem {
|
.navItem {
|
||||||
position : relative;
|
position : relative;
|
||||||
display : flex;
|
display : flex;
|
||||||
align-items : center;
|
|
||||||
justify-content : space-between;
|
justify-content : space-between;
|
||||||
|
align-items : center;
|
||||||
width : 100%;
|
width : 100%;
|
||||||
border : 1px solid #888888;
|
border : 1px solid #888888;
|
||||||
border-bottom : 0;
|
border-bottom : 0;
|
||||||
@@ -269,10 +277,10 @@
|
|||||||
overflow : hidden auto;
|
overflow : hidden auto;
|
||||||
color : white;
|
color : white;
|
||||||
text-decoration : none;
|
text-decoration : none;
|
||||||
scrollbar-color : #666666 #333333;
|
|
||||||
scrollbar-width : thin;
|
|
||||||
background-color : #333333;
|
background-color : #333333;
|
||||||
border-top : 1px solid #888888;
|
border-top : 1px solid #888888;
|
||||||
|
scrollbar-color : #666666 #333333;
|
||||||
|
scrollbar-width : thin;
|
||||||
.clear {
|
.clear {
|
||||||
position : absolute;
|
position : absolute;
|
||||||
top : 50%;
|
top : 50%;
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const RecentItems = createClass({
|
|||||||
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
|
//== Add current brew to appropriate recent items list (depending on storageKey) ==//
|
||||||
if(this.props.storageKey == 'edit'){
|
if(this.props.storageKey == 'edit'){
|
||||||
let editId = this.props.brew.editId;
|
let editId = this.props.brew.editId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed){
|
if(this.props.brew.googleId){
|
||||||
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
||||||
}
|
}
|
||||||
edited = _.filter(edited, (brew)=>{
|
edited = _.filter(edited, (brew)=>{
|
||||||
@@ -51,7 +51,7 @@ const RecentItems = createClass({
|
|||||||
}
|
}
|
||||||
if(this.props.storageKey == 'view'){
|
if(this.props.storageKey == 'view'){
|
||||||
let shareId = this.props.brew.shareId;
|
let shareId = this.props.brew.shareId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed){
|
if(this.props.brew.googleId){
|
||||||
shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`;
|
shareId = `${this.props.brew.googleId}${this.props.brew.shareId}`;
|
||||||
}
|
}
|
||||||
viewed = _.filter(viewed, (brew)=>{
|
viewed = _.filter(viewed, (brew)=>{
|
||||||
@@ -83,7 +83,7 @@ const RecentItems = createClass({
|
|||||||
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
let edited = JSON.parse(localStorage.getItem(EDIT_KEY) || '[]');
|
||||||
if(this.props.storageKey == 'edit') {
|
if(this.props.storageKey == 'edit') {
|
||||||
let prevEditId = prevProps.brew.editId;
|
let prevEditId = prevProps.brew.editId;
|
||||||
if(prevProps.brew.googleId && !this.props.brew.stubbed){
|
if(prevProps.brew.googleId){
|
||||||
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
|
prevEditId = `${prevProps.brew.googleId}${prevProps.brew.editId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +91,7 @@ const RecentItems = createClass({
|
|||||||
return brew.id !== prevEditId;
|
return brew.id !== prevEditId;
|
||||||
});
|
});
|
||||||
let editId = this.props.brew.editId;
|
let editId = this.props.brew.editId;
|
||||||
if(this.props.brew.googleId && !this.props.brew.stubbed){
|
if(this.props.brew.googleId){
|
||||||
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
editId = `${this.props.brew.googleId}${this.props.brew.editId}`;
|
||||||
}
|
}
|
||||||
edited.unshift({
|
edited.unshift({
|
||||||
|
|||||||
44
client/homebrew/navbar/reddit.navitem.jsx
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
const React = require('react');
|
||||||
|
const createClass = require('create-react-class');
|
||||||
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
|
|
||||||
|
const MAIN_URL = 'https://www.reddit.com/r/UnearthedArcana/submit?selftext=true';
|
||||||
|
|
||||||
|
|
||||||
|
const RedditShare = createClass({
|
||||||
|
displayName : 'RedditShareNavItem',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
|
brew : {
|
||||||
|
title : '',
|
||||||
|
sharedId : '',
|
||||||
|
text : ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
getText : function(){
|
||||||
|
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
handleClick : function(){
|
||||||
|
const url = [
|
||||||
|
MAIN_URL,
|
||||||
|
`title=${encodeURIComponent(this.props.brew.title ? this.props.brew.title : 'Check out my brew!')}`,
|
||||||
|
`text=${encodeURIComponent(this.props.brew.text)}`
|
||||||
|
].join('&');
|
||||||
|
|
||||||
|
window.open(url, '_blank');
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
render : function(){
|
||||||
|
return <Nav.item icon='fa-reddit-alien' color='red' onClick={this.handleClick}>
|
||||||
|
share on reddit
|
||||||
|
</Nav.item>;
|
||||||
|
},
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
module.exports = RedditShare;
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
const React = require('react');
|
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
|
|
||||||
module.exports = function (props) {
|
|
||||||
return (
|
|
||||||
<Nav.item
|
|
||||||
color='purple'
|
|
||||||
icon='fas fa-dungeon'
|
|
||||||
href='/vault'
|
|
||||||
newTab={false}
|
|
||||||
rel='noopener noreferrer'
|
|
||||||
>
|
|
||||||
Vault
|
|
||||||
</Nav.item>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -2,7 +2,7 @@ require('./brewItem.less');
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
const moment = require('moment');
|
const moment = require('moment');
|
||||||
import request from '../../../../utils/request-middleware.js';
|
const request = require('../../../../utils/request-middleware.js');
|
||||||
|
|
||||||
const googleDriveIcon = require('../../../../googleDrive.svg');
|
const googleDriveIcon = require('../../../../googleDrive.svg');
|
||||||
const homebreweryIcon = require('../../../../thumbnail.png');
|
const homebreweryIcon = require('../../../../thumbnail.png');
|
||||||
@@ -19,8 +19,7 @@ const BrewItem = createClass({
|
|||||||
stubbed : true
|
stubbed : true
|
||||||
},
|
},
|
||||||
updateListFilter : ()=>{},
|
updateListFilter : ()=>{},
|
||||||
reportError : ()=>{},
|
reportError : ()=>{}
|
||||||
renderStorage : true
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -96,7 +95,6 @@ const BrewItem = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
renderStorageIcon : function(){
|
renderStorageIcon : function(){
|
||||||
if(!this.props.renderStorage) return;
|
|
||||||
if(this.props.brew.googleId) {
|
if(this.props.brew.googleId) {
|
||||||
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
return <span title={this.props.brew.webViewLink ? 'Your Google Drive Storage': 'Another User\'s Google Drive Storage'}>
|
||||||
<a href={this.props.brew.webViewLink} target='_blank'>
|
<a href={this.props.brew.webViewLink} target='_blank'>
|
||||||
@@ -144,14 +142,10 @@ const BrewItem = createClass({
|
|||||||
}
|
}
|
||||||
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
<span title={`Authors:\n${brew.authors?.join('\n')}`}>
|
||||||
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
<i className='fas fa-user'/> {brew.authors?.map((author, index)=>(
|
||||||
<React.Fragment key={index}>
|
<>
|
||||||
{author === 'hidden'
|
<a key={index} href={`/user/${author}`}>{author}</a>
|
||||||
? <span title="Username contained an email address; hidden to protect user's privacy">{author}</span>
|
|
||||||
: <a href={`/user/${author}`}>{author}</a>
|
|
||||||
}
|
|
||||||
{index < brew.authors.length - 1 && ', '}
|
{index < brew.authors.length - 1 && ', '}
|
||||||
</React.Fragment>
|
</>))}
|
||||||
))}
|
|
||||||
</span>
|
</span>
|
||||||
<br />
|
<br />
|
||||||
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
<span title={`Last viewed: ${moment(brew.lastViewed).local().format(dateFormatString)}`}>
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
require('./editPage.less');
|
require('./editPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const _ = require('lodash');
|
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
import request from '../../utils/request-middleware.js';
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
@@ -23,16 +22,14 @@ const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
|||||||
|
|
||||||
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
const LockNotification = require('./lockNotification/lockNotification.jsx');
|
||||||
|
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
|
|
||||||
|
|
||||||
const googleDriveIcon = require('../../googleDrive.svg');
|
const googleDriveIcon = require('../../googleDrive.svg');
|
||||||
|
|
||||||
const SAVE_TIMEOUT = 10000;
|
const SAVE_TIMEOUT = 3000;
|
||||||
|
|
||||||
const EditPage = createClass({
|
const EditPage = createClass({
|
||||||
displayName : 'EditPage',
|
displayName : 'EditPage',
|
||||||
@@ -57,11 +54,8 @@ const EditPage = createClass({
|
|||||||
autoSave : true,
|
autoSave : true,
|
||||||
autoSaveWarning : false,
|
autoSaveWarning : false,
|
||||||
unsavedTime : new Date(),
|
unsavedTime : new Date(),
|
||||||
currentEditorViewPageNum : 1,
|
currentEditorPage : 0,
|
||||||
currentEditorCursorPageNum : 1,
|
displayLockMessage : this.props.brew.lock || false
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
displayLockMessage : this.props.brew.lock || false,
|
|
||||||
themeBundle : {}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -93,8 +87,6 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
|
||||||
|
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
@@ -118,18 +110,6 @@ const EditPage = createClass({
|
|||||||
this.editor.current.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
@@ -139,6 +119,7 @@ const EditPage = createClass({
|
|||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
isPending : true,
|
isPending : true,
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
|
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -149,10 +130,7 @@ const EditPage = createClass({
|
|||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata, field=undefined){
|
handleMetaChange : function(metadata){
|
||||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
|
||||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : {
|
brew : {
|
||||||
...prevState.brew,
|
...prevState.brew,
|
||||||
@@ -160,22 +138,13 @@ const EditPage = createClass({
|
|||||||
},
|
},
|
||||||
isPending : true,
|
isPending : true,
|
||||||
}), ()=>{if(this.state.autoSave) this.trySave();});
|
}), ()=>{if(this.state.autoSave) this.trySave();});
|
||||||
|
|
||||||
},
|
},
|
||||||
|
|
||||||
hasChanges : function(){
|
hasChanges : function(){
|
||||||
return !_.isEqual(this.state.brew, this.savedBrew);
|
return !_.isEqual(this.state.brew, this.savedBrew);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateBrew : function(newData){
|
|
||||||
this.setState((prevState)=>({
|
|
||||||
brew : {
|
|
||||||
...prevState.brew,
|
|
||||||
style : newData.style,
|
|
||||||
text : newData.text
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
},
|
|
||||||
|
|
||||||
trySave : function(immediate=false){
|
trySave : function(immediate=false){
|
||||||
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
|
||||||
if(this.hasChanges()){
|
if(this.hasChanges()){
|
||||||
@@ -228,9 +197,6 @@ const EditPage = createClass({
|
|||||||
htmlErrors : Markdown.validate(prevState.brew.text)
|
htmlErrors : Markdown.validate(prevState.brew.text)
|
||||||
}));
|
}));
|
||||||
|
|
||||||
await updateHistory(this.state.brew).catch(console.error);
|
|
||||||
await versionHistoryGarbageCollection().catch(console.error);
|
|
||||||
|
|
||||||
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
|
||||||
|
|
||||||
const brew = this.state.brew;
|
const brew = this.state.brew;
|
||||||
@@ -429,8 +395,8 @@ const EditPage = createClass({
|
|||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
|
|
||||||
|
<div className='content'>
|
||||||
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} />}
|
||||||
<div className="content">
|
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={this.editor}
|
||||||
@@ -440,27 +406,15 @@ const EditPage = createClass({
|
|||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
reportError={this.errorReported}
|
reportError={this.errorReported}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
|
||||||
updateBrew={this.updateBrew}
|
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={this.state.brew.text}
|
||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
require('./errorPage.less');
|
require('./errorPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
const UIPage = require('../basePages/uiPage/uiPage.jsx');
|
||||||
import Markdown from '../../../../shared/naturalcrit/markdown.js';
|
const Markdown = require('../../../../shared/naturalcrit/markdown.js');
|
||||||
const ErrorIndex = require('./errors/errorIndex.js');
|
const ErrorIndex = require('./errors/errorIndex.js');
|
||||||
|
|
||||||
const ErrorPage = ({ brew })=>{
|
const ErrorPage = ({ brew })=>{
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ const dedent = require('dedent-tabs').default;
|
|||||||
|
|
||||||
const loginUrl = 'https://www.naturalcrit.com/login';
|
const loginUrl = 'https://www.naturalcrit.com/login';
|
||||||
|
|
||||||
//001-050 : Brew errors
|
|
||||||
//050-100 : Other pages errors
|
|
||||||
|
|
||||||
const errorIndex = (props)=>{
|
const errorIndex = (props)=>{
|
||||||
return {
|
return {
|
||||||
// Default catch all
|
// Default catch all
|
||||||
@@ -139,29 +136,8 @@ const errorIndex = (props)=>{
|
|||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
**Brew ID:** ${props.brew.brewId}`,
|
||||||
|
|
||||||
// Theme load error
|
|
||||||
'09' : dedent`
|
|
||||||
## No Homebrewery theme document could be found.
|
|
||||||
|
|
||||||
The server could not locate the Homebrewery document. It was likely deleted by
|
|
||||||
its owner.
|
|
||||||
|
|
||||||
:
|
|
||||||
|
|
||||||
**Requested access:** ${props.brew.accessType}
|
|
||||||
|
|
||||||
**Brew ID:** ${props.brew.brewId}`,
|
|
||||||
|
|
||||||
//account page when account is not defined
|
|
||||||
'50' : dedent`
|
|
||||||
## You are not signed in
|
|
||||||
|
|
||||||
You are trying to access the account page, but are not signed in to an account.
|
|
||||||
|
|
||||||
Please login or signup at our [login page](https://www.naturalcrit.com/login?redirect=https://homebrewery.naturalcrit.com/account).`,
|
|
||||||
|
|
||||||
// Brew locked by Administrators error
|
// Brew locked by Administrators error
|
||||||
'51' : dedent`
|
'100' : dedent`
|
||||||
## This brew has been locked.
|
## This brew has been locked.
|
||||||
|
|
||||||
Only an author may request that this lock is removed.
|
Only an author may request that this lock is removed.
|
||||||
@@ -171,16 +147,6 @@ const errorIndex = (props)=>{
|
|||||||
**Brew ID:** ${props.brew.brewId}
|
**Brew ID:** ${props.brew.brewId}
|
||||||
|
|
||||||
**Brew Title:** ${props.brew.brewTitle}`,
|
**Brew Title:** ${props.brew.brewTitle}`,
|
||||||
|
|
||||||
// ####### Admin page error #######
|
|
||||||
'52': dedent`
|
|
||||||
## Access Denied
|
|
||||||
You need to provide correct administrator credentials to access this page.`,
|
|
||||||
|
|
||||||
'90' : dedent` An unexpected error occurred while looking for these brews.
|
|
||||||
Try again in a few minutes.`,
|
|
||||||
|
|
||||||
'91' : dedent` An unexpected error occurred while trying to get the total of brews.`,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
require('./homePage.less');
|
require('./homePage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
|
const _ = require('lodash');
|
||||||
const cx = require('classnames');
|
const cx = require('classnames');
|
||||||
import request from '../../utils/request-middleware.js';
|
const request = require('../../utils/request-middleware.js');
|
||||||
const { Meta } = require('vitreum/headtags');
|
const { Meta } = require('vitreum/headtags');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrewItem = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
const AccountNavItem = require('../../navbar/account.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const { fetchThemeBundle } = require('../../../../shared/helpers.js');
|
|
||||||
|
|
||||||
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
const SplitPane = require('naturalcrit/splitPane/splitPane.jsx');
|
||||||
const Editor = require('../../editor/editor.jsx');
|
const Editor = require('../../editor/editor.jsx');
|
||||||
@@ -34,19 +34,12 @@ const HomePage = createClass({
|
|||||||
brew : this.props.brew,
|
brew : this.props.brew,
|
||||||
welcomeText : this.props.brew.text,
|
welcomeText : this.props.brew.text,
|
||||||
error : undefined,
|
error : undefined,
|
||||||
currentEditorViewPageNum : 1,
|
currentEditorPage : 0
|
||||||
currentEditorCursorPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
themeBundle : {}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
editor : React.createRef(null),
|
editor : React.createRef(null),
|
||||||
|
|
||||||
componentDidMount : function() {
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSave : function(){
|
handleSave : function(){
|
||||||
request.post('/api')
|
request.post('/api')
|
||||||
.send(this.state.brew)
|
.send(this.state.brew)
|
||||||
@@ -62,22 +55,10 @@ const HomePage = createClass({
|
|||||||
handleSplitMove : function(){
|
handleSplitMove : function(){
|
||||||
this.editor.current.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
|
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
renderNavbar : function(){
|
renderNavbar : function(){
|
||||||
@@ -89,7 +70,6 @@ const HomePage = createClass({
|
|||||||
}
|
}
|
||||||
<NewBrewItem />
|
<NewBrewItem />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<VaultNavItem />
|
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<AccountNavItem />
|
<AccountNavItem />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
@@ -100,7 +80,8 @@ const HomePage = createClass({
|
|||||||
return <div className='homePage sitePage'>
|
return <div className='homePage sitePage'>
|
||||||
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
<Meta name='google-site-verification' content='NwnAQSSJZzAT7N-p5MY6ydQ7Njm67dtbu73ZSyE5Fy4' />
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
<div className="content">
|
|
||||||
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={this.editor}
|
||||||
@@ -108,25 +89,16 @@ const HomePage = createClass({
|
|||||||
onTextChange={this.handleTextChange}
|
onTextChange={this.handleTextChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
showEditButtons={false}
|
showEditButtons={false}
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={this.state.brew.text}
|
||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
themeBundle={this.state.themeBundle}
|
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
<div className={cx('floatingSaveButton', { show: this.state.welcomeText != this.state.brew.text })} onClick={this.handleSave}>
|
||||||
Save current <i className='fas fa-save' />
|
Save current <i className='fas fa-save' />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -91,6 +91,13 @@ If you are looking for more 5e Homebrew resources check out [r/UnearthedArcana](
|
|||||||
|
|
||||||
\page
|
\page
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Markdown+
|
## Markdown+
|
||||||
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
The Homebrewery aims to make homebrewing as simple as possible, providing a live editor with Markdown syntax that is more human-readable and faster to write with than raw HTML.
|
||||||
|
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
require('./newPage.less');
|
require('./newPage.less');
|
||||||
const React = require('react');
|
const React = require('react');
|
||||||
const createClass = require('create-react-class');
|
const createClass = require('create-react-class');
|
||||||
import request from '../../utils/request-middleware.js';
|
const request = require('../../utils/request-middleware.js');
|
||||||
|
|
||||||
import Markdown from 'naturalcrit/markdown.js';
|
const Markdown = require('naturalcrit/markdown.js');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
const PrintNavItem = require('../../navbar/print.navitem.jsx');
|
||||||
@@ -19,7 +19,7 @@ const Editor = require('../../editor/editor.jsx');
|
|||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const BREWKEY = 'homebrewery-new';
|
const BREWKEY = 'homebrewery-new';
|
||||||
const STYLEKEY = 'homebrewery-new-style';
|
const STYLEKEY = 'homebrewery-new-style';
|
||||||
@@ -44,10 +44,7 @@ const NewPage = createClass({
|
|||||||
saveGoogle : (global.account && global.account.googleId ? true : false),
|
saveGoogle : (global.account && global.account.googleId ? true : false),
|
||||||
error : null,
|
error : null,
|
||||||
htmlErrors : Markdown.validate(brew.text),
|
htmlErrors : Markdown.validate(brew.text),
|
||||||
currentEditorViewPageNum : 1,
|
currentEditorPage : 0
|
||||||
currentEditorCursorPageNum : 1,
|
|
||||||
currentBrewRendererPageNum : 1,
|
|
||||||
themeBundle : {}
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -80,15 +77,10 @@ const NewPage = createClass({
|
|||||||
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
saveGoogle : (saveStorage == 'GOOGLE-DRIVE' && this.state.saveGoogle)
|
||||||
});
|
});
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
|
||||||
|
|
||||||
localStorage.setItem(BREWKEY, brew.text);
|
localStorage.setItem(BREWKEY, brew.text);
|
||||||
if(brew.style)
|
if(brew.style)
|
||||||
localStorage.setItem(STYLEKEY, brew.style);
|
localStorage.setItem(STYLEKEY, brew.style);
|
||||||
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
localStorage.setItem(METAKEY, JSON.stringify({ 'renderer': brew.renderer, 'theme': brew.theme, 'lang': brew.lang }));
|
||||||
if(window.location.pathname != '/new') {
|
|
||||||
window.history.replaceState({}, window.location.title, '/new/');
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
@@ -110,18 +102,6 @@ const NewPage = createClass({
|
|||||||
this.editor.current.update();
|
this.editor.current.update();
|
||||||
},
|
},
|
||||||
|
|
||||||
handleEditorViewPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentEditorViewPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleEditorCursorPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentEditorCursorPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleTextChange : function(text){
|
handleTextChange : function(text){
|
||||||
//If there are errors, run the validator on every change to give quick feedback
|
//If there are errors, run the validator on every change to give quick feedback
|
||||||
let htmlErrors = this.state.htmlErrors;
|
let htmlErrors = this.state.htmlErrors;
|
||||||
@@ -130,6 +110,7 @@ const NewPage = createClass({
|
|||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, text: text },
|
brew : { ...prevState.brew, text: text },
|
||||||
htmlErrors : htmlErrors,
|
htmlErrors : htmlErrors,
|
||||||
|
currentEditorPage : this.editor.current.getCurrentPage() - 1 //Offset index since Marked starts pages at 0
|
||||||
}));
|
}));
|
||||||
localStorage.setItem(BREWKEY, text);
|
localStorage.setItem(BREWKEY, text);
|
||||||
},
|
},
|
||||||
@@ -141,10 +122,7 @@ const NewPage = createClass({
|
|||||||
localStorage.setItem(STYLEKEY, style);
|
localStorage.setItem(STYLEKEY, style);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleMetaChange : function(metadata, field=undefined){
|
handleMetaChange : function(metadata){
|
||||||
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
|
|
||||||
fetchThemeBundle(this, metadata.renderer, metadata.theme);
|
|
||||||
|
|
||||||
this.setState((prevState)=>({
|
this.setState((prevState)=>({
|
||||||
brew : { ...prevState.brew, ...metadata },
|
brew : { ...prevState.brew, ...metadata },
|
||||||
}), ()=>{
|
}), ()=>{
|
||||||
@@ -164,6 +142,8 @@ const NewPage = createClass({
|
|||||||
isSaving : true
|
isSaving : true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
console.log('saving new brew');
|
||||||
|
|
||||||
let brew = this.state.brew;
|
let brew = this.state.brew;
|
||||||
// Split out CSS to Style if CSS codefence exists
|
// Split out CSS to Style if CSS codefence exists
|
||||||
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
if(brew.text.startsWith('```css') && brew.text.indexOf('```\n\n') > 0) {
|
||||||
@@ -173,10 +153,12 @@ const NewPage = createClass({
|
|||||||
}
|
}
|
||||||
|
|
||||||
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
brew.pageCount=((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
|
||||||
|
|
||||||
const res = await request
|
const res = await request
|
||||||
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
.post(`/api${this.state.saveGoogle ? '?saveToGoogle=true' : ''}`)
|
||||||
.send(brew)
|
.send(brew)
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
|
console.log(err);
|
||||||
this.setState({ isSaving: false, error: err });
|
this.setState({ isSaving: false, error: err });
|
||||||
});
|
});
|
||||||
if(!res) return;
|
if(!res) return;
|
||||||
@@ -223,7 +205,7 @@ const NewPage = createClass({
|
|||||||
render : function(){
|
render : function(){
|
||||||
return <div className='newPage sitePage'>
|
return <div className='newPage sitePage'>
|
||||||
{this.renderNavbar()}
|
{this.renderNavbar()}
|
||||||
<div className="content">
|
<div className='content'>
|
||||||
<SplitPane onDragFinish={this.handleSplitMove}>
|
<SplitPane onDragFinish={this.handleSplitMove}>
|
||||||
<Editor
|
<Editor
|
||||||
ref={this.editor}
|
ref={this.editor}
|
||||||
@@ -232,26 +214,15 @@ const NewPage = createClass({
|
|||||||
onStyleChange={this.handleStyleChange}
|
onStyleChange={this.handleStyleChange}
|
||||||
onMetaChange={this.handleMetaChange}
|
onMetaChange={this.handleMetaChange}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
userThemes={this.props.userThemes}
|
|
||||||
snippetBundle={this.state.themeBundle.snippets}
|
|
||||||
onCursorPageChange={this.handleEditorCursorPageChange}
|
|
||||||
onViewPageChange={this.handleEditorViewPageChange}
|
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
/>
|
/>
|
||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.state.brew.text}
|
text={this.state.brew.text}
|
||||||
style={this.state.brew.style}
|
style={this.state.brew.style}
|
||||||
renderer={this.state.brew.renderer}
|
renderer={this.state.brew.renderer}
|
||||||
theme={this.state.brew.theme}
|
theme={this.state.brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
|
||||||
errors={this.state.htmlErrors}
|
errors={this.state.htmlErrors}
|
||||||
lang={this.state.brew.lang}
|
lang={this.state.brew.lang}
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
currentEditorPage={this.state.currentEditorPage}
|
||||||
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
|
|
||||||
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</SplitPane>
|
</SplitPane>
|
||||||
|
|||||||
@@ -12,38 +12,24 @@ const Account = require('../../navbar/account.navitem.jsx');
|
|||||||
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
|
||||||
|
|
||||||
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
|
||||||
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
|
const { printCurrentBrew } = require('../../../../shared/helpers.js');
|
||||||
|
|
||||||
const SharePage = createClass({
|
const SharePage = createClass({
|
||||||
displayName : 'SharePage',
|
displayName : 'SharePage',
|
||||||
getDefaultProps : function() {
|
getDefaultProps : function() {
|
||||||
return {
|
return {
|
||||||
brew : DEFAULT_BREW_LOAD,
|
brew : DEFAULT_BREW_LOAD
|
||||||
disableMeta : false
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState : function() {
|
|
||||||
return {
|
|
||||||
themeBundle : {},
|
|
||||||
currentBrewRendererPageNum : 1
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
componentDidMount : function() {
|
componentDidMount : function() {
|
||||||
document.addEventListener('keydown', this.handleControlKeys);
|
document.addEventListener('keydown', this.handleControlKeys);
|
||||||
|
|
||||||
fetchThemeBundle(this, this.props.brew.renderer, this.props.brew.theme);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
componentWillUnmount : function() {
|
componentWillUnmount : function() {
|
||||||
document.removeEventListener('keydown', this.handleControlKeys);
|
document.removeEventListener('keydown', this.handleControlKeys);
|
||||||
},
|
},
|
||||||
|
|
||||||
handleBrewRendererPageChange : function(pageNumber){
|
|
||||||
this.setState({ currentBrewRendererPageNum: pageNumber });
|
|
||||||
},
|
|
||||||
|
|
||||||
handleControlKeys : function(e){
|
handleControlKeys : function(e){
|
||||||
if(!(e.ctrlKey || e.metaKey)) return;
|
if(!(e.ctrlKey || e.metaKey)) return;
|
||||||
const P_KEY = 80;
|
const P_KEY = 80;
|
||||||
@@ -74,21 +60,13 @@ const SharePage = createClass({
|
|||||||
},
|
},
|
||||||
|
|
||||||
render : function(){
|
render : function(){
|
||||||
const titleStyle = this.props.disableMeta ? { cursor: 'default' } : {};
|
|
||||||
const titleEl = <Nav.item className='brewTitle' style={titleStyle}>{this.props.brew.title}</Nav.item>;
|
|
||||||
|
|
||||||
return <div className='sharePage sitePage'>
|
return <div className='sharePage sitePage'>
|
||||||
<Meta name='robots' content='noindex, nofollow' />
|
<Meta name='robots' content='noindex, nofollow' />
|
||||||
<Navbar>
|
<Navbar>
|
||||||
<Nav.section className='titleSection'>
|
<Nav.section className='titleSection'>
|
||||||
{
|
|
||||||
this.props.disableMeta ?
|
|
||||||
titleEl
|
|
||||||
:
|
|
||||||
<MetadataNav brew={this.props.brew}>
|
<MetadataNav brew={this.props.brew}>
|
||||||
{titleEl}
|
<Nav.item className='brewTitle'>{this.props.brew.title}</Nav.item>
|
||||||
</MetadataNav>
|
</MetadataNav>
|
||||||
}
|
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
|
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
@@ -119,12 +97,8 @@ const SharePage = createClass({
|
|||||||
<BrewRenderer
|
<BrewRenderer
|
||||||
text={this.props.brew.text}
|
text={this.props.brew.text}
|
||||||
style={this.props.brew.style}
|
style={this.props.brew.style}
|
||||||
lang={this.props.brew.lang}
|
|
||||||
renderer={this.props.brew.renderer}
|
renderer={this.props.brew.renderer}
|
||||||
theme={this.props.brew.theme}
|
theme={this.props.brew.theme}
|
||||||
themeBundle={this.state.themeBundle}
|
|
||||||
onPageChange={this.handleBrewRendererPageChange}
|
|
||||||
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
|
|
||||||
allowPrint={true}
|
allowPrint={true}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
.sharePage{
|
.sharePage{
|
||||||
nav .navSection.titleSection {
|
.navContent .navSection.titleSection {
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,60 +1,80 @@
|
|||||||
const React = require('react');
|
const React = require('react');
|
||||||
const { useState } = React;
|
const createClass = require('create-react-class');
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
|
|
||||||
const ListPage = require('../basePages/listPage/listPage.jsx');
|
const ListPage = require('../basePages/listPage/listPage.jsx');
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
const Nav = require('naturalcrit/nav/nav.jsx');
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
const Navbar = require('../../navbar/navbar.jsx');
|
||||||
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
const Account = require('../../navbar/account.navitem.jsx');
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
||||||
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
|
||||||
const VaultNavitem = require('../../navbar/vault.navitem.jsx');
|
|
||||||
|
|
||||||
const UserPage = (props)=>{
|
const UserPage = createClass({
|
||||||
props = {
|
displayName : 'UserPage',
|
||||||
|
getDefaultProps : function() {
|
||||||
|
return {
|
||||||
username : '',
|
username : '',
|
||||||
brews : [],
|
brews : [],
|
||||||
query : '',
|
query : '',
|
||||||
...props
|
error : null
|
||||||
};
|
};
|
||||||
|
},
|
||||||
|
getInitialState : function() {
|
||||||
|
const usernameWithS = this.props.username + (this.props.username.endsWith('s') ? `’` : `’s`);
|
||||||
|
|
||||||
const [error, setError] = useState(null);
|
const brews = _.groupBy(this.props.brews, (brew)=>{
|
||||||
|
return (brew.published ? 'published' : 'private');
|
||||||
const usernameWithS = props.username + (props.username.endsWith('s') ? `’` : `’s`);
|
});
|
||||||
const groupedBrews = _.groupBy(props.brews, (brew)=>brew.published ? 'published' : 'private');
|
|
||||||
|
|
||||||
const brewCollection = [
|
const brewCollection = [
|
||||||
{
|
{
|
||||||
title : `${usernameWithS} published brews`,
|
title : `${usernameWithS} published brews`,
|
||||||
class : 'published',
|
class : 'published',
|
||||||
brews : groupedBrews.published || []
|
brews : brews.published
|
||||||
},
|
}
|
||||||
...(props.username === global.account?.username ? [{
|
];
|
||||||
|
if(this.props.username == global.account?.username){
|
||||||
|
brewCollection.push(
|
||||||
|
{
|
||||||
title : `${usernameWithS} unpublished brews`,
|
title : `${usernameWithS} unpublished brews`,
|
||||||
class : 'unpublished',
|
class : 'unpublished',
|
||||||
brews : groupedBrews.private || []
|
brews : brews.private
|
||||||
}] : [])
|
}
|
||||||
];
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const navItems = (
|
return {
|
||||||
<Navbar>
|
brewCollection : brewCollection
|
||||||
|
};
|
||||||
|
},
|
||||||
|
errorReported : function(error) {
|
||||||
|
this.setState({
|
||||||
|
error
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
navItems : function() {
|
||||||
|
return <Navbar>
|
||||||
<Nav.section>
|
<Nav.section>
|
||||||
{error && (<ErrorNavItem error={error} parent={null}></ErrorNavItem>)}
|
{this.state.error ?
|
||||||
|
<ErrorNavItem error={this.state.error} parent={this}></ErrorNavItem> :
|
||||||
|
null
|
||||||
|
}
|
||||||
<NewBrew />
|
<NewBrew />
|
||||||
<HelpNavItem />
|
<HelpNavItem />
|
||||||
<VaultNavitem />
|
|
||||||
<RecentNavItem />
|
<RecentNavItem />
|
||||||
<Account />
|
<Account />
|
||||||
</Nav.section>
|
</Nav.section>
|
||||||
</Navbar>
|
</Navbar>;
|
||||||
);
|
},
|
||||||
|
|
||||||
return (
|
render : function(){
|
||||||
<ListPage brewCollection={brewCollection} navItems={navItems} query={props.query} reportError={(err)=>setError(err)} />
|
return <ListPage brewCollection={this.state.brewCollection} navItems={this.navItems()} query={this.props.query} reportError={this.errorReported}></ListPage>;
|
||||||
);
|
}
|
||||||
};
|
});
|
||||||
|
|
||||||
module.exports = UserPage;
|
module.exports = UserPage;
|
||||||
|
|||||||
@@ -1,431 +0,0 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 400, "skipBlankLines": true, "skipComments": true}]*/
|
|
||||||
/*eslint max-params:["warn", { max: 10 }], */
|
|
||||||
require('./vaultPage.less');
|
|
||||||
|
|
||||||
const React = require('react');
|
|
||||||
const { useState, useEffect, useRef } = React;
|
|
||||||
|
|
||||||
const Nav = require('naturalcrit/nav/nav.jsx');
|
|
||||||
const Navbar = require('../../navbar/navbar.jsx');
|
|
||||||
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
|
|
||||||
const Account = require('../../navbar/account.navitem.jsx');
|
|
||||||
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
|
|
||||||
const HelpNavItem = require('../../navbar/help.navitem.jsx');
|
|
||||||
const BrewItem = require('../basePages/listPage/brewItem/brewItem.jsx');
|
|
||||||
const SplitPane = require('../../../../shared/naturalcrit/splitPane/splitPane.jsx');
|
|
||||||
const ErrorIndex = require('../errorPage/errors/errorIndex.js');
|
|
||||||
|
|
||||||
import request from '../../utils/request-middleware.js';
|
|
||||||
|
|
||||||
const VaultPage = (props)=>{
|
|
||||||
const [pageState, setPageState] = useState(parseInt(props.query.page) || 1);
|
|
||||||
|
|
||||||
const [sortState, setSort] = useState(props.query.sort || 'title');
|
|
||||||
const [dirState, setdir] = useState(props.query.dir || 'asc');
|
|
||||||
|
|
||||||
//Response state
|
|
||||||
const [brewCollection, setBrewCollection] = useState(null);
|
|
||||||
const [totalBrews, setTotalBrews] = useState(null);
|
|
||||||
const [searching, setSearching] = useState(false);
|
|
||||||
const [error, setError] = useState(null);
|
|
||||||
|
|
||||||
const titleRef = useRef(null);
|
|
||||||
const authorRef = useRef(null);
|
|
||||||
const countRef = useRef(null);
|
|
||||||
const v3Ref = useRef(null);
|
|
||||||
const legacyRef = useRef(null);
|
|
||||||
const submitButtonRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
disableSubmitIfFormInvalid();
|
|
||||||
loadPage(pageState, true, props.query.sort, props.query.dir);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateStateWithBrews = (brews, page)=>{
|
|
||||||
setBrewCollection(brews || null);
|
|
||||||
setPageState(parseInt(page) || 1);
|
|
||||||
setSearching(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateUrl = (titleValue, authorValue, countValue, v3Value, legacyValue, page, sort, dir)=>{
|
|
||||||
const url = new URL(window.location.href);
|
|
||||||
const urlParams = new URLSearchParams(url.search);
|
|
||||||
|
|
||||||
urlParams.set('title', titleValue);
|
|
||||||
urlParams.set('author', authorValue);
|
|
||||||
urlParams.set('count', countValue);
|
|
||||||
urlParams.set('v3', v3Value);
|
|
||||||
urlParams.set('legacy', legacyValue);
|
|
||||||
urlParams.set('page', page);
|
|
||||||
urlParams.set('sort', sort);
|
|
||||||
urlParams.set('dir', dir);
|
|
||||||
|
|
||||||
url.search = urlParams.toString();
|
|
||||||
window.history.replaceState(null, '', url.toString());
|
|
||||||
};
|
|
||||||
|
|
||||||
const performSearch = async (title, author, count, v3, legacy, page, sort, dir)=>{
|
|
||||||
updateUrl(title, author, count, v3, legacy, page, sort, dir);
|
|
||||||
|
|
||||||
const response = await request
|
|
||||||
.get(`/api/vault?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}&count=${count}&page=${page}&sort=${sort}&dir=${dir}`)
|
|
||||||
.catch((error)=>{
|
|
||||||
console.log('error at loadPage: ', error);
|
|
||||||
setError(error);
|
|
||||||
updateStateWithBrews([], 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(response.ok)
|
|
||||||
updateStateWithBrews(response.body.brews, page);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadTotal = async (title, author, v3, legacy)=>{
|
|
||||||
setTotalBrews(null);
|
|
||||||
|
|
||||||
const response = await request.get(`/api/vault/total?title=${title}&author=${author}&v3=${v3}&legacy=${legacy}`)
|
|
||||||
.catch((error)=>{
|
|
||||||
console.log('error at loadTotal: ', error);
|
|
||||||
setError(error);
|
|
||||||
updateStateWithBrews([], 1);
|
|
||||||
});
|
|
||||||
|
|
||||||
if(response.ok)
|
|
||||||
setTotalBrews(response.body.totalBrews);
|
|
||||||
};
|
|
||||||
|
|
||||||
const loadPage = async (page, updateTotal, sort, dir)=>{
|
|
||||||
if(!validateForm()) return;
|
|
||||||
|
|
||||||
setSearching(true);
|
|
||||||
setError(null);
|
|
||||||
|
|
||||||
const title = titleRef.current.value || '';
|
|
||||||
const author = authorRef.current.value || '';
|
|
||||||
const count = countRef.current.value || 10;
|
|
||||||
const v3 = v3Ref.current.checked != false;
|
|
||||||
const legacy = legacyRef.current.checked != false;
|
|
||||||
const sortOption = sort || 'title';
|
|
||||||
const dirOption = dir || 'asc';
|
|
||||||
const pageProp = page || 1;
|
|
||||||
|
|
||||||
setSort(sortOption);
|
|
||||||
setdir(dirOption);
|
|
||||||
|
|
||||||
performSearch(title, author, count, v3, legacy, pageProp, sortOption, dirOption);
|
|
||||||
|
|
||||||
if(updateTotal)
|
|
||||||
loadTotal(title, author, v3, legacy);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderNavItems = ()=>(
|
|
||||||
<Navbar>
|
|
||||||
<Nav.section>
|
|
||||||
<Nav.item className='brewTitle'>
|
|
||||||
Vault: Search for brews
|
|
||||||
</Nav.item>
|
|
||||||
</Nav.section>
|
|
||||||
<Nav.section>
|
|
||||||
<NewBrew />
|
|
||||||
<HelpNavItem />
|
|
||||||
<RecentNavItem />
|
|
||||||
<Account />
|
|
||||||
</Nav.section>
|
|
||||||
</Navbar>
|
|
||||||
);
|
|
||||||
|
|
||||||
const validateForm = ()=>{
|
|
||||||
//form validity: title or author must be written, and at least one renderer set
|
|
||||||
const isTitleValid = titleRef.current.validity.valid && titleRef.current.value;
|
|
||||||
const isAuthorValid = authorRef.current.validity.valid && authorRef.current.value;
|
|
||||||
const isCheckboxChecked = legacyRef.current.checked || v3Ref.current.checked;
|
|
||||||
|
|
||||||
const isFormValid = (isTitleValid || isAuthorValid) && isCheckboxChecked;
|
|
||||||
|
|
||||||
return isFormValid;
|
|
||||||
};
|
|
||||||
|
|
||||||
const disableSubmitIfFormInvalid = ()=>{
|
|
||||||
submitButtonRef.current.disabled = !validateForm();
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderForm = ()=>(
|
|
||||||
<div className='brewLookup'>
|
|
||||||
<h2 className='formTitle'>Brew Lookup</h2>
|
|
||||||
<div className='formContents'>
|
|
||||||
<label>
|
|
||||||
Title of the brew
|
|
||||||
<input
|
|
||||||
ref={titleRef}
|
|
||||||
type='text'
|
|
||||||
name='title'
|
|
||||||
defaultValue={props.query.title || ''}
|
|
||||||
onKeyUp={disableSubmitIfFormInvalid}
|
|
||||||
pattern='.{3,}'
|
|
||||||
title='At least 3 characters'
|
|
||||||
onKeyDown={(e)=>{
|
|
||||||
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
|
|
||||||
loadPage(1, true);
|
|
||||||
}}
|
|
||||||
placeholder='v3 Reference Document'
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Author of the brew
|
|
||||||
<input
|
|
||||||
ref={authorRef}
|
|
||||||
type='text'
|
|
||||||
name='author'
|
|
||||||
pattern='.{1,}'
|
|
||||||
defaultValue={props.query.author || ''}
|
|
||||||
onKeyUp={disableSubmitIfFormInvalid}
|
|
||||||
onKeyDown={(e)=>{
|
|
||||||
if(e.key === 'Enter' && !submitButtonRef.current.disabled)
|
|
||||||
loadPage(1, true);
|
|
||||||
}}
|
|
||||||
placeholder='Username'
|
|
||||||
/>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
Results per page
|
|
||||||
<select ref={countRef} name='count' defaultValue={props.query.count || 20}>
|
|
||||||
<option value='10'>10</option>
|
|
||||||
<option value='20'>20</option>
|
|
||||||
<option value='40'>40</option>
|
|
||||||
<option value='60'>60</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
className='renderer'
|
|
||||||
ref={v3Ref}
|
|
||||||
type='checkbox'
|
|
||||||
defaultChecked={props.query.v3 !== 'false'}
|
|
||||||
onChange={disableSubmitIfFormInvalid}
|
|
||||||
/>
|
|
||||||
Search for v3 brews
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<label>
|
|
||||||
<input
|
|
||||||
className='renderer'
|
|
||||||
ref={legacyRef}
|
|
||||||
type='checkbox'
|
|
||||||
defaultChecked={props.query.legacy !== 'false'}
|
|
||||||
onChange={disableSubmitIfFormInvalid}
|
|
||||||
/>
|
|
||||||
Search for legacy brews
|
|
||||||
</label>
|
|
||||||
|
|
||||||
<button
|
|
||||||
id='searchButton'
|
|
||||||
ref={submitButtonRef}
|
|
||||||
onClick={()=>{
|
|
||||||
loadPage(1, true);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
<i
|
|
||||||
className={searching ? 'fas fa-spin fa-spinner': 'fas fa-search'}
|
|
||||||
/>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<legend>
|
|
||||||
<h3>Tips and tricks</h3>
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
Only <b>published</b> brews are searchable via this tool
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Usernames are case-sensitive
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Use <code>"word"</code> to match an exact string,
|
|
||||||
and <code>-</code> to exclude words (at least one word must not be negated)
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
Some common words like "a", "after", "through", "itself", "here", etc.,
|
|
||||||
are ignored in searches. The full list can be found
|
|
||||||
<a href='https://github.com/mongodb/mongo/blob/0e3b3ca8480ddddf5d0105d11a94bd4698335312/src/mongo/db/fts/stop_words_english.txt'>
|
|
||||||
here
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<small>New features will be coming, such as filters and search by tags.</small>
|
|
||||||
</legend>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
const renderSortOption = (optionTitle, optionValue)=>{
|
|
||||||
const oppositeDir = dirState === 'asc' ? 'desc' : 'asc';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`sort-option ${sortState === optionValue ? `active` : ''}`}>
|
|
||||||
<button onClick={()=>loadPage(1, false, optionValue, oppositeDir)}>
|
|
||||||
{optionTitle}
|
|
||||||
</button>
|
|
||||||
{sortState === optionValue && (
|
|
||||||
<i className={`sortDir fas ${dirState === 'asc' ? 'fa-sort-up' : 'fa-sort-down'}`} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderSortBar = ()=>{
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='sort-container'>
|
|
||||||
{renderSortOption('Title', 'title', props.query.dir)}
|
|
||||||
{renderSortOption('Created Date', 'createdAt', props.query.dir)}
|
|
||||||
{renderSortOption('Updated Date', 'updatedAt', props.query.dir)}
|
|
||||||
{renderSortOption('Views', 'views', props.query.dir)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderPaginationControls = ()=>{
|
|
||||||
if(!totalBrews) return null;
|
|
||||||
|
|
||||||
const countInt = parseInt(props.query.count || 20);
|
|
||||||
const totalPages = Math.ceil(totalBrews / countInt);
|
|
||||||
|
|
||||||
let startPage, endPage;
|
|
||||||
if(pageState <= 6) {
|
|
||||||
startPage = 1;
|
|
||||||
endPage = Math.min(totalPages, 10);
|
|
||||||
} else if(pageState + 4 >= totalPages) {
|
|
||||||
startPage = Math.max(1, totalPages - 9);
|
|
||||||
endPage = totalPages;
|
|
||||||
} else {
|
|
||||||
startPage = pageState - 5;
|
|
||||||
endPage = pageState + 4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const pagesAroundCurrent = new Array(endPage - startPage + 1)
|
|
||||||
.fill()
|
|
||||||
.map((_, index)=>(
|
|
||||||
<a
|
|
||||||
key={startPage + index}
|
|
||||||
className={`pageNumber ${pageState === startPage + index ? 'currentPage' : ''}`}
|
|
||||||
onClick={()=>loadPage(startPage + index, false, sortState, dirState)}
|
|
||||||
>
|
|
||||||
{startPage + index}
|
|
||||||
</a>
|
|
||||||
));
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='paginationControls'>
|
|
||||||
<button
|
|
||||||
className='previousPage'
|
|
||||||
onClick={()=>loadPage(pageState - 1, false, sortState, dirState)}
|
|
||||||
disabled={pageState === startPage}
|
|
||||||
>
|
|
||||||
<i className='fa-solid fa-chevron-left'></i>
|
|
||||||
</button>
|
|
||||||
<ol className='pages'>
|
|
||||||
{startPage > 1 && (
|
|
||||||
<a
|
|
||||||
className='pageNumber firstPage'
|
|
||||||
onClick={()=>loadPage(1, false, sortState, dirState)}
|
|
||||||
>
|
|
||||||
1 ...
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
{pagesAroundCurrent}
|
|
||||||
{endPage < totalPages && (
|
|
||||||
<a
|
|
||||||
className='pageNumber lastPage'
|
|
||||||
onClick={()=>loadPage(totalPages, false, sortState, dirState)}
|
|
||||||
>
|
|
||||||
... {totalPages}
|
|
||||||
</a>
|
|
||||||
)}
|
|
||||||
</ol>
|
|
||||||
<button
|
|
||||||
className='nextPage'
|
|
||||||
onClick={()=>loadPage(pageState + 1, false, sortState, dirState)}
|
|
||||||
disabled={pageState === totalPages}
|
|
||||||
>
|
|
||||||
<i className='fa-solid fa-chevron-right'></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const renderFoundBrews = ()=>{
|
|
||||||
if(searching) {
|
|
||||||
return (
|
|
||||||
<div className='foundBrews searching'>
|
|
||||||
<h3 className='searchAnim'>Searching</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(error) {
|
|
||||||
const errorText = ErrorIndex()[error.HBErrorCode.toString()] || '';
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='foundBrews noBrews'>
|
|
||||||
<h3>Error: {errorText}</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!brewCollection) {
|
|
||||||
return (
|
|
||||||
<div className='foundBrews noBrews'>
|
|
||||||
<h3>No search yet</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if(brewCollection.length === 0) {
|
|
||||||
return (
|
|
||||||
<div className='foundBrews noBrews'>
|
|
||||||
<h3>No brews found</h3>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='foundBrews'>
|
|
||||||
<span className='totalBrews'>
|
|
||||||
{`Brews found: `}
|
|
||||||
<span>{totalBrews}</span>
|
|
||||||
</span>
|
|
||||||
{brewCollection.map((brew, index)=>{
|
|
||||||
return (
|
|
||||||
<BrewItem
|
|
||||||
brew={{ ...brew }}
|
|
||||||
key={index}
|
|
||||||
reportError={props.reportError}
|
|
||||||
renderStorage={false}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
{renderPaginationControls()}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className='sitePage vaultPage'>
|
|
||||||
<link href='/themes/V3/Blank/style.css' rel='stylesheet' />
|
|
||||||
<link href='/themes/V3/5ePHB/style.css' rel='stylesheet' />
|
|
||||||
{renderNavItems()}
|
|
||||||
<div className="content">
|
|
||||||
<SplitPane showDividerButtons={false}>
|
|
||||||
<div className='form dataGroup'>{renderForm()}</div>
|
|
||||||
<div className='resultsContainer dataGroup'>
|
|
||||||
{renderSortBar()}
|
|
||||||
{renderFoundBrews()}
|
|
||||||
</div>
|
|
||||||
</SplitPane>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
module.exports = VaultPage;
|
|
||||||
@@ -1,396 +0,0 @@
|
|||||||
.vaultPage {
|
|
||||||
height : 100%;
|
|
||||||
overflow-y : hidden;
|
|
||||||
background-color : #2C3E50;
|
|
||||||
|
|
||||||
*:not(input) { user-select : none; }
|
|
||||||
|
|
||||||
.content .dataGroup {
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
background : white;
|
|
||||||
|
|
||||||
&.form .brewLookup {
|
|
||||||
position : relative;
|
|
||||||
padding : 50px clamp(20px, 4vw, 50px);
|
|
||||||
|
|
||||||
small {
|
|
||||||
font-size : 10pt;
|
|
||||||
color : #555555;
|
|
||||||
|
|
||||||
a { color : #333333; }
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding-inline : 5px;
|
|
||||||
font-family : monospace;
|
|
||||||
background : lightgrey;
|
|
||||||
border-radius : 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h1, h2, h3, h4 {
|
|
||||||
font-family : 'CodeBold';
|
|
||||||
letter-spacing : 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
legend {
|
|
||||||
h3 {
|
|
||||||
margin-block : 30px 20px;
|
|
||||||
font-size : 20px;
|
|
||||||
text-align : center;
|
|
||||||
border-bottom : 2px solid;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
padding-inline : 30px 10px;
|
|
||||||
li {
|
|
||||||
margin-block : 5px;
|
|
||||||
line-height : calc(1em + 5px);
|
|
||||||
list-style : disc;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
position : absolute;
|
|
||||||
top : 0;
|
|
||||||
right : 0;
|
|
||||||
left : 0;
|
|
||||||
display : block;
|
|
||||||
padding : 10px;
|
|
||||||
font-weight : 900;
|
|
||||||
color : white;
|
|
||||||
white-space : pre-wrap;
|
|
||||||
content : 'Error:\A At least one renderer should be enabled to make a search';
|
|
||||||
background : rgb(255, 60, 60);
|
|
||||||
opacity : 0;
|
|
||||||
transition : opacity 0.5s;
|
|
||||||
}
|
|
||||||
&:not(:has(input[type='checkbox']:checked))::after { opacity : 1; }
|
|
||||||
|
|
||||||
.formTitle {
|
|
||||||
margin : 20px 0;
|
|
||||||
font-size : 30px;
|
|
||||||
color : black;
|
|
||||||
text-align : center;
|
|
||||||
border-bottom : 2px solid;
|
|
||||||
}
|
|
||||||
|
|
||||||
.formContents {
|
|
||||||
position : relative;
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
|
|
||||||
label {
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
margin : 10px 0;
|
|
||||||
}
|
|
||||||
select { margin : 0 10px; }
|
|
||||||
|
|
||||||
input {
|
|
||||||
margin : 0 10px;
|
|
||||||
|
|
||||||
&:invalid { background : rgb(255, 188, 181); }
|
|
||||||
|
|
||||||
&[type='checkbox'] {
|
|
||||||
position : relative;
|
|
||||||
display : inline-block;
|
|
||||||
width : 50px;
|
|
||||||
height : 30px;
|
|
||||||
font-family : 'WalterTurncoat';
|
|
||||||
font-size : 20px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
letter-spacing : 2px;
|
|
||||||
appearance : none;
|
|
||||||
background : red;
|
|
||||||
isolation : isolate;
|
|
||||||
border-radius : 5px;
|
|
||||||
|
|
||||||
&::before,&::after {
|
|
||||||
position : absolute;
|
|
||||||
inset : 0;
|
|
||||||
z-index : 5;
|
|
||||||
padding-top : 2px;
|
|
||||||
text-align : center;
|
|
||||||
}
|
|
||||||
|
|
||||||
&::before {
|
|
||||||
display : block;
|
|
||||||
content : 'No';
|
|
||||||
}
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
display : none;
|
|
||||||
content : 'Yes';
|
|
||||||
}
|
|
||||||
|
|
||||||
&:checked {
|
|
||||||
background : green;
|
|
||||||
|
|
||||||
&::before { display : none; }
|
|
||||||
&::after { display : block; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#searchButton {
|
|
||||||
position : absolute;
|
|
||||||
right : 20px;
|
|
||||||
bottom : 0;
|
|
||||||
|
|
||||||
i {
|
|
||||||
margin-left : 10px;
|
|
||||||
animation-duration : 1000s;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.resultsContainer {
|
|
||||||
display : flex;
|
|
||||||
flex-direction : column;
|
|
||||||
height : 100%;
|
|
||||||
overflow-y : auto;
|
|
||||||
font-family : 'BookInsanityRemake';
|
|
||||||
font-size : 0.34cm;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-family : 'Open Sans';
|
|
||||||
font-weight : 900;
|
|
||||||
color : white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sort-container {
|
|
||||||
display : flex;
|
|
||||||
flex-wrap : wrap;
|
|
||||||
column-gap : 15px;
|
|
||||||
justify-content : center;
|
|
||||||
height : 30px;
|
|
||||||
color : white;
|
|
||||||
background-color : #555555;
|
|
||||||
border-top : 1px solid #666666;
|
|
||||||
border-bottom : 1px solid #666666;
|
|
||||||
|
|
||||||
.sort-option {
|
|
||||||
display : flex;
|
|
||||||
align-items : center;
|
|
||||||
padding : 0 8px;
|
|
||||||
|
|
||||||
&:hover { background-color : #444444; }
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
background-color : #333333;
|
|
||||||
|
|
||||||
button {
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
|
|
||||||
& + .sortDir { padding-left : 5px; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
padding : 0;
|
|
||||||
font-size : 11px;
|
|
||||||
font-weight : normal;
|
|
||||||
color : #CCCCCC;
|
|
||||||
text-transform : uppercase;
|
|
||||||
background-color : transparent;
|
|
||||||
|
|
||||||
&:hover { background : none; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.foundBrews {
|
|
||||||
position : relative;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
max-height : 100%;
|
|
||||||
padding : 50px 50px 70px 50px;
|
|
||||||
overflow-y : scroll;
|
|
||||||
background-color : #2C3E50;
|
|
||||||
|
|
||||||
h3 { font-size : 25px; }
|
|
||||||
|
|
||||||
&.noBrews {
|
|
||||||
display : grid;
|
|
||||||
place-items : center;
|
|
||||||
color : white;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.searching {
|
|
||||||
display : grid;
|
|
||||||
place-items : center;
|
|
||||||
color : white;
|
|
||||||
|
|
||||||
h3 { position : relative; }
|
|
||||||
|
|
||||||
h3.searchAnim::after {
|
|
||||||
position : absolute;
|
|
||||||
top : 50%;
|
|
||||||
right : 0;
|
|
||||||
width : max-content;
|
|
||||||
height : 1em;
|
|
||||||
content : '';
|
|
||||||
translate : calc(100% + 5px) -50%;
|
|
||||||
animation : trailingDots 2s ease infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.totalBrews {
|
|
||||||
position : fixed;
|
|
||||||
right : 0;
|
|
||||||
bottom : 0;
|
|
||||||
z-index : 1000;
|
|
||||||
padding : 8px 10px;
|
|
||||||
font-family : 'Open Sans';
|
|
||||||
font-size : 11px;
|
|
||||||
font-weight : 800;
|
|
||||||
color : white;
|
|
||||||
background-color : #333333;
|
|
||||||
|
|
||||||
.searchAnim {
|
|
||||||
position : relative;
|
|
||||||
display : inline-block;
|
|
||||||
width : 3ch;
|
|
||||||
height : 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchAnim::after {
|
|
||||||
position : absolute;
|
|
||||||
top : 50%;
|
|
||||||
right : 0;
|
|
||||||
width : max-content;
|
|
||||||
height : 1em;
|
|
||||||
content : '';
|
|
||||||
translate : -50% -50%;
|
|
||||||
animation : trailingDots 2s ease infinite;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.brewItem {
|
|
||||||
width : 47%;
|
|
||||||
margin-right : 40px;
|
|
||||||
color : black;
|
|
||||||
isolation : isolate;
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
position : absolute;
|
|
||||||
inset : 0;
|
|
||||||
z-index : -2;
|
|
||||||
display : block;
|
|
||||||
content : '';
|
|
||||||
background-image : url('/assets/parchmentBackground.jpg');
|
|
||||||
}
|
|
||||||
|
|
||||||
&:nth-child(even of .brewItem) { margin-right : 0; }
|
|
||||||
|
|
||||||
h2 {
|
|
||||||
font-family : 'MrEavesRemake';
|
|
||||||
font-size : 0.75cm;
|
|
||||||
font-weight : 800;
|
|
||||||
line-height : 0.988em;
|
|
||||||
color : var(--HB_Color_HeaderText);
|
|
||||||
}
|
|
||||||
.info {
|
|
||||||
position : relative;
|
|
||||||
z-index : 2;
|
|
||||||
font-family : 'ScalySansRemake';
|
|
||||||
font-size : 1.2em;
|
|
||||||
|
|
||||||
>span {
|
|
||||||
margin-right : 12px;
|
|
||||||
line-height : 1.5em;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.links { z-index : 2; }
|
|
||||||
|
|
||||||
hr {
|
|
||||||
margin : 0px;
|
|
||||||
visibility : hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.thumbnail { z-index : -1; }
|
|
||||||
}
|
|
||||||
|
|
||||||
.paginationControls {
|
|
||||||
position : absolute;
|
|
||||||
left : 50%;
|
|
||||||
display : grid;
|
|
||||||
grid-template-areas : 'previousPage currentPage nextPage';
|
|
||||||
grid-template-columns : 50px 1fr 50px;
|
|
||||||
place-items : center;
|
|
||||||
width : auto;
|
|
||||||
translate : -50%;
|
|
||||||
|
|
||||||
.pages {
|
|
||||||
display : flex;
|
|
||||||
grid-area : currentPage;
|
|
||||||
justify-content : space-evenly;
|
|
||||||
width : 100%;
|
|
||||||
height : 100%;
|
|
||||||
padding : 5px 8px;
|
|
||||||
text-align : center;
|
|
||||||
|
|
||||||
.pageNumber {
|
|
||||||
margin-inline : 1vw;
|
|
||||||
font-family : 'Open Sans';
|
|
||||||
font-weight : 900;
|
|
||||||
color : white;
|
|
||||||
text-underline-position : under;
|
|
||||||
text-wrap : nowrap;
|
|
||||||
cursor : pointer;
|
|
||||||
|
|
||||||
&.currentPage {
|
|
||||||
color : gold;
|
|
||||||
text-decoration : underline;
|
|
||||||
pointer-events : none;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.firstPage { margin-right : -5px; }
|
|
||||||
|
|
||||||
&.lastPage { margin-left : -5px; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
button {
|
|
||||||
width : max-content;
|
|
||||||
|
|
||||||
&.previousPage { grid-area : previousPage; }
|
|
||||||
|
|
||||||
&.nextPage { grid-area : nextPage; }
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes trailingDots {
|
|
||||||
|
|
||||||
0%,
|
|
||||||
32% { content : ' .'; }
|
|
||||||
|
|
||||||
33%,
|
|
||||||
65% { content : ' ..'; }
|
|
||||||
|
|
||||||
66%,
|
|
||||||
100% { content : ' ...'; }
|
|
||||||
}
|
|
||||||
|
|
||||||
// media query for when the page is smaller than 1079 px in width
|
|
||||||
@media screen and (max-width : 1079px) {
|
|
||||||
.vaultPage {
|
|
||||||
|
|
||||||
.dataGroup.form .brewLookup { padding : 1px 20px 20px 10px; }
|
|
||||||
|
|
||||||
.dataGroup.resultsContainer .foundBrews .brewItem {
|
|
||||||
width : 100%;
|
|
||||||
margin-inline : auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import * as IDB from 'idb-keyval/dist/index.js';
|
|
||||||
|
|
||||||
export function initCustomStore(db, store){
|
|
||||||
const createCustomStore = async ()=>IDB.createStore(db, store);
|
|
||||||
|
|
||||||
return {
|
|
||||||
entries : async ()=>IDB.entries(await createCustomStore()),
|
|
||||||
keys : async ()=>IDB.keys(await createCustomStore()),
|
|
||||||
values : async ()=>IDB.values(await createCustomStore()),
|
|
||||||
clear : async ()=>IDB.clear(await createCustomStore),
|
|
||||||
get : async (key)=>IDB.get(key, await createCustomStore()),
|
|
||||||
getMany : async (keys)=>IDB.getMany(keys, await createCustomStore()),
|
|
||||||
set : async (key, value)=>IDB.set(key, value, await createCustomStore()),
|
|
||||||
setMany : async (entries)=>IDB.setMany(entries, await createCustomStore()),
|
|
||||||
update : async (key, updateFn)=>IDB.update(key, updateFn, await createCustomStore()),
|
|
||||||
del : async (key)=>IDB.del(key, await createCustomStore()),
|
|
||||||
delMany : async (keys)=>IDB.delMany(keys, await createCustomStore())
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import request from 'superagent';
|
const request = require('superagent');
|
||||||
|
|
||||||
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
const addHeader = (request)=>request.set('Homebrewery-Version', global.version);
|
||||||
|
|
||||||
@@ -9,4 +9,4 @@ const requestMiddleware = {
|
|||||||
delete : (path)=>addHeader(request.delete(path)),
|
delete : (path)=>addHeader(request.delete(path)),
|
||||||
};
|
};
|
||||||
|
|
||||||
export default requestMiddleware;
|
module.exports = requestMiddleware;
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { initCustomStore } from './customIDBStore.js';
|
|
||||||
|
|
||||||
export const HISTORY_PREFIX = 'HOMEBREWERY-HISTORY';
|
|
||||||
export const HISTORY_SLOTS = 5;
|
|
||||||
|
|
||||||
// History values in minutes
|
|
||||||
const HISTORY_SAVE_DELAYS = {
|
|
||||||
'0' : 0,
|
|
||||||
'1' : 2,
|
|
||||||
'2' : 10,
|
|
||||||
'3' : 60,
|
|
||||||
'4' : 12 * 60,
|
|
||||||
'5' : 2 * 24 * 60
|
|
||||||
};
|
|
||||||
// const HISTORY_SAVE_DELAYS = {
|
|
||||||
// '0' : 0,
|
|
||||||
// '1' : 1,
|
|
||||||
// '2' : 2,
|
|
||||||
// '3' : 3,
|
|
||||||
// '4' : 4,
|
|
||||||
// '5' : 5
|
|
||||||
// };
|
|
||||||
|
|
||||||
const GARBAGE_COLLECT_DELAY = 28 * 24 * 60;
|
|
||||||
// const GARBAGE_COLLECT_DELAY = 10;
|
|
||||||
|
|
||||||
|
|
||||||
const HB_DB = 'HOMEBREWERY-DB';
|
|
||||||
const HB_STORE = 'HISTORY';
|
|
||||||
|
|
||||||
const IDB = initCustomStore(HB_DB, HB_STORE);
|
|
||||||
|
|
||||||
function getKeyBySlot(brew, slot){
|
|
||||||
// Return a string representing the key for this brew and history slot
|
|
||||||
return `${HISTORY_PREFIX}-${brew.shareId}-${slot}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseBrewForStorage(brew, slot = 0) {
|
|
||||||
// Strip out unneeded object properties
|
|
||||||
// Returns an array of [ key, brew ]
|
|
||||||
const archiveBrew = {
|
|
||||||
title : brew.title,
|
|
||||||
text : brew.text,
|
|
||||||
style : brew.style,
|
|
||||||
version : brew.version,
|
|
||||||
shareId : brew.shareId,
|
|
||||||
savedAt : brew?.savedAt || new Date(),
|
|
||||||
expireAt : new Date()
|
|
||||||
};
|
|
||||||
|
|
||||||
archiveBrew.expireAt.setMinutes(archiveBrew.expireAt.getMinutes() + HISTORY_SAVE_DELAYS[slot]);
|
|
||||||
|
|
||||||
const key = getKeyBySlot(brew, slot);
|
|
||||||
|
|
||||||
return [key, archiveBrew];
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function loadHistory(brew){
|
|
||||||
const DEFAULT_HISTORY_ITEM = { expireAt: '2000-01-01T00:00:00.000Z', shareId: brew.shareId, noData: true };
|
|
||||||
|
|
||||||
const historyKeys = [];
|
|
||||||
|
|
||||||
// Create array of all history keys
|
|
||||||
for (let i = 1; i <= HISTORY_SLOTS; i++){
|
|
||||||
historyKeys.push(getKeyBySlot(brew, i));
|
|
||||||
};
|
|
||||||
|
|
||||||
// Load all keys from IDB at once
|
|
||||||
const dataArray = await IDB.getMany(historyKeys);
|
|
||||||
return dataArray.map((data)=>{ return data ?? DEFAULT_HISTORY_ITEM; });
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateHistory(brew) {
|
|
||||||
const history = await loadHistory(brew);
|
|
||||||
|
|
||||||
// Walk each version position
|
|
||||||
for (let slot = HISTORY_SLOTS - 1; slot >= 0; slot--){
|
|
||||||
const storedVersion = history[slot];
|
|
||||||
|
|
||||||
// If slot has expired, update all lower slots and break
|
|
||||||
if(new Date() >= new Date(storedVersion.expireAt)){
|
|
||||||
|
|
||||||
// Create array of arrays : [ [key1, value1], [key2, value2], ..., [keyN, valueN] ]
|
|
||||||
// to pass to IDB.setMany
|
|
||||||
const historyUpdate = [];
|
|
||||||
|
|
||||||
for (let updateSlot = slot; updateSlot > 0; updateSlot--){
|
|
||||||
// Move data from updateSlot to updateSlot + 1
|
|
||||||
if(!history[updateSlot - 1]?.noData) {
|
|
||||||
historyUpdate.push(parseBrewForStorage(history[updateSlot - 1], updateSlot + 1));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Update the most recent brew
|
|
||||||
historyUpdate.push(parseBrewForStorage(brew, 1));
|
|
||||||
|
|
||||||
await IDB.setMany(historyUpdate);
|
|
||||||
|
|
||||||
// Break out of data checks because we found an expired value
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export async function versionHistoryGarbageCollection(){
|
|
||||||
const entries = await IDB.entries();
|
|
||||||
|
|
||||||
const expiredKeys = [];
|
|
||||||
for (const [key, value] of entries){
|
|
||||||
const expireAt = new Date(value.savedAt);
|
|
||||||
expireAt.setMinutes(expireAt.getMinutes() + GARBAGE_COLLECT_DELAY);
|
|
||||||
if(new Date() > expireAt){
|
|
||||||
expiredKeys.push(key);
|
|
||||||
};
|
|
||||||
};
|
|
||||||
if(expiredKeys.length > 0){
|
|
||||||
await IDB.delMany(expiredKeys);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,84 +1,57 @@
|
|||||||
.fac {
|
.fac {
|
||||||
display : inline-block;
|
display : inline-block;
|
||||||
background-color : currentColor;
|
|
||||||
mask-size : contain;
|
|
||||||
mask-repeat : no-repeat;
|
|
||||||
mask-position : center;
|
|
||||||
width : 1em;
|
|
||||||
aspect-ratio : 1;
|
|
||||||
}
|
}
|
||||||
.position-top-left {
|
.position-top-left {
|
||||||
mask-image: url('../icons/position-top-left.svg');
|
content: url('../icons/position-top-left.svg');
|
||||||
}
|
}
|
||||||
.position-top-right {
|
.position-top-right {
|
||||||
mask-image: url('../icons/position-top-right.svg');
|
content: url('../icons/position-top-right.svg');
|
||||||
}
|
}
|
||||||
.position-bottom-left {
|
.position-bottom-left {
|
||||||
mask-image: url('../icons/position-bottom-left.svg');
|
content: url('../icons/position-bottom-left.svg');
|
||||||
}
|
}
|
||||||
.position-bottom-right {
|
.position-bottom-right {
|
||||||
mask-image: url('../icons/position-bottom-right.svg');
|
content: url('../icons/position-bottom-right.svg');
|
||||||
}
|
}
|
||||||
.position-top {
|
.position-top {
|
||||||
mask-image: url('../icons/position-top.svg');
|
content: url('../icons/position-top.svg');
|
||||||
}
|
}
|
||||||
.position-right {
|
.position-right {
|
||||||
mask-image: url('../icons/position-right.svg');
|
content: url('../icons/position-right.svg');
|
||||||
}
|
}
|
||||||
.position-bottom {
|
.position-bottom {
|
||||||
mask-image: url('../icons/position-bottom.svg');
|
content: url('../icons/position-bottom.svg');
|
||||||
}
|
}
|
||||||
.position-left {
|
.position-left {
|
||||||
mask-image: url('../icons/position-left.svg');
|
content: url('../icons/position-left.svg');
|
||||||
}
|
}
|
||||||
.mask-edge {
|
.mask-edge {
|
||||||
mask-image: url('../icons/mask-edge.svg');
|
content: url('../icons/mask-edge.svg');
|
||||||
}
|
}
|
||||||
.mask-corner {
|
.mask-corner {
|
||||||
mask-image: url('../icons/mask-corner.svg');
|
content: url('../icons/mask-corner.svg');
|
||||||
}
|
}
|
||||||
.mask-center {
|
.mask-center {
|
||||||
mask-image: url('../icons/mask-center.svg');
|
content: url('../icons/mask-center.svg');
|
||||||
}
|
}
|
||||||
.book-front-cover {
|
.book-front-cover {
|
||||||
mask-image: url('../icons/book-front-cover.svg');
|
content: url('../icons/book-front-cover.svg');
|
||||||
}
|
}
|
||||||
.book-back-cover {
|
.book-back-cover {
|
||||||
mask-image: url('../icons/book-back-cover.svg');
|
content: url('../icons/book-back-cover.svg');
|
||||||
}
|
}
|
||||||
.book-inside-cover {
|
.book-inside-cover {
|
||||||
mask-image: url('../icons/book-inside-cover.svg');
|
content: url('../icons/book-inside-cover.svg');
|
||||||
}
|
}
|
||||||
.book-part-cover {
|
.book-part-cover {
|
||||||
mask-image: url('../icons/book-part-cover.svg');
|
content: url('../icons/book-part-cover.svg');
|
||||||
}
|
|
||||||
.image-wrap-left {
|
|
||||||
mask-image: url('../icons/image-wrap-left.svg');
|
|
||||||
}
|
|
||||||
.image-wrap-right {
|
|
||||||
mask-image: url('../icons/image-wrap-right.svg');
|
|
||||||
}
|
}
|
||||||
.davek {
|
.davek {
|
||||||
mask-image: url('../icons/Davek.svg');
|
content: url('../icons/Davek.svg');
|
||||||
}
|
}
|
||||||
.rellanic {
|
.rellanic {
|
||||||
mask-image: url('../icons/Rellanic.svg');
|
content: url('../icons/Rellanic.svg');
|
||||||
}
|
}
|
||||||
.iokharic {
|
.iokharic {
|
||||||
mask-image: url('../icons/Iokharic.svg');
|
content: url('../icons/Iokharic.svg');
|
||||||
}
|
|
||||||
.zoom-to-fit {
|
|
||||||
mask-image: url('../icons/zoom-to-fit.svg');
|
|
||||||
}
|
|
||||||
.fit-width {
|
|
||||||
mask-image: url('../icons/fit-width.svg');
|
|
||||||
}
|
|
||||||
.single-spread {
|
|
||||||
mask-image: url('../icons/single-spread.svg');
|
|
||||||
}
|
|
||||||
.facing-spread {
|
|
||||||
mask-image: url('../icons/facing-spread.svg');
|
|
||||||
}
|
|
||||||
.flow-spread {
|
|
||||||
mask-image: url('../icons/flow-spread.svg');
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(0.979101,0,0,0.919064,-29.0748,1.98095)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.979101,0,0,0.919064,23.058,1.98095)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.1 KiB |
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1.07509,0,0,1.07509,-3.75511,-3.75468)">
|
|
||||||
<g transform="matrix(0.843549,0,0,0.950644,8.38004,4.39672)">
|
|
||||||
<path d="M28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L28.455,52.413Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1.46702,0,0,0.986488,-23.0335,3.50686)">
|
|
||||||
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1.46702,0,0,0.986488,60.7211,3.50686)">
|
|
||||||
<path d="M23.967,5.877L23.967,88.383C23.967,90.556 22.781,92.321 21.319,92.321L21.157,92.321C19.695,92.321 18.509,90.556 18.509,88.383L18.509,5.877C18.509,3.703 19.695,1.939 21.157,1.939L21.319,1.939C22.781,1.939 23.967,3.703 23.967,5.877Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,24 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1.0781,0,0,1.0781,-3.90545,-3.90502)">
|
|
||||||
<g transform="matrix(0.590052,0,0,0.553871,-13.8993,-2.19227)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.590052,0,0,0.553871,-13.8993,44.3152)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.590052,0,0,0.553871,17.5184,-2.19227)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.590052,0,0,0.553871,50.0095,-2.19227)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.590052,0,0,0.553871,17.5184,44.3152)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(0.590052,0,0,0.553871,50.0095,44.3152)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 512.00006 512"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg10"
|
|
||||||
sodipodi:docname="noun-wrap-image-left-212078.svg"
|
|
||||||
width="512.00006"
|
|
||||||
height="512"
|
|
||||||
inkscape:export-filename="image-wrap-right.svg"
|
|
||||||
inkscape:export-xdpi="300"
|
|
||||||
inkscape:export-ydpi="300"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs10" /><sodipodi:namedview
|
|
||||||
id="namedview10"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#111111"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:showpageshadow="0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pagecheckerboard="1"
|
|
||||||
inkscape:deskcolor="#d1d1d1" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 185.80018,144 H 32"
|
|
||||||
id="path11"
|
|
||||||
sodipodi:nodetypes="cc"
|
|
||||||
clip-path="none"
|
|
||||||
inkscape:export-filename="image-wrap-right.svg"
|
|
||||||
inkscape:export-xdpi="300"
|
|
||||||
inkscape:export-ydpi="300" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 185.80018,368 H 32"
|
|
||||||
id="path11-8"
|
|
||||||
sodipodi:nodetypes="cc"
|
|
||||||
clip-path="none" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 480.00007,32 H 32"
|
|
||||||
id="path11-8-2-67"
|
|
||||||
clip-path="none"
|
|
||||||
sodipodi:nodetypes="cc" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 480.00008,480 H 32"
|
|
||||||
id="path11-8-2-67-2"
|
|
||||||
clip-path="none"
|
|
||||||
sodipodi:nodetypes="cc" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 160.0001,255.98832 32,256.01162"
|
|
||||||
id="path11-0"
|
|
||||||
sodipodi:nodetypes="cc"
|
|
||||||
clip-path="none" /><path
|
|
||||||
id="path23"
|
|
||||||
style="opacity:0.922046;fill:#000000;fill-opacity:1;stroke-width:64;stroke-linecap:round;stroke-dasharray:none;paint-order:fill markers stroke"
|
|
||||||
d="m 416.00008,96 a 160,160 0 0 1 96,32.50977 v 254.98046 a 160,160 0 0 1 -96,32.50977 160,160 0 0 1 -160,-160 160,160 0 0 1 160,-160 z" /></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,58 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
version="1.1"
|
|
||||||
x="0px"
|
|
||||||
y="0px"
|
|
||||||
viewBox="0 0 512.00006 512"
|
|
||||||
xml:space="preserve"
|
|
||||||
id="svg10"
|
|
||||||
sodipodi:docname="noun-wrap-image-left-212078.svg"
|
|
||||||
width="512.00006"
|
|
||||||
height="512"
|
|
||||||
inkscape:export-filename="image-wrap-right.svg"
|
|
||||||
inkscape:export-xdpi="300"
|
|
||||||
inkscape:export-ydpi="300"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"><defs
|
|
||||||
id="defs10" /><sodipodi:namedview
|
|
||||||
id="namedview10"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#111111"
|
|
||||||
borderopacity="1"
|
|
||||||
inkscape:showpageshadow="0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pagecheckerboard="1"
|
|
||||||
inkscape:deskcolor="#d1d1d1" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 326.1999,144 H 480.00008"
|
|
||||||
id="path11"
|
|
||||||
sodipodi:nodetypes="cc"
|
|
||||||
clip-path="none"
|
|
||||||
inkscape:export-filename="image-wrap-right.svg"
|
|
||||||
inkscape:export-xdpi="300"
|
|
||||||
inkscape:export-ydpi="300" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 326.1999,368 H 480.00008"
|
|
||||||
id="path11-8"
|
|
||||||
sodipodi:nodetypes="cc"
|
|
||||||
clip-path="none" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 32.00001,32 H 480.00008"
|
|
||||||
id="path11-8-2-67"
|
|
||||||
clip-path="none"
|
|
||||||
sodipodi:nodetypes="cc" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="M 32,480 H 480.00008"
|
|
||||||
id="path11-8-2-67-2"
|
|
||||||
clip-path="none"
|
|
||||||
sodipodi:nodetypes="cc" /><path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:64;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 351.99998,255.98832 128.0001,0.0233"
|
|
||||||
id="path11-0"
|
|
||||||
sodipodi:nodetypes="cc"
|
|
||||||
clip-path="none" /><path
|
|
||||||
id="path23"
|
|
||||||
style="opacity:0.922046;fill:#000000;fill-opacity:1;stroke-width:64;stroke-linecap:round;stroke-dasharray:none;paint-order:fill markers stroke"
|
|
||||||
d="M 96,96 A 160,160 0 0 0 0,128.50977 V 383.49023 A 160,160 0 0 0 96,416 160,160 0 0 0 256,256 160,160 0 0 0 96,96 Z" /></svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1.41826,0,0,1.3313,-26.7845,-19.5573)">
|
|
||||||
<path d="M78.584,16.13C78.584,15.335 78.164,14.69 77.647,14.69L30.632,14.69C30.115,14.69 29.695,15.335 29.695,16.13L29.695,88.365C29.695,89.16 30.115,89.805 30.632,89.805L77.647,89.805C78.164,89.805 78.584,89.16 78.584,88.365L78.584,16.13Z"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 777 B |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
|
|
||||||
<svg width="100%" height="100%" viewBox="0 0 100 100" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2;">
|
|
||||||
<g transform="matrix(1.0781,0,0,1.0781,-3.90545,-3.90502)">
|
|
||||||
<g transform="matrix(0.841196,0,0,0.947993,8.49652,4.52391)">
|
|
||||||
<path d="M44.333,52.413L28.455,52.413L28.455,58.581C28.455,59.719 27.684,60.745 26.501,61.181C25.318,61.616 23.956,61.375 23.051,60.571L11.114,49.96C9.878,48.862 9.878,47.08 11.114,45.981L23.051,35.371C23.956,34.566 25.318,34.326 26.501,34.761C27.684,35.197 28.455,36.223 28.455,37.361L28.455,43.528L44.333,43.528L44.333,29.439L37.382,29.439C36.099,29.439 34.943,28.755 34.452,27.705C33.961,26.656 34.233,25.448 35.14,24.644L47.097,14.052C48.335,12.956 50.343,12.956 51.581,14.052L63.539,24.644C64.446,25.448 64.717,26.656 64.226,27.705C63.735,28.755 62.579,29.439 61.296,29.439L54.346,29.439L54.346,43.528L70.223,43.528L70.223,37.361C70.223,36.223 70.995,35.197 72.177,34.761C73.36,34.326 74.722,34.566 75.627,35.371L87.564,45.981C88.8,47.08 88.8,48.862 87.564,49.96L75.627,60.571C74.722,61.375 73.36,61.616 72.177,61.181C70.995,60.745 70.223,59.719 70.223,58.581L70.223,52.413L54.346,52.413L54.346,66.502L61.296,66.502C62.579,66.502 63.735,67.187 64.226,68.236C64.717,69.286 64.446,70.494 63.539,71.297L51.581,81.889C50.343,82.986 48.335,82.986 47.097,81.889L35.14,71.297C34.233,70.494 33.961,69.286 34.452,68.236C34.943,67.187 36.099,66.502 37.382,66.502L44.333,66.503L44.333,52.413Z"/>
|
|
||||||
</g>
|
|
||||||
<g transform="matrix(1.0247,0,0,1.0247,-5.47698,-3.53855)">
|
|
||||||
<path d="M99.4,14.269L99.4,90.227C99.4,94.245 96.137,97.508 92.119,97.508L16.161,97.508C12.142,97.508 8.88,94.245 8.88,90.227L8.88,14.269C8.88,10.25 12.142,6.988 16.161,6.988L92.119,6.988C96.137,6.988 99.4,10.25 99.4,14.269ZM93.633,14.269C93.633,13.433 92.955,12.755 92.119,12.755L16.161,12.755C15.325,12.755 14.647,13.433 14.647,14.269L14.647,90.227C14.647,91.062 15.325,91.741 16.161,91.741L92.119,91.741C92.955,91.741 93.633,91.062 93.633,90.227L93.633,14.269Z"/>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 2.3 KiB |
@@ -8,8 +8,6 @@ const template = async function(name, title='', props = {}){
|
|||||||
});
|
});
|
||||||
const ogMetaTags = ogTags.join('\n');
|
const ogMetaTags = ogTags.join('\n');
|
||||||
|
|
||||||
const ssrModule = await import(`../build/${name}/ssr.cjs`);
|
|
||||||
|
|
||||||
return `<!DOCTYPE html>
|
return `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
@@ -23,7 +21,7 @@ const template = async function(name, title='', props = {}){
|
|||||||
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
<title>${title.length ? `${title} - The Homebrewery`: 'The Homebrewery - NaturalCrit'}</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main id="reactRoot">${ssrModule.default(props)}</main>
|
<main id="reactRoot">${require(`../build/${name}/ssr.js`)(props)}</main>
|
||||||
<script src=${`/${name}/bundle.js`}></script>
|
<script src=${`/${name}/bundle.js`}></script>
|
||||||
<script>start_app(${JSON.stringify(props)})</script>
|
<script>start_app(${JSON.stringify(props)})</script>
|
||||||
</body>
|
</body>
|
||||||
@@ -31,4 +29,4 @@ const template = async function(name, title='', props = {}){
|
|||||||
`;
|
`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default template;
|
module.exports = template;
|
||||||
@@ -4,9 +4,6 @@
|
|||||||
"secret" : "secret",
|
"secret" : "secret",
|
||||||
"web_port" : 8000,
|
"web_port" : 8000,
|
||||||
"enable_v3" : true,
|
"enable_v3" : true,
|
||||||
"enable_themes" : true,
|
|
||||||
"local_environments" : ["docker", "local"],
|
"local_environments" : ["docker", "local"],
|
||||||
"publicUrl" : "https://homebrewery.naturalcrit.com",
|
"publicUrl" : "https://homebrewery.naturalcrit.com"
|
||||||
"hb_images" : null,
|
|
||||||
"hb_fonts" : null
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,71 +0,0 @@
|
|||||||
import react from "eslint-plugin-react";
|
|
||||||
import jest from "eslint-plugin-jest";
|
|
||||||
import globals from "globals";
|
|
||||||
|
|
||||||
export default [{
|
|
||||||
ignores: ["build/"]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
files : ['**/*.js', '**/*.jsx'],
|
|
||||||
plugins : { react, jest },
|
|
||||||
languageOptions : {
|
|
||||||
ecmaVersion : "latest",
|
|
||||||
sourceType : "module",
|
|
||||||
parserOptions : { ecmaFeatures: { jsx: true } },
|
|
||||||
globals : { ...globals.browser, ...globals.node }
|
|
||||||
},
|
|
||||||
rules: {
|
|
||||||
/** Errors **/
|
|
||||||
"camelcase" : ["error", { properties: "never" }],
|
|
||||||
"no-array-constructor" : "error",
|
|
||||||
"no-iterator" : "error",
|
|
||||||
"no-nested-ternary" : "error",
|
|
||||||
"no-new-object" : "error",
|
|
||||||
"no-proto" : "error",
|
|
||||||
"react/jsx-no-bind" : ["error", { allowArrowFunctions: true }],
|
|
||||||
"react/jsx-uses-react" : "error",
|
|
||||||
"react/prefer-es6-class" : ["error", "never"],
|
|
||||||
"jest/valid-expect" : ["error", { maxArgs: 3 }],
|
|
||||||
|
|
||||||
/** Warnings **/
|
|
||||||
"max-lines" : ["warn", { max: 200, skipComments: true, skipBlankLines: true }],
|
|
||||||
"max-depth" : ["warn", { max: 4 }],
|
|
||||||
"max-params" : ["warn", { max: 5 }],
|
|
||||||
"no-restricted-syntax" : ["warn", "ClassDeclaration", "SwitchStatement"],
|
|
||||||
"no-unused-vars" : ["warn", { vars: "all", args: "none", varsIgnorePattern: "config|_|cx|createClass" }],
|
|
||||||
"react/jsx-uses-vars" : "warn",
|
|
||||||
|
|
||||||
/** Fixable **/
|
|
||||||
"arrow-parens" : ["warn", "always"],
|
|
||||||
"brace-style" : ["warn", "1tbs", { allowSingleLine: true }],
|
|
||||||
"jsx-quotes" : ["warn", "prefer-single"],
|
|
||||||
"no-var" : "warn",
|
|
||||||
"prefer-const" : "warn",
|
|
||||||
"prefer-template" : "warn",
|
|
||||||
"quotes" : ["warn", "single", { allowTemplateLiterals: true }],
|
|
||||||
"semi" : ["warn", "always"],
|
|
||||||
|
|
||||||
/** Whitespace **/
|
|
||||||
"array-bracket-spacing" : ["warn", "never"],
|
|
||||||
"arrow-spacing" : ["warn", { before: false, after: false }],
|
|
||||||
"comma-spacing" : ["warn", { before: false, after: true }],
|
|
||||||
"indent" : ["warn", "tab", { MemberExpression: "off" }],
|
|
||||||
"linebreak-style" : "off",
|
|
||||||
"no-trailing-spaces" : "warn",
|
|
||||||
"no-whitespace-before-property" : "warn",
|
|
||||||
"object-curly-spacing" : ["warn", "always"],
|
|
||||||
"react/jsx-indent-props" : ["warn", "tab"],
|
|
||||||
"space-in-parens" : ["warn", "never"],
|
|
||||||
"template-curly-spacing" : ["warn", "never"],
|
|
||||||
"keyword-spacing" : ["warn", {
|
|
||||||
before : true,
|
|
||||||
after : true,
|
|
||||||
overrides : { if: { before: false, after: false } }
|
|
||||||
}],
|
|
||||||
"key-spacing" : ["warn", {
|
|
||||||
multiLine : { beforeColon: true, afterColon: true, align: "colon" },
|
|
||||||
singleLine : { beforeColon: false, afterColon: true }
|
|
||||||
}]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
6684
package-lock.json
generated
100
package.json
@@ -1,32 +1,28 @@
|
|||||||
{
|
{
|
||||||
"name": "homebrewery",
|
"name": "homebrewery",
|
||||||
"description": "Create authentic looking D&D homebrews using only markdown",
|
"description": "Create authentic looking D&D homebrews using only markdown",
|
||||||
"version": "3.16.1",
|
"version": "3.13.1",
|
||||||
"type": "module",
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"npm": "^10.2.x",
|
"npm": "^10.2.x",
|
||||||
"node": "^20.18.x"
|
"node": "^20.8.x"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git://github.com/naturalcrit/homebrewery.git"
|
"url": "git://github.com/naturalcrit/homebrewery.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "node --experimental-require-module scripts/dev.js",
|
"dev": "node scripts/dev.js",
|
||||||
"quick": "node --experimental-require-module scripts/quick.js",
|
"quick": "node scripts/quick.js",
|
||||||
"build": "node --experimental-require-module scripts/buildHomebrew.js && node --experimental-require-module scripts/buildAdmin.js",
|
"build": "node scripts/buildHomebrew.js && node scripts/buildAdmin.js",
|
||||||
"builddev": "node --experimental-require-module scripts/buildHomebrew.js --dev",
|
"builddev": "node scripts/buildHomebrew.js --dev",
|
||||||
"lint": "eslint --fix",
|
"lint": "eslint --fix **/*.{js,jsx}",
|
||||||
"lint:dry": "eslint",
|
"lint:dry": "eslint **/*.{js,jsx}",
|
||||||
"stylelint": "stylelint --fix **/*.{less}",
|
"stylelint": "stylelint --fix **/*.{less}",
|
||||||
"stylelint:dry": "stylelint **/*.less",
|
"stylelint:dry": "stylelint **/*.less",
|
||||||
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
"circleci": "npm test && eslint **/*.{js,jsx} --max-warnings=0",
|
||||||
"verify": "npm run lint && npm test",
|
"verify": "npm run lint && npm test",
|
||||||
"test": "jest --runInBand",
|
"test": "jest --runInBand",
|
||||||
"test:api-unit": "jest \"server/.*.spec.js\" --verbose",
|
"test:api-unit": "jest server/*.spec.js --verbose",
|
||||||
"test:api-unit:themes": "jest \"server/.*.spec.js\" -t \"theme bundle\" --verbose",
|
|
||||||
"test:api-unit:css": "jest \"server/.*.spec.js\" -t \"Get CSS\" --verbose",
|
|
||||||
"test:api-unit:notifications": "jest \"server/.*.spec.js\" -t \"Notifications\" --verbose",
|
|
||||||
"test:coverage": "jest --coverage --silent --runInBand",
|
"test:coverage": "jest --coverage --silent --runInBand",
|
||||||
"test:dev": "jest --verbose --watch",
|
"test:dev": "jest --verbose --watch",
|
||||||
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
"test:basic": "jest tests/markdown/basic.test.js --verbose",
|
||||||
@@ -36,14 +32,12 @@
|
|||||||
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:block": "jest \".*(mustache-syntax).*\" -t '^Block:.*' --verbose --noStackTrace",
|
||||||
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
"test:mustache-syntax:injection": "jest \".*(mustache-syntax).*\" -t '^Injection:.*' --verbose --noStackTrace",
|
||||||
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
"test:definition-lists": "jest tests/markdown/definition-lists.test.js --verbose --noStackTrace",
|
||||||
"test:hard-breaks": "jest tests/markdown/hard-breaks.test.js --verbose --noStackTrace",
|
|
||||||
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
"test:emojis": "jest tests/markdown/emojis.test.js --verbose --noStackTrace",
|
||||||
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
"test:route": "jest tests/routes/static-pages.test.js --verbose",
|
||||||
"test:safehtml": "jest tests/html/safeHTML.test.js --verbose",
|
"phb": "node scripts/phb.js",
|
||||||
"phb": "node --experimental-require-module scripts/phb.js",
|
|
||||||
"prod": "set NODE_ENV=production && npm run build",
|
"prod": "set NODE_ENV=production && npm run build",
|
||||||
"postinstall": "npm run build",
|
"postinstall": "npm run build",
|
||||||
"start": "node --experimental-require-module server.js"
|
"start": "node server.js"
|
||||||
},
|
},
|
||||||
"author": "stolksdorf",
|
"author": "stolksdorf",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
@@ -57,23 +51,20 @@
|
|||||||
"shared",
|
"shared",
|
||||||
"server"
|
"server"
|
||||||
],
|
],
|
||||||
"transformIgnorePatterns": [
|
|
||||||
"node_modules/(?!nanoid/).*"
|
|
||||||
],
|
|
||||||
"coveragePathIgnorePatterns": [
|
"coveragePathIgnorePatterns": [
|
||||||
"build/*"
|
"build/*"
|
||||||
],
|
],
|
||||||
"coverageThreshold": {
|
"coverageThreshold": {
|
||||||
"global": {
|
"global": {
|
||||||
"statements": 50,
|
"statements": 25,
|
||||||
"branches": 40,
|
"branches": 10,
|
||||||
"functions": 40,
|
"functions": 22,
|
||||||
"lines": 50
|
"lines": 25
|
||||||
},
|
},
|
||||||
"server/homebrew.api.js": {
|
"server/homebrew.api.js": {
|
||||||
"statements": 70,
|
"statements": 65,
|
||||||
"branches": 50,
|
"branches": 50,
|
||||||
"functions": 65,
|
"functions": 60,
|
||||||
"lines": 70
|
"lines": 70
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -81,61 +72,66 @@
|
|||||||
"jest-expect-message"
|
"jest-expect-message"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"babel": {
|
||||||
|
"presets": [
|
||||||
|
"@babel/preset-env",
|
||||||
|
"@babel/preset-react"
|
||||||
|
],
|
||||||
|
"plugins": [
|
||||||
|
"@babel/plugin-transform-runtime"
|
||||||
|
]
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@babel/core": "^7.26.0",
|
"@babel/core": "^7.24.7",
|
||||||
"@babel/plugin-transform-runtime": "^7.25.9",
|
"@babel/plugin-transform-runtime": "^7.24.7",
|
||||||
"@babel/preset-env": "^7.26.0",
|
"@babel/preset-env": "^7.24.7",
|
||||||
"@babel/preset-react": "^7.25.9",
|
"@babel/preset-react": "^7.24.7",
|
||||||
"@googleapis/drive": "^8.14.0",
|
"@googleapis/drive": "^8.11.0",
|
||||||
"body-parser": "^1.20.2",
|
"body-parser": "^1.20.2",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"codemirror": "^5.65.6",
|
"codemirror": "^5.65.6",
|
||||||
"cookie-parser": "^1.4.7",
|
"cookie-parser": "^1.4.6",
|
||||||
"create-react-class": "^15.7.0",
|
"create-react-class": "^15.7.0",
|
||||||
"dedent-tabs": "^0.10.3",
|
"dedent-tabs": "^0.10.3",
|
||||||
"dompurify": "^3.2.1",
|
"dompurify": "^3.1.5",
|
||||||
"expr-eval": "^2.0.2",
|
"expr-eval": "^2.0.2",
|
||||||
"express": "^4.21.1",
|
"express": "^4.19.2",
|
||||||
"express-async-handler": "^1.2.0",
|
"express-async-handler": "^1.2.0",
|
||||||
"express-static-gzip": "2.2.0",
|
"express-static-gzip": "2.1.7",
|
||||||
"fs-extra": "11.2.0",
|
"fs-extra": "11.2.0",
|
||||||
"idb-keyval": "^6.2.1",
|
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"jwt-simple": "^0.5.6",
|
"jwt-simple": "^0.5.6",
|
||||||
"less": "^3.13.1",
|
"less": "^3.13.1",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
"marked": "11.2.0",
|
"marked": "11.2.0",
|
||||||
"marked-emoji": "^1.4.3",
|
"marked-emoji": "^1.4.1",
|
||||||
"marked-extended-tables": "^1.0.10",
|
"marked-extended-tables": "^1.0.8",
|
||||||
"marked-gfm-heading-id": "^3.2.0",
|
"marked-gfm-heading-id": "^3.2.0",
|
||||||
"marked-smartypants-lite": "^1.0.2",
|
"marked-smartypants-lite": "^1.0.2",
|
||||||
"markedLegacy": "npm:marked@^0.3.19",
|
"markedLegacy": "npm:marked@^0.3.19",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"mongoose": "^8.8.2",
|
"mongoose": "^8.4.5",
|
||||||
"nanoid": "5.0.8",
|
"nanoid": "3.3.4",
|
||||||
"nconf": "^0.12.1",
|
"nconf": "^0.12.1",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
"react-frame-component": "^4.1.3",
|
"react-frame-component": "^4.1.3",
|
||||||
"react-router-dom": "6.28.0",
|
"react-router-dom": "6.24.1",
|
||||||
"sanitize-filename": "1.6.3",
|
"sanitize-filename": "1.6.3",
|
||||||
"superagent": "^10.1.1",
|
"superagent": "^9.0.2",
|
||||||
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
"vitreum": "git+https://git@github.com/calculuschild/vitreum.git"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@stylistic/stylelint-plugin": "^3.1.1",
|
"eslint": "^8.57.0",
|
||||||
"babel-plugin-transform-import-meta": "^2.2.1",
|
"eslint-plugin-jest": "^28.6.0",
|
||||||
"eslint": "^9.15.0",
|
"eslint-plugin-react": "^7.34.3",
|
||||||
"eslint-plugin-jest": "^28.9.0",
|
|
||||||
"eslint-plugin-react": "^7.37.2",
|
|
||||||
"globals": "^15.12.0",
|
|
||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"jest-expect-message": "^1.1.3",
|
"jest-expect-message": "^1.1.3",
|
||||||
"jsdom-global": "^3.0.2",
|
|
||||||
"postcss-less": "^6.0.0",
|
"postcss-less": "^6.0.0",
|
||||||
"stylelint": "^16.10.0",
|
"stylelint": "^15.11.0",
|
||||||
"stylelint-config-recess-order": "^5.1.1",
|
"stylelint-config-recess-order": "^4.6.0",
|
||||||
"stylelint-config-recommended": "^14.0.1",
|
"stylelint-config-recommended": "^13.0.0",
|
||||||
|
"stylelint-stylistic": "^0.4.3",
|
||||||
"supertest": "^7.0.0"
|
"supertest": "^7.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
|
const fs = require('fs-extra');
|
||||||
|
const Proj = require('./project.json');
|
||||||
|
|
||||||
import fs from 'fs-extra';
|
const { pack } = require('vitreum');
|
||||||
import Proj from './project.json' with { type: 'json' };
|
|
||||||
import vitreum from 'vitreum';
|
|
||||||
const { pack } = vitreum;
|
|
||||||
|
|
||||||
import lessTransform from 'vitreum/transforms/less.js';
|
|
||||||
import assetTransform from 'vitreum/transforms/asset.js';
|
|
||||||
|
|
||||||
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||||
|
|
||||||
|
const lessTransform = require('vitreum/transforms/less.js');
|
||||||
|
const assetTransform = require('vitreum/transforms/asset.js');
|
||||||
|
//const Meta = require('vitreum/headtags');
|
||||||
|
|
||||||
const transforms = {
|
const transforms = {
|
||||||
'.less' : lessTransform,
|
'.less' : lessTransform,
|
||||||
'*' : assetTransform('./build')
|
'*' : assetTransform('./build')
|
||||||
@@ -18,7 +17,7 @@ const build = async ({ bundle, render, ssr })=>{
|
|||||||
const css = await lessTransform.generate({ paths: './shared' });
|
const css = await lessTransform.generate({ paths: './shared' });
|
||||||
await fs.outputFile('./build/admin/bundle.css', css);
|
await fs.outputFile('./build/admin/bundle.css', css);
|
||||||
await fs.outputFile('./build/admin/bundle.js', bundle);
|
await fs.outputFile('./build/admin/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/admin/ssr.cjs', ssr);
|
await fs.outputFile('./build/admin/ssr.js', ssr);
|
||||||
};
|
};
|
||||||
|
|
||||||
fs.emptyDirSync('./build/admin');
|
fs.emptyDirSync('./build/admin');
|
||||||
|
|||||||
@@ -1,15 +1,14 @@
|
|||||||
import fs from 'fs-extra';
|
const fs = require('fs-extra');
|
||||||
import zlib from 'zlib';
|
const zlib = require('zlib');
|
||||||
import Proj from './project.json' with { type: 'json' };
|
const Proj = require('./project.json');
|
||||||
import vitreum from 'vitreum';
|
|
||||||
const { pack, watchFile, livereload } = vitreum;
|
|
||||||
|
|
||||||
import lessTransform from 'vitreum/transforms/less.js';
|
const { pack, watchFile, livereload } = require('vitreum');
|
||||||
import assetTransform from 'vitreum/transforms/asset.js';
|
const isDev = !!process.argv.find((arg)=>arg=='--dev');
|
||||||
import babel from '@babel/core';
|
|
||||||
import less from 'less';
|
|
||||||
|
|
||||||
const isDev = !!process.argv.find((arg) => arg === '--dev');
|
const lessTransform = require('vitreum/transforms/less.js');
|
||||||
|
const assetTransform = require('vitreum/transforms/asset.js');
|
||||||
|
const babel = require('@babel/core');
|
||||||
|
const less = require('less');
|
||||||
|
|
||||||
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
const babelify = async (code)=>(await babel.transformAsync(code, { presets: [['@babel/preset-env', { 'exclude': ['proposal-dynamic-import'] }], '@babel/preset-react'], plugins: ['@babel/plugin-transform-runtime'] })).code;
|
||||||
|
|
||||||
@@ -25,7 +24,7 @@ const build = async ({ bundle, render, ssr })=>{
|
|||||||
//css = `@layer bundle {\n${css}\n}`;
|
//css = `@layer bundle {\n${css}\n}`;
|
||||||
await fs.outputFile('./build/homebrew/bundle.css', css);
|
await fs.outputFile('./build/homebrew/bundle.css', css);
|
||||||
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
await fs.outputFile('./build/homebrew/bundle.js', bundle);
|
||||||
await fs.outputFile('./build/homebrew/ssr.cjs', ssr);
|
await fs.outputFile('./build/homebrew/ssr.js', ssr);
|
||||||
|
|
||||||
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
|
await fs.copy('./client/homebrew/favicon.ico', './build/assets/favicon.ico');
|
||||||
|
|
||||||
@@ -52,7 +51,7 @@ fs.emptyDirSync('./build');
|
|||||||
const themes = { Legacy: {}, V3: {} };
|
const themes = { Legacy: {}, V3: {} };
|
||||||
|
|
||||||
let themeFiles = fs.readdirSync('./themes/Legacy');
|
let themeFiles = fs.readdirSync('./themes/Legacy');
|
||||||
for (let dir of themeFiles) {
|
for (dir of themeFiles) {
|
||||||
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`).toString());
|
||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.Legacy[dir] = (themeData);
|
themes.Legacy[dir] = (themeData);
|
||||||
@@ -69,7 +68,7 @@ fs.emptyDirSync('./build');
|
|||||||
}
|
}
|
||||||
|
|
||||||
themeFiles = fs.readdirSync('./themes/V3');
|
themeFiles = fs.readdirSync('./themes/V3');
|
||||||
for (let dir of themeFiles) {
|
for (dir of themeFiles) {
|
||||||
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`).toString());
|
||||||
themeData.path = dir;
|
themeData.path = dir;
|
||||||
themes.V3[dir] = (themeData);
|
themes.V3[dir] = (themeData);
|
||||||
@@ -105,14 +104,14 @@ fs.emptyDirSync('./build');
|
|||||||
const editorThemesBuildDir = './build/homebrew/cm-themes';
|
const editorThemesBuildDir = './build/homebrew/cm-themes';
|
||||||
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
|
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
|
||||||
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
|
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
|
||||||
const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
|
editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
|
||||||
|
|
||||||
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
const editorThemeFile = './themes/codeMirror/editorThemes.json';
|
||||||
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
if(fs.existsSync(editorThemeFile)) fs.rmSync(editorThemeFile);
|
||||||
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
const stream = fs.createWriteStream(editorThemeFile, { flags: 'a' });
|
||||||
stream.write('[\n"default"');
|
stream.write('[\n"default"');
|
||||||
|
|
||||||
for (let themeFile of editorThemeFiles) {
|
for (themeFile of editorThemeFiles) {
|
||||||
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
stream.write(`,\n"${themeFile.slice(0, -4)}"`);
|
||||||
}
|
}
|
||||||
stream.write('\n]\n');
|
stream.write('\n]\n');
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import DB from './server/db.js';
|
const DB = require('./server/db.js');
|
||||||
import server from './server/app.js';
|
const server = require('./server/app.js');
|
||||||
import config from './server/config.js';
|
const config = require('./server/config.js');
|
||||||
|
|
||||||
DB.connect(config).then(()=>{
|
DB.connect(config).then(()=>{
|
||||||
// Ensure that we have successfully connected to the database
|
// Ensure that we have successfully connected to the database
|
||||||
// before launching server
|
// before launching server
|
||||||
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
const PORT = process.env.PORT || config.get('web_port') || 8000;
|
||||||
server.listen(PORT, ()=>{
|
server.app.listen(PORT, ()=>{
|
||||||
const reset = '\x1b[0m'; // Reset to default style
|
const reset = '\x1b[0m'; // Reset to default style
|
||||||
const bright = '\x1b[1m'; // Bright (bold) style
|
const bright = '\x1b[1m'; // Bright (bold) style
|
||||||
const cyan = '\x1b[36m'; // Cyan color
|
const cyan = '\x1b[36m'; // Cyan color
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
import {model as NotificationModel } from './notifications.model.js';
|
const router = require('express').Router();
|
||||||
import express from 'express';
|
const Moment = require('moment');
|
||||||
import Moment from 'moment';
|
//const render = require('vitreum/steps/render');
|
||||||
import zlib from 'zlib';
|
const templateFn = require('../client/template.js');
|
||||||
import templateFn from '../client/template.js';
|
const zlib = require('zlib');
|
||||||
|
|
||||||
import HomebrewAPI from './homebrew.api.js';
|
|
||||||
import asyncHandler from 'express-async-handler';
|
|
||||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
|
||||||
|
|
||||||
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
process.env.ADMIN_USER = process.env.ADMIN_USER || 'admin';
|
||||||
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
|
||||||
@@ -28,7 +22,7 @@ const mw = {
|
|||||||
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
|
||||||
return next();
|
return next();
|
||||||
}
|
}
|
||||||
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
|
return res.status(401).send('Access denied');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,8 +66,23 @@ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
/* Searches for matching edit or share id, also attempts to partial match */
|
/* Searches for matching edit or share id, also attempts to partial match */
|
||||||
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
|
router.get('/admin/lookup/:id', mw.adminOnly, async (req, res, next)=>{
|
||||||
return res.json(req.brew);
|
HomebrewModel.findOne({
|
||||||
|
$or : [
|
||||||
|
{ editId: { $regex: req.params.id, $options: 'i' } },
|
||||||
|
{ shareId: { $regex: req.params.id, $options: 'i' } },
|
||||||
|
]
|
||||||
|
}).exec()
|
||||||
|
.then((brew)=>{
|
||||||
|
if(!brew) // No document found
|
||||||
|
return res.status(404).json({ error: 'Document not found' });
|
||||||
|
else
|
||||||
|
return res.json(brew);
|
||||||
|
})
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
return res.status(500).json({ error: 'Internal Server Error' });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Find 50 brews that aren't compressed yet */
|
/* Find 50 brews that aren't compressed yet */
|
||||||
@@ -91,25 +100,6 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
/* Cleans `<script` and `</script>` from the "text" field of a brew */
|
|
||||||
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
|
|
||||||
console.log(`[ADMIN] Cleaning script tags from ShareID ${req.params.id}`);
|
|
||||||
|
|
||||||
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
|
|
||||||
|
|
||||||
const brew = req.brew;
|
|
||||||
|
|
||||||
const properties = ['text', 'description', 'title'];
|
|
||||||
properties.forEach((property)=>{
|
|
||||||
brew[property] = cleanText(brew[property]);
|
|
||||||
});
|
|
||||||
|
|
||||||
splitTextStyleAndMetadata(brew);
|
|
||||||
|
|
||||||
req.body = brew;
|
|
||||||
|
|
||||||
return await HomebrewAPI.updateBrew(req, res);
|
|
||||||
});
|
|
||||||
|
|
||||||
/* Compresses the "text" field of a brew to binary */
|
/* Compresses the "text" field of a brew to binary */
|
||||||
router.put('/admin/compress/:id', (req, res)=>{
|
router.put('/admin/compress/:id', (req, res)=>{
|
||||||
@@ -148,48 +138,12 @@ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// ####################### NOTIFICATIONS
|
|
||||||
|
|
||||||
router.get('/admin/notification/all', async (req, res, next)=>{
|
|
||||||
try {
|
|
||||||
const notifications = await NotificationModel.getAll();
|
|
||||||
return res.json(notifications);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error getting all notifications: ', error.message);
|
|
||||||
return res.status(500).json({ message: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
|
|
||||||
try {
|
|
||||||
const notification = await NotificationModel.addNotification(req.body);
|
|
||||||
return res.status(201).json(notification);
|
|
||||||
} catch (error) {
|
|
||||||
console.log('Error adding notification: ', error.message);
|
|
||||||
return res.status(500).json({ message: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
|
|
||||||
try {
|
|
||||||
const notification = await NotificationModel.deleteNotification(req.params.id);
|
|
||||||
return res.json(notification);
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
|
|
||||||
return res.status(500).json({ message: error.message });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
router.get('/admin', mw.adminOnly, (req, res)=>{
|
router.get('/admin', mw.adminOnly, (req, res)=>{
|
||||||
templateFn('admin', {
|
templateFn('admin', {
|
||||||
url : req.originalUrl
|
url : req.originalUrl
|
||||||
})
|
})
|
||||||
.then((page)=>res.send(page))
|
.then((page)=>res.send(page))
|
||||||
.catch((err)=>{
|
.catch((err)=>res.sendStatus(500));
|
||||||
console.log(err);
|
|
||||||
res.sendStatus(500);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,117 +0,0 @@
|
|||||||
import supertest from 'supertest';
|
|
||||||
import HBApp from './app.js';
|
|
||||||
import {model as NotificationModel } from './notifications.model.js';
|
|
||||||
|
|
||||||
|
|
||||||
// Mimic https responses to avoid being redirected all the time
|
|
||||||
const app = supertest.agent(HBApp).set('X-Forwarded-Proto', 'https');
|
|
||||||
|
|
||||||
describe('Tests for admin api', ()=>{
|
|
||||||
afterEach(()=>{
|
|
||||||
jest.resetAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Notifications', ()=>{
|
|
||||||
it('should return list of all notifications', async ()=>{
|
|
||||||
const testNotifications = ['a', 'b'];
|
|
||||||
|
|
||||||
jest.spyOn(NotificationModel, 'find')
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return { exec: jest.fn().mockResolvedValue(testNotifications) };
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await app
|
|
||||||
.get('/admin/notification/all')
|
|
||||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
|
||||||
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual(testNotifications);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add a new notification', async ()=>{
|
|
||||||
const inputNotification = {
|
|
||||||
title : 'Test Notification',
|
|
||||||
text : 'This is a test notification',
|
|
||||||
startAt : new Date().toISOString(),
|
|
||||||
stopAt : new Date().toISOString(),
|
|
||||||
dismissKey : 'testKey'
|
|
||||||
};
|
|
||||||
|
|
||||||
const savedNotification = {
|
|
||||||
...inputNotification,
|
|
||||||
_id : expect.any(String),
|
|
||||||
createdAt : expect.any(String),
|
|
||||||
startAt : inputNotification.startAt,
|
|
||||||
stopAt : inputNotification.stopAt,
|
|
||||||
};
|
|
||||||
|
|
||||||
jest.spyOn(NotificationModel.prototype, 'save')
|
|
||||||
.mockImplementationOnce(function() {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await app
|
|
||||||
.post('/admin/notification/add')
|
|
||||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`)
|
|
||||||
.send(inputNotification);
|
|
||||||
|
|
||||||
expect(response.status).toBe(201);
|
|
||||||
expect(response.body).toEqual(savedNotification);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle error adding a notification without dismissKey', async () => {
|
|
||||||
const inputNotification = {
|
|
||||||
title : 'Test Notification',
|
|
||||||
text : 'This is a test notification',
|
|
||||||
startAt : new Date().toISOString(),
|
|
||||||
stopAt : new Date().toISOString()
|
|
||||||
};
|
|
||||||
|
|
||||||
//Change 'save' function to just return itself instead of actually interacting with the database
|
|
||||||
jest.spyOn(NotificationModel.prototype, 'save')
|
|
||||||
.mockImplementationOnce(function() {
|
|
||||||
return Promise.resolve(this);
|
|
||||||
});
|
|
||||||
|
|
||||||
const response = await app
|
|
||||||
.post('/admin/notification/add')
|
|
||||||
.set('Authorization', 'Basic ' + Buffer.from('admin:password3').toString('base64'))
|
|
||||||
.send(inputNotification);
|
|
||||||
|
|
||||||
expect(response.status).toBe(500);
|
|
||||||
expect(response.body).toEqual({ message: 'Dismiss key is required!' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should delete a notification based on its dismiss key', async ()=>{
|
|
||||||
const dismissKey = 'testKey';
|
|
||||||
|
|
||||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
|
||||||
.mockImplementationOnce((key) => {
|
|
||||||
return { exec: jest.fn().mockResolvedValue(key) };
|
|
||||||
});
|
|
||||||
const response = await app
|
|
||||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
|
||||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
|
||||||
|
|
||||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
|
||||||
expect(response.status).toBe(200);
|
|
||||||
expect(response.body).toEqual({ dismissKey: 'testKey' });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should handle error deleting a notification that doesnt exist', async ()=>{
|
|
||||||
const dismissKey = 'testKey';
|
|
||||||
|
|
||||||
jest.spyOn(NotificationModel, 'findOneAndDelete')
|
|
||||||
.mockImplementationOnce(() => {
|
|
||||||
return { exec: jest.fn().mockResolvedValue() };
|
|
||||||
});
|
|
||||||
const response = await app
|
|
||||||
.delete(`/admin/notification/delete/${dismissKey}`)
|
|
||||||
.set('Authorization', `Basic ${Buffer.from('admin:password3').toString('base64')}`);
|
|
||||||
|
|
||||||
expect(NotificationModel.findOneAndDelete).toHaveBeenCalledWith({'dismissKey': 'testKey'});
|
|
||||||
expect(response.status).toBe(500);
|
|
||||||
expect(response.body).toEqual({ message: 'Notification not found' });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
211
server/app.js
@@ -1,41 +1,23 @@
|
|||||||
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
/*eslint max-lines: ["warn", {"max": 500, "skipBlankLines": true, "skipComments": true}]*/
|
||||||
// Set working directory to project root
|
// Set working directory to project root
|
||||||
import { dirname } from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import packageJSON from './../package.json' with { type: "json" };
|
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
||||||
process.chdir(`${__dirname}/..`);
|
process.chdir(`${__dirname}/..`);
|
||||||
const version = packageJSON.version;
|
|
||||||
|
|
||||||
import _ from 'lodash';
|
|
||||||
import jwt from 'jwt-simple';
|
|
||||||
import express from 'express';
|
|
||||||
import yaml from 'js-yaml';
|
|
||||||
import config from './config.js';
|
|
||||||
import fs from 'fs-extra';
|
|
||||||
|
|
||||||
|
const _ = require('lodash');
|
||||||
|
const jwt = require('jwt-simple');
|
||||||
|
const express = require('express');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
const app = express();
|
const app = express();
|
||||||
|
const config = require('./config.js');
|
||||||
|
|
||||||
import api from './homebrew.api.js';
|
const { homebrewApi, getBrew } = require('./homebrew.api.js');
|
||||||
const { homebrewApi, getBrew, getUsersBrewThemes, getCSS } = api;
|
const GoogleActions = require('./googleActions.js');
|
||||||
import adminApi from './admin.api.js';
|
const serveCompressedStaticAssets = require('./static-assets.mv.js');
|
||||||
import vaultApi from './vault.api.js';
|
const sanitizeFilename = require('sanitize-filename');
|
||||||
import GoogleActions from './googleActions.js';
|
const asyncHandler = require('express-async-handler');
|
||||||
import serveCompressedStaticAssets from './static-assets.mv.js';
|
|
||||||
import sanitizeFilename from 'sanitize-filename';
|
|
||||||
import asyncHandler from 'express-async-handler';
|
|
||||||
import templateFn from '../client/template.js';
|
|
||||||
import {model as HomebrewModel } from './homebrew.model.js';
|
|
||||||
|
|
||||||
import { DEFAULT_BREW } from './brewDefaults.js';
|
const { DEFAULT_BREW } = require('./brewDefaults.js');
|
||||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
|
||||||
|
|
||||||
//==== Middleware Imports ====//
|
const { splitTextStyleAndMetadata } = require('../shared/helpers.js');
|
||||||
import contentNegotiation from './middleware/content-negotiation.js';
|
|
||||||
import bodyParser from 'body-parser';
|
|
||||||
import cookieParser from 'cookie-parser';
|
|
||||||
import forceSSL from './forcessl.mw.js';
|
|
||||||
|
|
||||||
|
|
||||||
const sanitizeBrew = (brew, accessType)=>{
|
const sanitizeBrew = (brew, accessType)=>{
|
||||||
@@ -47,13 +29,11 @@ const sanitizeBrew = (brew, accessType)=>{
|
|||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
app.set('trust proxy', 1 /* number of proxies between user and server */)
|
|
||||||
|
|
||||||
app.use('/', serveCompressedStaticAssets(`build`));
|
app.use('/', serveCompressedStaticAssets(`build`));
|
||||||
app.use(contentNegotiation);
|
app.use(require('./middleware/content-negotiation.js'));
|
||||||
app.use(bodyParser.json({ limit: '25mb' }));
|
app.use(require('body-parser').json({ limit: '25mb' }));
|
||||||
app.use(cookieParser());
|
app.use(require('cookie-parser')());
|
||||||
app.use(forceSSL);
|
app.use(require('./forcessl.mw.js'));
|
||||||
|
|
||||||
//Account Middleware
|
//Account Middleware
|
||||||
app.use((req, res, next)=>{
|
app.use((req, res, next)=>{
|
||||||
@@ -73,14 +53,14 @@ app.use((req, res, next)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.use(homebrewApi);
|
app.use(homebrewApi);
|
||||||
app.use(adminApi);
|
app.use(require('./admin.api.js'));
|
||||||
app.use(vaultApi);
|
|
||||||
|
|
||||||
const welcomeText = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
const welcomeTextLegacy = fs.readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
const welcomeText = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg.md', 'utf8');
|
||||||
const migrateText = fs.readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
const welcomeTextLegacy = require('fs').readFileSync('client/homebrew/pages/homePage/welcome_msg_legacy.md', 'utf8');
|
||||||
const changelogText = fs.readFileSync('changelog.md', 'utf8');
|
const migrateText = require('fs').readFileSync('client/homebrew/pages/homePage/migrate.md', 'utf8');
|
||||||
const faqText = fs.readFileSync('faq.md', 'utf8');
|
const changelogText = require('fs').readFileSync('changelog.md', 'utf8');
|
||||||
|
const faqText = require('fs').readFileSync('faq.md', 'utf8');
|
||||||
|
|
||||||
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
String.prototype.replaceAll = function(s, r){return this.split(s).join(r);};
|
||||||
|
|
||||||
@@ -101,8 +81,7 @@ app.get('/robots.txt', (req, res)=>{
|
|||||||
app.get('/', (req, res, next)=>{
|
app.get('/', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeText,
|
text : welcomeText,
|
||||||
renderer : 'V3',
|
renderer : 'V3'
|
||||||
theme : '5ePHB'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -118,8 +97,7 @@ app.get('/', (req, res, next)=>{
|
|||||||
app.get('/legacy', (req, res, next)=>{
|
app.get('/legacy', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : welcomeTextLegacy,
|
text : welcomeTextLegacy,
|
||||||
renderer : 'legacy',
|
renderer : 'legacy'
|
||||||
theme : '5ePHB'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -135,8 +113,7 @@ app.get('/legacy', (req, res, next)=>{
|
|||||||
app.get('/migrate', (req, res, next)=>{
|
app.get('/migrate', (req, res, next)=>{
|
||||||
req.brew = {
|
req.brew = {
|
||||||
text : migrateText,
|
text : migrateText,
|
||||||
renderer : 'V3',
|
renderer : 'V3'
|
||||||
theme : '5ePHB'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -153,8 +130,7 @@ app.get('/changelog', async (req, res, next)=>{
|
|||||||
req.brew = {
|
req.brew = {
|
||||||
title : 'Changelog',
|
title : 'Changelog',
|
||||||
text : changelogText,
|
text : changelogText,
|
||||||
renderer : 'V3',
|
renderer : 'V3'
|
||||||
theme : '5ePHB'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -171,8 +147,7 @@ app.get('/faq', async (req, res, next)=>{
|
|||||||
req.brew = {
|
req.brew = {
|
||||||
title : 'FAQ',
|
title : 'FAQ',
|
||||||
text : faqText,
|
text : faqText,
|
||||||
renderer : 'V3',
|
renderer : 'V3'
|
||||||
theme : '5ePHB'
|
|
||||||
},
|
},
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
@@ -220,26 +195,6 @@ app.get('/download/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|||||||
res.status(200).send(brew.text);
|
res.status(200).send(brew.text);
|
||||||
});
|
});
|
||||||
|
|
||||||
//Serve brew metadata
|
|
||||||
app.get('/metadata/:id', asyncHandler(getBrew('share')), (req, res)=>{
|
|
||||||
const { brew } = req;
|
|
||||||
sanitizeBrew(brew, 'share');
|
|
||||||
|
|
||||||
const fields = ['title', 'pageCount', 'description', 'authors', 'lang',
|
|
||||||
'published', 'views', 'shareId', 'createdAt', 'updatedAt',
|
|
||||||
'lastViewed', 'thumbnail', 'tags'
|
|
||||||
];
|
|
||||||
|
|
||||||
const metadata = fields.reduce((acc, field)=>{
|
|
||||||
if(brew[field] !== undefined) acc[field] = brew[field];
|
|
||||||
return acc;
|
|
||||||
}, {});
|
|
||||||
res.status(200).json(metadata);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Serve brew styling
|
|
||||||
app.get('/css/:id', asyncHandler(getBrew('share')), (req, res)=>{getCSS(req, res);});
|
|
||||||
|
|
||||||
//User Page
|
//User Page
|
||||||
app.get('/user/:username', async (req, res, next)=>{
|
app.get('/user/:username', async (req, res, next)=>{
|
||||||
const ownAccount = req.account && (req.account.username == req.params.username);
|
const ownAccount = req.account && (req.account.username == req.params.username);
|
||||||
@@ -273,8 +228,6 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.log(err);
|
console.log(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
brews.forEach(brew => brew.stubbed = true); //All brews from MongoDB are "stubbed"
|
|
||||||
|
|
||||||
if(ownAccount && req?.account?.googleId){
|
if(ownAccount && req?.account?.googleId){
|
||||||
const auth = await GoogleActions.authCheck(req.account, res);
|
const auth = await GoogleActions.authCheck(req.account, res);
|
||||||
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
let googleBrews = await GoogleActions.listGoogleBrews(auth)
|
||||||
@@ -282,12 +235,12 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
console.error(err);
|
console.error(err);
|
||||||
});
|
});
|
||||||
|
|
||||||
// If stub matches file from Google, use Google metadata over stub metadata
|
|
||||||
if(googleBrews && googleBrews.length > 0) {
|
if(googleBrews && googleBrews.length > 0) {
|
||||||
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
for (const brew of brews.filter((brew)=>brew.googleId)) {
|
||||||
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
const match = googleBrews.findIndex((b)=>b.editId === brew.editId);
|
||||||
if(match !== -1) {
|
if(match !== -1) {
|
||||||
brew.googleId = googleBrews[match].googleId;
|
brew.googleId = googleBrews[match].googleId;
|
||||||
|
brew.stubbed = true;
|
||||||
brew.pageCount = googleBrews[match].pageCount;
|
brew.pageCount = googleBrews[match].pageCount;
|
||||||
brew.renderer = googleBrews[match].renderer;
|
brew.renderer = googleBrews[match].renderer;
|
||||||
brew.version = googleBrews[match].version;
|
brew.version = googleBrews[match].version;
|
||||||
@@ -296,7 +249,6 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Remaining unstubbed google brews display current user as author
|
|
||||||
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
googleBrews = googleBrews.map((brew)=>({ ...brew, authors: [req.account.username] }));
|
||||||
brews = _.concat(brews, googleBrews);
|
brews = _.concat(brews, googleBrews);
|
||||||
}
|
}
|
||||||
@@ -313,11 +265,9 @@ app.get('/user/:username', async (req, res, next)=>{
|
|||||||
});
|
});
|
||||||
|
|
||||||
//Edit Page
|
//Edit Page
|
||||||
app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res, next)=>{
|
app.get('/edit/:id', asyncHandler(getBrew('edit')), (req, res, next)=>{
|
||||||
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
req.brew = req.brew.toObject ? req.brew.toObject() : req.brew;
|
||||||
|
|
||||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : req.brew.title || 'Untitled Brew',
|
title : req.brew.title || 'Untitled Brew',
|
||||||
description : req.brew.description || 'No description.',
|
description : req.brew.description || 'No description.',
|
||||||
@@ -329,10 +279,10 @@ app.get('/edit/:id', asyncHandler(getBrew('edit')), asyncHandler(async(req, res,
|
|||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
res.header('Cache-Control', 'no-cache, no-store'); //reload the latest saved brew when pressing back button, not the cached version before save.
|
||||||
return next();
|
return next();
|
||||||
}));
|
});
|
||||||
|
|
||||||
//New Page from ID
|
//New Page
|
||||||
app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res, next)=>{
|
app.get('/new/:id', asyncHandler(getBrew('share')), (req, res, next)=>{
|
||||||
sanitizeBrew(req.brew, 'share');
|
sanitizeBrew(req.brew, 'share');
|
||||||
splitTextStyleAndMetadata(req.brew);
|
splitTextStyleAndMetadata(req.brew);
|
||||||
const brew = {
|
const brew = {
|
||||||
@@ -342,31 +292,17 @@ app.get('/new/:id', asyncHandler(getBrew('share')), asyncHandler(async(req, res,
|
|||||||
style : req.brew.style,
|
style : req.brew.style,
|
||||||
renderer : req.brew.renderer,
|
renderer : req.brew.renderer,
|
||||||
theme : req.brew.theme,
|
theme : req.brew.theme,
|
||||||
tags : req.brew.tags,
|
tags : req.brew.tags
|
||||||
};
|
};
|
||||||
req.brew = _.defaults(brew, DEFAULT_BREW);
|
req.brew = _.defaults(brew, DEFAULT_BREW);
|
||||||
|
|
||||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
req.ogMeta = { ...defaultMetaTags,
|
||||||
title : 'New',
|
title : 'New',
|
||||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
description : 'Start crafting your homebrew on the Homebrewery!'
|
||||||
};
|
};
|
||||||
|
|
||||||
return next();
|
return next();
|
||||||
}));
|
});
|
||||||
|
|
||||||
//New Page
|
|
||||||
app.get('/new', asyncHandler(async(req, res, next)=>{
|
|
||||||
req.userThemes = await(getUsersBrewThemes(req.account?.username));
|
|
||||||
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
|
||||||
title : 'New',
|
|
||||||
description : 'Start crafting your homebrew on the Homebrewery!'
|
|
||||||
};
|
|
||||||
|
|
||||||
return next();
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Share Page
|
//Share Page
|
||||||
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
app.get('/share/:id', asyncHandler(getBrew('share')), asyncHandler(async (req, res, next)=>{
|
||||||
@@ -400,25 +336,26 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
const data = {};
|
const data = {};
|
||||||
data.title = 'Account Information Page';
|
data.title = 'Account Information Page';
|
||||||
|
|
||||||
if(!req.account) {
|
|
||||||
res.set('WWW-Authenticate', 'Bearer realm="Authorization Required"');
|
|
||||||
const error = new Error('No valid account');
|
|
||||||
error.status = 401;
|
|
||||||
error.HBErrorCode = '50';
|
|
||||||
error.page = data.title;
|
|
||||||
return next(error);
|
|
||||||
};
|
|
||||||
|
|
||||||
let auth;
|
let auth;
|
||||||
let googleCount = [];
|
let googleCount = [];
|
||||||
if(req.account) {
|
if(req.account) {
|
||||||
if(req.account.googleId) {
|
if(req.account.googleId) {
|
||||||
auth = await GoogleActions.authCheck(req.account, res, false)
|
try {
|
||||||
|
auth = await GoogleActions.authCheck(req.account, res, false);
|
||||||
googleCount = await GoogleActions.listGoogleBrews(auth)
|
} catch (e) {
|
||||||
.catch((err)=>{
|
auth = undefined;
|
||||||
console.error(err);
|
console.log('Google auth check failed!');
|
||||||
});
|
console.log(e);
|
||||||
|
}
|
||||||
|
if(auth.credentials.access_token) {
|
||||||
|
try {
|
||||||
|
googleCount = await GoogleActions.listGoogleBrews(auth);
|
||||||
|
} catch (e) {
|
||||||
|
googleCount = undefined;
|
||||||
|
console.log('List Google files failed!');
|
||||||
|
console.log(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const query = { authors: req.account.username, googleId: { $exists: false } };
|
const query = { authors: req.account.username, googleId: { $exists: false } };
|
||||||
@@ -432,7 +369,7 @@ app.get('/account', asyncHandler(async (req, res, next)=>{
|
|||||||
username : req.account.username,
|
username : req.account.username,
|
||||||
issued : req.account.issued,
|
issued : req.account.issued,
|
||||||
googleId : Boolean(req.account.googleId),
|
googleId : Boolean(req.account.googleId),
|
||||||
authCheck : Boolean(req.account.googleId && auth?.credentials.access_token),
|
authCheck : Boolean(req.account.googleId && auth.credentials.access_token),
|
||||||
mongoCount : mongoCount,
|
mongoCount : mongoCount,
|
||||||
googleCount : googleCount?.length
|
googleCount : googleCount?.length
|
||||||
};
|
};
|
||||||
@@ -462,39 +399,17 @@ if(isLocalEnvironment){
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add Static Local Paths
|
|
||||||
app.use('/staticImages', express.static(config.get('hb_images') && fs.existsSync(config.get('hb_images')) ? config.get('hb_images') :'staticImages'));
|
|
||||||
app.use('/staticFonts', express.static(config.get('hb_fonts') && fs.existsSync(config.get('hb_fonts')) ? config.get('hb_fonts'):'staticFonts'));
|
|
||||||
|
|
||||||
//Vault Page
|
|
||||||
app.get('/vault', asyncHandler(async(req, res, next)=>{
|
|
||||||
req.ogMeta = { ...defaultMetaTags,
|
|
||||||
title : 'The Vault',
|
|
||||||
description : 'Search for Brews'
|
|
||||||
};
|
|
||||||
return next();
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Send rendered page
|
|
||||||
app.use(asyncHandler(async (req, res, next)=>{
|
|
||||||
if (!req.route) return res.redirect('/'); // Catch-all for invalid routes
|
|
||||||
|
|
||||||
const page = await renderPage(req, res);
|
|
||||||
if(!page) return;
|
|
||||||
res.send(page);
|
|
||||||
}));
|
|
||||||
|
|
||||||
//Render the page
|
//Render the page
|
||||||
|
const templateFn = require('./../client/template.js');
|
||||||
const renderPage = async (req, res)=>{
|
const renderPage = async (req, res)=>{
|
||||||
// Create configuration object
|
// Create configuration object
|
||||||
const configuration = {
|
const configuration = {
|
||||||
local : isLocalEnvironment,
|
local : isLocalEnvironment,
|
||||||
publicUrl : config.get('publicUrl') ?? '',
|
publicUrl : config.get('publicUrl') ?? '',
|
||||||
environment : nodeEnv,
|
environment : nodeEnv
|
||||||
deployment : config.get('heroku_app_name') ?? ''
|
|
||||||
};
|
};
|
||||||
const props = {
|
const props = {
|
||||||
version : version,
|
version : require('./../package.json').version,
|
||||||
url : req.customUrl || req.originalUrl,
|
url : req.customUrl || req.originalUrl,
|
||||||
brew : req.brew,
|
brew : req.brew,
|
||||||
brews : req.brews,
|
brews : req.brews,
|
||||||
@@ -503,8 +418,7 @@ const renderPage = async (req, res)=>{
|
|||||||
enable_v3 : config.get('enable_v3'),
|
enable_v3 : config.get('enable_v3'),
|
||||||
enable_themes : config.get('enable_themes'),
|
enable_themes : config.get('enable_themes'),
|
||||||
config : configuration,
|
config : configuration,
|
||||||
ogMeta : req.ogMeta,
|
ogMeta : req.ogMeta
|
||||||
userThemes : req.userThemes
|
|
||||||
};
|
};
|
||||||
const title = req.brew ? req.brew.title : '';
|
const title = req.brew ? req.brew.title : '';
|
||||||
const page = await templateFn('homebrew', title, props)
|
const page = await templateFn('homebrew', title, props)
|
||||||
@@ -514,6 +428,13 @@ const renderPage = async (req, res)=>{
|
|||||||
return page;
|
return page;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
//Send rendered page
|
||||||
|
app.use(asyncHandler(async (req, res, next)=>{
|
||||||
|
const page = await renderPage(req, res);
|
||||||
|
if(!page) return;
|
||||||
|
res.send(page);
|
||||||
|
}));
|
||||||
|
|
||||||
//v=====----- Error-Handling Middleware -----=====v//
|
//v=====----- Error-Handling Middleware -----=====v//
|
||||||
//Format Errors as plain objects so all fields will appear in the string sent
|
//Format Errors as plain objects so all fields will appear in the string sent
|
||||||
const formatErrors = (key, value)=>{
|
const formatErrors = (key, value)=>{
|
||||||
@@ -535,7 +456,7 @@ app.use(async (err, req, res, next)=>{
|
|||||||
err.originalUrl = req.originalUrl;
|
err.originalUrl = req.originalUrl;
|
||||||
console.error(err);
|
console.error(err);
|
||||||
|
|
||||||
if(err.originalUrl?.startsWith('/api')) {
|
if(err.originalUrl?.startsWith('/api/')) {
|
||||||
// console.log('API error');
|
// console.log('API error');
|
||||||
res.status(err.status || err.response?.status || 500).send(err);
|
res.status(err.status || err.response?.status || 500).send(err);
|
||||||
return;
|
return;
|
||||||
@@ -571,4 +492,6 @@ app.use((req, res)=>{
|
|||||||
});
|
});
|
||||||
//^=====--------------------------------------=====^//
|
//^=====--------------------------------------=====^//
|
||||||
|
|
||||||
export default app;
|
module.exports = {
|
||||||
|
app : app
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import _ from 'lodash';
|
const _ = require('lodash');
|
||||||
|
|
||||||
// Default properties for newly-created brews
|
// Default properties for newly-created brews
|
||||||
const DEFAULT_BREW = {
|
const DEFAULT_BREW = {
|
||||||
@@ -32,7 +32,7 @@ const DEFAULT_BREW_LOAD = _.defaults(
|
|||||||
},
|
},
|
||||||
DEFAULT_BREW);
|
DEFAULT_BREW);
|
||||||
|
|
||||||
export {
|
module.exports = {
|
||||||
DEFAULT_BREW,
|
DEFAULT_BREW,
|
||||||
DEFAULT_BREW_LOAD
|
DEFAULT_BREW_LOAD
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import nconf from 'nconf';
|
module.exports = require('nconf')
|
||||||
|
|
||||||
export default nconf
|
|
||||||
.argv()
|
.argv()
|
||||||
.env({ lowerCase: true })
|
.env({ lowerCase: true })
|
||||||
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
.file('environment', { file: `config/${process.env.NODE_ENV}.json` })
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
// reused by both the main application and all tests which require database
|
// reused by both the main application and all tests which require database
|
||||||
// connection.
|
// connection.
|
||||||
|
|
||||||
import Mongoose from 'mongoose';
|
const Mongoose = require('mongoose');
|
||||||
|
|
||||||
const getMongoDBURL = (config)=>{
|
const getMongoDBURL = (config)=>{
|
||||||
return config.get('mongodb_uri') ||
|
return config.get('mongodb_uri') ||
|
||||||
@@ -31,7 +31,7 @@ const connect = async (config)=>{
|
|||||||
.catch((error)=>handleConnectionError(error));
|
.catch((error)=>handleConnectionError(error));
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
module.exports = {
|
||||||
connect,
|
connect : connect,
|
||||||
disconnect
|
disconnect : disconnect
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default (req, res, next)=>{
|
module.exports = (req, res, next)=>{
|
||||||
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
|
if(process.env.NODE_ENV === 'local' || process.env.NODE_ENV === 'docker') return next();
|
||||||
if(req.header('x-forwarded-proto') !== 'https') {
|
if(req.header('x-forwarded-proto') !== 'https') {
|
||||||
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
return res.redirect(302, `https://${req.get('Host')}${req.url}`);
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import googleDrive from '@googleapis/drive';
|
const googleDrive = require('@googleapis/drive');
|
||||||
import { nanoid } from 'nanoid';
|
const { nanoid } = require('nanoid');
|
||||||
import token from './token.js';
|
const token = require('./token.js');
|
||||||
import config from './config.js';
|
const config = require('./config.js');
|
||||||
|
|
||||||
|
|
||||||
let serviceAuth;
|
let serviceAuth;
|
||||||
if(!config.get('service_account')){
|
if(!config.get('service_account')){
|
||||||
@@ -26,15 +25,6 @@ if(!config.get('service_account')){
|
|||||||
|
|
||||||
const defaultAuth = serviceAuth || config.get('google_api_key');
|
const defaultAuth = serviceAuth || config.get('google_api_key');
|
||||||
|
|
||||||
const retryConfig = {
|
|
||||||
retry: 3, // Number of retry attempts
|
|
||||||
retryDelay: 100, // Initial delay in milliseconds
|
|
||||||
retryDelayMultiplier: 2, // Multiplier for exponential backoff
|
|
||||||
maxRetryDelay: 32000, // Maximum delay in milliseconds
|
|
||||||
httpMethodsToRetry: ['PATCH'], // Only retry PATCH requests
|
|
||||||
statusCodesToRetry: [[429, 429]], // Only retry on 429 status code
|
|
||||||
};
|
|
||||||
|
|
||||||
const GoogleActions = {
|
const GoogleActions = {
|
||||||
|
|
||||||
authCheck : (account, res, updateTokens=true)=>{
|
authCheck : (account, res, updateTokens=true)=>{
|
||||||
@@ -60,7 +50,7 @@ const GoogleActions = {
|
|||||||
account.googleRefreshToken = tokens.refresh_token;
|
account.googleRefreshToken = tokens.refresh_token;
|
||||||
}
|
}
|
||||||
account.googleAccessToken = tokens.access_token;
|
account.googleAccessToken = tokens.access_token;
|
||||||
const JWTToken = token(account);
|
const JWTToken = token.generateAccessToken(account);
|
||||||
|
|
||||||
//Save updated token to cookie
|
//Save updated token to cookie
|
||||||
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
|
//res.cookie('nc_session', JWTToken, { maxAge: 1000*60*60*24*365, path: '/', sameSite: 'lax' });
|
||||||
@@ -73,7 +63,7 @@ const GoogleActions = {
|
|||||||
getGoogleFolder : async (auth)=>{
|
getGoogleFolder : async (auth)=>{
|
||||||
const drive = googleDrive.drive({ version: 'v3', auth });
|
const drive = googleDrive.drive({ version: 'v3', auth });
|
||||||
|
|
||||||
const fileMetadata = {
|
fileMetadata = {
|
||||||
'name' : 'Homebrewery',
|
'name' : 'Homebrewery',
|
||||||
'mimeType' : 'application/vnd.google-apps.folder'
|
'mimeType' : 'application/vnd.google-apps.folder'
|
||||||
};
|
};
|
||||||
@@ -122,7 +112,9 @@ const GoogleActions = {
|
|||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log(`Error Listing Google Brews`);
|
console.log(`Error Listing Google Brews`);
|
||||||
|
console.error(err);
|
||||||
throw (err);
|
throw (err);
|
||||||
|
//TODO: Should break out here, but continues on for some reason.
|
||||||
});
|
});
|
||||||
fileList.push(...obj.data.files);
|
fileList.push(...obj.data.files);
|
||||||
NextPageToken = obj.data.nextPageToken;
|
NextPageToken = obj.data.nextPageToken;
|
||||||
@@ -155,7 +147,7 @@ const GoogleActions = {
|
|||||||
return brews;
|
return brews;
|
||||||
},
|
},
|
||||||
|
|
||||||
updateGoogleBrew : async (brew, userIp)=>{
|
updateGoogleBrew : async (brew)=>{
|
||||||
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
const drive = googleDrive.drive({ version: 'v3', auth: defaultAuth });
|
||||||
|
|
||||||
await drive.files.update({
|
await drive.files.update({
|
||||||
@@ -176,14 +168,11 @@ const GoogleActions = {
|
|||||||
media : {
|
media : {
|
||||||
mimeType : 'text/plain',
|
mimeType : 'text/plain',
|
||||||
body : brew.text
|
body : brew.text
|
||||||
},
|
}
|
||||||
headers: {
|
|
||||||
'X-Forwarded-For': userIp, // Set the X-Forwarded-For header
|
|
||||||
},
|
|
||||||
retryConfig
|
|
||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error saving to google');
|
console.log('Error saving to google');
|
||||||
|
console.error(err);
|
||||||
throw (err);
|
throw (err);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -222,6 +211,7 @@ const GoogleActions = {
|
|||||||
})
|
})
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
console.log('Error while creating new Google brew');
|
console.log('Error while creating new Google brew');
|
||||||
|
console.error(err);
|
||||||
throw (err);
|
throw (err);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -345,4 +335,4 @@ const GoogleActions = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export default GoogleActions;
|
module.exports = GoogleActions;
|
||||||
|
|||||||
@@ -1,24 +1,15 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import _ from 'lodash';
|
const _ = require('lodash');
|
||||||
import {model as HomebrewModel} from './homebrew.model.js';
|
const HomebrewModel = require('./homebrew.model.js').model;
|
||||||
import express from 'express';
|
const router = require('express').Router();
|
||||||
import zlib from 'zlib';
|
const zlib = require('zlib');
|
||||||
import GoogleActions from './googleActions.js';
|
const GoogleActions = require('./googleActions.js');
|
||||||
import Markdown from '../shared/naturalcrit/markdown.js';
|
const Markdown = require('../shared/naturalcrit/markdown.js');
|
||||||
import yaml from 'js-yaml';
|
const yaml = require('js-yaml');
|
||||||
import asyncHandler from 'express-async-handler';
|
const asyncHandler = require('express-async-handler');
|
||||||
import { nanoid } from 'nanoid';
|
const { nanoid } = require('nanoid');
|
||||||
import { splitTextStyleAndMetadata } from '../shared/helpers.js';
|
|
||||||
import checkClientVersion from './middleware/check-client-version.js';
|
|
||||||
|
|
||||||
const router = express.Router();
|
const { DEFAULT_BREW, DEFAULT_BREW_LOAD } = require('./brewDefaults.js');
|
||||||
|
|
||||||
import { DEFAULT_BREW, DEFAULT_BREW_LOAD } from './brewDefaults.js';
|
|
||||||
import Themes from '../themes/themes.json' with { type: 'json' };
|
|
||||||
|
|
||||||
const isStaticTheme = (renderer, themeName)=>{
|
|
||||||
return Themes[renderer]?.[themeName] !== undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
// const getTopBrews = (cb) => {
|
// const getTopBrews = (cb) => {
|
||||||
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
// HomebrewModel.find().sort({ views: -1 }).limit(5).exec(function(err, brews) {
|
||||||
@@ -46,61 +37,14 @@ const api = {
|
|||||||
}
|
}
|
||||||
return { id, googleId };
|
return { id, googleId };
|
||||||
},
|
},
|
||||||
//Get array of any of this user's brews tagged with `meta:theme`
|
|
||||||
getUsersBrewThemes : async (username)=>{
|
|
||||||
if(!username)
|
|
||||||
return {};
|
|
||||||
|
|
||||||
const fields = [
|
|
||||||
'title',
|
|
||||||
'tags',
|
|
||||||
'shareId',
|
|
||||||
'thumbnail',
|
|
||||||
'textBin',
|
|
||||||
'text',
|
|
||||||
'authors',
|
|
||||||
'renderer'
|
|
||||||
];
|
|
||||||
|
|
||||||
const userThemes = {};
|
|
||||||
|
|
||||||
const brews = await HomebrewModel.getByUser(username, true, fields, { tags: { $in: ['meta:theme', 'meta:Theme'] } });
|
|
||||||
|
|
||||||
if(brews) {
|
|
||||||
for (const brew of brews) {
|
|
||||||
userThemes[brew.renderer] ??= {};
|
|
||||||
userThemes[brew.renderer][brew.shareId] = {
|
|
||||||
name : brew.title,
|
|
||||||
renderer : brew.renderer,
|
|
||||||
baseTheme : brew.theme,
|
|
||||||
baseSnippets : false,
|
|
||||||
author : brew.authors[0],
|
|
||||||
path : brew.shareId,
|
|
||||||
thumbnail : brew.thumbnail || '/assets/naturalCritLogoWhite.svg'
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return userThemes;
|
|
||||||
},
|
|
||||||
getBrew : (accessType, stubOnly = false)=>{
|
getBrew : (accessType, stubOnly = false)=>{
|
||||||
// Create middleware with the accessType passed in as part of the scope
|
// Create middleware with the accessType passed in as part of the scope
|
||||||
return async (req, res, next)=>{
|
return async (req, res, next)=>{
|
||||||
// Get relevant IDs for the brew
|
// Get relevant IDs for the brew
|
||||||
const { id, googleId } = api.getId(req);
|
const { id, googleId } = api.getId(req);
|
||||||
|
|
||||||
const accessMap = {
|
|
||||||
edit : { editId: id },
|
|
||||||
share : { shareId: id },
|
|
||||||
admin : {
|
|
||||||
$or : [
|
|
||||||
{ editId: id },
|
|
||||||
{ shareId: id },
|
|
||||||
] }
|
|
||||||
};
|
|
||||||
|
|
||||||
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
// Try to find the document in the Homebrewery database -- if it doesn't exist, that's fine.
|
||||||
let stub = await HomebrewModel.get(accessMap[accessType])
|
let stub = await HomebrewModel.get(accessType === 'edit' ? { editId: id } : { shareId: id })
|
||||||
.catch((err)=>{
|
.catch((err)=>{
|
||||||
if(googleId) {
|
if(googleId) {
|
||||||
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
console.warn(`Unable to find document stub for ${accessType}Id ${id}`);
|
||||||
@@ -111,7 +55,7 @@ const api = {
|
|||||||
stub = stub?.toObject();
|
stub = stub?.toObject();
|
||||||
|
|
||||||
if(stub?.lock?.locked && accessType != 'edit') {
|
if(stub?.lock?.locked && accessType != 'edit') {
|
||||||
throw { HBErrorCode: '51', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
throw { HBErrorCode: '100', code: stub.lock.code, message: stub.lock.shareMessage, brewId: stub.shareId, brewTitle: stub.title };
|
||||||
}
|
}
|
||||||
|
|
||||||
// If there is a google id, try to find the google brew
|
// If there is a google id, try to find the google brew
|
||||||
@@ -160,20 +104,6 @@ const api = {
|
|||||||
next();
|
next();
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
getCSS : async (req, res)=>{
|
|
||||||
const { brew } = req;
|
|
||||||
if(!brew) return res.status(404).send('');
|
|
||||||
splitTextStyleAndMetadata(brew);
|
|
||||||
if(!brew.style) return res.status(404).send('');
|
|
||||||
|
|
||||||
res.set({
|
|
||||||
'Cache-Control' : 'no-cache',
|
|
||||||
'Content-Type' : 'text/css'
|
|
||||||
});
|
|
||||||
return res.status(200).send(brew.style);
|
|
||||||
},
|
|
||||||
|
|
||||||
mergeBrewText : (brew)=>{
|
mergeBrewText : (brew)=>{
|
||||||
let text = brew.text;
|
let text = brew.text;
|
||||||
if(brew.style !== undefined) {
|
if(brew.style !== undefined) {
|
||||||
@@ -212,7 +142,7 @@ const api = {
|
|||||||
return modified;
|
return modified;
|
||||||
},
|
},
|
||||||
excludeStubProps : (brew)=>{
|
excludeStubProps : (brew)=>{
|
||||||
const propsToExclude = ['text', 'textBin'];
|
const propsToExclude = ['text', 'textBin', 'renderer', 'pageCount'];
|
||||||
for (const prop of propsToExclude) {
|
for (const prop of propsToExclude) {
|
||||||
brew[prop] = undefined;
|
brew[prop] = undefined;
|
||||||
}
|
}
|
||||||
@@ -254,8 +184,11 @@ const api = {
|
|||||||
|
|
||||||
let googleId, saved;
|
let googleId, saved;
|
||||||
if(saveToGoogle) {
|
if(saveToGoogle) {
|
||||||
googleId = await api.newGoogleBrew(req.account, newHomebrew, res);
|
googleId = await api.newGoogleBrew(req.account, newHomebrew, res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.status || err?.response?.status || 500).send(err?.message || err);
|
||||||
|
});
|
||||||
if(!googleId) return;
|
if(!googleId) return;
|
||||||
api.excludeStubProps(newHomebrew);
|
api.excludeStubProps(newHomebrew);
|
||||||
newHomebrew.googleId = googleId;
|
newHomebrew.googleId = googleId;
|
||||||
@@ -276,57 +209,6 @@ const api = {
|
|||||||
|
|
||||||
res.status(200).send(saved);
|
res.status(200).send(saved);
|
||||||
},
|
},
|
||||||
getThemeBundle : async(req, res)=>{
|
|
||||||
/* getThemeBundle: Collects the theme and all parent themes
|
|
||||||
returns an object containing an array of css, and an array of snippets, in render order
|
|
||||||
|
|
||||||
req.params.id : The shareId ( User theme ) or name ( static theme )
|
|
||||||
req.params.renderer : The Markdown renderer used for this theme */
|
|
||||||
|
|
||||||
req.params.renderer = _.upperFirst(req.params.renderer);
|
|
||||||
let currentTheme;
|
|
||||||
const completeStyles = [];
|
|
||||||
const completeSnippets = [];
|
|
||||||
|
|
||||||
while (req.params.id) {
|
|
||||||
//=== User Themes ===//
|
|
||||||
if(!isStaticTheme(req.params.renderer, req.params.id)) {
|
|
||||||
await api.getBrew('share')(req, res, ()=>{})
|
|
||||||
.catch((err)=>{
|
|
||||||
if(err.HBErrorCode == '05')
|
|
||||||
err = { ...err, name: 'ThemeLoad Error', message: 'Theme Not Found', HBErrorCode: '09' };
|
|
||||||
throw err;
|
|
||||||
});
|
|
||||||
|
|
||||||
currentTheme = req.brew;
|
|
||||||
splitTextStyleAndMetadata(currentTheme);
|
|
||||||
|
|
||||||
// If there is anything in the snippets or style members, append them to the appropriate array
|
|
||||||
if(currentTheme?.snippets) completeSnippets.push(JSON.parse(currentTheme.snippets));
|
|
||||||
if(currentTheme?.style) completeStyles.push(`/* From Brew: ${req.protocol}://${req.get('host')}/share/${req.params.id} */\n\n${currentTheme.style}`);
|
|
||||||
|
|
||||||
req.params.id = currentTheme.theme;
|
|
||||||
req.params.renderer = currentTheme.renderer;
|
|
||||||
} else {
|
|
||||||
//=== Static Themes ===//
|
|
||||||
const localSnippets = `${req.params.renderer}_${req.params.id}`; // Just log the name for loading on client
|
|
||||||
const localStyle = `@import url(\"/themes/${req.params.renderer}/${req.params.id}/style.css\");`;
|
|
||||||
completeSnippets.push(localSnippets);
|
|
||||||
completeStyles.push(`/* From Theme ${req.params.id} */\n\n${localStyle}`);
|
|
||||||
|
|
||||||
req.params.id = Themes[req.params.renderer][req.params.id].baseTheme;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const returnObj = {
|
|
||||||
// Reverse the order of the arrays so they are listed oldest parent to youngest child.
|
|
||||||
styles : completeStyles.reverse(),
|
|
||||||
snippets : completeSnippets.reverse()
|
|
||||||
};
|
|
||||||
|
|
||||||
res.setHeader('Content-Type', 'application/json');
|
|
||||||
return res.status(200).send(returnObj);
|
|
||||||
},
|
|
||||||
updateBrew : async (req, res)=>{
|
updateBrew : async (req, res)=>{
|
||||||
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
// Initialize brew from request and body, destructure query params, and set the initial value for the after-save method
|
||||||
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
const brewFromClient = api.excludePropsFromUpdate(req.body);
|
||||||
@@ -359,13 +241,19 @@ const api = {
|
|||||||
brew.googleId = undefined;
|
brew.googleId = undefined;
|
||||||
} else if(!brew.googleId && saveToGoogle) {
|
} else if(!brew.googleId && saveToGoogle) {
|
||||||
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
// If we don't have a google id and the user wants to save to google, create the google brew and set the google id on the brew
|
||||||
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res);
|
brew.googleId = await api.newGoogleBrew(req.account, api.excludeGoogleProps(brew), res)
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err.status || err.response.status).send(err.message || err);
|
||||||
|
});
|
||||||
if(!brew.googleId) return;
|
if(!brew.googleId) return;
|
||||||
} else if(brew.googleId) {
|
} else if(brew.googleId) {
|
||||||
// If the google id exists and no other actions are being performed, update the google brew
|
// If the google id exists and no other actions are being performed, update the google brew
|
||||||
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew), req.ip);
|
const updated = await GoogleActions.updateGoogleBrew(api.excludeGoogleProps(brew))
|
||||||
|
.catch((err)=>{
|
||||||
|
console.error(err);
|
||||||
|
res.status(err?.response?.status || 500).send(err);
|
||||||
|
});
|
||||||
if(!updated) return;
|
if(!updated) return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,12 +363,11 @@ const api = {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
router.use('/api', checkClientVersion);
|
router.use('/api', require('./middleware/check-client-version.js'));
|
||||||
router.post('/api', asyncHandler(api.newBrew));
|
router.post('/api', asyncHandler(api.newBrew));
|
||||||
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
router.put('/api/update/:id', asyncHandler(api.getBrew('edit', true)), asyncHandler(api.updateBrew));
|
||||||
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
router.delete('/api/:id', asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
router.get('/api/remove/:id', asyncHandler(api.deleteBrew));
|
||||||
router.get('/api/theme/:renderer/:id', asyncHandler(api.getThemeBundle));
|
|
||||||
|
|
||||||
export default api;
|
module.exports = api;
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ describe('Tests for api', ()=>{
|
|||||||
let saved;
|
let saved;
|
||||||
|
|
||||||
beforeEach(()=>{
|
beforeEach(()=>{
|
||||||
jest.resetModules();
|
|
||||||
jest.restoreAllMocks();
|
|
||||||
|
|
||||||
saved = undefined;
|
saved = undefined;
|
||||||
saveFunc = jest.fn(async function() {
|
saveFunc = jest.fn(async function() {
|
||||||
saved = { ...this, _id: '1' };
|
saved = { ...this, _id: '1' };
|
||||||
@@ -36,9 +33,8 @@ describe('Tests for api', ()=>{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
google = require('./googleActions.js').default;
|
google = require('./googleActions.js');
|
||||||
model = require('./homebrew.model.js').model;
|
model = require('./homebrew.model.js').model;
|
||||||
api = require('./homebrew.api').default;
|
|
||||||
|
|
||||||
jest.mock('./googleActions.js');
|
jest.mock('./googleActions.js');
|
||||||
google.authCheck = jest.fn(()=>'client');
|
google.authCheck = jest.fn(()=>'client');
|
||||||
@@ -50,11 +46,11 @@ describe('Tests for api', ()=>{
|
|||||||
|
|
||||||
res = {
|
res = {
|
||||||
status : jest.fn(()=>res),
|
status : jest.fn(()=>res),
|
||||||
send : jest.fn(()=>{}),
|
send : jest.fn(()=>{})
|
||||||
set : jest.fn(()=>{}),
|
|
||||||
setHeader : jest.fn(()=>{})
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
api = require('./homebrew.api');
|
||||||
|
|
||||||
hbBrew = {
|
hbBrew = {
|
||||||
text : `brew text`,
|
text : `brew text`,
|
||||||
style : 'hello yes i am css',
|
style : 'hello yes i am css',
|
||||||
@@ -85,6 +81,10 @@ describe('Tests for api', ()=>{
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
afterEach(()=>{
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
describe('getId', ()=>{
|
describe('getId', ()=>{
|
||||||
it('should return only id if google id is not present', ()=>{
|
it('should return only id if google id is not present', ()=>{
|
||||||
const { id, googleId } = api.getId({
|
const { id, googleId } = api.getId({
|
||||||
@@ -308,7 +308,7 @@ describe('Tests for api', ()=>{
|
|||||||
const req = { brew: {} };
|
const req = { brew: {} };
|
||||||
const next = jest.fn();
|
const next = jest.fn();
|
||||||
|
|
||||||
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '51', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
await expect(fn(req, null, next)).rejects.toEqual({ 'HBErrorCode': '100', 'brewId': '1', 'brewTitle': 'test brew', 'code': 404, 'message': 'brew locked' });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -408,8 +408,8 @@ brew`);
|
|||||||
expect(sent).not.toEqual(googleBrew);
|
expect(sent).not.toEqual(googleBrew);
|
||||||
expect(result.text).toBeUndefined();
|
expect(result.text).toBeUndefined();
|
||||||
expect(result.textBin).toBeUndefined();
|
expect(result.textBin).toBeUndefined();
|
||||||
expect(result.renderer).toBe('v3');
|
expect(result.renderer).toBeUndefined();
|
||||||
expect(result.pageCount).toBe(1);
|
expect(result.pageCount).toBeUndefined();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -540,9 +540,9 @@ brew`);
|
|||||||
description : '',
|
description : '',
|
||||||
editId : expect.any(String),
|
editId : expect.any(String),
|
||||||
gDrive : false,
|
gDrive : false,
|
||||||
pageCount : 1,
|
pageCount : undefined,
|
||||||
published : false,
|
published : false,
|
||||||
renderer : 'V3',
|
renderer : undefined,
|
||||||
lang : 'en',
|
lang : 'en',
|
||||||
shareId : expect.any(String),
|
shareId : expect.any(String),
|
||||||
googleId : expect.any(String),
|
googleId : expect.any(String),
|
||||||
@@ -559,6 +559,16 @@ brew`);
|
|||||||
views : 0
|
views : 0
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should handle google error', async()=>{
|
||||||
|
google.newGoogleBrew = jest.fn(()=>{
|
||||||
|
throw 'err';
|
||||||
|
});
|
||||||
|
await api.newBrew({ body: { text: 'asdf', title: '' }, query: { saveToGoogle: true }, account: { username: 'test user' } }, res);
|
||||||
|
|
||||||
|
expect(res.status).toHaveBeenCalledWith(500);
|
||||||
|
expect(res.send).toHaveBeenCalledWith('err');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('deleteGoogleBrew', ()=>{
|
describe('deleteGoogleBrew', ()=>{
|
||||||
@@ -571,121 +581,6 @@ brew`);
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Theme bundle', ()=>{
|
|
||||||
it('should return Theme Bundle for a User Theme', async ()=>{
|
|
||||||
const brews = {
|
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: null, shareId: 'userThemeAID', style: 'User Theme A Style' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
|
||||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
|
||||||
|
|
||||||
await api.getThemeBundle(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
|
||||||
styles : ['/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'],
|
|
||||||
snippets : []
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return Theme Bundle for nested User Themes', async ()=>{
|
|
||||||
const brews = {
|
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
|
||||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
|
||||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: null, shareId: 'userThemeCID', style: 'User Theme C Style' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
|
||||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
|
||||||
|
|
||||||
await api.getThemeBundle(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
|
||||||
styles : [
|
|
||||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
|
||||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
|
||||||
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
|
||||||
],
|
|
||||||
snippets : []
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return Theme Bundle for a Static Theme', async ()=>{
|
|
||||||
const req = { params: { renderer: 'V3', id: '5ePHB' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
|
||||||
|
|
||||||
await api.getThemeBundle(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
|
||||||
styles : [
|
|
||||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
|
||||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`
|
|
||||||
],
|
|
||||||
snippets : [
|
|
||||||
'V3_Blank',
|
|
||||||
'V3_5ePHB'
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return Theme Bundle for nested User and Static Themes together', async ()=>{
|
|
||||||
const brews = {
|
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'userThemeBID', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
|
||||||
userThemeBID : { title: 'User Theme B', renderer: 'V3', theme: 'userThemeCID', shareId: 'userThemeBID', style: 'User Theme B Style' },
|
|
||||||
userThemeCID : { title: 'User Theme C', renderer: 'V3', theme: '5eDMG', shareId: 'userThemeCID', style: 'User Theme C Style' }
|
|
||||||
};
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
|
||||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
|
||||||
|
|
||||||
await api.getThemeBundle(req, res);
|
|
||||||
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(res.send).toHaveBeenCalledWith({
|
|
||||||
styles : [
|
|
||||||
`/* From Theme Blank */\n\n@import url("/themes/V3/Blank/style.css");`,
|
|
||||||
`/* From Theme 5ePHB */\n\n@import url("/themes/V3/5ePHB/style.css");`,
|
|
||||||
`/* From Theme 5eDMG */\n\n@import url("/themes/V3/5eDMG/style.css");`,
|
|
||||||
'/* From Brew: https://localhost/share/userThemeCID */\n\nUser Theme C Style',
|
|
||||||
'/* From Brew: https://localhost/share/userThemeBID */\n\nUser Theme B Style',
|
|
||||||
'/* From Brew: https://localhost/share/userThemeAID */\n\nUser Theme A Style'
|
|
||||||
],
|
|
||||||
snippets : [
|
|
||||||
'V3_Blank',
|
|
||||||
'V3_5ePHB',
|
|
||||||
'V3_5eDMG'
|
|
||||||
]
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should fail for an invalid Theme in the chain', async()=>{
|
|
||||||
const brews = {
|
|
||||||
userThemeAID : { title: 'User Theme A', renderer: 'V3', theme: 'missingTheme', shareId: 'userThemeAID', style: 'User Theme A Style' },
|
|
||||||
};
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
model.get = jest.fn((getParams)=>toBrewPromise(brews[getParams.shareId]));
|
|
||||||
const req = { params: { renderer: 'V3', id: 'userThemeAID' }, get: ()=>{ return 'localhost'; }, protocol: 'https' };
|
|
||||||
|
|
||||||
let err;
|
|
||||||
await api.getThemeBundle(req, res)
|
|
||||||
.catch((e)=>err = e);
|
|
||||||
|
|
||||||
expect(err).toEqual({
|
|
||||||
HBErrorCode : '09',
|
|
||||||
accessType : 'share',
|
|
||||||
brewId : 'missingTheme',
|
|
||||||
message : 'Theme Not Found',
|
|
||||||
name : 'ThemeLoad Error',
|
|
||||||
status : 404 });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('deleteBrew', ()=>{
|
describe('deleteBrew', ()=>{
|
||||||
it('should handle case where fetching the brew returns an error', async ()=>{
|
it('should handle case where fetching the brew returns an error', async ()=>{
|
||||||
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
api.getBrew = jest.fn(()=>async ()=>{ throw { message: 'err', HBErrorCode: '02' }; });
|
||||||
@@ -906,66 +801,4 @@ brew`);
|
|||||||
expect(saved.googleId).toEqual(brew.googleId);
|
expect(saved.googleId).toEqual(brew.googleId);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('Get CSS', ()=>{
|
|
||||||
it('should return brew style content as CSS text', async ()=>{
|
|
||||||
const testBrew = { title: 'test brew', text: '```css\n\nI Have a style!\n````\n\n' };
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
|
||||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
|
||||||
|
|
||||||
const fn = api.getBrew('share', true);
|
|
||||||
const req = { brew: {} };
|
|
||||||
const next = jest.fn();
|
|
||||||
await fn(req, null, next);
|
|
||||||
await api.getCSS(req, res);
|
|
||||||
|
|
||||||
expect(req.brew).toEqual(testBrew);
|
|
||||||
expect(req.brew).toHaveProperty('style', '\nI Have a style!\n');
|
|
||||||
expect(res.status).toHaveBeenCalledWith(200);
|
|
||||||
expect(res.send).toHaveBeenCalledWith('\nI Have a style!\n');
|
|
||||||
expect(res.set).toHaveBeenCalledWith({
|
|
||||||
'Cache-Control' : 'no-cache',
|
|
||||||
'Content-Type' : 'text/css'
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return 404 when brew has no style content', async ()=>{
|
|
||||||
const testBrew = { title: 'test brew', text: 'I don\'t have a style!' };
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
|
||||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
|
||||||
|
|
||||||
const fn = api.getBrew('share', true);
|
|
||||||
const req = { brew: {} };
|
|
||||||
const next = jest.fn();
|
|
||||||
await fn(req, null, next);
|
|
||||||
await api.getCSS(req, res);
|
|
||||||
|
|
||||||
expect(req.brew).toEqual(testBrew);
|
|
||||||
expect(req.brew).toHaveProperty('style');
|
|
||||||
expect(res.status).toHaveBeenCalledWith(404);
|
|
||||||
expect(res.send).toHaveBeenCalledWith('');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return 404 when brew does not exist', async ()=>{
|
|
||||||
const testBrew = { };
|
|
||||||
|
|
||||||
const toBrewPromise = (brew)=>new Promise((res)=>res({ toObject: ()=>brew }));
|
|
||||||
api.getId = jest.fn(()=>({ id: '1', googleId: undefined }));
|
|
||||||
model.get = jest.fn(()=>toBrewPromise(testBrew));
|
|
||||||
|
|
||||||
const fn = api.getBrew('share', true);
|
|
||||||
const req = { brew: {} };
|
|
||||||
const next = jest.fn();
|
|
||||||
await fn(req, null, next);
|
|
||||||
await api.getCSS(req, res);
|
|
||||||
|
|
||||||
expect(req.brew).toEqual(testBrew);
|
|
||||||
expect(req.brew).toHaveProperty('style');
|
|
||||||
expect(res.status).toHaveBeenCalledWith(404);
|
|
||||||
expect(res.send).toHaveBeenCalledWith('');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import mongoose from 'mongoose';
|
const mongoose = require('mongoose');
|
||||||
import { nanoid } from 'nanoid';
|
const { nanoid } = require('nanoid');
|
||||||
import _ from 'lodash';
|
const _ = require('lodash');
|
||||||
import zlib from 'zlib';
|
const zlib = require('zlib');
|
||||||
|
|
||||||
|
|
||||||
const HomebrewSchema = mongoose.Schema({
|
const HomebrewSchema = mongoose.Schema({
|
||||||
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
|
||||||
@@ -45,14 +44,14 @@ HomebrewSchema.statics.get = async function(query, fields=null){
|
|||||||
const brew = await Homebrew.findOne(query, fields).orFail()
|
const brew = await Homebrew.findOne(query, fields).orFail()
|
||||||
.catch((error)=>{throw 'Can not find brew';});
|
.catch((error)=>{throw 'Can not find brew';});
|
||||||
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
if(!_.isNil(brew.textBin)) { // Uncompress zipped text field
|
||||||
const unzipped = zlib.inflateRawSync(brew.textBin);
|
unzipped = zlib.inflateRawSync(brew.textBin);
|
||||||
brew.text = unzipped.toString();
|
brew.text = unzipped.toString();
|
||||||
}
|
}
|
||||||
return brew;
|
return brew;
|
||||||
};
|
};
|
||||||
|
|
||||||
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null, filter=null){
|
HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, fields=null){
|
||||||
const query = { authors: username, published: true, ...filter };
|
const query = { authors: username, published: true };
|
||||||
if(allowAccess){
|
if(allowAccess){
|
||||||
delete query.published;
|
delete query.published;
|
||||||
}
|
}
|
||||||
@@ -63,7 +62,7 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
|
|||||||
|
|
||||||
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
|
||||||
|
|
||||||
export {
|
module.exports = {
|
||||||
HomebrewSchema as schema,
|
schema : HomebrewSchema,
|
||||||
Homebrew as model
|
model : Homebrew,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import packageJSON from '../../package.json' with { type: "json" };
|
module.exports = (req, res, next)=>{
|
||||||
const version = packageJSON.version;
|
|
||||||
|
|
||||||
export default (req, res, next)=>{
|
|
||||||
const userVersion = req.get('Homebrewery-Version');
|
const userVersion = req.get('Homebrewery-Version');
|
||||||
|
const version = require('../../package.json').version;
|
||||||
|
|
||||||
if(userVersion != version) {
|
if(userVersion != version) {
|
||||||
return res.status(412).send({
|
return res.status(412).send({
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
import config from '../config.js';
|
module.exports = (req, res, next)=>{
|
||||||
const nodeEnv = config.get('node_env');
|
|
||||||
const isLocalEnvironment = config.get('local_environments').includes(nodeEnv);
|
|
||||||
|
|
||||||
export default (req, res, next)=>{
|
|
||||||
const isImageRequest = req.get('Accept')?.split(',')
|
const isImageRequest = req.get('Accept')?.split(',')
|
||||||
?.filter((h)=>!h.includes('q='))
|
?.filter((h)=>!h.includes('q='))
|
||||||
?.every((h)=>/image\/.*/.test(h));
|
?.every((h)=>/image\/.*/.test(h));
|
||||||
if(isImageRequest && !isLocalEnvironment && !req.url?.startsWith('/staticImages')) {
|
if(isImageRequest) {
|
||||||
return res.status(406).send({
|
return res.status(406).send({
|
||||||
message : 'Request for image at this URL is not supported'
|
message : 'Request for image at this URL is not supported'
|
||||||
});
|
});
|
||||||
|
|||||||
41
server/middleware/content-negotiation.spec.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const contentNegotiationMiddleware = require('./content-negotiation.js');
|
||||||
|
|
||||||
|
describe('content-negotiation-middleware', ()=>{
|
||||||
|
let request;
|
||||||
|
let response;
|
||||||
|
let next;
|
||||||
|
|
||||||
|
beforeEach(()=>{
|
||||||
|
request = {
|
||||||
|
get : function(key) {
|
||||||
|
return this[key];
|
||||||
|
}
|
||||||
|
};
|
||||||
|
response = {
|
||||||
|
status : jest.fn(()=>response),
|
||||||
|
send : jest.fn(()=>{})
|
||||||
|
};
|
||||||
|
next = jest.fn();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 406 on image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response);
|
||||||
|
|
||||||
|
expect(response.status).toHaveBeenLastCalledWith(406);
|
||||||
|
expect(response.send).toHaveBeenCalledWith({
|
||||||
|
message : 'Request for image at this URL is not supported'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call next on non-image request', ()=>{
|
||||||
|
contentNegotiationMiddleware({
|
||||||
|
Accept : 'text,image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8',
|
||||||
|
...request
|
||||||
|
}, response, next);
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||