0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-01-23 20:53:05 +00:00

Compare commits

..

135 Commits

Author SHA1 Message Date
Trevor Buckner
63bebe1efd Lint everything
Catching up on a bunch of linting so random changes stop showing up on PRs when the linter is run.
2025-10-06 00:02:24 -04:00
Trevor Buckner
22e26d635a Merge pull request #4460 from naturalcrit/cleanupLocalStorageKeysTest
Clean up localStorageMap code
2025-10-05 23:28:34 -04:00
Trevor Buckner
643e0ac650 small cleanups of localstorage keys code 2025-10-05 23:24:50 -04:00
Trevor Buckner
5395412ac5 Remove tests for getLocalStorageMap()
The function is a simple getter with trivial logic; test is effectively just asserting the size of the map, which coverage adds no meaningful value and adds cruft to the codebase.
2025-10-05 23:24:35 -04:00
Trevor Buckner
dc4610ea1b Merge pull request #4447 from dbolack-ab/issue_3426
Applies G-Ambatte's fix for Firefox browser lock.
2025-10-05 22:31:15 -04:00
Trevor Buckner
1e71e9e18a Use blockquote and table elements, not .classes 2025-10-05 22:19:43 -04:00
Trevor Buckner
4203e90d09 Merge branch 'master' into issue_3426 2025-10-05 22:09:06 -04:00
Trevor Buckner
dc94555c94 Merge pull request #4458 from naturalcrit/new/edit/home_commonSaveButton
Make the renderSaveButton() function common between edit/new/home
2025-10-05 22:03:32 -04:00
Trevor Buckner
41aebf084b Make the renderSaveButton() function common between edit/new/home
Each of the edit/home/new pages renders its save button differently. This makes it a common function with all the same possible render states (does the document have unsaved changes? Is it already saved? Was it auto-saved?).

- Common save button
- Adds the "save" button to /home page which wasn't there before
- Animates the "save" button in /home and /new when the user makes their first change to signal that yes, you do have to actually click the save button if you want to keep this.
- "reminder... you haven't saved for X minutes" still not functional on /new and /home since that involves more moving pieces.
2025-10-05 21:57:19 -04:00
David Bolack
74e17e154f Merge branch 'issue_3426' of github.com:dbolack-ab/homebrewery into issue_3426 2025-10-05 20:12:32 -05:00
Trevor Buckner
a944b23ca0 Merge pull request #4457 from naturalcrit/new/home/edit/_unsavedChanges_common
Make `unsavedChanges` state common
2025-10-05 20:09:14 -04:00
Trevor Buckner
12052853db Merge branch 'master' into new/home/edit/_unsavedChanges_common 2025-10-05 20:07:56 -04:00
Trevor Buckner
c0f67bef5a Merge pull request #4434 from naturalcrit/fix-red-background
Fix dev background
2025-10-05 19:51:01 -04:00
David Bolack
8f715a6615 Isolate change to Firefox 2025-10-05 18:36:14 -05:00
Víctor Losada Hernández
1f51abaf10 this makes more sense 2025-10-05 19:57:49 +02:00
Víctor Losada Hernández
c90a8c53a5 lets test this 2025-10-05 19:56:50 +02:00
Víctor Losada Hernández
ac18f4bd1d Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-05 19:43:29 +02:00
Víctor Losada Hernández
7393aef806 set up development config variavle 2025-10-05 19:42:01 +02:00
Trevor Buckner
2c4c4b8f92 Make unsavedChanges state common
/editPage.jsx uses `unsavedChanges` state to detect when autosave should fire, or unsaved changes warning should display.

/homePage.jsx uses a similar check (different variables) to detect when to show the popup "save now"! button

/newPage.jsx doesn't do any of this, but probably should pop up a warning when saving hasn't happened for a long time

This commit just gives all of the pages the same common `unsavedChanges` state, calculated in the same way, and updates any sections that depend on that updated state.

This is precursor work to adding "unsaved changes" warnings to all three pages.
2025-10-04 22:17:24 -04:00
Trevor Buckner
c751d647d9 Merge pull request #4440 from naturalcrit/UnifyNewHomeEdit-Structure&Naming
Clean Up Common features of new/home/edit
2025-10-04 21:52:28 -04:00
Trevor Buckner
6057b35d19 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-10-04 21:48:10 -04:00
Trevor Buckner
521d42f32f Merge pull request #4455 from G-Ambatte/enableOldKeyDeletion
Permanently enable old local storage key deletion
2025-10-04 21:47:21 -04:00
Trevor Buckner
e9f8302597 Merge branch 'master' into enableOldKeyDeletion 2025-10-04 21:47:06 -04:00
Trevor Buckner
f429b1755d Merge pull request #4456 from naturalcrit/RemoveHandlePageChangeWrappers
Remove handler function for cursor/renderer page. Use setState directly
2025-10-04 21:46:32 -04:00
Trevor Buckner
20e12ebcb5 Remove handler function for cursor/renderer page. Use setState directly 2025-10-04 21:39:24 -04:00
G.Ambatte
ae51213c8c Permanently enable old local storage key deletion 2025-10-05 12:25:51 +13:00
Trevor Buckner
8f7ae35f08 Merge branch 'master' into issue_3426 2025-10-04 18:32:15 -04:00
Trevor Buckner
44023f390c Merge pull request #4453 from G-Ambatte/fixDockerInstructionsForWindows-#4443
Add Windows-specific instructions to Docker README
2025-10-04 18:02:47 -04:00
Trevor Buckner
48b95712e2 Merge branch 'master' into fixDockerInstructionsForWindows-#4443 2025-10-04 18:01:58 -04:00
Trevor Buckner
16c28e16ce Merge pull request #4452 from G-Ambatte/standardizeLocalStorageKeyNames-#4119
Standardize local storage key names #4119
2025-10-04 17:54:10 -04:00
G.Ambatte
379b518c6b Merge branch 'master' into fixDockerInstructionsForWindows-#4443 2025-10-04 15:40:03 +13:00
G.Ambatte
962dcbdbf6 Update Docker instructions 2025-10-04 15:36:14 +13:00
G.Ambatte
400fa250ee Move key deletion out of key update check 2025-10-04 15:19:31 +13:00
G.Ambatte
e82921f81a Add key to activate deletion for testing 2025-10-04 14:54:57 +13:00
G.Ambatte
18367526bd Merge branch 'master' into standardizeLocalStorageKeyNames-#4119 2025-10-04 13:07:51 +13:00
David Bolack
f0bb06e706 Merge branch 'master' into issue_3426 2025-10-03 18:52:56 -05:00
Víctor Losada Hernández
aff9a85769 end of file character shit 2025-10-03 21:38:43 +02:00
Víctor Losada Hernández
e0379a0baa last cleanup 2025-10-03 21:38:10 +02:00
Víctor Losada Hernández
e8a0681015 Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-03 21:37:06 +02:00
Víctor Losada Hernández
3ed61ebe2c Merge branch 'fix-red-background' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-10-03 21:32:55 +02:00
Víctor Losada Hernández
c2e51b0baa removing isclient check to see what's what 2025-10-03 21:32:52 +02:00
Trevor Buckner
bc258f5239 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-10-02 22:02:53 -04:00
Trevor Buckner
e64fc83ea6 Merge pull request #4436 from G-Ambatte/addMongoIndexes
Add indexes to Mongo schema
2025-10-02 20:48:32 -04:00
G.Ambatte
ee6d2ac35d Merge branch 'master' into addMongoIndexes 2025-10-03 13:42:25 +13:00
G.Ambatte
f22f7196ca Update README.md 2025-10-03 13:41:53 +13:00
Trevor Buckner
ba23763294 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-10-02 19:45:42 -04:00
Trevor Buckner
7f832a55db Merge pull request #4439 from naturalcrit/CommonHandleBrewChange
Combine handle<Text/Style/Snippet/Meta>Change() into one common function
2025-10-02 19:39:35 -04:00
Trevor Buckner
1c6a39363c Combine handleText/Style/Snippet/Meta functions into common function
Also adds any related imports and key names
2025-10-02 19:33:15 -04:00
Trevor Buckner
bcca5fa97d In /homepage, rename brew state to currentBrew to match /new and /edit 2025-10-02 19:27:45 -04:00
Trevor Buckner
51b91567f6 Merge branch 'master' into fix-red-background 2025-10-02 18:39:31 -04:00
Trevor Buckner
bfe6142b04 Merge pull request #4438 from G-Ambatte/fixDefaultSaveLocation-#4437
Fix default save location functionality
2025-10-02 18:37:59 -04:00
Víctor Losada Hernández
aef835dfe7 Merge branch 'master' into fixDefaultSaveLocation-#4437 2025-10-02 12:42:09 +02:00
Víctor Losada Hernández
274fbcb29e Merge pull request #4435 from naturalcrit/remove-scrollbar-styles
remove custom scrollbar styles
2025-10-02 12:40:59 +02:00
Víctor Losada Hernández
eefda9fe45 simplifying per suggestion 2025-10-02 12:40:12 +02:00
G.Ambatte
900cf6aebb Change SAVEKEY definition to after username is populated 2025-10-02 22:59:24 +13:00
G.Ambatte
24db8f85ac Merge branch 'standardizeLocalStorageKeyNames-#4119' of https://github.com/G-Ambatte/homebrewery into standardizeLocalStorageKeyNames-#4119 2025-10-02 18:30:07 +13:00
G.Ambatte
82a8db129e Merge branch 'naturalcrit:master' into addMongoIndexes 2025-10-02 10:54:21 +13:00
Víctor Losada Hernández
6d4ad6af7d Merge branch 'master' of https://github.com/naturalcrit/homebrewery into remove-scrollbar-styles 2025-10-01 22:57:53 +02:00
Víctor Losada Hernández
e793db7b37 separating the words to make it less ugly 2025-10-01 22:55:32 +02:00
Víctor Losada Hernández
ff5450ad8c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-09-29 22:28:12 +02:00
Víctor Losada Hernández
4b753970c9 remove scrollbar 2025-09-29 22:19:19 +02:00
Trevor Buckner
07495b0dea Make Print/Vault/New common nav buttons to all pages 2025-09-29 12:48:25 -04:00
G.Ambatte
718dba3e4a Merge branch 'master' into standardizeLocalStorageKeyNames-#4119 2025-09-29 23:14:14 +13:00
Trevor Buckner
c6ed67db08 Merge branch 'master' into UnifyNewHomeEdit-Structure&Naming 2025-09-26 22:56:15 -04:00
David Bolack
c50c279ef3 Merge branch 'issue_3426' of github.com:dbolack-ab/homebrewery into issue_3426 2025-09-22 20:36:45 -05:00
David Bolack
cc246fb31a Merge branch 'master' into issue_3426 2025-09-22 20:36:09 -05:00
Trevor Buckner
fb75bd46d0 Merge pull request #4425 from naturalcrit/ChangeAutoSaveToTimer
Fix Autosave and unsaved changes warning
2025-09-22 19:57:02 -04:00
Trevor Buckner
c5071aa27e Restore unsaved warning timeout duration to 15 mins 2025-09-22 19:55:39 -04:00
Trevor Buckner
f0baa763ec lint 2025-09-22 19:52:42 -04:00
Trevor Buckner
3ec650557e Fix Autosave and unsaved changes warning
Use normal setTimeout for autosave instead of _.debounce. Fixes a lot of issues with functional component.

