diff --git a/README.md b/README.md index a2cea56..dde3485 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,22 @@ +# MedReport README -## Prerequisites - "node": ">=20.0.0", - "pnpm": ">=9.0.0" +## Prerequisites + +```json +"node": ">=20.0.0", +"pnpm": ">=9.0.0" +``` ## Project structure -``` + +```text / app - pages / components - custom components, helper components that not provided by any package. Place to extend an redefine components from packages -/ config - bunch of configs, that are provided by starter kit. +/ config - bunch of configs, that are provided by starter kit. / content - (temporary?) - to be removed when cleaned all dependencies / fonts - (temporary) - contains fonts, should be relocated to another place (maybe public) / lib - diffirent libs, services, utils - - fonts.ts - project fonts setup, which becomes available as a global css variable + - fonts.ts - project fonts setup, which becomes available as a global css variable / i18n - translations/localization setup / public - public assets / locales - translations under a corresponding local - at a specific namespace @@ -21,26 +26,47 @@ - theme.css - more specific variables, available as tailwindcss property-class - makerkit.css - Makerkit-specific global styles - markdoc.css - Styles for Markdoc Markdown files. - - + - / supabase - primary supabase / tooling - a workspace package, used for generation packages in node_modules and provides global links for its data. The most important is typescript config / utils - - ``` - ## Migration from old structure -```bash + +```bash pnpm clean pnpm i ``` ## Adding new dependency -```bash +```bash pnpm add -w ``` ## Supabase -TODO \ No newline at end of file + +Start supabase in docker + +```bash +npm run supabase:start +``` + +Link your local supabase with a supabase project + +```bash +npm run supabase:deploy +``` + +After editing supabase tables/functions etc update migration files + +```bash +npm run supabase:db:diff +``` + +To update database types run: + +```bash +npm run supabase:typegen:app +``` diff --git a/app/(marketing)/page.tsx b/app/(marketing)/page.tsx index 09dd6bc..1726d34 100644 --- a/app/(marketing)/page.tsx +++ b/app/(marketing)/page.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { MedReportLogo } from '@/components/med-report-title'; +import { MedReportLogo } from '@/components/med-report-logo'; import { ArrowRightIcon } from 'lucide-react'; import { CtaButton, Hero } from '@kit/ui/marketing'; @@ -49,7 +49,7 @@ function MainCallToActionButton() { - + diff --git a/app/(public)/company-offer/page.tsx b/app/(public)/company-offer/page.tsx new file mode 100644 index 0000000..bf68684 --- /dev/null +++ b/app/(public)/company-offer/page.tsx @@ -0,0 +1,102 @@ +'use client'; + +import React from 'react'; + +import { useRouter } from 'next/navigation'; + +import { MedReportLogo } from '@/components/med-report-logo'; +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'; + +export default function CompanyOffer() { + 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); + } + }; + + return ( +
+
+ +

+ +

+

+ +

