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

linting and fixing made tags

This commit is contained in:
Víctor Losada Hernández
2026-02-17 14:43:09 +01:00
parent c265268b02
commit 0a68b8ecf9
3 changed files with 141 additions and 131 deletions

View File

@@ -339,9 +339,9 @@ const MetadataEditor = createReactClass({
{this.renderThumbnail()} {this.renderThumbnail()}
</div> </div>
<TagInput <TagInput
label='tags' label='tags'
valuePatterns={/^(?:(?:group|meta|system|type):)?[A-Za-z0-9][A-Za-z0-9 \/.&_\-]{0,40}$/} valuePatterns={/^\s*(?:(?:group|meta|system|type)\s*:\s*)?[A-Za-z0-9][A-Za-z0-9 \/.&_\-]{0,40}\s*$/}
placeholder='add tag' unique={true} placeholder='add tag' unique={true}
values={this.props.metadata.tags} values={this.props.metadata.tags}
smallText='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.' smallText='You may start tags with "type", "system", "group" or "meta" followed by a colon ":", these will be colored in your userpage.'
@@ -358,8 +358,8 @@ const MetadataEditor = createReactClass({
{this.renderAuthors()} {this.renderAuthors()}
<TagInput <TagInput
label='invited authors' label='invited authors'
valuePatterns={/.+/} valuePatterns={/.+/}
validators={[(v)=>!this.props.metadata.authors?.includes(v)]} validators={[(v)=>!this.props.metadata.authors?.includes(v)]}
placeholder='invite author' unique={true} placeholder='invite author' unique={true}

View File

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

View File

@@ -1,16 +1,20 @@
.list input {
border-radius: 5px;
}
.tagInput-dropdown { .tagInput-dropdown {
.dropdown-options { .dropdown-options {
.item { .item {
&.type { &.type {
background-color: #00800035; background-color: #00800035;
} }
&.group { &.group {
background-color: #50505035; background-color: #50505035;
} }
&.meta { &.meta {
background-color: #00008035; background-color: #00008035;
} }
&.system { &.system {
background-color: #80000035; background-color: #80000035;
} }
} }