Also fix existing bug where multiple "unsaved data" warnings could be queued up if the user keeps typing while the warning is being displayed.
2025-09-22 19:49:57 -04:00
Trevor Buckner
242ff8712f Merge pull request #4420 from naturalcrit/MoveShareDropdownMenuToSeparateComponent
Move "share" dropdown to own component
2025-09-18 22:48:08 -04:00
Trevor Buckner
31a8101df7 Move "share" dropdown to own component 2025-09-13 19:37:59 -04:00
Trevor Buckner
788324fe31 Merge handText/Style/Snip/MetaChange into handleBrewChange 2025-09-11 22:03:25 -04:00
Trevor Buckner
da8772daa7 Use setXXXPageNum instead of handleXXXPageNum
No need for separate wrapper functions when we can just pass the setState functions directly.
2025-09-11 16:14:45 -04:00
G.Ambatte
87a36bb02d Add tests for localStorageKeyMap.js 2025-09-11 21:57:00 +12:00
G.Ambatte
1459f6a320 Tweak local storage update logic 2025-09-11 21:25:13 +12:00
G.Ambatte
a11fa72261 Change JSON file to JS getter function 2025-09-11 21:02:56 +12:00
G.Ambatte
2663d86627 Don't update storage values if key already exists 2025-09-10 20:31:50 +12:00
G.Ambatte
8d4ea7cfd8 Update listPage storage keys 2025-09-10 20:20:42 +12:00
G.Ambatte
b6818e963b Remove unused dismiss keys 2025-09-10 20:12:50 +12:00
G.Ambatte
dc1bc471aa Update SplitPane keys 2025-09-10 20:10:18 +12:00
G.Ambatte
5504c1b96b Update Autosave key 2025-09-10 20:00:53 +12:00
G.Ambatte
fd370c777d Update Editor theme key 2025-09-10 19:57:16 +12:00
G.Ambatte
58277585e1 Update Recent Items keys 2025-09-10 19:55:42 +12:00
G.Ambatte
885c0105f3 Update adminPage key 2025-09-10 19:36:39 +12:00
G.Ambatte
52486495c8 Update adminPage storage keys 2025-09-10 19:35:14 +12:00
G.Ambatte
328e071268 Update BrewRenderer toolbar keys 2025-09-10 19:22:54 +12:00
G.Ambatte
088ca9971c Update accountPage keys 2025-09-10 19:16:29 +12:00
G.Ambatte
c99f59d42b Update newPage.jsx keys 2025-09-10 19:08:47 +12:00
G.Ambatte
cb3eb77c61 Merge branch 'standardizeLocalStorageKeyNames-#4119' of https://github.com/G-Ambatte/homebrewery into standardizeLocalStorageKeyNames-#4119 2025-09-10 18:54:00 +12:00
G.Ambatte
7163b1a287 Add function to add dynamic keys to local storage map 2025-09-10 18:53:53 +12:00
G.Ambatte
08d228831d Add missing keys to JSON, tweak layout 2025-09-10 18:53:23 +12:00
G.Ambatte
ad8bb34c93 Merge branch 'master' into standardizeLocalStorageKeyNames-#4119 2025-09-10 16:58:16 +12:00
Trevor Buckner
02a7920b2c Merge pull request #4415 from naturalcrit/MakeEditPageFunctional
Refactor editPage.jsx into a functional component
2025-09-09 22:47:26 -04:00
Trevor Buckner
43c639246b Merge branch 'master' into MakeEditPageFunctional 2025-09-09 22:41:08 -04:00
Trevor Buckner
c2e6150edf Fix mistaken delete 2025-09-09 22:39:11 -04:00
Trevor Buckner
95a1d74644 Linting 2025-09-09 22:35:55 -04:00
Trevor Buckner
1044aa74b0 Cleanup 2025-09-09 22:27:58 -04:00
Trevor Buckner
8a0f350c47 Fix mutating HTMLErrors directly instead of setState 2025-09-09 22:19:43 -04:00
Trevor Buckner
6f2c397574 Restore autosave warning to 15 minutes 2025-09-09 20:47:09 -04:00
Trevor Buckner
8706f91b58 Fis autosaveWarning 2025-09-09 08:37:17 -04:00
G.Ambatte
1eb5b6d3a4 Copy exisitng key data to new keys 2025-09-09 22:04:40 +12:00
Trevor Buckner
90f6e7ec37 Make autosaving work
debouncing does not play nice with functional component. Any debounced function gets locked in as the original state, meaning we keep saving the original document and overwriting the current document when a save fires.

