0
0
mirror of https://github.com/naturalcrit/homebrewery.git synced 2026-03-22 08:58:11 +00:00
This commit is contained in:
Víctor Losada Hernández
2026-03-03 23:44:02 +01:00
parent 828bba61de
commit 8d18529c6d
24 changed files with 2679 additions and 2687 deletions

View File

@@ -1,6 +1,6 @@
import { createRoot } from "react-dom/client"; import { createRoot } from 'react-dom/client';
import Admin from "./admin.jsx"; import Admin from './admin.jsx';
const props = window.__INITIAL_PROPS__ || {}; 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', trigger : 'hover',
default : '', default : '',
placeholder : '', placeholder : '',
tooltip: '', tooltip : '',
autoSuggest : { autoSuggest : {
clearAutoSuggestOnClick : true, clearAutoSuggestOnClick : true,
suggestMethod : 'includes', suggestMethod : 'includes',
filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter filterOn : [] // should allow as array to filter on multiple attributes, or even custom filter
}, },
valuePatterns: /.+/ valuePatterns : /.+/
}; };
}, },
getInitialState : function() { getInitialState : function() {
@@ -42,7 +42,7 @@ const Combobox = createReactClass({
}, },
handleClickOutside : function(e){ handleClickOutside : function(e){
// Close dropdown when clicked outside // 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); this.handleDropdown(false);
} }
}, },
@@ -89,7 +89,7 @@ const Combobox = createReactClass({
} }
}} }}
onKeyDown={(e)=>{ onKeyDown={(e)=>{
if (e.key === "Enter") { if(e.key === 'Enter') {
e.preventDefault(); e.preventDefault();
this.props.onEntry(e); this.props.onEntry(e);
} }

View File

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

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
import { createRoot } from "react-dom/client"; import { createRoot } from 'react-dom/client';
import Homebrew from "./homebrew.jsx"; import Homebrew from './homebrew.jsx';
const props = window.__INITIAL_PROPS__ || {}; 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({ const Navbar = createReactClass({
displayName : 'Navbar', displayName : 'Navbar',
getInitialState: function() { getInitialState : function() {
return { return {
// showNonChromeWarning: false, // uncomment if needed // 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 _ from 'lodash';
import { DEFAULT_BREW_LOAD } from '../../../../server/brewDefaults.js'; 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 SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
@@ -57,22 +57,22 @@ const EditPage = (props)=>{
...props ...props
}; };
const [currentBrew , setCurrentBrew ] = useState(props.brew); const [currentBrew, setCurrentBrew] = useState(props.brew);
const [isSaving , setIsSaving ] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [lastSavedTime , setLastSavedTime ] = useState(new Date()); const [lastSavedTime, setLastSavedTime] = useState(new Date());
const [saveGoogle , setSaveGoogle ] = useState(!!props.brew.googleId); const [saveGoogle, setSaveGoogle] = useState(!!props.brew.googleId);
const [error , setError ] = useState(null); const [error, setError] = useState(null);
const [HTMLErrors , setHTMLErrors ] = useState(Markdown.validate(props.brew.text)); const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum ] = useState(1); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle ] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges ] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [alertTrashedGoogleBrew , setAlertTrashedGoogleBrew ] = useState(props.brew.trashed); const [alertTrashedGoogleBrew, setAlertTrashedGoogleBrew] = useState(props.brew.trashed);
const [alertLoginToTransfer , setAlertLoginToTransfer ] = useState(false); const [alertLoginToTransfer, setAlertLoginToTransfer] = useState(false);
const [confirmGoogleTransfer , setConfirmGoogleTransfer ] = useState(false); const [confirmGoogleTransfer, setConfirmGoogleTransfer] = useState(false);
const [autoSaveEnabled , setAutoSaveEnabled ] = useState(true); const [autoSaveEnabled, setAutoSaveEnabled] = useState(true);
const [warnUnsavedChanges , setWarnUnsavedChanges ] = useState(true); const [warnUnsavedChanges, setWarnUnsavedChanges] = useState(true);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));

View File

@@ -1,4 +1,4 @@
/* eslint-disable max-lines */
import './homePage.less'; import './homePage.less';
// Common imports // Common imports
@@ -8,7 +8,7 @@ import Markdown from '@shared/markdown.js';
import _ from 'lodash'; import _ from 'lodash';
import { DEFAULT_BREW } from '../../../../server/brewDefaults.js'; 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 SplitPane from '../../../components/splitPane/splitPane.jsx';
import Editor from '../../editor/editor.jsx'; import Editor from '../../editor/editor.jsx';
@@ -45,16 +45,16 @@ const HomePage =(props)=>{
...props ...props
}; };
const [currentBrew , setCurrentBrew] = useState(props.brew); const [currentBrew, setCurrentBrew] = useState(props.brew);
const [error , setError] = useState(undefined); const [error, setError] = useState(undefined);
const [HTMLErrors , setHTMLErrors] = useState(Markdown.validate(props.brew.text)); const [HTMLErrors, setHTMLErrors] = useState(Markdown.validate(props.brew.text));
const [currentEditorViewPageNum , setCurrentEditorViewPageNum] = useState(1); const [currentEditorViewPageNum, setCurrentEditorViewPageNum] = useState(1);
const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1); const [currentEditorCursorPageNum, setCurrentEditorCursorPageNum] = useState(1);
const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1); const [currentBrewRendererPageNum, setCurrentBrewRendererPageNum] = useState(1);
const [themeBundle , setThemeBundle] = useState({}); const [themeBundle, setThemeBundle] = useState({});
const [unsavedChanges , setUnsavedChanges] = useState(false); const [unsavedChanges, setUnsavedChanges] = useState(false);
const [isSaving , setIsSaving] = useState(false); const [isSaving, setIsSaving] = useState(false);
const [autoSaveEnabled , setAutoSaveEnable] = useState(false); const [autoSaveEnabled, setAutoSaveEnable] = useState(false);
const editorRef = useRef(null); const editorRef = useRef(null);
const lastSavedBrew = useRef(_.cloneDeep(props.brew)); const lastSavedBrew = useRef(_.cloneDeep(props.brew));

View File

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

View File

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

View File

@@ -21,7 +21,7 @@ process.env.ADMIN_PASS = process.env.ADMIN_PASS || 'password3';
export default function createAdminApi(vite) { export default function createAdminApi(vite) {
const router = express.Router(); const router = express.Router();
const mw = { const mw = {
adminOnly : (req, res, next)=>{ adminOnly : (req, res, next)=>{
if(!req.get('authorization')){ if(!req.get('authorization')){
return res return res
@@ -37,9 +37,9 @@ const mw = {
} }
throw { HBErrorCode: '52', code: 401, message: 'Access denied' }; throw { HBErrorCode: '52', code: 401, message: 'Access denied' };
} }
}; };
const junkBrewPipeline = [ const junkBrewPipeline = [
{ $match : { { $match : {
updatedAt : { $lt: Moment().subtract(30, 'days').toDate() }, updatedAt : { $lt: Moment().subtract(30, 'days').toDate() },
lastViewed : { $lt: Moment().subtract(30, 'days').toDate() } lastViewed : { $lt: Moment().subtract(30, 'days').toDate() }
@@ -47,25 +47,25 @@ const junkBrewPipeline = [
{ $project: { textBinSize: { $binarySize: '$textBin' } } }, { $project: { textBinSize: { $binarySize: '$textBin' } } },
{ $match: { textBinSize: { $lt: 140 } } }, { $match: { textBinSize: { $lt: 140 } } },
{ $limit: 100 } { $limit: 100 }
]; ];
/* Search for brews that aren't compressed (missing the compressed text field) */ /* Search for brews that aren't compressed (missing the compressed text field) */
const uncompressedBrewQuery = HomebrewModel.find({ const uncompressedBrewQuery = HomebrewModel.find({
'text' : { '$exists': true } 'text' : { '$exists': true }
}).lean().limit(10000).select('_id'); }).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 // 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)=>{ router.get('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((objs)=>res.json({ count: objs.length })) .then((objs)=>res.json({ count: objs.length }))
.catch((error)=>{ .catch((error)=>{
console.error(error); console.error(error);
res.status(500).json({ error: 'Internal Server 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 // 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)=>{ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 }) HomebrewModel.aggregate(junkBrewPipeline).option({ maxTimeMS: 60000 })
.then((docs)=>{ .then((docs)=>{
const ids = docs.map((doc)=>doc._id); const ids = docs.map((doc)=>doc._id);
@@ -76,15 +76,15 @@ router.post('/admin/cleanup', mw.adminOnly, (req, res)=>{
console.error(error); console.error(error);
res.status(500).json({ error: 'Internal Server Error' }); res.status(500).json({ error: 'Internal Server Error' });
}); });
}); });
/* Searches for matching edit or share id, also attempts to partial match */ /* Searches for matching edit or share id, also attempts to partial match */
router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{ router.get('/admin/lookup/:id', mw.adminOnly, asyncHandler(HomebrewAPI.getBrew('admin', false)), async (req, res, next)=>{
return res.json(req.brew); return res.json(req.brew);
}); });
/* Find 50 brews that aren't compressed yet */ /* Find 50 brews that aren't compressed yet */
router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
const query = uncompressedBrewQuery.clone(); const query = uncompressedBrewQuery.clone();
query.exec() query.exec()
@@ -96,10 +96,10 @@ router.get('/admin/finduncompressed', mw.adminOnly, (req, res)=>{
console.error(err); console.error(err);
res.status(500).send(err.message || 'Internal Server Error'); res.status(500).send(err.message || 'Internal Server Error');
}); });
}); });
/* Cleans `<script` and `</script>` from the "text" field of a 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)=>{ 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}`); 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, '');}; function cleanText(text){return text.replaceAll(/(<\/?s)cript/gi, '');};
@@ -119,10 +119,10 @@ router.put('/admin/clean/script/:id', asyncHandler(HomebrewAPI.getBrew('admin',
req.account = undefined; req.account = undefined;
return await HomebrewAPI.updateBrew(req, res); return await HomebrewAPI.updateBrew(req, res);
}); });
/* Get list of a user's documents */ /* Get list of a user's documents */
router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{ router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
const username = req.params.user; const username = req.params.user;
const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists const fields = { _id: 0, text: 0, textBin: 0 }; // Remove unnecessary fields from document lists
@@ -131,10 +131,10 @@ router.get('/admin/user/list/:user', mw.adminOnly, async (req, res)=>{
const brews = await HomebrewModel.getByUser(username, true, fields); const brews = await HomebrewModel.getByUser(username, true, fields);
return res.json(brews); return res.json(brews);
}); });
/* Compresses the "text" field of a brew to binary */ /* Compresses the "text" field of a brew to binary */
router.put('/admin/compress/:id', (req, res)=>{ router.put('/admin/compress/:id', (req, res)=>{
HomebrewModel.findOne({ _id: req.params.id }) HomebrewModel.findOne({ _id: req.params.id })
.then((brew)=>{ .then((brew)=>{
if(!brew) if(!brew)
@@ -152,9 +152,9 @@ router.put('/admin/compress/:id', (req, res)=>{
console.error(err); console.error(err);
res.status(500).send('Error while saving'); res.status(500).send('Error while saving');
}); });
}); });
router.get('/admin/stats', mw.adminOnly, async (req, res)=>{ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
try { try {
const totalBrewsCount = await HomebrewModel.countDocuments({}); const totalBrewsCount = await HomebrewModel.countDocuments({});
const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true }); const publishedBrewsCount = await HomebrewModel.countDocuments({ published: true });
@@ -167,11 +167,11 @@ router.get('/admin/stats', mw.adminOnly, async (req, res)=>{
console.error(error); console.error(error);
return res.status(500).json({ error: 'Internal Server 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 = { const countLocksQuery = {
lock : { $exists: true } lock : { $exists: true }
@@ -183,9 +183,9 @@ router.get('/api/lock/count', mw.adminOnly, asyncHandler(async (req, res)=>{
return res.json({ count }); return res.json({ count });
})); }));
router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{ router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
const countLocksPipeline = [ const countLocksPipeline = [
{ {
$match : $match :
@@ -210,9 +210,9 @@ router.get('/api/locks', mw.adminOnly, asyncHandler(async (req, res)=>{
lockedDocuments 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;
@@ -242,9 +242,9 @@ router.post('/api/lock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
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 = { const filter = {
shareId : req.params.id shareId : req.params.id
@@ -265,9 +265,9 @@ router.put('/api/unlock/:id', mw.adminOnly, asyncHandler(async (req, res)=>{
}); });
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)=>{ router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
const countReviewsPipeline = [ const countReviewsPipeline = [
{ {
$match : $match :
@@ -292,9 +292,9 @@ router.get('/api/lock/reviews', mw.adminOnly, asyncHandler(async (req, res)=>{
reviewDocuments 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 === // === This route is NOT Admin only ===
// Any user can request a review of their document // Any user can request a review of their document
const filter = { const filter = {
@@ -319,9 +319,9 @@ router.put('/api/lock/review/request/:id', asyncHandler(async (req, res)=>{
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 = { const filter = {
shareId : req.params.id, shareId : req.params.id,
@@ -341,11 +341,11 @@ router.put('/api/lock/review/remove/:id', mw.adminOnly, asyncHandler(async (req,
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)=>{ router.get('/admin/notification/all', async (req, res, next)=>{
try { try {
const notifications = await NotificationModel.getAll(); const notifications = await NotificationModel.getAll();
return res.json(notifications); return res.json(notifications);
@@ -354,9 +354,9 @@ router.get('/admin/notification/all', async (req, res, next)=>{
console.log('Error getting all notifications: ', error.message); console.log('Error getting all notifications: ', error.message);
return res.status(500).json({ message: error.message }); return res.status(500).json({ message: error.message });
} }
}); });
router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{ router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
try { try {
const notification = await NotificationModel.addNotification(req.body); const notification = await NotificationModel.addNotification(req.body);
return res.status(201).json(notification); return res.status(201).json(notification);
@@ -364,9 +364,9 @@ router.post('/admin/notification/add', mw.adminOnly, async (req, res, next)=>{
console.log('Error adding notification: ', error.message); console.log('Error adding notification: ', error.message);
return res.status(500).json({ message: error.message }); return res.status(500).json({ message: error.message });
} }
}); });
router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{ router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, next)=>{
try { try {
const notification = await NotificationModel.deleteNotification(req.params.id); const notification = await NotificationModel.deleteNotification(req.params.id);
return res.json(notification); return res.json(notification);
@@ -374,9 +374,9 @@ router.delete('/admin/notification/delete/:id', mw.adminOnly, async (req, res, n
console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }'); console.error('Error deleting notification: { key: ', req.params.id, ' error: ', error.message, ' }');
return res.status(500).json({ message: 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 = { const props = {
url : req.originalUrl url : req.originalUrl
}; };
@@ -387,7 +387,7 @@ router.get('/admin', mw.adminOnly, asyncHandler(async (req, res) => {
let html = fs.readFileSync(htmlPath, 'utf-8'); let html = fs.readFileSync(htmlPath, 'utf-8');
if (!isProd && vite?.transformIndexHtml) { if(!isProd && vite?.transformIndexHtml) {
html = await vite.transformIndexHtml(req.originalUrl, html); html = await vite.transformIndexHtml(req.originalUrl, html);
} }
@@ -395,7 +395,7 @@ router.get('/admin', mw.adminOnly, asyncHandler(async (req, res) => {
'<head>', '<head>',
`<head>\n<script id="props">window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>` `<head>\n<script id="props">window.__INITIAL_PROPS__ = ${JSON.stringify(props)}</script>`
)); ));
})); }));
return router; 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 */); app.set('trust proxy', 1 /* number of proxies between user and server */);
if (vite) { if(vite) {
app.use(vite.middlewares); app.use(vite.middlewares);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,41 +1,41 @@
// vite-plugins/generateAssetsPlugin.js // vite-plugins/generateAssetsPlugin.js
import fs from "fs-extra"; import fs from 'fs-extra';
import path from "path"; import path from 'path';
import less from "less"; import less from 'less';
export function generateAssetsPlugin(isDev = false) { export function generateAssetsPlugin(isDev = false) {
return { return {
name: "generate-assets", name : 'generate-assets',
async buildStart() { async buildStart() {
const buildDir = path.resolve(process.cwd(), "build"); const buildDir = path.resolve(process.cwd(), 'build');
// Copy favicon // 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 // Copy shared styles/fonts
const assets = fs.readdirSync("./shared/naturalcrit/styles"); const assets = fs.readdirSync('./shared/naturalcrit/styles');
for (const file of assets) { for (const file of assets) {
await fs.copy(`./shared/naturalcrit/styles/${file}`, `${buildDir}/fonts/${file}`); await fs.copy(`./shared/naturalcrit/styles/${file}`, `${buildDir}/fonts/${file}`);
} }
// Compile Legacy themes // Compile Legacy themes
const themes = { Legacy: {}, V3: {} }; const themes = { Legacy: {}, V3: {} };
const legacyDirs = fs.readdirSync("./themes/Legacy"); const legacyDirs = fs.readdirSync('./themes/Legacy');
for (const dir of legacyDirs) { 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; themeData.path = dir;
themes.Legacy[dir] = themeData; themes.Legacy[dir] = themeData;
const src = `./themes/Legacy/${dir}/style.less`; const src = `./themes/Legacy/${dir}/style.less`;
const outputDir = `${buildDir}/themes/Legacy/${dir}/style.css`; 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); await fs.outputFile(outputDir, lessOutput.css);
} }
// Compile V3 themes // Compile V3 themes
const v3Dirs = fs.readdirSync("./themes/V3"); const v3Dirs = fs.readdirSync('./themes/V3');
for (const dir of v3Dirs) { 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; themeData.path = dir;
themes.V3[dir] = themeData; themes.V3[dir] = themeData;
@@ -50,30 +50,30 @@ export function generateAssetsPlugin(isDev = false) {
const src = `./themes/V3/${dir}/style.less`; const src = `./themes/V3/${dir}/style.less`;
const outputDir = `${buildDir}/themes/V3/${dir}/style.css`; 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); await fs.outputFile(outputDir, lessOutput.css);
} }
// Write themes.json // 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 // Copy fonts/assets/icons
await fs.copy("./themes/fonts", `${buildDir}/fonts`); await fs.copy('./themes/fonts', `${buildDir}/fonts`);
await fs.copy("./themes/assets", `${buildDir}/assets`); await fs.copy('./themes/assets', `${buildDir}/assets`);
await fs.copy("./client/icons", `${buildDir}/icons`); await fs.copy('./client/icons', `${buildDir}/icons`);
// Compile CodeMirror editor themes // Compile CodeMirror editor themes
const editorThemesBuildDir = `${buildDir}/homebrew/cm-themes`; const editorThemesBuildDir = `${buildDir}/homebrew/cm-themes`;
await fs.copy("./node_modules/codemirror/theme", editorThemesBuildDir); await fs.copy('./node_modules/codemirror/theme', editorThemesBuildDir);
await fs.copy("./themes/codeMirror/customThemes", editorThemesBuildDir); await fs.copy('./themes/codeMirror/customThemes', editorThemesBuildDir);
const editorThemeFiles = fs.readdirSync(editorThemesBuildDir); const editorThemeFiles = fs.readdirSync(editorThemesBuildDir);
await fs.outputFile(`${buildDir}/homebrew/codeMirror/editorThemes.json`, 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 // 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 //old vitreum file, still imported in some pages
const injectTag = (tag, props, children) => { const injectTag = (tag, props, children)=>{
const injectNode = document.createElement(tag); const injectNode = document.createElement(tag);
Object.entries(props).forEach(([key, val]) => injectNode[key] = val); Object.entries(props).forEach(([key, val])=>injectNode[key] = val);
if (children) injectNode.appendChild(document.createTextNode(children)); if(children) injectNode.appendChild(document.createTextNode(children));
document.getElementsByTagName('head')[0].appendChild(injectNode); document.getElementsByTagName('head')[0].appendChild(injectNode);
}; };
const obj2props = (obj) => const obj2props = (obj)=>Object.entries(obj)
Object.entries(obj) .map(([k, v])=>`${k}="${v}"`)
.map(([k, v]) => `${k}="${v}"`) .join(' ');
.join(" "); const toStr = (chld)=>(Array.isArray(chld) ? chld.join('') : chld);
const toStr = (chld) => (Array.isArray(chld) ? chld.join("") : chld); const onServer = typeof window === 'undefined';
const onServer = typeof window === "undefined";
let NamedTags = {}; let NamedTags = {};
let UnnamedTags = []; let UnnamedTags = [];
export const HeadComponents = { export const HeadComponents = {
Title({ children }) { Title({ children }) {
if (onServer) NamedTags.title = `<title>${toStr(children)}</title>`; if(onServer) NamedTags.title = `<title>${toStr(children)}</title>`;
useEffect(() => { useEffect(()=>{
document.title = toStr(children); document.title = toStr(children);
}, [children]); }, [children]);
return null; return null;
}, },
Favicon({ type = "image/png", href = "", rel = "icon", id = "favicon" }) { Favicon({ type = 'image/png', href = '', rel = 'icon', id = 'favicon' }) {
if (onServer) NamedTags.favicon = `<link rel='shortcut icon' type="${type}" id="${id}" href="${href}" />`; if(onServer) NamedTags.favicon = `<link rel='shortcut icon' type="${type}" id="${id}" href="${href}" />`;
useEffect(() => { useEffect(()=>{
document.getElementById(id).href = href; document.getElementById(id).href = href;
}, [id, href]); }, [id, href]);
return null; return null;
}, },
Description({ children }) { Description({ children }) {
if (onServer) NamedTags.description = `<meta name='description' content='${toStr(children)}' />`; if(onServer) NamedTags.description = `<meta name='description' content='${toStr(children)}' />`;
return null; return null;
}, },
Noscript({ children }) { Noscript({ children }) {
if (onServer) UnnamedTags.push(`<noscript>${toStr(children)}</noscript>`); if(onServer) UnnamedTags.push(`<noscript>${toStr(children)}</noscript>`);
return null; return null;
}, },
Script({ children = [], ...props }) { Script({ children = [], ...props }) {
if (onServer) { if(onServer) {
UnnamedTags.push( UnnamedTags.push(
children.length children.length
? `<script ${obj2props(props)}>${toStr(children)}</script>` ? `<script ${obj2props(props)}>${toStr(children)}</script>`
@@ -53,31 +52,31 @@ export const HeadComponents = {
return null; return null;
}, },
Meta(props) { 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); props.property || props.name ? (NamedTags[props.property || props.name] = tag) : UnnamedTags.push(tag);
useEffect(() => { useEffect(()=>{
document document
.getElementsByTagName("head")[0] .getElementsByTagName('head')[0]
.insertAdjacentHTML("beforeend", Object.values(NamedTags).join("\n")); .insertAdjacentHTML('beforeend', Object.values(NamedTags).join('\n'));
}, [NamedTags]); }, [NamedTags]);
return null; return null;
}, },
Style({ children, type = "text/css" }) { Style({ children, type = 'text/css' }) {
if (onServer) UnnamedTags.push(`<style type="${type}">${toStr(children)}</style>`); if(onServer) UnnamedTags.push(`<style type="${type}">${toStr(children)}</style>`);
return null; return null;
}, },
}; };
export const Inject = ({ tag, children, ...props }) => { export const Inject = ({ tag, children, ...props })=>{
useEffect(() => { useEffect(()=>{
injectTag(tag, props, children); injectTag(tag, props, children);
}, []); }, []);
return null; 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 = {}; NamedTags = {};
UnnamedTags = []; UnnamedTags = [];
}; };