Compare commits
12 Commits
10580fa653
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0af3823148 | ||
|
|
23b54bb4f4 | ||
|
|
c5ddccc15d | ||
|
|
023bc897c2 | ||
|
|
d9198a8a12 | ||
|
|
0b8fadb771 | ||
|
|
9371ff7710 | ||
|
|
4f36f9c037 | ||
|
|
29ff8cb512 | ||
|
|
c0a5238e19 | ||
|
|
7bf5dd8899 | ||
|
|
2e62e4b0eb |
5
.env
5
.env
@@ -51,4 +51,7 @@ LOGGER=pino
|
|||||||
NEXT_PUBLIC_DEFAULT_LOCALE=et
|
NEXT_PUBLIC_DEFAULT_LOCALE=et
|
||||||
|
|
||||||
NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom
|
NEXT_PUBLIC_TEAM_NAVIGATION_STYLE=custom
|
||||||
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
|
NEXT_PUBLIC_USER_NAVIGATION_STYLE=custom
|
||||||
|
|
||||||
|
# MEDUSA
|
||||||
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||||
@@ -16,4 +16,6 @@ EMAIL_USER= # refer to your email provider's documentation
|
|||||||
EMAIL_PASSWORD= # refer to your email provider's documentation
|
EMAIL_PASSWORD= # refer to your email provider's documentation
|
||||||
EMAIL_HOST= # refer to your email provider's documentation
|
EMAIL_HOST= # refer to your email provider's documentation
|
||||||
EMAIL_PORT= # or 465 for SSL
|
EMAIL_PORT= # or 465 for SSL
|
||||||
EMAIL_TLS= # or false for SSL (see provider documentation)
|
EMAIL_TLS= # or false for SSL (see provider documentation)
|
||||||
|
|
||||||
|
NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY=
|
||||||
@@ -39,6 +39,8 @@ pnpm clean
|
|||||||
pnpm i
|
pnpm i
|
||||||
```
|
```
|
||||||
|
|
||||||
|
if you get missing dependency error do `pnpm i --force`
|
||||||
|
|
||||||
## Adding new dependency
|
## Adding new dependency
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -71,6 +73,12 @@ To update database types run:
|
|||||||
npm run supabase:typegen:app
|
npm run supabase:typegen:app
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Medusa store
|
||||||
|
|
||||||
|
To get medusa store working you need to update the env's to your running medusa app and migrate the tables from medusa project to your supabase project
|
||||||
|
|
||||||
|
You can get `NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY` from your medusa app settings
|
||||||
|
|
||||||
## Super admin
|
## Super admin
|
||||||
|
|
||||||
To access admin pages follow these steps:
|
To access admin pages follow these steps:
|
||||||
|
|||||||
@@ -0,0 +1,89 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useRouter } from 'next/navigation';
|
||||||
|
|
||||||
|
import { SubmitButton } from '@/components/ui/submit-button';
|
||||||
|
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
|
||||||
|
import { CompanySubmitData } from '@/lib/types/company';
|
||||||
|
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
|
import { useForm } from 'react-hook-form';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { FormItem } from '@kit/ui/form';
|
||||||
|
import { Input } from '@kit/ui/input';
|
||||||
|
import { Label } from '@kit/ui/label';
|
||||||
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
|
const CompanyOfferForm = () => {
|
||||||
|
const router = useRouter();
|
||||||
|
const language = useTranslation().i18n.language;
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
formState: { isValid, isSubmitting },
|
||||||
|
} = useForm({
|
||||||
|
resolver: zodResolver(companyOfferSchema),
|
||||||
|
mode: 'onChange',
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (data: CompanySubmitData) => {
|
||||||
|
const formData = new FormData();
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (value !== undefined) formData.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
sendCompanyOfferEmail(data, language)
|
||||||
|
.then(() => router.push('/company-offer/success'))
|
||||||
|
.catch((error) => alert('error: ' + error));
|
||||||
|
} catch (err: unknown) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
console.warn('Server validation error: ' + err.message);
|
||||||
|
}
|
||||||
|
console.warn('Server validation error: ', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
noValidate
|
||||||
|
className="flex flex-col gap-6 px-6 pt-8 text-left"
|
||||||
|
>
|
||||||
|
<FormItem>
|
||||||
|
<Label>
|
||||||
|
<Trans i18nKey={'common:formField:companyName'} />
|
||||||
|
</Label>
|
||||||
|
<Input {...register('companyName')} />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Label>
|
||||||
|
<Trans i18nKey={'common:formField:contactPerson'} />
|
||||||
|
</Label>
|
||||||
|
<Input {...register('contactPerson')} />
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Label>
|
||||||
|
<Trans i18nKey={'common:formField:email'} />
|
||||||
|
</Label>
|
||||||
|
<Input type="email" {...register('email')}></Input>
|
||||||
|
</FormItem>
|
||||||
|
<FormItem>
|
||||||
|
<Label>
|
||||||
|
<Trans i18nKey={'common:formField:phone'} />
|
||||||
|
</Label>
|
||||||
|
<Input type="tel" {...register('phone')} />
|
||||||
|
</FormItem>
|
||||||
|
<SubmitButton
|
||||||
|
disabled={!isValid || isSubmitting}
|
||||||
|
pendingText="Saatmine..."
|
||||||
|
type="submit"
|
||||||
|
>
|
||||||
|
<Trans i18nKey={'account:requestCompanyAccount:button'} />
|
||||||
|
</SubmitButton>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CompanyOfferForm;
|
||||||
@@ -1,53 +1,13 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
|
|
||||||
import { MedReportLogo } from '@/components/med-report-logo';
|
import { MedReportLogo } from '@/components/med-report-logo';
|
||||||
import { SubmitButton } from '@/components/ui/submit-button';
|
import { withI18n } from '@/lib/i18n/with-i18n';
|
||||||
import { sendCompanyOfferEmail } from '@/lib/services/mailer.service';
|
|
||||||
import { CompanySubmitData } from '@/lib/types/company';
|
|
||||||
import { companyOfferSchema } from '@/lib/validations/company-offer.schema';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { useForm } from 'react-hook-form';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { FormItem } from '@kit/ui/form';
|
|
||||||
import { Input } from '@kit/ui/input';
|
|
||||||
import { Label } from '@kit/ui/label';
|
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
export default function CompanyOffer() {
|
import CompanyOfferForm from './_components/company-offer-form';
|
||||||
const router = useRouter();
|
|
||||||
const {
|
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
formState: { isValid, isSubmitting },
|
|
||||||
} = useForm({
|
|
||||||
resolver: zodResolver(companyOfferSchema),
|
|
||||||
mode: 'onChange',
|
|
||||||
});
|
|
||||||
const language = useTranslation().i18n.language;
|
|
||||||
|
|
||||||
const onSubmit = async (data: CompanySubmitData) => {
|
|
||||||
const formData = new FormData();
|
|
||||||
Object.entries(data).forEach(([key, value]) => {
|
|
||||||
if (value !== undefined) formData.append(key, value);
|
|
||||||
});
|
|
||||||
|
|
||||||
try {
|
|
||||||
sendCompanyOfferEmail(data, language)
|
|
||||||
.then(() => router.push('/company-offer/success'))
|
|
||||||
.catch((error) => alert('error: ' + error));
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof Error) {
|
|
||||||
console.warn('Server validation error: ' + err.message);
|
|
||||||
}
|
|
||||||
console.warn('Server validation error: ', err);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
|
function CompanyOffer() {
|
||||||
return (
|
return (
|
||||||
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
|
<div className="border-border flex max-w-5xl flex-row overflow-hidden rounded-3xl border">
|
||||||
<div className="flex w-1/2 flex-col px-12 py-14 text-center">
|
<div className="flex w-1/2 flex-col px-12 py-14 text-center">
|
||||||
@@ -58,45 +18,11 @@ export default function CompanyOffer() {
|
|||||||
<p className="text-muted-foreground pt-2 text-sm">
|
<p className="text-muted-foreground pt-2 text-sm">
|
||||||
<Trans i18nKey={'account:requestCompanyAccount:description'} />
|
<Trans i18nKey={'account:requestCompanyAccount:description'} />
|
||||||
</p>
|
</p>
|
||||||
<form
|
<CompanyOfferForm />
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
|
||||||
noValidate
|
|
||||||
className="flex flex-col gap-6 px-6 pt-8 text-left"
|
|
||||||
>
|
|
||||||
<FormItem>
|
|
||||||
<Label>
|
|
||||||
<Trans i18nKey={'common:formField:companyName'} />
|
|
||||||
</Label>
|
|
||||||
<Input {...register('companyName')} />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem>
|
|
||||||
<Label>
|
|
||||||
<Trans i18nKey={'common:formField:contactPerson'} />
|
|
||||||
</Label>
|
|
||||||
<Input {...register('contactPerson')} />
|
|
||||||
</FormItem>
|
|
||||||
<FormItem>
|
|
||||||
<Label>
|
|
||||||
<Trans i18nKey={'common:formField:email'} />
|
|
||||||
</Label>
|
|
||||||
<Input type="email" {...register('email')}></Input>
|
|
||||||
</FormItem>
|
|
||||||
<FormItem>
|
|
||||||
<Label>
|
|
||||||
<Trans i18nKey={'common:formField:phone'} />
|
|
||||||
</Label>
|
|
||||||
<Input type="tel" {...register('phone')} />
|
|
||||||
</FormItem>
|
|
||||||
<SubmitButton
|
|
||||||
disabled={!isValid || isSubmitting}
|
|
||||||
pendingText="Saatmine..."
|
|
||||||
type="submit"
|
|
||||||
>
|
|
||||||
<Trans i18nKey={'account:requestCompanyAccount:button'} />
|
|
||||||
</SubmitButton>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
|
<div className="w-1/2 min-w-[460px] bg-[url(/assets/med-report-logo-big.png)] bg-cover bg-center bg-no-repeat"></div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default withI18n(CompanyOffer);
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ async function accountLoader(id: string) {
|
|||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select('*, memberships: accounts_memberships (*)')
|
.select('*, memberships: accounts_memberships (*)')
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import pathsConfig from '@/config/paths.config';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
||||||
|
import { SuccessNotification } from '@kit/notifications/components';
|
||||||
|
|
||||||
|
const MembershipConfirmationNotification: React.FC<{
|
||||||
|
userId: string;
|
||||||
|
}> = ({ userId }) => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
|
const { data: accountData } = usePersonalAccountData(userId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SuccessNotification
|
||||||
|
showLogo={false}
|
||||||
|
title={t('account:membershipConfirmation:successTitle', {
|
||||||
|
firstName: accountData?.name,
|
||||||
|
lastName: accountData?.last_name,
|
||||||
|
})}
|
||||||
|
descriptionKey="account:membershipConfirmation:successDescription"
|
||||||
|
buttonProps={{
|
||||||
|
buttonTitleKey: 'account:membershipConfirmation:successButton',
|
||||||
|
href: pathsConfig.app.selectPackage,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MembershipConfirmationNotification;
|
||||||
@@ -8,4 +8,4 @@ async function SiteLayout(props: React.PropsWithChildren) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(SiteLayout);
|
export default SiteLayout;
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { redirect } from 'next/navigation';
|
import { redirect } from 'next/navigation';
|
||||||
|
|
||||||
import pathsConfig from '@/config/paths.config';
|
import pathsConfig from '@/config/paths.config';
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
|
|
||||||
import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data';
|
|
||||||
import { SuccessNotification } from '@kit/notifications/components';
|
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
|
||||||
import { withI18n } from '~/lib/i18n/with-i18n';
|
import { withI18n } from '~/lib/i18n/with-i18n';
|
||||||
|
|
||||||
|
import MembershipConfirmationNotification from './_components/membership-confirmation-notification';
|
||||||
|
|
||||||
async function UpdateAccountSuccess() {
|
async function UpdateAccountSuccess() {
|
||||||
const { t } = useTranslation('account');
|
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -21,26 +19,7 @@ async function UpdateAccountSuccess() {
|
|||||||
redirect(pathsConfig.app.home);
|
redirect(pathsConfig.app.home);
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data: accountData } = usePersonalAccountData(user.id);
|
return <MembershipConfirmationNotification userId={user.id} />;
|
||||||
|
|
||||||
if (!accountData?.id) {
|
|
||||||
redirect(pathsConfig.app.home);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SuccessNotification
|
|
||||||
showLogo={false}
|
|
||||||
title={t('account:membershipConfirmation:successTitle', {
|
|
||||||
firstName: accountData?.name,
|
|
||||||
lastName: accountData?.last_name,
|
|
||||||
})}
|
|
||||||
descriptionKey="account:membershipConfirmation:successDescription"
|
|
||||||
buttonProps={{
|
|
||||||
buttonTitleKey: 'account:membershipConfirmation:successButton',
|
|
||||||
href: pathsConfig.app.selectPackage,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withI18n(UpdateAccountSuccess);
|
export default withI18n(UpdateAccountSuccess);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ async function getSupabaseHealthCheck() {
|
|||||||
try {
|
try {
|
||||||
const client = getSupabaseServerAdminClient();
|
const client = getSupabaseServerAdminClient();
|
||||||
|
|
||||||
const { error } = await client.rpc('is_set', {
|
const { error } = await client.schema('medreport').rpc('is_set', {
|
||||||
field_name: 'billing_provider',
|
field_name: 'billing_provider',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,6 @@
|
|||||||
|
import { use } from 'react';
|
||||||
|
|
||||||
|
import { PageBody } from '@kit/ui/page';
|
||||||
import { Trans } from '@kit/ui/trans';
|
import { Trans } from '@kit/ui/trans';
|
||||||
|
|
||||||
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
import { createI18nServerInstance } from '~/lib/i18n/i18n.server';
|
||||||
@@ -6,9 +9,7 @@ import { withI18n } from '~/lib/i18n/with-i18n';
|
|||||||
import Dashboard from '../_components/dashboard';
|
import Dashboard from '../_components/dashboard';
|
||||||
// local imports
|
// local imports
|
||||||
import { HomeLayoutPageHeader } from '../_components/home-page-header';
|
import { HomeLayoutPageHeader } from '../_components/home-page-header';
|
||||||
import { use } from 'react';
|
|
||||||
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
|
import { loadUserWorkspace } from '../_lib/server/load-user-workspace';
|
||||||
import { PageBody } from '@kit/ui/page';
|
|
||||||
|
|
||||||
export const generateMetadata = async () => {
|
export const generateMetadata = async () => {
|
||||||
const i18n = await createI18nServerInstance();
|
const i18n = await createI18nServerInstance();
|
||||||
|
|||||||
@@ -35,13 +35,13 @@ async function workspaceLoader() {
|
|||||||
accountsPromise(),
|
accountsPromise(),
|
||||||
workspacePromise,
|
workspacePromise,
|
||||||
requireUserInServerComponent(),
|
requireUserInServerComponent(),
|
||||||
tempAccountsPromise()
|
tempAccountsPromise(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accounts,
|
accounts,
|
||||||
workspace,
|
workspace,
|
||||||
user,
|
user,
|
||||||
tempVisibleAccounts
|
tempVisibleAccounts,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,9 +46,11 @@ async function loadAccountMembers(
|
|||||||
client: SupabaseClient<Database>,
|
client: SupabaseClient<Database>,
|
||||||
account: string,
|
account: string,
|
||||||
) {
|
) {
|
||||||
const { data, error } = await client.rpc('get_account_members', {
|
const { data, error } = await client
|
||||||
account_slug: account,
|
.schema('medreport')
|
||||||
});
|
.rpc('get_account_members', {
|
||||||
|
account_slug: account,
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
@@ -67,9 +69,11 @@ async function loadInvitations(
|
|||||||
client: SupabaseClient<Database>,
|
client: SupabaseClient<Database>,
|
||||||
account: string,
|
account: string,
|
||||||
) {
|
) {
|
||||||
const { data, error } = await client.rpc('get_account_invitations', {
|
const { data, error } = await client
|
||||||
account_slug: account,
|
.schema('medreport')
|
||||||
});
|
.rpc('get_account_invitations', {
|
||||||
|
account_slug: account,
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
|
|||||||
@@ -79,12 +79,11 @@ async function JoinTeamAccountPage(props: JoinTeamAccountPageProps) {
|
|||||||
// 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
|
||||||
// if the user can read the account, then they are already in the account
|
// if the user can read the account, then they are already in the account
|
||||||
const { data: isAlreadyTeamMember } = await client.rpc(
|
const { data: isAlreadyTeamMember } = await client
|
||||||
'is_account_team_member',
|
.schema('medreport')
|
||||||
{
|
.rpc('is_account_team_member', {
|
||||||
target_account_id: invitation.account.id,
|
target_account_id: invitation.account.id,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
// if the user is already in the account redirect to the home page
|
// if the user is already in the account redirect to the home page
|
||||||
if (isAlreadyTeamMember) {
|
if (isAlreadyTeamMember) {
|
||||||
|
|||||||
30
app/store/[countryCode]/(checkout)/checkout/page.tsx
Normal file
30
app/store/[countryCode]/(checkout)/checkout/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { retrieveCart } from "@lib/data/cart"
|
||||||
|
import { retrieveCustomer } from "@lib/data/customer"
|
||||||
|
import PaymentWrapper from "@modules/checkout/components/payment-wrapper"
|
||||||
|
import CheckoutForm from "@modules/checkout/templates/checkout-form"
|
||||||
|
import CheckoutSummary from "@modules/checkout/templates/checkout-summary"
|
||||||
|
import { Metadata } from "next"
|
||||||
|
import { notFound } from "next/navigation"
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Checkout",
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function Checkout() {
|
||||||
|
const cart = await retrieveCart()
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
return notFound()
|
||||||
|
}
|
||||||
|
|
||||||
|
const customer = await retrieveCustomer()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-1 small:grid-cols-[1fr_416px] content-container gap-x-40 py-12">
|
||||||
|
<PaymentWrapper cart={cart}>
|
||||||
|
<CheckoutForm cart={cart} customer={customer} />
|
||||||
|
</PaymentWrapper>
|
||||||
|
<CheckoutSummary cart={cart} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
45
app/store/[countryCode]/(checkout)/layout.tsx
Normal file
45
app/store/[countryCode]/(checkout)/layout.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { LocalizedClientLink } from '~/medusa/modules/common/components';
|
||||||
|
import { ChevronDownIcon } from '~/medusa/modules/common/icons';
|
||||||
|
import { MedusaCTA } from '~/medusa/modules/layout/components';
|
||||||
|
|
||||||
|
export default function CheckoutLayout({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="small:min-h-screen relative w-full bg-white">
|
||||||
|
<div className="h-16 border-b bg-white">
|
||||||
|
<nav className="content-container flex h-full items-center justify-between">
|
||||||
|
<LocalizedClientLink
|
||||||
|
href="/cart"
|
||||||
|
className="text-small-semi text-ui-fg-base flex flex-1 basis-0 items-center gap-x-2 uppercase"
|
||||||
|
data-testid="back-to-cart-link"
|
||||||
|
>
|
||||||
|
<ChevronDownIcon className="rotate-90" size={16} />
|
||||||
|
<span className="small:block txt-compact-plus text-ui-fg-subtle hover:text-ui-fg-base mt-px hidden">
|
||||||
|
Back to shopping cart
|
||||||
|
</span>
|
||||||
|
<span className="small:hidden txt-compact-plus text-ui-fg-subtle hover:text-ui-fg-base mt-px block">
|
||||||
|
Back
|
||||||
|
</span>
|
||||||
|
</LocalizedClientLink>
|
||||||
|
<LocalizedClientLink
|
||||||
|
href="/"
|
||||||
|
className="txt-compact-xlarge-plus text-ui-fg-subtle hover:text-ui-fg-base uppercase"
|
||||||
|
data-testid="store-link"
|
||||||
|
>
|
||||||
|
Medusa Store
|
||||||
|
</LocalizedClientLink>
|
||||||
|
<div className="flex-1 basis-0" />
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="relative" data-testid="checkout-container">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full items-center justify-center py-4">
|
||||||
|
<MedusaCTA />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/store/[countryCode]/(checkout)/not-found.tsx
Normal file
20
app/store/[countryCode]/(checkout)/not-found.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 async function NotFound() {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[calc(100vh-64px)] flex-col items-center justify-center gap-4">
|
||||||
|
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||||
|
<p className="text-small-regular text-ui-fg-base">
|
||||||
|
The page you tried to access does not exist.
|
||||||
|
</p>
|
||||||
|
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||||
|
import { getRegion } from '~/medusa/lib/data/regions';
|
||||||
|
import AddressBook from '~/medusa/modules/account/components/address-book';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Addresses',
|
||||||
|
description: 'View your addresses',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Addresses(props: {
|
||||||
|
params: Promise<{ countryCode: string }>;
|
||||||
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
const { countryCode } = params;
|
||||||
|
const customer = await retrieveCustomer();
|
||||||
|
const region = await getRegion(countryCode);
|
||||||
|
|
||||||
|
if (!customer || !region) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full" data-testid="addresses-page-wrapper">
|
||||||
|
<div className="mb-8 flex flex-col gap-y-4">
|
||||||
|
<h1 className="text-2xl-semi">Shipping Addresses</h1>
|
||||||
|
<p className="text-base-regular">
|
||||||
|
View and update your shipping addresses, you can add as many as you
|
||||||
|
like. Saving your addresses will make them available during checkout.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<AddressBook customer={customer} region={region} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import Spinner from '~/medusa/modules/common/icons/spinner';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="text-ui-fg-base flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner size={36} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { retrieveOrder } from '~/medusa/lib/data/orders';
|
||||||
|
import OrderDetailsTemplate from '~/medusa/modules/order/templates/order-details-template';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const order = await retrieveOrder(params.id).catch(() => null);
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `Order #${order.display_id}`,
|
||||||
|
description: `View your order`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function OrderDetailPage(props: Props) {
|
||||||
|
const params = await props.params;
|
||||||
|
const order = await retrieveOrder(params.id).catch(() => null);
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <OrderDetailsTemplate order={order} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { listOrders } from '~/medusa/lib/data/orders';
|
||||||
|
import OrderOverview from '~/medusa/modules/account/components/order-overview';
|
||||||
|
import TransferRequestForm from '~/medusa/modules/account/components/transfer-request-form';
|
||||||
|
import Divider from '~/medusa/modules/common/components/divider';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Orders',
|
||||||
|
description: 'Overview of your previous orders.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Orders() {
|
||||||
|
const orders = await listOrders();
|
||||||
|
|
||||||
|
if (!orders) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full" data-testid="orders-page-wrapper">
|
||||||
|
<div className="mb-8 flex flex-col gap-y-4">
|
||||||
|
<h1 className="text-2xl-semi">Orders</h1>
|
||||||
|
<p className="text-base-regular">
|
||||||
|
View your previous orders and their status. You can also create
|
||||||
|
returns or exchanges for your orders if needed.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<OrderOverview orders={orders} />
|
||||||
|
<Divider className="my-16" />
|
||||||
|
<TransferRequestForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
app/store/[countryCode]/(main)/account/@dashboard/page.tsx
Normal file
23
app/store/[countryCode]/(main)/account/@dashboard/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||||
|
import { listOrders } from '~/medusa/lib/data/orders';
|
||||||
|
import Overview from '~/medusa/modules/account/components/overview';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Account',
|
||||||
|
description: 'Overview of your account activity.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function OverviewTemplate() {
|
||||||
|
const customer = await retrieveCustomer().catch(() => null);
|
||||||
|
const orders = (await listOrders().catch(() => null)) || null;
|
||||||
|
|
||||||
|
if (!customer) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Overview customer={customer} orders={orders} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { retrieveCustomer } from '~/medusa/lib/data/customer';
|
||||||
|
import { listRegions } from '~/medusa/lib/data/regions';
|
||||||
|
import ProfilePhone from '~/medusa/modules/account//components/profile-phone';
|
||||||
|
import ProfileBillingAddress from '~/medusa/modules/account/components/profile-billing-address';
|
||||||
|
import ProfileEmail from '~/medusa/modules/account/components/profile-email';
|
||||||
|
import ProfileName from '~/medusa/modules/account/components/profile-name';
|
||||||
|
import ProfilePassword from '~/medusa/modules/account/components/profile-password';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Profile',
|
||||||
|
description: 'View and edit your Medusa Store profile.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Profile() {
|
||||||
|
const customer = await retrieveCustomer();
|
||||||
|
const regions = await listRegions();
|
||||||
|
|
||||||
|
if (!customer || !regions) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full" data-testid="profile-page-wrapper">
|
||||||
|
<div className="mb-8 flex flex-col gap-y-4">
|
||||||
|
<h1 className="text-2xl-semi">Profile</h1>
|
||||||
|
<p className="text-base-regular">
|
||||||
|
View and update your profile information, including your name, email,
|
||||||
|
and phone number. You can also update your billing address, or change
|
||||||
|
your password.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full flex-col gap-y-8">
|
||||||
|
<ProfileName customer={customer} />
|
||||||
|
<Divider />
|
||||||
|
<ProfileEmail customer={customer} />
|
||||||
|
<Divider />
|
||||||
|
<ProfilePhone customer={customer} />
|
||||||
|
<Divider />
|
||||||
|
{/* <ProfilePassword customer={customer} />
|
||||||
|
<Divider /> */}
|
||||||
|
<ProfileBillingAddress customer={customer} regions={regions} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Divider = () => {
|
||||||
|
return <div className="h-px w-full bg-gray-200" />;
|
||||||
|
};
|
||||||
|
``;
|
||||||
12
app/store/[countryCode]/(main)/account/@login/page.tsx
Normal file
12
app/store/[countryCode]/(main)/account/@login/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import LoginTemplate from '~/medusa/modules/account/templates/login-template';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Sign in',
|
||||||
|
description: 'Sign in to your Medusa Store account.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Login() {
|
||||||
|
return <LoginTemplate />;
|
||||||
|
}
|
||||||
21
app/store/[countryCode]/(main)/account/layout.tsx
Normal file
21
app/store/[countryCode]/(main)/account/layout.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Toaster } from '@medusajs/ui';
|
||||||
|
|
||||||
|
import { retrieveCustomer } from '~/medusa/lib/data';
|
||||||
|
import { AccountLayout } from '~/medusa/modules/account/templates';
|
||||||
|
|
||||||
|
export default async function AccountPageLayout({
|
||||||
|
dashboard,
|
||||||
|
login,
|
||||||
|
}: {
|
||||||
|
dashboard?: React.ReactNode;
|
||||||
|
login?: React.ReactNode;
|
||||||
|
}) {
|
||||||
|
const customer = await retrieveCustomer().catch(() => null);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AccountLayout customer={customer}>
|
||||||
|
{customer ? dashboard : login}
|
||||||
|
<Toaster />
|
||||||
|
</AccountLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
9
app/store/[countryCode]/(main)/account/loading.tsx
Normal file
9
app/store/[countryCode]/(main)/account/loading.tsx
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
import Spinner from '~/medusa/modules/common/icons/spinner';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return (
|
||||||
|
<div className="text-ui-fg-base flex h-full w-full items-center justify-center">
|
||||||
|
<Spinner size={36} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
5
app/store/[countryCode]/(main)/cart/loading.tsx
Normal file
5
app/store/[countryCode]/(main)/cart/loading.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import SkeletonCartPage from '~/medusa/modules/skeletons/templates/skeleton-cart-page';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <SkeletonCartPage />;
|
||||||
|
}
|
||||||
21
app/store/[countryCode]/(main)/cart/not-found.tsx
Normal file
21
app/store/[countryCode]/(main)/cart/not-found.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
}
|
||||||
23
app/store/[countryCode]/(main)/cart/page.tsx
Normal file
23
app/store/[countryCode]/(main)/cart/page.tsx
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
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 cart = await retrieveCart().catch((error) => {
|
||||||
|
console.error(error);
|
||||||
|
return notFound();
|
||||||
|
});
|
||||||
|
|
||||||
|
const customer = await retrieveCustomer();
|
||||||
|
|
||||||
|
return <CartTemplate cart={cart} customer={customer} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { StoreRegion } from '@medusajs/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCategoryByHandle,
|
||||||
|
listCategories,
|
||||||
|
} from '~/medusa/lib/data/categories';
|
||||||
|
import { listRegions } from '~/medusa/lib/data/regions';
|
||||||
|
import CategoryTemplate from '~/medusa/modules/categories/templates';
|
||||||
|
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ category: string[]; countryCode: string }>;
|
||||||
|
searchParams: Promise<{
|
||||||
|
sortBy?: SortOptions;
|
||||||
|
page?: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const product_categories = await listCategories();
|
||||||
|
|
||||||
|
if (!product_categories) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryCodes = await listRegions().then((regions: StoreRegion[]) =>
|
||||||
|
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
const categoryHandles = product_categories.map(
|
||||||
|
(category: any) => category.handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticParams = countryCodes
|
||||||
|
?.map((countryCode: string | undefined) =>
|
||||||
|
categoryHandles.map((handle: any) => ({
|
||||||
|
countryCode,
|
||||||
|
category: [handle],
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
return staticParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
try {
|
||||||
|
const productCategory = await getCategoryByHandle(params.category);
|
||||||
|
|
||||||
|
const title = productCategory.name + ' | Medusa Store';
|
||||||
|
|
||||||
|
const description = productCategory.description ?? `${title} category.`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${title} | Medusa Store`,
|
||||||
|
description,
|
||||||
|
alternates: {
|
||||||
|
canonical: `${params.category.join('/')}`,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CategoryPage(props: Props) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const params = await props.params;
|
||||||
|
const { sortBy, page } = searchParams;
|
||||||
|
|
||||||
|
const productCategory = await getCategoryByHandle(params.category);
|
||||||
|
|
||||||
|
if (!productCategory) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CategoryTemplate
|
||||||
|
category={productCategory}
|
||||||
|
sortBy={sortBy}
|
||||||
|
page={page}
|
||||||
|
countryCode={params.countryCode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
95
app/store/[countryCode]/(main)/collections/[handle]/page.tsx
Normal file
95
app/store/[countryCode]/(main)/collections/[handle]/page.tsx
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { StoreCollection, StoreRegion } from '@medusajs/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
getCollectionByHandle,
|
||||||
|
listCollections,
|
||||||
|
} from '~/medusa/lib/data/collections';
|
||||||
|
import { listRegions } from '~/medusa/lib/data/regions';
|
||||||
|
import CollectionTemplate from '~/medusa/modules/collections/templates';
|
||||||
|
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ handle: string; countryCode: string }>;
|
||||||
|
searchParams: Promise<{
|
||||||
|
page?: string;
|
||||||
|
sortBy?: SortOptions;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PRODUCT_LIMIT = 12;
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
const { collections } = await listCollections({
|
||||||
|
fields: '*products',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collections) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryCodes = await listRegions().then(
|
||||||
|
(regions: StoreRegion[]) =>
|
||||||
|
regions
|
||||||
|
?.map((r) => r.countries?.map((c) => c.iso_2))
|
||||||
|
.flat()
|
||||||
|
.filter(Boolean) as string[],
|
||||||
|
);
|
||||||
|
|
||||||
|
const collectionHandles = collections.map(
|
||||||
|
(collection: StoreCollection) => collection.handle,
|
||||||
|
);
|
||||||
|
|
||||||
|
const staticParams = countryCodes
|
||||||
|
?.map((countryCode: string) =>
|
||||||
|
collectionHandles.map((handle: string | undefined) => ({
|
||||||
|
countryCode,
|
||||||
|
handle,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.flat();
|
||||||
|
|
||||||
|
return staticParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const collection = await getCollectionByHandle(params.handle);
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const metadata = {
|
||||||
|
title: `${collection.title} | Medusa Store`,
|
||||||
|
description: `${collection.title} collection`,
|
||||||
|
} as Metadata;
|
||||||
|
|
||||||
|
return metadata;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function CollectionPage(props: Props) {
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const params = await props.params;
|
||||||
|
const { sortBy, page } = searchParams;
|
||||||
|
|
||||||
|
const collection = await getCollectionByHandle(params.handle).then(
|
||||||
|
(collection: StoreCollection) => collection,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CollectionTemplate
|
||||||
|
collection={collection}
|
||||||
|
page={page}
|
||||||
|
sortBy={sortBy}
|
||||||
|
countryCode={params.countryCode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
46
app/store/[countryCode]/(main)/layout.tsx
Normal file
46
app/store/[countryCode]/(main)/layout.tsx
Normal 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 />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
app/store/[countryCode]/(main)/not-found.tsx
Normal file
20
app/store/[countryCode]/(main)/not-found.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 gap-4">
|
||||||
|
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||||
|
<p className="text-small-regular text-ui-fg-base">
|
||||||
|
The page you tried to access does not exist.
|
||||||
|
</p>
|
||||||
|
<InteractiveLink href="/">Go to frontpage</InteractiveLink>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import SkeletonOrderConfirmed from '~/medusa/modules/skeletons/templates/skeleton-order-confirmed';
|
||||||
|
|
||||||
|
export default function Loading() {
|
||||||
|
return <SkeletonOrderConfirmed />;
|
||||||
|
}
|
||||||
25
app/store/[countryCode]/(main)/order/[id]/confirmed/page.tsx
Normal file
25
app/store/[countryCode]/(main)/order/[id]/confirmed/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { retrieveOrder } from '~/medusa/lib/data/orders';
|
||||||
|
import OrderCompletedTemplate from '~/medusa/modules/order/templates/order-completed-template';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ id: string }>;
|
||||||
|
};
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Order Confirmed',
|
||||||
|
description: 'You purchase was successful',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function OrderConfirmedPage(props: Props) {
|
||||||
|
const params = await props.params;
|
||||||
|
const order = await retrieveOrder(params.id).catch(() => null);
|
||||||
|
|
||||||
|
if (!order) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return <OrderCompletedTemplate order={order} />;
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Heading, Text } from '@medusajs/ui';
|
||||||
|
|
||||||
|
import { acceptTransferRequest } from '~/medusa/lib/data/orders';
|
||||||
|
import TransferImage from '~/medusa/modules/order/components/transfer-image';
|
||||||
|
|
||||||
|
export default async function TransferPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { id: string; token: string };
|
||||||
|
}) {
|
||||||
|
const { id, token } = params;
|
||||||
|
|
||||||
|
const { success, error } = await acceptTransferRequest(id, token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
|
||||||
|
<TransferImage />
|
||||||
|
<div className="flex flex-col gap-y-6">
|
||||||
|
{success && (
|
||||||
|
<>
|
||||||
|
<Heading level="h1" className="text-xl text-zinc-900">
|
||||||
|
Order transfered!
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
Order {id} has been successfully transfered to the new owner.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!success && (
|
||||||
|
<>
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
There was an error accepting the transfer. Please try again.
|
||||||
|
</Text>
|
||||||
|
{error && (
|
||||||
|
<Text className="text-red-500">Error message: {error}</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { Heading, Text } from '@medusajs/ui';
|
||||||
|
|
||||||
|
import { declineTransferRequest } from '~/medusa/lib/data/orders';
|
||||||
|
import TransferImage from '~/medusa/modules/order/components/transfer-image';
|
||||||
|
|
||||||
|
export default async function TransferPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { id: string; token: string };
|
||||||
|
}) {
|
||||||
|
const { id, token } = params;
|
||||||
|
|
||||||
|
const { success, error } = await declineTransferRequest(id, token);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
|
||||||
|
<TransferImage />
|
||||||
|
<div className="flex flex-col gap-y-6">
|
||||||
|
{success && (
|
||||||
|
<>
|
||||||
|
<Heading level="h1" className="text-xl text-zinc-900">
|
||||||
|
Order transfer declined!
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
Transfer of order {id} has been successfully declined.
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!success && (
|
||||||
|
<>
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
There was an error declining the transfer. Please try again.
|
||||||
|
</Text>
|
||||||
|
{error && (
|
||||||
|
<Text className="text-red-500">Error message: {error}</Text>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Heading, Text } from '@medusajs/ui';
|
||||||
|
|
||||||
|
import TransferActions from '~/medusa/modules/order/components/transfer-actions';
|
||||||
|
import TransferImage from '~/medusa/modules/order/components/transfer-image';
|
||||||
|
|
||||||
|
export default async function TransferPage({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: { id: string; token: string };
|
||||||
|
}) {
|
||||||
|
const { id, token } = params;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="mx-auto mt-10 mb-20 flex w-2/5 flex-col items-start gap-y-4">
|
||||||
|
<TransferImage />
|
||||||
|
<div className="flex flex-col gap-y-6">
|
||||||
|
<Heading level="h1" className="text-xl text-zinc-900">
|
||||||
|
Transfer request for order {id}
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
You've received a request to transfer ownership of your order (
|
||||||
|
{id}). If you agree to this request, you can approve the transfer by
|
||||||
|
clicking the button below.
|
||||||
|
</Text>
|
||||||
|
<div className="h-px w-full bg-zinc-200" />
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
If you accept, the new owner will take over all responsibilities and
|
||||||
|
permissions associated with this order.
|
||||||
|
</Text>
|
||||||
|
<Text className="text-zinc-600">
|
||||||
|
If you do not recognize this request or wish to retain ownership, no
|
||||||
|
further action is required.
|
||||||
|
</Text>
|
||||||
|
<div className="h-px w-full bg-zinc-200" />
|
||||||
|
<TransferActions id={id} token={token} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
40
app/store/[countryCode]/(main)/page.tsx
Normal file
40
app/store/[countryCode]/(main)/page.tsx
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { getRegion, listCollections } from '~/medusa/lib/data';
|
||||||
|
import FeaturedProducts from '~/medusa/modules/home/components/featured-products';
|
||||||
|
import Hero from '~/medusa/modules/home/components/hero';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Medusa Next.js Starter Template',
|
||||||
|
description:
|
||||||
|
'A performant frontend ecommerce starter template with Next.js 15 and Medusa.',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Home(props: {
|
||||||
|
params: Promise<{ countryCode: string }>;
|
||||||
|
}) {
|
||||||
|
const params = await props.params;
|
||||||
|
|
||||||
|
const { countryCode } = params;
|
||||||
|
|
||||||
|
const region = await getRegion(countryCode);
|
||||||
|
|
||||||
|
const { collections } = await listCollections({
|
||||||
|
fields: 'id, handle, title',
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!collections || !region) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Hero />
|
||||||
|
<div className="py-12">
|
||||||
|
<ul className="flex flex-col gap-x-6">
|
||||||
|
<FeaturedProducts collections={collections} region={region} />
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
108
app/store/[countryCode]/(main)/products/[handle]/page.tsx
Normal file
108
app/store/[countryCode]/(main)/products/[handle]/page.tsx
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { notFound } from 'next/navigation';
|
||||||
|
|
||||||
|
import { listProducts } from '~/medusa/lib/data/products';
|
||||||
|
import { getRegion, listRegions } from '~/medusa/lib/data/regions';
|
||||||
|
import ProductTemplate from '~/medusa/modules/products/templates';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
params: Promise<{ countryCode: string; handle: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function generateStaticParams() {
|
||||||
|
try {
|
||||||
|
const countryCodes = await listRegions().then((regions) =>
|
||||||
|
regions?.map((r) => r.countries?.map((c) => c.iso_2)).flat(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!countryCodes) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const promises = countryCodes.map(async (country) => {
|
||||||
|
const { response } = await listProducts({
|
||||||
|
countryCode: country,
|
||||||
|
queryParams: { limit: 100, fields: 'handle' },
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
country,
|
||||||
|
products: response.products,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const countryProducts = await Promise.all(promises);
|
||||||
|
|
||||||
|
return countryProducts
|
||||||
|
.flatMap((countryData) =>
|
||||||
|
countryData.products.map((product) => ({
|
||||||
|
countryCode: countryData.country,
|
||||||
|
handle: product.handle,
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.filter((param) => param.handle);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`Failed to generate static paths for product pages: ${
|
||||||
|
error instanceof Error ? error.message : 'Unknown error'
|
||||||
|
}.`,
|
||||||
|
);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function generateMetadata(props: Props): Promise<Metadata> {
|
||||||
|
const params = await props.params;
|
||||||
|
const { handle } = params;
|
||||||
|
const region = await getRegion(params.countryCode);
|
||||||
|
|
||||||
|
if (!region) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const product = await listProducts({
|
||||||
|
countryCode: params.countryCode,
|
||||||
|
queryParams: { handle },
|
||||||
|
}).then(({ response }) => response.products[0]);
|
||||||
|
|
||||||
|
if (!product) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: `${product.title} | Medusa Store`,
|
||||||
|
description: `${product.title}`,
|
||||||
|
openGraph: {
|
||||||
|
title: `${product.title} | Medusa Store`,
|
||||||
|
description: `${product.title}`,
|
||||||
|
images: product.thumbnail ? [product.thumbnail] : [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async function ProductPage(props: Props) {
|
||||||
|
const params = await props.params;
|
||||||
|
const region = await getRegion(params.countryCode);
|
||||||
|
|
||||||
|
if (!region) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const pricedProduct = await listProducts({
|
||||||
|
countryCode: params.countryCode,
|
||||||
|
queryParams: { handle: params.handle },
|
||||||
|
}).then(({ response }) => response.products[0]);
|
||||||
|
|
||||||
|
if (!pricedProduct) {
|
||||||
|
notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ProductTemplate
|
||||||
|
product={pricedProduct}
|
||||||
|
region={region}
|
||||||
|
countryCode={params.countryCode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
33
app/store/[countryCode]/(main)/store/page.tsx
Normal file
33
app/store/[countryCode]/(main)/store/page.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import { SortOptions } from '~/medusa/modules/store/components/refinement-list/sort-products';
|
||||||
|
import StoreTemplate from '~/medusa/modules/store/templates';
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: 'Store',
|
||||||
|
description: 'Explore all of our products.',
|
||||||
|
};
|
||||||
|
|
||||||
|
type Params = {
|
||||||
|
searchParams: Promise<{
|
||||||
|
sortBy?: SortOptions;
|
||||||
|
page?: string;
|
||||||
|
}>;
|
||||||
|
params: Promise<{
|
||||||
|
countryCode: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function StorePage(props: Params) {
|
||||||
|
const params = await props.params;
|
||||||
|
const searchParams = await props.searchParams;
|
||||||
|
const { sortBy, page } = searchParams;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StoreTemplate
|
||||||
|
sortBy={sortBy}
|
||||||
|
page={page}
|
||||||
|
countryCode={params.countryCode}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
29
app/store/not-found.tsx
Normal file
29
app/store/not-found.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { Metadata } from 'next';
|
||||||
|
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import { ArrowUpRightMini } from '@medusajs/icons';
|
||||||
|
import { Text } from '@medusajs/ui';
|
||||||
|
|
||||||
|
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 gap-4">
|
||||||
|
<h1 className="text-2xl-semi text-ui-fg-base">Page not found</h1>
|
||||||
|
<p className="text-small-regular text-ui-fg-base">
|
||||||
|
The page you tried to access does not exist.
|
||||||
|
</p>
|
||||||
|
<Link className="group flex items-center gap-x-1" href="/">
|
||||||
|
<Text className="text-ui-fg-interactive">Go to frontpage</Text>
|
||||||
|
<ArrowUpRightMini
|
||||||
|
className="duration-150 ease-in-out group-hover:rotate-45"
|
||||||
|
color="var(--fg-interactive)"
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
BIN
app/store/opengraph-image.jpg
Normal file
BIN
app/store/opengraph-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
BIN
app/store/twitter-image.jpg
Normal file
BIN
app/store/twitter-image.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 229 KiB |
@@ -137,6 +137,7 @@ async function syncData() {
|
|||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
// SAVE ANALYSIS GROUP
|
// SAVE ANALYSIS GROUP
|
||||||
const { data: insertedAnalysisGroup, error } = await supabase
|
const { data: insertedAnalysisGroup, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_groups')
|
.from('analysis_groups')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -174,6 +175,7 @@ async function syncData() {
|
|||||||
const analysisElement = item.UuringuElement;
|
const analysisElement = item.UuringuElement;
|
||||||
|
|
||||||
const { data: insertedAnalysisElement, error } = await supabase
|
const { data: insertedAnalysisElement, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_elements')
|
.from('analysis_elements')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -217,6 +219,7 @@ async function syncData() {
|
|||||||
if (analyses?.length) {
|
if (analyses?.length) {
|
||||||
for (const analysis of analyses) {
|
for (const analysis of analyses) {
|
||||||
const { data: insertedAnalysis, error } = await supabase
|
const { data: insertedAnalysis, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analyses')
|
.from('analyses')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -259,7 +262,7 @@ async function syncData() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await supabase.from('codes').upsert(codes);
|
await supabase.schema('medreport').from('codes').upsert(codes);
|
||||||
|
|
||||||
await supabase.schema('audit').from('sync_entries').insert({
|
await supabase.schema('audit').from('sync_entries').insert({
|
||||||
operation: 'ANALYSES_SYNC',
|
operation: 'ANALYSES_SYNC',
|
||||||
|
|||||||
@@ -105,10 +105,12 @@ async function syncData() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { error: providersError } = await supabase
|
const { error: providersError } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('connected_online_providers')
|
.from('connected_online_providers')
|
||||||
.upsert(mappedClinics);
|
.upsert(mappedClinics);
|
||||||
|
|
||||||
const { error: servicesError } = await supabase
|
const { error: servicesError } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('connected_online_services')
|
.from('connected_online_services')
|
||||||
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });
|
.upsert(mappedServices, { onConflict: 'id', ignoreDuplicates: false });
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use server'
|
'use server';
|
||||||
|
|
||||||
import logRequestResult from '@/lib/services/audit.service';
|
import logRequestResult from '@/lib/services/audit.service';
|
||||||
import { RequestStatus } from '@/lib/types/audit';
|
import { RequestStatus } from '@/lib/types/audit';
|
||||||
import {
|
import {
|
||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
ConnectedOnlineMethodName,
|
ConnectedOnlineMethodName,
|
||||||
} from '@/lib/types/connected-online';
|
} from '@/lib/types/connected-online';
|
||||||
import { ExternalApi } from '@/lib/types/external';
|
import { ExternalApi } from '@/lib/types/external';
|
||||||
import { Tables } from '@/supabase/database.types';
|
import { Tables } from '@/packages/supabase/src/database.types';
|
||||||
import { createClient } from '@/utils/supabase/server';
|
import { createClient } from '@/utils/supabase/server';
|
||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
@@ -106,11 +106,13 @@ export async function bookAppointment(
|
|||||||
{ data: dbService, error: serviceError },
|
{ data: dbService, error: serviceError },
|
||||||
] = await Promise.all([
|
] = await Promise.all([
|
||||||
supabase
|
supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('connected_online_providers')
|
.from('connected_online_providers')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', clinicId)
|
.eq('id', clinicId)
|
||||||
.limit(1),
|
.limit(1),
|
||||||
supabase
|
supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('connected_online_services')
|
.from('connected_online_services')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('sync_id', serviceSyncId)
|
.eq('sync_id', serviceSyncId)
|
||||||
@@ -132,8 +134,14 @@ export async function bookAppointment(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clinic: Tables<'connected_online_providers'> = dbClinic![0];
|
const clinic: Tables<
|
||||||
const service: Tables<'connected_online_services'> = dbService![0];
|
{ schema: 'medreport' },
|
||||||
|
'connected_online_providers'
|
||||||
|
> = dbClinic![0];
|
||||||
|
const service: Tables<
|
||||||
|
{ schema: 'medreport' },
|
||||||
|
'connected_online_services'
|
||||||
|
> = dbService![0];
|
||||||
|
|
||||||
// TODO the dummy data needs to be replaced with real values once they're present on the user/account
|
// TODO the dummy data needs to be replaced with real values once they're present on the user/account
|
||||||
const response = await axios.post(
|
const response = await axios.post(
|
||||||
@@ -183,6 +191,7 @@ export async function bookAppointment(
|
|||||||
const responseParts = responseData.Value.split(',');
|
const responseParts = responseData.Value.split(',');
|
||||||
|
|
||||||
const { error } = await supabase
|
const { error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('connected_online_reservation')
|
.from('connected_online_reservation')
|
||||||
.insert({
|
.insert({
|
||||||
booking_code: responseParts[1],
|
booking_code: responseParts[1],
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ import { toArray } from '@/lib/utils';
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import { XMLParser } from 'fast-xml-parser';
|
import { XMLParser } from 'fast-xml-parser';
|
||||||
import { uniqBy } from 'lodash';
|
import { uniqBy } from 'lodash';
|
||||||
|
|
||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
|
|
||||||
const BASE_URL = process.env.MEDIPOST_URL!;
|
const BASE_URL = process.env.MEDIPOST_URL!;
|
||||||
@@ -196,6 +197,7 @@ async function saveAnalysisGroup(
|
|||||||
supabase: SupabaseClient,
|
supabase: SupabaseClient,
|
||||||
) {
|
) {
|
||||||
const { data: insertedAnalysisGroup, error } = await supabase
|
const { data: insertedAnalysisGroup, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_groups')
|
.from('analysis_groups')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -215,13 +217,14 @@ async function saveAnalysisGroup(
|
|||||||
const analysisGroupId = insertedAnalysisGroup[0].id;
|
const analysisGroupId = insertedAnalysisGroup[0].id;
|
||||||
|
|
||||||
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
const analysisGroupCodes = toArray(analysisGroup.Kood);
|
||||||
const codes: Partial<Tables<'codes'>>[] = analysisGroupCodes.map((kood) => ({
|
const codes: Partial<Tables<{ schema: 'medreport' }, 'codes'>>[] =
|
||||||
hk_code: kood.HkKood,
|
analysisGroupCodes.map((kood) => ({
|
||||||
hk_code_multiplier: kood.HkKoodiKordaja,
|
hk_code: kood.HkKood,
|
||||||
coefficient: kood.Koefitsient,
|
hk_code_multiplier: kood.HkKoodiKordaja,
|
||||||
price: kood.Hind,
|
coefficient: kood.Koefitsient,
|
||||||
analysis_group_id: analysisGroupId,
|
price: kood.Hind,
|
||||||
}));
|
analysis_group_id: analysisGroupId,
|
||||||
|
}));
|
||||||
|
|
||||||
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
const analysisGroupItems = toArray(analysisGroup.Uuring);
|
||||||
|
|
||||||
@@ -229,6 +232,7 @@ async function saveAnalysisGroup(
|
|||||||
const analysisElement = item.UuringuElement;
|
const analysisElement = item.UuringuElement;
|
||||||
|
|
||||||
const { data: insertedAnalysisElement, error } = await supabase
|
const { data: insertedAnalysisElement, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_elements')
|
.from('analysis_elements')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -270,6 +274,7 @@ async function saveAnalysisGroup(
|
|||||||
if (analyses?.length) {
|
if (analyses?.length) {
|
||||||
for (const analysis of analyses) {
|
for (const analysis of analyses) {
|
||||||
const { data: insertedAnalysis, error } = await supabase
|
const { data: insertedAnalysis, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analyses')
|
.from('analyses')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -310,6 +315,7 @@ async function saveAnalysisGroup(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { error: codesError } = await supabase
|
const { error: codesError } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('codes')
|
.from('codes')
|
||||||
.upsert(codes, { ignoreDuplicates: false });
|
.upsert(codes, { ignoreDuplicates: false });
|
||||||
|
|
||||||
@@ -404,34 +410,41 @@ export async function composeOrderXML(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { data: analysisElements } = (await supabase
|
const { data: analysisElements } = (await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_elements')
|
.from('analysis_elements')
|
||||||
.select(`*, analysis_groups(*)`)
|
.select(`*, analysis_groups(*)`)
|
||||||
.in('id', orderedElements)) as {
|
.in('id', orderedElements)) as {
|
||||||
data: ({
|
data: ({
|
||||||
analysis_groups: Tables<'analysis_groups'>;
|
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||||
} & Tables<'analysis_elements'>)[];
|
} & Tables<{ schema: 'medreport' }, 'analysis_elements'>)[];
|
||||||
};
|
};
|
||||||
const { data: analyses } = (await supabase
|
const { data: analyses } = (await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analyses')
|
.from('analyses')
|
||||||
.select(`*, analysis_elements(*, analysis_groups(*))`)
|
.select(`*, analysis_elements(*, analysis_groups(*))`)
|
||||||
.in('id', orderedAnalyses)) as {
|
.in('id', orderedAnalyses)) as {
|
||||||
data: ({
|
data: ({
|
||||||
analysis_elements: Tables<'analysis_elements'> & {
|
analysis_elements: Tables<
|
||||||
analysis_groups: Tables<'analysis_groups'>;
|
{ schema: 'medreport' },
|
||||||
|
'analysis_elements'
|
||||||
|
> & {
|
||||||
|
analysis_groups: Tables<{ schema: 'medreport' }, 'analysis_groups'>;
|
||||||
};
|
};
|
||||||
} & Tables<'analyses'>)[];
|
} & Tables<{ schema: 'medreport' }, 'analyses'>)[];
|
||||||
};
|
};
|
||||||
|
|
||||||
const analysisGroups: Tables<'analysis_groups'>[] = uniqBy(
|
const analysisGroups: Tables<{ schema: 'medreport' }, 'analysis_groups'>[] =
|
||||||
(
|
uniqBy(
|
||||||
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ?? []
|
(
|
||||||
).concat(
|
analysisElements?.flatMap(({ analysis_groups }) => analysis_groups) ??
|
||||||
analyses?.flatMap(
|
[]
|
||||||
({ analysis_elements }) => analysis_elements.analysis_groups,
|
).concat(
|
||||||
) ?? [],
|
analyses?.flatMap(
|
||||||
),
|
({ analysis_elements }) => analysis_elements.analysis_groups,
|
||||||
'id',
|
) ?? [],
|
||||||
);
|
),
|
||||||
|
'id',
|
||||||
|
);
|
||||||
|
|
||||||
const specimenSection = [];
|
const specimenSection = [];
|
||||||
const analysisSection = [];
|
const analysisSection = [];
|
||||||
@@ -545,6 +558,7 @@ export async function syncPrivateMessage(
|
|||||||
const status = response.TellimuseOlek;
|
const status = response.TellimuseOlek;
|
||||||
|
|
||||||
const { data: analysisOrder, error: analysisOrderError } = await supabase
|
const { data: analysisOrder, error: analysisOrderError } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_orders')
|
.from('analysis_orders')
|
||||||
.select('user_id')
|
.select('user_id')
|
||||||
.eq('id', response.ValisTellimuseId);
|
.eq('id', response.ValisTellimuseId);
|
||||||
@@ -556,6 +570,7 @@ export async function syncPrivateMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { data: analysisResponse, error } = await supabase
|
const { data: analysisResponse, error } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_responses')
|
.from('analysis_responses')
|
||||||
.upsert(
|
.upsert(
|
||||||
{
|
{
|
||||||
@@ -576,7 +591,7 @@ export async function syncPrivateMessage(
|
|||||||
const analysisGroups = toArray(response.UuringuGrupp);
|
const analysisGroups = toArray(response.UuringuGrupp);
|
||||||
|
|
||||||
const responses: Omit<
|
const responses: Omit<
|
||||||
Tables<'analysis_response_elements'>,
|
Tables<{ schema: 'medreport' }, 'analysis_response_elements'>,
|
||||||
'id' | 'created_at' | 'updated_at'
|
'id' | 'created_at' | 'updated_at'
|
||||||
>[] = [];
|
>[] = [];
|
||||||
for (const analysisGroup of analysisGroups) {
|
for (const analysisGroup of analysisGroups) {
|
||||||
@@ -608,6 +623,7 @@ export async function syncPrivateMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { error: deleteError } = await supabase
|
const { error: deleteError } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_response_elements')
|
.from('analysis_response_elements')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('analysis_response_id', analysisResponse[0].id);
|
.eq('analysis_response_id', analysisResponse[0].id);
|
||||||
@@ -619,6 +635,7 @@ export async function syncPrivateMessage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { error: elementInsertError } = await supabase
|
const { error: elementInsertError } = await supabase
|
||||||
|
.schema('medreport')
|
||||||
.from('analysis_response_elements')
|
.from('analysis_response_elements')
|
||||||
.insert(responses);
|
.insert(responses);
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { DATE_TIME_FORMAT } from '@/lib/constants';
|
import { DATE_TIME_FORMAT } from '@/lib/constants';
|
||||||
import { Tables } from '@/supabase/database.types';
|
import { Tables } from '@/packages/supabase/src/database.types';
|
||||||
import { format } from 'date-fns';
|
import { format } from 'date-fns';
|
||||||
|
|
||||||
const isProd = process.env.NODE_ENV === 'production';
|
const isProd = process.env.NODE_ENV === 'production';
|
||||||
@@ -160,7 +160,7 @@ export const getAnalysisGroup = (
|
|||||||
analysisGroupOriginalId: string,
|
analysisGroupOriginalId: string,
|
||||||
analysisGroupName: string,
|
analysisGroupName: string,
|
||||||
specimenOrderNr: number,
|
specimenOrderNr: number,
|
||||||
analysisElement: Tables<'analysis_elements'>,
|
analysisElement: Tables<{ schema: 'medreport' }, 'analysis_elements'>,
|
||||||
) =>
|
) =>
|
||||||
`<UuringuGrupp>
|
`<UuringuGrupp>
|
||||||
<UuringuGruppId>${analysisGroupOriginalId}</UuringuGruppId>
|
<UuringuGruppId>${analysisGroupOriginalId}</UuringuGruppId>
|
||||||
|
|||||||
@@ -54,6 +54,9 @@
|
|||||||
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
"@makerkit/data-loader-supabase-core": "^0.0.10",
|
||||||
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
"@makerkit/data-loader-supabase-nextjs": "^1.2.5",
|
||||||
"@marsidev/react-turnstile": "^1.1.0",
|
"@marsidev/react-turnstile": "^1.1.0",
|
||||||
|
"@medusajs/icons": "^2.8.6",
|
||||||
|
"@medusajs/js-sdk": "latest",
|
||||||
|
"@medusajs/ui": "latest",
|
||||||
"@nosecone/next": "1.0.0-beta.7",
|
"@nosecone/next": "1.0.0-beta.7",
|
||||||
"@radix-ui/react-icons": "^1.3.2",
|
"@radix-ui/react-icons": "^1.3.2",
|
||||||
"@radix-ui/react-visually-hidden": "^1.2.3",
|
"@radix-ui/react-visually-hidden": "^1.2.3",
|
||||||
@@ -85,6 +88,8 @@
|
|||||||
"@kit/eslint-config": "workspace:*",
|
"@kit/eslint-config": "workspace:*",
|
||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
|
"@medusajs/types": "latest",
|
||||||
|
"@medusajs/ui-preset": "latest",
|
||||||
"@next/bundle-analyzer": "15.3.2",
|
"@next/bundle-analyzer": "15.3.2",
|
||||||
"@tailwindcss/postcss": "^4.1.10",
|
"@tailwindcss/postcss": "^4.1.10",
|
||||||
"@types/lodash": "^4.17.17",
|
"@types/lodash": "^4.17.17",
|
||||||
|
|||||||
@@ -20,8 +20,7 @@
|
|||||||
"@kit/prettier-config": "workspace:*",
|
"@kit/prettier-config": "workspace:*",
|
||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*"
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
export type UpsertSubscriptionParams =
|
export type UpsertSubscriptionParams =
|
||||||
Database['public']['Functions']['upsert_subscription']['Args'] & {
|
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
|
||||||
line_items: Array<LineItem>;
|
line_items: Array<LineItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -19,4 +19,4 @@ interface LineItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type UpsertOrderParams =
|
export type UpsertOrderParams =
|
||||||
Database['public']['Functions']['upsert_order']['Args'];
|
Database['medreport']['Functions']['upsert_order']['Args'];
|
||||||
|
|||||||
@@ -32,9 +32,7 @@
|
|||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3"
|
||||||
"react-i18next": "^15.5.1",
|
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { CurrentPlanBadge } from './current-plan-badge';
|
import { CurrentPlanBadge } from './current-plan-badge';
|
||||||
import { LineItemDetails } from './line-item-details';
|
import { LineItemDetails } from './line-item-details';
|
||||||
|
|
||||||
type Order = Tables<'orders'>;
|
type Order = Tables<{ schema: 'medreport' }, 'orders'>;
|
||||||
type LineItem = Tables<'order_items'>;
|
type LineItem = Tables<{ schema: 'medreport' }, 'order_items'>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
order: Order & {
|
order: Order & {
|
||||||
|
|||||||
@@ -18,8 +18,8 @@ import { CurrentPlanAlert } from './current-plan-alert';
|
|||||||
import { CurrentPlanBadge } from './current-plan-badge';
|
import { CurrentPlanBadge } from './current-plan-badge';
|
||||||
import { LineItemDetails } from './line-item-details';
|
import { LineItemDetails } from './line-item-details';
|
||||||
|
|
||||||
type Subscription = Tables<'subscriptions'>;
|
type Subscription = Tables<{ schema: 'medreport' }, 'subscriptions'>;
|
||||||
type LineItem = Tables<'subscription_items'>;
|
type LineItem = Tables<{ schema: 'medreport' }, 'subscription_items'>;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
subscription: Subscription & {
|
subscription: Subscription & {
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ class BillingEventHandlerService {
|
|||||||
logger.info(ctx, 'Processing subscription deleted event...');
|
logger.info(ctx, 'Processing subscription deleted event...');
|
||||||
|
|
||||||
const { error } = await client
|
const { error } = await client
|
||||||
|
.schema('medreport')
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.delete()
|
.delete()
|
||||||
.match({ id: subscriptionId });
|
.match({ id: subscriptionId });
|
||||||
@@ -109,7 +110,7 @@ class BillingEventHandlerService {
|
|||||||
logger.info(ctx, 'Successfully deleted subscription');
|
logger.info(ctx, 'Successfully deleted subscription');
|
||||||
},
|
},
|
||||||
onSubscriptionUpdated: async (subscription) => {
|
onSubscriptionUpdated: async (subscription) => {
|
||||||
const client = this.clientProvider();
|
const client = this.clientProvider().schema('medreport');
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
@@ -147,7 +148,7 @@ class BillingEventHandlerService {
|
|||||||
onCheckoutSessionCompleted: async (payload) => {
|
onCheckoutSessionCompleted: async (payload) => {
|
||||||
// Handle the checkout session completed event
|
// Handle the checkout session completed event
|
||||||
// here we add the subscription to the database
|
// here we add the subscription to the database
|
||||||
const client = this.clientProvider();
|
const client = this.clientProvider().schema('medreport');
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
// Check if the payload contains an order_id
|
// Check if the payload contains an order_id
|
||||||
@@ -212,7 +213,7 @@ class BillingEventHandlerService {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
onPaymentSucceeded: async (sessionId: string) => {
|
onPaymentSucceeded: async (sessionId: string) => {
|
||||||
const client = this.clientProvider();
|
const client = this.clientProvider().schema('medreport');
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
@@ -244,7 +245,7 @@ class BillingEventHandlerService {
|
|||||||
logger.info(ctx, 'Successfully updated payment status');
|
logger.info(ctx, 'Successfully updated payment status');
|
||||||
},
|
},
|
||||||
onPaymentFailed: async (sessionId: string) => {
|
onPaymentFailed: async (sessionId: string) => {
|
||||||
const client = this.clientProvider();
|
const client = this.clientProvider().schema('medreport');
|
||||||
const logger = await getLogger();
|
const logger = await getLogger();
|
||||||
|
|
||||||
const ctx = {
|
const ctx = {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export async function getBillingGatewayProvider(
|
|||||||
|
|
||||||
async function getBillingProvider(client: SupabaseClient<Database>) {
|
async function getBillingProvider(client: SupabaseClient<Database>) {
|
||||||
const { data, error } = await client
|
const { data, error } = await client
|
||||||
|
.schema('medreport')
|
||||||
.from('config')
|
.from('config')
|
||||||
.select('billing_provider')
|
.select('billing_provider')
|
||||||
.single();
|
.single();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { Tables } from '@kit/supabase/database';
|
|||||||
|
|
||||||
import { createBillingGatewayService } from '../billing-gateway/billing-gateway.service';
|
import { createBillingGatewayService } from '../billing-gateway/billing-gateway.service';
|
||||||
|
|
||||||
type Subscription = Tables<'subscriptions'>;
|
type Subscription = Tables<{ schema: 'medreport' }, 'subscriptions'>;
|
||||||
|
|
||||||
export function createBillingWebhooksService() {
|
export function createBillingWebhooksService() {
|
||||||
return new BillingWebhooksService();
|
return new BillingWebhooksService();
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@types/react": "19.1.4",
|
"@types/react": "19.1.4",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0"
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ import { createLemonSqueezySubscriptionPayloadBuilderService } from './lemon-squ
|
|||||||
import { createHmac } from './verify-hmac';
|
import { createHmac } from './verify-hmac';
|
||||||
|
|
||||||
type UpsertSubscriptionParams =
|
type UpsertSubscriptionParams =
|
||||||
Database['public']['Functions']['upsert_subscription']['Args'] & {
|
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
|
||||||
line_items: Array<LineItem>;
|
line_items: Array<LineItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
type UpsertOrderParams =
|
type UpsertOrderParams =
|
||||||
Database['public']['Functions']['upsert_order']['Args'];
|
Database['medreport']['Functions']['upsert_order']['Args'];
|
||||||
|
|
||||||
type BillingProvider = Enums<'billing_provider'>;
|
type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>;
|
||||||
|
|
||||||
interface LineItem {
|
interface LineItem {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -30,8 +30,7 @@
|
|||||||
"@types/react": "19.1.4",
|
"@types/react": "19.1.4",
|
||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0"
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { createStripeClient } from './stripe-sdk';
|
|||||||
import { createStripeSubscriptionPayloadBuilderService } from './stripe-subscription-payload-builder.service';
|
import { createStripeSubscriptionPayloadBuilderService } from './stripe-subscription-payload-builder.service';
|
||||||
|
|
||||||
type UpsertSubscriptionParams =
|
type UpsertSubscriptionParams =
|
||||||
Database['public']['Functions']['upsert_subscription']['Args'] & {
|
Database['medreport']['Functions']['upsert_subscription']['Args'] & {
|
||||||
line_items: Array<LineItem>;
|
line_items: Array<LineItem>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ interface LineItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UpsertOrderParams =
|
type UpsertOrderParams =
|
||||||
Database['public']['Functions']['upsert_order']['Args'];
|
Database['medreport']['Functions']['upsert_order']['Args'];
|
||||||
|
|
||||||
type BillingProvider = Enums<'billing_provider'>;
|
type BillingProvider = Enums<{ schema: 'medreport' }, 'billing_provider'>;
|
||||||
|
|
||||||
export class StripeWebhookHandlerService
|
export class StripeWebhookHandlerService
|
||||||
implements BillingWebhookHandlerService
|
implements BillingWebhookHandlerService
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
"@kit/ui": "workspace:*",
|
"@kit/ui": "workspace:*",
|
||||||
"@types/node": "^22.15.18",
|
"@types/node": "^22.15.18",
|
||||||
"@types/react": "19.1.4",
|
"@types/react": "19.1.4",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0"
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -22,8 +22,7 @@
|
|||||||
"@kit/supabase": "workspace:*",
|
"@kit/supabase": "workspace:*",
|
||||||
"@kit/team-accounts": "workspace:*",
|
"@kit/team-accounts": "workspace:*",
|
||||||
"@kit/tsconfig": "workspace:*",
|
"@kit/tsconfig": "workspace:*",
|
||||||
"@supabase/supabase-js": "2.49.4",
|
"@supabase/supabase-js": "2.49.4"
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
"*": {
|
"*": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
|
|
||||||
export type Tables = Database['public']['Tables'];
|
export type Tables = Database['medreport']['Tables'];
|
||||||
|
|
||||||
export type TableChangeType = 'INSERT' | 'UPDATE' | 'DELETE';
|
export type TableChangeType = 'INSERT' | 'UPDATE' | 'DELETE';
|
||||||
|
|
||||||
|
|||||||
@@ -44,9 +44,7 @@
|
|||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"sonner": "^2.0.3"
|
||||||
"sonner": "^2.0.3",
|
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
"prettier": "@kit/prettier-config",
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ import { Trans } from '@kit/ui/trans';
|
|||||||
import { useUpdateAccountData } from '../../hooks/use-update-account';
|
import { useUpdateAccountData } from '../../hooks/use-update-account';
|
||||||
import { AccountDetailsSchema } from '../../schema/account-details.schema';
|
import { AccountDetailsSchema } from '../../schema/account-details.schema';
|
||||||
|
|
||||||
type UpdateUserDataParams = Database['public']['Tables']['accounts']['Update'];
|
type UpdateUserDataParams =
|
||||||
|
Database['medreport']['Tables']['accounts']['Update'];
|
||||||
|
|
||||||
export function UpdateAccountDetailsForm({
|
export function UpdateAccountDetailsForm({
|
||||||
displayName,
|
displayName,
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ function UploadProfileAvatarForm(props: {
|
|||||||
uploadUserProfilePhoto(client, file, props.userId)
|
uploadUserProfilePhoto(client, file, props.userId)
|
||||||
.then((pictureUrl) => {
|
.then((pictureUrl) => {
|
||||||
return client
|
return client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.update({
|
.update({
|
||||||
picture_url: pictureUrl,
|
picture_url: pictureUrl,
|
||||||
@@ -90,6 +91,7 @@ function UploadProfileAvatarForm(props: {
|
|||||||
removeExistingStorageFile()
|
removeExistingStorageFile()
|
||||||
.then(() => {
|
.then(() => {
|
||||||
return client
|
return client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.update({
|
.update({
|
||||||
picture_url: null,
|
picture_url: null,
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ interface UserWorkspace {
|
|||||||
id: string | null;
|
id: string | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
picture_url: string | null;
|
picture_url: string | null;
|
||||||
subscription_status: Tables<'subscriptions'>['status'] | null;
|
subscription_status:
|
||||||
|
| Tables<{ schema: 'medreport' }, 'subscriptions'>['status']
|
||||||
|
| null;
|
||||||
};
|
};
|
||||||
|
|
||||||
user: User;
|
user: User;
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export function usePersonalAccountData(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const response = await client
|
const response = await client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select()
|
.select()
|
||||||
.eq('primary_owner_user_id', userId)
|
.eq('primary_owner_user_id', userId)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation } from '@tanstack/react-query';
|
|||||||
import { Database } from '@kit/supabase/database';
|
import { Database } from '@kit/supabase/database';
|
||||||
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
import { useSupabase } from '@kit/supabase/hooks/use-supabase';
|
||||||
|
|
||||||
type UpdateData = Database['public']['Tables']['accounts']['Update'];
|
type UpdateData = Database['medreport']['Tables']['accounts']['Update'];
|
||||||
|
|
||||||
export function useUpdateAccountData(accountId: string) {
|
export function useUpdateAccountData(accountId: string) {
|
||||||
const client = useSupabase();
|
const client = useSupabase();
|
||||||
@@ -11,9 +11,13 @@ export function useUpdateAccountData(accountId: string) {
|
|||||||
const mutationKey = ['account:data', accountId];
|
const mutationKey = ['account:data', accountId];
|
||||||
|
|
||||||
const mutationFn = async (data: UpdateData) => {
|
const mutationFn = async (data: UpdateData) => {
|
||||||
const response = await client.from('accounts').update(data).match({
|
const response = await client
|
||||||
id: accountId,
|
.schema('medreport')
|
||||||
});
|
.from('accounts')
|
||||||
|
.update(data)
|
||||||
|
.match({
|
||||||
|
id: accountId,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw response.error;
|
throw response.error;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ class AccountsApi {
|
|||||||
*/
|
*/
|
||||||
async getAccount(id: string) {
|
async getAccount(id: string) {
|
||||||
const { data, error } = await this.client
|
const { data, error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select('*')
|
.select('*')
|
||||||
.eq('id', id)
|
.eq('id', id)
|
||||||
@@ -35,6 +36,7 @@ class AccountsApi {
|
|||||||
*/
|
*/
|
||||||
async getAccountWorkspace() {
|
async getAccountWorkspace() {
|
||||||
const { data, error } = await this.client
|
const { data, error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('user_account_workspace')
|
.from('user_account_workspace')
|
||||||
.select(`*`)
|
.select(`*`)
|
||||||
.single();
|
.single();
|
||||||
@@ -56,39 +58,43 @@ class AccountsApi {
|
|||||||
const { data, error: userError } = authUser;
|
const { data, error: userError } = authUser;
|
||||||
|
|
||||||
if (userError) {
|
if (userError) {
|
||||||
|
console.error('Failed to get user', userError);
|
||||||
throw userError;
|
throw userError;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { user } = data;
|
const { user } = data;
|
||||||
|
|
||||||
const { data: accounts, error } = await this.client
|
const { data: accounts, error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
.select(
|
.select(
|
||||||
`
|
`
|
||||||
account_id,
|
account_id,
|
||||||
user_accounts (
|
accounts (
|
||||||
name,
|
name,
|
||||||
slug,
|
slug,
|
||||||
picture_url,
|
picture_url
|
||||||
)
|
)
|
||||||
`,
|
`,
|
||||||
)
|
)
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.eq('account_role', 'owner');
|
.eq('account_role', 'owner');
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
console.error('error', error);
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
|
|
||||||
return accounts.map(({ user_accounts }) => ({
|
return accounts.map(({ accounts }) => ({
|
||||||
label: user_accounts.name,
|
label: accounts.name,
|
||||||
value: user_accounts.slug,
|
value: accounts.slug,
|
||||||
image: user_accounts.picture_url,
|
image: accounts.picture_url,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadTempUserAccounts() {
|
async loadTempUserAccounts() {
|
||||||
const { data: accounts, error } = await this.client
|
const { data: accounts, error } = await this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('user_accounts')
|
.from('user_accounts')
|
||||||
.select(`name, slug`);
|
.select(`name, slug`);
|
||||||
|
|
||||||
@@ -129,6 +135,7 @@ class AccountsApi {
|
|||||||
*/
|
*/
|
||||||
async getOrder(accountId: string) {
|
async getOrder(accountId: string) {
|
||||||
const response = await this.client
|
const response = await this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('orders')
|
.from('orders')
|
||||||
.select('*, items: order_items !inner (*)')
|
.select('*, items: order_items !inner (*)')
|
||||||
.eq('account_id', accountId)
|
.eq('account_id', accountId)
|
||||||
@@ -149,6 +156,7 @@ class AccountsApi {
|
|||||||
*/
|
*/
|
||||||
async getCustomerId(accountId: string) {
|
async getCustomerId(accountId: string) {
|
||||||
const response = await this.client
|
const response = await this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('billing_customers')
|
.from('billing_customers')
|
||||||
.select('customer_id')
|
.select('customer_id')
|
||||||
.eq('account_id', accountId)
|
.eq('account_id', accountId)
|
||||||
|
|||||||
@@ -28,8 +28,7 @@
|
|||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3"
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./src/index.ts",
|
".": "./src/index.ts",
|
||||||
|
|||||||
@@ -3,6 +3,11 @@ import { BadgeX, Ban, ShieldPlus, VenetianMask } from 'lucide-react';
|
|||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
import { getSupabaseServerAdminClient } from '@kit/supabase/server-admin-client';
|
||||||
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
import { getSupabaseServerClient } from '@kit/supabase/server-client';
|
||||||
|
import {
|
||||||
|
AccountInvitationsTable,
|
||||||
|
AccountMembersTable,
|
||||||
|
InviteMembersDialogContainer,
|
||||||
|
} from '@kit/team-accounts/components';
|
||||||
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
import { Alert, AlertDescription, AlertTitle } from '@kit/ui/alert';
|
||||||
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
import { AppBreadcrumbs } from '@kit/ui/app-breadcrumbs';
|
||||||
import { Badge } from '@kit/ui/badge';
|
import { Badge } from '@kit/ui/badge';
|
||||||
@@ -28,14 +33,8 @@ import { AdminMembersTable } from './admin-members-table';
|
|||||||
import { AdminMembershipsTable } from './admin-memberships-table';
|
import { AdminMembershipsTable } from './admin-memberships-table';
|
||||||
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
|
import { AdminReactivateUserDialog } from './admin-reactivate-user-dialog';
|
||||||
|
|
||||||
import {
|
type Account = Tables<{ schema: 'medreport' }, 'accounts'>;
|
||||||
AccountInvitationsTable,
|
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'>;
|
||||||
AccountMembersTable,
|
|
||||||
InviteMembersDialogContainer,
|
|
||||||
} from '@kit/team-accounts/components';
|
|
||||||
|
|
||||||
type Account = Tables<'accounts'>;
|
|
||||||
type Membership = Tables<'accounts_memberships'>;
|
|
||||||
|
|
||||||
export function AdminAccountPage(props: {
|
export function AdminAccountPage(props: {
|
||||||
account: Account & { memberships: Membership[] };
|
account: Account & { memberships: Membership[] };
|
||||||
@@ -231,6 +230,7 @@ async function SubscriptionsTable(props: { accountId: string }) {
|
|||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const { data: subscription, error } = await client
|
const { data: subscription, error } = await client
|
||||||
|
.schema('medreport')
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select('*, subscription_items !inner (*)')
|
.select('*, subscription_items !inner (*)')
|
||||||
.eq('account_id', props.accountId)
|
.eq('account_id', props.accountId)
|
||||||
@@ -372,6 +372,7 @@ async function getMemberships(userId: string) {
|
|||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const memberships = await client
|
const memberships = await client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts_memberships')
|
.from('accounts_memberships')
|
||||||
.select<
|
.select<
|
||||||
string,
|
string,
|
||||||
@@ -394,7 +395,7 @@ async function getMemberships(userId: string) {
|
|||||||
async function getMembers(accountSlug: string) {
|
async function getMembers(accountSlug: string) {
|
||||||
const client = getSupabaseServerClient();
|
const client = getSupabaseServerClient();
|
||||||
|
|
||||||
const members = await client.rpc('get_account_members', {
|
const members = await client.schema('medreport').rpc('get_account_members', {
|
||||||
account_slug: accountSlug,
|
account_slug: accountSlug,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ import { AdminDeleteUserDialog } from './admin-delete-user-dialog';
|
|||||||
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
import { AdminImpersonateUserDialog } from './admin-impersonate-user-dialog';
|
||||||
import { AdminResetPasswordDialog } from './admin-reset-password-dialog';
|
import { AdminResetPasswordDialog } from './admin-reset-password-dialog';
|
||||||
|
|
||||||
type Account = Database['public']['Tables']['accounts']['Row'];
|
type Account = Database['medreport']['Tables']['accounts']['Row'];
|
||||||
|
|
||||||
const FiltersSchema = z.object({
|
const FiltersSchema = z.object({
|
||||||
type: z.enum(['all', 'team', 'personal']),
|
type: z.enum(['all', 'team', 'personal']),
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { DataTable } from '@kit/ui/enhanced-data-table';
|
|||||||
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
import { ProfileAvatar } from '@kit/ui/profile-avatar';
|
||||||
|
|
||||||
type Memberships =
|
type Memberships =
|
||||||
Database['public']['Functions']['get_account_members']['Returns'][number];
|
Database['medreport']['Functions']['get_account_members']['Returns'][number];
|
||||||
|
|
||||||
export function AdminMembersTable(props: { members: Memberships[] }) {
|
export function AdminMembersTable(props: { members: Memberships[] }) {
|
||||||
return <DataTable data={props.members} columns={getColumns()} />;
|
return <DataTable data={props.members} columns={getColumns()} />;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { ColumnDef } from '@tanstack/react-table';
|
|||||||
import { Tables } from '@kit/supabase/database';
|
import { Tables } from '@kit/supabase/database';
|
||||||
import { DataTable } from '@kit/ui/enhanced-data-table';
|
import { DataTable } from '@kit/ui/enhanced-data-table';
|
||||||
|
|
||||||
type Membership = Tables<'accounts_memberships'> & {
|
type Membership = Tables<{ schema: 'medreport' }, 'accounts_memberships'> & {
|
||||||
account: {
|
account: {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ import {
|
|||||||
ImpersonateUserSchema,
|
ImpersonateUserSchema,
|
||||||
ReactivateUserSchema,
|
ReactivateUserSchema,
|
||||||
} from './schema/admin-actions.schema';
|
} from './schema/admin-actions.schema';
|
||||||
|
import { CreateCompanySchema } from './schema/create-company.schema';
|
||||||
import { CreateUserSchema } from './schema/create-user.schema';
|
import { CreateUserSchema } from './schema/create-user.schema';
|
||||||
import { ResetPasswordSchema } from './schema/reset-password.schema';
|
import { ResetPasswordSchema } from './schema/reset-password.schema';
|
||||||
import { createAdminAccountsService } from './services/admin-accounts.service';
|
import { createAdminAccountsService } from './services/admin-accounts.service';
|
||||||
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
import { createAdminAuthUserService } from './services/admin-auth-user.service';
|
||||||
import { adminAction } from './utils/admin-action';
|
|
||||||
import { CreateCompanySchema } from './schema/create-company.schema';
|
|
||||||
import { createCreateCompanyAccountService } from './services/admin-create-company-account.service';
|
import { createCreateCompanyAccountService } from './services/admin-create-company-account.service';
|
||||||
|
import { adminAction } from './utils/admin-action';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @name banUserAction
|
* @name banUserAction
|
||||||
@@ -183,12 +183,16 @@ export const createUserAction = adminAction(
|
|||||||
);
|
);
|
||||||
|
|
||||||
const { error: accountError } = await adminClient
|
const { error: accountError } = await adminClient
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.update({ personal_code: personalCode })
|
.update({ personal_code: personalCode })
|
||||||
.eq('id', data.user.id);
|
.eq('id', data.user.id);
|
||||||
|
|
||||||
if (accountError) {
|
if (accountError) {
|
||||||
logger.error({ accountError }, 'Error inserting personal code to accounts');
|
logger.error(
|
||||||
|
{ accountError },
|
||||||
|
'Error inserting personal code to accounts',
|
||||||
|
);
|
||||||
throw new Error(`Error saving personal code: ${accountError.message}`);
|
throw new Error(`Error saving personal code: ${accountError.message}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class AdminAccountsService {
|
|||||||
|
|
||||||
async deleteAccount(accountId: string) {
|
async deleteAccount(accountId: string) {
|
||||||
const { error } = await this.adminClient
|
const { error } = await this.adminClient
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.delete()
|
.delete()
|
||||||
.eq('id', accountId);
|
.eq('id', accountId);
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ export class AdminDashboardService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const subscriptionsPromise = this.client
|
const subscriptionsPromise = this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select('*', selectParams)
|
.select('*', selectParams)
|
||||||
.eq('status', 'active')
|
.eq('status', 'active')
|
||||||
@@ -47,6 +48,7 @@ export class AdminDashboardService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const trialsPromise = this.client
|
const trialsPromise = this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('subscriptions')
|
.from('subscriptions')
|
||||||
.select('*', selectParams)
|
.select('*', selectParams)
|
||||||
.eq('status', 'trialing')
|
.eq('status', 'trialing')
|
||||||
@@ -64,6 +66,7 @@ export class AdminDashboardService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const accountsPromise = this.client
|
const accountsPromise = this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select('*', selectParams)
|
.select('*', selectParams)
|
||||||
.eq('is_personal_account', true)
|
.eq('is_personal_account', true)
|
||||||
@@ -81,6 +84,7 @@ export class AdminDashboardService {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const teamAccountsPromise = this.client
|
const teamAccountsPromise = this.client
|
||||||
|
.schema('medreport')
|
||||||
.from('accounts')
|
.from('accounts')
|
||||||
.select('*', selectParams)
|
.select('*', selectParams)
|
||||||
.eq('is_personal_account', false)
|
.eq('is_personal_account', false)
|
||||||
|
|||||||
@@ -35,9 +35,7 @@
|
|||||||
"lucide-react": "^0.510.0",
|
"lucide-react": "^0.510.0",
|
||||||
"next": "15.3.2",
|
"next": "15.3.2",
|
||||||
"react-hook-form": "^7.56.3",
|
"react-hook-form": "^7.56.3",
|
||||||
"react-i18next": "^15.5.1",
|
"sonner": "^2.0.3"
|
||||||
"sonner": "^2.0.3",
|
|
||||||
"zod": "^3.24.4"
|
|
||||||
},
|
},
|
||||||
"prettier": "@kit/prettier-config",
|
"prettier": "@kit/prettier-config",
|
||||||
"typesVersions": {
|
"typesVersions": {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ export function MultiFactorChallengeContainer({
|
|||||||
|
|
||||||
const verifyMFAChallenge = useVerifyMFAChallenge({
|
const verifyMFAChallenge = useVerifyMFAChallenge({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
router.replace(paths.redirectPath);
|
router.replace('/');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -55,12 +55,11 @@ export function SignInMethodsContainer(props: {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { data: hasConsentPersonalData } = await client.rpc(
|
const { data: hasConsentPersonalData } = await client
|
||||||
'has_consent_personal_data',
|
.schema('medreport')
|
||||||
{
|
.rpc('has_consent_personal_data', {
|
||||||
account_id: userId,
|
account_id: userId,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (hasConsentPersonalData) {
|
if (hasConsentPersonalData) {
|
||||||
router.replace(props.paths.returnPath);
|
router.replace(props.paths.returnPath);
|
||||||
|
|||||||
@@ -25,12 +25,11 @@ class AuthApi {
|
|||||||
throw new Error('User not authenticated');
|
throw new Error('User not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { data, error } = await this.client.rpc(
|
const { data, error } = await this.client
|
||||||
'has_unseen_membership_confirmation',
|
.schema('medreport')
|
||||||
{
|
.rpc('has_unseen_membership_confirmation', {
|
||||||
p_user_id: user.id,
|
p_user_id: user.id,
|
||||||
},
|
});
|
||||||
);
|
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -53,15 +52,17 @@ class AuthApi {
|
|||||||
throw new Error('User not authenticated');
|
throw new Error('User not authenticated');
|
||||||
}
|
}
|
||||||
|
|
||||||
const { error } = await this.client.rpc('update_account', {
|
const { error } = await this.client
|
||||||
p_name: data.firstName,
|
.schema('medreport')
|
||||||
p_last_name: data.lastName,
|
.rpc('update_account', {
|
||||||
p_personal_code: data.personalCode,
|
p_name: data.firstName,
|
||||||
p_phone: data.phone || '',
|
p_last_name: data.lastName,
|
||||||
p_city: data.city || '',
|
p_personal_code: data.personalCode,
|
||||||
p_has_consent_personal_data: data.userConsent,
|
p_phone: data.phone || '',
|
||||||
p_uid: user.id,
|
p_city: data.city || '',
|
||||||
});
|
p_has_consent_personal_data: data.userConsent,
|
||||||
|
p_uid: user.id,
|
||||||
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -85,12 +86,15 @@ class AuthApi {
|
|||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error('User not authenticated');
|
throw new Error('User not authenticated');
|
||||||
}
|
}
|
||||||
console.log('test', user, data);
|
|
||||||
const response = await this.client.from('account_params').insert({
|
const response = await this.client
|
||||||
account_id: user.id,
|
.schema('medreport')
|
||||||
height: data.height,
|
.from('account_params')
|
||||||
weight: data.weight,
|
.insert({
|
||||||
});
|
account_id: user.id,
|
||||||
|
height: data.height,
|
||||||
|
weight: data.weight,
|
||||||
|
});
|
||||||
|
|
||||||
if (response.error) {
|
if (response.error) {
|
||||||
throw response.error;
|
throw response.error;
|
||||||
|
|||||||
125
packages/features/medusa-storefront/README.md
Normal file
125
packages/features/medusa-storefront/README.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
<p align="center">
|
||||||
|
<a href="https://www.medusajs.com">
|
||||||
|
<picture>
|
||||||
|
<source media="(prefers-color-scheme: dark)" srcset="https://user-images.githubusercontent.com/59018053/229103275-b5e482bb-4601-46e6-8142-244f531cebdb.svg">
|
||||||
|
<source media="(prefers-color-scheme: light)" srcset="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
|
||||||
|
<img alt="Medusa logo" src="https://user-images.githubusercontent.com/59018053/229103726-e5b529a3-9b3f-4970-8a1f-c6af37f087bf.svg">
|
||||||
|
</picture>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<h1 align="center">
|
||||||
|
Medusa Next.js Starter Template
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
Combine Medusa's modules for your commerce backend with the newest Next.js 15 features for a performant storefront.</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/medusajs/medusa/blob/master/CONTRIBUTING.md">
|
||||||
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat" alt="PRs welcome!" />
|
||||||
|
</a>
|
||||||
|
<a href="https://discord.gg/xpCwq3Kfn8">
|
||||||
|
<img src="https://img.shields.io/badge/chat-on%20discord-7289DA.svg" alt="Discord Chat" />
|
||||||
|
</a>
|
||||||
|
<a href="https://twitter.com/intent/follow?screen_name=medusajs">
|
||||||
|
<img src="https://img.shields.io/twitter/follow/medusajs.svg?label=Follow%20@medusajs" alt="Follow @medusajs" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
To use the [Next.js Starter Template](https://medusajs.com/nextjs-commerce/), you should have a Medusa server running locally on port 9000.
|
||||||
|
For a quick setup, run:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
npx create-medusa-app@latest
|
||||||
|
```
|
||||||
|
|
||||||
|
Check out [create-medusa-app docs](https://docs.medusajs.com/learn/installation) for more details and troubleshooting.
|
||||||
|
|
||||||
|
# Overview
|
||||||
|
|
||||||
|
The Medusa Next.js Starter is built with:
|
||||||
|
|
||||||
|
- [Next.js](https://nextjs.org/)
|
||||||
|
- [Tailwind CSS](https://tailwindcss.com/)
|
||||||
|
- [Typescript](https://www.typescriptlang.org/)
|
||||||
|
- [Medusa](https://medusajs.com/)
|
||||||
|
|
||||||
|
Features include:
|
||||||
|
|
||||||
|
- Full ecommerce support:
|
||||||
|
- Product Detail Page
|
||||||
|
- Product Overview Page
|
||||||
|
- Product Collections
|
||||||
|
- Cart
|
||||||
|
- Checkout with Stripe
|
||||||
|
- User Accounts
|
||||||
|
- Order Details
|
||||||
|
- Full Next.js 15 support:
|
||||||
|
- App Router
|
||||||
|
- Next fetching/caching
|
||||||
|
- Server Components
|
||||||
|
- Server Actions
|
||||||
|
- Streaming
|
||||||
|
- Static Pre-Rendering
|
||||||
|
|
||||||
|
# Quickstart
|
||||||
|
|
||||||
|
### Setting up the environment variables
|
||||||
|
|
||||||
|
Navigate into your projects directory and get your environment variables ready:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
cd nextjs-starter-medusa/
|
||||||
|
mv .env.template .env.local
|
||||||
|
```
|
||||||
|
|
||||||
|
### Install dependencies
|
||||||
|
|
||||||
|
Use Yarn to install all dependencies.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn
|
||||||
|
```
|
||||||
|
|
||||||
|
### Start developing
|
||||||
|
|
||||||
|
You are now ready to start up your project.
|
||||||
|
|
||||||
|
```shell
|
||||||
|
yarn dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Open the code and start customizing
|
||||||
|
|
||||||
|
Your site is now running at http://localhost:8000!
|
||||||
|
|
||||||
|
# Payment integrations
|
||||||
|
|
||||||
|
By default this starter supports the following payment integrations
|
||||||
|
|
||||||
|
- [Stripe](https://stripe.com/)
|
||||||
|
|
||||||
|
To enable the integrations you need to add the following to your `.env.local` file:
|
||||||
|
|
||||||
|
```shell
|
||||||
|
NEXT_PUBLIC_STRIPE_KEY=<your-stripe-public-key>
|
||||||
|
```
|
||||||
|
|
||||||
|
You'll also need to setup the integrations in your Medusa server. See the [Medusa documentation](https://docs.medusajs.com) for more information on how to configure [Stripe](https://docs.medusajs.com/resources/commerce-modules/payment/payment-provider/stripe#main).
|
||||||
|
|
||||||
|
# Resources
|
||||||
|
|
||||||
|
## Learn more about Medusa
|
||||||
|
|
||||||
|
- [Website](https://www.medusajs.com/)
|
||||||
|
- [GitHub](https://github.com/medusajs)
|
||||||
|
- [Documentation](https://docs.medusajs.com/)
|
||||||
|
|
||||||
|
## Learn more about Next.js
|
||||||
|
|
||||||
|
- [Website](https://nextjs.org/)
|
||||||
|
- [GitHub](https://github.com/vercel/next.js)
|
||||||
|
- [Documentation](https://nextjs.org/docs)
|
||||||
39
packages/features/medusa-storefront/check-env-variables.js
Normal file
39
packages/features/medusa-storefront/check-env-variables.js
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
const c = require("ansi-colors")
|
||||||
|
|
||||||
|
const requiredEnvs = [
|
||||||
|
{
|
||||||
|
key: "NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY",
|
||||||
|
// TODO: we need a good doc to point this to
|
||||||
|
description:
|
||||||
|
"Learn how to create a publishable key: https://docs.medusajs.com/v2/resources/storefront-development/publishable-api-keys",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
function checkEnvVariables() {
|
||||||
|
const missingEnvs = requiredEnvs.filter(function (env) {
|
||||||
|
return !process.env[env.key]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (missingEnvs.length > 0) {
|
||||||
|
console.error(
|
||||||
|
c.red.bold("\n🚫 Error: Missing required environment variables\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
missingEnvs.forEach(function (env) {
|
||||||
|
console.error(c.yellow(` ${c.bold(env.key)}`))
|
||||||
|
if (env.description) {
|
||||||
|
console.error(c.dim(` ${env.description}\n`))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
console.error(
|
||||||
|
c.yellow(
|
||||||
|
"\nPlease set these variables in your .env file or environment before starting the application.\n"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
process.exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = checkEnvVariables
|
||||||
60
packages/features/medusa-storefront/package.json
Normal file
60
packages/features/medusa-storefront/package.json
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
{
|
||||||
|
"name": "medusa-next",
|
||||||
|
"version": "1.0.3",
|
||||||
|
"private": true,
|
||||||
|
"author": "Kasper Fabricius Kristensen <kasper@medusajs.com> & Victor Gerbrands <victor@medusajs.com> (https://www.medusajs.com)",
|
||||||
|
"description": "Next.js Starter to be used with Medusa V2",
|
||||||
|
"keywords": [
|
||||||
|
"medusa-storefront"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev --turbopack -p 8000",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start -p 8000",
|
||||||
|
"lint": "next lint",
|
||||||
|
"analyze": "ANALYZE=true next build"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@headlessui/react": "^2.2.0",
|
||||||
|
"@medusajs/js-sdk": "latest",
|
||||||
|
"@medusajs/ui": "latest",
|
||||||
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
|
"@stripe/react-stripe-js": "^1.7.2",
|
||||||
|
"@stripe/stripe-js": "^1.29.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
|
"next": "^15.3.1",
|
||||||
|
"pg": "^8.11.3",
|
||||||
|
"qs": "^6.12.1",
|
||||||
|
"react": "19.0.0-rc-66855b96-20241106",
|
||||||
|
"react-country-flag": "^3.1.0",
|
||||||
|
"react-dom": "19.0.0-rc-66855b96-20241106",
|
||||||
|
"server-only": "^0.0.1",
|
||||||
|
"tailwindcss-radix": "^2.8.0",
|
||||||
|
"webpack": "^5"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/core": "^7.17.5",
|
||||||
|
"@medusajs/types": "latest",
|
||||||
|
"@medusajs/ui-preset": "latest",
|
||||||
|
"@types/lodash": "^4.14.195",
|
||||||
|
"@types/node": "17.0.21",
|
||||||
|
"@types/pg": "^8.11.0",
|
||||||
|
"@types/react": "^18.3.12",
|
||||||
|
"@types/react-dom": "^18.3.1",
|
||||||
|
"@types/react-instantsearch-dom": "^6.12.3",
|
||||||
|
"ansi-colors": "^4.1.3",
|
||||||
|
"autoprefixer": "^10.4.2",
|
||||||
|
"babel-loader": "^8.2.3",
|
||||||
|
"eslint": "8.10.0",
|
||||||
|
"eslint-config-next": "15.0.3",
|
||||||
|
"postcss": "^8.4.8",
|
||||||
|
"prettier": "^2.8.8",
|
||||||
|
"tailwindcss": "^3.0.23",
|
||||||
|
"typescript": "^5.3.2"
|
||||||
|
},
|
||||||
|
"packageManager": "yarn@3.2.3",
|
||||||
|
"overrides": {
|
||||||
|
"react": "19.0.0-rc-66855b96-20241106",
|
||||||
|
"react-dom": "19.0.0-rc-66855b96-20241106"
|
||||||
|
}
|
||||||
|
}
|
||||||
14
packages/features/medusa-storefront/src/lib/config.ts
Normal file
14
packages/features/medusa-storefront/src/lib/config.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import Medusa from "@medusajs/js-sdk"
|
||||||
|
|
||||||
|
// Defaults to standard port for Medusa server
|
||||||
|
let MEDUSA_BACKEND_URL = "http://localhost:9000"
|
||||||
|
|
||||||
|
if (process.env.MEDUSA_BACKEND_URL) {
|
||||||
|
MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL
|
||||||
|
}
|
||||||
|
|
||||||
|
export const sdk = new Medusa({
|
||||||
|
baseUrl: MEDUSA_BACKEND_URL,
|
||||||
|
debug: process.env.NODE_ENV === "development",
|
||||||
|
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
|
||||||
|
})
|
||||||
68
packages/features/medusa-storefront/src/lib/constants.tsx
Normal file
68
packages/features/medusa-storefront/src/lib/constants.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import React from "react"
|
||||||
|
import { CreditCard } from "@medusajs/icons"
|
||||||
|
|
||||||
|
import Ideal from "@modules/common/icons/ideal"
|
||||||
|
import Bancontact from "@modules/common/icons/bancontact"
|
||||||
|
import PayPal from "@modules/common/icons/paypal"
|
||||||
|
|
||||||
|
/* Map of payment provider_id to their title and icon. Add in any payment providers you want to use. */
|
||||||
|
export const paymentInfoMap: Record<
|
||||||
|
string,
|
||||||
|
{ title: string; icon: React.JSX.Element }
|
||||||
|
> = {
|
||||||
|
pp_stripe_stripe: {
|
||||||
|
title: "Credit card",
|
||||||
|
icon: <CreditCard />,
|
||||||
|
},
|
||||||
|
"pp_stripe-ideal_stripe": {
|
||||||
|
title: "iDeal",
|
||||||
|
icon: <Ideal />,
|
||||||
|
},
|
||||||
|
"pp_stripe-bancontact_stripe": {
|
||||||
|
title: "Bancontact",
|
||||||
|
icon: <Bancontact />,
|
||||||
|
},
|
||||||
|
pp_paypal_paypal: {
|
||||||
|
title: "PayPal",
|
||||||
|
icon: <PayPal />,
|
||||||
|
},
|
||||||
|
pp_system_default: {
|
||||||
|
title: "Manual Payment",
|
||||||
|
icon: <CreditCard />,
|
||||||
|
},
|
||||||
|
// Add more payment providers here
|
||||||
|
}
|
||||||
|
|
||||||
|
// This only checks if it is native stripe for card payments, it ignores the other stripe-based providers
|
||||||
|
export const isStripe = (providerId?: string) => {
|
||||||
|
return providerId?.startsWith("pp_stripe_")
|
||||||
|
}
|
||||||
|
export const isPaypal = (providerId?: string) => {
|
||||||
|
return providerId?.startsWith("pp_paypal")
|
||||||
|
}
|
||||||
|
export const isManual = (providerId?: string) => {
|
||||||
|
return providerId?.startsWith("pp_system_default")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add currencies that don't need to be divided by 100
|
||||||
|
export const noDivisionCurrencies = [
|
||||||
|
"krw",
|
||||||
|
"jpy",
|
||||||
|
"vnd",
|
||||||
|
"clp",
|
||||||
|
"pyg",
|
||||||
|
"xaf",
|
||||||
|
"xof",
|
||||||
|
"bif",
|
||||||
|
"djf",
|
||||||
|
"gnf",
|
||||||
|
"kmf",
|
||||||
|
"mga",
|
||||||
|
"rwf",
|
||||||
|
"xpf",
|
||||||
|
"htg",
|
||||||
|
"vuv",
|
||||||
|
"xag",
|
||||||
|
"xdr",
|
||||||
|
"xau",
|
||||||
|
]
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
"use client"
|
||||||
|
|
||||||
|
import React, { createContext, useContext } from "react"
|
||||||
|
|
||||||
|
interface ModalContext {
|
||||||
|
close: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalContext = createContext<ModalContext | null>(null)
|
||||||
|
|
||||||
|
interface ModalProviderProps {
|
||||||
|
children?: React.ReactNode
|
||||||
|
close: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ModalProvider = ({ children, close }: ModalProviderProps) => {
|
||||||
|
return (
|
||||||
|
<ModalContext.Provider
|
||||||
|
value={{
|
||||||
|
close,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ModalContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useModal = () => {
|
||||||
|
const context = useContext(ModalContext)
|
||||||
|
if (context === null) {
|
||||||
|
throw new Error("useModal must be used within a ModalProvider")
|
||||||
|
}
|
||||||
|
return context
|
||||||
|
}
|
||||||
472
packages/features/medusa-storefront/src/lib/data/cart.ts
Normal file
472
packages/features/medusa-storefront/src/lib/data/cart.ts
Normal file
@@ -0,0 +1,472 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import medusaError from "@lib/util/medusa-error";
|
||||||
|
import { HttpTypes } from "@medusajs/types";
|
||||||
|
import { revalidateTag } from "next/cache";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import {
|
||||||
|
getAuthHeaders,
|
||||||
|
getCacheOptions,
|
||||||
|
getCacheTag,
|
||||||
|
getCartId,
|
||||||
|
removeCartId,
|
||||||
|
setCartId,
|
||||||
|
} from "./cookies";
|
||||||
|
import { getRegion } from "./regions";
|
||||||
|
import { sdk } from "@lib/config";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves a cart by its ID. If no ID is provided, it will use the cart ID from the cookies.
|
||||||
|
* @param cartId - optional - The ID of the cart to retrieve.
|
||||||
|
* @returns The cart object if found, or null if not found.
|
||||||
|
*/
|
||||||
|
export async function retrieveCart(cartId?: string) {
|
||||||
|
const id = cartId || (await getCartId());
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("carts")),
|
||||||
|
};
|
||||||
|
|
||||||
|
return await sdk.client
|
||||||
|
.fetch<HttpTypes.StoreCartResponse>(`/store/carts/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
fields:
|
||||||
|
"*items, *region, *items.product, *items.variant, *items.thumbnail, *items.metadata, +items.total, *promotions, +shipping_methods.name",
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
})
|
||||||
|
.then(({ cart }) => cart)
|
||||||
|
.catch(() => null);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getOrSetCart(countryCode: string) {
|
||||||
|
const region = await getRegion(countryCode);
|
||||||
|
|
||||||
|
if (!region) {
|
||||||
|
throw new Error(`Region not found for country code: ${countryCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cart = await retrieveCart();
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
const cartResp = await sdk.store.cart.create(
|
||||||
|
{ region_id: region.id },
|
||||||
|
{},
|
||||||
|
headers
|
||||||
|
);
|
||||||
|
cart = cartResp.cart;
|
||||||
|
|
||||||
|
await setCartId(cart.id);
|
||||||
|
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cart && cart?.region_id !== region.id) {
|
||||||
|
await sdk.store.cart.update(cart.id, { region_id: region.id }, {}, headers);
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateCart(data: HttpTypes.StoreUpdateCart) {
|
||||||
|
const cartId = await getCartId();
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
throw new Error(
|
||||||
|
"No existing cart found, please create one before updating"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return sdk.store.cart
|
||||||
|
.update(cartId, data, {}, headers)
|
||||||
|
.then(async ({ cart }) => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
|
||||||
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
|
||||||
|
return cart;
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function addToCart({
|
||||||
|
variantId,
|
||||||
|
quantity,
|
||||||
|
countryCode,
|
||||||
|
}: {
|
||||||
|
variantId: string;
|
||||||
|
quantity: number;
|
||||||
|
countryCode: string;
|
||||||
|
}) {
|
||||||
|
if (!variantId) {
|
||||||
|
throw new Error("Missing variant ID when adding to cart");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cart = await getOrSetCart(countryCode);
|
||||||
|
|
||||||
|
if (!cart) {
|
||||||
|
throw new Error("Error retrieving or creating cart");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
await sdk.store.cart
|
||||||
|
.createLineItem(
|
||||||
|
cart.id,
|
||||||
|
{
|
||||||
|
variant_id: variantId,
|
||||||
|
quantity,
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
.then(async () => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
|
||||||
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateLineItem({
|
||||||
|
lineId,
|
||||||
|
quantity,
|
||||||
|
}: {
|
||||||
|
lineId: string;
|
||||||
|
quantity: number;
|
||||||
|
}) {
|
||||||
|
if (!lineId) {
|
||||||
|
throw new Error("Missing lineItem ID when updating line item");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartId = await getCartId();
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
throw new Error("Missing cart ID when updating line item");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
await sdk.store.cart
|
||||||
|
.updateLineItem(cartId, lineId, { quantity }, {}, headers)
|
||||||
|
.then(async () => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
|
||||||
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteLineItem(lineId: string) {
|
||||||
|
if (!lineId) {
|
||||||
|
throw new Error("Missing lineItem ID when deleting line item");
|
||||||
|
}
|
||||||
|
|
||||||
|
const cartId = await getCartId();
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
throw new Error("Missing cart ID when deleting line item");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
await sdk.store.cart
|
||||||
|
.deleteLineItem(cartId, lineId, headers)
|
||||||
|
.then(async () => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
|
||||||
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function setShippingMethod({
|
||||||
|
cartId,
|
||||||
|
shippingMethodId,
|
||||||
|
}: {
|
||||||
|
cartId: string;
|
||||||
|
shippingMethodId: string;
|
||||||
|
}) {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return sdk.store.cart
|
||||||
|
.addShippingMethod(cartId, { option_id: shippingMethodId }, {}, headers)
|
||||||
|
.then(async () => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initiatePaymentSession(
|
||||||
|
cart: HttpTypes.StoreCart,
|
||||||
|
data: HttpTypes.StoreInitializePaymentSession
|
||||||
|
) {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return sdk.store.payment
|
||||||
|
.initiatePaymentSession(cart, data, {}, headers)
|
||||||
|
.then(async (resp) => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
return resp;
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyPromotions(codes: string[]) {
|
||||||
|
const cartId = await getCartId();
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
throw new Error("No existing cart found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
return sdk.store.cart
|
||||||
|
.update(cartId, { promo_codes: codes }, {}, headers)
|
||||||
|
.then(async () => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
|
||||||
|
const fulfillmentCacheTag = await getCacheTag("fulfillment");
|
||||||
|
revalidateTag(fulfillmentCacheTag);
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function applyGiftCard(code: string) {
|
||||||
|
// const cartId = getCartId()
|
||||||
|
// if (!cartId) return "No cartId cookie found"
|
||||||
|
// try {
|
||||||
|
// await updateCart(cartId, { gift_cards: [{ code }] }).then(() => {
|
||||||
|
// revalidateTag("cart")
|
||||||
|
// })
|
||||||
|
// } catch (error: any) {
|
||||||
|
// throw error
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeDiscount(code: string) {
|
||||||
|
// const cartId = getCartId()
|
||||||
|
// if (!cartId) return "No cartId cookie found"
|
||||||
|
// try {
|
||||||
|
// await deleteDiscount(cartId, code)
|
||||||
|
// revalidateTag("cart")
|
||||||
|
// } catch (error: any) {
|
||||||
|
// throw error
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function removeGiftCard(
|
||||||
|
codeToRemove: string,
|
||||||
|
giftCards: any[]
|
||||||
|
// giftCards: GiftCard[]
|
||||||
|
) {
|
||||||
|
// const cartId = getCartId()
|
||||||
|
// if (!cartId) return "No cartId cookie found"
|
||||||
|
// try {
|
||||||
|
// await updateCart(cartId, {
|
||||||
|
// gift_cards: [...giftCards]
|
||||||
|
// .filter((gc) => gc.code !== codeToRemove)
|
||||||
|
// .map((gc) => ({ code: gc.code })),
|
||||||
|
// }).then(() => {
|
||||||
|
// revalidateTag("cart")
|
||||||
|
// })
|
||||||
|
// } catch (error: any) {
|
||||||
|
// throw error
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function submitPromotionForm(
|
||||||
|
currentState: unknown,
|
||||||
|
formData: FormData
|
||||||
|
) {
|
||||||
|
const code = formData.get("code") as string;
|
||||||
|
try {
|
||||||
|
await applyPromotions([code]);
|
||||||
|
} catch (e: any) {
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Pass a POJO instead of a form entity here
|
||||||
|
export async function setAddresses(currentState: unknown, formData: FormData) {
|
||||||
|
try {
|
||||||
|
if (!formData) {
|
||||||
|
throw new Error("No form data found when setting addresses");
|
||||||
|
}
|
||||||
|
const cartId = getCartId();
|
||||||
|
if (!cartId) {
|
||||||
|
throw new Error("No existing cart found when setting addresses");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = {
|
||||||
|
shipping_address: {
|
||||||
|
first_name: formData.get("shipping_address.first_name"),
|
||||||
|
last_name: formData.get("shipping_address.last_name"),
|
||||||
|
address_1: formData.get("shipping_address.address_1"),
|
||||||
|
address_2: "",
|
||||||
|
company: formData.get("shipping_address.company"),
|
||||||
|
postal_code: formData.get("shipping_address.postal_code"),
|
||||||
|
city: formData.get("shipping_address.city"),
|
||||||
|
country_code: formData.get("shipping_address.country_code"),
|
||||||
|
province: formData.get("shipping_address.province"),
|
||||||
|
phone: formData.get("shipping_address.phone"),
|
||||||
|
},
|
||||||
|
email: formData.get("email"),
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const sameAsBilling = formData.get("same_as_billing");
|
||||||
|
if (sameAsBilling === "on") data.billing_address = data.shipping_address;
|
||||||
|
|
||||||
|
if (sameAsBilling !== "on")
|
||||||
|
data.billing_address = {
|
||||||
|
first_name: formData.get("billing_address.first_name"),
|
||||||
|
last_name: formData.get("billing_address.last_name"),
|
||||||
|
address_1: formData.get("billing_address.address_1"),
|
||||||
|
address_2: "",
|
||||||
|
company: formData.get("billing_address.company"),
|
||||||
|
postal_code: formData.get("billing_address.postal_code"),
|
||||||
|
city: formData.get("billing_address.city"),
|
||||||
|
country_code: formData.get("billing_address.country_code"),
|
||||||
|
province: formData.get("billing_address.province"),
|
||||||
|
phone: formData.get("billing_address.phone"),
|
||||||
|
};
|
||||||
|
await updateCart(data);
|
||||||
|
} catch (e: any) {
|
||||||
|
return e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
redirect(
|
||||||
|
`/${formData.get("shipping_address.country_code")}/checkout?step=delivery`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Places an order for a cart. If no cart ID is provided, it will use the cart ID from the cookies.
|
||||||
|
* @param cartId - optional - The ID of the cart to place an order for.
|
||||||
|
* @returns The cart object if the order was successful, or null if not.
|
||||||
|
*/
|
||||||
|
export async function placeOrder(cartId?: string) {
|
||||||
|
const id = cartId || (await getCartId());
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
throw new Error("No existing cart found when placing an order");
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
|
||||||
|
const cartRes = await sdk.store.cart
|
||||||
|
.complete(id, {}, headers)
|
||||||
|
.then(async (cartRes) => {
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
return cartRes;
|
||||||
|
})
|
||||||
|
.catch(medusaError);
|
||||||
|
|
||||||
|
if (cartRes?.type === "order") {
|
||||||
|
const countryCode =
|
||||||
|
cartRes.order.shipping_address?.country_code?.toLowerCase();
|
||||||
|
|
||||||
|
const orderCacheTag = await getCacheTag("orders");
|
||||||
|
revalidateTag(orderCacheTag);
|
||||||
|
|
||||||
|
removeCartId();
|
||||||
|
redirect(`/${countryCode}/order/${cartRes?.order.id}/confirmed`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return cartRes.cart;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the countrycode param and revalidates the regions cache
|
||||||
|
* @param regionId
|
||||||
|
* @param countryCode
|
||||||
|
*/
|
||||||
|
export async function updateRegion(countryCode: string, currentPath: string) {
|
||||||
|
const cartId = await getCartId();
|
||||||
|
const region = await getRegion(countryCode);
|
||||||
|
|
||||||
|
if (!region) {
|
||||||
|
throw new Error(`Region not found for country code: ${countryCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cartId) {
|
||||||
|
await updateCart({ region_id: region.id });
|
||||||
|
const cartCacheTag = await getCacheTag("carts");
|
||||||
|
revalidateTag(cartCacheTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
const regionCacheTag = await getCacheTag("regions");
|
||||||
|
revalidateTag(regionCacheTag);
|
||||||
|
|
||||||
|
const productsCacheTag = await getCacheTag("products");
|
||||||
|
revalidateTag(productsCacheTag);
|
||||||
|
|
||||||
|
redirect(`/${countryCode}${currentPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function listCartOptions() {
|
||||||
|
const cartId = await getCartId();
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
};
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("shippingOptions")),
|
||||||
|
};
|
||||||
|
|
||||||
|
return await sdk.client.fetch<{
|
||||||
|
shipping_options: HttpTypes.StoreCartShippingOption[];
|
||||||
|
}>("/store/shipping-options", {
|
||||||
|
query: { cart_id: cartId },
|
||||||
|
next,
|
||||||
|
headers,
|
||||||
|
cache: "force-cache",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import { sdk } from "@lib/config"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { getCacheOptions } from "./cookies"
|
||||||
|
|
||||||
|
export const listCategories = async (query?: Record<string, any>) => {
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("categories")),
|
||||||
|
}
|
||||||
|
|
||||||
|
const limit = query?.limit || 100
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<{ product_categories: HttpTypes.StoreProductCategory[] }>(
|
||||||
|
"/store/product-categories",
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
fields:
|
||||||
|
"*category_children, *products, *parent_category, *parent_category.parent_category",
|
||||||
|
limit,
|
||||||
|
...query,
|
||||||
|
},
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ product_categories }) => product_categories)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCategoryByHandle = async (categoryHandle: string[]) => {
|
||||||
|
const handle = `${categoryHandle.join("/")}`
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("categories")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<HttpTypes.StoreProductCategoryListResponse>(
|
||||||
|
`/store/product-categories`,
|
||||||
|
{
|
||||||
|
query: {
|
||||||
|
fields: "*category_children, *products",
|
||||||
|
handle,
|
||||||
|
},
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ product_categories }) => product_categories[0])
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { sdk } from "@lib/config"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { getCacheOptions } from "./cookies"
|
||||||
|
|
||||||
|
export const retrieveCollection = async (id: string) => {
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("collections")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<{ collection: HttpTypes.StoreCollection }>(
|
||||||
|
`/store/collections/${id}`,
|
||||||
|
{
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ collection }) => collection)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listCollections = async (
|
||||||
|
queryParams: Record<string, string> = {}
|
||||||
|
): Promise<{ collections: HttpTypes.StoreCollection[]; count: number }> => {
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("collections")),
|
||||||
|
}
|
||||||
|
|
||||||
|
queryParams.limit = queryParams.limit || "100"
|
||||||
|
queryParams.offset = queryParams.offset || "0"
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<{ collections: HttpTypes.StoreCollection[]; count: number }>(
|
||||||
|
"/store/collections",
|
||||||
|
{
|
||||||
|
query: queryParams,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ collections }) => ({ collections, count: collections.length }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCollectionByHandle = async (
|
||||||
|
handle: string
|
||||||
|
): Promise<HttpTypes.StoreCollection> => {
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("collections")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<HttpTypes.StoreCollectionListResponse>(`/store/collections`, {
|
||||||
|
query: { handle, fields: "*products" },
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
})
|
||||||
|
.then(({ collections }) => collections[0])
|
||||||
|
}
|
||||||
89
packages/features/medusa-storefront/src/lib/data/cookies.ts
Normal file
89
packages/features/medusa-storefront/src/lib/data/cookies.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import "server-only"
|
||||||
|
import { cookies as nextCookies } from "next/headers"
|
||||||
|
|
||||||
|
export const getAuthHeaders = async (): Promise<
|
||||||
|
{ authorization: string } | {}
|
||||||
|
> => {
|
||||||
|
try {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
const token = cookies.get("_medusa_jwt")?.value
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { authorization: `Bearer ${token}` }
|
||||||
|
} catch {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCacheTag = async (tag: string): Promise<string> => {
|
||||||
|
try {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
const cacheId = cookies.get("_medusa_cache_id")?.value
|
||||||
|
|
||||||
|
if (!cacheId) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${tag}-${cacheId}`
|
||||||
|
} catch (error) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCacheOptions = async (
|
||||||
|
tag: string
|
||||||
|
): Promise<{ tags: string[] } | {}> => {
|
||||||
|
if (typeof window !== "undefined") {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
const cacheTag = await getCacheTag(tag)
|
||||||
|
|
||||||
|
if (!cacheTag) {
|
||||||
|
return {}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { tags: [`${cacheTag}`] }
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setAuthToken = async (token: string) => {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
cookies.set("_medusa_jwt", token, {
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeAuthToken = async () => {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
cookies.set("_medusa_jwt", "", {
|
||||||
|
maxAge: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getCartId = async () => {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
return cookies.get("_medusa_cart_id")?.value
|
||||||
|
}
|
||||||
|
|
||||||
|
export const setCartId = async (cartId: string) => {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
cookies.set("_medusa_cart_id", cartId, {
|
||||||
|
maxAge: 60 * 60 * 24 * 7,
|
||||||
|
httpOnly: true,
|
||||||
|
sameSite: "strict",
|
||||||
|
secure: process.env.NODE_ENV === "production",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const removeCartId = async () => {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
cookies.set("_medusa_cart_id", "", {
|
||||||
|
maxAge: -1,
|
||||||
|
})
|
||||||
|
}
|
||||||
261
packages/features/medusa-storefront/src/lib/data/customer.ts
Normal file
261
packages/features/medusa-storefront/src/lib/data/customer.ts
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { sdk } from "@lib/config"
|
||||||
|
import medusaError from "@lib/util/medusa-error"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { revalidateTag } from "next/cache"
|
||||||
|
import { redirect } from "next/navigation"
|
||||||
|
import {
|
||||||
|
getAuthHeaders,
|
||||||
|
getCacheOptions,
|
||||||
|
getCacheTag,
|
||||||
|
getCartId,
|
||||||
|
removeAuthToken,
|
||||||
|
removeCartId,
|
||||||
|
setAuthToken,
|
||||||
|
} from "./cookies"
|
||||||
|
|
||||||
|
export const retrieveCustomer =
|
||||||
|
async (): Promise<HttpTypes.StoreCustomer | null> => {
|
||||||
|
const authHeaders = await getAuthHeaders()
|
||||||
|
|
||||||
|
if (!authHeaders) return null
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...authHeaders,
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("customers")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return await sdk.client
|
||||||
|
.fetch<{ customer: HttpTypes.StoreCustomer }>(`/store/customers/me`, {
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
fields: "*orders",
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
})
|
||||||
|
.then(({ customer }) => customer)
|
||||||
|
.catch(() => null)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateCustomer = async (body: HttpTypes.StoreUpdateCustomer) => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateRes = await sdk.store.customer
|
||||||
|
.update(body, {}, headers)
|
||||||
|
.then(({ customer }) => customer)
|
||||||
|
.catch(medusaError)
|
||||||
|
|
||||||
|
const cacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(cacheTag)
|
||||||
|
|
||||||
|
return updateRes
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signup(_currentState: unknown, formData: FormData) {
|
||||||
|
const password = formData.get("password") as string
|
||||||
|
const customerForm = {
|
||||||
|
email: formData.get("email") as string,
|
||||||
|
first_name: formData.get("first_name") as string,
|
||||||
|
last_name: formData.get("last_name") as string,
|
||||||
|
phone: formData.get("phone") as string,
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const token = await sdk.auth.register("customer", "emailpass", {
|
||||||
|
email: customerForm.email,
|
||||||
|
password: password,
|
||||||
|
})
|
||||||
|
|
||||||
|
await setAuthToken(token as string)
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const { customer: createdCustomer } = await sdk.store.customer.create(
|
||||||
|
customerForm,
|
||||||
|
{},
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
|
||||||
|
const loginToken = await sdk.auth.login("customer", "emailpass", {
|
||||||
|
email: customerForm.email,
|
||||||
|
password,
|
||||||
|
})
|
||||||
|
|
||||||
|
await setAuthToken(loginToken as string)
|
||||||
|
|
||||||
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(customerCacheTag)
|
||||||
|
|
||||||
|
await transferCart()
|
||||||
|
|
||||||
|
return createdCustomer
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function login(_currentState: unknown, formData: FormData) {
|
||||||
|
const email = formData.get("email") as string
|
||||||
|
const password = formData.get("password") as string
|
||||||
|
|
||||||
|
try {
|
||||||
|
await sdk.auth
|
||||||
|
.login("customer", "emailpass", { email, password })
|
||||||
|
.then(async (token) => {
|
||||||
|
await setAuthToken(token as string)
|
||||||
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(customerCacheTag)
|
||||||
|
})
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transferCart()
|
||||||
|
} catch (error: any) {
|
||||||
|
return error.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function signout(countryCode: string) {
|
||||||
|
await sdk.auth.logout()
|
||||||
|
|
||||||
|
await removeAuthToken()
|
||||||
|
|
||||||
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(customerCacheTag)
|
||||||
|
|
||||||
|
await removeCartId()
|
||||||
|
|
||||||
|
const cartCacheTag = await getCacheTag("carts")
|
||||||
|
revalidateTag(cartCacheTag)
|
||||||
|
|
||||||
|
redirect(`/${countryCode}/account`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function transferCart() {
|
||||||
|
const cartId = await getCartId()
|
||||||
|
|
||||||
|
if (!cartId) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = await getAuthHeaders()
|
||||||
|
|
||||||
|
await sdk.store.cart.transferCart(cartId, {}, headers)
|
||||||
|
|
||||||
|
const cartCacheTag = await getCacheTag("carts")
|
||||||
|
revalidateTag(cartCacheTag)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const addCustomerAddress = async (
|
||||||
|
currentState: Record<string, unknown>,
|
||||||
|
formData: FormData
|
||||||
|
): Promise<any> => {
|
||||||
|
const isDefaultBilling = (currentState.isDefaultBilling as boolean) || false
|
||||||
|
const isDefaultShipping = (currentState.isDefaultShipping as boolean) || false
|
||||||
|
|
||||||
|
const address = {
|
||||||
|
first_name: formData.get("first_name") as string,
|
||||||
|
last_name: formData.get("last_name") as string,
|
||||||
|
company: formData.get("company") as string,
|
||||||
|
address_1: formData.get("address_1") as string,
|
||||||
|
address_2: formData.get("address_2") as string,
|
||||||
|
city: formData.get("city") as string,
|
||||||
|
postal_code: formData.get("postal_code") as string,
|
||||||
|
province: formData.get("province") as string,
|
||||||
|
country_code: formData.get("country_code") as string,
|
||||||
|
phone: formData.get("phone") as string,
|
||||||
|
is_default_billing: isDefaultBilling,
|
||||||
|
is_default_shipping: isDefaultShipping,
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.store.customer
|
||||||
|
.createAddress(address, {}, headers)
|
||||||
|
.then(async ({ customer }) => {
|
||||||
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(customerCacheTag)
|
||||||
|
return { success: true, error: null }
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return { success: false, error: err.toString() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const deleteCustomerAddress = async (
|
||||||
|
addressId: string
|
||||||
|
): Promise<void> => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
await sdk.store.customer
|
||||||
|
.deleteAddress(addressId, headers)
|
||||||
|
.then(async () => {
|
||||||
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(customerCacheTag)
|
||||||
|
return { success: true, error: null }
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return { success: false, error: err.toString() }
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const updateCustomerAddress = async (
|
||||||
|
currentState: Record<string, unknown>,
|
||||||
|
formData: FormData
|
||||||
|
): Promise<any> => {
|
||||||
|
const addressId =
|
||||||
|
(currentState.addressId as string) || (formData.get("addressId") as string)
|
||||||
|
|
||||||
|
if (!addressId) {
|
||||||
|
return { success: false, error: "Address ID is required" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const address = {
|
||||||
|
first_name: formData.get("first_name") as string,
|
||||||
|
last_name: formData.get("last_name") as string,
|
||||||
|
company: formData.get("company") as string,
|
||||||
|
address_1: formData.get("address_1") as string,
|
||||||
|
address_2: formData.get("address_2") as string,
|
||||||
|
city: formData.get("city") as string,
|
||||||
|
postal_code: formData.get("postal_code") as string,
|
||||||
|
province: formData.get("province") as string,
|
||||||
|
country_code: formData.get("country_code") as string,
|
||||||
|
} as HttpTypes.StoreUpdateCustomerAddress
|
||||||
|
|
||||||
|
const phone = formData.get("phone") as string
|
||||||
|
|
||||||
|
if (phone) {
|
||||||
|
address.phone = phone
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.store.customer
|
||||||
|
.updateAddress(addressId, address, {}, headers)
|
||||||
|
.then(async () => {
|
||||||
|
const customerCacheTag = await getCacheTag("customers")
|
||||||
|
revalidateTag(customerCacheTag)
|
||||||
|
return { success: true, error: null }
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
return { success: false, error: err.toString() }
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { sdk } from "@lib/config"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||||
|
|
||||||
|
export const listCartShippingMethods = async (cartId: string) => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("fulfillment")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<HttpTypes.StoreShippingOptionListResponse>(
|
||||||
|
`/store/shipping-options`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
cart_id: cartId,
|
||||||
|
fields:
|
||||||
|
"+service_zone.fulfllment_set.type,*service_zone.fulfillment_set.location.address",
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ shipping_options }) => shipping_options)
|
||||||
|
.catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export const calculatePriceForShippingOption = async (
|
||||||
|
optionId: string,
|
||||||
|
cartId: string,
|
||||||
|
data?: Record<string, unknown>
|
||||||
|
) => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("fulfillment")),
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = { cart_id: cartId, data }
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
body.data = data
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<{ shipping_option: HttpTypes.StoreCartShippingOption }>(
|
||||||
|
`/store/shipping-options/${optionId}/calculate`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body,
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ shipping_option }) => shipping_option)
|
||||||
|
.catch((e) => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
11
packages/features/medusa-storefront/src/lib/data/index.ts
Normal file
11
packages/features/medusa-storefront/src/lib/data/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
export * from "./cart";
|
||||||
|
export * from "./categories";
|
||||||
|
export * from "./collections";
|
||||||
|
export * from "./cookies";
|
||||||
|
export * from "./customer";
|
||||||
|
export * from "./fulfillment";
|
||||||
|
export * from "./onboarding";
|
||||||
|
export * from "./orders";
|
||||||
|
export * from "./payment";
|
||||||
|
export * from "./products";
|
||||||
|
export * from "./regions";
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
"use server"
|
||||||
|
import { cookies as nextCookies } from "next/headers"
|
||||||
|
import { redirect } from "next/navigation"
|
||||||
|
|
||||||
|
export async function resetOnboardingState(orderId: string) {
|
||||||
|
const cookies = await nextCookies()
|
||||||
|
cookies.set("_medusa_onboarding", "false", { maxAge: -1 })
|
||||||
|
redirect(`http://localhost:7001/a/orders/${orderId}`)
|
||||||
|
}
|
||||||
112
packages/features/medusa-storefront/src/lib/data/orders.ts
Normal file
112
packages/features/medusa-storefront/src/lib/data/orders.ts
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { sdk } from "@lib/config"
|
||||||
|
import medusaError from "@lib/util/medusa-error"
|
||||||
|
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export const retrieveOrder = async (id: string) => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("orders")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<HttpTypes.StoreOrderResponse>(`/store/orders/${id}`, {
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
fields:
|
||||||
|
"*payment_collections.payments,*items,*items.metadata,*items.variant,*items.product",
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
})
|
||||||
|
.then(({ order }) => order)
|
||||||
|
.catch((err) => medusaError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const listOrders = async (
|
||||||
|
limit: number = 10,
|
||||||
|
offset: number = 0,
|
||||||
|
filters?: Record<string, any>
|
||||||
|
) => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("orders")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<HttpTypes.StoreOrderListResponse>(`/store/orders`, {
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
order: "-created_at",
|
||||||
|
fields: "*items,+items.metadata,*items.variant,*items.product",
|
||||||
|
...filters,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
})
|
||||||
|
.then(({ orders }) => orders)
|
||||||
|
.catch((err) => medusaError(err))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createTransferRequest = async (
|
||||||
|
state: {
|
||||||
|
success: boolean
|
||||||
|
error: string | null
|
||||||
|
order: HttpTypes.StoreOrder | null
|
||||||
|
},
|
||||||
|
formData: FormData
|
||||||
|
): Promise<{
|
||||||
|
success: boolean
|
||||||
|
error: string | null
|
||||||
|
order: HttpTypes.StoreOrder | null
|
||||||
|
}> => {
|
||||||
|
const id = formData.get("order_id") as string
|
||||||
|
|
||||||
|
if (!id) {
|
||||||
|
return { success: false, error: "Order ID is required", order: null }
|
||||||
|
}
|
||||||
|
|
||||||
|
const headers = await getAuthHeaders()
|
||||||
|
|
||||||
|
return await sdk.store.order
|
||||||
|
.requestTransfer(
|
||||||
|
id,
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
fields: "id, email",
|
||||||
|
},
|
||||||
|
headers
|
||||||
|
)
|
||||||
|
.then(({ order }) => ({ success: true, error: null, order }))
|
||||||
|
.catch((err) => ({ success: false, error: err.message, order: null }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const acceptTransferRequest = async (id: string, token: string) => {
|
||||||
|
const headers = await getAuthHeaders()
|
||||||
|
|
||||||
|
return await sdk.store.order
|
||||||
|
.acceptTransfer(id, { token }, {}, headers)
|
||||||
|
.then(({ order }) => ({ success: true, error: null, order }))
|
||||||
|
.catch((err) => ({ success: false, error: err.message, order: null }))
|
||||||
|
}
|
||||||
|
|
||||||
|
export const declineTransferRequest = async (id: string, token: string) => {
|
||||||
|
const headers = await getAuthHeaders()
|
||||||
|
|
||||||
|
return await sdk.store.order
|
||||||
|
.declineTransfer(id, { token }, {}, headers)
|
||||||
|
.then(({ order }) => ({ success: true, error: null, order }))
|
||||||
|
.catch((err) => ({ success: false, error: err.message, order: null }))
|
||||||
|
}
|
||||||
35
packages/features/medusa-storefront/src/lib/data/payment.ts
Normal file
35
packages/features/medusa-storefront/src/lib/data/payment.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
"use server"
|
||||||
|
|
||||||
|
import { sdk } from "@lib/config"
|
||||||
|
import { getAuthHeaders, getCacheOptions } from "./cookies"
|
||||||
|
import { HttpTypes } from "@medusajs/types"
|
||||||
|
|
||||||
|
export const listCartPaymentMethods = async (regionId: string) => {
|
||||||
|
const headers = {
|
||||||
|
...(await getAuthHeaders()),
|
||||||
|
}
|
||||||
|
|
||||||
|
const next = {
|
||||||
|
...(await getCacheOptions("payment_providers")),
|
||||||
|
}
|
||||||
|
|
||||||
|
return sdk.client
|
||||||
|
.fetch<HttpTypes.StorePaymentProviderListResponse>(
|
||||||
|
`/store/payment-providers`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
query: { region_id: regionId },
|
||||||
|
headers,
|
||||||
|
next,
|
||||||
|
cache: "force-cache",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then(({ payment_providers }) =>
|
||||||
|
payment_providers.sort((a, b) => {
|
||||||
|
return a.id > b.id ? 1 : -1
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.catch(() => {
|
||||||
|
return null
|
||||||
|
})
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user