8 Commits

Author SHA1 Message Date
615dde52e6 wip4 2025-07-10 14:52:18 +03:00
7f2c6f2374 wip4 2025-07-10 14:45:41 +03:00
6368a5b5ff wip3 2025-07-10 14:44:39 +03:00
8e82736f09 wip2 2025-07-10 14:44:06 +03:00
7b71da3825 feat(MED-122): use <Trans> instead of t() 2025-07-10 13:24:03 +03:00
ad213dd4f8 feat(MED-122): consistently format name in titlecase 2025-07-10 13:19:28 +03:00
0a0b1f0dee feat(MED-122): update current user account loader 2025-07-10 13:19:01 +03:00
bbcf0b6d83 feat(MED-122): update analysis packages page, pageheader 2025-07-10 11:36:57 +03:00
41 changed files with 644 additions and 223 deletions

View File

@@ -0,0 +1,34 @@
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';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('booking:title');
return {
title,
};
};
function BookingPage() {
return (
<>
<HomeLayoutPageHeader
title={<Trans i18nKey={'booking:title'} />}
description={<Trans i18nKey={'booking:description'} />}
/>
<PageBody>
<OrderCards />
</PageBody>
</>
);
}
export default withI18n(BookingPage);

View File

@@ -0,0 +1,5 @@
import SkeletonCartPage from '~/medusa/modules/skeletons/templates/skeleton-cart-page';
export default function Loading() {
return <SkeletonCartPage />;
}

View File

@@ -0,0 +1,21 @@
import { Metadata } from 'next';
import InteractiveLink from '~/medusa/modules/common/components/interactive-link';
export const metadata: Metadata = {
title: '404',
description: 'Something went wrong',
};
export default function NotFound() {
return (
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center">
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
<p className="text-small-regular text-ui-fg-base">
The cart you tried to access does not exist. Clear your cookies and try
again.
</p>
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
</div>
);
}

View File

