feat: Accounts page
This commit is contained in:
parent
60425c1acd
commit
1bdebb0f94
13 changed files with 236 additions and 48 deletions
|
|
@ -4,12 +4,18 @@ import Popup from '../overlay/Popup';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import styles from './Dropdown.module.scss';
|
import styles from './Dropdown.module.scss';
|
||||||
|
|
||||||
export type DropdownProps = JSX.HTMLAttributes<HTMLDivElement>;
|
export type DropdownProps = Omit<JSX.HTMLAttributes<HTMLDivElement>, 'onChange'> & {
|
||||||
|
onChange?: (selected: number) => any;
|
||||||
|
text?: string;
|
||||||
|
selected?: number;
|
||||||
|
};
|
||||||
|
|
||||||
function Dropdown(props: DropdownProps) {
|
function Dropdown(props: DropdownProps) {
|
||||||
const [split, rest] = splitProps(props, ['children', 'class']);
|
const [split, rest] = splitProps(props, ['children', 'class', 'text', 'onChange', 'selected']);
|
||||||
const [visible, setVisible] = createSignal(false);
|
const [visible, setVisible] = createSignal(false);
|
||||||
const [selected, setSelected] = createSignal<number>(0);
|
|
||||||
|
// eslint-disable-next-line solid/reactivity
|
||||||
|
const [selected, setSelected] = createSignal<number>(split.selected || 0);
|
||||||
|
|
||||||
let ref!: HTMLDivElement;
|
let ref!: HTMLDivElement;
|
||||||
|
|
||||||
|
|
@ -18,6 +24,9 @@ function Dropdown(props: DropdownProps) {
|
||||||
function select(index: number) {
|
function select(index: number) {
|
||||||
setSelected(index);
|
setSelected(index);
|
||||||
setVisible(false);
|
setVisible(false);
|
||||||
|
|
||||||
|
if (split.onChange)
|
||||||
|
split.onChange(selected());
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -29,8 +38,9 @@ function Dropdown(props: DropdownProps) {
|
||||||
iconRight={<ChevronDownIcon />}
|
iconRight={<ChevronDownIcon />}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="flex-1 h-full mt-px overflow-hidden"
|
class="flex-1 flex flex-row items-center text-nowrap gap-1 h-full overflow-hidden"
|
||||||
>
|
>
|
||||||
|
<span>{split.text}</span>
|
||||||
{items()[selected()]}
|
{items()[selected()]}
|
||||||
</div>
|
</div>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
||||||
16
desktop/src/ui/components/base/Tooltip.module.scss
Normal file
16
desktop/src/ui/components/base/Tooltip.module.scss
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
.tooltip {
|
||||||
|
@apply relative;
|
||||||
|
|
||||||
|
&:hover::after {
|
||||||
|
@apply opacity-100;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: attr(data-text);
|
||||||
|
@apply opacity-0 transition-opacity;
|
||||||
|
@apply absolute left-1/2 -translate-x-1/2 bottom-6;
|
||||||
|
@apply min-w-44 w-max max-w-52;
|
||||||
|
@apply backdrop-blur-lg rounded-md bg-black/60 p-2 text-center text-sm text-wrap text-white shadow shadow-black/30;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
16
desktop/src/ui/components/base/Tooltip.tsx
Normal file
16
desktop/src/ui/components/base/Tooltip.tsx
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
import { type JSX, splitProps } from 'solid-js';
|
||||||
|
import styles from './Tooltip.module.scss';
|
||||||
|
|
||||||
|
export type TooltipProps = JSX.HTMLAttributes<HTMLDivElement> & {
|
||||||
|
text: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Tooltip(props: TooltipProps) {
|
||||||
|
const [split, rest] = splitProps(props, ['class']);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div data-text={props.text} class={`${styles.tooltip} ${split.class}`} {...rest} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
|
|
@ -1,19 +1,38 @@
|
||||||
import { createResource } from 'solid-js';
|
import { Show, createSignal, splitProps } from 'solid-js';
|
||||||
import type { JSX } from 'solid-js';
|
import type { JSX } from 'solid-js';
|
||||||
import steveSrc from '../../../assets/images/steve.png';
|
import steveSrc from '../../../assets/images/steve.png';
|
||||||
|
|
||||||
type PlayerHeadProps = JSX.IntrinsicElements['img'] & { uuid: string };
|
type PlayerHeadProps = JSX.IntrinsicElements['img'] & {
|
||||||
|
uuid: string;
|
||||||
|
onError?: () => any;
|
||||||
|
};
|
||||||
|
|
||||||
async function fetchHeadSrc(uuid: string) {
|
function headSrc(uuid: string) {
|
||||||
return `https://crafatar.com/avatars/${uuid}?size=32`;
|
return `https://crafatar.com/avatars/${uuid}?size=48`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlayerHead(props: PlayerHeadProps) {
|
function PlayerHead(props: PlayerHeadProps) {
|
||||||
const [headSrc] = createResource(() => props.uuid, fetchHeadSrc, {
|
const [split, rest] = splitProps(props, ['uuid', 'class']);
|
||||||
initialValue: steveSrc,
|
const [isLoading, setLoading] = createSignal(true);
|
||||||
});
|
|
||||||
|
|
||||||
return <img src={headSrc()} {...props} />;
|
return (
|
||||||
|
<>
|
||||||
|
<Show when={isLoading() === true}>
|
||||||
|
<img
|
||||||
|
src={steveSrc}
|
||||||
|
class={`image-render-pixel ${split.class}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
|
<img
|
||||||
|
src={headSrc(split.uuid)}
|
||||||
|
onLoad={() => setLoading(false)}
|
||||||
|
onError={() => props.onError && props.onError()}
|
||||||
|
class={`image-render-pixel ${isLoading() ? 'hidden' : ''} ${split.class}`}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PlayerHead;
|
export default PlayerHead;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { LogOut01Icon, PlusIcon, Settings01Icon } from '@untitled-theme/icons-solid';
|
import { LogOut01Icon, PlusIcon, Settings01Icon } from '@untitled-theme/icons-solid';
|
||||||
|
import { useNavigate } from '@solidjs/router';
|
||||||
import Button from '../base/Button';
|
import Button from '../base/Button';
|
||||||
import PlayerHead from '../game/PlayerHead';
|
import PlayerHead from '../game/PlayerHead';
|
||||||
import useAccount from '../../hooks/useAccount';
|
import useAccount from '../../hooks/useAccount';
|
||||||
|
|
@ -33,6 +34,7 @@ function AccountComponent(props: AccountComponentProps) {
|
||||||
|
|
||||||
function AccountPopup(props: Popup.PopupProps) {
|
function AccountPopup(props: Popup.PopupProps) {
|
||||||
const loggedInAccount = useAccount();
|
const loggedInAccount = useAccount();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popup {...props}>
|
<Popup {...props}>
|
||||||
|
|
@ -53,7 +55,14 @@ function AccountPopup(props: Popup.PopupProps) {
|
||||||
<Button buttonStyle="ghost" iconLeft={<PlusIcon />}>Add Account</Button>
|
<Button buttonStyle="ghost" iconLeft={<PlusIcon />}>Add Account</Button>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<Button buttonStyle="icon" large>
|
<Button
|
||||||
|
buttonStyle="icon"
|
||||||
|
large
|
||||||
|
onClick={() => {
|
||||||
|
props.setVisible(false);
|
||||||
|
navigate('/settings/accounts');
|
||||||
|
}}
|
||||||
|
>
|
||||||
<Settings01Icon class="stroke-fg-primary" />
|
<Settings01Icon class="stroke-fg-primary" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
function useAccount(): Core.Account {
|
function useAccount() {
|
||||||
// TODO return the currently logged in account
|
// TODO return the currently logged in account
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import { type Resource, type ResourceReturn, createResource } from 'solid-js';
|
import { type ResourceReturn, createResource } from 'solid-js';
|
||||||
import type { Result } from '~bindings';
|
import type { Result } from '~bindings';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -18,6 +18,7 @@ import ClusterCover from '~ui/components/game/ClusterCover';
|
||||||
import type { Cluster } from '~bindings';
|
import type { Cluster } from '~bindings';
|
||||||
import useCommand from '~ui/hooks/useCommand';
|
import useCommand from '~ui/hooks/useCommand';
|
||||||
import { bridge } from '~index';
|
import { bridge } from '~index';
|
||||||
|
import { upperFirst } from '~utils/string';
|
||||||
|
|
||||||
// TODO: Replace this into it's own component
|
// TODO: Replace this into it's own component
|
||||||
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
|
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
|
||||||
|
|
@ -142,9 +143,9 @@ function ClusterCard(props: Cluster) {
|
||||||
</div>
|
</div>
|
||||||
<div class="z-10 p-3 flex flex-row items-center justify-between">
|
<div class="z-10 p-3 flex flex-row items-center justify-between">
|
||||||
<div class="flex flex-col gap-1.5">
|
<div class="flex flex-col gap-1.5">
|
||||||
<p class="h-4 font-semibold">{props.meta.name}</p>
|
<p class="h-4 font-medium">{props.meta.name}</p>
|
||||||
<p class="h-4 text-xs">
|
<p class="h-4 text-xs">
|
||||||
{props.meta.loader}
|
{upperFirst(props.meta.loader)}
|
||||||
{' '}
|
{' '}
|
||||||
{props.meta.mc_version}
|
{props.meta.mc_version}
|
||||||
{/* {' '}
|
{/* {' '}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { HeartIcon, SearchMdIcon } from '@untitled-theme/icons-solid';
|
import { SearchMdIcon } from '@untitled-theme/icons-solid';
|
||||||
import { createSignal } from 'solid-js';
|
import { For, createSignal } from 'solid-js';
|
||||||
import Dropdown from '~ui/components/base/Dropdown';
|
import Dropdown from '~ui/components/base/Dropdown';
|
||||||
import TextField from '~ui/components/base/TextField';
|
import TextField from '~ui/components/base/TextField';
|
||||||
import createSortable from '~utils/sorting';
|
import createSortable from '~utils/sorting';
|
||||||
|
|
@ -28,34 +28,27 @@ function BrowserMain() {
|
||||||
provider: [Provider.Curseforge],
|
provider: [Provider.Curseforge],
|
||||||
});
|
});
|
||||||
|
|
||||||
const [list, key, setList, setKey] = createSortable<CardProps>([], {
|
const sortable = createSortable<CardProps>([], {
|
||||||
'A-Z': (a, b) => a.name.localeCompare(b.name),
|
'A-Z': (a, b) => a.name.localeCompare(b.name),
|
||||||
'Z-A': (a, b) => b.name.localeCompare(a.name),
|
'Z-A': (a, b) => b.name.localeCompare(a.name),
|
||||||
'Last Updated': (a, b) => a.date_updated - b.date_updated,
|
'Last Updated': (a, b) => a.date_updated - b.date_updated,
|
||||||
'New': (a, b) => b.date_added - a.date_added,
|
'New': (a, b) => b.date_added - a.date_added,
|
||||||
// 'Provider': (a, b) => a.provider
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function onSortChange(selected: number) {
|
||||||
|
sortable.setKey(selected);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div class="flex flex-col flex-grow-1">
|
<div class="flex flex-col flex-grow-1">
|
||||||
<div class="flex flex-row gap-2 h-8">
|
<div class="flex flex-row gap-2 h-8">
|
||||||
<TextField iconLeft={<SearchMdIcon />} />
|
<TextField iconLeft={<SearchMdIcon />} />
|
||||||
<Dropdown class="w-32!">
|
<Dropdown onChange={onSortChange} text="Sort By: ">
|
||||||
<Dropdown.Row>
|
<For each={Object.keys(sortable.sortables)}>
|
||||||
<HeartIcon />
|
{sortable => (
|
||||||
<p>Test</p>
|
<Dropdown.Row>{sortable}</Dropdown.Row>
|
||||||
</Dropdown.Row>
|
)}
|
||||||
<Dropdown.Row>
|
</For>
|
||||||
<HeartIcon />
|
|
||||||
<p>Test 2</p>
|
|
||||||
</Dropdown.Row>
|
|
||||||
<Dropdown.Row>
|
|
||||||
<p>Test 333333333</p>
|
|
||||||
</Dropdown.Row>
|
|
||||||
<Dropdown.Row>
|
|
||||||
<HeartIcon />
|
|
||||||
<p>Lorem Ipsum dolor</p>
|
|
||||||
</Dropdown.Row>
|
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { Route } from '@solidjs/router';
|
||||||
import Sidebar from '../../components/Sidebar';
|
import Sidebar from '../../components/Sidebar';
|
||||||
import SettingsGeneral from './launcher/SettingsGeneral';
|
import SettingsGeneral from './launcher/SettingsGeneral';
|
||||||
import SettingsAppearance from './launcher/SettingsAppearance';
|
import SettingsAppearance from './launcher/SettingsAppearance';
|
||||||
|
import SettingsAccounts from './game/SettingsAccounts';
|
||||||
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
|
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
|
||||||
import ErrorBoundary from '~ui/components/ErrorBoundary';
|
import ErrorBoundary from '~ui/components/ErrorBoundary';
|
||||||
|
|
||||||
|
|
@ -12,6 +13,7 @@ function SettingsRoutes() {
|
||||||
<>
|
<>
|
||||||
<Route path="/" component={SettingsGeneral} />
|
<Route path="/" component={SettingsGeneral} />
|
||||||
<Route path="/appearance" component={SettingsAppearance} />
|
<Route path="/appearance" component={SettingsAppearance} />
|
||||||
|
<Route path="/accounts" component={SettingsAccounts} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
108
desktop/src/ui/pages/settings/game/SettingsAccounts.tsx
Normal file
108
desktop/src/ui/pages/settings/game/SettingsAccounts.tsx
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { ChevronRightIcon, InfoCircleIcon } from '@untitled-theme/icons-solid';
|
||||||
|
import { For, Show, createSignal, mergeProps } from 'solid-js';
|
||||||
|
import Button from '~ui/components/base/Button';
|
||||||
|
import Tooltip from '~ui/components/base/Tooltip';
|
||||||
|
import PlayerHead from '~ui/components/game/PlayerHead';
|
||||||
|
import ScrollableContainer from '~ui/components/ScrollableContainer';
|
||||||
|
import SettingsRow from '~ui/components/SettingsRow';
|
||||||
|
import Sidebar from '~ui/components/Sidebar';
|
||||||
|
|
||||||
|
const Accounts: {
|
||||||
|
username: string;
|
||||||
|
uuid: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
username: 'Lynith',
|
||||||
|
uuid: 'f247be7c-5b82-41c6-9148-793ded77e71f',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: 'Wyvest',
|
||||||
|
uuid: 'a5331404-0e77-440e-8bef-24c071dac1ae',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: 'nextday',
|
||||||
|
uuid: '000646f6-b779-419c-b5d6-2c5ba52419bf',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
username: 'xXfake_playerXx',
|
||||||
|
uuid: '000646f6-b779-419c-b5d6-aaa',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function SettingsAccounts() {
|
||||||
|
const [current, setCurrent] = createSignal<string>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar.Page>
|
||||||
|
<h1>Accounts</h1>
|
||||||
|
<ScrollableContainer>
|
||||||
|
|
||||||
|
<For each={Accounts}>
|
||||||
|
{account => (
|
||||||
|
<AccountRow
|
||||||
|
username={account.username}
|
||||||
|
uuid={account.uuid}
|
||||||
|
current={current() === account.uuid}
|
||||||
|
onClick={setCurrent}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</For>
|
||||||
|
|
||||||
|
</ScrollableContainer>
|
||||||
|
</Sidebar.Page>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AccountRowProps {
|
||||||
|
username: string;
|
||||||
|
uuid: string;
|
||||||
|
current?: boolean;
|
||||||
|
onClick: (uuid: string) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
function AccountRow(props: AccountRowProps) {
|
||||||
|
const defaultProps = mergeProps({ current: false }, props);
|
||||||
|
const [errored, setErrored] = createSignal(false);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => props.onClick(props.uuid)}
|
||||||
|
class={`flex flex-row bg-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed rounded-xl gap-3.5 p-4 items-center box-border border ${defaultProps.current ? 'border-brand' : 'border-transparent'}`}
|
||||||
|
>
|
||||||
|
<div class="flex justify-center items-center h-12 w-12">
|
||||||
|
<PlayerHead
|
||||||
|
class="w-12 h-12 rounded-md"
|
||||||
|
uuid={props.uuid}
|
||||||
|
onError={() => setErrored(true)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class={`flex flex-col gap-2 flex-1 ${errored() ? 'text-danger' : ''}`}>
|
||||||
|
<div class="flex flex-row items-center gap-1">
|
||||||
|
<h3 class="text-xl">{props.username}</h3>
|
||||||
|
<Show when={errored()}>
|
||||||
|
<Tooltip
|
||||||
|
text="Could not fetch this account's game profile"
|
||||||
|
>
|
||||||
|
<InfoCircleIcon class="w-4 h-4" />
|
||||||
|
</Tooltip>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
<p class="text-wrap text-sm text-fg-secondary">{props.uuid}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="">
|
||||||
|
<Button
|
||||||
|
buttonStyle="icon"
|
||||||
|
children={<ChevronRightIcon />}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
// TODO: Display fullscreen profile page
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default SettingsAccounts;
|
||||||
|
|
@ -1,26 +1,39 @@
|
||||||
import { type Accessor, type Setter, createSignal } from 'solid-js';
|
import { type Accessor, type Setter, createSignal } from 'solid-js';
|
||||||
|
|
||||||
export type SortingFunction<T> = (a: T, b: T) => number;
|
export type SortingFunction<T> = (a: T, b: T) => number;
|
||||||
export interface Sortables<T> { [key: string]: SortingFunction<T> }
|
export type Sortables<T> = Record<string, SortingFunction<T>>;
|
||||||
|
|
||||||
type CreateSortable<T, S extends Sortables<T> = Sortables<T>> = [
|
interface CreateSortable<T, S extends Sortables<T> = Sortables<T>> {
|
||||||
list: Accessor<T[]>,
|
list: Accessor<T[]>;
|
||||||
key: Accessor<keyof S>,
|
key: Accessor<keyof S>;
|
||||||
setList: Setter<T[]>,
|
setList: Setter<T[]>;
|
||||||
setKey: Setter<keyof S>,
|
setKey: (val: keyof S | number) => any;
|
||||||
];
|
sortables: S;
|
||||||
|
}
|
||||||
|
|
||||||
export function createSortable<T, S extends Sortables<T> = Sortables<T>>(initList: T[], initSortables: S): CreateSortable<T, S> {
|
export function createSortable<T, S extends Sortables<T> = Sortables<T>>(initList: T[], initSortables: S): CreateSortable<T, S> {
|
||||||
const sortables = initSortables;
|
const sortables = initSortables;
|
||||||
const [list, setList] = createSignal<T[]>(initList);
|
const [list, setList] = createSignal<T[]>(initList);
|
||||||
const [key, setKey] = createSignal<keyof S>(Object.keys(sortables)[0]!);
|
const [key, setKey] = createSignal<keyof S>(Object.keys(sortables)[0]!);
|
||||||
|
|
||||||
return [
|
return {
|
||||||
list,
|
list,
|
||||||
key,
|
key,
|
||||||
setList,
|
setList,
|
||||||
setKey,
|
setKey: (val: keyof S | number) => {
|
||||||
];
|
if (typeof val === 'string') {
|
||||||
|
setKey(val);
|
||||||
|
}
|
||||||
|
else if (typeof val === 'number') {
|
||||||
|
const key = Object.keys(sortables).at(val);
|
||||||
|
if (key === undefined)
|
||||||
|
throw new Error('Key cannot be undefined!');
|
||||||
|
|
||||||
|
setKey(key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
sortables,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createSortable;
|
export default createSortable;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export function upperFirst(str: string): string {
|
export function upperFirst(object: any): string {
|
||||||
|
const str = object.toString();
|
||||||
return str.charAt(0).toUpperCase() + str.slice(1);
|
return str.charAt(0).toUpperCase() + str.slice(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue