mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-20 07:26:19 +00:00
🆙 Init V3
This commit is contained in:
@@ -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 } />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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 } />;
|
||||
}
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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} />;
|
||||
};
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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);
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
export * from './NitroCardAccordionContext';
|
||||
export * from './NitroCardAccordionItemView';
|
||||
export * from './NitroCardAccordionSetView';
|
||||
export * from './NitroCardAccordionView';
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './DraggableWindow';
|
||||
export * from './DraggableWindowPosition';
|
||||
@@ -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';
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 } />;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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';
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export * from './TransitionAnimation';
|
||||
export * from './TransitionAnimationStyles';
|
||||
export * from './TransitionAnimationTypes';
|
||||
@@ -0,0 +1 @@
|
||||
export type AlignItemType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
|
||||
@@ -0,0 +1 @@
|
||||
export type AlignSelfType = 'start' | 'end' | 'center' | 'baseline' | 'stretch';
|
||||
@@ -0,0 +1 @@
|
||||
export type ButtonSizeType = 'lg' | 'sm' | 'md';
|
||||
@@ -0,0 +1 @@
|
||||
export type ColorVariantType = 'primary' | 'success' | 'danger' | 'secondary' | 'link' | 'black' | 'white' | 'dark' | 'warning' | 'muted' | 'light' | 'gray';
|
||||
@@ -0,0 +1 @@
|
||||
export type ColumnSizesType = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
|
||||
@@ -0,0 +1 @@
|
||||
export type DisplayType = 'none' | 'inline' | 'inline-block' | 'block' | 'grid' | 'table' | 'table-cell' | 'table-row' | 'flex' | 'inline-flex';
|
||||
@@ -0,0 +1 @@
|
||||
export type FloatType = 'start' | 'end' | 'none';
|
||||
@@ -0,0 +1 @@
|
||||
export type FontSizeType = 1 | 2 | 3 | 4 | 5 | 6;
|
||||
@@ -0,0 +1 @@
|
||||
export type FontWeightType = 'bold' | 'bolder' | 'normal' | 'light' | 'lighter';
|
||||
@@ -0,0 +1 @@
|
||||
export type JustifyContentType = 'start' | 'end' | 'center' | 'between' | 'around' | 'evenly';
|
||||
@@ -0,0 +1 @@
|
||||
export type OverflowType = 'auto' | 'hidden' | 'visible' | 'scroll' | 'y-scroll' | 'unset';
|
||||
@@ -0,0 +1 @@
|
||||
export type PositionType = 'static' | 'relative' | 'fixed' | 'absolute' | 'sticky';
|
||||
@@ -0,0 +1 @@
|
||||
export type SpacingType = 0 | 1 | 2 | 3 | 4 | 5;
|
||||
@@ -0,0 +1 @@
|
||||
export type TextAlignType = 'start' | 'center' | 'end';
|
||||
@@ -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);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './CreateTransitionToIcon';
|
||||
export * from './FriendlyTimeView';
|
||||
Reference in New Issue
Block a user