refactor navigation bar (#271)

This commit is contained in:
Bullish Bear 2023-11-02 21:10:16 +08:00 committed by GitHub
parent e9f95b493c
commit 21bcc1198b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 217 additions and 235 deletions

View File

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

View File

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

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

View File

@ -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;
render(
<div class={tw`h-screen`}>
<NavBar emit={testEventBus.emit} publicKey={ctx.publicKey} profileGetter={db} />
<NavBar emit={testEventBus.emit} publicKey={ctx.publicKey} profileGetter={db} isMobile={true} />
</div>,
document.body,
);
for await (const event of testEventBus.onChange()) {
console.log(event);
}
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 (
<div class={tw`h-screen`}>
<nav.NavBar
{...NavProps}
/>
</div>
);
}
render(<NavTest />, document.body);

View File

@ -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}]`
: ""
}
`}
onClick={() => {
props.emit({
type: "ChangeNavigation",
index: tab,
});
}}
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;
}
changeTab = (activeIndex: number) => {
if (activeIndex == this.state.activeIndex) {
return;
}
this.props.emit({
type: "ChangeNavigation",
id: this.tabs[activeIndex].id,
});
this.setState({
activeIndex: activeIndex,
});
};
render() {
return (
this.props.isMobile
? (
<div class={this.styles.mobileContainer}>
{this.tabs.map((tab, index) => (
<button
onClick={() => this.changeTab(index)}
class={this.styles.tabs(this.state.activeIndex == index)}
>
{tabComponent(props.activeNav === tab)}
</li>
);
})}
</ul>
</div>
<div
class={tw`w-20 h-full flex`}
style={{
minWidth: "5rem",
}}
>
<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>
</div>
);
{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>
)
);
}
}