Must pass in the parameters instead of pulling directly from state to work properly.
2025-09-09 01:57:13 -04:00
Trevor Buckner
90a81237ec rename handleAutoSave to toggleAutoSave 2025-09-08 23:18:25 -04:00
Trevor Buckner
883f59ff0d rename autosave state to autoSaveEnabled 2025-09-08 23:13:21 -04:00
Trevor Buckner
a75364c7f6 remove unused displayLockMessage state 2025-09-08 23:06:16 -04:00
Trevor Buckner
597ce7cb48 Convert renderNavBar and render 2025-09-08 23:05:47 -04:00
Trevor Buckner
d94afa9c50 convert functions and states 2025-09-08 19:33:02 -04:00
Víctor Losada Hernández
13de195a66 Merge pull request #4413 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-9d9674f2b4
Bump the dev-dependencies group with 3 updates
2025-09-08 16:28:08 +02:00
dependabot[bot]
32f9a44acf Bump the dev-dependencies group with 3 updates
Bumps the dev-dependencies group with 3 updates: [eslint](https://github.com/eslint/eslint), [stylelint](https://github.com/stylelint/stylelint) and [stylelint-config-recess-order](https://github.com/stormwarning/stylelint-config-recess-order).


Updates `eslint` from 9.34.0 to 9.35.0
- [Release notes](https://github.com/eslint/eslint/releases)
- [Changelog](https://github.com/eslint/eslint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/eslint/eslint/compare/v9.34.0...v9.35.0)

Updates `stylelint` from 16.23.1 to 16.24.0
- [Release notes](https://github.com/stylelint/stylelint/releases)
- [Changelog](https://github.com/stylelint/stylelint/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stylelint/stylelint/compare/16.23.1...16.24.0)

Updates `stylelint-config-recess-order` from 7.2.0 to 7.3.0
- [Release notes](https://github.com/stormwarning/stylelint-config-recess-order/releases)
- [Changelog](https://github.com/stormwarning/stylelint-config-recess-order/blob/main/CHANGELOG.md)
- [Commits](https://github.com/stormwarning/stylelint-config-recess-order/compare/v7.2.0...v7.3.0)

---
updated-dependencies:
- dependency-name: eslint
  dependency-version: 9.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: stylelint
  dependency-version: 16.24.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
- dependency-name: stylelint-config-recess-order
  dependency-version: 7.3.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-08 03:01:23 +00:00
Víctor Losada Hernández
bb32f9fe95 Merge pull request #3022 from G-Ambatte/newTheme-UnearthedArcana
[NEW THEME]:  Unearthed Arcana
2025-09-07 22:32:29 +02:00
G.Ambatte
63f4f5712e Merge branch 'master' into newTheme-UnearthedArcana 2025-09-08 08:25:23 +12:00
Víctor Losada Hernández
ede7ad683a Merge pull request #4400 from naturalcrit/dependabot/npm_and_yarn/dev-dependencies-117382e062
Bump jest from 30.0.5 to 30.1.1 in the dev-dependencies group
2025-09-03 19:31:11 +02:00
dependabot[bot]
172c11646a Bump jest from 30.0.5 to 30.1.1 in the dev-dependencies group
Bumps the dev-dependencies group with 1 update: [jest](https://github.com/jestjs/jest/tree/HEAD/packages/jest).


Updates `jest` from 30.0.5 to 30.1.1
- [Release notes](https://github.com/jestjs/jest/releases)
- [Changelog](https://github.com/jestjs/jest/blob/main/CHANGELOG.md)
- [Commits](https://github.com/jestjs/jest/commits/v30.1.1/packages/jest)

---
updated-dependencies:
- dependency-name: jest
  dependency-version: 30.1.1
  dependency-type: direct:development
  update-type: version-update:semver-minor
  dependency-group: dev-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-03 17:29:06 +00:00
Trevor Buckner
bbeac49552 Merge pull request #4411 from naturalcrit/MakeNewPageFunctionalComponent
make newPage functional
2025-09-02 22:40:26 -04:00
G.Ambatte
8a788a6ebf Merge branch 'master' into newTheme-UnearthedArcana 2025-08-27 13:24:25 +12:00
G.Ambatte
6c3a5f193d Merge branch 'master' into addMongoIndexes 2025-08-25 17:24:27 +12:00
Víctor Losada Hernández
4c5eef46a0 Merge branch 'master' into issue_3426 2025-08-21 16:33:19 +02:00
David Bolack
a1ab27b57f Applies G-Ambatte's fix 2025-07-30 19:47:59 -05:00
G.Ambatte
4d014bf379 Merge branch 'master' into addMongoIndexes 2025-07-20 14:06:53 +12:00
G.Ambatte
4856c803ed Use local_environments from config 2025-07-20 11:48:22 +12:00
G.Ambatte
d9cd270f3b Add autobuild to Mongoose connection (local/docker ONLY) 2025-07-20 11:43:14 +12:00
G.Ambatte
878ea1449d Add indexes to HomebrewSchema 2025-07-20 11:42:24 +12:00
Víctor Losada Hernández
d6a5a1f03c no idea what changed but now it works 2025-07-18 00:39:36 +02:00
Víctor Losada Hernández
f04d6cdd1f fix to current 2025-07-17 23:32:18 +02:00
Víctor Losada Hernández
4fd61ce92c Merge branch 'master' of https://github.com/naturalcrit/homebrewery into fix-red-background 2025-07-17 23:30:01 +02:00
Víctor Losada Hernández
88b70d340e final bit 2025-05-27 11:27:04 +02:00
Víctor Losada Hernández
ed05d8c754 move all to homebrew.jsx 2025-05-27 11:25:01 +02:00
Víctor Losada Hernández
077aaeb815 log 2025-05-27 10:54:07 +02:00
G.Ambatte
a917937f12 Merge branch 'master' into newTheme-UnearthedArcana 2024-07-22 14:35:14 +12:00
G.Ambatte
b54448f830 Merge branch 'master' into newTheme-UnearthedArcana 2024-02-25 11:55:40 +13:00
G.Ambatte
b88480c9ba Merge branch 'master' into newTheme-UnearthedArcana 2023-10-14 11:42:31 +13:00
G.Ambatte
a8897b2813 Merge branch 'master' into newTheme-UnearthedArcana 2023-09-09 09:49:15 +12:00
G.Ambatte
cb139ae775 Merge branch 'master' into newTheme-UnearthedArcana 2023-09-06 08:32:31 +12:00
G.Ambatte
89a788ff9f Add new theme - Unearthed Arcana 2023-09-03 16:56:21 +12:00
41 changed files with 1144 additions and 957 deletions

View File

@@ -49,7 +49,7 @@ Make an changes you need to `config/docker.json` then build the image. If it doe
"web_port" : 8000,
"enable_v3" : true,
"mongodb_uri": "mongodb://172.17.0.2/homebrewery",
"enable_themes" : true,
"enable_themes" : true
}
```
@@ -90,6 +90,13 @@ docker run --name homebrewery-mongodb -d --restart unless-stopped -v mongodata:/
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
```shell
# Make sure you run this in the homebrewery directory
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```
## Updating the Image
When Homebrewery code updates, your docker container will not automatically follow the changes. To do so you will need to rebuild your homebrewery image.
@@ -117,3 +124,9 @@ docker-compose build homebrewery
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v $(pwd)/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```
**NOTE:** If you are running from the Windows command line, this will not work as `$(pwd)` is not valid syntax. Use this command instead:
```shell
# Make sure you run this in the homebrewery directory
docker run --name homebrewery-app -d --restart unless-stopped -e NODE_ENV=docker -v %cd%/config/docker.json:/usr/src/app/config/docker.json -p 8000:8000 docker.io/library/homebrewery:latest
```

View File

@@ -75,8 +75,9 @@ it using the two commands:
1. `npm install`
1. `npm start`
You should now be able to go to [http://localhost:8000](http://localhost:8000)
in your browser and use The Homebrewery offline.
When the Homebrewery server is started for the first time, it will modify the database to create the indexes required for better Homebrewery performance. This may take a few moments to complete for each index, dependent on how much content is in your local database - a brand new, empty database should be done in seconds.
On completion, you should be able to go to [http://localhost:8000](http://localhost:8000) in your browser and use The Homebrewery offline.
If you had any issue at all, here are some links that may be useful:
- [Course](https://learn.mongodb.com/courses/m103-basic-cluster-administration) on cluster administration, useful for beginners
@@ -145,3 +146,4 @@ your contribution to the project, please join our [gitter chat][gitter-url].
[github-pr-docs-url]: https://docs.github.com/en/free-pro-team@latest/github/collaborating-with-issues-and-pull-requests/creating-a-pull-request
[gitter-url]: https://gitter.im/naturalcrit/Lobby

View File

@@ -7,15 +7,17 @@ import LockTools from './lockTools/lockTools.jsx';
const tabGroups = ['brew', 'notifications', 'authors', 'locks'];
const ADMIN_TAB = 'HB_adminPage_currentTab';
const Admin = ()=>{
const [currentTab, setCurrentTab] = useState('');
useEffect(()=>{
setCurrentTab(localStorage.getItem('hbAdminTab') || 'brew');
setCurrentTab(localStorage.getItem(ADMIN_TAB) || 'brew');
}, []);
useEffect(()=>{
localStorage.setItem('hbAdminTab', currentTab);
localStorage.setItem(ADMIN_TAB, currentTab);
}, [currentTab]);
return (

View File

@@ -2,7 +2,8 @@ require('./splitPane.less');
const React = require('react');
const { useState, useEffect } = React;
const storageKey = 'naturalcrit-pane-split';
const PANE_WIDTH_KEY = 'HB_editor_splitWidth';
const LIVE_SCROLL_KEY = 'HB_editor_liveScroll';
const SplitPane = (props)=>{
const {
@@ -18,9 +19,9 @@ const SplitPane = (props)=>{
const [liveScroll, setLiveScroll] = useState(false);
useEffect(()=>{
const savedPos = window.localStorage.getItem(storageKey);
const savedPos = window.localStorage.getItem(PANE_WIDTH_KEY);
setDividerPos(savedPos ? limitPosition(savedPos, 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)) : window.innerWidth / 2);
setLiveScroll(window.localStorage.getItem('liveScroll') === 'true');
setLiveScroll(window.localStorage.getItem(LIVE_SCROLL_KEY) === 'true');
window.addEventListener('resize', handleResize);
return ()=>window.removeEventListener('resize', handleResize);
@@ -29,13 +30,13 @@ const SplitPane = (props)=>{
const limitPosition = (x, min = 1, max = window.innerWidth - 13)=>Math.round(Math.min(max, Math.max(min, x)));
//when resizing, the divider should grow smaller if less space is given, then grow back if the space is restored, to the original position
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(storageKey), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleResize = ()=>setDividerPos(limitPosition(window.localStorage.getItem(PANE_WIDTH_KEY), 0.1 * (window.innerWidth - 13), 0.9 * (window.innerWidth - 13)));
const handleUp =(e)=>{
e.preventDefault();
if(isDragging) {
onDragFinish(dividerPos);
window.localStorage.setItem(storageKey, dividerPos);
window.localStorage.setItem(PANE_WIDTH_KEY, dividerPos);
}
setIsDragging(false);
};
@@ -52,7 +53,7 @@ const SplitPane = (props)=>{
};
const liveScrollToggle = ()=>{
window.localStorage.setItem('liveScroll', String(!liveScroll));
window.localStorage.setItem(LIVE_SCROLL_KEY, String(!liveScroll));
setLiveScroll(!liveScroll);
};

View File

@@ -24,6 +24,8 @@ const PAGEBREAK_REGEX_LEGACY = /\\page(?:break)?/m;
const COLUMNBREAK_REGEX_LEGACY = /\\column(:?break)?/m;
const PAGE_HEIGHT = 1056;
const TOOLBAR_STATE_KEY = 'HB_renderer_toolbarState';
const INITIAL_CONTENT = dedent`
<!DOCTYPE html><html><head>
<link href="//fonts.googleapis.com/css?family=Open+Sans:400,300,600,700" rel="stylesheet" type="text/css" />
@@ -122,7 +124,7 @@ const BrewRenderer = (props)=>{
//useEffect to store or gather toolbar state from storage
useEffect(()=>{
const toolbarState = JSON.parse(window.localStorage.getItem('hb_toolbarState'));
const toolbarState = JSON.parse(window.localStorage.getItem(TOOLBAR_STATE_KEY));
toolbarState && setDisplayOptions(toolbarState);
}, []);
@@ -284,7 +286,7 @@ const BrewRenderer = (props)=>{
const handleDisplayOptionsChange = (newDisplayOptions)=>{
setDisplayOptions(newDisplayOptions);
localStorage.setItem('hb_toolbarState', JSON.stringify(newDisplayOptions));
localStorage.setItem(TOOLBAR_STATE_KEY, JSON.stringify(newDisplayOptions));
};
const pagesStyle = {
@@ -293,12 +295,6 @@ const BrewRenderer = (props)=>{
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, displayOptions]);
@@ -327,10 +323,9 @@ const BrewRenderer = (props)=>{
contentDidMount={frameDidMount}
onClick={()=>{emitClick();}}
>
<div className={`brewRenderer ${global.config.deployment && 'deployment'}`}
<div className='brewRenderer'
onKeyDown={handleControlKeys}
tabIndex={-1}
style={ styleObject }
>
{/* Apply CSS from Style tab and render pages from Markdown tab */}

View File

@@ -6,7 +6,6 @@
overflow-y : scroll;
will-change : transform;
&:has(.facing, .flow) { padding : 60px 30px; }
&.deployment { background-color : darkred; }
:where(.pages) {
&.facing {
display : grid;

View File

@@ -9,6 +9,8 @@ import { Anchored, AnchoredBox, AnchoredTrigger } from '../../../components/Anch
const MAX_ZOOM = 300;
const MIN_ZOOM = 10;
const TOOLBAR_VISIBILITY = 'HB_renderer_toolbarVisibility';
const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPages, headerState, setHeaderState })=>{
const [pageNum, setPageNum] = useState(1);
@@ -21,8 +23,8 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
}, [visiblePages]);
useEffect(()=>{
const Visibility = localStorage.getItem('hb_toolbarVisibility');
if (Visibility) setToolsVisible(Visibility === 'true');
const Visibility = localStorage.getItem(TOOLBAR_VISIBILITY);
if(Visibility) setToolsVisible(Visibility === 'true');
}, []);
@@ -100,7 +102,7 @@ const ToolBar = ({ displayOptions, onDisplayOptionsChange, visiblePages, totalPa
<div className='toggleButton'>
<button title={`${toolsVisible ? 'Hide' : 'Show'} Preview Toolbar`} onClick={()=>{
setToolsVisible(!toolsVisible);
localStorage.setItem('hb_toolbarVisibility', !toolsVisible);
localStorage.setItem(TOOLBAR_VISIBILITY, !toolsVisible);
}}><i className='fas fa-glasses' /></button>
<button title={`${headerState ? 'Hide' : 'Show'} Header Navigation`} onClick={()=>{setHeaderState(!headerState);}}><i className='fas fa-rectangle-list' /></button>
</div>

View File

@@ -10,7 +10,7 @@ const CodeEditor = require('naturalcrit/codeEditor/codeEditor.jsx');
const SnippetBar = require('./snippetbar/snippetbar.jsx');
const MetadataEditor = require('./metadataEditor/metadataEditor.jsx');
const EDITOR_THEME_KEY = 'HOMEBREWERY-EDITOR-THEME';
const EDITOR_THEME_KEY = 'HB_editor_theme';
const PAGEBREAK_REGEX_V3 = /^(?=\\page(?:break)?(?: *{[^\n{}]*})?$)/m;
const SNIPPETBREAK_REGEX_V3 = /^\\snippet\ .*$/;
@@ -40,11 +40,8 @@ const Editor = createClass({
style : ''
},
onTextChange : ()=>{},
onStyleChange : ()=>{},
onMetaChange : ()=>{},
onSnipChange : ()=>{},
reportError : ()=>{},
onBrewChange : ()=>{},
reportError : ()=>{},
onCursorPageChange : ()=>{},
onViewPageChange : ()=>{},
@@ -143,7 +140,7 @@ const Editor = createClass({
handleViewChange : function(newView){
this.props.setMoveArrows(newView === 'text');
this.setState({
view : newView
}, ()=>{
@@ -438,7 +435,7 @@ const Editor = createClass({
language='gfm'
view={this.state.view}
value={this.props.brew.text}
onChange={this.props.onTextChange}
onChange={this.props.onBrewChange('text')}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
style={{ height: `calc(100% - ${this.state.snippetbarHeight}px)` }} />
@@ -451,7 +448,7 @@ const Editor = createClass({
language='css'
view={this.state.view}
value={this.props.brew.style ?? DEFAULT_STYLE_TEXT}
onChange={this.props.onStyleChange}
onChange={this.props.onBrewChange('style')}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}
@@ -467,7 +464,7 @@ const Editor = createClass({
<MetadataEditor
metadata={this.props.brew}
themeBundle={this.props.themeBundle}
onChange={this.props.onMetaChange}
onChange={this.props.onBrewChange('metadata')}
reportError={this.props.reportError}
userThemes={this.props.userThemes}/>
</>;
@@ -481,7 +478,7 @@ const Editor = createClass({
language='gfm'
view={this.state.view}
value={this.props.brew.snippets}
onChange={this.props.onSnipChange}
onChange={this.props.onBrewChange('snippets')}
enableFolding={true}
editorTheme={this.state.editorTheme}
rerenderParent={this.rerenderParent}

View File

@@ -18,7 +18,7 @@ module.exports = {
try {
Boolean(new URL(value));
return null;
} catch (e) {
} catch {
return 'Must be a valid URL';
}
}

View File

@@ -4,6 +4,8 @@ import './homebrew.less';
import React from 'react';
import { StaticRouter as Router, Route, Routes, useParams, useSearchParams } from 'react-router';
import { updateLocalStorage } from './utils/updateLocalStorage/updateLocalStorageKeys.js';
import HomePage from './pages/homePage/homePage.jsx';
import EditPage from './pages/editPage/editPage.jsx';
import UserPage from './pages/userPage/userPage.jsx';
@@ -17,7 +19,6 @@ const WithRoute = ({ el: Element, ...rest })=>{
const params = useParams();
const [searchParams] = useSearchParams();
const queryParams = Object.fromEntries(searchParams?.entries() || []);
return <Element {...rest} {...params} query={queryParams} />;
};
@@ -48,9 +49,20 @@ const Homebrew = (props)=>{
global.enable_themes = enable_themes;
global.config = config;
const backgroundObject = ()=>{
if(global.config.deployment || (config.local && config.development)){
const bgText = global.config.deployment || 'Local';
return {
backgroundImage : `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' version='1.1' height='100px' width='200px'><text x='0' y='15' fill='%23fff7' font-size='20'>${bgText}</text></svg>")`
};
}
return null;
};
updateLocalStorage();
return (
<Router location={url}>
<div className='homebrew'>
<div className={`homebrew${(config.deployment || config.local) ? ' deployment' : ''}`} style={backgroundObject()}>
<Routes>
<Route path='/edit/:id' element={<WithRoute el={EditPage} brew={brew} userThemes={userThemes}/>} />
<Route path='/share/:id' element={<WithRoute el={SharePage} brew={brew} />} />

View File

@@ -1,12 +1,14 @@
@import 'naturalcrit/styles/core.less';
.homebrew {
height : 100%;
background-color:@steel;
&.deployment { background-color : darkred; }
.sitePage {
display : flex;
flex-direction : column;
height : 100%;
overflow-y : hidden;
background-color : @steel;
.content {
position : relative;
flex : auto;

View File

@@ -2,9 +2,9 @@ require('./error-navitem.less');
const React = require('react');
const Nav = require('naturalcrit/nav/nav.jsx');
const ErrorNavItem = ({error = '', clearError})=>{
const ErrorNavItem = ({ error = '', clearError })=>{
const response = error.response;
const errorCode = error.code
const errorCode = error.code;
const status = response?.status;
const HBErrorCode = response?.body?.HBErrorCode;
const message = response?.body?.message;
@@ -15,7 +15,7 @@ const ErrorNavItem = ({error = '', clearError})=>{
errMsg += `\`\`\`\n${error.stack}\n`;
errMsg += `${JSON.stringify(response?.error, null, ' ')}\n\`\`\``;
console.log(errMsg);
} catch (e){}
} catch {}
if(status === 409) {
return <Nav.item className='save error' icon='fas fa-exclamation-triangle'>

View File

@@ -34,8 +34,8 @@ const NewBrew = ()=>{
}
const type = file.name.split('.').pop().toLowerCase();
alert(`This file is invalid: ${!type ? "Missing file extension" :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
alert(`This file is invalid: ${!type ? 'Missing file extension' :`.${type} files are not supported`}. Only .txt files exported from the Homebrewery are allowed.`);
console.log(file);

View File

@@ -5,8 +5,8 @@ const Moment = require('moment');
const Nav = require('naturalcrit/nav/nav.jsx');
const EDIT_KEY = 'homebrewery-recently-edited';
const VIEW_KEY = 'homebrewery-recently-viewed';
const EDIT_KEY = 'HB_nav_recentlyEdited';
const VIEW_KEY = 'HB_nav_recentlyViewed';
const RecentItems = createClass({

View File

@@ -0,0 +1,35 @@
import React from 'react';
import dedent from 'dedent-tabs';
import Nav from 'naturalcrit/nav/nav.jsx';
const getShareId = (brew)=>(
brew.googleId && !brew.stubbed
? brew.googleId + brew.shareId
: brew.shareId
);
const getRedditLink = (brew)=>{
const text = dedent`
Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${getShareId(brew)})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(brew.title.toWellFormed())}&text=${encodeURIComponent(text)}`;
};
export default ({ brew })=>(
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${getShareId(brew)}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${getShareId(brew)}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={getRedditLink(brew)} newTab rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
);

View File

@@ -13,7 +13,7 @@ const AccountPage = (props)=>{
// initialize save location from local storage based on user id
React.useEffect(()=>{
if(!saveLocation && accountDetails.username) {
SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${accountDetails.username}`;
SAVEKEY = `HB_editor_defaultSave_${accountDetails.username}`;
// if no SAVEKEY in local storage, default save location to Google Drive if user has Google account.
let saveLocation = window.localStorage.getItem(SAVEKEY);
saveLocation = saveLocation ?? (accountDetails.googleId ? 'GOOGLE-DRIVE' : 'HOMEBREWERY');

View File

@@ -7,7 +7,9 @@ const moment = require('moment');
const BrewItem = require('./brewItem/brewItem.jsx');
const USERPAGE_KEY_PREFIX = 'HOMEBREWERY-LISTPAGE';
const USERPAGE_SORT_DIR = 'HB_listPage_sortDir';
const USERPAGE_SORT_TYPE = 'HB_listPage_sortType';
const USERPAGE_GROUP_VISIBILITY_PREFIX = 'HB_listPage_visibility_group';
const DEFAULT_SORT_TYPE = 'alpha';
const DEFAULT_SORT_DIR = 'asc';
@@ -50,12 +52,12 @@ const ListPage = createClass({
// LOAD FROM LOCAL STORAGE
if(typeof window !== 'undefined') {
const newSortType = (this.state.sortType ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`) || DEFAULT_SORT_DIR));
const newSortType = (this.state.sortType ?? (localStorage.getItem(USERPAGE_SORT_TYPE) || DEFAULT_SORT_TYPE));
const newSortDir = (this.state.sortDir ?? (localStorage.getItem(USERPAGE_SORT_DIR) || DEFAULT_SORT_DIR));
this.updateUrl(this.state.filterString, newSortType, newSortDir);
const brewCollection = this.props.brewCollection.map((brewGroup)=>{
brewGroup.visible = (localStorage.getItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`) ?? 'true')=='true';
brewGroup.visible = (localStorage.getItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`) ?? 'true')=='true';
return brewGroup;
});
@@ -73,10 +75,10 @@ const ListPage = createClass({
saveToLocalStorage : function() {
this.state.brewCollection.map((brewGroup)=>{
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-VISIBILITY-${brewGroup.class}`, `${brewGroup.visible}`);
localStorage.setItem(`${USERPAGE_GROUP_VISIBILITY_PREFIX}_${brewGroup.class}`, `${brewGroup.visible}`);
});
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTTYPE`, this.state.sortType);
localStorage.setItem(`${USERPAGE_KEY_PREFIX}-SORTDIR`, this.state.sortDir);
localStorage.setItem(USERPAGE_SORT_TYPE, this.state.sortType);
localStorage.setItem(USERPAGE_SORT_DIR, this.state.sortDir);
},
renderBrews : function(brews){

View File

@@ -1,536 +1,415 @@
/* eslint-disable max-lines */
require('./editPage.less');
const React = require('react');
const _ = require('lodash');
const createClass = require('create-react-class');
import {makePatches, applyPatches, stringifyPatches, parsePatches} from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import './editPage.less';
import request from '../../utils/request-middleware.js';
const { Meta } = require('vitreum/headtags');
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
const Nav = require('naturalcrit/nav/nav.jsx');
const Navbar = require('../../navbar/navbar.jsx');
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
const NewBrew = require('../../navbar/newbrew.navitem.jsx');
const HelpNavItem = require('../../navbar/help.navitem.jsx');
const PrintNavItem = require('../../navbar/print.navitem.jsx');
const ErrorNavItem = require('../../navbar/error-navitem.jsx');
const Account = require('../../navbar/account.navitem.jsx');
const RecentNavItem = require('../../navbar/recent.navitem.jsx').both;
const VaultNavItem = require('../../navbar/vault.navitem.jsx');
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
const SplitPane = require('client/components/splitPane/splitPane.jsx');
const Editor = require('../../editor/editor.jsx');
const BrewRenderer = require('../../brewRenderer/brewRenderer.jsx');
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
const LockNotification = require('./lockNotification/lockNotification.jsx');
import Markdown from 'naturalcrit/markdown.js';
const { DEFAULT_BREW_LOAD } = require('../../../../server/brewDefaults.js');
const { printCurrentBrew, fetchThemeBundle } = require('../../../../shared/helpers.js');
// Page specific imports
import { Meta } from 'vitreum/headtags';
import { md5 } from 'hash-wasm';
import { gzipSync, strToU8 } from 'fflate';
import { makePatches, stringifyPatches } from '@sanity/diff-match-patch';
import ShareNavItem from '../../navbar/share.navitem.jsx';
import LockNotification from './lockNotification/lockNotification.jsx';
import { updateHistory, versionHistoryGarbageCollection } from '../../utils/versionHistory.js';
const googleDriveIcon = require('../../googleDrive.svg');
import googleDriveIcon from '../../googleDrive.svg';
const SAVE_TIMEOUT = 10000;
const UNSAVED_WARNING_TIMEOUT = 900000; //Warn user afer 15 minutes of unsaved changes
const UNSAVED_WARNING_POPUP_TIMEOUT = 4000; //Show the warning for 4 seconds
const EditPage = createClass({
displayName : 'EditPage',
getDefaultProps : function() {
return {
brew : DEFAULT_BREW_LOAD
};
},
getInitialState : function() {
return {
brew : this.props.brew,
isSaving : false,
unsavedChanges : false,
alertTrashedGoogleBrew : this.props.brew.trashed,
alertLoginToTransfer : false,
saveGoogle : this.props.brew.googleId ? true : false,
confirmGoogleTransfer : false,
error : null,
htmlErrors : Markdown.validate(this.props.brew.text),
url : '',
autoSave : true,
autoSaveWarning : false,
unsavedTime : new Date(),
currentEditorViewPageNum : 1,
currentEditorCursorPageNum : 1,
currentBrewRendererPageNum : 1,
displayLockMessage : this.props.brew.lock || false,
themeBundle : {}
};
},
const AUTOSAVE_KEY = 'HB_editor_autoSaveOn';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const SNIPKEY = 'HB_newPage_snippets';
const METAKEY = 'HB_newPage_meta';
editor : React.createRef(null),
savedBrew : null,
const useLocalStorage = false;
const neverSaved = false;
componentDidMount : function(){
this.setState({
url : window.location.href
});
const EditPage = (props)=>{
props = {
brew : DEFAULT_BREW_LOAD,
...props
};
this.savedBrew = JSON.parse(JSON.stringify(this.props.brew)); //Deep copy
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
const [lastSavedTime , setLastSavedTime ] = useState(new Date());
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true);
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true);
this.setState({ autoSave: JSON.parse(localStorage.getItem('AUTOSAVE_ON')) ?? true }, ()=>{
if(this.state.autoSave){
this.trySave();
} else {
this.setState({ autoSaveWarning: true });
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
const saveTimeout = useRef(null);
const warnUnsavedTimeout = useRef(null);
const trySaveRef = useRef(trySave); // CTRL+S listener lives outside React and needs ref to use trySave with latest copy of brew
const unsavedChangesRef = useRef(unsavedChanges); // Similarly, onBeforeUnload lives outside React and needs ref to unsavedChanges
useEffect(()=>{
const autoSavePref = JSON.parse(localStorage.getItem(AUTOSAVE_KEY) ?? true);
setAutoSaveEnabled(autoSavePref);
setWarnUnsavedChanges(!autoSavePref);
setHTMLErrors(Markdown.validate(currentBrew.text));
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
});
};
document.addEventListener('keydown', handleControlKeys);
window.onbeforeunload = ()=>{
if(this.state.isSaving || this.state.unsavedChanges){
if(unsavedChangesRef.current)
return 'You have unsaved changes!';
}
};
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
window.onBeforeUnload = null;
};
}, []);
this.setState((prevState)=>({
htmlErrors : Markdown.validate(prevState.brew.text)
}));
useEffect(()=>{
trySaveRef.current = trySave;
unsavedChangesRef.current = unsavedChanges;
});
fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, this.props.brew.renderer, this.props.brew.theme);
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
document.addEventListener('keydown', this.handleControlKeys);
},
componentWillUnmount : function() {
window.onbeforeunload = function(){};
document.removeEventListener('keydown', this.handleControlKeys);
},
componentDidUpdate : function(){
const hasChange = this.hasChanges();
if(this.state.unsavedChanges != hasChange){
this.setState({
unsavedChanges : hasChange
});
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current?.update();
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
},
};
handleControlKeys : function(e){
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode == S_KEY) this.trySave(true);
if(e.keyCode == P_KEY) printCurrentBrew();
if(e.keyCode == P_KEY || e.keyCode == S_KEY){
e.stopPropagation();
e.preventDefault();
}
},
const updateBrew = (newData)=>setCurrentBrew((prevBrew)=>({
...prevBrew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}));
handleSplitMove : function(){
this.editor.current.update();
},
const resetWarnUnsavedTimer = ()=>{
setTimeout(()=>setWarnUnsavedChanges(false), UNSAVED_WARNING_POPUP_TIMEOUT); // Hide the warning after 4 seconds
clearTimeout(warnUnsavedTimeout.current);
warnUnsavedTimeout.current = setTimeout(()=>setWarnUnsavedChanges(true), UNSAVED_WARNING_TIMEOUT); // 15 minutes between unsaved work warnings
};
handleEditorViewPageChange : function(pageNumber){
this.setState({ currentEditorViewPageNum: pageNumber });
},
handleEditorCursorPageChange : function(pageNumber){
this.setState({ currentEditorCursorPageNum: pageNumber });
},
handleBrewRendererPageChange : function(pageNumber){
this.setState({ currentBrewRendererPageNum: pageNumber });
},
handleTextChange : function(text){
//If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(text);
this.setState((prevState)=>({
brew : { ...prevState.brew, text: text },
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleSnipChange : function(snippet){
//If there are errors, run the validator on every change to give quick feedback
let htmlErrors = this.state.htmlErrors;
if(htmlErrors.length) htmlErrors = Markdown.validate(snippet);
this.setState((prevState)=>({
brew : { ...prevState.brew, snippets: snippet },
unsavedChanges : true,
htmlErrors : htmlErrors,
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleStyleChange : function(style){
this.setState((prevState)=>({
brew : { ...prevState.brew, style: style }
}), ()=>{if(this.state.autoSave) this.trySave();});
},
handleMetaChange : function(metadata, field=undefined){
if(field == 'theme' || field == 'renderer') // Fetch theme bundle only if theme or renderer was changed
fetchThemeBundle((err)=>{this.setState({ error: err })}, (theme)=>{this.setState({ themeBundle: theme })}, metadata.renderer, metadata.theme);
this.setState((prevState)=>({
brew : {
...prevState.brew,
...metadata
}
}), ()=>{if(this.state.autoSave) this.trySave();});
},
hasChanges : function(){
return !_.isEqual(this.state.brew, this.savedBrew);
},
updateBrew : function(newData){
this.setState((prevState)=>({
brew : {
...prevState.brew,
style : newData.style,
text : newData.text,
snippets : newData.snippets
}
}));
},
trySave : function(immediate=false){
if(!this.debounceSave) this.debounceSave = _.debounce(this.save, SAVE_TIMEOUT);
if(this.state.isSaving)
return;
if(immediate) {
this.debounceSave();
this.debounceSave.flush();
return;
}
if(this.hasChanges())
this.debounceSave();
else
this.debounceSave.cancel();
},
handleGoogleClick : function(){
const handleGoogleClick = ()=>{
if(!global.account?.googleId) {
this.setState({
alertLoginToTransfer : true
});
setAlertLoginToTransfer(true);
return;
}
this.setState((prevState)=>({
confirmGoogleTransfer : !prevState.confirmGoogleTransfer
}));
this.setState({
error : null
});
},
closeAlerts : function(event){
event.stopPropagation(); //Only handle click once so alert doesn't reopen
this.setState({
alertTrashedGoogleBrew : false,
alertLoginToTransfer : false,
confirmGoogleTransfer : false
});
},
setConfirmGoogleTransfer((prev)=>!prev);
setError(null);
};
toggleGoogleStorage : function(){
this.setState((prevState)=>({
saveGoogle : !prevState.saveGoogle,
error : null
}), ()=>this.trySave(true));
},
const closeAlerts = (e)=>{
e.stopPropagation(); //Only handle click once so alert doesn't reopen
setAlertTrashedGoogleBrew(false);
setAlertLoginToTransfer(false);
setConfirmGoogleTransfer(false);
};
save : async function(){
if(this.debounceSave && this.debounceSave.cancel) this.debounceSave.cancel();
const toggleGoogleStorage = ()=>{
setSaveGoogle((prev)=>!prev);
setError(null);
trySave(true);
};
const brewState = this.state.brew; // freeze the current state
const preSaveSnapshot = { ...brewState };
const trySave = (immediate = false, hasChanges = true)=>{
clearTimeout(saveTimeout.current);
if(isSaving) return;
if(!hasChanges && !immediate) return;
const newTimeout = immediate ? 0 : SAVE_TIMEOUT;
this.setState((prevState)=>({
isSaving : true,
error : null,
htmlErrors : Markdown.validate(prevState.brew.text)
}));
saveTimeout.current = setTimeout(async ()=>{
setIsSaving(true);
setError(null);
await save(currentBrew, saveGoogle)
.catch((err)=>{
setError(err);
});
setIsSaving(false);
setLastSavedTime(new Date());
if(!autoSaveEnabled) resetWarnUnsavedTimer();
}, newTimeout);
};
await updateHistory(this.state.brew).catch(console.error);
const save = async (brew, saveToGoogle)=>{
setHTMLErrors(Markdown.validate(brew.text));
await updateHistory(brew).catch(console.error);
await versionHistoryGarbageCollection().catch(console.error);
//Prepare content to send to server
const brew = { ...brewState };
brew.text = brew.text.normalize('NFC');
this.savedBrew.text = this.savedBrew.text.normalize('NFC');
brew.pageCount = ((brew.renderer=='legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1;
brew.patches = stringifyPatches(makePatches(encodeURI(this.savedBrew.text), encodeURI(brew.text)));
brew.hash = await md5(this.savedBrew.text);
//brew.text = undefined; - Temporary parallel path
brew.textBin = undefined;
const brewToSave = {
...brew,
text : brew.text.normalize('NFC'),
pageCount : ((brew.renderer === 'legacy' ? brew.text.match(/\\page/g) : brew.text.match(/^\\page$/gm)) || []).length + 1,
patches : stringifyPatches(makePatches(encodeURI(lastSavedBrew.current.text.normalize('NFC')), encodeURI(brew.text.normalize('NFC')))),
hash : await md5(lastSavedBrew.current.text),
textBin : undefined,
version : lastSavedBrew.current.version
};
const compressedBrew = gzipSync(strToU8(JSON.stringify(brew)));
const compressedBrew = gzipSync(strToU8(JSON.stringify(brewToSave)));
const transfer = saveToGoogle === _.isNil(brew.googleId);
const params = transfer ? `?${saveToGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : '';
const transfer = this.state.saveGoogle == _.isNil(this.state.brew.googleId);
const params = `${transfer ? `?${this.state.saveGoogle ? 'saveToGoogle' : 'removeFromGoogle'}=true` : ''}`;
const res = await request
.put(`/api/update/${brew.editId}${params}`)
.put(`/api/update/${brewToSave.editId}${params}`)
.set('Content-Encoding', 'gzip')
.set('Content-Type', 'application/json')
.send(compressedBrew)
.catch((err)=>{
console.log('Error Updating Local Brew');
this.setState({ error: err });
console.error('Error Updating Local Brew');
setError(err);
});
if(!res) return;
this.savedBrew = {
...preSaveSnapshot,
googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
const updatedFields = {
googleId : res.body.googleId ?? null,
editId : res.body.editId,
shareId : res.body.shareId,
version : res.body.version
};
this.setState((prevState) => ({
brew: {
...prevState.brew,
googleId : res.body.googleId ? res.body.googleId : null,
editId : res.body.editId,
shareId : res.body.shareId,
version : res.body.version
},
isSaving : false,
unsavedTime : new Date()
}), ()=>{
this.setState({ unsavedChanges : this.hasChanges() });
});
lastSavedBrew.current = {
...brew,
...updatedFields
};
history.replaceState(null, null, `/edit/${this.savedBrew.editId}`);
},
setCurrentBrew((prevBrew)=>({
...prevBrew,
...updatedFields
}));
renderGoogleDriveIcon : function(){
return <Nav.item className='googleDriveStorage' onClick={this.handleGoogleClick}>
<img src={googleDriveIcon} className={this.state.saveGoogle ? '' : 'inactive'} alt='Google Drive icon'/>
history.replaceState(null, null, `/edit/${res.body.editId}`);
};
{this.state.confirmGoogleTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
{ this.state.saveGoogle
? `Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?`
: `Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?`
}
const renderGoogleDriveIcon = ()=>(
<Nav.item className='googleDriveStorage' onClick={handleGoogleClick}>
<img src={googleDriveIcon} className={saveGoogle ? '' : 'inactive'} alt='Google Drive icon' />
{confirmGoogleTransfer && (
<div className='errorContainer' onClick={closeAlerts}>
{saveGoogle
? 'Would you like to transfer this brew from your Google Drive storage back to the Homebrewery?'
: 'Would you like to transfer this brew from the Homebrewery to your personal Google Drive storage?'}
<br />
<div className='confirm' onClick={this.toggleGoogleStorage}>
Yes
</div>
<div className='deny'>
No
</div>
<div className='confirm' onClick={toggleGoogleStorage}> Yes </div>
<div className='deny'> No </div>
</div>
}
)}
{this.state.alertLoginToTransfer &&
<div className='errorContainer' onClick={this.closeAlerts}>
You must be signed in to a Google account to transfer
between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer'
href={`https://www.naturalcrit.com/login?redirect=${this.state.url}`}>
<div className='confirm'>
Sign In
</div>
{alertLoginToTransfer && (
<div className='errorContainer' onClick={closeAlerts}>
You must be signed in to a Google account to transfer between the homebrewery and Google Drive!
<a target='_blank' rel='noopener noreferrer' href={`https://www.naturalcrit.com/login?redirect=${window.location.href}`}>
<div className='confirm'> Sign In </div>
</a>
<div className='deny'>
Not Now
</div>
<div className='deny'> Not Now </div>
</div>
}
)}
{this.state.alertTrashedGoogleBrew &&
<div className='errorContainer' onClick={this.closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br />If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'>
OK
</div>
{alertTrashedGoogleBrew && (
<div className='errorContainer' onClick={closeAlerts}>
This brew is currently in your Trash folder on Google Drive!<br />
If you want to keep it, make sure to move it before it is deleted permanently!<br />
<div className='confirm'> OK </div>
</div>
}
</Nav.item>;
},
renderSaveButton : function(){
)}
</Nav.item>
);
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(this.state.isSaving){
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
}
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
if(this.state.unsavedChanges && this.state.autoSaveWarning){
this.setAutosaveWarning();
const elapsedTime = Math.round((new Date() - this.state.unsavedTime) / 1000 / 60);
const text = elapsedTime == 0 ? 'Autosave is OFF.' : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
if(unsavedChanges && warnUnsavedChanges) {
resetWarnUnsavedTimer();
const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
const text = elapsedTime === 0
? 'Autosave is OFF.'
: `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
Reminder...
<div className='errorContainer'>
{text}
</div>
Reminder...
<div className='errorContainer'>{text}</div>
</Nav.item>;
}
// #3 - Unsaved changes exist, click to save, show SAVE NOW
// Use trySave(true) instead of save() to use debounced save function
if(this.state.unsavedChanges){
return <Nav.item className='save' onClick={()=>this.trySave(true)} color='blue' icon='fas fa-save'>Save Now</Nav.item>;
}
if(unsavedChanges)
return <Nav.item className='save' onClick={()=>trySave(true)} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(this.state.autoSave){
return <Nav.item className='save saved'>auto-saved.</Nav.item>;
}
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved.</Nav.item>;
},
return <Nav.item className='save saved'>saved</Nav.item>;
};
handleAutoSave : function(){
if(this.warningTimer) clearTimeout(this.warningTimer);
this.setState((prevState)=>({
autoSave : !prevState.autoSave,
autoSaveWarning : prevState.autoSave
}), ()=>{
localStorage.setItem('AUTOSAVE_ON', JSON.stringify(this.state.autoSave));
});
},
const toggleAutoSave = ()=>{
clearTimeout(warnUnsavedTimeout.current);
clearTimeout(saveTimeout.current);
localStorage.setItem(AUTOSAVE_KEY, JSON.stringify(!autoSaveEnabled));
setAutoSaveEnabled(!autoSaveEnabled);
setWarnUnsavedChanges(autoSaveEnabled);
};
setAutosaveWarning : function(){
setTimeout(()=>this.setState({ autoSaveWarning: false }), 4000); // 4 seconds to display
this.warningTimer = setTimeout(()=>{this.setState({ autoSaveWarning: true });}, 900000); // 15 minutes between warnings
this.warningTimer;
},
const renderAutoSaveButton = ()=>(
<Nav.item onClick={toggleAutoSave}>
Autosave <i className={autoSaveEnabled ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>
);
errorReported : function(error) {
this.setState({
error
});
},
renderAutoSaveButton : function(){
return <Nav.item onClick={this.handleAutoSave}>
Autosave <i className={this.state.autoSave ? 'fas fa-power-off active' : 'fas fa-power-off'}></i>
</Nav.item>;
},
processShareId : function() {
return this.state.brew.googleId && !this.state.brew.stubbed ?
this.state.brew.googleId + this.state.brew.shareId :
this.state.brew.shareId;
},
getRedditLink : function(){
const shareLink = this.processShareId();
const systems = this.props.brew.systems.length > 0 ? ` [${this.props.brew.systems.join(' - ')}]` : '';
const title = `${this.props.brew.title} ${systems}`;
const text = `Hey guys! I've been working on this homebrew. I'd love your feedback. Check it out.
**[Homebrewery Link](${global.config.baseUrl}/share/${shareLink})**`;
return `https://www.reddit.com/r/UnearthedArcana/submit?title=${encodeURIComponent(title.toWellFormed())}&text=${encodeURIComponent(text)}`;
},
clearError : function(){
setState({
error : null,
isSaving : false
})
},
renderNavbar : function(){
const shareLink = this.processShareId();
const clearError = ()=>{
setError(null);
setIsSaving(false);
};
const renderNavbar = ()=>{
return <Navbar>
<Nav.section>
<Nav.item className='brewTitle'>{this.state.brew.title}</Nav.item>
<Nav.item className='brewTitle'>{currentBrew.title}</Nav.item>
</Nav.section>
<Nav.section>
{this.renderGoogleDriveIcon()}
{this.state.error ?
<ErrorNavItem error={this.state.error} clearError={this.clearError}></ErrorNavItem> :
<Nav.dropdown className='save-menu'>
{this.renderSaveButton()}
{this.renderAutoSaveButton()}
</Nav.dropdown>
}
<NewBrew />
<HelpNavItem/>
<Nav.dropdown>
<Nav.item color='teal' icon='fas fa-share-alt'>
share
</Nav.item>
<Nav.item color='blue' href={`/share/${shareLink}`}>
view
</Nav.item>
<Nav.item color='blue' onClick={()=>{navigator.clipboard.writeText(`${global.config.baseUrl}/share/${shareLink}`);}}>
copy url
</Nav.item>
<Nav.item color='blue' href={this.getRedditLink()} newTab={true} rel='noopener noreferrer'>
post to reddit
</Nav.item>
</Nav.dropdown>
{renderGoogleDriveIcon()}
{error
? <ErrorNavItem error={error} clearError={clearError} />
: <Nav.dropdown className='save-menu'>
{renderSaveButton()}
{renderAutoSaveButton()}
</Nav.dropdown>}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem brew={this.state.brew} storageKey='edit' />
<Account />
<ShareNavItem brew={currentBrew} />
<RecentNavItem brew={currentBrew} storageKey='edit' />
<AccountNavItem/>
</Nav.section>
</Navbar>;
},
};
render : function(){
return <div className='editPage sitePage'>
return (
<div className='editPage sitePage'>
<Meta name='robots' content='noindex, nofollow' />
{this.renderNavbar()}
{this.props.brew.lock && <LockNotification shareId={this.props.brew.shareId} message={this.props.brew.lock.editMessage} reviewRequested={this.props.brew.lock.reviewRequested} />}
{renderNavbar()}
{currentBrew.lock && <LockNotification shareId={currentBrew.shareId} message={currentBrew.lock.editMessage} reviewRequested={currentBrew.lock.reviewRequested}/>}
<div className='content'>
<SplitPane onDragFinish={this.handleSplitMove}>
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={this.editor}
brew={this.state.brew}
onTextChange={this.handleTextChange}
onStyleChange={this.handleStyleChange}
onSnipChange={this.handleSnipChange}
onMetaChange={this.handleMetaChange}
reportError={this.errorReported}
renderer={this.state.brew.renderer}
userThemes={this.props.userThemes}
themeBundle={this.state.themeBundle}
updateBrew={this.updateBrew}
onCursorPageChange={this.handleEditorCursorPageChange}
onViewPageChange={this.handleEditorViewPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
ref={editorRef}
brew={currentBrew}
onBrewChange={handleBrewChange}
reportError={setError}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
updateBrew={updateBrew}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={this.state.brew.text}
style={this.state.brew.style}
renderer={this.state.brew.renderer}
theme={this.state.brew.theme}
themeBundle={this.state.themeBundle}
errors={this.state.htmlErrors}
lang={this.state.brew.lang}
onPageChange={this.handleBrewRendererPageChange}
currentEditorViewPageNum={this.state.currentEditorViewPageNum}
currentEditorCursorPageNum={this.state.currentEditorCursorPageNum}
currentBrewRendererPageNum={this.state.currentBrewRendererPageNum}
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
theme={currentBrew.theme}
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
allowPrint={true}
/>
</SplitPane>
</div>
</div>;
}
});
</div>
);
};
module.exports = EditPage;

View File

@@ -1,51 +1,84 @@
/* eslint-disable max-lines */
import './homePage.less';
import React from 'react';
import { useEffect, useState, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import { Meta } from 'vitreum/headtags';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import { fetchThemeBundle } from '../../../../shared/helpers.js';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
// Page specific imports
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const SNIPKEY = 'homebrewery-new-snippets';
const METAKEY = 'homebrewery-new-meta';
const useLocalStorage = false;
const neverSaved = true;
const HomePage =(props)=>{
props = {
brew : DEFAULT_BREW,
ver : '0.0.0',
...props
};
...props
};
const [brew , setBrew] = useState(props.brew);
const [welcomeText , setWelcomeText] = useState(props.brew.text);
const [currentBrew , setCurrentBrew] = useState(props.brew);
const [error , setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false);
const editorRef = useRef(null);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{
fetchThemeBundle(setError, setThemeBundle, brew.renderer, brew.theme);
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('keydown', handleControlKeys);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
}, []);
const save = ()=>{
request.post('/api')
.send(brew)
.send(currentBrew)
.end((err, res)=>{
if(err) {
setError(err);
@@ -56,24 +89,73 @@ const HomePage =(props)=>{
});
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const handleTextChange = (text)=>{
setBrew((prevBrew) => ({ ...prevBrew, text }));
const renderSaveButton = ()=>{
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0
// ? 'Autosave is OFF.'
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
@@ -84,11 +166,11 @@ const HomePage =(props)=>{
const renderNavbar = ()=>{
return <Navbar ver={props.ver}>
<Nav.section>
{error ?
<ErrorNavItem error={error} clearError={clearError}></ErrorNavItem> :
null
}
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
@@ -105,22 +187,22 @@ const HomePage =(props)=>{
<SplitPane onDragFinish={handleSplitMove}>
<Editor
ref={editorRef}
brew={brew}
onTextChange={handleTextChange}
renderer={brew.renderer}
brew={currentBrew}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
showEditButtons={false}
themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
/>
<BrewRenderer
text={brew.text}
style={brew.style}
renderer={brew.renderer}
onPageChange={handleBrewRendererPageChange}
text={currentBrew.text}
style={currentBrew.style}
renderer={currentBrew.renderer}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -128,7 +210,7 @@ const HomePage =(props)=>{
/>
</SplitPane>
</div>
<div className={`floatingSaveButton${welcomeText !== brew.text ? ' show' : ''}`} onClick={save}>
<div className={`floatingSaveButton${unsavedChanges ? ' show' : ''}`} onClick={save}>
Save current <i className='fas fa-save' />
</div>
@@ -136,7 +218,7 @@ const HomePage =(props)=>{
Create your own <i className='fas fa-magic' />
</a>
</div>
)
);
};
module.exports = HomePage;

View File

@@ -34,7 +34,13 @@
}
.navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange;
&:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
opacity: 0;
}
}
}

View File

@@ -1,29 +1,40 @@
/*eslint max-lines: ["warn", {"max": 300, "skipBlankLines": true, "skipComments": true}]*/
/* eslint-disable max-lines */
import './newPage.less';
// Common imports
import React, { useState, useEffect, useRef } from 'react';
import request from '../../utils/request-middleware.js';
import Markdown from 'naturalcrit/markdown.js';
import _ from 'lodash';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import SplitPane from 'client/components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
import BrewRenderer from '../../brewRenderer/brewRenderer.jsx';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '../../../../shared/helpers.js';
import Nav from 'naturalcrit/nav/nav.jsx';
import Navbar from '../../navbar/navbar.jsx';
import NewBrewItem from '../../navbar/newbrew.navitem.jsx';
import AccountNavItem from '../../navbar/account.navitem.jsx';
import ErrorNavItem from '../../navbar/error-navitem.jsx';
import HelpNavItem from '../../navbar/help.navitem.jsx';
import VaultNavItem from '../../navbar/vault.navitem.jsx';
import PrintNavItem from '../../navbar/print.navitem.jsx';
import { both as RecentNavItem } from '../../navbar/recent.navitem.jsx';
const BREWKEY = 'homebrewery-new';
const STYLEKEY = 'homebrewery-new-style';
const METAKEY = 'homebrewery-new-meta';
const SAVEKEY = `HOMEBREWERY-DEFAULT-SAVE-LOCATION-${global.account?.username || ''}`;
// Page specific imports
import { Meta } from 'vitreum/headtags';
const BREWKEY = 'HB_newPage_content';
const STYLEKEY = 'HB_newPage_style';
const METAKEY = 'HB_newPage_metadata';
const SNIPKEY = 'HB_newPage_snippets';
const SAVEKEYPREFIX = 'HB_editor_defaultSave_';
const useLocalStorage = true;
const neverSaved = true;
const NewPage = (props)=>{
props = {
@@ -40,14 +51,28 @@ const NewPage = (props)=>{
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const editorRef = useRef(null);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));
useEffect(()=>{
document.addEventListener('keydown', handleControlKeys);
loadBrew();
fetchThemeBundle(setError, setThemeBundle, currentBrew.renderer, currentBrew.theme);
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
if(e.keyCode === 83) trySaveRef.current(true);
if(e.keyCode === 80) printCurrentBrew();
if([83, 80].includes(e.keyCode)) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener('keydown', handleControlKeys);
return ()=>{
document.removeEventListener('keydown', handleControlKeys);
};
@@ -67,9 +92,11 @@ const NewPage = (props)=>{
brew.lang = metaStorage?.lang ?? brew.lang;
}
const SAVEKEY = `${SAVEKEYPREFIX}${global.account?.username}`;
const saveStorage = localStorage.getItem(SAVEKEY) || 'HOMEBREWERY';
setCurrentBrew(brew);
lastSavedBrew.current = brew;
setSaveGoogle(saveStorage == 'GOOGLE-DRIVE' && saveGoogle);
localStorage.setItem(BREWKEY, brew.text);
@@ -80,68 +107,38 @@ const NewPage = (props)=>{
window.history.replaceState({}, window.location.title, '/new/');
};
const handleControlKeys = (e)=>{
if(!(e.ctrlKey || e.metaKey)) return;
const S_KEY = 83;
const P_KEY = 80;
if(e.keyCode === S_KEY) save();
if(e.keyCode === P_KEY) printCurrentBrew();
if(e.keyCode === S_KEY || e.keyCode === P_KEY) {
e.preventDefault();
e.stopPropagation();
}
};
useEffect(()=>{
const hasChange = !_.isEqual(currentBrew, lastSavedBrew.current);
setUnsavedChanges(hasChange);
if(autoSaveEnabled) trySave(false, hasChange);
}, [currentBrew]);
const handleSplitMove = ()=>{
editorRef.current.update();
};
const handleEditorViewPageChange = (pageNumber)=>{
setCurrentEditorViewPageNum(pageNumber);
};
const handleBrewChange = (field)=>(value, subfield)=>{ //'text', 'style', 'snippets', 'metadata'
if(subfield == 'renderer' || subfield == 'theme')
fetchThemeBundle(setError, setThemeBundle, value.renderer, value.theme);
const handleEditorCursorPageChange = (pageNumber)=>{
setCurrentEditorCursorPageNum(pageNumber);
};
const handleBrewRendererPageChange = (pageNumber)=>{
setCurrentBrewRendererPageNum(pageNumber);
};
const handleTextChange = (text)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length)
HTMLErrors = Markdown.validate(text);
if(HTMLErrors.length && (field == 'text' || field == 'snippets'))
setHTMLErrors(Markdown.validate(value));
setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew)=>({ ...prevBrew, text }));
localStorage.setItem(BREWKEY, text);
};
if(field == 'metadata') setCurrentBrew((prev)=>({ ...prev, ...value }));
else setCurrentBrew((prev)=>({ ...prev, [field]: value }));
const handleStyleChange = (style)=>{
setCurrentBrew((prevBrew)=>({ ...prevBrew, style }));
localStorage.setItem(STYLEKEY, style);
};
const handleSnipChange = (snippet)=>{
//If there are HTML errors, run the validator on every change to give quick feedback
if(HTMLErrors.length)
HTMLErrors = Markdown.validate(snippet);
setHTMLErrors(HTMLErrors);
setCurrentBrew((prevBrew)=>({ ...prevBrew, snippets: snippet }));
};
const handleMetaChange = (metadata, field = undefined)=>{
if(field === 'theme' || field === 'renderer')
fetchThemeBundle(setError, setThemeBundle, metadata.renderer, metadata.theme);
setCurrentBrew((prev)=>({ ...prev, ...metadata }));
localStorage.setItem(METAKEY, JSON.stringify({
renderer : metadata.renderer,
theme : metadata.theme,
lang : metadata.lang
}));
if(useLocalStorage) {
if(field == 'text') localStorage.setItem(BREWKEY, value);
if(field == 'style') localStorage.setItem(STYLEKEY, value);
if(field == 'snippets') localStorage.setItem(SNIPKEY, value);
if(field == 'metadata') localStorage.setItem(METAKEY, JSON.stringify({
renderer : value.renderer,
theme : value.theme,
lang : value.lang
}));
}
};
const save = async ()=>{
@@ -173,15 +170,38 @@ const NewPage = (props)=>{
};
const renderSaveButton = ()=>{
if(isSaving){
return <Nav.item icon='fas fa-spinner fa-spin' className='save'>
save...
</Nav.item>;
} else {
return <Nav.item icon='fas fa-save' className='save' onClick={save}>
save
</Nav.item>;
}
// #1 - Currently saving, show SAVING
if(isSaving)
return <Nav.item className='save' icon='fas fa-spinner fa-spin'>saving...</Nav.item>;
// #2 - Unsaved changes exist, autosave is OFF and warning timer has expired, show AUTOSAVE WARNING
// if(unsavedChanges && warnUnsavedChanges) {
// resetWarnUnsavedTimer();
// const elapsedTime = Math.round((new Date() - lastSavedTime) / 1000 / 60);
// const text = elapsedTime === 0
// ? 'Autosave is OFF.'
// : `Autosave is OFF, and you haven't saved for ${elapsedTime} minutes.`;
// return <Nav.item className='save error' icon='fas fa-exclamation-circle'>
// Reminder...
// <div className='errorContainer'>{text}</div>
// </Nav.item>;
// }
// #3 - Unsaved changes exist, click to save, show SAVE NOW
if(unsavedChanges)
return <Nav.item className='save' onClick={save} color='blue' icon='fas fa-save'>save now</Nav.item>;
// #4 - No unsaved changes, autosave is ON, show AUTO-SAVED
if(autoSaveEnabled)
return <Nav.item className='save saved'>auto-saved</Nav.item>;
// #5 - No unsaved changes, and has never been saved, hide the button
if(neverSaved)
return <Nav.item className='save neverSaved'>save now</Nav.item>;
// DEFAULT - No unsaved changes, show SAVED
return <Nav.item className='save saved'>saved</Nav.item>;
};
const clearError = ()=>{
@@ -199,8 +219,10 @@ const NewPage = (props)=>{
{error
? <ErrorNavItem error={error} clearError={clearError} />
: renderSaveButton()}
<NewBrewItem />
<PrintNavItem />
<HelpNavItem />
<VaultNavItem />
<RecentNavItem />
<AccountNavItem />
</Nav.section>
@@ -215,15 +237,12 @@ const NewPage = (props)=>{
<Editor
ref={editorRef}
brew={currentBrew}
onTextChange={handleTextChange}
onStyleChange={handleStyleChange}
onMetaChange={handleMetaChange}
onSnipChange={handleSnipChange}
onBrewChange={handleBrewChange}
renderer={currentBrew.renderer}
userThemes={props.userThemes}
themeBundle={themeBundle}
onCursorPageChange={handleEditorCursorPageChange}
onViewPageChange={handleEditorViewPageChange}
onCursorPageChange={setCurrentEditorCursorPageNum}
onViewPageChange={setCurrentEditorViewPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}
@@ -236,7 +255,7 @@ const NewPage = (props)=>{
themeBundle={themeBundle}
errors={HTMLErrors}
lang={currentBrew.lang}
onPageChange={handleBrewRendererPageChange}
onPageChange={setCurrentBrewRendererPageNum}
currentEditorViewPageNum={currentEditorViewPageNum}
currentEditorCursorPageNum={currentEditorCursorPageNum}
currentBrewRendererPageNum={currentBrewRendererPageNum}

View File

@@ -1,6 +1,12 @@
.newPage {
.navItem.save {
.fadeInRight();
.transition(opacity);
background-color : @orange;
&:hover { background-color : @green; }
&.neverSaved {
.fadeOutRight();
opacity: 0;
}
}
}

View File

@@ -1,14 +1,16 @@
.vaultPage {
height : 100%;
overflow-y : hidden;
background-color : #2C3E50;
*:not(input) { user-select : none; }
.form {
background:white;
}
:where(.content .dataGroup) {
width : 100%;
height : 100%;
background : white;
&.form .brewLookup {
position : relative;
@@ -171,7 +173,6 @@
max-height : 100%;
padding : 70px 50px;
overflow-y : scroll;
background-color : #2C3E50;
container-type : inline-size;
h3 { font-size : 25px; }

View File

@@ -0,0 +1,35 @@
const getLocalStorageMap = function(){
const localStorageMap = {
'AUTOSAVE_ON' : 'HB_editor_autoSaveOn',
'HOMEBREWERY-EDITOR-THEME' : 'HB_editor_theme',
'liveScroll' : 'HB_editor_liveScroll',
'naturalcrit-pane-split' : 'HB_editor_splitWidth',
'HOMEBREWERY-LISTPAGE-SORTDIR' : 'HB_listPage_sortDir',
'HOMEBREWERY-LISTPAGE-SORTTYPE' : 'HB_listPage_sortType',
'HOMEBREWERY-LISTPAGE-VISIBILITY-published' : 'HB_listPage_visibility_group_published',
'HOMEBREWERY-LISTPAGE-VISIBILITY-unpublished' : 'HB_listPage_visibility_group_unpublished',
'hbAdminTab' : 'HB_adminPage_currentTab',
'homebrewery-new' : 'HB_newPage_content',
'homebrewery-new-meta' : 'HB_newPage_metadata',
'homebrewery-new-style' : 'HB_newPage_style',
'homebrewery-recently-edited' : 'HB_nav_recentlyEdited',
'homebrewery-recently-viewed' : 'HB_nav_recentlyViewed',
'hb_toolbarState' : 'HB_renderer_toolbarState',
'hb_toolbarVisibility' : 'HB_renderer_toolbarVisibility'
};
if(global?.account?.username){
const username = global.account.username;
localStorageMap[`HOMEBREWERY-DEFAULT-SAVE-LOCATION-${username}`] = `HB_editor_defaultSave_${username}`;
}
return localStorageMap;
};
export default getLocalStorageMap;

View File

@@ -0,0 +1,22 @@
import getLocalStorageMap from './localStorageKeyMap.js';
const updateLocalStorage = function(){
// Return if no window and thus no local storage
if(typeof window === 'undefined') return;
const localStorageKeyMap = getLocalStorageMap();
const storage = window.localStorage;
Object.keys(localStorageKeyMap).forEach((key)=>{
if(storage[key]){
if(!storage[localStorageKeyMap[key]]){
const data = storage.getItem(key);
storage.setItem(localStorageKeyMap[key], data);
};
storage.removeItem(key);
}
});
};
export { updateLocalStorage };

View File

@@ -1,4 +1,5 @@
{
"development": true,
"host" : "homebrewery.local.naturalcrit.com:8000",
"naturalcrit_url" : "local.naturalcrit.com:8010",
"secret" : "secret",

570
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -138,16 +138,16 @@
"devDependencies": {
"@stylistic/stylelint-plugin": "^4.0.0",
"babel-plugin-transform-import-meta": "^2.3.3",
"eslint": "^9.34.0",
"eslint": "^9.35.0",
"eslint-plugin-jest": "^29.0.1",
"eslint-plugin-react": "^7.37.5",
"globals": "^16.3.0",
"jest": "^30.0.5",
"jest": "^30.1.3",
"jest-expect-message": "^1.1.3",
"jsdom-global": "^3.0.2",
"postcss-less": "^6.0.0",
"stylelint": "^16.23.1",
"stylelint-config-recess-order": "^7.2.0",
"stylelint": "^16.24.0",
"stylelint-config-recess-order": "^7.3.0",
"stylelint-config-recommended": "^17.0.0",
"supertest": "^7.1.4"
}

View File

@@ -27,7 +27,10 @@ const disconnect = async ()=>{
};
const connect = async (config)=>{
return await Mongoose.connect(getMongoDBURL(config), { retryWrites: false })
return await Mongoose.connect(getMongoDBURL(config), {
retryWrites : false,
autoIndex : (config.get('local_environments').includes(config.get('node_env')))
})
.catch((error)=>handleConnectionError(error));
};

View File

@@ -8,9 +8,9 @@ import Markdown from '../shared/naturalcrit/markdown.js';
import yaml from 'js-yaml';
import asyncHandler from 'express-async-handler';
import { nanoid } from 'nanoid';
import {makePatches, applyPatches, stringifyPatches, parsePatch} from '@sanity/diff-match-patch';
import { makePatches, applyPatches, stringifyPatches, parsePatch } from '@sanity/diff-match-patch';
import { md5 } from 'hash-wasm';
import { splitTextStyleAndMetadata,
import { splitTextStyleAndMetadata,
brewSnippetsToJSON, debugTextMismatch } from '../shared/helpers.js';
import checkClientVersion from './middleware/check-client-version.js';
@@ -377,14 +377,14 @@ const api = {
// Patch to a throwaway variable while parallelizing - we're more concerned with error/no error.
const patchedResult = decodeURI(applyPatches(patches, encodeURI(brewFromServer.text))[0]);
if(patchedResult != brewFromClient.text)
throw("Patches did not apply cleanly, text mismatch detected");
throw ('Patches did not apply cleanly, text mismatch detected');
// brew.text = applyPatches(patches, brewFromServer.text)[0];
} catch (err) {
//debugTextMismatch(brewFromClient.text, brewFromServer.text, `edit/${brewFromClient.editId}`);
console.error('Failed to apply patches:', {
//patches : brewFromClient.patches,
brewId : brewFromClient.editId || 'unknown',
error : err
brewId : brewFromClient.editId || 'unknown',
error : err
});
// While running in parallel, don't throw the error upstream.
// throw err; // rethrow to preserve the 500 behavior

View File

@@ -7,29 +7,29 @@ import zlib from 'zlib';
const HomebrewSchema = mongoose.Schema({
shareId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
editId : { type: String, default: ()=>{return nanoid(12);}, index: { unique: true } },
googleId : { type: String },
title : { type: String, default: '' },
googleId : { type: String, index: true },
title : { type: String, default: '', index: true },
text : { type: String, default: '' },
textBin : { type: Buffer },
pageCount : { type: Number, default: 1 },
pageCount : { type: Number, default: 1, index: true },
description : { type: String, default: '' },
tags : [String],
tags : { type: [String], index: true },
systems : [String],
lang : { type: String, default: 'en' },
renderer : { type: String, default: '' },
authors : [String],
lang : { type: String, default: 'en', index: true },
renderer : { type: String, default: '', index: true },
authors : { type: [String], index: true },
invitedAuthors : [String],
published : { type: Boolean, default: false },
thumbnail : { type: String, default: '' },
published : { type: Boolean, default: false, index: true },
thumbnail : { type: String, default: '', index: true },
createdAt : { type: Date, default: Date.now },
updatedAt : { type: Date, default: Date.now },
lastViewed : { type: Date, default: Date.now },
createdAt : { type: Date, default: Date.now, index: true },
updatedAt : { type: Date, default: Date.now, index: true },
lastViewed : { type: Date, default: Date.now, index: true },
views : { type: Number, default: 0 },
version : { type: Number, default: 1 },
version : { type: Number, default: 1, index: true },
lock : { type: Object }
lock : { type: Object, index: true }
}, { versionKey: false });
HomebrewSchema.statics.increaseView = async function(query) {
@@ -43,6 +43,8 @@ HomebrewSchema.statics.increaseView = async function(query) {
return brew;
};
// STATIC FUNCTIONS
HomebrewSchema.statics.get = async function(query, fields=null){
const brew = await Homebrew.findOne(query, fields).orFail()
.catch((error)=>{throw 'Can not find brew';});
@@ -63,6 +65,15 @@ HomebrewSchema.statics.getByUser = async function(username, allowAccess=false, f
return brews;
};
// INDEXES
HomebrewSchema.index({ updatedAt: -1, lastViewed: -1 });
HomebrewSchema.index({ published: 1, title: 'text' });
HomebrewSchema.index({ lock: 1, sparse: true });
HomebrewSchema.path('lock.reviewRequested').index({ sparse: true });
const Homebrew = mongoose.model('Homebrew', HomebrewSchema);
export {

View File

@@ -8,7 +8,7 @@ const brewSnippetsToJSON = (menuTitle, userBrewSnippets, themeBundleSnippets=nul
const mpAsSnippets = [];
// Snippets from Themes first.
if(themeBundleSnippets) {
for (let themes of themeBundleSnippets) {
for (const themes of themeBundleSnippets) {
if(typeof themes !== 'string') {
const userSnippets = [];
const snipSplit = themes.snippets.trim().split(textSplit).slice(1);
@@ -76,9 +76,9 @@ const yamlSnippetsToText = (yamlObj)=>{
if(typeof yamlObj == 'string') return yamlObj;
let snippetsText = '';
for (let snippet of yamlObj) {
for (let subSnippet of snippet.subsnippets) {
for (const snippet of yamlObj) {
for (const subSnippet of snippet.subsnippets) {
snippetsText = `${snippetsText}\\snippet ${subSnippet.name}\n${subSnippet.gen || ''}\n`;
}
}
@@ -121,7 +121,7 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
const res = await request
.get(`/api/theme/${renderer}/${theme}`)
.catch((err)=>{
setError(err)
setError(err);
});
if(!res) {
setThemeBundle({});
@@ -133,14 +133,14 @@ const fetchThemeBundle = async (setError, setThemeBundle, renderer, theme)=>{
setError(null);
};
const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
const debugTextMismatch = (clientTextRaw, serverTextRaw, label)=>{
const clientText = clientTextRaw?.normalize('NFC') || '';
const serverText = serverTextRaw?.normalize('NFC') || '';
const clientBuffer = Buffer.from(clientText, 'utf8');
const serverBuffer = Buffer.from(serverText, 'utf8');
if (clientBuffer.equals(serverBuffer)) {
if(clientBuffer.equals(serverBuffer)) {
console.log(`${label} text matches byte-for-byte.`);
return;
}
@@ -151,7 +151,7 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Byte-level diff
for (let i = 0; i < Math.min(clientBuffer.length, serverBuffer.length); i++) {
if (clientBuffer[i] !== serverBuffer[i]) {
if(clientBuffer[i] !== serverBuffer[i]) {
console.log(`Byte mismatch at offset ${i}: client=0x${clientBuffer[i].toString(16)} server=0x${serverBuffer[i].toString(16)}`);
break;
}
@@ -159,14 +159,14 @@ const debugTextMismatch = (clientTextRaw, serverTextRaw, label) => {
// Char-level diff
for (let i = 0; i < Math.min(clientText.length, serverText.length); i++) {
if (clientText[i] !== serverText[i]) {
if(clientText[i] !== serverText[i]) {
console.log(`Char mismatch at index ${i}:`);
console.log(` Client: '${clientText[i]}' (U+${clientText.charCodeAt(i).toString(16).toUpperCase()})`);
console.log(` Server: '${serverText[i]}' (U+${serverText.charCodeAt(i).toString(16).toUpperCase()})`);
break;
}
}
}
};
export {
splitTextStyleAndMetadata,

View File

@@ -38,15 +38,6 @@
animation-duration : 0.4s;
}
.CodeMirror-vscrollbar {
&::-webkit-scrollbar { width : 20px; }
&::-webkit-scrollbar-thumb {
width : 20px;
background : linear-gradient(90deg, #858585 15px, #808080 15px);
}
}
//.cm-tab {
// background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAMCAQAAACOs/baAAAARUlEQVR4nGJgIAG8JkXxUAcCtDWemcGR1lY4MvgzCEKY7jSBjgxBDAG09UEQzAe0AMwMHrSOAwEGRtpaMIwAAAAA//8DAG4ID9EKs6YqAAAAAElFTkSuQmCC) no-repeat right;
//}

View File

@@ -435,7 +435,7 @@ const replaceVar = function(input, hoist=false, allowUnresolved=false) {
try {
return mathParser.evaluate(replacedLabel);
} catch (error) {
} catch {
return undefined; // Return undefined if invalid math result
}
}
@@ -680,7 +680,7 @@ const tableTerminators = [
Marked.use(MarkedVariables());
Marked.use(MarkedDefinitionLists());
Marked.use({ extensions : [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use({ extensions: [forcedParagraphBreaks, mustacheSpans, mustacheDivs, mustacheInjectInline] });
Marked.use(mustacheInjectBlock);
Marked.use(MarkedAlignedParagraphs());
Marked.use(MarkedSubSuperText());

View File

@@ -49,7 +49,7 @@ const cleanUrl = function (sanitize, base, href) {
prot = decodeURIComponent(unescape(href))
.replace(nonWordAndColonTest, '')
.toLowerCase();
} catch (e) {
} catch {
return null;
}
if(prot.indexOf('javascript:') === 0 || prot.indexOf('vbscript:') === 0 || prot.indexOf('data:') === 0) {
@@ -58,7 +58,7 @@ const cleanUrl = function (sanitize, base, href) {
}
try {
href = encodeURI(href).replace(/%25/g, '%');
} catch (e) {
} catch {
return null;
}
return href;

View File

@@ -611,3 +611,17 @@ h6,
}
.toc.wide li { break-inside : auto; }
}
/**********************************
Firefox endruns
**********************************/
@supports (-moz-user-select: none) { // This section will only apply to Firefox; it's the only browser that supports `-mos-xyz...`
.page {
blockquote, table {
page-break-inside: auto;
break-inside: auto;
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 236 B

View File

@@ -0,0 +1,6 @@
{
"name" : "UnearthedArcana",
"renderer" : "V3",
"baseTheme" : false,
"baseSnippets" : false
}

View File

@@ -0,0 +1,38 @@
@import (less) './themes/fonts/5e/fonts.less';
@import (less) './themes/assets/assets.less';
:root {
//Colors
--HB_Color_Background : #FFFFFF; // White
--HB_Color_WatercolorStain : #000000; // Black
}
.page {
font-family: Cambria,Georgia,serif;
font-size: 14px;
h1, h2, h3, h4 {
font-variant: small-caps;
font-weight: normal;
}
h1 {
column-span: all;
-webkit-column-span: all;
font-size: 40px;
}
h2 {
font-size: 26px;
}
h3 {
font-size: 20px;
border-bottom: 2px solid black;
}
h4 {
font-size: 18px;
}
h5 {
font-size: 16px;
}
h6 {
font-size: 14px;
}
}

View File

@@ -35,6 +35,13 @@
"baseTheme": "Blank",
"baseSnippets": "5ePHB",
"path": "Journal"
},
"UnearthedArcana": {
"name": "UnearthedArcana",
"renderer": "V3",
"baseTheme": false,
"baseSnippets": false,
"path": "UnearthedArcana"
}
}
}