mirror of
https://github.com/vercel/commerce.git
synced 2025-05-18 15:36:58 +00:00
feat: implement text/image-with-text/icon-with-text content block
Signed-off-by: Chloe <pinkcloudvnn@gmail.com>
This commit is contained in:
parent
4684d54ac3
commit
a1d65a54c1
@ -1,15 +1,26 @@
|
||||
import Footer from 'components/layout/footer';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
const Placeholder = () => {
|
||||
return (
|
||||
<div className="mx-auto mb-2 w-full max-w-7xl animate-pulse py-6">
|
||||
<div className="h-10 w-1/2 rounded bg-gray-200" />
|
||||
<div className="mt-6 h-96 w-full rounded bg-gray-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<Suspense>
|
||||
<div className="w-full">
|
||||
<div className="mx-8 max-w-2xl py-20 sm:mx-auto">
|
||||
<Suspense>{children}</Suspense>
|
||||
</div>
|
||||
<>
|
||||
<div className="min-h-[600px] w-full">
|
||||
<Suspense fallback={<Placeholder />}>
|
||||
<div className="py-6">{children}</div>
|
||||
</Suspense>
|
||||
</div>
|
||||
<Footer />
|
||||
</Suspense>
|
||||
<Suspense>
|
||||
<Footer />
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,12 @@
|
||||
import type { Metadata } from 'next';
|
||||
|
||||
import Prose from 'components/prose';
|
||||
import { getPage } from 'lib/shopify';
|
||||
import IconWithTextBlock, { IconBlockPlaceholder } from 'components/page/icon-with-text-block';
|
||||
import ImageWithTextBlock from 'components/page/image-with-text-block';
|
||||
import TextBlock from 'components/page/text-block';
|
||||
import { getPage, getPageMetaObjects } from 'lib/shopify';
|
||||
import { PageContent, PageMetafieldKey } from 'lib/shopify/types';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { Suspense } from 'react';
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
@ -26,22 +30,45 @@ export async function generateMetadata({
|
||||
};
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
const contentMap: Record<PageMetafieldKey, (content: PageContent) => JSX.Element> = {
|
||||
page_icon_section: (content) => (
|
||||
<Suspense fallback={<IconBlockPlaceholder />}>
|
||||
<IconWithTextBlock content={content} />
|
||||
</Suspense>
|
||||
),
|
||||
page_image_content: (content) => <ImageWithTextBlock content={content} />,
|
||||
page_section: (content) => <TextBlock content={content} />
|
||||
};
|
||||
|
||||
export default async function Page({ params }: { params: { page: string } }) {
|
||||
const page = await getPage(params.page);
|
||||
|
||||
if (!page) return notFound();
|
||||
|
||||
const pageContents = (
|
||||
await Promise.allSettled(page.metafields.map((metafield) => getPageMetaObjects(metafield)))
|
||||
)
|
||||
.filter((result) => result.status === 'fulfilled')
|
||||
.map((result) => (result as PromiseFulfilledResult<PageContent | null>).value)
|
||||
.filter(Boolean) as PageContent[];
|
||||
|
||||
return (
|
||||
<>
|
||||
<h1 className="mb-8 text-5xl font-bold">{page.title}</h1>
|
||||
<Prose className="mb-8" html={page.body as string} />
|
||||
<p className="text-sm italic">
|
||||
{`This document was last updated on ${new Intl.DateTimeFormat(undefined, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric'
|
||||
}).format(new Date(page.updatedAt))}.`}
|
||||
</p>
|
||||
<div className="mx-auto mb-2 max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||
<h1 className="text-3xl font-bold leading-tight tracking-tight text-gray-900">
|
||||
{page.title}
|
||||
</h1>
|
||||
</div>
|
||||
<main>
|
||||
<div className="mx-auto max-w-7xl py-6 sm:px-6 lg:px-8">
|
||||
<div className="flex flex-col space-y-16">
|
||||
{pageContents.map((content) => (
|
||||
<div key={content.id}>{contentMap[content.key](content)}</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
17
components/hero-icon.tsx
Normal file
17
components/hero-icon.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import * as HeroIcons from '@heroicons/react/24/outline';
|
||||
import startcase from 'lodash.startcase';
|
||||
|
||||
type IconName = keyof typeof HeroIcons;
|
||||
interface IconProps {
|
||||
icon: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const DynamicHeroIcon = ({ icon, className }: IconProps) => {
|
||||
const _icon = startcase(icon).replace(/\s/g, '');
|
||||
const SingleIcon = HeroIcons[`${_icon}Icon` as IconName];
|
||||
|
||||
return SingleIcon ? <SingleIcon className={className} /> : null;
|
||||
};
|
||||
|
||||
export default DynamicHeroIcon;
|
@ -53,13 +53,26 @@ export default async function Footer() {
|
||||
{copyrightName.length && !copyrightName.endsWith('.') ? '.' : ''} All rights reserved.
|
||||
</p>
|
||||
<div className="ml-0 flex flex-row items-center gap-2 md:ml-auto">
|
||||
<Image alt="visa" src="/icons/visa.png" width={30} height={20} />
|
||||
<Image alt="mastercard" src="/icons/mastercard.png" width={30} height={20} />
|
||||
<Image
|
||||
alt="visa"
|
||||
src="/icons/visa.png"
|
||||
width={30}
|
||||
height={30}
|
||||
className="h-auto w-[30px]"
|
||||
/>
|
||||
<Image
|
||||
alt="mastercard"
|
||||
src="/icons/mastercard.png"
|
||||
width={30}
|
||||
height={30}
|
||||
className="h-auto w-[30px]"
|
||||
/>
|
||||
<Image
|
||||
alt="american-express"
|
||||
src="/icons/american-express.png"
|
||||
width={30}
|
||||
height={20}
|
||||
height={30}
|
||||
className="h-auto w-[30px]"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
94
components/page/icon-with-text-block.tsx
Normal file
94
components/page/icon-with-text-block.tsx
Normal file
@ -0,0 +1,94 @@
|
||||
import Grid from 'components/grid';
|
||||
import DynamicHeroIcon from 'components/hero-icon';
|
||||
import { getMetaobjects, getMetaobjectsByIds } from 'lib/shopify';
|
||||
import { PageContent, ScreenSize } from 'lib/shopify/types';
|
||||
|
||||
export const IconBlockPlaceholder = () => {
|
||||
return (
|
||||
<div className="flex animate-pulse flex-col gap-5 px-4 md:px-0">
|
||||
<div className="h-10 w-1/2 rounded bg-gray-200" />
|
||||
<div className="h-40 w-full rounded bg-gray-200" />
|
||||
<div className="h-40 w-full rounded bg-gray-200" />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const IconWithTextBlock = async ({ content }: { content: PageContent }) => {
|
||||
// for icon with text content, we only need the first metaobject as the array always contains only one element due to the metafield definition set up on Shopify
|
||||
const metaobject = content.metaobjects[0];
|
||||
|
||||
if (!metaobject) return null;
|
||||
|
||||
const [contentBlocks, layouts, screenSizes] = await Promise.all([
|
||||
getMetaobjectsByIds(metaobject.content ? JSON.parse(metaobject.content) : []),
|
||||
getMetaobjectsByIds(metaobject.layouts ? JSON.parse(metaobject.layouts) : []),
|
||||
getMetaobjects('screen_sizes')
|
||||
]);
|
||||
|
||||
const availableLayouts = layouts.reduce(
|
||||
(acc, layout) => {
|
||||
const screenSize = screenSizes.find((screen) => screen.id === layout.screen_size);
|
||||
if (screenSize?.size) {
|
||||
acc[screenSize.size.toLowerCase() as ScreenSize] = Number(layout.number_of_columns);
|
||||
}
|
||||
|
||||
return acc;
|
||||
},
|
||||
{} as Record<ScreenSize, number>
|
||||
);
|
||||
|
||||
let classnames = {} as { [key: string]: boolean };
|
||||
|
||||
if (availableLayouts.small) {
|
||||
classnames = {
|
||||
...classnames,
|
||||
'sm:grid-cols-1': availableLayouts.small === 1,
|
||||
'sm:grid-cols-2': availableLayouts.small === 2,
|
||||
'sm:grid-cols-3': availableLayouts.small === 3,
|
||||
'sm:grid-cols-4': availableLayouts.small === 4
|
||||
};
|
||||
}
|
||||
|
||||
if (availableLayouts.medium) {
|
||||
classnames = {
|
||||
...classnames,
|
||||
'md:grid-cols-1': availableLayouts.medium === 1,
|
||||
'md:grid-cols-2': availableLayouts.medium === 2,
|
||||
'md:grid-cols-3': availableLayouts.medium === 3,
|
||||
'md:grid-cols-4': availableLayouts.medium === 4
|
||||
};
|
||||
}
|
||||
|
||||
if (availableLayouts.large) {
|
||||
classnames = {
|
||||
...classnames,
|
||||
'lg:grid-cols-1': availableLayouts.large === 1,
|
||||
'lg:grid-cols-2': availableLayouts.large === 2,
|
||||
'lg:grid-cols-3': availableLayouts.large === 3,
|
||||
'lg:grid-cols-4': availableLayouts.large === 4
|
||||
};
|
||||
}
|
||||
|
||||
const validClassnames = Object.keys(classnames)
|
||||
.filter((key) => classnames[key])
|
||||
.join(' ');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 px-4 md:px-0">
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{metaobject.title}</h3>
|
||||
<Grid className={validClassnames}>
|
||||
{contentBlocks.map((block) => (
|
||||
<Grid.Item key={block.id} className="flex flex-col gap-2">
|
||||
{block.icon_name && (
|
||||
<DynamicHeroIcon icon={block.icon_name} className="w-16 text-secondary" />
|
||||
)}
|
||||
<div className="text-lg font-medium">{block.title}</div>
|
||||
<p className="text-base text-gray-800">{block.content}</p>
|
||||
</Grid.Item>
|
||||
))}
|
||||
</Grid>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default IconWithTextBlock;
|
17
components/page/image-display.tsx
Normal file
17
components/page/image-display.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import { getImage } from 'lib/shopify';
|
||||
import Image from 'next/image';
|
||||
|
||||
const ImageDisplay = async ({ fileId, title }: { fileId: string; title: string }) => {
|
||||
const image = await getImage(fileId);
|
||||
return (
|
||||
<Image
|
||||
src={image.url}
|
||||
alt={image.altText || `Display Image for ${title} section`}
|
||||
width={image.width}
|
||||
height={image.height}
|
||||
className="h-full w-full object-contain"
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageDisplay;
|
37
components/page/image-with-text-block.tsx
Normal file
37
components/page/image-with-text-block.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
import { PageContent } from 'lib/shopify/types';
|
||||
import { Suspense } from 'react';
|
||||
import ImageDisplay from './image-display';
|
||||
import RichTextDisplay from './rich-text-display';
|
||||
|
||||
const ImageWithTextBlock = ({ content }: { content: PageContent }) => {
|
||||
if (!content.metaobjects.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-10">
|
||||
{content.metaobjects.map((metaobject) => {
|
||||
const contentBlocks = JSON.parse(metaobject.description || '{}');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 px-4 md:px-0" key={metaobject.id}>
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{metaobject.title}</h3>
|
||||
<div className="grid grid-cols-1 gap-5 md:grid-cols-3">
|
||||
<div className="relative col-span-1">
|
||||
<Suspense>
|
||||
<ImageDisplay
|
||||
title={metaobject.title as string}
|
||||
fileId={metaobject.file as string}
|
||||
/>
|
||||
</Suspense>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<RichTextDisplay contentBlocks={contentBlocks.children} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ImageWithTextBlock;
|
37
components/page/rich-text-display.tsx
Normal file
37
components/page/rich-text-display.tsx
Normal file
@ -0,0 +1,37 @@
|
||||
type Content =
|
||||
| { type: 'paragraph'; children: Array<{ type: 'text'; value: string; bold?: boolean }> }
|
||||
| {
|
||||
type: 'text';
|
||||
value: string;
|
||||
bold?: boolean;
|
||||
};
|
||||
|
||||
const RichTextBlock = ({ block }: { block: Content }) => {
|
||||
if (block.type === 'text') {
|
||||
return block.bold ? (
|
||||
<strong className="font-semibold">{block.value}</strong>
|
||||
) : (
|
||||
<span>{block.value}</span>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<p className="text-gray-800">
|
||||
{block.children.map((child, index) => (
|
||||
<RichTextBlock key={index} block={child} />
|
||||
))}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
const RichTextDisplay = ({ contentBlocks }: { contentBlocks: Content[] }) => {
|
||||
return (
|
||||
<div className="flex w-full flex-col gap-2">
|
||||
{contentBlocks.map((block, index) => (
|
||||
<RichTextBlock key={index} block={block} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RichTextDisplay;
|
23
components/page/text-block.tsx
Normal file
23
components/page/text-block.tsx
Normal file
@ -0,0 +1,23 @@
|
||||
import { PageContent } from 'lib/shopify/types';
|
||||
import RichTextDisplay from './rich-text-display';
|
||||
|
||||
const TextBlock = ({ content }: { content: PageContent }) => {
|
||||
if (!content.metaobjects.length) return null;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-8">
|
||||
{content.metaobjects.map((metaobject) => {
|
||||
const contentBlocks = JSON.parse(metaobject.content || '{}');
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5 px-4 md:px-0" key={metaobject.id}>
|
||||
<h3 className="text-xl font-semibold leading-6 text-gray-900">{metaobject.title}</h3>
|
||||
<RichTextDisplay contentBlocks={contentBlocks.children} />
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TextBlock;
|
@ -29,6 +29,7 @@ import {
|
||||
} from './queries/collection';
|
||||
import { getMenuQuery } from './queries/menu';
|
||||
import { getMetaobjectsQuery } from './queries/metaobject';
|
||||
import { getImageQuery, getMetaobjectsByIdsQuery } from './queries/node';
|
||||
import { getPageQuery, getPagesQuery } from './queries/page';
|
||||
import {
|
||||
getProductQuery,
|
||||
@ -44,8 +45,10 @@ import {
|
||||
Menu,
|
||||
Metaobject,
|
||||
Money,
|
||||
PAGE_TYPES,
|
||||
Page,
|
||||
PageInfo,
|
||||
PageMetafield,
|
||||
Product,
|
||||
ProductVariant,
|
||||
ShopifyAddToCartOperation,
|
||||
@ -57,8 +60,10 @@ import {
|
||||
ShopifyCollectionsOperation,
|
||||
ShopifyCreateCartOperation,
|
||||
ShopifyFilter,
|
||||
ShopifyImageOperation,
|
||||
ShopifyMenuOperation,
|
||||
ShopifyMetaobject,
|
||||
ShopifyMetaobjectOperation,
|
||||
ShopifyMetaobjectsOperation,
|
||||
ShopifyPageOperation,
|
||||
ShopifyPagesOperation,
|
||||
@ -490,10 +495,36 @@ export async function getMetaobjects(type: string) {
|
||||
return reshapeMetaobjects(removeEdgesAndNodes(res.body.data.metaobjects));
|
||||
}
|
||||
|
||||
export async function getMetaobjectsByIds(ids: string[]) {
|
||||
if (!ids.length) return [];
|
||||
|
||||
const res = await shopifyFetch<ShopifyMetaobjectOperation>({
|
||||
query: getMetaobjectsByIdsQuery,
|
||||
variables: { ids }
|
||||
});
|
||||
|
||||
return reshapeMetaobjects(res.body.data.nodes);
|
||||
}
|
||||
|
||||
export async function getPageMetaObjects(metafield: PageMetafield) {
|
||||
let metaobjectIds = parseMetaFieldValue<string | string[]>(metafield) || metafield.value;
|
||||
|
||||
if (!metaobjectIds) {
|
||||
return null;
|
||||
}
|
||||
|
||||
metaobjectIds = (Array.isArray(metaobjectIds) ? metaobjectIds : [metaobjectIds]) as string[];
|
||||
|
||||
const metaobjects = await getMetaobjectsByIds(metaobjectIds);
|
||||
|
||||
return { metaobjects, id: metafield.id, key: metafield.key };
|
||||
}
|
||||
|
||||
export async function getPage(handle: string): Promise<Page> {
|
||||
const metafieldIdentifiers = PAGE_TYPES.map((key) => ({ key, namespace: 'custom' }));
|
||||
const res = await shopifyFetch<ShopifyPageOperation>({
|
||||
query: getPageQuery,
|
||||
variables: { handle }
|
||||
variables: { handle, metafieldIdentifiers }
|
||||
});
|
||||
|
||||
return res.body.data.pageByHandle;
|
||||
@ -604,3 +635,12 @@ export async function revalidate(req: NextRequest): Promise<NextResponse> {
|
||||
|
||||
return NextResponse.json({ status: 200, revalidated: true, now: Date.now() });
|
||||
}
|
||||
|
||||
export const getImage = async (id: string): Promise<Image> => {
|
||||
const res = await shopifyFetch<ShopifyImageOperation>({
|
||||
query: getImageQuery,
|
||||
variables: { id }
|
||||
});
|
||||
|
||||
return res.body.data.node.image;
|
||||
};
|
||||
|
33
lib/shopify/queries/node.ts
Normal file
33
lib/shopify/queries/node.ts
Normal file
@ -0,0 +1,33 @@
|
||||
export const getImageQuery = /* GraphQL */ `
|
||||
query getImage($id: ID!) {
|
||||
node(id: $id) {
|
||||
... on MediaImage {
|
||||
image {
|
||||
altText
|
||||
width
|
||||
height
|
||||
url
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
export const getMetaobjectsByIdsQuery = /* GraphQL */ `
|
||||
query getMetaobjectsByIds($ids: [ID!]!) {
|
||||
nodes(ids: $ids) {
|
||||
... on Metaobject {
|
||||
id
|
||||
fields {
|
||||
reference {
|
||||
... on Metaobject {
|
||||
id
|
||||
}
|
||||
}
|
||||
key
|
||||
value
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
@ -19,9 +19,14 @@ const pageFragment = /* GraphQL */ `
|
||||
`;
|
||||
|
||||
export const getPageQuery = /* GraphQL */ `
|
||||
query getPage($handle: String!) {
|
||||
query getPage($handle: String!, $metafieldIdentifiers: [HasMetafieldsIdentifier!]!) {
|
||||
pageByHandle(handle: $handle) {
|
||||
...page
|
||||
metafields(identifiers: $metafieldIdentifiers) {
|
||||
value
|
||||
key
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
${pageFragment}
|
||||
|
@ -51,6 +51,15 @@ export type Money = {
|
||||
currencyCode: string;
|
||||
};
|
||||
|
||||
export type PageMetafield = {
|
||||
id: string;
|
||||
key: PageMetafieldKey;
|
||||
value: string;
|
||||
};
|
||||
|
||||
export const PAGE_TYPES = ['page_icon_section', 'page_section', 'page_image_content'] as const;
|
||||
export type PageMetafieldKey = (typeof PAGE_TYPES)[number];
|
||||
|
||||
export type Page = {
|
||||
id: string;
|
||||
title: string;
|
||||
@ -60,6 +69,12 @@ export type Page = {
|
||||
seo?: SEO;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
metafields: PageMetafield[];
|
||||
};
|
||||
|
||||
export type MetafieldIdentifier = {
|
||||
key: string;
|
||||
namespace: string;
|
||||
};
|
||||
|
||||
export type ShopifyMetaobject = {
|
||||
@ -282,7 +297,12 @@ export type ShopifyMenuOperation = {
|
||||
|
||||
export type ShopifyPageOperation = {
|
||||
data: { pageByHandle: Page };
|
||||
variables: { handle: string };
|
||||
variables: { handle: string; metafieldIdentifiers: MetafieldIdentifier[] };
|
||||
};
|
||||
|
||||
export type ShopifyImageOperation = {
|
||||
data: { node: { image: Image } };
|
||||
variables: { id: string };
|
||||
};
|
||||
|
||||
export type ShopifyMetaobjectsOperation = {
|
||||
@ -296,6 +316,11 @@ export type ShopifyPagesOperation = {
|
||||
};
|
||||
};
|
||||
|
||||
export type ShopifyMetaobjectOperation = {
|
||||
data: { nodes: ShopifyMetaobject[] };
|
||||
variables: { ids: string[] };
|
||||
};
|
||||
|
||||
export type ShopifyProductOperation = {
|
||||
data: { product: ShopifyProduct };
|
||||
variables: {
|
||||
@ -363,3 +388,13 @@ export type Filter = {
|
||||
value: unknown;
|
||||
}[];
|
||||
};
|
||||
|
||||
export type PageContent = {
|
||||
id: string;
|
||||
key: PageMetafieldKey;
|
||||
metaobjects: Metaobject[];
|
||||
};
|
||||
|
||||
export const SCREEN_SIZES = ['small', 'medium', 'large', 'extra_large'] as const;
|
||||
|
||||
export type ScreenSize = (typeof SCREEN_SIZES)[number];
|
||||
|
@ -28,6 +28,7 @@
|
||||
"clsx": "^2.1.0",
|
||||
"geist": "^1.3.0",
|
||||
"lodash.get": "^4.4.2",
|
||||
"lodash.startcase": "^4.4.0",
|
||||
"next": "14.1.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
@ -40,6 +41,7 @@
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"@tailwindcss/typography": "^0.5.11",
|
||||
"@types/lodash.get": "^4.4.9",
|
||||
"@types/lodash.startcase": "^4.4.9",
|
||||
"@types/node": "20.11.30",
|
||||
"@types/react": "18.2.72",
|
||||
"@types/react-dom": "18.2.22",
|
||||
|
16
pnpm-lock.yaml
generated
16
pnpm-lock.yaml
generated
@ -23,6 +23,9 @@ dependencies:
|
||||
lodash.get:
|
||||
specifier: ^4.4.2
|
||||
version: 4.4.2
|
||||
lodash.startcase:
|
||||
specifier: ^4.4.0
|
||||
version: 4.4.0
|
||||
next:
|
||||
specifier: 14.1.4
|
||||
version: 14.1.4(react-dom@18.2.0)(react@18.2.0)
|
||||
@ -55,6 +58,9 @@ devDependencies:
|
||||
'@types/lodash.get':
|
||||
specifier: ^4.4.9
|
||||
version: 4.4.9
|
||||
'@types/lodash.startcase':
|
||||
specifier: ^4.4.9
|
||||
version: 4.4.9
|
||||
'@types/node':
|
||||
specifier: 20.11.30
|
||||
version: 20.11.30
|
||||
@ -753,6 +759,12 @@ packages:
|
||||
'@types/lodash': 4.17.1
|
||||
dev: true
|
||||
|
||||
/@types/lodash.startcase@4.4.9:
|
||||
resolution: {integrity: sha512-C0M4DlN1pnn2vEEhLHkTHxiRZ+3GlTegpoAEHHGXnuJkSOXyJMHGiSc+SLRzBlFZWHsBkixe6FqvEAEU04g14g==}
|
||||
dependencies:
|
||||
'@types/lodash': 4.17.1
|
||||
dev: true
|
||||
|
||||
/@types/lodash@4.17.1:
|
||||
resolution: {integrity: sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==}
|
||||
dev: true
|
||||
@ -2632,6 +2644,10 @@ packages:
|
||||
resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==}
|
||||
dev: true
|
||||
|
||||
/lodash.startcase@4.4.0:
|
||||
resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==}
|
||||
dev: false
|
||||
|
||||
/log-update@6.0.0:
|
||||
resolution: {integrity: sha512-niTvB4gqvtof056rRIrTZvjNYE4rCUzO6X/X+kYjd7WFxXeJ0NwEFnRxX6ehkvv3jTwrXnNdtAak5XYZuIyPFw==}
|
||||
engines: {node: '>=18'}
|
||||
|
Loading…
x
Reference in New Issue
Block a user