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 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) {
|
||||
const [split, rest] = splitProps(props, ['children', 'class']);
|
||||
const [split, rest] = splitProps(props, ['children', 'class', 'text', 'onChange', 'selected']);
|
||||
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;
|
||||
|
||||
|
|
@ -18,6 +24,9 @@ function Dropdown(props: DropdownProps) {
|
|||
function select(index: number) {
|
||||
setSelected(index);
|
||||
setVisible(false);
|
||||
|
||||
if (split.onChange)
|
||||
split.onChange(selected());
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
@ -29,8 +38,9 @@ function Dropdown(props: DropdownProps) {
|
|||
iconRight={<ChevronDownIcon />}
|
||||
>
|
||||
<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()]}
|
||||
</div>
|
||||
</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 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) {
|
||||
return `https://crafatar.com/avatars/${uuid}?size=32`;
|
||||
function headSrc(uuid: string) {
|
||||
return `https://crafatar.com/avatars/${uuid}?size=48`;
|
||||
}
|
||||
|
||||
function PlayerHead(props: PlayerHeadProps) {
|
||||
const [headSrc] = createResource(() => props.uuid, fetchHeadSrc, {
|
||||
initialValue: steveSrc,
|
||||
});
|
||||
const [split, rest] = splitProps(props, ['uuid', 'class']);
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { LogOut01Icon, PlusIcon, Settings01Icon } from '@untitled-theme/icons-solid';
|
||||
import { useNavigate } from '@solidjs/router';
|
||||
import Button from '../base/Button';
|
||||
import PlayerHead from '../game/PlayerHead';
|
||||
import useAccount from '../../hooks/useAccount';
|
||||
|
|
@ -33,6 +34,7 @@ function AccountComponent(props: AccountComponentProps) {
|
|||
|
||||
function AccountPopup(props: Popup.PopupProps) {
|
||||
const loggedInAccount = useAccount();
|
||||
const navigate = useNavigate();
|
||||
|
||||
return (
|
||||
<Popup {...props}>
|
||||
|
|
@ -53,7 +55,14 @@ function AccountPopup(props: Popup.PopupProps) {
|
|||
<Button buttonStyle="ghost" iconLeft={<PlusIcon />}>Add Account</Button>
|
||||
</div>
|
||||
<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" />
|
||||
</Button>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
function useAccount(): Core.Account {
|
||||
function useAccount() {
|
||||
// TODO return the currently logged in account
|
||||
|
||||
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';
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import ClusterCover from '~ui/components/game/ClusterCover';
|
|||
import type { Cluster } from '~bindings';
|
||||
import useCommand from '~ui/hooks/useCommand';
|
||||
import { bridge } from '~index';
|
||||
import { upperFirst } from '~utils/string';
|
||||
|
||||
// TODO: Replace this into it's own component
|
||||
// possibly with the core ui lirbary with autogenerated and optimized svgs pls
|
||||
|
|
@ -142,9 +143,9 @@ function ClusterCard(props: Cluster) {
|
|||
</div>
|
||||
<div class="z-10 p-3 flex flex-row items-center justify-between">
|
||||
<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">
|
||||
{props.meta.loader}
|
||||
{upperFirst(props.meta.loader)}
|
||||
{' '}
|
||||
{props.meta.mc_version}
|
||||
{/* {' '}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { HeartIcon, SearchMdIcon } from '@untitled-theme/icons-solid';
|
||||
import { createSignal } from 'solid-js';
|
||||
import { SearchMdIcon } from '@untitled-theme/icons-solid';
|
||||
import { For, createSignal } from 'solid-js';
|
||||
import Dropdown from '~ui/components/base/Dropdown';
|
||||
import TextField from '~ui/components/base/TextField';
|
||||
import createSortable from '~utils/sorting';
|
||||
|
|
@ -28,34 +28,27 @@ function BrowserMain() {
|
|||
provider: [Provider.Curseforge],
|
||||
});
|
||||
|
||||
const [list, key, setList, setKey] = createSortable<CardProps>([], {
|
||||
const sortable = createSortable<CardProps>([], {
|
||||
'A-Z': (a, b) => a.name.localeCompare(b.name),
|
||||
'Z-A': (a, b) => b.name.localeCompare(a.name),
|
||||
'Last Updated': (a, b) => a.date_updated - b.date_updated,
|
||||
'New': (a, b) => b.date_added - a.date_added,
|
||||
// 'Provider': (a, b) => a.provider
|
||||
});
|
||||
|
||||
function onSortChange(selected: number) {
|
||||
sortable.setKey(selected);
|
||||
}
|
||||
|
||||
return (
|
||||
<div class="flex flex-col flex-grow-1">
|
||||
<div class="flex flex-row gap-2 h-8">
|
||||
<TextField iconLeft={<SearchMdIcon />} />
|
||||
<Dropdown class="w-32!">
|
||||
<Dropdown.Row>
|
||||
<HeartIcon />
|
||||
<p>Test</p>
|
||||
</Dropdown.Row>
|
||||
<Dropdown.Row>
|
||||
<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 onChange={onSortChange} text="Sort By: ">
|
||||
<For each={Object.keys(sortable.sortables)}>
|
||||
{sortable => (
|
||||
<Dropdown.Row>{sortable}</Dropdown.Row>
|
||||
)}
|
||||
</For>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import { Route } from '@solidjs/router';
|
|||
import Sidebar from '../../components/Sidebar';
|
||||
import SettingsGeneral from './launcher/SettingsGeneral';
|
||||
import SettingsAppearance from './launcher/SettingsAppearance';
|
||||
import SettingsAccounts from './game/SettingsAccounts';
|
||||
import AnimatedRoutes from '~ui/components/AnimatedRoutes';
|
||||
import ErrorBoundary from '~ui/components/ErrorBoundary';
|
||||
|
||||
|
|
@ -12,6 +13,7 @@ function SettingsRoutes() {
|
|||
<>
|
||||
<Route path="/" component={SettingsGeneral} />
|
||||
<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';
|
||||
|
||||
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>> = [
|
||||
list: Accessor<T[]>,
|
||||
key: Accessor<keyof S>,
|
||||
setList: Setter<T[]>,
|
||||
setKey: Setter<keyof S>,
|
||||
];
|
||||
interface CreateSortable<T, S extends Sortables<T> = Sortables<T>> {
|
||||
list: Accessor<T[]>;
|
||||
key: Accessor<keyof S>;
|
||||
setList: Setter<T[]>;
|
||||
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> {
|
||||
const sortables = initSortables;
|
||||
const [list, setList] = createSignal<T[]>(initList);
|
||||
const [key, setKey] = createSignal<keyof S>(Object.keys(sortables)[0]!);
|
||||
|
||||
return [
|
||||
return {
|
||||
list,
|
||||
key,
|
||||
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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue