Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions components/Filters/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,3 +125,8 @@ export const FILTER_MODULE_TYPE: FilterParamsType[] = [
title: 'Turbo Module',
},
];

export const FILTER_BOOKMARKS: FilterParamsType = {
param: 'bookmarks',
title: 'Bookmarked',
};
Comment thread
riteshshukla04 marked this conversation as resolved.
7 changes: 7 additions & 0 deletions components/Filters/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,13 @@ import { type StyleProp, View, type ViewStyle } from 'react-native';

import FiltersSection from '~/components/Filters/FiltersSection';
import { Tag } from '~/components/Tag';
import { useBookmarks } from '~/context/BookmarksContext';
import { type Query } from '~/types';
import { getPageQuery } from '~/util/search';
import tw from '~/util/tailwind';

import {
FILTER_BOOKMARKS,
FILTER_COMPATIBILITY,
FILTER_MODULE_TYPE,
FILTER_PLATFORMS,
Expand All @@ -25,6 +27,8 @@ type FiltersProps = {
export function Filters({ query, style, basePath = '/packages' }: FiltersProps) {
const pageQuery = getPageQuery(basePath, query);
const isMainSearch = basePath === '/packages';
const { bookmarkedIds } = useBookmarks();
const hasBookmarks = bookmarkedIds.size > 0;

return (
<View style={[tw`flex-1 items-center bg-palette-gray1 py-2 dark:bg-very-dark`, style]}>
Expand All @@ -48,6 +52,9 @@ export function Filters({ query, style, basePath = '/packages' }: FiltersProps)
basePath={basePath}
/>
))}
{hasBookmarks && (
<ToggleLink query={pageQuery} filterParam={FILTER_BOOKMARKS} basePath={basePath} />
)}
Comment thread
riteshshukla04 marked this conversation as resolved.
Outdated
</FiltersSection>
</View>
<FiltersSection title="Status">
Expand Down
38 changes: 38 additions & 0 deletions components/Icons/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,44 @@ export function Star({ width, height, style }: IconProps) {
);
}

export function StarFilled({ width, height, style }: IconProps) {
return (
<Svg width={width ?? 20} height={height ?? 19} viewBox="0 0 20 19" style={style}>
<Path
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"
fill="currentColor"
/>
</Svg>
);
}
Comment thread
riteshshukla04 marked this conversation as resolved.
Outdated

export function Bookmark({ width, height, style }: IconProps) {
return (
<Svg width={width ?? 16} height={height ?? 16} viewBox="0 0 256 256" style={style}>
<Path
d="M184,32H72A16,16,0,0,0,56,48V224a8,8,0,0,0,12.24,6.78L128,193.43l59.77,37.35A8,8,0,0,0,200,224V48A16,16,0,0,0,184,32Zm0,177.57-51.77-32.35a8,8,0,0,0-8.48,0L72,209.57V48H184Z"
fill="currentColor"
/>
</Svg>
);
}

export function BookmarkFilled({ width, height, style }: IconProps) {
Comment thread
riteshshukla04 marked this conversation as resolved.
return (
<Svg width={width ?? 16} height={height ?? 16} viewBox="0 0 256 256" style={style}>
<Path
d="M184,32H72A16,16,0,0,0,56,48V224a8,8,0,0,0,12.24,6.78L128,193.43l59.77,37.35A8,8,0,0,0,200,224V48A16,16,0,0,0,184,32Z"
fill="currentColor"
opacity="0.85"
/>
<Path
d="M184,32H72A16,16,0,0,0,56,48V224a8,8,0,0,0,12.24,6.78L128,193.43l59.77,37.35A8,8,0,0,0,200,224V48A16,16,0,0,0,184,32Zm0,177.57-51.77-32.35a8,8,0,0,0-8.48,0L72,209.57V48H184Z"
fill="currentColor"
/>
</Svg>
);
}

