From 828bba61de9adc06c7f26dad58d77e8b45e9ef1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Tue, 3 Mar 2026 22:39:29 +0100 Subject: [PATCH 1/2] combobox fix --- client/components/combobox.jsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 5060b8ecf..4d850b00d 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -21,6 +21,7 @@ const Combobox = createReactClass({ }; }, getInitialState : function() { + this.dropdownRef = React.createRef(); return { showDropdown : false, value : '', @@ -41,7 +42,7 @@ const Combobox = createReactClass({ }, handleClickOutside : function(e){ // Close dropdown when clicked outside - if(this.refs.dropdown && !this.refs.dropdown.contains(e.target)) { + if (this.dropdownRef.current && !this.dropdownRef.current.contains(e.target)) { this.handleDropdown(false); } }, @@ -128,7 +129,7 @@ const Combobox = createReactClass({ }); return (
{this.handleDropdown(false);} : undefined}> {this.renderTextInput()} {this.renderDropdown(dropdownChildren)} From 8d18529c6dc3f4daa40a0a73b7227e88cfaba65a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?V=C3=ADctor=20Losada=20Hern=C3=A1ndez?= Date: Tue, 3 Mar 2026 23:44:02 +0100 Subject: [PATCH 2/2] linting --- client/admin/main.jsx | 6 +- client/components/combobox.jsx | 8 +- client/homebrew/editor/editor.jsx | 2 +- .../editor/metadataEditor/metadataEditor.jsx | 12 +- .../tagInput/curatedTagSuggestionList.js | 376 +- client/homebrew/editor/tagInput/tagInput.jsx | 222 +- .../editor/tagInput/tagSuggestionList.js | 3958 ++++++++--------- client/homebrew/main.jsx | 6 +- client/homebrew/navbar/navbar.jsx | 4 +- client/homebrew/pages/editPage/editPage.jsx | 30 +- client/homebrew/pages/homePage/homePage.jsx | 20 +- client/homebrew/pages/newPage/newPage.jsx | 18 +- server.js | 32 +- server/admin.api.js | 450 +- server/app.js | 2 +- server/homebrew.api.js | 2 +- shared/markdown.js | 2 +- themes/Legacy/5ePHB/snippets/coverpage.gen.js | 2 +- themes/V3/Blank/snippets/license.gen.js | 50 +- .../V3/Blank/snippets/licenseDTRPGCC.gen.js | 8 +- .../V3/Blank/snippets/licenseMongoose.gen.js | 3 - vite.config.js | 52 +- vitePlugins/generateAssetsPlugin.js | 42 +- vitreum/headtags.js | 59 +- 24 files changed, 2679 insertions(+), 2687 deletions(-) diff --git a/client/admin/main.jsx b/client/admin/main.jsx index bd380789a..a5b7c84ad 100644 --- a/client/admin/main.jsx +++ b/client/admin/main.jsx @@ -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(); +createRoot(document.getElementById('reactRoot')).render(); diff --git a/client/components/combobox.jsx b/client/components/combobox.jsx index 4d850b00d..7c015d3d6 100644 --- a/client/components/combobox.jsx +++ b/client/components/combobox.jsx @@ -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); } diff --git a/client/homebrew/editor/editor.jsx b/client/homebrew/editor/editor.jsx index 7f55ebf08..06fd469a0 100644 --- a/client/homebrew/editor/editor.jsx +++ b/client/homebrew/editor/editor.jsx @@ -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 }); }); diff --git a/client/homebrew/editor/metadataEditor/metadataEditor.jsx b/client/homebrew/editor/metadataEditor/metadataEditor.jsx index acd457d98..8d6ccb890 100644 --- a/client/homebrew/editor/metadataEditor/metadataEditor.jsx +++ b/client/homebrew/editor/metadataEditor/metadataEditor.jsx @@ -338,9 +338,9 @@ const MetadataEditor = createReactClass({ {this.renderThumbnail()}
-
+
-
+
- + {this.renderLanguageDropdown()} @@ -363,9 +363,9 @@ const MetadataEditor = createReactClass({ {this.renderAuthors()} -
+
-
+
- +

Privacy

diff --git a/client/homebrew/editor/tagInput/curatedTagSuggestionList.js b/client/homebrew/editor/tagInput/curatedTagSuggestionList.js index d433175ef..9b9afce6f 100644 --- a/client/homebrew/editor/tagInput/curatedTagSuggestionList.js +++ b/client/homebrew/editor/tagInput/curatedTagSuggestionList.js @@ -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', ]; diff --git a/client/homebrew/editor/tagInput/tagInput.jsx b/client/homebrew/editor/tagInput/tagInput.jsx index 7f4a3a77f..c875114f5 100644 --- a/client/homebrew/editor/tagInput/tagInput.jsx +++ b/client/homebrew/editor/tagInput/tagInput.jsx @@ -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 ( -
+
submitTag(value)} - onEntry={(e) => { - if (e.key === "Enter") { + onSelect={(value)=>submitTag(value)} + onEntry={(e)=>{ + if(e.key === 'Enter') { e.preventDefault(); submitTag(e.target.value); } }} /> -
    - {tagList.map((t, i) => - t.editing ? ( - - setTagList((prev) => - prev.map((tag, idx) => (idx === i ? { ...tag, draft: e.target.value } : tag)), - ) +
      + {tagList.map((t, i)=>t.editing ? ( + 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 - /> - ) : ( -
    • editTag(i)}> - {t.value} - -
    • - ), + if(e.key === 'Escape') { + stopEditing(i); + e.target.blur(); + } + }} + autoFocus + /> + ) : ( +
    • editTag(i)}> + {t.value} + +
    • + ), )}
diff --git a/client/homebrew/editor/tagInput/tagSuggestionList.js b/client/homebrew/editor/tagInput/tagSuggestionList.js index 6b8e4060e..eb4891616 100644 --- a/client/homebrew/editor/tagInput/tagSuggestionList.js +++ b/client/homebrew/editor/tagInput/tagSuggestionList.js @@ -1,1980 +1,1980 @@ export default [ - "meta:Theme", - "5e", - "Subclass", - "meta:theme", - "subclass", - "Class", - "Homebrew", - "Race", - "Dungeons and Dragons", - "theme", - "Daggerheart", - "2024", - "One Piece", - "One Piece DND", - "Luffy", - "Dungeons and Devil Fruits", - "Strawhats", - "Template", - "Campaign Frame", - "class", - "Players Handbook", - "dnd", - "osr", - "Dungeon Masters Guide", - "shadowdark", - "dragonbane", - "PHB", - "example", - "Devil Fruits", - "system:pf2e", - "DnD", - "DMG", - "system:dnd5.5", - "Monster", - "homebrew", - "race", - "template", - "Warlock", - "monster", - "Fighter", - "warlock", - "druid", - "sorcerer", - "D&D", - "Magic Item", - "Barbarian", - "Artificer", - "2014", - "system:descent into avernus", - "Sorcerer", - "Adventure", - "Paladin", - "Ranger", - "user help", - "fighter", - "5th Edition", - "Spells", - "Monk", - "Spell", - "NPC", - "Cleric", - "spell", - "Rogue", - "css", - "Item", - "artificer", - "magic item", - "Rules", - "barbarian", - "wizard", - "russian", - "DnD5e", - "Wizard", - "paladin", - "bastionland", - "spells", - "Devil Fruit", - "Bard", - "5.5e", - "rogue", - "Tabletop System", - "Haki", - "Druid", - "mystic bastionland", - "item", - "Lore", - "bard", - "monk", - "system:dnd5e", - "world", - "ranger", - "WIP", - "cleric", - "Dragon", - "Naruto", - "Creature", - "snippet", - "npc", - "DeS", - "Magic", - "guide", - "v3", - "Beast", - "Classe", - "onering", - "Monsters", - "Races", - "Weapon", - "adventure", - "Subclasses", - "stat block", - "weapon", - "Species", - "DONE", - "archetype", - "RPG", - "Hollow Knight", - "5e'24", - "Martial", - "DND", - "Classe Nova", - "Curse of Strahd", - "Boss", - "Hollowed Kingdoms", - "baldurs mouth", - "5.24", - "Homewbrew", - "Encyclopedia", - "Revised", - "OneWorldHD", - "knight", - "DPS", - "srd", - "Undead", - "items", - "DnD 5e", - "Guide", - "Compendium", - "Feat", - "newspaper", - "magic", - "TTRPG", - "descent into avernus", - "reference", - "system:D&D 5e24", - "feat", - "Magic Items", - "Campaign", - "resource", - "Feats", - "Anime", - "dd5", - "races", - "Monstrosity", - "DM Screen", - "2024 Rules", - "Rework", - "Character Build", - "Done", - "5e 2024", - "Construct", - "myth", - "magic items", - "creature", - "Legendary", - "Strahd", - "background", - "Player", - "style", - "Legacy", - "Player Handbook", - "martial", - "Character", - "Dungeons & Dragons", - "Table", - "reddit", - "monsters", - "OneDND", - "dragon", - "Suporte", - "Soulbound", - "Expanded Handbook", - "bestiary", - "Humanoid", - "system:dnd", - "Oredell", - "system:class", - "V3", - "system:5e", - "5e'14", - "Age of Sigmar", - "Items", - "Eberron", - "horror", - "dnd5e", - "star wars", - "Background", - "compendium", - "revision", - "Elemental", - "character", - "Marcial", - "SW5e", - "notes", - "DM", - "Fey", - "one-shot", - "resources", - "NEW", - "campaign", - "wondrous item", - "Setting", - "Archive", - "NIMRE", - "Tanque", - "Jogavel", - "Dnd 5e", - "LotM", - "setting", - "revised", - "Warhammer", - "rework", - "Conjurador", - "GMBinder", - "ItemSet", - "NPCs", - "classes", - "CR 2", - "AetherSail", - "undead", - "Artifact", - "cards", - "Example", - "Guides", - "Theme", - "Swamp", - "CR 1", - "patron", - "Extra", - "concept", - "final fantasy", - "Aberration", - "Elder Scrolls", - "rules", - "meta:gratis", - "Dice Pool", - "Cue", - "dark", - "type:Style", - "rpg", - "meta:free", - "summoner", - "OC", - "rare", - "Fleshing Out", - "francais", - "Dnd", - "Creatures", - "Sims 4", - "sous-classe", - "ffxiv", - "Underdark", - "add-on", - "weapons", - "Subrace", - "dungeons and dragons", - "Naruto 5e", - "Lafari", - "Very Rare", - "d6", - "red", - "Equipment", - "CR 3", - "Patron", - "5E", - "objet magique", - "spellcaster", - "feats", - "5.24e", - "system:d&d5e", - "Rare", - "Thudnfer", - "daggerheart", - "CR 1/2", - "Archetype", - "dnd 5e", - "Pathfinder", - "lineage", - "Celestial", - "Support", - "species", - "ARCHIVED", - "Horror", - "humanoid", - "Subclase", - "book:PHB E&E", - "final fantasy xiv", - "Jungle", - "Feywild", - "Fiend", - "subclasses", - "HB", - "Legacy Challenge", - "Project Horizon", - "vampire", - "WoW", - "DND 5e", - "Weapons", - "system:book clone", - "CoS", - "N5e", - "Summon", - "Spellcaster", - "Koretra", - "Voidborn", - "one shot", - "Templates", - "tables", - "Iphexar", - "Shattered Obelisk", - "Sword Coast", - "elemental", - "lore", - "character sheet", - "discord", - "BetterMonsters", - "players", - "group:simple skans", - "Coastal", - "Forest", - "Unearthed Arcana", - "Old", - "Collection", - "Ancestry", - "Caevash", - "Strixhaven", - "Limbus Company", - "Daydreams & Deviants", - "Statblocks", - "Urban", - "Classes", - "familiar", - "Blood", - "oneshot", - "n5e", - "wip", - "masks", - "Planeshifted", - "Carrioss", - "avernus", - "object", - "1", - "Ruins", - "Anime Character", - "wild shape", - "mask", - "dj9 game", - "Handbook", - "Curse", - "oath", - "Midralis", - "Appendix", - "5.5", - "tales of the valiant", - "player classes", - "Caster", - "Large", - "Fateforge", - "Cursed", - "beast", - "Pathfinder 2e", - "Design", - "Uncommon", - "Endeur", - "Elemental Water", - "SCC", - "sci-fi", - "Dragons", - "human", - "Gods", - "Medium", - "System", - "Help", - "Handout", - "Skyrim", - "BnB", - "draft", - "PC", - "Variant", - "syntax", - "OneShot", - "Clase", - "UESTRPG", - "Savannah", - "Reef", - "CR 5", - "Forgotten Realms", - "collection", - "Combat", - "Historia", - "artifact", - "Expansion", - "Attunement", - "Variant Rules", - "d&d", - "Party Build", - "Evocation", - "homerule", - "Forbidden West", - "how-to", - "tov", - "Mystical Item", - "CR 4", - "Necromancer", - "General Rules", - "Needs Update", - "stat blocks", - "DC Comics", - "Sword", - "Armor", - "blood hunter", - "Blood Hunter", - "Meio Conjurador", - "Mydia", - "3rd Party", - "N5E", - "Delvebound", - "Ocean", - "Subterranean", - "half-caster", - "Demon", - "Fire", - "meta:Template", - "Character Sheet", - "PF2e", - "png", - "fantasy", - "Setting Guide", - "Style", - "Cor", - "legacy", - "Minion", - "meta:khaoz age", - "Book", - "magical item", - "immersion", - "reminder cards", - "Regras", - "Durnovar", - "2025", - "Camp1", - "Abyss", - "armor", - "construct", - "firearms", - "Backgrounds", - "Companion", - "Melee", - "fey", - "Incomplete", - "Vampire", - "Bestiary", - "Worldbuilding", - "History", - "Conjuration", - "necromancy", - "dragons", - "ancestry", - "Mech", - "PbtA", - "COS", - "One Shot", - "Factions", - "Transmutation", - "Spellcasting", - "card", - "Ardh", - "redveil", - "conditions", - "elturel", - "rhye", - "group:James Haeck", - "CaelYuu", - "system:5.24e", - "system:GM Binder", - "Grassland", - "melee", - "Potions", - "anime", - "D&D 5e", - "Healer", - "Gambling", - "Lightning", - "fighting style", - "support", - "Style Template", - "mtg", - "NSFW", - "aventura", - "Manuals", - "plant", - "Incarnate", - "Crafting", - "DnDBeyond", - "Monster Girl", - "Monster Girl Encyclopedia", - "pet", - "npcs", - "Stat Block", - "Styleguide", - "mitologia", - "Objeto maravilloso", - "vaesen rpg", - "dnd-2024", - "Class Handbook", - "Space", - "Taiga", - "CR 6", - "Pact Boon", - "race/ancestry", - "Necromancy", - "cantrip", - "LoL", - "Raza", - "handout", - "Mechanic", - "Conversion", - "Wondrous Item", - "Familiar", - "necromancer", - "uncommon", - "curse", - "Campaign Setting", - "Tetra", - "1e", - "module", - "Evolving", - "boiling sea", - "deck", - "OSR", - "RU", - "VL", - "Underdeep", - "Deep Ocean", - "Giant", - "statblock", - "combat", - "Time", - "5E24", - "session zero", - "elf", - "tank", - "sorcerous origin", - "Drakkenheim", - "Tavern", - "Domain", - "healer", - "Valenor", - "blood", - "Oath", - "spooky", - "fire", - "meta:5e24 Style", - "Notes", - "City", - "Conjurador Completo", - "prop", - "Dotherys", - "Rietuma 3.0", - "5e24", - "Library of Ruina", - "español", - "Project Echo", - "battle of Japan", - "Plant", - "Badlands", - "Neverwinter", - "Fantasy", - "Beastfolk", - "Unarmed", - "Cold", - "Damage", - "attunement", - "Hurthud", - "3rd Level", - "spelljammer", - "mostro", - "Custom", - "PT-BR", - "Alternative Realms", - "The Foot", - "boss", - "demo", - "Supplement", - "FitD", - "classe", - "5.14", - "Copy", - "DnD5e24", - "X-Men", - "TNA", - "CR 8", - "Desert", - "CR 7", - "arme", - "random", - "spellcasting", - "Deprecated", - "Cards", - "finished", - "Ben 10", - "equipment", - "Geography", - "Games", - "For Players", - "Faerun", - "scroll", - "Faction", - "Alchemist", - "drow", - "Lineage", - "mix-blend-mode", - "columns", - "User Help", - "Reami Dimenticati", - "Класс", - "D100", - "nsfw", - "hucaen", - "v1.0", - "Cortex", - "Fallout", - "ww5e", - "MAGIC", - "DnD2024", - "ToV", - "D&D2024", - "The Backrooms", - "Freshwater", - "D20", - "Dragonborn", - "custom", - "sword", - "Dungeons and Dragons 5e", - "Water", - "legendary", - "Dungeon", - "Ravenloft", - "aberration", - "Longsword", - "transmutation", - "Fairy Tail", - "Character background", - "Exandria", - "Updated", - "pitch", - "Half-Caster", - "Complete", - "Money", - "player", - "forgotten-realms", - "Festival", - "Casino", - "SCAG", - "Currency", - "North", - "Toril", - "Scourged Land of Valenor", - "Oota", - "parchment", - "Literature", - "Serrith", - "PNJ", - "Divinity Original Sin 2", - "Wild", - "videogame", - "magic the gathering", - "sweetblossom", - "GMscreen", - "MandM", - "D&D 5.24", - "Camp2", - "Remaster", - "riassunti", - "type:Resource", - "system:D&D", - "tag:Class", - "Excelsior", - "Stat Blocks", - "Sci-Fi", - "Ooze", - "CR 1/8", - "sublclass", - "chart", - "Mountains", - "guns", - "Nature", - "Orc", - "Poison", - "Devil", - "fiend", - "DC", - "pt-br", - "ABnB", - "One-Shot", - "strahd", - "Ring", - "Theme song", - "orc", - "summon", - "Psion", - "Psionics", - "Dungeon Master", - "vehicle", - "DM only", - "Demigod", - "Antica Energia", - "Pirates", - "Sourcebook", - "devil", - "Cantrip", - "mystery", - "MtG", - "conversion", - "Festivals", - "Casinos", - "Taverns", - "Betting", - "Drinking", - "phandelver", - "Warhammer 40k", - "mutant", - "styling", - "FATE", - "Lone Wolf", - "icon", - "New Dawn", - "Magic Set", - "Paladin Subclass", - "Alter Class", - "difficulty classses", - "combat tables", - "phb", - "Project Moon", - "Undertomes", - "EGO", - "Campagne 1", - "Constelação", - "Arvore I", - "Fim da Jornada", - "greek god", - "dwarf", - "Firearms", - "3.5e", - "generator", - "Elf", - "meta: Scenario", - "enchantment", - "buff", - "ITW", - "Tank", - "Archived", - "Martial Archetype", - "caster", - "BR", - "Knight", - "Utility", - "SWADE", - "Star Wars", - "pc", - "Mystic", - "Useful", - "Netherdeep", - "crafting", - "Sapient Undead", - "Maverick", - "Revision", - "Resource", - "Humblewood", - "one piece", - "Bag of Holding", - "medium", - "lightning", - "backgrounds", - "4th Level", - "path", - "BREAK-RPG", - "dark fantasy", - "Players", - "poison", - "psionic", - "gazook89", - "homebrew subclass", - "wild-wasteland", - "CWD", - "Paid", - "Tales of the Valiant", - "Dreadhold", - "arma", - "system:Mutants and Masterminds", - "#Tiefschlaf", - "Brew", - "Myra", - "Swashbuckler", - "dead by daylight", - "Exceptional", - "COD Zombies", - "Hills", - "Tundra", - "type:Campaign", - "wild magic", - "Food", - "Death", - "homebrew rules", - "Remake", - "Witcher", - "water", - "Pet", - "book", - "AAH", - "pact", - "Ice", - "Character Creation", - "animal", - "Pokemon", - "clase", - "5e14", - "DBZ", - "CLONE", - "Evil", - "Tarsere", - "Mythology", - "pf2e", - "Magical", - "type:race", - "Sorcerous Origin", - "Information", - "styles", - "Module", - "gish", - "frames", - "DeltaGreen", - "Magic item", - "food", - "chef", - "basics", - "giant", - "Brew Creation", - "One-shot", - "ttrpg", - "Path", - "Don't Starve", - "MGE", - "firearm", - "DnDBehindTheScreen", - "store", - "The Artisan", - "timeline", - "college", - "dev", - "dungeon of the dead three", - "Cradle", - "Dnd5e", - "dungeon", - "Amaranthine", - "Regno di Oltremare", - "bestia", - "rewrite", - "WiP", - "Subclasse", - "mutants and masterminds", - "The Embrace", - "meta:documentation", - "Mutants And Masterminds", - "khedoria", - "Encounter Pack", - "giorni", - "Statblock", - "Enemies", - "Goblinoids", - "Heavens", - "system:2e", - "Vaalbara", - "Dwarf", - "airos", - "table", - "Artificer Specialization", - "Buff", - "Book 1", - "Ranged", - "cypher", - "utilities", - "40k", - "Psychic", - "Fear", - "steampunk", - "shadow", - "subclase", - "Barbarian Subclass", - "Elements", - "pact boon", - "Clan", - "Fly", - "solo", - "sourcebook", - "Marvel Comics", - "compilation", - "Firearm", - "sidekick", - "infusions", - "Mechanics", - "Summoner", - "Aasimar", - "Human", - "Vehicle", - "Shadow", - "Clone", - "custom css", - "ocean", - "sotdl", - "bandit", - "Wind", - "Printer Friendly", - "Obsolete", - "mechanics", - "illusion", - "5th edition", - "League of Legends", - "Vestige", - "dungeons", - "Dungeons", - "and", - "Elden Ring", - "L5R", - "d20", - "Poisons", - "d15", - "Dungeons And Dragons", - "MTG", - "divine", - "characters", - "witch", - "Anime Homebrew", - "Zombie", - "thunder", - "Jujutsu Kaisen", - "campagne", - "Deadlands", - "spell list", - "1 Person", - "Ritual", - "screen", - "nature", - "Divination", - "Compattare", - "dtrpg", - "quick ref", - "Mago", - "Illivia", - "Shonen", - "Core Deities", - "green ronin", - "Bless", - "D&D5e", - "version:0.1.0", - "curato", - "system:Ord", - "Images", - "Sealed Artifact", - "Giants", - "CR 9", - "CR 11", - "CR 0", - "CR 14", - "Shadowfell", - "Tier 1", - "d100", - "Elemental Air", - "artificiel", - "Cultist", - "Cyberpunk", - "Huge", - "Warrior", - "Gun", - "quest", - "LYRA", - "Music", - "Tiefling", - "Master", - "Witch", - "Linnorm", - "1st-level", - "Mount", - "Animal", - "Comics", - "Superhero", - "creatures", - "Hunter", - "Control", - "Dragon Ball", - "Dragon Ball Z", - "Dagger", - "questingforamonster", - "ICRPG", - "Booklet", - "f and t", - "common", - "Chaos", - "spellblade", - "Constitution", - "artisan", - "arcane", - "Released", - "ring", - "runes", - "gun", - "Supportive Material", - "The Witcher", - "Desarmado", - "Monster Monday", - "Bleach", - "Demon Slayer", - "mice", - "worldbuilding", - "Necrotic", - "ability score", - "demon", - "Armybook Shivatiano", - "warrior", - "Fighter Subclass", - "system", - "whisperveil", - "psychic", - "warhammer", - "Aventura", - "Culture", - "Material", - "meta:npc", - "shops", - "magic weapon", - "nhera", - "Dark Fantasy", - "Regles", - "Wonderous Item", - "Features", - "pokemon", - "Ghosts of Saltmarsh", - "monstrosity", - "DL TWW", - "companion", - "alternate layout", - "Tutorials", - "Kitsune", - "don", - "heroique", - "mini campaign", - "drago", - "Aquatic", - "tool", - "handmade", - "released", - "Spellblade", - "pregen", - "level 2", - "Baldurs gate 3", - "My Hero", - "Technically a subclass", - "5.24e Remastered Subclasses", - "dinosaurs", - "5E.2024", - "Razas", - "Horizon", - "Clothing", - "+2", - "castellano", - "pentacle prophecy", - "tag:Spells", - "gruppo A", - "Rpg", - "razze", - "type:Adventure", - "unfinished", - "3.5", - "gunslinger", - "BBEG", - "Arcane", - "component", - "Bow", - "backstory", - "phandalin", - "Skills", - "Pact", - "Elemental Earth", - "Joke", - "invocation", - "martial class", - "Super Villain", - "Eldritch", - "Elemental Fire", - "Homebrew Class", - "eldritch", - "cyberpunk", - "Player Race", - "Class Mod", - "Heatcoast", - "meta:Guide", - "Yemao", - "evil", - "Named NPC", - "CLASS", - "Angel", - "vecna", - "PT", - "PTBR", - "Ancient", - "Small", - "WotC Style", - "5e Homebrew", - "1st Level", - "dagger", - "Brancalonia", - "encounter", - "cat", - "primal path", - "Ambientazione", - "Magie", - "candlekeep", - "Ongoing", - "Oneshot", - "Wondrous", - "Janbrewery", - "Tattoos", - "5e (2014)", - "concentration", - "very rare", - "Set", - "Kobold", - "martial archetype", - "God", - "blog", - "New Gate", - "Healing", - "OneDnD", - "Incantesimi", - "Player Options", - "contest", - "pirate", - "Manuel", - "Alchimie", - "Herboristerie", - "Ingredients", - "starlost", - "Campaign 1", - "Abandoned", - "Previous Editions", - "Enchantment", - "Tools", - "Oblivion", - "domain", - "5th Level", - "DnD Beyond", - "Reference", - "Sorcerer Subclass", - "Dragon Magazine", - "feature", - "german", - "conjuration", - "strixhaven", - "Sentient", - "JJK", - "10 Generations", - "character creation", - "LevelUp", - "pallid grove", - "primer", - "Requires Attunement", - "College", - "Aesthetic", - "critter", - "home game", - "spanish", - "stats", - "Lairon", - "Hunters Guild", - "original setting", - "Bosses", - "Radiant Citadel", - "actions", - "Reworked", - "Elystera", - "Wyvern", - "vikings", - "thief", - "enemies", - "Obsession", - "Yi Sang", - "aberrazione", - "Limbus", - "animals", - "minecraft", - "mice of legend", - "osric", - "20 Minutes Till Dawn", - "campaign frame", - "latigo", - "DH", - "Eldritch Invocation", - "system:daggerheart", - "100ni", - "meta:Sheet", - "fa-solid fa-sheet-plastic:Ficha", - "tag:Berean", - "AD&D", - "B/X", - "The Codex Of Anomalous Entities", - "monster manual", - "Polar Waters", - "CR 12", - "CR 10", - "blood magic", - "Gunslinger", - "grimoire", - "Drakes", - "Japanese", - "subrace", - "ooze", - "Stats", - "Half Caster", - "Sea", - "time", - "Brawler", - "Session 0", - "Halloween", - "Runeterra", - "Divine", - "Random", - "Lifestar", - "arcane trickster", - "Paddy4530", - "evocation", - "light", - "Steampunk", - "shaman", - "Primal Path", - "monk subclass", - "Full Caster", - "World", - "Planning", - "spirit", - "Nova Era", - "abjuration", - "Christmas", - "Critical Role", - "Gish", - "Bandit", - "Monster Manual", - "party member", - "mgazt", - "Playable Race", - "Donjon.bin.sh", - "Final Fantasy", - "Roleplay", - "monstre", - "fairy", - "frame", - "Minecraft", - "Stealth", - "Manual", - "half caster", - "Storm", - "Sorcery", - "format work", - "Kingdom Hearts", - "hexblade", - "block", - "page layouts", - "Monk Subclass", - "FinyaFluKaiKolja", - "Radiant", - "group:playtest", - "Korrahir", - "noble", - "exorcist", - "xapien", - "Raven Queen", - "markdown", - "damage", - "Alchemy", - "morrigan", - "genasi", - "ZNH", - "folklore", - "Fate", - "hechicero", - "Air", - "Magic Weapon", - "Anime DND 5e", - "Dragon Ball Z TTRPG", - "Dragon Ball Z RPG", - "Dragon Ball Z DND", - "Dragon Ball Z 5e", - "samurai", - "Goblin", - "Base Sheet", - "Shackled City Adventure Path", - "Natureza", - "control", - "Normarch", - "Reddit", - "Genshin Impact", - "Abjuration", - "Myr", - "Flight", - "Vampyre", - "nightmare", - "Lycan", - "Occult", - "circle", - "Christmas Special", - "DoDD", - "Character Options", - "traduction", - "Characters", - "Gear", - "system:sf2e", - "drakkenheim", - "downtime", - "amulet", - "Feiticeiros e Maldicoes", - "Tecnica amaldicoada", - "prorpg", - "enemy", - "No Mercy", - "rain world", - "slugcat", - "fly", - "meta:User Guide", - "Fallout TTRPG", - "regles", - "Ill Tides", - "Light-hearted", - "Vastria", - "school", - "Fillible Online", - "Mezgarr", - "Berserk", - "invocations", - "Classe Refeita", - "Auroboros", - "bosses", - "fabula ultima", - "Shagya", - "wild", - "DnD 2024", - "KaiburrKathHound", - "Barbarian Path", - "fauna", - "5E.2014", - "system:curse of strahd", - "Unofficial", - "how to", - "Glaive", - "A5E", - "pt", - "Consumible", - "Realmers'", - "Versatile Lineage", - "Shichibukai", - "2024e", - "Rencontre", - "tag:Spell List", - "elementalist", - "noncaster", - "blasphemous", - "Mordhiem", - "Wildfrost", - "#Regelwerk", - "Rewrite", - "Maldición de Strahd", - "Scion", - "Entities", - "Hoarwyrm", - "Player utility", - "CR 1/4", - "Temperate Forest", - "Demons", - "Drow", - "type:rules", - "fay", - "2e", - "familier", - "supplement", - "Amberwar", - "slime", - "Lycanthropy", - "meta: Terres de Leyt", - "Strong", - "AAH Vol. 1", - "Force", - "Jump", - "Aboleths", - "lol", - "location", - "small", - "customizable", - "Modern", - "Sky", - "portugues", - "Hero", - "Villain", - "element", - "Tyranny of Dragons", - "Adventure Guide", - "New Class", - "Witchlight", - "Shardblade", - "Plateaux", - "WOTC", - "Snippet", - "Terra", - "Otherworldly Patron", - "ritual", - "hag", - "Cyberpunk 2077", - "tavern", - "Artificer Specialist", - "Werewolf", - "Boesia", - "vampiric", - "monastic tradition", - "Gothic", - "celestial", - "Unfinished", - "Core", - "Arcane Tradition", - "Troll", - "Origin", - "Draconic", - "dj9 member", - "test", - "Hag", - "gem", - "Invocations", - "Dark Sun", - "aarkhen", - "How to", - "ravenloft", - "faerie", - "Playtest", - "Shaman", - "dead", - "Tomba Aniquilacio", - "Pacto", - "fullcaster", - "Electric", - "Ability Score", - "4D", - "pathfinder", - "insect", - "hook", - "page layout", - "healing", - "Lineages", - "Flying", - "Martial Arts", - "journal", - "Aide de jeu", - "hunter", - "headers", - "Dark Souls", - "courtyard", - "crossroads", - "Quest", - "CotF", - "defense", - "Semryss", - "invoked class", - "Session Notes", - "goblin", - "infernal", - "fate", - "oni", - "spellbook", - "Summoning", - "slut", - "whore", - "Greyhawk", - "Mobility", - "Reddit Remake", - "Guild", - "Cosmic Mart", - "7th Level", - "dragonborn", - "curse of strahd", - "Ranger Subclass", - "dossier", - "dossie", - "de", - "pnpde", - "Plane Shift", - "halloween", - "group:aventura", - "9th Level", - "tome", - "cold", - "acid", - "deprecated", - "mind flayer", - "MECHA", - "EssentialsKit", - "2d6", - "ToD", - "Work In Progress", - "Bond", - "Versatile", - "Dead", - "SYWTBAGM", - "summoning", - "english", - "Eilistraee", - "Draft", - "DoD", - "map", - "Frightened", - "Psychic Damage", - "eberron", - "recompensa", - "wizard subclass", - "teiran", - "Saltmarsh", - "jp setting", - "Illithid", - "Longbow", - "hell", - "Monarch", - "type:feat", - "reglas", - "cooking", - "Abenteuer", - "reloaded", - "incompleto", - "mecanica", - "Location", - "Grimlores Grimoire", - "2024 Subclass", - "Chiesa di Toleno", - "finalfantasy", - "The Undertomes", - "Lobotomy Corporation", - "SDHTA", - "D&D 2024", - "other", - "ally", - "images", - "Player's Guide", - "Avalon Sword", - "Cael'Yuu", - "dnd-2014", - "Regelwerk", - "Español", - "br", - "dnd 5.0", - "monstro", - "grand cemetery", - "Phoenix", - "dnd 2024", - "Bloodhunter", - "Sintonizacion", - "dungeons & dragons", - "Fix", - "Rulebook", - "Shadowdark", - "heroic", - "HFW", - "Earthdawn", - "24e", - "cormyr", - "suzail", - "dc20", - "tag:Rules", - "The Griffon's Saddlebag", - "LOTM", - "tag:Adventure", - "drunken master", - "eldritch invocation", - "Персонаж", - "Orcs", - "Lizardfolk", - "Frostfell", - "CR 17", - "Shapechanger", - "Farmland", - "Mages", - "Any", - "CR 13", - "Earth", - "Mountain", - "Drake", - "transformation", - "GM", - "Lich", - "lovecraft", - "unique", - "Optional Rules", - "int", - "creator", - "Primal", - "simple", - "golem", - "Void", - "Armour", - "spellsword", - "General", - "Asian", - "Bringers of chaos", - "Optional Feature", - "subraces", - "Galanoth", - "barbarian subclass", - "felhearth", - "modular", - "Vampires", - "wysteria", - "adaptation", - "beasts", - "naruto", - "ninja", - "Psionic", - "Guns", - "Crystal", - "Guardian", - "NonProfit", - "Mimic", - "languages", - "Epic Boons", - "Primer", - "Icewind Dale", - "joke", - "lycan", - "CR3", - "Armors", - "ff7", - "materia", - "final fantasy 7 remake", - "esper", - "ff7 remake", - "gargantuan", - "Frog", - "CR5", - "blank", - "monster hunter", - "league of legends", - "french", - "Pokémon", - "kobold", - "soul", - "ffxi", - "d10", - "Roman", - "Cute", - "DD5", - "variant", - "tree", - "fr", - "Scenario", - "lycanthrope", - "druide", - "staff", - "eios", - "arkheneios", - "Runic", - "Work", - "Ukrainian", - "cover-page", - "mage", - "deities", - "gods", - "Boss Fight", - "Lair", - "WBTW", - "roguish archetype", - "Character Option", - "Shortsword", - "Illrigger", - "Bloodborne", - "cr6", - "Priest", - "Hamon", - "Toonkind", - "rol", - "Strength", - "forgotten realms", - "Spanish", - "Conclave", - "Electro", - "Magical Tattoos", - "Matt Mercer", - "Wildemount", - "Mighty Nien", - "Campaign 2", - "Resistances", - "Bug", - "impression", - "PF", - "Magnus Archives", - "ice", - "speed", - "Generic NPC", - "Titanic", - "Ink Friendly", - "bleed", - "elder scrolls", - "Immortal", - "LMOP", - "Travel", - "Olphus", - "3d6", - "heist", - "World History", - "ghost", - "genie", - "kids on bikes", - "Russia", - "conclave", - "overhaul", - "manual", - "Adventures In Eden", - "Downtime", - "hamon", - "cloak", - "shadowfell", - "Hellfire", - "Paladin Oath", - "Genshin", - "Nation", - "air", - "Magical Item", - "War", - "Original", - "Monstrous Compendium", - "Calamity", - "Warden", - "Apocalypse", - "shield", - "AC", - "expansion", - "Concentration", - "charm", - "Weave", - "lycanthropy", - "raza", - "far realm", - "fighter subclass", - "ita", - "Pirate", - "Laranja", - "Grapple", - "EastByForce", - "hobgoblin", - "oneshot-notes", - "Holy", - "optional", - "type:cenario", - "group:core", - "The Brewery", - "Alcance", - "Morrowind", - "Indigo", - "Divino", - "2nd Level", - "Sub-Class", - "cantrips", - "Cloak", - "battle master", - "Dark", - "Puzzle", - "Lucky", - "consumable", - "rebalance", - "Shove", - "Area Control", - "Vanguard", - "funny", - "e5", - "Dragonlance", - "psion", - "initiative", - "Tactician", - "Inspiration", - "artificier", - "way", - "inspired", - "historia", - "Medusa", - "2 part", - "holy", - "gift", - "Nimble", - "mostri", - "phoenix", - "travel", - "Class Template", - "Intimidation", - "constructs", - "P666", - "Formatting", - "Divinity", - "Rod", - "Language", - "yokai", - "rune", - "western", - "vampires", - "flying", - "cute", - "Enemy", - "boon", - "Tables", - "ShadowFight", - "meta: Theme", - "SCS", - "vanthampur villa", - "CoA", - "shop", - "destiny", - "magical weapon", - "Arcane Arcade", - "XP to Level 3", - "Dice Average RPG", - "Pip-Boy", - "Dragon Heist", - "session notes", - "tattoo", - "flick", - "P6:66", - "Comic Character", - "experiment", - "Minerva", - "type:Spellbook", - "Realmfall", - "Wand", - "halfling", - "sw5e", - "implementar AP", - "Mask", - "Gazook89", - "Weltenrauch-Chroniken", - "MiA", - "Made in Abyss", - "français", - "fae", - "Lemuria", - "Mork Borg", - "guerrier", - "prunus", - "condition", - "pf2", - "tr", - "costrutto", - "German", - "project moon", - "5r", - "galles", - "Project moon", - "Yisang", - "Spicebush", - "player-accessible", - "Especie", - "Westmarch", - "a", - "Cart", - "Magus", - "group:Mchael Galvis", - "tip", - "werewolf", - "mundane", - "garrett", - "unarmed", - "Arcane Odyssey", - "Tomb of Divinity", - "pets", - "Video Game", - "4 part", - "pbta", - "Druids", - "multiclass", - "manuale", - "mimic", - "plane shift", - "Dotes", - "Hechizos", - "Infernal", - "Enhanced", - "done", - "Mission report", - "Blanks", - "Masks", - "Ultimate Ability", - "shadow-slave", - "Advertising", - "transform", - "Fullmetal Alchemist", - "Fullmetal Alchemist Brotherhood", - "tag:TAoF&F", - "Dwarves", - "Humans", - "Nine Hells", - "Devils", - "Archons", - "CR 15", - "Troglodytes", - "Goliath", - "retired", - "boots", - "ranged", - "shields", - "Zhentarim", - "World of Warcraft", - "Frontline", - "Guildmaster's Guide to Ravnica", - "Dungeons & Dragons 5e", - "beholder", - "NEEDS FIXING", - "mechanic", - "Loot", - "champion", - "Runes", - "Shield", - "Punch", - "Sniper", - "Magical Girl", - "NotDND", - "story", - "Sleep", - "Bard College", - "Illusion", - "Thunder", - "Defender", - "Genasi", - "troll", - "Gehenna", - "Yugoloth", - "social", - "Player Class", - "homebrew class", - "CR 16", - "Ghost", - "Kobolds", - "Trolls", - "Yuan-Ti", - "Elder Scrolls Offline", - "armure", - "Mage", - "CR 18", - "Technology", - "Mystery", - "darkness", - "Airship", - "New Campaign", - "Warframe", - "Wizard Subclass", - "Gold", - "Candor", - "Overhaul", - "Dragon Knight", - "Enoreth", - "Artifacts", - "New", - "AMMO", - "Campagne", - "Valbise", - "Subclasseptember", - "Mecha", - "Yu-Gi-Oh", - "Goblinoid", - "underwater", - "SW5E", - "bardo" -] \ No newline at end of file + 'meta:Theme', + '5e', + 'Subclass', + 'meta:theme', + 'subclass', + 'Class', + 'Homebrew', + 'Race', + 'Dungeons and Dragons', + 'theme', + 'Daggerheart', + '2024', + 'One Piece', + 'One Piece DND', + 'Luffy', + 'Dungeons and Devil Fruits', + 'Strawhats', + 'Template', + 'Campaign Frame', + 'class', + 'Players Handbook', + 'dnd', + 'osr', + 'Dungeon Masters Guide', + 'shadowdark', + 'dragonbane', + 'PHB', + 'example', + 'Devil Fruits', + 'system:pf2e', + 'DnD', + 'DMG', + 'system:dnd5.5', + 'Monster', + 'homebrew', + 'race', + 'template', + 'Warlock', + 'monster', + 'Fighter', + 'warlock', + 'druid', + 'sorcerer', + 'D&D', + 'Magic Item', + 'Barbarian', + 'Artificer', + '2014', + 'system:descent into avernus', + 'Sorcerer', + 'Adventure', + 'Paladin', + 'Ranger', + 'user help', + 'fighter', + '5th Edition', + 'Spells', + 'Monk', + 'Spell', + 'NPC', + 'Cleric', + 'spell', + 'Rogue', + 'css', + 'Item', + 'artificer', + 'magic item', + 'Rules', + 'barbarian', + 'wizard', + 'russian', + 'DnD5e', + 'Wizard', + 'paladin', + 'bastionland', + 'spells', + 'Devil Fruit', + 'Bard', + '5.5e', + 'rogue', + 'Tabletop System', + 'Haki', + 'Druid', + 'mystic bastionland', + 'item', + 'Lore', + 'bard', + 'monk', + 'system:dnd5e', + 'world', + 'ranger', + 'WIP', + 'cleric', + 'Dragon', + 'Naruto', + 'Creature', + 'snippet', + 'npc', + 'DeS', + 'Magic', + 'guide', + 'v3', + 'Beast', + 'Classe', + 'onering', + 'Monsters', + 'Races', + 'Weapon', + 'adventure', + 'Subclasses', + 'stat block', + 'weapon', + 'Species', + 'DONE', + 'archetype', + 'RPG', + 'Hollow Knight', + '5e\'24', + 'Martial', + 'DND', + 'Classe Nova', + 'Curse of Strahd', + 'Boss', + 'Hollowed Kingdoms', + 'baldurs mouth', + '5.24', + 'Homewbrew', + 'Encyclopedia', + 'Revised', + 'OneWorldHD', + 'knight', + 'DPS', + 'srd', + 'Undead', + 'items', + 'DnD 5e', + 'Guide', + 'Compendium', + 'Feat', + 'newspaper', + 'magic', + 'TTRPG', + 'descent into avernus', + 'reference', + 'system:D&D 5e24', + 'feat', + 'Magic Items', + 'Campaign', + 'resource', + 'Feats', + 'Anime', + 'dd5', + 'races', + 'Monstrosity', + 'DM Screen', + '2024 Rules', + 'Rework', + 'Character Build', + 'Done', + '5e 2024', + 'Construct', + 'myth', + 'magic items', + 'creature', + 'Legendary', + 'Strahd', + 'background', + 'Player', + 'style', + 'Legacy', + 'Player Handbook', + 'martial', + 'Character', + 'Dungeons & Dragons', + 'Table', + 'reddit', + 'monsters', + 'OneDND', + 'dragon', + 'Suporte', + 'Soulbound', + 'Expanded Handbook', + 'bestiary', + 'Humanoid', + 'system:dnd', + 'Oredell', + 'system:class', + 'V3', + 'system:5e', + '5e\'14', + 'Age of Sigmar', + 'Items', + 'Eberron', + 'horror', + 'dnd5e', + 'star wars', + 'Background', + 'compendium', + 'revision', + 'Elemental', + 'character', + 'Marcial', + 'SW5e', + 'notes', + 'DM', + 'Fey', + 'one-shot', + 'resources', + 'NEW', + 'campaign', + 'wondrous item', + 'Setting', + 'Archive', + 'NIMRE', + 'Tanque', + 'Jogavel', + 'Dnd 5e', + 'LotM', + 'setting', + 'revised', + 'Warhammer', + 'rework', + 'Conjurador', + 'GMBinder', + 'ItemSet', + 'NPCs', + 'classes', + 'CR 2', + 'AetherSail', + 'undead', + 'Artifact', + 'cards', + 'Example', + 'Guides', + 'Theme', + 'Swamp', + 'CR 1', + 'patron', + 'Extra', + 'concept', + 'final fantasy', + 'Aberration', + 'Elder Scrolls', + 'rules', + 'meta:gratis', + 'Dice Pool', + 'Cue', + 'dark', + 'type:Style', + 'rpg', + 'meta:free', + 'summoner', + 'OC', + 'rare', + 'Fleshing Out', + 'francais', + 'Dnd', + 'Creatures', + 'Sims 4', + 'sous-classe', + 'ffxiv', + 'Underdark', + 'add-on', + 'weapons', + 'Subrace', + 'dungeons and dragons', + 'Naruto 5e', + 'Lafari', + 'Very Rare', + 'd6', + 'red', + 'Equipment', + 'CR 3', + 'Patron', + '5E', + 'objet magique', + 'spellcaster', + 'feats', + '5.24e', + 'system:d&d5e', + 'Rare', + 'Thudnfer', + 'daggerheart', + 'CR 1/2', + 'Archetype', + 'dnd 5e', + 'Pathfinder', + 'lineage', + 'Celestial', + 'Support', + 'species', + 'ARCHIVED', + 'Horror', + 'humanoid', + 'Subclase', + 'book:PHB E&E', + 'final fantasy xiv', + 'Jungle', + 'Feywild', + 'Fiend', + 'subclasses', + 'HB', + 'Legacy Challenge', + 'Project Horizon', + 'vampire', + 'WoW', + 'DND 5e', + 'Weapons', + 'system:book clone', + 'CoS', + 'N5e', + 'Summon', + 'Spellcaster', + 'Koretra', + 'Voidborn', + 'one shot', + 'Templates', + 'tables', + 'Iphexar', + 'Shattered Obelisk', + 'Sword Coast', + 'elemental', + 'lore', + 'character sheet', + 'discord', + 'BetterMonsters', + 'players', + 'group:simple skans', + 'Coastal', + 'Forest', + 'Unearthed Arcana', + 'Old', + 'Collection', + 'Ancestry', + 'Caevash', + 'Strixhaven', + 'Limbus Company', + 'Daydreams & Deviants', + 'Statblocks', + 'Urban', + 'Classes', + 'familiar', + 'Blood', + 'oneshot', + 'n5e', + 'wip', + 'masks', + 'Planeshifted', + 'Carrioss', + 'avernus', + 'object', + '1', + 'Ruins', + 'Anime Character', + 'wild shape', + 'mask', + 'dj9 game', + 'Handbook', + 'Curse', + 'oath', + 'Midralis', + 'Appendix', + '5.5', + 'tales of the valiant', + 'player classes', + 'Caster', + 'Large', + 'Fateforge', + 'Cursed', + 'beast', + 'Pathfinder 2e', + 'Design', + 'Uncommon', + 'Endeur', + 'Elemental Water', + 'SCC', + 'sci-fi', + 'Dragons', + 'human', + 'Gods', + 'Medium', + 'System', + 'Help', + 'Handout', + 'Skyrim', + 'BnB', + 'draft', + 'PC', + 'Variant', + 'syntax', + 'OneShot', + 'Clase', + 'UESTRPG', + 'Savannah', + 'Reef', + 'CR 5', + 'Forgotten Realms', + 'collection', + 'Combat', + 'Historia', + 'artifact', + 'Expansion', + 'Attunement', + 'Variant Rules', + 'd&d', + 'Party Build', + 'Evocation', + 'homerule', + 'Forbidden West', + 'how-to', + 'tov', + 'Mystical Item', + 'CR 4', + 'Necromancer', + 'General Rules', + 'Needs Update', + 'stat blocks', + 'DC Comics', + 'Sword', + 'Armor', + 'blood hunter', + 'Blood Hunter', + 'Meio Conjurador', + 'Mydia', + '3rd Party', + 'N5E', + 'Delvebound', + 'Ocean', + 'Subterranean', + 'half-caster', + 'Demon', + 'Fire', + 'meta:Template', + 'Character Sheet', + 'PF2e', + 'png', + 'fantasy', + 'Setting Guide', + 'Style', + 'Cor', + 'legacy', + 'Minion', + 'meta:khaoz age', + 'Book', + 'magical item', + 'immersion', + 'reminder cards', + 'Regras', + 'Durnovar', + '2025', + 'Camp1', + 'Abyss', + 'armor', + 'construct', + 'firearms', + 'Backgrounds', + 'Companion', + 'Melee', + 'fey', + 'Incomplete', + 'Vampire', + 'Bestiary', + 'Worldbuilding', + 'History', + 'Conjuration', + 'necromancy', + 'dragons', + 'ancestry', + 'Mech', + 'PbtA', + 'COS', + 'One Shot', + 'Factions', + 'Transmutation', + 'Spellcasting', + 'card', + 'Ardh', + 'redveil', + 'conditions', + 'elturel', + 'rhye', + 'group:James Haeck', + 'CaelYuu', + 'system:5.24e', + 'system:GM Binder', + 'Grassland', + 'melee', + 'Potions', + 'anime', + 'D&D 5e', + 'Healer', + 'Gambling', + 'Lightning', + 'fighting style', + 'support', + 'Style Template', + 'mtg', + 'NSFW', + 'aventura', + 'Manuals', + 'plant', + 'Incarnate', + 'Crafting', + 'DnDBeyond', + 'Monster Girl', + 'Monster Girl Encyclopedia', + 'pet', + 'npcs', + 'Stat Block', + 'Styleguide', + 'mitologia', + 'Objeto maravilloso', + 'vaesen rpg', + 'dnd-2024', + 'Class Handbook', + 'Space', + 'Taiga', + 'CR 6', + 'Pact Boon', + 'race/ancestry', + 'Necromancy', + 'cantrip', + 'LoL', + 'Raza', + 'handout', + 'Mechanic', + 'Conversion', + 'Wondrous Item', + 'Familiar', + 'necromancer', + 'uncommon', + 'curse', + 'Campaign Setting', + 'Tetra', + '1e', + 'module', + 'Evolving', + 'boiling sea', + 'deck', + 'OSR', + 'RU', + 'VL', + 'Underdeep', + 'Deep Ocean', + 'Giant', + 'statblock', + 'combat', + 'Time', + '5E24', + 'session zero', + 'elf', + 'tank', + 'sorcerous origin', + 'Drakkenheim', + 'Tavern', + 'Domain', + 'healer', + 'Valenor', + 'blood', + 'Oath', + 'spooky', + 'fire', + 'meta:5e24 Style', + 'Notes', + 'City', + 'Conjurador Completo', + 'prop', + 'Dotherys', + 'Rietuma 3.0', + '5e24', + 'Library of Ruina', + 'español', + 'Project Echo', + 'battle of Japan', + 'Plant', + 'Badlands', + 'Neverwinter', + 'Fantasy', + 'Beastfolk', + 'Unarmed', + 'Cold', + 'Damage', + 'attunement', + 'Hurthud', + '3rd Level', + 'spelljammer', + 'mostro', + 'Custom', + 'PT-BR', + 'Alternative Realms', + 'The Foot', + 'boss', + 'demo', + 'Supplement', + 'FitD', + 'classe', + '5.14', + 'Copy', + 'DnD5e24', + 'X-Men', + 'TNA', + 'CR 8', + 'Desert', + 'CR 7', + 'arme', + 'random', + 'spellcasting', + 'Deprecated', + 'Cards', + 'finished', + 'Ben 10', + 'equipment', + 'Geography', + 'Games', + 'For Players', + 'Faerun', + 'scroll', + 'Faction', + 'Alchemist', + 'drow', + 'Lineage', + 'mix-blend-mode', + 'columns', + 'User Help', + 'Reami Dimenticati', + 'Класс', + 'D100', + 'nsfw', + 'hucaen', + 'v1.0', + 'Cortex', + 'Fallout', + 'ww5e', + 'MAGIC', + 'DnD2024', + 'ToV', + 'D&D2024', + 'The Backrooms', + 'Freshwater', + 'D20', + 'Dragonborn', + 'custom', + 'sword', + 'Dungeons and Dragons 5e', + 'Water', + 'legendary', + 'Dungeon', + 'Ravenloft', + 'aberration', + 'Longsword', + 'transmutation', + 'Fairy Tail', + 'Character background', + 'Exandria', + 'Updated', + 'pitch', + 'Half-Caster', + 'Complete', + 'Money', + 'player', + 'forgotten-realms', + 'Festival', + 'Casino', + 'SCAG', + 'Currency', + 'North', + 'Toril', + 'Scourged Land of Valenor', + 'Oota', + 'parchment', + 'Literature', + 'Serrith', + 'PNJ', + 'Divinity Original Sin 2', + 'Wild', + 'videogame', + 'magic the gathering', + 'sweetblossom', + 'GMscreen', + 'MandM', + 'D&D 5.24', + 'Camp2', + 'Remaster', + 'riassunti', + 'type:Resource', + 'system:D&D', + 'tag:Class', + 'Excelsior', + 'Stat Blocks', + 'Sci-Fi', + 'Ooze', + 'CR 1/8', + 'sublclass', + 'chart', + 'Mountains', + 'guns', + 'Nature', + 'Orc', + 'Poison', + 'Devil', + 'fiend', + 'DC', + 'pt-br', + 'ABnB', + 'One-Shot', + 'strahd', + 'Ring', + 'Theme song', + 'orc', + 'summon', + 'Psion', + 'Psionics', + 'Dungeon Master', + 'vehicle', + 'DM only', + 'Demigod', + 'Antica Energia', + 'Pirates', + 'Sourcebook', + 'devil', + 'Cantrip', + 'mystery', + 'MtG', + 'conversion', + 'Festivals', + 'Casinos', + 'Taverns', + 'Betting', + 'Drinking', + 'phandelver', + 'Warhammer 40k', + 'mutant', + 'styling', + 'FATE', + 'Lone Wolf', + 'icon', + 'New Dawn', + 'Magic Set', + 'Paladin Subclass', + 'Alter Class', + 'difficulty classses', + 'combat tables', + 'phb', + 'Project Moon', + 'Undertomes', + 'EGO', + 'Campagne 1', + 'Constelação', + 'Arvore I', + 'Fim da Jornada', + 'greek god', + 'dwarf', + 'Firearms', + '3.5e', + 'generator', + 'Elf', + 'meta: Scenario', + 'enchantment', + 'buff', + 'ITW', + 'Tank', + 'Archived', + 'Martial Archetype', + 'caster', + 'BR', + 'Knight', + 'Utility', + 'SWADE', + 'Star Wars', + 'pc', + 'Mystic', + 'Useful', + 'Netherdeep', + 'crafting', + 'Sapient Undead', + 'Maverick', + 'Revision', + 'Resource', + 'Humblewood', + 'one piece', + 'Bag of Holding', + 'medium', + 'lightning', + 'backgrounds', + '4th Level', + 'path', + 'BREAK-RPG', + 'dark fantasy', + 'Players', + 'poison', + 'psionic', + 'gazook89', + 'homebrew subclass', + 'wild-wasteland', + 'CWD', + 'Paid', + 'Tales of the Valiant', + 'Dreadhold', + 'arma', + 'system:Mutants and Masterminds', + '#Tiefschlaf', + 'Brew', + 'Myra', + 'Swashbuckler', + 'dead by daylight', + 'Exceptional', + 'COD Zombies', + 'Hills', + 'Tundra', + 'type:Campaign', + 'wild magic', + 'Food', + 'Death', + 'homebrew rules', + 'Remake', + 'Witcher', + 'water', + 'Pet', + 'book', + 'AAH', + 'pact', + 'Ice', + 'Character Creation', + 'animal', + 'Pokemon', + 'clase', + '5e14', + 'DBZ', + 'CLONE', + 'Evil', + 'Tarsere', + 'Mythology', + 'pf2e', + 'Magical', + 'type:race', + 'Sorcerous Origin', + 'Information', + 'styles', + 'Module', + 'gish', + 'frames', + 'DeltaGreen', + 'Magic item', + 'food', + 'chef', + 'basics', + 'giant', + 'Brew Creation', + 'One-shot', + 'ttrpg', + 'Path', + 'Don\'t Starve', + 'MGE', + 'firearm', + 'DnDBehindTheScreen', + 'store', + 'The Artisan', + 'timeline', + 'college', + 'dev', + 'dungeon of the dead three', + 'Cradle', + 'Dnd5e', + 'dungeon', + 'Amaranthine', + 'Regno di Oltremare', + 'bestia', + 'rewrite', + 'WiP', + 'Subclasse', + 'mutants and masterminds', + 'The Embrace', + 'meta:documentation', + 'Mutants And Masterminds', + 'khedoria', + 'Encounter Pack', + 'giorni', + 'Statblock', + 'Enemies', + 'Goblinoids', + 'Heavens', + 'system:2e', + 'Vaalbara', + 'Dwarf', + 'airos', + 'table', + 'Artificer Specialization', + 'Buff', + 'Book 1', + 'Ranged', + 'cypher', + 'utilities', + '40k', + 'Psychic', + 'Fear', + 'steampunk', + 'shadow', + 'subclase', + 'Barbarian Subclass', + 'Elements', + 'pact boon', + 'Clan', + 'Fly', + 'solo', + 'sourcebook', + 'Marvel Comics', + 'compilation', + 'Firearm', + 'sidekick', + 'infusions', + 'Mechanics', + 'Summoner', + 'Aasimar', + 'Human', + 'Vehicle', + 'Shadow', + 'Clone', + 'custom css', + 'ocean', + 'sotdl', + 'bandit', + 'Wind', + 'Printer Friendly', + 'Obsolete', + 'mechanics', + 'illusion', + '5th edition', + 'League of Legends', + 'Vestige', + 'dungeons', + 'Dungeons', + 'and', + 'Elden Ring', + 'L5R', + 'd20', + 'Poisons', + 'd15', + 'Dungeons And Dragons', + 'MTG', + 'divine', + 'characters', + 'witch', + 'Anime Homebrew', + 'Zombie', + 'thunder', + 'Jujutsu Kaisen', + 'campagne', + 'Deadlands', + 'spell list', + '1 Person', + 'Ritual', + 'screen', + 'nature', + 'Divination', + 'Compattare', + 'dtrpg', + 'quick ref', + 'Mago', + 'Illivia', + 'Shonen', + 'Core Deities', + 'green ronin', + 'Bless', + 'D&D5e', + 'version:0.1.0', + 'curato', + 'system:Ord', + 'Images', + 'Sealed Artifact', + 'Giants', + 'CR 9', + 'CR 11', + 'CR 0', + 'CR 14', + 'Shadowfell', + 'Tier 1', + 'd100', + 'Elemental Air', + 'artificiel', + 'Cultist', + 'Cyberpunk', + 'Huge', + 'Warrior', + 'Gun', + 'quest', + 'LYRA', + 'Music', + 'Tiefling', + 'Master', + 'Witch', + 'Linnorm', + '1st-level', + 'Mount', + 'Animal', + 'Comics', + 'Superhero', + 'creatures', + 'Hunter', + 'Control', + 'Dragon Ball', + 'Dragon Ball Z', + 'Dagger', + 'questingforamonster', + 'ICRPG', + 'Booklet', + 'f and t', + 'common', + 'Chaos', + 'spellblade', + 'Constitution', + 'artisan', + 'arcane', + 'Released', + 'ring', + 'runes', + 'gun', + 'Supportive Material', + 'The Witcher', + 'Desarmado', + 'Monster Monday', + 'Bleach', + 'Demon Slayer', + 'mice', + 'worldbuilding', + 'Necrotic', + 'ability score', + 'demon', + 'Armybook Shivatiano', + 'warrior', + 'Fighter Subclass', + 'system', + 'whisperveil', + 'psychic', + 'warhammer', + 'Aventura', + 'Culture', + 'Material', + 'meta:npc', + 'shops', + 'magic weapon', + 'nhera', + 'Dark Fantasy', + 'Regles', + 'Wonderous Item', + 'Features', + 'pokemon', + 'Ghosts of Saltmarsh', + 'monstrosity', + 'DL TWW', + 'companion', + 'alternate layout', + 'Tutorials', + 'Kitsune', + 'don', + 'heroique', + 'mini campaign', + 'drago', + 'Aquatic', + 'tool', + 'handmade', + 'released', + 'Spellblade', + 'pregen', + 'level 2', + 'Baldurs gate 3', + 'My Hero', + 'Technically a subclass', + '5.24e Remastered Subclasses', + 'dinosaurs', + '5E.2024', + 'Razas', + 'Horizon', + 'Clothing', + '+2', + 'castellano', + 'pentacle prophecy', + 'tag:Spells', + 'gruppo A', + 'Rpg', + 'razze', + 'type:Adventure', + 'unfinished', + '3.5', + 'gunslinger', + 'BBEG', + 'Arcane', + 'component', + 'Bow', + 'backstory', + 'phandalin', + 'Skills', + 'Pact', + 'Elemental Earth', + 'Joke', + 'invocation', + 'martial class', + 'Super Villain', + 'Eldritch', + 'Elemental Fire', + 'Homebrew Class', + 'eldritch', + 'cyberpunk', + 'Player Race', + 'Class Mod', + 'Heatcoast', + 'meta:Guide', + 'Yemao', + 'evil', + 'Named NPC', + 'CLASS', + 'Angel', + 'vecna', + 'PT', + 'PTBR', + 'Ancient', + 'Small', + 'WotC Style', + '5e Homebrew', + '1st Level', + 'dagger', + 'Brancalonia', + 'encounter', + 'cat', + 'primal path', + 'Ambientazione', + 'Magie', + 'candlekeep', + 'Ongoing', + 'Oneshot', + 'Wondrous', + 'Janbrewery', + 'Tattoos', + '5e (2014)', + 'concentration', + 'very rare', + 'Set', + 'Kobold', + 'martial archetype', + 'God', + 'blog', + 'New Gate', + 'Healing', + 'OneDnD', + 'Incantesimi', + 'Player Options', + 'contest', + 'pirate', + 'Manuel', + 'Alchimie', + 'Herboristerie', + 'Ingredients', + 'starlost', + 'Campaign 1', + 'Abandoned', + 'Previous Editions', + 'Enchantment', + 'Tools', + 'Oblivion', + 'domain', + '5th Level', + 'DnD Beyond', + 'Reference', + 'Sorcerer Subclass', + 'Dragon Magazine', + 'feature', + 'german', + 'conjuration', + 'strixhaven', + 'Sentient', + 'JJK', + '10 Generations', + 'character creation', + 'LevelUp', + 'pallid grove', + 'primer', + 'Requires Attunement', + 'College', + 'Aesthetic', + 'critter', + 'home game', + 'spanish', + 'stats', + 'Lairon', + 'Hunters Guild', + 'original setting', + 'Bosses', + 'Radiant Citadel', + 'actions', + 'Reworked', + 'Elystera', + 'Wyvern', + 'vikings', + 'thief', + 'enemies', + 'Obsession', + 'Yi Sang', + 'aberrazione', + 'Limbus', + 'animals', + 'minecraft', + 'mice of legend', + 'osric', + '20 Minutes Till Dawn', + 'campaign frame', + 'latigo', + 'DH', + 'Eldritch Invocation', + 'system:daggerheart', + '100ni', + 'meta:Sheet', + 'fa-solid fa-sheet-plastic:Ficha', + 'tag:Berean', + 'AD&D', + 'B/X', + 'The Codex Of Anomalous Entities', + 'monster manual', + 'Polar Waters', + 'CR 12', + 'CR 10', + 'blood magic', + 'Gunslinger', + 'grimoire', + 'Drakes', + 'Japanese', + 'subrace', + 'ooze', + 'Stats', + 'Half Caster', + 'Sea', + 'time', + 'Brawler', + 'Session 0', + 'Halloween', + 'Runeterra', + 'Divine', + 'Random', + 'Lifestar', + 'arcane trickster', + 'Paddy4530', + 'evocation', + 'light', + 'Steampunk', + 'shaman', + 'Primal Path', + 'monk subclass', + 'Full Caster', + 'World', + 'Planning', + 'spirit', + 'Nova Era', + 'abjuration', + 'Christmas', + 'Critical Role', + 'Gish', + 'Bandit', + 'Monster Manual', + 'party member', + 'mgazt', + 'Playable Race', + 'Donjon.bin.sh', + 'Final Fantasy', + 'Roleplay', + 'monstre', + 'fairy', + 'frame', + 'Minecraft', + 'Stealth', + 'Manual', + 'half caster', + 'Storm', + 'Sorcery', + 'format work', + 'Kingdom Hearts', + 'hexblade', + 'block', + 'page layouts', + 'Monk Subclass', + 'FinyaFluKaiKolja', + 'Radiant', + 'group:playtest', + 'Korrahir', + 'noble', + 'exorcist', + 'xapien', + 'Raven Queen', + 'markdown', + 'damage', + 'Alchemy', + 'morrigan', + 'genasi', + 'ZNH', + 'folklore', + 'Fate', + 'hechicero', + 'Air', + 'Magic Weapon', + 'Anime DND 5e', + 'Dragon Ball Z TTRPG', + 'Dragon Ball Z RPG', + 'Dragon Ball Z DND', + 'Dragon Ball Z 5e', + 'samurai', + 'Goblin', + 'Base Sheet', + 'Shackled City Adventure Path', + 'Natureza', + 'control', + 'Normarch', + 'Reddit', + 'Genshin Impact', + 'Abjuration', + 'Myr', + 'Flight', + 'Vampyre', + 'nightmare', + 'Lycan', + 'Occult', + 'circle', + 'Christmas Special', + 'DoDD', + 'Character Options', + 'traduction', + 'Characters', + 'Gear', + 'system:sf2e', + 'drakkenheim', + 'downtime', + 'amulet', + 'Feiticeiros e Maldicoes', + 'Tecnica amaldicoada', + 'prorpg', + 'enemy', + 'No Mercy', + 'rain world', + 'slugcat', + 'fly', + 'meta:User Guide', + 'Fallout TTRPG', + 'regles', + 'Ill Tides', + 'Light-hearted', + 'Vastria', + 'school', + 'Fillible Online', + 'Mezgarr', + 'Berserk', + 'invocations', + 'Classe Refeita', + 'Auroboros', + 'bosses', + 'fabula ultima', + 'Shagya', + 'wild', + 'DnD 2024', + 'KaiburrKathHound', + 'Barbarian Path', + 'fauna', + '5E.2014', + 'system:curse of strahd', + 'Unofficial', + 'how to', + 'Glaive', + 'A5E', + 'pt', + 'Consumible', + 'Realmers\'', + 'Versatile Lineage', + 'Shichibukai', + '2024e', + 'Rencontre', + 'tag:Spell List', + 'elementalist', + 'noncaster', + 'blasphemous', + 'Mordhiem', + 'Wildfrost', + '#Regelwerk', + 'Rewrite', + 'Maldición de Strahd', + 'Scion', + 'Entities', + 'Hoarwyrm', + 'Player utility', + 'CR 1/4', + 'Temperate Forest', + 'Demons', + 'Drow', + 'type:rules', + 'fay', + '2e', + 'familier', + 'supplement', + 'Amberwar', + 'slime', + 'Lycanthropy', + 'meta: Terres de Leyt', + 'Strong', + 'AAH Vol. 1', + 'Force', + 'Jump', + 'Aboleths', + 'lol', + 'location', + 'small', + 'customizable', + 'Modern', + 'Sky', + 'portugues', + 'Hero', + 'Villain', + 'element', + 'Tyranny of Dragons', + 'Adventure Guide', + 'New Class', + 'Witchlight', + 'Shardblade', + 'Plateaux', + 'WOTC', + 'Snippet', + 'Terra', + 'Otherworldly Patron', + 'ritual', + 'hag', + 'Cyberpunk 2077', + 'tavern', + 'Artificer Specialist', + 'Werewolf', + 'Boesia', + 'vampiric', + 'monastic tradition', + 'Gothic', + 'celestial', + 'Unfinished', + 'Core', + 'Arcane Tradition', + 'Troll', + 'Origin', + 'Draconic', + 'dj9 member', + 'test', + 'Hag', + 'gem', + 'Invocations', + 'Dark Sun', + 'aarkhen', + 'How to', + 'ravenloft', + 'faerie', + 'Playtest', + 'Shaman', + 'dead', + 'Tomba Aniquilacio', + 'Pacto', + 'fullcaster', + 'Electric', + 'Ability Score', + '4D', + 'pathfinder', + 'insect', + 'hook', + 'page layout', + 'healing', + 'Lineages', + 'Flying', + 'Martial Arts', + 'journal', + 'Aide de jeu', + 'hunter', + 'headers', + 'Dark Souls', + 'courtyard', + 'crossroads', + 'Quest', + 'CotF', + 'defense', + 'Semryss', + 'invoked class', + 'Session Notes', + 'goblin', + 'infernal', + 'fate', + 'oni', + 'spellbook', + 'Summoning', + 'slut', + 'whore', + 'Greyhawk', + 'Mobility', + 'Reddit Remake', + 'Guild', + 'Cosmic Mart', + '7th Level', + 'dragonborn', + 'curse of strahd', + 'Ranger Subclass', + 'dossier', + 'dossie', + 'de', + 'pnpde', + 'Plane Shift', + 'halloween', + 'group:aventura', + '9th Level', + 'tome', + 'cold', + 'acid', + 'deprecated', + 'mind flayer', + 'MECHA', + 'EssentialsKit', + '2d6', + 'ToD', + 'Work In Progress', + 'Bond', + 'Versatile', + 'Dead', + 'SYWTBAGM', + 'summoning', + 'english', + 'Eilistraee', + 'Draft', + 'DoD', + 'map', + 'Frightened', + 'Psychic Damage', + 'eberron', + 'recompensa', + 'wizard subclass', + 'teiran', + 'Saltmarsh', + 'jp setting', + 'Illithid', + 'Longbow', + 'hell', + 'Monarch', + 'type:feat', + 'reglas', + 'cooking', + 'Abenteuer', + 'reloaded', + 'incompleto', + 'mecanica', + 'Location', + 'Grimlores Grimoire', + '2024 Subclass', + 'Chiesa di Toleno', + 'finalfantasy', + 'The Undertomes', + 'Lobotomy Corporation', + 'SDHTA', + 'D&D 2024', + 'other', + 'ally', + 'images', + 'Player\'s Guide', + 'Avalon Sword', + 'Cael\'Yuu', + 'dnd-2014', + 'Regelwerk', + 'Español', + 'br', + 'dnd 5.0', + 'monstro', + 'grand cemetery', + 'Phoenix', + 'dnd 2024', + 'Bloodhunter', + 'Sintonizacion', + 'dungeons & dragons', + 'Fix', + 'Rulebook', + 'Shadowdark', + 'heroic', + 'HFW', + 'Earthdawn', + '24e', + 'cormyr', + 'suzail', + 'dc20', + 'tag:Rules', + 'The Griffon\'s Saddlebag', + 'LOTM', + 'tag:Adventure', + 'drunken master', + 'eldritch invocation', + 'Персонаж', + 'Orcs', + 'Lizardfolk', + 'Frostfell', + 'CR 17', + 'Shapechanger', + 'Farmland', + 'Mages', + 'Any', + 'CR 13', + 'Earth', + 'Mountain', + 'Drake', + 'transformation', + 'GM', + 'Lich', + 'lovecraft', + 'unique', + 'Optional Rules', + 'int', + 'creator', + 'Primal', + 'simple', + 'golem', + 'Void', + 'Armour', + 'spellsword', + 'General', + 'Asian', + 'Bringers of chaos', + 'Optional Feature', + 'subraces', + 'Galanoth', + 'barbarian subclass', + 'felhearth', + 'modular', + 'Vampires', + 'wysteria', + 'adaptation', + 'beasts', + 'naruto', + 'ninja', + 'Psionic', + 'Guns', + 'Crystal', + 'Guardian', + 'NonProfit', + 'Mimic', + 'languages', + 'Epic Boons', + 'Primer', + 'Icewind Dale', + 'joke', + 'lycan', + 'CR3', + 'Armors', + 'ff7', + 'materia', + 'final fantasy 7 remake', + 'esper', + 'ff7 remake', + 'gargantuan', + 'Frog', + 'CR5', + 'blank', + 'monster hunter', + 'league of legends', + 'french', + 'Pokémon', + 'kobold', + 'soul', + 'ffxi', + 'd10', + 'Roman', + 'Cute', + 'DD5', + 'variant', + 'tree', + 'fr', + 'Scenario', + 'lycanthrope', + 'druide', + 'staff', + 'eios', + 'arkheneios', + 'Runic', + 'Work', + 'Ukrainian', + 'cover-page', + 'mage', + 'deities', + 'gods', + 'Boss Fight', + 'Lair', + 'WBTW', + 'roguish archetype', + 'Character Option', + 'Shortsword', + 'Illrigger', + 'Bloodborne', + 'cr6', + 'Priest', + 'Hamon', + 'Toonkind', + 'rol', + 'Strength', + 'forgotten realms', + 'Spanish', + 'Conclave', + 'Electro', + 'Magical Tattoos', + 'Matt Mercer', + 'Wildemount', + 'Mighty Nien', + 'Campaign 2', + 'Resistances', + 'Bug', + 'impression', + 'PF', + 'Magnus Archives', + 'ice', + 'speed', + 'Generic NPC', + 'Titanic', + 'Ink Friendly', + 'bleed', + 'elder scrolls', + 'Immortal', + 'LMOP', + 'Travel', + 'Olphus', + '3d6', + 'heist', + 'World History', + 'ghost', + 'genie', + 'kids on bikes', + 'Russia', + 'conclave', + 'overhaul', + 'manual', + 'Adventures In Eden', + 'Downtime', + 'hamon', + 'cloak', + 'shadowfell', + 'Hellfire', + 'Paladin Oath', + 'Genshin', + 'Nation', + 'air', + 'Magical Item', + 'War', + 'Original', + 'Monstrous Compendium', + 'Calamity', + 'Warden', + 'Apocalypse', + 'shield', + 'AC', + 'expansion', + 'Concentration', + 'charm', + 'Weave', + 'lycanthropy', + 'raza', + 'far realm', + 'fighter subclass', + 'ita', + 'Pirate', + 'Laranja', + 'Grapple', + 'EastByForce', + 'hobgoblin', + 'oneshot-notes', + 'Holy', + 'optional', + 'type:cenario', + 'group:core', + 'The Brewery', + 'Alcance', + 'Morrowind', + 'Indigo', + 'Divino', + '2nd Level', + 'Sub-Class', + 'cantrips', + 'Cloak', + 'battle master', + 'Dark', + 'Puzzle', + 'Lucky', + 'consumable', + 'rebalance', + 'Shove', + 'Area Control', + 'Vanguard', + 'funny', + 'e5', + 'Dragonlance', + 'psion', + 'initiative', + 'Tactician', + 'Inspiration', + 'artificier', + 'way', + 'inspired', + 'historia', + 'Medusa', + '2 part', + 'holy', + 'gift', + 'Nimble', + 'mostri', + 'phoenix', + 'travel', + 'Class Template', + 'Intimidation', + 'constructs', + 'P666', + 'Formatting', + 'Divinity', + 'Rod', + 'Language', + 'yokai', + 'rune', + 'western', + 'vampires', + 'flying', + 'cute', + 'Enemy', + 'boon', + 'Tables', + 'ShadowFight', + 'meta: Theme', + 'SCS', + 'vanthampur villa', + 'CoA', + 'shop', + 'destiny', + 'magical weapon', + 'Arcane Arcade', + 'XP to Level 3', + 'Dice Average RPG', + 'Pip-Boy', + 'Dragon Heist', + 'session notes', + 'tattoo', + 'flick', + 'P6:66', + 'Comic Character', + 'experiment', + 'Minerva', + 'type:Spellbook', + 'Realmfall', + 'Wand', + 'halfling', + 'sw5e', + 'implementar AP', + 'Mask', + 'Gazook89', + 'Weltenrauch-Chroniken', + 'MiA', + 'Made in Abyss', + 'français', + 'fae', + 'Lemuria', + 'Mork Borg', + 'guerrier', + 'prunus', + 'condition', + 'pf2', + 'tr', + 'costrutto', + 'German', + 'project moon', + '5r', + 'galles', + 'Project moon', + 'Yisang', + 'Spicebush', + 'player-accessible', + 'Especie', + 'Westmarch', + 'a', + 'Cart', + 'Magus', + 'group:Mchael Galvis', + 'tip', + 'werewolf', + 'mundane', + 'garrett', + 'unarmed', + 'Arcane Odyssey', + 'Tomb of Divinity', + 'pets', + 'Video Game', + '4 part', + 'pbta', + 'Druids', + 'multiclass', + 'manuale', + 'mimic', + 'plane shift', + 'Dotes', + 'Hechizos', + 'Infernal', + 'Enhanced', + 'done', + 'Mission report', + 'Blanks', + 'Masks', + 'Ultimate Ability', + 'shadow-slave', + 'Advertising', + 'transform', + 'Fullmetal Alchemist', + 'Fullmetal Alchemist Brotherhood', + 'tag:TAoF&F', + 'Dwarves', + 'Humans', + 'Nine Hells', + 'Devils', + 'Archons', + 'CR 15', + 'Troglodytes', + 'Goliath', + 'retired', + 'boots', + 'ranged', + 'shields', + 'Zhentarim', + 'World of Warcraft', + 'Frontline', + 'Guildmaster\'s Guide to Ravnica', + 'Dungeons & Dragons 5e', + 'beholder', + 'NEEDS FIXING', + 'mechanic', + 'Loot', + 'champion', + 'Runes', + 'Shield', + 'Punch', + 'Sniper', + 'Magical Girl', + 'NotDND', + 'story', + 'Sleep', + 'Bard College', + 'Illusion', + 'Thunder', + 'Defender', + 'Genasi', + 'troll', + 'Gehenna', + 'Yugoloth', + 'social', + 'Player Class', + 'homebrew class', + 'CR 16', + 'Ghost', + 'Kobolds', + 'Trolls', + 'Yuan-Ti', + 'Elder Scrolls Offline', + 'armure', + 'Mage', + 'CR 18', + 'Technology', + 'Mystery', + 'darkness', + 'Airship', + 'New Campaign', + 'Warframe', + 'Wizard Subclass', + 'Gold', + 'Candor', + 'Overhaul', + 'Dragon Knight', + 'Enoreth', + 'Artifacts', + 'New', + 'AMMO', + 'Campagne', + 'Valbise', + 'Subclasseptember', + 'Mecha', + 'Yu-Gi-Oh', + 'Goblinoid', + 'underwater', + 'SW5E', + 'bardo' +]; \ No newline at end of file diff --git a/client/homebrew/main.jsx b/client/homebrew/main.jsx index 77a88d30f..635729d49 100644 --- a/client/homebrew/main.jsx +++ b/client/homebrew/main.jsx @@ -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(); +createRoot(document.getElementById('reactRoot')).render(); diff --git a/client/homebrew/navbar/navbar.jsx b/client/homebrew/navbar/navbar.jsx index aa77dd2a0..db9a836c9 100644 --- a/client/homebrew/navbar/navbar.jsx +++ b/client/homebrew/navbar/navbar.jsx @@ -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' }; }, diff --git a/client/homebrew/pages/editPage/editPage.jsx b/client/homebrew/pages/editPage/editPage.jsx index d40058557..176158e2c 100644 --- a/client/homebrew/pages/editPage/editPage.jsx +++ b/client/homebrew/pages/editPage/editPage.jsx @@ -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)); diff --git a/client/homebrew/pages/homePage/homePage.jsx b/client/homebrew/pages/homePage/homePage.jsx index 030e05a04..580d69e76 100644 --- a/client/homebrew/pages/homePage/homePage.jsx +++ b/client/homebrew/pages/homePage/homePage.jsx @@ -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)); diff --git a/client/homebrew/pages/newPage/newPage.jsx b/client/homebrew/pages/newPage/newPage.jsx index 7f3247d04..7ddb05c0b 100644 --- a/client/homebrew/pages/newPage/newPage.jsx +++ b/client/homebrew/pages/newPage/newPage.jsx @@ -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)); diff --git a/server.js b/server.js index 02aeee356..db97336e2 100644 --- a/server.js +++ b/server.js @@ -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}`); diff --git a/server/admin.api.js b/server/admin.api.js index 93e0036d1..f55eefdf2 100644 --- a/server/admin.api.js +++ b/server/admin.api.js @@ -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 `` 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 `` 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( - '', - `\n` - )); -})); + res.send(html.replace( + '', + `\n` + )); + })); return router; diff --git a/server/app.js b/server/app.js index 07fc3ba9c..18b1d68bc 100644 --- a/server/app.js +++ b/server/app.js @@ -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); } diff --git a/server/homebrew.api.js b/server/homebrew.api.js index 4d4f5a911..bb8d76ce5 100644 --- a/server/homebrew.api.js +++ b/server/homebrew.api.js @@ -481,7 +481,7 @@ const api = { await HomebrewModel.deleteOne({ editId: id }); return next(); } - throw(err); + throw (err); } let brew = req.brew; diff --git a/shared/markdown.js b/shared/markdown.js index adb058042..09a6b37e3 100644 --- a/shared/markdown.js +++ b/shared/markdown.js @@ -1,4 +1,4 @@ -/* eslint-disable max-depth */ + /* eslint-disable max-lines */ import _ from 'lodash'; import { marked as Marked } from 'marked'; diff --git a/themes/Legacy/5ePHB/snippets/coverpage.gen.js b/themes/Legacy/5ePHB/snippets/coverpage.gen.js index c134930cf..9c2de2943 100644 --- a/themes/Legacy/5ePHB/snippets/coverpage.gen.js +++ b/themes/Legacy/5ePHB/snippets/coverpage.gen.js @@ -99,7 +99,7 @@ const subtitles = [ function coverPageGen() { -return ` diff --git a/themes/V3/Blank/snippets/license.gen.js b/themes/V3/Blank/snippets/license.gen.js index a37cb9a47..be836173c 100644 --- a/themes/V3/Blank/snippets/license.gen.js +++ b/themes/V3/Blank/snippets/license.gen.js @@ -35,30 +35,30 @@ export default { }} `; }, - cczero : ` \ This work is openly licensed via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)\n\n`, - ccby : ` \ This work is openly licensed via [CC BY 4.0](https://creativecommons.org/publicdomain/by/4.0/)\n\n`, - ccbysa : ` \ This work is openly licensed via [CC BY-SA 4.0](https://creativecommons.org/publicdomain/by-sa/4.0/)\n\n`, - ccbync : ` \ This work is openly licensed via [CC BY-NC 4.0](https://creativecommons.org/publicdomain/by-nc/4.0/)\n\n`, - ccbyncsa : ` \ This work is openly licensed via [CC BY-NC-SA](https://creativecommons.org/publicdomain/by-nc-sa/4.0/)\n\n`, - ccbynd : ` \ This work is openly licensed via [CC BY-ND 4.0](https://creativecommons.org/publicdomain/by-nd/4.0/)\n\n`, - ccbyncnd : ` \ This work is openly licensed via [CC NY-NC-ND 4.0](https://creativecommons.org/publicdomain/by-nc-nd/4.0/)\n\n`, - cczeroBadge : `![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)`, - ccbyBadge : `![CC BY](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg)`, - ccbysaBadge : `![CC BY-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-sa.svg)`, - ccbyncBadge : `![CC BY-NC](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc.svg)`, - ccbyncsaBadge : `![CC BY-NC-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-sa.svg)`, - ccbyndBadge : `![CC BY-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nd.svg)`, - ccbyncndBadge : `![CC BY-NC-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-nd.svg)`, - shadowDarkNotice : `\[Product Name]\ is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.\n`, - shadowDarkBlack : `![Shadowdark Black Logo](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_Black.png){width:200px}`, - shadowDarkWhite : `![Shadowdark White Logo](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_White.png){width:200px}`, - bladesDarkNotice : `This work is based on Blades in the Dark \(found at (http://www.bladesinthedark.com/)\), product of One Seven Design, developed and authored by John Harper, and licensed for our use under the Creative Commons Attribution 3.0 Unported license \(http://creativecommons.org/licenses/by/3.0/\).\n`, - bladesDarkLogo : `![Forged in the Dark](https://homebrewery.naturalcrit.com/assets/license_logos/Evil-Hat_Forged-In-The-Dark_Logo-V2.png)`, + cczero : ` \ This work is openly licensed via [CC0](https://creativecommons.org/publicdomain/zero/1.0/)\n\n`, + ccby : ` \ This work is openly licensed via [CC BY 4.0](https://creativecommons.org/publicdomain/by/4.0/)\n\n`, + ccbysa : ` \ This work is openly licensed via [CC BY-SA 4.0](https://creativecommons.org/publicdomain/by-sa/4.0/)\n\n`, + ccbync : ` \ This work is openly licensed via [CC BY-NC 4.0](https://creativecommons.org/publicdomain/by-nc/4.0/)\n\n`, + ccbyncsa : ` \ This work is openly licensed via [CC BY-NC-SA](https://creativecommons.org/publicdomain/by-nc-sa/4.0/)\n\n`, + ccbynd : ` \ This work is openly licensed via [CC BY-ND 4.0](https://creativecommons.org/publicdomain/by-nd/4.0/)\n\n`, + ccbyncnd : ` \ This work is openly licensed via [CC NY-NC-ND 4.0](https://creativecommons.org/publicdomain/by-nc-nd/4.0/)\n\n`, + cczeroBadge : `![CC0](http://mirrors.creativecommons.org/presskit/buttons/88x31/svg/cc-zero.svg)`, + ccbyBadge : `![CC BY](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by.svg)`, + ccbysaBadge : `![CC BY-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-sa.svg)`, + ccbyncBadge : `![CC BY-NC](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc.svg)`, + ccbyncsaBadge : `![CC BY-NC-SA](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-sa.svg)`, + ccbyndBadge : `![CC BY-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nd.svg)`, + ccbyncndBadge : `![CC BY-NC-ND](https://mirrors.creativecommons.org/presskit/buttons/88x31/svg/by-nc-nd.svg)`, + shadowDarkNotice : `\[Product Name]\ is an independent product published under the Shadowdark RPG Third-Party License and is not affiliated with The Arcane Library, LLC. Shadowdark RPG © 2023 The Arcane Library, LLC.\n`, + shadowDarkBlack : `![Shadowdark Black Logo](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_Black.png){width:200px}`, + shadowDarkWhite : `![Shadowdark White Logo](https://homebrewery.naturalcrit.com/assets/license_logos/The-Arcane-Library_Third-Party-License_White.png){width:200px}`, + bladesDarkNotice : `This work is based on Blades in the Dark \(found at (http://www.bladesinthedark.com/)\), product of One Seven Design, developed and authored by John Harper, and licensed for our use under the Creative Commons Attribution 3.0 Unported license \(http://creativecommons.org/licenses/by/3.0/\).\n`, + bladesDarkLogo : `![Forged in the Dark](https://homebrewery.naturalcrit.com/assets/license_logos/Evil-Hat_Forged-In-The-Dark_Logo-V2.png)`, bladesDarkLogoAttribution : `*Blades in the Dark^tm^ is a trademark of One Seven Design. The Forged in the Dark Logo is © One Seven Design, and is used with permission.*`, - iconsCompatibility : 'Compatibility with Icons requires Icons Superpowered Roleplaying from Ad Infinitum Adventures. Ad Infinitum Adventures does not guarantee compatibility, and does not endorse this product.', - iconsTrademark : 'Icons Superpowered Roleplaying is a trademark of Steve Kenson, published exclusively by Ad Infinitum Adventures. The Icons Superpowered Roleplaying Compatibility Logo is a trademark of Ad Infinitum Adventures and is used under the Icons Superpowered Roleplaying Compatibility License.', - icondsSection15 : 'Open Game License v 1.0, Copyright 2000, Wizards of the Coast, Inc.\n::\nFudge System Reference Document, Copyright 2005, Grey Ghost Press, Inc.; Authors Steffan O\’Sullivan and Ann Dupuis, with additional material by Peter Bonney, Deird’Re Brooks, Reimer Behrends, Shawn Garbett, Steven Hammond, Ed Heil, Bernard Hsiung, Sedge Lewis, Gordon McCormick, Kent Matthewson, Peter Mikelsons, Anthony Roberson, Andy Skinner, Stephan Szabo, John Ughrin, Dmitri Zagidulin\n::\nFATE (Fantastic Adventures in Tabletop Entertainment), Copyright 2003 by Evil Hat Productions LLC; Authors Robert Donoghue and Fred Hicks\n::\nSpirit of the Century, Copyright 2006, Evil Hat Productions LLC. Authors Robert Donoghue, Fred Hicks, and Leonard Balsera.\n::\nIcons, Copyright 2010, Ad Infinitum Adventures; Author Steve Kenson.\n', - iconsCompatibilityLogo : '![Icons Compatibility Logo](/assets/license_logos/Ad-Infinitum-Adventures_Icons-Compatibility-License_Logo.png){width:200px}', - grTrue20Sec15 : 'True20 Adventure Roleplaying, Revised Edition OGL Section 15.\n\n15. COPYRIGHT NOTICE\nOpen Game License v 1.0 Copyright 2000, Wizards of the Coast, Inc.\n\nSystem Reference Document, Copyright 2000, Wizards of the Coast, Inc., Authors Jonathan Tweet, Monte Cook, Skip Williams, based on original material by E. Gary Gygax and Dave Arneson.\n\nModern System Reference Document Copyright 2002-2004, Wizards of the Coast, Inc.; Authors Bill Slavicsek, Jeff Grubb, Rich Redman, Charles Ryan, Eric Cagle, David Noonan, Stan!, Christopher Perkins, Rodney Thompson, and JD Wiker, based on material by Jonathan Tweet, Monte Cook, Skip Williams, Richard Baker, Peter Adkison, Bruce R. Cordell, John Tynes, Andy Collins, and JD Wiker.\n\nAdvanced Player’s Manual, Copyright 2005, Green Ronin Publishing: Author Skip Williams.\n\nAdvanced Player’s Guide, Copyright 2004, White Wolf Publishing, Inc.\n\nAlgernon Files, Copyright 2004, Blackwyrm Games; Authors Aaron Sullivan and Dave Mattingly.\n\nArmies of the Abyss, Copyright 2002, Green Ronin Publishing; Authors Erik Mona and Chris Pramas.\n\nThe Avatar’s Handbook, Copyright 2003, Green Ronin Publishing; Authors Jesse Decker and Chris Tomasson.\n\nBastards & Bloodlines, Copyright 2003, Green Ronin Publishing, Author Owen K.C. Stephens\n\nBlue Rose, Copyright 2005, Green Ronin Publishing; Authors Jeremy Crawford, Dawn Elliot, Steve Kenson, and John Snead.\n\nBlue Rose Companion, Copyright 2005, Green Ronin Publishing; Editor Jeremy Crawford.\n\nThe Book of Fiends, Copyright 2003, Green Ronin Publishing; Authors Aaron Loeb, Erik Mona, Chris Pramas, and Robert J. Schwalb.\n\nBook of the Righteous, Copyright 2002, Aaron Loeb.\n\nChallenging Challenge Ratings: Immortal’s Handbook, Copyright 2003, Craig Cochrane.\n\nConan The Roleplaying Game, Copyright 2003 Conan Properties International LCC; Authorized Publisher Mongoose Publishing Ltd; Author Ian Sturrock.\n\nCORE Explanatory Notice, Copyright 2003, Benjamin R. Durbin\n\nCreatures of Freeport, Copyright 2004, Green Ronin Publishing, LLC; Authors Graeme Davis and Keith Baker.\n\nCrime and Punishment, Copyright 2003, Author Keith Baker\n\nCrooks!, Copyright 2003, Green Ronin Publishing; Authors Sean Glenn, Kyle Hunter, and Erik Mona.\n\nCry Havoc, Copyright 2003, Skip Williams. All rights reserved.\n\nChallenging Challenge Ratings: Immortal’s Handbook, Copyright 2003, Craig Cochrane.\n\nDarwin’s World 2nd Edition, Copyright 2003, RPG Objects; Authors Dominic Covey and Chris Davis.\n\nDesign Parameters: Immortal’s Handbook, Copyright 2003, Craig Cochrane.\n\nFading Suns d20, Copyright 2001, Holistic Design, Inc.\n\nGalactic Races, Copyright 2001, Fantasy Flight Games.\n\nGimmick’s Guide to Gadgets, Copyright 2005, Green Ronin Publishing; Author Mike Mearls.\n\nGrim Tales, Copyright 2004, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Cyberware game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Firearms game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Horror game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Spellcasting game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Vehicle game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nHot Pursuit, Copyright 2005, Corey Reid, published by Adamant Entertainment, Inc.\n\nImmortals Handbook, Copyright 2003, Craig Cochrane.\n\nLegions of Hell, Copyright 2001, Green Ronin Publishing; Author Chris Pramas.\n\nA Magical Medieval Society: Western Europe, Copyright 2003, Expeditious Retreat Press; Authors Suzi Yee and Joseph Browning.\n\nThe Mastermind’s Manual, Copyright 2006, Green Ronin Publishing; Author Steve Kenson.\n\nModern Player’s Companion, Copyright 2003, The Game Mechanics, Inc; Author: Stan!\n\nMonster’s Handbook, Copyright 2002, Fantasy Flight Publishing, Inc.\n\nMonte Cook Presents: Iron Heroes, Copyright 2005, Monte J. Cook. All rights reserved.\n\nMonte Cook’s: Arcana Unearthed, Copyright 2003, Monte J. Cook. All rights reserved.\n\nMutants & Masterminds, Copyright 2002, Green Ronin Publishing; Author Steve Kenson.\n\nMutants & Masterminds, Second Edition, Copyright 2005, Green Ronin Publishing; Author Steve Kenson.\n\nMutants & Masterminds Annual #1, Copyright 2004, Green Ronin Publishing, LLC; Editor Erik Mona.\n\nMythic Heroes, Copyright 2005, Benjamin R. Durbin, published by Bad Axe Games, LLC.\n\nOGL Horror, Copyright 2003, Mongoose Publishing Limited.\n\nPossessors: Children of the Outer Gods, Copyright 2003, Philip Reed and Christopher Shy, www.philipjreed.com and www.studioronin.com.\n\nThe Psychic’s Handbook, Copyright 2004, Green Ronin Publishing; Author Steve Kenson.\n\nThe Quintessential Fighter, Copyright 2001 Mongoose Publishing\n\nRelics and Rituals: Excalibur,Copyright 2004, White Wolf Publishing, Inc.\n\nRokugan, Copyright 2001 AEG\n\nThe Seven Saxons, by Benjamin R. Durbin and Ryan Smalley, Copyright 2005, Bad Axe Games, LLC.\n\nSilver Age Sentinels d20, Copyright 2002, Guardians of Order, Inc.; Authors Stephen Kenson, Mark C. Mackinnon, Jeff Mackintosh, Jesse Scoble.\n\nSkull & Bones, Copyright 2003, Green Ronin, Green Ronin Publisihing, Authors Ian Sturrock, T.S. Luikart, and Gareth-Michael Skarka.\n\nSpycraft Copyright 2002, Alderac Entertainment Group.\n\nSpycraft Espionage Handbook, Copyright 2002, Alderac Entertainment Group, Inc.; Authors Patrick Kapera and Kevin Wilson.\n\nSpycraft Faceman/Snoop Class Guide, Copyright 2003, Alderac Entertainment Group, Inc.; Authors Alexander Flagg, Clayton A. Oliver.\n\nSpycraft Fixer/Pointman Class Guide, Copyright 2003, Alderac Entertainment Group, Inc.; Authors Scott Gearin.\n\nSpycraft Mastermind Guide, Copyright 2004, Alderac Entertainment Group, Inc.; Steve Crow, Alexander Flagg, B. D. Flory, Clayton A. Oliver.\n\nSpycraft Modern Arms Guide, Copyright 2002, Alderac Entertainment Group, Inc.; Authors Chad Brunner, Tim D’Allard, Rob Drake, Michael Fish, Scott Gearin, Owen Hershey, Patrick Kapera, Michael Petrovich, Jim Wardrip, Stephen Wilcoxon.\n\nSpycraft Soldier/Wheelman Class Guide, Copyright 2003, Alderac Entertainment Group, Inc.; Authors Chad Brunner, Shawn Carman, B. D. Flory, Scott Gearin, Patrick Kapera.\n\nSpycraft U.S. Militaries Guide, Copyright 2004, Alderac Entertainment Group, Inc.; Authors Dave McAlister, Clayton A. Oliver, Patrick Kapera.\n\nSpycraft, Copyright 2005, Alderac Entertainment Group.\n\nSwords of Our Fathers, Copyright 2003, The Game Mechanics\n\nTales of the Caliphate Nights, Copyright 2006, Paradigm Concepts, Inc., Author Aaron Infante-Levy\n\nTome of Horrors, Copyright 2002, Necromancer Games., Inc.; Author Scott Greene, based on original material by Gary Gygax.\n\nTrue20 Adventure Roleplaying, Copyright 2005, Green Ronin Publishing; Author Steve Kenson.\n\nTrue20 Bestiary, Copyright 2006, Green Ronin Publishing; Author Matthew E. Kaiser.\n\nTrue20 Companion, Copyright 2007, Green Ronin Publishing; Authors Erica Balsley, David Jarvis, Matthew E. Kaiser, Steve Kenson, and Sean Preston.\n\nThe Unholy Warrior’s Handbook, Copyright 2003, Green Ronin Publishing; Author Robert J. Schwalb.\n\nUltramodern Firearms, Copyright 2002, Green Ronin Publishing; Author Charles McManus Ryan.\n\nUnearthed Arcana, Copyright 2004, Wizards of the Coast, Inc.; Andy Collins, Jesse Decker, David Noonan, Rich Redman.\n\nWrath & Rage, Copyright 2002, Green Ronin Publishing, Author Jim Bishop\n\nTrue20 Adventure Roleplaying, Revised Edition, Copyright 2008, Green Ronin Publishing; Author Steve Kenson.', - grTrue20CompatLogo : `![True20 Compatibility Logo](/assets/license_logos/true_20.jpg){width:1.5in}` + iconsCompatibility : 'Compatibility with Icons requires Icons Superpowered Roleplaying from Ad Infinitum Adventures. Ad Infinitum Adventures does not guarantee compatibility, and does not endorse this product.', + iconsTrademark : 'Icons Superpowered Roleplaying is a trademark of Steve Kenson, published exclusively by Ad Infinitum Adventures. The Icons Superpowered Roleplaying Compatibility Logo is a trademark of Ad Infinitum Adventures and is used under the Icons Superpowered Roleplaying Compatibility License.', + icondsSection15 : 'Open Game License v 1.0, Copyright 2000, Wizards of the Coast, Inc.\n::\nFudge System Reference Document, Copyright 2005, Grey Ghost Press, Inc.; Authors Steffan O\’Sullivan and Ann Dupuis, with additional material by Peter Bonney, Deird’Re Brooks, Reimer Behrends, Shawn Garbett, Steven Hammond, Ed Heil, Bernard Hsiung, Sedge Lewis, Gordon McCormick, Kent Matthewson, Peter Mikelsons, Anthony Roberson, Andy Skinner, Stephan Szabo, John Ughrin, Dmitri Zagidulin\n::\nFATE (Fantastic Adventures in Tabletop Entertainment), Copyright 2003 by Evil Hat Productions LLC; Authors Robert Donoghue and Fred Hicks\n::\nSpirit of the Century, Copyright 2006, Evil Hat Productions LLC. Authors Robert Donoghue, Fred Hicks, and Leonard Balsera.\n::\nIcons, Copyright 2010, Ad Infinitum Adventures; Author Steve Kenson.\n', + iconsCompatibilityLogo : '![Icons Compatibility Logo](/assets/license_logos/Ad-Infinitum-Adventures_Icons-Compatibility-License_Logo.png){width:200px}', + grTrue20Sec15 : 'True20 Adventure Roleplaying, Revised Edition OGL Section 15.\n\n15. COPYRIGHT NOTICE\nOpen Game License v 1.0 Copyright 2000, Wizards of the Coast, Inc.\n\nSystem Reference Document, Copyright 2000, Wizards of the Coast, Inc., Authors Jonathan Tweet, Monte Cook, Skip Williams, based on original material by E. Gary Gygax and Dave Arneson.\n\nModern System Reference Document Copyright 2002-2004, Wizards of the Coast, Inc.; Authors Bill Slavicsek, Jeff Grubb, Rich Redman, Charles Ryan, Eric Cagle, David Noonan, Stan!, Christopher Perkins, Rodney Thompson, and JD Wiker, based on material by Jonathan Tweet, Monte Cook, Skip Williams, Richard Baker, Peter Adkison, Bruce R. Cordell, John Tynes, Andy Collins, and JD Wiker.\n\nAdvanced Player’s Manual, Copyright 2005, Green Ronin Publishing: Author Skip Williams.\n\nAdvanced Player’s Guide, Copyright 2004, White Wolf Publishing, Inc.\n\nAlgernon Files, Copyright 2004, Blackwyrm Games; Authors Aaron Sullivan and Dave Mattingly.\n\nArmies of the Abyss, Copyright 2002, Green Ronin Publishing; Authors Erik Mona and Chris Pramas.\n\nThe Avatar’s Handbook, Copyright 2003, Green Ronin Publishing; Authors Jesse Decker and Chris Tomasson.\n\nBastards & Bloodlines, Copyright 2003, Green Ronin Publishing, Author Owen K.C. Stephens\n\nBlue Rose, Copyright 2005, Green Ronin Publishing; Authors Jeremy Crawford, Dawn Elliot, Steve Kenson, and John Snead.\n\nBlue Rose Companion, Copyright 2005, Green Ronin Publishing; Editor Jeremy Crawford.\n\nThe Book of Fiends, Copyright 2003, Green Ronin Publishing; Authors Aaron Loeb, Erik Mona, Chris Pramas, and Robert J. Schwalb.\n\nBook of the Righteous, Copyright 2002, Aaron Loeb.\n\nChallenging Challenge Ratings: Immortal’s Handbook, Copyright 2003, Craig Cochrane.\n\nConan The Roleplaying Game, Copyright 2003 Conan Properties International LCC; Authorized Publisher Mongoose Publishing Ltd; Author Ian Sturrock.\n\nCORE Explanatory Notice, Copyright 2003, Benjamin R. Durbin\n\nCreatures of Freeport, Copyright 2004, Green Ronin Publishing, LLC; Authors Graeme Davis and Keith Baker.\n\nCrime and Punishment, Copyright 2003, Author Keith Baker\n\nCrooks!, Copyright 2003, Green Ronin Publishing; Authors Sean Glenn, Kyle Hunter, and Erik Mona.\n\nCry Havoc, Copyright 2003, Skip Williams. All rights reserved.\n\nChallenging Challenge Ratings: Immortal’s Handbook, Copyright 2003, Craig Cochrane.\n\nDarwin’s World 2nd Edition, Copyright 2003, RPG Objects; Authors Dominic Covey and Chris Davis.\n\nDesign Parameters: Immortal’s Handbook, Copyright 2003, Craig Cochrane.\n\nFading Suns d20, Copyright 2001, Holistic Design, Inc.\n\nGalactic Races, Copyright 2001, Fantasy Flight Games.\n\nGimmick’s Guide to Gadgets, Copyright 2005, Green Ronin Publishing; Author Mike Mearls.\n\nGrim Tales, Copyright 2004, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Cyberware game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Firearms game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Horror game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Spellcasting game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nGrim Tales, Vehicle game mechanics; Copyright 2003, Benjamin R. Durbin, published by Bad Axe Games, LCC.\n\nHot Pursuit, Copyright 2005, Corey Reid, published by Adamant Entertainment, Inc.\n\nImmortals Handbook, Copyright 2003, Craig Cochrane.\n\nLegions of Hell, Copyright 2001, Green Ronin Publishing; Author Chris Pramas.\n\nA Magical Medieval Society: Western Europe, Copyright 2003, Expeditious Retreat Press; Authors Suzi Yee and Joseph Browning.\n\nThe Mastermind’s Manual, Copyright 2006, Green Ronin Publishing; Author Steve Kenson.\n\nModern Player’s Companion, Copyright 2003, The Game Mechanics, Inc; Author: Stan!\n\nMonster’s Handbook, Copyright 2002, Fantasy Flight Publishing, Inc.\n\nMonte Cook Presents: Iron Heroes, Copyright 2005, Monte J. Cook. All rights reserved.\n\nMonte Cook’s: Arcana Unearthed, Copyright 2003, Monte J. Cook. All rights reserved.\n\nMutants & Masterminds, Copyright 2002, Green Ronin Publishing; Author Steve Kenson.\n\nMutants & Masterminds, Second Edition, Copyright 2005, Green Ronin Publishing; Author Steve Kenson.\n\nMutants & Masterminds Annual #1, Copyright 2004, Green Ronin Publishing, LLC; Editor Erik Mona.\n\nMythic Heroes, Copyright 2005, Benjamin R. Durbin, published by Bad Axe Games, LLC.\n\nOGL Horror, Copyright 2003, Mongoose Publishing Limited.\n\nPossessors: Children of the Outer Gods, Copyright 2003, Philip Reed and Christopher Shy, www.philipjreed.com and www.studioronin.com.\n\nThe Psychic’s Handbook, Copyright 2004, Green Ronin Publishing; Author Steve Kenson.\n\nThe Quintessential Fighter, Copyright 2001 Mongoose Publishing\n\nRelics and Rituals: Excalibur,Copyright 2004, White Wolf Publishing, Inc.\n\nRokugan, Copyright 2001 AEG\n\nThe Seven Saxons, by Benjamin R. Durbin and Ryan Smalley, Copyright 2005, Bad Axe Games, LLC.\n\nSilver Age Sentinels d20, Copyright 2002, Guardians of Order, Inc.; Authors Stephen Kenson, Mark C. Mackinnon, Jeff Mackintosh, Jesse Scoble.\n\nSkull & Bones, Copyright 2003, Green Ronin, Green Ronin Publisihing, Authors Ian Sturrock, T.S. Luikart, and Gareth-Michael Skarka.\n\nSpycraft Copyright 2002, Alderac Entertainment Group.\n\nSpycraft Espionage Handbook, Copyright 2002, Alderac Entertainment Group, Inc.; Authors Patrick Kapera and Kevin Wilson.\n\nSpycraft Faceman/Snoop Class Guide, Copyright 2003, Alderac Entertainment Group, Inc.; Authors Alexander Flagg, Clayton A. Oliver.\n\nSpycraft Fixer/Pointman Class Guide, Copyright 2003, Alderac Entertainment Group, Inc.; Authors Scott Gearin.\n\nSpycraft Mastermind Guide, Copyright 2004, Alderac Entertainment Group, Inc.; Steve Crow, Alexander Flagg, B. D. Flory, Clayton A. Oliver.\n\nSpycraft Modern Arms Guide, Copyright 2002, Alderac Entertainment Group, Inc.; Authors Chad Brunner, Tim D’Allard, Rob Drake, Michael Fish, Scott Gearin, Owen Hershey, Patrick Kapera, Michael Petrovich, Jim Wardrip, Stephen Wilcoxon.\n\nSpycraft Soldier/Wheelman Class Guide, Copyright 2003, Alderac Entertainment Group, Inc.; Authors Chad Brunner, Shawn Carman, B. D. Flory, Scott Gearin, Patrick Kapera.\n\nSpycraft U.S. Militaries Guide, Copyright 2004, Alderac Entertainment Group, Inc.; Authors Dave McAlister, Clayton A. Oliver, Patrick Kapera.\n\nSpycraft, Copyright 2005, Alderac Entertainment Group.\n\nSwords of Our Fathers, Copyright 2003, The Game Mechanics\n\nTales of the Caliphate Nights, Copyright 2006, Paradigm Concepts, Inc., Author Aaron Infante-Levy\n\nTome of Horrors, Copyright 2002, Necromancer Games., Inc.; Author Scott Greene, based on original material by Gary Gygax.\n\nTrue20 Adventure Roleplaying, Copyright 2005, Green Ronin Publishing; Author Steve Kenson.\n\nTrue20 Bestiary, Copyright 2006, Green Ronin Publishing; Author Matthew E. Kaiser.\n\nTrue20 Companion, Copyright 2007, Green Ronin Publishing; Authors Erica Balsley, David Jarvis, Matthew E. Kaiser, Steve Kenson, and Sean Preston.\n\nThe Unholy Warrior’s Handbook, Copyright 2003, Green Ronin Publishing; Author Robert J. Schwalb.\n\nUltramodern Firearms, Copyright 2002, Green Ronin Publishing; Author Charles McManus Ryan.\n\nUnearthed Arcana, Copyright 2004, Wizards of the Coast, Inc.; Andy Collins, Jesse Decker, David Noonan, Rich Redman.\n\nWrath & Rage, Copyright 2002, Green Ronin Publishing, Author Jim Bishop\n\nTrue20 Adventure Roleplaying, Revised Edition, Copyright 2008, Green Ronin Publishing; Author Steve Kenson.', + grTrue20CompatLogo : `![True20 Compatibility Logo](/assets/license_logos/true_20.jpg){width:1.5in}` }; \ No newline at end of file diff --git a/themes/V3/Blank/snippets/licenseDTRPGCC.gen.js b/themes/V3/Blank/snippets/licenseDTRPGCC.gen.js index 57683d4e7..9fed127e0 100644 --- a/themes/V3/Blank/snippets/licenseDTRPGCC.gen.js +++ b/themes/V3/Blank/snippets/licenseDTRPGCC.gen.js @@ -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` diff --git a/themes/V3/Blank/snippets/licenseMongoose.gen.js b/themes/V3/Blank/snippets/licenseMongoose.gen.js index 5c939572d..9593dadd6 100644 --- a/themes/V3/Blank/snippets/licenseMongoose.gen.js +++ b/themes/V3/Blank/snippets/licenseMongoose.gen.js @@ -1,6 +1,3 @@ - -import dedent from 'dedent'; - // Mongoose Publishing Licenses export default { diff --git a/vite.config.js b/vite.config.js index 6025f8736..d9eacd502 100644 --- a/vite.config.js +++ b/vite.config.js @@ -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 : ['.'], }, }, }); diff --git a/vitePlugins/generateAssetsPlugin.js b/vitePlugins/generateAssetsPlugin.js index eaf74509b..caea2c1e8 100644 --- a/vitePlugins/generateAssetsPlugin.js +++ b/vitePlugins/generateAssetsPlugin.js @@ -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`); }, }; } diff --git a/vitreum/headtags.js b/vitreum/headtags.js index 54cdf5922..5f4019ecf 100644 --- a/vitreum/headtags.js +++ b/vitreum/headtags.js @@ -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 = `${toStr(children)}`; - useEffect(() => { + if(onServer) NamedTags.title = `${toStr(children)}`; + useEffect(()=>{ document.title = toStr(children); }, [children]); return null; }, - Favicon({ type = "image/png", href = "", rel = "icon", id = "favicon" }) { - if (onServer) NamedTags.favicon = ``; - useEffect(() => { + Favicon({ type = 'image/png', href = '', rel = 'icon', id = 'favicon' }) { + if(onServer) NamedTags.favicon = ``; + useEffect(()=>{ document.getElementById(id).href = href; }, [id, href]); return null; }, Description({ children }) { - if (onServer) NamedTags.description = ``; + if(onServer) NamedTags.description = ``; return null; }, Noscript({ children }) { - if (onServer) UnnamedTags.push(``); + if(onServer) UnnamedTags.push(``); return null; }, Script({ children = [], ...props }) { - if (onServer) { + if(onServer) { UnnamedTags.push( children.length ? `` @@ -53,31 +52,31 @@ export const HeadComponents = { return null; }, Meta(props) { - let tag = ``; + const tag = ``; 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({ children, type = 'text/css' }) { + if(onServer) UnnamedTags.push(``); 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 = []; };