Add option to favorite apps

This commit is contained in:
hzrd149 2025-01-16 17:11:48 -06:00
parent 0b6e8e9947
commit 1045c26472
18 changed files with 511 additions and 419 deletions

View File

@ -0,0 +1,5 @@
---
"nostrudel": minor
---
Add option to favorite apps

View File

@ -100,8 +100,8 @@
"react-diff-viewer-continued": "^3.4.0",
"react-dom": "^19.0.0",
"react-error-boundary": "^4.1.2",
"react-force-graph-2d": "^1.26.2",
"react-force-graph-3d": "^1.25.2",
"react-force-graph-2d": "^1.27.0",
"react-force-graph-3d": "^1.26.0",
"react-hook-form": "^7.54.2",
"react-markdown": "^9.0.3",
"react-mosaic-component": "^6.1.1",
@ -133,14 +133,14 @@
},
"devDependencies": {
"@capacitor-community/http": "^1.4.1",
"@capacitor-mlkit/barcode-scanning": "^6.1.0",
"@capacitor/android": "^6.1.1",
"@capacitor/app": "^6.0.0",
"@capacitor-mlkit/barcode-scanning": "^6.2.0",
"@capacitor/android": "^6.2.0",
"@capacitor/app": "^6.0.2",
"@capacitor/assets": "^3.0.5",
"@capacitor/cli": "^6.1.1",
"@capacitor/core": "^6.1.1",
"@capacitor/ios": "^6.1.1",
"@capacitor/preferences": "^6.0.2",
"@capacitor/cli": "^6.2.0",
"@capacitor/core": "^6.2.0",
"@capacitor/ios": "^6.2.0",
"@capacitor/preferences": "^6.0.3",
"@changesets/cli": "^2.27.11",
"@types/canvas-confetti": "^1.9.0",
"@types/chroma-js": "^2.4.5",
@ -153,8 +153,8 @@
"@types/leaflet.locatecontrol": "^0.74.6",
"@types/lodash.throttle": "^4.1.9",
"@types/ngeohash": "^0.6.8",
"@types/react": "^19.0.7",
"@types/react-dom": "^19.0.3",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-window": "^1.8.8",
"@types/three": "^0.160.0",
"@types/webscopeio__react-textarea-autocomplete": "^4.7.5",

493
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@ -1,74 +1,52 @@
import { useMemo } from "react";
import { Spacer } from "@chakra-ui/react";
import { useLocation } from "react-router";
import { nip19 } from "nostr-tools";
import { Divider, Spacer } from "@chakra-ui/react";
import {
DirectMessagesIcon,
NotificationsIcon,
ProfileIcon,
RelayIcon,
SearchIcon,
NotesIcon,
LightningIcon,
SettingsIcon,
} from "../../icons";
import { LightningIcon, SettingsIcon } from "../../icons";
import useCurrentAccount from "../../../hooks/use-current-account";
import PuzzlePiece01 from "../../icons/puzzle-piece-01";
import Package from "../../icons/package";
import Rocket02 from "../../icons/rocket-02";
import useRecentIds from "../../../hooks/use-recent-ids";
import { internalApps, internalTools } from "../../../views/other-stuff/apps";
import { App } from "../../../views/other-stuff/component/app-card";
import { defaultFavoriteApps, internalApps, internalTools } from "../../navigation/apps";
import NavItem from "./nav-item";
import { QuestionIcon } from "@chakra-ui/icons";
import Plus from "../../icons/plus";
import useFavoriteInternalIds from "../../../hooks/use-favorite-internal-ids";
export default function NavItems() {
const location = useLocation();
const account = useCurrentAccount();
const { recent: recentApps } = useRecentIds("apps");
const otherStuff = useMemo(() => {
const { ids: favorites = defaultFavoriteApps } = useFavoriteInternalIds("apps", "app");
const { recent } = useRecentIds("apps", 3);
const favoriteApps = useMemo(() => {
const internal = [...internalApps, ...internalTools];
const apps = recentApps.map((id) => internal.find((app) => app.id === id)).filter(Boolean) as App[];
if (apps.length > 3) {
apps.length = 3;
} else {
if (apps.length < 3 && !apps.some((a) => a.id === "streams")) {
apps.push(internal.find((app) => app.id === "streams")!);
}
if (apps.length < 3 && !apps.some((a) => a.id === "articles")) {
apps.push(internal.find((app) => app.id === "articles")!);
}
if (apps.length < 3 && !apps.some((a) => a.id === "channels")) {
apps.push(internal.find((app) => app.id === "channels")!);
}
}
return apps;
}, [recentApps]);
return favorites.map((id) => internal.find((app) => app.id === id)).filter((a) => !!a);
}, [favorites]);
const recentApps = useMemo(() => {
const internal = [...internalApps, ...internalTools];
return recent
.filter((id) => !favorites.includes(id))
.map((id) => internal.find((app) => app.id === id))
.filter((a) => !!a);
}, [recent, favorites]);
return (
<>
{account && !account.readonly && (
<NavItem icon={Plus} label="Create new" colorScheme="primary" to="/new" variant="solid" />
)}
<NavItem to="/launchpad" icon={Rocket02} label="Launchpad" />
<NavItem to="/" icon={NotesIcon} colorScheme={location.pathname === "/" ? "primary" : "gray"} label="Notes" />
<NavItem label="Discover" to="/discovery" icon={PuzzlePiece01} />
{account && (
<>
<NavItem to="/notifications" icon={NotificationsIcon} label="Notifications" />
<NavItem to="/messages" icon={DirectMessagesIcon} label="Messages" />
</>
)}
<NavItem to="/search" icon={SearchIcon} label="Search" />
{account?.pubkey && <NavItem to={"/u/" + nip19.npubEncode(account.pubkey)} icon={ProfileIcon} label="Profile" />}
<NavItem to="/relays" icon={RelayIcon} label="Relays" />
{otherStuff.map((app) => (
{favoriteApps.map((app) => (
<NavItem key={app.id} to={app.to} icon={app.icon || QuestionIcon} label={app.title} />
))}
<NavItem to="/other-stuff" icon={Package} label="More" />
<NavItem to="/other-stuff" icon={Package} label="All Apps" />
{recentApps.length > 0 && (
<>
<Divider />
{recentApps.map((app) => (
<NavItem key={app.id} to={app.to} icon={app.icon || QuestionIcon} label={app.title} />
))}
</>
)}
<Spacer />
<NavItem to="/support" icon={LightningIcon} label="Support" />
<NavItem label="Settings" icon={SettingsIcon} to="/settings" />

View File

@ -0,0 +1,45 @@
import { PropsWithChildren, Suspense } from "react";
import { Outlet, useMatch } from "react-router";
import { Box, Flex, FlexProps, Spinner } from "@chakra-ui/react";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
import SimpleHeader from "./simple-header";
import { ErrorBoundary } from "../../error-boundary";
export default function ContainedParentView({
children,
path,
title,
width = "xs",
}: PropsWithChildren<{ path: string; title?: string; width?: FlexProps["w"]; contain?: boolean }>) {
const match = useMatch(path);
const isMobile = useBreakpointValue({ base: true, lg: false });
const showMenu = !isMobile || !!match;
if (showMenu)
return (
<Flex flex={1} overflow="hidden" h="full">
<Flex width={width} direction="column">
{title && <SimpleHeader title={title} position="initial" />}
<Flex direction="column" p="2" gap="2" overflowY="auto" overflowX="hidden">
{children}
</Flex>
</Flex>
{!isMobile && (
<Suspense fallback={<Spinner />}>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</Suspense>
)}
</Flex>
);
else
return (
<Suspense fallback={<Spinner />}>
<ErrorBoundary>
<Outlet />
</ErrorBoundary>
</Suspense>
);
}

View File

@ -15,7 +15,7 @@ export default function SimpleHeader({ children, title, ...props }: Omit<FlexPro
top="var(--safe-top)"
mt="var(--safe-top)"
backgroundColor="var(--chakra-colors-chakra-body-bg)"
zIndex="popover"
zIndex="modal"
{...props}
>
<BackIconButton hideFrom="lg" />

View File

@ -1,6 +1,6 @@
import { PropsWithChildren, Suspense } from "react";
import { Outlet, useMatch } from "react-router";
import { Box, Flex, Spinner } from "@chakra-ui/react";
import { Box, Flex, FlexProps, Spinner } from "@chakra-ui/react";
import { useBreakpointValue } from "../../../providers/global/breakpoint-provider";
import SimpleHeader from "./simple-header";
@ -10,7 +10,9 @@ export default function SimpleParentView({
children,
path,
title,
}: PropsWithChildren<{ path: string; title?: string }>) {
width = "xs",
contain,
}: PropsWithChildren<{ path: string; title?: string; width?: FlexProps["w"]; contain?: boolean }>) {
const match = useMatch(path);
const isMobile = useBreakpointValue({ base: true, lg: false });
const showMenu = !isMobile || !!match;
@ -20,9 +22,9 @@ export default function SimpleParentView({
if (showMenu)
return (
<Flex flex={1} direction={floating ? "row" : "column"}>
<Box w={floating ? "xs" : 0} flexGrow={0} flexShrink={0} />
<Box w={floating ? width : 0} flexGrow={0} flexShrink={0} />
<Flex
minW="xs"
width={width}
direction="column"
position={floating ? "fixed" : "initial"}
top="var(--safe-top)"

View File

@ -1,7 +1,7 @@
import { ReactNode } from "react";
import { Flex, FlexProps } from "@chakra-ui/react";
import SimpleHeader from "./simple-header";
import { ReactNode } from "react";
export default function SimpleView({
children,
@ -20,8 +20,6 @@ export default function SimpleView({
<Flex
direction="column"
overflowY="auto"
overflowX="hidden"
px={flush ? 0 : "4"}
pt={flush ? 0 : "4"}
pb={flush ? 0 : "max(1rem, var(--safe-bottom))"}

View File

@ -0,0 +1,53 @@
import { useState } from "react";
import { IconButton, IconButtonProps } from "@chakra-ui/react";
import { kinds } from "nostr-tools";
import { modifyEventTags, unixNow } from "applesauce-core/helpers";
import { Operations } from "applesauce-lists/helpers";
import { App, defaultFavoriteApps } from "./apps";
import useFavoriteInternalIds from "../../hooks/use-favorite-internal-ids";
import { useSigningContext } from "../../providers/global/signing-provider";
import { usePublishEvent } from "../../providers/global/publish-provider";
import { StarEmptyIcon, StarFullIcon } from "../icons";
export default function AppFavoriteButton({
app,
...props
}: { app: App } & Omit<IconButtonProps, "children" | "aria-label" | "isLoading" | "onClick">) {
const publish = usePublishEvent();
const { finalizeDraft } = useSigningContext();
const { favorites } = useFavoriteInternalIds("apps", "app");
const isFavorite = favorites?.tags.some((t) => t[0] === "app" && t[1] === app.id);
const [loading, setLoading] = useState(false);
const handleClick = async () => {
const prev =
favorites ||
(await finalizeDraft({
kind: kinds.Application,
created_at: unixNow(),
content: "",
tags: [["d", "nostrudel-favorite-apps"], ...defaultFavoriteApps.map((id) => ["app", id])],
}));
setLoading(true);
const tag = ["app", app.id];
const draft = await modifyEventTags(prev, {
public: isFavorite ? Operations.removeNameValueTag(tag) : Operations.addNameValueTag(tag),
});
await publish(isFavorite ? "Unfavorite app" : "Favorite app", draft);
setLoading(false);
};
return (
<IconButton
icon={isFavorite ? <StarFullIcon boxSize="1.1em" /> : <StarEmptyIcon boxSize="1.1em" />}
aria-label={isFavorite ? "Unfavorite app" : "Favorite app"}
title={isFavorite ? "Unfavorite app" : "Favorite app"}
onClick={handleClick}
isLoading={loading}
color={isFavorite ? "yellow.400" : undefined}
{...props}
/>
);
}

View File

@ -1,3 +1,5 @@
import { ComponentWithAs, IconProps } from "@chakra-ui/react";
import {
ArticleIcon,
BadgeIcon,
@ -11,21 +13,52 @@ import {
MapIcon,
MediaIcon,
MuteIcon,
NotesIcon,
NotificationsIcon,
SearchIcon,
TorrentIcon,
TrackIcon,
VideoIcon,
WikiIcon,
} from "../../components/icons";
import { App } from "./component/app-card";
import ShieldOff from "../../components/icons/shield-off";
import MessageQuestionSquare from "../../components/icons/message-question-square";
import UploadCloud01 from "../../components/icons/upload-cloud-01";
import Edit04 from "../../components/icons/edit-04";
import Users03 from "../../components/icons/users-03";
import FileAttachment01 from "../../components/icons/file-attachment-01";
} from "../icons";
import ShieldOff from "../icons/shield-off";
import MessageQuestionSquare from "../icons/message-question-square";
import UploadCloud01 from "../icons/upload-cloud-01";
import Edit04 from "../icons/edit-04";
import Users03 from "../icons/users-03";
import FileAttachment01 from "../icons/file-attachment-01";
import Rocket02 from "../icons/rocket-02";
import PuzzlePiece01 from "../icons/puzzle-piece-01";
export type App = {
icon?: ComponentWithAs<"svg", IconProps>;
image?: string;
title: string;
description: string;
id: string;
isExternal?: boolean;
to: string;
};
export const internalApps: App[] = [
{ title: "Notes", description: "Short text posts from your friends", icon: NotesIcon, id: "notes", to: "/notes" },
{ title: "Launchpad", description: "Quick account overview", icon: Rocket02, id: "launchpad", to: "/launchpad" },
{ title: "Discover", description: "Discover new feeds", icon: PuzzlePiece01, id: "discover", to: "/discovery" },
{
title: "Notifications",
description: "Notifications feed",
icon: NotificationsIcon,
id: "notifications",
to: "/notifications",
},
{
title: "Messages",
description: "Direct Messages",
icon: DirectMessagesIcon,
id: "messages",
to: "/messages",
},
{ title: "Search", description: "Search for users and notes", icon: SearchIcon, id: "search", to: "/search" },
{
title: "Streams",
description: "Watch live streams",
@ -219,4 +252,6 @@ export const externalTools: App[] = [
},
];
export const defaultFavoriteApps = ["launchpad", "notes", "discover", "notifications", "messages", "search"];
export const allApps = [...internalApps, ...internalTools, ...externalTools];

View File

@ -2,7 +2,7 @@ import { ComponentWithAs, Flex, FlexProps } from "@chakra-ui/react";
const VerticalPageLayout: ComponentWithAs<"div", FlexProps> = ({ children, ...props }: FlexProps) => {
return (
<Flex direction="column" pt="2" pb="12" gap="2" px="2" overflowX="hidden" w="full" {...props}>
<Flex direction="column" pt="2" pb="12" gap="2" px="2" w="full" {...props}>
{children}
</Flex>
);

View File

@ -0,0 +1,16 @@
import { kinds } from "nostr-tools";
import useReplaceableEvent from "./use-replaceable-event";
import useCurrentAccount from "./use-current-account";
export default function useFavoriteInternalIds(identifier: string, tagName = "id", pubkey?: string) {
const account = useCurrentAccount();
pubkey = pubkey || account?.pubkey;
const favorites = useReplaceableEvent(
pubkey ? { kind: kinds.Application, pubkey, identifier: `nostrudel-favorite-${identifier}` } : undefined,
);
const ids = favorites?.tags.filter((t) => t[0] === tagName && t[1]).map((t) => t[1]);
return { ids, favorites };
}

View File

@ -32,4 +32,5 @@ body,
width: 100%;
display: flex;
flex-direction: column;
min-height: 100%;
}

View File

@ -3,7 +3,7 @@ import { Button, Card, CardBody, CardHeader, CardProps, Heading, Input, Link, Si
import { Link as RouterLink } from "react-router";
import useRecentIds from "../../../hooks/use-recent-ids";
import { allApps } from "../../other-stuff/apps";
import { allApps } from "../../../components/navigation/apps";
import AppCard from "../../other-stuff/component/app-card";
export default function ToolsCard({ ...props }: Omit<CardProps, "children">) {

View File

@ -1,5 +1,5 @@
import { memo, useCallback, useContext, useEffect, useMemo, useState } from "react";
import { Button, ButtonGroup, Card, Flex, IconButton } from "@chakra-ui/react";
import { Button, ButtonGroup, Flex, IconButton } from "@chakra-ui/react";
import { UNSAFE_DataRouterContext, useLocation, useNavigate } from "react-router";
import { NostrEvent, kinds } from "nostr-tools";
@ -12,7 +12,6 @@ import useCurrentAccount from "../../hooks/use-current-account";
import IntersectionObserverProvider from "../../providers/local/intersection-observer";
import { useTimelineCurserIntersectionCallback } from "../../hooks/use-timeline-cursor-intersection-callback";
import TimelineActionAndStatus from "../../components/timeline/timeline-action-and-status";
import UserDnsIdentity from "../../components/user/user-dns-identity";
import SendMessageForm from "./components/send-message-form";
import { groupMessages } from "../../helpers/nostr/dms";
import ThreadDrawer from "./components/thread-drawer";
@ -24,8 +23,10 @@ import RelaySet from "../../classes/relay-set";
import useAppSettings from "../../hooks/use-user-app-settings";
import { truncateId } from "../../helpers/string";
import useRouterMarker from "../../hooks/use-router-marker";
import { BackIconButton } from "../../components/router/back-button";
import decryptionCacheService from "../../services/decryption-cache";
import SimpleView from "../../components/layout/presets/simple-view";
import UserDnsIdentityIcon from "../../components/user/user-dns-identity-icon";
import SimpleHeader from "../../components/layout/presets/simple-header";
/** This is broken out from DirectMessageChatPage for performance reasons. Don't use outside of file */
const ChatLog = memo(({ messages }: { messages: NostrEvent[] }) => {
@ -113,14 +114,17 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
return (
<ThreadsProvider timeline={loader}>
<IntersectionObserverProvider callback={callback}>
<Card size="sm" flexShrink={0} p="2" flexDirection="row">
<Flex gap="2" alignItems="center">
<BackIconButton />
<UserAvatar pubkey={pubkey} size="sm" />
<UserLink pubkey={pubkey} fontWeight="bold" />
<UserDnsIdentity pubkey={pubkey} onlyIcon />
</Flex>
<Flex flex={1} direction="column" pr="var(--safe-right)" pl="var(--safe-left)" h="full" overflow="hidden">
<SimpleHeader
position="initial"
title={
<Flex gap="2" alignItems="center">
<UserAvatar pubkey={pubkey} size="sm" />
<UserLink pubkey={pubkey} fontWeight="bold" />
<UserDnsIdentityIcon pubkey={pubkey} />
</Flex>
}
>
<ButtonGroup ml="auto">
{!autoDecryptDMs && (
<Button onClick={decryptAll} isLoading={loading}>
@ -134,16 +138,19 @@ function DirectMessageChatPage({ pubkey }: { pubkey: string }) {
onClick={openDrawerList}
/>
</ButtonGroup>
</Card>
<Flex h="0" flex={1} overflowX="hidden" overflowY="scroll" direction="column-reverse" gap="2" py="4" px="2">
<ChatLog messages={messages} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
<SendMessageForm flexShrink={0} pubkey={pubkey} />
{location.state?.thread && (
<ThreadDrawer isOpen onClose={closeDrawer} threadId={location.state.thread} pubkey={pubkey} />
)}
</IntersectionObserverProvider>
</SimpleHeader>
<IntersectionObserverProvider callback={callback}>
<Flex h="0" flex={1} overflowX="hidden" overflowY="auto" direction="column-reverse" gap="2" py="4" px="2">
<ChatLog messages={messages} />
<TimelineActionAndStatus timeline={loader} />
</Flex>
<SendMessageForm flexShrink={0} pubkey={pubkey} px="2" pb="2" />
{location.state?.thread && (
<ThreadDrawer isOpen onClose={closeDrawer} threadId={location.state.thread} pubkey={pubkey} />
)}
</IntersectionObserverProvider>
</Flex>
</ThreadsProvider>
);
}

View File

@ -1,6 +1,6 @@
import { useMemo } from "react";
import { Card, CardBody, Flex, LinkBox, LinkOverlay, Text } from "@chakra-ui/react";
import { Outlet, Link as RouterLink, useLocation, useParams } from "react-router";
import { Link as RouterLink, useLocation } from "react-router";
import { useObservable } from "applesauce-react/hooks";
import { nip19 } from "nostr-tools";
@ -21,6 +21,8 @@ import { CheckIcon } from "../../components/icons";
import UserDnsIdentity from "../../components/user/user-dns-identity";
import useEventIntersectionRef from "../../hooks/use-event-intersection-ref";
import { useKind4Decrypt } from "../../hooks/use-kind4-decryption";
import SimpleParentView from "../../components/layout/presets/simple-parent-view";
import ContainedParentView from "../../components/layout/presets/contained-parent-view";
function MessagePreview({ message, pubkey }: { message: NostrEvent; pubkey: string }) {
const ref = useEventIntersectionRef(message);
@ -59,8 +61,7 @@ function ConversationCard({ conversation }: { conversation: KnownConversation })
);
}
function DirectMessagesPage() {
const params = useParams();
function MessagesHomePage() {
const { people } = usePeopleListContext();
const account = useCurrentAccount()!;
@ -76,44 +77,28 @@ function DirectMessagesPage() {
return filtered.sort((a, b) => b.messages[0].created_at - a.messages[0].created_at);
}, [messages, people, account.pubkey]);
const isChatOpen = !!params.pubkey;
const callback = useTimelineCurserIntersectionCallback(timeline);
return (
<Flex gap="4" h={{ base: "calc(100vh - 3.5rem)", md: "100vh" }} overflow="hidden">
<Flex
gap="2"
direction="column"
w={!isChatOpen ? { base: "full", lg: "sm" } : "sm"}
overflowX="hidden"
overflowY="auto"
py="2"
px={{ base: "2", lg: 0 }}
hideBelow={!isChatOpen ? undefined : "xl"}
>
<Flex gap="2">
<PeopleListSelection flexShrink={0} />
</Flex>
<IntersectionObserverProvider callback={callback}>
{conversations.map((conversation) => (
<ConversationCard key={conversation.pubkeys.join("-")} conversation={conversation} />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
<ContainedParentView path="/messages" width="md">
<Flex gap="2">
<PeopleListSelection flexShrink={0} size="sm" />
</Flex>
<Flex gap="2" direction="column" flex={1} hideBelow={!isChatOpen ? "xl" : undefined} overflow="hidden">
<Outlet />
</Flex>
</Flex>
<IntersectionObserverProvider callback={callback}>
{conversations.map((conversation) => (
<ConversationCard key={conversation.pubkeys.join("-")} conversation={conversation} />
))}
</IntersectionObserverProvider>
<TimelineActionAndStatus timeline={timeline} />
</ContainedParentView>
);
}
export default function DirectMessagesView() {
export default function MessagesHomeView() {
return (
<RequireCurrentAccount>
<PeopleListProvider initList="global">
<DirectMessagesPage />
<MessagesHomePage />
</PeopleListProvider>
</RequireCurrentAccount>
);

View File

@ -1,17 +1,9 @@
import { Link as RouterLink, To } from "react-router";
import { Card, ComponentWithAs, Flex, Heading, IconProps, Image, LinkBox, Text } from "@chakra-ui/react";
import { Link as RouterLink } from "react-router";
import { Card, Flex, Heading, Image, LinkBox, Text } from "@chakra-ui/react";
import HoverLinkOverlay from "../../../components/hover-link-overlay";
export type App = {
icon?: ComponentWithAs<"svg", IconProps>;
image?: string;
title: string;
description: string;
id: string;
isExternal?: boolean;
to: string;
};
import { App } from "../../../components/navigation/apps";
import AppFavoriteButton from "../../../components/navigation/app-favorite-button";
export function AppIcon({ app, size }: { app: App; size: string }) {
if (app.icon) {
@ -21,7 +13,15 @@ export function AppIcon({ app, size }: { app: App; size: string }) {
return null;
}
export default function AppCard({ app, onClick }: { app: App; onClick?: () => void }) {
export default function AppCard({
app,
canFavorite = true,
onClick,
}: {
app: App;
onClick?: () => void;
canFavorite?: boolean;
}) {
return (
<Flex as={LinkBox} gap="4" alignItems="flex-start">
<Card p="3" borderRadius="lg">
@ -41,6 +41,8 @@ export default function AppCard({ app, onClick }: { app: App; onClick?: () => vo
</Heading>
<Text>{app.description}</Text>
</Flex>
{canFavorite && <AppFavoriteButton app={app} variant="ghost" ms="auto" my="2" mr="2" />}
</Flex>
);
}

View File

@ -2,10 +2,10 @@ import { useState } from "react";
import { Heading, Input, SimpleGrid, Tab, TabList, TabPanel, TabPanels, Tabs } from "@chakra-ui/react";
import VerticalPageLayout from "../../components/vertical-page-layout";
import AppCard, { App } from "./component/app-card";
import AppCard from "./component/app-card";
import useRouteSearchValue from "../../hooks/use-route-search-value";
import useRecentIds from "../../hooks/use-recent-ids";
import { allApps, externalTools, internalTools } from "./apps";
import { allApps, App, externalTools, internalTools } from "../../components/navigation/apps";
import { useBreakpointValue } from "../../providers/global/breakpoint-provider";
const tabs = ["all", "tools", "3rd-party-tools"];
@ -51,7 +51,14 @@ export default function OtherStuffView() {
<SimpleGrid spacing="2" columns={columns}>
{recentApps.slice(0, 6).map((id) => {
const app = allApps.find((a) => a.id === id);
return app ? <AppCard key={app.id} app={app} onClick={() => useApp(app.id)} /> : null;
return app ? (
<AppCard
key={app.id}
app={app}
onClick={() => useApp(app.id)}
canFavorite={!externalTools.includes(app)}
/>
) : null;
})}
</SimpleGrid>
</>
@ -72,7 +79,12 @@ export default function OtherStuffView() {
<TabPanels>
<TabPanel as={SimpleGrid} spacing="2" columns={columns} px="0" py="4">
{allApps.sort(sortByName).map((app) => (
<AppCard key={app.id} app={app} onClick={() => useApp(app.id)} />
<AppCard
key={app.id}
app={app}
onClick={() => useApp(app.id)}
canFavorite={!externalTools.includes(app)}
/>
))}
</TabPanel>
<TabPanel as={SimpleGrid} spacing="2" columns={columns} px="0" py="4">
@ -82,7 +94,7 @@ export default function OtherStuffView() {
</TabPanel>
<TabPanel as={SimpleGrid} spacing="2" columns={columns} px="0" py="4">
{externalTools.sort(sortByName).map((app) => (
<AppCard key={app.id} app={app} onClick={() => useApp(app.id)} />
<AppCard key={app.id} app={app} onClick={() => useApp(app.id)} canFavorite={false} />
))}
</TabPanel>
</TabPanels>