mirror of
https://github.com/BlowaterNostr/blowater.git
synced 2024-10-18 15:43:20 +00:00
refactor navigation bar (#271)
This commit is contained in:
parent
e9f95b493c
commit
21bcc1198b
23
UI/app.tsx
23
UI/app.tsx
@ -467,14 +467,11 @@ export function AppComponent(props: {
|
||||
<div class={tw`w-full h-full flex flex-col`}>
|
||||
<div class={tw`w-full flex-1 flex overflow-hidden`}>
|
||||
<div class={tw`mobile:hidden`}>
|
||||
{nav.NavBar({
|
||||
profilePicURL: model.myProfile?.picture,
|
||||
publicKey: myAccountCtx.publicKey,
|
||||
database: app.database,
|
||||
pool: props.pool,
|
||||
emit: app.eventBus.emit,
|
||||
...model.navigationModel,
|
||||
})}
|
||||
<nav.NavBar
|
||||
publicKey={app.ctx.publicKey}
|
||||
profileGetter={app.database}
|
||||
emit={app.eventBus.emit}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@ -510,13 +507,11 @@ export function AppComponent(props: {
|
||||
|
||||
<div class={tw`desktop:hidden`}>
|
||||
{
|
||||
<nav.MobileNavBar
|
||||
profilePicURL={model.myProfile?.picture}
|
||||
publicKey={myAccountCtx.publicKey}
|
||||
database={app.database}
|
||||
pool={props.pool}
|
||||
<nav.NavBar
|
||||
publicKey={app.ctx.publicKey}
|
||||
profileGetter={app.database}
|
||||
emit={app.eventBus.emit}
|
||||
{...model.navigationModel}
|
||||
isMobile={true}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
|
@ -255,7 +255,7 @@ export async function* UI_Interaction_Update(args: {
|
||||
// Navigation
|
||||
//
|
||||
else if (event.type == "ChangeNavigation") {
|
||||
model.navigationModel.activeNav = event.index;
|
||||
model.navigationModel.activeNav = event.id;
|
||||
model.rightPanelModel = {
|
||||
show: false,
|
||||
};
|
||||
|
40
UI/icons2/about-icon.tsx
Normal file
40
UI/icons2/about-icon.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
/** @jsx h */
|
||||
import { h } from "https://esm.sh/preact@10.17.1";
|
||||
|
||||
export function AboutIcon(props: {
|
||||
class?: string | h.JSX.SignalLike<string | undefined> | undefined;
|
||||
style?:
|
||||
| string
|
||||
| h.JSX.CSSProperties
|
||||
| h.JSX.SignalLike<string | h.JSX.CSSProperties>
|
||||
| undefined;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
class={props.class}
|
||||
style={props.style}
|
||||
viewBox="0 0 52 52"
|
||||
data-name="Layer 1"
|
||||
id="Layer_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<rect
|
||||
height="6"
|
||||
rx="3"
|
||||
transform="translate(52 27.52) rotate(180)"
|
||||
width="6"
|
||||
x="23"
|
||||
y="10.76"
|
||||
>
|
||||
</rect>
|
||||
<path d="M27,41.24a2,2,0,0,1-2-2v-13H23a2,2,0,0,1,0-4h4a2,2,0,0,1,2,2v15A2,2,0,0,1,27,41.24Z">
|
||||
</path>
|
||||
<path d="M26,52A26,26,0,1,1,52,26,26,26,0,0,1,26,52ZM26,4A22,22,0,1,0,48,26,22,22,0,0,0,26,4Z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
27
UI/icons2/app-list-icon.tsx
Normal file
27
UI/icons2/app-list-icon.tsx
Normal file
@ -0,0 +1,27 @@
|
||||
/** @jsx h */
|
||||
import { h } from "https://esm.sh/preact@10.17.1";
|
||||
|
||||
export function AppListIcon(props: {
|
||||
class?: string | h.JSX.SignalLike<string | undefined> | undefined;
|
||||
style?:
|
||||
| string
|
||||
| h.JSX.CSSProperties
|
||||
| h.JSX.SignalLike<string | h.JSX.CSSProperties>
|
||||
| undefined;
|
||||
}) {
|
||||
return (
|
||||
<svg
|
||||
class={props.class}
|
||||
style={props.style}
|
||||
viewBox="0 0 28 28"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" stroke-width="0"></g>
|
||||
<g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path d="M20.841 2.65519C19.9623 1.77651 18.5377 1.77652 17.659 2.6552L13.5 6.8142V6.25402C13.5 5.01138 12.4926 4.00402 11.25 4.00402H4.25C3.00736 4.00402 2 5.01138 2 6.25402V24.254C2 25.2205 2.7835 26.004 3.75 26.004H21.75C22.9926 26.004 24 24.9967 24 23.754V16.7499C24 15.5073 22.9926 14.4999 21.75 14.4999H21.1942L25.349 10.3452C26.2277 9.46648 26.2277 8.04186 25.349 7.16318L20.841 2.65519ZM17.3058 14.4999H13.5V10.6941L17.3058 14.4999ZM18.7197 3.71586C19.0126 3.42296 19.4874 3.42296 19.7803 3.71585L24.2883 8.22384C24.5812 8.51673 24.5812 8.99161 24.2883 9.2845L19.7803 13.7925C19.4874 14.0854 19.0126 14.0854 18.7197 13.7925L14.2117 9.2845C13.9188 8.99161 13.9188 8.51673 14.2117 8.22384L18.7197 3.71586ZM12 6.25402V14.4999H3.5V6.25402C3.5 5.83981 3.83579 5.50402 4.25 5.50402H11.25C11.6642 5.50402 12 5.83981 12 6.25402ZM3.5 23.754L3.5 15.9999H12V24.504H4.25C3.83579 24.504 3.5 24.1682 3.5 23.754ZM13.5 15.9999H21.75C22.1642 15.9999 22.5 16.3357 22.5 16.7499V23.754C22.5 24.1682 22.1642 24.504 21.75 24.504H13.5V15.9999Z">
|
||||
</path>
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
@ -1,41 +1,24 @@
|
||||
/** @jsx h */
|
||||
import { h, render } from "https://esm.sh/preact@10.17.1";
|
||||
|
||||
import { NewIndexedDB } from "./dexie-db.ts";
|
||||
|
||||
import * as nav from "./nav.tsx";
|
||||
import { NavBar } from "./nav.tsx";
|
||||
import { InMemoryAccountContext } from "../lib/nostr-ts/nostr.ts";
|
||||
import { Database_Contextual_View } from "../database.ts";
|
||||
import { fail } from "https://deno.land/std@0.176.0/testing/asserts.ts";
|
||||
import { testEventBus, testEventsAdapter } from "./_setup.test.ts";
|
||||
import { tw } from "https://esm.sh/twind@0.16.16";
|
||||
|
||||
import { EventBus } from "../event-bus.ts";
|
||||
import { UI_Interaction_Event } from "./app_update.ts";
|
||||
import { PublicKey } from "../lib/nostr-ts/key.ts";
|
||||
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
|
||||
const db = await Database_Contextual_View.New(testEventsAdapter);
|
||||
if (db instanceof Error) fail(db.message);
|
||||
const ctx = InMemoryAccountContext.Generate();
|
||||
|
||||
const db = await NewIndexedDB();
|
||||
if (db instanceof Error) {
|
||||
throw db;
|
||||
}
|
||||
const NavProps: nav.Props = {
|
||||
publicKey: PublicKey.FromHex(
|
||||
"0add27aa36e2e5e2591370f485af1344447705cfc37b1a1e6b0224be878c9687",
|
||||
) as PublicKey,
|
||||
database: db,
|
||||
pool: new ConnectionPool(),
|
||||
eventEmitter: new EventBus<UI_Interaction_Event>(),
|
||||
AddRelayButtonClickedError: "",
|
||||
AddRelayInput: "",
|
||||
activeNav: "DM",
|
||||
picture: undefined,
|
||||
};
|
||||
|
||||
export default function NavTest() {
|
||||
return (
|
||||
render(
|
||||
<div class={tw`h-screen`}>
|
||||
<nav.NavBar
|
||||
{...NavProps}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
<NavBar emit={testEventBus.emit} publicKey={ctx.publicKey} profileGetter={db} />
|
||||
<NavBar emit={testEventBus.emit} publicKey={ctx.publicKey} profileGetter={db} isMobile={true} />
|
||||
</div>,
|
||||
document.body,
|
||||
);
|
||||
|
||||
render(<NavTest />, document.body);
|
||||
for await (const event of testEventBus.onChange()) {
|
||||
console.log(event);
|
||||
}
|
||||
|
291
UI/nav.tsx
291
UI/nav.tsx
@ -1,14 +1,8 @@
|
||||
/** @jsx h */
|
||||
import { h } from "https://esm.sh/preact@10.17.1";
|
||||
import { ComponentChild, h } from "https://esm.sh/preact@10.17.1";
|
||||
import { tw } from "https://esm.sh/twind@0.16.16";
|
||||
import { AboutIcon, AppListIcon } from "./icons/mod.tsx";
|
||||
import * as db from "../database.ts";
|
||||
|
||||
import { Avatar } from "./components/avatar.tsx";
|
||||
import { CenterClass, NoOutlineClass } from "./components/tw.ts";
|
||||
import { emitFunc, EventEmitter } from "../event-bus.ts";
|
||||
import { PublicKey } from "../lib/nostr-ts/key.ts";
|
||||
import { ConnectionPool } from "../lib/nostr-ts/relay.ts";
|
||||
import {
|
||||
PrimaryBackgroundColor,
|
||||
PrimaryTextColor,
|
||||
@ -17,192 +11,135 @@ import {
|
||||
} from "./style/colors.ts";
|
||||
import { ChatIcon } from "./icons2/chat-icon.tsx";
|
||||
import { UserIcon } from "./icons2/user-icon.tsx";
|
||||
import { SocialIcon } from "./icons2/social-icon.tsx";
|
||||
import { SettingIcon } from "./icons2/setting-icon.tsx";
|
||||
|
||||
export type Props = {
|
||||
profilePicURL: string | undefined;
|
||||
publicKey: PublicKey;
|
||||
database: db.Database_Contextual_View;
|
||||
pool: ConnectionPool;
|
||||
emit: emitFunc<NavigationUpdate>;
|
||||
} & NavigationModel;
|
||||
|
||||
export type ActiveNav = ActiveTab | "Setting";
|
||||
export type ActiveTab = "DM" | /* "Group" | */ "Profile" | "About" | "AppList";
|
||||
|
||||
export type NavigationModel = {
|
||||
activeNav: ActiveNav;
|
||||
};
|
||||
import { Component } from "https://esm.sh/preact@10.17.1";
|
||||
import { ProfileGetter } from "./search.tsx";
|
||||
import { ProfileData } from "../features/profile.ts";
|
||||
import { AboutIcon } from "./icons2/about-icon.tsx";
|
||||
import { AppListIcon } from "./icons2/app-list-icon.tsx";
|
||||
import { CenterClass, NoOutlineClass } from "./components/tw.ts";
|
||||
import { emitFunc } from "../event-bus.ts";
|
||||
|
||||
export type NavigationUpdate = {
|
||||
type: "ChangeNavigation";
|
||||
index: ActiveNav;
|
||||
id: NavTabID;
|
||||
};
|
||||
|
||||
const navTabLayoutOrder: ActiveTab[] = ["DM", /*"Group",*/ "Profile", "About", "AppList"];
|
||||
const tabs = {
|
||||
"DM": (active: boolean) => (
|
||||
<ChatIcon
|
||||
class={tw`w-[2rem] h-[2rem]`}
|
||||
style={{
|
||||
stroke: active ? PrimaryTextColor : SecondaryTextColor,
|
||||
fill: "none",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
"Profile": (active: boolean) => (
|
||||
<UserIcon
|
||||
class={tw`w-[2rem] h-[2rem]`}
|
||||
style={{
|
||||
stroke: active ? PrimaryTextColor : SecondaryTextColor,
|
||||
fill: "none",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
"About": (active: boolean) => (
|
||||
<AboutIcon
|
||||
class={tw`w-[2rem] h-[2rem]`}
|
||||
style={{
|
||||
fill: active ? PrimaryTextColor : SecondaryTextColor,
|
||||
}}
|
||||
/>
|
||||
),
|
||||
"AppList": (active: boolean) => (
|
||||
<AppListIcon
|
||||
class={tw`w-[2rem] h-[2rem]`}
|
||||
style={{
|
||||
stroke: active ? PrimaryTextColor : SecondaryTextColor,
|
||||
fill: "none",
|
||||
}}
|
||||
/>
|
||||
),
|
||||
export type NavigationModel = {
|
||||
activeNav: NavTabID;
|
||||
};
|
||||
|
||||
export function NavBar(props: Props) {
|
||||
return (
|
||||
<div
|
||||
class={tw`bg-[${PrimaryBackgroundColor}] w-[5.75rem] h-full flex flex-col justify-between overflow-y-auto px-[1.12rem] py-[3rem]`}
|
||||
>
|
||||
<div>
|
||||
<Avatar
|
||||
picture={props.profilePicURL}
|
||||
class={tw`w-[3.5rem] h-[3.5rem] m-auto mb-[2rem]`}
|
||||
/>
|
||||
<ul>
|
||||
{navTabLayoutOrder.map((tab) => {
|
||||
const tabComponent = tabs[tab];
|
||||
return (
|
||||
<li
|
||||
class={tw`
|
||||
w-[3.5rem] h-[3.5rem] cursor-pointer hover:bg-[${SecondaryBackgroundColor}] rounded-[1rem] mb-[0.5rem] ${CenterClass}
|
||||
${
|
||||
props.activeNav === tab
|
||||
? `bg-[${SecondaryBackgroundColor}] hover:bg-[${SecondaryBackgroundColor}]`
|
||||
: ""
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
props.emit({
|
||||
type: "ChangeNavigation",
|
||||
index: tab,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{tabComponent(props.activeNav === tab)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
props.emit({
|
||||
type: "ChangeNavigation",
|
||||
index: "Setting",
|
||||
});
|
||||
}}
|
||||
class={tw`
|
||||
w-[3.5rem] h-[3.5rem] mx-auto hover:bg-[${SecondaryBackgroundColor}] rounded-[1rem] ${CenterClass} ${NoOutlineClass}
|
||||
${props.activeNav === "Setting" ? `bg-[${SecondaryBackgroundColor}]` : ""}
|
||||
`}
|
||||
>
|
||||
<SettingIcon
|
||||
class={tw`w-[2rem] h-[2rem]`}
|
||||
style={{
|
||||
stroke: props.activeNav === "Setting" ? PrimaryTextColor : SecondaryTextColor,
|
||||
fill: "none",
|
||||
}}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
type Props = {
|
||||
publicKey: PublicKey;
|
||||
profileGetter: ProfileGetter;
|
||||
emit: emitFunc<NavigationUpdate>;
|
||||
isMobile?: boolean;
|
||||
};
|
||||
|
||||
export function MobileNavBar(props: Props) {
|
||||
return (
|
||||
<div
|
||||
class={tw`bg-[${PrimaryBackgroundColor}] h-[5.75rem] w-full flex justify-between overflow-x-auto py-[1.12rem] px-[3rem]`}
|
||||
>
|
||||
<div
|
||||
class={tw`flex`}
|
||||
style={{
|
||||
minWidth: "fit-content",
|
||||
}}
|
||||
>
|
||||
<ul class={tw`flex`}>
|
||||
{navTabLayoutOrder.map((tab) => {
|
||||
const tabComponent = tabs[tab];
|
||||
return (
|
||||
<li
|
||||
class={tw`
|
||||
w-[3.5rem] h-[3.5rem] cursor-pointer hover:bg-[${SecondaryBackgroundColor}] rounded-[1rem] mr-[0.5rem] ${CenterClass}
|
||||
${
|
||||
props.activeNav === tab
|
||||
? `bg-[${SecondaryBackgroundColor}] hover:bg-[${SecondaryBackgroundColor}]`
|
||||
: ""
|
||||
type State = {
|
||||
activeIndex: number;
|
||||
};
|
||||
|
||||
type NavTabID = "DM" | "Profile" | "About" | "AppList" | "Setting";
|
||||
type NavTab = {
|
||||
icon: (active: boolean) => ComponentChild;
|
||||
id: NavTabID;
|
||||
};
|
||||
|
||||
export class NavBar extends Component<Props, State> {
|
||||
styles = {
|
||||
container:
|
||||
tw`h-full w-16 flex flex-col gap-y-4 overflow-y-auto bg-[${PrimaryBackgroundColor}] py-8 items-center`,
|
||||
icons: (active: boolean, fill?: boolean) => (
|
||||
tw`w-6 h-6 ${fill ? "fill-current" : "stroke-current"} text-[${
|
||||
active ? PrimaryTextColor : SecondaryTextColor
|
||||
}]`
|
||||
),
|
||||
avatar: tw`w-12 h-12`,
|
||||
tabsContainer: tw`last:flex-1 last:flex last:items-end`,
|
||||
tabs: (active: boolean) =>
|
||||
tw`rounded-lg w-10 h-10 ${
|
||||
active ? `bg-[${SecondaryBackgroundColor}]` : ""
|
||||
} hover:bg-[${SecondaryBackgroundColor}] ${CenterClass} ${NoOutlineClass}`,
|
||||
mobileContainer: tw`h-16 flex justify-evenly bg-[${PrimaryBackgroundColor}] items-center`,
|
||||
};
|
||||
|
||||
myProfile: ProfileData | undefined;
|
||||
tabs: NavTab[] = [
|
||||
{
|
||||
icon: (active: boolean) => <ChatIcon class={this.styles.icons(active)} />,
|
||||
id: "DM",
|
||||
},
|
||||
{
|
||||
icon: (active: boolean) => <UserIcon class={this.styles.icons(active)} />,
|
||||
id: "Profile",
|
||||
},
|
||||
{
|
||||
icon: (active: boolean) => <AboutIcon class={this.styles.icons(active, true)} />,
|
||||
id: "About",
|
||||
},
|
||||
{
|
||||
icon: (active: boolean) => <AppListIcon class={this.styles.icons(active, true)} />,
|
||||
id: "AppList",
|
||||
},
|
||||
{
|
||||
icon: (active: boolean) => <SettingIcon class={this.styles.icons(active)} />,
|
||||
id: "Setting",
|
||||
},
|
||||
];
|
||||
|
||||
componentWillMount() {
|
||||
this.setState({
|
||||
activeIndex: 0,
|
||||
});
|
||||
this.myProfile = this.props.profileGetter.getProfilesByPublicKey(this.props.publicKey)?.profile;
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
props.emit({
|
||||
|
||||
changeTab = (activeIndex: number) => {
|
||||
if (activeIndex == this.state.activeIndex) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.emit({
|
||||
type: "ChangeNavigation",
|
||||
index: tab,
|
||||
id: this.tabs[activeIndex].id,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{tabComponent(props.activeNav === tab)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
<div
|
||||
class={tw`w-20 h-full flex`}
|
||||
style={{
|
||||
minWidth: "5rem",
|
||||
}}
|
||||
>
|
||||
|
||||
this.setState({
|
||||
activeIndex: activeIndex,
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
this.props.isMobile
|
||||
? (
|
||||
<div class={this.styles.mobileContainer}>
|
||||
{this.tabs.map((tab, index) => (
|
||||
<button
|
||||
onClick={() => {
|
||||
props.emit({
|
||||
type: "ChangeNavigation",
|
||||
index: "Setting",
|
||||
});
|
||||
}}
|
||||
class={tw`
|
||||
w-[3.5rem] h-[3.5rem] mx-auto hover:bg-[${SecondaryBackgroundColor}] rounded-[1rem] ${CenterClass} ${NoOutlineClass}
|
||||
${props.activeNav === "Setting" ? `bg-[${SecondaryBackgroundColor}]` : ""}
|
||||
`}
|
||||
onClick={() => this.changeTab(index)}
|
||||
class={this.styles.tabs(this.state.activeIndex == index)}
|
||||
>
|
||||
<SettingIcon
|
||||
class={tw`w-[2rem] h-[2rem]`}
|
||||
style={{
|
||||
stroke: props.activeNav === "Setting" ? PrimaryTextColor : SecondaryTextColor,
|
||||
fill: "none",
|
||||
}}
|
||||
/>
|
||||
{tab.icon(this.state.activeIndex == index)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
: (
|
||||
<div class={this.styles.container}>
|
||||
<Avatar class={this.styles.avatar} picture={this.myProfile?.picture} />
|
||||
{this.tabs.map((tab, index) => (
|
||||
<div class={this.styles.tabsContainer}>
|
||||
<button
|
||||
onClick={() => this.changeTab(index)}
|
||||
class={this.styles.tabs(this.state.activeIndex == index)}
|
||||
>
|
||||
{tab.icon(this.state.activeIndex == index)}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user