0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-22 04:38:11 +00:00

Merge pull request #4663 from naturalcrit/update-dependencies

linting
This commit is contained in:
Víctor Losada Hernández
2026-03-03 23:53:13 +01:00
committed by GitHub
24 changed files with 2679 additions and 2687 deletions

View File

@@ -1,6 +1,6 @@
import { createRoot } from "react-dom/client";
import Admin from "./admin.jsx";
import { createRoot } from 'react-dom/client';
import Admin from './admin.jsx';
const props = window.__INITIAL_PROPS__ || {};
createRoot(document.getElementById("reactRoot")).render(<Admin {...props} />);
createRoot(document.getElementById('reactRoot')).render(<Admin {...props} />);

View File

@@ -11,13 +11,13 @@ const Combobox = createReactClass({
trigger : 'hover',
default : '',
placeholder : '',
tooltip: '',
tooltip : '',
autoSuggest : {
clearAutoSuggestOnClick : true,
suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
},
valuePatterns: /.+/
valuePatterns : /.+/
};
},
getInitialState : function() {
@@ -42,7 +42,7 @@ const Combobox = createReactClass({
},
handleClickOutside : function(e){
// Close dropdown when clicked outside
if (this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) {
if(this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) {
this.handleDropdown(false);
}
},
@@ -89,7 +89,7 @@ const Combobox = createReactClass({
}
}}
onKeyDown={(e)=>{
if (e.key === "Enter") {
if(e.key === 'Enter') {
e.preventDefault();
this.props.onEntry(e);
}

View File

@@ -88,7 +88,7 @@ const Editor = createReactClass({
const snippetBar = document.querySelector('.editor > .snippetBar');
if(!snippetBar) return;
this.resizeObserver = new ResizeObserver(entries=>{
this.resizeObserver = new ResizeObserver((entries)=>{
const height = document.querySelector('.editor > .snippetBar').offsetHeight;
this.setState({ snippetBarHeight: height });
});

View File

@@ -338,9 +338,9 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()}
</div>
<div className="field tags">
<div className='field tags'>
<label>Tags</label>
<div className="value" >
<div className='value' >
<TagInput
label='tags'
valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/\\.&_\-]{0,40}\s*$/}
@@ -351,7 +351,7 @@ const MetadataEditor = createReactClass({
/>
</div>
</div>
{this.renderLanguageDropdown()}
@@ -363,9 +363,9 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()}
<div className="field invitedAuthors">
<div className='field invitedAuthors'>
<label>Invited authors</label>
<div className="value">
<div className='value'>
<TagInput
label='invited authors'
valuePatterns={/.+/}
@@ -378,7 +378,7 @@ const MetadataEditor = createReactClass({
/>
</div>
</div>
<h2>Privacy</h2>

View File

@@ -1,210 +1,210 @@
export default [
// ############################## Systems
// D&D
"system:D&D Original",
"system:D&D Basic",
"system:AD&D 1e",
"system:AD&D 2e",
"system:D&D 3e",
"system:D&D 3.5e",
"system:D&D 4e",
"system:D&D 5e",
"system:D&D 5e 2024",
"system:BD&D (B/X)",
"system:D&D Essentials",
'system:D&D Original',
'system:D&D Basic',
'system:AD&D 1e',
'system:AD&D 2e',
'system:D&D 3e',
'system:D&D 3.5e',
'system:D&D 4e',
'system:D&D 5e',
'system:D&D 5e 2024',
'system:BD&D (B/X)',
'system:D&D Essentials',
// Other Famous RPGs
"system:Pathfinder 1e",
"system:Pathfinder 2e",
"system:Vampire: The Masquerade",
"system:Werewolf: The Apocalypse",
"system:Mage: The Ascension",
"system:Call of Cthulhu",
"system:Shadowrun",
"system:Star Wars RPG (D6/D20/Edge of the Empire)",
"system:Warhammer Fantasy Roleplay",
"system:Cyberpunk 2020",
"system:Blades in the Dark",
"system:Daggerheart",
"system:Draw Steel",
"system:Mutants and Masterminds",
'system:Pathfinder 1e',
'system:Pathfinder 2e',
'system:Vampire: The Masquerade',
'system:Werewolf: The Apocalypse',
'system:Mage: The Ascension',
'system:Call of Cthulhu',
'system:Shadowrun',
'system:Star Wars RPG (D6/D20/Edge of the Empire)',
'system:Warhammer Fantasy Roleplay',
'system:Cyberpunk 2020',
'system:Blades in the Dark',
'system:Daggerheart',
'system:Draw Steel',
'system:Mutants and Masterminds',
// Meta
"meta:V3",
"meta:Legacy",
"meta:Template",
"meta:Theme",
"meta:free",
"meta:Character Sheet",
"meta:Documentation",
"meta:NPC",
"meta:Guide",
"meta:Resource",
"meta:Notes",
"meta:Example",
'meta:V3',
'meta:Legacy',
'meta:Template',
'meta:Theme',
'meta:free',
'meta:Character Sheet',
'meta:Documentation',
'meta:NPC',
'meta:Guide',
'meta:Resource',
'meta:Notes',
'meta:Example',
// Book type
"type:Campaign",
"type:Campaign Setting",
"type:Adventure",
"type:One-Shot",
"type:Setting",
"type:World",
"type:Lore",
"type:History",
"type:Dungeon Master",
"type:Encounter Pack",
"type:Encounter",
"type:Session Notes",
"type:reference",
"type:Handbook",
"type:Manual",
"type:Manuals",
"type:Compendium",
"type:Bestiary",
'type:Campaign',
'type:Campaign Setting',
'type:Adventure',
'type:One-Shot',
'type:Setting',
'type:World',
'type:Lore',
'type:History',
'type:Dungeon Master',
'type:Encounter Pack',
'type:Encounter',
'type:Session Notes',
'type:reference',
'type:Handbook',
'type:Manual',
'type:Manuals',
'type:Compendium',
'type:Bestiary',
// ###################################### RPG Keywords
// Classes / Subclasses / Archetypes
"Class",
"Subclass",
"Archetype",
"Martial",
"Half-Caster",
"Full Caster",
"Artificer",
"Barbarian",
"Bard",
"Cleric",
"Druid",
"Fighter",
"Monk",
"Paladin",
"Rogue",
"Sorcerer",
"Warlock",
"Wizard",
'Class',
'Subclass',
'Archetype',
'Martial',
'Half-Caster',
'Full Caster',
'Artificer',
'Barbarian',
'Bard',
'Cleric',
'Druid',
'Fighter',
'Monk',
'Paladin',
'Rogue',
'Sorcerer',
'Warlock',
'Wizard',
// Races / Species / Lineages
"Race",
"Ancestry",
"Lineage",
"Aasimar",
"Beastfolk",
"Dragonborn",
"Dwarf",
"Elf",
"Goblin",
"Half-Elf",
"Half-Orc",
"Human",
"Kobold",
"Lizardfolk",
"Lycan",
"Orc",
"Tiefling",
"Vampire",
"Yuan-Ti",
'Race',
'Ancestry',
'Lineage',
'Aasimar',
'Beastfolk',
'Dragonborn',
'Dwarf',
'Elf',
'Goblin',
'Half-Elf',
'Half-Orc',
'Human',
'Kobold',
'Lizardfolk',
'Lycan',
'Orc',
'Tiefling',
'Vampire',
'Yuan-Ti',
// Magic / Spells / Items
"Magic",
"Magic Item",
"Magic Items",
"Wondrous Item",
"Magic Weapon",
"Artifact",
"Spell",
"Spells",
"Cantrip",
"Cantrips",
"Eldritch",
"Eldritch Invocation",
"Invocation",
"Invocations",
"Pact boon",
"Pact Boon",
"Spellcaster",
"Spellblade",
"Magical Tattoos",
"Enchantment",
"Enchanted",
"Attunement",
"Requires Attunement",
"Rune",
"Runes",
"Wand",
"Rod",
"Scroll",
"Potion",
"Potions",
"Item",
"Items",
"Bag of Holding",
'Magic',
'Magic Item',
'Magic Items',
'Wondrous Item',
'Magic Weapon',
'Artifact',
'Spell',
'Spells',
'Cantrip',
'Cantrips',
'Eldritch',
'Eldritch Invocation',
'Invocation',
'Invocations',
'Pact boon',
'Pact Boon',
'Spellcaster',
'Spellblade',
'Magical Tattoos',
'Enchantment',
'Enchanted',
'Attunement',
'Requires Attunement',
'Rune',
'Runes',
'Wand',
'Rod',
'Scroll',
'Potion',
'Potions',
'Item',
'Items',
'Bag of Holding',
// Monsters / Creatures / Enemies
"Monster",
"Creatures",
"Creature",
"Beast",
"Beasts",
"Humanoid",
"Undead",
"Fiend",
"Aberration",
"Ooze",
"Giant",
"Dragon",
"Monstrosity",
"Demon",
"Devil",
"Elemental",
"Construct",
"Constructs",
"Boss",
"BBEG",
'Monster',
'Creatures',
'Creature',
'Beast',
'Beasts',
'Humanoid',
'Undead',
'Fiend',
'Aberration',
'Ooze',
'Giant',
'Dragon',
'Monstrosity',
'Demon',
'Devil',
'Elemental',
'Construct',
'Constructs',
'Boss',
'BBEG',
// ############################# Media / Pop Culture
"One Piece",
"Dragon Ball",
"Dragon Ball Z",
"Naruto",
"Jujutsu Kaisen",
"Fairy Tail",
"Final Fantasy",
"Kingdom Hearts",
"Elder Scrolls",
"Skyrim",
"WoW",
"World of Warcraft",
"Marvel Comics",
"DC Comics",
"Pokemon",
"League of Legends",
"Runeterra",
"Arcane",
"Yu-Gi-Oh",
"Minecraft",
"Don't Starve",
"Witcher",
"Witcher 3",
"Cyberpunk",
"Cyberpunk 2077",
"Fallout",
"Divinity Original Sin 2",
"Fullmetal Alchemist",
"Fullmetal Alchemist Brotherhood",
"Lobotomy Corporation",
"Bloodborne",
"Dragonlance",
"Shackled City Adventure Path",
"Baldurs Gate 3",
"Library of Ruina",
"Radiant Citadel",
"Ravenloft",
"Forgotten Realms",
"Exandria",
"Critical Role",
"Star Wars",
"SW5e",
"Star Wars 5e",
'One Piece',
'Dragon Ball',
'Dragon Ball Z',
'Naruto',
'Jujutsu Kaisen',
'Fairy Tail',
'Final Fantasy',
'Kingdom Hearts',
'Elder Scrolls',
'Skyrim',
'WoW',
'World of Warcraft',
'Marvel Comics',
'DC Comics',
'Pokemon',
'League of Legends',
'Runeterra',
'Arcane',
'Yu-Gi-Oh',
'Minecraft',
'Don\'t Starve',
'Witcher',
'Witcher 3',
'Cyberpunk',
'Cyberpunk 2077',
'Fallout',
'Divinity Original Sin 2',
'Fullmetal Alchemist',
'Fullmetal Alchemist Brotherhood',
'Lobotomy Corporation',
'Bloodborne',
'Dragonlance',
'Shackled City Adventure Path',
'Baldurs Gate 3',
'Library of Ruina',
'Radiant Citadel',
'Ravenloft',
'Forgotten Realms',
'Exandria',
'Critical Role',
'Star Wars',
'SW5e',
'Star Wars 5e',
];

View File

@@ -1,71 +1,71 @@
import "./tagInput.less";
import React, { useState, useEffect } from "react";
import Combobox from "../../../components/combobox.jsx";
import './tagInput.less';
import React, { useState, useEffect } from 'react';
import Combobox from '../../../components/combobox.jsx';
import tagSuggestionList from "./curatedTagSuggestionList.js";
import tagSuggestionList from './curatedTagSuggestionList.js';
const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, placeholder = "", smallText = "", onChange }) => {
const TagInput = ({ tooltip, label, valuePatterns, values = [], unique = true, placeholder = '', smallText = '', onChange })=>{
const [tagList, setTagList] = useState(
values.map((value) => ({
values.map((value)=>({
value,
editing: false,
draft: "",
editing : false,
draft : '',
})),
);
useEffect(() => {
useEffect(()=>{
const incoming = values || [];
const current = tagList.map((t) => t.value);
const current = tagList.map((t)=>t.value);
const changed = incoming.length !== current.length || incoming.some((v, i) => v !== current[i]);
const changed = incoming.length !== current.length || incoming.some((v, i)=>v !== current[i]);
if (changed) {
if(changed) {
setTagList(
incoming.map((value) => ({
incoming.map((value)=>({
value,
editing: false,
editing : false,
})),
);
}
}, [values]);
useEffect(() => {
useEffect(()=>{
onChange?.({
target: { value: tagList.map((t) => t.value) },
target : { value: tagList.map((t)=>t.value) },
});
}, [tagList]);
// substrings to be normalized to the first value on the array
const duplicateGroups = [
["5e 2024", "5.5e", "5e'24", "5.24", "5e24", "5.5"],
["5e", "5th Edition"],
["Dungeons & Dragons", "Dungeons and Dragons", "Dungeons n dragons"],
["D&D", "DnD", "dnd", "Dnd", "dnD", "d&d", "d&D", "D&d"],
["P2e", "p2e", "P2E", "Pathfinder 2e"],
['5e 2024', '5.5e', '5e\'24', '5.24', '5e24', '5.5'],
['5e', '5th Edition'],
['Dungeons & Dragons', 'Dungeons and Dragons', 'Dungeons n dragons'],
['D&D', 'DnD', 'dnd', 'Dnd', 'dnD', 'd&d', 'd&D', 'D&d'],
['P2e', 'p2e', 'P2E', 'Pathfinder 2e'],
];
const normalizeValue = (input) => {
const normalizeValue = (input)=>{
const lowerInput = input.toLowerCase();
let normalizedTag = input;
for (const group of duplicateGroups) {
for (const tag of group) {
if (!tag) continue;
if(!tag) continue;
const index = lowerInput.indexOf(tag.toLowerCase());
if (index !== -1) {
if(index !== -1) {
normalizedTag = input.slice(0, index) + group[0] + input.slice(index + tag.length);
break;
}
}
}
if (normalizedTag.includes(":")) {
const [rawType, rawValue = ""] = normalizedTag.split(":");
if(normalizedTag.includes(':')) {
const [rawType, rawValue = ''] = normalizedTag.split(':');
const tagType = rawType.trim().toLowerCase();
const tagValue = rawValue.trim();
if (tagValue.length > 0) {
if(tagValue.length > 0) {
normalizedTag = `${tagType}:${tagValue[0].toUpperCase()}${tagValue.slice(1)}`;
}
//trims spaces around colon and capitalizes the first word after the colon
@@ -75,56 +75,56 @@ const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, pl
return normalizedTag;
};
const submitTag = (newValue, index = null) => {
const submitTag = (newValue, index = null)=>{
const trimmed = newValue?.trim();
if (!trimmed) return;
if (!valuePatterns.test(trimmed)) return;
if(!trimmed) return;
if(!valuePatterns.test(trimmed)) return;
const normalizedTag = normalizeValue(trimmed);
setTagList((prev) => {
const existsIndex = prev.findIndex((t) => t.value.toLowerCase() === normalizedTag.toLowerCase());
if (unique && existsIndex !== -1) return prev;
if (index !== null) {
return prev.map((t, i) => (i === index ? { ...t, value: normalizedTag, editing: false } : t));
setTagList((prev)=>{
const existsIndex = prev.findIndex((t)=>t.value.toLowerCase() === normalizedTag.toLowerCase());
if(unique && existsIndex !== -1) return prev;
if(index !== null) {
return prev.map((t, i)=>(i === index ? { ...t, value: normalizedTag, editing: false } : t));
}
return [...prev, { value: normalizedTag, editing: false }];
});
};
const removeTag = (index) => {
setTagList((prev) => prev.filter((_, i) => i !== index));
const removeTag = (index)=>{
setTagList((prev)=>prev.filter((_, i)=>i !== index));
};
const editTag = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: true, draft: t.value } : t)));
const editTag = (index)=>{
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: true, draft: t.value } : t)));
};
const stopEditing = (index) => {
setTagList((prev) => prev.map((t, i) => (i === index ? { ...t, editing: false, draft: "" } : t)));
const stopEditing = (index)=>{
setTagList((prev)=>prev.map((t, i)=>(i === index ? { ...t, editing: false, draft: '' } : t)));
};
const suggestionOptions = tagSuggestionList.map((tag) => {
const tagType = tag.split(":");
const suggestionOptions = tagSuggestionList.map((tag)=>{
const tagType = tag.split(':');
let classes = "item";
let classes = 'item';
switch (tagType[0]) {
case "type":
classes = "item type";
break;
case "group":
classes = "item group";
break;
case "meta":
classes = "item meta";
break;
case "system":
classes = "item system";
break;
default:
classes = "item";
break;
case 'type':
classes = 'item type';
break;
case 'group':
classes = 'item group';
break;
case 'meta':
classes = 'item meta';
break;
case 'system':
classes = 'item system';
break;
default:
classes = 'item';
break;
}
return (
@@ -135,73 +135,69 @@ const TagInput = ({tooltip, label, valuePatterns, values = [], unique = true, pl
});
return (
<div className="tagInputWrap">
<div className='tagInputWrap'>
<Combobox
trigger="click"
className="tagInput-dropdown"
default=""
trigger='click'
className='tagInput-dropdown'
default=''
placeholder={placeholder}
options={label === "tags" ? suggestionOptions : []}
options={label === 'tags' ? suggestionOptions : []}
tooltip={tooltip}
autoSuggest={
label === "tags"
label === 'tags'
? {
suggestMethod: "startsWith",
clearAutoSuggestOnClick: true,
filterOn: ["value", "title"],
}
: { suggestMethod: "includes", clearAutoSuggestOnClick: true, filterOn: [] }
suggestMethod : 'startsWith',
clearAutoSuggestOnClick : true,
filterOn : ['value', 'title'],
}
: { suggestMethod: 'includes', clearAutoSuggestOnClick: true, filterOn: [] }
}
valuePatterns={valuePatterns.source}
onSelect={(value) => submitTag(value)}
onEntry={(e) => {
if (e.key === "Enter") {
onSelect={(value)=>submitTag(value)}
onEntry={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
submitTag(e.target.value);
}
}}
/>
<ul className="list">
{tagList.map((t, i) =>
t.editing ? (
<input
key={i}
type="text"
value={t.draft} // always use draft
pattern={valuePatterns.source}
onChange={(e) =>
setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: e.target.value } : tag)),
)
<ul className='list'>
{tagList.map((t, i)=>t.editing ? (
<input
key={i}
type='text'
value={t.draft} // always use draft
pattern={valuePatterns.source}
onChange={(e)=>setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: e.target.value } : tag)),
)
}
onKeyDown={(e)=>{
if(e.key === 'Enter') {
e.preventDefault();
submitTag(t.draft, i); // submit draft
setTagList((prev)=>prev.map((tag, idx)=>(idx === i ? { ...tag, draft: '' } : tag)),
);
}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
submitTag(t.draft, i); // submit draft
setTagList((prev) =>
prev.map((tag, idx) => (idx === i ? { ...tag, draft: "" } : tag)),
);
}
if (e.key === "Escape") {
stopEditing(i);
e.target.blur();
}
}}
autoFocus
/>
) : (
<li key={i} className="tag" onClick={() => editTag(i)}>
{t.value}
<button
type="button"
onClick={(e) => {
e.stopPropagation();
removeTag(i);
}}>
<i className="fa fa-times fa-fw" />
</button>
</li>
),
if(e.key === 'Escape') {
stopEditing(i);
e.target.blur();
}
}}
autoFocus
/>
) : (
<li key={i} className='tag' onClick={()=>editTag(i)}>
{t.value}
<button
type='button'
onClick={(e)=>{
e.stopPropagation();
removeTag(i);
}}>
<i className='fa fa-times fa-fw' />
</button>
</li>
),
)}
</ul>
</div>

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { createRoot } from "react-dom/client";
import Homebrew from "./homebrew.jsx";
import { createRoot } from 'react-dom/client';
import Homebrew from './homebrew.jsx';
const props = window.__INITIAL_PROPS__ || {};
createRoot(document.getElementById("reactRoot")).render(<Homebrew {...props} />);
createRoot(document.getElementById('reactRoot')).render(<Homebrew {...props} />);

View File

@@ -7,10 +7,10 @@ import PatreonNavItem from './patreon.navitem.jsx';
const Navbar = createReactClass({
displayName : 'Navbar',
getInitialState: function() {
getInitialState : function() {
return {
// showNonChromeWarning: false, // uncomment if needed
ver: global.version || '0.0.0'
ver : global.version || '0.0.0'
};
},

View File

@@ -8,7 +8,7 @@ import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
@@ -57,22 +57,22 @@ const EditPage = (props)=>{
...props
};
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 [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);
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);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));

View File

@@ -1,4 +1,4 @@
/* eslint-disable max-lines */
import './homePage.less';
// Common imports
@@ -8,7 +8,7 @@ import Markdown from '@shared/markdown.js';
import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js';
import { printCurrentBrew, fetchThemeBundle, splitTextStyleAndMetadata } from '@shared/helpers.js';
import { printCurrentBrew, fetchThemeBundle } from '@shared/helpers.js';
import SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx';
@@ -45,16 +45,16 @@ const HomePage =(props)=>{
...props
};
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 [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 [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [autoSaveEnabled, setAutoSaveEnable] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));

View File

@@ -42,17 +42,17 @@ const NewPage = (props)=>{
...props
};
const [currentBrew , setCurrentBrew ] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false);
const [saveGoogle , setSaveGoogle ] = useState(global.account?.googleId ? true : false);
const [error , setError ] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1);
const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving, setIsSaving] = useState(false);
const [saveGoogle, setSaveGoogle] = useState(global.account?.googleId ? true : false);
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 [autoSaveEnabled , setAutoSaveEnabled ] = useState(false);
const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges, setUnsavedChanges] = useState(false);
const [autoSaveEnabled, setAutoSaveEnabled] = useState(false);
const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew));

View File

@@ -1,33 +1,33 @@
import DB from "./server/db.js";
import createApp from "./server/app.js";
import config from "./server/config.js";
import { createServer as createViteServer } from "vite";
import DB from './server/db.js';
import createApp from './server/app.js';
import config from './server/config.js';
import { createServer as createViteServer } from 'vite';
const isDev = process.env.NODE_ENV === "local";
const isDev = process.env.NODE_ENV === 'local';
async function start() {
let vite;
if (isDev) {
if(isDev) {
vite = await createViteServer({
server: { middlewareMode: true },
appType: "custom",
server : { middlewareMode: true },
appType : 'custom',
});
}
await DB.connect(config).catch((err) => {
console.error("Database connection failed:", err);
await DB.connect(config).catch((err)=>{
console.error('Database connection failed:', err);
process.exit(1);
});
const app = await createApp(vite);
const PORT = process.env.PORT || config.get("web_port") || 3000;
app.listen(PORT, () => {
const reset = "\x1b[0m"; // Reset to default style
const bright = "\x1b[1m"; // Bright (bold) style
const cyan = "\x1b[36m"; // Cyan color
const underline = "\x1b[4m"; // Underlined style
const PORT = process.env.PORT || config.get('web_port') || 3000;
app.listen(PORT, ()=>{
const reset = '\x1b[0m'; // Reset to default style
const bright = '\x1b[1m'; // Bright (bold) style
const cyan = '\x1b[36m'; // Cyan color
const underline = '\x1b[4m'; // Underlined style
console.log(`\n\tserver started at: ${new Date().toLocaleString()}`);
console.log(`\tserver on port: ${PORT}`);

View File

@@ -21,52 +21,52 @@ process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
export default function createAdminApi(vite) {
const router = express.Router();
const mw = {
adminOnly : (req, res, next)=>{
if(!req.get('authorization')){
return res
const mw = {
adminOnly : (req, res, next)=>{
if(!req.get('authorization')){
return res
.set('WWW-Authenticate', 'Basic realm="Authorization Required"')
.status(401)
.send('Authorization Required');
}
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
}
const [username, password] = Buffer.from(req.get('authorization').split(' ').pop(), 'base64')
.toString('ascii')
.split(':');
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
return next();
if(process.env.ADMIN_USER === username && process.env.ADMIN_PASS === password){
return next();
}
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
}
throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
}
};
};
const junkBrewPipeline = [
{ $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
} },
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
{ $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 }
];
const junkBrewPipeline = [
{ $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
} },
{ $project: { textBinSize: { $binarySize: '$textBin' } } },
{ $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 }
];
/* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true }
}).lean().limit(10000).select('_id');
/* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true }
}).lean().limit(10000).select('_id');
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
// Search for up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length }))
.catch((error)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
});
});
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
// Delete up to 100 brews that have not been viewed or updated in 30 days and are shorter than 140 bytes
router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{
const ids = docs.map((doc)=>doc._id);
return HomebrewModel.deleteMany({ _id: { $in: ids } });
@@ -76,18 +76,18 @@ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
console.error(error);
res.status(500).json({ error: 'Internal Server Error' });
});
});
});
/* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
return res.json(req.brew);
});
/* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
return res.json(req.brew);
});
/* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
const query = uncompressedBrewQuery.clone();
/* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
const query = uncompressedBrewQuery.clone();
query.exec()
query.exec()
.then((objs)=>{
const ids = objs.map((obj)=>obj._id);
res.json({ count: ids.length, ids });
@@ -96,46 +96,46 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
console.error(err);
res.status(500).send(err.message || 'Internal Server Error');
});
});
/* Cleans `<script` and `</script>` from the "text" field of a brew */
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
const brew = req.brew;
const properties = ['text', 'description', 'title'];
properties.forEach((property)=>{
brew[property] = cleanText(brew[property]);
});
splitTextStyleAndMetadata(brew);
/* Cleans `<script` and `</script>` from the "text" field of a brew */
router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res)=>{
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Cleaning script tags from ShareID ${req.params.id}`);
req.body = brew;
function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
// Remove Account from request to prevent Admin user from being added to brew as an Author
req.account = undefined;
const brew = req.brew;
return await HomebrewAPI.updateBrew(req, res);
});
const properties = ['text', 'description', 'title'];
properties.forEach((property)=>{
brew[property] = cleanText(brew[property]);
});
/* Get list of a user's documents */
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
const username = req.params.user;
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
splitTextStyleAndMetadata(brew);
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
req.body = brew;
const brews = await HomebrewModel.getByUser(username, true, fields);
// Remove Account from request to prevent Admin user from being added to brew as an Author
req.account = undefined;
return res.json(brews);
});
return await HomebrewAPI.updateBrew(req, res);
});
/* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.findOne({ _id: req.params.id })
/* Get list of a user's documents */
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
const username = req.params.user;
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
console.log(`[ADMIN: ${req.account?.username || 'Not Logged In'}] Get brew list for ${username}`);
const brews = await HomebrewModel.getByUser(username, true, fields);
return res.json(brews);
});
/* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.findOne({ _id: req.params.id })
.then((brew)=>{
if(!brew)
return res.status(404).send('Brew not found');
@@ -152,250 +152,250 @@ router.put('/admin/compress/:id', (req, res)=>{
console.error(err);
res.status(500).send('Error while saving');
});
});
});
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try {
const totalBrewsCount = await HomebrewModel.countDocuments({});
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try {
const totalBrewsCount = await HomebrewModel.countDocuments({});
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
return res.json({
totalBrews : totalBrewsCount,
totalPublishedBrews : publishedBrewsCount
});
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Internal Server Error' });
}
});
return res.json({
totalBrews : totalBrewsCount,
totalPublishedBrews : publishedBrewsCount
});
} catch (error) {
console.error(error);
return res.status(500).json({ error: 'Internal Server Error' });
}
});
// ####################### LOCKS
// ####################### LOCKS
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
const countLocksQuery = {
lock : { $exists: true }
};
const count = await HomebrewModel.countDocuments(countLocksQuery)
const countLocksQuery = {
lock : { $exists: true }
};
const count = await HomebrewModel.countDocuments(countLocksQuery)
.catch((error)=>{
throw { name: 'Lock Count Error', message: 'Unable to get lock count', status: 500, HBErrorCode: '61', error };
});
return res.json({ count });
return res.json({ count });
}));
}));
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
const countLocksPipeline = [
{
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
const countLocksPipeline = [
{
$match :
{
'lock' : { '$exists': 1 }
},
},
{
$project : {
shareId : 1,
editId : 1,
title : 1,
lock : 1
},
{
$project : {
shareId : 1,
editId : 1,
title : 1,
lock : 1
}
}
}
];
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
];
const lockedDocuments = await HomebrewModel.aggregate(countLocksPipeline)
.catch((error)=>{
throw { name: 'Can Not Get Locked Brews', message: 'Unable to get locked brew collection', status: 500, HBErrorCode: '68', error };
});
return res.json({
lockedDocuments
});
return res.json({
lockedDocuments
});
}));
}));
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
const lock = req.body;
const lock = req.body;
lock.applied = new Date;
lock.applied = new Date;
const filter = {
shareId : req.params.id
};
const filter = {
shareId : req.params.id
};
const brew = await HomebrewModel.findOne(filter);
const brew = await HomebrewModel.findOne(filter);
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to lock', shareId: req.params.id, status: 500, HBErrorCode: '63' };
if(brew.lock && !lock.overwrite) {
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
}
if(brew.lock && !lock.overwrite) {
throw { name: 'Already Locked', message: 'Lock already exists on brew', shareId: req.params.id, title: brew.title, status: 500, HBErrorCode: '64' };
}
lock.overwrite = undefined;
lock.overwrite = undefined;
brew.lock = lock;
brew.markModified('lock');
brew.lock = lock;
brew.markModified('lock');
await brew.save()
await brew.save()
.catch((error)=>{
throw { name: 'Lock Error', message: 'Unable to set lock', shareId: req.params.id, status: 500, HBErrorCode: '62', error };
});
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
return res.json({ name: 'LOCKED', message: `Lock applied to brew ID ${brew.shareId} - ${brew.title}`, ...lock });
}));
}));
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
const filter = {
shareId : req.params.id
};
const filter = {
shareId : req.params.id
};
const brew = await HomebrewModel.findOne(filter);
const brew = await HomebrewModel.findOne(filter);
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
if(!brew) throw { name: 'Brew Not Found', message: 'Cannot find brew to unlock', shareId: req.params.id, status: 500, HBErrorCode: '66' };
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
if(!brew.lock) throw { name: 'Not Locked', message: 'Cannot unlock as brew is not locked', shareId: req.params.id, status: 500, HBErrorCode: '67' };
brew.lock = undefined;
brew.markModified('lock');
brew.lock = undefined;
brew.markModified('lock');
await brew.save()
await brew.save()
.catch((error)=>{
throw { name: 'Cannot Unlock', message: 'Unable to clear lock', shareId: req.params.id, status: 500, HBErrorCode: '65', error };
});
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
}));
return res.json({ name: 'Unlocked', message: `Lock removed from brew ID ${req.params.id}` });
}));
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
const countReviewsPipeline = [
{
router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
const countReviewsPipeline = [
{
$match :
{
'lock.reviewRequested' : { '$exists': 1 }
},
},
{
$project : {
shareId : 1,
editId : 1,
title : 1,
lock : 1
},
{
$project : {
shareId : 1,
editId : 1,
title : 1,
lock : 1
}
}
}
];
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
];
const reviewDocuments = await HomebrewModel.aggregate(countReviewsPipeline)
.catch((error)=>{
throw { name: 'Can Not Get Reviews', message: 'Unable to get review collection', status: 500, HBErrorCode: '68', error };
});
return res.json({
reviewDocuments
});
return res.json({
reviewDocuments
});
}));
}));
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
// === This route is NOT Admin only ===
// Any user can request a review of their document
const filter = {
shareId : req.params.id,
lock : { $exists: 1 }
};
const filter = {
shareId : req.params.id,
lock : { $exists: 1 }
};
const brew = await HomebrewModel.findOne(filter);
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
const brew = await HomebrewModel.findOne(filter);
if(!brew) { throw { name: 'Brew Not Found', message: `Cannot find a locked brew with ID ${req.params.id}`, code: 500, HBErrorCode: '70' }; };
if(brew.lock.reviewRequested){
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
};
if(brew.lock.reviewRequested){
throw { name: 'Review Already Requested', message: `Review already requested for brew ${brew.shareId} - ${brew.title}`, code: 500, HBErrorCode: '71' };
};
brew.lock.reviewRequested = new Date();
brew.markModified('lock');
brew.lock.reviewRequested = new Date();
brew.markModified('lock');
await brew.save()
await brew.save()
.catch((error)=>{
throw { name: 'Can Not Set Review Request', message: `Unable to set request for review on brew ID ${req.params.id}`, code: 500, HBErrorCode: '69', error };
});
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
return res.json({ name: 'Review Requested', message: `Review requested on brew ID ${brew.shareId} - ${brew.title}` });
}));
}));
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
const filter = {
shareId : req.params.id,
'lock.reviewRequested' : { $exists: 1 }
};
const filter = {
shareId : req.params.id,
'lock.reviewRequested' : { $exists: 1 }
};
const brew = await HomebrewModel.findOne(filter);
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
const brew = await HomebrewModel.findOne(filter);
if(!brew) { throw { name: 'Can Not Clear Review Request', message: `Brew ID ${req.params.id} does not have a review pending!`, HBErrorCode: '73' }; };
brew.lock.reviewRequested = undefined;
brew.markModified('lock');
brew.lock.reviewRequested = undefined;
brew.markModified('lock');
await brew.save()
await brew.save()
.catch((error)=>{
throw { name: 'Can Not Clear Review Request', message: `Unable to remove request for review on brew ID ${req.params.id}`, HBErrorCode: '72', error };
});
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
return res.json({ name: 'Review Request Cleared', message: `Review request removed for brew ID ${brew.shareId} - ${brew.title}` });
}));
}));
// ####################### NOTIFICATIONS
// ####################### NOTIFICATIONS
router.get('/admin/notification/all', async (req, res, next)=>{
try {
const notifications = await NotificationModel.getAll();
return res.json(notifications);
router.get('/admin/notification/all', async (req, res, next)=>{
try {
const notifications = await NotificationModel.getAll();
return res.json(notifications);
} catch (error) {
console.log('Error getting all notifications: ', error.message);
return res.status(500).json({ message: error.message });
}
});
} catch (error) {
console.log('Error getting all notifications: ', error.message);
return res.status(500).json({ message: error.message });
}
});
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.addNotification(req.body);
return res.status(201).json(notification);
} catch (error) {
console.log('Error adding notification: ', error.message);
return res.status(500).json({ message: error.message });
}
});
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.addNotification(req.body);
return res.status(201).json(notification);
} catch (error) {
console.log('Error adding notification: ', error.message);
return res.status(500).json({ message: error.message });
}
});
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.deleteNotification(req.params.id);
return res.json(notification);
} catch (error) {
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
return res.status(500).json({ message: error.message });
}
});
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
try {
const notification = await NotificationModel.deleteNotification(req.params.id);
return res.json(notification);
} catch (error) {
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
return res.status(500).json({ message: error.message });
}
});
router.get('/admin', mw.adminOnly, asyncHandler(async (req, res) => {
router.get('/admin', mw.adminOnly, asyncHandler(async (req, res)=>{
const props = {
url : req.originalUrl
};
url : req.originalUrl
};
const htmlPath = isProd
? path.resolve('build', 'index.html')
: path.resolve('index.html');
const htmlPath = isProd
? path.resolve('build', 'index.html')
: path.resolve('index.html');
let html = fs.readFileSync(htmlPath, 'utf-8');
let html = fs.readFileSync(htmlPath, 'utf-8');
if (!isProd && vite?.transformIndexHtml) {
html = await vite.transformIndexHtml(req.originalUrl, html);
}
if(!isProd && vite?.transformIndexHtml) {
html = await vite.transformIndexHtml(req.originalUrl, html);
}
res.send(html.replace(
'<head>',
`<head>\n<script id="props">window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>`
));
}));
res.send(html.replace(
'<head>',
`<head>\n<script id="props">window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>`
));
}));
return router;

View File

@@ -55,7 +55,7 @@ export default async function createApp(vite) {
app.set('trust proxy', 1 /* number of proxies between user and server */);
if (vite) {
if(vite) {
app.use(vite.middlewares);
}

View File

@@ -481,7 +481,7 @@ const api = {
await HomebrewModel.deleteOne({ editId: id });
return next();
}
throw(err);
throw (err);
}
let brew = req.brew;

View File

@@ -1,4 +1,4 @@
/* eslint-disable max-depth */
/* eslint-disable max-lines */
import _ from 'lodash';
import { marked as Marked } from 'marked';

View File

@@ -99,7 +99,7 @@ const subtitles = [
function coverPageGen() {
return `<style>
return `<style>
.phb#p1{ text-align:center; }
.phb#p1:after{ display:none; }
</style>

File diff suppressed because one or more lines are too long

View File

@@ -179,10 +179,10 @@ export default {
`;
},
// Verify Logo redistribution
monteCookLogoDarkLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkLarge.png)`,
monteCookLogoDarkSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkSmall.png)`,
monteCookLogoLightLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightLarge.png)`,
monteCookLogoLightSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightSmall.png)`,
monteCookLogoDarkLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkLarge.png)`,
monteCookLogoDarkSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCDarkSmall.png)`,
monteCookLogoLightLarge : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightLarge.png)`,
monteCookLogoLightSmall : `![Cypher System Compatible](https://homebrewery.naturalcrit.com/assets/license_logos/CSCLightSmall.png)`,
// Onyx Path Canis Minor - Verify logos and access
onyxPathCanisMinorColophon : function () {
return dedent`

View File

@@ -1,6 +1,3 @@
import dedent from 'dedent';
// Mongoose Publishing Licenses
export default {

View File

@@ -1,38 +1,38 @@
// vite.config.js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import { generateAssetsPlugin } from "./vitePlugins/generateAssetsPlugin.js";
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';
import { generateAssetsPlugin } from './vitePlugins/generateAssetsPlugin.js';
export default defineConfig({
plugins: [react(), generateAssetsPlugin()],
resolve: {
alias: {
"@vitreum": path.resolve(__dirname, "./vitreum"),
"@shared": path.resolve(__dirname, "./shared"),
"@sharedStyles": path.resolve(__dirname, "./shared/naturalcrit/styles"),
"@navbar": path.resolve(__dirname, "./client/homebrew/navbar"),
"@themes": path.resolve(__dirname, "./themes"),
plugins : [react(), generateAssetsPlugin()],
resolve : {
alias : {
'@vitreum' : path.resolve(__dirname, './vitreum'),
'@shared' : path.resolve(__dirname, './shared'),
'@sharedStyles' : path.resolve(__dirname, './shared/naturalcrit/styles'),
'@navbar' : path.resolve(__dirname, './client/homebrew/navbar'),
'@themes' : path.resolve(__dirname, './themes'),
},
},
build: {
outDir: "build",
emptyOutDir: false,
rollupOptions: {
output: {
entryFileNames: "[name]/bundle.js",
chunkFileNames: "[name]/[name]-[hash].js",
assetFileNames: "[name]/[name].[ext]",
build : {
outDir : 'build',
emptyOutDir : false,
rollupOptions : {
output : {
entryFileNames : '[name]/bundle.js',
chunkFileNames : '[name]/[name]-[hash].js',
assetFileNames : '[name]/[name].[ext]',
},
},
},
define: {
global: "window.__INITIAL_PROPS__",
define : {
global : 'window.__INITIAL_PROPS__',
},
server: {
port: 8000,
fs: {
allow: ["."],
server : {
port : 8000,
fs : {
allow : ['.'],
},
},
});

View File

@@ -1,41 +1,41 @@
// vite-plugins/generateAssetsPlugin.js
import fs from "fs-extra";
import path from "path";
import less from "less";
import fs from 'fs-extra';
import path from 'path';
import less from 'less';
export function generateAssetsPlugin(isDev = false) {
return {
name: "generate-assets",
name : 'generate-assets',
async buildStart() {
const buildDir = path.resolve(process.cwd(), "build");
const buildDir = path.resolve(process.cwd(), 'build');
// Copy favicon
await fs.copy("./client/homebrew/favicon.ico", `${buildDir}/assets/favicon.ico`);
await fs.copy('./client/homebrew/favicon.ico', `${buildDir}/assets/favicon.ico`);
// Copy shared styles/fonts
const assets = fs.readdirSync("./shared/naturalcrit/styles");
const assets = fs.readdirSync('./shared/naturalcrit/styles');
for (const file of assets) {
await fs.copy(`./shared/naturalcrit/styles/${file}`, `${buildDir}/fonts/${file}`);
}
// Compile Legacy themes
const themes = { Legacy: {}, V3: {} };
const legacyDirs = fs.readdirSync("./themes/Legacy");
const legacyDirs = fs.readdirSync('./themes/Legacy');
for (const dir of legacyDirs) {
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`, "utf-8"));
const themeData = JSON.parse(fs.readFileSync(`./themes/Legacy/${dir}/settings.json`, 'utf-8'));
themeData.path = dir;
themes.Legacy[dir] = themeData;
const src = `./themes/Legacy/${dir}/style.less`;
const outputDir = `${buildDir}/themes/Legacy/${dir}/style.css`;
const lessOutput = await less.render(fs.readFileSync(src, "utf-8"), { compress: !isDev });
const lessOutput = await less.render(fs.readFileSync(src, 'utf-8'), { compress: !isDev });
await fs.outputFile(outputDir, lessOutput.css);
}
// Compile V3 themes
const v3Dirs = fs.readdirSync("./themes/V3");
const v3Dirs = fs.readdirSync('./themes/V3');
for (const dir of v3Dirs) {
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`, "utf-8"));
const themeData = JSON.parse(fs.readFileSync(`./themes/V3/${dir}/settings.json`, 'utf-8'));
themeData.path = dir;
themes.V3[dir] = themeData;
@@ -50,30 +50,30 @@ export function generateAssetsPlugin(isDev = false) {
const src = `./themes/V3/${dir}/style.less`;
const outputDir = `${buildDir}/themes/V3/${dir}/style.css`;
const lessOutput = await less.render(fs.readFileSync(src, "utf-8"), { compress: !isDev });
const lessOutput = await less.render(fs.readFileSync(src, 'utf-8'), { compress: !isDev });
await fs.outputFile(outputDir, lessOutput.css);
}
// Write themes.json
await fs.outputFile("./themes/themes.json", JSON.stringify(themes, null, 2));
await fs.outputFile('./themes/themes.json', JSON.stringify(themes, null, 2));
// Copy fonts/assets/icons
await fs.copy("./themes/fonts", `${buildDir}/fonts`);
await fs.copy("./themes/assets", `${buildDir}/assets`);
await fs.copy("./client/icons", `${buildDir}/icons`);
await fs.copy('./themes/fonts', `${buildDir}/fonts`);
await fs.copy('./themes/assets', `${buildDir}/assets`);
await fs.copy('./client/icons', `${buildDir}/icons`);
// Compile CodeMirror editor themes
const editorThemesBuildDir = `${buildDir}/homebrew/cm-themes`;
await fs.copy("./node_modules/codemirror/theme", editorThemesBuildDir);
await fs.copy("./themes/codeMirror/customThemes", editorThemesBuildDir);
await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
await fs.outputFile(`${buildDir}/homebrew/codeMirror/editorThemes.json`,
JSON.stringify(["default", ...editorThemeFiles.map((f) => f.slice(0, -4))], null, 2),
JSON.stringify(['default', ...editorThemeFiles.map((f)=>f.slice(0, -4))], null, 2),
);
// Copy remaining CodeMirror assets
await fs.copy("./themes/codeMirror", `${buildDir}/homebrew/codeMirror`);
await fs.copy('./themes/codeMirror', `${buildDir}/homebrew/codeMirror`);
},
};
}

View File

@@ -1,49 +1,48 @@
import React, { useEffect } from "react";
import React, { useEffect } from 'react';
//old vitreum file, still imported in some pages
const injectTag = (tag, props, children) => {
const injectNode = document.createElement(tag);
Object.entries(props).forEach(([key, val]) => injectNode[key] = val);
if (children) injectNode.appendChild(document.createTextNode(children));
document.getElementsByTagName('head')[0].appendChild(injectNode);
const injectTag = (tag, props, children)=>{
const injectNode = document.createElement(tag);
Object.entries(props).forEach(([key, val])=>injectNode[key] = val);
if(children) injectNode.appendChild(document.createTextNode(children));
document.getElementsByTagName('head')[0].appendChild(injectNode);
};
const obj2props = (obj) =>
Object.entries(obj)
.map(([k, v]) => `${k}="${v}"`)
.join(" ");
const toStr = (chld) => (Array.isArray(chld) ? chld.join("") : chld);
const onServer = typeof window === "undefined";
const obj2props = (obj)=>Object.entries(obj)
.map(([k, v])=>`${k}="${v}"`)
.join(' ');
const toStr = (chld)=>(Array.isArray(chld) ? chld.join('') : chld);
const onServer = typeof window === 'undefined';
let NamedTags = {};
let UnnamedTags = [];
export const HeadComponents = {
Title({ children }) {
if (onServer) NamedTags.title = `<title>${toStr(children)}</title>`;
useEffect(() => {
if(onServer) NamedTags.title = `<title>${toStr(children)}</title>`;
useEffect(()=>{
document.title = toStr(children);
}, [children]);
return null;
},
Favicon({ type = "image/png", href = "", rel = "icon", id = "favicon" }) {
if (onServer) NamedTags.favicon = `<link rel='shortcut icon' type="${type}" id="${id}" href="${href}" />`;
useEffect(() => {
Favicon({ type = 'image/png', href = '', rel = 'icon', id = 'favicon' }) {
if(onServer) NamedTags.favicon = `<link rel='shortcut icon' type="${type}" id="${id}" href="${href}" />`;
useEffect(()=>{
document.getElementById(id).href = href;
}, [id, href]);
return null;
},
Description({ children }) {
if (onServer) NamedTags.description = `<meta name='description' content='${toStr(children)}' />`;
if(onServer) NamedTags.description = `<meta name='description' content='${toStr(children)}' />`;
return null;
},
Noscript({ children }) {
if (onServer) UnnamedTags.push(`<noscript>${toStr(children)}</noscript>`);
if(onServer) UnnamedTags.push(`<noscript>${toStr(children)}</noscript>`);
return null;
},
Script({ children = [], ...props }) {
if (onServer) {
if(onServer) {
UnnamedTags.push(
children.length
? `<script ${obj2props(props)}>${toStr(children)}</script>`
@@ -53,31 +52,31 @@ export const HeadComponents = {
return null;
},
Meta(props) {
let tag = `<meta ${obj2props(props)} />`;
const tag = `<meta ${obj2props(props)} />`;
props.property || props.name ? (NamedTags[props.property || props.name] = tag) : UnnamedTags.push(tag);
useEffect(() => {
useEffect(()=>{
document
.getElementsByTagName("head")[0]
.insertAdjacentHTML("beforeend", Object.values(NamedTags).join("\n"));
.getElementsByTagName('head')[0]
.insertAdjacentHTML('beforeend', Object.values(NamedTags).join('\n'));
}, [NamedTags]);
return null;
},
Style({ children, type = "text/css" }) {
if (onServer) UnnamedTags.push(`<style type="${type}">${toStr(children)}</style>`);
Style({ children, type = 'text/css' }) {
if(onServer) UnnamedTags.push(`<style type="${type}">${toStr(children)}</style>`);
return null;
},
};
export const Inject = ({ tag, children, ...props }) => {
useEffect(() => {
export const Inject = ({ tag, children, ...props })=>{
useEffect(()=>{
injectTag(tag, props, children);
}, []);
return null;
};
export const generate = () => Object.values(NamedTags).concat(UnnamedTags).join("\n");
export const generate = ()=>Object.values(NamedTags).concat(UnnamedTags).join('\n');
export const flush = () => {
export const flush = ()=>{
NamedTags = {};
UnnamedTags = [];
};