MED-213: lifestyle
MED-213: lifestyle
This commit is contained in:
49
app/home/(user)/_components/ai/ai-blocks.tsx
Normal file
49
app/home/(user)/_components/ai/ai-blocks.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
'use server';
|
||||
|
||||
import React, { Suspense } from 'react';
|
||||
|
||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
||||
|
||||
import { isValidOpenAiEnv } from '../../_lib/server/is-valid-open-ai-env';
|
||||
import LifeStyleCard from './life-style-card';
|
||||
import OrderAnalysesPackageCard from './order-analyses-package-card';
|
||||
import Recommendations from './recommendations';
|
||||
import RecommendationsSkeleton from './recommendations-skeleton';
|
||||
import { AnalysisResponses } from './types';
|
||||
|
||||
const AIBlocks = async ({
|
||||
account,
|
||||
analysisResponses,
|
||||
}: {
|
||||
account: AccountWithParams;
|
||||
analysisResponses?: AnalysisResponses;
|
||||
}) => {
|
||||
const isOpenAiAvailable = await isValidOpenAiEnv();
|
||||
|
||||
if (!isOpenAiAvailable) {
|
||||
return <OrderAnalysesPackageCard />;
|
||||
}
|
||||
|
||||
if (analysisResponses?.length === 0) {
|
||||
return (
|
||||
<>
|
||||
<OrderAnalysesPackageCard />
|
||||
<Suspense fallback={<RecommendationsSkeleton amount={1} />}>
|
||||
<LifeStyleCard
|
||||
account={account}
|
||||
analysisResponses={analysisResponses}
|
||||
/>
|
||||
</Suspense>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Suspense fallback={<RecommendationsSkeleton />}>
|
||||
<LifeStyleCard account={account} analysisResponses={analysisResponses} />
|
||||
<Recommendations account={account} />
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export default AIBlocks;
|
||||
44
app/home/(user)/_components/ai/life-style-card.tsx
Normal file
44
app/home/(user)/_components/ai/life-style-card.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
'use server';
|
||||
|
||||
import React from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
||||
import { pathsConfig } from '@/packages/shared/src/config';
|
||||
import { ChevronRight } from 'lucide-react';
|
||||
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import { Card, CardHeader } from '@kit/ui/shadcn/card';
|
||||
|
||||
import { loadLifeStyle } from '../../_lib/server/load-life-style';
|
||||
import { AnalysisResponses } from './types';
|
||||
|
||||
const LifeStyleCard = async ({
|
||||
account,
|
||||
analysisResponses,
|
||||
}: {
|
||||
account: AccountWithParams;
|
||||
analysisResponses?: AnalysisResponses;
|
||||
}) => {
|
||||
const data = await loadLifeStyle(account, analysisResponses);
|
||||
|
||||
return (
|
||||
<Card variant="gradient-success" className="flex flex-col justify-between">
|
||||
<CardHeader className="flex-row justify-between">
|
||||
<h5>
|
||||
<Trans i18nKey="dashboard:heroCard.lifeStyle.title" />
|
||||
</h5>
|
||||
<Link href={pathsConfig.app.lifeStyle}>
|
||||
<Button size="icon" variant="outline" className="px-2 text-black">
|
||||
<ChevronRight className="size-4 stroke-2" />
|
||||
</Button>
|
||||
</Link>
|
||||
</CardHeader>
|
||||
<span className="text-primary p-4 text-sm">{data.summary}</span>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default LifeStyleCard;
|
||||
@@ -0,0 +1,51 @@
|
||||
import React from 'react';
|
||||
|
||||
import Link from 'next/link';
|
||||
|
||||
import { pathsConfig } from '@/packages/shared/src/config';
|
||||
import { ChevronRight, HeartPulse } from 'lucide-react';
|
||||
|
||||
import { Trans } from '@kit/ui/makerkit/trans';
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from '@kit/ui/shadcn/card';
|
||||
|
||||
const OrderAnalysesPackageCard = () => {
|
||||
return (
|
||||
<Card
|
||||
variant="gradient-success"
|
||||
className="xs:w-1/2 flex w-full flex-col justify-between sm:w-auto"
|
||||
>
|
||||
<CardHeader className="flex-row sm:pb-0">
|
||||
<div
|
||||
className={
|
||||
'bg-primary/10 mb-6 flex size-8 items-center-safe justify-center-safe rounded-full text-white'
|
||||
}
|
||||
>
|
||||
<HeartPulse className="size-4 fill-green-500" />
|
||||
</div>
|
||||
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||
<Link href={pathsConfig.app.orderAnalysisPackage}>
|
||||
<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">
|
||||
<h5>
|
||||
<Trans i18nKey="dashboard:heroCard.orderPackage.title" />
|
||||
</h5>
|
||||
<CardDescription className="text-primary">
|
||||
<Trans i18nKey="dashboard:heroCard.orderPackage.description" />
|
||||
</CardDescription>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
export default OrderAnalysesPackageCard;
|
||||
67
app/home/(user)/_components/ai/recommendations-skeleton.tsx
Normal file
67
app/home/(user)/_components/ai/recommendations-skeleton.tsx
Normal file
@@ -0,0 +1,67 @@
|
||||
import React from 'react';
|
||||
|
||||
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
|
||||
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from '@kit/ui/shadcn/card';
|
||||
import { Skeleton } from '@kit/ui/skeleton';
|
||||
|
||||
const RecommendationsSkeleton = ({ amount = 2 }: { amount?: number }) => {
|
||||
const emptyData = [
|
||||
{
|
||||
title: '1',
|
||||
description: '',
|
||||
subtitle: '',
|
||||
variant: { id: '' },
|
||||
price: 1,
|
||||
},
|
||||
];
|
||||
return Array.from({ length: amount }, (_, index) => {
|
||||
const { title, description, subtitle } = emptyData[0]!;
|
||||
|
||||
return (
|
||||
<Skeleton key={title + index}>
|
||||
<Card>
|
||||
<CardHeader className="flex-row">
|
||||
<div
|
||||
className={
|
||||
'mb-6 flex size-8 items-center-safe justify-center-safe'
|
||||
}
|
||||
/>
|
||||
<div className="ml-auto flex size-8 items-center-safe justify-center-safe">
|
||||
<Button size="icon" className="px-2" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex">
|
||||
<div className="flex flex-1 flex-col items-start">
|
||||
<h5>
|
||||
{title}
|
||||
{description && (
|
||||
<>
|
||||
{' '}
|
||||
<InfoTooltip
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</h5>
|
||||
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 self-end text-sm"></div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Skeleton>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
export default RecommendationsSkeleton;
|
||||
@@ -4,9 +4,9 @@ import React from 'react';
|
||||
|
||||
import { AccountWithParams } from '@/packages/features/accounts/src/types/accounts';
|
||||
|
||||
import { loadAnalyses } from '../_lib/server/load-analyses';
|
||||
import { loadRecommendations } from '../_lib/server/load-recommendations';
|
||||
import OrderAnalysesCards from './order-analyses-cards';
|
||||
import { loadAnalyses } from '../../_lib/server/load-analyses';
|
||||
import { loadRecommendations } from '../../_lib/server/load-recommendations';
|
||||
import OrderAnalysesCards from '../order-analyses-cards';
|
||||
|
||||
export default async function Recommendations({
|
||||
account,
|
||||
@@ -15,7 +15,7 @@ export default async function Recommendations({
|
||||
}) {
|
||||
const { analyses, countryCode } = await loadAnalyses();
|
||||
|
||||
const analysisRecommendations = await loadRecommendations(analyses, account);
|
||||
const analysisRecommendations = await loadRecommendations(account);
|
||||
const orderAnalyses = analyses.filter((analysis) =>
|
||||
analysisRecommendations.includes(analysis.title),
|
||||
);
|
||||
18
app/home/(user)/_components/ai/types.ts
Normal file
18
app/home/(user)/_components/ai/types.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { Database } from '@/packages/supabase/src/database.types';
|
||||
|
||||
export interface ILifeStyleResponse {
|
||||
lifestyle: {
|
||||
title: string;
|
||||
description: string;
|
||||
score: 0 | 1 | 2;
|
||||
}[];
|
||||
summary: string | null;
|
||||
}
|
||||
|
||||
export enum PROMPT_NAME {
|
||||
LIFE_STYLE = 'Life Style',
|
||||
ANALYSIS_RECOMMENDATIONS = 'Analysis Recommendations',
|
||||
}
|
||||
|
||||
export type AnalysisResponses =
|
||||
Database['medreport']['Functions']['get_latest_analysis_response_elements_for_current_user']['Returns'];
|
||||
@@ -57,72 +57,64 @@ export default function OrderAnalysesCards({
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="xs:grid-cols-3 mt-4 grid gap-6">
|
||||
{analyses.map(({ title, variant, description, subtitle, price }) => {
|
||||
const formattedPrice =
|
||||
typeof price === 'number'
|
||||
? formatCurrency({
|
||||
currencyCode: 'eur',
|
||||
locale: language,
|
||||
value: price,
|
||||
})
|
||||
: null;
|
||||
return (
|
||||
<Card
|
||||
key={title}
|
||||
variant="gradient-success"
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="flex-row">
|
||||
<div
|
||||
className={
|
||||
'bg-primary/10 mb-6 flex size-8 items-center-safe justify-center-safe rounded-full text-white'
|
||||
}
|
||||
>
|
||||
<HeartPulse className="size-4 fill-green-500" />
|
||||
</div>
|
||||
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="px-2 text-black"
|
||||
onClick={() => handleSelect(variant.id)}
|
||||
>
|
||||
{variantAddingToCart === variant.id ? (
|
||||
<Loader2 className="size-4 animate-spin stroke-2" />
|
||||
) : (
|
||||
<ShoppingCart className="size-4 stroke-2" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex gap-2">
|
||||
<div className="flex flex-1 flex-col items-start gap-2">
|
||||
<h5>
|
||||
{title}
|
||||
{description && (
|
||||
<>
|
||||
<InfoTooltip
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>{formattedPrice}</span>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</h5>
|
||||
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 self-end text-sm">
|
||||
<span>{formattedPrice}</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
return analyses.map(({ title, variant, description, subtitle, price }) => {
|
||||
const formattedPrice =
|
||||
typeof price === 'number'
|
||||
? formatCurrency({
|
||||
currencyCode: 'eur',
|
||||
locale: language,
|
||||
value: price,
|
||||
})
|
||||
: null;
|
||||
return (
|
||||
<Card
|
||||
key={title}
|
||||
variant="gradient-success"
|
||||
className="flex flex-col justify-between"
|
||||
>
|
||||
<CardHeader className="flex-row">
|
||||
<div className="bg-primary/10 mb-6 flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||
<HeartPulse className="size-4 fill-green-500" />
|
||||
</div>
|
||||
<div className="bg-warning ml-auto flex size-8 items-center-safe justify-center-safe rounded-full text-white">
|
||||
<Button
|
||||
size="icon"
|
||||
variant="outline"
|
||||
className="px-2 text-black"
|
||||
onClick={() => handleSelect(variant.id)}
|
||||
>
|
||||
{variantAddingToCart === variant.id ? (
|
||||
<Loader2 className="size-4 animate-spin stroke-2" />
|
||||
) : (
|
||||
<ShoppingCart className="size-4 stroke-2" />
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex gap-2">
|
||||
<div className="flex flex-1 flex-col items-start gap-2">
|
||||
<h5>
|
||||
{title}
|
||||
{description && (
|
||||
<>
|
||||
<InfoTooltip
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>{formattedPrice}</span>
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</h5>
|
||||
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 self-end text-sm">
|
||||
<span>{formattedPrice}</span>
|
||||
</div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,74 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { InfoTooltip } from '@/packages/shared/src/components/ui/info-tooltip';
|
||||
|
||||
import { Button } from '@kit/ui/shadcn/button';
|
||||
import {
|
||||
Card,
|
||||
CardDescription,
|
||||
CardFooter,
|
||||
CardHeader,
|
||||
} from '@kit/ui/shadcn/card';
|
||||
import { Skeleton } from '@kit/ui/skeleton';
|
||||
|
||||
const RecommendationsSkeleton = () => {
|
||||
const emptyData = [
|
||||
{
|
||||
title: '1',
|
||||
description: '',
|
||||
subtitle: '',
|
||||
variant: { id: '' },
|
||||
price: 1,
|
||||
},
|
||||
{
|
||||
title: '2',
|
||||
description: '',
|
||||
subtitle: '',
|
||||
variant: { id: '' },
|
||||
price: 1,
|
||||
},
|
||||
];
|
||||
return (
|
||||
<div className="xs:grid-cols-3 mt-4 grid gap-6">
|
||||
{emptyData.map(({ title, description, subtitle }) => (
|
||||
<Skeleton key={title}>
|
||||
<Card>
|
||||
<CardHeader className="flex-row">
|
||||
<div
|
||||
className={
|
||||
'mb-6 flex size-8 items-center-safe justify-center-safe'
|
||||
}
|
||||
/>
|
||||
<div className="ml-auto flex size-8 items-center-safe justify-center-safe">
|
||||
<Button size="icon" className="px-2" />
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardFooter className="flex">
|
||||
<div className="flex flex-1 flex-col items-start">
|
||||
<h5>
|
||||
{title}
|
||||
{description && (
|
||||
<>
|
||||
{' '}
|
||||
<InfoTooltip
|
||||
content={
|
||||
<div className="flex flex-col gap-2">
|
||||
<span>{description}</span>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</h5>
|
||||
{subtitle && <CardDescription>{subtitle}</CardDescription>}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 self-end text-sm"></div>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</Skeleton>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RecommendationsSkeleton;
|
||||
Reference in New Issue
Block a user