Skip to content

Commit

Permalink
feat: product page migration wip
Browse files Browse the repository at this point in the history
  • Loading branch information
federicobadini committed Feb 9, 2024
1 parent 7b9efb4 commit 7de0e46
Show file tree
Hide file tree
Showing 49 changed files with 1,941 additions and 485 deletions.
2 changes: 2 additions & 0 deletions frontend/.env.development
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ NEXT_PUBLIC_FLAG__STORE_HOME_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__TROUBLESHOOTING_COLLECTIONS_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_PRODUCT_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_PARTS_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_TOOLS_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_MARKETING_PAGE_ENABLED=true
2 changes: 2 additions & 0 deletions frontend/.env.test
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,5 @@ NODE_OPTIONS="--dns-result-order ipv4first"
NEXT_PUBLIC_DEV_API_AUTH_TOKEN=
NEXT_PUBLIC_FLAG__APP_ROUTER_PRODUCT_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_PARTS_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_TOOLS_PAGE_ENABLED=true
NEXT_PUBLIC_FLAG__APP_ROUTER_MARKETING_PAGE_ENABLED=true
102 changes: 102 additions & 0 deletions frontend/app/(defaultLayout)/app-router/Parts/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import { flags } from '@config/flags';
import { productListPath } from '@helpers/path-helpers';
import {
destylizeDeviceItemType,
destylizeDeviceTitle,
} from '@helpers/product-list-helpers';
import { invariant } from '@ifixit/helpers';
import ProductList from '@pages/api/nextjs/cache/product-list';
import { ifixitOrigin, shouldSkipCache } from 'app/_helpers/app-helpers';
import {
getDeviceCanonicalPath,
parseSearchParams,
} from 'app/_helpers/product-list-helpers';
import { notFound, redirect } from 'next/navigation';
import { ProductListType } from '@models/product-list';
import { search } from 'app/_data/product-list';

export interface DevicePartsPageProps {
params: {
slug?: string[];
};
searchParams: {
disableCacheGets?: string | string[] | undefined;
};
}

export default async function DevicePartsPage({
params,
searchParams,
}: DevicePartsPageProps) {
if (!flags.APP_ROUTER_PARTS_PAGE_ENABLED) notFound();

if (params.slug == null) notFound();

const [deviceHandle, itemTypeHandle, ...otherPathSegments] = params.slug;

if (deviceHandle == null || otherPathSegments.length > 0) notFound();

const deviceTitle = destylizeDeviceTitle(deviceHandle);
const itemType = itemTypeHandle
? destylizeDeviceItemType(itemTypeHandle)
: null;

const productList = await ProductList.get(
{
filters: {
deviceTitle: {
eqi: deviceTitle,
},
},
ifixitOrigin: ifixitOrigin(),
},
{ forceMiss: shouldSkipCache(searchParams) }
);

if (productList == null) notFound();

const shouldRedirectToCanonical =
typeof productList?.deviceTitle === 'string' &&
productList.deviceTitle !== deviceTitle;

const canonicalPath = getDeviceCanonicalPath(
productList?.deviceTitle,
itemType
);

if (shouldRedirectToCanonical) {
invariant(canonicalPath != null, 'canonical path is required');
redirect(canonicalPath);
}

if (productList.redirectTo) {
const path = productListPath({
productList: productList.redirectTo,
itemType: itemType ?? undefined,
});
return redirect(`${path}?${params}`);
}

const urlState = parseSearchParams(searchParams);
const { hitsCount } = await search({
productListType: ProductListType.DeviceParts,
baseFilters: `device:${JSON.stringify(deviceTitle)}`,
// Todo: handle this
excludePro: true,
...urlState,
});

console.log({
productListType: ProductListType.DeviceParts,
excludePro: true,
...urlState,
});

return (
<>
<div>Device parts page</div>
<div>{JSON.stringify(productList)}</div>
<div>{hitsCount}</div>
</>
);
}
55 changes: 51 additions & 4 deletions frontend/app/(defaultLayout)/app-router/Parts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,57 @@
import { flags } from '@config/flags';
import { notFound } from 'next/navigation';
import { productListPath } from '@helpers/path-helpers';
import { ProductListType } from '@models/product-list';
import ProductList from '@pages/api/nextjs/cache/product-list';
import { ProductListView } from '@templates/product-list/ProductListView';
import { search } from 'app/_data/product-list';
import { AlgoliaSearchProvider } from 'app/_data/product-list/useAlgoliaSearch';
import { ifixitOrigin, shouldSkipCache } from 'app/_helpers/app-helpers';
import { parseSearchParams } from 'app/_helpers/product-list-helpers';
import { notFound, redirect } from 'next/navigation';

