✨(frontend) implement document favorites feature
- Added functionality to mark documents as favorites, including new hooks `useMakeFavoriteDoc` and `useRemoveFavoriteDoc` for managing favorite status. - Enhanced the document management API to support favorite filtering with the `is_favorite` parameter. - Created a new e2e test for the favorite workflow to ensure proper functionality. - Updated the UI components to reflect favorite status, including changes in `DocsGridActions`, `DocsGridItem`, and the new `LeftPanelFavorites` component for displaying pinned documents. - Adjusted SVG assets for better visual representation of pinned documents.
This commit is contained in:
committed by
Anthony LC
parent
4f4c8905ff
commit
63885117e1
@@ -16,6 +16,7 @@ and this project adheres to
|
|||||||
- 💄(frontend) Add left panel #420
|
- 💄(frontend) Add left panel #420
|
||||||
- 💄(frontend) add filtering to left panel #475
|
- 💄(frontend) add filtering to left panel #475
|
||||||
- ✨(frontend) new share modal ui #489
|
- ✨(frontend) new share modal ui #489
|
||||||
|
- ✨(frontend) add favorite feature #515
|
||||||
|
|
||||||
## Changed
|
## Changed
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
import { createDoc, verifyDocName } from './common';
|
||||||
|
|
||||||
|
type SmallDoc = {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
test.describe('Document favorite', () => {
|
||||||
|
test('it check the favorite workflow', async ({ page, browserName }) => {
|
||||||
|
const id = Math.random().toString(7);
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Create document
|
||||||
|
const createdDoc = await createDoc(page, `Doc ${id}`, browserName, 1);
|
||||||
|
await verifyDocName(page, createdDoc[0]);
|
||||||
|
|
||||||
|
// Reload page
|
||||||
|
await page.reload();
|
||||||
|
await page.goto('/');
|
||||||
|
|
||||||
|
// Get all documents
|
||||||
|
let docs: SmallDoc[] = [];
|
||||||
|
const response = await page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().endsWith('documents/?page=1') &&
|
||||||
|
response.status() === 200,
|
||||||
|
);
|
||||||
|
const result = await response.json();
|
||||||
|
docs = result.results as SmallDoc[];
|
||||||
|
await page.getByRole('heading', { name: 'All docs' }).click();
|
||||||
|
await expect(page.getByText(`Doc ${id}`)).toBeVisible();
|
||||||
|
const doc = docs.find((doc) => doc.title === createdDoc[0]) as SmallDoc;
|
||||||
|
|
||||||
|
// Check document
|
||||||
|
expect(doc).not.toBeUndefined();
|
||||||
|
expect(doc?.title).toBe(createdDoc[0]);
|
||||||
|
|
||||||
|
// Open document actions
|
||||||
|
const button = page.getByTestId(`docs-grid-actions-button-${doc.id}`);
|
||||||
|
await expect(button).toBeVisible();
|
||||||
|
await button.click();
|
||||||
|
|
||||||
|
// Pin document
|
||||||
|
const pinButton = page.getByTestId(`docs-grid-actions-pin-${docs[0].id}`);
|
||||||
|
await expect(pinButton).toBeVisible();
|
||||||
|
await pinButton.click();
|
||||||
|
|
||||||
|
// Check response
|
||||||
|
const responsePin = await page.waitForResponse(
|
||||||
|
(response) =>
|
||||||
|
response.url().includes(`documents/${doc.id}/favorite/`) &&
|
||||||
|
response.status() === 201,
|
||||||
|
);
|
||||||
|
expect(responsePin.ok()).toBeTruthy();
|
||||||
|
|
||||||
|
// Check left panel favorites
|
||||||
|
const leftPanelFavorites = page.getByTestId('left-panel-favorites');
|
||||||
|
await expect(leftPanelFavorites).toBeVisible();
|
||||||
|
await expect(leftPanelFavorites.getByText(`Doc ${id}`)).toBeVisible();
|
||||||
|
|
||||||
|
//
|
||||||
|
await button.click();
|
||||||
|
const unpinButton = page.getByTestId(
|
||||||
|
`docs-grid-actions-unpin-${docs[0].id}`,
|
||||||
|
);
|
||||||
|
await expect(unpinButton).toBeVisible();
|
||||||
|
await unpinButton.click();
|
||||||
|
|
||||||
|
// Check left panel favorites
|
||||||
|
await expect(leftPanelFavorites.getByText(`Doc ${id}`)).toBeHidden();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -96,7 +96,7 @@ test.describe('Document grid item options', () => {
|
|||||||
let docs: SmallDoc[] = [];
|
let docs: SmallDoc[] = [];
|
||||||
const response = await page.waitForResponse(
|
const response = await page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('documents/?page=1') &&
|
response.url().endsWith('documents/?page=1') &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -124,7 +124,7 @@ test.describe('Document grid item options', () => {
|
|||||||
|
|
||||||
const refetchResponse = await page.waitForResponse(
|
const refetchResponse = await page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('documents/?page=1') &&
|
response.url().endsWith('documents/?page=1') &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -189,7 +189,7 @@ test.describe('Documents filters', () => {
|
|||||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||||
const response = await page.waitForResponse(
|
const response = await page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('documents/?page=1') &&
|
response.url().endsWith('documents/?page=1') &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -230,7 +230,7 @@ test.describe('Documents filters', () => {
|
|||||||
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
await expect(page.getByTestId('docs-grid-loader')).toBeVisible();
|
||||||
const responseMyDocs = await page.waitForResponse(
|
const responseMyDocs = await page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('documents/?page=1&is_creator_me=true') &&
|
response.url().endsWith('documents/?page=1&is_creator_me=true') &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
const resultMyDocs = await responseMyDocs.json();
|
const resultMyDocs = await responseMyDocs.json();
|
||||||
@@ -268,7 +268,7 @@ test.describe('Documents Grid', () => {
|
|||||||
|
|
||||||
const response = await page.waitForResponse(
|
const response = await page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes('documents/?page=1') &&
|
response.url().endsWith('documents/?page=1') &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
@@ -294,13 +294,13 @@ test.describe('Documents Grid', () => {
|
|||||||
let docs: SmallDoc[] = [];
|
let docs: SmallDoc[] = [];
|
||||||
const responsePromisePage1 = page.waitForResponse(
|
const responsePromisePage1 = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes(`/documents/?page=1`) &&
|
response.url().endsWith(`/documents/?page=1`) &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
const responsePromisePage2 = page.waitForResponse(
|
const responsePromisePage2 = page.waitForResponse(
|
||||||
(response) =>
|
(response) =>
|
||||||
response.url().includes(`/documents/?page=2`) &&
|
response.url().endsWith(`/documents/?page=2`) &&
|
||||||
response.status() === 200,
|
response.status() === 200,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -2,5 +2,6 @@ export * from './useCreateDoc';
|
|||||||
export * from './useDoc';
|
export * from './useDoc';
|
||||||
export * from './useDocOptions';
|
export * from './useDocOptions';
|
||||||
export * from './useDocs';
|
export * from './useDocs';
|
||||||
|
export * from './useCreateFavoriteDoc';
|
||||||
export * from './useUpdateDoc';
|
export * from './useUpdateDoc';
|
||||||
export * from './useUpdateDocLink';
|
export * from './useUpdateDocLink';
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
import { Doc } from '@/features/docs';
|
||||||
|
|
||||||
|
export type CreateFavoriteDocParams = Pick<Doc, 'id'>;
|
||||||
|
|
||||||
|
export const createFavoriteDoc = async ({ id }: CreateFavoriteDocParams) => {
|
||||||
|
const response = await fetchAPI(`documents/${id}/favorite/`, {
|
||||||
|
method: 'POST',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to make the doc as favorite',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface CreateFavoriteDocProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
listInvalideQueries?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateFavoriteDoc({
|
||||||
|
onSuccess,
|
||||||
|
listInvalideQueries,
|
||||||
|
}: CreateFavoriteDocProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, APIError, CreateFavoriteDocParams>({
|
||||||
|
mutationFn: createFavoriteDoc,
|
||||||
|
onSuccess: () => {
|
||||||
|
listInvalideQueries?.forEach((queryKey) => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [queryKey],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
|
import { Doc } from '@/features/docs';
|
||||||
|
|
||||||
|
export type DeleteFavoriteDocParams = Pick<Doc, 'id'>;
|
||||||
|
|
||||||
|
export const deleteFavoriteDoc = async ({ id }: DeleteFavoriteDocParams) => {
|
||||||
|
const response = await fetchAPI(`documents/${id}/favorite/`, {
|
||||||
|
method: 'DELETE',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new APIError(
|
||||||
|
'Failed to remove the doc as favorite',
|
||||||
|
await errorCauses(response),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
interface DeleteFavoriteDocProps {
|
||||||
|
onSuccess?: () => void;
|
||||||
|
listInvalideQueries?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteFavoriteDoc({
|
||||||
|
onSuccess,
|
||||||
|
listInvalideQueries,
|
||||||
|
}: DeleteFavoriteDocProps) {
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
return useMutation<void, APIError, DeleteFavoriteDocParams>({
|
||||||
|
mutationFn: deleteFavoriteDoc,
|
||||||
|
onSuccess: () => {
|
||||||
|
listInvalideQueries?.forEach((queryKey) => {
|
||||||
|
void queryClient.invalidateQueries({
|
||||||
|
queryKey: [queryKey],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
onSuccess?.();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ export type DocsParams = {
|
|||||||
ordering?: DocsOrdering;
|
ordering?: DocsOrdering;
|
||||||
is_creator_me?: boolean;
|
is_creator_me?: boolean;
|
||||||
title?: string;
|
title?: string;
|
||||||
|
is_favorite?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DocsResponse = APIList<Doc>;
|
export type DocsResponse = APIList<Doc>;
|
||||||
@@ -49,6 +50,9 @@ export const getDocs = async (params: DocsParams): Promise<DocsResponse> => {
|
|||||||
if (params.title && params.title.length > 0) {
|
if (params.title && params.title.length > 0) {
|
||||||
searchParams.set('title', params.title);
|
searchParams.set('title', params.title);
|
||||||
}
|
}
|
||||||
|
if (params.is_favorite !== undefined) {
|
||||||
|
searchParams.set('is_favorite', params.is_favorite.toString());
|
||||||
|
}
|
||||||
|
|
||||||
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
|
const response = await fetchAPI(`documents/?${searchParams.toString()}`);
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<svg width="32" height="36" viewBox="0 0 32 36" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="28" height="34" viewBox="0 0 28 34" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
|
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" fill="white"/>
|
||||||
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" stroke="#DCDCFC" stroke-width="0.472222"/>
|
<rect x="2.01394" y="1.23611" width="25.9722" height="33.5278" rx="3.54167" stroke="#DCDCFC" stroke-width="0.472222"/>
|
||||||
<path d="M6.5 8.55556H15" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
|
<path d="M6.5 8.55556H15" stroke="#6A6AF4" stroke-width="1.88889" stroke-linecap="round"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 853 B After Width: | Height: | Size: 853 B |
@@ -2,7 +2,14 @@ import { useModal } from '@openfun/cunningham-react';
|
|||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
import { DropdownMenu, DropdownMenuOption, Icon } from '@/components';
|
||||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
import {
|
||||||
|
Doc,
|
||||||
|
KEY_LIST_DOC,
|
||||||
|
ModalRemoveDoc,
|
||||||
|
useCreateFavoriteDoc,
|
||||||
|
} from '@/features/docs/doc-management';
|
||||||
|
|
||||||
|
import { useDeleteFavoriteDoc } from '../../doc-management/api/useDeleteFavoriteDoc';
|
||||||
|
|
||||||
interface DocsGridActionsProps {
|
interface DocsGridActionsProps {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
@@ -11,8 +18,26 @@ interface DocsGridActionsProps {
|
|||||||
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
export const DocsGridActions = ({ doc }: DocsGridActionsProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const deleteModal = useModal();
|
const deleteModal = useModal();
|
||||||
|
const removeFavoriteDoc = useDeleteFavoriteDoc({
|
||||||
|
listInvalideQueries: [KEY_LIST_DOC],
|
||||||
|
});
|
||||||
|
const makeFavoriteDoc = useCreateFavoriteDoc({
|
||||||
|
listInvalideQueries: [KEY_LIST_DOC],
|
||||||
|
});
|
||||||
|
|
||||||
const options: DropdownMenuOption[] = [
|
const options: DropdownMenuOption[] = [
|
||||||
|
{
|
||||||
|
label: doc.is_favorite ? t('Unpin') : t('Pin'),
|
||||||
|
icon: 'push_pin',
|
||||||
|
callback: () => {
|
||||||
|
if (doc.is_favorite) {
|
||||||
|
removeFavoriteDoc.mutate({ id: doc.id });
|
||||||
|
} else {
|
||||||
|
makeFavoriteDoc.mutate({ id: doc.id });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
label: t('Remove'),
|
label: t('Remove'),
|
||||||
icon: 'delete',
|
icon: 'delete',
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
|
|||||||
$flex={6}
|
$flex={6}
|
||||||
$padding={{ right: 'base' }}
|
$padding={{ right: 'base' }}
|
||||||
>
|
>
|
||||||
<SimpleDocItem doc={doc} />
|
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
|
||||||
</Box>
|
</Box>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<Box $flex={1.3}>
|
<Box $flex={1.3}>
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ export const Header = () => {
|
|||||||
{!isDesktop && (
|
{!isDesktop && (
|
||||||
<Button
|
<Button
|
||||||
size="medium"
|
size="medium"
|
||||||
onClick={togglePanel}
|
onClick={() => togglePanel()}
|
||||||
aria-label={t('Open the header menu')}
|
aria-label={t('Open the header menu')}
|
||||||
color="primary-text"
|
color="primary-text"
|
||||||
icon={<Icon iconName={isPanelOpen ? 'close' : 'menu'} />}
|
icon={<Icon iconName={isPanelOpen ? 'close' : 'menu'} />}
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import { usePathname } from 'next/navigation';
|
||||||
|
import { useCallback, useEffect } from 'react';
|
||||||
import { createGlobalStyle, css } from 'styled-components';
|
import { createGlobalStyle, css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, SeparatedSection } from '@/components';
|
import { Box, SeparatedSection } from '@/components';
|
||||||
@@ -20,11 +22,22 @@ const MobileLeftPanelStyle = createGlobalStyle`
|
|||||||
|
|
||||||
export const LeftPanel = () => {
|
export const LeftPanel = () => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
const { isPanelOpen } = useLeftPanelStore();
|
|
||||||
const theme = useCunninghamTheme();
|
const theme = useCunninghamTheme();
|
||||||
|
const { togglePanel, isPanelOpen } = useLeftPanelStore();
|
||||||
|
|
||||||
|
const pathname = usePathname();
|
||||||
const colors = theme.colorsTokens();
|
const colors = theme.colorsTokens();
|
||||||
const spacings = theme.spacingsTokens();
|
const spacings = theme.spacingsTokens();
|
||||||
|
|
||||||
|
const toggle = useCallback(() => {
|
||||||
|
togglePanel(false);
|
||||||
|
}, [togglePanel]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
toggle();
|
||||||
|
}, [pathname, toggle]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
@@ -34,10 +47,17 @@ export const LeftPanel = () => {
|
|||||||
height: calc(100vh - ${HEADER_HEIGHT}px);
|
height: calc(100vh - ${HEADER_HEIGHT}px);
|
||||||
width: 300px;
|
width: 300px;
|
||||||
min-width: 300px;
|
min-width: 300px;
|
||||||
|
overflow: hidden;
|
||||||
border-right: 1px solid ${colors['greyscale-200']};
|
border-right: 1px solid ${colors['greyscale-200']};
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
<LeftPanelHeader />
|
<Box
|
||||||
|
$css={css`
|
||||||
|
flex: 0 0 auto;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<LeftPanelHeader />
|
||||||
|
</Box>
|
||||||
<LeftPanelContent />
|
<LeftPanelContent />
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,20 +1,36 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, SeparatedSection } from '@/components';
|
import { Box, SeparatedSection } from '@/components';
|
||||||
|
|
||||||
import { LeftPanelTargetFilters } from './LefPanelTargetFilters';
|
import { LeftPanelTargetFilters } from './LefPanelTargetFilters';
|
||||||
|
import { LeftPanelFavorites } from './LeftPanelFavorites';
|
||||||
|
|
||||||
export const LeftPanelContent = () => {
|
export const LeftPanelContent = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isHome = router.pathname === '/';
|
const isHome = router.pathname === '/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box $width="100%">
|
<>
|
||||||
{isHome && (
|
{isHome && (
|
||||||
<SeparatedSection>
|
<>
|
||||||
<LeftPanelTargetFilters />
|
<Box
|
||||||
</SeparatedSection>
|
$width="100%"
|
||||||
|
$css={css`
|
||||||
|
flex: 0 0 auto;
|
||||||
|
`}
|
||||||
|
>
|
||||||
|
<SeparatedSection>
|
||||||
|
<LeftPanelTargetFilters />
|
||||||
|
</SeparatedSection>
|
||||||
|
</Box>
|
||||||
|
<Box $flex={1} $css="overflow-y: auto; overflow-x: hidden;">
|
||||||
|
<SeparatedSection showSeparator={false}>
|
||||||
|
<LeftPanelFavorites />
|
||||||
|
</SeparatedSection>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</Box>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
|
import { Box, InfiniteScroll, StyledLink, Text } from '@/components';
|
||||||
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
|
import { useInfiniteDocs } from '@/features/docs';
|
||||||
|
import { SimpleDocItem } from '@/features/docs/docs-grid/components/SimpleDocItem';
|
||||||
|
|
||||||
|
export const LeftPanelFavorites = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
|
const spacing = spacingsTokens();
|
||||||
|
|
||||||
|
const docs = useInfiniteDocs({
|
||||||
|
page: 1,
|
||||||
|
is_favorite: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const invitations = docs.data?.pages.flatMap((page) => page.results) || [];
|
||||||
|
|
||||||
|
if (invitations.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
$justify="center"
|
||||||
|
$padding={{ horizontal: 'xs' }}
|
||||||
|
$gap={spacing['2xs']}
|
||||||
|
$height="100%"
|
||||||
|
data-testid="left-panel-favorites"
|
||||||
|
>
|
||||||
|
<Text $size="sm" $variation="700" $weight="700">
|
||||||
|
{t('Pinned documents')}
|
||||||
|
</Text>
|
||||||
|
<InfiniteScroll
|
||||||
|
hasMore={docs.hasNextPage}
|
||||||
|
isLoading={docs.isFetchingNextPage}
|
||||||
|
next={() => void docs.fetchNextPage()}
|
||||||
|
>
|
||||||
|
{invitations.map((doc) => (
|
||||||
|
<Box
|
||||||
|
$css={css`
|
||||||
|
padding: ${spacing['2xs']};
|
||||||
|
border-radius: 4px;
|
||||||
|
&:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
background-color: var(--c--theme--colors--greyscale-100);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
key={doc.id}
|
||||||
|
>
|
||||||
|
<StyledLink href={`/docs/${doc.id}`}>
|
||||||
|
<SimpleDocItem showAccesses doc={doc} />
|
||||||
|
</StyledLink>
|
||||||
|
</Box>
|
||||||
|
))}
|
||||||
|
</InfiniteScroll>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -2,10 +2,17 @@ import { create } from 'zustand';
|
|||||||
|
|
||||||
interface LeftPanelState {
|
interface LeftPanelState {
|
||||||
isPanelOpen: boolean;
|
isPanelOpen: boolean;
|
||||||
togglePanel: () => void;
|
togglePanel: (value?: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useLeftPanelStore = create<LeftPanelState>((set) => ({
|
export const useLeftPanelStore = create<LeftPanelState>((set, get) => ({
|
||||||
isPanelOpen: false,
|
isPanelOpen: false,
|
||||||
togglePanel: () => set((state) => ({ isPanelOpen: !state.isPanelOpen })),
|
togglePanel: (value?: boolean) => {
|
||||||
|
const sanitizedValue =
|
||||||
|
value !== undefined && typeof value === 'boolean'
|
||||||
|
? value
|
||||||
|
: !get().isPanelOpen;
|
||||||
|
|
||||||
|
set({ isPanelOpen: sanitizedValue });
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
Reference in New Issue
Block a user