@@ -0,0 +1,59 @@
import { PageBody, PageHeader } from '@/packages/ui/src/makerkit/page';
import { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { retrieveCart } from '~/medusa/lib/data/cart';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import CartTemplate from '~/medusa/modules/cart/templates';
export const metadata: Metadata = {
title: 'Cart',
description: 'View your cart',
};
export default async function Cart() {
const cart2 = await retrieveCart().catch((error) => {
console.error(error);
return notFound();
});
const customer = await retrieveCustomer();
const cart: NonNullable<typeof cart2> = {
items: [
{
id: '1',
quantity: 1,
cart: cart2!,
item_total: 100,
item_subtotal: 100,
item_tax_total: 100,
original_total: 100,
original_subtotal: 100,
original_tax_total: 100,
total: 100,
subtotal: 100,
tax_total: 100,
title: 'Test',
requires_shipping: true,
discount_total: 0,
discount_tax_total: 0,
metadata: {},
created_at: new Date(),
is_discountable: true,
is_tax_inclusive: true,
unit_price: 100,
cart_id: '1',
},
],
}
return (
<PageBody>
<PageHeader title={`Ostukorv`} description={`Vali kalendrist sobiv kuupäev ja broneeri endale vastuvõtuaeg.`} />
<CartTemplate cart={cart} customer={customer} />
</PageBody>
);
}

View File

@@ -0,0 +1,42 @@
import { Scale } from 'lucide-react';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n';
import { Button } from '@kit/ui/button';
import SelectAnalysisPackages from '@/components/select-analysis-packages';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import ComparePackagesModal from '../../_components/compare-packages-modal';
export const generateMetadata = async () => {
const i18n = await createI18nServerInstance();
const title = i18n.t('order-analysis-package:title');
return {
title,
};
};
async function OrderAnalysisPackagePage() {
return (
<PageBody>
<div className="space-y-3 text-center">
<h3>
<Trans i18nKey={'marketing:selectPackage'} />
</h3>
<ComparePackagesModal
triggerElement={
<Button variant="secondary" className="gap-2">
<Trans i18nKey={'marketing:comparePackages'} />
<Scale className="size-4 stroke-[1.5px]" />
</Button>
}
/>
</div>
<SelectAnalysisPackages />
</PageBody>
);
}
export default withI18n(OrderAnalysisPackagePage);

View File

@@ -1,15 +1,13 @@
import { use } from 'react'; import { redirect } from 'next/navigation';
import { PageBody } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { PageBody, PageHeader } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans';
import { toTitleCase } from '@/lib/utils';
import Dashboard from '../_components/dashboard'; import Dashboard from '../_components/dashboard';
// local imports import { loadCurrentUserAccount } from '../_lib/server/load-user-account';
import { HomeLayoutPageHeader } from '../_components/home-page-header';
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
export const generateMetadata = async () => { export const generateMetadata = async () => {
const i18n = await createI18nServerInstance(); const i18n = await createI18nServerInstance();
@@ -20,15 +18,24 @@ export const generateMetadata = async () => {
}; };
}; };
function UserHomePage() { async function UserHomePage() {
const { tempVisibleAccounts } = use(loadUserWorkspace()); const account = await loadCurrentUserAccount();
if (!account) {
redirect('/');
}
return ( return (
<> <>
<HomeLayoutPageHeader <PageHeader title={
title={<Trans i18nKey={'common:routes.home'} />} <>
description={<></>} <Trans i18nKey={'common:welcome'} />
{account.name ? `, ${toTitleCase(account.name)}` : ''}
</>
}
description={
<Trans i18nKey={'dashboard:recentlyCheckedDescription'} />
}
/> />
<PageBody> <PageBody>
<Dashboard /> <Dashboard />
</PageBody> </PageBody>

View File

@@ -18,12 +18,10 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from '@kit/ui/table'; } from '@kit/ui/table';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import { PackageHeader } from '@/components/package-header';
import { PackageHeader } from '../../../../components/package-header'; import { InfoTooltip } from '@/components/ui/info-tooltip';
import { InfoTooltip } from '../../../../components/ui/info-tooltip';
const dummyCards = [ const dummyCards = [
{ {

View File

@@ -1,7 +1,7 @@
'use client'; 'use client';
import Link from 'next/link';
import { InfoTooltip } from '@/components/ui/info-tooltip'; import { InfoTooltip } from '@/components/ui/info-tooltip';
import { toTitleCase } from '@/lib/utils';
import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons'; import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons';
import { import {
Activity, Activity,
@@ -15,8 +15,6 @@ import {
User, User,
} from 'lucide-react'; } from 'lucide-react';
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import { import {
Card, Card,
@@ -26,7 +24,6 @@ import {
CardHeader, CardHeader,
CardProps, CardProps,
} from '@kit/ui/card'; } from '@kit/ui/card';
import { PageDescription } from '@kit/ui/page';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
@@ -107,6 +104,7 @@ const dummyRecommendations = [
tooltipContent: 'Selgitus', tooltipContent: 'Selgitus',
price: '20,00 €', price: '20,00 €',
buttonText: 'Telli', buttonText: 'Telli',
href: '/home/booking',
}, },
{ {
icon: <BlendingModeIcon className="size-4" />, icon: <BlendingModeIcon className="size-4" />,
@@ -115,6 +113,7 @@ const dummyRecommendations = [
tooltipContent: 'Selgitus', tooltipContent: 'Selgitus',
description: 'LDL-Kolesterool', description: 'LDL-Kolesterool',
buttonText: 'Broneeri', buttonText: 'Broneeri',
href: '/home/booking',
}, },
{ {
icon: <Droplets />, icon: <Droplets />,
@@ -124,24 +123,13 @@ const dummyRecommendations = [
description: 'Score-Risk 2', description: 'Score-Risk 2',
price: '20,00 €', price: '20,00 €',
buttonText: 'Telli', buttonText: 'Telli',
href: '/home/booking',
}, },
]; ];
export default function Dashboard() { export default function Dashboard() {
const userWorkspace = useUserWorkspace();
const account = usePersonalAccountData(userWorkspace.user.id);
return ( return (
<> <>
<div>
<h4>
<Trans i18nKey={'common:welcome'} />
{account?.data?.name ? `, ${toTitleCase(account.data.name)}` : ''}
</h4>
<PageDescription>
<Trans i18nKey={'dashboard:recentlyCheckedDescription'} />:
</PageDescription>
</div>
<div className="grid auto-rows-fr grid-cols-5 gap-3"> <div className="grid auto-rows-fr grid-cols-5 gap-3">
{dummyCards.map( {dummyCards.map(
({ ({
@@ -196,6 +184,7 @@ export default function Dashboard() {
tooltipContent, tooltipContent,
price, price,
buttonText, buttonText,
href,
}, },
index, index,
) => { ) => {
@@ -222,9 +211,17 @@ export default function Dashboard() {
</div> </div>
<div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4"> <div className="grid w-36 auto-rows-fr grid-cols-2 items-center gap-4">
<p className="text-sm font-medium"> {price}</p> <p className="text-sm font-medium"> {price}</p>
<Button size="sm" variant="secondary"> {href ? (
{buttonText} <Link href={href}>
</Button> <Button size="sm" variant="secondary">
{buttonText}
</Button>
</Link>
) : (
<Button size="sm" variant="secondary">
{buttonText}
</Button>
)}
</div> </div>
</div> </div>
); );

View File

@@ -1,16 +1,15 @@
import Link from 'next/link';
import { ShoppingCart } from 'lucide-react';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { AppLogo } from '~/components/app-logo'; import { AppLogo } from '~/components/app-logo';
import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container';
import { Search } from '~/components/ui/search'; import { Search } from '~/components/ui/search';
import { SIDEBAR_WIDTH_PROPERTY } from '@/packages/ui/src/shadcn/constants';
import { Button } from '@kit/ui/button';
import { SIDEBAR_WIDTH_PROPERTY } from '../../../../packages/ui/src/shadcn/constants';
// home imports
import { UserNotifications } from '../_components/user-notifications'; import { UserNotifications } from '../_components/user-notifications';
import { type UserWorkspace } from '../_lib/server/load-user-workspace'; import { type UserWorkspace } from '../_lib/server/load-user-workspace';
import { Button } from '@kit/ui/button';
import { ShoppingCart } from 'lucide-react';
export function HomeMenuNavigation(props: { workspace: UserWorkspace }) { export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
const { workspace, user, accounts } = props.workspace; const { workspace, user, accounts } = props.workspace;
@@ -30,10 +29,12 @@ export function HomeMenuNavigation(props: { workspace: UserWorkspace }) {
<Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant='ghost'> <Button className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' variant='ghost'>
<span className='flex items-center text-nowrap'> 231,89</span> <span className='flex items-center text-nowrap'> 231,89</span>
</Button> </Button>
<Button variant="ghost" className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' > <Link href='/home/cart'>
<ShoppingCart className="stroke-[1.5px]" /> <Button variant="ghost" className='relative px-4 py-2 h-10 border-1 mr-0 cursor-pointer' >
<Trans i18nKey="common:shoppingCart" /> (0) <ShoppingCart className="stroke-[1.5px]" />
</Button> <Trans i18nKey="common:shoppingCart" /> (0)
</Button>
</Link>
<UserNotifications userId={user.id} /> <UserNotifications userId={user.id} />
<div> <div>

View File

@@ -7,6 +7,6 @@ export function HomeLayoutPageHeader(
}>, }>,
) { ) {
return ( return (
<PageHeader description={props.description}>{props.children}</PageHeader> <PageHeader description={props.description} title={props.title}>{props.children}</PageHeader>
); );
} }

View File

@@ -0,0 +1,77 @@
"use client";
import { ChevronRight, HeartPulse } from 'lucide-react';
import Link from 'next/link';
import { Button } from '@kit/ui/button';
import {
Card,
CardHeader,
CardDescription,
CardProps,
CardFooter,
} 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',
},
];
export default function OrderCards() {
return (
<div className="grid grid-cols-3 gap-6 mt-4">
{dummyCards.map(({
title,
description,
icon,
cardVariant,
descriptionColor,
iconBg,
}) => (
<Card
key={title}
variant={cardVariant}
className="flex flex-col justify-between"
>
<CardHeader className="items-end-safe">
<div
className={cn(
'flex size-8 items-center-safe justify-center-safe rounded-full text-white',
iconBg,
)}
>
{icon}
</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>
</Card>
))}
</div>
);
}

View File

@@ -2,6 +2,7 @@ import { cache } from 'react';
import { createAccountsApi } from '@kit/accounts/api'; import { createAccountsApi } from '@kit/accounts/api';
import { getSupabaseServerClient } from '@kit/supabase/server-client'; import { getSupabaseServerClient } from '@kit/supabase/server-client';
import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>; export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>;
@@ -13,6 +14,13 @@ export type UserAccount = Awaited<ReturnType<typeof loadUserAccount>>;
*/ */
export const loadUserAccount = cache(accountLoader); export const loadUserAccount = cache(accountLoader);
export async function loadCurrentUserAccount() {
const user = await requireUserInServerComponent();
return user?.identities?.[0]?.id
? await loadUserAccount(user?.identities?.[0]?.id)
: null;
}
async function accountLoader(accountId: string) { async function accountLoader(accountId: string) {
const client = getSupabaseServerClient(); const client = getSupabaseServerClient();
const api = createAccountsApi(client); const api = createAccountsApi(client);

View File

@@ -1,6 +1,6 @@
import { requireUserInServerComponent } from '../../lib/server/require-user-in-server-component'; import { requireUserInServerComponent } from '@/lib/server/require-user-in-server-component';
import ConsentDialog from './(user)/_components/consent-dialog'; import ConsentDialog from './(user)/_components/consent-dialog';
import { loadUserAccount } from './(user)/_lib/server/load-user-account'; import { loadCurrentUserAccount } from './(user)/_lib/server/load-user-account';
export default async function HomeLayout({ export default async function HomeLayout({
children, children,
@@ -8,9 +8,7 @@ export default async function HomeLayout({
children: React.ReactNode; children: React.ReactNode;
}) { }) {
const user = await requireUserInServerComponent(); const user = await requireUserInServerComponent();
const account = user?.identities?.[0]?.id const account = await loadCurrentUserAccount()
? await loadUserAccount(user?.identities?.[0]?.id)
: null;
if (account && account?.has_consent_anonymized_company_statistics === null) { if (account && account?.has_consent_anonymized_company_statistics === null) {
return ( return (

View File

@@ -68,13 +68,11 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
const invitation = await api.getInvitation(adminClient, token); const invitation = await api.getInvitation(adminClient, token);
// the invitation is not found or expired // the invitation is not found or expired
if (!invitation) {
return ( return (
<AuthLayoutShell Logo={AppLogo}> <AuthLayoutShell Logo={AppLogo}>
<InviteNotFoundOrExpired /> <InviteNotFoundOrExpired />
</AuthLayoutShell> </AuthLayoutShell>
); );
}
// we need to verify the user isn't already in the account // we need to verify the user isn't already in the account
// we do so by checking if the user can read the account // we do so by checking if the user can read the account

View File

@@ -1,24 +1,16 @@
import Image from 'next/image';
import Link from 'next/link'; import Link from 'next/link';
import { CaretRightIcon } from '@radix-ui/react-icons'; import { CaretRightIcon } from '@radix-ui/react-icons';
import { Scale } from 'lucide-react'; import { Scale } from 'lucide-react';
import { Trans } from '@kit/ui/trans';
import { Button } from '@kit/ui/button'; import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
} from '@kit/ui/card';
import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
import { withI18n } from '~/lib/i18n/with-i18n'; import { withI18n } from '~/lib/i18n/with-i18n';
import SelectAnalysisPackages from '@/components/select-analysis-packages';
import { MedReportLogo } from '../../components/med-report-logo'; import { MedReportLogo } from '../../components/med-report-logo';
import { PackageHeader } from '../../components/package-header';
import { ButtonTooltip } from '../../components/ui/button-tooltip';
import pathsConfig from '../../config/paths.config'; import pathsConfig from '../../config/paths.config';
import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal'; import ComparePackagesModal from '../home/(user)/_components/compare-packages-modal';
@@ -30,100 +22,30 @@ export const generateMetadata = async () => {
}; };
}; };
const dummyCards = [
{
titleKey: 'product:standard.label',
price: 40,
nrOfAnalyses: 4,
tagColor: 'bg-cyan',
descriptionKey: 'marketing:standard.description',
},
{
titleKey: 'product:standardPlus.label',
price: 85,
nrOfAnalyses: 10,
tagColor: 'bg-warning',
descriptionKey: 'product:standardPlus.description',
},
{
titleKey: 'product:premium.label',
price: 140,
nrOfAnalyses: '12+',
tagColor: 'bg-purple',
descriptionKey: 'product:premium.description',
},
];
async function SelectPackagePage() { async function SelectPackagePage() {
const { t, language } = await createI18nServerInstance();
return ( return (
<div className="container mx-auto my-24 flex flex-col items-center space-y-12"> <div className="container mx-auto my-24 flex flex-col items-center space-y-12">
<MedReportLogo /> <MedReportLogo />
<div className="space-y-3 text-center"> <div className="space-y-3 text-center">
<h3>{t('marketing:selectPackage')}</h3> <h3>
<Trans i18nKey={'marketing:selectPackage'} />
</h3>
<ComparePackagesModal <ComparePackagesModal
triggerElement={ triggerElement={
<Button variant="secondary" className="gap-2"> <Button variant="secondary" className="gap-2">
{t('marketing:comparePackages')} <Trans i18nKey={'marketing:comparePackages'} />
<Scale className="size-4 stroke-[1.5px]" /> <Scale className="size-4 stroke-[1.5px]" />
</Button> </Button>
} }
/> />
</div> </div>
<div className="grid grid-cols-3 gap-6"> <SelectAnalysisPackages />
{dummyCards.map( <Link href={pathsConfig.app.home}>
( <Button variant="secondary" className="align-center">
{ titleKey, price, nrOfAnalyses, tagColor, descriptionKey }, <Trans i18nKey={'marketing:notInterestedInAudit'} />{' '}
index, <CaretRightIcon className="size-4" />
) => { </Button>
return ( </Link>
<Card key={index}>
<CardHeader className="relative">
<ButtonTooltip
content="Content pending"
className="absolute top-5 right-5 z-10"
/>
<Image
src="/assets/card-image.png"
alt="background"
width={326}
height={195}
className="max-h-48 w-full opacity-10"
/>
</CardHeader>
<CardContent className="space-y-1 text-center">
<PackageHeader
title={t(titleKey)}
tagColor={tagColor}
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
language={language}
price={price}
/>
<CardDescription>{t(descriptionKey)}</CardDescription>
</CardContent>
<CardFooter>
<Button className="w-full">
{t('marketing:selectThisPackage')}
</Button>
</CardFooter>
</Card>
);
},
)}
<div className="col-span-3 grid grid-cols-subgrid">
<div className="col-start-2 justify-self-center-safe">
<Link href={pathsConfig.app.home}>
<Button variant="secondary" className="align-center">
{t('marketing:notInterestedInAudit')}{' '}
<CaretRightIcon className="size-4" />
</Button>
</Link>
</div>
</div>
</div>
</div> </div>
); );
} }

View File

@@ -1,46 +1,117 @@
import { Metadata } from 'next'; import { use } from 'react';
import { StoreCartShippingOption } from '@medusajs/types'; import { cookies } from 'next/headers';
import { listCartOptions, retrieveCart } from '~/medusa/lib/data/cart'; import { z } from 'zod';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import { getBaseURL } from '~/medusa/lib/util/env';
import CartMismatchBanner from '~/medusa/modules/layout/components/cart-mismatch-banner';
import Footer from '~/medusa/modules/layout/templates/footer';
import Nav from '~/medusa/modules/layout/templates/nav';
import FreeShippingPriceNudge from '~/medusa/modules/shipping/components/free-shipping-price-nudge';
export const metadata: Metadata = { import { UserWorkspaceContextProvider } from '@kit/accounts/components';
metadataBase: new URL(getBaseURL()), import { Page, PageMobileNavigation, PageNavigation } from '@kit/ui/page';
}; import { SidebarProvider } from '@kit/ui/shadcn-sidebar';
export default async function PageLayout(props: { children: React.ReactNode }) { import { AppLogo } from '~/components/app-logo';
const customer = await retrieveCustomer(); import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config';
const cart = await retrieveCart(); import { withI18n } from '~/lib/i18n/with-i18n';
let shippingOptions: StoreCartShippingOption[] = []; import { loadUserWorkspace } from '@/app/home/(user)/_lib/server/load-user-workspace';
import { HomeSidebar } from '@/app/home/(user)/_components/home-sidebar';
import { HomeMenuNavigation } from '@/app/home/(user)/_components/home-menu-navigation';
import { HomeMobileNavigation } from '@/app/home/(user)/_components/home-mobile-navigation';
if (cart) { function UserHomeLayout({ children }: React.PropsWithChildren) {
const { shipping_options } = await listCartOptions(); const state = use(getLayoutState());
shippingOptions = shipping_options; if (state.style === 'sidebar') {
return <SidebarLayout>{children}</SidebarLayout>;
} }
return <HeaderLayout>{children}</HeaderLayout>;
}
export default withI18n(UserHomeLayout);
function SidebarLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
const state = use(getLayoutState());
return (
<UserWorkspaceContextProvider value={workspace}>
<SidebarProvider defaultOpen={state.open}>
<Page style={'sidebar'}>
<PageNavigation>
<HomeSidebar />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} />
</PageMobileNavigation>
{children}
</Page>
</SidebarProvider>
</UserWorkspaceContextProvider>
);
}
function HeaderLayout({ children }: React.PropsWithChildren) {
const workspace = use(loadUserWorkspace());
return (
<UserWorkspaceContextProvider value={workspace}>
<Page style={'header'}>
<PageNavigation>
<HomeMenuNavigation workspace={workspace} />
</PageNavigation>
<PageMobileNavigation className={'flex items-center justify-between'}>
<MobileNavigation workspace={workspace} />
</PageMobileNavigation>
<SidebarProvider defaultOpen>
<Page style={'sidebar'}>
<PageNavigation>
<HomeSidebar />
</PageNavigation>
{children}
</Page>
</SidebarProvider>
</Page>
</UserWorkspaceContextProvider>
);
}
function MobileNavigation({
workspace,
}: {
workspace: Awaited<ReturnType<typeof loadUserWorkspace>>;
}) {
return ( return (
<> <>
<Nav /> <AppLogo />
{customer && cart && (
<CartMismatchBanner customer={customer} cart={cart} />
)}
{cart && ( <HomeMobileNavigation workspace={workspace} />
<FreeShippingPriceNudge
variant="popup"
cart={cart}
shippingOptions={shippingOptions}
/>
)}
{props.children}
<Footer />
</> </>
); );
} }
async function getLayoutState() {
const cookieStore = await cookies();
const LayoutStyleSchema = z.enum(['sidebar', 'header', 'custom']);
const layoutStyleCookie = cookieStore.get('layout-style');
const sidebarOpenCookie = cookieStore.get('sidebar:state');
const sidebarOpen = sidebarOpenCookie
? sidebarOpenCookie.value === 'false'
: !personalAccountNavigationConfig.sidebarCollapsed;
const parsedStyle = LayoutStyleSchema.safeParse(layoutStyleCookie?.value);
const style = parsedStyle.success
? parsedStyle.data
: personalAccountNavigationConfig.style;
return {
open: sidebarOpen,
style,
};
}

View File

@@ -0,0 +1,46 @@
import { Metadata } from 'next';
import { StoreCartShippingOption } from '@medusajs/types';
import { listCartOptions, retrieveCart } from '~/medusa/lib/data/cart';
import { retrieveCustomer } from '~/medusa/lib/data/customer';
import { getBaseURL } from '~/medusa/lib/util/env';
import CartMismatchBanner from '~/medusa/modules/layout/components/cart-mismatch-banner';
import Footer from '~/medusa/modules/layout/templates/footer';
import Nav from '~/medusa/modules/layout/templates/nav';
import FreeShippingPriceNudge from '~/medusa/modules/shipping/components/free-shipping-price-nudge';
export const metadata: Metadata = {
metadataBase: new URL(getBaseURL()),
};
export default async function PageLayout(props: { children: React.ReactNode }) {
const customer = await retrieveCustomer();
const cart = await retrieveCart();
let shippingOptions: StoreCartShippingOption[] = [];
if (cart) {
const { shipping_options } = await listCartOptions();
shippingOptions = shipping_options;
}
return (
<>
<Nav />
{customer && cart && (
<CartMismatchBanner customer={customer} cart={cart} />
)}
{cart && (
<FreeShippingPriceNudge
variant="popup"
cart={cart}
shippingOptions={shippingOptions}
/>
)}
{props.children}
<Footer />
</>
);
}

View File

@@ -0,0 +1,108 @@
'use client';
import Image from 'next/image';
import { useTranslation } from 'react-i18next';
import { Button } from '@kit/ui/button';
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
} from '@kit/ui/card';
import { Trans } from '@kit/ui/trans';
import { PackageHeader } from './package-header';
import { ButtonTooltip } from './ui/button-tooltip';
export interface IAnalysisPackage {
titleKey: string;
price: number;
nrOfAnalyses: number | string;
tagColor: string;
descriptionKey: string;
}
const analysisPackages = [
{
titleKey: 'product:standard.label',
price: 40,
nrOfAnalyses: 4,
tagColor: 'bg-cyan',
descriptionKey: 'marketing:standard.description',
},
{
titleKey: 'product:standardPlus.label',
price: 85,
nrOfAnalyses: 10,
tagColor: 'bg-warning',
descriptionKey: 'product:standardPlus.description',
},
{
titleKey: 'product:premium.label',
price: 140,
nrOfAnalyses: '12+',
tagColor: 'bg-purple',
descriptionKey: 'product:premium.description',
},
] satisfies IAnalysisPackage[];
export default function SelectAnalysisPackages() {
const {
t,
i18n: { language },
} = useTranslation();
return (
<div className="grid grid-cols-3 gap-6">
{analysisPackages.length > 0 ? analysisPackages.map(
(
{ titleKey, price, nrOfAnalyses, tagColor, descriptionKey },
index,
) => {
return (
<Card key={index}>
<CardHeader className="relative">
<ButtonTooltip
content="Content pending"
className="absolute top-5 right-5 z-10"
/>
<Image
src="/assets/card-image.png"
alt="background"
width={326}
height={195}
className="max-h-48 w-full opacity-10"
/>
</CardHeader>
<CardContent className="space-y-1 text-center">
<PackageHeader
title={t(titleKey)}
tagColor={tagColor}
analysesNr={t('product:nrOfAnalyses', { nr: nrOfAnalyses })}
language={language}
price={price}
/>
<CardDescription>
<Trans i18nKey={descriptionKey} />
</CardDescription>
</CardContent>
<CardFooter>
<Button className="w-full">
<Trans i18nKey='order-analysis-package:selectThisPackage' />
</Button>
</CardFooter>
</Card>
);
},
) : (
<h4>
<Trans i18nKey='order-analysis-package:noPackagesAvailable' />
</h4>
)}
</div>
);
}

View File

@@ -57,11 +57,11 @@ const pathsConfig = PathsSchema.parse({
accountBillingReturn: `/home/[account]/billing/return`, accountBillingReturn: `/home/[account]/billing/return`,
joinTeam: '/join', joinTeam: '/join',
selectPackage: '/select-package', selectPackage: '/select-package',
booking: '/home/booking',
orderAnalysisPackage: '/home/order-analysis-package',
// these routes are added as placeholders and can be changed when the pages are added // these routes are added as placeholders and can be changed when the pages are added
booking: '/booking',
myOrders: '/my-orders', myOrders: '/my-orders',
analysisResults: '/analysis-results', analysisResults: '/analysis-results',
orderAnalysisPackage: '/order-analysis-package',
orderAnalysis: '/order-analysis', orderAnalysis: '/order-analysis',
orderHealthAnalysis: '/order-health-analysis', orderHealthAnalysis: '/order-health-analysis',
}, },

View File

@@ -34,6 +34,8 @@ export const defaultI18nNamespaces = [
'marketing', 'marketing',
'dashboard', 'dashboard',
'product', 'product',
'booking',
'order-analysis-package',
]; ];
/** /**

View File

@@ -26,9 +26,10 @@ import { SubMenuModeToggle } from '@kit/ui/mode-toggle';
import { ProfileAvatar } from '@kit/ui/profile-avatar'; import { ProfileAvatar } from '@kit/ui/profile-avatar';
import { Trans } from '@kit/ui/trans'; import { Trans } from '@kit/ui/trans';
import { cn } from '@kit/ui/utils'; import { cn } from '@kit/ui/utils';
import { usePersonalAccountData } from '../hooks/use-personal-account-data'; import { usePersonalAccountData } from '../hooks/use-personal-account-data';
import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar'; import { Avatar, AvatarFallback, AvatarImage } from '@kit/ui/avatar';
import { toTitleCase } from '~/lib/utils';
const PERSONAL_ACCOUNT_SLUG = 'personal'; const PERSONAL_ACCOUNT_SLUG = 'personal';
@@ -124,7 +125,7 @@ export function PersonalAccountDropdown({
data-test={'account-dropdown-display-name'} data-test={'account-dropdown-display-name'}
className={'truncate text-sm'} className={'truncate text-sm'}
> >
{displayName} {toTitleCase(displayName)}
</span> </span>
</div> </div>

View File

@@ -117,6 +117,7 @@ class AccountsApi {
*/ */
async getSubscription(accountId: string) { async getSubscription(accountId: string) {
const response = await this.client const response = await this.client
.schema('medreport')
.from('subscriptions') .from('subscriptions')
.select('*, items: subscription_items !inner (*)') .select('*, items: subscription_items !inner (*)')
.eq('account_id', accountId) .eq('account_id', accountId)

View File

@@ -1,7 +1,10 @@
{ {
"extends": "@kit/tsconfig/base.json", "extends": "@kit/tsconfig/base.json",
"compilerOptions": { "compilerOptions": {
"tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json",
"paths": {
"~/lib/utils": ["../../../lib/utils.ts"]
}
}, },
"include": ["*.ts", "*.tsx", "src"], "include": ["*.ts", "*.tsx", "src"],
"exclude": ["node_modules"] "exclude": ["node_modules"]

View File

@@ -76,7 +76,6 @@ export function PasswordSignUpForm({
data-test={'personal-code-input'} data-test={'personal-code-input'}
required required
type="text" type="text"
placeholder={t('personalCodePlaceholder')}
{...field} {...field}
/> />
</FormControl> </FormControl>

View File

@@ -44,7 +44,7 @@ export const getRegion = async (countryCode: string) => {
} }
const regions = await listRegions() const regions = await listRegions()
console.log("regions", regions)
if (!regions) { if (!regions) {
return null return null
} }
@@ -57,7 +57,7 @@ export const getRegion = async (countryCode: string) => {
const region = countryCode const region = countryCode
? regionMap.get(countryCode) ? regionMap.get(countryCode)
: regionMap.get("us") : regionMap.get("et")
return region return region
} catch (e: any) { } catch (e: any) {

View File

@@ -1,23 +1,6 @@
import { Heading, Text } from "@medusajs/ui"
import InteractiveLink from "@modules/common/components/interactive-link"
const EmptyCartMessage = () => { const EmptyCartMessage = () => {
return ( return (
<div className="py-48 px-2 flex flex-col justify-center items-start" data-testid="empty-cart-message"> <div className="py-48 px-2 flex flex-col justify-center items-start" data-testid="empty-cart-message">
<Heading
level="h1"
className="flex flex-row text-3xl-regular gap-x-2 items-baseline"
>
Cart
</Heading>
<Text className="text-base-regular mt-4 mb-6 max-w-[32rem]">
You don&apos;t have anything in your cart. Let&apos;s change that, use
the link below to start browsing our products.
</Text>
<div>
<InteractiveLink href="/store">Explore products</InteractiveLink>
</div>
</div> </div>
) )
} }

View File

@@ -18,12 +18,12 @@ const CartTemplate = ({
{cart?.items?.length ? ( {cart?.items?.length ? (
<div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40"> <div className="grid grid-cols-1 small:grid-cols-[1fr_360px] gap-x-40">
<div className="flex flex-col bg-white py-6 gap-y-6"> <div className="flex flex-col bg-white py-6 gap-y-6">
{!customer && ( {/* {!customer && (
<> <>
<SignInPrompt /> <SignInPrompt />
<Divider /> <Divider />
</> </>
)} )} */}
<ItemsTemplate cart={cart} /> <ItemsTemplate cart={cart} />
</div> </div>
<div className="relative"> <div className="relative">

View File

@@ -13,9 +13,9 @@ const ItemsTemplate = ({ cart }: ItemsTemplateProps) => {
const items = cart?.items const items = cart?.items
return ( return (
<div> <div>
<div className="pb-3 flex items-center"> {/* <div className="pb-3 flex items-center">
<Heading className="text-[2rem] leading-[2.75rem]">Cart</Heading> <Heading className="text-[2rem] leading-[2.75rem]">Cart</Heading>
</div> </div> */}
<Table> <Table>
<Table.Header className="border-t-0"> <Table.Header className="border-t-0">
<Table.Row className="text-ui-fg-subtle txt-medium-plus"> <Table.Row className="text-ui-fg-subtle txt-medium-plus">

View File

@@ -14,7 +14,7 @@ export function formatCurrency(params: {
locale: string; locale: string;
value: string | number; value: string | number;
}) { }) {
const [lang, region] = params.locale.split('-'); const [lang, region] = (params.locale ?? 'et-ET').split('-');
return new Intl.NumberFormat(region ?? lang, { return new Intl.NumberFormat(region ?? lang, {
style: 'currency', style: 'currency',

View File

@@ -133,13 +133,13 @@ export function PageDescription(props: React.PropsWithChildren) {
export function PageTitle(props: React.PropsWithChildren) { export function PageTitle(props: React.PropsWithChildren) {
return ( return (
<h1 <h4
className={ className={
'font-heading text-base leading-none font-bold tracking-tight dark:text-white' 'font-heading leading-none font-bold tracking-tight dark:text-white'
} }
> >
{props.children} {props.children}
</h1> </h4>
); );
} }
@@ -167,6 +167,10 @@ export function PageHeader({
)} )}
> >
<div className={'flex flex-col gap-y-2'}> <div className={'flex flex-col gap-y-2'}>
<If condition={title}>
<PageTitle>{title}</PageTitle>
</If>
<div className="flex items-center gap-x-2.5"> <div className="flex items-center gap-x-2.5">
{displaySidebarTrigger ? ( {displaySidebarTrigger ? (
<SidebarTrigger className="text-muted-foreground hover:text-secondary-foreground hidden h-4.5 w-4.5 cursor-pointer lg:inline-flex" /> <SidebarTrigger className="text-muted-foreground hover:text-secondary-foreground hidden h-4.5 w-4.5 cursor-pointer lg:inline-flex" />
@@ -183,10 +187,6 @@ export function PageHeader({
<PageDescription>{description}</PageDescription> <PageDescription>{description}</PageDescription>
</If> </If>
</div> </div>
<If condition={title}>
<PageTitle>{title}</PageTitle>
</If>
</div> </div>
{children} {children}

View File

@@ -0,0 +1,8 @@
{
"title": "Select service",
"description": "Select the appropriate service or package according to your health needs or goals.",
"analysisPackages": {
"title": "Analysis packages",
"description": "Get to know the personal analysis packages and order"
}
}

View File

@@ -37,8 +37,5 @@
"footerDescription": "Here you can add a description about your company or product", "footerDescription": "Here you can add a description about your company or product",
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.", "copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
"heroSubtitle": "A simple, convenient, and quick overview of your health condition", "heroSubtitle": "A simple, convenient, and quick overview of your health condition",
"selectPackage": "Select package",
"selectThisPackage": "Select this package",
"comparePackages": "Compare packages",
"notInterestedInAudit": "Currently not interested in a health audit" "notInterestedInAudit": "Currently not interested in a health audit"
} }

View File

@@ -0,0 +1,7 @@
{
"title": "Select analysis package",
"noPackagesAvailable": "No packages available",
"selectThisPackage": "Select this package",
"selectPackage": "Select package",
"comparePackages": "Compare packages"
}

View File

@@ -0,0 +1,8 @@
{
"title": "Vali teenus",
"description": "Vali sobiv teenus või pakett vastavalt oma tervisemurele või -eesmärgile.",
"analysisPackages": {
"title": "Analüüside paketid",
"description": "Tutvu personaalsete analüüsi pakettidega ja telli"
}
}

View File

@@ -61,7 +61,7 @@
"search": "Otsi{{end}}", "search": "Otsi{{end}}",
"myActions": "Minu toimingud", "myActions": "Minu toimingud",
"healthPackageComparison": { "healthPackageComparison": {
"label": "Tervisepakketide võrdlus", "label": "Tervisepakettide võrdlus",
"description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde." "description": "Alljärgnevalt on antud eelinfo (sugu, vanus ja kehamassiindeksi) põhjal tehtud personalne terviseauditi valik. Tabelis on võimalik soovitatud terviseuuringute paketile lisada üksikuid uuringuid juurde."
}, },
"routes": { "routes": {

View File

@@ -37,8 +37,5 @@
"footerDescription": "Here you can add a description about your company or product", "footerDescription": "Here you can add a description about your company or product",
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.", "copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
"heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest", "heroSubtitle": "Lihtne, mugav ja kiire ülevaade oma tervisest",
"selectPackage": "Vali pakett",
"selectThisPackage": "Vali see pakett",
"comparePackages": "Võrdle pakette",
"notInterestedInAudit": "Ei soovi hetkel terviseauditit" "notInterestedInAudit": "Ei soovi hetkel terviseauditit"
} }

View File

@@ -0,0 +1,7 @@
{
"title": "Vali analüüsi pakett",
"noPackagesAvailable": "Teenuste loetelu ei leitud, proovi hiljem uuesti",
"selectThisPackage": "Vali see pakett",
"selectPackage": "Vali pakett",
"comparePackages": "Võrdle pakette"
}

View File

@@ -0,0 +1,8 @@
{
"title": "Select service",
"description": "Select the appropriate service or package according to your health needs or goals.",
"analysisPackages": {
"title": "Analysis packages",
"description": "Get to know the personal analysis packages and order"
}
}

View File

@@ -37,8 +37,5 @@
"footerDescription": "Here you can add a description about your company or product", "footerDescription": "Here you can add a description about your company or product",
"copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.", "copyright": "© Copyright {{year}} {{product}}. All Rights Reserved.",
"heroSubtitle": "A simple, convenient, and quick overview of your health condition", "heroSubtitle": "A simple, convenient, and quick overview of your health condition",
"selectPackage": "Select package",
"selectThisPackage": "Select this package",
"comparePackages": "Compare packages",
"notInterestedInAudit": "Currently not interested in a health audit" "notInterestedInAudit": "Currently not interested in a health audit"
} }

View File

@@ -0,0 +1,7 @@
{
"title": "Select analysis package",
"noPackagesAvailable": "No packages available",
"selectThisPackage": "Select this package",
"selectPackage": "Select package",
"comparePackages": "Compare packages"
}

View File

@@ -89,4 +89,8 @@
.lucide { .lucide {
@apply size-4; @apply size-4;
} }
button {
cursor: pointer;
}
} }