export interface PartsPageProps {}
export interface AllPartsPageProps {
params: {};
searchParams: {
disableCacheGets?: string | string[] | undefined;
};
}

export default async function PartsPage() {
export default async function AllPartsPage({
params,
searchParams,
}: AllPartsPageProps) {
if (!flags.APP_ROUTER_PARTS_PAGE_ENABLED) notFound();

return <div>Parts page</div>;
const productList = await ProductList.get(
{
filters: { handle: { eq: 'Parts' } },
ifixitOrigin: ifixitOrigin(),
},
{ forceMiss: shouldSkipCache(searchParams) }
);

if (productList == null) notFound();

if (productList.redirectTo) {
const path = productListPath({
productList: productList.redirectTo,
});
return redirect(`${path}?${params}`);
}

const urlState = parseSearchParams(searchParams);
const algoliaSearchResponse = await search({
productListType: ProductListType.DeviceParts,
// Todo: handle this
excludePro: true,
...urlState,
});

return (
<>
<AlgoliaSearchProvider {...algoliaSearchResponse}>
<ProductListView productList={productList} />
</AlgoliaSearchProvider>
</>
);
}
62 changes: 62 additions & 0 deletions frontend/app/(defaultLayout)/app-router/Shop/[handle]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { flags } from '@config/flags';
import { productListPath } from '@helpers/path-helpers';
import { invariant } from '@ifixit/helpers';
import ProductList from '@pages/api/nextjs/cache/product-list';
import { ifixitOrigin, shouldSkipCache } from 'app/_helpers/app-helpers';
import { notFound, redirect } from 'next/navigation';

export interface ToolCategoryPageProps {
params: {
handle?: string;
};
searchParams: {
disableCacheGets?: string | string[] | undefined;
};
}

export default async function ShopPage({
params,
searchParams,
}: ToolCategoryPageProps) {
if (!flags.APP_ROUTER_MARKETING_PAGE_ENABLED) notFound();

if (params.handle == null) notFound();

const productList = await ProductList.get(
{
filters: {
handle: { eqi: params.handle },
type: { eq: 'marketing' },
},
ifixitOrigin: ifixitOrigin(),
},
{ forceMiss: shouldSkipCache(searchParams) }
);

if (productList == null) notFound();

const shouldRedirectToCanonical =
typeof productList?.handle === 'string' &&
productList.handle !== params.handle;
const canonicalPath =
typeof productList?.handle === 'string'
? `/Shop/${productList.handle}`
: null;

if (shouldRedirectToCanonical) {
invariant(canonicalPath != null, 'canonical path is required');
redirect(canonicalPath);
}
if (productList.redirectTo) {
const path = productListPath({
productList: productList.redirectTo,
});
return redirect(`${path}?${params}`);
}
return (
<>
<div>Marketing page</div>
<div>{JSON.stringify(productList)}</div>
</>
);
}
80 changes: 80 additions & 0 deletions frontend/app/(defaultLayout)/app-router/Tools/[handle]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { flags } from '@config/flags';
import { productListPath } from '@helpers/path-helpers';
import { invariant } from '@ifixit/helpers';
import { ProductListType } from '@models/product-list';
import ProductList from '@pages/api/nextjs/cache/product-list';
import { search } from 'app/_data/product-list';
import { ifixitOrigin, shouldSkipCache } from 'app/_helpers/app-helpers';
import { parseSearchParams } from 'app/_helpers/product-list-helpers';
import { notFound, redirect } from 'next/navigation';

export interface ToolCategoryPageProps {
params: {
handle?: string;
};
searchParams: {
disableCacheGets?: string | string[] | undefined;
};
}

export default async function ToolCategoryPage({
params,
searchParams,
}: ToolCategoryPageProps) {
if (!flags.APP_ROUTER_TOOLS_PAGE_ENABLED) notFound();

if (params.handle == null) notFound();

const productList = await ProductList.get(
{
filters: {
handle: { eqi: params.handle },
type: { in: ['marketing', 'tools'] },
},
ifixitOrigin: ifixitOrigin(),
},
{ forceMiss: shouldSkipCache(searchParams) }
);

if (productList == null) notFound();

const isMarketing = productList?.type === ProductListType.Marketing;
const isMiscapitalized =
typeof productList?.handle === 'string' &&
productList.handle !== params.handle;
const shouldRedirectToCanonical = isMiscapitalized || isMarketing;
const canonicalPath =
typeof productList?.handle === 'string'
? isMarketing
? `/Shop/${productList.handle}`
: `/Tools/${productList.handle}`
: null;

if (shouldRedirectToCanonical) {
invariant(canonicalPath != null, 'canonical path is required');
redirect(canonicalPath);
}

if (productList.redirectTo) {
const path = productListPath({
productList: productList.redirectTo,
});
return redirect(`${path}?${params}`);
}

const urlState = parseSearchParams(searchParams);
const { hitsCount } = await search({
productListType: ProductListType.AllTools,
excludePro: true,
baseFilters: productList.filters || undefined,
...urlState,
});

return (
<>
<div>Tool category page</div>
<div>{JSON.stringify(productList)}</div>
<div>{hitsCount}</div>
</>
);
}
54 changes: 54 additions & 0 deletions frontend/app/(defaultLayout)/app-router/Tools/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { flags } from '@config/flags';
import { productListPath } from '@helpers/path-helpers';
import { ProductListType } from '@models/product-list';
import ProductList from '@pages/api/nextjs/cache/product-list';
import { search } from 'app/_data/product-list';
import { ifixitOrigin, shouldSkipCache } from 'app/_helpers/app-helpers';
import { parseSearchParams } from 'app/_helpers/product-list-helpers';
import { notFound, redirect } from 'next/navigation';

export interface AllToolsPageProps {
params: {};
searchParams: {
disableCacheGets?: string | string[] | undefined;
};
}

export default async function AllToolsPage({
params,
searchParams,
}: AllToolsPageProps) {
if (!flags.APP_ROUTER_TOOLS_PAGE_ENABLED) notFound();

const productList = await ProductList.get(
{
filters: { handle: { eq: 'Tools' } },
ifixitOrigin: ifixitOrigin(),
},
{ forceMiss: shouldSkipCache(searchParams) }
);

if (productList == null) notFound();

if (productList.redirectTo) {
const path = productListPath({
productList: productList.redirectTo,
});
return redirect(`${path}?${params}`);
}

const urlState = parseSearchParams(searchParams);
const { hitsCount } = await search({
productListType: ProductListType.AllTools,
excludePro: true,
...urlState,
});

return (
<>
<div>All tools page</div>
<div>{JSON.stringify(productList)}</div>
<div>{hitsCount}</div>
</>
);
}
16 changes: 16 additions & 0 deletions frontend/app/_data/product-list/concerns/algolia.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { createFetchRequester } from '@algolia/requester-fetch';
import { ALGOLIA_API_KEY, ALGOLIA_APP_ID } from '@config/env';
import algoliasearch, { SearchClient } from 'algoliasearch';

export type { SearchResponse } from '@algolia/client-search';

export function createClient() {
const client = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_API_KEY, {
requester: createFetchRequester(),
});
return client;
}

export type MultipleQueriesQuery = Parameters<
SearchClient['multipleQueries']
>[0][number];
Loading

0 comments on commit 7de0e46

Please sign in to comment.