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

docs(headless SSR): add tabs to ssr samples #4303

Merged
merged 10 commits into from
Sep 19, 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
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ResultList from '@/common/components/react/result-list';
import SearchBox from '@/common/components/react/search-box';
import {SearchPageProvider} from '@/common/components/react/search-page';
import SearchParameterManager from '@/common/components/react/search-parameter-manager';
import TabManager from '@/common/components/react/tab-manager';
import {
fetchStaticState,
setNavigatorContextProvider,
Expand Down Expand Up @@ -62,6 +63,7 @@ export default async function Search(url: {
>
<SearchParameterManager />
<SearchBox />
<TabManager />
<ResultList />
<AuthorFacet />
</SearchPageProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
defineSearchBox,
defineContext,
defineSearchParameterManager,
defineTabManager,
defineTab,
} from '@coveo/headless/ssr';

export const config = {
Expand All @@ -23,7 +25,27 @@ export const config = {
context: defineContext(),
searchBox: defineSearchBox(),
resultList: defineResultList(),
authorFacet: defineFacet({options: {facetId: 'author-1', field: 'author'}}),
tabManager: defineTabManager(),
tabAll: defineTab({
options: {id: 'all', expression: ''},
initialState: {isActive: true},
}),
tabCountries: defineTab({
options: {
id: 'countries',
expression: '@source="Coveo Sample - Atlas"',
},
}),
tabVideos: defineTab({
options: {id: 'videos', expression: '@filetype=YouTubeVideo'},
}),
authorFacet: defineFacet({
options: {
facetId: 'author-1',
field: 'author',
tabs: {included: ['all', 'videos']},
},
}),
searchParameterManager: defineSearchParameterManager(),
},
} satisfies SearchEngineDefinitionOptions<
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {TabManager as TabManagerController} from '@coveo/headless/ssr';

interface TabManagerCommonProps {
controller: Omit<TabManagerController, 'state' | 'subscribe'> | undefined;
value: string;
children: React.ReactNode;
}

export default function TabManagerCommon({
controller,
value,
children,
}: TabManagerCommonProps) {
return <div role="tablist">{children}</div>;
}
32 changes: 32 additions & 0 deletions packages/samples/headless-ssr/common/components/common/tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import {Tab, TabManager, TabState} from '@coveo/headless/ssr';

Check warning on line 1 in packages/samples/headless-ssr/common/components/common/tab.tsx

View workflow job for this annotation

GitHub Actions / Check with linter

'TabManager' is defined but never used

interface TabCommonProps {
state: TabState;
methods: Omit<Tab, 'state' | 'subscribe'> | undefined;
activeTab: string;
tabName: string;
tabLabel: string;
}

export default function TabCommon({
state,
methods,
activeTab,
tabName,
tabLabel,
}: TabCommonProps) {
function handleClickTab() {
if (activeTab !== tabName) methods?.select();
}

return (
<button
role="tab"
aria-selected={state.isActive}
key={tabName}
onClick={() => handleClickTab()}
>
{state.isActive ? <strong>{tabLabel}</strong> : tabLabel}
</button>
);
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import {FacetState, Facet as FacetController} from '@coveo/headless/ssr';
import {
FacetState,
Facet as FacetController,
TabManager,
} from '@coveo/headless/ssr';
import {useEffect, useState, FunctionComponent} from 'react';
import FacetCommon from '../common/facet';

interface FacetProps {
title: string;
staticState: FacetState;
tabManager?: TabManager;
controller?: FacetController;
}

export const Facet: FunctionComponent<FacetProps> = ({
title,
staticState,
tabManager,
controller,
}) => {
const [state, setState] = useState(staticState);
Expand All @@ -20,6 +26,10 @@ export const Facet: FunctionComponent<FacetProps> = ({
[controller]
);

if (!state.enabled) {
return;
}

return (
<FacetCommon
title={title}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {HydrationMetadata} from '../common/hydration-metadata';
import {Facet} from './facet';
import {ResultList} from './result-list';
import {SearchBox} from './search-box';
import {Tab} from './tab';
import {TabManager} from './tabs-manager';

export default function SearchPage({
staticState,
Expand Down Expand Up @@ -59,10 +61,37 @@ export default function SearchPage({
staticState={staticState.controllers.searchBox.state}
controller={hydratedState?.controllers.searchBox}
/>
<TabManager
staticState={staticState.controllers.tabManager.state}
controller={hydratedState?.controllers.tabManager}
>
<Tab
staticState={staticState.controllers.tabAll.state}
controller={hydratedState?.controllers.tabAll}
tabManager={hydratedState?.controllers.tabManager}
tabName={'all'}
tabLabel={'All'}
></Tab>
<Tab
staticState={staticState.controllers.tabCountries.state}
controller={hydratedState?.controllers.tabCountries}
tabManager={hydratedState?.controllers.tabManager}
tabName={'countries'}
tabLabel={'Countries'}
></Tab>
<Tab
staticState={staticState.controllers.tabVideos.state}
controller={hydratedState?.controllers.tabVideos}
tabManager={hydratedState?.controllers.tabManager}
tabName={'videos'}
tabLabel={'Videos'}
></Tab>
</TabManager>
<Facet
title="Author"
staticState={staticState.controllers.authorFacet.state}
controller={hydratedState?.controllers.authorFacet}
tabManager={hydratedState?.controllers.tabManager}
/>
<ResultList
staticState={staticState.controllers.resultList.state}
Expand Down
36 changes: 36 additions & 0 deletions packages/samples/headless-ssr/common/components/generic/tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import {Tab as TabController, TabManager, TabState} from '@coveo/headless/ssr';
import {useEffect, useState, FunctionComponent} from 'react';
import TabCommon from '../common/tab';

interface TabProps {
staticState: TabState;
controller?: TabController;
tabManager?: TabManager;
tabName: string;
tabLabel: string;
}

export const Tab: FunctionComponent<TabProps> = ({
staticState,
controller,
tabManager,
tabName,
tabLabel,
}) => {
const [state, setState] = useState(staticState);

useEffect(
() => controller?.subscribe(() => setState({...controller.state})),
[controller]
);

return (
<TabCommon
state={state}
methods={controller}
activeTab={tabManager?.state.activeTab ?? ''}
tabName={tabName}
tabLabel={tabLabel}
/>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {
TabManagerState,
TabManager as TabManagerController,
} from '@coveo/headless-react/ssr';
import {useEffect, useState, FunctionComponent} from 'react';
import TabManagerCommon from '../common/tab-manager';

interface TabManagerProps {
staticState: TabManagerState;
controller?: TabManagerController;
children: React.ReactNode;
}

export const TabManager: FunctionComponent<TabManagerProps> = ({
staticState,
controller,
children,
}: TabManagerProps) => {
const [state, setState] = useState(staticState);

useEffect(
() => controller?.subscribe?.(() => setState({...controller.state})),
[controller]
);

return (
<TabManagerCommon
controller={controller}
value={state.activeTab}
children={children}
/>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ import FacetCommon from '../common/facet';
export const AuthorFacet = () => {
const {state, methods} = useAuthorFacet();

if (!state.enabled) {
return;
}

return (
<FacetCommon
title="Author"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
'use client';

import {useTabManager} from '../../lib/react/engine';
import TabManagerCommon from '../common/tab-manager';
import Tab from './tab';

export default function TabManager() {
const {state, methods} = useTabManager();

return (
<TabManagerCommon controller={methods} value={state.activeTab}>
<Tab tabName={'all'} tabLabel="All"></Tab>
<Tab tabName={'countries'} tabLabel="Countries"></Tab>
<Tab tabName={'videos'} tabLabel="Videos"></Tab>
</TabManagerCommon>
);
}
44 changes: 44 additions & 0 deletions packages/samples/headless-ssr/common/components/react/tab.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
'use client';

import {TabManager} from '@coveo/headless-react/ssr';

Check warning on line 3 in packages/samples/headless-ssr/common/components/react/tab.tsx

View workflow job for this annotation

GitHub Actions / Check with linter

'TabManager' is defined but never used
import {
useTabAll,
useTabCountries,
useTabManager,
useTabVideos,
} from '../../lib/react/engine';
import TabCommon from '../common/tab';

export default function Tab({
tabName,
tabLabel,
}: {
tabName: string;
tabLabel: string;
}) {
const tabManager = useTabManager();

let controller;

if (tabName === 'all') {
controller = useTabAll;
} else if (tabName === 'countries') {
controller = useTabCountries;
} else if (tabName === 'videos') {
controller = useTabVideos;
} else {
throw new Error(`Unknown tab: ${tabName}`);
}

const {state, methods} = controller();

return (
<TabCommon
state={state}
methods={methods}
activeTab={tabManager.state.activeTab}
tabName={tabName}
tabLabel={tabLabel}
/>
);
}
4 changes: 4 additions & 0 deletions packages/samples/headless-ssr/common/lib/react/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export const {
export const {
useResultList,
useSearchBox,
useTabManager,
useTabAll,
useTabCountries,
useTabVideos,
useAuthorFacet,
useSearchParameterManager,
} = engineDefinition.controllers;
6 changes: 3 additions & 3 deletions packages/samples/headless-ssr/cypress/e2e/smoke.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,15 @@ const msgSelector = '#hydrated-msg';
const timestampSelector = '#timestamp';
const resultListSelector = '.result-list li';
const searchBoxSelector = '.search-box input';
const routes = ['generic', 'react'] as const;
const routes = ['generic?tab=all', 'react?tab=all'] as const;

const isPageDev =
process.env.NODE_ENV === 'development' &&
Cypress.env('NEXTJS_ROUTER') === 'pages';

// Note: Thresholds might need to be adjusted as the page tested changes (e.g. more components are added etc)
const vitals: Record<(typeof routes)[number], Cypress.ReportWebVitalsConfig> = {
generic: {
'generic?tab=all': {
thresholds: {
fcp: isPageDev ? 2000 : 200,
lcp: isPageDev ? 2000 : 200,
Expand All @@ -30,7 +30,7 @@ const vitals: Record<(typeof routes)[number], Cypress.ReportWebVitalsConfig> = {
inp: 400,
},
},
react: {
'react?tab=all': {
thresholds: {
fcp: isPageDev ? 2000 : 400,
lcp: isPageDev ? 2000 : 400,
Expand Down
Loading
Loading