initial commit
This commit is contained in:
33
app/home/(user)/(dashboard)/booking/[handle]/page.tsx
Normal file
33
app/home/(user)/(dashboard)/booking/[handle]/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
import { HomeLayoutPageHeader } from '../../../_components/home-page-header';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
const title = i18n.t('booking:title');
|
||||
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
function BookingHandlePage() {
|
||||
return (
|
||||
<>
|
||||
<HomeLayoutPageHeader
|
||||
title={<Trans i18nKey={'booking:title'} />}
|
||||
description={<Trans i18nKey={'booking:description'} />}
|
||||
/>
|
||||
|
||||
<PageBody></PageBody>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export default withI18n(BookingHandlePage);
|
||||
@@ -1,12 +1,15 @@
|
||||
import { use } from 'react';
|
||||
|
||||
import { PageBody } from '@kit/ui/page';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
|
||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||
|
||||
|
||||
import { HomeLayoutPageHeader } from '../../_components/home-page-header';
|
||||
import OrderCards from '../../_components/order-cards';
|
||||
import ServiceCategories from '../../_components/service-categories';
|
||||
import { loadTtoServices } from '../../_lib/server/load-tto-services';
|
||||
|
||||
export const generateMetadata = async () => {
|
||||
const i18n = await createI18nServerInstance();
|
||||
@@ -18,6 +21,8 @@ export const generateMetadata = async () => {
|
||||
};
|
||||
|
||||
function BookingPage() {
|
||||
const { heroCategories, ttoCategories } = use(loadTtoServices());
|
||||
console.log('ttoCategories', heroCategories, ttoCategories);
|
||||
return (
|
||||
<>
|
||||
<HomeLayoutPageHeader
|
||||
@@ -26,7 +31,8 @@ function BookingPage() {
|
||||
/>
|
||||
|
||||
<PageBody>
|
||||
<OrderCards />
|
||||
<OrderCards heroCategories={heroCategories} />
|
||||
<ServiceCategories categories={ttoCategories} />
|
||||
</PageBody>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -1,74 +1,68 @@
|
||||
"use client";
|
||||
'use client';
|
||||
|
||||
import { ChevronRight, HeartPulse } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { cn } from '@/lib/utils';
|
||||
import { pathsConfig } from '@/packages/shared/src/config';
|
||||
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
|
||||
import { ChevronRight, HeartPulse } from 'lucide-react';
|
||||
|
||||
import { Button } from '@kit/ui/button';
|
||||
import {
|
||||
Card,
|
||||
CardHeader,
|
||||
CardDescription,
|
||||
CardProps,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
CardProps,
|
||||
} from '@kit/ui/card';
|
||||
import { Trans } from '@kit/ui/trans';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
const dummyCards = [
|
||||
{
|
||||
title: 'booking:analysisPackages.title',
|
||||
description: 'booking:analysisPackages.description',
|
||||
descriptionColor: 'text-primary',
|
||||
icon: (
|
||||
<Link href={'/home/order-analysis-package'}>
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
),
|
||||
cardVariant: 'gradient-success' as CardProps['variant'],
|
||||
iconBg: 'bg-warning',
|
||||
},
|
||||
];
|
||||
import { ServiceCategory } from './service-categories';
|
||||
|
||||
export default function OrderCards() {
|
||||
export default function OrderCards({
|
||||
heroCategories,
|
||||
}: {
|
||||
heroCategories: ServiceCategory[];
|
||||
}) {
|
||||
return (
|
||||
<div className="grid grid-cols-3 gap-6 mt-4">
|
||||
{dummyCards.map(({
|
||||
title,
|
||||
description,
|
||||
icon,
|
||||
cardVariant,
|
||||
descriptionColor,
|
||||
iconBg,
|
||||
}) => (
|
||||
<div className="mt-4 grid grid-cols-3 gap-6">
|
||||
{heroCategories.map(({ name, description, color, handle }) => (
|
||||
<Card
|
||||
key={title}
|
||||
variant={cardVariant}
|
||||
key={name}
|
||||
variant={`gradient-${color}` as CardProps['variant']}
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="items-end-safe">
|
||||
<CardHeader className="relative flex flex-row justify-between">
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
|
||||
iconBg,
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full',
|
||||
`text-${color}`,
|
||||
`bg-${color}/10`,
|
||||
{
|
||||
'bg-primary/10': color === 'success',
|
||||
},
|
||||
)}
|
||||
>
|
||||
{icon}
|
||||
<ComponentInstanceIcon
|
||||
className={cn('size-4', `fill-${color}`)}
|
||||
/>
|
||||
</div>
|
||||
<div className="absolute top-2 right-2 flex size-8 items-center-safe justify-center-safe rounded-xl text-white">
|
||||
<Link
|
||||
href={pathsConfig.app.bookingHandle.replace('[handle]', handle)}
|
||||
>
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="px-2 text-black"
|
||||
>
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex flex-col items-start gap-2">
|
||||
<div
|
||||
className={'flex size-8 items-center-safe justify-center-safe rounded-full text-white bg-primary\/10 mb-6'}
|
||||
>
|
||||
<HeartPulse className="size-4 fill-green-500" />
|
||||
</div>
|
||||
<h5>
|
||||
<Trans i18nKey={title} />
|
||||
</h5>
|
||||
<CardDescription className={descriptionColor}>
|
||||
<Trans i18nKey={description} />
|
||||
</CardDescription>
|
||||
<CardFooter className="mt-5 flex flex-col items-start gap-2">
|
||||
<h5>{name}</h5>
|
||||
<CardDescription>{description}</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
|
||||
61
app/home/(user)/_components/service-categories.tsx
Normal file
61
app/home/(user)/_components/service-categories.tsx
Normal file
@@ -0,0 +1,61 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
import { createPath, pathsConfig } from '@/packages/shared/src/config';
|
||||
import { ComponentInstanceIcon } from '@radix-ui/react-icons';
|
||||
|
||||
import { cn } from '@kit/ui/shadcn';
|
||||
import { Card, CardDescription, CardTitle } from '@kit/ui/shadcn/card';
|
||||
|
||||
export interface ServiceCategory {
|
||||
name: string;
|
||||
handle: string;
|
||||
color: string;
|
||||
description: string;
|
||||
}
|
||||
|
||||
const ServiceCategories = ({
|
||||
categories,
|
||||
}: {
|
||||
categories: ServiceCategory[];
|
||||
}) => {
|
||||
return (
|
||||
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 md:grid-cols-3">
|
||||
{categories.map((category, index) => (
|
||||
<Card
|
||||
key={index}
|
||||
className="flex cursor-pointer gap-2 p-4 shadow hover:shadow-md"
|
||||
onClick={() => {
|
||||
redirect(
|
||||
pathsConfig.app.bookingHandle.replace(
|
||||
'[handle]',
|
||||
category.handle,
|
||||
),
|
||||
);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className={cn(
|
||||
'flex size-8 items-center-safe justify-center-safe rounded-full',
|
||||
`bg-${category.color}/10`,
|
||||
`text-${category.color}`,
|
||||
)}
|
||||
>
|
||||
<ComponentInstanceIcon />
|
||||
</div>
|
||||
<div>
|
||||
<h5 className="mb-2 text-lg font-semibold">{category.name}</h5>
|
||||
<CardDescription className="">
|
||||
{category.description}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ServiceCategories;
|
||||
@@ -1,9 +1,11 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { listProductTypes } from "@lib/data/products";
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
import { getProductCategories } from '@lib/data/categories';
|
||||
import { listProductTypes } from '@lib/data/products';
|
||||
import { listRegions } from '@lib/data/regions';
|
||||
|
||||
import { OrderAnalysisCard } from '../../_components/order-analyses-cards';
|
||||
import { ServiceCategory } from '../../_components/service-categories';
|
||||
|
||||
async function countryCodesLoader() {
|
||||
const countryCodes = await listRegions().then((regions) =>
|
||||
@@ -14,7 +16,9 @@ async function countryCodesLoader() {
|
||||
export const loadCountryCodes = cache(countryCodesLoader);
|
||||
|
||||
async function productCategoriesLoader() {
|
||||
const productCategories = await getProductCategories({ fields: "*products, *products.variants" });
|
||||
const productCategories = await getProductCategories({
|
||||
fields: '*products, *products.variants, is_active',
|
||||
});
|
||||
return productCategories.product_categories ?? [];
|
||||
}
|
||||
export const loadProductCategories = cache(productCategoriesLoader);
|
||||
@@ -29,25 +33,34 @@ async function analysesLoader() {
|
||||
const [countryCodes, productCategories] = await Promise.all([
|
||||
loadCountryCodes(),
|
||||
loadProductCategories(),
|
||||
]);
|
||||
]);
|
||||
const countryCode = countryCodes[0]!;
|
||||
|
||||
const category = productCategories.find(({ metadata }) => metadata?.page === 'order-analysis');
|
||||
|
||||
const category = productCategories.find(
|
||||
({ metadata }) => metadata?.page === 'order-analysis',
|
||||
);
|
||||
const serviceCategories = productCategories.filter(
|
||||
({ parent_category }) => parent_category?.handle === 'tto-categories',
|
||||
);
|
||||
console.log('serviceCategories', serviceCategories);
|
||||
return {
|
||||
analyses: category?.products?.map<OrderAnalysisCard>(({ title, description, subtitle, variants, status, metadata }) => {
|
||||
const variant = variants![0]!;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
variant: {
|
||||
id: variant.id,
|
||||
analyses:
|
||||
category?.products?.map<OrderAnalysisCard>(
|
||||
({ title, description, subtitle, variants, status, metadata }) => {
|
||||
const variant = variants![0]!;
|
||||
return {
|
||||
title,
|
||||
description,
|
||||
subtitle,
|
||||
variant: {
|
||||
id: variant.id,
|
||||
},
|
||||
isAvailable:
|
||||
status === 'published' && !!metadata?.analysisIdOriginal,
|
||||
};
|
||||
},
|
||||
isAvailable: status === 'published' && !!metadata?.analysisIdOriginal,
|
||||
};
|
||||
}) ?? [],
|
||||
) ?? [],
|
||||
countryCode,
|
||||
}
|
||||
};
|
||||
}
|
||||
export const loadAnalyses = cache(analysesLoader);
|
||||
|
||||
49
app/home/(user)/_lib/server/load-tto-services.ts
Normal file
49
app/home/(user)/_lib/server/load-tto-services.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { cache } from 'react';
|
||||
|
||||
import { getProductCategories } from '@lib/data';
|
||||
|
||||
import { ServiceCategory } from '../../_components/service-categories';
|
||||
|
||||
async function ttoServicesLoader() {
|
||||
const response = await getProductCategories({
|
||||
fields: '*products, is_active, metadata',
|
||||
});
|
||||
console.log('response.product_categories', response.product_categories);
|
||||
const heroCategories = response.product_categories?.filter(
|
||||
({ parent_category, is_active, metadata }) =>
|
||||
parent_category?.handle === 'tto-categories' &&
|
||||
is_active &&
|
||||
metadata?.isHero,
|
||||
);
|
||||
|
||||
const ttoCategories = response.product_categories?.filter(
|
||||
({ parent_category, is_active, metadata }) =>
|
||||
parent_category?.handle === 'tto-categories' &&
|
||||
is_active &&
|
||||
!metadata?.isHero,
|
||||
);
|
||||
|
||||
return {
|
||||
heroCategories:
|
||||
heroCategories.map<ServiceCategory>(
|
||||
({ name, handle, metadata, description }) => ({
|
||||
name,
|
||||
handle,
|
||||
color:
|
||||
typeof metadata?.color === 'string' ? metadata.color : 'primary',
|
||||
description,
|
||||
}),
|
||||
) ?? [],
|
||||
ttoCategories:
|
||||
ttoCategories.map<ServiceCategory>(
|
||||
({ name, handle, metadata, description }) => ({
|
||||
name,
|
||||
handle,
|
||||
color:
|
||||
typeof metadata?.color === 'string' ? metadata.color : 'primary',
|
||||
description,
|
||||
}),
|
||||
) ?? [],
|
||||
};
|
||||
}
|
||||
export const loadTtoServices = cache(ttoServicesLoader);
|
||||
Reference in New Issue
Block a user