Skip to content

Commit ad732d6

Browse files
feat:- bookmarks
1 parent 77ea66f commit ad732d6

4 files changed

Lines changed: 121 additions & 14 deletions

File tree

components/Icons/index.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ export function Star({ width, height, style }: IconProps) {
3535
);
3636
}
3737

38+
export function StarFilled({ width, height, style }: IconProps) {
39+
return (
40+
<Svg width={width ?? 20} height={height ?? 19} viewBox="0 0 20 19" style={style}>
41+
<Path
42+
d="M9.764.081l2.723 6.323 6.856.636-5.173 4.544 1.514 6.716-5.92-3.515-5.92 3.515 1.514-6.716L.186 7.04l6.855-.636L9.764.081z"
43+
fill="currentColor"
44+
/>
45+
</Svg>
46+
);
47+
}
48+
3849
export function Web({ width, height, style }: IconProps) {
3950
return (
4051
<Svg width={width ?? 18} height={height ?? 19} viewBox="0 0 18 19" style={style}>

components/Library/index.tsx

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { Platform, View } from 'react-native';
22

33
import { A, HoverEffect, useLayout } from '~/common/styleguide';
44
import CompatibilityTags from '~/components/CompatibilityTags';
5-
import { GitHub } from '~/components/Icons';
5+
import { GitHub, Star, StarFilled } from '~/components/Icons';
66
import LibraryDescription from '~/components/Library/LibraryDescription';
77
import UpdatedAtView from '~/components/Library/UpdateAtView';
88
import Tooltip from '~/components/Tooltip';
9+
import { useBookmarks } from '~/context/BookmarksContext';
910
import { type LibraryType } from '~/types';
1011
import tw from '~/util/tailwind';
1112

@@ -31,8 +32,16 @@ export default function Library({
3132
}: Props) {
3233
const { github } = library;
3334
const { isSmallScreen, isBelowMaxWidth } = useLayout();
35+
const { isBookmarked: checkIsBookmarked, toggleBookmark: toggleBookmarkGlobal } = useBookmarks();
3436

3537
const libName = library.npmPkg ?? github.name;
38+
const bookmarkId = library.npmPkg ?? library.github.fullName;
39+
const isBookmarked = checkIsBookmarked(bookmarkId);
40+
41+
const handleToggleBookmark = () => {
42+
void toggleBookmarkGlobal(bookmarkId);
43+
};
44+
3645
const hasSecondaryMetadata =
3746
github.license ||
3847
github.urls.homepage ||
@@ -43,13 +52,29 @@ export default function Library({
4352
return (
4453
<View
4554
style={[
46-
tw`mb-4 border rounded-md flex-row overflow-hidden border-palette-gray2 dark:border-default`,
55+
tw`mb-4 border rounded-md flex-row overflow-hidden border-palette-gray2 dark:border-default relative`,
4756
isSmallScreen && tw`flex-col`,
4857
skipMetadata && tw`w-[48.5%] mx-[0.75%] min-h-[206px]`,
4958
skipMetadata && (isSmallScreen || isBelowMaxWidth) && tw`w-[98.5%] max-w-[98.5%]`,
5059
skipSecondaryMetadata && tw`min-h-0`,
5160
library.unmaintained && tw`opacity-85`,
5261
]}>
62+
<Tooltip
63+
sideOffset={8}
64+
trigger={
65+
<HoverEffect
66+
onPress={handleToggleBookmark}
67+
style={tw`absolute top-2 right-2 z-10 p-1.5 rounded-md bg-white dark:bg-palette-gray8 border border-palette-gray2 dark:border-palette-gray6`}
68+
aria-label={isBookmarked ? 'Remove bookmark' : 'Bookmark library'}>
69+
{isBookmarked ? (
70+
<StarFilled width={16} height={16} style={tw`text-yellow-500`} />
71+
) : (
72+
<Star width={16} height={16} style={tw`text-palette-gray4 dark:text-palette-gray5`} />
73+
)}
74+
</HoverEffect>
75+
}>
76+
{isBookmarked ? 'Remove from bookmarks' : 'Add to bookmarks'}
77+
</Tooltip>
5378
<View style={[tw`pb-3.5 flex-1 p-4 pl-5`, isSmallScreen && tw`pt-2.5 pb-3 px-3.5`]}>
5479
{library.unmaintained && (
5580
<View

context/BookmarksContext.tsx

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import AsyncStorage from '@react-native-async-storage/async-storage';
2+
import { createContext, type PropsWithChildren, useContext, useEffect, useState } from 'react';
3+
4+
const BOOKMARK_KEY = '@ReactNativeDirectory:BookmarkedLibraries';
5+
6+
type BookmarksContextType = {
7+
bookmarkedIds: Set<string>;
8+
isBookmarked: (id: string) => boolean;
9+
toggleBookmark: (id: string) => Promise<void>;
10+
isLoading: boolean;
11+
};
12+
13+
const BookmarksContext = createContext<BookmarksContextType | null>(null);
14+
15+
export function BookmarksProvider({ children }: PropsWithChildren) {
16+
const [bookmarkedIds, setBookmarkedIds] = useState<Set<string>>(new Set());
17+
const [isLoading, setIsLoading] = useState(true);
18+
19+
useEffect(() => {
20+
async function loadBookmarks() {
21+
try {
22+
const stored = await AsyncStorage.getItem(BOOKMARK_KEY);
23+
if (stored) {
24+
const parsed: string[] = JSON.parse(stored);
25+
setBookmarkedIds(new Set(parsed));
26+
}
27+
} catch (error) {
28+
console.error('Failed to load bookmarks:', error);
29+
} finally {
30+
setIsLoading(false);
31+
}
32+
}
33+
34+
loadBookmarks();
35+
}, []);
36+
37+
const isBookmarked = (id: string) => bookmarkedIds.has(id);
38+
39+
const toggleBookmark = async (id: string) => {
40+
try {
41+
const newSet = new Set(bookmarkedIds);
42+
if (newSet.has(id)) {
43+
newSet.delete(id);
44+
} else {
45+
newSet.add(id);
46+
}
47+
48+
await AsyncStorage.setItem(BOOKMARK_KEY, JSON.stringify([...newSet]));
49+
setBookmarkedIds(newSet);
50+
} catch (error) {
51+
console.error('Failed to toggle bookmark:', error);
52+
}
53+
};
54+
55+
return (
56+
<BookmarksContext.Provider value={{ bookmarkedIds, isBookmarked, toggleBookmark, isLoading }}>
57+
{children}
58+
</BookmarksContext.Provider>
59+
);
60+
}
61+
62+
export function useBookmarks() {
63+
const context = useContext(BookmarksContext);
64+
if (!context) {
65+
throw new Error('useBookmarks must be used within a BookmarksProvider');
66+
}
67+
return context;
68+
}

pages/_app.tsx

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import Head from 'next/head';
66
import { SafeAreaProvider } from 'react-native-safe-area-context';
77

88
import Footer from '~/components/Footer';
9+
import { BookmarksProvider } from '~/context/BookmarksContext';
910
import CustomAppearanceProvider from '~/context/CustomAppearanceProvider';
1011
import tw from '~/util/tailwind';
1112

@@ -23,18 +24,20 @@ Sentry.init({
2324
function App({ pageProps, Component }: AppProps) {
2425
return (
2526
<CustomAppearanceProvider>
26-
<SafeAreaProvider>
27-
<Head>
28-
<meta
29-
name="viewport"
30-
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=2,viewport-fit=cover"
31-
/>
32-
</Head>
33-
<main style={tw`flex flex-col flex-1`}>
34-
<Component {...pageProps} />
35-
</main>
36-
<Footer />
37-
</SafeAreaProvider>
27+
<BookmarksProvider>
28+
<SafeAreaProvider>
29+
<Head>
30+
<meta
31+
name="viewport"
32+
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,viewport-fit=cover"
33+
/>
34+
</Head>
35+
<main style={tw`flex flex-col flex-1`}>
36+
<Component {...pageProps} />
37+
</main>
38+
<Footer />
39+
</SafeAreaProvider>
40+
</BookmarksProvider>
3841
</CustomAppearanceProvider>
3942
);
4043
}

0 commit comments

Comments
 (0)