Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pinned Apps (2 of 2) #1103

Merged
merged 3 commits into from
Apr 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/components/lib/Text/Text.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import styled from 'styled-components';

type Props = {
color?: string;
font?: string;
font?: 'text-xs' | 'text-s' | 'text-base' | 'text-l' | 'text-xl' | 'text-2xl' | 'text-3xl' | 'text-hero';
weight?: string;
};

Expand Down
4 changes: 3 additions & 1 deletion src/components/near-org/ComponentWrapperPage.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect } from 'react';

import { VmComponent } from '@/components/vm/VmComponent';
import { useGatewayEvents } from '@/hooks/useGatewayEvents';
import { useCurrentComponentStore } from '@/stores/current-component';

import { MetaTags } from '../MetaTags';
Expand All @@ -16,6 +17,7 @@ type Props = {

export function ComponentWrapperPage(props: Props) {
const setCurrentComponentSrc = useCurrentComponentStore((store) => store.setSrc);
const { emitGatewayEvent } = useGatewayEvents();

useEffect(() => {
if (
Expand All @@ -37,7 +39,7 @@ export function ComponentWrapperPage(props: Props) {
return (
<>
{props.meta && <MetaTags {...props.meta} />}
<VmComponent src={props.src} props={props.componentProps} />
<VmComponent src={props.src} props={{ ...props.componentProps, emitGatewayEvent }} />
</>
);
}
108 changes: 108 additions & 0 deletions src/components/sidebar-navigation/PinnedApps.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
/* eslint-disable @next/next/no-img-element */

import { useRouter } from 'next/router';
import { useEffect } from 'react';

import { useBosComponents } from '@/hooks/useBosComponents';
import { useAuthStore } from '@/stores/auth';

import { Button } from '../lib/Button';
import { Text } from '../lib/Text';
import { Tooltip } from '../lib/Tooltip';
import { useNavigationStore } from './store';
import * as S from './styles';
import { currentPathMatchesRoute } from './utils';

export const PinnedApps = () => {
const router = useRouter();
const accountId = useAuthStore((store) => store.accountId);
const loadPinnedApps = useNavigationStore((store) => store.loadPinnedApps);
const pinnedApps = useNavigationStore((store) => store.pinnedApps);
const expandedDrawer = useNavigationStore((store) => store.expandedDrawer);
const isSidebarExpanded = useNavigationStore((store) => store.isSidebarExpanded && !store.expandedDrawer);
const tooltipsDisabled = isSidebarExpanded;
const components = useBosComponents();
const discoverAppsPath = `/${components.componentsPage}?tab=apps`;
const discoverTooltipContent = 'Discover apps to pin';

const isNavigationItemActive = (route: string | string[], exactMatch = false) => {
if (expandedDrawer) return false;
return currentPathMatchesRoute(router.asPath, route, exactMatch);
};

useEffect(() => {
loadPinnedApps(accountId);
}, [accountId, loadPinnedApps]);

return (
<S.Section>
<S.SectionLabel>
Pinned Apps
<Tooltip content={discoverTooltipContent} side="right">
<S.SectionLabelIconLink href={discoverAppsPath}>
<i className="ph-bold ph-circles-three-plus" />
</S.SectionLabelIconLink>
</Tooltip>
</S.SectionLabel>

{pinnedApps && pinnedApps.length > 0 ? (
<S.Stack $gap="0.5rem">
{pinnedApps?.map((app) => (
<Tooltip
content={app.displayName}
side="right"
disabled={tooltipsDisabled}
key={app.authorAccountId + app.componentName}
>
<S.NavigationItem
$active={isNavigationItemActive(`/${app.authorAccountId}/widget/${app.componentName}`)}
$type="featured"
href={`/${app.authorAccountId}/widget/${app.componentName}`}
>
{app.imageUrl ? (
<S.NavigationItemThumbnail>
<img src={app.imageUrl} alt={app.displayName} />
</S.NavigationItemThumbnail>
) : (
<i className="ph-bold ph-app-window" />
)}
<span>{app.displayName}</span>
</S.NavigationItem>
</Tooltip>
))}
</S.Stack>
) : (
<>
{isSidebarExpanded ? (
<S.Stack $gap="1rem" $frozenWidth>
<Text font="text-xs" color="sand11">
Discover apps from the NEAR developer community to pin.
</Text>

<Button
label="Discover Apps"
size="small"
variant="secondary"
href={discoverAppsPath}
className="sidebar-auto-width-button"
/>
</S.Stack>
) : (
<S.Stack>
<Tooltip content={discoverTooltipContent} side="right" disabled={tooltipsDisabled}>
<S.NavigationItem
$active={isNavigationItemActive(discoverAppsPath)}
$type="featured"
href={discoverAppsPath}
>
<i className="ph-bold ph-circles-three-plus" />
<span>Discover Apps</span>
</S.NavigationItem>
</Tooltip>
</S.Stack>
)}
</>
)}
</S.Section>
);
};
3 changes: 3 additions & 0 deletions src/components/sidebar-navigation/Sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useRouter } from 'next/router';

import { Tooltip } from '../lib/Tooltip';
import NearIconSvg from './icons/near-icon.svg';
import { PinnedApps } from './PinnedApps';
import { useNavigationStore } from './store';
import * as S from './styles';
import { currentPathMatchesRoute } from './utils';
Expand Down Expand Up @@ -71,6 +72,8 @@ export const Sidebar = () => {
</S.Stack>
</S.Section>

<PinnedApps />

<S.Section>
<S.SectionLabel>Resources</S.SectionLabel>

Expand Down
32 changes: 15 additions & 17 deletions src/components/sidebar-navigation/SmallScreenHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,23 +61,13 @@ export const SmallScreenHeader = () => {
)}

{signedIn ? (
<>
<S.SmallScreenHeaderActions $hidden={isOpenedOnSmallScreens}>
<VmComponent
showLoadingSpinner={false}
src={components.navigation.smallScreenHeader}
props={{ availableStorage: availableStorageDisplay, withdrawTokens, logOut }}
/>
</S.SmallScreenHeaderActions>

<S.SmallScreenHeaderIconButton
type="button"
aria-label="Expand/Collapse Menu"
onClick={toggleExpandedSidebarOnSmallScreens}
>
<i className={`ph ${isOpenedOnSmallScreens ? 'ph-x' : 'ph-list'}`} />
</S.SmallScreenHeaderIconButton>
</>
<S.SmallScreenHeaderActions $hidden={isOpenedOnSmallScreens}>
<VmComponent
showLoadingSpinner={false}
src={components.navigation.smallScreenHeader}
props={{ availableStorage: availableStorageDisplay, withdrawTokens, logOut }}
/>
</S.SmallScreenHeaderActions>
) : (
<Button
label="Create Account"
Expand All @@ -87,6 +77,14 @@ export const SmallScreenHeader = () => {
/>
)}

<S.SmallScreenHeaderIconButton
type="button"
aria-label="Expand/Collapse Menu"
onClick={toggleExpandedSidebarOnSmallScreens}
>
<i className={`ph ${isOpenedOnSmallScreens ? 'ph-x' : 'ph-list'}`} />
</S.SmallScreenHeaderIconButton>

<S.SmallScreenNavigationBackground $expanded={isOpenedOnSmallScreens} />
</S.SmallScreenHeader>
);
Expand Down
76 changes: 73 additions & 3 deletions src/components/sidebar-navigation/store.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { MouseEvent } from 'react';
import { create } from 'zustand';

import { isSmallScreen, SIDEBAR_EXPANDED_PREFERENCE_KEY } from './utils';
import type { PinnedApp } from './utils';
import { fetchPinnedApps, isSmallScreen, PINNED_APPS_CACHE_KEY, SIDEBAR_EXPANDED_PREFERENCE_KEY } from './utils';

export type NavigationDrawer = 'discover' | 'marketing';

Expand All @@ -12,12 +13,16 @@ type NavigationState = {
hasInitialized: boolean;
isOpenedOnSmallScreens: boolean;
isSidebarExpanded: boolean;
pinnedApps: PinnedApp[] | null;
};

type NavigationStore = NavigationState & {
initialize: () => void;
handleBubbledClickInDrawer: () => void;
handleBubbledClickInSidebar: (event: MouseEvent<HTMLDivElement>) => void;
loadPinnedApps: (accountId: string | null) => Promise<void>;
modifyPinnedApps: (app: PinnedApp, modification: 'PINNED' | 'UNPINNED') => void;
reset: () => void;
set: (state: Partial<NavigationState>) => void;
setCurrentPageTitle: (title: string | null) => void;
setInitialExpandedDrawer: (drawer: NavigationDrawer) => void;
Expand All @@ -33,6 +38,7 @@ export const useNavigationStore = create<NavigationStore>((set) => ({
hasInitialized: false,
isOpenedOnSmallScreens: false,
isSidebarExpanded: true,
pinnedApps: null,

initialize: () => {
set((state) => {
Expand Down Expand Up @@ -69,6 +75,72 @@ export const useNavigationStore = create<NavigationStore>((set) => ({
});
},

loadPinnedApps: async (accountId) => {
/*
Event if we don't have an accountId yet (wallet state is still resolving on the client side),
we can load the previously cached pinned apps instantly to improve perceived performance in
the sidebar. The pinned apps cache is cleared out when the user logs out - which triggers the
reset() action.
*/

const cachedPinnedAppsStringified = localStorage.getItem(PINNED_APPS_CACHE_KEY);
if (cachedPinnedAppsStringified) {
try {
const cachedPinnedApps = JSON.parse(cachedPinnedAppsStringified);
set({ pinnedApps: cachedPinnedApps });
} catch (error) {
console.error('Failed to parse cached pinned apps', error);
}
}

if (!accountId) return;

const pinnedApps = await fetchPinnedApps(accountId);
localStorage.setItem(PINNED_APPS_CACHE_KEY, JSON.stringify(pinnedApps));

set({ pinnedApps });
},

modifyPinnedApps: (modifiedApp, modification) => {
set((state) => {
let pinnedApps = [...(state.pinnedApps || [])];

if (modification === 'PINNED') {
const existingPinnedApp = pinnedApps.find((pinnedApp) => {
const pinnedAppId = pinnedApp.authorAccountId + pinnedApp.componentName;
const modifiedAppId = modifiedApp.authorAccountId + modifiedApp.componentName;
return pinnedAppId === modifiedAppId;
});

if (!existingPinnedApp) {
// Avoid adding a duplicate if it's already pinned
pinnedApps.push(modifiedApp);
}
} else if (modification === 'UNPINNED') {
pinnedApps = pinnedApps.filter((pinnedApp) => {
const pinnedAppId = pinnedApp.authorAccountId + pinnedApp.componentName;
const modifiedAppId = modifiedApp.authorAccountId + modifiedApp.componentName;
return pinnedAppId !== modifiedAppId;
});
} else {
console.error('Unimplemented modification type in modifyPinnedApps():', modification);
}

localStorage.setItem(PINNED_APPS_CACHE_KEY, JSON.stringify(pinnedApps));

return {
pinnedApps,
};
});
},

reset: () => {
localStorage.removeItem(PINNED_APPS_CACHE_KEY);
set({ pinnedApps: null });
},

set: (state) => set(() => state),

setCurrentPageTitle: (currentPageTitle) => set(() => ({ currentPageTitle })),

setInitialExpandedDrawer: (drawer) => {
Expand Down Expand Up @@ -126,6 +198,4 @@ export const useNavigationStore = create<NavigationStore>((set) => ({
}
});
},

set: (state) => set(() => state),
}));
Loading
Loading