feat: Accounts page

This commit is contained in:
LynithDev 2024-06-16 19:41:17 +02:00
parent 60425c1acd
commit 1bdebb0f94
13 changed files with 236 additions and 48 deletions

View file

@ -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>

View 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;
}
}

View 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;

View file

@ -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;

View file

@ -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>

View file

@ -1,4 +1,4 @@
function useAccount(): Core.Account {
function useAccount() {
// TODO return the currently logged in account
return {

View file

@ -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';
/**

View file

@ -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}
{/* {' '}

View file

@ -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>

View file

@ -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} />
</>
);
}

View 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;

View file

@ -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;

View file

@ -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);
}