refactored user-view

This commit is contained in:
euzu
2025-02-24 16:21:59 +01:00
parent 83d991838f
commit cb71b4f32f
12 changed files with 233 additions and 229 deletions
+1 -1
View File
@@ -37,7 +37,7 @@ export default function AppUser(props: AppUserProps) {
<div className="user-app">
<div className={'user-app__header'}>
<div className={'user-app__header__caption'}><span className={'user-app__header__logo'}>{appLogo}</span>{appTitle}</div>
<div className={'app-header__toolbar'}><select onChange={handleLanguage}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l} selected={l === i18next.language}>{l}</option>)}</select></div>
<div className={'app-header__toolbar'}><select onChange={handleLanguage} defaultValue={i18next.language}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l}>{l}</option>)}</select></div>
<div className={'user-app__header__toolbar'}><button title={translate('LABEL.LOGOUT')} onClick={handleLogout}>{getIconByName('Logout')}</button></div>
</div>
<div className={'user-app__main'}>
+1 -1
View File
@@ -165,7 +165,7 @@ export default function App(props: AppProps) {
<div className="app">
<div className={'app-header'}>
<div className={'app-header__caption'}><span className={'app-header__logo'}>{appLogo}</span>{appTitle}</div>
<div className={'app-header__toolbar'}><select onChange={handleLanguage}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l} selected={l === i18next.language}>{l}</option>)}</select></div>
<div className={'app-header__toolbar'}><select onChange={handleLanguage} defaultValue={i18next.language}>{services.config().getUiConfig().languages.map(l => <option key={l} value={l}>{l}</option>)}</select></div>
<div className={'app-header__toolbar'}><button title={translate('LABEL.CONFIGURATION')} onClick={handlePreferences}>{getIconByName('Config')}</button></div>
<div className={'app-header__toolbar'}><button title={translate('LABEL.LOGOUT')} onClick={handleLogout}>{getIconByName('Logout')}</button></div>
</div>
@@ -7,14 +7,16 @@
border: 1px solid var(--border-color);
border-radius: var(--border-radius);
flex: 1 1 auto;
font-size: inherit;
&__content {
display: flex;
flex-flow: row nowrap;
flex: 1 1 auto;
gap: 1px;
overflow: hidden;
border-radius: var(--border-radius);
font-size: inherit;
user-select: none;
}
&:focus-within {
@@ -25,32 +27,32 @@
position: absolute;
color: var(--label-color);
transform: translate(6px, -0.8rem);
background-color: var(--background-color);
padding: 0 2px;
background-color: var(--card-background-color);
border-radius: var(--border-radius);
padding: 0 6px;
font-size: 1.1rem !important;
}
input {
color: var(--text-color);
padding: 0.5rem;
border: none;
border: none !important;
flex: 1 1 auto;
&:focus,
&:hover {
border: none;
outline: none;
}
height: 100%;
font-size: 1.1rem !important;
outline: none !important;
padding: 0 4px 0 12px;
}
button {
@include common.iconButton();
border-radius: 0;
border: none !important;
border-radius: 0 !important;
&:last-child {
border-bottom-right-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
}
&:active {
transform: scale(0.95);
&:not(:last-child) {
border-right: 1px solid var(--card-background-color) !important;
}
}
}
@@ -1,11 +1,13 @@
.playlist-filter {
display: flex;
flex-flow: row nowrap;
padding: 10px;
padding: 10px 0 10px 0;
max-height: 2.5rem;
font-size: 0.5rem;
&__option-active {
box-sizing: border-box !important;
transform: scale(0.8);
transform: scale(0.9);
background-color: var(--option-active-background-color) !important;
color: var(--option-active-color) !important;
svg {
@@ -13,7 +15,6 @@
}
}
&__error-message {
display: flex;
flex-flow: row;
@@ -1,4 +1,4 @@
import React, {useCallback, useRef, KeyboardEvent, useState} from "react";
import React, {KeyboardEvent, useCallback, useEffect, useRef, useState} from "react";
import './playlist-filter.scss';
import {getIconByName} from "../../icons/icons";
import InputField from "../input-field/input-field";
@@ -14,16 +14,24 @@ function isValidRegExp(pattern: string): boolean {
}
interface PlaylistFilterProps {
options?: { filter: string, regexp: boolean }
onFilter: (filter: string, regexp: boolean) => void;
}
export default function PlaylistFilter(props: PlaylistFilterProps) {
const {onFilter} = props;
const {onFilter, options} = props;
const translate = useTranslator();
const textField = useRef<HTMLInputElement>(undefined);
const [useRegexp, setUseRegexp] = useState<boolean>(false);
const [errorMsg, setErrorMsg] = useState<string | undefined>(undefined);
useEffect(()=> {
setUseRegexp(options?.regexp ?? false);
if (textField.current) {
textField.current.value = options?.filter ?? '';
}
}, [options?.regexp, options?.filter]);
const handleSearch = useCallback(() => {
const value = textField.current.value;
setErrorMsg(undefined);
@@ -36,7 +44,7 @@ export default function PlaylistFilter(props: PlaylistFilterProps) {
} else {
onFilter(value, useRegexp);
}
}, [onFilter,useRegexp, translate]);
}, [onFilter, useRegexp, translate]);
const handleClear = useCallback(() => {
textField.current.value = '';
@@ -56,7 +64,8 @@ export default function PlaylistFilter(props: PlaylistFilterProps) {
return <div className={'playlist-filter'}>
<InputField label={translate('LABEL.SEARCH')}>
<input type="text" ref={textField} onKeyUp={handleKeyPress}/>
<button title={translate('LABEL.REGEXP')} className={useRegexp ? 'playlist-filter__option-active' : ''} onClick={handleRegexp}>{getIconByName('Regexp')}</button>
<button title={translate('LABEL.REGEXP')} className={useRegexp ? 'playlist-filter__option-active' : ''}
onClick={handleRegexp}>{getIconByName('Regexp')}</button>
<button title={translate('LABEL.CLEAR')} onClick={handleClear}>{getIconByName('ClearSearch')}</button>
<button title={translate('LABEL.SEARCH')} onClick={handleSearch}>{getIconByName('Search')}</button>
</InputField>
+1 -1
View File
@@ -7,7 +7,7 @@ interface SvgIconProps {
}
function SvgIcon(props: SvgIconProps): React.JSX.Element {
return <svg className="svg-icon" focusable="false" aria-hidden="true" data-testid={props.name} viewBox='0 0 24 24'>
return <svg className="svg-icon" focusable="false" aria-hidden="true" fill="inherit" data-testid={props.name} viewBox='0 0 24 24'>
<path d={props.path}/>
</svg>;
}
@@ -131,16 +131,20 @@ export default function UserPlaylist(props: UserPlaylistProps) {
}, [activeTab, getActiveCategories]);
const handleFilter = useCallback((filter: string, regexp: boolean): void => {
let filer_value = regexp ? filter : filter.toLowerCase();
const filtered = (categories as any)?.[activeTab]?.filter((cat: PlaylistCategory) => {
if (regexp) {
// eslint-disable-next-line eqeqeq
return cat.name.trim().match(filer_value) != undefined;
} else {
return (cat.name.trim().toLowerCase().indexOf(filer_value) > -1);
}
}) ?? [];
setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: filtered}));
let filter_value = regexp ? filter : filter.toLowerCase();
if (filter_value?.length) {
const filtered = (categories as any)?.[activeTab]?.filter((cat: PlaylistCategory) => {
if (regexp) {
// eslint-disable-next-line eqeqeq
return cat.name.trim().match(filter_value) != undefined;
} else {
return (cat.name.trim().toLowerCase().indexOf(filter_value) > -1);
}
}) ?? [];
setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: filtered}));
} else {
setFilteredCategories(filteredCategories => ({...filteredCategories, [activeTab]: (categories as any)?.[activeTab]}));
}
}, [activeTab, categories]);
const handleShowSelected = useCallback((event: any) => {
@@ -156,7 +160,7 @@ export default function UserPlaylist(props: UserPlaylistProps) {
<button title={translate('LABEL.SAVE')} onClick={handleSave}>{translate('LABEL.SAVE')}</button>
</div>
<TabSet tabs={tabs} active={activeTab} onTabChange={handleTabChange}></TabSet>
{CATEGORY_TABS.map(tab => <div className={'user-playlist__categories-panel' + (activeTab !== tab.key ? ' hidden' : '')}>
{CATEGORY_TABS.map(tab => <div key={tab.key} className={'user-playlist__categories-panel' + (activeTab !== tab.key ? ' hidden' : '')}>
<div className={'user-playlist__categories__toolbar'}>
<div className={'user-playlist__categories__toolbar-filter'}>
<PlaylistFilter onFilter={handleFilter}></PlaylistFilter>
+17 -19
View File
@@ -114,21 +114,25 @@
vertical-align: middle;
padding-left: 4px;
padding-right: 4px;
}
input {
border: none !important;
height: 2rem;
white-space: nowrap;
}
&-number {
max-width: 50px;
&-value {
display: flex;
flex-flow: row nowrap;
align-items: center;
gap: 4px;
}
&-date {
width: 120px;
.visibility {
opacity: 0.5;
cursor: pointer;
&:hover {
color: var(--icon-button-hover-background-color);
}
}
&-toolbar {
& {
display: flex;
@@ -173,22 +177,16 @@
}
&-col {
display: flex;
flex-flow: column;
padding-top: 4px;
padding-bottom: 4px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px;
input {
border-top: 1px solid var(--table-border-color) !important;
border-bottom: 1px solid var(--table-border-color) !important;
&-toolbar {
padding-bottom: 4px;
}
}
}
}
}
input {
@include preferences.prefsInput;
}
}
+138 -175
View File
@@ -1,28 +1,31 @@
import React, {useCallback, useEffect, useMemo, useState} from "react";
import React, {useCallback, useEffect, useState} from "react";
import './user-view.scss';
import ServerConfig, {TargetUser} from "../../model/server-config";
import ServerConfig, {Credentials, TargetUser} from "../../model/server-config";
import {getIconByName} from "../../icons/icons";
import TextGenerator from "../../utils/text-generator";
import {useSnackbar} from "notistack";
import {useServices} from "../../provider/service-provider";
import ConfigUtils from "../../utils/config-utils";
import TabSet, {TabSetTab} from "../tab-set/tab-set";
import TagSelect from "../tag-select/tags-select";
import DatePicker from "react-date-picker";
import useTranslator from "../../hook/use-translator";
import DateUtils from "../../utils/date-utils";
import PlaylistFilter from "../playlist-filter/playlist-filter";
const PROXY_OPTIONS = [
{value: 'reverse', label: 'Reverse'},
{value: 'redirect', label: 'Redirect'}
];
const STATUS_OPTIONS = [
{value: 'Active', label: 'Active'},
{value: 'Expired', label: 'Expired'},
{value: 'Banned', label: 'Banned'},
{value: 'Trial', label: 'Trial'},
{value: 'Disabled', label: 'Disabled'},
{value: 'Pending', label: 'Pending'},
];
const COLUMNS = [
{field: 'username', label: 'LABEL.USERNAME'},
{field: 'password', label: 'LABEL.PASSWORD', hidden: true},
{field: 'token', label: 'LABEL.TOKEN', hidden: true},
{field: 'server', label: 'LABEL.SERVER'},
{field: 'proxy', label: 'LABEL.PROXY'},
{field: 'max_connections', label: 'LABEL.MAX_CON'},
{field: 'status', label: 'LABEL.STATUS'},
{field: 'exp_date', label: 'LABEL.EXP_DATE', render: (value: any, hidden?: boolean) => DateUtils.formatDate(value)},
]
COLUMNS.forEach(col => {
if (!col.render) {
col.render = (value: any, hidden?: boolean) => value ? (hidden ? '***' : '' + value) : '';
}
})
const prepareCredentials = (targetUser: TargetUser[]) => {
targetUser.forEach((user) => {
@@ -55,12 +58,16 @@ export default function UserView(props: UserViewProps) {
const services = useServices();
const translate = useTranslator();
const {enqueueSnackbar/*, closeSnackbar*/} = useSnackbar();
const [serverOptions, setServerOptions] = useState<{ value: string, label: string }[]>([]);
const [targets, setTargets] = useState<TargetUser[]>([]);
const [activeTarget, setActiveTarget] = useState<string>(undefined);
const [tabs, setTabs] = useState<TabSetTab[]>([]);
const [serverOptions, setServerOptions] = useState<{ value: string, label: string }[]>([]);
const proxy_options = useMemo(() => PROXY_OPTIONS.map(c => ({...c, label: translate(c.label)})), [translate]);
const status_options = useMemo(() => STATUS_OPTIONS.map(c => ({...c, label: translate(c.label)})), [translate]);
const [showHiddenFields, setShowHiddenFields] = useState<Record<string, boolean>>({});
const [filteredUser, setFilteredUser] = useState<Record<string, {
filter: string,
regexp: boolean,
user: Credentials[]
}>>({});
useEffect(() => {
if (config) {
@@ -80,92 +87,82 @@ export default function UserView(props: UserViewProps) {
missing?.forEach(target => {
result.push({src: false, target: target.target, credentials: target.credentials} as any);
});
const targets = result || [];
const targets: TargetUser [] = result || [];
setTargets(targets);
if (targets?.length) {
setActiveTarget(targets[0].target);
const hiddenFields = COLUMNS.filter(c => c.hidden);
const hidden: any = {};
targets.forEach(target => target.credentials.forEach(c => {
hiddenFields.forEach(h => {
hidden[c.username + h.field] = true
});
}));
setShowHiddenFields(hidden);
}
setTabs(targets.map(target => ({key: target.target, label: target.target})));
}
}, [config])
const handleUserAdd = useCallback((evt: any) => {
const target_name = evt.target.dataset.target;
const target = targets.find(target => target.target === target_name);
if (target) {
const usernameExists = (uname: string): boolean => {
for (const target of targets) {
if (target.credentials.find(c => c.username === uname)) {
return true;
}
}
return false;
};
let cnt = 0;
let username = TextGenerator.generateUsername().toLowerCase();
while (usernameExists(username)) {
username = TextGenerator.generateUsername().toLowerCase();
cnt++;
if (cnt > 1000) {
username = "";
break;
}
}
const created_at = Math.floor(Date.now() / 1000);
target.credentials.push({
username,
password: TextGenerator.generatePassword(),
token: TextGenerator.generatePassword(),
proxy: 'reverse',
created_at,
exp_date: undefined,
max_connections: undefined,
status: "Active",
});
setTargets([...targets]);
}
}, [targets]);
// const target_name = evt.target.dataset.target;
// const target = targets.find(target => target.target === target_name);
// if (target) {
// const usernameExists = (uname: string): boolean => {
// for (const target of targets) {
// if (target.credentials.find(c => c.username === uname)) {
// return true;
// }
// }
// return false;
// };
// let cnt = 0;
// let username = TextGenerator.generateUsername().toLowerCase();
// while (usernameExists(username)) {
// username = TextGenerator.generateUsername().toLowerCase();
// cnt++;
// if (cnt > 1000) {
// username = "";
// break;
// }
// }
// const created_at = Math.floor(Date.now() / 1000);
// target.credentials.push({
// username,
// password: TextGenerator.generatePassword(),
// token: TextGenerator.generatePassword(),
// proxy: 'reverse',
// created_at,
// exp_date: undefined,
// max_connections: undefined,
// status: "Active",
// });
// setTargets([...targets]);
// }
}, []);
const handleUserRemove = useCallback((evt: any) => {
const idx = evt.target.dataset.idx;
const username = evt.target.dataset.user;
const target_name = evt.target.dataset.target;
const target = targets.find(target => target.target === target_name);
if (target) {
target.credentials.splice(idx, 1);
setTargets([...targets]);
let idx = target.credentials.findIndex(c => c.username === username)
if (idx >= 0) {
target.credentials.splice(idx, 1);
setTargets([...targets]);
}
}
}, [targets]);
const handleValueChange = useCallback((evt: any) => {
const handleUserEdit = useCallback((evt: any) => {
const username = evt.target.dataset.user;
const target_name = evt.target.dataset.target;
const target = targets.find(target => target.target === target_name);
if (target) {
const idx = evt.target.dataset.idx;
const field: any = evt.target.dataset.field;
(target.credentials[idx] as any)[field] = evt.target.value;
setTargets([...targets]); // this is needed for DatePicker for change detection
}
}, [targets]);
const user = target.credentials.find(c => c.username === username);
if (user) {
const handleChange = useCallback((fieldWithTargetAndIndex: string, value: any) => {
const parts = fieldWithTargetAndIndex.split('@');
const target_name = parts[0];
const target = targets.find(target => target.target === target_name);
if (target) {
const idx = parseInt(parts[1]);
const credentials: any = target.credentials[idx];
credentials[parts[2]] = value;
}
}, [targets]);
const handleSelectChange = useCallback((event: any) => {
const parts = event.target.name.split('@');
const target_name = parts[0];
const target = targets.find(target => target.target === target_name);
if (target) {
const idx = parseInt(parts[1]);
const credentials: any = target.credentials[idx];
credentials[parts[2]] = event.target.value;
}
}
}, [targets]);
@@ -215,13 +212,47 @@ export default function UserView(props: UserViewProps) {
});
}, [targets, services, enqueueSnackbar, translate]);
const handleVisibility = useCallback((evt: any) => {
let key = evt.target.dataset.hiddenkey;
setShowHiddenFields((hiddenFields: any) => ({...hiddenFields, [key]: !hiddenFields[key]}));
}, []);
const handleTabChange = useCallback((target: string) => {
setActiveTarget(target);
}, []);
const handleFilter = useCallback((filter: string, regexp: boolean): void => {
let filter_value = regexp ? filter : filter.toLowerCase();
const target = targets.find(t => t.target === activeTarget);
if (target) {
if (filter_value?.length) {
const filtered = target.credentials.filter((credential: Credentials) => {
if (regexp) {
// eslint-disable-next-line eqeqeq
return credential.username.trim().match(filter_value) != undefined;
} else {
return (credential.username.trim().toLowerCase().indexOf(filter_value) > -1);
}
}) ?? [];
setFilteredUser((filteredUser: any) => ({
...filteredUser,
[activeTarget]: {filter, regexp, user: filtered}
}));
} else {
setFilteredUser((filteredUser: any) => ({
...filteredUser, [activeTarget]: undefined
}));
}
}
}, [activeTarget, targets]);
return <div className={'user'}>
<div className={'user__toolbar'}><label>{translate('LABEL.USER')}</label>
<PlaylistFilter onFilter={handleFilter} options={filteredUser[activeTarget]}></PlaylistFilter>
<button title={'Save'} onClick={handleSave}>{translate('LABEL.SAVE')}</button>
</div>
<TabSet tabs={tabs} active={activeTarget} onTabChange={handleTabChange}></TabSet>
@@ -241,105 +272,37 @@ export default function UserView(props: UserViewProps) {
<div className={'user__target-user-table-container'}>
<div className={'user__target-user-table'}>
<div className={'user__target-user-row user__target-user-table-header'}>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.USERNAME')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.PASSWORD')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.TOKEN')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.SERVER')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.PROXY')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.MAX_CON')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.STATUS')}</label></div>
<div className={'user__target-user-col user__target-user-col-header'}>
<label>{translate('LABEL.EXP_DATE')}</label></div>
{COLUMNS.map(col => <div key={col.field}
className={'user__target-user-col user__target-user-col-header'}>
<label>{translate(col.label)}</label></div>)}
<div className={'user__target-user-col user__target-user-col-header'}></div>
</div>
{target.credentials.map((usr, idx) =>
{(filteredUser[target.target]?.user ?? target.credentials).map((usr, idx) =>
<div key={'credential' + idx} className={'user__target-user-row'}>
{['username', 'password', 'token'].map((field) =>
<div key={'target_' + target.target + '_' + field + '_' + usr.username}
className={'user__target-user-col'}>
<div className={'user__target-user-col-label'}>
<label>{field.charAt(0).toUpperCase() + field.slice(1)}</label>
</div>
<input data-target={target.target} data-idx={idx}
defaultValue={(usr as any)[field]}
data-field={field} onChange={handleValueChange}></input>
</div>
)}
<div key={'target_' + target.target + '_server_' + usr.username}
className={'user__target-user-col '}>
{COLUMNS.map(c => <div
key={'target_' + target.target + '_' + c.field + '_' + usr.username}
className={'user__target-user-col'}>
<div className={'user__target-user-col-label'}>
<label>{translate('LABEL.SERVER')}</label></div>
<TagSelect options={serverOptions}
name={target.target + '@' + idx + '@server'}
defaultValues={(usr as any)?.['server']} radio={true}
multi={false} onSelect={handleChange}></TagSelect>
</div>
<div key={'target_' + target.target + '_proxy_' + usr.username}
className={'user__target-user-col '}>
<div className={'user__target-user-col-label'}>
<label>{translate('LABEL.PROXY')}</label></div>
<TagSelect options={proxy_options}
name={target.target + '@' + idx + '@proxy'}
defaultValues={(usr as any)?.['proxy']} radio={true}
multi={false} onSelect={handleChange}></TagSelect>
</div>
{['max_connections'].map((field) =>
<div key={'target_' + target.target + '_' + field + '_' + usr.username}
className={'user__target-user-col'}>
<div className={'user__target-user-col-label'}>
<label>{field.charAt(0).toUpperCase() + field.slice(1)}</label>
</div>
<input data-target={target.target} data-idx={idx}
defaultValue={(usr as any)[field]}
className={'user__target-user-col-number'}
data-field={field} onChange={handleValueChange}></input>
<label>{translate(c.label)}</label>
</div>
)}
<div key={'target_' + target.target + '_status_' + usr.username}
className={'user__target-user-col '}>
<div className={'user__target-user-col-label'}>
<label>{translate('LABEL.STATUS')}</label></div>
<select name={target.target + '@' + idx + '@status'}
defaultValue={(usr as any)?.['status']}
onChange={handleSelectChange}>
{status_options.map(option =>
<option key={option.value + idx}>{option.label}</option>
)}
</select>
</div>
{['exp_date'].map((field) =>
<div key={'target_' + target.target + '_' + field + '_' + usr.username}
className={'user__target-user-col'}>
<div className={'user__target-user-col-label'}>
<label>{field.charAt(0).toUpperCase() + field.slice(1)}</label>
</div>
<DatePicker
value={(usr as any)[field]}
className={'user__target-user-col-date'}
onChange={(date: any) => handleValueChange({
target: {
value: date,
dataset: {
target: target.target,
idx,
field,
}
},
})}></DatePicker>
<div className={'user__target-user-col-value'}>{c.hidden &&
<span className={'visibility'} data-hiddenkey={usr.username + c.field}
onClick={handleVisibility}>
{getIconByName('Visibility')}</span>}
{c.render((usr as any)[c.field], (showHiddenFields[usr.username + c.field] ?? c.hidden))}
</div>
)}
<div className={'user__target-user-col user__target-user-col-toolbar'}>
<span data-target={target.target} data-idx={idx} onClick={handleUserRemove}>
</div>)
}
<div className={'user__target-user-col-toolbar'}>
<span data-target={target.target} data-user={usr.username}
onClick={handleUserRemove}>
{getIconByName('PersonRemove')}
</span>
<span data-target={target.target} data-user={usr.username}
onClick={handleUserEdit}>
{getIconByName('Edit')}
</span>
</div>
</div>
)}
+2
View File
@@ -49,6 +49,8 @@ const DEFAULT_ICONS = {
Logout: 'm17 7-1.41 1.41L18.17 11H8v2h10.17l-2.58 2.58L17 17l5-5zM4 5h8V3H4c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h8v-2H4z',
SelectAll: 'M 2,9.2105263 H 13.578947 V 11.315789 H 2 Z M 2,5 H 13.578947 V 7.105263 H 2 Z m 0,8.421054 h 7.368421 v 2.105263 H 2 Z m 18.515789,-2.178949 -4.473684,4.46316 -2.231579,-2.23158 -1.48421,1.484211 3.715789,3.726316 L 22,12.726317 Z',
DeselectAll: 'M 2,5 V 7.1538458 H 13.612371 V 5 Z M 2,9.3076922 V 11.461539 H 13.612371 V 9.3076922 Z m 14.082473,1.7899618 -1.265978,1.392431 2.327832,2.559791 -2.327832,2.557693 L 16.082473,19 18.408247,16.442307 20.734022,19 22,17.607569 19.674225,15.049876 22,12.490085 20.734022,11.097654 18.408247,13.655347 Z M 2,13.615385 v 2.153847 h 7.3896896 v -2.153847 z',
Visibility: 'M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5M12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5m0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3',
Edit: 'M3 17.25V21h3.75L17.81 9.94l-3.75-3.75zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34a.996.996 0 0 0-1.41 0l-1.83 1.83 3.75 3.75z'
}
+3
View File
@@ -47,6 +47,9 @@ $media-size-mobile: 600px;
border-radius: var(--icon-button-border-radius);
aspect-ratio: 1 / 1;
cursor: pointer;
box-sizing: border-box;
max-height: 100%;
width: min-content;
svg {
color: var(--text-color);
+22
View File
@@ -0,0 +1,22 @@
function formatDate(date: any): string {
if (typeof date === 'string') {
return date;
}
const day = String(date.getDate()).padStart(2, '0');
const month = String(date.getMonth() + 1).padStart(2, '0');
const year = date.getFullYear();
return `${year}-${month}-${day}`;
}
const DateUtils = {
formatDate: (date: any) => {
if (date) {
console.log(date);
return formatDate(date);
}
return '';
}
}
export default DateUtils;