diff --git a/src/api/utils/PrefixUtils.ts b/src/api/utils/PrefixUtils.ts index d010084..5da5133 100644 --- a/src/api/utils/PrefixUtils.ts +++ b/src/api/utils/PrefixUtils.ts @@ -6,9 +6,6 @@ export const PRESET_PREFIX_EFFECTS: { id: string; label: string; icon: string }[ { id: 'outline', label: 'Outline', icon: '🔲' }, { id: 'pulse', label: 'Pulse', icon: '💫' }, { id: 'bold-glow', label: 'Neon', icon: '💡' }, - { id: 'rainbow', label: 'Rainbow', icon: '🌈' }, - { id: 'shake', label: 'Shake', icon: '📳' }, - { id: 'wave', label: 'Wave', icon: '🌊' }, ]; export const parsePrefixColors = (text: string, colorStr: string): string[] => @@ -43,40 +40,11 @@ export const getPrefixEffectStyle = (effect: string, color?: string): Record -{ - if(steps <= 1) return [ startColor ]; - - const parseHex = (hex: string) => - { - const h = hex.replace('#', ''); - return { r: parseInt(h.substring(0, 2), 16), g: parseInt(h.substring(2, 4), 16), b: parseInt(h.substring(4, 6), 16) }; - }; - - const start = parseHex(startColor); - const end = parseHex(endColor); - - return Array.from({ length: steps }, (_, i) => - { - const t = i / (steps - 1); - const r = Math.round(start.r + (end.r - start.r) * t); - const g = Math.round(start.g + (end.g - start.g) * t); - const b = Math.round(start.b + (end.b - start.b) * t); - return `#${ r.toString(16).padStart(2, '0') }${ g.toString(16).padStart(2, '0') }${ b.toString(16).padStart(2, '0') }`; - }); -}; - export const PREFIX_EFFECT_KEYFRAMES = ` @keyframes prefix-pulse { 0%, 100% { opacity: 1; } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx index f0158f2..e723833 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutCustomPrefixView.tsx @@ -1,7 +1,5 @@ import { PurchasePrefixComposer } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { LocalizeText, SendMessageComposer, PRESET_PREFIX_EFFECTS, generateGradientColors } from '../../../../../api'; -import { PrefixPreview } from '../../../../../layout'; import { LocalizeText, SanitizeHtml, SendMessageComposer, PRESET_PREFIX_EFFECTS, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../../api'; import { CatalogLayoutProps } from './CatalogLayout.types'; import data from '@emoji-mart/data'; @@ -24,13 +22,11 @@ export const CatalogLayoutCustomPrefixView: FC = props => }, [ page, hideNavigation ]); const [ prefixText, setPrefixText ] = useState(''); - const [ colorMode, setColorMode ] = useState<'single' | 'perLetter' | 'gradient'>('single'); + const [ colorMode, setColorMode ] = useState<'single' | 'perLetter'>('single'); const [ singleColor, setSingleColor ] = useState('#FFFFFF'); const [ letterColors, setLetterColors ] = useState>({}); const [ selectedLetterIndex, setSelectedLetterIndex ] = useState(null); const [ customColorInput, setCustomColorInput ] = useState('#FFFFFF'); - const [ gradientStart, setGradientStart ] = useState('#FF0000'); - const [ gradientEnd, setGradientEnd ] = useState('#0066FF'); const [ selectedIcon, setSelectedIcon ] = useState(''); const [ showIconPicker, setShowIconPicker ] = useState(false); const [ selectedEffect, setSelectedEffect ] = useState(''); @@ -40,16 +36,15 @@ export const CatalogLayoutCustomPrefixView: FC = props => { if(colorMode === 'single') return singleColor; - if(colorMode === 'gradient') - { - const steps = Math.max(prefixText.length, 2); - return generateGradientColors(gradientStart, gradientEnd, steps).join(','); - } - if(!prefixText.length) return singleColor; return [ ...prefixText ].map((_, i) => letterColors[i] || singleColor).join(','); - }, [ colorMode, singleColor, letterColors, prefixText, gradientStart, gradientEnd ]); + }, [ colorMode, singleColor, letterColors, prefixText ]); + + const previewColors = useMemo(() => + { + return parsePrefixColors(prefixText || '...', colorString || '#FFFFFF'); + }, [ prefixText, colorString ]); const isValid = useMemo(() => { @@ -57,12 +52,9 @@ export const CatalogLayoutCustomPrefixView: FC = props => if(colorMode === 'single') return /^#[0-9A-Fa-f]{6}$/.test(singleColor); - if(colorMode === 'gradient') - return /^#[0-9A-Fa-f]{6}$/.test(gradientStart) && /^#[0-9A-Fa-f]{6}$/.test(gradientEnd); - const colors = colorString.split(','); return colors.every(c => /^#[0-9A-Fa-f]{6}$/.test(c)); - }, [ prefixText, colorMode, singleColor, colorString, gradientStart, gradientEnd ]); + }, [ prefixText, colorMode, singleColor, colorString ]); const handlePurchase = () => { @@ -80,7 +72,7 @@ export const CatalogLayoutCustomPrefixView: FC = props => setSingleColor(color); setCustomColorInput(color); } - else if(colorMode === 'perLetter' && selectedLetterIndex !== null) + else if(selectedLetterIndex !== null) { setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: color })); setCustomColorInput(color); @@ -104,7 +96,7 @@ export const CatalogLayoutCustomPrefixView: FC = props => { setSingleColor(value); } - else if(colorMode === 'perLetter' && selectedLetterIndex !== null) + else if(selectedLetterIndex !== null) { setLetterColors(prev => ({ ...prev, [selectedLetterIndex]: value })); } @@ -129,12 +121,18 @@ export const CatalogLayoutCustomPrefixView: FC = props => setLetterColors(newColors); }; + const hasMultiColor = colorMode === 'perLetter' && previewColors.length > 1 && new Set(previewColors).size > 1; + const currentActiveColor = colorMode === 'single' ? singleColor : (selectedLetterIndex !== null ? (letterColors[selectedLetterIndex] || singleColor) : singleColor); + const effectStyle = getPrefixEffectStyle(selectedEffect, previewColors[0] || '#FFFFFF'); + return (
+ + { /* Header */ } { page.localization.getImage(0) && } @@ -150,37 +148,20 @@ export const CatalogLayoutCustomPrefixView: FC = props => } }>
- - - Username + + { selectedIcon && { selectedIcon } } + + {'{'} + { hasMultiColor + ? [ ...(prefixText || '...') ].map((char, i) => ( + { char } + )) + : (prefixText || '...') + } + {'}'} + -
- - { /* Chat Bubble Preview */ } -
-
{ LocalizeText('catalog.prefix.chat.preview') }
-
-
- { (prefixText || '...') && - } - Username: - Hello everyone! -
-
-
+ Username
{ /* Text + Icon Row */ } @@ -237,7 +218,6 @@ export const CatalogLayoutCustomPrefixView: FC = props => { showIconPicker && ( <>
setShowIconPicker(false) } /> -
= props => className="flex-1 px-2 py-1.5 text-xs font-bold transition-all" style={ { background: colorMode === 'perLetter' ? 'rgba(59,130,246,0.25)' : 'rgba(0,0,0,0.1)', - borderRight: '1px solid rgba(0,0,0,0.1)', opacity: colorMode === 'perLetter' ? 1 : 0.6 } } onClick={ () => { setColorMode('perLetter'); if(prefixText.length > 0) setSelectedLetterIndex(0); } }> { LocalizeText('catalog.prefix.color.per.letter') } -
- { /* Gradient Controls */ } - { colorMode === 'gradient' && ( -
-
-
- - { const v = e.target.value; setGradientStart(v); } } /> -
- → -
- - { const v = e.target.value; setGradientEnd(v); } } /> -
-
- { /* Gradient preview bar */ } -
-
- ) } - { /* Per-Letter Selector */ } { colorMode === 'perLetter' && prefixText.length > 0 && (
@@ -427,49 +347,6 @@ export const CatalogLayoutCustomPrefixView: FC = props =>
) } - { /* Color Palette (single & perLetter modes) */ } - { colorMode !== 'gradient' && ( -
- { colorMode === 'perLetter' && selectedLetterIndex !== null && - - { LocalizeText('catalog.prefix.color.selected') } "{ prefixText[selectedLetterIndex] || '' }" - - } -
- { PRESET_COLORS.map((color, idx) => - { - const isActive = currentActiveColor === color; - return ( -
handleColorSelect(color) } /> - ); - }) } -
-
- { /* Color Palette */ }
{ colorMode === 'perLetter' && selectedLetterIndex !== null && @@ -506,22 +383,28 @@ export const CatalogLayoutCustomPrefixView: FC = props => boxShadow: `0 0 6px ${ customColorInput }40, inset 0 1px 0 rgba(255,255,255,0.3)` } }> handleCustomColorChange(e.target.value) } /> -
+ onChange={ e => handleColorSelect(e.target.value) } /> + + handleCustomColorChange(e.target.value) } />
- ) } +
{ /* Purchase Footer */ }
= () => { @@ -18,8 +16,8 @@ export const FurniEditorView: FC<{}> = () => const { items, total, page, loading, error, clearError, selectedItem, catalogItems, furniDataEntry, - interactions, lastResult, - searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, createItem, loadInteractions + interactions, + searchItems, loadDetail, loadBySpriteId, updateItem, deleteItem, loadInteractions } = useFurniEditor(); useEffect(() => @@ -59,14 +57,13 @@ export const FurniEditorView: FC<{}> = () => useEffect(() => { - const handler = (e: CustomEvent<{ spriteId: number }>) => + const handler = async (e: CustomEvent<{ spriteId: number }>) => { const { spriteId } = e.detail; - if(!spriteId || spriteId <= 0) return; + const ok = await loadBySpriteId(spriteId); - loadBySpriteId(spriteId); - setActiveTab(TAB_EDIT); + if(ok) setActiveTab(TAB_EDIT); }; window.addEventListener('furni-editor:open', handler as EventListener); @@ -74,10 +71,11 @@ export const FurniEditorView: FC<{}> = () => return () => window.removeEventListener('furni-editor:open', handler as EventListener); }, [ loadBySpriteId ]); - const handleSelect = useCallback((id: number) => + const handleSelect = useCallback(async (id: number) => { - loadDetail(id); - setActiveTab(TAB_EDIT); + const ok = await loadDetail(id); + + if(ok) setActiveTab(TAB_EDIT); }, [ loadDetail ]); const handleBack = useCallback(() => @@ -90,17 +88,10 @@ export const FurniEditorView: FC<{}> = () => setIsVisible(false); }, []); - const handleCreated = useCallback((id: number) => - { - loadDetail(id); - setActiveTab(TAB_EDIT); - }, [ loadDetail ]); - - if(!GetSessionDataManager()?.isModerator) return null; if(!isVisible) return null; return ( - + setActiveTab(TAB_SEARCH) }> @@ -136,7 +127,6 @@ export const FurniEditorView: FC<{}> = () => furniDataEntry={ furniDataEntry } interactions={ interactions } loading={ loading } - lastResult={ lastResult } onUpdate={ updateItem } onDelete={ deleteItem } onBack={ handleBack } @@ -144,7 +134,6 @@ export const FurniEditorView: FC<{}> = () => /> } - ); diff --git a/src/components/furni-editor/views/FurniEditorCreateView.tsx b/src/components/furni-editor/views/FurniEditorCreateView.tsx index d7461e0..f47530c 100644 --- a/src/components/furni-editor/views/FurniEditorCreateView.tsx +++ b/src/components/furni-editor/views/FurniEditorCreateView.tsx @@ -1,23 +1,18 @@ -import { FC, useCallback, useEffect, useState } from 'react'; -import { FaPlus } from 'react-icons/fa'; -import { Column } from '../../../common'; +import { FC, useCallback, useState } from 'react'; +import { Button, Column, Flex, Text } from '../../../common'; interface FurniEditorCreateViewProps { interactions: string[]; loading: boolean; - lastResult: { success: boolean; message: string; id: number } | null; - onCreate: (fields: Record) => void; + onCreate: (fields: Record) => Promise; onCreated: (id: number) => void; } -const inputClass = 'text-[11px] border-2 border-card-grid-item-border rounded px-2 py-1 bg-white focus:outline-none focus:border-primary transition-colors w-full'; -const labelClass = 'text-[9px] text-[#666] uppercase font-bold mb-0.5 block'; - export const FurniEditorCreateView: FC = props => { - const { interactions, loading, lastResult, onCreate, onCreated } = props; - const [ toast, setToast ] = useState<{ type: 'success' | 'error'; message: string; id?: number } | null>(null); + const { interactions, loading, onCreate, onCreated } = props; + const [ success, setSuccess ] = useState(null); const [ form, setForm ] = useState({ itemName: '', @@ -41,50 +36,39 @@ export const FurniEditorCreateView: FC = props => customparams: '', }); - useEffect(() => - { - if(!lastResult) return; - - if(lastResult.success && lastResult.id > 0) - { - setToast({ type: 'success', message: `Item created with ID #${ lastResult.id }`, id: lastResult.id }); - setTimeout(() => onCreated(lastResult.id), 1500); - } - else if(!lastResult.success) - { - setToast({ type: 'error', message: lastResult.message }); - } - - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - }, [ lastResult ]); - const setField = useCallback((key: string, value: unknown) => { setForm(prev => ({ ...prev, [key]: value })); + setSuccess(null); }, []); - const handleCreate = useCallback(() => + const handleCreate = useCallback(async () => { if(!form.itemName || !form.publicName) return; - onCreate(form); - }, [ form, onCreate ]); + + const id = await onCreate(form); + + if(id) + { + setSuccess(id); + setTimeout(() => onCreated(id), 1000); + } + }, [ form, onCreate, onCreated ]); + + const inputClass = 'form-control form-control-sm'; + const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; return ( - { /* Toast */ } - { toast && -
- { toast.message } + { success && +
+ Item created with ID #{ success }!
} - { /* Basic Info */ } -
-
- Basic Info -
-
+
+ Basic Info +
setField('itemName', e.target.value) } placeholder="my_custom_furni" /> @@ -99,7 +83,7 @@ export const FurniEditorCreateView: FC = props =>
- setField('type', e.target.value) }> @@ -107,12 +91,9 @@ export const FurniEditorCreateView: FC = props =>
- { /* Dimensions */ } -
-
- Dimensions -
-
+
+ Dimensions +
setField('width', Number(e.target.value)) } /> @@ -128,17 +109,14 @@ export const FurniEditorCreateView: FC = props =>
- { /* Permissions */ } -
-
- Permissions -
-
+
+ Permissions +
{ [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( -
- { /* Interaction */ } -
-
- Interaction +
+ Interaction +
+
+ + +
+
+ + setField('interactionModesCount', Number(e.target.value)) } /> +
-
-
-
- - -
-
- - setField('interactionModesCount', Number(e.target.value)) } /> -
-
-
- - setField('customparams', e.target.value) } /> -
+
+ + setField('customparams', e.target.value) } />
- { /* Create Button */ } -
- -
+ + + ); }; diff --git a/src/components/furni-editor/views/FurniEditorEditView.tsx b/src/components/furni-editor/views/FurniEditorEditView.tsx index bb50765..dc648ef 100644 --- a/src/components/furni-editor/views/FurniEditorEditView.tsx +++ b/src/components/furni-editor/views/FurniEditorEditView.tsx @@ -1,7 +1,5 @@ import { FC, useCallback, useEffect, useState } from 'react'; -import { FaSave, FaSync, FaTrash, FaArrowLeft } from 'react-icons/fa'; -import { Column } from '../../../common'; -import { LayoutFurniIconImageView } from '../../../common/layout/LayoutFurniIconImageView'; +import { Button, Column, Flex, Text } from '../../../common'; import { CatalogRef, FurniDetail } from '../../../hooks/furni-editor'; interface FurniEditorEditViewProps @@ -11,24 +9,20 @@ interface FurniEditorEditViewProps furniDataEntry: Record | null; interactions: string[]; loading: boolean; - lastResult: { success: boolean; message: string; id: number } | null; - onUpdate: (id: number, fields: Record) => void; - onDelete: (id: number) => void; + onUpdate: (id: number, fields: Record) => Promise; + onDelete: (id: number) => Promise; onBack: () => void; onRefresh: (id: number) => void; } -const ic = 'text-[13px] border border-[#c5cdd6] rounded px-2 py-1 bg-white focus:outline-none focus:border-[#1e7295] focus:shadow-[0_0_0_1px_rgba(30,114,149,0.15)] transition-all w-full'; -const ro = 'text-[13px] border border-[#d5dbe0] rounded px-2 py-1 bg-[#f0f2f4] text-[#777] w-full cursor-not-allowed'; -const lb = 'text-[11px] text-[#1e7295] uppercase font-bold tracking-wider leading-none'; -const sectionTitle = 'text-[12px] text-[#1e7295] uppercase font-bold tracking-wider'; - export const FurniEditorEditView: FC = props => { - const { item, catalogItems, furniDataEntry, interactions, loading, lastResult, onUpdate, onDelete, onBack, onRefresh } = props; + const { item, catalogItems, furniDataEntry, interactions, loading, onUpdate, onDelete, onBack, onRefresh } = props; const [ form, setForm ] = useState({ + itemName: '', publicName: '', + spriteId: 0, type: 's', width: 1, length: 1, @@ -48,14 +42,15 @@ export const FurniEditorEditView: FC = props => }); const [ confirmDelete, setConfirmDelete ] = useState(false); - const [ toast, setToast ] = useState<{ type: 'success' | 'error'; message: string } | null>(null); useEffect(() => { if(!item) return; setForm({ + itemName: item.itemName || '', publicName: item.publicName || '', + spriteId: item.spriteId || 0, type: item.type || 's', width: item.width || 1, length: item.length || 1, @@ -77,119 +72,105 @@ export const FurniEditorEditView: FC = props => setConfirmDelete(false); }, [ item ]); - useEffect(() => - { - if(!lastResult) return; - - setToast({ type: lastResult.success ? 'success' : 'error', message: lastResult.message }); - if(lastResult.success && lastResult.id > 0) onRefresh(lastResult.id); - - const timer = setTimeout(() => setToast(null), 3000); - return () => clearTimeout(timer); - }, [ lastResult ]); - const setField = useCallback((key: string, value: unknown) => { setForm(prev => ({ ...prev, [key]: value })); }, []); - const handleSave = useCallback(() => + const handleSave = useCallback(async () => { - onUpdate(item.id, form); - }, [ item, form, onUpdate ]); + const ok = await onUpdate(item.id, form); - const handleDelete = useCallback(() => + if(ok) onRefresh(item.id); + }, [ item, form, onUpdate, onRefresh ]); + + const handleDelete = useCallback(async () => { if(!confirmDelete) return setConfirmDelete(true); - onDelete(item.id); - }, [ confirmDelete, item, onDelete ]); + + const ok = await onDelete(item.id); + + if(ok) onBack(); + }, [ confirmDelete, item, onDelete, onBack ]); + + const inputClass = 'form-control form-control-sm'; + const labelClass = 'text-[11px] font-bold text-[#333] mb-0'; return ( - - { toast && -
- { toast.message } -
- } - - { /* Header */ } -
-
- -
-
-
{ item.publicName }
-
- { navigator.clipboard.writeText(String(item.id)); setToast({ type: 'success', message: `ID ${item.id} copied!` }); } }>#{item.id} - | - sprite:{ item.spriteId } - | - { item.itemName } - - { item.type === 's' ? 'FLOOR' : 'WALL' } - - { item.usageCount > 0 && { item.usageCount } in use } -
-
-
- - -
-
+ + + + + + + + { item.id } + | + + + + { item.spriteId } + + ({ item.usageCount } in use) + { /* Basic Info */ } -
-
- - -
-
- - setField('publicName', e.target.value) } /> -
-
- - -
-
- - +
+ Basic Info +
+
+ + setField('itemName', e.target.value) } /> +
+
+ + setField('publicName', e.target.value) } /> +
+
+ + setField('spriteId', Number(e.target.value)) } /> +
+
+ + +
{ /* Dimensions */ } -
-
Dimensions
-
+
+ Dimensions +
- - setField('width', Number(e.target.value)) } /> + + setField('width', Number(e.target.value)) } />
- - setField('length', Number(e.target.value)) } /> + + setField('length', Number(e.target.value)) } />
- - setField('stackHeight', Number(e.target.value)) } /> + + setField('stackHeight', Number(e.target.value)) } />
{ /* Permissions */ } -
-
Permissions
-
+
+ Permissions +
{ [ 'allowStack', 'allowWalk', 'allowSit', 'allowLay', 'allowGift', 'allowTrade', 'allowRecycle', 'allowMarketplaceSell', 'allowInventoryStack' ].map(key => ( -
{ /* Interaction */ } -
-
Interaction
-
+
+ Interaction +
- - setField('interactionType', e.target.value) }> - { interactions.map(i => ) } + { interactions.map(i => ( + + )) }
- - setField('interactionModesCount', Number(e.target.value)) } /> -
-
- - setField('customparams', e.target.value) } /> + + setField('interactionModesCount', Number(e.target.value)) } />
+
+ + setField('customparams', e.target.value) } /> +
+ { /* Catalog References */ } + { catalogItems.length > 0 && +
+ Catalog ({ catalogItems.length }) +
+ { catalogItems.map(ci => ( +
+ { ci.catalogName } (page: { ci.pageName }) + { ci.costCredits }c + { ci.costPoints }p +
+ )) } +
+
+ } + + { /* FurniData.json Entry */ } + { furniDataEntry && +
+ FurniData.json +
+ { Object.entries(furniDataEntry).map(([ key, value ]) => ( +
+ { key } + { String(value ?? '') } +
+ )) } +
+
+ } + { /* Actions */ } -
- - + -
+ { confirmDelete ? 'Confirm Delete' : 'Delete' } + + ); }; diff --git a/src/components/furni-editor/views/FurniEditorSearchView.tsx b/src/components/furni-editor/views/FurniEditorSearchView.tsx index 1327b45..7b3f8c6 100644 --- a/src/components/furni-editor/views/FurniEditorSearchView.tsx +++ b/src/components/furni-editor/views/FurniEditorSearchView.tsx @@ -1,7 +1,5 @@ import { FC, useCallback, useEffect, useState } from 'react'; -import { FaSearch } from 'react-icons/fa'; -import { Column, Text } from '../../../common'; -import { LayoutFurniIconImageView } from '../../../common/layout/LayoutFurniIconImageView'; +import { Button, Column, Flex, Text } from '../../../common'; import { FurniItem } from '../../../hooks/furni-editor'; interface FurniEditorSearchViewProps @@ -14,8 +12,6 @@ interface FurniEditorSearchViewProps onSelect: (id: number) => void; } -const inputClass = 'text-[14px] border border-[#c5cdd6] rounded px-2 py-1.5 bg-white focus:outline-none focus:border-[#1e7295] transition-colors w-full'; - export const FurniEditorSearchView: FC = props => { const { items, total, page, loading, onSearch, onSelect } = props; @@ -41,122 +37,97 @@ export const FurniEditorSearchView: FC = props => return ( - { /* Search Bar */ } -
-
- + + + Search setQuery(e.target.value) } onKeyDown={ handleKeyDown } /> -
-
- - setTypeFilter(e.target.value) } + > - - + + -
- -
+
+ + - { /* Results counter */ } - { total > 0 && -
- { total } items found { totalPages > 1 && - Page { page }/{ totalPages } } -
- } - - { /* Results Table */ } -
- { loading && -
-
Loading...
-
- } - - { !loading && items.length === 0 && -
- No items found -
- } - - { !loading && items.length > 0 && - - - - - - - - - - + +
IDSpriteNamePublic NameTypeInteraction
+ + + + + + + + + + + + { items.map(item => ( + onSelect(item.id) } + > + + + + + + - - - { items.map(item => ( - onSelect(item.id) } - > - - - - - - - - - )) } - -
IDSpriteNamePublic NameTypeInteraction
{ item.id }{ item.spriteId }{ item.itemName }{ item.publicName } + + { item.type === 's' ? 'Floor' : 'Wall' } + + { item.interactionType || '-' }
-
- -
-
{ item.id }{ item.spriteId }{ item.itemName }{ item.publicName } - - { item.type === 's' ? 'Floor' : 'Wall' } - - { item.interactionType || '-' }
- } -
+ )) } + { items.length === 0 && !loading && + + No items found + + } + + + - { /* Pagination */ } { totalPages > 1 && -
-
- Page { page } of { totalPages } -
-
- - -
-
+ + + } ); diff --git a/src/components/inventory/views/prefix/InventoryPrefixView.tsx b/src/components/inventory/views/prefix/InventoryPrefixView.tsx index 4f3ea6d..d959546 100644 --- a/src/components/inventory/views/prefix/InventoryPrefixView.tsx +++ b/src/components/inventory/views/prefix/InventoryPrefixView.tsx @@ -1,8 +1,32 @@ import { FC, useEffect, useState } from 'react'; import { FaTrashAlt } from 'react-icons/fa'; -import { IPrefixItem, LocalizeText } from '../../../../api'; +import { IPrefixItem, LocalizeText, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api'; import { useInventoryPrefixes, useNotification } from '../../../../hooks'; -import { NitroButton, PrefixPreview } from '../../../../layout'; +import { NitroButton } from '../../../../layout'; + +const PrefixPreview: FC<{ text: string; color: string; icon: string; effect?: string; className?: string; textSize?: string }> = ({ text, color, icon, effect = '', className = '', textSize = 'text-sm' }) => +{ + const colors = parsePrefixColors(text, color); + const hasMultiColor = colors.length > 1 && new Set(colors).size > 1; + const fxStyle = getPrefixEffectStyle(effect, colors[0] || '#FFFFFF'); + + return ( + + { effect === 'pulse' && } + { icon && { icon } } + + {'{'} + { hasMultiColor + ? [ ...text ].map((char, i) => ( + { char } + )) + : text + } + {'}'} + + + ); +}; const PrefixItemView: FC<{ prefix: IPrefixItem; diff --git a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx index 48aac61..1dba1a9 100644 --- a/src/components/room/widgets/chat/ChatWidgetMessageView.tsx +++ b/src/components/room/widgets/chat/ChatWidgetMessageView.tsx @@ -1,8 +1,7 @@ import { GetRoomEngine, RoomChatSettings, RoomObjectCategory } from '@nitrots/nitro-renderer'; import { FC, useEffect, useMemo, useRef, useState } from 'react'; -import { ChatBubbleMessage } from '../../../../api'; +import { ChatBubbleMessage, parsePrefixColors, getPrefixEffectStyle, PREFIX_EFFECT_KEYFRAMES } from '../../../../api'; import { useOnClickChat } from '../../../../hooks'; -import { PrefixPreview } from '../../../../layout'; interface ChatWidgetMessageViewProps { @@ -91,8 +90,27 @@ export const ChatWidgetMessageView: FC = ({ ) }
- { chat.prefixText && - } + { chat.prefixEffect === 'pulse' && } + { chat.prefixText && (() => { + const colors = parsePrefixColors(chat.prefixText, chat.prefixColor); + const hasMultiColor = colors.length > 1 && new Set(colors).size > 1; + const fxStyle = getPrefixEffectStyle(chat.prefixEffect, colors[0] || '#FFFFFF'); + return ( + + { chat.prefixIcon && { chat.prefixIcon } } + + {'{'} + { hasMultiColor + ? [ ...chat.prefixText ].map((char, i) => ( + { char } + )) + : chat.prefixText + } + {'}'} + + + ); + })() }
diff --git a/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx b/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx index 531fa2b..048c20c 100644 --- a/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx +++ b/src/components/wired/views/triggers/WiredTriggerAvatarSaysSomethingView.tsx @@ -16,11 +16,10 @@ export const WiredTriggerAvatarSaysSomethingView: FC<{}> = () => const [ hideMessage, setHideMessage ] = useState(false); const [ ownerOnly, setOwnerOnly ] = useState(false); const { trigger = null, setStringParam = null, setIntParams = null } = useWired(); - const isAnyTextMode = (matchMode === MATCH_ALL); const save = () => { - setStringParam(isAnyTextMode ? '' : message); + setStringParam(message); setIntParams([ matchMode, hideMessage ? 1 : 0, @@ -40,11 +39,7 @@ export const WiredTriggerAvatarSaysSomethingView: FC<{}> = () =>
{ LocalizeText('wiredfurni.params.whatissaid') } - setMessage(event.target.value) } /> + setMessage(event.target.value) } />
diff --git a/src/css/index.css b/src/css/index.css index b48faa3..806e0e5 100644 --- a/src/css/index.css +++ b/src/css/index.css @@ -78,27 +78,6 @@ body { } @layer components { - @keyframes prefix-pulse { - 0%, 100% { opacity: 1; } - 50% { opacity: 0.5; } - } - - @keyframes prefix-rainbow { - 0% { filter: hue-rotate(0deg); } - 100% { filter: hue-rotate(360deg); } - } - - @keyframes prefix-shake { - 0%, 100% { transform: translateX(0); } - 25% { transform: translateX(-1px) rotate(-1deg); } - 75% { transform: translateX(1px) rotate(1deg); } - } - - @keyframes prefix-wave { - 0%, 100% { transform: translateY(0); } - 50% { transform: translateY(-3px); } - } - @keyframes blink { 0%, diff --git a/src/hooks/furni-editor/useFurniEditor.ts b/src/hooks/furni-editor/useFurniEditor.ts index 5d2c95a..e4258e5 100644 --- a/src/hooks/furni-editor/useFurniEditor.ts +++ b/src/hooks/furni-editor/useFurniEditor.ts @@ -1,7 +1,4 @@ -import { FurniEditorBySpriteComposer, FurniEditorCreateComposer, FurniEditorDeleteComposer, FurniEditorDetailComposer, FurniEditorDetailEvent as FurniEditorDetailMsgEvent, FurniEditorInteractionsComposer, FurniEditorInteractionsEvent as FurniEditorInteractionsMsgEvent, FurniEditorResultEvent as FurniEditorResultMsgEvent, FurniEditorSearchComposer, FurniEditorSearchEvent as FurniEditorSearchMsgEvent, FurniEditorUpdateComposer } from '@nitrots/nitro-renderer'; import { useCallback, useState } from 'react'; -import { SendMessageComposer } from '../../api'; -import { useMessageEvent } from '../events'; export interface FurniItem { @@ -49,6 +46,18 @@ export interface CatalogRef pageName: string; } +const API_BASE = '/api/admin/furni-editor'; + +async function apiFetch(url: string, options?: RequestInit): Promise +{ + const res = await fetch(url, { credentials: 'include', ...options }); + const data = await res.json(); + + if(!res.ok || data.error) throw new Error(data.error || 'API error'); + + return data; +} + export const useFurniEditor = () => { const [ items, setItems ] = useState([]); @@ -60,114 +69,171 @@ export const useFurniEditor = () => const [ catalogItems, setCatalogItems ] = useState([]); const [ interactions, setInteractions ] = useState([]); const [ furniDataEntry, setFurniDataEntry ] = useState | null>(null); - const [ lastResult, setLastResult ] = useState<{ success: boolean; message: string; id: number } | null>(null); const clearError = useCallback(() => setError(null), []); - // Listen for search results - useMessageEvent(FurniEditorSearchMsgEvent, (event: any) => + const searchItems = useCallback(async (query: string, type: string, pg: number) => { - const parser = event.getParser(); - - setItems(parser.items); - setTotal(parser.total); - setPage(parser.page); - setLoading(false); - }); - - // Listen for detail results - useMessageEvent(FurniEditorDetailMsgEvent, (event: any) => - { - const parser = event.getParser(); - - setSelectedItem(parser.item as FurniDetail); - setCatalogItems(parser.catalogItems as CatalogRef[]); + setLoading(true); + setError(null); try { - setFurniDataEntry(parser.furniDataJson ? JSON.parse(parser.furniDataJson) : null); + const params = new URLSearchParams({ q: query, limit: '20', page: String(pg) }); + + if(type) params.set('type', type); + + const data = await apiFetch<{ items: FurniItem[]; total: number; page: number }>(`${ API_BASE }?${ params }`); + + setItems(data.items); + setTotal(data.total); + setPage(data.page); } - catch + catch(e: any) { - setFurniDataEntry(null); + setError(e.message); } - - setLoading(false); - }); - - // Listen for interactions results - useMessageEvent(FurniEditorInteractionsMsgEvent, (event: any) => - { - const parser = event.getParser(); - - setInteractions(parser.interactions); - }); - - // Listen for operation results (update/create/delete) - useMessageEvent(FurniEditorResultMsgEvent, (event: any) => - { - const parser = event.getParser(); - - setLastResult({ success: parser.success, message: parser.message, id: parser.id }); - setLoading(false); - - if(!parser.success) + finally { - setError(parser.message); + setLoading(false); } - }); + }, []); - const searchItems = useCallback((query: string, type: string, pg: number) => + const loadDetail = useCallback(async (id: number): Promise => { setLoading(true); setError(null); - SendMessageComposer(new FurniEditorSearchComposer(query, type, pg)); + + try + { + const data = await apiFetch<{ item: FurniDetail; catalogItems: CatalogRef[]; furniDataEntry: Record | null }>(`${ API_BASE }/detail?id=${ id }`); + + setSelectedItem(data.item); + setCatalogItems(data.catalogItems); + setFurniDataEntry(data.furniDataEntry); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } }, []); - const loadDetail = useCallback((id: number) => + const updateItem = useCallback(async (id: number, fields: Record) => { setLoading(true); setError(null); - SendMessageComposer(new FurniEditorDetailComposer(id)); + + try + { + await apiFetch(`${ API_BASE }/update?id=${ id }`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields) + }); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } }, []); - const loadBySpriteId = useCallback((spriteId: number) => + const createItem = useCallback(async (fields: Record) => { setLoading(true); setError(null); - SendMessageComposer(new FurniEditorBySpriteComposer(spriteId)); + + try + { + const data = await apiFetch<{ id: number }>(`${ API_BASE }`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(fields) + }); + + return data.id; + } + catch(e: any) + { + setError(e.message); + + return null; + } + finally + { + setLoading(false); + } }, []); - const updateItem = useCallback((id: number, fields: Record) => + const deleteItem = useCallback(async (id: number) => { setLoading(true); setError(null); - SendMessageComposer(new FurniEditorUpdateComposer(id, JSON.stringify(fields))); + + try + { + await apiFetch(`${ API_BASE }/delete?id=${ id }`, { method: 'POST' }); + + return true; + } + catch(e: any) + { + setError(e.message); + + return false; + } + finally + { + setLoading(false); + } }, []); - const createItem = useCallback((fields: Record) => + const loadInteractions = useCallback(async () => { - setLoading(true); - setError(null); - SendMessageComposer(new FurniEditorCreateComposer(JSON.stringify(fields))); + try + { + const data = await apiFetch<{ interactions: Array }>(`${ API_BASE }/interactions`); + + setInteractions(data.interactions.map(i => typeof i === 'string' ? i : i.name)); + } + catch {} }, []); - const deleteItem = useCallback((id: number) => + const loadBySpriteId = useCallback(async (spriteId: number): Promise => { - setLoading(true); - setError(null); - SendMessageComposer(new FurniEditorDeleteComposer(id)); - }, []); + try + { + const data = await apiFetch<{ id: number }>(`${ API_BASE }/by-sprite?spriteId=${ spriteId }`); - const loadInteractions = useCallback(() => - { - SendMessageComposer(new FurniEditorInteractionsComposer()); - }, []); + return await loadDetail(data.id); + } + catch(e: any) + { + setError(e.message); + + return false; + } + }, [ loadDetail ]); return { items, total, page, loading, error, clearError, selectedItem, setSelectedItem, catalogItems, furniDataEntry, - interactions, lastResult, + interactions, searchItems, loadDetail, loadBySpriteId, updateItem, createItem, deleteItem, loadInteractions }; }; diff --git a/src/layout/PrefixPreview.tsx b/src/layout/PrefixPreview.tsx deleted file mode 100644 index 00779e8..0000000 --- a/src/layout/PrefixPreview.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import { FC, useMemo } from 'react'; -import { parsePrefixColors, getPrefixEffectStyle } from '../api'; - -interface PrefixPreviewProps -{ - text: string; - color: string; - icon?: string; - effect?: string; - className?: string; - textSize?: string; -} - -export const PrefixPreview: FC = ({ text, color, icon = '', effect = '', className = '', textSize = 'text-sm' }) => -{ - const colors = useMemo(() => parsePrefixColors(text, color), [ text, color ]); - const hasMultiColor = colors.length > 1 && new Set(colors).size > 1; - const fxStyle = useMemo(() => getPrefixEffectStyle(effect, colors[0] || '#FFFFFF'), [ effect, colors ]); - const isWave = effect === 'wave'; - - return ( - - { icon && { icon } } - - {'{'} - { (hasMultiColor || isWave) - ? [ ...text ].map((char, i) => - { - const charStyle: Record = { - color: colors[i] || colors[colors.length - 1], - ...getPrefixEffectStyle(effect, colors[i]) - }; - - if(isWave) - { - charStyle.display = 'inline-block'; - charStyle.animationDelay = `${ i * 0.08 }s`; - } - - return { char }; - }) - : text - } - {'}'} - - - ); -}; diff --git a/src/layout/index.ts b/src/layout/index.ts index 41a8969..a7041de 100644 --- a/src/layout/index.ts +++ b/src/layout/index.ts @@ -3,7 +3,6 @@ export * from './NitroButton'; export * from './NitroCard'; export * from './NitroInput'; export * from './NitroItemCountBadge'; -export * from './PrefixPreview'; export * from './classNames'; export * from './limited-edition'; export * from './styleNames';