export function Web({ width, height, style }: IconProps) {
return (
<Svg width={width ?? 18} height={height ?? 19} viewBox="0 0 18 19" style={style}>
Expand Down
37 changes: 35 additions & 2 deletions components/Library/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import { Platform, View } from 'react-native';

import { A, HoverEffect, useLayout } from '~/common/styleguide';
import CompatibilityTags from '~/components/CompatibilityTags';
import { GitHub } from '~/components/Icons';
import { Bookmark, BookmarkFilled, GitHub } from '~/components/Icons';
import LibraryDescription from '~/components/Library/LibraryDescription';
import UpdatedAtView from '~/components/Library/UpdateAtView';
import Tooltip from '~/components/Tooltip';
import { useBookmarks } from '~/context/BookmarksContext';
import { type LibraryType } from '~/types';
import tw from '~/util/tailwind';

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

const libName = library.npmPkg ?? github.name;
const bookmarkId = library.npmPkg ?? library.github.fullName;
const isBookmarked = checkIsBookmarked(bookmarkId);

function handleToggleBookmark() {
toggleBookmarkGlobal(bookmarkId);
}

const hasSecondaryMetadata =
github.license ||
github.urls.homepage ||
Expand All @@ -43,13 +52,37 @@ export default function Library({
return (
<View
style={[
tw`mb-4 flex-row overflow-hidden rounded-md border border-palette-gray2 dark:border-default`,
tw`relative mb-4 flex-row overflow-hidden rounded-md border border-palette-gray2 dark:border-default`,
isSmallScreen && tw`flex-col`,
skipMetadata && tw`mx-[0.75%] min-h-[206px] w-[48.5%]`,
skipMetadata && (isSmallScreen || isBelowMaxWidth) && tw`w-[98.5%] max-w-[98.5%]`,
skipSecondaryMetadata && tw`min-h-0`,
library.unmaintained && tw`opacity-85`,
]}>
<Tooltip
sideOffset={8}
trigger={
<HoverEffect
onPress={handleToggleBookmark}
style={tw`dark:bg-palette-gray8 absolute right-2 top-2 z-10 rounded-md border border-palette-gray2 bg-white p-1.5 dark:border-palette-gray6`}
Comment thread
Simek marked this conversation as resolved.
Outdated
aria-label={isBookmarked ? 'Remove bookmark' : 'Bookmark library'}>
Comment thread
Simek marked this conversation as resolved.
Outdated
{isBookmarked ? (
<BookmarkFilled
width={16}
height={16}
style={tw`text-primary-dark dark:text-primary`}
Comment thread
Simek marked this conversation as resolved.
Outdated
/>
) : (
<Bookmark
width={16}
height={16}
style={tw`text-palette-gray4 dark:text-palette-gray5`}
/>
)}
</HoverEffect>
}>
{isBookmarked ? 'Remove from bookmarks' : 'Add to bookmarks'}
</Tooltip>
Comment thread
Simek marked this conversation as resolved.
Outdated
<View style={[tw`flex-1 p-4 pb-3.5 pl-5`, isSmallScreen && tw`px-3.5 pb-3 pt-2.5`]}>
{library.unmaintained && (
<View
Expand Down
34 changes: 33 additions & 1 deletion components/Package/PackageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import { View } from 'react-native';

import { A, HoverEffect, P, useLayout } from '~/common/styleguide';
import CompatibilityTags from '~/components/CompatibilityTags';
import { GitHub } from '~/components/Icons';
import { Bookmark, BookmarkFilled, GitHub } from '~/components/Icons';
import LibraryDescription from '~/components/Library/LibraryDescription';
import UnmaintainedLabel from '~/components/Library/UnmaintainedLabel';
import TrustedBadge from '~/components/Package/TrustedBadge';
import UserAvatar from '~/components/Package/UserAvatar';
import Tooltip from '~/components/Tooltip';
import { useBookmarks } from '~/context/BookmarksContext';
import { type LibraryType, type NpmRegistryVersionData } from '~/types';
import tw from '~/util/tailwind';

Expand All @@ -20,8 +21,15 @@ type Props = {

export default function PackageHeader({ library, registryData, rightSlot }: Props) {
const { isSmallScreen } = useLayout();
const { isBookmarked: checkIsBookmarked, toggleBookmark: toggleBookmarkGlobal } = useBookmarks();

const ghUsername = library.github.fullName.split('/')[0];
const bookmarkId = library.npmPkg ?? library.github.fullName;
const isBookmarked = checkIsBookmarked(bookmarkId);

function handleToggleBookmark() {
toggleBookmarkGlobal(bookmarkId);
}

return (
<>
Expand Down Expand Up @@ -60,6 +68,30 @@ export default function PackageHeader({ library, registryData, rightSlot }: Prop
/>
</A>
</HoverEffect>
<Tooltip
sideOffset={8}
trigger={
<HoverEffect
onPress={handleToggleBookmark}
style={tw`size-5`}
aria-label={isBookmarked ? 'Remove bookmark' : 'Bookmark library'}>
{isBookmarked ? (
<BookmarkFilled
width={20}
height={20}
style={tw`text-primary-dark dark:text-primary`}
Comment thread
Simek marked this conversation as resolved.
Outdated
/>
) : (
<Bookmark
width={20}
height={20}
style={tw`text-palette-gray5 dark:text-palette-gray4`}
/>
Comment thread
riteshshukla04 marked this conversation as resolved.
Outdated
)}
</HoverEffect>
}>
{isBookmarked ? 'Remove from bookmarks' : 'Add to bookmarks'}
</Tooltip>
</View>
{rightSlot}
</View>
Expand Down
93 changes: 93 additions & 0 deletions context/BookmarksContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { createContext, type PropsWithChildren, useContext, useEffect, useState } from 'react';

const BOOKMARK_COOKIE_NAME = 'rnd_bookmarks';
const COOKIE_MAX_AGE = 365 * 24 * 60 * 60; // 1 year in seconds
Comment thread
riteshshukla04 marked this conversation as resolved.
Outdated

type BookmarksContextType = {
bookmarkedIds: Set<string>;
isBookmarked: (id: string) => boolean;
toggleBookmark: (id: string) => void;
isLoading: boolean;
};

const BookmarksContext = createContext<BookmarksContextType | null>(null);

function getCookie(name: string): string | null {
if (typeof document === 'undefined') {
return null;
}
const match = document.cookie.match(new RegExp(`(^| )${name}=([^;]+)`));
return match ? decodeURIComponent(match[2]) : null;
}

function setCookie(name: string, value: string, maxAge: number) {
if (typeof document === 'undefined') {
return;
}
document.cookie = `${name}=${encodeURIComponent(value)}; path=/; max-age=${maxAge}; SameSite=Lax`;
}

export function getBookmarksFromCookie(cookieString?: string): string[] {
if (typeof cookieString === 'string') {
const match = cookieString.match(new RegExp(`(^| )${BOOKMARK_COOKIE_NAME}=([^;]+)`));
if (match) {
try {
return JSON.parse(decodeURIComponent(match[2]));
} catch {
return [];
}
}
return [];
}

const value = getCookie(BOOKMARK_COOKIE_NAME);
if (!value) {
return [];
}
try {
return JSON.parse(value);
} catch {
return [];
}
}

export function BookmarksProvider({ children }: PropsWithChildren) {
const [bookmarkedIds, setBookmarkedIds] = useState<Set<string>>(new Set());
const [isLoading, setIsLoading] = useState(true);

useEffect(() => {
const bookmarks = getBookmarksFromCookie();
setBookmarkedIds(new Set(bookmarks));
setIsLoading(false);
}, []);

function isBookmarked(id: string) {
return bookmarkedIds.has(id);
}

function toggleBookmark(id: string) {
const newSet = new Set(bookmarkedIds);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}

setCookie(BOOKMARK_COOKIE_NAME, JSON.stringify([...newSet]), COOKIE_MAX_AGE);
setBookmarkedIds(newSet);
}

return (
<BookmarksContext.Provider value={{ bookmarkedIds, isBookmarked, toggleBookmark, isLoading }}>
{children}
</BookmarksContext.Provider>
);
}

export function useBookmarks() {
const context = useContext(BookmarksContext);
if (!context) {
throw new Error('useBookmarks must be used within a BookmarksProvider');
}
return context;
}
27 changes: 15 additions & 12 deletions pages/_app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import Head from 'next/head';
import { SafeAreaProvider } from 'react-native-safe-area-context';

import Footer from '~/components/Footer';
import { BookmarksProvider } from '~/context/BookmarksContext';
import CustomAppearanceProvider from '~/context/CustomAppearanceProvider';
import tw from '~/util/tailwind';

Expand All @@ -23,18 +24,20 @@ Sentry.init({
function App({ pageProps, Component }: AppProps) {
return (
<CustomAppearanceProvider>
<SafeAreaProvider>
<Head>
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=2,viewport-fit=cover"
/>
</Head>
<main style={tw`flex flex-1 flex-col`}>
<Component {...pageProps} />
</main>
<Footer />
</SafeAreaProvider>
<BookmarksProvider>
<SafeAreaProvider>
<Head>
<meta
name="viewport"
content="width=device-width,initial-scale=1,minimum-scale=1,maximum-scale=1,viewport-fit=cover"
/>
</Head>
<main style={tw`flex flex-1 flex-col`}>
<Component {...pageProps} />
</main>
<Footer />
</SafeAreaProvider>
</BookmarksProvider>
</CustomAppearanceProvider>
);
}
Expand Down
15 changes: 14 additions & 1 deletion pages/api/libraries/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { drop, take } from 'es-toolkit';
import { type NextApiRequest, type NextApiResponse } from 'next';

import data from '~/assets/data.json';
import { getBookmarksFromCookie } from '~/context/BookmarksContext';
import { type DataAssetType, type QueryOrder, type SortedDataType } from '~/types';
import { NUM_PER_PAGE } from '~/util/Constants';
import { parseQueryParams } from '~/util/parseQueryParams';
Expand Down Expand Up @@ -60,6 +61,11 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
const sortDirection = parsedQuery.direction ?? 'descending';
const libraries = sortDirection === 'ascending' ? ReversedSortedData[sortBy] : SortedData[sortBy];

// Get bookmarks from cookie if bookmarks filter is enabled
const bookmarkedIds = parsedQuery.bookmarks
? new Set(getBookmarksFromCookie(req.headers.cookie))
: null;

const filteredLibraries = handleFilterLibraries({
libraries,
sortBy,
Expand Down Expand Up @@ -98,6 +104,8 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
turboModule: parsedQuery.turboModule,
nightlyProgram: parsedQuery.nightlyProgram,
owner: parsedQuery.owner,
bookmarks: parsedQuery.bookmarks,
bookmarkedIds,
});

const offset = parsedQuery.offset ? Number.parseInt(parsedQuery.offset.toString(), 10) : 0;
Expand All @@ -113,7 +121,12 @@ export default function handler(req: NextApiRequest, res: NextApiResponse) {
: filteredLibraries;
const filteredAndPaginatedLibraries = take(drop(relevanceSortedLibraries, offset), limit);

res.setHeader('Cache-Control', 'public, s-maxage=600, stale-while-revalidate=300');
// Don't cache responses with bookmarks filter since it depends on user-specific cookies
if (parsedQuery.bookmarks) {
res.setHeader('Cache-Control', 'private, no-cache, no-store, must-revalidate');
} else {
res.setHeader('Cache-Control', 'public, s-maxage=600, stale-while-revalidate=300');
}

return res.json({
libraries: filteredAndPaginatedLibraries,
Expand Down
Loading