+
+ + + + + + + + + + + + + + + + + + + +
+
+
+
+ ); +} diff --git a/app/(public)/company-offer/success/page.tsx b/app/(public)/company-offer/success/page.tsx new file mode 100644 index 0000000..6c136c7 --- /dev/null +++ b/app/(public)/company-offer/success/page.tsx @@ -0,0 +1,16 @@ +'use client'; + +import { SuccessNotification } from '@/packages/features/notifications/src/components'; + +export default function CompanyRegistrationSuccess() { + return ( + + ); +} diff --git a/app/(public)/register-company/page.tsx b/app/(public)/register-company/page.tsx deleted file mode 100644 index c4a75df..0000000 --- a/app/(public)/register-company/page.tsx +++ /dev/null @@ -1,89 +0,0 @@ -"use client"; - -import { MedReportLogo } from "@/components/med-report-title"; -import React from "react"; -import { yupResolver } from "@hookform/resolvers/yup"; -import { useForm } from "react-hook-form"; -import { companySchema } from "@/lib/validations/companySchema"; -import { CompanySubmitData } from "@/lib/types/company"; -import { submitCompanyRegistration } from "@/lib/services/register-company.service"; -import { useRouter } from "next/navigation"; -import { Label } from "@kit/ui/label"; -import { Input } from "@kit/ui/input"; -import { SubmitButton } from "@/components/ui/submit-button"; -import { FormItem } from "@kit/ui/form"; -import { Trans } from "@kit/ui/trans"; - -export default function RegisterCompany() { - const router = useRouter(); - const { - register, - handleSubmit, - formState: { errors, isValid, isSubmitting }, - } = useForm({ - resolver: yupResolver(companySchema), - mode: "onChange", - }); - - async function onSubmit(data: CompanySubmitData) { - const formData = new FormData(); - Object.entries(data).forEach(([key, value]) => { - if (value !== undefined) formData.append(key, value); - }); - - try { - await submitCompanyRegistration(formData); - router.push("/register-company/success"); - } catch (err: unknown) { - if (err instanceof Error) { - alert("Server validation error: " + err.message); - } - alert("Server validation error"); - } - } - - return ( -
-
- -

Ettevõtte andmed

-

- Pakkumise saamiseks palun sisesta ettevõtte andmed millega MedReport - kasutada kavatsed. -

-
- - - - - - - - - - - - - - - - - - - -
-
-
-
-
- ); -} diff --git a/app/(public)/register-company/success/page.tsx b/app/(public)/register-company/success/page.tsx deleted file mode 100644 index 3ed223d..0000000 --- a/app/(public)/register-company/success/page.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { MedReportLogo } from "@/components/med-report-title"; -import { Button } from "@kit/ui/button"; -import Image from "next/image"; -import Link from "next/link"; - -export default function CompanyRegistrationSuccess() { - return ( -
- -
- Success -

Päring edukalt saadetud!

-

Saadame teile esimesel võimalusel vastuse

-
- -
- ); -} diff --git a/app/(public)/sign-in/page.tsx b/app/(public)/sign-in/page.tsx index aca9ead..3d64786 100644 --- a/app/(public)/sign-in/page.tsx +++ b/app/(public)/sign-in/page.tsx @@ -1,6 +1,8 @@ +import React from 'react'; + +import Link from 'next/link'; + import { Button } from '@kit/ui/button'; -import Link from "next/link"; -import React from "react"; export default async function SignIn() { return ( @@ -15,7 +17,7 @@ export default async function SignIn() { ID-Kaart ); diff --git a/app/auth/layout.tsx b/app/auth/callback/layout.tsx similarity index 100% rename from app/auth/layout.tsx rename to app/auth/callback/layout.tsx diff --git a/app/auth/confirm/layout.tsx b/app/auth/confirm/layout.tsx new file mode 100644 index 0000000..ff7b33b --- /dev/null +++ b/app/auth/confirm/layout.tsx @@ -0,0 +1,9 @@ +import { AuthLayoutShell } from '@kit/auth/shared'; + +import { AppLogo } from '~/components/app-logo'; + +function AuthLayout({ children }: React.PropsWithChildren) { + return {children}; +} + +export default AuthLayout; diff --git a/app/auth/password-reset/layout.tsx b/app/auth/password-reset/layout.tsx new file mode 100644 index 0000000..ff7b33b --- /dev/null +++ b/app/auth/password-reset/layout.tsx @@ -0,0 +1,9 @@ +import { AuthLayoutShell } from '@kit/auth/shared'; + +import { AppLogo } from '~/components/app-logo'; + +function AuthLayout({ children }: React.PropsWithChildren) { + return {children}; +} + +export default AuthLayout; diff --git a/app/auth/sign-in/layout.tsx b/app/auth/sign-in/layout.tsx new file mode 100644 index 0000000..ff7b33b --- /dev/null +++ b/app/auth/sign-in/layout.tsx @@ -0,0 +1,9 @@ +import { AuthLayoutShell } from '@kit/auth/shared'; + +import { AppLogo } from '~/components/app-logo'; + +function AuthLayout({ children }: React.PropsWithChildren) { + return {children}; +} + +export default AuthLayout; diff --git a/app/auth/sign-in/page.tsx b/app/auth/sign-in/page.tsx index 16ad40c..4fc9223 100644 --- a/app/auth/sign-in/page.tsx +++ b/app/auth/sign-in/page.tsx @@ -1,5 +1,7 @@ import Link from 'next/link'; +import { register } from 'module'; + import { SignInMethodsContainer } from '@kit/auth/sign-in'; import { Button } from '@kit/ui/button'; import { Heading } from '@kit/ui/heading'; @@ -26,7 +28,8 @@ export const generateMetadata = async () => { }; async function SignInPage({ searchParams }: SignInPageProps) { - const { invite_token: inviteToken, next = '' } = await searchParams; + const { invite_token: inviteToken, next = pathsConfig.app.home } = + await searchParams; const signUpPath = pathsConfig.auth.signUp + @@ -36,6 +39,7 @@ async function SignInPage({ searchParams }: SignInPageProps) { callback: pathsConfig.auth.callback, returnPath: next ?? pathsConfig.app.home, joinTeam: pathsConfig.app.joinTeam, + updateAccount: pathsConfig.auth.updateAccount, }; return ( diff --git a/app/auth/sign-up/layout.tsx b/app/auth/sign-up/layout.tsx new file mode 100644 index 0000000..ff7b33b --- /dev/null +++ b/app/auth/sign-up/layout.tsx @@ -0,0 +1,9 @@ +import { AuthLayoutShell } from '@kit/auth/shared'; + +import { AppLogo } from '~/components/app-logo'; + +function AuthLayout({ children }: React.PropsWithChildren) { + return {children}; +} + +export default AuthLayout; diff --git a/app/auth/sign-up/page.tsx b/app/auth/sign-up/page.tsx index 40b1eab..61acc81 100644 --- a/app/auth/sign-up/page.tsx +++ b/app/auth/sign-up/page.tsx @@ -27,6 +27,7 @@ interface Props { const paths = { callback: pathsConfig.auth.callback, appHome: pathsConfig.app.home, + updateAccount: pathsConfig.auth.updateAccount, }; async function SignUpPage({ searchParams }: Props) { diff --git a/app/auth/update-account/layout.tsx b/app/auth/update-account/layout.tsx new file mode 100644 index 0000000..8212f3c --- /dev/null +++ b/app/auth/update-account/layout.tsx @@ -0,0 +1,11 @@ +import { withI18n } from '~/lib/i18n/with-i18n'; + +async function SiteLayout(props: React.PropsWithChildren) { + return ( +
+ {props.children} +
+ ); +} + +export default withI18n(SiteLayout); diff --git a/app/auth/update-account/page.tsx b/app/auth/update-account/page.tsx new file mode 100644 index 0000000..8d0231c --- /dev/null +++ b/app/auth/update-account/page.tsx @@ -0,0 +1,43 @@ +import { redirect } from 'next/navigation'; + +import { BackButton } from '@/components/back-button'; +import { MedReportLogo } from '@/components/med-report-logo'; +import pathsConfig from '@/config/paths.config'; +import { signOutAction } from '@/lib/actions/sign-out'; +import { UpdateAccountForm } from '@/packages/features/auth/src/components/update-account-form'; +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + +import { Trans } from '@kit/ui/trans'; + +import { withI18n } from '~/lib/i18n/with-i18n'; + +async function UpdateAccount() { + const client = getSupabaseServerClient(); + + const { + data: { user }, + } = await client.auth.getUser(); + + if (!user) { + redirect(pathsConfig.auth.signIn); + } + + return ( +
+
+ + +

+ +

+

+ +

+ +
+
+
+ ); +} + +export default withI18n(UpdateAccount); diff --git a/app/auth/update-account/success/page.tsx b/app/auth/update-account/success/page.tsx new file mode 100644 index 0000000..a31b5d2 --- /dev/null +++ b/app/auth/update-account/success/page.tsx @@ -0,0 +1,17 @@ +import { getSupabaseServerClient } from '@/packages/supabase/src/clients/server-client'; + +import { UpdateAccountSuccessNotification } from '@kit/notifications/components'; + +import { withI18n } from '~/lib/i18n/with-i18n'; + +async function UpdateAccountSuccess() { + const client = getSupabaseServerClient(); + + const { + data: { user }, + } = await client.auth.getUser(); + + return ; +} + +export default withI18n(UpdateAccountSuccess); diff --git a/app/auth/verify/layout.tsx b/app/auth/verify/layout.tsx new file mode 100644 index 0000000..ff7b33b --- /dev/null +++ b/app/auth/verify/layout.tsx @@ -0,0 +1,9 @@ +import { AuthLayoutShell } from '@kit/auth/shared'; + +import { AppLogo } from '~/components/app-logo'; + +function AuthLayout({ children }: React.PropsWithChildren) { + return {children}; +} + +export default AuthLayout; diff --git a/app/home/(user)/_components/dashboard.tsx b/app/home/(user)/_components/dashboard.tsx new file mode 100644 index 0000000..f6cc187 --- /dev/null +++ b/app/home/(user)/_components/dashboard.tsx @@ -0,0 +1,237 @@ +'use client'; + +import { InfoTooltip } from '@/components/ui/info-tooltip'; +import { toTitleCase } from '@/lib/utils'; +import { BlendingModeIcon, RulerHorizontalIcon } from '@radix-ui/react-icons'; +import { + Activity, + ChevronRight, + Clock9, + Droplets, + LineChart, + Pill, + Scale, + TrendingUp, + User, +} from 'lucide-react'; + +import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data'; +import { useUserWorkspace } from '@kit/accounts/hooks/use-user-workspace'; +import { Button } from '@kit/ui/button'; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardProps, +} from '@kit/ui/card'; +import { PageDescription } from '@kit/ui/page'; +import { Trans } from '@kit/ui/trans'; +import { cn } from '@kit/ui/utils'; + +const dummyCards = [ + { + title: 'dashboard:gender', + description: 'dashboard:male', + icon: , + iconBg: 'bg-success', + }, + { + title: 'dashboard:age', + description: '43', + icon: , + iconBg: 'bg-success', + }, + { + title: 'dashboard:height', + description: '183', + icon: , + iconBg: 'bg-success', + }, + { + title: 'dashboard:weight', + description: '92kg', + icon: , + iconBg: 'bg-warning', + }, + { + title: 'dashboard:bmi', + description: '27.5', + icon: , + iconBg: 'bg-warning', + }, + { + title: 'dashboard:bloodPressure', + description: '160/98', + icon: , + iconBg: 'bg-warning', + }, + { + title: 'dashboard:cholesterol', + description: '5', + icon: , + iconBg: 'bg-destructive', + }, + { + title: 'dashboard:ldlCholesterol', + description: '3,6', + icon: , + iconBg: 'bg-warning', + }, + { + title: 'Score 2', + description: 'Normis', + icon: , + iconBg: 'bg-success', + }, + { + title: 'dashboard:smoking', + description: 'dashboard:respondToQuestion', + descriptionColor: 'text-primary', + icon: ( + + ), + cardVariant: 'gradient-success' as CardProps['variant'], + }, +]; + +const dummyRecommendations = [ + { + icon: , + color: 'bg-cyan/10 text-cyan', + title: 'Kolesterooli kontroll', + description: 'HDL-kolestrool', + tooltipContent: 'Selgitus', + price: '20,00 €', + buttonText: 'Telli', + }, + { + icon: , + color: 'bg-primary/10 text-primary', + title: 'Kolesterooli kontroll', + tooltipContent: 'Selgitus', + description: 'LDL-Kolesterool', + buttonText: 'Broneeri', + }, + { + icon: , + color: 'bg-destructive/10 text-destructive', + title: 'Vererõhu kontroll', + tooltipContent: 'Selgitus', + description: 'Score-Risk 2', + price: '20,00 €', + buttonText: 'Telli', + }, +]; + +export default function Dashboard() { + const userWorkspace = useUserWorkspace(); + const account = usePersonalAccountData(userWorkspace.user.id); + + return ( + <> +
+

+ + {account?.data?.name ? `, ${toTitleCase(account.data.name)}` : ''} +

+ + : + +
+
+ {dummyCards.map( + ({ + title, + description, + icon, + iconBg, + cardVariant, + descriptionColor, + }) => ( + + +
+ {icon} +
+
+ +
+ +
+ + + +
+
+ ), + )} +
+ + +

+ +

+
+ + {dummyRecommendations.map( + ( + { + icon, + color, + title, + description, + tooltipContent, + price, + buttonText, + }, + index, + ) => { + return ( +
+
+
+ {icon} +
+
+
+ {title} + +
+

+ {description} +

+
+
+
+

{price}

+ +
+
+ ); + }, + )} +
+
+ + ); +} diff --git a/app/home/(user)/_components/home-menu-navigation.tsx b/app/home/(user)/_components/home-menu-navigation.tsx index 6845350..6e05700 100644 --- a/app/home/(user)/_components/home-menu-navigation.tsx +++ b/app/home/(user)/_components/home-menu-navigation.tsx @@ -1,7 +1,12 @@ + +import { Trans } from '@kit/ui/trans'; +import { cn } from '@kit/ui/utils'; + import { AppLogo } from '~/components/app-logo'; import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; -import { Trans } from '@kit/ui/trans'; +import { Search } from '~/components/ui/search'; +import { SIDEBAR_WIDTH } from '../../../../packages/ui/src/shadcn/constants'; // home imports import { UserNotifications } from '../_components/user-notifications'; import { type UserWorkspace } from '../_lib/server/load-user-workspace'; @@ -10,31 +15,33 @@ import { ShoppingCart } from 'lucide-react'; export function HomeMenuNavigation(props: { workspace: UserWorkspace }) { const { workspace, user, accounts } = props.workspace; + return ( -
-
+
+
- {/* searbar */} -
- {/* TODO: implement account budget */} - - {/* TODO: implement cart */} - - + +
+ +
); diff --git a/app/home/(user)/_components/home-sidebar.tsx b/app/home/(user)/_components/home-sidebar.tsx index e434119..e1e53b6 100644 --- a/app/home/(user)/_components/home-sidebar.tsx +++ b/app/home/(user)/_components/home-sidebar.tsx @@ -1,61 +1,29 @@ -import { If } from '@kit/ui/if'; import { Sidebar, SidebarContent, - SidebarFooter, SidebarHeader, SidebarNavigation, } from '@kit/ui/shadcn-sidebar'; -import { cn } from '@kit/ui/utils'; +import { Trans } from '@kit/ui/trans'; -import { AppLogo } from '~/components/app-logo'; -import { ProfileAccountDropdownContainer } from '~/components/personal-account-dropdown-container'; -import featuresFlagConfig from '~/config/feature-flags.config'; import { personalAccountNavigationConfig } from '~/config/personal-account-navigation.config'; -import { UserNotifications } from '~/home/(user)/_components/user-notifications'; -// home imports -import type { UserWorkspace } from '../_lib/server/load-user-workspace'; -import { HomeAccountSelector } from './home-account-selector'; - -interface HomeSidebarProps { - workspace: UserWorkspace; -} - -export function HomeSidebar(props: HomeSidebarProps) { - const { workspace, user, accounts } = props.workspace; +export function HomeSidebar() { const collapsible = personalAccountNavigationConfig.sidebarCollapsedStyle; return ( - -
- - } - > - - - -
- -
+ +
+
+ +
- - - - ); } diff --git a/app/home/(user)/billing/_lib/server/user-billing.service.ts b/app/home/(user)/billing/_lib/server/user-billing.service.ts index 0fba68f..41506e8 100644 --- a/app/home/(user)/billing/_lib/server/user-billing.service.ts +++ b/app/home/(user)/billing/_lib/server/user-billing.service.ts @@ -2,6 +2,7 @@ import 'server-only'; import { SupabaseClient } from '@supabase/supabase-js'; +import { Database } from '@/packages/supabase/src/database.types'; import { z } from 'zod'; import { createAccountsApi } from '@kit/accounts/api'; @@ -13,7 +14,6 @@ import { requireUser } from '@kit/supabase/require-user'; import appConfig from '~/config/app.config'; import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; -import { Database } from '~/lib/database.types'; import { PersonalAccountCheckoutSchema } from '../schema/personal-account-checkout.schema'; diff --git a/app/home/(user)/layout.tsx b/app/home/(user)/layout.tsx index 6ede447..97f38ae 100644 --- a/app/home/(user)/layout.tsx +++ b/app/home/(user)/layout.tsx @@ -39,7 +39,7 @@ function SidebarLayout({ children }: React.PropsWithChildren) { - + @@ -58,8 +58,8 @@ function HeaderLayout({ children }: React.PropsWithChildren) { return ( - - + + @@ -67,7 +67,14 @@ function HeaderLayout({ children }: React.PropsWithChildren) { - {children} + + + + + + {children} + + ); diff --git a/app/home/(user)/page.tsx b/app/home/(user)/page.tsx index 9015e90..6a91bba 100644 --- a/app/home/(user)/page.tsx +++ b/app/home/(user)/page.tsx @@ -3,10 +3,12 @@ import { Trans } from '@kit/ui/trans'; import { createI18nServerInstance } from '~/lib/i18n/i18n.server'; import { withI18n } from '~/lib/i18n/with-i18n'; +import Dashboard from './_components/dashboard'; // local imports import { HomeLayoutPageHeader } from './_components/home-page-header'; import { use } from 'react'; import { loadUserWorkspace } from './_lib/server/load-user-workspace'; +import { PageBody } from '@kit/ui/page'; export const generateMetadata = async () => { const i18n = await createI18nServerInstance(); @@ -23,16 +25,12 @@ function UserHomePage() { <> } - description={} + description={<>} /> - {tempVisibleAccounts.length && ( - <> - Member of companies: -
{JSON.stringify(tempVisibleAccounts, null, 2)}
- - )} - + + + ); } diff --git a/app/home/[account]/billing/_lib/server/team-billing.service.ts b/app/home/[account]/billing/_lib/server/team-billing.service.ts index 93cb17e..b2c285d 100644 --- a/app/home/[account]/billing/_lib/server/team-billing.service.ts +++ b/app/home/[account]/billing/_lib/server/team-billing.service.ts @@ -2,6 +2,7 @@ import 'server-only'; import { SupabaseClient } from '@supabase/supabase-js'; +import { Database } from '@/packages/supabase/src/database.types'; import { z } from 'zod'; import { LineItemSchema } from '@kit/billing'; @@ -14,7 +15,6 @@ import { createTeamAccountsApi } from '@kit/team-accounts/api'; import appConfig from '~/config/app.config'; import billingConfig from '~/config/billing.config'; import pathsConfig from '~/config/paths.config'; -import { Database } from '~/lib/database.types'; import { TeamCheckoutSchema } from '../schema/team-billing.schema'; diff --git a/app/home/[account]/members/_lib/server/members-page.loader.ts b/app/home/[account]/members/_lib/server/members-page.loader.ts index 4db49b5..e77fd3f 100644 --- a/app/home/[account]/members/_lib/server/members-page.loader.ts +++ b/app/home/[account]/members/_lib/server/members-page.loader.ts @@ -2,8 +2,9 @@ import 'server-only'; import { SupabaseClient } from '@supabase/supabase-js'; +import { Database } from '@/packages/supabase/src/database.types'; + import { loadTeamWorkspace } from '~/home/[account]/_lib/server/team-account-workspace.loader'; -import { Database } from '~/lib/database.types'; /** * Load data for the members page diff --git a/components/app-logo.tsx b/components/app-logo.tsx index d11f922..82a1179 100644 --- a/components/app-logo.tsx +++ b/components/app-logo.tsx @@ -1,6 +1,6 @@ import Link from 'next/link'; -import { MedReportLogo } from './med-report-title'; +import { MedReportLogo } from './med-report-logo'; function LogoImage({ className, diff --git a/components/back-button.tsx b/components/back-button.tsx new file mode 100644 index 0000000..0e01177 --- /dev/null +++ b/components/back-button.tsx @@ -0,0 +1,31 @@ +'use client'; + +import { useRouter } from 'next/navigation'; + +import { ArrowLeft } from '@/public/assets/arrow-left'; + +import { Trans } from '@kit/ui/trans'; + +export function BackButton({ onBack }: { onBack?: () => void }) { + const router = useRouter(); + return ( +
{ + if (onBack) { + onBack(); + } else { + router.back(); + } + }} + > + +
+ ); +} diff --git a/components/header-auth.tsx b/components/header-auth.tsx index d560200..0fd2179 100644 --- a/components/header-auth.tsx +++ b/components/header-auth.tsx @@ -1,9 +1,11 @@ -import { signOutAction } from "@/lib/actions/sign-out"; -import { hasEnvVars } from "@/utils/supabase/check-env-vars"; -import Link from "next/link"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { createClient } from "@/utils/supabase/server"; +import Link from 'next/link'; + +import { signOutAction } from '@/lib/actions/sign-out'; +import { hasEnvVars } from '@/utils/supabase/check-env-vars'; +import { createClient } from '@/utils/supabase/server'; + +import { Badge } from '@kit/ui/badge'; +import { Button } from '@kit/ui/button'; export default async function AuthButton() { const supabase = await createClient(); @@ -15,11 +17,11 @@ export default async function AuthButton() { if (!hasEnvVars) { return ( <> -
+
Please update .env.local file with anon key and url @@ -28,18 +30,18 @@ export default async function AuthButton() { @@ -52,14 +54,14 @@ export default async function AuthButton() {
Hey, {user.email}!
-
) : (
-
diff --git a/components/med-report-title.tsx b/components/med-report-logo.tsx similarity index 100% rename from components/med-report-title.tsx rename to components/med-report-logo.tsx diff --git a/components/ui/info-tooltip.tsx b/components/ui/info-tooltip.tsx new file mode 100644 index 0000000..7883844 --- /dev/null +++ b/components/ui/info-tooltip.tsx @@ -0,0 +1,16 @@ +import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@kit/ui/tooltip"; +import { Info } from "lucide-react"; + +export function InfoTooltip({ content }: { content?: string }) { + if (!content) return null; + return ( + + + + + + {content} + + + ); +} diff --git a/components/ui/search.tsx b/components/ui/search.tsx new file mode 100644 index 0000000..84dac99 --- /dev/null +++ b/components/ui/search.tsx @@ -0,0 +1,33 @@ +import React, { JSX, ReactNode } from 'react'; + +import { cn } from '@kit/ui/utils'; + +export type SearchProps = React.InputHTMLAttributes & { + startElement?: string | JSX.Element; + className?: string; +}; + +const Search = React.forwardRef( + ({ className, startElement, ...props }, ref) => { + return ( +
+ {!!startElement && startElement} + +
+ ); + }, +); + +Search.displayName = 'Search'; + +export { Search }; diff --git a/config/paths.config.ts b/config/paths.config.ts index ad0bf12..f4cce84 100644 --- a/config/paths.config.ts +++ b/config/paths.config.ts @@ -8,9 +8,17 @@ const PathsSchema = z.object({ callback: z.string().min(1), passwordReset: z.string().min(1), passwordUpdate: z.string().min(1), + updateAccount: z.string().min(1), + updateAccountSuccess: z.string().min(1), }), app: z.object({ home: z.string().min(1), + booking: z.string().min(1), + myOrders: z.string().min(1), + analysisResults: z.string().min(1), + orderAnalysisPackage: z.string().min(1), + orderAnalysis: z.string().min(1), + orderHealthAnalysis: z.string().min(1), personalAccountSettings: z.string().min(1), personalAccountBilling: z.string().min(1), personalAccountBillingReturn: z.string().min(1), @@ -31,6 +39,8 @@ const pathsConfig = PathsSchema.parse({ callback: '/auth/callback', passwordReset: '/auth/password-reset', passwordUpdate: '/update-password', + updateAccount: '/auth/update-account', + updateAccountSuccess: '/auth/update-account/success', }, app: { home: '/home', @@ -43,6 +53,13 @@ const pathsConfig = PathsSchema.parse({ accountMembers: `/home/[account]/members`, accountBillingReturn: `/home/[account]/billing/return`, joinTeam: '/join', + // these routes are added as placeholders and can be changed when the pages are added + booking: '/booking', + myOrders: '/my-orders', + analysisResults: '/analysis-results', + orderAnalysisPackage: '/order-analysis-package', + orderAnalysis: '/order-analysis', + orderHealthAnalysis: '/order-health-analysis' }, } satisfies z.infer); diff --git a/config/personal-account-navigation.config.tsx b/config/personal-account-navigation.config.tsx index d534f32..f52c78e 100644 --- a/config/personal-account-navigation.config.tsx +++ b/config/personal-account-navigation.config.tsx @@ -1,47 +1,72 @@ -import { CreditCard, Home, User } from 'lucide-react'; +import { + FileLineChart, + HeartPulse, + LineChart, + MousePointerClick, + ShoppingCart, + Stethoscope, + TestTube2, +} from 'lucide-react'; import { z } from 'zod'; import { NavigationConfigSchema } from '@kit/ui/navigation-schema'; -import featureFlagsConfig from '~/config/feature-flags.config'; import pathsConfig from '~/config/paths.config'; -const iconClasses = 'w-4'; +const iconClasses = 'w-4 stroke-[1.5px]'; const routes = [ { - label: 'common:routes.application', children: [ { - label: 'common:routes.home', + label: 'common:routes.overview', path: pathsConfig.app.home, - Icon: , + Icon: , + end: true, + }, + { + label: 'common:routes.booking', + path: pathsConfig.app.booking, + Icon: , + end: true, + }, + { + label: 'common:routes.myOrders', + path: pathsConfig.app.myOrders, + Icon: , + end: true, + }, + { + label: 'common:routes.analysisResults', + path: pathsConfig.app.analysisResults, + Icon: , + end: true, + }, + { + label: 'common:routes.orderAnalysisPackage', + path: pathsConfig.app.orderAnalysisPackage, + Icon: , + end: true, + }, + { + label: 'common:routes.orderAnalysis', + path: pathsConfig.app.orderAnalysis, + Icon: , + end: true, + }, + { + label: 'common:routes.orderHealthAnalysis', + path: pathsConfig.app.orderHealthAnalysis, + Icon: , end: true, }, ], }, - { - label: 'common:routes.settings', - children: [ - { - label: 'common:routes.profile', - path: pathsConfig.app.personalAccountSettings, - Icon: , - }, - featureFlagsConfig.enablePersonalAccountBilling - ? { - label: 'common:routes.billing', - path: pathsConfig.app.personalAccountBilling, - Icon: , - } - : undefined, - ].filter((route) => !!route), - }, ] satisfies z.infer['routes']; export const personalAccountNavigationConfig = NavigationConfigSchema.parse({ routes, - style: process.env.NEXT_PUBLIC_USER_NAVIGATION_STYLE, - sidebarCollapsed: process.env.NEXT_PUBLIC_HOME_SIDEBAR_COLLAPSED, - sidebarCollapsedStyle: process.env.NEXT_PUBLIC_SIDEBAR_COLLAPSED_STYLE, + style: 'custom', + sidebarCollapsed: false, + sidebarCollapsedStyle: 'icon', }); diff --git a/lib/actions/sign-out.tsx b/lib/actions/sign-out.tsx index 0137a93..a168684 100644 --- a/lib/actions/sign-out.tsx +++ b/lib/actions/sign-out.tsx @@ -1,10 +1,11 @@ -"use server"; +'use server'; -import { createClient } from "@/utils/supabase/server"; -import { redirect } from "next/navigation"; +import { redirect } from 'next/navigation'; + +import { createClient } from '@/utils/supabase/server'; export const signOutAction = async () => { const supabase = await createClient(); await supabase.auth.signOut(); - return redirect("/sign-in"); + return redirect('/'); }; diff --git a/lib/i18n/i18n.settings.ts b/lib/i18n/i18n.settings.ts index bb70925..78308fa 100644 --- a/lib/i18n/i18n.settings.ts +++ b/lib/i18n/i18n.settings.ts @@ -32,6 +32,7 @@ export const defaultI18nNamespaces = [ 'teams', 'billing', 'marketing', + 'dashboard', ]; /** diff --git a/lib/services/mailer.service.ts b/lib/services/mailer.service.ts new file mode 100644 index 0000000..515bfcc --- /dev/null +++ b/lib/services/mailer.service.ts @@ -0,0 +1,42 @@ +'use server'; + +import { getMailer } from '@kit/mailers'; +import { enhanceAction } from '@kit/next/actions'; + +import { CompanySubmitData } from '../types/company'; +import { emailSchema } from '../validations/email.schema'; + +export const sendCompanyOfferEmail = async ( + data: CompanySubmitData, + language: string, +) => { + const { renderCompanyOfferEmail } = await import('@kit/email-templates'); + const { html, subject, to } = await renderCompanyOfferEmail({ + language, + companyData: data, + }); + + await sendEmail({ + subject, + html, + to, + }); +}; + +export const sendEmail = enhanceAction( + async ({ subject, html, to }) => { + const mailer = await getMailer(); + + await mailer.sendEmail({ + to, + subject, + html, + }); + + return {}; + }, + { + schema: emailSchema, + auth: false, + }, +); diff --git a/lib/services/register-company.service.ts b/lib/services/register-company.service.ts deleted file mode 100644 index c030462..0000000 --- a/lib/services/register-company.service.ts +++ /dev/null @@ -1,31 +0,0 @@ -"use server"; - -import * as yup from "yup"; -import { companySchema } from "@/lib/validations/companySchema"; - -export async function submitCompanyRegistration(formData: FormData) { - const data = { - companyName: formData.get("companyName")?.toString() || "", - contactPerson: formData.get("contactPerson")?.toString() || "", - email: formData.get("email")?.toString() || "", - phone: formData.get("phone")?.toString() || "", - }; - - try { - await companySchema.validate(data, { abortEarly: false }); - - console.log("Valid data:", data); - } catch (validationError) { - if (validationError instanceof yup.ValidationError) { - const errors = validationError.inner.map((err) => ({ - path: err.path, - message: err.message, - })); - throw new Error( - "Validation failed: " + - errors.map((e) => `${e.path}: ${e.message}`).join(", ") - ); - } - throw validationError; - } -} diff --git a/lib/utils.ts b/lib/utils.ts index 405dd01..1eb8901 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -1,5 +1,5 @@ -import { clsx, type ClassValue } from "clsx"; -import { twMerge } from "tailwind-merge"; +import { type ClassValue, clsx } from 'clsx'; +import { twMerge } from 'tailwind-merge'; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); @@ -9,3 +9,12 @@ export function toArray(input?: T | T[] | null): T[] { if (!input) return []; return Array.isArray(input) ? input : [input]; } + +export function toTitleCase(str?: string) { + if (!str) return ''; + return str.replace( + /\w\S*/g, + (text: string) => + text.charAt(0).toUpperCase() + text.substring(1).toLowerCase(), + ); +} diff --git a/lib/validations/company-offer.schema.ts b/lib/validations/company-offer.schema.ts new file mode 100644 index 0000000..4ac5b5d --- /dev/null +++ b/lib/validations/company-offer.schema.ts @@ -0,0 +1,16 @@ +import { z } from 'zod'; + +export const companyOfferSchema = z.object({ + companyName: z.string({ + required_error: 'Company name is required', + }), + contactPerson: z.string({ + required_error: 'Contact person is required', + }), + email: z + .string({ + required_error: 'Email is required', + }) + .email('Invalid email'), + phone: z.string().optional(), +}); diff --git a/lib/validations/companySchema.ts b/lib/validations/companySchema.ts deleted file mode 100644 index b265f81..0000000 --- a/lib/validations/companySchema.ts +++ /dev/null @@ -1,8 +0,0 @@ -import * as yup from "yup"; - -export const companySchema = yup.object({ - companyName: yup.string().required("Company name is required"), - contactPerson: yup.string().required("Contact person is required"), - email: yup.string().email("Invalid email").required("Email is required"), - phone: yup.string().optional(), -}); diff --git a/lib/validations/email.schema.ts b/lib/validations/email.schema.ts new file mode 100644 index 0000000..58cc00d --- /dev/null +++ b/lib/validations/email.schema.ts @@ -0,0 +1,7 @@ +import { z } from 'zod'; + +export const emailSchema = z.object({ + to: z.string().email(), + subject: z.string().min(1).max(200), + html: z.string().min(1).max(5000), +}); diff --git a/middleware.ts b/middleware.ts index 0101fed..c9283b7 100644 --- a/middleware.ts +++ b/middleware.ts @@ -139,7 +139,7 @@ function getPatterns() { handler: adminMiddleware, }, { - pattern: new URLPattern({ pathname: '/auth/*?' }), + pattern: new URLPattern({ pathname: '/auth/update-account' }), handler: async (req: NextRequest, res: NextResponse) => { const { data: { user }, @@ -147,21 +147,27 @@ function getPatterns() { // the user is logged out, so we don't need to do anything if (!user) { - return; + return NextResponse.redirect(new URL('/', req.nextUrl.origin).href); } - // check if we need to verify MFA (user is authenticated but needs to verify MFA) - const isVerifyMfa = req.nextUrl.pathname === pathsConfig.auth.verifyMfa; + const client = createMiddlewareClient(req, res); + const userIsSuperAdmin = await isSuperAdmin(client); - // If user is logged in and does not need to verify MFA, - // redirect to home page. - if (!isVerifyMfa) { - const nextPath = - req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home; + if (userIsSuperAdmin) { + // check if we need to verify MFA (user is authenticated but needs to verify MFA) + const isVerifyMfa = + req.nextUrl.pathname === pathsConfig.auth.verifyMfa; - return NextResponse.redirect( - new URL(nextPath, req.nextUrl.origin).href, - ); + // If user is logged in and does not need to verify MFA, + // redirect to home page. + if (!isVerifyMfa) { + const nextPath = + req.nextUrl.searchParams.get('next') ?? pathsConfig.app.home; + + return NextResponse.redirect( + new URL(nextPath, req.nextUrl.origin).href, + ); + } } }, }, diff --git a/package.json b/package.json index b85c538..45a1b03 100644 --- a/package.json +++ b/package.json @@ -25,9 +25,7 @@ "supabase:db:lint": "supabase db lint", "supabase:db:diff": "supabase db diff", "supabase:deploy": "supabase link --project-ref $SUPABASE_PROJECT_REF && supabase db push", - "supabase:typegen": "pnpm run supabase:typegen:packages && pnpm run supabase:typegen:app", - "supabase:typegen:packages": "supabase gen types typescript --local > ../../packages/supabase/src/database.types.ts", - "supabase:typegen:app": "supabase gen types typescript --local > ./supabase/database.types.ts", + "supabase:typegen": "supabase gen types typescript --local > ./packages/supabase/src/database.types.ts", "supabase:db:dump:local": "supabase db dump --local --data-only", "sync-analysis-groups:dev": "NODE_ENV=local ts-node jobs/sync-analysis-groups.ts", "sync-connected-online:dev": "NODE_ENV=local ts-node jobs/sync-connected-online.ts" @@ -114,4 +112,4 @@ "node": ">=20.0.0", "pnpm": ">=9.0.0" } -} \ No newline at end of file +} diff --git a/packages/email-templates/README.md b/packages/email-templates/README.md index dbd6ada..83ec48a 100644 --- a/packages/email-templates/README.md +++ b/packages/email-templates/README.md @@ -2,4 +2,4 @@ This package is responsible for managing email templates using the react.email library. -Here you can define email templates using React components and export them as a function that returns the email content. \ No newline at end of file +Here you can define email templates using React components and export them as a function that returns the email content. diff --git a/packages/email-templates/src/emails/company-offer.email.tsx b/packages/email-templates/src/emails/company-offer.email.tsx new file mode 100644 index 0000000..fa9f30f --- /dev/null +++ b/packages/email-templates/src/emails/company-offer.email.tsx @@ -0,0 +1,90 @@ +import { + Body, + Head, + Html, + Preview, + Tailwind, + Text, + render, +} from '@react-email/components'; + +import { BodyStyle } from '../components/body-style'; +import { EmailContent } from '../components/content'; +import { EmailHeader } from '../components/header'; +import { EmailHeading } from '../components/heading'; +import { EmailWrapper } from '../components/wrapper'; +import { initializeEmailI18n } from '../lib/i18n'; + +export async function renderCompanyOfferEmail({ + language, + companyData, +}: { + language?: string; + companyData: { + companyName: string; + contactPerson: string; + email: string; + phone?: string; + }; +}) { + const namespace = 'company-offer-email'; + + const { t } = await initializeEmailI18n({ + language, + namespace, + }); + + const to = process.env.CONTACT_EMAIL || ''; + + const previewText = t(`${namespace}:previewText`, { + companyName: companyData.companyName, + }); + + const subject = t(`${namespace}:subject`, { + companyName: companyData.companyName, + }); + + const html = await render( + + + + + + {previewText} + + + + + + {previewText} + + + + + {t(`${namespace}:companyName`)} {companyData.companyName} + + + + {t(`${namespace}:contactPerson`)} {companyData.contactPerson} + + + + {t(`${namespace}:email`)} {companyData.email} + + + + {t(`${namespace}:phone`)} {companyData.phone || 'N/A'} + + + + + + , + ); + + return { + html, + subject, + to, + }; +} diff --git a/packages/email-templates/src/index.ts b/packages/email-templates/src/index.ts index c7c2eb4..80e5f8a 100644 --- a/packages/email-templates/src/index.ts +++ b/packages/email-templates/src/index.ts @@ -1,3 +1,4 @@ export * from './emails/invite.email'; export * from './emails/account-delete.email'; export * from './emails/otp.email'; +export * from './emails/company-offer.email'; diff --git a/packages/email-templates/src/locales/et/company-offer-email.json b/packages/email-templates/src/locales/et/company-offer-email.json new file mode 100644 index 0000000..3a39792 --- /dev/null +++ b/packages/email-templates/src/locales/et/company-offer-email.json @@ -0,0 +1,8 @@ +{ + "subject": "Uus ettevõtte liitumispäring", + "previewText": "Ettevõte {{companyName}} soovib pakkumist", + "companyName": "Ettevõtte nimi:", + "contactPerson": "Kontaktisik:", + "email": "E-mail:", + "phone": "Telefon:" +} diff --git a/packages/features/accounts/src/hooks/use-personal-account-data.ts b/packages/features/accounts/src/hooks/use-personal-account-data.ts index 6a901e5..6c53da9 100644 --- a/packages/features/accounts/src/hooks/use-personal-account-data.ts +++ b/packages/features/accounts/src/hooks/use-personal-account-data.ts @@ -26,7 +26,8 @@ export function usePersonalAccountData( ` id, name, - picture_url + picture_url, + last_name `, ) .eq('primary_owner_user_id', userId) diff --git a/packages/features/admin/src/components/admin-account-page.tsx b/packages/features/admin/src/components/admin-account-page.tsx index 0930ad2..71f67bc 100644 --- a/packages/features/admin/src/components/admin-account-page.tsx +++ b/packages/features/admin/src/components/admin-account-page.tsx @@ -215,7 +215,6 @@ async function TeamAccountPage(props: {
-
Company Members diff --git a/packages/features/auth/src/components/sign-in-methods-container.tsx b/packages/features/auth/src/components/sign-in-methods-container.tsx index c2c558e..c6d243e 100644 --- a/packages/features/auth/src/components/sign-in-methods-container.tsx +++ b/packages/features/auth/src/components/sign-in-methods-container.tsx @@ -4,6 +4,8 @@ import { useRouter } from 'next/navigation'; import type { Provider } from '@supabase/supabase-js'; +import { useSupabase } from '@/packages/supabase/src/hooks/use-supabase'; + import { isBrowser } from '@kit/shared/utils'; import { If } from '@kit/ui/if'; import { Separator } from '@kit/ui/separator'; @@ -20,6 +22,7 @@ export function SignInMethodsContainer(props: { callback: string; joinTeam: string; returnPath: string; + updateAccount: string; }; providers: { @@ -28,13 +31,14 @@ export function SignInMethodsContainer(props: { oAuth: Provider[]; }; }) { + const client = useSupabase(); const router = useRouter(); const redirectUrl = isBrowser() ? new URL(props.paths.callback, window?.location.origin).toString() : ''; - const onSignIn = () => { + const onSignIn = async (userId?: string) => { // if the user has an invite token, we should join the team if (props.inviteToken) { const searchParams = new URLSearchParams({ @@ -45,8 +49,28 @@ export function SignInMethodsContainer(props: { router.replace(joinTeamPath); } else { - // otherwise, we should redirect to the return path - router.replace(props.paths.returnPath); + if (!userId) { + router.replace(props.paths.callback); + return; + } + + try { + const { data: hasPersonalCode } = await client.rpc( + 'has_personal_code', + { + account_id: userId, + }, + ); + + if (hasPersonalCode) { + router.replace(props.paths.returnPath); + } else { + router.replace(props.paths.updateAccount); + } + } catch { + router.replace(props.paths.callback); + return; + } } }; diff --git a/packages/features/auth/src/components/sign-up-methods-container.tsx b/packages/features/auth/src/components/sign-up-methods-container.tsx index 0f9be0e..39f4338 100644 --- a/packages/features/auth/src/components/sign-up-methods-container.tsx +++ b/packages/features/auth/src/components/sign-up-methods-container.tsx @@ -17,6 +17,7 @@ export function SignUpMethodsContainer(props: { paths: { callback: string; appHome: string; + updateAccount: string; }; providers: { diff --git a/packages/features/auth/src/components/update-account-form.tsx b/packages/features/auth/src/components/update-account-form.tsx new file mode 100644 index 0000000..2cad0f0 --- /dev/null +++ b/packages/features/auth/src/components/update-account-form.tsx @@ -0,0 +1,225 @@ +'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: '', + email: user.email, + phone: '', + city: '', + weight: 0, + height: 0, + userConsent: false, + }, + }); + return ( +
+ + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + + ( + + + + + + + + + + )} + /> + +
+ ( + + + + + + + field.onChange( + e.target.value === '' ? null : Number(e.target.value), + ) + } + /> + + + + )} + /> + + ( + + + + + + + field.onChange( + e.target.value === '' ? null : Number(e.target.value), + ) + } + /> + + + + )} + /> +
+ + ( + +
+ + + + + + +
+ + + + + +
+ )} + /> + + + + + ); +} diff --git a/packages/features/auth/src/schemas/update-account.schema.ts b/packages/features/auth/src/schemas/update-account.schema.ts new file mode 100644 index 0000000..ee1e331 --- /dev/null +++ b/packages/features/auth/src/schemas/update-account.schema.ts @@ -0,0 +1,44 @@ +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', + }), +}); diff --git a/packages/features/auth/src/server/actions/update-account-actions.ts b/packages/features/auth/src/server/actions/update-account-actions.ts new file mode 100644 index 0000000..eb24be0 --- /dev/null +++ b/packages/features/auth/src/server/actions/update-account-actions.ts @@ -0,0 +1,44 @@ +'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 { 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) => { + 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); + } + redirect(pathsConfig.auth.updateAccountSuccess); + }, + { + schema: UpdateAccountSchema, + }, +); diff --git a/packages/features/auth/src/server/api.ts b/packages/features/auth/src/server/api.ts new file mode 100644 index 0000000..6c10ca1 --- /dev/null +++ b/packages/features/auth/src/server/api.ts @@ -0,0 +1,93 @@ +import { SupabaseClient } from '@supabase/supabase-js'; + +import { Database } from '@kit/supabase/database'; + +import { AccountSubmitData } from './actions/update-account-actions'; + +/** + * Class representing an API for interacting with user accounts. + * @constructor + * @param {SupabaseClient} client - The Supabase client instance. + */ +class AuthApi { + constructor(private readonly client: SupabaseClient) {} + + /** + * @name hasPersonalCode + * @description Check if given account ID has added personal code. + * @param id + */ + async hasPersonalCode(id: string) { + const { data, error } = await this.client.rpc('has_personal_code', { + account_id: id, + }); + + if (error) { + throw error; + } + + return data; + } + + /** + * @name updateAccount + * @description Update required fields for the account. + * @param data + */ + async updateAccount(data: AccountSubmitData) { + const { + data: { user }, + } = await this.client.auth.getUser(); + + if (!user) { + throw new Error('User not authenticated'); + } + + const { error } = await this.client.rpc('update_account', { + p_name: data.firstName, + p_last_name: data.lastName, + p_personal_code: data.personalCode, + p_phone: data.phone || '', + p_city: data.city || '', + p_has_consent_personal_data: data.userConsent, + p_uid: user.id, + }); + + if (error) { + throw error; + } + + if (data.height || data.weight) { + await this.updateAccountParams(data); + } + } + + /** + * @name updateAccountParams + * @description Update account parameters. + * @param data + */ + async updateAccountParams(data: AccountSubmitData) { + const { + data: { user }, + } = await this.client.auth.getUser(); + + if (!user) { + throw new Error('User not authenticated'); + } + console.log('test', user, data); + const response = await this.client.from('account_params').insert({ + account_id: user.id, + height: data.height, + weight: data.weight, + }); + + if (response.error) { + throw response.error; + } + } +} + +export function createAuthApi(client: SupabaseClient) { + return new AuthApi(client); +} diff --git a/packages/features/auth/src/sign-up.ts b/packages/features/auth/src/sign-up.ts index d76b414..6825d4d 100644 --- a/packages/features/auth/src/sign-up.ts +++ b/packages/features/auth/src/sign-up.ts @@ -1,2 +1,3 @@ export * from './components/sign-up-methods-container'; export * from './schemas/password-sign-up.schema'; +export * from './components/update-account-form'; diff --git a/packages/features/auth/tsconfig.json b/packages/features/auth/tsconfig.json index c4697e9..2edb84a 100644 --- a/packages/features/auth/tsconfig.json +++ b/packages/features/auth/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@kit/tsconfig/base.json", + "extends": "../../../tsconfig.json", "compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, diff --git a/packages/features/notifications/src/components/index.ts b/packages/features/notifications/src/components/index.ts index 9bd47eb..08883b6 100644 --- a/packages/features/notifications/src/components/index.ts +++ b/packages/features/notifications/src/components/index.ts @@ -1 +1,3 @@ export * from './notifications-popover'; +export * from './success-notification'; +export * from './update-account-success-notification'; diff --git a/packages/features/notifications/src/components/notifications-popover.tsx b/packages/features/notifications/src/components/notifications-popover.tsx index 8ba367c..2069bbb 100644 --- a/packages/features/notifications/src/components/notifications-popover.tsx +++ b/packages/features/notifications/src/components/notifications-popover.tsx @@ -126,7 +126,7 @@ export function NotificationsPopover(params: { { if (params.onClick) { diff --git a/packages/features/notifications/src/components/success-notification.tsx b/packages/features/notifications/src/components/success-notification.tsx new file mode 100644 index 0000000..7c6be5c --- /dev/null +++ b/packages/features/notifications/src/components/success-notification.tsx @@ -0,0 +1,50 @@ +import Image from 'next/image'; +import Link from 'next/link'; + +import { MedReportLogo } from '@/components/med-report-logo'; + +import { Button } from '@kit/ui/button'; +import { Trans } from '@kit/ui/trans'; + +export const SuccessNotification = ({ + showLogo = true, + title, + titleKey, + descriptionKey, + buttonProps, +}: { + showLogo?: boolean; + title?: string; + titleKey?: string; + descriptionKey?: string; + buttonProps?: { + buttonTitleKey: string; + href: string; + }; +}) => { + return ( +
+ {showLogo && } +
+ Success +

{title || }

+

+ +

+
+ {buttonProps && ( + + )} +
+ ); +}; diff --git a/packages/features/notifications/src/components/update-account-success-notification.tsx b/packages/features/notifications/src/components/update-account-success-notification.tsx new file mode 100644 index 0000000..a15455b --- /dev/null +++ b/packages/features/notifications/src/components/update-account-success-notification.tsx @@ -0,0 +1,39 @@ +'use client'; + +import { redirect } from 'next/navigation'; + +import pathsConfig from '@/config/paths.config'; +import { useTranslation } from 'react-i18next'; + +import { usePersonalAccountData } from '@kit/accounts/hooks/use-personal-account-data'; + +import { SuccessNotification } from './success-notification'; + +export const UpdateAccountSuccessNotification = ({ + userId, +}: { + userId?: string; +}) => { + const { t } = useTranslation('account'); + + if (!userId) { + redirect(pathsConfig.app.home); + } + + const { data: accountData } = usePersonalAccountData(userId); + + return ( + + ); +}; diff --git a/packages/features/notifications/tsconfig.json b/packages/features/notifications/tsconfig.json index 9a53360..e2b4bf5 100644 --- a/packages/features/notifications/tsconfig.json +++ b/packages/features/notifications/tsconfig.json @@ -1,5 +1,5 @@ { - "extends": "@kit/tsconfig/base.json", + "extends": "../../../tsconfig.json", "compilerOptions": { "tsBuildInfoFile": "node_modules/.cache/tsbuildinfo.json" }, diff --git a/packages/mailers/shared/src/schema/mailer.schema.ts b/packages/mailers/shared/src/schema/mailer.schema.ts index 1fd1f58..dd0e652 100644 --- a/packages/mailers/shared/src/schema/mailer.schema.ts +++ b/packages/mailers/shared/src/schema/mailer.schema.ts @@ -5,7 +5,7 @@ export const MailerSchema = z to: z.string().email(), // this is not necessarily formatted // as an email so we type it loosely - from: z.string().min(1), + from: z.string().min(1).optional(), subject: z.string(), }) .and( diff --git a/packages/supabase/src/database.types.ts b/packages/supabase/src/database.types.ts index 96430a7..a6343a6 100644 --- a/packages/supabase/src/database.types.ts +++ b/packages/supabase/src/database.types.ts @@ -48,6 +48,48 @@ export type Database = { } Relationships: [] } + request_entries: { + Row: { + comment: string | null + created_at: string + id: number + personal_code: number | null + request_api: string + request_api_method: string + requested_end_date: string | null + requested_start_date: string | null + service_id: number | null + service_provider_id: number | null + status: Database["audit"]["Enums"]["request_status"] + } + Insert: { + comment?: string | null + created_at?: string + id?: number + personal_code?: number | null + request_api: string + request_api_method: string + requested_end_date?: string | null + requested_start_date?: string | null + service_id?: number | null + service_provider_id?: number | null + status: Database["audit"]["Enums"]["request_status"] + } + Update: { + comment?: string | null + created_at?: string + id?: number + personal_code?: number | null + request_api?: string + request_api_method?: string + requested_end_date?: string | null + requested_start_date?: string | null + service_id?: number | null + service_provider_id?: number | null + status?: Database["audit"]["Enums"]["request_status"] + } + Relationships: [] + } sync_entries: { Row: { changed_by_role: string @@ -83,6 +125,7 @@ export type Database = { [_ in never]: never } Enums: { + request_status: "SUCCESS" | "FAIL" sync_status: "SUCCESS" | "FAIL" } CompositeTypes: { @@ -116,15 +159,43 @@ export type Database = { } public: { Tables: { + account_params: { + Row: { + account_id: string + height: number | null + id: string + recorded_at: string + weight: number | null + } + Insert: { + account_id?: string + height?: number | null + id?: string + recorded_at?: string + weight?: number | null + } + Update: { + account_id?: string + height?: number | null + id?: string + recorded_at?: string + weight?: number | null + } + Relationships: [] + } accounts: { Row: { + city: string | null created_at: string | null created_by: string | null email: string | null + has_consent_personal_data: boolean | null id: string is_personal_account: boolean + last_name: string | null name: string personal_code: string | null + phone: string | null picture_url: string | null primary_owner_user_id: string public_data: Json @@ -133,13 +204,17 @@ export type Database = { updated_by: string | null } Insert: { + city?: string | null created_at?: string | null created_by?: string | null email?: string | null + has_consent_personal_data?: boolean | null id?: string is_personal_account?: boolean + last_name?: string | null name: string personal_code?: string | null + phone?: string | null picture_url?: string | null primary_owner_user_id?: string public_data?: Json @@ -148,13 +223,17 @@ export type Database = { updated_by?: string | null } Update: { + city?: string | null created_at?: string | null created_by?: string | null email?: string | null + has_consent_personal_data?: boolean | null id?: string is_personal_account?: boolean + last_name?: string | null name?: string personal_code?: string | null + phone?: string | null picture_url?: string | null primary_owner_user_id?: string public_data?: Json @@ -200,20 +279,6 @@ export type Database = { referencedRelation: "accounts" referencedColumns: ["id"] }, - { - foreignKeyName: "accounts_memberships_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_accounts" - referencedColumns: ["account_id"] - }, - { - foreignKeyName: "accounts_memberships_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_personal_accounts" - referencedColumns: ["account_id"] - }, { foreignKeyName: "accounts_memberships_account_id_fkey" columns: ["account_id"] @@ -515,20 +580,6 @@ export type Database = { referencedRelation: "accounts" referencedColumns: ["id"] }, - { - foreignKeyName: "billing_customers_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_accounts" - referencedColumns: ["account_id"] - }, - { - foreignKeyName: "billing_customers_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_personal_accounts" - referencedColumns: ["account_id"] - }, { foreignKeyName: "billing_customers_account_id_fkey" columns: ["account_id"] @@ -627,6 +678,158 @@ export type Database = { } Relationships: [] } + connected_online_providers: { + Row: { + can_select_worker: boolean + created_at: string + email: string | null + id: number + name: string + personal_code_required: boolean + phone_number: string | null + updated_at: string | null + } + Insert: { + can_select_worker: boolean + created_at?: string + email?: string | null + id: number + name: string + personal_code_required: boolean + phone_number?: string | null + updated_at?: string | null + } + Update: { + can_select_worker?: boolean + created_at?: string + email?: string | null + id?: number + name?: string + personal_code_required?: boolean + phone_number?: string | null + updated_at?: string | null + } + Relationships: [] + } + connected_online_reservation: { + Row: { + booking_code: string + clinic_id: number + comments: string | null + created_at: string + discount_code: string | null + id: number + lang: string + requires_payment: boolean + service_id: number + service_user_id: number | null + start_time: string + sync_user_id: number + updated_at: string | null + user_id: string + } + Insert: { + booking_code: string + clinic_id: number + comments?: string | null + created_at?: string + discount_code?: string | null + id?: number + lang: string + requires_payment: boolean + service_id: number + service_user_id?: number | null + start_time: string + sync_user_id: number + updated_at?: string | null + user_id: string + } + Update: { + booking_code?: string + clinic_id?: number + comments?: string | null + created_at?: string + discount_code?: string | null + id?: number + lang?: string + requires_payment?: boolean + service_id?: number + service_user_id?: number | null + start_time?: string + sync_user_id?: number + updated_at?: string | null + user_id?: string + } + Relationships: [] + } + connected_online_services: { + Row: { + clinic_id: number + code: string + created_at: string + description: string | null + display: string | null + duration: number + has_free_codes: boolean + id: number + name: string + neto_duration: number | null + online_hide_duration: number | null + online_hide_price: number | null + price: number + price_periods: string | null + requires_payment: boolean + sync_id: number + updated_at: string | null + } + Insert: { + clinic_id: number + code: string + created_at?: string + description?: string | null + display?: string | null + duration: number + has_free_codes: boolean + id: number + name: string + neto_duration?: number | null + online_hide_duration?: number | null + online_hide_price?: number | null + price: number + price_periods?: string | null + requires_payment: boolean + sync_id: number + updated_at?: string | null + } + Update: { + clinic_id?: number + code?: string + created_at?: string + description?: string | null + display?: string | null + duration?: number + has_free_codes?: boolean + id?: number + name?: string + neto_duration?: number | null + online_hide_duration?: number | null + online_hide_price?: number | null + price?: number + price_periods?: string | null + requires_payment?: boolean + sync_id?: number + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "connected_online_services_clinic_id_fkey" + columns: ["clinic_id"] + isOneToOne: false + referencedRelation: "connected_online_providers" + referencedColumns: ["id"] + }, + ] + } invitations: { Row: { account_id: string @@ -672,20 +875,6 @@ export type Database = { referencedRelation: "accounts" referencedColumns: ["id"] }, - { - foreignKeyName: "invitations_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_accounts" - referencedColumns: ["account_id"] - }, - { - foreignKeyName: "invitations_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_personal_accounts" - referencedColumns: ["account_id"] - }, { foreignKeyName: "invitations_account_id_fkey" columns: ["account_id"] @@ -709,6 +898,129 @@ export type Database = { }, ] } + medreport_product_groups: { + Row: { + created_at: string + id: number + name: string + updated_at: string | null + } + Insert: { + created_at?: string + id?: number + name: string + updated_at?: string | null + } + Update: { + created_at?: string + id?: number + name?: string + updated_at?: string | null + } + Relationships: [] + } + medreport_products: { + Row: { + created_at: string + id: number + name: string + product_group_id: number | null + updated_at: string | null + } + Insert: { + created_at?: string + id?: number + name: string + product_group_id?: number | null + updated_at?: string | null + } + Update: { + created_at?: string + id?: number + name?: string + product_group_id?: number | null + updated_at?: string | null + } + Relationships: [ + { + foreignKeyName: "medreport_products_product_groups_id_fkey" + columns: ["product_group_id"] + isOneToOne: false + referencedRelation: "medreport_product_groups" + referencedColumns: ["id"] + }, + ] + } + medreport_products_analyses_relations: { + Row: { + analysis_element_id: number | null + analysis_id: number | null + product_id: number + } + Insert: { + analysis_element_id?: number | null + analysis_id?: number | null + product_id: number + } + Update: { + analysis_element_id?: number | null + analysis_id?: number | null + product_id?: number + } + Relationships: [ + { + foreignKeyName: "medreport_products_analyses_analysis_element_id_fkey" + columns: ["analysis_element_id"] + isOneToOne: true + referencedRelation: "analysis_elements" + referencedColumns: ["id"] + }, + { + foreignKeyName: "medreport_products_analyses_analysis_id_fkey" + columns: ["analysis_id"] + isOneToOne: true + referencedRelation: "analyses" + referencedColumns: ["id"] + }, + { + foreignKeyName: "medreport_products_analyses_product_id_fkey" + columns: ["product_id"] + isOneToOne: true + referencedRelation: "medreport_products" + referencedColumns: ["id"] + }, + ] + } + medreport_products_external_services_relations: { + Row: { + connected_online_service_id: number + product_id: number + } + Insert: { + connected_online_service_id: number + product_id: number + } + Update: { + connected_online_service_id?: number + product_id?: number + } + Relationships: [ + { + foreignKeyName: "medreport_products_connected_online_services_id_fkey" + columns: ["connected_online_service_id"] + isOneToOne: true + referencedRelation: "connected_online_services" + referencedColumns: ["id"] + }, + { + foreignKeyName: "medreport_products_connected_online_services_product_id_fkey" + columns: ["product_id"] + isOneToOne: false + referencedRelation: "medreport_products" + referencedColumns: ["id"] + }, + ] + } nonces: { Row: { client_token: string @@ -808,20 +1120,6 @@ export type Database = { referencedRelation: "accounts" referencedColumns: ["id"] }, - { - foreignKeyName: "notifications_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_accounts" - referencedColumns: ["account_id"] - }, - { - foreignKeyName: "notifications_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_personal_accounts" - referencedColumns: ["account_id"] - }, { foreignKeyName: "notifications_account_id_fkey" columns: ["account_id"] @@ -921,20 +1219,6 @@ export type Database = { referencedRelation: "accounts" referencedColumns: ["id"] }, - { - foreignKeyName: "orders_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_accounts" - referencedColumns: ["account_id"] - }, - { - foreignKeyName: "orders_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_personal_accounts" - referencedColumns: ["account_id"] - }, { foreignKeyName: "orders_account_id_fkey" columns: ["account_id"] @@ -1106,20 +1390,6 @@ export type Database = { referencedRelation: "accounts" referencedColumns: ["id"] }, - { - foreignKeyName: "subscriptions_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_accounts" - referencedColumns: ["account_id"] - }, - { - foreignKeyName: "subscriptions_account_id_fkey" - columns: ["account_id"] - isOneToOne: false - referencedRelation: "invitations_with_personal_accounts" - referencedColumns: ["account_id"] - }, { foreignKeyName: "subscriptions_account_id_fkey" columns: ["account_id"] @@ -1145,23 +1415,6 @@ export type Database = { } } Views: { - invitations_with_accounts: { - Row: { - account_id: string | null - invite_token: string | null - personal_code: string | null - } - Relationships: [] - } - invitations_with_personal_accounts: { - Row: { - account_id: string | null - account_slug: string | null - invite_token: string | null - personal_code: string | null - } - Relationships: [] - } user_account_workspace: { Row: { id: string | null @@ -1241,13 +1494,17 @@ export type Database = { create_team_account: { Args: { account_name: string } Returns: { + city: string | null created_at: string | null created_by: string | null email: string | null + has_consent_personal_data: boolean | null id: string is_personal_account: boolean + last_name: string | null name: string personal_code: string | null + phone: string | null picture_url: string | null primary_owner_user_id: string public_data: Json @@ -1329,6 +1586,10 @@ export type Database = { } Returns: boolean } + has_personal_code: { + Args: { account_id: string } + Returns: boolean + } has_role_on_account: { Args: { account_id: string; account_role?: string } Returns: boolean @@ -1395,6 +1656,18 @@ export type Database = { Args: { target_account_id: string; new_owner_id: string } Returns: undefined } + update_account: { + Args: { + p_name: string + p_last_name: string + p_personal_code: string + p_phone: string + p_city: string + p_has_consent_personal_data: boolean + p_uid: string + } + Returns: undefined + } upsert_order: { Args: { target_account_id: string @@ -1611,6 +1884,7 @@ export type CompositeTypes< export const Constants = { audit: { Enums: { + request_status: ["SUCCESS", "FAIL"], sync_status: ["SUCCESS", "FAIL"], }, }, diff --git a/packages/ui/src/makerkit/navigation-config.schema.ts b/packages/ui/src/makerkit/navigation-config.schema.ts index 7d9a971..7023826 100644 --- a/packages/ui/src/makerkit/navigation-config.schema.ts +++ b/packages/ui/src/makerkit/navigation-config.schema.ts @@ -29,7 +29,7 @@ const RouteChild = z.object({ }); const RouteGroup = z.object({ - label: z.string(), + label: z.string().optional(), collapsible: z.boolean().optional(), collapsed: z.boolean().optional(), children: z.array(RouteChild), @@ -37,12 +37,8 @@ const RouteGroup = z.object({ }); export const NavigationConfigSchema = z.object({ - style: z.enum(['custom', 'sidebar', 'header']).default('sidebar'), - sidebarCollapsed: z - .enum(['false', 'true']) - .default('true') - .optional() - .transform((value) => value === `true`), + style: z.enum(['custom', 'sidebar', 'header']).default('custom'), + sidebarCollapsed: z.boolean().optional(), sidebarCollapsedStyle: z.enum(['offcanvas', 'icon', 'none']).default('icon'), routes: z.array(z.union([RouteGroup, Divider])), }); diff --git a/packages/ui/src/makerkit/page.tsx b/packages/ui/src/makerkit/page.tsx index 34c57c9..5d6ee31 100644 --- a/packages/ui/src/makerkit/page.tsx +++ b/packages/ui/src/makerkit/page.tsx @@ -14,10 +14,6 @@ type PageProps = React.PropsWithChildren<{ sticky?: boolean; }>; -const ENABLE_SIDEBAR_TRIGGER = process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER - ? process.env.NEXT_PUBLIC_ENABLE_SIDEBAR_TRIGGER === 'true' - : true; - export function Page(props: PageProps) { switch (props.style) { case 'header': @@ -79,7 +75,7 @@ function PageWithHeader(props: PageProps) { const { Navigation, Children, MobileNavigation } = getSlotsFromPage(props); return ( -
+
@@ -113,7 +109,10 @@ export function PageBody( className?: string; }>, ) { - const className = cn('flex w-full flex-1 flex-col lg:px-4', props.className); + const className = cn( + 'flex w-full flex-1 flex-col space-y-6 lg:px-4', + props.className, + ); return
{props.children}
; } @@ -125,7 +124,7 @@ export function PageNavigation(props: React.PropsWithChildren) { export function PageDescription(props: React.PropsWithChildren) { return (
-
+
{props.children}
@@ -153,7 +152,7 @@ export function PageHeader({ title, description, className, - displaySidebarTrigger = ENABLE_SIDEBAR_TRIGGER, + displaySidebarTrigger = false, }: React.PropsWithChildren<{ className?: string; title?: string | React.ReactNode; diff --git a/packages/ui/src/makerkit/version-updater.tsx b/packages/ui/src/makerkit/version-updater.tsx index 28bdbb8..5bb3877 100644 --- a/packages/ui/src/makerkit/version-updater.tsx +++ b/packages/ui/src/makerkit/version-updater.tsx @@ -70,7 +70,7 @@ export function VersionUpdater(props: { intervalTimeInSecond?: number }) { setDismissed(true); }} > - +