🆙 Init V3

This commit is contained in:
DuckieTM
2026-01-31 09:10:52 +01:00
commit 7feb10ab15
1733 changed files with 53405 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import { CSSProperties, FC, useMemo } from 'react';
import { Grid, GridProps } from './Grid';
export interface AutoGridProps extends GridProps
{
columnMinWidth?: number;
columnMinHeight?: number;
}
export const AutoGrid: FC<AutoGridProps> = props =>
{
const { columnMinWidth = 40, columnMinHeight = 40, columnCount = 0, fullHeight = false, maxContent = true, overflow = 'auto', style = {}, ...rest } = props;
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
newStyle['--nitro-grid-column-min-height'] = (columnMinHeight + 'px');
if(columnCount > 1) newStyle.gridTemplateColumns = `repeat(auto-fill, minmax(${ columnMinWidth }px, 1fr))`;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ columnMinWidth, columnMinHeight, columnCount, style ]);
return <Grid columnCount={ columnCount } fullHeight={ fullHeight } overflow={ overflow } style={ getStyle } { ...rest } />;
};
+84
View File
@@ -0,0 +1,84 @@
import { CSSProperties, DetailedHTMLProps, FC, HTMLAttributes, MutableRefObject, ReactNode, useMemo } from 'react';
import { ColorVariantType, DisplayType, FloatType, OverflowType, PositionType } from './types';
export interface BaseProps<T = HTMLElement> extends DetailedHTMLProps<HTMLAttributes<T>, T>
{
innerRef?: MutableRefObject<T>;
display?: DisplayType;
fit?: boolean;
fitV?: boolean;
grow?: boolean;
shrink?: boolean;
fullWidth?: boolean;
fullHeight?: boolean;
overflow?: OverflowType;
position?: PositionType;
float?: FloatType;
pointer?: boolean;
visible?: boolean;
textColor?: ColorVariantType;
classNames?: string[];
children?: ReactNode;
}
export const Base: FC<BaseProps<HTMLDivElement>> = props =>
{
const { ref = null, innerRef = null, display = null, fit = false, fitV = false, grow = false, shrink = false, fullWidth = false, fullHeight = false, overflow = null, position = null, float = null, pointer = false, visible = null, textColor = null, classNames = [], className = '', style = {}, children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(display && display.length) newClassNames.push(display);
if(fit || fullWidth) newClassNames.push('w-full');
if(fit || fullHeight) newClassNames.push('h-full');
if(fitV) newClassNames.push('vw-full', 'vh-full');
if(grow) newClassNames.push('!flex-grow');
if(shrink) newClassNames.push('!flex-shrink-0');
if(overflow) newClassNames.push('overflow-' + overflow);
if(position) newClassNames.push(position);
if(float) newClassNames.push('float-' + float);
if(pointer) newClassNames.push('cursor-pointer');
if(visible !== null) newClassNames.push(visible ? 'visible' : 'invisible');
if(textColor) newClassNames.push('text-' + textColor);
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ display, fit, fitV, grow, shrink, fullWidth, fullHeight, overflow, position, float, pointer, visible, textColor, classNames ]);
const getClassName = useMemo(() =>
{
let newClassName = getClassNames.join(' ');
if(className.length) newClassName += (' ' + className);
return newClassName.trim();
}, [ getClassNames, className ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style ]);
return (
<div ref={ innerRef } className={ getClassName } style={ getStyle } { ...rest }>
{ children }
</div>
);
};
+71
View File
@@ -0,0 +1,71 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
import { ButtonSizeType, ColorVariantType } from './types';
export interface ButtonProps extends FlexProps
{
variant?: ColorVariantType;
size?: ButtonSizeType;
active?: boolean;
disabled?: boolean;
}
export const Button: FC<ButtonProps> = props =>
{
const { variant = 'primary', size = 'sm', active = false, disabled = false, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
// fucked up method i know (i dont have a clue what im doing because im a ninja)
const newClassNames: string[] = [ 'pointer-events-auto inline-block font-normal leading-normal text-[#fff] text-center no-underline align-middle cursor-pointer select-none border-[1px] border-[solid] border-[transparent] px-[.75rem] py-[.375rem] text-[.9rem] rounded-[.25rem] [transition:color_.15s_ease-in-out,_background-color_.15s_ease-in-out,_border-color_.15s_ease-in-out,_box-shadow_.15s_ease-in-out]' ];
if(variant)
{
if(variant == 'primary')
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
if(variant == 'success')
newClassNames.push('text-white bg-[#00800b] border-[#00800b] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#006d09] hover:border-[#006609]');
if(variant == 'danger')
newClassNames.push('text-white bg-[#a81a12] border-[#a81a12] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#8f160f] hover:border-[#86150e]');
if(variant == 'warning')
newClassNames.push('text-white bg-[#ffc107] border-[#ffc107] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-[#000] hover:bg-[#ffca2c] hover:border-[#ffc720]');
if(variant == 'black')
newClassNames.push('text-white bg-[#000] border-[#000] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#000] hover:border-[#000]');
if(variant == 'secondary')
newClassNames.push('text-white bg-[#185d79] border-[#185d79] [box-shadow:inset_0_2px_#ffffff26,_inset_0_-2px_#0000001a,_0_1px_#0000001a] hover:text-white hover:bg-[#144f67] hover:border-[#134a61]');
if(variant == 'dark')
newClassNames.push('text-white bg-dark [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#18181bfb] hover:border-[#161619fb]');
if(variant == 'gray')
newClassNames.push('text-white bg-[#1e7295] border-[#1e7295] [box-shadow:inset_0_2px_#ffffff26,inset_0_-2px_#0000001a,0_1px_#0000001a] hover:text-white hover:bg-[#1a617f] hover:border-[#185b77]');
}
if(size)
{
if(size == 'sm')
{
newClassNames.push('!px-[.5rem] !py-[.25rem] !text-[.7875rem] !rounded-[.2rem] !min-h-[28px]');
}
}
if(active) newClassNames.push('active');
if(disabled) newClassNames.push('pointer-events-none opacity-[.65] [box-shadow:none]');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ variant, size, active, disabled, classNames ]);
return <Flex center classNames={ getClassNames } { ...rest } />;
};
+22
View File
@@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
export interface ButtonGroupProps extends BaseProps<HTMLDivElement>
{
}
export const ButtonGroup: FC<ButtonGroupProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'btn-group' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Base classNames={ getClassNames } { ...rest } />;
}
+46
View File
@@ -0,0 +1,46 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
import { useGridContext } from './GridContext';
import { ColumnSizesType } from './types';
export interface ColumnProps extends FlexProps
{
size?: ColumnSizesType;
offset?: ColumnSizesType;
column?: boolean;
}
export const Column: FC<ColumnProps> = props =>
{
const { size = 0, offset = 0, column = true, gap = 2, classNames = [], ...rest } = props;
const { isCssGrid = false } = useGridContext();
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(size)
{
let colClassName = `col-span-${ size }`;
if(isCssGrid) colClassName = `${ colClassName }`;
newClassNames.push(colClassName);
}
if(offset)
{
let colClassName = `offset-${ offset }`;
if(isCssGrid) colClassName = `g-start-${ offset }`;
newClassNames.push(colClassName);
}
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ size, offset, isCssGrid, classNames ]);
return <Flex classNames={ getClassNames } column={ column } gap={ gap } { ...rest } />;
};
+50
View File
@@ -0,0 +1,50 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface FlexProps extends BaseProps<HTMLDivElement>
{
column?: boolean;
reverse?: boolean;
gap?: SpacingType;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
}
export const Flex: FC<FlexProps> = props =>
{
const { display = 'flex', column = undefined, reverse = false, gap = null, center = false, alignSelf = null, alignItems = null, justifyContent = null, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(column)
{
if(reverse) newClassNames.push('flex-col-span-reverse');
else newClassNames.push('flex-col');
}
else
{
if(reverse) newClassNames.push('flex-row-reverse');
}
if(gap) newClassNames.push('gap-' + gap);
if(alignSelf) newClassNames.push('self-' + alignSelf);
if(alignItems) newClassNames.push('items-' + alignItems);
if(justifyContent) newClassNames.push('justify-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('items-center', 'justify-center');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ column, reverse, gap, center, alignSelf, alignItems, justifyContent, classNames ]);
return <Base classNames={ getClassNames } display={ display } { ...rest } />;
};
+22
View File
@@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from './Flex';
export interface FormGroupProps extends FlexProps
{
}
export const FormGroup: FC<FormGroupProps> = props =>
{
const { classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'form-group' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Flex classNames={ getClassNames } { ...rest } />;
};
+64
View File
@@ -0,0 +1,64 @@
import { CSSProperties, FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { GridContextProvider } from './GridContext';
import { AlignItemType, AlignSelfType, JustifyContentType, SpacingType } from './types';
export interface GridProps extends BaseProps<HTMLDivElement>
{
inline?: boolean;
gap?: SpacingType;
maxContent?: boolean;
columnCount?: number;
center?: boolean;
alignSelf?: AlignSelfType;
alignItems?: AlignItemType;
justifyContent?: JustifyContentType;
}
export const Grid: FC<GridProps> = props =>
{
const { inline = false, gap = 2, maxContent = false, columnCount = 0, center = false, alignSelf = null, alignItems = null, justifyContent = null, fullHeight = true, classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(inline) newClassNames.push('inline-grid');
else newClassNames.push('grid grid-rows-[repeat(var(--bs-rows,_1),_1fr)] grid-cols-[repeat(var(--bs-columns,_12),_1fr)]');
if(gap) newClassNames.push('gap-' + gap);
else if(gap === 0) newClassNames.push('gap-0');
if(maxContent) newClassNames.push('[flex-basis:max-content]');
if(alignSelf) newClassNames.push('self-' + alignSelf);
if(alignItems) newClassNames.push('items-' + alignItems);
if(justifyContent) newClassNames.push('justify-' + justifyContent);
if(!alignItems && !justifyContent && center) newClassNames.push('items-center', 'justify-center');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ inline, gap, maxContent, alignSelf, alignItems, justifyContent, center, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(columnCount) newStyle['--bs-columns'] = columnCount.toString();
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ columnCount, style ]);
return (
<GridContextProvider value={ { isCssGrid: true } }>
<Base classNames={ getClassNames } fullHeight={ fullHeight } style={ getStyle } { ...rest } />
</GridContextProvider>
);
};
+17
View File
@@ -0,0 +1,17 @@
import { createContext, FC, ProviderProps, useContext } from 'react';
export interface IGridContext
{
isCssGrid: boolean;
}
const GridContext = createContext<IGridContext>({
isCssGrid: false
});
export const GridContextProvider: FC<ProviderProps<IGridContext>> = props =>
{
return <GridContext.Provider value={ props.value }>{ props.children }</GridContext.Provider>;
};
export const useGridContext = () => useContext(GridContext);
+38
View File
@@ -0,0 +1,38 @@
import { CSSProperties, FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { ColorVariantType } from './types';
export interface HorizontalRuleProps extends BaseProps<HTMLDivElement>
{
variant?: ColorVariantType;
height?: number;
}
export const HorizontalRule: FC<HorizontalRuleProps> = props =>
{
const { variant = 'black', height = 1, classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [];
if(variant) newClassNames.push('bg-' + variant);
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ variant, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = { display: 'list-item' };
if(height > 0) newStyle.height = height;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ height, style ]);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
};
+55
View File
@@ -0,0 +1,55 @@
import { useVirtualizer } from '@tanstack/react-virtual';
import { FC, ReactElement, useRef, useState } from 'react';
import { Base } from './Base';
interface InfiniteScrollProps<T = any>
{
rows: T[];
overscan?: number;
scrollToBottom?: boolean;
rowRender: (row: T) => ReactElement;
}
export const InfiniteScroll: FC<InfiniteScrollProps> = props =>
{
const { rows = [], overscan = 5, scrollToBottom = false, rowRender = null } = props;
const [ scrollIndex, setScrollIndex ] = useState<number>(rows.length - 1);
const parentRef = useRef<HTMLDivElement>(null);
const virtualizer = useVirtualizer({
count: rows.length,
overscan,
getScrollElement: () => parentRef.current,
estimateSize: () => 45,
});
const items = virtualizer.getVirtualItems();
return (
<Base fit innerRef={ parentRef } overflow="auto" position="relative">
<div
style={ {
height: virtualizer.getTotalSize(),
width: '100%',
position: 'relative'
} }>
<div
style={ {
position: 'absolute',
top: 0,
left: 0,
width: '100%',
transform: `translateY(${ items[0]?.start ?? 0 }px)`
} }>
{ items.map((virtualRow) => (
<div
key={ virtualRow.key }
ref={ virtualizer.measureElement }
data-index={ virtualRow.index }>
{ rowRender(rows[virtualRow.index]) }
</div>
)) }
</div>
</div>
</Base>
);
};
+54
View File
@@ -0,0 +1,54 @@
import { FC, PropsWithChildren, useEffect, useRef, useState } from 'react';
export const ReactPopover: FC<PropsWithChildren<{
content: JSX.Element;
trigger?: 'click' | 'hover';
}>> = props =>
{
const { content = null, trigger = null, children = null } = props;
const [ show, setShow ] = useState(false);
const wrapperRef = useRef(null);
const handleMouseOver = () => (trigger === 'hover') && setShow(true);
const handleMouseLeft = () => (trigger === 'hover') && setShow(false);
useEffect(() =>
{
if(!show) return;
const handleClickOutside = (event: MouseEvent) =>
{
if(wrapperRef.current && !wrapperRef.current.contains(event.target)) setShow(false);
};
document.addEventListener('mousedown', handleClickOutside);
return () =>
{
// Unbind the event listener on clean up
document.removeEventListener('mousedown', handleClickOutside);
};
}, [ show, wrapperRef ]);
return (
<div
ref={ wrapperRef }
className="relative flex justify-center w-fit h-fit"
onMouseEnter={ handleMouseOver }
onMouseLeave={ handleMouseLeft }>
<div
onClick={ () => setShow(!show) }
>
{ children }
</div>
<div
className="min-w-fit w-[200px] h-fit absolute bottom-[100%] z-50 transition-all"
hidden={ !show }>
<div className="rounded bg-white p-3 shadow-[10px_30px_150px_rgba(46,38,92,0.25)] mb-[10px]">
{ content }
</div>
</div>
</div>
);
};
+21
View File
@@ -0,0 +1,21 @@
import { FC } from 'react';
import ReactSlider, { ReactSliderProps } from 'react-slider';
import { Button } from './Button';
import { Flex } from './Flex';
import { FaAngleLeft, FaAngleRight } from 'react-icons/fa';
export interface SliderProps extends ReactSliderProps
{
disabledButton?: boolean;
}
export const Slider: FC<SliderProps> = props =>
{
const { disabledButton, max, min, value, onChange, ...rest } = props;
return <Flex fullWidth gap={ 1 }>
{ !disabledButton && <Button disabled={ min >= value } onClick={ () => onChange(min < value ? value - 1 : min, 0) }><FaAngleLeft /></Button> }
<ReactSlider className={ 'nitro-slider' } max={ max } min={ min } value={ value } onChange={ onChange } { ...rest } />
{ !disabledButton && <Button disabled={ max <= value } onClick={ () => onChange(max > value ? value + 1 : max, 0) }><FaAngleRight /></Button> }
</Flex>;
}
+79
View File
@@ -0,0 +1,79 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from './Base';
import { ColorVariantType, FontSizeType, FontWeightType, TextAlignType } from './types';
export interface TextProps extends BaseProps<HTMLDivElement> {
variant?: ColorVariantType;
fontWeight?: FontWeightType;
fontSize?: FontSizeType;
fontSizeCustom?: number;
align?: TextAlignType;
bold?: boolean;
underline?: boolean;
italics?: boolean;
truncate?: boolean;
center?: boolean;
textEnd?: boolean;
small?: boolean;
wrap?: boolean;
noWrap?: boolean;
textBreak?: boolean;
}
export const Text: FC<TextProps> = props => {
const {
variant = 'black',
fontWeight = null,
fontSize = 0,
fontSizeCustom,
align = null,
bold = false,
underline = false,
italics = false,
truncate = false,
center = false,
textEnd = false,
small = false,
wrap = false,
noWrap = false,
textBreak = false,
...rest
} = props;
const getClassNames = useMemo(() => {
const newClassNames: string[] = ['inline'];
if (variant) {
if (variant === 'primary') newClassNames.push('text-[#1e7295]');
if (variant == 'secondary') newClassNames.push('text-[#185d79]');
if (variant === 'black') newClassNames.push('text-[#000000]');
if (variant == 'dark') newClassNames.push('text-[#18181b]');
if (variant === 'gray') newClassNames.push('text-[#6b7280]');
if (variant === 'white') newClassNames.push('text-[#ffffff]');
if (variant == 'success') newClassNames.push('text-[#00800b]');
if (variant == 'danger') newClassNames.push('text-[#a81a12]');
if (variant == 'warning') newClassNames.push('text-[#ffc107]');
}
if (bold) newClassNames.push('font-bold');
if (fontWeight) newClassNames.push('font-' + fontWeight);
if (fontSize) newClassNames.push('fs-' + fontSize);
if (fontSizeCustom) newClassNames.push('fs-custom');
if (align) newClassNames.push('text-' + align);
if (underline) newClassNames.push('underline');
if (italics) newClassNames.push('italic');
if (truncate) newClassNames.push('text-truncate');
if (center) newClassNames.push('text-center');
if (textEnd) newClassNames.push('text-end');
if (small) newClassNames.push('text-sm');
if (wrap) newClassNames.push('text-wrap');
if (noWrap) newClassNames.push('text-nowrap');
if (textBreak) newClassNames.push('text-break');
return newClassNames;
}, [variant, fontWeight, fontSize, fontSizeCustom, align, bold, underline, italics, truncate, center, textEnd, small, wrap, noWrap, textBreak]);
const style = fontSizeCustom ? { '--font-size': `${fontSizeCustom}px` } as React.CSSProperties : undefined;
return <Base classNames={getClassNames} style={style} {...rest} />;
};
+19
View File
@@ -0,0 +1,19 @@
import { FC, useMemo } from 'react';
import { Column, ColumnProps } from '..';
export const NitroCardContentView: FC<ColumnProps> = props =>
{
const { overflow = 'auto', classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
// Theme Changer
const newClassNames: string[] = [ 'container-fluid', 'h-full p-[8px] overflow-auto', 'bg-light' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return <Column classNames={ getClassNames } overflow={ overflow } { ...rest } />;
};
+17
View File
@@ -0,0 +1,17 @@
import { createContext, FC, ProviderProps, useContext } from 'react';
interface INitroCardContext
{
theme: string;
}
const NitroCardContext = createContext<INitroCardContext>({
theme: null
});
export const NitroCardContextProvider: FC<ProviderProps<INitroCardContext>> = props =>
{
return <NitroCardContext.Provider value={ props.value }>{ props.children }</NitroCardContext.Provider>;
};
export const useNitroCardContext = () => useContext(NitroCardContext);
+41
View File
@@ -0,0 +1,41 @@
import { FC, MouseEvent } from 'react';
import { FaFlag } from 'react-icons/fa';
import { Base, Column, ColumnProps, Flex } from '..';
interface NitroCardHeaderViewProps extends ColumnProps
{
headerText: string;
isGalleryPhoto?: boolean;
noCloseButton?: boolean;
onReportPhoto?: (event: MouseEvent) => void;
onCloseClick: (event: MouseEvent) => void;
}
export const NitroCardHeaderView: FC<NitroCardHeaderViewProps> = props =>
{
const { headerText = null, isGalleryPhoto = false, noCloseButton = false, onReportPhoto = null, onCloseClick = null, justifyContent = 'center', alignItems = 'center', classNames = [], children = null, ...rest } = props;
const onMouseDown = (event: MouseEvent<HTMLDivElement>) =>
{
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
};
return (
<Column center className={ 'relative flex items-center justify-center flex-col drag-handler min-h-card-header max-h-card-header bg-card-header' } { ...rest }>
<Flex center fullWidth>
<span className="text-xl text-white drop-shadow-lg">{ headerText }</span>
{ isGalleryPhoto &&
<Base className="end-4 nitro-card-header-report-camera" position="absolute" onClick={ onReportPhoto }>
<FaFlag className="fa-icon" />
</Base>
}
<div className="absolute flex items-center justify-center cursor-pointer right-2 p-[2px] ubuntu-close-button" onClick={ onCloseClick } onMouseDownCapture={ onMouseDown }>
</div>
</Flex>
</Column>
);
};
+35
View File
@@ -0,0 +1,35 @@
import { FC, useMemo, useRef } from 'react';
import { Column, ColumnProps } from '..';
import { DraggableWindow, DraggableWindowPosition, DraggableWindowProps } from '../draggable-window';
import { NitroCardContextProvider } from './NitroCardContext';
export interface NitroCardViewProps extends DraggableWindowProps, ColumnProps
{
theme?: string;
}
export const NitroCardView: FC<NitroCardViewProps> = props =>
{
const { theme = 'primary', uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, overflow = 'hidden', position = 'relative', gap = 0, classNames = [], ...rest } = props;
const elementRef = useRef<HTMLDivElement>();
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'resize', 'rounded', 'shadow', ];
// Card Theme Changer
newClassNames.push('border-[1px] border-[#283F5D]');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<NitroCardContextProvider value={ { theme } }>
<DraggableWindow disableDrag={ disableDrag } handleSelector={ handleSelector } uniqueKey={ uniqueKey } windowPosition={ windowPosition }>
<Column classNames={ getClassNames } gap={ gap } innerRef={ elementRef } overflow={ overflow } position={ position } { ...rest } />
</DraggableWindow>
</NitroCardContextProvider>
);
};
@@ -0,0 +1,21 @@
import { createContext, Dispatch, FC, ProviderProps, SetStateAction, useContext } from 'react';
export interface INitroCardAccordionContext
{
closers: Function[];
setClosers: Dispatch<SetStateAction<Function[]>>;
closeAll: () => void;
}
const NitroCardAccordionContext = createContext<INitroCardAccordionContext>({
closers: null,
setClosers: null,
closeAll: null
});
export const NitroCardAccordionContextProvider: FC<ProviderProps<INitroCardAccordionContext>> = props =>
{
return <NitroCardAccordionContext.Provider { ...props } />;
};
export const useNitroCardAccordionContext = () => useContext(NitroCardAccordionContext);
@@ -0,0 +1,18 @@
import { FC } from 'react';
import { Flex, FlexProps } from '../..';
export interface NitroCardAccordionItemViewProps extends FlexProps
{
}
export const NitroCardAccordionItemView: FC<NitroCardAccordionItemViewProps> = props =>
{
const { alignItems = 'center', gap = 1, children = null, ...rest } = props;
return (
<Flex alignItems={ alignItems } gap={ gap } { ...rest }>
{ children }
</Flex>
);
};
@@ -0,0 +1,84 @@
import { FC, useCallback, useEffect, useMemo, useState } from 'react';
import { FaCaretDown, FaCaretUp } from 'react-icons/fa';
import { Column, ColumnProps, Flex, Text } from '../..';
import { useNitroCardAccordionContext } from './NitroCardAccordionContext';
export interface NitroCardAccordionSetViewProps extends ColumnProps
{
headerText: string;
isExpanded?: boolean;
}
export const NitroCardAccordionSetView: FC<NitroCardAccordionSetViewProps> = props =>
{
const { headerText = '', isExpanded = false, gap = 0, classNames = [], children = null, ...rest } = props;
const [ isOpen, setIsOpen ] = useState(false);
const { setClosers = null, closeAll = null } = useNitroCardAccordionContext();
const onClick = () =>
{
closeAll();
setIsOpen(prevValue => !prevValue);
};
const onClose = useCallback(() => setIsOpen(false), []);
const getClassNames = useMemo(() =>
{
const newClassNames = [ 'nitro-card-accordion-set' ];
if(isOpen) newClassNames.push('active');
if(classNames && classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ isOpen, classNames ]);
useEffect(() =>
{
setIsOpen(isExpanded);
}, [ isExpanded ]);
useEffect(() =>
{
const closeFunction = onClose;
setClosers(prevValue =>
{
const newClosers = [ ...prevValue ];
newClosers.push(closeFunction);
return newClosers;
});
return () =>
{
setClosers(prevValue =>
{
const newClosers = [ ...prevValue ];
const index = newClosers.indexOf(closeFunction);
if(index >= 0) newClosers.splice(index, 1);
return newClosers;
});
};
}, [ onClose, setClosers ]);
return (
<Column classNames={ getClassNames } gap={ gap } { ...rest }>
<Flex pointer className="nitro-card-accordion-set-header px-2 py-1" justifyContent="between" onClick={ onClick }>
<Text>{ headerText }</Text>
{ isOpen && <FaCaretUp className="fa-icon" /> }
{ !isOpen && <FaCaretDown className="fa-icon" /> }
</Flex>
{ isOpen &&
<Column fullHeight className="nitro-card-accordion-set-content" gap={ 0 } overflow="auto">
{ children }
</Column> }
</Column>
);
};
@@ -0,0 +1,25 @@
import { FC, useCallback, useState } from 'react';
import { Column, ColumnProps } from '../..';
import { NitroCardAccordionContextProvider } from './NitroCardAccordionContext';
interface NitroCardAccordionViewProps extends ColumnProps
{
}
export const NitroCardAccordionView: FC<NitroCardAccordionViewProps> = props =>
{
const { ...rest } = props;
const [ closers, setClosers ] = useState<Function[]>([]);
const closeAll = useCallback(() =>
{
for(const closer of closers) closer();
}, [ closers ]);
return (
<NitroCardAccordionContextProvider value={ { closers, setClosers, closeAll } }>
<Column gap={ 0 } { ...rest } />
</NitroCardAccordionContextProvider>
);
};
+4
View File
@@ -0,0 +1,4 @@
export * from './NitroCardAccordionContext';
export * from './NitroCardAccordionItemView';
export * from './NitroCardAccordionSetView';
export * from './NitroCardAccordionView';
+6
View File
@@ -0,0 +1,6 @@
export * from './NitroCardContentView';
export * from './NitroCardContext';
export * from './NitroCardHeaderView';
export * from './NitroCardView';
export * from './accordion';
export * from './tabs';
@@ -0,0 +1,36 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from '../../Flex';
import { LayoutItemCountView } from '../../layout';
interface NitroCardTabsItemViewProps extends FlexProps
{
isActive?: boolean;
count?: number;
}
export const NitroCardTabsItemView: FC<NitroCardTabsItemViewProps> = props =>
{
const { isActive = false, count = 0, overflow = 'hidden', position = 'relative', pointer = true, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'overflow-hidden relative cursor-pointer rounded-t-md flex bg-card-tab-item px-3 py-1 z-[1] border-card-border border-t border-l border-r before:absolute before:w-[93%] before:h-[3px] before:rounded-md before:top-[1.5px] before:left-0 before:right-0 before:m-auto before:z-[1] before:bg-[#C2C9D1]',
isActive && 'bg-card-tab-item-active -mb-[1px] before:bg-white' ];
//if (isActive) newClassNames.push('bg-[#dfdfdf] border-b-[1px_solid_black]');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ isActive, classNames ]);
return (
<Flex classNames={ getClassNames } overflow={ overflow } pointer={ pointer } position={ position } { ...rest }>
<Flex center shrink>
{ children }
</Flex>
{ (count > 0) &&
<LayoutItemCountView count={ count } /> }
</Flex>
);
};
@@ -0,0 +1,22 @@
import { FC, useMemo } from 'react';
import { Flex, FlexProps } from '../..';
export const NitroCardTabsView: FC<FlexProps> = props =>
{
const { justifyContent = 'center', gap = 1, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'justify-center gap-0.5 flex bg-card-tabs min-h-card-tabs max-h-card-tabs pt-1 border-b border-card-border px-2 -mt-[1px]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Flex classNames={ getClassNames } gap={ gap } justifyContent={ justifyContent } { ...rest }>
{ children }
</Flex>
);
};
+2
View File
@@ -0,0 +1,2 @@
export * from './NitroCardTabsItemView';
export * from './NitroCardTabsView';
@@ -0,0 +1,245 @@
import { MouseEventType, TouchEventType } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, Key, MouseEvent as ReactMouseEvent, ReactNode, TouchEvent as ReactTouchEvent, useCallback, useEffect, useRef, useState } from 'react';
import { createPortal } from 'react-dom';
import { GetLocalStorage, SetLocalStorage, WindowSaveOptions } from '../../api';
import { DraggableWindowPosition } from './DraggableWindowPosition';
const CURRENT_WINDOWS: HTMLElement[] = [];
const POS_MEMORY: Map<Key, { x: number, y: number }> = new Map();
const BOUNDS_THRESHOLD_TOP: number = 0;
const BOUNDS_THRESHOLD_LEFT: number = 0;
export interface DraggableWindowProps {
uniqueKey?: Key;
handleSelector?: string;
windowPosition?: string;
disableDrag?: boolean;
dragStyle?: CSSProperties;
offsetLeft?: number;
offsetTop?: number;
children?: ReactNode;
}
export const DraggableWindow: FC<DraggableWindowProps> = props => {
const { uniqueKey = null, handleSelector = '.drag-handler', windowPosition = DraggableWindowPosition.CENTER, disableDrag = false, dragStyle = {}, children = null, offsetLeft = 0, offsetTop = 0 } = props;
const [delta, setDelta] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
const [offset, setOffset] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
const [start, setStart] = useState<{ x: number, y: number }>({ x: 0, y: 0 });
const [isDragging, setIsDragging] = useState(false);
const [isPositioned, setIsPositioned] = useState(false); // New state to control visibility
const [dragHandler, setDragHandler] = useState<HTMLElement>(null);
const elementRef = useRef<HTMLDivElement>();
const bringToTop = useCallback(() => {
let zIndex = 400;
for (const existingWindow of CURRENT_WINDOWS) {
zIndex += 1;
existingWindow.style.zIndex = zIndex.toString();
}
}, []);
const moveCurrentWindow = useCallback(() => {
const index = CURRENT_WINDOWS.indexOf(elementRef.current);
if (index === -1) {
CURRENT_WINDOWS.push(elementRef.current);
} else if (index === (CURRENT_WINDOWS.length - 1)) return;
else if (index >= 0) {
CURRENT_WINDOWS.splice(index, 1);
CURRENT_WINDOWS.push(elementRef.current);
}
bringToTop();
}, [bringToTop]);
const onMouseDown = useCallback((event: ReactMouseEvent<HTMLDivElement>) => {
moveCurrentWindow();
}, [moveCurrentWindow]);
const onTouchStart = useCallback((event: ReactTouchEvent<HTMLDivElement>) => {
moveCurrentWindow();
}, [moveCurrentWindow]);
const startDragging = useCallback((startX: number, startY: number) => {
setStart({ x: startX, y: startY });
setIsDragging(true);
}, []);
const onDragMouseDown = useCallback((event: MouseEvent) => {
startDragging(event.clientX, event.clientY);
}, [startDragging]);
const onTouchDown = useCallback((event: TouchEvent) => {
const touch = event.touches[0];
startDragging(touch.clientX, touch.clientY);
}, [startDragging]);
const clampPosition = useCallback((newX: number, newY: number) => {
if (!elementRef.current) return { x: newX, y: newY };
const windowWidth = elementRef.current.offsetWidth;
const windowHeight = elementRef.current.offsetHeight;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const clampedX = Math.max(BOUNDS_THRESHOLD_LEFT, Math.min(newX, viewportWidth - windowWidth));
const clampedY = Math.max(BOUNDS_THRESHOLD_TOP, Math.min(newY, viewportHeight - windowHeight));
return { x: clampedX, y: clampedY };
}, []);
const onDragMouseMove = useCallback((event: MouseEvent) => {
if (!elementRef.current || !isDragging) return;
const newDeltaX = event.clientX - start.x;
const newDeltaY = event.clientY - start.y;
const newOffsetX = offset.x + newDeltaX;
const newOffsetY = offset.y + newDeltaY;
const clampedPos = clampPosition(newOffsetX, newOffsetY);
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
}, [start, offset, clampPosition, isDragging]);
const onDragTouchMove = useCallback((event: TouchEvent) => {
if (!elementRef.current || !isDragging) return;
const touch = event.touches[0];
const newDeltaX = touch.clientX - start.x;
const newDeltaY = touch.clientY - start.y;
const newOffsetX = offset.x + newDeltaX;
const newOffsetY = offset.y + newDeltaY;
const clampedPos = clampPosition(newOffsetX, newOffsetY);
setDelta({ x: clampedPos.x - offset.x, y: clampedPos.y - offset.y });
}, [start, offset, clampPosition, isDragging]);
const completeDrag = useCallback(() => {
if (!elementRef.current || !dragHandler || !isDragging) return;
const finalOffsetX = offset.x + delta.x;
const finalOffsetY = offset.y + delta.y;
const clampedPos = clampPosition(finalOffsetX, finalOffsetY);
setDelta({ x: 0, y: 0 });
setOffset({ x: clampedPos.x, y: clampedPos.y });
setIsDragging(false);
if (uniqueKey !== null) {
const newStorage = { ...GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`) } as WindowSaveOptions;
newStorage.offset = { x: clampedPos.x, y: clampedPos.y };
SetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`, newStorage);
}
}, [dragHandler, delta, offset, uniqueKey, clampPosition, isDragging]);
const onDragMouseUp = useCallback((event: MouseEvent) => {
completeDrag();
}, [completeDrag]);
const onDragTouchUp = useCallback((event: TouchEvent) => {
completeDrag();
}, [completeDrag]);
useEffect(() => {
const element = elementRef.current as HTMLElement;
if (!element) return;
CURRENT_WINDOWS.push(element);
bringToTop();
if (!disableDrag) {
const handle = element.querySelector(handleSelector);
if (handle) setDragHandler(handle as HTMLElement);
}
const windowWidth = element.offsetWidth || 340;
const windowHeight = element.offsetHeight || 462;
let offsetX = 0;
let offsetY = 0;
switch (windowPosition) {
case DraggableWindowPosition.TOP_CENTER:
offsetY = 50 + offsetTop;
offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
break;
case DraggableWindowPosition.CENTER:
offsetY = (window.innerHeight - windowHeight) / 2 + offsetTop;
offsetX = (window.innerWidth - windowWidth) / 2 + offsetLeft;
break;
case DraggableWindowPosition.TOP_LEFT:
offsetY = 50 + offsetTop;
offsetX = 50 + offsetLeft;
break;
}
const clampedPos = clampPosition(offsetX, offsetY);
element.style.left = '0px';
element.style.top = '0px';
setOffset({ x: clampedPos.x, y: clampedPos.y });
setDelta({ x: 0, y: 0 });
setIsPositioned(true); // Mark as positioned after setting initial offset
return () => {
const index = CURRENT_WINDOWS.indexOf(element);
if (index >= 0) CURRENT_WINDOWS.splice(index, 1);
};
}, [handleSelector, windowPosition, uniqueKey, disableDrag, offsetLeft, offsetTop, bringToTop]);
useEffect(() => {
const element = elementRef.current as HTMLElement;
if (!element || !isPositioned) return;
element.style.transform = `translate(${offset.x + delta.x}px, ${offset.y + delta.y}px)`;
element.style.visibility = 'visible';
}, [offset, delta, isPositioned]);
useEffect(() => {
if (!dragHandler) return;
dragHandler.addEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
dragHandler.addEventListener(TouchEventType.TOUCH_START, onTouchDown);
return () => {
dragHandler.removeEventListener(MouseEventType.MOUSE_DOWN, onDragMouseDown);
dragHandler.removeEventListener(TouchEventType.TOUCH_START, onTouchDown);
};
}, [dragHandler, onDragMouseDown, onTouchDown]);
useEffect(() => {
if (!isDragging) return;
document.addEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
document.addEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
document.addEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
document.addEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
return () => {
document.removeEventListener(MouseEventType.MOUSE_UP, onDragMouseUp);
document.removeEventListener(TouchEventType.TOUCH_END, onDragTouchUp);
document.removeEventListener(MouseEventType.MOUSE_MOVE, onDragMouseMove);
document.removeEventListener(TouchEventType.TOUCH_MOVE, onDragTouchMove);
};
}, [isDragging, onDragMouseUp, onDragMouseMove, onDragTouchUp, onDragTouchMove]);
useEffect(() => {
if (!uniqueKey) return;
const localStorage = GetLocalStorage<WindowSaveOptions>(`nitro.windows.${uniqueKey}`);
if (!localStorage || !localStorage.offset) return;
const clampedPos = clampPosition(localStorage.offset.x, localStorage.offset.y);
setDelta({ x: 0, y: 0 });
setOffset({ x: clampedPos.x, y: clampedPos.y });
setIsPositioned(true); // Ensure positioned when loading from storage
}, [uniqueKey, clampPosition]);
return createPortal(
<div
ref={elementRef}
className="absolute draggable-window"
style={{ ...dragStyle, visibility: isPositioned ? 'visible' : 'hidden' }} // Hide until positioned
onMouseDownCapture={onMouseDown}
onTouchStartCapture={onTouchStart}
>
{children}
</div>,
document.getElementById('draggable-windows-container')
);
};
@@ -0,0 +1,7 @@
export class DraggableWindowPosition
{
public static CENTER: string = 'DWP_CENTER';
public static TOP_CENTER: string = 'DWP_TOP_CENTER';
public static TOP_LEFT: string = 'DWP_TOP_LEFT';
public static NOTHING: string = 'DWP_NOTHING';
}
+2
View File
@@ -0,0 +1,2 @@
export * from './DraggableWindow';
export * from './DraggableWindowPosition';
+22
View File
@@ -0,0 +1,22 @@
export * from './AutoGrid';
export * from './Base';
export * from './Button';
export * from './ButtonGroup';
export * from './Column';
export * from './Flex';
export * from './FormGroup';
export * from './Grid';
export * from './GridContext';
export * from './HorizontalRule';
export * from './InfiniteScroll';
export * from './Text';
export * from './card';
export * from './card/accordion';
export * from './card/tabs';
export * from './draggable-window';
export * from './layout';
export * from './layout/limited-edition';
export * from './types';
export * from "./Slider";
export * from './utils';
+103
View File
@@ -0,0 +1,103 @@
import { AvatarScaleType, AvatarSetType, GetAvatarRenderManager } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import { Base, BaseProps } from '../Base';
const AVATAR_IMAGE_CACHE: Map<string, string> = new Map();
export interface LayoutAvatarImageViewProps extends BaseProps<HTMLDivElement>
{
figure: string;
gender?: string;
headOnly?: boolean;
direction?: number;
scale?: number;
}
export const LayoutAvatarImageView: FC<LayoutAvatarImageViewProps> = props =>
{
const { figure = '', gender = 'M', headOnly = false, direction = 0, scale = 1, classNames = [], style = {}, ...rest } = props;
const [ avatarUrl, setAvatarUrl ] = useState<string>(null);
const [ isReady, setIsReady ] = useState<boolean>(false);
const isDisposed = useRef(false);
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'avatar-image relative w-[90px] h-[130px] bg-no-repeat bg-[center_-8px] pointer-events-none' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(avatarUrl && avatarUrl.length) newStyle.backgroundImage = `url('${ avatarUrl }')`;
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ avatarUrl, scale, style ]);
useEffect(() =>
{
if(!isReady) return;
const figureKey = [ figure, gender, direction, headOnly ].join('-');
if(AVATAR_IMAGE_CACHE.has(figureKey))
{
setAvatarUrl(AVATAR_IMAGE_CACHE.get(figureKey));
}
else
{
const resetFigure = (_figure: string) =>
{
if(isDisposed.current) return;
const avatarImage = GetAvatarRenderManager().createAvatarImage(_figure, AvatarScaleType.LARGE, gender, { resetFigure: (figure: string) => resetFigure(figure), dispose: null, disposed: false });
let setType = AvatarSetType.FULL;
if(headOnly) setType = AvatarSetType.HEAD;
avatarImage.setDirection(setType, direction);
const imageUrl = avatarImage.processAsImageUrl(setType);
if(imageUrl && !isDisposed.current)
{
if(!avatarImage.isPlaceholder()) AVATAR_IMAGE_CACHE.set(figureKey, imageUrl);
setAvatarUrl(imageUrl);
}
avatarImage.dispose();
};
resetFigure(figure);
}
}, [ figure, gender, direction, headOnly, isReady ]);
useEffect(() =>
{
isDisposed.current = false;
setIsReady(true);
return () =>
{
isDisposed.current = true;
};
}, []);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,23 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
export interface LayoutBackgroundImageProps extends BaseProps<HTMLDivElement>
{
imageUrl?: string;
}
export const LayoutBackgroundImage: FC<LayoutBackgroundImageProps> = props =>
{
const { imageUrl = null, fit = true, style = null, ...rest } = props;
const getStyle = useMemo(() =>
{
const newStyle = { ...style };
if(imageUrl) newStyle.background = `url(${ imageUrl }) center no-repeat`;
return newStyle;
}, [ style, imageUrl ]);
return <Base fit={ fit } style={ getStyle } { ...rest } />;
};
+109
View File
@@ -0,0 +1,109 @@
import { BadgeImageReadyEvent, GetEventDispatcher, GetSessionDataManager, NitroSprite, TextureUtils } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { GetConfigurationValue, LocalizeBadgeDescription, LocalizeBadgeName, LocalizeText } from '../../api';
import { Base, BaseProps } from '../Base';
export interface LayoutBadgeImageViewProps extends BaseProps<HTMLDivElement>
{
badgeCode: string;
isGroup?: boolean;
showInfo?: boolean;
customTitle?: string;
isGrayscale?: boolean;
scale?: number;
}
export const LayoutBadgeImageView: FC<LayoutBadgeImageViewProps> = props =>
{
const { badgeCode = null, isGroup = false, showInfo = false, customTitle = null, isGrayscale = false, scale = 1, classNames = [], style = {}, children = null, ...rest } = props;
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative w-[40px] h-[40px] bg-no-repeat bg-center' ];
if(isGroup) newClassNames.push('group-badge');
if(isGrayscale) newClassNames.push('grayscale');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames, isGroup, isGrayscale ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(imageElement)
{
newStyle.backgroundImage = `url(${ (isGroup) ? imageElement.src : GetConfigurationValue<string>('badge.asset.url').replace('%badgename%', badgeCode.toString()) })`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
newStyle.width = (imageElement.width * scale);
newStyle.height = (imageElement.height * scale);
}
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ badgeCode, isGroup, imageElement, scale, style ]);
useEffect(() =>
{
if(!badgeCode || !badgeCode.length) return;
let didSetBadge = false;
const onBadgeImageReadyEvent = async (event: BadgeImageReadyEvent) =>
{
if(event.badgeId !== badgeCode) return;
const element = await TextureUtils.generateImage(new NitroSprite(event.image));
console.log ('boe');
element.onload = () => setImageElement(element);
didSetBadge = true;
GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
};
GetEventDispatcher().addEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
const texture = isGroup ? GetSessionDataManager().getGroupBadgeImage(badgeCode) : GetSessionDataManager().getBadgeImage(badgeCode);
if(texture && !didSetBadge)
{
(async () =>
{
const element = await TextureUtils.generateImage(new NitroSprite(texture));
element.onload = () => setImageElement(element);
})();
}
return () => GetEventDispatcher().removeEventListener(BadgeImageReadyEvent.IMAGE_READY, onBadgeImageReadyEvent);
}, [ badgeCode, isGroup ]);
return (
<Base className="group" classNames={ getClassNames } style={ getStyle } { ...rest }>
{ (showInfo && GetConfigurationValue<boolean>('badge.descriptions.enabled', true)) &&
<Base className="hidden group-hover:block before:absolute before:content-['_'] before:w-[0] before:h-[0] before:!border-l-[10px] before:!border-b-[10px] before:!border-t-[10px] before:top-[10px] before:-right-[10px] before:[border-left-color:white] before:[border-top-color:transparent] before:[border-bottom-color:transparent] z-50 absolute pointer-events-none select-none w-[210px] rounded-[.25rem] bg-[#fff] -left-[220px] text-black py-1 px-2 small">
<div className="font-bold mb-1">{ isGroup ? customTitle : LocalizeBadgeName(badgeCode) }</div>
<div>{ isGroup ? LocalizeText('group.badgepopup.body') : LocalizeBadgeDescription(badgeCode) }</div>
</Base> }
{ children }
</Base>
);
};
@@ -0,0 +1,42 @@
import { FC, useMemo } from 'react';
import { LocalizeText } from '../../api';
import { Base, BaseProps } from '../Base';
interface LayoutCounterTimeViewProps extends BaseProps<HTMLDivElement>
{
day: string;
hour: string;
minutes: string;
seconds: string;
}
export const LayoutCounterTimeView: FC<LayoutCounterTimeViewProps> = props =>
{
const { day = '00', hour = '00', minutes = '00', seconds = '00', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-counter-time' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<div className="flex gap-1 top-2 end-2">
<Base classNames={ getClassNames } { ...rest }>
<div>{ day != '00' ? day : hour }{ day != '00' ? LocalizeText('countdown_clock_unit_days') : LocalizeText('countdown_clock_unit_hours') }</div>
</Base>
<div style={ { marginTop: '3px' } }>:</div>
<Base className="nitro-counter-time" { ...rest }>
<div>{ minutes }{ LocalizeText('countdown_clock_unit_minutes') }</div>
</Base>
<Base style={ { marginTop: '3px' } }>:</Base>
<Base className="nitro-counter-time" { ...rest }>
<div>{ seconds }{ LocalizeText('countdown_clock_unit_seconds') }</div>
</Base>
{ children }
</div>
);
};
+44
View File
@@ -0,0 +1,44 @@
import { CSSProperties, FC, useMemo } from 'react';
import { GetConfigurationValue } from '../../api';
import { Base, BaseProps } from '../Base';
export interface CurrencyIconProps extends BaseProps<HTMLDivElement>
{
type: number | string;
}
export const LayoutCurrencyIcon: FC<CurrencyIconProps> = props =>
{
const { type = '', classNames = [], style = {}, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-currency-icon', 'bg-center bg-no-repeat w-[15px] h-[15px]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const urlString = useMemo(() =>
{
let url = GetConfigurationValue<string>('currency.asset.icon.url', '');
url = url.replace('%type%', type.toString());
return `url(${ url })`;
}, [ type ]);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
newStyle.backgroundImage = urlString;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, urlString ]);
return <Base classNames={ getClassNames } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,17 @@
import { FC } from 'react';
import { GetImageIconUrlForProduct } from '../../api';
import { LayoutImage, LayoutImageProps } from './LayoutImage';
interface LayoutFurniIconImageViewProps extends LayoutImageProps
{
productType: string;
productClassId: number;
extraData?: string;
}
export const LayoutFurniIconImageView: FC<LayoutFurniIconImageViewProps> = props =>
{
const { productType = 's', productClassId = -1, extraData = '', ...rest } = props;
return <LayoutImage className="furni-image" imageUrl={ GetImageIconUrlForProduct(productType, productClassId, extraData) } { ...rest } />;
};
@@ -0,0 +1,70 @@
import { GetRoomEngine, IGetImageListener, ImageResult, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { ProductTypeEnum } from '../../api';
import { Base, BaseProps } from '../Base';
interface LayoutFurniImageViewProps extends BaseProps<HTMLDivElement>
{
productType: string;
productClassId: number;
direction?: number;
extraData?: string;
scale?: number;
}
export const LayoutFurniImageView: FC<LayoutFurniImageViewProps> = props =>
{
const { productType = 's', productClassId = -1, direction = 2, extraData = '', scale = 1, style = {}, ...rest } = props;
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(imageElement?.src?.length)
{
newStyle.backgroundImage = `url('${ imageElement.src }')`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
}
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ imageElement, scale, style ]);
useEffect(() =>
{
let imageResult: ImageResult = null;
const listener: IGetImageListener = {
imageReady: async (id, texture, image) => setImageElement(await TextureUtils.generateImage(texture)),
imageFailed: null
};
switch(productType.toLocaleLowerCase())
{
case ProductTypeEnum.FLOOR:
imageResult = GetRoomEngine().getFurnitureFloorImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
break;
case ProductTypeEnum.WALL:
imageResult = GetRoomEngine().getFurnitureWallImage(productClassId, new Vector3d(direction), 64, listener, 0, extraData);
break;
}
if(!imageResult) return;
(async () => setImageElement(await TextureUtils.generateImage(imageResult.data)))();
}, [ productType, productClassId, direction, extraData ]);
if(!imageElement) return null;
return <Base classNames={ [ 'furni-image' ] } style={ getStyle } { ...rest } />;
};
+41
View File
@@ -0,0 +1,41 @@
import { FC } from 'react';
import { LocalizeText } from '../../api';
import { Column } from '../Column';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { LayoutAvatarImageView } from './LayoutAvatarImageView';
interface LayoutGiftTagViewProps
{
figure?: string;
userName?: string;
message?: string;
editable?: boolean;
onChange?: (value: string) => void;
}
export const LayoutGiftTagView: FC<LayoutGiftTagViewProps> = props =>
{
const { figure = null, userName = null, message = null, editable = false, onChange = null } = props;
return (
<Flex className="nitro-gift-card text-black" overflow="hidden">
<div className="flex items-center justify-center gift-face flex-shrink-0">
{ !userName && <div className="gift-incognito"></div> }
{ figure && <div className="gift-avatar">
<LayoutAvatarImageView direction={ 2 } figure={ figure } headOnly={ true } />
</div> }
</div>
<Flex className="w-full pt-4 pb-4 pe-4 ps-3" overflow="hidden">
<Column className="!flex-grow" justifyContent="between" overflow="auto">
{ !editable &&
<Text textBreak className="gift-message">{ message }</Text> }
{ editable && (onChange !== null) &&
<textarea className="gift-message h-full" maxLength={ 140 } placeholder={ LocalizeText('catalog.gift_wrapping_new.message_hint') } value={ message } onChange={ (e) => onChange(e.target.value) }></textarea> }
{ userName &&
<Text italics textEnd className="pe-1">{ LocalizeText('catalog.gift_wrapping_new.message_from', [ 'name' ], [ userName ]) }</Text> }
</Column>
</Flex>
</Flex>
);
};
+76
View File
@@ -0,0 +1,76 @@
import { FC, useMemo } from 'react';
import { Base } from '../Base';
import { Column, ColumnProps } from '../Column';
import { LayoutItemCountView } from './LayoutItemCountView';
import { LayoutLimitedEditionStyledNumberView } from './limited-edition';
export interface LayoutGridItemProps extends ColumnProps
{
itemImage?: string;
itemColor?: string;
itemActive?: boolean;
itemCount?: number;
itemCountMinimum?: number;
itemUniqueSoldout?: boolean;
itemUniqueNumber?: number;
itemUnseen?: boolean;
itemHighlight?: boolean;
disabled?: boolean;
}
export const LayoutGridItem: FC<LayoutGridItemProps> = props =>
{
const { itemImage = undefined, itemColor = undefined, itemActive = false, itemCount = 1, itemCountMinimum = 1, itemUniqueSoldout = false, itemUniqueNumber = -2, itemUnseen = false, itemHighlight = false, disabled = false, center = true, column = true, style = {}, classNames = [], position = 'relative', overflow = 'hidden', children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'layout-grid-item', 'border', 'border-2', 'border-muted', 'rounded' ];
if(itemActive) newClassNames.push('!bg-[#ececec] !border-[#fff]');
if(itemUniqueSoldout || (itemUniqueNumber > 0)) newClassNames.push('unique-item');
if(itemUniqueSoldout) newClassNames.push('sold-out');
if(itemUnseen) newClassNames.push('unseen');
if(itemHighlight) newClassNames.push('has-highlight');
if(disabled) newClassNames.push('disabled');
if(itemImage === null) newClassNames.push('icon', 'loading-icon');
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ itemActive, itemUniqueSoldout, itemUniqueNumber, itemUnseen, itemHighlight, disabled, itemImage, classNames ]);
const getStyle = useMemo(() =>
{
let newStyle = { ...style };
if(itemImage && !(itemUniqueSoldout || (itemUniqueNumber > 0))) newStyle.backgroundImage = `url(${ itemImage })`;
if(itemColor) newStyle.backgroundColor = itemColor;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ style, itemImage, itemColor, itemUniqueSoldout, itemUniqueNumber ]);
return (
<Column pointer center={ center } classNames={ getClassNames } column={ column } overflow={ overflow } position={ position } style={ getStyle } { ...rest }>
{ (itemCount > itemCountMinimum) &&
<LayoutItemCountView count={ itemCount } /> }
{ (itemUniqueNumber > 0) &&
<>
<Base fit className="unique-bg-override" style={ { backgroundImage: `url(${ itemImage })` } } />
<div className="absolute bottom-0 unique-item-counter">
<LayoutLimitedEditionStyledNumberView value={ itemUniqueNumber } />
</div>
</> }
{ children }
</Column>
);
};
+13
View File
@@ -0,0 +1,13 @@
import { DetailedHTMLProps, FC, HTMLAttributes } from 'react';
export interface LayoutImageProps extends DetailedHTMLProps<HTMLAttributes<HTMLImageElement>, HTMLImageElement>
{
imageUrl?: string;
}
export const LayoutImage: FC<LayoutImageProps> = props =>
{
const { imageUrl = null, className = '', ...rest } = props;
return <img alt="" className={ 'no-select ' + className } src={ imageUrl } { ...rest } />;
};
+28
View File
@@ -0,0 +1,28 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutItemCountViewProps extends BaseProps<HTMLDivElement>
{
count: number;
}
export const LayoutItemCountView: FC<LayoutItemCountViewProps> = props =>
{
const { count = 0, position = 'absolute', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'inline-block px-[.65em] py-[.35em] text-[.75em] font-bold leading-none text-[#fff] text-center whitespace-nowrap align-baseline rounded-[.25rem]', '!border-[1px] !border-[solid] !border-[#283F5D]', 'border-black', 'bg-danger', 'px-1', 'top-[2px] right-[2px] text-[9.5px] px-[3px] py-[2px] ' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } position="absolute" { ...rest }>
{ count }
{ children }
</Base>
);
};
@@ -0,0 +1,15 @@
import { FC } from 'react';
import { Base, BaseProps } from '../Base';
export const LayoutLoadingSpinnerView: FC<BaseProps<HTMLDivElement>> = props =>
{
const { ...rest } = props;
return (
<Base classNames={ [ 'spinner-container' ] } { ...rest } >
<Base className="spinner" />
<Base className="spinner" />
<Base className="spinner" />
</Base>
);
};
@@ -0,0 +1,73 @@
import { GetRoomEngine, NitroRectangle, NitroTexture } from '@nitrots/nitro-renderer';
import { FC, useRef } from 'react';
import { LocalizeText, PlaySound, SoundNames } from '../../api';
import { DraggableWindow } from '../draggable-window';
interface LayoutMiniCameraViewProps {
roomId: number;
textureReceiver: (texture: NitroTexture) => Promise<void>;
onClose: () => void;
}
export const LayoutMiniCameraView: FC<LayoutMiniCameraViewProps> = props => {
const { roomId = -1, textureReceiver = null, onClose = null } = props;
const elementRef = useRef<HTMLDivElement>();
const getCameraBounds = () => {
if (!elementRef || !elementRef.current) return null;
const frameBounds = elementRef.current.getBoundingClientRect();
return new NitroRectangle(
Math.floor(frameBounds.x),
Math.floor(frameBounds.y),
Math.floor(frameBounds.width),
Math.floor(frameBounds.height)
);
};
const takePicture = () => {
PlaySound(SoundNames.CAMERA_SHUTTER);
textureReceiver(GetRoomEngine().createTextureFromRoom(roomId, 1, getCameraBounds()));
};
return (
<DraggableWindow handleSelector=".nitro-room-thumbnail-camera">
<div className="nitro-room-thumbnail-camera px-2">
<div
style={{
position: 'relative',
paddingBottom: '192px', // Matches the space needed to position buttons as per the design
}}
>
<div ref={elementRef} className="camera-frame" />
<div
style={{
position: 'absolute',
bottom: '10px',
left: '10px',
right: '10px',
display: 'flex',
justifyContent: 'space-between',
}}
>
<button
className="btn btn-sm btn-danger"
style={{ width: '80px' }}
onClick={onClose}
>
{LocalizeText('cancel')}
</button>
<button
className="btn btn-sm btn-success"
style={{ width: '80px' }}
onClick={takePicture}
>
{LocalizeText('navigator.thumbeditor.save')}
</button>
</div>
</div>
</div>
</DraggableWindow>
);
};
@@ -0,0 +1,35 @@
import { FC, useMemo } from 'react';
import { NotificationAlertType } from '../../api';
import { NitroCardContentView, NitroCardHeaderView, NitroCardView, NitroCardViewProps } from '../card';
export interface LayoutNotificationAlertViewProps extends NitroCardViewProps
{
title?: string;
type?: string;
onClose: () => void;
}
export const LayoutNotificationAlertView: FC<LayoutNotificationAlertViewProps> = props =>
{
const { title = '', onClose = null, classNames = [], children = null,type = NotificationAlertType.DEFAULT, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-alert' ];
newClassNames.push('nitro-alert-' + type);
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames, type ]);
return (
<NitroCardView classNames={ getClassNames } theme="primary-slim" { ...rest }>
<NitroCardHeaderView headerText={ title } onCloseClick={ onClose } />
<NitroCardContentView grow className="text-black" gap={ 0 } justifyContent="between" overflow="hidden">
{ children }
</NitroCardContentView>
</NitroCardView>
);
};
@@ -0,0 +1,58 @@
import { AnimatePresence, motion } from 'framer-motion';
import { FC, useEffect, useMemo, useState } from 'react';
import { Flex, FlexProps } from '../Flex';
export interface LayoutNotificationBubbleViewProps extends FlexProps
{
fadesOut?: boolean;
timeoutMs?: number;
onClose: () => void;
}
export const LayoutNotificationBubbleView: FC<LayoutNotificationBubbleViewProps> = props =>
{
const { fadesOut = true, timeoutMs = 8000, onClose = null, overflow = 'hidden', classNames = [], ...rest } = props;
const [ isVisible, setIsVisible ] = useState(false);
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'text-sm bg-[#1c1c20f2] px-[5px] py-[6px] [box-shadow:inset_0_5px_#22222799,_inset_0_-4px_#12121599] ', 'rounded' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
useEffect(() =>
{
setIsVisible(true);
return () => setIsVisible(false);
}, []);
useEffect(() =>
{
if(!fadesOut) return;
const timeout = setTimeout(() =>
{
setIsVisible(false);
setTimeout(() => onClose(), 300);
}, timeoutMs);
return () => clearTimeout(timeout);
}, [ fadesOut, timeoutMs, onClose ]);
return (
<AnimatePresence>
{ isVisible &&
<motion.div
initial={ { opacity: 0 }}
animate={ { opacity: 1 }}
exit={ { opacity: 0 }}>
<Flex overflow={ overflow } classNames={ getClassNames } onClick={ onClose } { ...rest } />
</motion.div> }
</AnimatePresence>
);
};
+123
View File
@@ -0,0 +1,123 @@
import { GetRoomEngine, IPetCustomPart, PetFigureData, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useRef, useState } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutPetImageViewProps extends BaseProps<HTMLDivElement>
{
figure?: string;
typeId?: number;
paletteId?: number;
petColor?: number;
customParts?: IPetCustomPart[];
posture?: string;
headOnly?: boolean;
direction?: number;
scale?: number;
}
export const LayoutPetImageView: FC<LayoutPetImageViewProps> = props =>
{
const { figure = '', typeId = -1, paletteId = -1, petColor = 0xFFFFFF, customParts = [], posture = 'std', headOnly = false, direction = 0, scale = 1, style = {}, ...rest } = props;
const [ petUrl, setPetUrl ] = useState<string>(null);
const [ width, setWidth ] = useState(0);
const [ height, setHeight ] = useState(0);
const isDisposed = useRef(false);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(petUrl && petUrl.length) newStyle.backgroundImage = `url(${ petUrl })`;
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
newStyle.width = width;
newStyle.height = height;
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ petUrl, scale, style, width, height ]);
useEffect(() =>
{
let url = null;
let petTypeId = typeId;
let petPaletteId = paletteId;
let petColor1 = petColor;
let petCustomParts: IPetCustomPart[] = customParts;
let petHeadOnly = headOnly;
if(figure && figure.length)
{
const petFigureData = new PetFigureData(figure);
petTypeId = petFigureData.typeId;
petPaletteId = petFigureData.paletteId;
petColor1 = petFigureData.color;
petCustomParts = petFigureData.customParts;
}
if(petTypeId === 16) petHeadOnly = false;
const imageResult = GetRoomEngine().getRoomObjectPetImage(petTypeId, petPaletteId, petColor1, new Vector3d((direction * 45)), 64, {
imageReady: async (id, texture, image) =>
{
if(isDisposed.current) return;
if(image)
{
setPetUrl(image.src);
setWidth(image.width);
setHeight(image.height);
}
else if(texture)
{
setPetUrl(await TextureUtils.generateImageUrl(texture));
setWidth(texture.width);
setHeight(texture.height);
}
},
imageFailed: (id) =>
{
}
}, petHeadOnly, 0, petCustomParts, posture);
if(imageResult)
{
(async () =>
{
const image = await imageResult.getImage();
if(image)
{
setPetUrl(image.src);
setWidth(image.width);
setHeight(image.height);
}
})();
}
}, [ figure, typeId, paletteId, petColor, customParts, posture, headOnly, direction ]);
useEffect(() =>
{
isDisposed.current = false;
return () =>
{
isDisposed.current = true;
};
}, []);
const url = `url('${ petUrl }')`;
return <Base classNames={ [ 'pet-image' ] } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,30 @@
import { FC } from 'react';
import { ProductTypeEnum } from '../../api';
import { LayoutBadgeImageView } from './LayoutBadgeImageView';
import { LayoutCurrencyIcon } from './LayoutCurrencyIcon';
import { LayoutFurniImageView } from './LayoutFurniImageView';
interface LayoutPrizeProductImageViewProps
{
productType: string;
classId: number;
extraParam?: string;
}
export const LayoutPrizeProductImageView: FC<LayoutPrizeProductImageViewProps> = props =>
{
const { productType = ProductTypeEnum.FLOOR, classId = -1, extraParam = undefined } = props;
switch(productType)
{
case ProductTypeEnum.WALL:
case ProductTypeEnum.FLOOR:
return <LayoutFurniImageView productClassId={ classId } productType={ productType } />;
case ProductTypeEnum.BADGE:
return <LayoutBadgeImageView badgeCode={ extraParam }/>;
case ProductTypeEnum.HABBO_CLUB:
return <LayoutCurrencyIcon type="hc" />;
}
return null;
};
+32
View File
@@ -0,0 +1,32 @@
import { FC, useMemo } from 'react';
import { Base, Column, ColumnProps, Flex } from '..';
interface LayoutProgressBarProps extends ColumnProps
{
text?: string;
progress: number;
maxProgress?: number;
}
export const LayoutProgressBar: FC<LayoutProgressBarProps> = props =>
{
const { text = '', progress = 0, maxProgress = 100, position = 'relative', justifyContent = 'center', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'border-[1px] border-[solid] border-[#fff] p-[2px] h-[20px] rounded-[.25rem] overflow-hidden bg-[#1E7295] ', 'text-white' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Column classNames={ getClassNames } justifyContent={ justifyContent } position={ position } { ...rest }>
{ text && (text.length > 0) &&
<Flex center fit className="[text-shadow:0px_4px_4px_rgba(0,_0,_0,_.25)] z-20" position="absolute">{ text }</Flex> }
<Base className="h-full z-10 [transition:all_1s] rounded-[.125rem] bg-[repeating-linear-gradient(#2DABC2,_#2DABC2_50%,_#2B91A7_50%,_#2B91A7_100%)]" style={ { width: (~~((((progress - 0) * (100 - 0)) / (maxProgress - 0)) + 0) + '%') } } />
{ children }
</Column>
);
};
@@ -0,0 +1,28 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutRarityLevelViewProps extends BaseProps<HTMLDivElement>
{
level: number;
}
export const LayoutRarityLevelView: FC<LayoutRarityLevelViewProps> = props =>
{
const { level = 0, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'nitro-rarity-level' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } { ...rest }>
<div>{ level }</div>
{ children }
</Base>
);
};
@@ -0,0 +1,59 @@
import { GetRoomEngine, TextureUtils, Vector3d } from '@nitrots/nitro-renderer';
import { CSSProperties, FC, useEffect, useMemo, useState } from 'react';
import { Base, BaseProps } from '../Base';
interface LayoutRoomObjectImageViewProps extends BaseProps<HTMLDivElement>
{
roomId: number;
objectId: number;
category: number;
direction?: number;
scale?: number;
}
export const LayoutRoomObjectImageView: FC<LayoutRoomObjectImageViewProps> = props =>
{
const { roomId = -1, objectId = 1, category = -1, direction = 2, scale = 1, style = {}, ...rest } = props;
const [ imageElement, setImageElement ] = useState<HTMLImageElement>(null);
const getStyle = useMemo(() =>
{
let newStyle: CSSProperties = {};
if(imageElement?.src?.length)
{
newStyle.backgroundImage = `url('${ imageElement.src }')`;
newStyle.width = imageElement.width;
newStyle.height = imageElement.height;
}
if(scale !== 1)
{
newStyle.transform = `scale(${ scale })`;
if(!(scale % 1)) newStyle.imageRendering = 'pixelated';
}
if(Object.keys(style).length) newStyle = { ...newStyle, ...style };
return newStyle;
}, [ imageElement, scale, style ]);
useEffect(() =>
{
const imageResult = GetRoomEngine().getRoomObjectImage(roomId, objectId, category, new Vector3d(direction * 45), 64, {
imageReady: async (id, texture, image) => setImageElement(await TextureUtils.generateImage(texture)),
imageFailed: null
});
// needs (roomObjectImage.data.width > 140) || (roomObjectImage.data.height > 200) scale 1
if(!imageResult) return;
(async () => setImageElement(await TextureUtils.generateImage(imageResult.data)))();
}, [ roomId, objectId, category, direction, scale ]);
if(!imageElement) return null;
return <Base classNames={ [ 'furni-image' ] } style={ getStyle } { ...rest } />;
};
@@ -0,0 +1,89 @@
import { GetRenderer, GetTicker, NitroTicker, RoomPreviewer, TextureUtils } from '@nitrots/nitro-renderer';
import { FC, MouseEvent, useEffect, useRef } from 'react';
export const LayoutRoomPreviewerView: FC<{
roomPreviewer: RoomPreviewer;
height?: number;
}> = props =>
{
const { roomPreviewer = null, height = 0 } = props;
const elementRef = useRef<HTMLDivElement>();
const onClick = (event: MouseEvent<HTMLDivElement>) =>
{
if(!roomPreviewer) return;
if(event.shiftKey) roomPreviewer.changeRoomObjectDirection();
else roomPreviewer.changeRoomObjectState();
};
useEffect(() =>
{
if(!elementRef) return;
const width = elementRef.current.parentElement.clientWidth;
const texture = TextureUtils.createRenderTexture(width, height);
const update = async (ticker: NitroTicker) =>
{
if(!roomPreviewer || !elementRef.current) return;
roomPreviewer.updatePreviewRoomView();
const renderingCanvas = roomPreviewer.getRenderingCanvas();
if(!renderingCanvas.canvasUpdated) return;
GetRenderer().render({
target: texture,
container: renderingCanvas.master,
clear: true
});
let canvas = GetRenderer().texture.generateCanvas(texture);
const base64 = canvas.toDataURL('image/png');
canvas = null;
elementRef.current.style.backgroundImage = `url(${ base64 })`;
};
GetTicker().add(update);
const resizeObserver = new ResizeObserver(() =>
{
if(!roomPreviewer || !elementRef.current) return;
const width = elementRef.current.parentElement.offsetWidth;
roomPreviewer.modifyRoomCanvas(width, height);
update(GetTicker());
});
roomPreviewer.getRoomCanvas(width, height);
resizeObserver.observe(elementRef.current);
return () =>
{
GetTicker().remove(update);
resizeObserver.disconnect();
texture.destroy(true);
};
}, [ roomPreviewer, elementRef, height ]);
return (
<div
ref={ elementRef }
className="relative w-full rounded-md shadow-room-previewer"
style={ {
height,
minHeight: height,
maxHeight: height
} }
onClick={ onClick } />
);
};
@@ -0,0 +1,37 @@
import { FC, useMemo } from 'react';
import { GetConfigurationValue } from '../../api';
import { Base, BaseProps } from '../Base';
export interface LayoutRoomThumbnailViewProps extends BaseProps<HTMLDivElement>
{
roomId?: number;
customUrl?: string;
}
export const LayoutRoomThumbnailView: FC<LayoutRoomThumbnailViewProps> = props =>
{
const { roomId = -1, customUrl = null, shrink = true, overflow = 'hidden', classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'relative w-[110px] h-[110px] bg-[url("@/assets/images/navigator/thumbnail_placeholder.png")] bg-no-repeat bg-center', 'rounded', '!border-[1px] !border-[solid] !border-[#283F5D]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
const getImageUrl = useMemo(() =>
{
if(customUrl && customUrl.length) return (GetConfigurationValue<string>('image.library.url') + customUrl);
return (GetConfigurationValue<string>('thumbnails.url').replace('%thumbnail%', roomId.toString()));
}, [ customUrl, roomId ]);
return (
<Base classNames={ getClassNames } overflow={ overflow } shrink={ shrink } { ...rest }>
{ getImageUrl && <img alt="" src={ getImageUrl } /> }
{ children }
</Base>
);
};
+42
View File
@@ -0,0 +1,42 @@
import { FC } from 'react';
import { LocalizeText } from '../../api';
import { Base } from '../Base';
import { Column } from '../Column';
import { Flex } from '../Flex';
import { Text } from '../Text';
import { DraggableWindow } from '../draggable-window';
interface LayoutTrophyViewProps
{
color: string;
message: string;
date: string;
senderName: string;
customTitle?: string;
onCloseClick: () => void;
}
export const LayoutTrophyView: FC<LayoutTrophyViewProps> = props =>
{
const { color = '', message = '', date = '', senderName = '', customTitle = null, onCloseClick = null } = props;
return (
<DraggableWindow handleSelector=".drag-handler">
<Column alignItems="center" className={ `nitro-layout-trophy trophy-${ color }` } gap={ 0 }>
<Flex center fullWidth className="trophy-header drag-handler" position="relative">
<Base pointer className="trophy-close" position="absolute" onClick={ onCloseClick } />
<Text bold>{ LocalizeText('widget.furni.trophy.title') }</Text>
</Flex>
<Column className="trophy-content py-1" gap={ 1 }>
{ customTitle &&
<Text bold>{ customTitle }</Text> }
{ message }
</Column>
<Flex alignItems="center" className="trophy-footer mt-1" justifyContent="between">
<Text bold>{ date }</Text>
<Text bold>{ senderName }</Text>
</Flex>
</Column>
</DraggableWindow>
);
};
+29
View File
@@ -0,0 +1,29 @@
import { FC, useMemo } from 'react';
import { GetUserProfile } from '../../api';
import { Base, BaseProps } from '../Base';
export interface UserProfileIconViewProps extends BaseProps<HTMLDivElement>
{
userId?: number;
userName?: string;
}
export const UserProfileIconView: FC<UserProfileIconViewProps> = props =>
{
const { userId = 0, userName = null, classNames = [], pointer = true, children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'bg-[url("@/assets/images/friends/friends-spritesheet.png")]', 'w-[13px] h-[11px] bg-[-51px_-91px]' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } pointer={ pointer } onClick={ event => GetUserProfile(userId) } { ...rest }>
{ children }
</Base>
);
};
+24
View File
@@ -0,0 +1,24 @@
export * from './LayoutAvatarImageView';
export * from './LayoutBackgroundImage';
export * from './LayoutBadgeImageView';
export * from './LayoutCounterTimeView';
export * from './LayoutCurrencyIcon';
export * from './LayoutFurniIconImageView';
export * from './LayoutFurniImageView';
export * from './LayoutGiftTagView';
export * from './LayoutGridItem';
export * from './LayoutImage';
export * from './LayoutItemCountView';
export * from './LayoutLoadingSpinnerView';
export * from './LayoutMiniCameraView';
export * from './LayoutNotificationAlertView';
export * from './LayoutNotificationBubbleView';
export * from './LayoutPetImageView';
export * from './LayoutProgressBar';
export * from './LayoutRarityLevelView';
export * from './LayoutRoomObjectImageView';
export * from './LayoutRoomPreviewerView';
export * from './LayoutRoomThumbnailView';
export * from './LayoutTrophyView';
export * from './UserProfileIconView';
export * from './limited-edition';
@@ -0,0 +1,35 @@
import { FC, useMemo } from 'react';
import { Base, BaseProps } from '../../Base';
import { LayoutLimitedEditionStyledNumberView } from './LayoutLimitedEditionStyledNumberView';
interface LayoutLimitedEditionCompactPlateViewProps extends BaseProps<HTMLDivElement>
{
uniqueNumber: number;
uniqueSeries: number;
}
export const LayoutLimitedEditionCompactPlateView: FC<LayoutLimitedEditionCompactPlateViewProps> = props =>
{
const { uniqueNumber = 0, uniqueSeries = 0, classNames = [], children = null, ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'unique-compact-plate', 'z-index-1' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } { ...rest }>
<div>
<LayoutLimitedEditionStyledNumberView value={ uniqueNumber } />
</div>
<div>
<LayoutLimitedEditionStyledNumberView value={ uniqueSeries } />
</div>
{ children }
</Base>
);
};
@@ -0,0 +1,40 @@
import { FC, useMemo } from 'react';
import { LocalizeText } from '../../../api';
import { Base, BaseProps } from '../../Base';
import { Column } from '../../Column';
import { LayoutLimitedEditionStyledNumberView } from './LayoutLimitedEditionStyledNumberView';
interface LayoutLimitedEditionCompletePlateViewProps extends BaseProps<HTMLDivElement>
{
uniqueLimitedItemsLeft: number;
uniqueLimitedSeriesSize: number;
}
export const LayoutLimitedEditionCompletePlateView: FC<LayoutLimitedEditionCompletePlateViewProps> = props =>
{
const { uniqueLimitedItemsLeft = 0, uniqueLimitedSeriesSize = 0, classNames = [], ...rest } = props;
const getClassNames = useMemo(() =>
{
const newClassNames: string[] = [ 'unique-complete-plate' ];
if(classNames.length) newClassNames.push(...classNames);
return newClassNames;
}, [ classNames ]);
return (
<Base classNames={ getClassNames } { ...rest }>
<Column className="plate-container" gap={ 0 }>
<div className="flex justify-between items-center">
{ LocalizeText('unique.items.left') }
<div><LayoutLimitedEditionStyledNumberView value={ uniqueLimitedItemsLeft } /></div>
</div>
<div className="flex justify-between items-center">
{ LocalizeText('unique.items.number.sold') }
<div><LayoutLimitedEditionStyledNumberView value={ uniqueLimitedSeriesSize } /></div>
</div>
</Column>
</Base>
);
};
@@ -0,0 +1,18 @@
import { FC } from 'react';
interface LayoutLimitedEditionStyledNumberViewProps
{
value: number;
}
export const LayoutLimitedEditionStyledNumberView: FC<LayoutLimitedEditionStyledNumberViewProps> = props =>
{
const { value = 0 } = props;
const numbers = value.toString().split('');
return (
<>
{ numbers.map((number, index) => <i key={ index } className={ 'limited-edition-number n-' + number } />) }
</>
);
};
@@ -0,0 +1,3 @@
export * from './LayoutLimitedEditionCompactPlateView';
export * from './LayoutLimitedEditionCompletePlateView';
export * from './LayoutLimitedEditionStyledNumberView';
@@ -0,0 +1,52 @@
import { FC, ReactNode, useEffect, useState } from 'react';
import { Transition } from 'react-transition-group';
import { getTransitionAnimationStyle } from './TransitionAnimationStyles';
interface TransitionAnimationProps
{
type: string;
inProp: boolean;
timeout?: number;
className?: string;
children?: ReactNode;
}
export const TransitionAnimation: FC<TransitionAnimationProps> = props =>
{
const { type = null, inProp = false, timeout = 300, className = null, children = null } = props;
const [ isChildrenVisible, setChildrenVisible ] = useState(false);
useEffect(() =>
{
let timeoutData: ReturnType<typeof setTimeout> = null;
if(inProp)
{
setChildrenVisible(true);
}
else
{
timeoutData = setTimeout(() =>
{
setChildrenVisible(false);
clearTimeout(timeout);
}, timeout);
}
return () =>
{
if(timeoutData) clearTimeout(timeoutData);
};
}, [ inProp, timeout ]);
return (
<Transition in={ inProp } timeout={ timeout }>
{ state => (
<div className={ (className ?? '') + ' animate__animated' } style={ { ...getTransitionAnimationStyle(type, state, timeout) } }>
{ isChildrenVisible && children }
</div>
) }
</Transition>
);
};
@@ -0,0 +1,136 @@
import { CSSProperties } from 'react';
import { TransitionStatus } from 'react-transition-group';
import { ENTERING, EXITING } from 'react-transition-group/Transition';
import { TransitionAnimationTypes } from './TransitionAnimationTypes';
export function getTransitionAnimationStyle(type: string, transition: TransitionStatus, timeout: number = 300): Partial<CSSProperties>
{
switch(type)
{
case TransitionAnimationTypes.BOUNCE:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'bounceIn',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'bounceOut',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.SLIDE_LEFT:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'slideInLeft',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'slideOutLeft',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.SLIDE_RIGHT:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'slideInRight',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'slideOutRight',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.FLIP_X:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'flipInX',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'flipOutX',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.FADE_UP:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'fadeInUp',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'fadeOutDown',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.FADE_IN:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'fadeIn',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'fadeOut',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.FADE_DOWN:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'fadeInDown',
animationDuration: `${ timeout }ms`
};
case EXITING:
return {
animationName: 'fadeOutUp',
animationDuration: `${ timeout }ms`
};
}
case TransitionAnimationTypes.HEAD_SHAKE:
switch(transition)
{
default:
return {};
case ENTERING:
return {
animationName: 'headShake',
animationDuration: `${ timeout }ms`
};
}
}
return null;
}
@@ -0,0 +1,11 @@
export class TransitionAnimationTypes
{
public static BOUNCE: string = 'bounce';
public static SLIDE_LEFT: string = 'slideLeft';
public static SLIDE_RIGHT: string = 'slideRight';
public static FLIP_X: string = 'flipX';
public static FADE_IN: string = 'fadeIn';
public static FADE_DOWN: string = 'fadeDown';
public static FADE_UP: string = 'fadeUp';
public static HEAD_SHAKE: string = 'headShake';
}
+3
View File
@@ -0,0 +1,3 @@
export * from './TransitionAnimation';
export * from './TransitionAnimationStyles';
export * from './TransitionAnimationTypes';
+1
View File
@@ -0,0 +1 @@
export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
+1
View File
@@ -0,0 +1 @@
export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
+1
View File
@@ -0,0 +1 @@
export type ButtonSizeType = 'lg' | 'sm' | 'md';
+1
View File
@@ -0,0 +1 @@
export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted' | 'light' | 'gray';
+1
View File
@@ -0,0 +1 @@
export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
+1
View File
@@ -0,0 +1 @@
export type DisplayType = 'none' | 'inline' | 'inline-block' | 'block' | 'grid' | 'table' | 'table-cell' | 'table-row' | 'flex' | 'inline-flex';
+1
View File
@@ -0,0 +1 @@
export type FloatType = 'start' | 'end' | 'none';
+1
View File
@@ -0,0 +1 @@
export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6;
+1
View File
@@ -0,0 +1 @@
export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter';
+1
View File
@@ -0,0 +1 @@
export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
+1
View File
@@ -0,0 +1 @@
export type OverflowType = 'auto' | 'hidden' | 'visible' | 'scroll' | 'y-scroll' | 'unset';
+1
View File
@@ -0,0 +1 @@
export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky';
+1
View File
@@ -0,0 +1 @@
export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5;
+1
View File
@@ -0,0 +1 @@
export type TextAlignType = 'start' | 'center' | 'end';
+14
View File
@@ -0,0 +1,14 @@
export * from './AlignItemType';
export * from './AlignSelfType';
export * from './ButtonSizeType';
export * from './ColorVariantType';
export * from './ColumnSizesType';
export * from './DisplayType';
export * from './FloatType';
export * from './FontSizeType';
export * from './FontWeightType';
export * from './JustifyContentType';
export * from './OverflowType';
export * from './PositionType';
export * from './SpacingType';
export * from './TextAlignType';
@@ -0,0 +1,13 @@
import { GetEventDispatcher, NitroToolbarAnimateIconEvent } from '@nitrots/nitro-renderer';
export const CreateTransitionToIcon = (image: HTMLImageElement, fromElement: HTMLElement, icon: string) =>
{
const bounds = fromElement.getBoundingClientRect();
const x = (bounds.x + (bounds.width / 2));
const y = (bounds.y + (bounds.height / 2));
const event = new NitroToolbarAnimateIconEvent(image, x, y);
event.iconName = icon;
GetEventDispatcher().dispatchEvent(event);
};
+28
View File
@@ -0,0 +1,28 @@
import { FC, useEffect, useMemo, useState } from 'react';
import { FriendlyTime } from '../../api';
import { Base, BaseProps } from '../Base';
interface FriendlyTimeViewProps extends BaseProps<HTMLDivElement>
{
seconds: number;
isShort?: boolean;
}
export const FriendlyTimeView: FC<FriendlyTimeViewProps> = props =>
{
const { seconds = 0, isShort = false, children = null, ...rest } = props;
const [ updateId, setUpdateId ] = useState(-1);
const getStartSeconds = useMemo(() => (Math.round(new Date().getSeconds()) - seconds), [ seconds ]);
useEffect(() =>
{
const interval = setInterval(() => setUpdateId(prevValue => (prevValue + 1)), 10000);
return () => clearInterval(interval);
}, []);
const value = (Math.round(new Date().getSeconds()) - getStartSeconds);
return <Base { ...rest }>{ isShort ? FriendlyTime.shortFormat(value) : FriendlyTime.format(value) }</Base>;
};
+2
View File
@@ -0,0 +1,2 @@
export * from './CreateTransitionToIcon';
export * from './FriendlyTimeView';