feat: add settings
This commit is contained in:
parent
28050733cb
commit
eebd4e90c2
12 changed files with 376 additions and 75 deletions
|
|
@ -3,6 +3,7 @@ pub mod users;
|
||||||
pub mod processor;
|
pub mod processor;
|
||||||
pub mod package;
|
pub mod package;
|
||||||
pub mod other;
|
pub mod other;
|
||||||
|
pub mod settings;
|
||||||
|
|
||||||
#[macro_export(local_inner_macros)]
|
#[macro_export(local_inner_macros)]
|
||||||
macro_rules! collect_commands {
|
macro_rules! collect_commands {
|
||||||
|
|
@ -19,6 +20,8 @@ macro_rules! collect_commands {
|
||||||
cluster::create_cluster,
|
cluster::create_cluster,
|
||||||
cluster::get_clusters,
|
cluster::get_clusters,
|
||||||
|
|
||||||
|
settings::get_global_profile,
|
||||||
|
|
||||||
other::open_dev_tools,
|
other::open_dev_tools,
|
||||||
]
|
]
|
||||||
}};
|
}};
|
||||||
|
|
|
||||||
10
apps/onelauncher/desktop/src/api/commands/settings.rs
Normal file
10
apps/onelauncher/desktop/src/api/commands/settings.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
use onelauncher_core::entity::setting_profiles::Model;
|
||||||
|
use onelauncher_core::api::setting_profiles;
|
||||||
|
|
||||||
|
use crate::api::error::SerializableResult;
|
||||||
|
|
||||||
|
#[specta::specta]
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn get_global_profile() -> SerializableResult<Model> {
|
||||||
|
Ok(setting_profiles::get_global_profile().await)
|
||||||
|
}
|
||||||
|
|
@ -22,7 +22,7 @@ function Navbar() {
|
||||||
<div className="flex flex-row items-center gap-x-10 py-1">
|
<div className="flex flex-row items-center gap-x-10 py-1">
|
||||||
<NavbarLink label="Home" path="/app" />
|
<NavbarLink label="Home" path="/app" />
|
||||||
<NavbarLink label="Browser" path="/app/browser" />
|
<NavbarLink label="Browser" path="/app/browser" />
|
||||||
<NavbarLink label="Settings" path="/app" />
|
<NavbarLink label="Settings" path="/app/settings" />
|
||||||
</div>
|
</div>
|
||||||
<div className="relative flex flex-row items-center justify-end gap-x-2">
|
<div className="relative flex flex-row items-center justify-end gap-x-2">
|
||||||
<Button color="ghost" size="icon">
|
<Button color="ghost" size="icon">
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
|
||||||
|
interface ScrollableContainerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ScrollableContainer(props: ScrollableContainerProps) {
|
||||||
|
return (
|
||||||
|
<div className="h-full h-screen w-[calc(100%+14px)] flex flex-1 flex-col overflow-hidden">
|
||||||
|
<OverlayScrollbarsComponent className="os-hide-horizontal-scrollbar relative flex-1">
|
||||||
|
<div className="absolute h-full w-[calc(100%-14px)] flex flex-col gap-2">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ScrollableContainer;
|
||||||
45
apps/onelauncher/frontend/src/components/SettingsRow.tsx
Normal file
45
apps/onelauncher/frontend/src/components/SettingsRow.tsx
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
import type { JSX } from "react";
|
||||||
|
|
||||||
|
export type SettingsRowProps = {
|
||||||
|
title: JSX.Element | string;
|
||||||
|
description: JSX.Element | string;
|
||||||
|
icon: JSX.Element;
|
||||||
|
disabled?: boolean;
|
||||||
|
children?: JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
function SettingsRow(props: SettingsRowProps) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`
|
||||||
|
flex flex-row items-center gap-3.5 rounded-xl p-4
|
||||||
|
${props.disabled ? 'bg-component-bg-disabled' : 'bg-page-elevated hover:bg-component-bg-hover'}
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<div className="flex h-8 w-8 items-center justify-center">
|
||||||
|
{props.icon}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-1 flex-col gap-2">
|
||||||
|
<h3 className="text-lg capitalize leading-tight">{props.title}</h3>
|
||||||
|
<p className="text-sm text-fg-secondary">{props.description}</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex h-9 flex-row items-center gap-2">
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SettingsRowHeaderProps {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
SettingsRow.Header = ({ className, children }: SettingsRowHeaderProps) => {
|
||||||
|
const headerClasses = `mt-4 mb-1 ml-2 text-md text-fg-secondary uppercase ${className || ''}`;
|
||||||
|
return <h3 className={headerClasses}>{children}</h3>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default SettingsRow;
|
||||||
30
apps/onelauncher/frontend/src/components/base/Switch.tsx
Normal file
30
apps/onelauncher/frontend/src/components/base/Switch.tsx
Normal file
|
|
@ -0,0 +1,30 @@
|
||||||
|
import type React from 'react';
|
||||||
|
import type { RefAttributes } from 'react';
|
||||||
|
import type { SwitchProps as AriaSwitchProps } from 'react-aria-components';
|
||||||
|
import { Switch as AriaSwitch } from 'react-aria-components';
|
||||||
|
import { tv } from 'tailwind-variants';
|
||||||
|
|
||||||
|
export const switchVariants = tv({
|
||||||
|
base: [
|
||||||
|
'w-[40px] h-[22px] p-3 flex flex-row relative rounded-full transition-colors overflow-hidden bg-brand',
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export type SwitchProps = AriaSwitchProps & RefAttributes<HTMLLabelElement> & {
|
||||||
|
className?: string;
|
||||||
|
children?: React.ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Switch({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: SwitchProps) {
|
||||||
|
return (
|
||||||
|
<AriaSwitch className={switchVariants({ className })} {...props}>
|
||||||
|
<div className="bg-orange-400" />
|
||||||
|
{props.children}
|
||||||
|
</AriaSwitch>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Switch;
|
||||||
|
|
@ -1,33 +1,21 @@
|
||||||
import { Dialog, DialogTrigger, Heading, Input, Label, Modal as AriaModal, TextField, ModalOverlay } from 'react-aria-components';
|
import type { ClassNameString } from '@/types/global';
|
||||||
import Button from '../base/Button';
|
import {
|
||||||
|
Dialog as AriaDialog,
|
||||||
|
DialogTrigger,
|
||||||
|
Modal as ModalOverlay
|
||||||
|
} from 'react-aria-components';
|
||||||
|
import type { DialogProps } from 'react-aria-components';
|
||||||
|
|
||||||
// just testing stuff
|
interface ModalProps extends DialogProps, ClassNameString { }
|
||||||
export function Modal() {
|
|
||||||
return (
|
function Modal({ className, ...props }: ModalProps) {
|
||||||
<>
|
return (
|
||||||
<DialogTrigger>
|
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
||||||
<Button className={"focus:outline-none"}>Slmlar</Button>
|
<AriaDialog className="min-w-md flex flex-col gap-y-2 border border-white/5 rounded-lg bg-page p-4 text-center focus:outline-none" {...props} />
|
||||||
<ModalOverlay isDismissable className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 p-4">
|
</ModalOverlay>
|
||||||
<AriaModal>
|
);
|
||||||
<Dialog className="min-w-md flex flex-col gap-y-2 border border-white/5 rounded-lg bg-page p-4 text-center focus:outline-none">
|
}
|
||||||
<form>
|
|
||||||
<Heading slot="title">Sign up</Heading>
|
Modal.Trigger = DialogTrigger;
|
||||||
<TextField autoFocus>
|
|
||||||
<Label>First Name:</Label>
|
export default Modal;
|
||||||
<Input />
|
|
||||||
</TextField>
|
|
||||||
<TextField>
|
|
||||||
<Label>Last Name:</Label>
|
|
||||||
<Input />
|
|
||||||
</TextField>
|
|
||||||
<Button slot="close">
|
|
||||||
Submit
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
</Dialog>
|
|
||||||
</AriaModal>
|
|
||||||
</ModalOverlay>
|
|
||||||
</DialogTrigger>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
|
import Button from '@/components/base/Button';
|
||||||
import { TextField } from '@/components/base/TextField';
|
import { TextField } from '@/components/base/TextField';
|
||||||
import { Modal } from '@/components/overlay/Modal';
|
import Modal from '@/components/overlay/Modal';
|
||||||
import { createFileRoute } from '@tanstack/react-router';
|
import { createFileRoute } from '@tanstack/react-router';
|
||||||
|
|
||||||
export const Route = createFileRoute('/app/browser/')({
|
export const Route = createFileRoute('/app/browser/')({
|
||||||
|
|
@ -13,7 +14,14 @@ function RouteComponent() {
|
||||||
|
|
||||||
<TextField />
|
<TextField />
|
||||||
|
|
||||||
<Modal />
|
{/* we can open the modal but we cant close it for some reason */}
|
||||||
|
<Modal.Trigger>
|
||||||
|
<Button>Open Modal</Button>
|
||||||
|
|
||||||
|
<Modal>
|
||||||
|
<p>sadsadsad</p>
|
||||||
|
</Modal>
|
||||||
|
</Modal.Trigger>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -3,13 +3,19 @@ import type { Model } from '@/bindings.gen';
|
||||||
import Button from '@/components/base/Button';
|
import Button from '@/components/base/Button';
|
||||||
import useCommand from '@/hooks/useCommand';
|
import useCommand from '@/hooks/useCommand';
|
||||||
import { bindings } from '@/main';
|
import { bindings } from '@/main';
|
||||||
import { useIsFetching } from '@tanstack/react-query';
|
import { useIsFetching, useQueryClient } from '@tanstack/react-query';
|
||||||
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
import { createFileRoute, useNavigate } from '@tanstack/react-router';
|
||||||
|
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||||
|
import DefaultInstancePhoto from "@/assets/images/default_instance_cover.jpg"
|
||||||
|
|
||||||
export const Route = createFileRoute('/app/')({
|
export const Route = createFileRoute('/app/')({
|
||||||
component: RouteComponent,
|
component: RouteComponent,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/*
|
||||||
|
Please note this route has a very big issue related to scrolling
|
||||||
|
and i am very angry rn so i will not be fixing it rn
|
||||||
|
*/
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const result = useCommand("getClusters", bindings.commands.getClusters)
|
const result = useCommand("getClusters", bindings.commands.getClusters)
|
||||||
|
|
||||||
|
|
@ -23,7 +29,7 @@ function RouteComponent() {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='h-96 overflow-y-auto'>
|
<div className='flex flex-col'>
|
||||||
<ClusterGroup clusters={result.data} />
|
<ClusterGroup clusters={result.data} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -102,6 +108,8 @@ function Banner() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClusterCreate() {
|
function ClusterCreate() {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
const result = useCommand("createCluster", () => bindings.commands.createCluster({
|
const result = useCommand("createCluster", () => bindings.commands.createCluster({
|
||||||
icon_url: "asd",
|
icon_url: "asd",
|
||||||
mc_loader: "vanilla",
|
mc_loader: "vanilla",
|
||||||
|
|
@ -121,7 +129,9 @@ function ClusterCreate() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
queryClient.invalidateQueries({
|
||||||
|
queryKey: ["getClusters"],
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -143,54 +153,56 @@ function ClusterGroup(props: ClusterGroupProps) {
|
||||||
|
|
||||||
if (isFetching) {
|
if (isFetching) {
|
||||||
return (
|
return (
|
||||||
<div className='flex items-center justify-center h-fit'>
|
<div className='flex items-center justify-center h-full'>
|
||||||
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" />
|
<div className="w-8 h-8 border-4 border-brand rounded-full border-t-transparent animate-spin" />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="h-full w-full">
|
||||||
<div className='grid grid-cols-4 gap-2'>
|
<OverlayScrollbarsComponent
|
||||||
{props.clusters?.map((data) => (
|
className="h-full w-full"
|
||||||
<ClusterCard key={data.id} {...data} />
|
>
|
||||||
))}
|
<div className='grid grid-cols-4 gap-4 max-h-96 2xl:grid-cols-6 pb-4'>
|
||||||
</div>
|
{props.clusters?.map((data) => (
|
||||||
</>
|
<ClusterCard key={data.id} {...data} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</OverlayScrollbarsComponent>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function ClusterCard(props: Model) {
|
function ClusterCard(props: Model) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<div
|
className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
|
||||||
className="group relative h-[152px] flex flex-col rounded-xl border border-component-border/5 bg-component-bg active:bg-component-bg-pressed hover:bg-component-bg-hover"
|
>
|
||||||
>
|
<div className="relative flex-1 overflow-hidden rounded-t-xl">
|
||||||
<div className="relative flex-1 overflow-hidden rounded-t-xl">
|
<div
|
||||||
<div
|
className="absolute h-full w-full transition-transform group-hover:!scale-110"
|
||||||
className="absolute h-full w-full transition-transform group-hover:!scale-110"
|
>
|
||||||
>
|
<img
|
||||||
<img
|
className="h-full w-full object-cover"
|
||||||
className="h-full w-full object-cover"
|
src={DefaultInstancePhoto}
|
||||||
src='https://github.com/emirsassan.png'
|
/>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
|
|
||||||
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
|
|
||||||
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{props.name}</p>
|
|
||||||
<p className="h-4 text-xs">
|
|
||||||
{props.mc_loader}
|
|
||||||
{' '}
|
|
||||||
{props.mc_version}
|
|
||||||
{/* {' '}
|
|
||||||
{props.packages.mods && `• ${props.mods} mods`} */}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* <LaunchButton cluster={props} iconOnly /> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="z-10 flex flex-row items-center justify-between gap-x-3 p-3">
|
||||||
|
<div className="h-full flex flex-col gap-1.5 overflow-hidden">
|
||||||
|
<p className="h-4 text-ellipsis whitespace-nowrap font-medium">{props.name}</p>
|
||||||
|
<p className="h-4 text-xs">
|
||||||
|
{props.mc_loader}
|
||||||
|
{' '}
|
||||||
|
{props.mc_version}
|
||||||
|
{/* {' '}
|
||||||
|
{props.packages.mods && `• ${props.mods} mods`} */}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* <LaunchButton cluster={props} iconOnly /> */}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
34
apps/onelauncher/frontend/src/routes/app/settings/index.tsx
Normal file
34
apps/onelauncher/frontend/src/routes/app/settings/index.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import Sidebar from './route'
|
||||||
|
import useCommand from '@/hooks/useCommand'
|
||||||
|
import { bindings } from '@/main'
|
||||||
|
import SettingsRow from '@/components/SettingsRow'
|
||||||
|
import DiscordIcon from "@/assets/logos/discord.svg"
|
||||||
|
import ToggleButton from '@/components/base/ToggleButton'
|
||||||
|
import Switch from '@/components/base/Switch'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/app/settings/')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
const result = useCommand("getGlobalProfile", bindings.commands.getGlobalProfile)
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Sidebar.Page>
|
||||||
|
<div>
|
||||||
|
<h1>General Settings</h1>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
description="Enable Discord Rich Presence."
|
||||||
|
icon={<img src={DiscordIcon} className="w-6 invert-100" />}
|
||||||
|
title="Discord RPC"
|
||||||
|
>
|
||||||
|
<Switch>Thing</Switch>
|
||||||
|
</SettingsRow>
|
||||||
|
|
||||||
|
<pre>{JSON.stringify(result.data, null, 2)}</pre>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
import { createFileRoute } from '@tanstack/react-router'
|
||||||
|
import Sidebar from './route'
|
||||||
|
import SettingsRow from '@/components/SettingsRow'
|
||||||
|
import { Maximize01Icon } from '@untitled-theme/icons-react'
|
||||||
|
import Button from '@/components/base/Button'
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/app/settings/minecraft')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<Sidebar.Page>
|
||||||
|
<div>
|
||||||
|
<h1>Minecraft Settings</h1>
|
||||||
|
|
||||||
|
<SettingsRow.Header>Game</SettingsRow.Header>
|
||||||
|
|
||||||
|
<SettingsRow
|
||||||
|
description="Force Minecraft to start in fullscreen mode."
|
||||||
|
icon={<Maximize01Icon />}
|
||||||
|
title="Force Fullscreen"
|
||||||
|
>
|
||||||
|
<Button>something</Button>
|
||||||
|
</SettingsRow>
|
||||||
|
</div>
|
||||||
|
</Sidebar.Page>
|
||||||
|
)
|
||||||
|
}
|
||||||
123
apps/onelauncher/frontend/src/routes/app/settings/route.tsx
Normal file
123
apps/onelauncher/frontend/src/routes/app/settings/route.tsx
Normal file
|
|
@ -0,0 +1,123 @@
|
||||||
|
import { createFileRoute, Outlet, useNavigate, useRouterState } from '@tanstack/react-router'
|
||||||
|
import { Brush01Icon, CodeSnippet02Icon, MessageTextSquare01Icon, RefreshCcw02Icon, Rocket02Icon, Sliders04Icon, Users01Icon } from '@untitled-theme/icons-react';
|
||||||
|
import { useEffect, type JSX } from 'react';
|
||||||
|
|
||||||
|
export const Route = createFileRoute('/app/settings')({
|
||||||
|
component: RouteComponent,
|
||||||
|
})
|
||||||
|
|
||||||
|
function RouteComponent() {
|
||||||
|
return (
|
||||||
|
<div className="h-full flex flex-1 flex-row gap-x-7">
|
||||||
|
<div className="mt-8 flex flex-col justify-between">
|
||||||
|
<Sidebar
|
||||||
|
base="/app/settings"
|
||||||
|
links={{
|
||||||
|
'Launcher Settings': [
|
||||||
|
[<Rocket02Icon />, 'General', '/'],
|
||||||
|
[<Brush01Icon />, 'Appearance', '/appearance'],
|
||||||
|
// [<Key01Icon />, 'APIs', '/apis'],
|
||||||
|
// [<Globe01Icon />, 'Language', '/language'],
|
||||||
|
],
|
||||||
|
'Game Settings': [
|
||||||
|
[<Sliders04Icon />, 'Minecraft settings', '/minecraft'],
|
||||||
|
[<Users01Icon />, 'Accounts', '/accounts'],
|
||||||
|
],
|
||||||
|
'About': [
|
||||||
|
[<RefreshCcw02Icon />, 'Changelog', '/changelog'],
|
||||||
|
[<MessageTextSquare01Icon />, 'Feedback', '/feedback'],
|
||||||
|
[<CodeSnippet02Icon />, 'Developer Options', '/developer'],
|
||||||
|
],
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/* <Info /> */}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="h-full w-full flex flex-col">
|
||||||
|
<Outlet />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type SidebarProps = {
|
||||||
|
base: string;
|
||||||
|
links: Record<string, ([JSX.Element, string, string, URLSearchParams?] | undefined)[]>;
|
||||||
|
};
|
||||||
|
|
||||||
|
function Sidebar(props: SidebarProps) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const routerState = useRouterState();
|
||||||
|
|
||||||
|
const location = routerState.location.pathname
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (props.base.endsWith('/'))
|
||||||
|
throw new Error('Base should not end with a slash');
|
||||||
|
});
|
||||||
|
|
||||||
|
function goto(href: string, params?: URLSearchParams) {
|
||||||
|
const currParams = new URLSearchParams(location);
|
||||||
|
if (params)
|
||||||
|
for (const [key, value] of params)
|
||||||
|
currParams.set(key, value);
|
||||||
|
|
||||||
|
const url = `${props.base}${href}`;
|
||||||
|
navigate({ to: url })
|
||||||
|
}
|
||||||
|
|
||||||
|
function isActive(link: string, params: URLSearchParams | undefined) {
|
||||||
|
const pathnameMatch = location === `${props.base}${link}` || `${location}/` === `${props.base}${link}`;
|
||||||
|
const paramsMatch = params ? location.endsWith(params.toString()) : true;
|
||||||
|
return pathnameMatch && paramsMatch;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-56 flex flex-col pr-2">
|
||||||
|
{Object.keys(props.links).map((section, i) => (
|
||||||
|
<div className="flex flex-col gap-y-2">
|
||||||
|
<div key={i}>
|
||||||
|
<h3 className="m-1.5 mt-5 text-xs text-fg-secondary font-medium">{section.toUpperCase()}</h3>
|
||||||
|
<div className="flex flex-col gap-y-1 fill-fg-primary text-fg-primary font-medium">
|
||||||
|
{props.links[section].map((link) => {
|
||||||
|
if (!link)
|
||||||
|
return;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a
|
||||||
|
className={
|
||||||
|
`px-3 py-1 rounded-md text-md border border-component-bg hover:bg-component-bg-hover active:bg-component-bg-pressed ${isActive(link[2], link[3]) ? 'bg-component-bg border-border/05' : 'border-transparent'}`
|
||||||
|
}
|
||||||
|
onClick={() => goto(link[2], link[3])}
|
||||||
|
>
|
||||||
|
<span className="flex flex-row items-center gap-x-3 *:w-5">
|
||||||
|
{link[0]}
|
||||||
|
{link[1]}
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
)
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SidebarPageProps {
|
||||||
|
className?: string;
|
||||||
|
children: JSX.Element;
|
||||||
|
}
|
||||||
|
|
||||||
|
Sidebar.Page = function (props: SidebarPageProps) {
|
||||||
|
return (
|
||||||
|
<div {...props} className={`flex flex-col flex-1 gap-y-1 ${props.className || ''}`}>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Sidebar;
|
||||||
|
|
||||||
Loading…
Add table
Reference in a new issue