Merge branch 'main' into feature/MED-100

This commit is contained in:
2025-07-21 11:38:47 +03:00
46 changed files with 6714 additions and 501 deletions

View File

@@ -14,7 +14,8 @@
"./personal-account-settings": "./src/components/personal-account-settings/index.ts",
"./components": "./src/components/index.ts",
"./hooks/*": "./src/hooks/*.ts",
"./api": "./src/server/api.ts"
"./api": "./src/server/api.ts",
"./types/*": "./src/types/*.ts"
},
"dependencies": {
"nanoid": "^5.1.5"
@@ -43,7 +44,6 @@
"next-themes": "0.4.6",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",

View File

@@ -2,6 +2,8 @@ import { SupabaseClient } from '@supabase/supabase-js';
import { Database } from '@kit/supabase/database';
import { UserAnalysis } from '../types/accounts';
/**
* Class representing an API for interacting with user accounts.
* @constructor
@@ -169,6 +171,51 @@ class AccountsApi {
return response.data?.customer_id;
}
async getUserAnalysis(): Promise<UserAnalysis | null> {
const authUser = await this.client.auth.getUser();
const { data, error: userError } = authUser;
if (userError) {
console.error('Failed to get user', userError);
throw userError;
}
const { user } = data;
const { data: analysisResponses } = await this.client
.schema('medreport')
.from('analysis_responses')
.select('*')
.eq('user_id', user.id);
if (!analysisResponses) {
return null;
}
const analysisResponseIds = analysisResponses.map((r) => r.id);
const { data: analysisResponseElements } = await this.client
.schema('medreport')
.from('analysis_response_elements')
.select('*')
.in('analysis_response_id', analysisResponseIds);
if (!analysisResponseElements) {
return null;
}
const elementMap = new Map(
analysisResponseElements.map((e) => [e.analysis_response_id, e]),
);
return analysisResponses
.filter((r) => elementMap.has(r.id))
.map((r) => ({
...r,
element: elementMap.get(r.id)!,
}));
}
}
export function createAccountsApi(client: SupabaseClient<Database>) {

View File

@@ -0,0 +1,6 @@
import { Database } from '@kit/supabase/database';
export type UserAnalysis =
(Database['medreport']['Tables']['analysis_responses']['Row'] & {
element: Database['medreport']['Tables']['analysis_response_elements']['Row'];
})[];

View File

@@ -27,8 +27,7 @@
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3"
"react-dom": "19.1.0"
},
"exports": {
".": "./src/index.ts",

View File

@@ -17,7 +17,8 @@
"./captcha/client": "./src/captcha/client/index.ts",
"./captcha/server": "./src/captcha/server/index.ts",
"./resend-email-link": "./src/components/resend-auth-link-form.tsx",
"./lib/utils/*": "./src/lib/utils/*.ts"
"./lib/utils/*": "./src/lib/utils/*.ts",
"./api": "./src/server/api.ts"
},
"devDependencies": {
"@hookform/resolvers": "^5.0.1",
@@ -34,7 +35,6 @@
"@types/react": "19.1.4",
"lucide-react": "^0.510.0",
"next": "15.3.2",
"react-hook-form": "^7.56.3",
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",

View File

@@ -1,226 +0,0 @@
'use client';
import Link from 'next/link';
import { User } from '@supabase/supabase-js';
import { ExternalLink } from '@/public/assets/external-link';
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { Button } from '@kit/ui/button';
import { Checkbox } from '@kit/ui/checkbox';
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@kit/ui/form';
import { Input } from '@kit/ui/input';
import { Trans } from '@kit/ui/trans';
import { UpdateAccountSchema } from '../schemas/update-account.schema';
import { onUpdateAccount } from '../server/actions/update-account-actions';
export function UpdateAccountForm({ user }: { user: User }) {
const form = useForm({
resolver: zodResolver(UpdateAccountSchema),
mode: 'onChange',
defaultValues: {
firstName: '',
lastName: '',
personalCode: user.user_metadata.personalCode ?? '',
email: user.email,
phone: '',
city: '',
weight: user.user_metadata.weight ?? undefined,
height: user.user_metadata.height ?? undefined,
userConsent: false,
},
});
return (
<Form {...form}>
<form
className="flex flex-col gap-6 px-6 pt-10 text-left"
onSubmit={form.handleSubmit(onUpdateAccount)}
>
<FormField
name="firstName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:firstName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="lastName"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:lastName'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="personalCode"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:personalCode'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:email'} />
</FormLabel>
<FormControl>
<Input {...field} disabled />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="phone"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:phone'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="city"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:city'} />
</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex flex-row justify-between gap-4">
<FormField
name="weight"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:weight'} />
</FormLabel>
<FormControl>
<Input
type="number"
placeholder="kg"
{...field}
value={field.value ?? ''}
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
name="height"
render={({ field }) => (
<FormItem>
<FormLabel>
<Trans i18nKey={'common:formField:height'} />
</FormLabel>
<FormControl>
<Input
placeholder="cm"
type="number"
{...field}
value={field.value ?? ''}
onChange={(e) =>
field.onChange(
e.target.value === '' ? null : Number(e.target.value),
)
}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField
name="userConsent"
render={({ field }) => (
<FormItem>
<div className="flex flex-row items-center gap-2 pb-1">
<FormControl>
<Checkbox
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
<FormLabel>
<Trans i18nKey={'account:updateAccount:userConsentLabel'} />
</FormLabel>
</div>
<Link
href={''}
className="flex flex-row items-center gap-2 text-sm hover:underline"
target="_blank"
>
<ExternalLink />
<Trans i18nKey={'account:updateAccount:userConsentUrlTitle'} />
</Link>
</FormItem>
)}
/>
<Button disabled={form.formState.isSubmitting} type="submit">
<Trans i18nKey={'account:updateAccount:button'} />
</Button>
</form>
</Form>
);
}

View File

@@ -1,44 +0,0 @@
import { z } from 'zod';
export const UpdateAccountSchema = z.object({
firstName: z
.string({
required_error: 'First name is required',
})
.nonempty(),
lastName: z
.string({
required_error: 'Last name is required',
})
.nonempty(),
personalCode: z
.string({
required_error: 'Personal code is required',
})
.nonempty(),
email: z.string().email({
message: 'Email is required',
}),
phone: z
.string({
required_error: 'Phone number is required',
})
.nonempty(),
city: z.string().optional(),
weight: z
.number({
required_error: 'Weight is required',
invalid_type_error: 'Weight must be a number',
})
.gt(0, { message: 'Weight must be greater than 0' }),
height: z
.number({
required_error: 'Height is required',
invalid_type_error: 'Height must be a number',
})
.gt(0, { message: 'Height must be greater than 0' }),
userConsent: z.boolean().refine((val) => val === true, {
message: 'Must be true',
}),
});

View File

@@ -1,59 +0,0 @@
'use server';
import { redirect } from 'next/navigation';
import { enhanceAction } from '@kit/next/actions';
import { getSupabaseServerClient } from '@kit/supabase/server-client';
import pathsConfig from '~/config/paths.config';
import { updateCustomer } from '@lib/data/customer';
import { UpdateAccountSchema } from '../../schemas/update-account.schema';
import { createAuthApi } from '../api';
export interface AccountSubmitData {
firstName: string;
lastName: string;
personalCode: string;
email: string;
phone?: string;
city?: string;
weight: number | null;
height: number | null;
userConsent: boolean;
}
export const onUpdateAccount = enhanceAction(
async (params: AccountSubmitData) => {
const client = getSupabaseServerClient();
const api = createAuthApi(client);
try {
await api.updateAccount(params);
console.log('SUCCESS', pathsConfig.auth.updateAccountSuccess);
} catch (err: unknown) {
if (err instanceof Error) {
console.warn('On update account error: ' + err.message);
}
console.warn('On update account error: ', err);
}
await updateCustomer({
first_name: params.firstName,
last_name: params.lastName,
phone: params.phone,
});
const hasUnseenMembershipConfirmation =
await api.hasUnseenMembershipConfirmation();
if (hasUnseenMembershipConfirmation) {
redirect(pathsConfig.auth.membershipConfirmation);
} else {
redirect(pathsConfig.app.selectPackage);
}
},
{
schema: UpdateAccountSchema,
},
);

View File

@@ -1,14 +1,16 @@
import Medusa from "@medusajs/js-sdk"
import Medusa from "@medusajs/js-sdk";
// Defaults to standard port for Medusa server
let MEDUSA_BACKEND_URL = "http://localhost:9000"
let MEDUSA_BACKEND_URL = "http://localhost:9000";
if (process.env.MEDUSA_BACKEND_URL) {
MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL
MEDUSA_BACKEND_URL = process.env.MEDUSA_BACKEND_URL;
}
export const sdk = new Medusa({
export const SDK_CONFIG = {
baseUrl: MEDUSA_BACKEND_URL,
debug: process.env.NODE_ENV === "development",
publishableKey: process.env.NEXT_PUBLIC_MEDUSA_PUBLISHABLE_KEY,
})
};
export const sdk = new Medusa(SDK_CONFIG);

View File

@@ -1,13 +1,13 @@
"use server"
"use server";
import { sdk } from "@lib/config"
import { HttpTypes } from "@medusajs/types"
import { getCacheOptions } from "./cookies"
import { sdk, SDK_CONFIG } 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 }>(
@@ -17,19 +17,19 @@ export const retrieveCollection = async (id: string) => {
cache: "force-cache",
}
)
.then(({ collection }) => collection)
}
.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"
};
queryParams.limit = queryParams.limit || "100";
queryParams.offset = queryParams.offset || "0";
console.log("SDK_CONFIG: ", SDK_CONFIG.baseUrl);
return sdk.client
.fetch<{ collections: HttpTypes.StoreCollection[]; count: number }>(
"/store/collections",
@@ -39,15 +39,15 @@ export const listCollections = async (
cache: "force-cache",
}
)
.then(({ collections }) => ({ collections, count: collections.length }))
}
.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`, {
@@ -55,5 +55,5 @@ export const getCollectionByHandle = async (
next,
cache: "force-cache",
})
.then(({ collections }) => collections[0])
}
.then(({ collections }) => collections[0]);
};

View File

@@ -38,7 +38,9 @@ export const SuccessNotification = ({
width={326}
height={195}
/>
<h1 className="pb-2">{title || <Trans i18nKey={titleKey} />}</h1>
<h1 className="pb-2 text-center">
{title || <Trans i18nKey={titleKey} />}
</h1>
<p className="text-muted-foreground text-sm">
<Trans i18nKey={descriptionKey} />
</p>

View File

@@ -44,7 +44,6 @@
"next": "15.3.2",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-hook-form": "^7.56.3",
"sonner": "^2.0.3"
},
"prettier": "@kit/prettier-config",