diff --git a/.idea/scopes/Backend.xml b/.idea/scopes/Backend.xml new file mode 100644 index 00000000..b43573cd --- /dev/null +++ b/.idea/scopes/Backend.xml @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/SteamOrganizer.Server/Controllers/Steam/WebApiController.cs b/SteamOrganizer.Server/Controllers/Steam/WebApiController.cs index 0d453c14..ee78873d 100644 --- a/SteamOrganizer.Server/Controllers/Steam/WebApiController.cs +++ b/SteamOrganizer.Server/Controllers/Steam/WebApiController.cs @@ -53,10 +53,10 @@ await parser } [HttpGet] - public async Task GetGames(GamesRequest request) + public async Task GetGames([FromQuery] GamesRequest request) { var response = await parser.SetCurrency(request.Currency).GetGames( - request.SteamId!.Value, + Tools.ToSteamId64(request.SteamId!.Value), request.WithDetails ).ConfigureAwait(false); diff --git a/SteamOrganizer.Server/Services/SteamParser/Responses/PlayerGames.cs b/SteamOrganizer.Server/Services/SteamParser/Responses/PlayerGames.cs index 316b95aa..5d7a6b5a 100644 --- a/SteamOrganizer.Server/Services/SteamParser/Responses/PlayerGames.cs +++ b/SteamOrganizer.Server/Services/SteamParser/Responses/PlayerGames.cs @@ -8,6 +8,7 @@ public sealed class PlayerGameInfo public float Playtime_forever { get; set; } public required string Name { get; set; } public string? FormattedPrice { get; set; } + public int? Price { get; set; } } public sealed class PlayerGames @@ -32,7 +33,8 @@ public sealed class PlayerGamesResponse public sealed class GamePrice { - public decimal Initial { get; set; } + public int? Initial { get; set; } + public decimal Final { get; set; } public string? Formatted { get; set; } } diff --git a/SteamOrganizer.Server/Services/SteamParser/SteamParser.cs b/SteamOrganizer.Server/Services/SteamParser/SteamParser.cs index 35ad2c7f..bd9de8fa 100644 --- a/SteamOrganizer.Server/Services/SteamParser/SteamParser.cs +++ b/SteamOrganizer.Server/Services/SteamParser/SteamParser.cs @@ -244,7 +244,7 @@ await Parallel.ForAsync(0, (int)Math.Ceiling(friends.Length / 100f), if (details.Data?.Price_overview != null) { - details.Data.Price_overview.Initial /= 100; + details.Data.Price_overview.Final = (decimal)details.Data.Price_overview.Initial! / 100u; } FormatPrice(games,info, details); @@ -301,13 +301,20 @@ private void FormatPrice(PlayerGames games, PlayerGameInfo gameInfo, GameDetails { if (details.Data?.Price_overview == null) { - gameInfo.FormattedPrice = details.Success ? "Free" : "Unknown"; + if (details.Success) + { + gameInfo.Price = 0; + gameInfo.FormattedPrice = "Free"; + return; + } + gameInfo.FormattedPrice = "Unknown"; return; } var price = details.Data.Price_overview; - games.GamesPrice += price.Initial; - gameInfo.FormattedPrice = price.Formatted ?? (price.Formatted = _currency.Format(price.Initial)); + games.GamesPrice += price.Final; + gameInfo.Price = price.Initial; + gameInfo.FormattedPrice = price.Formatted ?? (price.Formatted = _currency.Format(price.Final)); games.PaidGames++; } diff --git a/SteamOrganizer.Server/SteamOrganizer.Server.csproj b/SteamOrganizer.Server/SteamOrganizer.Server.csproj index 3774dbbf..f133aadd 100644 --- a/SteamOrganizer.Server/SteamOrganizer.Server.csproj +++ b/SteamOrganizer.Server/SteamOrganizer.Server.csproj @@ -12,4 +12,8 @@ + + + + diff --git a/SteamOrganizer.Web/index.html b/SteamOrganizer.Web/index.html index 50040e56..36e24451 100644 --- a/SteamOrganizer.Web/index.html +++ b/SteamOrganizer.Web/index.html @@ -1,4 +1,3 @@ - @@ -8,6 +7,6 @@
- + diff --git a/SteamOrganizer.Web/public/sprites.svg b/SteamOrganizer.Web/public/sprites.svg index 0146a9d1..5c4036b7 100644 --- a/SteamOrganizer.Web/public/sprites.svg +++ b/SteamOrganizer.Web/public/sprites.svg @@ -224,5 +224,11 @@ + + + + + + \ No newline at end of file diff --git a/SteamOrganizer.Web/src/App.tsx b/SteamOrganizer.Web/src/App.tsx index 0ebee375..7390585b 100644 --- a/SteamOrganizer.Web/src/App.tsx +++ b/SteamOrganizer.Web/src/App.tsx @@ -1,24 +1,24 @@ -import Accounts from "./pages/Accounts/Accounts.tsx"; -import {MainLayout} from "./layouts/MainLayout.tsx"; -import NotFoundPage from "./pages/NotFoundPage.tsx"; +import Accounts from "./pages/Accounts/Accounts"; +import {MainLayout} from "./layouts/MainLayout"; +import NotFoundPage from "./pages/NotFoundPage"; import {createBrowserRouter, Navigate, RouterProvider} from 'react-router-dom' -import Actions from "./pages/Actions.tsx"; -import SignIn from "./pages/SignIn.tsx"; -import Backups from "./pages/Backups/Backups.tsx"; +import Actions from "./pages/Actions"; +import SignIn from "./pages/SignIn"; +import Backups from "./pages/Backups/Backups"; import {useEffect} from "react"; -import {config, loadConfig} from "./store/config.ts"; -import db from "./services/indexedDb.ts"; +import {config, loadConfig} from "./store/config"; +import db from "@/shared/services/indexedDb"; import {Defs} from "./defines" -import {ModalsHost} from "./components/primitives/Modal.tsx"; -import {Profile} from "@/pages/Profile/Profile.tsx"; -import {AuthProvider} from "@/providers/authProvider.tsx"; -import {toast, ToastsHost, ToastVariant} from "@/components/primitives/Toast.tsx"; -import {databaseKey, dbTimestamp, importAccounts, loadAccounts} from "@/store/accounts.ts"; -import {isAuthorized} from "@/services/gAuth.ts"; -import {getLatestBackup, loadBackup, restoreBackup} from "@/store/backups.ts"; -import {decrypt} from "@/services/cryptography.ts"; -import {flagStore} from "@/store/local.tsx"; -import Logo from "@/components/elements/Logo.tsx"; +import {ModalsHost} from "./shared/ui/Modal"; +import {Profile} from "@/pages/Profile/Profile"; +import {AuthProvider} from "@/providers/authProvider"; +import {toast, ToastsHost, ToastVariant} from "@/shared/ui/Toast"; +import {databaseKey, dbTimestamp, importAccounts, loadAccounts} from "@/store/accounts"; +import {isAuthorized} from "@/shared/services/gAuth"; +import {getLatestBackup, loadBackup, restoreBackup} from "@/store/backups"; +import {decrypt} from "@/shared/services/cryptography"; +import {flagStore} from "@/store/local"; +import Logo from "@/components/Logo"; const router = createBrowserRouter([ { diff --git a/SteamOrganizer.Web/src/components/elements/CollectionIndicator.tsx b/SteamOrganizer.Web/src/components/CollectionIndicator.tsx similarity index 100% rename from SteamOrganizer.Web/src/components/elements/CollectionIndicator.tsx rename to SteamOrganizer.Web/src/components/CollectionIndicator.tsx diff --git a/SteamOrganizer.Web/src/components/elements/ConfirmPopup.tsx b/SteamOrganizer.Web/src/components/ConfirmPopup.tsx similarity index 66% rename from SteamOrganizer.Web/src/components/elements/ConfirmPopup.tsx rename to SteamOrganizer.Web/src/components/ConfirmPopup.tsx index 9998a7b0..c352da94 100644 --- a/SteamOrganizer.Web/src/components/elements/ConfirmPopup.tsx +++ b/SteamOrganizer.Web/src/components/ConfirmPopup.tsx @@ -1,16 +1,18 @@ -import {EPlacementX, EPlacementY, type IPopupProps, Popup} from "@/components/primitives/Popup.tsx"; -import {type FC} from "react"; -import Button, {EButtonVariant} from "@/components/primitives/Button.tsx"; +import {type FC, ReactElement} from "react"; +import Button, {EButtonVariant} from "@/shared/ui/Button"; +import {Popup} from "@/shared/ui/Popup/Popup"; +import {EPlacement} from "@/shared/ui/Popup/positioning"; -interface IConfirmPopupProps extends IPopupProps { +interface IConfirmPopupProps { text: string; onYes?: () => void; onNo?: () => void; + children: ReactElement; } export const ConfirmPopup: FC = ({ children, text, onYes, onNo }) => { return ( - ( + (
{text}
diff --git a/SteamOrganizer.Web/src/components/elements/FieldWrapper.tsx b/SteamOrganizer.Web/src/components/FieldWrapper.tsx similarity index 89% rename from SteamOrganizer.Web/src/components/elements/FieldWrapper.tsx rename to SteamOrganizer.Web/src/components/FieldWrapper.tsx index e2d49e6c..180b3130 100644 --- a/SteamOrganizer.Web/src/components/elements/FieldWrapper.tsx +++ b/SteamOrganizer.Web/src/components/FieldWrapper.tsx @@ -1,8 +1,4 @@ import {cloneElement, FC, forwardRef, ReactElement, type ReactNode, useImperativeHandle, useRef} from "react"; -import clsx from "clsx"; -import Input, {IInputProps} from "@/components/primitives/Input.tsx"; -import {CheckBox} from "@/components/primitives/CheckBox"; -import {config, delayedSaveConfig} from "@/store/config.ts"; export const enum ETitlePosition { Top, @@ -31,7 +27,7 @@ export const FieldWrapper:FC = ({className,title, titlePos,
{titlePos !== ETitlePosition.Inline &&
-

{title}

+

{title}

{titleChildren}
} diff --git a/SteamOrganizer.Web/src/components/FilterInput/FilterInput.tsx b/SteamOrganizer.Web/src/components/FilterInput/FilterInput.tsx new file mode 100644 index 00000000..9852bc49 --- /dev/null +++ b/SteamOrganizer.Web/src/components/FilterInput/FilterInput.tsx @@ -0,0 +1,65 @@ +import {type FC, Fragment, useState} from "react"; +import Input, {type IInputProps} from "@/shared/ui/Input"; +import {Popup} from "@/shared/ui/Popup/Popup"; +import {EPlacement} from "@/shared/ui/Popup/positioning"; +import {ELabelVariant, Label} from "@/shared/ui/Label/Label"; +import {EFilterType, FiltersDefinition, type IFilterCell, IFilterConfig} from "./types"; +import {FlagsFilter, OrderFilter, SearchFilter} from "./Filters"; +import styles from "./Filters.module.css" + +interface ISearchInputProps extends IInputProps { + filters: FiltersDefinition; + config: IFilterConfig; + onFilterChanged: () => void; +} + +const filterType = { + [EFilterType.Search]: SearchFilter, + [EFilterType.Order]: OrderFilter, + [EFilterType.Flags]: FlagsFilter +} + +const FilterCell: FC<{items: IFilterCell[], callback: () => void, config: IFilterConfig}> = ({ items, config, callback }) => { + return
+ { + items.map((item, index) => { + const Component = filterType[item.type]; + + return + {item.label && + }) + } +
+} + + +export const FilterInput: FC = ({ onFilterChanged, config, filters, ...props}) => { + const [isFocused,setFocused] = useState(undefined) + + const content =
+ {filters.map((item, index) => { + return + })} +
+ + + return ( + + setFocused(undefined)} + onInput={(e) => { + setFocused(false) + config[EFilterType.Search].keyword = e.currentTarget.value; + onFilterChanged(); + }} + className={"pr-24 h-full bg-primary placeholder:font-semibold rounded-lg"} + {...props} + /> + + ) +} diff --git a/SteamOrganizer.Web/src/components/FilterInput/Filters.module.css b/SteamOrganizer.Web/src/components/FilterInput/Filters.module.css new file mode 100644 index 00000000..ce9966b5 --- /dev/null +++ b/SteamOrganizer.Web/src/components/FilterInput/Filters.module.css @@ -0,0 +1,19 @@ +.filterInput { + @apply bg-primary mt-2 rounded-lg border-tertiary border p-3 flex flex-wrap gap-8 pointer-events-auto; +} + +.popup { + @apply rounded-none rounded-b-lg bg-transparent p-0 flex-x-center gap-3 pointer-events-none; +} + +.searchFilter { + @apply flex-x-center flex-wrap bg-accent rounded-lg my-3 gap-y-3; +} + +.flagFilter { + @apply w-full justify-between flex-y-center mt-1; +} + +.orderFilter { + @apply flex-x-center flex-wrap bg-accent rounded-lg gap-y-3; +} \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/FilterInput/Filters.tsx b/SteamOrganizer.Web/src/components/FilterInput/Filters.tsx new file mode 100644 index 00000000..e2ea1c62 --- /dev/null +++ b/SteamOrganizer.Web/src/components/FilterInput/Filters.tsx @@ -0,0 +1,69 @@ +import React, {FC, useState} from "react"; +import {RadioButton} from "@/shared/ui/RadioButton/RadioButton"; +import {CheckBox} from "@/shared/ui/CheckBox/CheckBox"; +import {EFilterType, IFilterConfig, IPropField} from "@/components/FilterInput/types"; +import {Icon, SvgIcon} from "@/defines"; +import styles from "./Filters.module.css" + +type FilterComponent = FC<{ fields: IPropField[], config: IFilterConfig, callback: () => void }> + +const OrderFilter: FilterComponent = ({ fields, config, callback }) => { + const node = config[EFilterType.Order]; + + return ( + <> + field.name)]} + onStateChanged={(o) => { + node.by = o === 0 ? null : [fields[o - 1].prop, o]; + callback(); + }} + className={`my-3 ${styles.orderFilter}`}/> + + { + node.direction = o; + callback(); + }} + className={`w-fit ${styles.orderFilter}`}/> + + ) +} + +const FlagsFilter: FilterComponent = ({ fields, config, callback }) => { + const node = config[EFilterType.Flags]; + + return fields.map((field, index) => +
+ {field.name} + } + initialState={node[field.prop]} + onStateChanged={(o) => { + node[field.prop] = o + callback(); + }} allowIndeterminate={true}/> +
+ ) +} + +const SearchFilter: FilterComponent = ({ fields, config, callback }) => { + const node = config[EFilterType.Search]; + + if(fields.length == 1) { + return; + } + + return ( + field.name)} + initialState={node.by[0]} + onStateChanged={(i) => { + node.by = [i, fields[i].prop]; + callback(); + }} + className={styles.searchFilter}/> + ) +} + +export { SearchFilter, OrderFilter, FlagsFilter } \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/FilterInput/types.ts b/SteamOrganizer.Web/src/components/FilterInput/types.ts new file mode 100644 index 00000000..8d7a4d40 --- /dev/null +++ b/SteamOrganizer.Web/src/components/FilterInput/types.ts @@ -0,0 +1,38 @@ +export const enum EFilterType { + Search, + Order, + Flags +} + + +export interface IPropField { + name: string; + prop: string; +} + +export interface IFilterCell { + label?: string; + type: EFilterType; + fields: IPropField[]; +} + +export type FiltersDefinition = IFilterCell[][] + +export const enum EOrderDirection { + Ascending = 0, + Descending = 1 +} + +export interface IFilterConfig { + [EFilterType.Search]: { + keyword?: string, + by: [ number, string] + }, + [EFilterType.Order]?: { + by?: [string, number] | null, + direction?: EOrderDirection + }, + [EFilterType.Flags]?: { + [key: string]: boolean | null + } +} diff --git a/SteamOrganizer.Web/src/components/FilterInput/useFilterManager.ts b/SteamOrganizer.Web/src/components/FilterInput/useFilterManager.ts new file mode 100644 index 00000000..608d6691 --- /dev/null +++ b/SteamOrganizer.Web/src/components/FilterInput/useFilterManager.ts @@ -0,0 +1,97 @@ +import {useEffect, useRef, useState} from "react"; +import {EFilterType, EOrderDirection, IFilterConfig} from "@/components/FilterInput/types"; +import {ObservableCollection, ObservableObject} from "@/shared/lib/observer/observableObject"; +import {ObservableProxy} from "@/shared/lib/observer/observableProxy"; +import {debounce} from "@/shared/lib/utils"; + +export const useFilterManager = ( + initialConfig: IFilterConfig, + source?: ObservableProxy | ObservableCollection, + serializeName?: string, + filterFunction?: (data: T[]) => T[], +) => { + const [state] = useState(() => { + const collection = (source instanceof ObservableProxy ? source.observer : source) || new ObservableObject(undefined); + const proxy = source instanceof ObservableProxy ? source : new ObservableProxy(collection); + return { collection, proxy }; + }); + + const [filterConfig] = useState(() => { + if(!serializeName) { + return initialConfig; + } + const saved = localStorage.getItem(serializeName); + return saved ? JSON.parse(saved) : initialConfig; + }); + + const saveRef = useRef(debounce(() => localStorage.setItem(serializeName, JSON.stringify(filterConfig)), 500)) + + const filter = filterFunction || ((data: any[]) => { + const keyword = filterConfig[EFilterType.Search].keyword?.toUpperCase(); + + let filtered = data.filter(data => { + if (keyword && !data[filterConfig[EFilterType.Search].by[1]].toUpperCase().includes(keyword)) { + return; + } + + const flags = filterConfig[EFilterType.Flags]; + + if (flags) { + for (const key in flags) { + + // If null we should exclude data with such flag + // If true we should exclude data without such flag + // If false - flag not set + if ((flags[key] === null && data[key]) || (flags[key] && !data[key])) { + return; + } + } + } + + return true; + }); + + const order = filterConfig[EFilterType.Order]; + + if(order.by) { + const prop = order.by[0] + const direction = order.direction ?? EOrderDirection.Ascending; + + filtered = filtered.sort((a, b) => { + // For undefined values we should put them at the end or at the beginning + if (a[prop] === undefined) return direction === EOrderDirection.Ascending ? -1 : 1; + if (b[prop] === undefined) return direction === EOrderDirection.Ascending ? 1 : -1; + + if (a[prop] > b[prop]) { + return direction === 0 ? 1 : -1; + } + if (a[prop] < b[prop]) { + return direction === 0 ? -1 : 1; + } + return 0; + }) + } + return filtered + }) + + useEffect(() => { + state.proxy.addMiddleware(filter); + + return () => { + state.proxy.removeMiddleware(filter); + state.proxy.dispose(); + }; + }, [source]); + + return { + proxy: state.proxy, + collection: state.collection, + filterConfig, + callback: () => { + state.proxy.proxyCallback(state.collection.value); + if(serializeName) { + saveRef.current() + } + } + }; +}; \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/Header/NetworkIndicator.tsx b/SteamOrganizer.Web/src/components/Header/NetworkIndicator.tsx index 32957500..38ac6424 100644 --- a/SteamOrganizer.Web/src/components/Header/NetworkIndicator.tsx +++ b/SteamOrganizer.Web/src/components/Header/NetworkIndicator.tsx @@ -1,7 +1,7 @@ import {FC} from "react"; -import {flagStore, useFlagStore, useIsOffline} from "@/store/local.tsx"; +import {flagStore, useFlagStore, useIsOffline} from "@/store/local"; import {Icon, SvgIcon} from "@/defines"; -import {Tooltip} from "@/components/primitives/Popup.tsx"; +import {Tooltip} from "@/shared/ui/Popup/Tooltip"; export const NetworkIndicator: FC = () => { const isOffline = useIsOffline(); diff --git a/SteamOrganizer.Web/src/components/Header/UpdateIndicator.tsx b/SteamOrganizer.Web/src/components/Header/UpdateIndicator.tsx index f04c654a..6899373b 100644 --- a/SteamOrganizer.Web/src/components/Header/UpdateIndicator.tsx +++ b/SteamOrganizer.Web/src/components/Header/UpdateIndicator.tsx @@ -1,7 +1,6 @@ -import {FC, useState} from "react"; -import {ESavingState} from "@/components/Header/SaveIndicator.tsx"; +import {type FC} from "react"; import {Gradients, Icon, SvgIcon} from "src/defines"; -import {flagStore, useFlagStore} from "@/store/local.tsx"; +import {flagStore, useFlagStore} from "@/store/local"; export const UpdateIndicator: FC = () => { const [count,] = useFlagStore(nameof(flagStore.store.dbUpdateCount)); diff --git a/SteamOrganizer.Web/src/components/Header/index.tsx b/SteamOrganizer.Web/src/components/Header/index.tsx index 94336b00..8844d0d7 100644 --- a/SteamOrganizer.Web/src/components/Header/index.tsx +++ b/SteamOrganizer.Web/src/components/Header/index.tsx @@ -1,11 +1,13 @@ import {FC} from "react"; -import { setState } from "@/components/Sidebar/Sidebar.tsx"; +import { setState } from "@/components/Sidebar/Sidebar"; import { Icon, SvgIcon} from "src/defines"; -import {openSettings } from "@/pages/Modals/Settings.tsx"; +import {openSettings } from "@/pages/Modals/Settings"; import {SaveIndicator} from "./SaveIndicator"; -import {ESidebarState} from "@/types/uiMetadata.ts"; -import {UpdateIndicator} from "@/components/Header/UpdateIndicator.tsx"; -import {NetworkIndicator} from "@/components/Header/NetworkIndicator.tsx"; +import {ESidebarState} from "@/types/uiMetadata"; +import {UpdateIndicator} from "@/components/Header/UpdateIndicator"; +import {NetworkIndicator} from "@/components/Header/NetworkIndicator"; +import {EPlacement} from "@/shared/ui/Popup/positioning"; +import {Popup} from "@/shared/ui/Popup/Popup"; export const Header: FC = () => { @@ -22,7 +24,13 @@ export const Header: FC = () => { - + +

No notifications

+
+ }> + +
diff --git a/SteamOrganizer.Web/src/components/elements/Logo.tsx b/SteamOrganizer.Web/src/components/Logo.tsx similarity index 100% rename from SteamOrganizer.Web/src/components/elements/Logo.tsx rename to SteamOrganizer.Web/src/components/Logo.tsx diff --git a/SteamOrganizer.Web/src/components/Sidebar/Sidebar.module.pcss b/SteamOrganizer.Web/src/components/Sidebar/Sidebar.module.css similarity index 100% rename from SteamOrganizer.Web/src/components/Sidebar/Sidebar.module.pcss rename to SteamOrganizer.Web/src/components/Sidebar/Sidebar.module.css diff --git a/SteamOrganizer.Web/src/components/Sidebar/Sidebar.tsx b/SteamOrganizer.Web/src/components/Sidebar/Sidebar.tsx index 35bd1843..ddb378cb 100644 --- a/SteamOrganizer.Web/src/components/Sidebar/Sidebar.tsx +++ b/SteamOrganizer.Web/src/components/Sidebar/Sidebar.tsx @@ -3,21 +3,21 @@ import React, { type Dispatch, type FC, forwardRef, type ReactElement, - type ReactNode, type SetStateAction, useContext, useRef, useState } from 'react' -import {useSlider} from "@/hooks/useSlider.ts"; -import useMediaQuery from "@/hooks/useMediaQuery.ts"; +import {useSlider} from "@/shared/hooks/useSlider"; +import useMediaQuery from "@/shared/hooks/useMediaQuery"; import { useLocation, useNavigate} from "react-router-dom"; import {Gradients, Icon, SvgIcon} from "src/defines"; -import { popup, Tooltip} from "@/components/primitives/Popup.tsx"; -import {UserInfo} from "@/components/Sidebar/UserInfo.tsx"; -import styles from "./Sidebar.module.pcss" -import {ESidebarState} from "@/types/uiMetadata.ts"; -import {uiStore, useIsOffline} from "@/store/local.tsx"; +import {UserInfo} from "@/components/Sidebar/UserInfo"; +import styles from "./Sidebar.module.css" +import {ESidebarState} from "@/types/uiMetadata"; +import {uiStore, useIsOffline} from "@/store/local"; +import {Tooltip} from "@/shared/ui/Popup/Tooltip"; +import { popupDefaults } from "@/shared/ui/Popup/Popup"; interface ISidebarItemProps { icon: ReactElement @@ -73,13 +73,12 @@ const NavLink = forwardRef(({ link, icon, tex export const SidebarItem: FC = (props) => { const sidebar = useContext(SidebarContext) - return ( - + return sidebar.state === ESidebarState.Partial ? + - ); + : + ; } export const Sidebar: FC = () => { diff --git a/SteamOrganizer.Web/src/components/Sidebar/UserInfo.module.pcss b/SteamOrganizer.Web/src/components/Sidebar/UserInfo.module.css similarity index 100% rename from SteamOrganizer.Web/src/components/Sidebar/UserInfo.module.pcss rename to SteamOrganizer.Web/src/components/Sidebar/UserInfo.module.css diff --git a/SteamOrganizer.Web/src/components/Sidebar/UserInfo.tsx b/SteamOrganizer.Web/src/components/Sidebar/UserInfo.tsx index 1bf86345..1832c559 100644 --- a/SteamOrganizer.Web/src/components/Sidebar/UserInfo.tsx +++ b/SteamOrganizer.Web/src/components/Sidebar/UserInfo.tsx @@ -1,13 +1,14 @@ -import React, {FC, useState} from "react"; -import Button, {EButtonVariant} from "@/components/primitives/Button.tsx"; +import React, {type FC, useState} from "react"; +import Button, {EButtonVariant} from "@/shared/ui/Button"; import {Icon, SvgIcon} from "src/defines"; -import {useAuth} from "@/providers/authProvider.tsx"; -import {Loader} from "@/components/primitives/Loader.tsx"; +import {useAuth} from "@/providers/authProvider"; +import {Loader} from "@/shared/ui/Loader"; import {AnimatePresence, motion} from "framer-motion"; -import {popup, Tooltip} from "@/components/primitives/Popup.tsx"; -import styles from "./UserInfo.module.pcss" -import {ESidebarState} from "@/types/uiMetadata.ts"; -import {useIsOffline} from "@/store/local.tsx"; +import styles from "./UserInfo.module.css" +import {ESidebarState} from "@/types/uiMetadata"; +import {useIsOffline} from "@/store/local"; +import {popupDefaults} from "@/shared/ui/Popup/Popup"; +import {TooltipConditional} from "@/shared/ui/Popup/Tooltip"; export const UserInfo: FC<{state: ESidebarState}> = ({ state }) => { const {user, signIn, signOut } = useAuth() @@ -62,15 +63,15 @@ export const UserInfo: FC<{state: ESidebarState}> = ({ state }) => { transition={{delay: 0.05}} exit={{height: 0}}>
- + - +
} diff --git a/SteamOrganizer.Web/src/components/primitives/CheckBox/index.tsx b/SteamOrganizer.Web/src/components/primitives/CheckBox/index.tsx deleted file mode 100644 index f1f4cc51..00000000 --- a/SteamOrganizer.Web/src/components/primitives/CheckBox/index.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import {forwardRef, type HTMLAttributes, useState} from "react"; -import {cn} from "@/lib/utils.ts"; -import {IBindable} from "@/components/primitives/types/IBindable.ts"; - -interface ICheckBoxProps extends HTMLAttributes, IBindable { - allowIndeterminate?: boolean -} - -export const CheckBox = forwardRef(({ className, allowIndeterminate,bindKey, onChanged, bindTo, ...props }, ref) => { - const [checked, setChecked] = useState(bindTo && bindKey ? bindTo[bindKey] : false); - - const toggleCheck = () => { - setChecked(prev => { - const value = prev === null ? false : prev ? (allowIndeterminate ? null : false) : true; - bindTo && bindKey && (bindTo[bindKey] = value) && onChanged?.(); - return value; - }); - }; - - return ( - - ) -}) \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/primitives/Popup.tsx b/SteamOrganizer.Web/src/components/primitives/Popup.tsx deleted file mode 100644 index 0807d4e6..00000000 --- a/SteamOrganizer.Web/src/components/primitives/Popup.tsx +++ /dev/null @@ -1,250 +0,0 @@ -import {AnimatePresence, motion, MotionProps, type Point} from "framer-motion"; -import React, { - ButtonHTMLAttributes, - cloneElement, - FC, forwardRef, HTMLAttributes, - isValidElement, - ReactElement, - ReactNode, - useEffect, useImperativeHandle, - useRef, - useState -} from "react"; -import {createPortal} from "react-dom"; -import {cn} from "@/lib/utils.ts"; - -export const enum EPlacementX { - Center, - Left, - Right, -} - -export const enum EPlacementY { - Center, - Top, - Bottom -} - -type ContentType = (() => ReactElement | string) | ReactElement | string - -interface IPopupBaseProps extends MotionProps { - offset?: Point, - alignX?: EPlacementX, - alignY?: EPlacementY, - className?: string, - wrapIf?: boolean, -} - -interface ITooltipProps extends IPopupBaseProps { - children: ReactNode; - message?: ContentType; - openDelay?: number; - canHover?: boolean; -} - -export interface IPopupProps extends IPopupBaseProps{ - children: ReactNode; - content?: ContentType; -} - -const clampY = (anchor: DOMRect, tip: DOMRect, placement: EPlacementY, offset: number = 0) => { - const bottomPos = anchor.bottom + offset; - const topPos = anchor.top - tip.height - offset; - const middle = anchor.height / 2 - const centerPos = anchor.top + middle - (tip.height / 2); - - const isWithinBounds = (pos: number) => pos > 0 && pos + tip.height < window.innerHeight; - - switch (placement) { - case EPlacementY.Bottom: - return isWithinBounds(bottomPos) ? bottomPos : topPos; - case EPlacementY.Top: - return isWithinBounds(topPos) ? topPos : bottomPos; - case EPlacementY.Center: - return isWithinBounds(centerPos) ? centerPos : - (isWithinBounds(bottomPos) ? bottomPos - middle : topPos + middle) - } -}; - -const clampX = (anchor: DOMRect, tip: DOMRect, placement: EPlacementX, offset: number = 0) => { - const leftPos = anchor.left - tip.width - offset; - const rightPos = anchor.right + offset; - const centerPos = anchor.left + (anchor.width / 2) - (tip.width / 2); - - const isWithinBounds = (pos: number) => pos > 0 && pos + tip.width < window.innerWidth; - - switch (placement) { - case EPlacementX.Left: - return isWithinBounds(leftPos) ? leftPos : (isWithinBounds(rightPos) ? rightPos : centerPos); - - case EPlacementX.Right: - return isWithinBounds(rightPos) ? rightPos : (isWithinBounds(leftPos) ? leftPos : centerPos); - - case EPlacementX.Center: - const align = Math.min(window.innerWidth - (centerPos + tip.width), 0) - return Math.max(centerPos + align, Math.abs(offset)); - } -}; - -const rootId = document.getElementById("root") - -const getPopup = ( - popupRef: React.MutableRefObject, - triggerProps: { ref: React.MutableRefObject } & any, - children: ReactNode, - props: object, - isOpen: boolean, - className: string, - popup: any) => { - - let trigger: ReactElement = isValidElement(children) ? cloneElement(children, triggerProps) : -
- {children} -
- - return ( - <> - {trigger} - {createPortal( - - { - isOpen && popup && - - {typeof popup === 'function' ? popup() : popup} - - } - , - rootId - )} - - ); -} - -const align = (trigger: HTMLElement, - popup: HTMLElement, - alignX: EPlacementX, alignY: EPlacementY, offset: Point ) => { - const triggerRect = trigger.getBoundingClientRect() as DOMRect; - const popupRect = popup.getBoundingClientRect() - popup.style.top = clampY(triggerRect, popupRect, alignY, offset.y) + 'px' - popup.style.left = clampX(triggerRect, popupRect, alignX, offset.x) + 'px' -} - -export const Popup: FC = ({ - children, - className, - content, - alignX= EPlacementX.Right, - alignY = EPlacementY.Center, - offset= { x: 5, y: 0 }, - ...props - }) => { - const [isOpen, setOpen] = useState(false); - const triggerRef = useRef(null); - const popupRef = useRef(null); - - useEffect(() => { - if(!popupRef.current || !isOpen) { - return - } - const onClickOutside = () => setOpen(false); - align(triggerRef.current, popupRef.current, alignX, alignY, offset) - setTimeout(() => document.addEventListener('pointerdown', onClickOutside), 10) - return () => document.removeEventListener('pointerdown', onClickOutside) - }, [isOpen]); - - const triggerProps = { - ref: triggerRef, - onClick: () => setOpen(true), - } - return getPopup(popupRef, triggerProps, children, - props, - isOpen, className , content) -} - -export const Tooltip = forwardRef ( - ({ children, - message, - className, - alignX= EPlacementX.Center, - alignY = EPlacementY.Top, - offset= { x: 5, y: 5 }, - openDelay = 300, - canHover = true, - wrapIf = true, - ...props },ref) => { - - if(!wrapIf) { - return children; - } - - const [isOpen, setOpen] = useState(false); - const triggerRef = useRef(null); - const tipRef = useRef(null); - let timer: number; - - useImperativeHandle(ref, () => tipRef.current) - - useEffect(() => { - if(!tipRef.current) { - return - } - align(triggerRef.current, tipRef.current, alignX, alignY, offset) - }, [isOpen, children]); - - useEffect(() => { - const close = () => setOpen(false) - if(isOpen) { - document.addEventListener('touchstart', close) - } - return () => document.removeEventListener('touchstart', close) - }, [isOpen]); - - const debounceOpen = (e: MouseEvent) => { - clearTimeout(timer) - if(!isOpen) { - timer = window.setTimeout(() => { - if(e.relatedTarget) { - setOpen(true); - } - }, openDelay); - } - } - - const debounceClose = () => { - clearTimeout(timer) - if(isOpen) { - timer = window.setTimeout(() => setOpen(false), 50); - } - } - - const events = { - onMouseOver: debounceOpen, - onFocus: debounceOpen, - onMouseLeave: debounceClose, - } - - if(canHover) { - Object.assign(props, events) - } - - return getPopup(tipRef, { ref: triggerRef, ...events }, children, - {...props}, isOpen, className, message) - }) - -export const popup = { - side: { - openDelay: 0, - offset: { x: 20, y: 0 }, - alignX: EPlacementX.Right, - alignY: EPlacementY.Center, - initial: { opacity: 0, translateX: "-10px" }, - animate: { opacity: 1, translateX: 0 }, - exit: { opacity: 0, translateX: "10px" }, - canHover: false - } -} \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/primitives/RadioButton.tsx b/SteamOrganizer.Web/src/components/primitives/RadioButton.tsx deleted file mode 100644 index 186e9886..00000000 --- a/SteamOrganizer.Web/src/components/primitives/RadioButton.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import {FC, ReactNode, useState} from "react"; - -interface IRadioButtonGroupProps { - children: ReactNode[] - activeIndex?: number; - setActive: React.Dispatch>; -} - -interface IRadioButtonProps { - isActive: boolean; - children: ReactNode; - index: number; - setActive: React.Dispatch>; - className?: string; -} - - -export const RadioButtonGroup: FC = ({ children, activeIndex = 0, setActive }) => { - - return ( - children.map((child, index) => { - return - {child} - - }) - ) -} - -const RadioButton: FC = ({isActive, children, index, setActive, className}) => { - - return ( -
{ - setActive(index); - }} - className={`rounded transition-transform cursor-pointer hover:scale-110 ${isActive ? "bg-chip text-foreground-accent" : "grad-chip text-foreground"}`}> - {children} -
- ) -} \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/primitives/Tabs.tsx b/SteamOrganizer.Web/src/components/primitives/Tabs.tsx deleted file mode 100644 index 6dbbb3d6..00000000 --- a/SteamOrganizer.Web/src/components/primitives/Tabs.tsx +++ /dev/null @@ -1,64 +0,0 @@ -import {AnimatePresence, motion} from "framer-motion"; -import React, {FC, forwardRef, Key, ReactNode, useState} from "react"; -import {cn} from "@/lib/utils.ts"; - -interface ITabProps { - children: ReactNode, - title: ReactNode | ((active: boolean) => ReactNode), -} - -interface ITabPanelProps { - children: React.ReactElement | React.ReactElement[]; - activeKey: Key; - className?: string; - indicator?: ReactNode -} - -export const TabPanel: FC = forwardRef ( - ({ children, activeKey, className, indicator }, ref) => { - const [active, setActive] = useState(activeKey) - let renderContent: ReactNode; - - return ( -
-
- { - React.Children.map(children, (child) => { - let isActive: boolean = false; - if (child.key == active) { - renderContent = child - isActive = true - } - return ( - - ) - }) - } -
- -
- - - {renderContent} - - -
-
- ) -}) - -export const Tab: FC = ({ children}) => children diff --git a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/BaseVirtualLayout.ts b/SteamOrganizer.Web/src/components/primitives/VirtualScroller/BaseVirtualLayout.ts deleted file mode 100644 index c99bca8c..00000000 --- a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/BaseVirtualLayout.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {Dispatch, SetStateAction} from "react"; -import {Observer} from "@/lib/observer/observer.ts"; - -export abstract class BaseVirtualLayout { - protected readonly chunkSetter: Dispatch>; - protected readonly scroller: HTMLElement; - protected readonly observer: Observer>; - protected readonly sizer: HTMLDivElement; - protected readonly layout: HTMLDivElement; - public isInitialized: boolean = false; - public collection: ArrayLike; - - startRow: number = 0; - rowHeight: number = 0; - offsetIndex: number = 0; - limit: number = 0; - - public constructor(observer: Observer>, - chunkSetter: Dispatch>, - scroller: HTMLElement, - sizer: HTMLDivElement, - layout: HTMLDivElement) { - - this.collection = observer.value; - this.chunkSetter = chunkSetter; - this.scroller = scroller; - this.observer = observer; - this.sizer = sizer; - this.layout = layout; - observer.onChanged(this.onChangedCallback) - } - - protected onChangedCallback = (data: ArrayLike) => { - this.collection = data - this.refresh() - } - - public abstract render() : void; - public abstract refresh(reset?: boolean) : void; - public dispose() - { - this.observer.unsubscribe(this.onChangedCallback) - } -} \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/GridLayout.ts b/SteamOrganizer.Web/src/components/primitives/VirtualScroller/GridLayout.ts deleted file mode 100644 index ff38f1d0..00000000 --- a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/GridLayout.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { BaseVirtualLayout } from "./BaseVirtualLayout.ts"; - -export class GridLayout extends BaseVirtualLayout { - public columns: number = NaN; - private timer: number = 0; - private resizeObserver: ResizeObserver; - - public constructor(...args: ConstructorParameters) { - super(...args); - - this.resizeObserver = new ResizeObserver(() => { - clearTimeout(this.timer) - this.timer = window.setTimeout(() => { - this.refresh(false) - this.timer = 0; - },30) - }); - this.resizeObserver.observe(this.sizer) - } - - public override dispose() { - super.dispose() - this.resizeObserver.disconnect() - } - - private calculateSizes() { - const gridStyles = window.getComputedStyle(this.layout); - - const row = gridStyles.getPropertyValue('grid-template-rows').match(/[\d.]+/)?.[0]; - const col = gridStyles.getPropertyValue('grid-template-columns').match(/[\d.]+/)?.[0]; - if(!row || !col) { - return - } - const columnGap = parseFloat(gridStyles.getPropertyValue('grid-column-gap')); - const rowGap = parseFloat(gridStyles.getPropertyValue('grid-row-gap')); - const columnWidth = parseFloat(col) + columnGap; - this.rowHeight = parseFloat(row) + rowGap; - this.columns = Math.ceil(this.layout.clientWidth / columnWidth); - - if(!this.isInitialized) { - this.isInitialized = true; - } - } - - public render() { - if(!this.isInitialized) { - if(this.collection?.length > 0) { - this.chunkSetter([0]) - } - return - } - const visibleRows =Math.ceil(this.scroller.clientHeight / this.rowHeight); - this.startRow = Math.floor(this.scroller.scrollTop / this.rowHeight) - const renderIndex = Math.max(this.startRow * this.columns, 0); - - const endIndex = Math.min( - ((this.startRow + 1) * this.columns) + (this.columns * visibleRows), - this.collection.length - ); - const limitCount = endIndex - renderIndex; - - if(isNaN(limitCount) || (renderIndex === this.offsetIndex && limitCount === this.limit)) { - return; - } - this.limit = limitCount; - this.offsetIndex = renderIndex - this.chunkSetter(Array.from({ length: limitCount }, - (_,i) => i + renderIndex)); - } - - public refresh (reset: boolean = true) { - - if(reset) { - this.offsetIndex = -1; - } - - this.calculateSizes() - const height = this.collection?.length / this.columns * this.rowHeight; - this.sizer.style.height = height ? `${height}px` : null; - this.render() - } -} \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/StackLayout.ts b/SteamOrganizer.Web/src/components/primitives/VirtualScroller/StackLayout.ts deleted file mode 100644 index df60fb6b..00000000 --- a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/StackLayout.ts +++ /dev/null @@ -1,22 +0,0 @@ -import {BaseVirtualLayout} from "@/components/primitives/VirtualScroller/BaseVirtualLayout.ts"; - -export class StackLayout extends BaseVirtualLayout { - - public isInitialized: boolean = true; - - public render() { - this.rowHeight = 200; - this.startRow = Math.floor(this.scroller.scrollTop / this.rowHeight) - const visibleRows =Math.ceil(this.scroller.clientHeight / this.rowHeight); - const endIndex = Math.min(this.startRow + visibleRows, this.collection.length); - const limitCount = endIndex - this.startRow; - - this.chunkSetter(Array.from({ length: limitCount }, - (_,i) => i + this.startRow)); - } - - public refresh() { - this.render() - } - -} \ No newline at end of file diff --git a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/VirtualScroller.tsx b/SteamOrganizer.Web/src/components/primitives/VirtualScroller/VirtualScroller.tsx deleted file mode 100644 index b7623016..00000000 --- a/SteamOrganizer.Web/src/components/primitives/VirtualScroller/VirtualScroller.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React, {FC, ReactElement, ReactNode, useEffect, useRef, useState} from "react"; -import {useScrollbar} from "@/hooks/useScrollbar.ts"; -import {BaseVirtualLayout} from "./BaseVirtualLayout.ts"; -import {Observer} from "@/lib/observer/observer.ts"; - -interface IVirtualScrollerProps { - className?: string; - collection: Observer; - renderElement: (object: any, index: number) => ReactNode; - layout: typeof BaseVirtualLayout, - emptyIndicator?: ReactElement, - searchEmptyIndicator?: ReactElement, - useDragMoving?: boolean -} - -const VirtualScroller: FC = ( - { - collection, - renderElement, - layout, - className, - emptyIndicator, - searchEmptyIndicator, - useDragMoving -}) => { - const [items, setItems] = useState([]) - const areaRef = useRef(null!); - const sizerRef = useRef(null!); - const layoutRef = useRef(); - - const {hostRef, scrollRef} = useScrollbar({ - scroll: () => layoutRef.current!.render() - }); - - useEffect(() => { - - if(useDragMoving) { - scrollRef.current.setAttribute("drag-scroller",""); - } - - // @ts-ignore - We are sure that layout is not abstract - layoutRef.current = new layout(collection, setItems, - scrollRef.current!, sizerRef.current, areaRef.current) - - return () => layoutRef.current?.dispose() - },[]) - - useEffect(() => { - if(!layoutRef.current.isInitialized) { - layoutRef.current.refresh() - } - },[items]) - - let padding = 0, top = 0, opacity = 0; - - if(layoutRef.current) { - const layout = layoutRef.current; - padding = layout.rowHeight * layout?.startRow; - top = padding + layout.rowHeight; - - if(layout.isInitialized) { - opacity = 1; - } - } - - - return ( -
-
-
- {items.map((i) => { - return renderElement(layoutRef.current.collection[i], i); - } - )} -
- {collection.value?.length < 1 ? emptyIndicator : (items.length < 1 && searchEmptyIndicator )} -
-
- ); -}; - -export default VirtualScroller; \ No newline at end of file diff --git a/SteamOrganizer.Web/src/defines/index.css b/SteamOrganizer.Web/src/defines/index.css index 7e1bb74c..5eb54de0 100644 --- a/SteamOrganizer.Web/src/defines/index.css +++ b/SteamOrganizer.Web/src/defines/index.css @@ -23,4 +23,9 @@ .os-theme-dark, .os-theme-light { --os-size: 7px; +} + +.animate-test { + animation: ping 1s cubic-bezier(0, 0, 0.2, 1); + animation-iteration-count: 1; } \ No newline at end of file diff --git a/SteamOrganizer.Web/src/defines/index.tsx b/SteamOrganizer.Web/src/defines/index.tsx index f6796c7f..5e2644d4 100644 --- a/SteamOrganizer.Web/src/defines/index.tsx +++ b/SteamOrganizer.Web/src/defines/index.tsx @@ -76,7 +76,9 @@ export const enum Icon { AccountAdd, SearchInList, NoDataToDisplay, - PushBackupCloud + PushBackupCloud, + Copy, + Sack } export function Defs() { diff --git a/SteamOrganizer.Web/src/entity/account.ts b/SteamOrganizer.Web/src/entity/account.ts index 3fa9a8f9..8244170e 100644 --- a/SteamOrganizer.Web/src/entity/account.ts +++ b/SteamOrganizer.Web/src/entity/account.ts @@ -1,8 +1,15 @@ -import { type EVisibilityState, type SteamPlayerBans, type SteamPlayerGames, type SteamPlayerSummary} from "@/types/steamPlayerSummary.ts"; -import {getPlayerInfo, getSteamTime} from "@/services/steamApi.ts"; +import { + EEconomyBanType, + type EVisibilityState, + type SteamPlayerBans, + type SteamPlayerGames, + type SteamPlayerSummary +} from "@/types/steamPlayerSummary"; +import {getPlayerInfo, getSteamTime} from "@/shared/api/steamApi"; import type {IAccountCredential} from "@/types/accountCredentials.ts"; -import {accounts} from "@/store/accounts.ts"; -import {ISteamAuth} from "@/entity/steamAuth.ts"; +import {accounts} from "@/store/accounts"; +import {ISteamAuth} from "@/entity/steamAuth"; +import {ETimeUnit, formatTimeDifference, TimeFormat} from "@/shared/lib/timeFormatting"; const codeInterval = 30; const steamCodeCharacters = "23456789BCDFGHJKMNPQRTVWXY"; @@ -43,6 +50,36 @@ export class Account { _years?: number; + private _getBanSinceInfo() { + const total = this.gameBansCount + this.vacBansCount; + return this.daysSinceLastBan ? + (total > 1 ? "\nLast ban received " : "\nBan received ") + + `${formatTimeDifference(this.daysSinceLastBan, ETimeUnit.Days, TimeFormat.Years | TimeFormat.Days)} ago` : + null + } + + public getVacBanInfo() { + return this.vacBansCount ? + `This account has been banned by VAC${(this.vacBansCount > 1 ? ` in several games (${this.vacBansCount})` : "")}.${this._getBanSinceInfo()}` : + null + } + + public getGameBanInfo() { + return this.gameBansCount ? + `This account has a game ban${(this.gameBansCount > 1 ? ` in several games (${this.gameBansCount})` : "")}.${this._getBanSinceInfo()}` : + null + } + + public getCommunityBanInfo() { + return this.haveCommunityBan ? "The account is not allowed to interact with the Steam community" : null; + } + + public getTradeBanInfo() { + return `${(this.economyBan === EEconomyBanType.Probation ? + "The account is temporarily blocked." : "The account is permanently blocked." + )} Trade/exchange/sending of gifts is prohibited on this account` + } + public getYears() { if(!this.createdDate) { return diff --git a/SteamOrganizer.Web/src/layouts/MainLayout.tsx b/SteamOrganizer.Web/src/layouts/MainLayout.tsx index d9579920..99f4fe5a 100644 --- a/SteamOrganizer.Web/src/layouts/MainLayout.tsx +++ b/SteamOrganizer.Web/src/layouts/MainLayout.tsx @@ -1,8 +1,8 @@ import { type FC } from "react"; -import {Sidebar } from "../components/Sidebar/Sidebar.tsx"; +import {Sidebar } from "@/components/Sidebar/Sidebar"; import {Header} from "@/components/Header"; import { Outlet } from "react-router-dom"; -import {RouterProvider} from "@/providers/routerProvider.tsx"; +import {RouterProvider} from "@/providers/routerProvider"; diff --git a/SteamOrganizer.Web/src/main.tsx b/SteamOrganizer.Web/src/main.tsx index 693c55c1..8e12feda 100644 --- a/SteamOrganizer.Web/src/main.tsx +++ b/SteamOrganizer.Web/src/main.tsx @@ -1,11 +1,11 @@ import React from 'react' import ReactDOM from 'react-dom/client' -import App from './App.tsx' +import App from './App' import 'overlayscrollbars/overlayscrollbars.css'; import '@/defines/index.css' -import { ClickScrollPlugin, OverlayScrollbars } from "overlayscrollbars"; +import {ClickScrollPlugin, OverlayScrollbars} from "overlayscrollbars"; -if(import.meta.env.MODE === 'production') { +if(import.meta.env.VITE_SKIP_SW === true) { if ('serviceWorker' in navigator) { navigator.serviceWorker.register('/sw.js') } diff --git a/SteamOrganizer.Web/src/pages/Accounts/Accounts.tsx b/SteamOrganizer.Web/src/pages/Accounts/Accounts.tsx index 0cfc066d..3bf86952 100644 --- a/SteamOrganizer.Web/src/pages/Accounts/Accounts.tsx +++ b/SteamOrganizer.Web/src/pages/Accounts/Accounts.tsx @@ -1,46 +1,54 @@ -import AccountCard from "./elements/AccountCard.tsx"; -import AccountsNav from "./elements/AccountsNav.tsx"; -import VirtualScroller from "@/components/primitives/VirtualScroller/VirtualScroller.tsx"; -import {GridLayout} from "@/components/primitives/VirtualScroller/GridLayout.ts"; -import {accounts} from "@/store/accounts.ts"; -import {LoaderStatic} from "@/components/primitives/Loader.tsx"; -import {useLoader} from "@/hooks/useLoader.ts"; -import {useEffect} from "react"; -import {setDocumentTitle} from "@/lib/utils.ts"; -import {ObservableProxy} from "@/lib/observer/observableProxy.ts"; -import {Account} from "@/entity/account.ts"; -import {Icon } from "@/defines"; -import Button, {EButtonVariant} from "@/components/primitives/Button.tsx"; -import {openAddAccount} from "@/pages/Modals/AddAccount.tsx"; -import {CollectionIndicator, SearchCollectionIndicator} from "@/components/elements/CollectionIndicator.tsx"; -import Logo from "@/components/elements/Logo.tsx"; +import AccountCard from "./components/AccountCard"; +import AccountsNav from "./components/AccountsNav"; +import {GridLayout} from "@/shared/ui/VirtualScroller/GridLayout"; +import {accounts} from "@/store/accounts"; +import {LoaderStatic} from "@/shared/ui/Loader"; +import {useObservableLoader} from "@/shared/hooks/useObservableLoader"; +import {Account} from "@/entity/account"; +import {Icon} from "@/defines"; +import Button, {EButtonVariant} from "@/shared/ui/Button"; +import {CollectionIndicator, SearchCollectionIndicator} from "@/components/CollectionIndicator"; +import {VirtualScroller} from "@/shared/ui/VirtualScroller/VirtualScroller"; +import {openAddAccount} from "@/pages/Modals/AddAccount"; +import {ObservableProxy} from "@/shared/lib/observer/observableProxy"; +import {EFilterType, IFilterConfig} from "@/components/FilterInput/types"; +import {useFilterManager} from "@/components/FilterInput/useFilterManager"; +import {useTitle} from "@/shared/hooks/useTitle"; -const filterProxy = new ObservableProxy(accounts) +const accountsProxy = new ObservableProxy(accounts) +const defaultConfig = { + [EFilterType.Search]: { by: [0, nameof(Account.prototype.nickname)] }, + [EFilterType.Flags]: {}, + [EFilterType.Order]: {} +} as IFilterConfig export default function Accounts() { - const isLoading = useLoader(accounts) - useEffect(() => setDocumentTitle('Accounts'), []); + useTitle('Accounts') + const isLoading = useObservableLoader(accounts) + const { proxy, callback, filterConfig } = useFilterManager(defaultConfig, accountsProxy, "accountsFilter"); + + if(isLoading) { + return + } return ( - - {isLoading ? : - Import backup or add new account
using the button below}> - - - } - searchEmptyIndicator={} - renderElement={(o: Account, i) => } - /> - } + + accounts.value.length ? : + Import backup or add new account
using the button below}> + +
+ } + onRenderElement={(o: Account, i) => } + />
-) + ) } \ No newline at end of file diff --git a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountCard.module.pcss b/SteamOrganizer.Web/src/pages/Accounts/components/AccountCard.module.css similarity index 84% rename from SteamOrganizer.Web/src/pages/Accounts/elements/AccountCard.module.pcss rename to SteamOrganizer.Web/src/pages/Accounts/components/AccountCard.module.css index 458002e5..d6f1958b 100644 --- a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountCard.module.pcss +++ b/SteamOrganizer.Web/src/pages/Accounts/components/AccountCard.module.css @@ -31,19 +31,24 @@ } } } - -.banChip { - @apply chip bg-close +.bans > div { + @apply chip bg-close; } .anyBans { - @apply chip bg-success inline-block + height: 20px; + @apply chip bg-success inline-block; } .id { - @apply text-foreground font-medium w-full mt-2 mb-3 + @apply text-foreground font-medium cursor-pointer w-fit mt-2 mb-3 +} + +.id b { + @apply text-secondary; } + .nick { @apply text-sm text-nowrap overflow-hidden text-ellipsis } @@ -54,6 +59,10 @@ margin-bottom: 5px; } +.topInfo > div { + @apply chip; +} + .main { @apply ml-3 overflow-hidden } diff --git a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountCard.tsx b/SteamOrganizer.Web/src/pages/Accounts/components/AccountCard.tsx similarity index 68% rename from SteamOrganizer.Web/src/pages/Accounts/elements/AccountCard.tsx rename to SteamOrganizer.Web/src/pages/Accounts/components/AccountCard.tsx index a260eda3..c9399ba5 100644 --- a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountCard.tsx +++ b/SteamOrganizer.Web/src/pages/Accounts/components/AccountCard.tsx @@ -1,16 +1,17 @@ -import type { Account } from "@/entity/account.ts"; +import type {Account} from "@/entity/account.ts"; import React, {type FC, useContext, useRef} from "react"; import {Link} from "react-router-dom"; -import { Icon, SvgIcon} from "src/defines"; -import {accounts, saveDbMutation} from "@/store/accounts.ts"; -import styles from "./AccountCard.module.pcss" -import {defaultAvatar} from "@/store/config.ts"; -import {Tooltip} from "@/components/primitives/Popup.tsx"; -import {dateFormatter} from "@/lib/utils.ts"; -import {ConfirmPopup} from "@/components/elements/ConfirmPopup.tsx"; -import {AccountsContext} from "@/pages/Accounts/elements/AccountsNav.tsx"; -import {Draggable} from "@/components/primitives/Draggable.tsx"; -import {flagStore, useFlagStore} from "@/store/local.tsx"; +import {Icon, SvgIcon} from "src/defines"; +import {accounts, saveDbMutation} from "@/store/accounts"; +import styles from "./AccountCard.module.css" +import {defaultAvatar} from "@/store/config"; +import {Tooltip, TooltipConditional} from "@/shared/ui/Popup/Tooltip"; +import {dateFormatter} from "@/shared/lib/timeFormatting"; +import {ConfirmPopup} from "@/components/ConfirmPopup"; +import {AccountsContext} from "@/pages/Accounts/components/AccountsNav"; +import {Draggable} from "@/shared/ui/Draggable"; +import {flagStore, useFlagStore} from "@/store/local"; +import {CopyArea} from "@/shared/ui/CopyButton/CopyButton"; interface IAccountCardProps { acc: Account, @@ -65,7 +66,7 @@ const CardMain: FC
- +

Account added: {dateFormatter.format(acc.addedDate)} {acc.lastUpdateDate && `\nAccount updated: ${dateFormatter.format(acc.lastUpdateDate)}`} @@ -86,8 +87,7 @@ const CardMain: FC } { acc.authenticator && - "This account is 2FA protected\nClick to copy the code"}> + "This account is 2FA protected\nClick to copy the code"}> navigator.clipboard.writeText(await acc.generate2faCode() ?? "")} className="fill-success cursor-pointer"/> @@ -98,7 +98,10 @@ const CardMain: FC

- {acc.nickname} + 300 ? acc.note.substring(0, 300) + " ..." : acc.note}> + {acc.nickname} + @@ -109,25 +112,32 @@ const CardMain: FC
-
Level: {acc.steamLevel ?? '—'}
-
Years: {acc.getYears() ?? '—'}
+
Level: {acc.steamLevel ?? '—'}
+ `Registration: ` + dateFormatter.format(acc.createdDate)}> +
Years: {acc.getYears() ?? '—'}
+
+
{bansCount ?
- {bansCount > 2 && +{bansCount - 2}} - {!!acc.vacBansCount &&
VAC
} - {!!acc.gameBansCount &&
Game
} - {!!acc.haveCommunityBan &&
Community
} - {!!acc.economyBan &&
Trade
} -
: -
-
No bans ✓
-
+ {bansCount > 2 && +{bansCount - 2}} + {!!acc.vacBansCount && + acc.getVacBanInfo()}> +
VAC
+
} + {!!acc.gameBansCount && + acc.getGameBanInfo()}> +
Game
+
} + {!!acc.haveCommunityBan && acc.getCommunityBanInfo()}>
Community
} + {!!acc.economyBan && acc.getTradeBanInfo()}>
Trade
} +
:
No bans ✓
} -

ID: {acc.id ?? '—'}

+ + ID: {acc.id ?? '—'} +
diff --git a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountsNav.module.pcss b/SteamOrganizer.Web/src/pages/Accounts/components/AccountsNav.module.css similarity index 100% rename from SteamOrganizer.Web/src/pages/Accounts/elements/AccountsNav.module.pcss rename to SteamOrganizer.Web/src/pages/Accounts/components/AccountsNav.module.css diff --git a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountsNav.tsx b/SteamOrganizer.Web/src/pages/Accounts/components/AccountsNav.tsx similarity index 73% rename from SteamOrganizer.Web/src/pages/Accounts/elements/AccountsNav.tsx rename to SteamOrganizer.Web/src/pages/Accounts/components/AccountsNav.tsx index d1d25dff..9c38b4b1 100644 --- a/SteamOrganizer.Web/src/pages/Accounts/elements/AccountsNav.tsx +++ b/SteamOrganizer.Web/src/pages/Accounts/components/AccountsNav.tsx @@ -1,61 +1,39 @@ -import React, {createContext, type FC, type ReactNode, useEffect, useRef, useState} from "react"; +import React, { type ChangeEvent, createContext, type FC, type ReactNode, useRef, useState} from "react"; import clsx from "clsx"; -import styles from "./AccountsNav.module.pcss"; -import Input from "@/components/primitives/Input.tsx"; +import styles from "./AccountsNav.module.css"; import {Gradients, Icon, SvgIcon} from "src/defines"; -import {modal} from "@/components/primitives/Modal.tsx"; -import {AddAccount, openAddAccount} from "@/pages/Modals/AddAccount.tsx"; -import {config} from "@/store/config.ts"; -import {toast, ToastVariant} from "@/components/primitives/Toast.tsx"; -import {ToggleButton} from "@/components/primitives/ToggleButton.tsx"; -import {IDraggableContext, IDraggableInfo} from "@/components/primitives/Draggable.tsx"; -import {openSettings} from "@/pages/Modals/Settings.tsx"; -import {type ObservableProxy} from "@/lib/observer/observableProxy.ts"; -import {type Account} from "@/entity/account.ts"; -import {accounts, importAccounts, updateAccounts} from "@/store/accounts.ts"; -import {ExportData} from "@/pages/Modals/ExportData.tsx"; -import {formatFileDate} from "@/lib/utils.ts"; -import {Tooltip} from "@/components/primitives/Popup.tsx"; -import {verifyVersion} from "@/services/cryptography.ts"; -import {DecryptionPopup} from "@/pages/Modals/Authentication.tsx"; -import {flagStore, useFlagStore} from "@/store/local.tsx"; +import {modal} from "@/shared/ui/Modal"; +import {openAddAccount } from "@/pages/Modals/AddAccount"; +import {toast, ToastVariant} from "@/shared/ui/Toast"; +import {ToggleButton} from "@/shared/ui/ToggleButton"; +import {IDraggableContext, IDraggableInfo} from "@/shared/ui/Draggable"; +import {accounts, importAccounts, updateAccounts} from "@/store/accounts"; +import {ExportData} from "@/pages/Modals/ExportData"; +import {formatFileDate} from "@/shared/lib/timeFormatting"; +import {verifyVersion} from "@/shared/services/cryptography"; +import {DecryptionPopup} from "@/pages/Modals/Authentication"; +import {flagStore, useFlagStore} from "@/store/local"; +import {Tooltip} from "@/shared/ui/Popup/Tooltip"; +import {FilterInput} from "@/components/FilterInput/FilterInput"; +import {IFilterConfig} from "@/components/FilterInput/types"; +import {accountsFilters} from "@/pages/Accounts/filtersDefinition"; interface IAccountsNavProps { children: ReactNode; - proxy: ObservableProxy; + callback: () => void; + filterConfig: IFilterConfig } export const AccountsContext = createContext(undefined!) -const AccountsNav: FC = ({ children, proxy }) => { +const AccountsNav: FC = ({ children, callback, filterConfig }) => { const [expanded, setExpanded] = useState(false); const [isDragging, setDragging] = useState(false); const [isDragState, setDragState] = useState(false); const infoRef = useRef({}); - const searchRef = useRef(null!); const fileInputRef = useRef(null!); const [isUpdating] = useFlagStore(nameof(flagStore.store.isDbUpdating)) - useEffect(() => { - const filter = (data: Account[]) => { - if(!searchRef.current.value) return data; - return data.filter(acc => - acc.nickname.toUpperCase().includes(searchRef.current.value.toUpperCase()) - ) - } - - const onInput = () => proxy.proxyCallback(accounts.value); - - proxy.addMiddleware(filter); - searchRef.current.addEventListener("input", onInput); - - return () => { - proxy.removeMiddleware(filter); - searchRef.current?.removeEventListener("input", onInput); - } - }, []); - - const addClicked = () => { setExpanded(false) openAddAccount() @@ -70,7 +48,7 @@ const AccountsNav: FC = ({ children, proxy }) => { }) } - const importFileSelected = (e: React.ChangeEvent) => { + const importFileSelected = (e: ChangeEvent) => { setExpanded(false) if(!e.target.files?.length) return; @@ -107,7 +85,7 @@ const AccountsNav: FC = ({ children, proxy }) => {